|
2 | 2 | import abc |
3 | 3 | import dataclasses |
4 | 4 | import logging |
5 | | -from typing import Callable, Iterable, Iterator, Mapping, Optional, Type, overload |
| 5 | +from typing import Callable, Iterable, Iterator, List, Mapping, Optional, Type, overload |
6 | 6 |
|
7 | 7 | import numpy as np |
8 | 8 | import torch as th |
9 | 9 | import torch.utils.tensorboard as thboard |
10 | 10 | import tqdm |
11 | 11 | from stable_baselines3.common import ( |
12 | 12 | base_class, |
| 13 | + callbacks, |
13 | 14 | distributions, |
| 15 | + off_policy_algorithm, |
14 | 16 | on_policy_algorithm, |
15 | 17 | policies, |
16 | 18 | vec_env, |
|
20 | 22 |
|
21 | 23 | from imitation.algorithms import base |
22 | 24 | from imitation.data import buffer, rollout, types, wrappers |
| 25 | +from imitation.policies import replay_buffer_wrapper |
23 | 26 | from imitation.rewards import reward_nets, reward_wrapper |
24 | 27 | from imitation.util import logger, networks, util |
25 | 28 |
|
@@ -92,6 +95,47 @@ def compute_train_stats( |
92 | 95 | } |
93 | 96 |
|
94 | 97 |
|
| 98 | +class TrainDiscriminatorCallback(callbacks.BaseCallback): |
| 99 | + """Callback for training discriminator after collecting rollouts.""" |
| 100 | + |
| 101 | + def __init__(self, adversarial_trainer, *args, **kwargs): |
| 102 | + """Builds TrainDiscriminatorCallback. |
| 103 | +
|
| 104 | + Args: |
| 105 | + *args: Passed through to `callbacks.BaseCallback`. |
| 106 | + **kwargs: Passed through to `callbacks.BaseCallback`. |
| 107 | + """ |
| 108 | + self.adversarial_trainer = adversarial_trainer |
| 109 | + self.gen_ctx_manager = None |
| 110 | + super().__init__(*args, **kwargs) |
| 111 | + |
| 112 | + def _on_step(self) -> bool: |
| 113 | + return True |
| 114 | + |
| 115 | + def _on_rollout_end(self) -> None: |
| 116 | + gen_trajs, ep_lens = self.adversarial_trainer.venv_buffering.pop_trajectories() |
| 117 | + self.adversarial_trainer._check_fixed_horizon(ep_lens) |
| 118 | + gen_samples = rollout.flatten_trajectories_with_rew(gen_trajs) |
| 119 | + self.adversarial_trainer._gen_replay_buffer.store(gen_samples) |
| 120 | + |
| 121 | + for _ in range(self.adversarial_trainer.n_disc_updates_per_round): |
| 122 | + with networks.training(self.adversarial_trainer.reward_train): |
| 123 | + # switch to training mode (affects dropout, normalization) |
| 124 | + self.adversarial_trainer.train_disc() |
| 125 | + |
| 126 | + # update the rollouts with the reward of the latest discriminator |
| 127 | + self.adversarial_trainer.update_rewards_of_rollouts() |
| 128 | + |
| 129 | + # This is a hacky way to enable logger.accumulate_means for generator |
| 130 | + # This is done to avoid nested loggers of discriminator and generator |
| 131 | + self.gen_ctx_manager = self.adversarial_trainer.logger.accumulate_means("gen") |
| 132 | + self.gen_ctx_manager.__enter__() |
| 133 | + |
| 134 | + def _on_training_end(self) -> None: |
| 135 | + assert self.gen_ctx_manager is not None |
| 136 | + self.gen_ctx_manager.__exit__(None, None, None) |
| 137 | + |
| 138 | + |
95 | 139 | class AdversarialTrainer(base.DemonstrationAlgorithm[types.Transitions]): |
96 | 140 | """Base class for adversarial imitation learning algorithms like GAIL and AIRL.""" |
97 | 141 |
|
@@ -228,16 +272,22 @@ def __init__( |
228 | 272 |
|
229 | 273 | self.venv_buffering = wrappers.BufferingWrapper(self.venv) |
230 | 274 |
|
| 275 | + self.disc_trainer_callback = TrainDiscriminatorCallback(self) |
231 | 276 | if debug_use_ground_truth: |
232 | 277 | # Would use an identity reward fn here, but RewardFns can't see rewards. |
233 | 278 | self.venv_wrapped = self.venv_buffering |
234 | | - self.gen_callback = None |
| 279 | + self.gen_callback: List[callbacks.BaseCallback] = [ |
| 280 | + self.disc_trainer_callback |
| 281 | + ] |
235 | 282 | else: |
236 | 283 | self.venv_wrapped = reward_wrapper.RewardVecEnvWrapper( |
237 | 284 | self.venv_buffering, |
238 | 285 | reward_fn=self.reward_train.predict_processed, |
239 | 286 | ) |
240 | | - self.gen_callback = self.venv_wrapped.make_log_callback() |
| 287 | + self.gen_callback = [ |
| 288 | + self.venv_wrapped.make_log_callback(), |
| 289 | + self.disc_trainer_callback, |
| 290 | + ] |
241 | 291 | self.venv_train = self.venv_wrapped |
242 | 292 |
|
243 | 293 | self.gen_algo.set_env(self.venv_train) |
@@ -314,6 +364,34 @@ def _next_expert_batch(self) -> Mapping: |
314 | 364 | assert self._endless_expert_iterator is not None |
315 | 365 | return next(self._endless_expert_iterator) |
316 | 366 |
|
| 367 | + def update_rewards_of_rollouts(self) -> None: |
| 368 | + """Updates the rewards of the rollouts using the latest discriminator.""" |
| 369 | + if isinstance(self.gen_algo, on_policy_algorithm.OnPolicyAlgorithm): |
| 370 | + buffer = self.gen_algo.rollout_buffer |
| 371 | + assert buffer is not None |
| 372 | + reward_fn_inputs = replay_buffer_wrapper._rollout_buffer_to_reward_fn_input( |
| 373 | + self.gen_algo.rollout_buffer |
| 374 | + ) |
| 375 | + rewards = self._reward_net.predict(**reward_fn_inputs) |
| 376 | + rewards = rewards.reshape(buffer.rewards.shape) |
| 377 | + last_values = buffer.advantages[-1] - buffer.rewards[-1] + buffer.values[-1] |
| 378 | + last_values = last_values / buffer.gamma |
| 379 | + # here we assume that the actual last_values cannot exactly be 0.0 and so if |
| 380 | + # last_values is 0.0 then we know that the episode terminated |
| 381 | + last_dones = last_values == 0.0 |
| 382 | + self.gen_algo.rollout_buffer.rewards[:] = rewards |
| 383 | + self.gen_algo.rollout_buffer.compute_returns_and_advantage( |
| 384 | + th.tensor(last_values), last_dones |
| 385 | + ) |
| 386 | + elif isinstance(self.gen_algo, off_policy_algorithm.OffPolicyAlgorithm): |
| 387 | + buffer = self.gen_algo.replay_buffer |
| 388 | + assert buffer is not None |
| 389 | + reward_fn_inputs = replay_buffer_wrapper._replay_buffer_to_reward_fn_input( |
| 390 | + buffer |
| 391 | + ) |
| 392 | + rewards = self._reward_net.predict(**reward_fn_inputs) |
| 393 | + buffer.rewards[:] = rewards.reshape(buffer.rewards.shape) |
| 394 | + |
317 | 395 | def train_disc( |
318 | 396 | self, |
319 | 397 | *, |
@@ -452,10 +530,6 @@ def train( |
452 | 530 | ) |
453 | 531 | for r in tqdm.tqdm(range(0, n_rounds), desc="round"): |
454 | 532 | self.train_gen(self.gen_train_timesteps) |
455 | | - for _ in range(self.n_disc_updates_per_round): |
456 | | - with networks.training(self.reward_train): |
457 | | - # switch to training mode (affects dropout, normalization) |
458 | | - self.train_disc() |
459 | 533 | if callback: |
460 | 534 | callback(r) |
461 | 535 | self.logger.dump(self._global_step) |
|
0 commit comments