From 45e99fa14f2cc0324eb93190053e97b285ced2f1 Mon Sep 17 00:00:00 2001 From: Ruo Yu Tao Date: Mon, 28 Oct 2024 15:13:18 -0400 Subject: [PATCH 1/2] add pellet probe scripts for P.O. PacMan --- batch_run_analytical.py | 25 +- lamb/models.py | 26 + lamb/utils/data.py | 44 ++ lamb/utils/file_system.py | 33 +- lamb/utils/replay/__init__.py | 2 + lamb/utils/replay/flat.py | 182 ++++++ lamb/utils/replay/trajectory.py | 618 ++++++++++++++++++ results/pocman_pellet_probe_trajectory.zip | Bin 0 -> 159609 bytes scripts/README.md | 36 + scripts/batch_run_ppo_epoch.py | 486 ++++++++++++++ scripts/collect_probe_trajectories.py | 150 +++++ scripts/collect_rnn_trajectories.py | 197 ++++++ scripts/combine_probe_datasets.py | 43 ++ scripts/hyperparams/analytical_30seeds.py | 2 +- scripts/hyperparams/analytical_8_30seeds.py | 2 +- scripts/hyperparams/parity_check_30seeds.py | 31 + scripts/hyperparams/parity_check_8_30seeds.py | 32 + .../hyperparams/pocman_LD_ppo_best_ckpt.py | 39 ++ scripts/hyperparams/pocman_ppo_best_ckpt.py | 39 ++ scripts/train_probe.py | 185 ++++++ scripts/visualization/viz_pocman_probes.py | 371 +++++++++++ 21 files changed, 2530 insertions(+), 13 deletions(-) create mode 100644 lamb/utils/replay/__init__.py create mode 100644 lamb/utils/replay/flat.py create mode 100644 lamb/utils/replay/trajectory.py create mode 100644 results/pocman_pellet_probe_trajectory.zip create mode 100644 scripts/README.md create mode 100644 scripts/batch_run_ppo_epoch.py create mode 100644 scripts/collect_probe_trajectories.py create mode 100644 scripts/collect_rnn_trajectories.py create mode 100644 scripts/combine_probe_datasets.py create mode 100644 scripts/hyperparams/parity_check_30seeds.py create mode 100644 scripts/hyperparams/parity_check_8_30seeds.py create mode 100644 scripts/hyperparams/pocman_LD_ppo_best_ckpt.py create mode 100644 scripts/hyperparams/pocman_ppo_best_ckpt.py create mode 100644 scripts/train_probe.py create mode 100644 scripts/visualization/viz_pocman_probes.py diff --git a/batch_run_analytical.py b/batch_run_analytical.py index 9b2dced..942de6d 100644 --- a/batch_run_analytical.py +++ b/batch_run_analytical.py @@ -64,7 +64,6 @@ def get_args(): parser.add_argument('--pi_steps', type=int, default=10000, help='For memory iteration, how many steps of policy improvement do we do per iteration?') - parser.add_argument('--policy_optim_alg', type=str, default='policy_grad', help='policy improvement algorithm to use. "policy_iter" - policy iteration, "policy_grad" - policy gradient, ' '"discrep_max" - discrepancy maximization, "discrep_min" - discrepancy minimization') @@ -75,7 +74,9 @@ def get_args(): parser.add_argument('--random_policies', default=100, type=int, help='How many random policies do we use for random kitchen sinks??') parser.add_argument('--leave_out_optimal', action='store_true', - help="Do we include the optimal policy when we select the initial policy") + help="Do we include the optimal policy when we select the initial policy?") + parser.add_argument('--mem_aug_before_init_pi', action='store_true', + help="Do we augment our memory before selecting the highest LD initial policy?") parser.add_argument('--n_mem_states', default=2, type=int, help='for memory_id = 0, how many memory states do we have?') @@ -121,6 +122,13 @@ def get_kitchen_sink_policy(policies: jnp.ndarray, pomdp: POMDP, measure: Callab all_policy_measures, _, _ = batch_measures(policies, pomdp) return policies[jnp.argmax(all_policy_measures)] +def get_mem_kitchen_sink_policy(policies: jnp.ndarray, + mem_params: jnp.ndarray, + pomdp: POMDP): + mem_policies = policies.repeat(mem_params.shape[-1], axis=1) + batch_measures = jax.vmap(mem_discrep_loss, in_axes=(None, 0, None)) + all_policy_measures = batch_measures(mem_params, mem_policies, pomdp) + return policies[jnp.argmax(all_policy_measures)] def make_experiment(args): @@ -193,17 +201,20 @@ def update_pg_step(inps, i): if args.leave_out_optimal: pis_with_memoryless_optimal = pi_paramses[:-1] + # We initialize mem params here + mem_shape = (1, pomdp.action_space.n, pomdp.observation_space.n, args.n_mem_states, args.n_mem_states) + mem_params = random.normal(mem_rng, shape=mem_shape) * 0.5 + # now we get our kitchen sink policies kitchen_sinks_info = {} - ld_pi_params = get_kitchen_sink_policy(pis_with_memoryless_optimal, pomdp, discrep_loss) + if args.mem_aug_before_init_pi: + ld_pi_params = get_kitchen_sink_policy(pis_with_memoryless_optimal, pomdp, discrep_loss) + else: + ld_pi_params = get_mem_kitchen_sink_policy(pis_with_memoryless_optimal, mem_params, pomdp) pis_to_learn_mem = jnp.stack([ld_pi_params]) kitchen_sinks_info['ld'] = ld_pi_params.copy() - # We initialize 3 mem params: 1 for LD - mem_shape = (pis_to_learn_mem.shape[0], pomdp.action_space.n, pomdp.observation_space.n, args.n_mem_states, args.n_mem_states) - mem_params = random.normal(mem_rng, shape=mem_shape) * 0.5 - mem_tx_params = jax.vmap(optim.init, in_axes=0)(mem_params) info['beginning']['init_mem_params'] = mem_params.copy() diff --git a/lamb/models.py b/lamb/models.py index b56b1f8..202509b 100644 --- a/lamb/models.py +++ b/lamb/models.py @@ -539,6 +539,32 @@ def __call__(self, hidden, x): return hidden, pi, jnp.squeeze(v, axis=-1) + +class PelletPredictorNN(nn.Module): + hidden_size: int + n_outs: int + n_hidden_layers: int = 1 + + @nn.compact + def __call__(self, x): + out = nn.Dense(self.hidden_size, kernel_init=orthogonal(2), bias_init=constant(0.0))( + x + ) + out = nn.relu(out) + + for i in range(self.n_hidden_layers): + out = nn.Dense( + self.hidden_size, kernel_init=orthogonal(0.01), bias_init=constant(0.0) + )(x) + out = nn.relu(out) + + logits = nn.Dense( + self.n_outs, kernel_init=orthogonal(0.01), bias_init=constant(0.0) + )(out) + predictions = nn.sigmoid(logits) + return predictions, logits + + def get_network_fn(env: environment.Environment, env_params: environment.EnvParams, memoryless: bool = False): if isinstance(env, Battleship) or (hasattr(env, '_unwrapped') and isinstance(env._unwrapped, Battleship)): diff --git a/lamb/utils/data.py b/lamb/utils/data.py index ec9bfb9..73b9c65 100644 --- a/lamb/utils/data.py +++ b/lamb/utils/data.py @@ -1,5 +1,49 @@ +import functools +from typing import Callable, Optional + +import jax +from jax import numpy as jnp import numpy as np +def add_dim_to_args( + func: Callable, + axis: int = 1, + starting_arg_index: Optional[int] = 1, + ending_arg_index: Optional[int] = None, + kwargs_on_device_keys: Optional[list] = None, +): + """Adds a dimension to the specified arguments of a function. + + Args: + func (Callable): The function to wrap. + axis (int, optional): The axis to add the dimension to. Defaults to 1. + starting_arg_index (Optional[int], optional): The index of the first argument to + add the dimension to. Defaults to 1. + ending_arg_index (Optional[int], optional): The index of the last argument to + add the dimension to. Defaults to None. + kwargs_on_device_keys (Optional[list], optional): The keys of the kwargs that should + be added to. Defaults to None. + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if ending_arg_index is None: + end_index = len(args) + else: + end_index = ending_arg_index + + args = list(args) + args[starting_arg_index:end_index] = [ + jax.tree.map(lambda x: jnp.expand_dims(x, axis=axis), a) + for a in args[starting_arg_index:end_index] + ] + for k, v in kwargs.items(): + if kwargs_on_device_keys is None or k in kwargs_on_device_keys: + kwargs[k] = jax.tree.map(lambda x: jnp.expand_dims(x, axis=1), v) + return func(*args, **kwargs) + + return wrapper + def one_hot(x, n): return np.eye(n)[x] diff --git a/lamb/utils/file_system.py b/lamb/utils/file_system.py index 051dc6b..4829923 100644 --- a/lamb/utils/file_system.py +++ b/lamb/utils/file_system.py @@ -92,16 +92,40 @@ def load_info(results_path: Path) -> dict: return np.load(results_path, allow_pickle=True).item() -def load_train_state(key: jax.random.PRNGKey, fpath: Path): +def load_train_state(key: jax.random.PRNGKey, fpath: Path, + update_idx_to_take: int = None, + best_over_rng: bool = False): # load our params orbax_checkpointer = orbax.checkpoint.PyTreeCheckpointer() restored = orbax_checkpointer.restore(fpath) args = restored['args'] unpacked_ts = restored['out']['runner_state'][0] - + if update_idx_to_take is None: + best_idx = 0 + if best_over_rng: + # we take the max here since we just want episode returns over all seeds + # and we take the mean over axis=-1 since we do n episodes of eval. + perf_across_seeds = restored['out']['final_eval_metric']['returned_discounted_episode_returns'].max(axis=-2).mean(axis=-1) + best_idx = np.squeeze(np.argmax(perf_across_seeds, axis=-1)) + + params = jax.tree_map(lambda x: x[0, 0, 0, 0, 0, 0, best_idx], unpacked_ts['params']) + else: + perf_across_seeds_expanded = restored['out']['metric']['returned_discounted_episode_returns'].squeeze().mean(axis=-1).mean(axis=-1) + all_ckpt_params = jax.tree.map(lambda x: x[0, 0, 0, 0, 0, 0], restored['out']['checkpoint']) + n_ckpt_steps = jax.tree.flatten(all_ckpt_params)[0][0].shape[1] + perf_interval = perf_across_seeds_expanded.shape[1] // n_ckpt_steps + perf_across_seeds = perf_across_seeds_expanded[:, ::perf_interval] + timestep_perf = perf_across_seeds[:, update_idx_to_take] + best_idx = np.argmax(timestep_perf) + params = jax.tree_map(lambda x: x[best_idx, update_idx_to_take], all_ckpt_params) + + + gamma = args['gamma'] + if 'config' in restored: + gamma = restored['config']['GAMMA'] env, env_params = get_gymnax_env(args['env'], key, - restored['config']['GAMMA'], + gamma=gamma, action_concat=args['action_concat']) network_fn, action_size = get_network_fn(env, env_params, memoryless=args['memoryless']) @@ -110,8 +134,9 @@ def load_train_state(key: jax.random.PRNGKey, fpath: Path): double_critic=args['double_critic'], hidden_size=args['hidden_size']) tx = optax.adam(args['lr'][0]) + ts = TrainState.create(apply_fn=network.apply, - params=jax.tree_map(lambda x: x[0, 0, 0, 0, 0, 0], unpacked_ts['params']), + params=params, tx=tx) return env, env_params, args, network, ts diff --git a/lamb/utils/replay/__init__.py b/lamb/utils/replay/__init__.py new file mode 100644 index 0000000..8e3da3f --- /dev/null +++ b/lamb/utils/replay/__init__.py @@ -0,0 +1,2 @@ +from .flat import make_flat_buffer, TransitionSample +from .trajectory import make_trajectory_buffer, TrajectoryBufferSample \ No newline at end of file diff --git a/lamb/utils/replay/flat.py b/lamb/utils/replay/flat.py new file mode 100644 index 0000000..ed78933 --- /dev/null +++ b/lamb/utils/replay/flat.py @@ -0,0 +1,182 @@ +""" +Taken from https://github.com/instadeepai/flashbax/blob/main/flashbax/buffers/flat_buffer.py +""" +import warnings +from typing import TYPE_CHECKING, Generic, Optional + +from chex import PRNGKey +from typing_extensions import NamedTuple + +if TYPE_CHECKING: # https://github.com/python/mypy/issues/6239 + from dataclasses import dataclass +else: + from chex import dataclass + +import jax + +from lamb.utils.data import add_dim_to_args + +from .trajectory import ( + Experience, + TrajectoryBuffer, + TrajectoryBufferState, + make_trajectory_buffer +) + +class ExperiencePair(NamedTuple, Generic[Experience]): + first: Experience + second: Experience + + +@dataclass(frozen=True) +class TransitionSample(Generic[Experience]): + experience: ExperiencePair[Experience] + + +def validate_sample_batch_size(sample_batch_size: int, max_length: int): + if sample_batch_size > max_length: + raise ValueError("sample_batch_size must be less than or equal to max_length") + + +def validate_min_length(min_length: int, add_batch_size: int, max_length: int): + used_min_length = min_length // add_batch_size + 1 + if used_min_length > max_length: + raise ValueError("min_length used is too large for the buffer size.") + + +def validate_max_length_add_batch_size(max_length: int, add_batch_size: int): + if max_length // add_batch_size < 2: + raise ValueError( + f"""max_length//add_batch_size must be greater than 2. It is currently + {max_length}//{add_batch_size} = {max_length//add_batch_size}""" + ) + + +def validate_flat_buffer_args( + max_length: int, + min_length: int, + sample_batch_size: int, + add_batch_size: int, +): + """Validates the arguments for the flat buffer.""" + + validate_sample_batch_size(sample_batch_size, max_length) + validate_min_length(min_length, add_batch_size, max_length) + validate_max_length_add_batch_size(max_length, add_batch_size) + + +def create_flat_buffer( + max_length: int, + min_length: int, + sample_batch_size: int, + add_sequences: bool, + add_batch_size: Optional[int], +) -> TrajectoryBuffer: + """Creates a trajectory buffer that acts as a flat buffer. + + Args: + max_length (int): The maximum length of the buffer. + min_length (int): The minimum length of the buffer. + sample_batch_size (int): The batch size of the samples. + add_sequences (Optional[bool], optional): Whether data is being added in sequences + to the buffer. If False, single transitions are being added each time add + is called. Defaults to False. + add_batch_size (Optional[int], optional): If adding data in batches, what is the + batch size that is being added each time. If None, single transitions or single + sequences are being added each time add is called. Defaults to None. + + Returns: + The buffer.""" + + if add_batch_size is None: + # add_batch_size being None implies that we are adding single transitions + add_batch_size = 1 + add_batches = False + else: + add_batches = True + + validate_flat_buffer_args( + max_length=max_length, + min_length=min_length, + sample_batch_size=sample_batch_size, + add_batch_size=add_batch_size, + ) + + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="Setting max_size dynamically sets the `max_length_time_axis` to " + f"be `max_size`//`add_batch_size = {max_length // add_batch_size}`." + "This allows one to control exactly how many transitions are stored in the buffer." + "Note that this overrides the `max_length_time_axis` argument.", + ) + + buffer = make_trajectory_buffer( + max_length_time_axis=None, # Unused because max_size is specified + min_length_time_axis=min_length // add_batch_size + 1, + add_batch_size=add_batch_size, + sample_batch_size=sample_batch_size, + sample_sequence_length=2, + period=1, + max_size=max_length, + ) + + add_fn = buffer.add + + if not add_batches: + add_fn = add_dim_to_args( + add_fn, axis=0, starting_arg_index=1, ending_arg_index=2 + ) + + if not add_sequences: + axis = 1 - int(not add_batches) # 1 if add_batches else 0 + add_fn = add_dim_to_args( + add_fn, axis=axis, starting_arg_index=1, ending_arg_index=2 + ) + + def sample_fn(state: TrajectoryBufferState, rng_key: PRNGKey) -> TransitionSample: + """Samples a batch of transitions from the buffer.""" + sampled_batch = buffer.sample(state, rng_key).experience + first = jax.tree.map(lambda x: x[:, 0], sampled_batch) + second = jax.tree.map(lambda x: x[:, 1], sampled_batch) + return TransitionSample(experience=ExperiencePair(first=first, second=second)) + + def all_fn(state: TrajectoryBufferState) -> TransitionSample: + """Returns all transitions.""" + first = jax.tree.map(lambda x: x[0, :-1], state.experience) + second = jax.tree.map(lambda x: x[0:, 1:], state.experience) + return TransitionSample(experience=ExperiencePair(first=first, second=second)) + + return buffer.replace(add=add_fn, sample=sample_fn, all=all_fn) # type: ignore + + +def make_flat_buffer( + max_length: int, + min_length: int, + sample_batch_size: int, + add_sequences: bool = False, + add_batch_size: Optional[int] = None, +) -> TrajectoryBuffer: + """Makes a trajectory buffer act as a flat buffer. + + Args: + max_length (int): The maximum length of the buffer. + min_length (int): The minimum length of the buffer. + sample_batch_size (int): The batch size of the samples. + add_sequences (Optional[bool], optional): Whether data is being added in sequences + to the buffer. If False, single transitions are being added each time add + is called. Defaults to False. + add_batch_size (Optional[int], optional): If adding data in batches, what is the + batch size that is being added each time. If None, single transitions or single + sequences are being added each time add is called. Defaults to None. + + Returns: + The buffer.""" + + return create_flat_buffer( + max_length=max_length, + min_length=min_length, + sample_batch_size=sample_batch_size, + add_sequences=add_sequences, + add_batch_size=add_batch_size, + ) diff --git a/lamb/utils/replay/trajectory.py b/lamb/utils/replay/trajectory.py new file mode 100644 index 0000000..8a9e170 --- /dev/null +++ b/lamb/utils/replay/trajectory.py @@ -0,0 +1,618 @@ +""" +Taken from https://github.com/instadeepai/flashbax/blob/main/flashbax/buffers/trajectory_buffer.py +""" + + +""""Pure functions defining the trajectory buffer. The trajectory buffer takes batches of n-step +experience data, where n is the number of time steps within a trajectory. The trajectory buffer +concatenates consecutive batches of experience data along the time axis, retaining their ordering. +This allows for random sampling of the trajectories within the buffer. +""" + +import functools +import warnings +from typing import TYPE_CHECKING, Callable, Generic, Optional, TypeVar + +if TYPE_CHECKING: # https://github.com/python/mypy/issues/6239 + from dataclasses import dataclass +else: + from chex import dataclass + +import chex +import jax +import jax.numpy as jnp +from jax import Array + +Experience = TypeVar("Experience", bound=chex.ArrayTree) + + +def get_tree_shape_prefix(tree: chex.ArrayTree, n_axes: int = 1) -> chex.Shape: + """Get the shape of the leading axes (up to n_axes) of a pytree. This assumes all + leaves have a common leading axes size (e.g. a common batch size).""" + flat_tree, tree_def = jax.tree_util.tree_flatten(tree) + leaf = flat_tree[0] + leading_axis_shape = leaf.shape[0:n_axes] + chex.assert_tree_shape_prefix(tree, leading_axis_shape) + return leading_axis_shape + + +@dataclass(frozen=True) +class TrajectoryBufferState(Generic[Experience]): + """State of the trajectory replay buffer. + + Attributes: + experience: Arbitrary pytree containing the experience data, for example a single + timestep (s,a,r). These are stacked along the first axis. + current_index: Index where the next batch of experience data will be added to. + is_full: Whether the buffer state is completely full with experience (otherwise it will + have some empty padded values). + """ + + experience: Experience + current_index: Array + is_full: Array + + +@dataclass(frozen=True) +class TrajectoryBufferSample(Generic[Experience]): + """Container for samples from the buffer + + Attributes: + experience: Arbitrary pytree containing a batch of experience data. + """ + + experience: Experience + + +def init( + experience: Experience, + add_batch_size: int, + max_length_time_axis: int, +) -> TrajectoryBufferState[Experience]: + """ + Initialise the buffer state. + + Args: + experience: A single timestep (e.g. (s,a,r)) used for inferring + the structure of the experience data that will be saved in the buffer state. + add_batch_size: Batch size of experience added to the buffer's state using the `add` + function. I.e. the leading batch size of added experience should have size + `add_batch_size`. + max_length_time_axis: Maximum length of the buffer along the time axis (second axis of the + experience data). + + Returns: + state: Initial state of the replay buffer. All values are empty as no experience has + been added yet. + """ + # Set experience value to be empty. + experience = jax.tree.map(jnp.empty_like, experience) + + # Broadcast to [add_batch_size, max_length_time_axis] + experience = jax.tree.map( + lambda x: jnp.broadcast_to( + x[None, None, ...], (add_batch_size, max_length_time_axis, *x.shape) + ), + experience, + ) + + state = TrajectoryBufferState( + experience=experience, + is_full=jnp.array(False, dtype=bool), + current_index=jnp.array(0), + ) + return state + + +def add( + state: TrajectoryBufferState[Experience], + batch: Experience, +) -> TrajectoryBufferState[Experience]: + """ + Add a batch of experience to the buffer state. Assumes that this carries on from the episode + where the previous added batch of experience ended. For example, if we consider a single + trajectory within the batch; if the last timestep of the previous added trajectory's was at + time `t` then the first timestep of the current trajectory will be at time `t + 1`. + + Args: + state: The buffer state. + batch: A batch of experience. The leading axis of the pytree is the batch dimension. + This must match `add_batch_size` and the structure of the experience used + during initialisation of the buffer state. This batch is added along the time axis of + the buffer state. + + + Returns: + A new buffer state with the batch of experience added. + """ + # Check that the batch has the correct shape. + chex.assert_tree_shape_prefix(batch, get_tree_shape_prefix(state.experience)) + # Check that the batch has the correct dtypes. + chex.assert_trees_all_equal_dtypes(batch, state.experience) + + # Get the length of the time axis of the buffer state. + max_length_time_axis = get_tree_shape_prefix(state.experience, n_axes=2)[1] + # Check that the sequence length is less than or equal the maximum length of the time axis. + chex.assert_axis_dimension_lteq( + jax.tree_util.tree_leaves(batch)[0], 1, max_length_time_axis + ) + + # Get the length of the sequence of the batch. + seq_len = get_tree_shape_prefix(batch, n_axes=2)[1] + + # Calculate index location in the state where we will assign the batch of experience. + indices = (jnp.arange(seq_len) + state.current_index) % max_length_time_axis + + # Update the buffer state. + experience = jax.tree.map( + lambda experience_field, batch_field: experience_field.at[:, indices].set( + batch_field + ), + state.experience, + batch, + ) + + new_index = state.current_index + seq_len + is_full = state.is_full | (new_index >= max_length_time_axis) + new_index = new_index % max_length_time_axis + + state = state.replace( # type: ignore + experience=experience, + current_index=new_index, + is_full=is_full, + ) + + return state + + +def get_invalid_indices( + state: TrajectoryBufferState[Experience], + sample_sequence_length: int, + period: int, + add_batch_size: int, + max_length_time_axis: int, +) -> Array: + """ + Get the indices of the items that will be invalid when sampling from the buffer state. This + is used to mask out the invalid items when sampling. The indices are in the format of a + flattened array and refer to items, not the actual data. To convert item indices into data + indices, we would perform the following: + + indices = item_indices * period + row_indices = indices // max_length_time_axis + time_indices = indices % max_length_time_axis + + Item indices essentially refer to a flattened array picture of the + items (i.e. subsequences that can be sampled) in the buffer state. + + + Args: + state: The buffer state. + sample_sequence_length: The length of the sequence that will be sampled from the buffer + state. + period: The period refers to the interval between sampled sequences. It serves to regulate + how much overlap there is between the trajectories that are sampled. To understand the + degree of overlap, you can calculate it as the difference between the + sample_sequence_length and the period. For instance, if you set period=1, it means that + trajectories will be sampled uniformly with the potential for any degree of overlap. On + the other hand, if period is equal to sample_sequence_length - 1, then trajectories can + be sampled in a way where only the first and last timesteps overlap with each other. + This helps you control the extent of overlap between consecutive sequences in your + sampling process. + add_batch_size: The number of trajectories that will be added to the buffer state. + max_length_time_axis: The maximum length of the time axis of the buffer state. + + Returns: + The indices of the items (with shape : [add_batch_size, num_items]) that will be invalid + when sampling from the buffer state. + """ + # We get the max subsequence data index as done in the add function. + max_divisible_length = max_length_time_axis - (max_length_time_axis % period) + max_subsequence_data_index = max_divisible_length - 1 + # We get the data index that is at least sample_sequence_length away from the + # current index. + previous_valid_data_index = ( + state.current_index - sample_sequence_length + ) % max_length_time_axis + # We ensure that this index is not above the maximum mappable data index of the buffer. + previous_valid_data_index = jnp.minimum( + previous_valid_data_index, max_subsequence_data_index + ) + # We then convert the data index into the item index and add one to get the index + # of the item that is broken apart. + invalid_item_starting_index = (previous_valid_data_index // period) + 1 + # We then take the modulo of the invalid item index to ensure that it is within the + # bounds of the priority array. max_length_time_axis // period is the maximum number + # of items/subsequences that can be sampled from the buffer state. + invalid_item_starting_index = invalid_item_starting_index % ( + max_length_time_axis // period + ) + + # Calculate the maximum number of items/subsequences that can start within a + # sample length of data. We add one to account for situations where the max + # number of items has been broken. Often, this will unfortunately mask an item + # that is valid however this should not be a severe issue as it would be only + # one additional item. + max_num_invalid_items = (sample_sequence_length // period) + 1 + # Get the actual indices of the items we cannot sample from. + invalid_item_indices = ( + jnp.arange(max_num_invalid_items) + invalid_item_starting_index + ) % (max_length_time_axis // period) + # Since items that are broken are broken in the same place in each row, we + # broadcast and add the total number of items to each index to reference + # the invalid items in each add_batch row. + invalid_item_indices = invalid_item_indices + jnp.arange(add_batch_size)[ + :, None + ] * (max_length_time_axis // period) + + return invalid_item_indices + + +def calculate_uniform_item_indices( + state: TrajectoryBufferState[Experience], + rng_key: chex.PRNGKey, + batch_size: int, + sample_sequence_length: int, + period: int, + add_batch_size: int, + max_length_time_axis: int, +) -> Array: + """Randomly sample a batch of item indices from the buffer state. This is done uniformly. + + Args: + state: The buffer's state. + rng_key: Random key. + batch_size: Batch size of sampled experience. + sample_sequence_length: Length of trajectory to sample. + period: The period refers to the interval between sampled sequences. It serves to regulate + how much overlap there is between the trajectories that are sampled. To understand the + degree of overlap, you can calculate it as the difference between the + sample_sequence_length and the period. For instance, if you set period=1, it means that + trajectories will be sampled uniformly with the potential for any degree of overlap. On + the other hand, if period is equal to sample_sequence_length - 1, then trajectories can + be sampled in a way where only the first and last timesteps overlap with each other. + This helps you control the extent of overlap between consecutive sequences in your + sampling process. + add_batch_size: The number of trajectories that will be added to the buffer state. + max_length_time_axis: The maximum length of the time axis of the buffer state. + + Returns: + The indices of the items that will be sampled from the buffer state. + + """ + # Get the max subsequence data index to ensure we dont sample items + # that should not ever be sampled i.e. a subsequence beyond the period + # boundary. + max_divisible_length = max_length_time_axis - (max_length_time_axis % period) + max_subsequence_data_index = max_divisible_length - 1 + # Get the maximum valid time index of the data buffer based on + # whether it is full or not. + max_data_time_index = jnp.where( + state.is_full, + max_subsequence_data_index, + state.current_index - sample_sequence_length, + ) + # Convert the max time index to the maximum non-valid item index. This is the item + # index that we can sample up to (excluding). We add 1 since the max time index is the last + # valid time index that we can sample from and we want the exclusive upper bound + # or in the case of a full buffer, the size of one row of the item array. + max_item_time_index = (max_data_time_index // period) + 1 + + # Get the indices of the items that will be invalid when sampling. + invalid_item_indices = get_invalid_indices( + state=state, + sample_sequence_length=sample_sequence_length, + period=period, + add_batch_size=add_batch_size, + max_length_time_axis=max_length_time_axis, + ) + # Since all the invalid indices are repeated albeit with a batch offset, + # we can just take the first row of the invalid indices for calculation. + invalid_item_indices = invalid_item_indices[0] + + # We then get the upper bound of the item indices that we can sample from. + # When being initially populated with data, the max time index will already account + # for the items that cannot be sampled meaning that invalid indices are not needed. + # Additionally, there is separate logic that needs to be performed when the buffer is not full. + # When the buffer is full, the max time index will not account for the items that cannot be + # sampled meaning that we need to subtract the number of invalid items from the + # max item index. + num_invalid_items = jnp.where(state.is_full, invalid_item_indices.shape[0], 0) + upper_bound = max_item_time_index - num_invalid_items + + # Since the invalid item indices are always consecutive (in a circular manner), + # we can get the offset by taking the last item index and adding one. + time_offset = invalid_item_indices[-1] + 1 + + # We then sample a batch of item indices over the time axis. + sampled_item_time_indices = jax.random.randint( + rng_key, (batch_size,), 0, upper_bound + ) + # We then add the offset and modulo the indices to ensure that they are within + # the bounds of the item array (which doesnt actually exist). We modulo by the + # max item index to ensure that we loop back to the start of the item array. + sampled_item_time_indices = ( + sampled_item_time_indices + time_offset + ) % max_item_time_index + + # We then get the batch indices by sampling a batch of indices over the batch axis. + sampled_item_batch_indices = jax.random.randint( + rng_key, (batch_size,), 0, add_batch_size + ) + + # We then calculate the item indices by multiplying the batch indices by the + # number of items in each batch and adding the time indices. This gives us + # a flattened array picture of the items we will sample from. + item_indices = ( + sampled_item_batch_indices * (max_length_time_axis // period) + ) + sampled_item_time_indices + + return item_indices + + +def sample( + state: TrajectoryBufferState[Experience], + rng_key: chex.PRNGKey, + batch_size: int, + sequence_length: int, + period: int, +) -> TrajectoryBufferSample[Experience]: + """ + Sample a batch of trajectories from the buffer. + + Args: + state: The buffer's state. + rng_key: Random key. + batch_size: Batch size of sampled experience. + sequence_length: Length of trajectory to sample. + period: The period refers to the interval between sampled sequences. It serves to regulate + how much overlap there is between the trajectories that are sampled. To understand the + degree of overlap, you can calculate it as the difference between the + sample_sequence_length and the period. For instance, if you set period=1, it means that + trajectories will be sampled uniformly with the potential for any degree of overlap. On + the other hand, if period is equal to sample_sequence_length - 1, then trajectories can + be sampled in a way where only the first and last timesteps overlap with each other. + This helps you control the extent of overlap between consecutive sequences in your + sampling process. + + Returns: + A batch of experience. + """ + add_batch_size, max_length_time_axis = get_tree_shape_prefix( + state.experience, n_axes=2 + ) + # Calculate the indices of the items that will be sampled. + item_indices = calculate_uniform_item_indices( + state, + rng_key, + batch_size, + sequence_length, + period, + add_batch_size, + max_length_time_axis, + ) + + # Convert the item indices to the indices of the data buffer + flat_data_indices = item_indices * period + # Get the batch index and time index of the sampled items. + batch_data_indices = flat_data_indices // max_length_time_axis + time_data_indices = flat_data_indices % max_length_time_axis + + # The buffer is circular, so we can loop back to the start (`% max_length_time_axis`) + # if the time index is greater than the length. We then add the sequence length to get + # the end index of the sequence. + time_data_indices = ( + jnp.arange(sequence_length) + time_data_indices[:, jnp.newaxis] + ) % max_length_time_axis + + # Slice the experience in the buffer to get a batch of trajectories of length sequence_length + batch_trajectory = jax.tree.map( + lambda x: x[batch_data_indices[:, jnp.newaxis], time_data_indices], + state.experience, + ) + + return TrajectoryBufferSample(experience=batch_trajectory) + + +def can_sample( + state: TrajectoryBufferState[Experience], min_length_time_axis: int +) -> Array: + """Indicates whether the buffer has been filled above the minimum length, such that it + may be sampled from.""" + return state is not None and (state.is_full | (state.current_index >= min_length_time_axis)) + + +def all_fn(state: TrajectoryBufferState[Experience]) -> TrajectoryBufferSample[Experience]: + sampled = state.experience + if not state.is_full: + sampled = jax.tree.map(lambda x: x[:, :state.current_index], state.experience) + return TrajectoryBufferSample(experience=sampled) + + +BufferState = TypeVar("BufferState", bound=TrajectoryBufferState) +BufferSample = TypeVar("BufferSample", bound=TrajectoryBufferSample) + + +@dataclass(frozen=True) +class TrajectoryBuffer(Generic[Experience, BufferState, BufferSample]): + """Pure functions defining the trajectory buffer. This buffer assumes batches added to the + buffer are a pytree with a shape prefix of (batch_size, trajectory_length). Consecutive batches + are then concatenated along the second axis (i.e. the time axis). During sampling this allows + for trajectories to be sampled - by slicing consecutive sequences along the time axis. + + Attributes: + init: A pure function which may be used to initialise the buffer state using a single + timestep (e.g. (s,a,r)). + add: A pure function for adding a new batch of experience to the buffer state. + sample: A pure function for sampling a batch of data from the replay buffer, with a leading + axis of size (`sample_batch_size`, `sample_sequence_length`). Note `sample_batch_size` + and `sample_sequence_length` may be different to the batch size and sequence length of + data added to the state using the `add` function. + can_sample: Whether the buffer can be sampled from, which is determined by if the + number of trajectories added to the buffer state is greater than or equal to the + `min_length`. + + See `make_trajectory_buffer` for how this container is instantiated. + """ + + init: Callable[[Experience], BufferState] + add: Callable[ + [BufferState, Experience], + BufferState, + ] + sample: Callable[ + [BufferState, chex.PRNGKey], + BufferSample, + ] + can_sample: Callable[[BufferState], Array] + all: Callable[[BufferState], BufferSample] + + +def validate_size( + max_length_time_axis: Optional[int], max_size: Optional[int], add_batch_size: int +) -> None: + if max_size is not None and max_length_time_axis is not None: + raise ValueError( + "Cannot specify both `max_size` and `max_length_time_axis` arguments." + ) + if max_size is not None: + warnings.warn( + "Setting max_size dynamically sets the `max_length_time_axis` to " + f"be `max_size`//`add_batch_size = {max_size // add_batch_size}`." + "This allows one to control exactly how many timesteps are stored in the buffer." + "Note that this overrides the `max_length_time_axis` argument.", + stacklevel=1, + ) + + +def validate_trajectory_buffer_args( + max_length_time_axis: Optional[int], + min_length_time_axis: int, + add_batch_size: int, + sample_sequence_length: int, + period: int, + max_size: Optional[int], +) -> None: + """Validate the arguments of the trajectory buffer.""" + + validate_size(max_length_time_axis, max_size, add_batch_size) + + if max_size is not None: + max_length_time_axis = max_size // add_batch_size + + if sample_sequence_length > min_length_time_axis: + warnings.warn( + "`sample_sequence_length` greater than `min_length_time_axis`, therefore " + "overriding `min_length_time_axis`" + "to be set to `sample_sequence_length`, as we need at least `sample_sequence_length` " + "timesteps added to the buffer before we can sample.", + stacklevel=1, + ) + min_length_time_axis = sample_sequence_length + + if period > sample_sequence_length: + warnings.warn( + "Setting period greater than sample_sequence_length will result in no overlap between" + f"trajectories, however, {period-sample_sequence_length} transitions will " + "never be sampled. Setting period to be equal to sample_sequence_length will " + "also result in no overlap between trajectories, however, all transitions will " + "be sampled. Setting period to be `sample_sequence_length - 1` is generally " + "desired to ensure that only starting and ending transitions are shared " + "between trajectories allowing for utilising last transitions for bootstrapping.", + stacklevel=1, + ) + + if max_length_time_axis is not None: + if sample_sequence_length > max_length_time_axis: + raise ValueError( + "`sample_sequence_length` must be less than or equal to `max_length_time_axis`." + ) + + if min_length_time_axis > max_length_time_axis: + raise ValueError( + "`min_length_time_axis` must be less than or equal to `max_length_time_axis`." + ) + + +def make_trajectory_buffer( + add_batch_size: int, + sample_batch_size: int, + sample_sequence_length: int, + period: int, + min_length_time_axis: int, + max_size: Optional[int] = None, + max_length_time_axis: Optional[int] = None, +) -> TrajectoryBuffer: + """Makes a trajectory buffer. + + Args: + add_batch_size: Batch size of experience added to the buffer. Used to initialise the leading + axis of the buffer state's experience. + sample_batch_size: Batch size of experience returned from the `sample` method of the + buffer. + sample_sequence_length: Trajectory length of experience of sampled batches. Note that this + may differ from the trajectory length of experience added to the buffer. + period: The period refers to the interval between sampled sequences. It serves to regulate + how much overlap there is between the trajectories that are sampled. To understand the + degree of overlap, you can calculate it as the difference between the + sample_sequence_length and the period. For instance, if you set period=1, it means that + trajectories will be sampled uniformly with the potential for any degree of overlap. On + the other hand, if period is equal to sample_sequence_length - 1, then trajectories can + be sampled in a way where only the first and last timesteps overlap with each other. + This helps you control the extent of overlap between consecutive sequences in your + sampling process. + min_length_time_axis: Minimum length of the buffer (along the time axis) before sampling is + allowed. + max_size: Optional argument to specify the size of the buffer based on timesteps. + This sets the maximum number of timesteps that can be stored in the buffer and sets + the `max_length_time_axis` to be `max_size`//`add_batch_size`. This allows one to + control exactly how many timesteps are stored in the buffer. Note that this + overrides the `max_length_time_axis` argument. + max_length_time_axis: Optional Argument to specify the maximum length of the buffer in terms + of time steps within the 'time axis'. The second axis (the time axis) of the buffer + state's experience field will be of size `max_length_time_axis`. + + + Returns: + A trajectory buffer. + """ + validate_trajectory_buffer_args( + max_length_time_axis=max_length_time_axis, + min_length_time_axis=min_length_time_axis, + add_batch_size=add_batch_size, + sample_sequence_length=sample_sequence_length, + period=period, + max_size=max_size, + ) + + if sample_sequence_length > min_length_time_axis: + min_length_time_axis = sample_sequence_length + + if max_size is not None: + max_length_time_axis = max_size // add_batch_size + + init_fn = functools.partial( + init, + add_batch_size=add_batch_size, + max_length_time_axis=max_length_time_axis, + ) + add_fn = functools.partial( + add, + ) + sample_fn = functools.partial( + sample, + batch_size=sample_batch_size, + sequence_length=sample_sequence_length, + period=period, + ) + can_sample_fn = functools.partial( + can_sample, min_length_time_axis=min_length_time_axis + ) + + return TrajectoryBuffer( + init=init_fn, + add=add_fn, + sample=sample_fn, + can_sample=can_sample_fn, + all=all_fn, + ) diff --git a/results/pocman_pellet_probe_trajectory.zip b/results/pocman_pellet_probe_trajectory.zip new file mode 100644 index 0000000000000000000000000000000000000000..9513a4b1521541aa4817f4dc48b6969e067b429a GIT binary patch literal 159609 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�-Y9lgyc0Urx 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 literal 0 HcmV?d00001 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..864cf60 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,36 @@ +# Scripts + +This `scripts` directory includes scripts for plotting all experimental +results in our work, as well as scripts for a few additional experiments +in the paper. + +## P.O. PacMan Memory Probe +To train our memory probe, we need to first collect checkpoints from +a P.O. PacMan run. We can do so with the `pocman_*ppo_best_ckpt.py` scripts +in `scripts/hyperparams`. This script will train P.O. PacMan agents with the +best swept hyperparams. + +After training, we need to run the `scripts/collect_rnn_trajectories.py` script +to collect 1M samples from each behavior policy (LD and vanilla PPO). This script +will collect RNN hidden states from two RNNs (`--rnn_path_0` and `--rnn_path_1`), +while following the `--behavior_path` RNN as the behavior policy. We collect +1M time steps with each variant as the behavior policy, for a combined dataset of +2M samples. We use the `scripts/combine_probe_datasets.py` script to combine these +datasets, resulting in a `results/combined_probe_datasets` data buffer. + +Now we train our probe with the `scripts/train_probe.py` script. Pass in the +PATH to the combined dataset above as the argument to `--dataset_path`. Use the +`--features_idx` argument to select which RNN hidden states to use for training (0 or 1). +The index and ordering of these hidden states will depend on which RNN paths were +used in `--rnn_path_0` and `--rnn_path_1`. + +Once our probe has been trained, we can collect trajectories with each trained probe +with `scripts/collect_probe_trajectories.py`. We can visualize these collected +probe trajectories with `scripts/visualization/viz_pocman_probe.py`. +We provide the collected probe trajectories in `results/pocman_pellet_probe_trajectory.zip`. +To generate this visualization, simply uncompress this file and pass each file in as +the argument to `scripts/visualization/viz_pocman_probe.py`. + + + + diff --git a/scripts/batch_run_ppo_epoch.py b/scripts/batch_run_ppo_epoch.py new file mode 100644 index 0000000..164c078 --- /dev/null +++ b/scripts/batch_run_ppo_epoch.py @@ -0,0 +1,486 @@ +from collections import deque +from dataclasses import replace +from functools import partial +import inspect +from typing import Literal + +from flax.training.train_state import TrainState +from flax.training import orbax_utils +import jax +import jax.numpy as jnp +import numpy as np +import optax +import orbax.checkpoint +from tap import Tap + +from lamb.agents.ppo import Transition, env_step +from lamb.envs import get_gymnax_env +from lamb.envs.jax_wrappers import LogEnvState +from lamb.models import get_network_fn, ScannedRNN +from lamb.utils.file_system import get_results_path + + +class BatchPPOHyperparams(Tap): + env: str = 'tmaze_5' + num_envs: int = 4 + default_max_steps_in_episode: int = 1000 + gamma: float = 0.99 # will be replaced if env has gamma property. + + num_steps: int = 128 + num_epochs: int = 50 + update_epochs: int = 4 + num_minibatches: int = 4 + + memoryless: bool = False + double_critic: bool = False + action_concat: bool = False + + lr: list[float] = [2.5e-4] + lambda0: list[float] = [0.95] # GAE lambda_0 + lambda1: list[float] = [0.5] # GAE lambda_1 + alpha: list[float] = [1.] # adv = alpha * adv_lambda_0 + (1 - alpha) * adv_lambda_1 + ld_weight: list[float] = [0.0] # how much to we weight the LD loss vs. value loss? only applies when optimize LD is True. + vf_coeff: list[float] = [0.5] + + hidden_size: int = 128 + total_steps: int = int(1.5e6) + entropy_coeff: float = 0.01 + clip_eps: float = 0.2 + max_grad_norm: float = 0.5 + anneal_lr: bool = True + + num_eval_envs: int = 10 + steps_log_freq: int = 1 + update_log_freq: int = 1 + save_checkpoints: bool = False # Do we save train_state along with our per timestep outputs? + save_runner_state: bool = False # Do we save the checkpoint in the end? + seed: int = 2020 + n_seeds: int = 5 + platform: Literal['cpu', 'gpu'] = 'cpu' + debug: bool = False + + study_name: str = 'batch_ppo_test' + + def process_args(self) -> None: + self.vf_coeff = jnp.array(self.vf_coeff) + self.lr = jnp.array(self.lr) + self.lambda0 = jnp.array(self.lambda0) + self.lambda1 = jnp.array(self.lambda1) + self.alpha = jnp.array(self.alpha) + self.ld_weight = jnp.array(self.ld_weight) + +def filter_period_first_dim(x, n: int): + if isinstance(x, jnp.ndarray) or isinstance(x, np.ndarray): + return x[::n] + + +def make_train(args: BatchPPOHyperparams, rand_key: jax.random.PRNGKey): + num_updates = ( + args.total_steps // args.num_steps // args.num_envs + ) + args.minibatch_size = ( + args.num_envs * args.num_steps // args.num_minibatches + ) + env_key, rand_key = jax.random.split(rand_key) + env, env_params = get_gymnax_env(args.env, env_key, + gamma=args.gamma, + action_concat=args.action_concat) + + if hasattr(env, 'gamma'): + args.gamma = env.gamma + + assert hasattr(env_params, 'max_steps_in_episode') + + double_critic = args.double_critic + memoryless = args.memoryless + + network_fn, action_size = get_network_fn(env, env_params, memoryless=memoryless) + + network = network_fn(action_size, + double_critic=double_critic, + hidden_size=args.hidden_size) + + steps_filter = partial(filter_period_first_dim, n=args.steps_log_freq) + update_filter = partial(filter_period_first_dim, n=args.update_log_freq) + + # Used for vmapping over our double critic. + transition_axes_map = Transition( + None, None, 2, None, None, None, None + ) + + _env_step = partial(env_step, network=network, env=env, env_params=env_params) + + def train(vf_coeff, ld_weight, alpha, lambda1, lambda0, lr, rng): + def linear_schedule(count): + frac = ( + 1.0 + - (count // (args.num_minibatches * args.update_epochs)) + / num_updates + ) + return lr * frac + + + # INIT NETWORK + rng, _rng = jax.random.split(rng) + init_x = ( + jnp.zeros( + (1, args.num_envs, *env.observation_space(env_params).shape) + ), + jnp.zeros((1, args.num_envs)), + ) + init_hstate = ScannedRNN.initialize_carry(args.num_envs, args.hidden_size) + network_params = network.init(_rng, init_hstate, init_x) + if args.anneal_lr: + tx = optax.chain( + optax.clip_by_global_norm(args.max_grad_norm), + optax.adam(learning_rate=linear_schedule, eps=1e-5), + ) + else: + tx = optax.chain( + optax.clip_by_global_norm(args.max_grad_norm), + optax.adam(lr, eps=1e-5), + ) + train_state = TrainState.create( + apply_fn=network.apply, + params=network_params, + tx=tx, + ) + + # INIT ENV + rng, _rng = jax.random.split(rng) + reset_rng = jax.random.split(_rng, args.num_envs) + obsv, env_state = env.reset(reset_rng, env_params) + init_hstate = ScannedRNN.initialize_carry(args.num_envs, args.hidden_size) + + # We first need to populate our LogEnvState stats. + rng, _rng = jax.random.split(rng) + init_rng = jax.random.split(_rng, args.num_envs) + init_obsv, init_env_state = env.reset(init_rng, env_params) + init_init_hstate = ScannedRNN.initialize_carry(args.num_envs, args.hidden_size) + + init_runner_state = ( + train_state, + env_state, + init_obsv, + jnp.zeros(args.num_envs, dtype=bool), + init_init_hstate, + _rng, + ) + + starting_runner_state, _ = jax.lax.scan( + _env_step, init_runner_state, None, env_params.max_steps_in_episode + ) + + def recursive_replace(env_state, new_env_state, names): + if not isinstance(env_state, LogEnvState): + return replace(env_state, env_state=recursive_replace(env_state.env_state, new_env_state.env_state, names)) + new_log_vals = {name: getattr(new_env_state, name) for name in names} + return replace(env_state, **new_log_vals) + + replace_field_names = ['returned_episode_returns', 'returned_discounted_episode_returns', 'returned_episode_lengths'] + env_state = recursive_replace(env_state, starting_runner_state[1], replace_field_names) + + # TRAIN LOOP + def _update_step(runner_state, i): + # COLLECT TRAJECTORIES + initial_hstate = runner_state[-2] + runner_state, traj_batch = jax.lax.scan( + _env_step, runner_state, jnp.arange(args.num_steps), args.num_steps + ) + + # CALCULATE ADVANTAGE + train_state, env_state, last_obs, last_done, hstate, rng = runner_state + ac_in = (last_obs[np.newaxis, :], last_done[np.newaxis, :]) + _, _, last_val = network.apply(train_state.params, hstate, ac_in) + last_val = last_val.squeeze(0) + def _calculate_gae(traj_batch, last_val, last_done, gae_lambda): + def _get_advantages(carry, transition): + gae, next_value, next_done, gae_lambda = carry + done, value, reward = transition.done, transition.value, transition.reward + delta = reward + args.gamma * next_value * (1 - next_done) - value + gae = delta + args.gamma * gae_lambda * (1 - next_done) * gae + return (gae, value, done, gae_lambda), gae + _, advantages = jax.lax.scan(_get_advantages, + (jnp.zeros_like(last_val), last_val, last_done, gae_lambda), + traj_batch, reverse=True, unroll=16) + return advantages, advantages + traj_batch.value + + gae_lambda = jnp.array(lambda0) + if double_critic: + # last_val is index 1 here b/c we squeezed earlier. + _calculate_gae = jax.vmap(_calculate_gae, + in_axes=[transition_axes_map, 1, None, 0], + out_axes=2) + gae_lambda = jnp.array([lambda0, lambda1]) + advantages, targets = _calculate_gae(traj_batch, last_val, last_done, gae_lambda) + + # UPDATE NETWORK + def _update_epoch(update_state, unused): + def _update_minbatch(train_state, batch_info): + init_hstate, traj_batch, advantages, targets = batch_info + + def _loss_fn(params, init_hstate, traj_batch, gae, targets): + # RERUN NETWORK + _, pi, value = network.apply( + params, init_hstate[0], (traj_batch.obs, traj_batch.done) + ) + log_prob = pi.log_prob(traj_batch.action) + + # CALCULATE VALUE LOSS + value_pred_clipped = traj_batch.value + ( + value - traj_batch.value + ).clip(-args.clip_eps, args.clip_eps) + value_losses = jnp.square(value - targets) + value_losses_clipped = jnp.square(value_pred_clipped - targets) + value_loss = ( + jnp.maximum(value_losses, value_losses_clipped).mean() + ) + # Lambda discrepancy loss + if double_critic: + value_loss = ld_weight * (jnp.square(value[..., 0] - value[..., 1])).mean() + \ + (1 - ld_weight) * value_loss + + # CALCULATE ACTOR LOSS + ratio = jnp.exp(log_prob - traj_batch.log_prob) + + # which advantage do we use to update our policy? + if double_critic: + gae = (alpha * gae[..., 0] + + (1 - alpha) * gae[..., 1]) + gae = (gae - gae.mean()) / (gae.std() + 1e-8) + loss_actor1 = ratio * gae + loss_actor2 = ( + jnp.clip( + ratio, + 1.0 - args.clip_eps, + 1.0 + args.clip_eps, + ) + * gae + ) + loss_actor = -jnp.minimum(loss_actor1, loss_actor2) + loss_actor = loss_actor.mean() + entropy = pi.entropy().mean() + + total_loss = ( + loss_actor + + vf_coeff * value_loss + - args.entropy_coeff * entropy + ) + return total_loss, (value_loss, loss_actor, entropy) + + grad_fn = jax.value_and_grad(_loss_fn, has_aux=True) + total_loss, grads = grad_fn( + train_state.params, init_hstate, traj_batch, advantages, targets + ) + train_state = train_state.apply_gradients(grads=grads) + return train_state, total_loss + + ( + train_state, + init_hstate, + traj_batch, + advantages, + targets, + rng, + ) = update_state + + rng, _rng = jax.random.split(rng) + permutation = jax.random.permutation(_rng, args.num_envs) + batch = (init_hstate, traj_batch, advantages, targets) + + shuffled_batch = jax.tree.map( + lambda x: jnp.take(x, permutation, axis=1), batch + ) + + minibatches = jax.tree.map( + lambda x: jnp.swapaxes( + jnp.reshape( + x, + [x.shape[0], args.num_minibatches, -1] + + list(x.shape[2:]), + ), + 1, + 0, + ), + shuffled_batch, + ) + + train_state, total_loss = jax.lax.scan( + _update_minbatch, train_state, minibatches + ) + update_state = ( + train_state, + init_hstate, + traj_batch, + advantages, + targets, + rng, + ) + return update_state, total_loss + + init_hstate = initial_hstate[None, :] # TBH + update_state = ( + train_state, + init_hstate, + traj_batch, + advantages, + targets, + rng, + ) + update_state, loss_info = jax.lax.scan( + _update_epoch, update_state, None, args.update_epochs + ) + train_state = update_state[0] + + # save metrics only every steps_log_freq + metric = traj_batch.info + metric = jax.tree.map(steps_filter, metric) + + rng = update_state[-1] + if args.debug: + + def callback(info): + timesteps = ( + info["timestep"][info["returned_episode"]] * args.num_envs + ) + avg_return_values = jnp.mean(info["returned_episode_returns"][info["returned_episode"]]) + if len(timesteps) > 0: + print( + f"timesteps={timesteps[0]} - {timesteps[-1]}, avg episodic return={avg_return_values:.2f}" + ) + + jax.debug.callback(callback, metric) + + runner_state = (train_state, env_state, last_obs, last_done, hstate, rng) + + return runner_state, metric + + def _epoch(runner_state, _): + runner_state, metric = jax.lax.scan( + _update_step, runner_state, jnp.arange(round(num_updates / args.num_epochs)), round(num_updates / args.num_epochs) + ) + # save metrics only every update_log_freq + metric = jax.tree.map(update_filter, metric) + + res = {'metric': metric} + if args.save_checkpoints: + res['checkpoint'] = runner_state[0].params + + return runner_state, res + + rng, _rng = jax.random.split(rng) + runner_state = ( + train_state, + env_state, + obsv, + jnp.zeros((args.num_envs), dtype=bool), + init_hstate, + _rng, + ) + + runner_state, metric = jax.lax.scan( + _epoch, runner_state, jnp.arange(args.num_epochs), args.num_epochs + ) + # combine epochs with time steps + metric['metric'] = jax.tree.map(lambda x: x.reshape(-1, *x.shape[2:]), metric['metric']) + + # returned metric has an extra dimension. + # runner_state, metric = jax.lax.scan( + # _update_step, runner_state, jnp.arange(num_updates), num_updates + # ) + # + # # save metrics only every update_log_freq + # metric = jax.tree.map(update_filter, metric) + + # TODO: offline eval here. + final_train_state = runner_state[0] + + reset_rng = jax.random.split(_rng, args.num_eval_envs) + eval_obsv, eval_env_state = env.reset(reset_rng, env_params) + + eval_init_hstate = ScannedRNN.initialize_carry(args.num_eval_envs, args.hidden_size) + + eval_runner_state = ( + final_train_state, + eval_env_state, + eval_obsv, + jnp.zeros((args.num_eval_envs), dtype=bool), + eval_init_hstate, + _rng, + ) + + # COLLECT EVAL TRAJECTORIES + eval_runner_state, eval_traj_batch = jax.lax.scan( + _env_step, eval_runner_state, None, env_params.max_steps_in_episode + ) + + res = {"runner_state": runner_state, "metric": metric['metric'], 'final_eval_metric': eval_traj_batch.info} + + if args.save_checkpoints: + res['checkpoint'] = metric['checkpoint'] + return res + + return train + + +if __name__ == "__main__": + # jax.disable_jit(True) + # okay some weirdness here. NUM_ENVS needs to match with NUM_MINIBATCHES + args = BatchPPOHyperparams().parse_args() + jax.config.update('jax_platform_name', args.platform) + + rng = jax.random.PRNGKey(args.seed) + make_train_rng, rng = jax.random.split(rng) + rngs = jax.random.split(rng, args.n_seeds) + train_fn = make_train(args, make_train_rng) + + train_args = list(inspect.signature(train_fn).parameters.keys()) + + vmaps_train = train_fn + swept_args = deque() + + # we need to go backwards, since JAX returns indices + # in the order in which they're vmapped. + for i, arg in reversed(list(enumerate(train_args))): + dims = [None] * len(train_args) + dims[i] = 0 + vmaps_train = jax.vmap(vmaps_train, in_axes=dims) + if arg == 'rng': + swept_args.appendleft(rngs) + else: + assert hasattr(args, arg) + swept_args.appendleft(getattr(args, arg)) + + train_jit = jax.jit(vmaps_train) + out = train_jit(*swept_args) + + # our final_eval_metric returns max_num_steps. + # we can filter that down by the max episode length amongst the runs. + final_eval = out['final_eval_metric'] + + # the +1 at the end is to include the done step + largest_episode = final_eval['returned_episode'].argmax(axis=-2).max() + 1 + + def get_first_n_filter(x): + return x[..., :largest_episode, :] + out['final_eval_metric'] = jax.tree.map(get_first_n_filter, final_eval) + + if not args.save_runner_state: + del out['runner_state'] + + results_path = get_results_path(args, return_npy=False) # returns a results directory + + all_results = { + 'argument_order': train_args, + 'out': out, + 'args': args.as_dict() + } + + # Save all results with Orbax + orbax_checkpointer = orbax.checkpoint.PyTreeCheckpointer() + save_args = orbax_utils.save_args_from_target(all_results) + + print(f"Saving results to {results_path}") + orbax_checkpointer.save(results_path, all_results, save_args=save_args) + + print("Done.") diff --git a/scripts/collect_probe_trajectories.py b/scripts/collect_probe_trajectories.py new file mode 100644 index 0000000..8a1eccd --- /dev/null +++ b/scripts/collect_probe_trajectories.py @@ -0,0 +1,150 @@ +from functools import partial +from pathlib import Path +from typing import Union, Literal + +from chex import dataclass +from jumanji.environments.routing.pac_man import State +from jumanji.environments.routing.pac_man.types import Position +import jax +import jax.numpy as jnp +import numpy as np +import orbax.checkpoint +from tap import Tap + +from porl.agents.ppo import env_step +from porl.envs.pocman import PocMan +from porl.models.actor_critic import ScannedRNN, PelletPredictorNN +from porl.utils.file_system import load_train_state, numpyify_and_save + +from definitions import ROOT_DIR + + +class PocmanProbeCollectHyperparams(Tap): + probe_path_0: Union[str, Path] + probe_path_1: Union[str, Path] + rnn_path_0: Union[str, Path] + rnn_path_1: Union[str, Path] + + behavior_policy_idx: Literal[0, 1] = 1 + seed: int = 2024 + + def configure(self) -> None: + self.add_argument('--probe_path_0', type=Path) + self.add_argument('--probe_path_1', type=Path) + self.add_argument('--rnn_path_0', type=Path) + self.add_argument('--rnn_path_1', type=Path) + + +def state_to_dict(state: State): + state_dict = {} + for k, v in state.items(): + if isinstance(v, Position): + state_dict[k] = {'x': v.x, 'y': v.y} + else: + state_dict[k] = v + return state_dict + + +def unpack_and_flatten_state(state) -> dict: + while (not isinstance(state, State)): + state = state.env_state + + flattened_state = jax.tree.map(lambda x: x[0], state) + return state_to_dict(flattened_state) + + +def load_probe(fpath: Path): + orbax_checkpointer = orbax.checkpoint.PyTreeCheckpointer() + restored = orbax_checkpointer.restore(fpath) + args = restored['args'] + unpacked_ts = restored['final_train_state'] + + # TODO: refactor this + n_pellet_predictions = unpacked_ts['params']['params']['Dense_3']['bias'].shape[0] + + network = PelletPredictorNN(hidden_size=args['hidden_size'], + n_outs=n_pellet_predictions, + n_hidden_layers=args['n_hidden_layers']) + return network, unpacked_ts + +def predictions_to_map(predictions: jnp.ndarray, env: PocMan): + predictions = predictions.squeeze() + env_generator = env._unwrapped.generator + + # we first subtract by 1, so that all walls are -1, and + # empty spaces are 0. + preds_map = env_generator.numpy_maze - 1 + + preds_map = preds_map.at[env_generator.pellet_spaces[:, 1], env_generator.pellet_spaces[:, 0]].set(predictions) + return preds_map + + +if __name__ == "__main__": + # jax.disable_jit(True) + args = PocmanProbeCollectHyperparams().parse_args() + + key = jax.random.PRNGKey(args.seed) + load_key_0, load_key_1, key = jax.random.split(key, 3) + + probe_network_0, probe_ts_0 = load_probe(args.probe_path_0) + probe_network_1, probe_ts_1 = load_probe(args.probe_path_1) + + # TODO: This is kind of sketch. We've refactored this now, so change this when done retraining. + env, env_params, rnn_args0, rnn_network0, rnn_ts0 = load_train_state(load_key_0, args.rnn_path_0, + update_idx_to_take=2, + best_over_rng=True) + _, _, rnn_args1, rnn_network1, rnn_ts1 = load_train_state(load_key_1, args.rnn_path_1, + update_idx_to_take=2, + best_over_rng=True) + + predictions_to_map = jax.jit(partial(predictions_to_map, env=env)) + + networks = [rnn_network0, rnn_network1] + tses = [rnn_ts0, rnn_ts1] + ts = tses[args.behavior_policy_idx] + _env_step = jax.jit(partial(env_step, network=networks[args.behavior_policy_idx], env=env, env_params=env_params)) + + @jax.jit + def predict_probes(obs, done, hs0, hs1): + ac_in = (obs[jnp.newaxis, :], done[jnp.newaxis, :]) + hs0, _, _ = rnn_network0.apply(rnn_ts0.params, hs0, ac_in) + hs1, _, _ = rnn_network1.apply(rnn_ts1.params, hs1, ac_in) + + predictions0, _ = probe_network_0.apply(probe_ts_0['params'], hs0) + predictions1, _ = probe_network_1.apply(probe_ts_1['params'], hs1) + return (predictions0, predictions1), (hs0, hs1) + + key, reset_key = jax.random.split(key) + reset_key = reset_key[None, ...] + obsv, state = env.reset(reset_key, env_params) + states = [unpack_and_flatten_state(state)] + predictions, pred_maps = [], [] + + assert rnn_args1['hidden_size'] == rnn_args0['hidden_size'] + hstate = ScannedRNN.initialize_carry(1, rnn_args1['hidden_size']) + hs0 = ScannedRNN.initialize_carry(1, rnn_args0['hidden_size']) + hs1 = ScannedRNN.initialize_carry(1, rnn_args1['hidden_size']) + + done = jnp.array([False]) + rs = (ts, state, obsv, done, hstate, key) + while not jnp.any(done): + preds, (hs0, hs1) = predict_probes(obsv, done, hs0, hs1) + predictions.append(preds) + pred_maps.append((predictions_to_map(preds[0]), predictions_to_map(preds[1]))) + rs, transition = _env_step(rs, None) + ts, state, obsv, done, hstate, key = rs + states.append(unpack_and_flatten_state(state)) + + preds, (hs0, hs1) = predict_probes(obsv, done, hs0, hs1) + pred_maps.append((predictions_to_map(preds[0]), predictions_to_map(preds[1]))) + + res = { + 'states': states, + 'predictions': pred_maps + } + + res_path = Path(ROOT_DIR, 'results', f'pocman_pellet_probe_trajectory_bidx_{args.behavior_policy_idx}.npy') + + print(f"Saving Pocman probe trajectory to {res_path}") + numpyify_and_save(res_path, res) + print("Done.") diff --git a/scripts/collect_rnn_trajectories.py b/scripts/collect_rnn_trajectories.py new file mode 100644 index 0000000..681b8e1 --- /dev/null +++ b/scripts/collect_rnn_trajectories.py @@ -0,0 +1,197 @@ +from functools import partial +from pathlib import Path +from typing import Union, NamedTuple + +import chex +import jax +import jax.numpy as jnp +from jax_tqdm import scan_tqdm +import numpy as np +from tap import Tap +from flax.training import orbax_utils +import orbax.checkpoint + +from lamb.envs.pocman import State +from lamb.models import ScannedRNN +from lamb.utils.file_system import load_train_state, make_hash_md5 + + +class CollectHyperparams(Tap): + rnn_path_0: Union[str, Path] + rnn_path_1: Union[str, Path] + behavior_path: Union[str, Path] + + update_idx_to_take: int = None + + num_envs: int = 4 + n_samples: int = int(1e6) + + seed: int = 2024 + platform: str = 'cpu' + + def configure(self) -> None: + self.add_argument('--rnn_path_0', type=Path) + self.add_argument('--rnn_path_1', type=Path) + self.add_argument('--behavior_path', type=Path) + + +def ppo_pocman_step(runner_state, unused, + behavior_network, rnn_network_0, rnn_network_1, + env, env_params): + def get_pocman_state(s) -> State: + if isinstance(s, State): + return s + if hasattr(s, 'env_state'): + return get_pocman_state(s.env_state) + else: + raise TypeError('No Pocman env_state found.') + + (behavior_ts, rnn_ts_0, rnn_ts_1, env_state, last_obs, last_done, + behavior_hstate, rnn_hstate_0, rnn_hstate_1, rng) = runner_state + rng, _rng = jax.random.split(rng) + + # SELECT ACTION + ac_in = (last_obs[np.newaxis, :], last_done[np.newaxis, :]) + next_behavior_hstate, pi, value = behavior_network.apply(behavior_ts.params, behavior_hstate, ac_in) + action = pi.sample(seed=_rng) + log_prob = pi.log_prob(action) + value, action, log_prob = ( + value.squeeze(0), + action.squeeze(0), + log_prob.squeeze(0), + ) + + # get our RNN hidden states that we're sampling + next_rnn_hstate_0, _, _ = rnn_network_0.apply(rnn_ts_0.params, rnn_hstate_0, ac_in) + next_rnn_hstate_1, _, _ = rnn_network_1.apply(rnn_ts_1.params, rnn_hstate_1, ac_in) + + # STEP ENV + rng, _rng = jax.random.split(rng) + rng_step = jax.random.split(_rng, next_behavior_hstate.shape[0]) + obsv, next_env_state, reward, done, info = env.step(rng_step, env_state, action, env_params) + + # transition = Transition( + # last_done, action, value, reward, log_prob, last_obs, info + # ) + pocman_state = get_pocman_state(env_state) + possible_locations = jnp.array(env._unwrapped.generator.reachable_spaces) + + def get_single_occupancy(loc: jnp.ndarray): + return jnp.all(possible_locations == loc[None, ...], axis=-1) + + # We vmap twice, once for the batch dimension in VecEnv, + # the second time for the 4 ghosts + ghost_occupancy = jax.vmap(jax.vmap(get_single_occupancy))(pocman_state.ghost_locations).sum(axis=-2) + datum = { + 'x_0': rnn_hstate_0, + 'x_1': rnn_hstate_1, + 'pellet_occupancy': jnp.all(pocman_state.pellet_locations != 0, axis=-1), + 'ghost_occupancy': jnp.clip(ghost_occupancy, a_max=1), + # 'state': pocman_state + } + runner_state = (behavior_ts, rnn_ts_0, rnn_ts_1, next_env_state, obsv, done, + next_behavior_hstate, next_rnn_hstate_0, next_rnn_hstate_1, rng) + return runner_state, datum + + +def make_collect(args: CollectHyperparams, key: chex.PRNGKey): + steps_to_collect = args.n_samples // args.num_envs + + behavior_key, rnn_key_0, rnn_key_1, key = jax.random.split(key, 4) + + env, env_params, behavior_args, behavior_network, behavior_ts = load_train_state(behavior_key, args.behavior_path, + update_idx_to_take=args.update_idx_to_take, + best_over_rng=True) + _, _, rnn_args_0, rnn_network_0, rnn_ts_0 = load_train_state(rnn_key_0, args.rnn_path_0, + update_idx_to_take=args.update_idx_to_take, + best_over_rng=True) + _, _, rnn_args_1, rnn_network_1, rnn_ts_1 = load_train_state(rnn_key_1, args.rnn_path_1, + update_idx_to_take=args.update_idx_to_take, + best_over_rng=True) + + _env_step = partial(ppo_pocman_step, behavior_network=behavior_network, + rnn_network_0=rnn_network_0, rnn_network_1=rnn_network_1, + env=env, env_params=env_params) + _env_step = scan_tqdm(steps_to_collect)(_env_step) + + ckpts = { + 'behavior': {'args': behavior_args, 'ts': behavior_ts, 'path': args.behavior_path}, + 'rnn_0': {'args': rnn_args_0, 'ts': rnn_ts_0, 'path': args.rnn_path_0}, + 'rnn_1': {'args': rnn_args_1, 'ts': rnn_ts_1, 'path': args.rnn_path_1} + } + + def collect(rng): + # INIT ENV + rng, _rng = jax.random.split(rng) + reset_rng = jax.random.split(_rng, args.num_envs) + obsv, env_state = env.reset(reset_rng, env_params) + + # init hidden state + init_behavior_hstate = ScannedRNN.initialize_carry(args.num_envs, behavior_args['hidden_size']) + init_rnn_hstate_0 = ScannedRNN.initialize_carry(args.num_envs, rnn_args_0['hidden_size']) + init_rnn_hstate_1 = ScannedRNN.initialize_carry(args.num_envs, rnn_args_1['hidden_size']) + init_runner_state = ( + behavior_ts, + rnn_ts_0, + rnn_ts_1, + env_state, + obsv, + jnp.zeros(args.num_envs, dtype=bool), + init_behavior_hstate, + init_rnn_hstate_0, + init_rnn_hstate_1, + _rng, + ) + + runner_state, dataset = jax.lax.scan( + _env_step, init_runner_state, jnp.arange(steps_to_collect), steps_to_collect + ) + + # Now we flatten back down + flat_dataset = jax.tree.map(lambda x: x.reshape(-1, *x.shape[2:]), dataset) + + return flat_dataset + + return collect, ckpts + + +if __name__ == "__main__": + # jax.disable_jit(True) + args = CollectHyperparams().parse_args() + jax.config.update('jax_platform_name', args.platform) + + key = jax.random.PRNGKey(args.seed) + make_key, collect_key, key = jax.random.split(key, 3) + + collect_fn, ckpt_info = make_collect(args, make_key) + collect_fn = jax.jit(collect_fn) + + dataset = collect_fn(collect_key) + + def path_to_str(d: dict): + for k, v in d.items(): + if isinstance(v, Path): + d[k] = str(v) + elif isinstance(v, dict): + path_to_str(v) + + + to_save = { + 'dataset': dataset, + 'args': args.as_dict(), + 'ckpt': ckpt_info, + } + path_to_str(to_save) + + save_path = args.behavior_path.parent / \ + f'buffer_{args.n_samples}_timestep_{args.update_idx_to_take}_seed_{args.seed}_{make_hash_md5(args.as_dict())}' + + # Save all results with Orbax + orbax_checkpointer = orbax.checkpoint.PyTreeCheckpointer() + save_args = orbax_utils.save_args_from_target(to_save) + + print(f"Saving results to {save_path}") + orbax_checkpointer.save(save_path, to_save, save_args=save_args) + + print("Done.") + diff --git a/scripts/combine_probe_datasets.py b/scripts/combine_probe_datasets.py new file mode 100644 index 0000000..42a1185 --- /dev/null +++ b/scripts/combine_probe_datasets.py @@ -0,0 +1,43 @@ +from pathlib import Path + +import jax +import jax.numpy as jnp +import numpy as np +import orbax.checkpoint + +from lamb.utils.file_system import numpyify_and_save +from definitions import ROOT_DIR + + +if __name__ == "__main__": + + jax.config.update('jax_platform_name', 'cpu') + d0_path = Path("../results/pocman_ppo_best_ckpt/buffer_1000000_timestep_2_seed_2024_390f282614e5b7398cf10e565ea811e7") + d1_path = Path("../results/pocman_LD_ppo_best_ckpt/buffer_1000000_timestep_2_seed_2024_e232a0c2fa52cb6e68f01be9dac6b7b9") + + orbax_checkpointer = orbax.checkpoint.PyTreeCheckpointer() + restored = orbax_checkpointer.restore(d0_path) + + args_0, ckpt_0, dataset_0 = restored['args'], restored['ckpt'], restored['dataset'] + dataset_0['ghost_occupancy'] = dataset_0['ghost_occupancy'].astype(np.int8) + + restored = orbax_checkpointer.restore(d1_path) + + args_1, ckpt_1, dataset_1 = restored['args'], restored['ckpt'], restored['dataset'] + dataset_1['ghost_occupancy'] = dataset_1['ghost_occupancy'].astype(np.int8) + + combined_dataset = jax.tree.map(lambda x, y: jnp.concatenate((x, y), axis=0), dataset_0, dataset_1) + + save_dir = Path(ROOT_DIR, 'results', 'combined_probe_datasets') + save_dir.mkdir(exist_ok=True) + save_path = save_dir / f'combined_{d0_path.stem.split("_")[-1]}.npy' + to_save = { + 'args': [args_0, args_1], + 'ckpt': [ckpt_0, ckpt_1], + 'dataset': combined_dataset + } + + print(f"Saving results to {save_path}") + numpyify_and_save(save_path, to_save) + + print("Done.") diff --git a/scripts/hyperparams/analytical_30seeds.py b/scripts/hyperparams/analytical_30seeds.py index 7b8cf1e..70661a4 100644 --- a/scripts/hyperparams/analytical_30seeds.py +++ b/scripts/hyperparams/analytical_30seeds.py @@ -5,7 +5,7 @@ hparams = { 'file_name': f'runs_{exp_name}.txt', - 'entry': '-m batch_run_kitchen_sinks_ld_only', + 'entry': '-m batch_run_analytical', 'args': [{ 'spec': [ 'tiger-alt-start', 'tmaze_5_two_thirds_up', '4x3.95', diff --git a/scripts/hyperparams/analytical_8_30seeds.py b/scripts/hyperparams/analytical_8_30seeds.py index 912db9b..20a99be 100644 --- a/scripts/hyperparams/analytical_8_30seeds.py +++ b/scripts/hyperparams/analytical_8_30seeds.py @@ -5,7 +5,7 @@ hparams = { 'file_name': f'runs_{exp_name}.txt', - 'entry': '-m batch_run_kitchen_sinks_ld_only', + 'entry': '-m batch_run_analytical', 'args': [{ 'spec': [ 'tiger-alt-start', 'tmaze_5_two_thirds_up', '4x3.95', diff --git a/scripts/hyperparams/parity_check_30seeds.py b/scripts/hyperparams/parity_check_30seeds.py new file mode 100644 index 0000000..08177b3 --- /dev/null +++ b/scripts/hyperparams/parity_check_30seeds.py @@ -0,0 +1,31 @@ +from pathlib import Path + +exp_name = Path(__file__).stem + +hparams = { + 'file_name': + f'runs_{exp_name}.txt', + 'entry': '-m batch_run_analytical', + 'args': [{ + 'spec': [ + 'parity_check' + ], + 'policy_optim_alg': 'policy_grad', + 'leave_out_optimal': True, + 'mem_aug_before_init_pi': True, + 'value_type': 'q', + 'error_type': 'l2', + 'alpha': 1., + 'mi_steps': 10000, + 'pi_steps': 10000, + 'optimizer': 'adam', + 'lr': 0.75, + 'n_mem_states': [2, 4], + 'mi_iterations': 1, + 'random_policies': 100, + 'n_seeds': 30, + 'platform': 'gpu' + }, + + ] +} diff --git a/scripts/hyperparams/parity_check_8_30seeds.py b/scripts/hyperparams/parity_check_8_30seeds.py new file mode 100644 index 0000000..b6b377f --- /dev/null +++ b/scripts/hyperparams/parity_check_8_30seeds.py @@ -0,0 +1,32 @@ +from pathlib import Path + +exp_name = Path(__file__).stem + +hparams = { + 'file_name': + f'runs_{exp_name}.txt', + 'entry': '-m batch_run_analytical', + 'args': [{ + 'spec': [ + 'parity_check' + ], + 'policy_optim_alg': 'policy_grad', + 'leave_out_optimal': True, + 'mem_aug_before_init_pi': True, + 'value_type': 'q', + 'error_type': 'l2', + 'alpha': 1., + 'mi_steps': 10000, + 'pi_steps': 10000, + 'optimizer': 'adam', + 'lr': 0.75, + 'n_mem_states': 8, + 'mi_iterations': 1, + 'random_policies': 100, + 'seed': [2024 + s for s in range(6)], + 'n_seeds': 5, + 'platform': 'gpu' + }, + + ] +} diff --git a/scripts/hyperparams/pocman_LD_ppo_best_ckpt.py b/scripts/hyperparams/pocman_LD_ppo_best_ckpt.py new file mode 100644 index 0000000..75b4d55 --- /dev/null +++ b/scripts/hyperparams/pocman_LD_ppo_best_ckpt.py @@ -0,0 +1,39 @@ +from pathlib import Path + +exp_name = Path(__file__).stem + +lrs = [2.5e-4] +lambda0s = [0.1] +lambda1s = [0.5] +alphas = [1] +ld_weights = [0.25] + +hparams = { + 'file_name': + f'runs_{exp_name}.txt', + 'entry': '-m scripts.batch_run_ppo_epoch', + 'args': [ + { + 'env': 'pocman', + 'double_critic': True, + 'action_concat': True, + 'lr': lrs, + 'lambda0': ' '.join(map(str, lambda0s)), + 'lambda1': lambda1s, + 'alpha': ' '.join(map(str, alphas)), + 'ld_weight': ld_weights, + 'hidden_size': 512, + 'entropy_coeff': 0.05, + 'num_epochs': 25, + 'steps_log_freq': 4, + 'update_log_freq': 200, + 'total_steps': int(1e7), + 'save_checkpoints': True, + 'save_runner_state': True, + 'seed': 2036, + 'n_seeds': 5, + 'platform': 'gpu', + 'study_name': exp_name + } + ] +} diff --git a/scripts/hyperparams/pocman_ppo_best_ckpt.py b/scripts/hyperparams/pocman_ppo_best_ckpt.py new file mode 100644 index 0000000..1ba98c4 --- /dev/null +++ b/scripts/hyperparams/pocman_ppo_best_ckpt.py @@ -0,0 +1,39 @@ +from pathlib import Path + +exp_name = Path(__file__).stem + +lrs = [2.5e-5] +lambda0s = [0.5] +lambda1s = [0.95] +alphas = [1] +ld_weights = [0] + +hparams = { + 'file_name': + f'runs_{exp_name}.txt', + 'entry': '-m scripts.batch_run_ppo_epoch', + 'args': [ + { + 'env': 'pocman', + 'double_critic': False, + 'action_concat': True, + 'lr': lrs, + 'lambda0': ' '.join(map(str, lambda0s)), + 'lambda1': ' '.join(map(str, lambda1s)), + 'alpha': ' '.join(map(str, alphas)), + 'ld_weight': ' '.join(map(str, ld_weights)), + 'hidden_size': 512, + 'entropy_coeff': 0.05, + 'num_epochs': 25, + 'steps_log_freq': 4, + 'update_log_freq': 200, + 'total_steps': int(1e7), + 'save_checkpoints': True, + 'save_runner_state': True, + 'seed': 2036, + 'n_seeds': 5, + 'platform': 'gpu', + 'study_name': exp_name + } + ] +} diff --git a/scripts/train_probe.py b/scripts/train_probe.py new file mode 100644 index 0000000..6d27590 --- /dev/null +++ b/scripts/train_probe.py @@ -0,0 +1,185 @@ +from collections import namedtuple +from pathlib import Path +from typing import Union + +from flax.training import orbax_utils +from flax.training.train_state import TrainState +import jax +import jax.numpy as jnp +import numpy as np +from tap import Tap +import optax +import orbax.checkpoint + +from lamb.models import PelletPredictorNN +from lamb.utils.replay import make_flat_buffer +from lamb.utils.replay.trajectory import TrajectoryBufferState +from lamb.utils.file_system import make_hash_md5, load_info + +from definitions import ROOT_DIR + + +class PocmanProbeHyperparams(Tap): + dataset_path: Union[str, Path] + features_idx: int = 0 + prediction_target_key: str = 'pellet_occupancy' # Key in dataset that we set as target + + hidden_size: int = 512 + n_hidden_layers: int = 2 + lr: float = 1e-4 + + epochs: int = 100 + train_steps: int = int(1e6) + batch_size: int = 32 + + study_name: str = 'test' + debug: bool = False + seed: int = 2024 + platform: str = 'cpu' + + def configure(self) -> None: + self.add_argument('--dataset_path', type=Path) + + +def filter_period_first_dim(x, n: int): + if isinstance(x, jnp.ndarray) or isinstance(x, np.ndarray): + return x[::n] + + +def make_train(args: PocmanProbeHyperparams): + args.steps_per_epoch = args.train_steps // args.epochs + + if args.dataset_path.suffix == '.npy': + restored = load_info(args.dataset_path) + else: + orbax_checkpointer = orbax.checkpoint.PyTreeCheckpointer() + restored = orbax_checkpointer.restore(args.dataset_path) + + dataset_args, dataset = restored['args'], restored['dataset'] + + experience = jax.tree.map(lambda x: jnp.array(x), dataset) + if 'x' not in experience: + experience['x'] = experience[f'x_{args.features_idx}'] + + for key in list(experience.keys()): + if key.startswith('x_'): + del experience[key] + + Experience = namedtuple('Experience', list(experience.keys())) + experience = Experience(**experience) + + n_pellet_predictions = getattr(experience, args.prediction_target_key).shape[-1] + + network = PelletPredictorNN(hidden_size=args.hidden_size, + n_outs=n_pellet_predictions, + n_hidden_layers=args.n_hidden_layers) + + + experience_size = experience.x.shape[0] + buffer = make_flat_buffer( + max_length=experience_size, + min_length=args.batch_size, + sample_batch_size=args.batch_size, + # add_batch_size=experience_size + ) + buffer = buffer.replace( + init=jax.jit(buffer.init), + add=jax.jit(buffer.add, donate_argnums=0), + sample=jax.jit(buffer.sample), + can_sample=jax.jit(buffer.can_sample), + ) + + buffer_state = TrajectoryBufferState( + current_index=jnp.array(0, dtype=int), + is_full=jnp.array(True), + experience=jax.tree_util.tree_map(lambda x: x[None, ...], experience) + ) + + def train(rng): + params_rng, rng = jax.random.split(rng) + params = network.init(params_rng, experience.x[:1]) + tx = optax.adam(args.lr, eps=1e-5) + + train_state = TrainState.create( + apply_fn=network.apply, + params=params, + tx=tx, + ) + + def _epoch_step(runner_state, i): + # @scan_tqdm(args.train_steps, print_rate=100) + @jax.jit + def _update_step(runner_state, i): + ts, rng = runner_state + + sample_key, rng = jax.random.split(rng) + batch = buffer.sample(buffer_state, sample_key) + + target = getattr(batch.experience.first, args.prediction_target_key).astype(float) + + def _loss_fn(params: dict): + _, logits = network.apply(params, batch.experience.first.x) + loss = optax.losses.sigmoid_binary_cross_entropy(logits, target).sum(axis=-1) + return loss.mean() + + grad_fn = jax.jit(jax.value_and_grad(_loss_fn)) + loss, grads = grad_fn(ts.params) + new_ts = ts.apply_gradients(grads=grads) + return (new_ts, rng), loss + + runner_state, losses = jax.lax.scan( + _update_step, runner_state, jnp.arange(args.steps_per_epoch), args.steps_per_epoch + ) + if args.debug: + jax.debug.print("Step {step} average loss: {loss}", step=(i * args.steps_per_epoch), loss=losses.mean()) + + return runner_state, losses.mean() + + runner_state = (train_state, rng) + runner_state, epoch_losses = jax.lax.scan( + _epoch_step, runner_state, jnp.arange(args.epochs), args.epochs + ) + return { + 'final_train_state': runner_state[0], 'epoch_losses': epoch_losses, + 'ckpt': restored['ckpt'] + } + + return train + + +if __name__ == '__main__': + jax.disable_jit(True) + args = PocmanProbeHyperparams().parse_args() + jax.config.update('jax_platform_name', args.platform) + + key = jax.random.PRNGKey(args.seed) + train_key, key = jax.random.split(key) + + train_fn = make_train(args) + # train_fn = jax.jit(train_fn) + + out = train_fn(train_key) + + out['args'] = args.as_dict() + + def path_to_str(d: dict): + for k, v in d.items(): + if isinstance(v, Path): + d[k] = str(v) + elif isinstance(v, dict): + path_to_str(v) + path_to_str(out) + + # Save all results with Orbax + orbax_checkpointer = orbax.checkpoint.PyTreeCheckpointer() + save_args = orbax_utils.save_args_from_target(out) + + results_dir = Path(ROOT_DIR, 'results', args.study_name) + results_dir.mkdir(exist_ok=True) + results_path = results_dir / f"{args.prediction_target_key}_seed_{args.seed}_features_idx_{args.features_idx}_{make_hash_md5(out['args'])}" + + print(f"Saving results to {results_path}") + orbax_checkpointer.save(results_path, out, save_args=save_args) + + print("Done.") + diff --git a/scripts/visualization/viz_pocman_probes.py b/scripts/visualization/viz_pocman_probes.py new file mode 100644 index 0000000..e863c29 --- /dev/null +++ b/scripts/visualization/viz_pocman_probes.py @@ -0,0 +1,371 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import NamedTuple, Tuple + +import numpy as np +import matplotlib.animation +import matplotlib.colors as mcolors +import matplotlib.pyplot as plt + +class Position(NamedTuple): + x: np.int32 + y: np.int32 + + def __eq__(self, other: object) -> np.ndarray: + if not isinstance(other, Position): + return NotImplemented + return (self.x == other.x) & (self.y == other.y) + + +@dataclass +class State: + """The state of the environment. + + key: random key used for auto-reset. + grid: jax array (int) of the ingame maze with walls. + pellets: int tracking the number of pellets. + frightened_state_time: jax array (int) of shape () + tracks number of steps for the scatter state. + pellet_locations: jax array (int) of pellet locations. + power_up_locations: jax array (int) of power-up locations + player_locations: current 2D position of agent. + ghost_locations: jax array (int) of current ghost positions. + initial_player_locations: starting 2D position of agent. + initial_ghost_positions: jax array (int) of initial ghost positions. + ghost_init_targets: jax array (int) of initial ghost targets. + used to direct ghosts on respawn. + old_ghost_locations: jax array (int) of shape ghost positions from last step. + used to prevent ghost backtracking. + ghost_init_steps: jax array (int) of number of initial ghost steps. + used to determine per ghost initialisation. + ghost_actions: jax array (int) of ghost action at current step. + last_direction: (int) tracking the last direction of the player. + dead: (bool) used to track player death. + visited_index: jax array (int) of visited locations. + used to prevent repeated pellet points. + ghost_starts: jax array (int) of reset positions for ghosts + used to reset ghost positions if eaten + scatter_targets: jax array (int) of scatter targets. + target locations for ghosts when scatter behavior is active. + step_count: (int32) of total steps taken from reset till current timestep. + ghost_eaten: jax array (bool) tracking if ghost has been eaten before. + score: (int32) of total points aquired. + """ + + key: np.ndarray # (2,) + grid: np.ndarray # (31,28) + pellets: np.int32 # () + frightened_state_time: np.int32 # () + pellet_locations: np.ndarray # (316,2) + power_up_locations: np.ndarray # (4,2) + player_locations: Position # Position(row, col) each of shape () + ghost_locations: np.ndarray # (4,2) + initial_player_locations: Position # Position(row, col) each of shape () + initial_ghost_positions: np.ndarray # (4,2) + ghost_init_targets: np.ndarray # (4,2) + old_ghost_locations: np.ndarray # (4,2) + ghost_init_steps: np.ndarray # (4,) + ghost_actions: np.ndarray # (4,) + last_direction: np.int32 # () + dead: bool # () + visited_index: np.ndarray # (320,2) + ghost_starts: np.ndarray # (4,2) + scatter_targets: np.ndarray # (4,2) + step_count: np.int32 # () + ghost_eaten: np.ndarray # (4,) + score: np.int32 # () + +def create_grid_image(observation: State) -> np.ndarray: + """ + Generate the observation of the current state. + + Args: + state: 'State` object corresponding to the new state of the environment. + + Returns: + rgb: A 3-dimensional array representing the RGB observation of the current state. + """ + + # Make walls blue and passages black + layer_1 = (1 - observation.grid) * 0.0 + layer_2 = (1 - observation.grid) * 0.0 + layer_3 = (1 - observation.grid) * 0.6 + + player_loc = observation.player_locations + ghost_pos = observation.ghost_locations + pellets_loc = observation.power_up_locations + is_scared = observation.frightened_state_time + idx = observation.pellet_locations + n = 3 + + # Power pellet are pink + for i in range(len(pellets_loc)): + p = pellets_loc[i] + layer_1[p[1], p[0]] = 1.0 + layer_2[p[1], p[0]] = 0.8 + layer_3[p[1], p[0]] = 0.6 + + # Set player is yellow + layer_1[player_loc.x, player_loc.y] = 1 + layer_2[player_loc.x, player_loc.y] = 1 + layer_3[player_loc.x, player_loc.y] = 0 + + cr = np.array([1, 1, 0, 1]) + cg = np.array([0, 0.7, 1, 0.5]) + cb = np.array([0, 1, 1, 0.0]) + # Set ghost locations + + layers = (layer_1, layer_2, layer_3) + + def set_ghost_colours( + layers: np.ndarray, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + layer_1, layer_2, layer_3 = layers + for i in range(4): + y = ghost_pos[i][0] + x = ghost_pos[i][1] + + layer_1[x, y] = cr[i] + layer_2[x, y] = cg[i] + layer_3[x, y] = cb[i] + return layer_1, layer_2, layer_3 + + def set_ghost_colours_scared( + layers: np.ndarray, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + layer_1, layer_2, layer_3 = layers + for i in range(4): + y = ghost_pos[i][0] + x = ghost_pos[i][1] + layer_1[x, y] = 0 + layer_2[x, y] = 0 + layer_3[x, y] = 1 + return layer_1, layer_2, layer_3 + + if is_scared > 0: + layers = set_ghost_colours_scared(layers) + else: + layers = set_ghost_colours(layers) + + layer_1, layer_2, layer_3 = layers + + layer_1[0, 0] = 0 + layer_2[0, 0] = 0 + layer_3[0, 0] = 0.6 + + obs = [layer_1, layer_2, layer_3] + rgb = np.stack(obs, axis=-1) + + expand_rgb = np.kron(rgb, np.ones((n, n, 1))) + layer_1 = expand_rgb[:, :, 0] + layer_2 = expand_rgb[:, :, 1] + layer_3 = expand_rgb[:, :, 2] + + # place normal pellets + for i in range(len(idx)): + if np.array(idx[i]).sum != 0: + loc = idx[i] + c = loc[1] * n + 1 + r = loc[0] * n + 1 + layer_1[c, r] = 1.0 + layer_2[c, r] = 0.8 + layer_3[c, r] = 0.6 + + layers = (layer_1, layer_2, layer_3) + + # Draw details + def set_ghost_colours_details( + layers: np.ndarray, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + layer_1, layer_2, layer_3 = layers + for i in range(4): + y = ghost_pos[i][0] + x = ghost_pos[i][1] + c = x * n + 1 + r = y * n + 1 + + layer_1[c, r] = cr[i] + layer_2[c, r] = cg[i] + layer_3[c, r] = cb[i] + + # Make notch in top + layer_1[c - 1, r - 1] = 0.0 + layer_2[c - 1, r - 1] = 0.0 + layer_3[c - 1, r - 1] = 0.0 + + # Make notch in top + layer_1[c - 1, r + 1] = 0.0 + layer_2[c - 1, r + 1] = 0.0 + layer_3[c - 1, r + 1] = 0.0 + + # Eyes + layer_1[c, r + 1] = 1 + layer_2[c, r + 1] = 1 + layer_3[c, r + 1] = 1 + + layer_1[c, r - 1] = 1 + layer_2[c, r - 1] = 1 + layer_3[c, r - 1] = 1 + + return layer_1, layer_2, layer_3 + + def set_ghost_colours_scared_details( + layers: np.ndarray, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + layer_1, layer_2, layer_3 = layers + for i in range(4): + y = ghost_pos[i][0] + x = ghost_pos[i][1] + + c = x * n + 1 + r = y * n + 1 + + layer_1[x * n + 1, y * n + 1] = 0 + layer_2[x * n + 1, y * n + 1] = 0 + layer_3[x * n + 1, y * n + 1] = 1 + + # Make notch in top + layer_1[c - 1, r - 1] = 0.0 + layer_2[c - 1, r - 1] = 0.0 + layer_3[c - 1, r - 1] = 0.0 + + # Make notch in top + layer_1[c - 1, r + 1] = 0.0 + layer_2[c - 1, r + 1] = 0.0 + layer_3[c - 1, r + 1] = 0.0 + + # Eyes + layer_1[c, r + 1] = 1 + layer_2[c, r + 1] = 0.6 + layer_3[c, r + 1] = 0.2 + + layer_1[c, r - 1] = 1 + layer_2[c, r - 1] = 0.6 + layer_3[c, r - 1] = 0.2 + + return layer_1, layer_2, layer_3 + + if is_scared > 0: + layers = set_ghost_colours_scared_details(layers) + else: + layers = set_ghost_colours_details(layers) + + layer_1, layer_2, layer_3 = layers + + # Power pellet is pink + for i in range(len(pellets_loc)): + p = pellets_loc[i] + layer_1[p[1] * n + 2, p[0] * n + 1] = 1 + layer_2[p[1] * n + 1, p[0] * n + 1] = 0.8 + layer_3[p[1] * n + 1, p[0] * n + 1] = 0.6 + + # Set player is yellow + layer_1[player_loc.x * n + 1, player_loc.y * n + 1] = 1 + layer_2[player_loc.x * n + 1, player_loc.y * n + 1] = 1 + layer_3[player_loc.x * n + 1, player_loc.y * n + 1] = 0 + + obs = [layer_1, layer_2, layer_3] + rgb = np.stack(obs, axis=-1) + expand_rgb + + return rgb + + +def visualize_grid(grid, fig, ax, add_colorbar=False): + # Check if the grid has the correct dimensions + if grid.shape != (21, 19): + raise ValueError("Grid must be 21x19 in size") + + # Create a custom colormap for values between 0 and 1 + cmap = mcolors.LinearSegmentedColormap.from_list('custom_cmap', ['white', '#f39c12']) # Using a brighter blue + + # Create a masked array for the grid + masked_grid = np.ma.masked_where(grid == -1, grid) + + # Plot the grid + if fig is None: + fig, ax = plt.subplots() + cax = ax.imshow(masked_grid, cmap=cmap, vmin=0, vmax=1) + + # Add color bar + if add_colorbar: + cbar = fig.colorbar(cax, ax=ax, fraction=0.046, pad=0.04) + cbar.set_label('Value') + + # Plot the -1 values as black + for (i, j), value in np.ndenumerate(grid): + if value == -1: + ax.add_patch(plt.Rectangle((j - 0.5, i - 0.5), 1, 1, color='black')) + + # Set grid lines + ax.set_xticks(np.arange(-.5, 19, 1), minor=True) + ax.set_yticks(np.arange(-.5, 21, 1), minor=True) + ax.grid(which='minor', color='gray', linestyle='-', linewidth=0.5) + ax.tick_params(which='minor', size=0) + + if fig is None: + # Display the grid + plt.show() + + +def load_info(results_path: Path) -> dict: + return np.load(results_path, allow_pickle=True).item() + + +def pack_states(states: list[dict]) -> list[State]: + packed_states = [] + for s in states: + dict_s = {} + for k, v in list(s.items()): + if isinstance(v, dict): + assert 'x' in v and 'y' in v + dict_s[k] = Position(x=v['x'], y=v['y']) + else: + assert isinstance(v, np.ndarray) or isinstance(v, list) + dict_s[k] = v + packed_states.append(State(**dict_s)) + return packed_states + + +if __name__ == "__main__": + traj_path = Path('../../results/pocman_pellet_probe_trajectory_bidx_1.npy') + save_vod_path = Path('../../results/pocman_pellet_probe_trajectory_bidx_1.mp4') + + dataset = load_info(traj_path) + states, predictions = pack_states(dataset['states']), dataset['predictions'] + + # now we make our animation + fig, axes = plt.subplots(3, num=f"PocmanPredictionAnimation", figsize=(4, 12)) + + def make_frame(idx: int) -> None: + state_ax, p0_ax, p1_ax = axes + for a in axes: + a.clear() + + # First we make our state image + state = states[idx] + state_img = create_grid_image(state) + state_ax.set_axis_off() + state_ax.imshow(state_img) + + # Now we make our prediction images + prediction0, prediction1 = predictions[idx] + visualize_grid(prediction0, fig, p0_ax, add_colorbar=(idx==0)) + visualize_grid(prediction1, fig, p1_ax, add_colorbar=(idx==0)) + + fig.suptitle(f"PacMan Score: {int(state.score)}", size=10) + + + animation = matplotlib.animation.FuncAnimation( + fig, + make_frame, + frames=len(states), + interval=400, + ) + + + # plt.show() + # Save the animation as a gif. + animation.save(save_vod_path) + + print(f"Saved animation to {save_vod_path}") From f1989c72c2b94c0ff2ab9e58f2fe48f2aea25d3c Mon Sep 17 00:00:00 2001 From: Ruo Yu Tao Date: Mon, 28 Oct 2024 15:33:19 -0400 Subject: [PATCH 2/2] add description of parity check experiments in additional_experiments.md --- scripts/{README.md => additional_experiments.md} | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) rename scripts/{README.md => additional_experiments.md} (81%) diff --git a/scripts/README.md b/scripts/additional_experiments.md similarity index 81% rename from scripts/README.md rename to scripts/additional_experiments.md index 864cf60..b79928e 100644 --- a/scripts/README.md +++ b/scripts/additional_experiments.md @@ -1,9 +1,17 @@ -# Scripts +# Additional experiments This `scripts` directory includes scripts for plotting all experimental results in our work, as well as scripts for a few additional experiments in the paper. +## Parity Check experiments +The parity check closed-form optimization experiments were done with the +`batch_run_analytical.py` script, except with the option `--mem_aug_before_init_pi`. +This option augments our POMDP with a random memory function before +choosing the initial policy that maximizes the λ-discrepancy. See the +`parity_check*_30seeds.py` hyperparameter files to run these experiments. + + ## P.O. PacMan Memory Probe To train our memory probe, we need to first collect checkpoints from a P.O. PacMan run. We can do so with the `pocman_*ppo_best_ckpt.py` scripts