{ "cells": [ { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {}, "id": "view-in-github" }, "source": [ "\"Open   \"Open" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Tutorial 1: Interventions\n", "\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", "**Pproduction editors:** Gagana B, Spiros Chavlis" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Tutorial Objectives\n", "\n", "*Estimated timing of tutorial: 40 min*\n", "\n", "We list our overall day objectives below, with the sections we will focus on in this notebook in bold:\n", "\n", "1. **Master definitions of causality**\n", "2. **Understand that estimating causality is possible**\n", "3. **Learn 4 different methods and understand when they fail**\n", " * **Perturbations**\n", " * Correlations\n", " * Simultaneous fitting/regression\n", " * Instrumental variables\n", "\n", "**Tutorial setting**\n", "\n", "How do we know if a relationship is causal? What does that mean? And how can we estimate causal relationships within neural data?\n", "\n", "The methods we'll learn today are very general and can be applied to all sorts of data, and in many circumstances.\n", "Causal questions are everywhere!\n", "\n", "**Tutorial 1 Objectives:**\n", "\n", "1. Simulate a neural system\n", "2. Understand perturbation as a method of estimating causality" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Tutorial slides\n", "# @markdown These are the slides for the videos in all tutorials today\n", "from IPython.display import IFrame\n", "link_id = \"gp4m9\"\n", "print(f\"If you want to download the slides: https://osf.io/download/{link_id}/\")\n", "IFrame(src=f\"https://mfr.ca-1.osf.io/render?url=https://osf.io/{link_id}/?direct%26mode=render%26action=download%26mode=render\", width=854, height=480)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Setup" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Install and import feedback gadget\n", "\n", "!pip3 install vibecheck datatops --quiet\n", "\n", "from vibecheck import DatatopsContentReviewContainer\n", "def content_review(notebook_section: str):\n", " return DatatopsContentReviewContainer(\n", " \"\", # No text prompt\n", " notebook_section,\n", " {\n", " \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n", " \"name\": \"neuromatch_cn\",\n", " \"user_key\": \"y1x3mpx5\",\n", " },\n", " ).render()\n", "\n", "\n", "feedback_prefix = \"W3D5_T1\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "code", "execution": {} }, "outputs": [], "source": [ "# Imports\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from mpl_toolkits.axes_grid1 import make_axes_locatable" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Figure Settings\n", "import logging\n", "logging.getLogger('matplotlib.font_manager').disabled = True\n", "\n", "%config InlineBackend.figure_format = 'retina'\n", "plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/course-content/main/nma.mplstyle\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Plotting Functions\n", "\n", "def see_neurons(A, ax, show=False):\n", " \"\"\"\n", " Visualizes the connectivity matrix.\n", "\n", " Args:\n", " A (np.ndarray): the connectivity matrix of shape (n_neurons, n_neurons)\n", " ax (plt.axis): the matplotlib axis to display on\n", "\n", " Returns:\n", " Nothing, but visualizes A.\n", " \"\"\"\n", " A = A.T # make up for opposite connectivity\n", " n = len(A)\n", " ax.set_aspect('equal')\n", " thetas = np.linspace(0, np.pi * 2, n, endpoint=False)\n", " x, y = np.cos(thetas), np.sin(thetas),\n", " ax.scatter(x, y, c='k', s=150)\n", "\n", " # Renormalize\n", " A = A / A.max()\n", " for i in range(n):\n", " for j in range(n):\n", " if A[i, j] > 0:\n", " ax.arrow(x[i], y[i], x[j] - x[i], y[j] - y[i], color='k', alpha=A[i, j],\n", " head_width=.15, width = A[i, j] / 25, shape='right',\n", " length_includes_head=True)\n", " ax.axis('off')\n", " if show:\n", " plt.show()\n", "\n", "\n", "def plot_connectivity_matrix(A, ax=None, show=False):\n", " \"\"\"Plot the (weighted) connectivity matrix A as a heatmap\n", "\n", " Args:\n", " A (ndarray): connectivity matrix (n_neurons by n_neurons)\n", " ax: axis on which to display connectivity matrix\n", " \"\"\"\n", " if ax is None:\n", " ax = plt.gca()\n", " lim = np.abs(A).max()\n", " im = ax.imshow(A, vmin=-lim, vmax=lim, cmap=\"coolwarm\")\n", " ax.tick_params(labelsize=10)\n", " ax.xaxis.label.set_size(15)\n", " ax.yaxis.label.set_size(15)\n", " cbar = ax.figure.colorbar(im, ax=ax, ticks=[0], shrink=.7)\n", " cbar.ax.set_ylabel(\"Connectivity Strength\", rotation=90,\n", " labelpad= 20,va=\"bottom\")\n", " ax.set(xlabel=\"Connectivity from\", ylabel=\"Connectivity to\")\n", " if show:\n", " plt.show()\n", "\n", "\n", "def plot_connectivity_graph_matrix(A):\n", " \"\"\"Plot both connectivity graph and matrix side by side\n", "\n", " Args:\n", " A (ndarray): connectivity matrix (n_neurons by n_neurons)\n", " \"\"\"\n", " fig, axs = plt.subplots(1, 2, figsize=(10, 5))\n", " see_neurons(A, axs[0]) # we are invoking a helper function that visualizes the connectivity matrix\n", " plot_connectivity_matrix(A)\n", "\n", " fig.suptitle(\"Neuron Connectivity\")\n", " plt.show()\n", "\n", "\n", "def plot_neural_activity(X):\n", " \"\"\"Plot first 10 timesteps of neural activity\n", "\n", " Args:\n", " X (ndarray): neural activity (n_neurons by timesteps)\n", "\n", " \"\"\"\n", " f, ax = plt.subplots()\n", " im = ax.imshow(X[:, :10])\n", " divider = make_axes_locatable(ax)\n", " cax1 = divider.append_axes(\"right\", size=\"5%\", pad=0.15)\n", " plt.colorbar(im, cax=cax1)\n", " ax.set(xlabel='Timestep', ylabel='Neuron', title='Simulated Neural Activity')\n", " plt.show()\n", "\n", "\n", "def plot_true_vs_estimated_connectivity(estimated_connectivity, true_connectivity, selected_neuron=None):\n", " \"\"\"Visualize true vs estimated connectivity matrices\n", "\n", " Args:\n", " estimated_connectivity (ndarray): estimated connectivity (n_neurons by n_neurons)\n", " true_connectivity (ndarray): ground-truth connectivity (n_neurons by n_neurons)\n", " selected_neuron (int or None): None if plotting all connectivity, otherwise connectivity\n", " from selected_neuron will be shown\n", "\n", " \"\"\"\n", "\n", " fig, axs = plt.subplots(1, 2, figsize=(10, 5))\n", "\n", " if selected_neuron is not None:\n", " plot_connectivity_matrix(np.expand_dims(estimated_connectivity, axis=1),\n", " ax=axs[0])\n", " plot_connectivity_matrix(true_connectivity[:, [selected_neuron]], ax=axs[1])\n", " axs[0].set_xticks([0])\n", " axs[1].set_xticks([0])\n", " axs[0].set_xticklabels([selected_neuron])\n", " axs[1].set_xticklabels([selected_neuron])\n", " else:\n", " plot_connectivity_matrix(estimated_connectivity, ax=axs[0])\n", " plot_connectivity_matrix(true_connectivity, ax=axs[1])\n", "\n", " axs[1].set(title=\"True connectivity\")\n", " axs[0].set(title=\"Estimated connectivity\")\n", " plt.show()" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Section 1: Defining and estimating causality\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Video 1: Defining causality\n", "from ipywidgets import widgets\n", "from IPython.display import YouTubeVideo\n", "from IPython.display import IFrame\n", "from IPython.display import display\n", "\n", "\n", "class PlayVideo(IFrame):\n", " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", " self.id = id\n", " if source == 'Bilibili':\n", " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", " elif source == 'Osf':\n", " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", "\n", "\n", "def display_videos(video_ids, W=400, H=300, fs=1):\n", " tab_contents = []\n", " for i, video_id in enumerate(video_ids):\n", " out = widgets.Output()\n", " with out:\n", " if video_ids[i][0] == 'Youtube':\n", " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", " height=H, fs=fs, rel=0)\n", " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", " else:\n", " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", " height=H, fs=fs, autoplay=False)\n", " if video_ids[i][0] == 'Bilibili':\n", " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", " elif video_ids[i][0] == 'Osf':\n", " print(f'Video available at https://osf.io/{video.id}')\n", " display(video)\n", " tab_contents.append(out)\n", " return tab_contents\n", "\n", "\n", "video_ids = [('Youtube', 'yiddT2sMbZM'), ('Bilibili', 'BV1ZD4y1m7GG')]\n", "tab_contents = display_videos(video_ids, W=854, H=480)\n", "tabs = widgets.Tab()\n", "tabs.children = tab_contents\n", "for i in range(len(tab_contents)):\n", " tabs.set_title(i, video_ids[i][0])\n", "display(tabs)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Defining_causality_Video\")" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "This video recaps the definition of causality and presents a simple case of 2 neurons.\n", "\n", "
\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", "
\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", "
\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", "\n", "\\begin{equation}\n", "\\delta_{A\\to B} = \\mathbb{E}[B | A=1] - \\mathbb{E}[B | A=0]\n", "\\end{equation}\n", "\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[0])\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": { "execution": {} }, "source": [ "You should get a difference in means of `0.9907195190159408` (so very close to 1)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "both", "execution": {} }, "outputs": [], "source": [ "# to_remove solution\n", "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[0])\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", "diff_in_means = neuron_B(A_1).mean() - neuron_B(A_0).mean()\n", "print(diff_in_means)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Randomized_controlled_trial_for_two_neurons_Exercise\")" ] }, { "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": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Video 2: Simulated neural system model\n", "from ipywidgets import widgets\n", "from IPython.display import YouTubeVideo\n", "from IPython.display import IFrame\n", "from IPython.display import display\n", "\n", "\n", "class PlayVideo(IFrame):\n", " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", " self.id = id\n", " if source == 'Bilibili':\n", " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", " elif source == 'Osf':\n", " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", "\n", "\n", "def display_videos(video_ids, W=400, H=300, fs=1):\n", " tab_contents = []\n", " for i, video_id in enumerate(video_ids):\n", " out = widgets.Output()\n", " with out:\n", " if video_ids[i][0] == 'Youtube':\n", " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", " height=H, fs=fs, rel=0)\n", " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", " else:\n", " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", " height=H, fs=fs, autoplay=False)\n", " if video_ids[i][0] == 'Bilibili':\n", " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", " elif video_ids[i][0] == 'Osf':\n", " print(f'Video available at https://osf.io/{video.id}')\n", " display(video)\n", " tab_contents.append(out)\n", " return tab_contents\n", "\n", "\n", "video_ids = [('Youtube', 'oPJz49dAuL8'), ('Bilibili', 'BV1Xt4y1Q7uC')]\n", "tab_contents = display_videos(video_ids, W=854, H=480)\n", "tabs = widgets.Tab()\n", "tabs.children = tab_contents\n", "for i in range(len(tab_contents)):\n", " tabs.set_title(i, video_ids[i][0])\n", "display(tabs)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Simulated_neural_system_model_Video\")" ] }, { "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": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "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[0])\n", "\n", " # _, s_val_test, _ = np.linalg.svd(A)\n", " # assert s_val_test[0] < 1, \"largest singular value >= 1\"\n", "\n", " return A\n", "\n", "\n", "# Initializes the system\n", "n_neurons = 6\n", "A = create_connectivity(n_neurons)\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": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "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", " 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": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# to_remove solution\n", "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", " 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", " # Update activity vector for next step\n", " X[:, t + 1] = sigmoid(A @ X[:, t] + epsilon) # 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", "with plt.xkcd():\n", " plot_neural_activity(X)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_System_simulation_Exercise\")" ] }, { "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": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Video 3: Perturbing systems\n", "from ipywidgets import widgets\n", "from IPython.display import YouTubeVideo\n", "from IPython.display import IFrame\n", "from IPython.display import display\n", "\n", "\n", "class PlayVideo(IFrame):\n", " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", " self.id = id\n", " if source == 'Bilibili':\n", " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", " elif source == 'Osf':\n", " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", "\n", "\n", "def display_videos(video_ids, W=400, H=300, fs=1):\n", " tab_contents = []\n", " for i, video_id in enumerate(video_ids):\n", " out = widgets.Output()\n", " with out:\n", " if video_ids[i][0] == 'Youtube':\n", " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", " height=H, fs=fs, rel=0)\n", " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", " else:\n", " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", " height=H, fs=fs, autoplay=False)\n", " if video_ids[i][0] == 'Bilibili':\n", " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", " elif video_ids[i][0] == 'Osf':\n", " print(f'Video available at https://osf.io/{video.id}')\n", " display(video)\n", " tab_contents.append(out)\n", " return tab_contents\n", "\n", "\n", "video_ids = [('Youtube', 'wOZunGtuqQE'), ('Bilibili', 'BV1Hv411q7Ka')]\n", "tab_contents = display_videos(video_ids, W=854, H=480)\n", "tabs = widgets.Tab()\n", "tabs.children = tab_contents\n", "for i in range(len(tab_contents)):\n", " tabs.set_title(i, video_ids[i][0])\n", "display(tabs)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Perturbing_systems_Video\")" ] }, { "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", "```python\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": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "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", " for t in range(timesteps - 1):\n", " if t % 2 == 0:\n", " X[:, t] = np.random.choice([0, 1], size=n_neurons)\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", "\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[0].imshow(X[:, :10])\n", "im1 = axs[1].imshow(X_perturbed[:, :10])\n", "\n", "# Matplotlib boilerplate code\n", "divider = make_axes_locatable(axs[0])\n", "cax0 = divider.append_axes(\"right\", size=\"5%\", pad=0.15)\n", "plt.colorbar(im0, cax=cax0)\n", "\n", "divider = make_axes_locatable(axs[1])\n", "cax1 = divider.append_axes(\"right\", size=\"5%\", pad=0.15)\n", "plt.colorbar(im1, cax=cax1)\n", "\n", "axs[0].set_ylabel(\"Neuron\", fontsize=15)\n", "axs[1].set_xlabel(\"Timestep\", fontsize=15)\n", "axs[0].set_xlabel(\"Timestep\", fontsize=15);\n", "axs[0].set_title(\"Standard dynamics\", fontsize=15)\n", "axs[1].set_title(\"Perturbed dynamics\", fontsize=15)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Section 3.2: Recovering connectivity from perturbed dynamics" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Video 4: Calculating causality\n", "from ipywidgets import widgets\n", "from IPython.display import YouTubeVideo\n", "from IPython.display import IFrame\n", "from IPython.display import display\n", "\n", "\n", "class PlayVideo(IFrame):\n", " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", " self.id = id\n", " if source == 'Bilibili':\n", " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", " elif source == 'Osf':\n", " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", "\n", "\n", "def display_videos(video_ids, W=400, H=300, fs=1):\n", " tab_contents = []\n", " for i, video_id in enumerate(video_ids):\n", " out = widgets.Output()\n", " with out:\n", " if video_ids[i][0] == 'Youtube':\n", " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", " height=H, fs=fs, rel=0)\n", " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", " else:\n", " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", " height=H, fs=fs, autoplay=False)\n", " if video_ids[i][0] == 'Bilibili':\n", " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", " elif video_ids[i][0] == 'Osf':\n", " print(f'Video available at https://osf.io/{video.id}')\n", " display(video)\n", " tab_contents.append(out)\n", " return tab_contents\n", "\n", "\n", "video_ids = [('Youtube', 'EDZtcsIAVGM'), ('Bilibili', 'BV15A411v7JS')]\n", "tab_contents = display_videos(video_ids, W=854, H=480)\n", "tabs = widgets.Tab()\n", "tabs.children = tab_contents\n", "for i in range(len(tab_contents)):\n", " tabs.set_title(i, video_ids[i][0])\n", "display(tabs)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Calculating_causality_Video\")" ] }, { "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", " (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,\n", " 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", "# Measure connectivity of neuron 1\n", "estimated_connectivity = get_perturbed_connectivity_from_single_neuron(perturbed_X, selected_neuron)\n", "plot_true_vs_estimated_connectivity(estimated_connectivity, A, selected_neuron)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# to_remove solution\n", "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", " (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,\n", " 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", " difference_in_means = np.mean(this_neuron_output[one_idx]) - np.mean(this_neuron_output[zero_idx])\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", "# Measure connectivity of neuron 1\n", "estimated_connectivity = get_perturbed_connectivity_from_single_neuron(perturbed_X, selected_neuron)\n", "with plt.xkcd():\n", " plot_true_vs_estimated_connectivity(estimated_connectivity, A, selected_neuron)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Perturbed_dynamics_to_recover_connectivity_Exercise\")" ] }, { "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", "print(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": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "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": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "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(2, 2, figsize=(10, 10))\n", "see_neurons(A, axs[0, 0]) # we are invoking a helper function that visualizes the connectivity matrix\n", "plot_connectivity_matrix(A, ax=axs[0, 1])\n", "axs[0, 1].set_title(\"True connectivity matrix A\")\n", "\n", "see_neurons(R.T, axs[1, 0]) # we are invoking a helper function that visualizes the connectivity matrix\n", "plot_connectivity_matrix(R.T, ax=axs[1, 1])\n", "axs[1, 1].set_title(\"Estimated connectivity matrix R\")\n", "\n", "fig.suptitle(\"True vs estimated connectivity\")\n", "plt.show()" ] }, { "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": [ "print(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": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Video 5: Summary\n", "from ipywidgets import widgets\n", "from IPython.display import YouTubeVideo\n", "from IPython.display import IFrame\n", "from IPython.display import display\n", "\n", "\n", "class PlayVideo(IFrame):\n", " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", " self.id = id\n", " if source == 'Bilibili':\n", " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", " elif source == 'Osf':\n", " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", "\n", "\n", "def display_videos(video_ids, W=400, H=300, fs=1):\n", " tab_contents = []\n", " for i, video_id in enumerate(video_ids):\n", " out = widgets.Output()\n", " with out:\n", " if video_ids[i][0] == 'Youtube':\n", " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", " height=H, fs=fs, rel=0)\n", " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", " else:\n", " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", " height=H, fs=fs, autoplay=False)\n", " if video_ids[i][0] == 'Bilibili':\n", " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", " elif video_ids[i][0] == 'Osf':\n", " print(f'Video available at https://osf.io/{video.id}')\n", " display(video)\n", " tab_contents.append(out)\n", " return tab_contents\n", "\n", "\n", "video_ids = [('Youtube', 'p3fZW5Woqa4'), ('Bilibili', 'BV1FT4y1j7PW')]\n", "tab_contents = display_videos(video_ids, W=854, H=480)\n", "tabs = widgets.Tab()\n", "tabs.children = tab_contents\n", "for i in range(len(tab_contents)):\n", " tabs.set_title(i, video_ids[i][0])\n", "display(tabs)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {} }, "outputs": [], "source": [ "# @title Submit your feedback\n", "content_review(f\"{feedback_prefix}_Summary_Video\")" ] }, { "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", "* *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", "* See [here](https://www.nature.com/articles/s41562-018-0466-5) for an application to neuroscience\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "---\n", "# Bonus\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## 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.9.17" } }, "nbformat": 4, "nbformat_minor": 0 }