mirror of
https://github.com/saymrwulf/stable-baselines3.git
synced 2026-09-15 22:10:25 +00:00
Add callback support
This commit is contained in:
parent
cc3b023533
commit
b66003cfb3
13 changed files with 555 additions and 84 deletions
|
|
@ -8,3 +8,4 @@ omit =
|
||||||
exclude_lines =
|
exclude_lines =
|
||||||
pragma: no cover
|
pragma: no cover
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ Breaking Changes:
|
||||||
New Features:
|
New Features:
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
- Add `seed()` method to `VecEnv` class
|
- Add `seed()` method to `VecEnv` class
|
||||||
|
- Add support for Callback (cf https://github.com/hill-a/stable-baselines/pull/644)
|
||||||
|
|
||||||
Bug Fixes:
|
Bug Fixes:
|
||||||
^^^^^^^^^^
|
^^^^^^^^^^
|
||||||
|
|
@ -24,6 +25,7 @@ Others:
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
- Add type check
|
- Add type check
|
||||||
- Converted all format string to f-strings
|
- Converted all format string to f-strings
|
||||||
|
- Add test for `OrnsteinUhlenbeckActionNoise`
|
||||||
|
|
||||||
Documentation:
|
Documentation:
|
||||||
^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
37
tests/test_callbacks.py
Normal file
37
tests/test_callbacks.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from torchy_baselines import SAC
|
||||||
|
from torchy_baselines.common.callbacks import (CallbackList, CheckpointCallback, EvalCallback,
|
||||||
|
EveryNTimesteps, StopTrainingOnRewardThreshold)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("model_class", [SAC])
|
||||||
|
def test_callbacks(model_class):
|
||||||
|
# Create RL model
|
||||||
|
model = model_class('MlpPolicy', 'Pendulum-v0')
|
||||||
|
|
||||||
|
checkpoint_callback = CheckpointCallback(save_freq=1000, save_path='./logs/')
|
||||||
|
|
||||||
|
# For testing: use the same training env
|
||||||
|
eval_env = model.get_env()
|
||||||
|
# Stop training if the performance is good enough
|
||||||
|
callback_on_best = StopTrainingOnRewardThreshold(reward_threshold=-1200, verbose=1)
|
||||||
|
|
||||||
|
eval_callback = EvalCallback(eval_env, callback_on_new_best=callback_on_best,
|
||||||
|
best_model_save_path='./logs/best_model',
|
||||||
|
log_path='./logs/results', eval_freq=100)
|
||||||
|
|
||||||
|
# Equivalent to the `checkpoint_callback`
|
||||||
|
# but here in an event-driven manner
|
||||||
|
checkpoint_on_event = CheckpointCallback(save_freq=1, save_path='./logs/',
|
||||||
|
name_prefix='event')
|
||||||
|
event_callback = EveryNTimesteps(n_steps=1000, callback=checkpoint_on_event)
|
||||||
|
|
||||||
|
callback = CallbackList([checkpoint_callback, eval_callback, event_callback])
|
||||||
|
|
||||||
|
model.learn(1000, callback=callback)
|
||||||
|
model.learn(500, callback=None)
|
||||||
|
# Transform callback into a callback list automatically
|
||||||
|
model.learn(500, callback=[checkpoint_callback, eval_callback])
|
||||||
|
# Automatic wrapping, old way of doing callbacks
|
||||||
|
model.learn(500, callback=lambda _locals, _globals : True)
|
||||||
|
|
@ -4,12 +4,13 @@ import pytest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from torchy_baselines import A2C, CEMRL, PPO, SAC, TD3
|
from torchy_baselines import A2C, CEMRL, PPO, SAC, TD3
|
||||||
from torchy_baselines.common.noise import NormalActionNoise
|
from torchy_baselines.common.noise import NormalActionNoise, OrnsteinUhlenbeckActionNoise
|
||||||
|
|
||||||
action_noise = NormalActionNoise(np.zeros(1), 0.1 * np.ones(1))
|
action_noise = NormalActionNoise(np.zeros(1), 0.1 * np.ones(1))
|
||||||
|
|
||||||
|
|
||||||
def test_td3():
|
@pytest.mark.parametrize('action_noise', [action_noise, OrnsteinUhlenbeckActionNoise(np.zeros(1), 0.1 * np.ones(1))])
|
||||||
|
def test_td3(action_noise):
|
||||||
model = TD3('MlpPolicy', 'Pendulum-v0', policy_kwargs=dict(net_arch=[64, 64]),
|
model = TD3('MlpPolicy', 'Pendulum-v0', policy_kwargs=dict(net_arch=[64, 64]),
|
||||||
learning_starts=100, verbose=1, create_eval_env=True, action_noise=action_noise)
|
learning_starts=100, verbose=1, create_eval_env=True, action_noise=action_noise)
|
||||||
model.learn(total_timesteps=1000, eval_freq=500)
|
model.learn(total_timesteps=1000, eval_freq=500)
|
||||||
|
|
|
||||||
|
|
@ -102,19 +102,17 @@ class CEMRL(TD3):
|
||||||
def learn(self, total_timesteps, callback=None, log_interval=4,
|
def learn(self, total_timesteps, callback=None, log_interval=4,
|
||||||
eval_env=None, eval_freq=-1, n_eval_episodes=5, tb_log_name="CEMRL", reset_num_timesteps=True):
|
eval_env=None, eval_freq=-1, n_eval_episodes=5, tb_log_name="CEMRL", reset_num_timesteps=True):
|
||||||
|
|
||||||
timesteps_since_eval, episode_num, evaluations, obs, eval_env = self._setup_learn(eval_env)
|
timesteps_since_eval, episode_num, evaluations, obs, eval_env, callback = self._setup_learn(eval_env, callback)
|
||||||
actor_steps = 0
|
actor_steps = 0
|
||||||
|
continue_training = True
|
||||||
|
|
||||||
|
callback.on_training_start(locals(), globals())
|
||||||
|
|
||||||
while self.num_timesteps < total_timesteps:
|
while self.num_timesteps < total_timesteps:
|
||||||
|
|
||||||
self.fitnesses = []
|
self.fitnesses = []
|
||||||
self.es_params = self.es.ask(self.pop_size)
|
self.es_params = self.es.ask(self.pop_size)
|
||||||
|
|
||||||
if callback is not None:
|
|
||||||
# Only stop training if return value is False, not when it is None.
|
|
||||||
if callback(locals(), globals()) is False:
|
|
||||||
break
|
|
||||||
|
|
||||||
if self.num_timesteps > 0:
|
if self.num_timesteps > 0:
|
||||||
# self.train(episode_timesteps)
|
# self.train(episode_timesteps)
|
||||||
# Gradient steps for half of the population
|
# Gradient steps for half of the population
|
||||||
|
|
@ -180,7 +178,7 @@ class CEMRL(TD3):
|
||||||
|
|
||||||
rollout = self.collect_rollouts(self.env, n_episodes=self.n_episodes_rollout,
|
rollout = self.collect_rollouts(self.env, n_episodes=self.n_episodes_rollout,
|
||||||
n_steps=-1, action_noise=self.action_noise,
|
n_steps=-1, action_noise=self.action_noise,
|
||||||
deterministic=False, callback=None,
|
deterministic=False, callback=callback,
|
||||||
learning_starts=self.learning_starts,
|
learning_starts=self.learning_starts,
|
||||||
num_timesteps=self.num_timesteps,
|
num_timesteps=self.num_timesteps,
|
||||||
replay_buffer=self.replay_buffer,
|
replay_buffer=self.replay_buffer,
|
||||||
|
|
@ -188,7 +186,10 @@ class CEMRL(TD3):
|
||||||
log_interval=log_interval)
|
log_interval=log_interval)
|
||||||
|
|
||||||
# Unpack
|
# Unpack
|
||||||
episode_reward, episode_timesteps, n_episodes, obs = rollout
|
episode_reward, episode_timesteps, n_episodes, obs, continue_training = rollout
|
||||||
|
|
||||||
|
if continue_training is False:
|
||||||
|
break
|
||||||
|
|
||||||
episode_num += n_episodes
|
episode_num += n_episodes
|
||||||
self.num_timesteps += episode_timesteps
|
self.num_timesteps += episode_timesteps
|
||||||
|
|
@ -196,7 +197,13 @@ class CEMRL(TD3):
|
||||||
actor_steps += episode_timesteps
|
actor_steps += episode_timesteps
|
||||||
self.fitnesses.append(episode_reward)
|
self.fitnesses.append(episode_reward)
|
||||||
|
|
||||||
|
if continue_training is False:
|
||||||
|
break
|
||||||
|
|
||||||
self._update_current_progress(self.num_timesteps, total_timesteps)
|
self._update_current_progress(self.num_timesteps, total_timesteps)
|
||||||
self.es.tell(self.es_params, self.fitnesses)
|
self.es.tell(self.es_params, self.fitnesses)
|
||||||
timesteps_since_eval += actor_steps
|
timesteps_since_eval += actor_steps
|
||||||
|
|
||||||
|
callback.on_training_end()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@ from torchy_baselines.common.vec_env import DummyVecEnv, VecEnv, unwrap_vec_norm
|
||||||
from torchy_baselines.common.monitor import Monitor
|
from torchy_baselines.common.monitor import Monitor
|
||||||
from torchy_baselines.common.evaluation import evaluate_policy
|
from torchy_baselines.common.evaluation import evaluate_policy
|
||||||
from torchy_baselines.common.save_util import data_to_json, json_to_data
|
from torchy_baselines.common.save_util import data_to_json, json_to_data
|
||||||
|
from torchy_baselines.common.type_aliases import GymEnv, TensorDict, OptimizerStateDict
|
||||||
|
from torchy_baselines.common.noise import ActionNoise
|
||||||
|
|
||||||
# TODO: define aliases, ex GymEnv = Union[gym.Env, VecEnv]
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from torchy_baselines.common.noise import ActionNoise
|
from torchy_baselines.common.callbacks import BaseCallback
|
||||||
|
|
||||||
|
|
||||||
class BaseRLModel(ABC):
|
class BaseRLModel(ABC):
|
||||||
|
|
@ -51,7 +52,7 @@ class BaseRLModel(ABC):
|
||||||
"""
|
"""
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
policy: Type[BasePolicy],
|
policy: Type[BasePolicy],
|
||||||
env: Union[gym.Env, VecEnv, str],
|
env: Union[GymEnv, str],
|
||||||
policy_base: Type[BasePolicy],
|
policy_base: Type[BasePolicy],
|
||||||
policy_kwargs : Dict[str, Any] = None,
|
policy_kwargs : Dict[str, Any] = None,
|
||||||
verbose: int = 0,
|
verbose: int = 0,
|
||||||
|
|
@ -75,7 +76,7 @@ class BaseRLModel(ABC):
|
||||||
if verbose > 0:
|
if verbose > 0:
|
||||||
print(f"Using {self.device} device")
|
print(f"Using {self.device} device")
|
||||||
|
|
||||||
self.env = None # type: Union[gym.Env, VecEnv]
|
self.env = None # type: GymEnv
|
||||||
# get VecNormalize object if needed
|
# get VecNormalize object if needed
|
||||||
self._vec_normalize_env = unwrap_vec_normalize(env)
|
self._vec_normalize_env = unwrap_vec_normalize(env)
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
|
|
@ -129,7 +130,7 @@ class BaseRLModel(ABC):
|
||||||
raise ValueError("Error: the model does not support multiple envs requires a single vectorized"
|
raise ValueError("Error: the model does not support multiple envs requires a single vectorized"
|
||||||
" environment.")
|
" environment.")
|
||||||
|
|
||||||
def _get_eval_env(self, eval_env: Union[gym.Env, VecEnv, None]) -> Union[gym.Env, VecEnv, None]:
|
def _get_eval_env(self, eval_env: Optional[GymEnv]) -> Optional[GymEnv]:
|
||||||
"""
|
"""
|
||||||
Return the environment that will be used for evaluation.
|
Return the environment that will be used for evaluation.
|
||||||
|
|
||||||
|
|
@ -145,6 +146,27 @@ class BaseRLModel(ABC):
|
||||||
assert eval_env.num_envs == 1
|
assert eval_env.num_envs == 1
|
||||||
return eval_env
|
return eval_env
|
||||||
|
|
||||||
|
# Type hint as string to avoid circular import
|
||||||
|
def _init_callback(self, callback) -> 'BaseCallback':
|
||||||
|
"""
|
||||||
|
Note: we cannot use type hint here because of circular import.
|
||||||
|
|
||||||
|
:param callback: (Union[callable, [BaseCallback], BaseCallback, None])
|
||||||
|
:return: (BaseCallback)
|
||||||
|
"""
|
||||||
|
# Avoid circular import
|
||||||
|
from torchy_baselines.common.callbacks import BaseCallback, CallbackList, ConvertCallback
|
||||||
|
|
||||||
|
# Convert a list of callbacks into a callback
|
||||||
|
if isinstance(callback, list):
|
||||||
|
callback = CallbackList(callback)
|
||||||
|
# Convert functional callback to object
|
||||||
|
if not isinstance(callback, BaseCallback):
|
||||||
|
callback = ConvertCallback(callback)
|
||||||
|
|
||||||
|
callback.init_callback(self)
|
||||||
|
return callback
|
||||||
|
|
||||||
def scale_action(self, action: np.ndarray) -> np.ndarray:
|
def scale_action(self, action: np.ndarray) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Rescale the action from [low, high] to [-1, 1]
|
Rescale the action from [low, high] to [-1, 1]
|
||||||
|
|
@ -206,7 +228,7 @@ class BaseRLModel(ABC):
|
||||||
"""
|
"""
|
||||||
return np.nan if len(arr) == 0 else np.mean(arr)
|
return np.nan if len(arr) == 0 else np.mean(arr)
|
||||||
|
|
||||||
def get_env(self) -> Union[VecEnv, None]:
|
def get_env(self) -> Optional[VecEnv]:
|
||||||
"""
|
"""
|
||||||
Returns the current environment (can be None if not defined).
|
Returns the current environment (can be None if not defined).
|
||||||
|
|
||||||
|
|
@ -230,7 +252,7 @@ class BaseRLModel(ABC):
|
||||||
# return true if no check failed
|
# return true if no check failed
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_env(self, env: Union[gym.Env, VecEnv]) -> None:
|
def set_env(self, env: GymEnv) -> None:
|
||||||
"""
|
"""
|
||||||
Checks the validity of the environment, and if it is coherent, set it as the current environment.
|
Checks the validity of the environment, and if it is coherent, set it as the current environment.
|
||||||
Furthermore wrap any non vectorized env into a vectorized
|
Furthermore wrap any non vectorized env into a vectorized
|
||||||
|
|
@ -252,7 +274,7 @@ class BaseRLModel(ABC):
|
||||||
self.n_envs = env.num_envs
|
self.n_envs = env.num_envs
|
||||||
self.env = env
|
self.env = env
|
||||||
|
|
||||||
def get_parameters(self) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
def get_parameters(self) -> Tuple[TensorDict, OptimizerStateDict]:
|
||||||
"""
|
"""
|
||||||
Returns policy and optimizer parameters as a tuple
|
Returns policy and optimizer parameters as a tuple
|
||||||
|
|
||||||
|
|
@ -260,7 +282,7 @@ class BaseRLModel(ABC):
|
||||||
"""
|
"""
|
||||||
return self.get_policy_parameters(), self.get_opt_parameters()
|
return self.get_policy_parameters(), self.get_opt_parameters()
|
||||||
|
|
||||||
def get_policy_parameters(self) -> Dict[str, Any]:
|
def get_policy_parameters(self) -> TensorDict:
|
||||||
"""
|
"""
|
||||||
Get current model policy parameters as dictionary of variable name -> tensors.
|
Get current model policy parameters as dictionary of variable name -> tensors.
|
||||||
|
|
||||||
|
|
@ -269,7 +291,7 @@ class BaseRLModel(ABC):
|
||||||
return self.policy.state_dict()
|
return self.policy.state_dict()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_opt_parameters(self)-> Dict[str, Any]:
|
def get_opt_parameters(self)-> OptimizerStateDict:
|
||||||
"""
|
"""
|
||||||
Get current model optimizer parameters as dictionary of variable names -> tensors
|
Get current model optimizer parameters as dictionary of variable names -> tensors
|
||||||
:return: (dict) Dictionary of variable name -> tensor of model's optimizer parameters
|
:return: (dict) Dictionary of variable name -> tensor of model's optimizer parameters
|
||||||
|
|
@ -280,7 +302,7 @@ class BaseRLModel(ABC):
|
||||||
def learn(self, total_timesteps: int,
|
def learn(self, total_timesteps: int,
|
||||||
callback=None, log_interval: int = 100,
|
callback=None, log_interval: int = 100,
|
||||||
tb_log_name: str = "run",
|
tb_log_name: str = "run",
|
||||||
eval_env: Union[gym.Env, VecEnv, None] = None,
|
eval_env: Optional[GymEnv] = None,
|
||||||
eval_freq: int = -1,
|
eval_freq: int = -1,
|
||||||
n_eval_episodes: int = 5,
|
n_eval_episodes: int = 5,
|
||||||
reset_num_timesteps: bool = True):
|
reset_num_timesteps: bool = True):
|
||||||
|
|
@ -316,7 +338,7 @@ class BaseRLModel(ABC):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def load_parameters(self, load_dict: Dict[str, Any], opt_params: Dict[str, Any]) -> None:
|
def load_parameters(self, load_dict: TensorDict, opt_params: OptimizerStateDict) -> None:
|
||||||
"""
|
"""
|
||||||
Load model parameters from a dictionary
|
Load model parameters from a dictionary
|
||||||
load_dict should contain all keys from torch.model.state_dict()
|
load_dict should contain all keys from torch.model.state_dict()
|
||||||
|
|
@ -325,14 +347,14 @@ class BaseRLModel(ABC):
|
||||||
|
|
||||||
|
|
||||||
:param load_dict: dict of parameters from model.state_dict()
|
:param load_dict: dict of parameters from model.state_dict()
|
||||||
:param opt_params: dict of optimizer state_dicts should be handled in child_class
|
:param opt_params: dict of optimizer state_dicts should be handled in child class
|
||||||
"""
|
"""
|
||||||
if opt_params is not None:
|
if opt_params is not None:
|
||||||
raise ValueError("Optimizer Parameters where given but no overloaded load function exists for this class")
|
raise ValueError("Optimizer Parameters where given but no overloaded load function exists for this class")
|
||||||
self.policy.load_state_dict(load_dict)
|
self.policy.load_state_dict(load_dict)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, load_path: str, env: Union[gym.Env, VecEnv, None] = None, **kwargs):
|
def load(cls, load_path: str, env: Optional[GymEnv] = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Load the model from a zip-file
|
Load the model from a zip-file
|
||||||
|
|
||||||
|
|
@ -368,7 +390,8 @@ class BaseRLModel(ABC):
|
||||||
return model
|
return model
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _load_from_file(load_path: str, load_data: bool = True):
|
def _load_from_file(load_path: str, load_data: bool = True) -> (Tuple[Optional[Dict[str, Any]],
|
||||||
|
Optional[TensorDict], Optional[OptimizerStateDict]]):
|
||||||
""" Load model data from a .zip archive
|
""" Load model data from a .zip archive
|
||||||
|
|
||||||
:param load_path: Where to load the model from
|
:param load_path: Where to load the model from
|
||||||
|
|
@ -450,12 +473,14 @@ class BaseRLModel(ABC):
|
||||||
if self.eval_env is not None:
|
if self.eval_env is not None:
|
||||||
self.eval_env.seed(seed)
|
self.eval_env.seed(seed)
|
||||||
|
|
||||||
def _setup_learn(self, eval_env):
|
def _setup_learn(self, eval_env: Optional[GymEnv], callback=None) -> (Tuple[int, int,
|
||||||
|
List[Any], np.ndarray, Optional[VecEnv], Any]):
|
||||||
"""
|
"""
|
||||||
Initialize different variables needed for training.
|
Initialize different variables needed for training.
|
||||||
|
|
||||||
:param eval_env: (gym.Env or VecEnv)
|
:param eval_env: (Optional[GymEnv])
|
||||||
:return: (int, int, [float], np.ndarray, VecEnv)
|
:param callback: (Union[None, BaseCallback, List[BaseCallback, Callable]])
|
||||||
|
:return: (int, int, [float], np.ndarray, VecEnv, BaseCallback)
|
||||||
"""
|
"""
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
self.ep_info_buffer = deque(maxlen=100)
|
self.ep_info_buffer = deque(maxlen=100)
|
||||||
|
|
@ -463,6 +488,8 @@ class BaseRLModel(ABC):
|
||||||
if self.action_noise is not None:
|
if self.action_noise is not None:
|
||||||
self.action_noise.reset()
|
self.action_noise.reset()
|
||||||
|
|
||||||
|
callback = self._init_callback(callback)
|
||||||
|
|
||||||
timesteps_since_eval, episode_num = 0, 0
|
timesteps_since_eval, episode_num = 0, 0
|
||||||
evaluations = []
|
evaluations = []
|
||||||
|
|
||||||
|
|
@ -470,10 +497,11 @@ class BaseRLModel(ABC):
|
||||||
eval_env.seed(self.seed)
|
eval_env.seed(self.seed)
|
||||||
|
|
||||||
eval_env = self._get_eval_env(eval_env)
|
eval_env = self._get_eval_env(eval_env)
|
||||||
obs = self.env.reset() # type: Union[gym.Env, VecEnv]
|
obs = self.env.reset() # type: GymEnv
|
||||||
return timesteps_since_eval, episode_num, evaluations, obs, eval_env
|
|
||||||
|
|
||||||
def _update_info_buffer(self, infos):
|
return timesteps_since_eval, episode_num, evaluations, obs, eval_env, callback
|
||||||
|
|
||||||
|
def _update_info_buffer(self, infos: List[Dict[str, Any]]) -> None:
|
||||||
"""
|
"""
|
||||||
Retrieve reward and episode length and update the buffer
|
Retrieve reward and episode length and update the buffer
|
||||||
if using Monitor wrapper.
|
if using Monitor wrapper.
|
||||||
|
|
@ -485,11 +513,19 @@ class BaseRLModel(ABC):
|
||||||
if maybe_ep_info is not None:
|
if maybe_ep_info is not None:
|
||||||
self.ep_info_buffer.extend([maybe_ep_info])
|
self.ep_info_buffer.extend([maybe_ep_info])
|
||||||
|
|
||||||
def collect_rollouts(self, env, n_episodes=1, n_steps=-1, action_noise=None,
|
def collect_rollouts(self,
|
||||||
deterministic=False, callback=None,
|
env: VecEnv,
|
||||||
learning_starts=0, num_timesteps=0,
|
callback: 'BaseCallback', # Type hint as string to avoid circular import
|
||||||
replay_buffer=None, obs=None,
|
n_episodes: int = 1,
|
||||||
episode_num=0, log_interval=None):
|
n_steps: int = -1,
|
||||||
|
action_noise: Optional[ActionNoise] = None,
|
||||||
|
deterministic: bool = False,
|
||||||
|
learning_starts: int = 0,
|
||||||
|
num_timesteps: int = 0,
|
||||||
|
replay_buffer=None,
|
||||||
|
obs: Optional[np.ndarray] = None,
|
||||||
|
episode_num: int = 0,
|
||||||
|
log_interval: Optional[int] = None) -> Tuple[float, int, int, Optional[np.ndarray], bool]:
|
||||||
"""
|
"""
|
||||||
Collect rollout using the current policy (and possibly fill the replay buffer)
|
Collect rollout using the current policy (and possibly fill the replay buffer)
|
||||||
TODO: move this method to off-policy base class.
|
TODO: move this method to off-policy base class.
|
||||||
|
|
@ -499,7 +535,7 @@ class BaseRLModel(ABC):
|
||||||
:param n_steps: (int)
|
:param n_steps: (int)
|
||||||
:param action_noise: (ActionNoise)
|
:param action_noise: (ActionNoise)
|
||||||
:param deterministic: (bool)
|
:param deterministic: (bool)
|
||||||
:param callback: (callable)
|
:param callback: (BaseCallback)
|
||||||
:param learning_starts: (int)
|
:param learning_starts: (int)
|
||||||
:param num_timesteps: (int)
|
:param num_timesteps: (int)
|
||||||
:param replay_buffer: (ReplayBuffer)
|
:param replay_buffer: (ReplayBuffer)
|
||||||
|
|
@ -524,6 +560,9 @@ class BaseRLModel(ABC):
|
||||||
if self.on_policy_exploration:
|
if self.on_policy_exploration:
|
||||||
self.rollout_data = {key: [] for key in ['observations', 'actions', 'rewards', 'dones', 'values']}
|
self.rollout_data = {key: [] for key in ['observations', 'actions', 'rewards', 'dones', 'values']}
|
||||||
|
|
||||||
|
callback.on_rollout_start()
|
||||||
|
continue_training = True
|
||||||
|
|
||||||
while total_steps < n_steps or total_episodes < n_episodes:
|
while total_steps < n_steps or total_episodes < n_episodes:
|
||||||
done = False
|
done = False
|
||||||
# Reset environment: not needed for VecEnv
|
# Reset environment: not needed for VecEnv
|
||||||
|
|
@ -531,6 +570,12 @@ class BaseRLModel(ABC):
|
||||||
episode_reward, episode_timesteps = 0.0, 0
|
episode_reward, episode_timesteps = 0.0, 0
|
||||||
|
|
||||||
while not done:
|
while not done:
|
||||||
|
|
||||||
|
# Only stop training if return value is False, not when it is None.
|
||||||
|
if callback() is False:
|
||||||
|
continue_training = False
|
||||||
|
return 0.0, total_steps, total_episodes, None, continue_training
|
||||||
|
|
||||||
if self.use_sde and self.sde_sample_freq > 0 and n_steps % self.sde_sample_freq == 0:
|
if self.use_sde and self.sde_sample_freq > 0 and n_steps % self.sde_sample_freq == 0:
|
||||||
# Sample a new noise matrix
|
# Sample a new noise matrix
|
||||||
self.actor.reset_noise()
|
self.actor.reset_noise()
|
||||||
|
|
@ -650,11 +695,13 @@ class BaseRLModel(ABC):
|
||||||
self.rollout_data['returns'][step] = last_return
|
self.rollout_data['returns'][step] = last_return
|
||||||
self.rollout_data['advantage'] = self.rollout_data['returns'] - self.rollout_data['values']
|
self.rollout_data['advantage'] = self.rollout_data['returns'] - self.rollout_data['values']
|
||||||
|
|
||||||
return mean_reward, total_steps, total_episodes, obs
|
callback.on_rollout_end()
|
||||||
|
|
||||||
|
return mean_reward, total_steps, total_episodes, obs, continue_training
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _save_to_file_zip(save_path: str, data: Dict[str, Any] = None,
|
def _save_to_file_zip(save_path: str, data: Dict[str, Any] = None,
|
||||||
params: Dict[str, Any] = None, opt_params: Dict[str, Any] = None) -> None:
|
params: TensorDict = None, opt_params: OptimizerStateDict = None) -> None:
|
||||||
"""
|
"""
|
||||||
Save model to a zip archive.
|
Save model to a zip archive.
|
||||||
|
|
||||||
|
|
@ -730,7 +777,7 @@ class BaseRLModel(ABC):
|
||||||
opt_params_to_save = self.get_opt_parameters()
|
opt_params_to_save = self.get_opt_parameters()
|
||||||
self._save_to_file_zip(path, data=data, params=params_to_save, opt_params=opt_params_to_save)
|
self._save_to_file_zip(path, data=data, params=params_to_save, opt_params=opt_params_to_save)
|
||||||
|
|
||||||
def _eval_policy(self, eval_freq: int, eval_env: int, n_eval_episodes: int,
|
def _eval_policy(self, eval_freq: int, eval_env: GymEnv, n_eval_episodes: int,
|
||||||
timesteps_since_eval: int, render: bool = False, deterministic: bool = True) -> int:
|
timesteps_since_eval: int, render: bool = False, deterministic: bool = True) -> int:
|
||||||
"""
|
"""
|
||||||
Evaluate the current policy on a test environment.
|
Evaluate the current policy on a test environment.
|
||||||
|
|
|
||||||
320
torchy_baselines/common/callbacks.py
Normal file
320
torchy_baselines/common/callbacks.py
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
import os
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Union, List, Dict, Any, Optional
|
||||||
|
|
||||||
|
import gym
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from torchy_baselines.common.base_class import BaseRLModel # pytype: disable=pyi-error
|
||||||
|
from torchy_baselines.common.vec_env import VecEnv, sync_envs_normalization
|
||||||
|
from torchy_baselines.common.evaluation import evaluate_policy
|
||||||
|
from torchy_baselines.common.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCallback(ABC):
|
||||||
|
"""
|
||||||
|
Base class for callback.
|
||||||
|
|
||||||
|
:param verbose: (int)
|
||||||
|
"""
|
||||||
|
def __init__(self, verbose: int = 0):
|
||||||
|
super(BaseCallback, self).__init__()
|
||||||
|
self.model = None # type: BaseRLModel
|
||||||
|
self.training_env = None # type: Union[gym.Env, VecEnv, None]
|
||||||
|
self.n_calls = 0 # type: int
|
||||||
|
self.num_timesteps = 0 # type: int
|
||||||
|
self.verbose = verbose
|
||||||
|
self.locals = None # type: Dict[str, Any]
|
||||||
|
self.globals = None # type: Dict[str, Any]
|
||||||
|
self.logger = None # type: Logger
|
||||||
|
# Sometimes, for event callback, it is useful
|
||||||
|
# to have access to the parent object
|
||||||
|
self.parent = None # type: Optional[BaseCallback]
|
||||||
|
|
||||||
|
def init_callback(self, model: BaseRLModel) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the callback by saving references to the
|
||||||
|
RL model and the training environment for convenience.
|
||||||
|
"""
|
||||||
|
self.model = model
|
||||||
|
self.training_env = model.get_env()
|
||||||
|
self.logger = Logger.CURRENT
|
||||||
|
self._init_callback()
|
||||||
|
|
||||||
|
def _init_callback(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_training_start(self, locals_: Dict[str, Any], globals_: Dict[str, Any]) -> None:
|
||||||
|
# Those are reference and will be updated automatically
|
||||||
|
self.locals = locals_
|
||||||
|
self.globals = globals_
|
||||||
|
self._on_training_start()
|
||||||
|
|
||||||
|
def _on_training_start(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_rollout_start(self) -> None:
|
||||||
|
self._on_rollout_start()
|
||||||
|
|
||||||
|
def _on_rollout_start(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
"""
|
||||||
|
:return: (bool) If the callback returns False, training is aborted early.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __call__(self) -> bool:
|
||||||
|
"""
|
||||||
|
This method will be called by the model. This is the equivalent to the callback function.
|
||||||
|
:return: (bool) If the callback returns False, training is aborted early.
|
||||||
|
"""
|
||||||
|
self.n_calls += 1
|
||||||
|
# timesteps start at zero
|
||||||
|
self.num_timesteps = self.model.num_timesteps + 1
|
||||||
|
|
||||||
|
return self._on_step()
|
||||||
|
|
||||||
|
def on_training_end(self) -> None:
|
||||||
|
self._on_training_end()
|
||||||
|
|
||||||
|
def _on_training_end(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_rollout_end(self) -> None:
|
||||||
|
self._on_rollout_end()
|
||||||
|
|
||||||
|
def _on_rollout_end(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EventCallback(BaseCallback):
|
||||||
|
"""
|
||||||
|
Base class for triggering callback on event.
|
||||||
|
|
||||||
|
:param callback: (Optional[BaseCallback]) Callback that will be called
|
||||||
|
when an event is triggered.
|
||||||
|
:param verbose: (int)
|
||||||
|
"""
|
||||||
|
def __init__(self, callback: Optional[BaseCallback] = None, verbose: int = 0):
|
||||||
|
super(EventCallback, self).__init__(verbose=verbose)
|
||||||
|
self.callback = callback
|
||||||
|
# Give access to the parent
|
||||||
|
if callback is not None:
|
||||||
|
self.callback.parent = self
|
||||||
|
|
||||||
|
def init_callback(self, model: BaseRLModel) -> None:
|
||||||
|
super(EventCallback, self).init_callback(model)
|
||||||
|
self.callback.init_callback(self.model)
|
||||||
|
|
||||||
|
def _on_training_start(self) -> None:
|
||||||
|
self.callback.on_training_start(self.locals, self.globals)
|
||||||
|
|
||||||
|
def _on_event(self) -> bool:
|
||||||
|
if self.callback is not None:
|
||||||
|
return self.callback()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackList(BaseCallback):
|
||||||
|
def __init__(self, callbacks: List[BaseCallback]):
|
||||||
|
super(CallbackList, self).__init__()
|
||||||
|
assert isinstance(callbacks, list)
|
||||||
|
self.callbacks = callbacks
|
||||||
|
|
||||||
|
def _init_callback(self) -> None:
|
||||||
|
for callback in self.callbacks:
|
||||||
|
callback.init_callback(self.model)
|
||||||
|
|
||||||
|
def _on_training_start(self) -> None:
|
||||||
|
for callback in self.callbacks:
|
||||||
|
callback.on_training_start(self.locals, self.globals)
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
continue_training = True
|
||||||
|
for callback in self.callbacks:
|
||||||
|
# # Update variables
|
||||||
|
# callback.num_timesteps = self.num_timesteps
|
||||||
|
# callback.n_calls = self.n_calls
|
||||||
|
# Return False (stop training) if at least one callback returns False
|
||||||
|
continue_training = callback() and continue_training
|
||||||
|
return continue_training
|
||||||
|
|
||||||
|
def _on_training_end(self) -> None:
|
||||||
|
for callback in self.callbacks:
|
||||||
|
callback.on_training_end()
|
||||||
|
|
||||||
|
|
||||||
|
class CheckpointCallback(BaseCallback):
|
||||||
|
"""
|
||||||
|
Callback for saving a model every `save_freq` steps
|
||||||
|
|
||||||
|
:param save_freq: (int)
|
||||||
|
:param save_path: (str) Path to the folder where the model will be saved.
|
||||||
|
:param name_prefix: (str) Common prefix to the saved models
|
||||||
|
"""
|
||||||
|
def __init__(self, save_freq: int, save_path: str, name_prefix='rl_model', verbose=0):
|
||||||
|
super(CheckpointCallback, self).__init__(verbose)
|
||||||
|
self.save_freq = save_freq
|
||||||
|
self.save_path = save_path
|
||||||
|
self.name_prefix = name_prefix
|
||||||
|
|
||||||
|
def _init_callback(self) -> None:
|
||||||
|
# Create folder if needed
|
||||||
|
if self.save_path is not None:
|
||||||
|
os.makedirs(self.save_path, exist_ok=True)
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
if self.n_calls % self.save_freq == 0:
|
||||||
|
path = os.path.join(self.save_path, f'{self.name_prefix}_{self.num_timesteps}_steps')
|
||||||
|
self.model.save(path)
|
||||||
|
if self.verbose > 1:
|
||||||
|
print(f"Saving model checkpoint to {path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertCallback(BaseCallback):
|
||||||
|
"""
|
||||||
|
Convert functional callback (old-style) to object.
|
||||||
|
|
||||||
|
:param on_step: (callable)
|
||||||
|
:param verbose: (int)
|
||||||
|
"""
|
||||||
|
def __init__(self, callback, verbose=0):
|
||||||
|
super(ConvertCallback, self).__init__(verbose)
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
if self.callback is not None:
|
||||||
|
return self.callback(self.locals, self.globals)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class EvalCallback(EventCallback):
|
||||||
|
"""
|
||||||
|
Callback for evaluating an agent.
|
||||||
|
|
||||||
|
:param eval_env: (Union[gym.Env, VecEnv]) The environment used for initialization
|
||||||
|
:param callback_on_new_best: (Optional[BaseCallback]) Callback to trigger
|
||||||
|
when there is a new best model according to the `mean_reward`
|
||||||
|
:param n_eval_episodes: (int) The number of episodes to test the agent
|
||||||
|
:param eval_freq: (int) Evaluate the agent every eval_freq call of the callback.
|
||||||
|
:param log_path: (str) Path to a log file (.npz) where the evaluations
|
||||||
|
will be saved. It will be updated at each evaluation.
|
||||||
|
:param best_model_save_path: (str) Path to a folder where the best model
|
||||||
|
according to performance on the eval env will be saved.
|
||||||
|
:param deterministic: (bool) Whether the evaluation should
|
||||||
|
use a stochastic or deterministic actions.
|
||||||
|
:param verbose: (int)
|
||||||
|
"""
|
||||||
|
def __init__(self, eval_env: Union[gym.Env, VecEnv],
|
||||||
|
callback_on_new_best: Optional[BaseCallback] = None,
|
||||||
|
n_eval_episodes: int = 5,
|
||||||
|
eval_freq: int = 10000,
|
||||||
|
log_path: str = None,
|
||||||
|
best_model_save_path: str = None,
|
||||||
|
deterministic: bool = True,
|
||||||
|
verbose: int = 1):
|
||||||
|
super(EvalCallback, self).__init__(callback_on_new_best, verbose=verbose)
|
||||||
|
self.n_eval_episodes = n_eval_episodes
|
||||||
|
self.eval_freq = eval_freq
|
||||||
|
self.best_mean_reward = -np.inf
|
||||||
|
self.deterministic = deterministic
|
||||||
|
if isinstance(eval_env, VecEnv):
|
||||||
|
assert eval_env.num_envs == 1, "You must pass only one environment for evaluation"
|
||||||
|
|
||||||
|
self.eval_env = eval_env
|
||||||
|
self.best_model_save_path = best_model_save_path
|
||||||
|
self.log_path = log_path
|
||||||
|
self.evaluations_results = []
|
||||||
|
self.evaluations_timesteps = []
|
||||||
|
|
||||||
|
def _init_callback(self):
|
||||||
|
# Does not work when eval_env is a gym.Env and training_env is a VecEnv
|
||||||
|
# assert type(self.training_env) is type(self.eval_env), ("training and eval env are not of the same type",
|
||||||
|
# "{} != {}".format(self.training_env, self.eval_env))
|
||||||
|
|
||||||
|
# Create folders if needed
|
||||||
|
if self.best_model_save_path is not None:
|
||||||
|
os.makedirs(self.best_model_save_path, exist_ok=True)
|
||||||
|
if self.log_path is not None:
|
||||||
|
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
|
||||||
|
if self.n_calls % self.eval_freq == 0:
|
||||||
|
# Sync training and eval env if there is VecNormalize
|
||||||
|
sync_envs_normalization(self.training_env, self.eval_env)
|
||||||
|
|
||||||
|
episode_rewards, _ = evaluate_policy(self.model, self.eval_env, n_eval_episodes=self.n_eval_episodes,
|
||||||
|
deterministic=self.deterministic, return_episode_rewards=True)
|
||||||
|
|
||||||
|
if self.log_path is not None:
|
||||||
|
self.evaluations_timesteps.append(self.num_timesteps)
|
||||||
|
self.evaluations_results.append(episode_rewards)
|
||||||
|
np.savez(self.log_path, timesteps=self.evaluations_timesteps, results=self.evaluations_results)
|
||||||
|
|
||||||
|
mean_reward, std_reward = np.mean(episode_rewards), np.std(episode_rewards)
|
||||||
|
if self.verbose > 0:
|
||||||
|
print(f"Eval num_timesteps={self.num_timesteps}, "
|
||||||
|
f"episode_reward={mean_reward:.2f} +/- {std_reward:.2f}")
|
||||||
|
|
||||||
|
if mean_reward > self.best_mean_reward:
|
||||||
|
if self.verbose > 0:
|
||||||
|
print("New best mean reward!")
|
||||||
|
if self.best_model_save_path is not None:
|
||||||
|
self.model.save(os.path.join(self.best_model_save_path, 'best_model'))
|
||||||
|
self.best_mean_reward = mean_reward
|
||||||
|
# Trigger callback if needed
|
||||||
|
if self.callback is not None:
|
||||||
|
return self._on_event()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class StopTrainingOnRewardThreshold(BaseCallback):
|
||||||
|
"""
|
||||||
|
Stop the training once a threshold in episodic reward
|
||||||
|
has been reached (i.e. when the model is good enough).
|
||||||
|
|
||||||
|
It must be used with the `EvalCallback`.
|
||||||
|
|
||||||
|
:param reward_threshold: (float) Minimum expected reward per episode
|
||||||
|
to stop training.
|
||||||
|
:param verbose: (int)
|
||||||
|
"""
|
||||||
|
def __init__(self, reward_threshold: float, verbose: int = 0):
|
||||||
|
super(StopTrainingOnRewardThreshold, self).__init__(verbose=verbose)
|
||||||
|
self.reward_threshold = reward_threshold
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
assert self.parent is not None, ("`StopTrainingOnMinimumReward` callback must be used "
|
||||||
|
"with an `EvalCallback`")
|
||||||
|
# Convert np.bool to bool, otherwise callback() is False won't work
|
||||||
|
continue_training = bool(self.parent.best_mean_reward < self.reward_threshold)
|
||||||
|
if self.verbose > 0 and not continue_training:
|
||||||
|
print(f"Stopping training because the mean reward {self.parent.best_mean_reward:.2f} "
|
||||||
|
f" is above the threshold {self.reward_threshold}")
|
||||||
|
return continue_training
|
||||||
|
|
||||||
|
|
||||||
|
class EveryNTimesteps(EventCallback):
|
||||||
|
"""
|
||||||
|
Trigger a callback every `n_steps` timesteps
|
||||||
|
|
||||||
|
:param n_steps: (int) Number of timesteps between two trigger.
|
||||||
|
:param callback: (BaseCallback) Callback that will be called
|
||||||
|
when the event is triggered.
|
||||||
|
"""
|
||||||
|
def __init__(self, n_steps: int, callback: BaseCallback):
|
||||||
|
super(EveryNTimesteps, self).__init__(callback)
|
||||||
|
self.n_steps = n_steps
|
||||||
|
self.last_time_trigger = 0
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
if (self.num_timesteps - self.last_time_trigger) >= self.n_steps:
|
||||||
|
self.last_time_trigger = self.num_timesteps
|
||||||
|
return self._on_event()
|
||||||
|
return True
|
||||||
|
|
@ -1,19 +1,26 @@
|
||||||
"""
|
"""
|
||||||
Taken from stable-baselines
|
Taken from stable-baselines
|
||||||
"""
|
"""
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
class ActionNoise(object):
|
class ActionNoise(ABC):
|
||||||
"""
|
"""
|
||||||
The action noise base class
|
The action noise base class
|
||||||
"""
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super(ActionNoise, self).__init__()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""
|
"""
|
||||||
call end of episode reset for the noise
|
call end of episode reset for the noise
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __call__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
class NormalActionNoise(ActionNoise):
|
class NormalActionNoise(ActionNoise):
|
||||||
"""
|
"""
|
||||||
|
|
@ -25,6 +32,7 @@ class NormalActionNoise(ActionNoise):
|
||||||
def __init__(self, mean, sigma):
|
def __init__(self, mean, sigma):
|
||||||
self._mu = mean
|
self._mu = mean
|
||||||
self._sigma = sigma
|
self._sigma = sigma
|
||||||
|
super(NormalActionNoise, self).__init__()
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
return np.random.normal(self._mu, self._sigma)
|
return np.random.normal(self._mu, self._sigma)
|
||||||
|
|
@ -54,6 +62,7 @@ class OrnsteinUhlenbeckActionNoise(ActionNoise):
|
||||||
self.initial_noise = initial_noise
|
self.initial_noise = initial_noise
|
||||||
self.noise_prev = None
|
self.noise_prev = None
|
||||||
self.reset()
|
self.reset()
|
||||||
|
super(OrnsteinUhlenbeckActionNoise, self).__init__()
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
noise = self.noise_prev + self._theta * (self._mu - self.noise_prev) * self._dt + \
|
noise = self.noise_prev + self._theta * (self._mu - self.noise_prev) * self._dt + \
|
||||||
|
|
|
||||||
14
torchy_baselines/common/type_aliases.py
Normal file
14
torchy_baselines/common/type_aliases.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
"""
|
||||||
|
Common aliases for type hing
|
||||||
|
"""
|
||||||
|
from typing import Union, Type, Optional, Dict, Any, List, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import gym
|
||||||
|
|
||||||
|
from torchy_baselines.common.vec_env import VecEnv
|
||||||
|
|
||||||
|
|
||||||
|
GymEnv = Union[gym.Env, VecEnv]
|
||||||
|
TensorDict = Dict[str, torch.Tensor]
|
||||||
|
OptimizerStateDict = Dict[str, Any]
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import gym
|
import gym
|
||||||
from gym import spaces
|
from gym import spaces
|
||||||
|
|
@ -16,6 +17,8 @@ import numpy as np
|
||||||
from torchy_baselines.common.base_class import BaseRLModel
|
from torchy_baselines.common.base_class import BaseRLModel
|
||||||
from torchy_baselines.common.buffers import RolloutBuffer
|
from torchy_baselines.common.buffers import RolloutBuffer
|
||||||
from torchy_baselines.common.utils import explained_variance, get_schedule_fn
|
from torchy_baselines.common.utils import explained_variance, get_schedule_fn
|
||||||
|
from torchy_baselines.common.vec_env import VecEnv
|
||||||
|
from torchy_baselines.common.callbacks import BaseCallback
|
||||||
from torchy_baselines.common import logger
|
from torchy_baselines.common import logger
|
||||||
from torchy_baselines.ppo.policies import PPOPolicy
|
from torchy_baselines.ppo.policies import PPOPolicy
|
||||||
|
|
||||||
|
|
@ -149,17 +152,30 @@ class PPO(BaseRLModel):
|
||||||
clipped_actions = np.clip(clipped_actions, self.action_space.low, self.action_space.high)
|
clipped_actions = np.clip(clipped_actions, self.action_space.low, self.action_space.high)
|
||||||
return clipped_actions
|
return clipped_actions
|
||||||
|
|
||||||
def collect_rollouts(self, env, rollout_buffer, n_rollout_steps=256, callback=None,
|
def collect_rollouts(self,
|
||||||
obs=None):
|
env: VecEnv,
|
||||||
|
callback: BaseCallback,
|
||||||
|
rollout_buffer: RolloutBuffer,
|
||||||
|
n_rollout_steps: int = 256,
|
||||||
|
obs: Optional[np.ndarray] = None) -> Tuple[Optional[np.ndarray], bool]:
|
||||||
|
|
||||||
n_steps = 0
|
n_steps = 0
|
||||||
|
continue_training = True
|
||||||
rollout_buffer.reset()
|
rollout_buffer.reset()
|
||||||
# Sample new weights for the state dependent exploration
|
# Sample new weights for the state dependent exploration
|
||||||
# TODO: ensure episodic setting?
|
# TODO: ensure episodic setting?
|
||||||
if self.use_sde:
|
if self.use_sde:
|
||||||
self.policy.reset_noise(env.num_envs)
|
self.policy.reset_noise(env.num_envs)
|
||||||
|
|
||||||
|
callback.on_rollout_start()
|
||||||
|
|
||||||
while n_steps < n_rollout_steps:
|
while n_steps < n_rollout_steps:
|
||||||
|
|
||||||
|
if callback() is False:
|
||||||
|
continue_training = False
|
||||||
|
return None, continue_training
|
||||||
|
|
||||||
|
|
||||||
if self.use_sde and self.sde_sample_freq > 0 and n_steps % self.sde_sample_freq == 0:
|
if self.use_sde and self.sde_sample_freq > 0 and n_steps % self.sde_sample_freq == 0:
|
||||||
# Sample a new noise matrix
|
# Sample a new noise matrix
|
||||||
self.policy.reset_noise(env.num_envs)
|
self.policy.reset_noise(env.num_envs)
|
||||||
|
|
@ -185,7 +201,9 @@ class PPO(BaseRLModel):
|
||||||
|
|
||||||
rollout_buffer.compute_returns_and_advantage(values, dones=dones)
|
rollout_buffer.compute_returns_and_advantage(values, dones=dones)
|
||||||
|
|
||||||
return obs
|
callback.on_rollout_end()
|
||||||
|
|
||||||
|
return obs, continue_training
|
||||||
|
|
||||||
def train(self, gradient_steps, batch_size=64):
|
def train(self, gradient_steps, batch_size=64):
|
||||||
# Update optimizer learning rate
|
# Update optimizer learning rate
|
||||||
|
|
@ -268,20 +286,21 @@ class PPO(BaseRLModel):
|
||||||
def learn(self, total_timesteps, callback=None, log_interval=1,
|
def learn(self, total_timesteps, callback=None, log_interval=1,
|
||||||
eval_env=None, eval_freq=-1, n_eval_episodes=5, tb_log_name="PPO", reset_num_timesteps=True):
|
eval_env=None, eval_freq=-1, n_eval_episodes=5, tb_log_name="PPO", reset_num_timesteps=True):
|
||||||
|
|
||||||
timesteps_since_eval, iteration, evaluations, obs, eval_env = self._setup_learn(eval_env)
|
timesteps_since_eval, iteration, evaluations, obs, eval_env, callback = self._setup_learn(eval_env, callback)
|
||||||
|
|
||||||
if self.tensorboard_log is not None and SummaryWriter is not None:
|
if self.tensorboard_log is not None and SummaryWriter is not None:
|
||||||
self.tb_writer = SummaryWriter(log_dir=os.path.join(self.tensorboard_log, tb_log_name))
|
self.tb_writer = SummaryWriter(log_dir=os.path.join(self.tensorboard_log, tb_log_name))
|
||||||
|
|
||||||
|
callback.on_training_start(locals(), globals())
|
||||||
|
|
||||||
while self.num_timesteps < total_timesteps:
|
while self.num_timesteps < total_timesteps:
|
||||||
|
|
||||||
if callback is not None:
|
obs, continue_training = self.collect_rollouts(self.env, callback, self.rollout_buffer, n_rollout_steps=self.n_steps,
|
||||||
# Only stop training if return value is False, not when it is None.
|
obs=obs)
|
||||||
if callback(locals(), globals()) is False:
|
|
||||||
break
|
if continue_training is False:
|
||||||
|
break
|
||||||
|
|
||||||
obs = self.collect_rollouts(self.env, self.rollout_buffer, n_rollout_steps=self.n_steps,
|
|
||||||
obs=obs)
|
|
||||||
iteration += 1
|
iteration += 1
|
||||||
self.num_timesteps += self.n_steps * self.n_envs
|
self.num_timesteps += self.n_steps * self.n_envs
|
||||||
timesteps_since_eval += self.n_steps * self.n_envs
|
timesteps_since_eval += self.n_steps * self.n_envs
|
||||||
|
|
@ -308,6 +327,8 @@ class PPO(BaseRLModel):
|
||||||
# if self.tb_writer is not None:
|
# if self.tb_writer is not None:
|
||||||
# self.tb_writer.add_scalar('Eval/reward', mean_reward, self.num_timesteps)
|
# self.tb_writer.add_scalar('Eval/reward', mean_reward, self.num_timesteps)
|
||||||
|
|
||||||
|
callback.on_training_end()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_opt_parameters(self):
|
def get_opt_parameters(self):
|
||||||
|
|
|
||||||
|
|
@ -259,25 +259,24 @@ class SAC(BaseRLModel):
|
||||||
eval_env=None, eval_freq=-1, n_eval_episodes=5, tb_log_name="SAC",
|
eval_env=None, eval_freq=-1, n_eval_episodes=5, tb_log_name="SAC",
|
||||||
reset_num_timesteps=True):
|
reset_num_timesteps=True):
|
||||||
|
|
||||||
timesteps_since_eval, episode_num, evaluations, obs, eval_env = self._setup_learn(eval_env)
|
timesteps_since_eval, episode_num, evaluations, obs, eval_env, callback = self._setup_learn(eval_env, callback)
|
||||||
|
|
||||||
|
callback.on_training_start(locals(), globals())
|
||||||
|
|
||||||
while self.num_timesteps < total_timesteps:
|
while self.num_timesteps < total_timesteps:
|
||||||
|
|
||||||
if callback is not None:
|
|
||||||
# Only stop training if return value is False, not when it is None.
|
|
||||||
if callback(locals(), globals()) is False:
|
|
||||||
break
|
|
||||||
|
|
||||||
rollout = self.collect_rollouts(self.env, n_episodes=self.n_episodes_rollout,
|
rollout = self.collect_rollouts(self.env, n_episodes=self.n_episodes_rollout,
|
||||||
n_steps=self.train_freq, action_noise=self.action_noise,
|
n_steps=self.train_freq, action_noise=self.action_noise,
|
||||||
deterministic=False, callback=None,
|
deterministic=False, callback=callback,
|
||||||
learning_starts=self.learning_starts,
|
learning_starts=self.learning_starts,
|
||||||
num_timesteps=self.num_timesteps,
|
num_timesteps=self.num_timesteps,
|
||||||
replay_buffer=self.replay_buffer,
|
replay_buffer=self.replay_buffer,
|
||||||
obs=obs, episode_num=episode_num,
|
obs=obs, episode_num=episode_num,
|
||||||
log_interval=log_interval)
|
log_interval=log_interval)
|
||||||
# Unpack
|
# Unpack
|
||||||
episode_reward, episode_timesteps, n_episodes, obs = rollout
|
episode_reward, episode_timesteps, n_episodes, obs, continue_training = rollout
|
||||||
|
|
||||||
|
if continue_training is False:
|
||||||
|
break
|
||||||
|
|
||||||
self.num_timesteps += episode_timesteps
|
self.num_timesteps += episode_timesteps
|
||||||
episode_num += n_episodes
|
episode_num += n_episodes
|
||||||
|
|
@ -292,6 +291,7 @@ class SAC(BaseRLModel):
|
||||||
timesteps_since_eval = self._eval_policy(eval_freq, eval_env, n_eval_episodes,
|
timesteps_since_eval = self._eval_policy(eval_freq, eval_env, n_eval_episodes,
|
||||||
timesteps_since_eval, deterministic=True)
|
timesteps_since_eval, deterministic=True)
|
||||||
|
|
||||||
|
callback.on_training_end()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_opt_parameters(self):
|
def get_opt_parameters(self):
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
from typing import List, Tuple, Callable, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
import torch as th
|
import torch as th
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
|
|
||||||
|
|
@ -27,9 +30,18 @@ class Actor(BaseNetwork):
|
||||||
a positive standard deviation (cf paper). It allows to keep variance
|
a positive standard deviation (cf paper). It allows to keep variance
|
||||||
above zero and prevent it from growing too fast. In practice, `exp()` is usually enough.
|
above zero and prevent it from growing too fast. In practice, `exp()` is usually enough.
|
||||||
"""
|
"""
|
||||||
def __init__(self, obs_dim, action_dim, net_arch, activation_fn=nn.ReLU,
|
def __init__(self,
|
||||||
use_sde=False, log_std_init=-3, clip_noise=None,
|
obs_dim: int,
|
||||||
lr_sde=3e-4, full_std=False, sde_net_arch=None, use_expln=False):
|
action_dim: int,
|
||||||
|
net_arch: List[int],
|
||||||
|
activation_fn: nn.Module = nn.ReLU,
|
||||||
|
use_sde: bool = False,
|
||||||
|
log_std_init: float = -3,
|
||||||
|
clip_noise: Optional[float] = None,
|
||||||
|
lr_sde: float = 3e-4,
|
||||||
|
full_std: bool = False,
|
||||||
|
sde_net_arch: Optional[List[int]] = None,
|
||||||
|
use_expln: bool = False):
|
||||||
super(Actor, self).__init__()
|
super(Actor, self).__init__()
|
||||||
|
|
||||||
self.latent_pi, self.log_std = None, None
|
self.latent_pi, self.log_std = None, None
|
||||||
|
|
@ -65,7 +77,7 @@ class Actor(BaseNetwork):
|
||||||
actor_net = create_mlp(obs_dim, action_dim, net_arch, activation_fn, squash_out=True)
|
actor_net = create_mlp(obs_dim, action_dim, net_arch, activation_fn, squash_out=True)
|
||||||
self.mu = nn.Sequential(*actor_net)
|
self.mu = nn.Sequential(*actor_net)
|
||||||
|
|
||||||
def get_std(self):
|
def get_std(self) -> torch.Tensor:
|
||||||
"""
|
"""
|
||||||
Retrieve the standard deviation of the action distribution.
|
Retrieve the standard deviation of the action distribution.
|
||||||
Only useful when using SDE.
|
Only useful when using SDE.
|
||||||
|
|
@ -81,7 +93,7 @@ class Actor(BaseNetwork):
|
||||||
mean_actions = self.mu(latent_pi)
|
mean_actions = self.mu(latent_pi)
|
||||||
return self.action_dist.proba_distribution(mean_actions, self.log_std, latent_sde)
|
return self.action_dist.proba_distribution(mean_actions, self.log_std, latent_sde)
|
||||||
|
|
||||||
def _get_latent(self, obs):
|
def _get_latent(self, obs) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
latent_pi = self.latent_pi(obs)
|
latent_pi = self.latent_pi(obs)
|
||||||
|
|
||||||
if self.sde_feature_extractor is not None:
|
if self.sde_feature_extractor is not None:
|
||||||
|
|
@ -90,7 +102,7 @@ class Actor(BaseNetwork):
|
||||||
latent_sde = latent_pi
|
latent_sde = latent_pi
|
||||||
return latent_pi, latent_sde
|
return latent_pi, latent_sde
|
||||||
|
|
||||||
def evaluate_actions(self, obs, action):
|
def evaluate_actions(self, obs: torch.Tensor, action: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
"""
|
"""
|
||||||
Evaluate actions according to the current policy,
|
Evaluate actions according to the current policy,
|
||||||
given the observations. Only useful when using SDE.
|
given the observations. Only useful when using SDE.
|
||||||
|
|
@ -106,13 +118,13 @@ class Actor(BaseNetwork):
|
||||||
# value = self.value_net(latent_vf)
|
# value = self.value_net(latent_vf)
|
||||||
return log_prob, distribution.entropy()
|
return log_prob, distribution.entropy()
|
||||||
|
|
||||||
def reset_noise(self):
|
def reset_noise(self) -> None:
|
||||||
"""
|
"""
|
||||||
Sample new weights for the exploration matrix, when using SDE.
|
Sample new weights for the exploration matrix, when using SDE.
|
||||||
"""
|
"""
|
||||||
self.action_dist.sample_weights(self.log_std)
|
self.action_dist.sample_weights(self.log_std)
|
||||||
|
|
||||||
def forward(self, obs, deterministic=True):
|
def forward(self, obs: torch.Tensor, deterministic: bool = True) -> torch.Tensor:
|
||||||
if self.use_sde:
|
if self.use_sde:
|
||||||
latent_pi, latent_sde = self._get_latent(obs)
|
latent_pi, latent_sde = self._get_latent(obs)
|
||||||
if deterministic:
|
if deterministic:
|
||||||
|
|
@ -141,8 +153,8 @@ class Critic(BaseNetwork):
|
||||||
:param net_arch: ([int]) Network architecture
|
:param net_arch: ([int]) Network architecture
|
||||||
:param activation_fn: (nn.Module) Activation function
|
:param activation_fn: (nn.Module) Activation function
|
||||||
"""
|
"""
|
||||||
def __init__(self, obs_dim, action_dim,
|
def __init__(self, obs_dim: int, action_dim: int,
|
||||||
net_arch, activation_fn=nn.ReLU):
|
net_arch: List[int], activation_fn: nn.Module = nn.ReLU):
|
||||||
super(Critic, self).__init__()
|
super(Critic, self).__init__()
|
||||||
|
|
||||||
q1_net = create_mlp(obs_dim + action_dim, 1, net_arch, activation_fn)
|
q1_net = create_mlp(obs_dim + action_dim, 1, net_arch, activation_fn)
|
||||||
|
|
@ -151,14 +163,12 @@ class Critic(BaseNetwork):
|
||||||
q2_net = create_mlp(obs_dim + action_dim, 1, net_arch, activation_fn)
|
q2_net = create_mlp(obs_dim + action_dim, 1, net_arch, activation_fn)
|
||||||
self.q2_net = nn.Sequential(*q2_net)
|
self.q2_net = nn.Sequential(*q2_net)
|
||||||
|
|
||||||
self.q_networks = [self.q1_net, self.q2_net]
|
def forward(self, obs: torch.Tensor, action: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
|
||||||
def forward(self, obs, action):
|
|
||||||
qvalue_input = th.cat([obs, action], dim=1)
|
qvalue_input = th.cat([obs, action], dim=1)
|
||||||
return [q_net(qvalue_input) for q_net in self.q_networks]
|
return self.q1_net(qvalue_input), self.q2_net(qvalue_input)
|
||||||
|
|
||||||
def q1_forward(self, obs, action):
|
def q1_forward(self, obs: torch.Tensor, action: torch.Tensor) -> torch.Tensor:
|
||||||
return self.q_networks[0](th.cat([obs, action], dim=1))
|
return self.q1_net(th.cat([obs, action], dim=1))
|
||||||
|
|
||||||
|
|
||||||
class ValueFunction(BaseNetwork):
|
class ValueFunction(BaseNetwork):
|
||||||
|
|
|
||||||
|
|
@ -251,25 +251,25 @@ class TD3(BaseRLModel):
|
||||||
def learn(self, total_timesteps, callback=None, log_interval=4,
|
def learn(self, total_timesteps, callback=None, log_interval=4,
|
||||||
eval_env=None, eval_freq=-1, n_eval_episodes=5, tb_log_name="TD3", reset_num_timesteps=True):
|
eval_env=None, eval_freq=-1, n_eval_episodes=5, tb_log_name="TD3", reset_num_timesteps=True):
|
||||||
|
|
||||||
timesteps_since_eval, episode_num, evaluations, obs, eval_env = self._setup_learn(eval_env)
|
timesteps_since_eval, episode_num, evaluations, obs, eval_env, callback = self._setup_learn(eval_env, callback)
|
||||||
|
|
||||||
|
callback.on_training_start(locals(), globals())
|
||||||
|
|
||||||
while self.num_timesteps < total_timesteps:
|
while self.num_timesteps < total_timesteps:
|
||||||
|
|
||||||
if callback is not None:
|
|
||||||
# Only stop training if return value is False, not when it is None.
|
|
||||||
if callback(locals(), globals()) is False:
|
|
||||||
break
|
|
||||||
|
|
||||||
rollout = self.collect_rollouts(self.env, n_episodes=self.n_episodes_rollout,
|
rollout = self.collect_rollouts(self.env, n_episodes=self.n_episodes_rollout,
|
||||||
n_steps=self.train_freq, action_noise=self.action_noise,
|
n_steps=self.train_freq, action_noise=self.action_noise,
|
||||||
deterministic=False, callback=None,
|
deterministic=False, callback=callback,
|
||||||
learning_starts=self.learning_starts,
|
learning_starts=self.learning_starts,
|
||||||
num_timesteps=self.num_timesteps,
|
num_timesteps=self.num_timesteps,
|
||||||
replay_buffer=self.replay_buffer,
|
replay_buffer=self.replay_buffer,
|
||||||
obs=obs, episode_num=episode_num,
|
obs=obs, episode_num=episode_num,
|
||||||
log_interval=log_interval)
|
log_interval=log_interval)
|
||||||
# Unpack
|
# Unpack
|
||||||
episode_reward, episode_timesteps, n_episodes, obs = rollout
|
episode_reward, episode_timesteps, n_episodes, obs, continue_training = rollout
|
||||||
|
|
||||||
|
if continue_training is False:
|
||||||
|
break
|
||||||
|
|
||||||
episode_num += n_episodes
|
episode_num += n_episodes
|
||||||
self.num_timesteps += episode_timesteps
|
self.num_timesteps += episode_timesteps
|
||||||
|
|
@ -294,6 +294,8 @@ class TD3(BaseRLModel):
|
||||||
timesteps_since_eval = self._eval_policy(eval_freq, eval_env, n_eval_episodes,
|
timesteps_since_eval = self._eval_policy(eval_freq, eval_env, n_eval_episodes,
|
||||||
timesteps_since_eval, deterministic=True)
|
timesteps_since_eval, deterministic=True)
|
||||||
|
|
||||||
|
callback.on_training_end()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_opt_parameters(self):
|
def get_opt_parameters(self):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue