{ "cells": [ { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {}, "id": "view-in-github" }, "source": [ "  " ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Tutorial 1: Interventions\n", "**Week 3, Day 5: Network Causality**\n", "\n", "**By Neuromatch Academy**\n", "\n", "**Content creators**: Ari Benjamin, Tony Liu, Konrad Kording\n", "\n", "**Content reviewers**: Mike X Cohen, Madineh Sarvestani, Ella Batty, Yoni Friedman, Michael Waskom\n", "\n", "**Post-production team:** Gagana B, Spiros Chavlis" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ " \n", " Click here for text recap of video \n", "\n", "Let's think carefully about the statement \"**A causes B**\". To be concrete, let's take two neurons. What does it mean to say that neuron $A$ causes neuron $B$ to fire?\n", "\n", "The *interventional* definition of causality says that:\n", "\n", "\\begin{equation}\n", "(A \\text{ causes } B) \\Leftrightarrow ( \\text{ If we force }A \\text { to be different, then }B\\text{ changes})\n", "\\end{equation}\n", "\n", "To determine if $A$ causes $B$ to fire, we can inject current into neuron $A$ and see what happens to $B$.\n", "\n", "**A mathematical definition of causality**: \n", "Over many trials, the average causal effect $\\delta_{A\\to B}$ of neuron $A$ upon neuron $B$ is the average change in neuron $B$'s activity when we set $A=1$ versus when we set $A=0$.\n", "\n", "\n", "\\begin{equation}\n", "\\delta_{A\\to B} = \\mathbb{E}[B | A=1] - \\mathbb{E}[B | A=0] \n", "\\begin{equation}\n", "\n", "\n", "where $\\mathbb{E}[B | A=1]$ is the expected value of B if A is 1 and $\\mathbb{E}[B | A=0]$ is the expected value of B if A is 0.\n", "\n", "Note that this is an average effect. While one can get more sophisticated about conditional effects ($A$ only effects $B$ when it's not refractory, perhaps), we will only consider average effects today.\n", "\n", "
\n", "\n", "**Relation to a randomized controlled trial (RCT)**:\n", "The logic we just described is the logic of a randomized control trial (RCT). If you randomly give 100 people a drug and 100 people a placebo, the effect is the difference in outcomes." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Coding Exercise 1: Randomized controlled trial for two neurons\n", "\n", "Let's pretend we can perform a randomized controlled trial for two neurons. Our model will have neuron $A$ synapsing on Neuron $B$:\n", "\n", "\\begin{equation}\n", "B = A + \\epsilon\n", "\\end{equation}\n", "\n", "where $A$ and $B$ represent the activities of the two neurons and $\\epsilon$ is standard normal noise $\\epsilon\\sim\\mathcal{N}(0,1)$.\n", "\n", "Our goal is to perturb $A$ and confirm that $B$ changes." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "both", "execution": {} }, "outputs": [], "source": [ "def neuron_B(activity_of_A):\n", " \"\"\"Model activity of neuron B as neuron A activity + noise\n", "\n", " Args:\n", " activity_of_A (ndarray): An array of shape (T,) containing the neural activity of neuron A\n", "\n", " Returns:\n", " ndarray: activity of neuron B\n", " \"\"\"\n", " noise = np.random.randn(activity_of_A.shape)\n", "\n", " return activity_of_A + noise\n", "\n", "np.random.seed(12)\n", "\n", "# Neuron A activity of zeros\n", "A_0 = np.zeros(5000)\n", "\n", "# Neuron A activity of ones\n", "A_1 = np.ones(5000)\n", "\n", "###########################################################################\n", "## TODO for students: Estimate the causal effect of A upon B\n", "## Use eq above (difference in mean of B when A=0 vs. A=1)\n", "raise NotImplementedError('student exercise: please look at effects of A on B')\n", "###########################################################################\n", "\n", "diff_in_means = ...\n", "print(diff_in_means)" ] }, { "cell_type": "markdown", "metadata": { "cellView": "both", "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main//tutorials/W3D5_NetworkCausality/solutions/W3D5_Tutorial1_Solution_9ae3afbe.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "You should get a difference in means of 0.990719 (so very close to one). " ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Section 2: Simulating a system of neurons\n", "\n", "*Estimated timing to here from start of tutorial: 9 min*\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "Can we still estimate causal effects when the neurons are in big networks? This is the main question we will ask today. Let's first create our system, and the rest of today we will spend analyzing it.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Video 2: Simulated neural system model\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 2: Simulated neural system model\n", "from ipywidgets import widgets\n", "\n", "out2 = widgets.Output()\n", "with out2:\n", " from IPython.display import IFrame\n", " class BiliVideo(IFrame):\n", " def __init__(self, id, page=1, width=400, height=300, **kwargs):\n", " self.id=id\n", " src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)\n", " super(BiliVideo, self).__init__(src, width, height, **kwargs)\n", "\n", " video = BiliVideo(id=\"BV1Xt4y1Q7uC\", width=730, height=410, fs=1)\n", " print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))\n", " display(video)\n", "\n", "out1 = widgets.Output()\n", "with out1:\n", " from IPython.display import YouTubeVideo\n", " video = YouTubeVideo(id=\"oPJz49dAuL8\", width=730, height=410, fs=1, rel=0)\n", " print('Video available at https://youtube.com/watch?v=' + video.id)\n", " display(video)\n", "\n", "out = widgets.Tab([out1, out2])\n", "out.set_title(0, 'Youtube')\n", "out.set_title(1, 'Bilibili')\n", "\n", "display(out)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "**Video correction**: the connectivity graph plots and associated explanations in this and other videos show the wrong direction of connectivity (the arrows should be pointing the opposite direction). This has been fixed in the figures below." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "This video introduces a big causal system (interacting neurons) with understandable dynamical properties and how to simulate it.\n", "\n", "
\n", " Click here for text recap of video \n", "\n", "\n", "Our system has N interconnected neurons that affect each other over time. Each neuron at time $t+1$ is a function of the activity of the other neurons from the previous time $t$. \n", "\n", "Neurons affect each other nonlinearly: each neuron's activity at time $t+1$ consists of a linearly weighted sum of all neural activities at time $t$, with added noise, passed through a nonlinearity:\n", "\n", "\\begin{equation}\n", "\\vec{x}_{t+1} = \\sigma(A\\vec{x}_t + \\epsilon_t),\n", "\\end{equation}\n", "\n", "- $\\vec{x}_t$ is an $n$-dimensional vector representing our $n$-neuron system at timestep $t$\n", "- $\\sigma$ is a sigmoid nonlinearity\n", "- $A$ is our $n \\times n$ *causal ground truth connectivity matrix* (more on this later)\n", "- $\\epsilon_t$ is random noise: $\\epsilon_t \\sim N(\\vec{0}, I_n)$\n", "- $\\vec{x}_0$ is initialized to $\\vec{0}$\n", "\n", "$A$ is a connectivity matrix, so the element $A_{ij}$ represents the causal effect of neuron $i$ on neuron $j$. In our system, neurons will receive connections from only 10% of the whole population on average." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "We will create the true connectivity matrix between 6 neurons and visualize it in two different ways: as a graph with directional edges between connected neurons and as an image of the connectivity matrix.\n", "\n", "*Check your understanding*: do you understand how the left plot relates to the right plot below?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Execute this cell to get helper function create_connectivity and visualize connectivity\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "#@markdown Execute this cell to get helper function create_connectivity and visualize connectivity\n", "\n", "def create_connectivity(n_neurons, random_state=42):\n", " \"\"\"\n", " Generate our nxn causal connectivity matrix.\n", "\n", " Args:\n", " n_neurons (int): the number of neurons in our system.\n", " random_state (int): random seed for reproducibility\n", "\n", " Returns:\n", " A (np.ndarray): our 0.1 sparse connectivity matrix\n", " \"\"\"\n", " np.random.seed(random_state)\n", " A_0 = np.random.choice([0, 1], size=(n_neurons, n_neurons), p=[0.9, 0.1])\n", "\n", " # set the timescale of the dynamical system to about 100 steps\n", " _, s_vals, _ = np.linalg.svd(A_0)\n", " A = A_0 / (1.01 * s_vals)\n", "\n", " # _, s_val_test, _ = np.linalg.svd(A)\n", " # assert s_val_test < 1, \"largest singular value >= 1\"\n", "\n", " return A\n", "\n", "## Initializes the system\n", "n_neurons = 6\n", "A = create_connectivity(n_neurons) # we are invoking a helper function that generates our nxn causal connectivity matrix.\n", "\n", "# Let's plot it!\n", "plot_connectivity_graph_matrix(A)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Coding Exercise 2: System simulation\n", "\n", "In this exercise we're going to simulate the system. Please complete the following function so that at every timestep the activity vector $x$ is updated according to:\n", "\n", "\\begin{equation}\n", "\\vec{x}_{t+1} = \\sigma(A\\vec{x}_t + \\epsilon_t).\n", "\\end{equation}\n", "\n", "We will use helper function sigmoid, defined in the cell below." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Execute to enable helper function sigmoid\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @markdown Execute to enable helper function sigmoid\n", "\n", "def sigmoid(x):\n", " \"\"\"\n", " Compute sigmoid nonlinearity element-wise on x\n", "\n", " Args:\n", " x (np.ndarray): the numpy data array we want to transform\n", " Returns\n", " (np.ndarray): x with sigmoid nonlinearity applied\n", " \"\"\"\n", " return 1 / (1 + np.exp(-x))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "def simulate_neurons(A, timesteps, random_state=42):\n", " \"\"\"Simulates a dynamical system for the specified number of neurons and timesteps.\n", "\n", " Args:\n", " A (np.array): the connectivity matrix\n", " timesteps (int): the number of timesteps to simulate our system.\n", " random_state (int): random seed for reproducibility\n", "\n", " Returns:\n", " - X has shape (n_neurons, timeteps). A schematic:\n", " ___t____t+1___\n", " neuron 0 | 0 1 |\n", " | 1 0 |\n", " neuron i | 0 -> 1 |\n", " | 0 0 |\n", " |___1____0_____|\n", "\n", " \"\"\"\n", " np.random.seed(random_state)\n", "\n", " n_neurons = len(A)\n", " X = np.zeros((n_neurons, timesteps))\n", "\n", " for t in range(timesteps - 1):\n", "\n", " # Create noise vector\n", " epsilon = np.random.multivariate_normal(np.zeros(n_neurons), np.eye(n_neurons))\n", "\n", " ########################################################################\n", " ## TODO: Fill in the update rule for our dynamical system.\n", " ## Fill in function and remove\n", " raise NotImplementedError(\"Complete simulate_neurons\")\n", " ########################################################################\n", "\n", " # Update activity vector for next step\n", " X[:, t + 1] = sigmoid(...) # we are using helper function sigmoid\n", "\n", " return X\n", "\n", "\n", "# Set simulation length\n", "timesteps = 5000\n", "\n", "# Simulate our dynamical system\n", "X = simulate_neurons(A, timesteps)\n", "\n", "# Visualize\n", "plot_neural_activity(X)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main//tutorials/W3D5_NetworkCausality/solutions/W3D5_Tutorial1_Solution_ac49c6ab.py)\n", "\n", "*Example output:*\n", "\n", " \n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Section 3: Recovering connectivity through perturbation\n", "\n", "*Estimated timing to here from start of tutorial: 22 min*\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Video 3: Perturbing systems\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 3: Perturbing systems\n", "from ipywidgets import widgets\n", "\n", "out2 = widgets.Output()\n", "with out2:\n", " from IPython.display import IFrame\n", " class BiliVideo(IFrame):\n", " def __init__(self, id, page=1, width=400, height=300, **kwargs):\n", " self.id=id\n", " src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)\n", " super(BiliVideo, self).__init__(src, width, height, **kwargs)\n", "\n", " video = BiliVideo(id=\"BV1Hv411q7Ka\", width=730, height=410, fs=1)\n", " print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))\n", " display(video)\n", "\n", "out1 = widgets.Output()\n", "with out1:\n", " from IPython.display import YouTubeVideo\n", " video = YouTubeVideo(id=\"wOZunGtuqQE\", width=730, height=410, fs=1, rel=0)\n", " print('Video available at https://youtube.com/watch?v=' + video.id)\n", " display(video)\n", "\n", "out = widgets.Tab([out1, out2])\n", "out.set_title(0, 'Youtube')\n", "out.set_title(1, 'Bilibili')\n", "\n", "display(out)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Section 3.1: Random perturbation in our system of neurons\n", "\n", "We want to get the causal effect of each neuron upon each other neuron. The ground truth of the causal effects is the connectivity matrix $A$.\n", "\n", "Remember that we would like to calculate:\n", "\n", "\\begin{equation}\n", "\\delta_{A\\to B} = \\mathbb{E}[B | A=1] - \\mathbb{E}[B | A=0] \n", "\\end{equation}\n", "\n", "\n", "We'll do this by randomly setting the system state to 0 or 1 and observing the outcome after one timestep. If we do this $N$ times, the effect of neuron $i$ upon neuron $j$ is:\n", "\n", "\\begin{equation}\n", "\\delta_{x^i\\to x^j} \\approx \\frac1N \\sum_{\\substack{t=0, \\ t \\text{ even}}}^N[x_{t+1}^j | x^i_t=1] - \\frac1N \\sum_{\\substack{t=0, \\ t \\text{ even}}}^N[x_{t+1}^j | x^i_t=0]\n", "\\end{equation}\n", "\n", "This is just the average difference of the activity of neuron $j$ in the two conditions.\n", "\n", "We are going to calculate the above equation, but imagine it like *intervening* in activity every other timestep." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "We will use helper function simulate_neurons_perturb. While the rest of the function is the same as the simulate_neurons function in the previous exercise, every time step we now additionally include:\n", "\n", "if t % 2 == 0:\n", " X[:,t] = np.random.choice([0,1], size=n_neurons)\n", "\n", "\n", "This means that at every other timestep, every neuron's activity is changed to either 0 or 1. \n", "\n", "Pretty serious perturbation, huh? You don't want that going on in your brain.\n", "\n", "**Now visually compare the dynamics:** Run this next cell and see if you can spot how the dynamics have changed." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Execute this cell to visualize perturbed dynamics\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @markdown Execute this cell to visualize perturbed dynamics\n", "def simulate_neurons_perturb(A, timesteps):\n", " \"\"\"\n", " Simulates a dynamical system for the specified number of neurons and timesteps,\n", " BUT every other timestep the activity is clamped to a random pattern of 1s and 0s\n", "\n", " Args:\n", " A (np.array): the true connectivity matrix\n", " timesteps (int): the number of timesteps to simulate our system.\n", "\n", " Returns:\n", " The results of the simulated system.\n", " - X has shape (n_neurons, timeteps)\n", " \"\"\"\n", " n_neurons = len(A)\n", " X = np.zeros((n_neurons, timesteps))\n", "\n", " for t in range(timesteps - 1):\n", "\n", " if t % 2 == 0:\n", " X[:, t] = np.random.choice([0, 1], size=n_neurons)\n", "\n", " epsilon = np.random.multivariate_normal(np.zeros(n_neurons), np.eye(n_neurons))\n", " X[:, t + 1] = sigmoid(A.dot(X[:, t]) + epsilon) # we are using helper function sigmoid\n", "\n", " return X\n", "\n", "timesteps = 5000 # Simulate for 5000 timesteps.\n", "\n", "# Simulate our dynamical system for the given amount of time\n", "X_perturbed = simulate_neurons_perturb(A, timesteps)\n", "\n", "# Plot our standard versus perturbed dynamics\n", "fig, axs = plt.subplots(1, 2, figsize=(15, 4))\n", "im0 = axs.imshow(X[:, :10])\n", "im1 = axs.imshow(X_perturbed[:, :10])\n", "\n", "# Matplotlib boilerplate code\n", "divider = make_axes_locatable(axs)\n", "cax0 = divider.append_axes(\"right\", size=\"5%\", pad=0.15)\n", "plt.colorbar(im0, cax=cax0)\n", "\n", "divider = make_axes_locatable(axs)\n", "cax1 = divider.append_axes(\"right\", size=\"5%\", pad=0.15)\n", "plt.colorbar(im1, cax=cax1)\n", "\n", "axs.set_ylabel(\"Neuron\", fontsize=15)\n", "axs.set_xlabel(\"Timestep\", fontsize=15)\n", "axs.set_xlabel(\"Timestep\", fontsize=15);\n", "axs.set_title(\"Standard dynamics\", fontsize=15)\n", "axs.set_title(\"Perturbed dynamics\", fontsize=15);" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Section 3.2: Recovering connectivity from perturbed dynamics" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Video 4: Calculating causality\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 4: Calculating causality\n", "from ipywidgets import widgets\n", "\n", "out2 = widgets.Output()\n", "with out2:\n", " from IPython.display import IFrame\n", " class BiliVideo(IFrame):\n", " def __init__(self, id, page=1, width=400, height=300, **kwargs):\n", " self.id=id\n", " src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)\n", " super(BiliVideo, self).__init__(src, width, height, **kwargs)\n", "\n", " video = BiliVideo(id=\"BV15A411v7JS\", width=730, height=410, fs=1)\n", " print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))\n", " display(video)\n", "\n", "out1 = widgets.Output()\n", "with out1:\n", " from IPython.display import YouTubeVideo\n", " video = YouTubeVideo(id=\"EDZtcsIAVGM\", width=730, height=410, fs=1, rel=0)\n", " print('Video available at https://youtube.com/watch?v=' + video.id)\n", " display(video)\n", "\n", "out = widgets.Tab([out1, out2])\n", "out.set_title(0, 'Youtube')\n", "out.set_title(1, 'Bilibili')\n", "\n", "display(out)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "### Coding Exercise 3.2: Using perturbed dynamics to recover connectivity \n", "\n", "From the above perturbed dynamics, write a function that recovers the causal effect of a given single neuron (selected_neuron) upon all other neurons in the system. Remember from above you're calculating:\n", "\n", "\\begin{equation}\n", "\\delta_{x^i\\to x^j} \\approx \\frac1N \\sum_{\\substack{t=0, \\ t \\text{ even}}}^N[x_{t+1}^j | x^i_t=1] - \\frac1N \\sum_{\\substack{t=0, \\ t \\text{ even}}}^N[x_{t+1}^j | x^i_t=0]\n", "\\end{equation}\n", "\n", "\n", "Recall that we perturbed every neuron at every other timestep. Despite perturbing every neuron, in this exercise we are concentrating on computing the causal effect of a single neuron (we will look at all neurons effects on all neurons next). We want to exclusively use the timesteps without perturbation for $x^j_{t+1}$ and the timesteps with perturbation for $x^j_{t}$ in the formulas above. In numpy, indexing occurs as array[ start_index : end_index : count_by]. So getting every other element in an array (such as every other timestep) is as easy as array[::2]." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "def get_perturbed_connectivity_from_single_neuron(perturbed_X, selected_neuron):\n", " \"\"\"\n", " Computes the connectivity matrix from the selected neuron using differences in means.\n", "\n", " Args:\n", " perturbed_X (np.ndarray): the perturbed dynamical system matrix of shape (n_neurons, timesteps)\n", " selected_neuron (int): the index of the neuron we want to estimate connectivity for\n", "\n", " Returns:\n", " estimated_connectivity (np.ndarray): estimated connectivity for the selected neuron, of shape (n_neurons,)\n", " \"\"\"\n", " # Extract the perturbations of neuron 1 (every other timestep)\n", " neuron_perturbations = perturbed_X[selected_neuron, ::2]\n", "\n", " # Extract the observed outcomes of all the neurons (every other timestep)\n", " all_neuron_output = perturbed_X[:, 1::2]\n", "\n", " # Initialize estimated connectivity matrix\n", " estimated_connectivity = np.zeros(n_neurons)\n", "\n", " # Loop over neurons\n", " for neuron_idx in range(n_neurons):\n", "\n", " # Get this output neurons (neuron_idx) activity\n", " this_neuron_output = all_neuron_output[neuron_idx, :]\n", "\n", " # Get timesteps where the selected neuron == 0 vs == 1\n", " one_idx = np.argwhere(neuron_perturbations == 1)\n", " zero_idx = np.argwhere(neuron_perturbations == 0)\n", "\n", " ########################################################################\n", " ## TODO: Insert your code here to compute the neuron activation from perturbations.\n", " # Fill out function and remove\n", " raise NotImplementedError(\"Complete the function get_perturbed_connectivity_single_neuron\")\n", " ########################################################################\n", "\n", " difference_in_means = ...\n", "\n", " estimated_connectivity[neuron_idx] = difference_in_means\n", "\n", " return estimated_connectivity\n", "\n", "\n", "# Initialize the system\n", "n_neurons = 6\n", "timesteps = 5000\n", "selected_neuron = 1\n", "\n", "# Simulate our perturbed dynamical system\n", "perturbed_X = simulate_neurons_perturb(A, timesteps)\n", "\n", "\n", "## Uncomment below to test your function\n", "\n", "# Measure connectivity of neuron 1\n", "# estimated_connectivity = get_perturbed_connectivity_from_single_neuron(perturbed_X, selected_neuron)\n", "\n", "# plot_true_vs_estimated_connectivity(estimated_connectivity, A, selected_neuron)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/main//tutorials/W3D5_NetworkCausality/solutions/W3D5_Tutorial1_Solution_b51df5f6.py)\n", "\n", "*Example output:*\n", "\n", " \n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "We can quantify how close our estimated connectivity matrix is to our true connectivity matrix by correlating them. We should see almost perfect correlation between our estimates and the true connectivity - do we?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# Correlate true vs estimated connectivity matrix\n", "np.corrcoef(A[:, selected_neuron], estimated_connectivity)[1, 0]" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "**Note on interpreting A**: Strictly speaking, $A$ is not the matrix of causal effects but rather the dynamics matrix. So why compare them like this? The answer is that $A$ and the effect matrix both are $0$ everywhere except where there is a directed connection. So they should have a correlation of $1$ if we estimate the effects correctly. (Their scales, however, are different. This is in part because the nonlinearity $\\sigma$ squashes the values of $x$ to $[0,1]$.) See the Appendix after Tutorial 2 for more discussion of using correlation as a metric." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "\n", "Nice job! You just estimated connectivity for a single neuron.\n", "\n", "We're now going to use the same strategy for all neurons at once. We provide this helper function get_perturbed_connectivity_all_neurons. If you're curious about how this works and have extra time, check out Bonus Section 1.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Execute to get helper function get_perturbed_connectivity_all_neurons\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @markdown Execute to get helper function get_perturbed_connectivity_all_neurons\n", "\n", "def get_perturbed_connectivity_all_neurons(perturbed_X):\n", " \"\"\"\n", " Estimates the connectivity matrix of perturbations through stacked correlations.\n", "\n", " Args:\n", " perturbed_X (np.ndarray): the simulated dynamical system X of shape\n", " (n_neurons, timesteps)\n", "\n", " Returns:\n", " R (np.ndarray): the estimated connectivity matrix of shape\n", " (n_neurons, n_neurons)\n", " \"\"\"\n", " # select perturbations (P) and outcomes (Outs)\n", " # we perturb the system every over time step, hence the 2 in slice notation\n", " P = perturbed_X[:, ::2]\n", " Outs = perturbed_X[:, 1::2]\n", "\n", " # stack perturbations and outcomes into a 2n by (timesteps / 2) matrix\n", " S = np.concatenate([P, Outs], axis=0)\n", "\n", " # select the perturbation -> outcome block of correlation matrix (upper right)\n", " R = np.corrcoef(S)[:n_neurons, n_neurons:]\n", "\n", " return R\n", "\n", "\n", "# Parameters\n", "n_neurons = 6\n", "timesteps = 5000\n", "\n", "# Generate nxn causal connectivity matrix\n", "A = create_connectivity(n_neurons)\n", "\n", "# Simulate perturbed dynamical system\n", "perturbed_X = simulate_neurons_perturb(A, timesteps)\n", "\n", "# Get estimated connectivity matrix\n", "R = get_perturbed_connectivity_all_neurons(perturbed_X)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Execute this cell to visualize true vs estimated connectivity\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "#@markdown Execute this cell to visualize true vs estimated connectivity\n", "\n", "# Let's visualize the true connectivity and estimated connectivity together\n", "fig, axs = plt.subplots(1, 2, figsize=(10, 5))\n", "see_neurons(A, axs) # we are invoking a helper function that visualizes the connectivity matrix\n", "plot_connectivity_matrix(A, ax=axs)\n", "plt.suptitle(\"True connectivity matrix A\");\n", "plt.show()\n", "fig, axs = plt.subplots(1,2, figsize=(10,5))\n", "see_neurons(R.T,axs) # we are invoking a helper function that visualizes the connectivity matrix\n", "plot_connectivity_matrix(R.T, ax=axs)\n", "plt.suptitle(\"Estimated connectivity matrix R\");" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "We can again calculate the correlation coefficient between the elements of the two matrices. In the cell below, we compute the correlation coefficient between true and estimated connectivity matrices. It is almost 1 so we do a good job recovering the true causality of the system!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "np.corrcoef(A.transpose().flatten(), R.flatten())[1, 0]" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Summary\n", "\n", "*Estimated timing of tutorial: 40 min*\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Video 5: Summary\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 5: Summary\n", "from ipywidgets import widgets\n", "\n", "out2 = widgets.Output()\n", "with out2:\n", " from IPython.display import IFrame\n", " class BiliVideo(IFrame):\n", " def __init__(self, id, page=1, width=400, height=300, **kwargs):\n", " self.id=id\n", " src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)\n", " super(BiliVideo, self).__init__(src, width, height, **kwargs)\n", "\n", " video = BiliVideo(id=\"BV1FT4y1j7PW\", width=730, height=410, fs=1)\n", " print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))\n", " display(video)\n", "\n", "out1 = widgets.Output()\n", "with out1:\n", " from IPython.display import YouTubeVideo\n", " video = YouTubeVideo(id=\"p3fZW5Woqa4\", width=730, height=410, fs=1, rel=0)\n", " print('Video available at https://youtube.com/watch?v=' + video.id)\n", " display(video)\n", "\n", "out = widgets.Tab([out1, out2])\n", "out.set_title(0, 'Youtube')\n", "out.set_title(1, 'Bilibili')\n", "\n", "display(out)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "In this tutorial, we learned about how to define and estimate causality using perturbations. In particular we:\n", "\n", "1) Learned how to simulate a system of connected neurons\n", "\n", "2) Learned how to estimate the connectivity between neurons by directly perturbing neural activity" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "\n", "If you are interested in causality after NMA ends, here are some useful texts to consult.\n", "\n", "\n", "* *Causal Inference for Statistics, Social, and Biomedical Sciences* by Imbens and Rubin\n", "* *Causal Inference: What If* by Hernan and Rubin\n", "* *Mostly Harmless Econometrics* by Angrist and Pischke\n", "* https://www.nature.com/articles/s41562-018-0466-5 for application to neuroscience\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Bonus\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "## Bonus Section 1: Computation of the estimated connectivity matrix\n", "\n", "**This is an explanation of what the code is doing in get_perturbed_connectivity_all_neurons()**\n", "\n", "First, we compute an estimated connectivity matrix $R$. We extract\n", "perturbation matrix $P$ and outcomes matrix $O$:\n", "\n", "\\begin{equation}\n", "P = \\begin{bmatrix}\n", "\\mid & \\mid & ... & \\mid \\\\ \n", "x_0 & x_2 & ... & x_T \\\\ \n", "\\mid & \\mid & ... & \\mid\n", "\\end{bmatrix}_{n \\times T/2}\n", "\\end{equation}\n", "\n", "\\begin{equation}\n", "O = \\begin{bmatrix}\n", "\\mid & \\mid & ... & \\mid \\\\ \n", "x_1 & x_3 & ... & x_{T-1} \\\\ \n", "\\mid & \\mid & ... & \\mid\n", "\\end{bmatrix}_{n \\times T/2}\n", "\\end{equation}\n", "\n", "And calculate the correlation of matrix $S$, which is $P$ and $O$ stacked on each other:\n", "\n", "\\begin{equation}\n", "S = \\begin{bmatrix}\n", "P \\\\ \n", "O\n", "\\end{bmatrix}_{2n \\times T/2}\n", "\\end{equation}\n", "\n", "We then extract $R$ as the upper right $n \\times n$ block of $\\text{corr}(S)$:\n", "\n", "\n", "This is because the upper right block corresponds to the estimated perturbation effect on outcomes for each pair of neurons in our system.\n", "\n", "This method gives an estimated connectivity matrix that is the proportional to the result you would obtain with differences in means, and differs only in a proportionality constant that depends on the variance of $x$." ] } ], "metadata": { "colab": { "collapsed_sections": [], "include_colab_link": true, "name": "W3D5_Tutorial1", "provenance": [], "toc_visible": true }, "kernel": { "display_name": "Python 3", "language": "python", "name": "python3" }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.13" } }, "nbformat": 4, "nbformat_minor": 0 }