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 2: Generalization in Neuroscience

Week 1, Day 1: Generalization

By Neuromatch Academy

Content creators: Samuele Bolotta, Patrick Mineault and Shreya Saxena

Content reviewers: Samuele Bolotta, Lily Chamakura, RyeongKyung Yoon, Yizhou Chen, Ruiyi Zhang, Aakash Agrawal, Alish Dipani, Hossein Rezaei, Yousef Ghanbari, Mostafa Abdollahi, Hlib Solodzhuk, Alex Murphy

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


Tutorial Objectives

Estimated timing of tutorial: 75 minutes

This tutorial will introduce you to generalization in neuroscience. We’ll look at a classic neuroscience paper, Sussillo et al. (2015), that compares how artificial and biological neural networks solve different motor tasks. This paper looks at how linear arm movements are generated in motor cortex; later extensions of these ideas (e.g. Codol et al. 2024; Almani et al. 2024) address how more complex skilled movements, including handwriting, can be generated, continuing our theme for today’s tutorials. We’ll look at a popular AI-derived framework for understanding how the brain solves tasks: task-driven neural networks.

Our learning goals for this tutorial are as follows:

  1. Understand core goals in neuroscience. Examine the fundamental questions that drive neuroscience research, such as the ‘What’, ‘How’, and ‘Why’ behind neurological functions and behaviors.

  2. Conceptualize what generalization means in the context of neuroscience, understanding how principles of neural generalization can inform and be informed by artificial intelligence.

  3. Evaluate the impact of architectural choices. Discuss how different architectural decisions and the selection of priors in model design can introduce inductive biases, affecting the generalization capabilities of both neural and artificial systems.

  4. Illustrate robustness in noisy environments. Identify and describe real-world instances where the pursuit of robustness against noise has led to converging strategies in both neuroscience and artificial intelligence.


Setup

Install and import feedback gadget

Source

Import dependencies

/tmp/ipykernel_5442/3612161156.py:26: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
  from tqdm.autonotebook import tqdm

Figure settings

Source

Plotting functions

Source

Set device (GPU or CPU). Execute set_device()

Source
GPU is not enabled in this notebook. 
If you want to enable it, in the menu under `Runtime` -> 
`Hardware accelerator.` and select `GPU` from the dropdown menu

Set random seed, when using pytorch

Executing set_seed(seed=seed) will specify the exact random seed to use

Source

Data retrieval

Source

Helper functions

Source

Video 1: Overview

Youtube
Bilibili

Definitions

  • Peristimulus Time Histogram (PSTH) - A visualization tool to temporally display neuronal firing rates around an external stimulus event


Section 1: Motivation: How the brain generates motor commands

Let’s put ourselves in the mindset of a neuroscientist trying to understand how the brain generates motor sequences. A classic example of a complex motor sequence is handwriting, which we looked at in the last tutorial. This skill involves coordinated movement of the arm, wrist and fingers.

The mapping between the goal (e.g. moving the arm) and the sequence of motor commands that drive different muscles is highly nonlinear and recurrent. Furthermore, the brain must generalize beyond its training data; for instance, when the recurrent connections of the brain attempt to maintain stability in the face of noise in the environment (homeostasis effects), or when the arm is under different pressures. How does the brain do that?

Image adapted from Rizzoglio et al. (2023). From monkeys to humans: observation-based EMG brain–computer interface decoders for humans with paralysis. Journal of Neural Engineering. 10.1088/1741-2552/ad038e. CC-BY 4.0.

Our neuroscientist is trying to address a how question: how does the brain find solutions to complex control problems that generalize? Recurrent neural networks (RNNs) are a type of artificial neural network that have proven to be a very useful tool to address these how questions. They mimic the adaptability and plasticity observed in biological neural networks through interconnected artificial neurons, weights dictating connection strengths, and activation functions triggering neuron responses. Task-driven neural networks are trained in silico (using computers) to solve similar tasks to ones that the brain must solve. With the specification to prefer simple solutions over more complex solutions, model representations are often similar to the ones that the brain seems to find. The trained artificial neural networks can then be used to investigate, mechanistically, how a task is solved in the brain.

Picture which shows an RNN.

Let’s illustrate these ideas with a classic paper in this field: Sussillo et al. (2015). They train an RNN to solve a motor task by learning to map movement direction commands to arm movements recorded via electromyography (EMG). Looking inside this RNN gives them the ability to ask questions about generalization. They then compare the model representations from the RNN to spike data from the motor cortex (M1) of a monkey performing a similar task. Do they perform similarly? Is the RNN performing the same kind of task as the brain, in a similar way?

Video 2: Setup

Youtube
Bilibili

Section 2: Training an unregularized task-driven neural network

In this activity, our goal is to train recurrent neural networks to mimic the muscle activity (EMG) of monkeys during arm movements. The challenge is to transform simple inputs (task features) into complex patterns of muscle activity over time and space.

Shape of the inputs torch.Size([27, 250, 16])
Shape of the output torch.Size([27, 250, 2])

These dimensions correspond to the following:

  • 27 conditions, one for each trial in the data

  • 250 time steps (each trial was 2.5 seconds in length, measured at 100 Hz)

  • The inputs are 16-dimensional, with 15 dimensions corresponding to the reach condition (explained below) and the 16th dimension corresponding to the Go Cue.

  • The outputs are 2-dimensional, corresponding to the target electromyography (EMG) data for 2 muscles.

The inputs to the model are 16-dimensional vectors that specify which reach to perform. Each reach condition is represented by a unique 15-dimensional vector, which remains constant during the movement preparation period from -1s to 0s. The 16th dimension, a Go Cue, features a small bump at 0s, signaling the model to start generating the output.

To illustrate, consider a robot arm that needs to reach different objects positioned in various locations. The condition-specific inputs would guide the robot on which object to reach for (e.g., left, right, up, down). Prior to each reach, the robot’s “brain” (neural activity) prepares in a particular manner, and this readiness is recorded. These readiness signals are then fed into the RNN, helping it understand and prepare for the specific reach.

Initially, the robot remains still (hold cue). Following a brief delay, it receives a signal to begin the reach. These delays and specific inputs train the robot to accurately perform the reach for each condition.

Take a look at the output shapes printed above and mentally visualize the structure of the array. Let’s look at the few of the inputs and outputs to get a better understanding of the data. Note that “Reach direction NN” refers to one of the reach conditions (e.g. move down / up / left / right).

<Figure size 800x300 with 1 Axes>
<Figure size 800x300 with 1 Axes>
<Figure size 800x300 with 1 Axes>
<Figure size 800x300 with 1 Axes>
<Figure size 800x300 with 1 Axes>
<Figure size 800x300 with 1 Axes>

As you can see:

  • the movement (reach condition) to be generated is encoded by a 16-dimensional signal during the hold period (from -1 to 0s).

  • the 16-dimensional signal is unique for each reach direction

  • The Go Cue (feature 16, timepoint 0) signals that it’s time to start generating the movement

  • The movements of the muscles are generated right after the GO Cue.

Let’s create a PyTorch Dataset object to hold the data we just described.

Coding exercise 2.1: Defining an unregularized RNN

Let’s start by defining an unregularized RNN by inheriting from nn.Module. This RNN takes in the time-varying control inputs and outputs muscle outputs.

The model is a single-layer recurrent neural network defined in continuous time. The network takes in an input vector (all timepoints) and outputs muscle EMG (also for all timepoints). The hidden state of the network x\mathbf{x} evolves according to the equation:

τdxdt=−x+Bu+Jr+b\tau \frac{d\mathbf{x}}{dt} = -\mathbf{x} + \mathbf{B}\mathbf{u} + \mathbf{J}\mathbf{r} + \mathbf{b}

Here we have:

  • u\mathbf{u} is the stimulus input at the current timestep

  • Bu\mathbf{B}\mathbf{u} is the feedforward drive (activity) of the neural network, the inputs u\mathbf{u} are linearly projected through a set of weights B\mathbf{B}

  • r=∣tanh⁡(x)∣\mathbf{r} = |\tanh(\mathbf{x})| is the hidden activity passed through a rectifying, saturating non-linear activation function

  • Jr\mathbf{J}\mathbf{r} is the recurrent drive (activity) of the neural network, the recurrent activity r\mathbf{r} is linearly projected through a set of weights J\mathbf{J}

  • b\mathbf{b} is a constant

  • τ\tau is a scalar corresponding to the time scale of the network, 50 ms

To transform this to a standard discrete-time neural network, we use Euler integration with a time step equal to the resolution of the simulation, 10ms. See the Comp Neuro W2D2 to brush up on this idea if this idea is not clear.

Thus, the discretized update equation will be:

xt+1=xt+Δtdxtdt\mathbf{x_{t+1}} = \mathbf{x}_{t} + \Delta t\frac{d\mathbf{x_t}}{dt}

There are a few more parameters to consider:

  • The scale of the parameters of the input mixing matrix B\mathbf{B} is determined by hh (not shown in equation)

  • The scale of the parameters of the input mixing matrix J\mathbf{J} is determined by gg (not shown in equation).

  • We initialize gg to a value larger than 1, so the neural network is in a so-called chaotic regime.

  • The network is initialized with hidden state x=0\mathbf{x}=0

Finally, the model’s predicted EMG activity is given by a linear readout:

z=Wr+cz = \mathbf{W}\mathbf{r} + c

In this notation, we can interpret r\mathbf{r} as the firing rates of the neurons. Take a look at the equation above: the hidden state, x\mathbf{x} (representation of stimuli in the brain), is evolving over time and is driven by current stimuli input u\mathbf{u} and firing rates of neurons in the previous time step, r\mathbf{r}. EMG activity, zz, is a direct linear projection from firing rates.

Let’s code up this unregularized neural network.

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[13], line 12
     10 for inputs, targets in unregularized_train_loader:
     11     hidden = model.init_hidden(batch_size)
---> 12     output, hidden_after = model(inputs[:, 0, :].to(device), hidden.to(device))
     13     assert output.shape == targets[:, 0].shape
     14     assert hidden_after.shape == hidden.shape

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 20, in UnregularizedRNN.forward(self, input, hidden)
     16 def forward(self, input, hidden):
     17     #################################################
     18     # TODO for students: fill in the missing variables
     19     # Fill out function and remove
---> 20     raise NotImplementedError("Student exercise: fill in the missing variables")
     21     #################################################
     22 
     23     # Calculate the visible firing rate from the hidden state.
     24     firing_rate_before = ...

NotImplementedError: Student exercise: fill in the missing variables

Great! Now we have the model ready to go.

Note: In further code below, we’ll reference the inputs variable generated in the cell above (it will be from the last unpacking operation in unregularized_train_loader).

Coding exercise 2.2: Evaluate function

A trajectory in the current context means a complete temporal sequence over the duration of each trial. We’ll need a function that can generate an entire trajectory based on a set of inputs. To do that, we’ll first initialize the hidden state of the model, then recurrently feed the inputs and the hidden states back into the model. Fill in the missing lines to generate an entire trajectory.

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[15], line 1
----> 1 trajectory, hidden_states = generate_trajectory(model,
      2                                              inputs[0].unsqueeze(0),
      3                                              device)
      5 with plt.xkcd():
      6 
      7   # Remove unitary dimension, detach from PyTorch computational
      8   # graph, move to the CPU and convert to a numpy array
      9   hidden_states = hidden_states.squeeze().detach().cpu().numpy()

Cell In[14], line 5, in generate_trajectory(model, inputs, device)
      1 def generate_trajectory(model, inputs, device):
      2     #################################################
      3     # TODO for students: fill in the missing variables
      4     # Fill out function and remove
----> 5     raise NotImplementedError("Student exercise: fill in the missing variables")
      6     #################################################
      7     inputs = inputs.to(device)

NotImplementedError: Student exercise: fill in the missing variables

Remember, our model is untrained. It’s not supposed to give great results yet. However, it’s very good practice to see the outputs of an untrained network to get a sense of the starting position the model is at before any training has occurred. In some NeuroAI applications, untrained models can generate activity that is surprisingly similar to some brain activity in some scenarios, so it pays to know what your model looks like not only after training, but also before.

This untrained model generates funky oscillatory activity, but we’ll fix that during training!

Activity 2.1: Evaluating the model

We’re now ready to train our model. This involves:

  • Loading the data

  • Instantiating the model

  • Building a training loop

  • Using stochastic gradient descent (SGD) to train the model

To save you time, however, we’ve trained the model in advance. You’ll load a trained checkpoint. This trained model can generate the right muscle outputs in response to the right inputs. Let’s do a quick check to make sure this is the case.

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[16], line 25
     22 sample_target = outputs[train_idx[idx], ...].to(device)
     24 # Generate trajectory
---> 25 generated_target, hidden_states = generate_trajectory(unregularized_model, sample_input.unsqueeze(0), device)
     27 # Plotting
     28 plot_inputs_over_time(times, sample_input.cpu())

Cell In[14], line 5, in generate_trajectory(model, inputs, device)
      1 def generate_trajectory(model, inputs, device):
      2     #################################################
      3     # TODO for students: fill in the missing variables
      4     # Fill out function and remove
----> 5     raise NotImplementedError("Student exercise: fill in the missing variables")
      6     #################################################
      7     inputs = inputs.to(device)

NotImplementedError: Student exercise: fill in the missing variables

Earlier, we just plotted the time course of the hidden activity and generated muscle activity. Now, we do the same but we also include the observed muscle activity, which is the target of our training procedure. This is so we can compare how good the model is at capturing the dynamics of the muscle data when the monkey performed the task.

And as we can see, it does a pretty good job!

Activity 2.2: Comparing trained RNN with the brain

Our trained RNN transforms a temporal sequence of inputs into a temporal sequence of muscle activations. In effect, the RNN is a stand-in (model) for motor cortex. The activity of the hidden units of the RNN might look like motor cortex. We plot different peri-stimulus temporal histograms (PSTHs) of real neurons when an animal performs a specific arm movement (reach condition). Then, we plot different PSTHs of hidden units of the unregularized RNN for comparison.

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[17], line 1
----> 1 hidden_states = hidden_states.squeeze().detach().cpu().numpy()
      3 plot_hidden_unit_activations(hidden_states=hidden_states,
      4                              times=times,
      5                              neurons_to_plot=10,
      6                              title='PSTHs of Hidden Units in UnregularizedRNN')

NameError: name 'hidden_states' is not defined

Let’s do the same now with real neural data. We load smoothed spiking data from Susillo et al. (2015). These are visualizations from trials where monkeys performed the same kind of reaching movement that we trained the artificial neural network to do.

<Figure size 800x400 with 1 Axes>

Qualitatively, that doesn’t look like much of a match. A trained RNN that performs the same task as the brain can have very different latent representations. In later tutorials, we’ll cover in detail quantitative methods for evaluating how well representations in artificial and biological neural networks match each other; in particular. For now, let’s try to get a qualitative match between the two PSTHs by applying better regularization techniques.

Video 3: Inductive Bias

Youtube
Bilibili

Section 3: Training a regularized task-driven neural network

The previous network found complicated solutions to the problem of generating muscle activity: the hidden activity of the network didn’t match to what we see in real neurons. We’d like to obtain more naturalistic solutions that better match the brain. We’ll attempt this by regularizing this network. Later, we’ll test this regularized network for robustness and generalization to see if this also affects the network’s ability to extrapolate beyond its training data.

Susillo et al. propose to regularize the network in many different ways.

Weight regularization

Synapses are expensive to grow and maintain. The authors thus propose to use a standard L2 penalty on the sum of squares of the weights:

R0=∑ijBij2+Wij2R_0 = \sum_{ij} B_{ij}^2 + W_{ij}^2

Recall that WW are the weights that map the RNN’s latent activity to the predicted EMG data, while BB are the weights from the feedforward drive of the system. The penalty limits the magnitude of the weights, potentially leading to more biologically plausible solutions.

Firing rate regularization

Neural activity in biological neural networks is expensive. Thus, we add a penalty for the magnitude of the hidden activity in the network.

R1=1NT∑itrit2R_1 = \frac{1}{NT} \sum_{it} r_{it} ^ 2

Here NN is the number of hidden neurons and TT is the total number of discrete time steps in the simulation, and ritr{it} is the activity of the hidden neurons over time.

Multiple delays

Neural activity should be robust to the exact delays between the preparatory input and the Go Cue. We augment the dataset with multiple delays between the signal and the Go Cue so that more varied examples exist that aren’t closely tied to the exact starting time of the Go Cue. This means we become more robust to such vairiations, which also boosts generalization.

Less chaotic initialization regime

We initialized the previous RNN with large weights, putting the network in a chaotic regime, with high sensitivity to changes in activity. We’ll dial down the initialization range in this network to obtain dynamics at the edge of chaos. In practice, this involves changing the parameter that controls the magnitude of the initial recurrent weights gg from 4 to 1.5.

With all these changes in mind, we’re ready to implement the regularized model.

We split the dataset into training and validation sets. Importantly, we keep the different delays corresponding to the same condition in the same set. Otherwise, we might experience data leakage, where we inadvertently allow information about test set data to become part of our training data, leading to an overly optimistic accuracy when we come to evaluate our model.

Activity 3.1: Evaluating the model

Let’s evaluate the model by looking at its generated activity! Once again, we skip the training of the model and load a model that has been trained previously, for convenience.

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[21], line 28
     25 sample_target = outputs[train_idx[idx], ...].to(device)
     27 # Generate trajectory
---> 28 generated_target, hidden_states = generate_trajectory(regularized_model, sample_input.unsqueeze(0), device)
     30 # Plotting
     31 plot_inputs_over_time(times, sample_input.cpu())

Cell In[14], line 5, in generate_trajectory(model, inputs, device)
      1 def generate_trajectory(model, inputs, device):
      2     #################################################
      3     # TODO for students: fill in the missing variables
      4     # Fill out function and remove
----> 5     raise NotImplementedError("Student exercise: fill in the missing variables")
      6     #################################################
      7     inputs = inputs.to(device)

NotImplementedError: Student exercise: fill in the missing variables

Looks promising! But what do the RNN’s latent / hidden representations look like?

Activity 3.2: Comparing trained RNN with real data

Let’s see if this regularized network’s activity is aligned with the brain.

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[22], line 1
----> 1 hidden_states=hidden_states.squeeze().detach().cpu().numpy()
      3 plot_hidden_unit_activations(hidden_states=hidden_states,
      4                              times=times,
      5                              neurons_to_plot=10,
      6                              title='PSTHs of Hidden Units in RegularizedRNN')

NameError: name 'hidden_states' is not defined
<Figure size 800x400 with 1 Axes>

Discussion point

  1. Which RNN (a. untrained, b. trained without regularization, c. trained with regularization) looks more like the real brain data?

Video 4: A brain-like network

Youtube
Bilibili

Section 4: Robustness to change in RNNs and the brain

In this section, we perturb our regularized and unregularized inputs and structural connectivity by adding random noise to the inputs to replicate a noisy recording device. This in essence means the new inputs to the model will contain true signal as well as random noise that is not connected to the true signal.

Will regularization help the neural network be more resilient to these changes?

Let’s start by perturbing the inputs. For this purpose, we use the smaller train dataset without delay augmentations (See Section 3: Multiple Delays, above, for an explanation of delay augmentations).

Testing perturbation strength 0.0125
Loading...
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[25], line 57
     54 max_error = ((outputs - outputs.mean(axis=[0, 1], keepdims=True)) ** 2).mean()
     56 perturbation_strengths = [0.0125, 0.025, 0.05, 0.1, 0.2]
---> 57 results_unregularized = test_perturbed_inputs(unregularized_model, perturbation_strengths, test_loader, nn.MSELoss(), device, max_error)
     58 results_regularized = test_perturbed_inputs(regularized_model, perturbation_strengths, test_loader, nn.MSELoss(), device, max_error)

Cell In[25], line 38, in test_perturbed_inputs(model, perturbation_strengths, test_loader, criterion, device, max_error)
     33 # Compute error for original inputs
     34 # original_loss = compute_loss(model, inputs, targets, criterion, device)
     35 # Compute error for perturbed inputs
     37 perturbed_inputs = perturb_inputs(model, inputs, strength)
---> 38 perturbed_loss = compute_loss(model, perturbed_inputs, targets, criterion, device)
     40 # Store the normalized error.
     41 rel_error = perturbed_loss / max_error * 100

Cell In[25], line 14, in compute_loss(model, inputs, targets, criterion, device)
     12 losses = []
     13 for t in range(inputs.shape[1]):  # Iterate over time steps
---> 14     model_output = model(inputs[:, t, :], h)
     15     output, h, *rest = model_output[:2]
     16     loss = criterion(output, targets[:, t])  # Assume targets is a sequence of same length as inputs

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 20, in UnregularizedRNN.forward(self, input, hidden)
     16 def forward(self, input, hidden):
     17     #################################################
     18     # TODO for students: fill in the missing variables
     19     # Fill out function and remove
---> 20     raise NotImplementedError("Student exercise: fill in the missing variables")
     21     #################################################
     22 
     23     # Calculate the visible firing rate from the hidden state.
     24     firing_rate_before = ...

NotImplementedError: Student exercise: fill in the missing variables
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[26], line 2
      1 #Plot perturbation results
----> 2 plot_perturbation_results(perturbation_strengths, results_regularized, results_unregularized, "Perturbation of the inputs")

NameError: name 'results_regularized' is not defined

Note here that the higher the bars, the more errors the model has (the worse it performs). On the X-axis, we can see the magnitude of the perturbation. For small perturbations, the regularized model (blue bars) performs better than the unregularized model (red bars) for most of the perturbation strengths. When the perturbation magnitude is large, then we start to see that the regularized model starts to perform worse.

Now, we proceed to a different type of perturbation: this time on the model weights (not the inputs).

Coding exercise 4.1: Weights perturbation

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[28], line 8
      5 perturbation_strengths = [0.01, .1, .2, .4, .8]
      7 # Function calls for regularized and unregularized models
----> 8 results_regularized_weights = test_perturbed_structure(regularized_model, perturbation_strengths, test_loader, nn.MSELoss(), device, max_error)
      9 results_unregularized_weights = test_perturbed_structure(unregularized_model, perturbation_strengths, test_loader, nn.MSELoss(), device, max_error)

Cell In[27], line 19, in test_perturbed_structure(model, perturbation_percentages, test_loader, criterion, device, max_error)
     16 def test_perturbed_structure(model, perturbation_percentages, test_loader, criterion, device, max_error):
     17     ###########################################################
     18     # Fill in the missing lines to complete the exercise
---> 19     raise NotImplementedError("Student exercise")
     20     ###########################################################
     21     model.eval()  # Set the model to evaluation mode

NotImplementedError: Student exercise
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[29], line 2
      1 # Plot perturbation results
----> 2 plot_perturbation_results(perturbation_strengths, results_regularized_weights, results_unregularized_weights, "Perturbation of the weights")

NameError: name 'results_regularized_weights' is not defined

Discussion point 1

Why is the regularized RNN more resistant to noise?

Discussion point 2

Is the regularized RNN “how the brain works” in the motor cortex? If not, why is it useful to model this way? Does this experiment suggest that a system that can perform a task and generalize well is necessarily brain-like? If not, what else is required? Plasticity? Complex decision-making? Sensory integration? Agency? Autonomy?

Video 5: Final Thoughts

Youtube
Bilibili

The Big Picture

  • Neuroscientists often want to uncover how the brain performs a task.

  • Artificial neural networks are a useful tool for investigating how a computation might be performed.

  • We often care about biological plausibility: the operations performed by the network should roughly match those that the brain can perform. For instance, while RNNs have been surpassed by transformers for natural language processing, they remain popular in neuroscience.

  • Neuroscientists often care about whether a model generalizes across circumstances that an organism might face.

  • We tested robustness to noise (e.g. neural noise) and robustness to weight changes (e.g. rollover in synapses) across different models and found regularized models offered a useful inductive bias which generalized better.

  • Inductive biases such as preferring simpler models (like in the Sussillo paper) often lead to models that more closely resemble real neural data.

  • The tools to improve generalization in task-driven neural networks are similar to the ones used in AI more generally: regularization, augmentations, but also potentially pretraining the model and transfer learning.

  • Networks that perform similar tasks to the brain and generalize well sometimes converge to similar solutions to the brain. We don’t fully understand this phenomenon, and we’ll introduce tools throughout the course to more quantitatively assess the correspondence.