Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Open In Colab   Open in Kaggle

Tutorial 3: Neural network modularity

Week 2, Day 1: Macrocircuits

By Neuromatch Academy

Content creators: Ruiyi Zhang

Content reviewers: Xaq Pitkow, Hlib Solodzhuk, Patrick Mineault, Alex Murphy

Production editors: Konstantine Tsafatinos, Ella Batty, Spiros Chavlis, Samuele Bolotta, Hlib Solodzhuk, Alex Murphy


Tutorial objectives

Estimated timing of tutorial: 1 hour

This tutorial will exemplify the importance of modularity in neural network architecture. We will train deep reinforcement learning (RL) agents with two types of neural network architectures, one modular and the other holistic, in a neuroscience navigation task.

As you have learned in previous lectures, better learning and generalization are important benefits of an appropriate inductive bias. This applies to both biological and artificial learning systems. Indeed, it has been shown that macaques trained in this navigation task can master the training task well and generalize to novel tasks derived from it. Since the brain is quite modular, it will be interesting to see if artificial models with a more modular architecture also allow for better learning and generalization than those with a less modular, holistic architecture.

Our learning objectives for today are:

  1. Build RL agents with different neural architectures for a spatial navigation task.

  2. Compare the differences in learning between RL agents with different architectures.

  3. Compare the differences in generalization between RL agents with different architectures.

  4. Use neural decoding to understand why the modular architecture for this specific task is advantageous.

  5. Keep the No-Free-Lunch Theorem in mind: the benefits of a modular architecture for one task cannot apply to all possible tasks.

This tutorial is based on this paper.


Setup

Install and import feedback gadget

Source

Imports

Source

Figure settings

Source

Helper functions

Source

Plotting functions

Source

Set random seed

Source

Data retrieval

Source

Video 1: Introduction

Youtube
Bilibili

Section 1: RL agents in a spatial navigation task

We will use a naturalistic virtual navigation task to train and test RL agents. This task was previously used to investigate the neural computations underlying macaques’ flexible behaviors.

Task setup

Video 2: Task Setup

Youtube
Bilibili

At the beginning of each trial, the subject is situated at the center of the ground plane facing forward; a target is presented at a random location within the field of view (distance: 100 to 400 cm, angle: -35 to +35∘+35^{\circ}) on the ground plane and disappears after 300 ms. The subject can freely control its linear and angular velocities with a joystick (maximum: 200 cm/s and 90∘90^{\circ}/s, referred to as the joystick gain) to move along its heading in the virtual environment. The objective is to navigate toward the memorized target location and then stop inside the reward zone, a circular region centered at the target location with a radius of 65 cm. A reward is given only if the subject stops inside the reward zone (see figure below).

The subject’s self-location is not directly observable because there are no stable landmarks; instead, the subject needs to use optic flow cues on the ground plane to perceive self-motion and perform path integration. Each textural element of the optic flow, an isosceles triangle, appears at random locations and orientations, disappearing after only a short lifetime, making it impossible to use as a stable landmark. A new trial starts after the subject stops moving.

Navigation task scheme

Task modeling

Video 3: Task Modeling

Youtube
Bilibili

We formulate this task as a Partially Observable Markov Decision Process (POMDP) in discrete time, with continuous state and action spaces (see figure below). At each time step tt, the environment is in the state st\boldsymbol{s}_t (including the agent’s position and velocity and the target’s position). The agent takes an action at\boldsymbol{a}_t (controlling its linear and angular velocities) to update st\boldsymbol{s}_t to the next state st+1\boldsymbol{s}_{t+1} following the environmental dynamics given by the transition probability T(st+1∣st,at)T(\boldsymbol{s}_{t+1}|\boldsymbol{s}_{t},\boldsymbol{a}_{t}), and receives a reward rtr_t from the environment following the reward function R(st,at)R(\boldsymbol{s}_{t},\boldsymbol{a}_{t}) (1 if the agent stops inside the reward zone otherwise 0).

Actor-critic model

We use a model-free actor-critic approach to learning, with the actor and critic implemented using distinct neural networks:

  • The actor: At each tt, the actor receives two sources of inputs it\boldsymbol{i}_{t} about the state: observation ot\boldsymbol{o}_t and last action at−1\boldsymbol{a}_{t-1}. It then outputs an action at\boldsymbol{a}_{t}, aiming to maximize the state-action value QtQ_t. This value is a function of the state and action, representing the expected discounted rewards when an action is taken at a state, and future rewards are then accumulated from tt until the trial’s last step.

  • The critic: Since the ground truth value is unknown, the critic is used to approximate the value. In addition to receiving the same inputs as the actor to infer the state, the critic also takes as inputs the action at\boldsymbol{a}_t taken by the actor in this state. It then outputs the estimated QtQ_t for this action, trained through the temporal-difference reward prediction error (TD error) after receiving the reward rtr_t (∣rt+γQt+1−Qt∣(|r_t+\gamma Q_{t+1}-Q_{t}|, where γ\gamma denotes the temporal discount factor).

The state st\boldsymbol{s}_t is not fully observable, so the agent must maintain an internal state representation (belief btb_t) for deciding at\boldsymbol{a}_{t} and QtQ_t. Both actor and critic undergo end-to-end training through back-propagation without explicit objectives for shaping btb_t. Consequently, networks are free to learn diverse forms of btb_t encoded in their neural activities that aid them in achieving their learning objectives. Ideally, networks may develop an effective belief update rule, using the two sources of evidence in the inputs it={ot,at−1}\boldsymbol{i}_t=\{\boldsymbol{o}_t, \boldsymbol{a}_{t-1}\}. They may predict the state st\boldsymbol{s}_t based on its internal model of the dynamics, its previous belief bt−1b_{t-1}, and the last self-action at−1\boldsymbol{a}_{t-1} (e.g., a motor efference copy). The second source is a partial and noisy observation ot\boldsymbol{o}_t of st\boldsymbol{s}_t drawn from the observation probability O(ot∣st)O(\boldsymbol{o}_t|\boldsymbol{s}_t). Note that the actual OO in the brain for this task is unknown. For simplicity, we model ot\boldsymbol{o}_t as a low-dimensional vector, including the target’s location when visible (the first 300 ms, Δt=0.1\Delta t=0.1 s), and the agent’s observation of its velocities through optic flow, with velocities subject to Gaussian additive noise.

Video 4: Task Parameters

Youtube
Bilibili

Given this task modeling, we first specify some parameters.

Parameters definition

Source

Coding Exercise 1: Task environment

Video 5: Task Environment

Youtube
Bilibili

We then define the task environment in the following code. You can see from the code how a target is sampled for each trial. You will need to fill in the missing code to define the task dynamics T(st+1∣st,at)T(\boldsymbol{s}_{t+1}|\boldsymbol{s}_{t},\boldsymbol{a}_{t}).

The code for the dynamics implements the following mathematical equations:

sxt+1=sxt+svtcos⁡(sθt) Δts_{x_{t+1}}=s_{x_{t}}+s_{v_{t}} \cos (s_{\theta_t})\,\Delta t\\
syt+1=syt+svtsin⁡(sθt) Δts_{y_{t+1}}=s_{y_{t}}+s_{v_{t}} \sin (s_{\theta_t})\,\Delta t\\
sθt+1=sθt+sωt Δts_{\theta_{t+1}}=s_{\theta_{t}}+s_{\omega_{t}}\,\Delta t \\
svt+1=Gvavt+ηvts_{v_{t+1}}=G_{v}a_{v_t} + \eta_{v_t} \\
sωt+1=Gωaωt+ηωts_{\omega_{t+1}}=G_{\omega} a_{\omega_t} + \eta_{\omega_t}\\

where the state elements include:

  • x position sxts_{x_{t}}

  • y position syts_{y_{t}}

  • heading sθts_{\theta_{t}}

  • linear velocity svts_{v_{t}}

  • angular velocity sωts_{\omega_{t}}

GvG_{v} and GωG_{\omega} are the joystick gains mapping actions to linear and angular velocities. ηvt\eta_{v_t} and ηωt\eta_{\omega_t} are process noises.

Coding exercise 2: RL agent

Agent observation

Next, we define the observation probability O(ot∣st)O(\boldsymbol{o}_t|\boldsymbol{s}_t) from which the observation ot\boldsymbol{o}_t is drawn.

The observation for self-movement is defined as

ot=Htst+ζt\boldsymbol{o}_t=H_t \boldsymbol{s}_t+\boldsymbol{\zeta}_t

where ζt\boldsymbol{\zeta}_t is a zero-mean Gaussian observation noise, and the observation model HtH_{t} is a matrix that only takes the velocity elements (linear and angular velocities) from the true state, filtering out positional elements as they are unobservable. Essentially, the observation for self-movement is a noisy version of the agent’s linear and angular velocities.

Observation dynamics

Source

Actor-critic RL agent

Video 6: RL Agent

Youtube
Bilibili

Each RL agent requires an actor and a critic network. Actor and critic networks can have a variety of architectures. Our goal here is to investigate whether functionally specialized modules provide advantages for our task. Therefore, we designed architectures incorporating modules with distinct levels of specialization for comparison. The first architecture is a holistic actor/critic, comprising a single module where all neurons jointly compute the belief btb_t and the action at\boldsymbol{a}_t/value QtQ_t. In contrast, the second architecture is a modular actor/critic, featuring modules specialized in computing different variables (see the figure below).

The specialization of each module is determined as follows.

First, we can confine the computation of beliefs. Since computing beliefs about the evolving state requires integrating evidence over time, a network capable of computing belief must possess some form of memory. Recurrent neural networks (RNNs) satisfy this requirement by using a hidden state that evolves over time. In contrast, computations of value and action do not need additional memory when the belief is provided, making memoryless multi-layer perceptrons (MLPs) sufficient. Consequently, adopting an architecture with an RNN followed by a memoryless MLP (modular actor/critic) ensures that the computation of belief is exclusively confined to the RNN.

Second, we can confine the computation of the state-action value QtQ_t for the critic. Since a critic is trained end-to-end to compute QtQ_t, stacking two modules between all inputs and outputs does not limit the computation of QtQ_t to a specific module. However, since QtQ_t is a function of the action at\boldsymbol{a}_t, we can confine the computation of QtQ_t to the second module of the modular critic by supplying at\boldsymbol{a}_t only to the second module. This ensures that the first module, lacking access to the action, cannot accurately compute QtQ_t. Therefore, the modular critic’s RNN is dedicated to computing btb_t and sends it to the MLP dedicated to computing QtQ_t. This architecture enforces modularity.

Besides the critic, the modular actor has higher specialization than the holistic actor, which lacks confined btb_t computation. Thought bubbles in the figure below denote the variables that can be computed within each module enforced through architecture rather than indicating they are encoded in each module. For example, btb_t in modular architectures is passed to the second module, but an accurate btb_t computation can only be completed in the first RNN module.

Actor-critic architecture for holistic and modular representations

We will compare two agents: the modular agent, which uses modular actor and modular critic networks, and the holistic agent, which uses holistic actor and holistic critic networks.

For simplicity, we will only present the code for actors here.

Holistic actor

We define the holistic actor as follows. Fill in the missing code to define the holistic architecture.

Test your implementation of `HolisticActor’!

Source
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[13], line 8
      5 actor_test = HolisticActor(arg.OBS_DIM, arg.ACTION_DIM, arg.TARGET_DIM)
      7 x_test = torch.randn(1, 1, 6); hidden_in_test = (torch.randn(1, 1, 220), torch.randn(1, 1, 220))
----> 8 a, hidden_out = actor_test(x_test, hidden_in_test)
     10 if torch.norm(a.reshape(-1) - torch.tensor([0.1145, -0.1694])) < 1e-2:
     11     print('Your function is correct!')

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1518, in Module._wrapped_call_impl(self, *args, **kwargs)
   1516     return self._compiled_call_impl(*args, **kwargs)  # type: ignore[misc]
   1517 else:
-> 1518     return self._call_impl(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1527, in Module._call_impl(self, *args, **kwargs)
   1522 # If we don't have any hooks, we want to skip the rest of the logic in
   1523 # this function, and just call forward.
   1524 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1525         or _global_backward_pre_hooks or _global_backward_hooks
   1526         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1527     return forward_call(*args, **kwargs)
   1529 try:
   1530     result = None

Cell In[12], line 36, in HolisticActor.forward(self, x, hidden_in)
     23 """
     24 Computes the action based on the current input and hidden state.
     25 
   (...)
     32 - hidden_out (tuple): The updated hidden state from the LSTM.
     33 """
     34 ###################################################################
     35 ## Fill out the following then remove
---> 36 raise NotImplementedError("Student exercise: complete forward propagation through holistic actor.")
     37 ###################################################################
     38 #######################################################
     39 # TODO: Pass the input 'x' and the previous hidden state 'hidden_in' to the RNN module 'self.rnn'.
   (...)
     43 # The first position corresponds to 'x', and the second position corresponds to the hidden state.
     44 #######################################################
     45 x, hidden_out = ...

NotImplementedError: Student exercise: complete forward propagation through holistic actor.

Modular actor

We define the modular actor as follows. Fill in the missing code to define the modular architecture.

Test your implementation of `ModularActor’!

Source
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[15], line 8
      5 actor_test = ModularActor(arg.OBS_DIM, arg.ACTION_DIM, arg.TARGET_DIM)
      7 x_test = torch.randn(1, 1, 6); hidden_in_test = (torch.randn(1, 1, 128), torch.randn(1, 1, 128))
----> 8 a, hidden_out = actor_test(x_test, hidden_in_test)
     10 if torch.norm(a.reshape(-1) - torch.tensor([0.0068, 0.0307])) < 1e-2:
     11     print('Your function is correct!')

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1518, in Module._wrapped_call_impl(self, *args, **kwargs)
   1516     return self._compiled_call_impl(*args, **kwargs)  # type: ignore[misc]
   1517 else:
-> 1518     return self._call_impl(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1527, in Module._call_impl(self, *args, **kwargs)
   1522 # If we don't have any hooks, we want to skip the rest of the logic in
   1523 # this function, and just call forward.
   1524 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1525         or _global_backward_pre_hooks or _global_backward_hooks
   1526         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1527     return forward_call(*args, **kwargs)
   1529 try:
   1530     result = None

Cell In[14], line 39, in ModularActor.forward(self, x, hidden_in)
     26 """
     27 Computes the action based on the current input and hidden state.
     28 
   (...)
     35 - hidden_out (tuple): The updated hidden state from the LSTM.
     36 """
     37 ###################################################################
     38 ## Fill out the following then remove
---> 39 raise NotImplementedError("Student exercise: complete forward propagation through modular actor.")
     40 ###################################################################
     41 #######################################################
     42 # TODO: Pass 'x' to the MLP module, which consists of two linear layers with ReLU nonlinearity.
     43 # First, pass 'x' to the first linear layer, 'self.l1', followed by 'F.relu'.
     44 # Second, pass 'x' again to the second linear layer, 'self.l2', followed by 'F.relu'.
     45 #######################################################
     46 x, hidden_out = self.rnn(x, hidden_in)

NotImplementedError: Student exercise: complete forward propagation through modular actor.

Coding Exercise 2 Discussion

  1. To ensure a fair comparison, the total number of trainable parameters is designed to be similar between the two architectures. How many trainable parameters are there in each architecture?


Section 2: Evaluate agents in the training task

Estimated timing to here from start of tutorial: 25 minutes

With the code for the environment and agents complete, we will now write an evaluation function allowing the agent to interact with the environment where the quality of the model can be assessed.

Coding Exercise 3: Evaluation function

We first sample 1,000 targets for the RL agent to steer towards.

Sample 1000 targets

Source

Then, we define the evaluation function. For each target, an agent takes multiple steps to steer to it. In each step, the agent selects an action based on the input information about the state, and the environment updates according to the agent’s action.

Fill in the missing code that calculates the reward.

Since training RL agents takes a lot of time, here we load the pre-trained modular and holistic agents and evaluate these two agents on the same sampled 1,000 targets. We will then store the evaluation data in pandas Dataframe object.

Loading...
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[18], line 3
      1 modular_agent = Agent(arg, ModularActor)
      2 modular_agent.load(Path('agents/modular'), 0)
----> 3 modular_df = evaluation(modular_agent)

Cell In[17], line 40, in evaluation(agent, gain_factor)
     36 states = []
     38 for t in range(arg.EPISODE_LEN):
     39     # 1. Agent takes an action given the state-related input
---> 40     action, hidden_out = agent.select_action(state_input, hidden_in)
     42     # 2. Environment updates to the next state given state and action,
     43     #    as well as checking if the agent has reached the reward zone,
     44     #    and if the agent has stopped.
     45     next_state, reached_target = env(state, action, t)

Cell In[11], line 31, in Agent.select_action(self, state_input, hidden_in)
     19 """
     20 Selects an action based on the current state input and hidden state.
     21 
   (...)
     28 - hidden_out (tensor): The updated hidden state from the actor model.
     29 """
     30 with torch.no_grad():
---> 31     action, hidden_out = self.actor(state_input, hidden_in)
     32 return action, hidden_out

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1518, in Module._wrapped_call_impl(self, *args, **kwargs)
   1516     return self._compiled_call_impl(*args, **kwargs)  # type: ignore[misc]
   1517 else:
-> 1518     return self._call_impl(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1527, in Module._call_impl(self, *args, **kwargs)
   1522 # If we don't have any hooks, we want to skip the rest of the logic in
   1523 # this function, and just call forward.
   1524 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1525         or _global_backward_pre_hooks or _global_backward_hooks
   1526         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1527     return forward_call(*args, **kwargs)
   1529 try:
   1530     result = None

Cell In[14], line 39, in ModularActor.forward(self, x, hidden_in)
     26 """
     27 Computes the action based on the current input and hidden state.
     28 
   (...)
     35 - hidden_out (tuple): The updated hidden state from the LSTM.
     36 """
     37 ###################################################################
     38 ## Fill out the following then remove
---> 39 raise NotImplementedError("Student exercise: complete forward propagation through modular actor.")
     40 ###################################################################
     41 #######################################################
     42 # TODO: Pass 'x' to the MLP module, which consists of two linear layers with ReLU nonlinearity.
     43 # First, pass 'x' to the first linear layer, 'self.l1', followed by 'F.relu'.
     44 # Second, pass 'x' again to the second linear layer, 'self.l2', followed by 'F.relu'.
     45 #######################################################
     46 x, hidden_out = self.rnn(x, hidden_in)

NotImplementedError: Student exercise: complete forward propagation through modular actor.
Loading...
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[19], line 3
      1 holistic_agent = Agent(arg, HolisticActor)
      2 holistic_agent.load(Path('agents/holistic'), 0)
----> 3 holistic_df = evaluation(holistic_agent)

Cell In[17], line 40, in evaluation(agent, gain_factor)
     36 states = []
     38 for t in range(arg.EPISODE_LEN):
     39     # 1. Agent takes an action given the state-related input
---> 40     action, hidden_out = agent.select_action(state_input, hidden_in)
     42     # 2. Environment updates to the next state given state and action,
     43     #    as well as checking if the agent has reached the reward zone,
     44     #    and if the agent has stopped.
     45     next_state, reached_target = env(state, action, t)

Cell In[11], line 31, in Agent.select_action(self, state_input, hidden_in)
     19 """
     20 Selects an action based on the current state input and hidden state.
     21 
   (...)
     28 - hidden_out (tensor): The updated hidden state from the actor model.
     29 """
     30 with torch.no_grad():
---> 31     action, hidden_out = self.actor(state_input, hidden_in)
     32 return action, hidden_out

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1518, in Module._wrapped_call_impl(self, *args, **kwargs)
   1516     return self._compiled_call_impl(*args, **kwargs)  # type: ignore[misc]
   1517 else:
-> 1518     return self._call_impl(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1527, in Module._call_impl(self, *args, **kwargs)
   1522 # If we don't have any hooks, we want to skip the rest of the logic in
   1523 # this function, and just call forward.
   1524 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1525         or _global_backward_pre_hooks or _global_backward_hooks
   1526         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1527     return forward_call(*args, **kwargs)
   1529 try:
   1530     result = None

Cell In[12], line 36, in HolisticActor.forward(self, x, hidden_in)
     23 """
     24 Computes the action based on the current input and hidden state.
     25 
   (...)
     32 - hidden_out (tuple): The updated hidden state from the LSTM.
     33 """
     34 ###################################################################
     35 ## Fill out the following then remove
---> 36 raise NotImplementedError("Student exercise: complete forward propagation through holistic actor.")
     37 ###################################################################
     38 #######################################################
     39 # TODO: Pass the input 'x' and the previous hidden state 'hidden_in' to the RNN module 'self.rnn'.
   (...)
     43 # The first position corresponds to 'x', and the second position corresponds to the hidden state.
     44 #######################################################
     45 x, hidden_out = ...

NotImplementedError: Student exercise: complete forward propagation through holistic actor.

Coding Exercise 4: Agent trajectory in a single trial

After evaluation, we want to visualize the agents’ steering trajectories. Fill in the missing code to compute the distance between the stop location and the target location. Given the reward boundary is 65 cm, are these trials rewarded?

Modular agent trajectory

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[20], line 3
      1 with plt.xkcd():
      2     trial_idx = 21
----> 3     trial = modular_df.iloc[trial_idx]
      5     fig = plt.figure(figsize=(2.2, 1.7), dpi=200)
      6     ax = fig.add_subplot(111)

NameError: name 'modular_df' is not defined

Click for solution

Example output:

Solution hint

Holistic agent trajectory

Holistic agent trajectory

Source
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[21], line 5
      3 with plt.xkcd():
      4     trial_idx = 21
----> 5     trial = holistic_df.iloc[trial_idx]
      7     fig = plt.figure(figsize=(2.2, 1.7), dpi=200)
      8     ax = fig.add_subplot(111)

NameError: name 'holistic_df' is not defined

Coding Exercise 4 Discussion

  1. Is there any difference between the trajectories for the modular and holistic agents? If so, what does it imply?

Activity: Comparing performance of agents

Agent trajectories across trials

We can also visualize multiple trials together. Here, we visualize trajectories steering towards 500 targets.

Modular agent’s trajectories

Source
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[22], line 4
      1 # @title Modular agent's trajectories
      3 target_idexes = np.arange(0, 500)
----> 4 df = modular_df
      6 fig = plt.figure(figsize=(2.2, 1.7), dpi=200)
      7 ax = fig.add_subplot(111)

NameError: name 'modular_df' is not defined

Holistic agent’s trajectories

Source
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[23], line 4
      1 # @title Holistic agent's trajectories
      3 target_idexes = np.arange(0, 500)
----> 4 df = holistic_df
      6 fig = plt.figure(figsize=(2.2, 1.7), dpi=200)
      7 ax = fig.add_subplot(111)

NameError: name 'holistic_df' is not defined

Discussion

  1. Is there any difference between the two agents’ trajectories?

Agents trained with multiple random seeds

It is well known that an RL agent’s performance can vary significantly with different random seeds. Therefore, no conclusions can be drawn based on one training run with a single random seed. Therefore, to make more convincing conclusions, we must run the same experiment across different random initializations in order to be sure that any repeatedly-obtainable result is robustly seen across such different random initializations.

Both agents were trained across 8 random seeds. All of them were evaluated using the same sample of 1,000 targets.

Let’s load this saved trajectory data.

Load agents

Source

Reward fraction comparison

We first compute the fraction of rewarded trials in the total 1,000 trials for all training runs with different random seeds for the modular and holistic agents. We visualize this using a bar plot, with each red dot denoting the performance of a random seed.

Reward function comparison

Source
<Figure size 440x340 with 1 Axes>

Time spent comparison

Despite similar performance measured by a rewarded fraction, we did observe qualitative differences in the trajectories of the two agents in the previous sections. It is possible that the holistic agent’s more curved trajectories, although reaching the target, are less efficient, i.e., they waste more time.

Therefore, we also plot the time spent by both agents for the same 1,000 targets.

Time spent comparison

Source
<Figure size 440x340 with 1 Axes>

Discussion

  1. Which agent’s behavior is more desired in the RL framework? Why?

Hint: Consider the objective functions for the critic and actor. The discount factor γ\gamma used in training is smaller than 1.

Training curve

So far, we have only tested the agents after training. We can also visualize the performance curve for both agents during the training course.

During training, for every 500 training trials, the agent’s performance (fraction of rewarded trials) was evaluated. These data were saved; we’ll load them back in for visualization.

Load training curves

Source

Let us plot the training curves for both agents. The shaded area denotes the standard error of the mean across all random seeds.

Visualize training curves

Source
<Figure size 440x340 with 1 Axes>

Conclusion for the training task: The modular agent learned faster and developed more efficient behaviors than the holistic agent.


Section 3: A novel gain task

Estimated timing to here from start of tutorial: 50 minutes

The prior task had a fixed joystick gain that meant consistent linear and angular velocities. We will now look at a novel task that tests the generalization capabilities of these models by varying this setting between training and testing. Will the model generalize well?

Video 7: Novel Task

Youtube
Bilibili

A good architecture should not only learn fast but also generalize better.

One crucial parameter in our task is the joystick gain, which linearly maps motor actions (dimensionless, bounded in [−1,1][-1,1]) to corresponding velocities in the environment, i.e., velocity=gain ⋅\cdot action (see the environment dynamics above). During training, the gain remains fixed at 200 cm/s and 90∘90^{\circ}/s for linear and angular components, referred to as the 1×1\times gain.

To test agents’ generalization abilities, here, we increase the gain to 2×2\times. This means the maximum velocities in the environment are doubled to 400 cm/s and 180∘180^{\circ}/s. Note that agents were only exposed to the 1×1\times gain during training. Therefore, if they use the same sequence of actions as in training with the 2×2\times gain, they will overshoot targets.

Let us evaluate modular and holistic agents with the same 1000 sampled targets again, but now with the 2×2\times gain.

Change in gain

Load holistic & modular agents

Source
Loading...
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[30], line 4
      2 modular_agent = Agent(arg, ModularActor)
      3 modular_agent.load(Path('agents/modular'), 0)
----> 4 modular_dfs_2x = evaluation(modular_agent, gain_factor=gain_factor)
      6 holistic_agent = Agent(arg, HolisticActor)
      7 holistic_agent.load(Path('agents/holistic'), 0)

Cell In[17], line 40, in evaluation(agent, gain_factor)
     36 states = []
     38 for t in range(arg.EPISODE_LEN):
     39     # 1. Agent takes an action given the state-related input
---> 40     action, hidden_out = agent.select_action(state_input, hidden_in)
     42     # 2. Environment updates to the next state given state and action,
     43     #    as well as checking if the agent has reached the reward zone,
     44     #    and if the agent has stopped.
     45     next_state, reached_target = env(state, action, t)

Cell In[11], line 31, in Agent.select_action(self, state_input, hidden_in)
     19 """
     20 Selects an action based on the current state input and hidden state.
     21 
   (...)
     28 - hidden_out (tensor): The updated hidden state from the actor model.
     29 """
     30 with torch.no_grad():
---> 31     action, hidden_out = self.actor(state_input, hidden_in)
     32 return action, hidden_out

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1518, in Module._wrapped_call_impl(self, *args, **kwargs)
   1516     return self._compiled_call_impl(*args, **kwargs)  # type: ignore[misc]
   1517 else:
-> 1518     return self._call_impl(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.10.22/x64/lib/python3.10/site-packages/torch/nn/modules/module.py:1527, in Module._call_impl(self, *args, **kwargs)
   1522 # If we don't have any hooks, we want to skip the rest of the logic in
   1523 # this function, and just call forward.
   1524 if not (self._backward_hooks or self._backward_pre_hooks or self._forward_hooks or self._forward_pre_hooks
   1525         or _global_backward_pre_hooks or _global_backward_hooks
   1526         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1527     return forward_call(*args, **kwargs)
   1529 try:
   1530     result = None

Cell In[14], line 39, in ModularActor.forward(self, x, hidden_in)
     26 """
     27 Computes the action based on the current input and hidden state.
     28 
   (...)
     35 - hidden_out (tuple): The updated hidden state from the LSTM.
     36 """
     37 ###################################################################
     38 ## Fill out the following then remove
---> 39 raise NotImplementedError("Student exercise: complete forward propagation through modular actor.")
     40 ###################################################################
     41 #######################################################
     42 # TODO: Pass 'x' to the MLP module, which consists of two linear layers with ReLU nonlinearity.
     43 # First, pass 'x' to the first linear layer, 'self.l1', followed by 'F.relu'.
     44 # Second, pass 'x' again to the second linear layer, 'self.l2', followed by 'F.relu'.
     45 #######################################################
     46 x, hidden_out = self.rnn(x, hidden_in)

NotImplementedError: Student exercise: complete forward propagation through modular actor.

Let us first visualize the modular agents’ trajectories with 2×2\times gain.

Modular agent’s trajectories

Source
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[31], line 4
      1 # @title Modular agent's trajectories
      3 target_idexes = np.arange(0, 500)
----> 4 df = modular_dfs_2x
      6 fig = plt.figure(figsize=(2.2, 1.7), dpi=200)
      7 ax = fig.add_subplot(111)

NameError: name 'modular_dfs_2x' is not defined

Let us now visualize the holistic agents’ trajectories with 2×2\times gain.

Holistic agent’s trajectories

Source
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[32], line 4
      1 # @title Holistic agent's trajectories
      3 target_idexes = np.arange(0, 500)
----> 4 df = holistic_dfs_2x
      6 fig = plt.figure(figsize=(2.2, 1.7), dpi=200)
      7 ax = fig.add_subplot(111)

NameError: name 'holistic_dfs_2x' is not defined

Discussion

  1. Do the two agents have the same generalization abilities?

Again, let us load the saved evaluation data with the 2×2\times gain for agents with all 8 random seeds.

Load data

Source

Now, let’s compare reward fraction with 2×2\times gain.

Reward function comparison

Source
<Figure size 440x340 with 1 Axes>

Let us also compare traveled distance vs target distance with 2×2\times gain for different agents.

Traveled distance comparison

Source
<Figure size 400x400 with 1 Axes>

Discussion:

  1. What have you found? Which agent generalized better?

Decoding analysis

Although we have confirmed that the modular agent exhibited better generalization performance in the gain task, the underlying mechanism remains unclear. One reasonable hypothesis is that the modular agent has a robust belief that remains accurate with the 2×2\times gain. Therefore, this robust belief can support good actions. Ultimately, agents should be aware of its location to avoid overshooting.

To test this hypothesis, we recorded the activities of RNN neurons in the agents’ actors, as these neurons are responsible for computing the beliefs underlying actions. Since beliefs should represent the agents’ locations in the environment, we used linear regression (with ℓ2\ell_2 regularization) to decode agents’ locations from the recorded RNN activities.

We defined the decoding error, representing the Euclidean distance between the true and decoded locations, as an indicator of belief accuracy.

Load data

Source

We plot the distribution of the decoding error for every step in every trial across random seeds for each agent, under 1×1× or 2×2× gain.

Decoding error comparison

Source
<Figure size 440x340 with 1 Axes>

While both agents demonstrated small decoding errors with the training gain, we observed that the holistic agent, which struggled with generalization at the 2×2× gain, also exhibited reduced accuracy in determining its own location. This helps explain why the modular agent can generalize better at higher gains.

Conclusion for the gain task: In the gain task, the modular agent exhibited better generalization than the holistic agent, supported by a more accurate internal belief when faced with larger gains.


The Big Picture

Estimated timing of tutorial: 1 hour

In this tutorial, we explored the difference in agents’ performance based on their architecture. We revealed that modular architecture, with separate modules for learning different aspects of behavior, is superior to a holistic architecture with a single module. The modular architecture with stronger inductive bias achieves good performance faster and has the capability to generalize to other tasks as well. Intriguingly, this modularity is a property we also observe in the brains, which could be important for generalization in the brain as well.


Addendum: Generalization, but no free lunch

The No Free Lunch theorems proved that no inductive bias can excel across all tasks. It has been studied in the paper that agents with a modular architecture can acquire the underlying structure of the training task. In contrast, holistic agents tend to acquire different knowledge than modular agents during training, such as forming beliefs based on unreliable information sources or exhibiting less efficient control actions. The novel gain task has a structure similar to the training task, consequently, a modular agent that accurately learns the training task’s structure can leverage its knowledge in these novel tasks.

However, it is worth noting that an infinite number of new tasks can be constructed, diverging from the training task’s structure but aligning with the ‘inferior’ beliefs and control acquired by holistic agents.

For example, although we have seen that the holistic agent developed inefficient trajectories with higher curvature, which is detrimental for the training task, if we consider a new task with a different reward function, such as preferring more curved trajectories, the holistic agent should perform better in this particular task than the modular agent. There is no free lunch.