Open In Colab   Open in Kaggle

チュートリアル 1: 介入

第3週 第5日目: ネットワーク因果性

Neuromatch Academyによる

コンテンツ作成者: Ari Benjamin, Tony Liu, Konrad Kording

コンテンツレビュアー: Mike X Cohen, Madineh Sarvestani, Ella Batty, Yoni Friedman, Michael Waskom

制作編集者: Gagana B, Spiros Chavlis


チュートリアルの目標

チュートリアルの推定所要時間: 40分

以下に本日の全体的な目標を示します。このノートブックで重点的に扱うセクションは太字にしています:

  1. 因果性の定義をマスターする
  2. 因果性の推定が可能であることを理解する
  3. 4つの異なる方法を学び、それらが失敗する場合を理解する

チュートリアルの設定

ある関係が因果的であるかどうかはどうやってわかるのでしょうか?それは何を意味するのでしょうか?そして神経データ内の因果関係をどのように推定できるのでしょうか?

今日学ぶ方法は非常に一般的で、あらゆる種類のデータや多くの状況に適用可能です。
因果的な問いはどこにでもあります!

チュートリアル1の目標:

  1. 神経系のシミュレーション
  2. 因果性推定の方法としての摂動を理解する
# @title Tutorial slides
# @markdown These are the slides for the videos in all tutorials today
from IPython.display import IFrame
link_id = "gp4m9"
print(f"If you want to download the slides: https://osf.io/download/{link_id}/")
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)

セットアップ

# @title Install and import feedback gadget


from vibecheck import DatatopsContentReviewContainer
def content_review(notebook_section: str):
    return DatatopsContentReviewContainer(
        "",  # No text prompt
        notebook_section,
        {
            "url": "https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab",
            "name": "neuromatch_cn",
            "user_key": "y1x3mpx5",
        },
    ).render()


feedback_prefix = "W3D5_T1"
# Imports
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
# @title Figure Settings
import logging
logging.getLogger('matplotlib.font_manager').disabled = True

%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/main/nma.mplstyle")
# @title Plotting Functions

def see_neurons(A, ax, show=False):
  """
  Visualizes the connectivity matrix.

  Args:
    A (np.ndarray): the connectivity matrix of shape (n_neurons, n_neurons)
    ax (plt.axis): the matplotlib axis to display on

  Returns:
    Nothing, but visualizes A.
  """
  A = A.T  # make up for opposite connectivity
  n = len(A)
  ax.set_aspect('equal')
  thetas = np.linspace(0, np.pi * 2, n, endpoint=False)
  x, y = np.cos(thetas), np.sin(thetas),
  ax.scatter(x, y, c='k', s=150)

  # Renormalize
  A = A / A.max()
  for i in range(n):
    for j in range(n):
      if A[i, j] > 0:
        ax.arrow(x[i], y[i], x[j] - x[i], y[j] - y[i], color='k', alpha=A[i, j],
                 head_width=.15, width = A[i, j] / 25, shape='right',
                 length_includes_head=True)
  ax.axis('off')
  if show:
    plt.show()


def plot_connectivity_matrix(A, ax=None, show=False):
  """Plot the (weighted) connectivity matrix A as a heatmap

    Args:
      A (ndarray): connectivity matrix (n_neurons by n_neurons)
      ax: axis on which to display connectivity matrix
  """
  if ax is None:
    ax = plt.gca()
  lim = np.abs(A).max()
  im = ax.imshow(A, vmin=-lim, vmax=lim, cmap="coolwarm")
  ax.tick_params(labelsize=10)
  ax.xaxis.label.set_size(15)
  ax.yaxis.label.set_size(15)
  cbar = ax.figure.colorbar(im, ax=ax, ticks=[0], shrink=.7)
  cbar.ax.set_ylabel("Connectivity Strength", rotation=90,
                     labelpad= 20,va="bottom")
  ax.set(xlabel="Connectivity from", ylabel="Connectivity to")
  if show:
    plt.show()


def plot_connectivity_graph_matrix(A):
  """Plot both connectivity graph and matrix side by side

    Args:
      A (ndarray): connectivity matrix (n_neurons by n_neurons)
  """
  fig, axs = plt.subplots(1, 2, figsize=(10, 5))
  see_neurons(A, axs[0])  # we are invoking a helper function that visualizes the connectivity matrix
  plot_connectivity_matrix(A)

  fig.suptitle("Neuron Connectivity")
  plt.show()


def plot_neural_activity(X):
  """Plot first 10 timesteps of neural activity

  Args:
    X (ndarray): neural activity (n_neurons by timesteps)

  """
  f, ax = plt.subplots()
  im = ax.imshow(X[:, :10])
  divider = make_axes_locatable(ax)
  cax1 = divider.append_axes("right", size="5%", pad=0.15)
  plt.colorbar(im, cax=cax1)
  ax.set(xlabel='Timestep', ylabel='Neuron', title='Simulated Neural Activity')
  plt.show()


def plot_true_vs_estimated_connectivity(estimated_connectivity, true_connectivity, selected_neuron=None):
  """Visualize true vs estimated connectivity matrices

  Args:
    estimated_connectivity (ndarray): estimated connectivity (n_neurons by n_neurons)
    true_connectivity (ndarray): ground-truth connectivity (n_neurons by n_neurons)
    selected_neuron (int or None): None if plotting all connectivity, otherwise connectivity
      from selected_neuron will be shown

  """

  fig, axs = plt.subplots(1, 2, figsize=(10, 5))

  if selected_neuron is not None:
    plot_connectivity_matrix(np.expand_dims(estimated_connectivity, axis=1),
                             ax=axs[0])
    plot_connectivity_matrix(true_connectivity[:, [selected_neuron]], ax=axs[1])
    axs[0].set_xticks([0])
    axs[1].set_xticks([0])
    axs[0].set_xticklabels([selected_neuron])
    axs[1].set_xticklabels([selected_neuron])
  else:
    plot_connectivity_matrix(estimated_connectivity, ax=axs[0])
    plot_connectivity_matrix(true_connectivity, ax=axs[1])

  axs[1].set(title="True connectivity")
  axs[0].set(title="Estimated connectivity")
  plt.show()

セクション1: 因果性の定義と推定

# @title Video 1: Defining causality
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display


class PlayVideo(IFrame):
  def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
    self.id = id
    if source == 'Bilibili':
      src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
    elif source == 'Osf':
      src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
    super(PlayVideo, self).__init__(src, width, height, **kwargs)


def display_videos(video_ids, W=400, H=300, fs=1):
  tab_contents = []
  for i, video_id in enumerate(video_ids):
    out = widgets.Output()
    with out:
      if video_ids[i][0] == 'Youtube':
        video = YouTubeVideo(id=video_ids[i][1], width=W,
                             height=H, fs=fs, rel=0)
        print(f'Video available at https://youtube.com/watch?v={video.id}')
      else:
        video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
                          height=H, fs=fs, autoplay=False)
        if video_ids[i][0] == 'Bilibili':
          print(f'Video available at https://www.bilibili.com/video/{video.id}')
        elif video_ids[i][0] == 'Osf':
          print(f'Video available at https://osf.io/{video.id}')
      display(video)
    tab_contents.append(out)
  return tab_contents


video_ids = [('Youtube', 'yiddT2sMbZM'), ('Bilibili', 'BV1ZD4y1m7GG')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
  tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_Defining_causality_Video")

このビデオでは因果性の定義を復習し、2つのニューロンの単純なケースを示します。

ビデオのテキスト要約はこちらをクリック

AがBを引き起こす」という文を慎重に考えてみましょう。具体的に2つのニューロンを例にとります。ニューロンAAがニューロンBBの発火を引き起こすとはどういう意味でしょうか?

介入的な因果性の定義は以下のように言います:


(A が B を引き起こす)(もし A を強制的に変えれば B が変わる)(A \text{ が } B \text{ を引き起こす}) \Leftrightarrow ( \text{もし } A \text{ を強制的に変えれば } B \text{ が変わる})

AABBの発火を引き起こすかどうかを判断するには、ニューロンAAに電流を注入してBBに何が起こるかを観察します。

因果性の数学的定義:
多くの試行の平均として、ニューロンAAがニューロンBBに及ぼす平均因果効果δAB\delta_{A\to B}は、A=1A=1に設定した場合とA=0A=0に設定した場合のBBの活動の平均変化です。


δAB=E[BA=1]E[BA=0]\delta_{A\to B} = \mathbb{E}[B | A=1] - \mathbb{E}[B | A=0]

ここでE[BA=1]\mathbb{E}[B | A=1]AAが1のときのBBの期待値、E[BA=0]\mathbb{E}[B | A=0]AAが0のときのBBの期待値です。

これは平均効果であることに注意してください。条件付き効果(例えばAAが不応期でないときのみBBに影響するなど)をより詳細に扱うこともできますが、今日は平均効果のみを考えます。

ランダム化比較試験(RCT)との関係:
先ほど説明した論理はランダム化比較試験(RCT)の論理です。100人に薬をランダムに投与し、100人にプラセボを与えた場合、効果は結果の差になります。

コーディング演習1: 2つのニューロンのランダム化比較試験

2つのニューロンに対してランダム化比較試験を行えると仮定しましょう。モデルはニューロンAAがニューロンBBにシナプスを形成しているものとします:

B=A+ϵB = A + \epsilon

ここでAABBは2つのニューロンの活動を表し、ϵ\epsilonは標準正規分布のノイズϵN(0,1)\epsilon\sim\mathcal{N}(0,1)です。

目標はAAを摂動してBBが変化することを確認することです。

def neuron_B(activity_of_A):
  """Model activity of neuron B as neuron A activity + noise

  Args:
    activity_of_A (ndarray): An array of shape (T,) containing the neural activity of neuron A

  Returns:
    ndarray: activity of neuron B
  """
  noise = np.random.randn(activity_of_A.shape[0])

  return activity_of_A + noise

np.random.seed(12)

# Neuron A activity of zeros
A_0 = np.zeros(5000)

# Neuron A activity of ones
A_1 = np.ones(5000)

###########################################################################
## TODO for students: Estimate the causal effect of A upon B
## Use eq above (difference in mean of B when A=0 vs. A=1)
raise NotImplementedError('student exercise: please look at effects of A on B')
###########################################################################

diff_in_means = ...
print(diff_in_means)

平均の差は0.9907195190159408(ほぼ1に非常に近い)になるはずです。

解答を見る$

# @title Submit your feedback
content_review(f"{feedback_prefix}_Randomized_controlled_trial_for_two_neurons_Exercise")

セクション2: ニューロン系のシミュレーション

チュートリアル開始からここまでの推定所要時間: 9分

ニューロンが大規模ネットワークにいる場合でも因果効果を推定できるでしょうか?これが今日の主な問いです。まずシステムを作成し、残りはそれを解析することに費やします。

# @title Video 2: Simulated neural system model
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display


class PlayVideo(IFrame):
  def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
    self.id = id
    if source == 'Bilibili':
      src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
    elif source == 'Osf':
      src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
    super(PlayVideo, self).__init__(src, width, height, **kwargs)


def display_videos(video_ids, W=400, H=300, fs=1):
  tab_contents = []
  for i, video_id in enumerate(video_ids):
    out = widgets.Output()
    with out:
      if video_ids[i][0] == 'Youtube':
        video = YouTubeVideo(id=video_ids[i][1], width=W,
                             height=H, fs=fs, rel=0)
        print(f'Video available at https://youtube.com/watch?v={video.id}')
      else:
        video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
                          height=H, fs=fs, autoplay=False)
        if video_ids[i][0] == 'Bilibili':
          print(f'Video available at https://www.bilibili.com/video/{video.id}')
        elif video_ids[i][0] == 'Osf':
          print(f'Video available at https://osf.io/{video.id}')
      display(video)
    tab_contents.append(out)
  return tab_contents


video_ids = [('Youtube', 'oPJz49dAuL8'), ('Bilibili', 'BV1Xt4y1Q7uC')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
  tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_Simulated_neural_system_model_Video")

ビデオ訂正: このビデオおよび他のビデオの接続グラフのプロットと説明は接続の方向が逆になっています(矢印は逆方向を向くべきです)。これは以下の図で修正済みです。

このビデオでは、理解しやすい動的特性を持つ大規模な因果システム(相互作用するニューロン)とそのシミュレーション方法を紹介します。

ビデオのテキスト要約はこちらをクリック

私たちのシステムはN個の相互接続されたニューロンからなり、時間を通じて互いに影響しあいます。各ニューロンの時刻t+1t+1での状態は、前時刻ttの他のニューロンの活動の関数です。

ニューロンは非線形に互いに影響します:各ニューロンの時刻t+1t+1での活動は、時刻ttのすべてのニューロン活動の線形加重和にノイズを加え、非線形関数を通したものです:

xt+1=σ(Axt+ϵt),\vec{x}_{t+1} = \sigma(A\vec{x}_t + \epsilon_t),
  • xt\vec{x}_tは時刻ttでのnn次元ベクトルで、nn個のニューロンの状態を表します
  • σ\sigmaはシグモイド非線形関数
  • AAn×nn \times n因果的真の接続行列(後述)
  • ϵt\epsilon_tはランダムノイズで、ϵtN(0,In)\epsilon_t \sim N(\vec{0}, I_n)
  • x0\vec{x}_0はゼロベクトルで初期化されます

AAは接続行列で、要素AijA_{ij}はニューロンiiがニューロンjjに及ぼす因果効果を表します。システムでは、ニューロンは全体の約10%のニューロンから接続を受け取ります。

6つのニューロン間の真の接続行列を作成し、2つの異なる方法で可視化します:接続されたニューロン間の方向付きエッジを持つグラフとして、そして接続行列の画像として。

理解度チェック: 左のプロットが右のプロットにどう対応しているか理解できますか?

# @markdown Execute this cell to get helper function `create_connectivity` and visualize connectivity

def create_connectivity(n_neurons, random_state=42):
  """
  Generate our nxn causal connectivity matrix.

  Args:
    n_neurons (int): the number of neurons in our system.
    random_state (int): random seed for reproducibility

  Returns:
    A (np.ndarray): our 0.1 sparse connectivity matrix
  """
  np.random.seed(random_state)
  A_0 = np.random.choice([0, 1], size=(n_neurons, n_neurons), p=[0.9, 0.1])

  # set the timescale of the dynamical system to about 100 steps
  _, s_vals, _ = np.linalg.svd(A_0)
  A = A_0 / (1.01 * s_vals[0])

  # _, s_val_test, _ = np.linalg.svd(A)
  # assert s_val_test[0] < 1, "largest singular value >= 1"

  return A


# Initializes the system
n_neurons = 6
A = create_connectivity(n_neurons)

# Let's plot it!
plot_connectivity_graph_matrix(A)

コーディング演習2: システムのシミュレーション

この演習ではシステムをシミュレートします。以下の関数を完成させて、各時刻で活動ベクトルxxが以下のように更新されるようにしてください:

xt+1=σ(Axt+ϵt).\vec{x}_{t+1} = \sigma(A\vec{x}_t + \epsilon_t).

補助関数sigmoidは以下のセルで定義されています。

# @markdown Execute to enable helper function `sigmoid`

def sigmoid(x):
  """
  Compute sigmoid nonlinearity element-wise on x

  Args:
      x (np.ndarray): the numpy data array we want to transform
  Returns
      (np.ndarray): x with sigmoid nonlinearity applied
  """
  return 1 / (1 + np.exp(-x))
def simulate_neurons(A, timesteps, random_state=42):
  """Simulates a dynamical system for the specified number of neurons and timesteps.

  Args:
    A (np.array): the connectivity matrix
    timesteps (int): the number of timesteps to simulate our system.
    random_state (int): random seed for reproducibility

  Returns:
    - X has shape (n_neurons, timeteps). A schematic:
                ___t____t+1___
    neuron  0  |   0    1     |
              |   1    0     |
    neuron  i  |   0 -> 1     |
              |   0    0     |
              |___1____0_____|
  """
  np.random.seed(random_state)

  n_neurons = len(A)
  X = np.zeros((n_neurons, timesteps))

  for t in range(timesteps - 1):

    # Create noise vector
    epsilon = np.random.multivariate_normal(np.zeros(n_neurons), np.eye(n_neurons))

    ########################################################################
    ## TODO: Fill in the update rule for our dynamical system.
    ## Fill in function and remove
    raise NotImplementedError("Complete simulate_neurons")
    ########################################################################

    # Update activity vector for next step
    X[:, t + 1] = sigmoid(...)  # we are using helper function sigmoid

  return X


# Set simulation length
timesteps = 5000

# Simulate our dynamical system
X = simulate_neurons(A, timesteps)

# Visualize
plot_neural_activity(X)

解答を見る$

例の出力:

Solution hint
# @title Submit your feedback
content_review(f"{feedback_prefix}_System_simulation_Exercise")

セクション3: 摂動による接続性の回復

チュートリアル開始からここまでの推定所要時間: 22分

# @title Video 3: Perturbing systems
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display


class PlayVideo(IFrame):
  def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
    self.id = id
    if source == 'Bilibili':
      src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
    elif source == 'Osf':
      src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
    super(PlayVideo, self).__init__(src, width, height, **kwargs)


def display_videos(video_ids, W=400, H=300, fs=1):
  tab_contents = []
  for i, video_id in enumerate(video_ids):
    out = widgets.Output()
    with out:
      if video_ids[i][0] == 'Youtube':
        video = YouTubeVideo(id=video_ids[i][1], width=W,
                             height=H, fs=fs, rel=0)
        print(f'Video available at https://youtube.com/watch?v={video.id}')
      else:
        video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
                          height=H, fs=fs, autoplay=False)
        if video_ids[i][0] == 'Bilibili':
          print(f'Video available at https://www.bilibili.com/video/{video.id}')
        elif video_ids[i][0] == 'Osf':
          print(f'Video available at https://osf.io/{video.id}')
      display(video)
    tab_contents.append(out)
  return tab_contents


video_ids = [('Youtube', 'wOZunGtuqQE'), ('Bilibili', 'BV1Hv411q7Ka')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
  tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_Perturbing_systems_Video")

セクション3.1: ニューロン系におけるランダム摂動

各ニューロンが他のニューロンに及ぼす因果効果を得たいです。因果効果の真の値は接続行列AAです。

以下を計算したいことを思い出してください:

δAB=E[BA=1]E[BA=0]\delta_{A\to B} = \mathbb{E}[B | A=1] - \mathbb{E}[B | A=0]

システムの状態をランダムに0または1に設定し、1タイムステップ後の結果を観察することでこれを行います。これをNN回繰り返すと、ニューロンiiがニューロンjjに及ぼす効果は:

δxixj1Nt=0, t evenN[xt+1jxti=1]1Nt=0, t evenN[xt+1jxti=0]\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]

これは単にニューロンjjの活動の2つの条件における平均差です。

上記の式を計算しますが、交互のタイムステップで活動に介入していると想像してください。

補助関数simulate_neurons_perturbを使います。前の演習のsimulate_neurons関数とほぼ同じですが、各タイムステップで以下を追加しています:

if t % 2 == 0:
    X[:,t] = np.random.choice([0,1], size=n_neurons)

これは交互のタイムステップで全ニューロンの活動が0か1にランダムに変更されることを意味します。

かなり強い摂動ですね。脳内でこんなことが起きてほしくないでしょう。

動的変化を視覚的に比較してみましょう: 次のセルを実行して、動的がどう変わったか見てみてください。

# @markdown Execute this cell to visualize perturbed dynamics
def simulate_neurons_perturb(A, timesteps):
  """
  Simulates a dynamical system for the specified number of neurons and timesteps,
  BUT every other timestep the activity is clamped to a random pattern of 1s and 0s

  Args:
    A (np.array): the true connectivity matrix
    timesteps (int): the number of timesteps to simulate our system.

  Returns:
    The results of the simulated system.
    - X has shape (n_neurons, timeteps)
  """
  n_neurons = len(A)
  X = np.zeros((n_neurons, timesteps))
  for t in range(timesteps - 1):
    if t % 2 == 0:
      X[:, t] = np.random.choice([0, 1], size=n_neurons)
    epsilon = np.random.multivariate_normal(np.zeros(n_neurons), np.eye(n_neurons))
    X[:, t + 1] = sigmoid(A.dot(X[:, t]) + epsilon)  # we are using helper function sigmoid

  return X


timesteps = 5000  # Simulate for 5000 timesteps.

# Simulate our dynamical system for the given amount of time
X_perturbed = simulate_neurons_perturb(A, timesteps)

# Plot our standard versus perturbed dynamics
fig, axs = plt.subplots(1, 2, figsize=(15, 4))
im0 = axs[0].imshow(X[:, :10])
im1 = axs[1].imshow(X_perturbed[:, :10])

# Matplotlib boilerplate code
divider = make_axes_locatable(axs[0])
cax0 = divider.append_axes("right", size="5%", pad=0.15)
plt.colorbar(im0, cax=cax0)

divider = make_axes_locatable(axs[1])
cax1 = divider.append_axes("right", size="5%", pad=0.15)
plt.colorbar(im1, cax=cax1)

axs[0].set_ylabel("Neuron", fontsize=15)
axs[1].set_xlabel("Timestep", fontsize=15)
axs[0].set_xlabel("Timestep", fontsize=15);
axs[0].set_title("Standard dynamics", fontsize=15)
axs[1].set_title("Perturbed dynamics", fontsize=15)
plt.show()

セクション3.2: 摂動された動的から接続性を回復する

# @title Video 4: Calculating causality
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display


class PlayVideo(IFrame):
  def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
    self.id = id
    if source == 'Bilibili':
      src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
    elif source == 'Osf':
      src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
    super(PlayVideo, self).__init__(src, width, height, **kwargs)


def display_videos(video_ids, W=400, H=300, fs=1):
  tab_contents = []
  for i, video_id in enumerate(video_ids):
    out = widgets.Output()
    with out:
      if video_ids[i][0] == 'Youtube':
        video = YouTubeVideo(id=video_ids[i][1], width=W,
                             height=H, fs=fs, rel=0)
        print(f'Video available at https://youtube.com/watch?v={video.id}')
      else:
        video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
                          height=H, fs=fs, autoplay=False)
        if video_ids[i][0] == 'Bilibili':
          print(f'Video available at https://www.bilibili.com/video/{video.id}')
        elif video_ids[i][0] == 'Osf':
          print(f'Video available at https://osf.io/{video.id}')
      display(video)
    tab_contents.append(out)
  return tab_contents


video_ids = [('Youtube', 'EDZtcsIAVGM'), ('Bilibili', 'BV15A411v7JS')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
  tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_Calculating_causality_Video")

コーディング演習3.2: 摂動された動的から接続性を回復する

上記の摂動された動的から、システム内の単一のニューロン(selected_neuron)が他のすべてのニューロンに及ぼす因果効果を回復する関数を書いてください。上記の式を計算します:

δxixj1Nt=0, t evenN[xt+1jxti=1]1Nt=0, t evenN[xt+1jxti=0]\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]

交互のタイムステップで全ニューロンを摂動しましたが、この演習では単一のニューロンの因果効果の計算に集中します。xt+1jx^j_{t+1}には摂動していないタイムステップ、xtjx^j_tには摂動しているタイムステップを使います。numpyのインデックス指定はarray[開始:終了:間隔]で、例えば交互の要素を取得するにはarray[::2]とします。

def get_perturbed_connectivity_from_single_neuron(perturbed_X, selected_neuron):
  """
  Computes the connectivity matrix from the selected neuron using differences in means.

  Args:
    perturbed_X (np.ndarray): the perturbed dynamical system matrix of shape
                              (n_neurons, timesteps)
    selected_neuron (int): the index of the neuron we want to estimate connectivity for

  Returns:
      estimated_connectivity (np.ndarray): estimated connectivity for the selected neuron,
                                           of shape (n_neurons,)
  """
  # Extract the perturbations of neuron 1 (every other timestep)
  neuron_perturbations = perturbed_X[selected_neuron, ::2]

  # Extract the observed outcomes of all the neurons (every other timestep)
  all_neuron_output = perturbed_X[:, 1::2]

  # Initialize estimated connectivity matrix
  estimated_connectivity = np.zeros(n_neurons)

  # Loop over neurons
  for neuron_idx in range(n_neurons):

    # Get this output neurons (neuron_idx) activity
    this_neuron_output = all_neuron_output[neuron_idx, :]

    # Get timesteps where the selected neuron == 0 vs == 1
    one_idx = np.argwhere(neuron_perturbations == 1)
    zero_idx = np.argwhere(neuron_perturbations == 0)

    ########################################################################
    ## TODO: Insert your code here to compute the neuron activation from perturbations.
    # Fill out function and remove
    raise NotImplementedError("Complete the function get_perturbed_connectivity_single_neuron")
    ########################################################################

    difference_in_means = ...

    estimated_connectivity[neuron_idx] = difference_in_means

  return estimated_connectivity


# Initialize the system
n_neurons = 6
timesteps = 5000
selected_neuron = 1

# Simulate our perturbed dynamical system
perturbed_X = simulate_neurons_perturb(A, timesteps)

# Measure connectivity of neuron 1
estimated_connectivity = get_perturbed_connectivity_from_single_neuron(perturbed_X, selected_neuron)
plot_true_vs_estimated_connectivity(estimated_connectivity, A, selected_neuron)

解答を見る$

例の出力:

Solution hint
# @title Submit your feedback
content_review(f"{feedback_prefix}_Perturbed_dynamics_to_recover_connectivity_Exercise")

推定した接続行列と真の接続行列の類似度を相関係数で定量化できます。推定値と真の接続性の間にほぼ完全な相関が見られるはずです。そうなっていますか?

# Correlate true vs estimated connectivity matrix
print(np.corrcoef(A[:, selected_neuron], estimated_connectivity)[1, 0])

AAの解釈についての注意: 厳密にはAAは因果効果の行列ではなく動的行列です。ではなぜ比較するのか?AAと効果行列はどちらも有向接続がある場所以外は0であるため、効果を正しく推定できれば相関は1になるはずです。(ただしスケールは異なります。これは非線形関数σ\sigmaxxの値を[0,1][0,1]に圧縮するためです。)チュートリアル2の付録で相関を指標として使うことについてさらに議論しています。

よくできました!単一ニューロンの接続性を推定できました。

次に同じ戦略を全ニューロンに対して使います。補助関数get_perturbed_connectivity_all_neuronsを用意しています。仕組みに興味があれば余裕があるときにボーナスセクション1を見てください。

# @markdown Execute to get helper function `get_perturbed_connectivity_all_neurons`

def get_perturbed_connectivity_all_neurons(perturbed_X):
  """
  Estimates the connectivity matrix of perturbations through stacked correlations.

  Args:
    perturbed_X (np.ndarray): the simulated dynamical system X of shape
                              (n_neurons, timesteps)

  Returns:
    R (np.ndarray): the estimated connectivity matrix of shape
                    (n_neurons, n_neurons)
  """
  # select perturbations (P) and outcomes (Outs)
  # we perturb the system every over time step, hence the 2 in slice notation
  P = perturbed_X[:, ::2]
  Outs = perturbed_X[:, 1::2]

  # stack perturbations and outcomes into a 2n by (timesteps / 2) matrix
  S = np.concatenate([P, Outs], axis=0)

  # select the perturbation -> outcome block of correlation matrix (upper right)
  R = np.corrcoef(S)[:n_neurons, n_neurons:]

  return R


# Parameters
n_neurons = 6
timesteps = 5000

# Generate nxn causal connectivity matrix
A = create_connectivity(n_neurons)

# Simulate perturbed dynamical system
perturbed_X = simulate_neurons_perturb(A, timesteps)

# Get estimated connectivity matrix
R = get_perturbed_connectivity_all_neurons(perturbed_X)
#@markdown Execute this cell to visualize true vs estimated connectivity

# Let's visualize the true connectivity and estimated connectivity together
fig, axs = plt.subplots(2, 2, figsize=(10, 10))
see_neurons(A, axs[0, 0])  # we are invoking a helper function that visualizes the connectivity matrix
plot_connectivity_matrix(A, ax=axs[0, 1])
axs[0, 1].set_title("True connectivity matrix A")

see_neurons(R.T, axs[1, 0])  # we are invoking a helper function that visualizes the connectivity matrix
plot_connectivity_matrix(R.T, ax=axs[1, 1])
axs[1, 1].set_title("Estimated connectivity matrix R")

fig.suptitle("True vs estimated connectivity")
plt.show()

再び真の接続行列と推定接続行列の要素間の相関係数を計算します。ほぼ1なので真の因果関係をよく回復できています!

print(np.corrcoef(A.transpose().flatten(), R.flatten())[1, 0])

まとめ

チュートリアルの推定所要時間: 40分

# @title Video 5: Summary
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display


class PlayVideo(IFrame):
  def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
    self.id = id
    if source == 'Bilibili':
      src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
    elif source == 'Osf':
      src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
    super(PlayVideo, self).__init__(src, width, height, **kwargs)


def display_videos(video_ids, W=400, H=300, fs=1):
  tab_contents = []
  for i, video_id in enumerate(video_ids):
    out = widgets.Output()
    with out:
      if video_ids[i][0] == 'Youtube':
        video = YouTubeVideo(id=video_ids[i][1], width=W,
                             height=H, fs=fs, rel=0)
        print(f'Video available at https://youtube.com/watch?v={video.id}')
      else:
        video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
                          height=H, fs=fs, autoplay=False)
        if video_ids[i][0] == 'Bilibili':
          print(f'Video available at https://www.bilibili.com/video/{video.id}')
        elif video_ids[i][0] == 'Osf':
          print(f'Video available at https://osf.io/{video.id}')
      display(video)
    tab_contents.append(out)
  return tab_contents


video_ids = [('Youtube', 'p3fZW5Woqa4'), ('Bilibili', 'BV1FT4y1j7PW')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
  tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_Summary_Video")

このチュートリアルでは、摂動を用いた因果性の定義と推定方法について学びました。特に以下を行いました:

  1. 連結されたニューロン系のシミュレーション方法を学んだ

  2. 神経活動を直接摂動することでニューロン間の接続性を推定する方法を学んだ

NMA終了後に因果性に興味がある方のために、参考になる文献を紹介します。

  • ImbensとRubinによる『Causal Inference for Statistics, Social, and Biomedical Sciences』
  • HernanとRubinによる『Causal Inference: What If』
  • AngristとPischkeによる『Mostly Harmless Econometrics』
  • 神経科学への応用例はこちらを参照してください

ボーナス

ボーナスセクション1: 推定接続行列の計算

これはgetperturbedconnectivityallneurons()get_perturbed_connectivity_all_neurons()のコードが何をしているかの説明です

まず推定接続行列RRを計算します。摂動行列PPと結果行列OOを抽出します:

P=[...x0x2...xT...]n×T/2P = \begin{bmatrix} \mid & \mid & ... & \mid \\ x_0 & x_2 & ... & x_T \\ \mid & \mid & ... & \mid \end{bmatrix}_{n \times T/2} O=[...x1x3...xT1...]n×T/2O = \begin{bmatrix} \mid & \mid & ... & \mid \\ x_1 & x_3 & ... & x_{T-1} \\ \mid & \mid & ... & \mid \end{bmatrix}_{n \times T/2}

そしてPPOOを縦に連結した行列SSの相関行列を計算します:

S=[PO]2n×T/2S = \begin{bmatrix} P \\ O \end{bmatrix}_{2n \times T/2}

RRcorr(S)\text{corr}(S)の右上のn×nn \times nブロックとして抽出されます:

これは上部右ブロックがシステム内の各ニューロンペアに対する摂動の結果への効果を表すためです。

この方法は平均差分で得られる結果に比例する推定接続行列を与え、xxの分散に依存する比例定数の違いのみがあります。