[RLlib] Curiosity exploration module: tf/tf2.x/tf-eager support. (#11945)

This commit is contained in:
Sven Mika
2020-11-29 12:31:24 +01:00
committed by GitHub
parent a537b852e6
commit fb318addcb
10 changed files with 227 additions and 76 deletions
+2
View File
@@ -245,6 +245,8 @@ def make_ddpg_optimizers(policy, config):
learning_rate=config["actor_lr"])
policy._critic_optimizer = tf1.train.AdamOptimizer(
learning_rate=config["critic_lr"])
# TODO: (sven) make this function return both optimizers and
# TFPolicy handle optimizers vs loss terms correctly (like torch).
return None
+1 -1
View File
@@ -62,7 +62,7 @@ class LocalSyncParallelOptimizer:
self.loss_inputs = input_placeholders + rnn_inputs
self.build_graph = build_graph
# First initialize the shared loss network
# First initialize the shared loss network.
with tf1.name_scope(TOWER_SCOPE_NAME):
self._shared_loss = build_graph(self.loss_inputs)
shared_ops = tf1.get_collection(
+12 -4
View File
@@ -11,7 +11,7 @@ from ray.rllib.models.repeated_values import RepeatedValues
from ray.rllib.policy.policy import Policy, LEARNER_STATS_KEY
from ray.rllib.policy.rnn_sequencing import pad_batch_to_sequences_of_same_size
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.utils import add_mixins
from ray.rllib.utils import add_mixins, force_list
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_tf
from ray.rllib.utils.tf_ops import convert_to_non_tf_type
@@ -278,9 +278,16 @@ def build_eager_tf_policy(name,
self._loss_initialized = True
if optimizer_fn:
self._optimizer = optimizer_fn(self, config)
optimizers = optimizer_fn(self, config)
else:
self._optimizer = tf.keras.optimizers.Adam(config["lr"])
optimizers = tf.keras.optimizers.Adam(config["lr"])
optimizers = force_list(optimizers)
if getattr(self, "exploration", None):
optimizers = self.exploration.get_exploration_optimizer(
optimizers)
# TODO: (sven) Allow tf policy to have more than 1 optimizer.
# Just like torch Policy does.
self._optimizer = optimizers[0] if optimizers else None
if after_init:
after_init(self, observation_space, action_space, config)
@@ -577,7 +584,8 @@ def build_eager_tf_policy(name,
if apply_gradients_fn:
apply_gradients_fn(self, self._optimizer, grads_and_vars)
else:
self._optimizer.apply_gradients(grads_and_vars)
self._optimizer.apply_gradients(
[(g, v) for g, v in grads_and_vars if g is not None])
def _compute_gradients(self, samples):
"""Computes and returns grads as eager tensors."""
+13 -3
View File
@@ -8,11 +8,14 @@ from ray.rllib.policy import eager_tf_policy
from ray.rllib.policy.policy import Policy, LEARNER_STATS_KEY
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.policy.tf_policy import TFPolicy
from ray.rllib.utils import add_mixins
from ray.rllib.utils import add_mixins, force_list
from ray.rllib.utils.annotations import override, DeveloperAPI
from ray.rllib.utils.framework import try_import_tf
from ray.rllib.utils.typing import AgentID, ModelGradients, TensorType, \
TrainerConfigDict
tf1, tf, tfv = try_import_tf()
@DeveloperAPI
def build_tf_policy(
@@ -244,9 +247,16 @@ def build_tf_policy(
@override(TFPolicy)
def optimizer(self):
if optimizer_fn:
return optimizer_fn(self, self.config)
optimizers = optimizer_fn(self, self.config)
else:
return base.optimizer(self)
optimizers = base.optimizer(self)
optimizers = force_list(optimizers)
if getattr(self, "exploration", None):
optimizers = self.exploration.get_exploration_optimizer(
optimizers)
# TODO: (sven) Allow tf-eager policy to have more than 1 optimizer.
# Just like torch Policy does.
return optimizers[0] if optimizers else None
@override(TFPolicy)
def gradients(self, optimizer, loss):
+1 -1
View File
@@ -320,7 +320,7 @@ def build_torch_policy(
else:
optimizers = TorchPolicy.optimizer(self)
optimizers = force_list(optimizers)
if hasattr(self, "exploration"):
if getattr(self, "exploration", None):
optimizers = self.exploration.get_exploration_optimizer(
optimizers)
return optimizers
+159 -35
View File
@@ -4,18 +4,22 @@ from typing import Optional, Tuple, Union
from ray.rllib.models.action_dist import ActionDistribution
from ray.rllib.models.catalog import ModelCatalog
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.models.modelv2 import ModelV2, NullContextManager
from ray.rllib.models.tf.tf_action_dist import Categorical, MultiCategorical
from ray.rllib.models.torch.misc import SlimFC
from ray.rllib.models.torch.torch_action_dist import TorchCategorical, \
TorchMultiCategorical
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.utils.annotations import override
from ray.rllib.utils.exploration.exploration import Exploration
from ray.rllib.utils.framework import try_import_torch, TensorType
from ray.rllib.utils.framework import get_activation_fn, try_import_tf, \
try_import_torch
from ray.rllib.utils.from_config import from_config
from ray.rllib.utils.tf_ops import get_placeholder, one_hot as tf_one_hot
from ray.rllib.utils.torch_ops import one_hot
from ray.rllib.utils.typing import FromConfigSpec, ModelConfigDict
from ray.rllib.utils.typing import FromConfigSpec, ModelConfigDict, TensorType
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
F = None
if nn is not None:
@@ -91,9 +95,7 @@ class Curiosity(Exploration):
DQN). If None, uses the FromSpecDict provided in the Policy's
default config.
"""
if framework != "torch":
raise ValueError("Only torch is currently supported for Curiosity")
elif not isinstance(action_space, (Discrete, MultiDiscrete)):
if not isinstance(action_space, (Discrete, MultiDiscrete)):
raise ValueError(
"Only (Multi)Discrete action spaces supported for Curiosity "
"so far!")
@@ -139,12 +141,15 @@ class Curiosity(Exploration):
self._curiosity_inverse_fcnet = self._create_fc_net(
[2 * self.feature_dim] + list(self.inverse_net_hiddens) +
[self.action_dim], self.inverse_net_activation)
[self.action_dim],
self.inverse_net_activation,
name="inverse_net")
self._curiosity_forward_fcnet = self._create_fc_net(
[self.feature_dim + self.action_dim] + list(
self.forward_net_hiddens) + [self.feature_dim],
self.forward_net_activation)
self.forward_net_activation,
name="forward_net")
# This is only used to select the correct action
self.exploration_submodule = from_config(
@@ -172,26 +177,48 @@ class Curiosity(Exploration):
@override(Exploration)
def get_exploration_optimizer(self, optimizers):
feature_params = list(self._curiosity_feature_net.parameters())
inverse_params = list(self._curiosity_inverse_fcnet.parameters())
forward_params = list(self._curiosity_forward_fcnet.parameters())
# Now that the Policy's own optimizer(s) have been created (from
# the Model parameters (IMPORTANT: w/o(!) the curiosity params),
# we can add our curiosity sub-modules to the Policy's Model.
self.model._curiosity_feature_net = \
self._curiosity_feature_net.to(self.device)
self.model._curiosity_inverse_fcnet = \
self._curiosity_inverse_fcnet.to(self.device)
self.model._curiosity_forward_fcnet = \
self._curiosity_forward_fcnet.to(self.device)
# Create, but don't add Adam for curiosity NN updating to the policy.
# If we added and returned it here, it would be used in the policy's
# update loop, which we don't want (curiosity updating happens inside
# `postprocess_trajectory`).
self._optimizer = torch.optim.Adam(
forward_params + inverse_params + feature_params, lr=self.lr)
if self.framework == "torch":
feature_params = list(self._curiosity_feature_net.parameters())
inverse_params = list(self._curiosity_inverse_fcnet.parameters())
forward_params = list(self._curiosity_forward_fcnet.parameters())
# Now that the Policy's own optimizer(s) have been created (from
# the Model parameters (IMPORTANT: w/o(!) the curiosity params),
# we can add our curiosity sub-modules to the Policy's Model.
self.model._curiosity_feature_net = \
self._curiosity_feature_net.to(self.device)
self.model._curiosity_inverse_fcnet = \
self._curiosity_inverse_fcnet.to(self.device)
self.model._curiosity_forward_fcnet = \
self._curiosity_forward_fcnet.to(self.device)
self._optimizer = torch.optim.Adam(
forward_params + inverse_params + feature_params, lr=self.lr)
else:
self.model._curiosity_feature_net = self._curiosity_feature_net
self.model._curiosity_inverse_fcnet = self._curiosity_inverse_fcnet
self.model._curiosity_forward_fcnet = self._curiosity_forward_fcnet
# Feature net is a RLlib ModelV2, the other 2 are keras Models.
self._optimizer_var_list = \
self._curiosity_feature_net.base_model.variables + \
self._curiosity_inverse_fcnet.variables + \
self._curiosity_forward_fcnet.variables
self.model.register_variables(self._optimizer_var_list)
self._optimizer = tf1.train.AdamOptimizer(learning_rate=self.lr)
# Create placeholders and initialize the loss.
if self.framework == "tf":
self._obs_ph = get_placeholder(
space=self.model.obs_space, name="_curiosity_obs")
self._next_obs_ph = get_placeholder(
space=self.model.obs_space, name="_curiosity_next_obs")
self._action_ph = get_placeholder(
space=self.model.action_space, name="_curiosity_action")
self._forward_l2_norm_sqared, self._update_op = \
self._postprocess_helper_tf(
self._obs_ph, self._next_obs_ph, self._action_ph)
return optimizers
@@ -202,6 +229,87 @@ class Curiosity(Exploration):
Also calculates forward and inverse losses and updates the curiosity
module on the provided batch using our optimizer.
"""
if self.framework != "torch":
self._postprocess_tf(policy, sample_batch, tf_sess)
else:
self._postprocess_torch(policy, sample_batch)
def _postprocess_tf(self, policy, sample_batch, tf_sess):
# tf1 static-graph: Perform session call on our loss and update ops.
if self.framework == "tf":
forward_l2_norm_sqared, _ = tf_sess.run(
[self._forward_l2_norm_sqared, self._update_op],
feed_dict={
self._obs_ph: sample_batch[SampleBatch.OBS],
self._next_obs_ph: sample_batch[SampleBatch.NEXT_OBS],
self._action_ph: sample_batch[SampleBatch.ACTIONS],
})
# tf-eager: Perform model calls, loss calculations, and optimizer
# stepping on the fly.
else:
forward_l2_norm_sqared, _ = self._postprocess_helper_tf(
sample_batch[SampleBatch.OBS],
sample_batch[SampleBatch.NEXT_OBS],
sample_batch[SampleBatch.ACTIONS],
)
# Scale intrinsic reward by eta hyper-parameter.
sample_batch[SampleBatch.REWARDS] = \
sample_batch[SampleBatch.REWARDS] + \
self.eta * forward_l2_norm_sqared
return sample_batch
def _postprocess_helper_tf(self, obs, next_obs, actions):
with (tf.GradientTape()
if self.framework != "tf" else NullContextManager()) as tape:
# Push both observations through feature net to get both phis.
phis, _ = self.model._curiosity_feature_net({
SampleBatch.OBS: tf.concat([obs, next_obs], axis=0)
})
phi, next_phi = tf.split(phis, 2)
# Predict next phi with forward model.
predicted_next_phi = self.model._curiosity_forward_fcnet(
tf.concat(
[phi, tf_one_hot(actions, self.action_space)], axis=-1))
# Forward loss term (predicted phi', given phi and action vs
# actually observed phi').
forward_l2_norm_sqared = 0.5 * tf.reduce_sum(
tf.square(predicted_next_phi - next_phi), axis=-1)
forward_loss = tf.reduce_mean(forward_l2_norm_sqared)
# Inverse loss term (prediced action that led from phi to phi' vs
# actual action taken).
phi_cat_next_phi = tf.concat([phi, next_phi], axis=-1)
dist_inputs = self.model._curiosity_inverse_fcnet(phi_cat_next_phi)
action_dist = Categorical(dist_inputs, self.model) if \
isinstance(self.action_space, Discrete) else \
MultiCategorical(
dist_inputs, self.model, self.action_space.nvec)
# Neg log(p); p=probability of observed action given the inverse-NN
# predicted action distribution.
inverse_loss = -action_dist.logp(actions)
inverse_loss = tf.reduce_mean(inverse_loss)
# Calculate the ICM loss.
loss = (1.0 - self.beta) * inverse_loss + self.beta * forward_loss
# Step the optimizer.
if self.framework != "tf":
grads = tape.gradient(loss, self._optimizer_var_list)
grads_and_vars = [(g, v)
for g, v in zip(grads, self._optimizer_var_list)
if g is not None]
update_op = self._optimizer.apply_gradients(grads_and_vars)
else:
update_op = self._optimizer.minimize(
loss, var_list=self._optimizer_var_list)
# Return the squared l2 norm and the optimizer update op.
return forward_l2_norm_sqared, update_op
def _postprocess_torch(self, policy, sample_batch):
# Push both observations through feature net to get both phis.
phis, _ = self.model._curiosity_feature_net({
SampleBatch.OBS: torch.cat([
@@ -253,7 +361,7 @@ class Curiosity(Exploration):
# Return the postprocessed sample batch (with the corrected rewards).
return sample_batch
def _create_fc_net(self, layer_dims, activation):
def _create_fc_net(self, layer_dims, activation, name=None):
"""Given a list of layer dimensions (incl. input-dim), creates FC-net.
Args:
@@ -262,16 +370,32 @@ class Curiosity(Exploration):
activation (str): An activation specifier string (e.g. "relu").
Examples:
If layer_dims is [4,8,6] we'll have a two layer net: 4->8 and 8->6,
where the second layer does not have an activation anymore.
If layer_dims is [4,8,6] we'll have a two layer net: 4->8 (8 nodes)
and 8->6 (6 nodes), where the second layer (6 nodes) does not have
an activation anymore. 4 is the input dimension.
"""
layers = []
layers = [
tf.keras.layers.Input(
shape=(layer_dims[0], ), name="{}_in".format(name))
] if self.framework != "torch" else []
for i in range(len(layer_dims) - 1):
act = activation if i < len(layer_dims) - 2 else None
layers.append(
SlimFC(
in_size=layer_dims[i],
out_size=layer_dims[i + 1],
initializer=torch.nn.init.xavier_uniform_,
activation_fn=act))
return nn.Sequential(*layers)
if self.framework == "torch":
layers.append(
SlimFC(
in_size=layer_dims[i],
out_size=layer_dims[i + 1],
initializer=torch.nn.init.xavier_uniform_,
activation_fn=act))
else:
layers.append(
tf.keras.layers.Dense(
units=layer_dims[i + 1],
activation=get_activation_fn(act),
name="{}_{}".format(name, i)))
if self.framework == "torch":
return nn.Sequential(*layers)
else:
return tf.keras.Sequential(layers)
+3 -2
View File
@@ -157,7 +157,8 @@ class Exploration:
return sample_batch
@DeveloperAPI
def get_exploration_optimizer(self, optimizers: List[LocalOptimizer]):
def get_exploration_optimizer(self, optimizers: List[LocalOptimizer]) -> \
List[LocalOptimizer]:
"""May add optimizer(s) to the Policy's own `optimizers`.
The number of optimizers (Policy's plus Exploration's optimizers) must
@@ -176,7 +177,7 @@ class Exploration:
@DeveloperAPI
def get_exploration_loss(self, policy_loss: List[TensorType],
train_batch: SampleBatch):
train_batch: SampleBatch) -> List[TensorType]:
"""May add loss term(s) to the Policy's own loss(es).
Args:
+28 -27
View File
@@ -24,7 +24,7 @@ class MyCallBack(DefaultCallbacks):
policy_id, policies, postprocessed_batch,
original_batches, **kwargs):
pos = np.argmax(postprocessed_batch["obs"], -1)
x, y = pos % 10, pos // 10
x, y = pos % 8, pos // 8
self.deltas.extend((x**2 + y**2)**0.5)
def on_sample_end(self, *, worker, samples, **kwargs):
@@ -125,23 +125,21 @@ class TestCuriosity(unittest.TestCase):
def tearDownClass(cls):
ray.shutdown()
def test_curiosity_on_large_frozen_lake(self):
def test_curiosity_on_frozen_lake(self):
config = ppo.DEFAULT_CONFIG.copy()
# A very large frozen-lake that's hard for a random policy to solve
# A very large frozen-lake that's hard for a random policy to solve
# due to 0.0 feedback.
config["env"] = "FrozenLake-v0"
config["env_config"] = {
"desc": [
"SFFFFFFFFF",
"FFFFFFFFFF",
"FFFFFFFFFF",
"FFFFFFFFFF",
"FFFFFFFFFF",
"FFFFFFFFFF",
"FFFFFFFFFF",
"FFFFFFFFFF",
"FFFFFFFFFF",
"FFFFFFFFFG",
"SFFFFFFF",
"FFFFFFFF",
"FFFFFFFF",
"FFFFFFFF",
"FFFFFFFF",
"FFFFFFFF",
"FFFFFFFF",
"FFFFFFFG",
],
"is_slippery": False
}
@@ -149,13 +147,13 @@ class TestCuriosity(unittest.TestCase):
config["callbacks"] = MyCallBack
# Limit horizon to make it really hard for non-curious agent to reach
# the goal state.
config["horizon"] = 23
config["horizon"] = 16
# Local only.
config["num_workers"] = 0
config["lr"] = 0.001
num_iterations = 10
for _ in framework_iterator(config, frameworks="torch"):
for fw in framework_iterator(config):
# W/ Curiosity. Expect to learn something.
config["exploration_config"] = {
"type": "Curiosity",
@@ -182,18 +180,21 @@ class TestCuriosity(unittest.TestCase):
trainer.stop()
self.assertTrue(learnt)
# W/o Curiosity. Expect to learn nothing.
config["exploration_config"] = {
"type": "StochasticSampling",
}
trainer = ppo.PPOTrainer(config=config)
rewards_wo = 0.0
for _ in range(num_iterations):
result = trainer.train()
rewards_wo += result["episode_reward_mean"]
print(result)
trainer.stop()
self.assertTrue(rewards_wo == 0.0)
if fw == "tf":
# W/o Curiosity. Expect to learn nothing.
print("Trying w/o curiosity (not expected to learn).")
config["exploration_config"] = {
"type": "StochasticSampling",
}
trainer = ppo.PPOTrainer(config=config)
rewards_wo = 0.0
for _ in range(num_iterations):
result = trainer.train()
rewards_wo += result["episode_reward_mean"]
print(result)
trainer.stop()
self.assertTrue(rewards_wo == 0.0)
print("Did not reach goal w/o curiosity!")
def test_curiosity_on_partially_observable_domain(self):
config = ppo.DEFAULT_CONFIG.copy()
+3 -2
View File
@@ -225,11 +225,12 @@ def get_variable(value,
# TODO: (sven) move to models/utils.py
def get_activation_fn(name, framework="tf"):
def get_activation_fn(name: Optional[str] = None, framework: str = "tf"):
"""Returns a framework specific activation function, given a name string.
Args:
name (str): One of "relu" (default), "tanh", "swish", or "linear".
name (Optional[str]): One of "relu" (default), "tanh", "swish", or
"linear" or None.
framework (str): One of "tf" or "torch".
Returns:
+5 -1
View File
@@ -132,7 +132,10 @@ def make_tf_callable(session_or_none, dynamic_shape=False):
assert session_or_none is not None
def make_wrapper(fn):
if session_or_none:
# Static-graph mode: Create placeholders and make a session call each
# time the wrapped function is called. Return this session call's
# outputs.
if session_or_none is not None:
args_placeholders = []
kwargs_placeholders = {}
symbolic_out = [None]
@@ -183,6 +186,7 @@ def make_tf_callable(session_or_none, dynamic_shape=False):
return ret
return call
# Eager mode (call function as is).
else:
return fn