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

Microlearning

Microlearning: Credit assignment and local learning rules in artificial and neural systems

By Neuromatch Academy

Content creators: Colleen J. Gillon & Klara Kaleb

Content reviewers: Colleen J. Gillon, Klara Kaleb, Eva Dyer

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


Background

To learn effectively, our brain must coordinate synaptic updates across its network of neurons. The question of how the brain does this is called the credit assignment problem. Deep neural networks are a leading model of learning in the brain, and are typically trained using gradient descent via backpropagation. However, backpropagation is widely agreed to be biologically implausible. Therefore, to understand how the brain solves the credit assignment problem, we must find learning rules that are both biologically plausible and effective for learning. In this project, we will explore more biologically plausible learning rules proposed as alternatives to backpropagation, compare them to error backpropagation, and test whether we can infer what type of learning rule the brain might be using.

This project builds on a basic feedforward network trained to classify MNIST images (Q1). We then implement biologically plausible rules, compute performance and learning-related metrics (Q2-Q4 & Q7), to then evaluate (1) how consistent and learning-rule specific the metrics are (Q5, Q8-9), or (2) how these rules fare in more complex learning scenarios (Q6, Q10).

Relevant references:

Install and import feedback gadget

Source

Project Background

Youtube
Bilibili

Project slides

If you want to download the slides: https://osf.io/download/fjaqp/
Loading...

Project Template

Source
Loading...

Tutorial links

This project provides a perfect match for continuing your endeavors in W2D3, which is dedicated to Microlearning. Here, you are going to have the opportunity to explore Hebbian learning explicitly and get a broader prism on how the learning occurs at the synaptic level by implementing more of the algorithms.


Section 1: Initial setup

Importing dependencies

Source

Figure settings

Source

1.1 Download MNIST dataset

The first step is to download the MNIST handwritten digits dataset [1], which you will be using in this project. It is provided as a training dataset (60,000 examples) and a test dataset (10,000 examples). We can split the training dataset to obtain a training (e.g., 80%) and a validation set (e.g., 20%). In addition, since the dataset is quite large, we also suggest keeping only half of each subset, as this will make training the models faster.

[1] Deng, L. (2012). The MNIST database of handwritten digit images for machine learning research. IEEE Signal Processing Magazine, 29(6), 141–142, https://ieeexplore.ieee.org/document/6296535.

Note: The download process may try a few sources before succeeding. HTTP Error 403: Forbidden or HTTP Error 503: Service Unavailable errors can be ignored.

download_mnist(): Function to download MNIST.

Source
  0%|          | 0/9912422 [00:00<?, ?it/s]
100%|██████████| 9912422/9912422 [00:00<00:00, 137611870.81it/s]

  0%|          | 0/28881 [00:00<?, ?it/s]
100%|██████████| 28881/28881 [00:00<00:00, 16006302.04it/s]

  0%|          | 0/1648877 [00:00<?, ?it/s]
100%|██████████| 1648877/1648877 [00:00<00:00, 98396428.83it/s]

  0%|          | 0/4542 [00:00<?, ?it/s]
100%|██████████| 4542/4542 [00:00<00:00, 2948085.54it/s]

Number of examples retained:
  24001 (training)
  5999 (validation)
  5000 (test)

1.2 Explore the dataset

Source
<Figure size 600x480 with 1 Axes>

Section 2: Training a basic model

2.1 Defining a basic model

Next, we can define a basic model to train on this MNIST classification task.

First, it is helpful to define a few hyperparameters (NUM_INPUTS and NUM_OUTPUTS) to store the input size and output size the model needs to have for this task.

The MultiLayerPerceptron class, provided here, initializes a multilayer perceptron (MLP) with one hidden layer. Feel free to expand or change the class, if you would like to use a different or more complex model, or add functionalities.

This class has several basic methods:

  • __init__(self): To initialize the model.

  • _set_activation(self): To set the specified activation function for the hidden layer. (The output layer has a softmax activation.)

  • forward(self, X): To define how activity is passed through the model.

It also has additional methods that will be helpful later on for collecting metrics from the models:

  • _store_initial_weights_biases(self): To store the initial weights and biases.

  • forward_backprop(self, X): For when we will be comparing the gradients computed by alternative learning rules to the gradients computed by error backpropagation.

  • list_parameters(self): For convenience in retrieving a list of the model’s parameters.

  • gather_gradient_dict(self): For gathering the gradients of the model’s parameters.

2.2 Initializing the model

We can now initialize an MLP. Feel free to change the number of hidden units, change the activation function or include biases in the model.

Currently, the "sigmoid", "TanH", "ReLU" and "identity" activation functions are implemented, but you can add more by editing the _set_activation(self) method of the MultiLayerPerceptron class defined above.

We have set BIAS=False for simplicity.

We will also initialize the dataloaders. Feel free to select a different batch size (BATCH_SIZE) when training your models.

2.3 Defining and initializing an optimizer

Here, we define a basic optimizer that updates the weights and biases of the model based on the gradients saved. This optimizer is equivalent to a simple Stochastic Gradient Descent optimizer (torch.optim.SGD()) applied to mini-batch data.

It has two methods:

  • __init__(self): To initialize the optimizer. Any arguments passed after params should be added to the defaults dictionary, which is passed to the parents class. These arguments are then added to each parameter group’s dictionary, allowing them to be accessed in step(self).

  • step(self): Makes an update to the model parameters.

This optimizer can be extended later, if needed, when implementing more complex learning rules.

We can now initialize an optimizer. Feel free to change to learning rate (LR) that the optimizer is initialized with, or to add a weight decay.

2.4 Training a basic model

Source

Once the model and optimizer have been initialized, we can now train our model using backpropagation for a few epochs, and collect the classification results dictionary.

  0%|          | 0/5 [00:00<?, ?it/s]
 20%|██        | 1/5 [00:06<00:27,  6.99s/it]
 40%|████      | 2/5 [00:14<00:22,  7.58s/it]
 60%|██████    | 3/5 [00:21<00:14,  7.07s/it]
 80%|████████  | 4/5 [00:29<00:07,  7.62s/it]
100%|██████████| 5/5 [00:37<00:00,  7.57s/it]
100%|██████████| 5/5 [00:37<00:00,  7.48s/it]

Note: The training function does not use the test_loader. This additional dataloader can be used to evaluate the models if, for example, you need use the validation set for model selection.


Section 3: Inspecting a model’s performance

The results dictionary (MLP_results_dict) returned by train_model() contains the following keys:

  • avg_train_losses: Average training loss per epoch.

  • avg_valid_losses: Average validation loss per epoch.

  • avg_train_accuracies: Average training accuracies per epoch.

  • avg_valid_losses: Average validation accuracies per epoch.

  • train_correct_by_class: Number of correctly classified training images for each class (last epoch only).

  • train_seen_by_class: Number of training images for each class (last epoch only).

  • valid_correct_by_class: Number of correctly classified validation images for each class (last epoch only).

  • valid_seen_by_class: Number of validation images for each class (last epoch only).

Next, we can inspect our model’s performance by visualizing various metrics, e.g., classification loss, accuracy, accuracy by class, weights. A few example functions are provided for plotted various metrics collected across learning.

Source

3.1 Loss and accuracy across learning

First, we can look at how the loss (full lines) and accuracy (dashed lines) evolve across learning. The model appears to learn quickly, achieving a performance on the validation dataset that is well above chance accuracy (black dashed line), even with only 5 training epochs.

<Figure size 700x350 with 2 Axes>

3.2 Final accuracy per class

We can also look at the accuracy breakdown per class.

<Figure size 600x300 with 1 Axes>

3.3 Classified example images

We can visualize examples of how the model has classified certain images (correctly or incorrectly).

<Figure size 600x480 with 1 Axes>

3.4 Weights before and after learning

We can also observe how the weights changed through learning by visualizing the initial weights (top), the weights after learning (middle), and the difference between the two (bottom).

The average weights before learning (dashed line) and after learning (full line) are plotted on the colorbar for each layer.

<Figure size 600x500 with 4 Axes>

❓ What other metrics might you collect or visualize to understand how the model is learning to perform this task?

Note: In this section, we have visualized a variety of metrics that can be used to evaluate and compare models. Later in the project, you may want to collect and record these metrics for each trained model, instead of just visualizing the them.


Section 4: Implementing a biologically plausible learning rule

4.1 Hebbian learning

Now, it is time to implement a more biologically plausible learning rule, like Hebbian learning. This rule is famously associated with the phrase “Neurons that fire together wire together.”

In Hebbian learning, the weight wijw_{ij} between pre-synaptic neuron i and post-synaptic neuron j is updated as follows:
Δwij=η(ai⋅aj){\Delta}w_{ij} = {\eta}(a_{i} \cdot a_{j}),

where:

  • η{\eta} is the learning rate,

  • aia_{i} is the activation of the pre-synaptic neuron i, and

  • aja_{j} is the activation of the post-synaptic neuron j.

This means that the weight update between two neurons is proportional to the correlation in their activity.

Hebbian learning schematic

Source
Loading...

(Left) Hebbian learning when only positive neural activity is allowed.

Input neurons (top) are connected to output neurons (bottom). Active neurons are marked with + (active, light blue) and ++ (very active, dark blue). Connections marked with + will be weakly increased, and those marked with ++ will be strongly increased by Hebbian learning.

(Right) Hebbian learning when positive and negative neural activity is allowed.
Same as (Left), but negatively active neurons are marked with - (slightly active, light red) and -- (very active, dark red). Connections marked with - will be weakly decreased, and those marked with -- will be strongly decreased by Hebbian learning.

4.1.1 Preventing runaway potentiation

Networks trained with Hebbian learning are typically implemented with neurons that only have positive activations (Left), like neurons in the brain. This means that weights between neurons can only increase.

Allowing neurons to have both positive and negative activations (Right) would allow weights to also decrease with Hebbian learning. However, this is not typically done since negative neural activity (i.e., a negative firing rate) is not biologically plausible.

Instead, various techniques can be used to prevent runaway potentiation in Hebbian learning (i.e., weights increasing more and more, without limit). These include normalization techniques like Oja’s rule. In the example below, we take the approach of simply centering weight updates around 0.

4.2 Implementing learning rules in torch

4.2.1 Defining a custom autograd function

One way to train a torch model using a learning rule other than backpropagation is to create a custom autograd function. With a custom autograd function, we can redefine the forward() and backward() methods that pass activations forward through the network, and pass gradients backward through the network. Specifically, this allows us to specify how the gradients used to update the model should be calculated.

To implement Hebbian learning in the model, we will create a custom autograd function called HebbianFunction.

4.2.2 Defining the forward() method

The forward method of an autograd function serves two purposes:

  1. Compute an output for the given input.

  2. Gather and store all the information needed to compute weight updates during the backward pass.

As explained above, to calculate the Hebbian weight change between neurons in two layers, we need the activity of the input neurons and the activity of the output neurons. So, the forward() method should receive input neuron activity as its input, and return output neuron activity as its output.

The inputs to forward() are therefore:

  • context: Passed implictly to forward(), and used to store any information needed to calculate gradients during the backward pass.

  • input: The input neuron activity.

  • weight: The linear layer’s weights.

  • bias: The linear layer’s biases (can be None).

  • nonlinearity: The nonlinearity function, as it will be needed to calculate the output neuron’s activity.

  • target: As will be explained later, for Hebbian learning, it can be very useful to use the targets to training the last layer of the network, instead of true output activity. So here, if targets are passed to forward(), this is stored instead of the output.

In the forward pass, output neuron activity is computed, and the following variables are saved for the backward pass: input, weight, bias and output_for_update (i.e., the computed output neuron activity or the target, if it’s provided, averaged across the batch).

4.2.3 Defining the backward() method

The backward() method of an autograd function computes and returns gradients using only two input variables:

  • context: In which information was stored during the forward pass.

  • grad_output: The grad_input from the downstream layer (since gradients are computed backwards through the network.

Here, for Hebbian learning, we do not use a backpropagated gradient. For this reason, grad_output is ignored and no grad_input is computed.

Instead, the gradients for the weights and biases are computed using the variables stored in context:

  • grad_weight:

    • Computed as input neuron activity multiplied by output activity.

    • To avoid weight changes scaling linearly with the number of inputs, we also divide by the number of input neurons.

  • grad_bias:

    • Computed simply as the output neuron activity, since biases have the same dimension as the output of a layer.

    • Biases are enabled here. However, it should be noted that although they are used often in networks using error backpropagation, they are not used as often in Hebbian learning.

Notes:

  • The backward() method expects to return as many gradient values as the number of inputs passed to the forward() method (except context). It may raise an error if it’s not implemented this way. So this is why, in the example below, backward() returns grad_nonlinearity and grad_target, but they are both None values.

  • The backward() method computes the gradients, but does not apply weight updates. These are applied when optimizer.step() is called. In the BasicOptimizer, defined above, the optimizer step optionally applies a weight decay, then subtracts the gradients computed here multiplied by the learning rate.

  • Standard optimizers, including the BasicOptimizer, expect to receive error gradients (i.e., values they should subtract from the parameters). However, we have computed Hebbian gradients (i.e., values that should be added to the parameters). For this reason, the gradients computed are multiplied by -1 in the backward() method before they are returned.

To learn more about torch’s autograd function and creating custom functions, see the following torch documentation and tutorials:

4.2.4 Defining a HebbianMultiLayerPerceptron class

Lastly, we provide a HebbianMultiLayerPerceptron class. This class inherits from MultiLayerPerceptron, but implements its own forward() method. This forward() method uses HebbianFunction() to send inputs through each layer of the network and its activation function, ensuring that gradients are computed correctly for Hebbian learning.

4.3 Training a model with Hebbian learning

4.3.1 Simplifying the task to a 2-class task

When implementing biologically plausible learning rules, one often faces a performance trade-off. This is because it is often harder for a network to learn without the precise error gradients that error backpropagation offers. More sophisticated designs can really enhance performance. However, since we are starting with the basics, we will instead start by simplifying the task from a 10-class task to a 2-class task.

Source
<Figure size 600x300 with 1 Axes>

The number of examples for each class in the training and validation sets are shown. Dashed lines show what counts would be expected if there were the same number of examples in each class.

4.3.2 Training on a 2-class task

We’ll test the Hebbian learning model’s performance with 10 epochs of training.

  0%|          | 0/10 [00:00<?, ?it/s]
 10%|█         | 1/10 [00:02<00:19,  2.21s/it]
 20%|██        | 2/10 [00:03<00:15,  1.96s/it]
 30%|███       | 3/10 [00:05<00:12,  1.78s/it]
 40%|████      | 4/10 [00:08<00:13,  2.20s/it]
 50%|█████     | 5/10 [00:10<00:11,  2.21s/it]
 60%|██████    | 6/10 [00:12<00:08,  2.18s/it]
 70%|███████   | 7/10 [00:14<00:05,  1.94s/it]
 80%|████████  | 8/10 [00:15<00:03,  1.76s/it]
 90%|█████████ | 9/10 [00:16<00:01,  1.64s/it]
100%|██████████| 10/10 [00:18<00:00,  1.58s/it]
100%|██████████| 10/10 [00:18<00:00,  1.84s/it]

<Figure size 700x350 with 2 Axes>
<Figure size 600x300 with 1 Axes>
<Figure size 120x480 with 1 Axes>
<Figure size 600x500 with 4 Axes>

Try running the previous cell several times to see how often the model succeeds in learning this simple classification task.

❓ Why does this network struggle to learn this simple classification task?

4.3.3 Training with targets

As you may have notice, the classification targets do not actually appear in the basic Hebbian learning rule. How, then, can the network learn to perform a supervised task?

To allow a network trained with Hebbian learning to learn a supervised task, one can take the approach of clamping the outputs to the targets. In other words, we can update the final layer’s weights using the targets tjt_{j} instead of the final layer’s activations aja_{j}.

  0%|          | 0/10 [00:00<?, ?it/s]
 10%|█         | 1/10 [00:01<00:12,  1.37s/it]
 20%|██        | 2/10 [00:04<00:18,  2.29s/it]
 30%|███       | 3/10 [00:06<00:15,  2.23s/it]
 40%|████      | 4/10 [00:08<00:13,  2.17s/it]
 50%|█████     | 5/10 [00:10<00:10,  2.11s/it]
 60%|██████    | 6/10 [00:13<00:09,  2.35s/it]
 70%|███████   | 7/10 [00:16<00:07,  2.59s/it]
 80%|████████  | 8/10 [00:18<00:04,  2.40s/it]
 90%|█████████ | 9/10 [00:20<00:02,  2.26s/it]
100%|██████████| 10/10 [00:24<00:00,  2.90s/it]
100%|██████████| 10/10 [00:24<00:00,  2.47s/it]

<Figure size 700x350 with 2 Axes>
<Figure size 600x300 with 1 Axes>
<Figure size 120x480 with 1 Axes>
<Figure size 600x500 with 4 Axes>

Try running the previous cell several times. The model should now be more successful at learning this simple classification task.

❓ Is the model successful every time? If not, what might be contributing to the variability in performance?
❓ Going further: How does changing the training hyperparameters (learning rate and number of epochs) affect network learning?

Note: The clamped outputs setting cannot be used with Oja’s rule (i.e., one of the Hebbian learning modifications used to prevent runaway weight increases). This is because the values subtracted when using Oja’s rule would be calculated on the target outputs instead of the actual outputs, and these values would end up being too big.

4.3.4 Increasing task difficulty

Next, let’s see what happens when we increase the number of classes in the task. The following function handles the entire initialization and training process:

Source
  0%|          | 0/5 [00:00<?, ?it/s]
 20%|██        | 1/5 [00:01<00:07,  1.93s/it]
 40%|████      | 2/5 [00:03<00:05,  1.97s/it]
 60%|██████    | 3/5 [00:07<00:05,  2.90s/it]
 80%|████████  | 4/5 [00:10<00:02,  2.80s/it]
100%|██████████| 5/5 [00:12<00:00,  2.52s/it]
100%|██████████| 5/5 [00:12<00:00,  2.52s/it]

<Figure size 600x300 with 1 Axes>
<Figure size 700x350 with 2 Axes>
<Figure size 600x300 with 1 Axes>
<Figure size 600x500 with 4 Axes>

Now, let’s try training a model with Hebbian learning (and outputs clamped to targets). Since the task is harder, we’ll increase the number of training epochs to 15.

  0%|          | 0/15 [00:00<?, ?it/s]
  7%|▋         | 1/15 [00:02<00:29,  2.09s/it]
 13%|█▎        | 2/15 [00:04<00:27,  2.12s/it]
 20%|██        | 3/15 [00:06<00:25,  2.14s/it]
 27%|██▋       | 4/15 [00:09<00:26,  2.43s/it]
 33%|███▎      | 5/15 [00:14<00:32,  3.29s/it]
 40%|████      | 6/15 [00:16<00:28,  3.12s/it]
 47%|████▋     | 7/15 [00:19<00:23,  2.98s/it]
 53%|█████▎    | 8/15 [00:23<00:22,  3.18s/it]
 60%|██████    | 9/15 [00:27<00:20,  3.43s/it]
 67%|██████▋   | 10/15 [00:30<00:16,  3.30s/it]
 73%|███████▎  | 11/15 [00:33<00:13,  3.34s/it]
 80%|████████  | 12/15 [00:35<00:09,  3.01s/it]
 87%|████████▋ | 13/15 [00:39<00:06,  3.24s/it]
 93%|█████████▎| 14/15 [00:41<00:02,  2.89s/it]
100%|██████████| 15/15 [00:44<00:00,  2.77s/it]
100%|██████████| 15/15 [00:44<00:00,  2.95s/it]

<Figure size 700x350 with 2 Axes>
<Figure size 600x300 with 1 Axes>
<Figure size 600x500 with 4 Axes>

Try running the previous cell a few times to see how often the model is successful.

❓ How is the model learning in each layer?
❓ How do the weight updates learned with Hebbian learning compare to those learned with error backpropagation?

We can try using different learning rates to encourage more learning in the second layer.

  0%|          | 0/15 [00:00<?, ?it/s]
  7%|▋         | 1/15 [00:03<00:45,  3.28s/it]
 13%|█▎        | 2/15 [00:05<00:34,  2.62s/it]
 20%|██        | 3/15 [00:07<00:30,  2.57s/it]
 27%|██▋       | 4/15 [00:12<00:35,  3.27s/it]
 33%|███▎      | 5/15 [00:15<00:31,  3.13s/it]
 40%|████      | 6/15 [00:18<00:27,  3.05s/it]
 47%|████▋     | 7/15 [00:20<00:23,  2.88s/it]
 53%|█████▎    | 8/15 [00:24<00:21,  3.10s/it]
 60%|██████    | 9/15 [00:28<00:20,  3.36s/it]
 67%|██████▋   | 10/15 [00:31<00:16,  3.22s/it]
 73%|███████▎  | 11/15 [00:34<00:13,  3.32s/it]
 80%|████████  | 12/15 [00:37<00:09,  3.21s/it]
 87%|████████▋ | 13/15 [00:41<00:06,  3.45s/it]
 93%|█████████▎| 14/15 [00:45<00:03,  3.63s/it]
100%|██████████| 15/15 [00:47<00:00,  3.18s/it]
100%|██████████| 15/15 [00:47<00:00,  3.18s/it]

<Figure size 700x350 with 2 Axes>
<Figure size 600x300 with 1 Axes>
<Figure size 600x500 with 4 Axes>

Performance tends to be highly variable and unstable. At best, the network is able to classify 2 classes, but generally not all 3 classes.

4.3.5 Combining Hebbian learning and error backpropagation

What happens if we use Hebbian learning for the first layer, but use error backpropagation to train the second layer?

HebbianBackpropMultiLayerPerceptron(): Class combining Hebbian learning and backpropagation.

Source
  0%|          | 0/5 [00:00<?, ?it/s]
 20%|██        | 1/5 [00:09<00:36,  9.23s/it]
 40%|████      | 2/5 [00:18<00:27,  9.01s/it]
 60%|██████    | 3/5 [00:27<00:18,  9.07s/it]
 80%|████████  | 4/5 [00:37<00:09,  9.53s/it]
100%|██████████| 5/5 [00:45<00:00,  9.13s/it]
100%|██████████| 5/5 [00:45<00:00,  9.18s/it]

<Figure size 700x350 with 2 Axes>
<Figure size 600x300 with 1 Axes>
<Figure size 600x500 with 4 Axes>

Using Hebbian learning and error backpropagation allows us to achieve above chance performance on the the full MNIST classification task, though performance is still much lower than when using error backpropagation on its own.

❓ What are some of the properties of Hebbian learning that might explain its weaker performance on this task when compared to error backpropagation?
❓ Going further: Are there tasks that Hebbian learning might be better at than error backpropagation?

Section 4.4. Computing the variance and bias of a model’s gradients.

To better understand how a model trained with a biologically plausible learning rule (e.g., Hebbian learning) learns, it can be useful to compare its learning to error backpropagation. Specifically, we can compare the gradients computed with both learning rules to one another.

Variance

One property we can compute is the variance. The variance tells us how consistent are the gradients obtained for each weight across examples when computed with one learning rule compared to the other. We might expect a good learning rule to be more consistent, and therefore have lower variance in its gradients (left column in the bullseye image).

However, it would be unfair to compare the variance of small gradients (like those computed with error backpropagation) with the variance of large gradients (like those computed with Hebbian learning). So, we will estimate variance in the gradients using a scale-invariant measure: the signal-to-noise ratio (SNR). Note that high SNR corresponds to low variance, and vice versa.

Bias

Another property we can compute is how biased gradients computed with a specific learning rule are with respect to ideal gradients. Now, since the ideal gradients for learning a task are generally unknown, we need to estimate them. We can do so using error backpropagation, as it is the best algorithm we know for learning a task along the gradient of its error. A good learning rule would then be expected to have low bias in its gradients with respect to error backpropagation gradients.

As with the variance, our bias estimate should be scale-invariant, so we will estimate it using the Cosine similarity. Note that high Cosine similarity with error backpropagation gradients corresponds to low bias, and vice versa.

Bias and variance schematic

Source
Loading...

The bullseye image illustrates the differences and interplay between bias and variance in a variable. In the left column, the variable being measured shows low variance, as the green dots are densely concentrated. In the right column, the dots are more dispersed, reflecting a higher variance.

Although the examples in each column show the same variance, they show different biases. In examples in the top row, the variable being measured has a low bias with respect to the bullseye, as the dots are centered on the bullseye. In contrast, in the bottom row, the variable being measured has a high bias a variable with low bias with respect to the bullseye, as the dots are off-center with respect to the bullseye.

4.4.1 Estimating gradient variance using SNR

The following functions measure and plot the SNR of the gradients:

Source

For the error backpropagation model and the Hebbian learning model, we compute the SNR before training the model, using the validation set. This allows us to evaluate the gradients a learning rule proposes for an untrained model. Notably, we pass one example at a time through the model, to obtain gradients for each example.

Note: Since we obtain a gradient SNR for each layer of the model, here we plot the gradient SNR averaged across layers.

<Figure size 300x400 with 1 Axes>

The dots in the plots show the SNR for each layer (small dot: first layer; larger dot: second layer).

Hebbian learning appears to have produced gradients with a very high SNR (and therefore lower variance) in the first layer (small dot). In constrast, the second layer (big dot) shows a lower SNR than error backpropagation.

Notably, this is evaluated on the full 10-class task which Hebbian learning struggles to learn.

❓ What does the Hebbian learning SNR look like for the 2-class version of the task?
❓ What might this mean about how Hebbian learning learns?
❓ Going further: How might this result relate to the Hebbian learning rule’s performance on the classification task?

4.4.2 Estimating the gradient bias with respect to error backpropagation using the Cosine similarity.

The following functions measure and plot the Cosine similarity of the gradients to error backpropagation gradients:

Source
Collecting Cosine similarities for backprop-trained model...
  0%|          | 0/10 [00:00<?, ?it/s]
 10%|█         | 1/10 [00:07<01:11,  7.95s/it]
 20%|██        | 2/10 [00:17<01:12,  9.10s/it]
 30%|███       | 3/10 [00:26<01:01,  8.74s/it]
 40%|████      | 4/10 [00:35<00:53,  8.98s/it]
 50%|█████     | 5/10 [00:45<00:46,  9.22s/it]
 60%|██████    | 6/10 [00:54<00:36,  9.10s/it]
 70%|███████   | 7/10 [01:04<00:28,  9.57s/it]
 80%|████████  | 8/10 [01:13<00:18,  9.33s/it]
 90%|█████████ | 9/10 [01:21<00:08,  8.94s/it]
100%|██████████| 10/10 [01:29<00:00,  8.80s/it]
100%|██████████| 10/10 [01:29<00:00,  8.99s/it]

Collecting Cosine similarities for Hebbian-trained model...
  0%|          | 0/10 [00:00<?, ?it/s]
 10%|█         | 1/10 [00:08<01:12,  8.07s/it]
 20%|██        | 2/10 [00:17<01:10,  8.78s/it]
 30%|███       | 3/10 [00:26<01:01,  8.79s/it]
 40%|████      | 4/10 [00:35<00:53,  8.93s/it]
 50%|█████     | 5/10 [00:43<00:44,  8.83s/it]
 60%|██████    | 6/10 [00:52<00:35,  8.77s/it]
 70%|███████   | 7/10 [01:02<00:27,  9.24s/it]
 80%|████████  | 8/10 [01:11<00:18,  9.04s/it]
 90%|█████████ | 9/10 [01:19<00:08,  8.87s/it]
100%|██████████| 10/10 [01:27<00:00,  8.41s/it]
100%|██████████| 10/10 [01:27<00:00,  8.73s/it]

<Figure size 800x400 with 1 Axes>

As expected, gradient updates proposed by error backpropagation necessarily have no bias with respect to themselves (cosine similarity near 1.0). In contrast, although gradient updates proposed by Hebbian learning for the second layer (big dots) are well aligned with error backpropagation updates, the updates proposed for the first layer (small dots) are highly biased (cosine similarity near 0.0).

❓ What might this result tell us about how Hebbian learning learns in this task?
❓ What does the Hebbian learning cosine similarity to error backpropagation look like for the 2-class version of the task?
❓ Going further: How might this result relate to the Hebbian learning rule’s performance on the classification task?

❓ Taken together, what do the bias and variance properties of each layer tell us about how Hebbian learning learns in this task?
❓ Going further: How learning rule-specific are the bias and variance properties of the gradients compared to other performance or learning metrics?
❓ Going further: How do the bias and variance of the gradients relate to the performance of a learning rule on a task?


Section 5: Implementing additional learning rules

In this notebook, we implemented learning in a neural network using Hebbian learning, and examined how this learning rule performed under various scenarios. Hebbian learning is only one biologically plausible learning rule among many others. Importantly, many of these other rules are better suited to supervised learning tasks like image classification.

Examples of basic biologically plausible learning rules to explore include node perturbation, weight_perturbation, feedback alignment, and the Kolen Pollack.

Take a look at Neuromatch’s NeuroAI tutorial for the Microlearning day for implementations of these algorithms using numpy. Then, see whether you can reimplement one or several of them using custom torch autograd functions, as demonstrated in this notebook.

Implementing certain learning rules may also require making some changes to how the optimizer step is performed. To do so, you can adapt the BasicOptimizer() or any other torch optimizer, as needed.

The following repositories could be very helpful resources, as they implement these learning rules using custom torch autograd functions:

  • Feedback alignment: lib/fa_linear.py

  • Kolen-Pollack: linear.py. Also take a look at main.py to see how they adapt the SGD optimizer to produce the correct updates for Kolen Pollack.


Section 6: Tips & suggestions

Here are a few tips that may be helpful as you delve into questions from the project template.

  • Testing whether the metrics are specific to a learning rule: There are a few ways to assess whether certain performance and learning metrics are specific to a learning rule. Examples include:

    • Visualization: You could plot the metrics or a lower-dimensional version of the metrics to visualize whether the metrics for each learning rule form separate clusters or whether they all mix together.

    • Classification: You could test whether a linear classifier can be trained to correctly predict the learning rule from the metrics.

  • Assessing how your models respond to more challenging learning scenarios: There are several challenging learning scenarios you can implement. Examples include:

    • Online learning: Training with a batch size of one.

    • Non-stationary data: Changing the distribution of the data across learning. Here, the restrict_classes(dataset) function may be useful. For example, you could initially train a model on a dataset with no examples from the 6 class, and then introduce examples from the 6 class partway through training to see how this affects learning.

Additional References

Original papers introducing these biologically plausible learning rules:

⭐ We hope you enjoy working on your project and, through the process, make some interesting discoveries about the challenges and potentials of biologically plausible learning! ⭐