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 4: Biological meta reinforcement learning

Week 2, Day 4: Macro-Learning

By Neuromatch Academy

Content creators: Hlib Solodzhuk, Ximeng Mao, Grace Lindsay

Content reviewers: Aakash Agrawal, Alish Dipani, Hossein Rezaei, Yousef Ghanbari, Mostafa Abdollahi, Hlib Solodzhuk, Ximeng Mao, Samuele Bolotta, Grace Lindsay, Alex Murphy

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


Tutorial Objectives

Estimated timing of tutorial: 70 minutes

In this tutorial, you will observe how meta-learning may occur in the brain, specifically through reinforcement learning and the Baldwin effect.


Setup

Install and import feedback gadget

Source

Imports

Source

Figure settings

Source

Plotting functions

Source

Set device (GPU or CPU)

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

Helper functions

Source

Data retrieval

Source

Set random seed

Source

Section 0: Let’s play a game!

First, watch the video to understand our shift to reinforcement learning. Then, try out your own reinforcement learning skills! Below, you will play an interactive game, and your task is to maximize the total reward you receive!

Video 1: Reinforcement Learning Task

Youtube
Bilibili

The rules are simple: on each turn, you are shown two distinct objects in two different hands (left or right). You should pick a hand; after that, you will immediately observe the reward for this particular choice. Good luck with maximizing your score! After playing, discuss in a group whether you have any clues about the underlying structure of the game and whether the most optimal strategy exists to play this game.

Make sure you execute this cell to play the game!

Source
Loading...
Loading...
Loading...
Loading...
<Figure size 800x400 with 2 Axes>
Loading...
Loading...

Section 1: Harlow Experiment & Advantage Actor Critic (A2C) Agent

Estimated timing to here from start of tutorial: 10 minutes

In this section, we will introduce the reinforcement learning environment, replicating the 1940s Harlow experiment, and observe how different agents can learn to perform a single task.

Video 2: Reinforcement Learning on the Harlow Task

Youtube
Bilibili

Any RL system consists of an agent who tries to succeed in a task by observing the state of the environment, executing an action, and receiving the outcome (reward) based on its action. The environment is the source of the reward signal in the RL framework.

First, we will define the environment for this task.

Let’s evaluate a simple strategy for this task: an agent always chooses the side that was previously rewarded (meaning it stays with the same hand if it received a reward and changes its action if it was punished). Do you think this agent uses information about the current state? How much cumulative reward do you expect this “dummy” agent to get?

Make sure you execute this cell to observe the plot!

Source
<Figure size 800x600 with 1 Axes>

For now, simply run all the cells in this section without exploring the content. You can come back to look through the code if you have time after completing all the tutorials.

(Click to expand)

The dummy agent’s strategy doesn’t use object identity, which is key to consistently selecting the right action. Let’s see if we can do better than the dummy agent’s strategy. After defining the environment, observing the agent’s behavior, and implementing such a simple policy, it is the right time to remind ourselves about more sophisticated agent architectures capable of learning the environment’s dynamics. For this, we will use the Advantage Actor-Critic (A2C) algorithm.

The main idea behind A2C, as its name suggests, is that it consists of two networks, named actor and critic. The actor network learns the policy (mapping states to actions), while the critic network learns the value function (estimating the expected future rewards from a given state). In most cases, they share the same “body” (i.e., model layers), and only the last linear projection to the output is specific to each network. The “advantage” term comes from the training step: instead of raw rewards, in A2C, we calculate the advantage function, which estimates how much better or worse an action is compared to the average action value for a given state.

The architecture of the agent is as follows: it receives the previous state, previous reward, and previously chosen action as input, which is linearly projected to the hidden_size (this creates an embedding); then, its core consists of recurrent LSTM cells, their number is exactly hidden_size. Right after this RNN layer, there are two distinct linear projections: one for the actor (output dimension coincides with the number of actions; for the Harlow experiment, it is 2) and the other for the critic (outputs one value).

We don’t propose an exercise to code for the agent; when you have time, simply go through the cell below to understand the implementation.

In the cell below, we define the training procedure for the A2C agent and its evaluation afterward. The function train_evaluate_agent performs num_gradient_steps gradient steps (by default 25), and for each of the steps, the agent is exposed to the environment’s states sequence of length num_trials (by default 6, as in the classical Harlow experiment). Each gradient step, it performs backpropagation of the loss for these 6 trials by calculating advantage and weighting actor and critic losses with the entropy of the policy (for more information, please refer to this resource, p.14). After the training, the evaluation phase starts, gathering rewards for num_evaluation_trials trials (by default 20).

Note: the evaluation is completed on the same task (i.e., the same set of two objects) as the model was trained on.

Let’s see what the score is for the default A2C agent in this Harlow experiment (as the number of evaluation trials is 20, the maximum score to obtain is exactly 20).

Score is 12.

Can we think of a way to improve the network’s learning of the Harlow tasks?


Section 2: Baldwin Effect

Estimated timing to here from start of tutorial: 20 minutes

This section introduces the meta-nature of the Harlow experiment and how it can be related to concepts we learned in the previous meta-learning tutorial. It also discusses the Baldwin effect in evolutionary biology and proposes that you code its implementation.

Video 3: Baldwin Effect

Youtube
Bilibili

Agent’s Rate to Learn

As introduced in the video, the Baldwin effect argues that we don’t inherit the features/weights that make us good at specific tasks but rather the ability to learn quickly to gain the needed features in the context of the tasks we face during our own lifetime. This way, evolution works like the outer loop in a meta-learning context.

In the next section, we will implement this evolutionary approach to meta-learning. But first, we need to write a function that lets us evaluate how well an agent can learn each instantiation of the Harlow task. This looks something like this:

[1] θ - network parameters[2] Sample batch of tasks τi∼p(τ)[3] for all τi do[4] θi←θ[5] for k in range(number of gradient steps)[6] Evaluate ∇θiLτi(θi)[7] Compute adapted parameters with gradient descent:  θi←θi−α∇θiLτi(θi)[8] end for[9] Calculate score of updated agent on this task fi=score(τi(θi))[10] end for[11] Score of the agent for all tasks is F=∑ifi\begin{align*} & [1] \: \theta \: \text{- network parameters} \\ & [2] \: \text{Sample batch of tasks }\tau_i \sim p(\tau) \\ & [3] \: \text{for all }\tau_i \text{ do} \\ & [4] \: \quad\quad \theta_i \leftarrow \theta \\ & [5] \: \quad\quad \text{for} \: k \: \text{in range(number of gradient steps)} \\ & [6] \: \quad\quad\quad\quad\text{Evaluate }\nabla_{\theta_i} \mathcal{L}_{\tau_i}(\theta_i) \\ & [7] \: \quad\quad\quad\quad\text{Compute adapted parameters with gradient descent:} \: \: \theta_i \leftarrow \theta_i - \alpha \nabla_{\theta_i} \mathcal{L}_{\tau_i}(\theta_i) \\ & [8] \: \quad\quad\text{end for} \\ & [9] \: \quad\quad\text{Calculate score of updated agent on this task }f_i = \text{score}(\tau_i(\theta_i)) \\ & [10] \: \text{end for} \\ & [11] \: \text{Score of the agent for all tasks is} \: F = \sum_{i} f_i \\ \end{align*}

At first, we sample a bunch of tasks from the environment (different pairs of objects; line [2]). The crucial concept involved in this algorithm is preserved in line [4], where for each new task, we don’t start with updated parameters but with the ones we had before training and evaluating the agent. Then, we perform training for the defined number of gradient steps and evaluate the agent’s performance on this same task (we have defined this function in the second section of the tutorial; it basically covers lines [5] - [9]). One task is not enough to evaluate the agent’s ability to learn quickly — this is why we sampled a bunch of them, and the general score for the agent is defined as the sum of rewards for all tasks.

In the cell below, which we have written for you, you can see the implementation of the evaluation of a randomly created agent on 10 tasks (thus, the maximum score that can be obtained is: 10 (number of tasks) x 20 (number of evaluation trials per task) = 200). In the next section of the tutorial, we will provide the evolutionary framework in which we are going to learn “base” or “starting” weights.

Observe the box plot of the scores as well as their sum.

Total score is 54.
<Figure size 800x600 with 1 Axes>

Not surprisingly, this random agent does not perform very well.

Coding Exercise 1: Genetic Algorithm

Genetic algorithms (GA) mimic some of the evolutionary processes while generating better and better (= more desired) individuals in the population. At first, we initialize a population that consists of randomly defined agents (so we have a list of population_size A2C agents, which will, in the very end, evolve into agents that quickly learn the new task from the Harlow experiment environment).

Each epoch (which is the classical term for machine learning) is defined as a generation in GA as we generate new individuals in the population. For each generation, we choose top-score individuals from the population (of size tournament_size). The population size should be big enough to preserve diversity and not too big for selecting top-score ones. This is exactly where selection occurs and it is the only such place in the whole algorithm. We then we select a random batch of these high-performing individuals of size parents_num. From those, we create offspring of size new_generation_new_individuals, which will replace random individuals from the population. Notice that we replace random individuals with new ones without comparing their performance (it is just one of the paradigms in genetic algorithms). We continue running generations until we are happy with the best-fit individual appearing in the population or until we run out of time. We end when we reach the maximum number of generations.

The most interesting part happens when we create offspring—simulating evolutionary processes, we randomly select two parents (two agents), and for each of the layers in their networks, we randomly select which one will go to the child (simulating crossing over), and then we add Gaussian noise to each of the layers (simulating mutation).

The following cells consist of 3 functions:

  • create_initial_population, which creates a population and evaluates each individual by their score;

  • create_new_agent, which performs operations of crossing over and mutation on parents’ networks to create the offspring;

The first cell defines the noise constants to be used for each of the (hyper)parameters while mutating them.

  • update_population, which deletes random individuals at the end of the generation and evaluates and adds new ones.

Your task is to complete this function!

To get the desired results of the genetic algorithm, one should wait for the population to evolve enough. Unfortunately, we don’t have that much time, so to see the initial results, we will only run for 1 generation.

Creating Initial Population:   0%|          | 0/50 [00:00<?, ?it/s]
Creating Initial Population:   2%|▏         | 1/50 [00:01<01:24,  1.71s/it]
Creating Initial Population:   4%|▍         | 2/50 [00:03<01:22,  1.71s/it]
Creating Initial Population:   6%|▌         | 3/50 [00:05<01:20,  1.72s/it]
Creating Initial Population:   8%|▊         | 4/50 [00:06<01:17,  1.69s/it]
Creating Initial Population:  10%|█         | 5/50 [00:08<01:16,  1.70s/it]
Creating Initial Population:  12%|█▏        | 6/50 [00:10<01:15,  1.71s/it]
Creating Initial Population:  14%|█▍        | 7/50 [00:12<01:16,  1.78s/it]
Creating Initial Population:  16%|█▌        | 8/50 [00:14<01:16,  1.81s/it]
Creating Initial Population:  18%|█▊        | 9/50 [00:15<01:12,  1.77s/it]
Creating Initial Population:  20%|██        | 10/50 [00:17<01:09,  1.75s/it]
Creating Initial Population:  22%|██▏       | 11/50 [00:19<01:07,  1.73s/it]
Creating Initial Population:  24%|██▍       | 12/50 [00:20<01:05,  1.72s/it]
Creating Initial Population:  26%|██▌       | 13/50 [00:22<01:03,  1.73s/it]
Creating Initial Population:  28%|██▊       | 14/50 [00:24<01:02,  1.74s/it]
Creating Initial Population:  30%|███       | 15/50 [00:26<01:01,  1.74s/it]
Creating Initial Population:  32%|███▏      | 16/50 [00:27<00:59,  1.75s/it]
Creating Initial Population:  34%|███▍      | 17/50 [00:29<00:57,  1.76s/it]
Creating Initial Population:  36%|███▌      | 18/50 [00:31<00:56,  1.78s/it]
Creating Initial Population:  38%|███▊      | 19/50 [00:33<00:55,  1.80s/it]
Creating Initial Population:  40%|████      | 20/50 [00:35<00:54,  1.80s/it]
Creating Initial Population:  42%|████▏     | 21/50 [00:36<00:52,  1.80s/it]
Creating Initial Population:  44%|████▍     | 22/50 [00:38<00:50,  1.79s/it]
Creating Initial Population:  46%|████▌     | 23/50 [00:40<00:48,  1.79s/it]
Creating Initial Population:  48%|████▊     | 24/50 [00:42<00:46,  1.78s/it]
Creating Initial Population:  50%|█████     | 25/50 [00:44<00:44,  1.78s/it]
Creating Initial Population:  52%|█████▏    | 26/50 [00:45<00:42,  1.77s/it]
Creating Initial Population:  54%|█████▍    | 27/50 [00:47<00:40,  1.77s/it]
Creating Initial Population:  56%|█████▌    | 28/50 [00:49<00:38,  1.75s/it]
Creating Initial Population:  58%|█████▊    | 29/50 [00:50<00:36,  1.73s/it]
Creating Initial Population:  60%|██████    | 30/50 [00:52<00:34,  1.72s/it]
Creating Initial Population:  62%|██████▏   | 31/50 [00:54<00:32,  1.71s/it]
Creating Initial Population:  64%|██████▍   | 32/50 [00:55<00:30,  1.71s/it]
Creating Initial Population:  66%|██████▌   | 33/50 [00:57<00:28,  1.70s/it]
Creating Initial Population:  68%|██████▊   | 34/50 [00:59<00:27,  1.71s/it]
Creating Initial Population:  70%|███████   | 35/50 [01:01<00:25,  1.70s/it]
Creating Initial Population:  72%|███████▏  | 36/50 [01:02<00:23,  1.69s/it]
Creating Initial Population:  74%|███████▍  | 37/50 [01:04<00:22,  1.69s/it]
Creating Initial Population:  76%|███████▌  | 38/50 [01:06<00:20,  1.69s/it]
Creating Initial Population:  78%|███████▊  | 39/50 [01:07<00:18,  1.70s/it]
Creating Initial Population:  80%|████████  | 40/50 [01:09<00:16,  1.70s/it]
Creating Initial Population:  82%|████████▏ | 41/50 [01:11<00:15,  1.69s/it]
Creating Initial Population:  84%|████████▍ | 42/50 [01:12<00:13,  1.71s/it]
Creating Initial Population:  86%|████████▌ | 43/50 [01:14<00:12,  1.73s/it]
Creating Initial Population:  88%|████████▊ | 44/50 [01:16<00:10,  1.74s/it]
Creating Initial Population:  90%|█████████ | 45/50 [01:18<00:08,  1.73s/it]
Creating Initial Population:  92%|█████████▏| 46/50 [01:20<00:07,  1.75s/it]
Creating Initial Population:  94%|█████████▍| 47/50 [01:21<00:05,  1.74s/it]
Creating Initial Population:  96%|█████████▌| 48/50 [01:23<00:03,  1.75s/it]
Creating Initial Population:  98%|█████████▊| 49/50 [01:25<00:01,  1.77s/it]
Creating Initial Population: 100%|██████████| 50/50 [01:27<00:00,  1.78s/it]
Creating Initial Population: 100%|██████████| 50/50 [01:27<00:00,  1.74s/it]

Generation: 0, mean population score: 50.0, best score: 100
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[20], line 32
     29 parents_population = random.choices(tournament_population, k=parents_size)
     31 # Update population
---> 32 population, best_score = update_population(env, optimizer_func, population, parents_population, best_score)
     33 mean_population_scores.append(np.mean([agent_score[1] for agent_score in population]))

Cell In[19], line 16, in update_population(env, optimizer_func, population, parents_population, best_score, new_generation_new_individuals)
      2 """
      3 Updates population with new individuals which are the result of crossing over and mutation of two parents agents.
      4 Removes the same amount of random agents from the population.
   (...)
     12 - new_generation_new_individuals (int, default = 5): the number of individuals to create (and the old ones to remove).
     13 """
     14 ###################################################################
     15 ## Fill out the following then remove
---> 16 raise NotImplementedError("Student exercise: complete update of the population logic.")
     17 ###################################################################
     18 
     19 # Create new individuals with progress bar
     20 new_individuals = []

NotImplementedError: Student exercise: complete update of the population logic.

If you change num_generations to 800 in the previous code cell, the plot for the mean score in the population will roughly take the following form.

Picture which depicts the plot of mean scores per generation.

At the very start of the tutorial, we downloaded the best agent we obtained from training on 800 generations (you can get the same if you add extra infrastructure code to “catch” an agent as soon as its score reaches some threshold value; in this case, it can even be set up to 200). In the next section, we are going to compare its performance with a randomly initialized agent and observe that, indeed, during evolutionary processes, we developed agents with parameters that are able to learn more quickly.

Think!: Evolutionary Theories in Code

In this section, we have observed how the Baldwin effect evolves individuals’ parameters so that they quickly learn. We would like to propose that you think about other evolutionary biology ideas. For example, what would this process look like if we took a Lamarckian approach? Discuss what should be changed in the implementation to use these ideas (simply put, what parts of the code should be changed to implement Lamarckian inheritance).

Take time to think and then discuss as a group.


Section 3: Newbie & Experienced Bird

Estimated timing to here from start of tutorial: 50 minutes

This section proposes a comparison of the evolutionary trained (found) agent that performs the Harlow experiment with the previously mentioned model, which is initialized from scratch (and thus only trains on the given task but does not benefit from meta-learning).

Make sure you execute this cell to observe the plot!

Source
Total score of newbie agent is 54.
Total score of experienced agent is 180.
<Figure size 800x600 with 1 Axes>

Section 4: An alternative model of how the brain solves the Harlow experiment

Estimated timing to here from start of tutorial: 60 minutes

There are no code snippets or exercises in this section. However, you can watch the video to learn about an alternative approach to biological learning, demonstrating how learning within a single brain can work in the Harlow experiment.

Video 4: An Alternative Model

Youtube
Bilibili

Summary

Estimated timing of tutorial: 70 minutes

Here is a summary of what we’ve learned:

  1. The Baldwin effect says that evolution will select organisms that are good at learning.

  2. We can use evolutionary/genetic algorithms to replicate this process. This is the “outer loop” of a meta-learning problem

  3. To be more biologically plausible, we can use reinforcement learning as the inner loop.

  4. This process creates agents that can quickly find the rewarding object in a Harlow experiment.

The Big Picture

If aspects of the environment change over time, previously learned hard-coded strategies might not work anymore. Still, as in the previous tutorial, these changes share similar features across the tasks. The Harlow experiment illustrates that although there is no direct instruction on how to obtain the maximum possible reward, and the environment’s state changes with each new pair of objects, the agent is still able to capture the meta-structure of the experiment—the reward is associated with the object, not its relative placement. Only one trial is needed to identify which of the two new objects is rewarded.

Earlier in this tutorial, we instructed you to skip over the implementation and rationale behind the A2C code. If you have some extra time, why not jump back above and give that section a read in full? Either now or later at your own discretion. We just wanted to remind you that further details were given (but we initially asked you to skip over). If anything is unclear at this stage, it could be a good way to fill in some gaps in your knowledge.