はじめに
この講座ではmatlab初心者向けに解析で使ういくつかの機能について、神経科学に関係する内容に触れながらその使い方を解説します。
この投稿は関連する前半での実装をもとにしています。前半がお済みでない方はまずはそちらから先にお読みください。
モデルニューロンの応答の計算
前半でGaborフィルタの実装が終わったので、モデルニューロンの応答を計算していきましょう。
モデルの定義
まず初めに、単純型細胞のモデルとして、Linear-Nonlinear(LN) Cascadeタイプのモデルを想定します。
時刻$t$におけるスパイクの発火の有無を以下のように定義します。今回は話は簡単にするために、モデルニューロンは時刻$t_i$の刺激$\mathbf{s} (t_i)$に対して発火するか、しないかの2状態のみを取るものとします。
\begin{aligned}
p(t) &= \mathbf{k}^T \mathbf{s}(t) & \text{Linear process} \\
q(t) &= f\left( p(t) \right) & \text{Nonlinear process} \\
r(t) &\sim P \left( q(t) \mid \Theta \right) & \text{Spike generation}
\end{aligned}
数式を目の前にすると身構えてしまう人もいるかもしれませんが、今回はかなり単純な定義を採用します。1
単純型細胞モデルの実装
$p(t) = \mathbf{k}^T \mathbf{s}(t)$は内積です。直感的な説明をすると、ここではフィルタと刺激がどの程度似ているのかを見積もります。今回は$\mathbf{k}$としてGaborフィルタを採用します。
2次元のフィルタ行列K1
(size: [16 x 16]
)と3次元の刺激行列NS
(size: [16 x 16 x 2048]
)は1次元目(行、y軸)と2次元目(列、x軸)の長さがともに等しいので「基本的な演算で互換性のある配列サイズ」となっています。そのため「暗黙的な拡張」が適用されて、計算結果は今回の場合NS
と同じサイズ(size: [16 x 16 x 2048]
)の3次元行列になります。
matlabでは要約統計量の計算は基本的に若い次元(1次元目、2次元目、、、)から大きさが1より大きい次元に適用されます。K1.*NS
の計算結果に対して2回sum
を計算していますが、それぞれが1次元目、2次元目について合計を計算することになります。丁寧な記述を心がけるのであれば次元を指定する引数に値を渡しましょう。squeeze
関数は大きさが1の次元を削除します。このため、計算結果P1
は1次元の配列(size: [2048 x 1]
)になります。
% prepare a set of noise stimuli
NS = randn(16, 16, 2048);
% make 2D Gabor filter as a model (kernel) of neuron
K1 = make2DGabor(16, 0.25, 0, pi/6, 2, 1/4, 0);
% projection of the stimuli NS by kernel K1 to 1D space
P1 = squeeze(sum(sum(K1.*NS)));
$q(t) = f\left( p(t) \right)$は非線形処理です。モデル自体を内積だけの式 $ r(t) = \mathbf{k}^T \mathbf{s}(t) $ だけで定義してもよいのですが、非線形関数$f(\cdot)$を入れておくと、処理が都合よくなることがあるので使っておきます。今回は$f(\cdot)$として半波整流max(0, x)
を仮定します。半波整流はスパイクが負の数を表現できないということを表現します。
% rectify the projected stimuli
Q1 = max(0, P1);
$r(t) \sim P \left( q(t) \mid \Theta \right)$は細胞の発火が適当なパラメータ$\Theta$のもとで内部変数$q(t)$に依存して確率的に定まるということを意味します。真面目に考える際は、細胞の発火が何らかの確率分布に従うという条件を置くこともあります。今回は$q(t)$がある閾値$\theta \in \Theta$をこえた場合に必ず発火するという仮定を置きます。この部分は非線形処理$f(\cdot)$の中で記述されることもあります。2
% generate a spike responce by threshold
R1 = 2 < Q1;
ここまでで、各処理段階での結果を得ることができました。3
ここまでの実装では、各処理をそれぞれ丁寧に実装していました。ただ、複数のモデルニューロンを扱う際などは、毎回同じ処理を記述することは間違いのもとですし、コードの見通しが悪くなってしまいます。だからと言ってわざわざ関数ファイルを作ってまで実装する必要性もそこまで感じられません。そんな中途半端な処理を実装する方法として、「無名関数」を使った実装と、スクリプト内関数を使った実装を以下で紹介します。
(補足) 無名関数を使った処理の実装
それぞれの処理を「無名関数」を用いて実装してみましょう。
無名関数はわざわざ関数ファイルを作るほどではない程度の処理を実装する際に使用します。funcname = @(input) implementation
の形で記述します。4
% project: projection of the stimuli NS by kernel K1 to 1D space
project = @(Kernel, Stimuli) squeeze(sum(sum(Kernel.*Stimuli)));
% rectify: nonlinear process
% ramp function a.k.a ReLU(Rectified Linear Unit)
rectify = @(projected) max(0, projected);
% isfired: spike generation
isfired = @(rectified, threshold) threshold < rectified;
P1 = project(K1, NS);
Q1 = rectify(P1);
R1 = isfired(Q1, 2);
(補足) サブ関数による実装例
function
キーワードを用いた関数は1つのファイル内に複数定義することができます。5
参考までに同じ実装をサブ関数での実装例を紹介しておきます。関数の記述は普通の関数の場合と同じです。サブ関数はスクリプトファイル内のみから実行可能で、スクリプトや関数の実装に続けて記述します。ファイル内では必ず末尾に実装します。
... % script implementation
P1 = project(K1, NS);
Q1 = rectify(P1);
R1 = isfired(Q1, 2);
... % script implementation
function P = project(K, S)
% project: projection of the stimuli by kernel to 1D space
% K: Kernel
% S: Stimuli
P = squeeze(sum(sum(K.*S)));
end
function Q = rectify(P)
% rectify: nonlinear process
% ramp function a.k.a ReLU(Rectified Linear Unit)
% P: projected stimuli
Q = max(0, P);
end
function R = isfired(Q, T)
% isfired: spike generation
% R: rectified (and projected) stimuli
% T: threshold
R = T < R;
end
時系列データの可視化
それぞれの処理段階での結果の時系列を表示してみましょう。
グラフが見やすいように先ほど計算したデータの一部のみを表示するように設定します。
結果が格納される配列に対して、インデックス配列を適用することで、指定した部分のみを取り出すことができます。
plot
関数を用いることで時系列データを表示します。
入力引数でグラフの色やスタイルを変更することができます。
% define time axis
t = 1:256;
figure();
subplot(3, 1, 1);
plot(t, P1(t));
subplot(3, 1, 2);
plot(t, Q1(t), '.');
subplot(3, 1, 3);
plot(t, R1(t), 'r:*');
画像ファイルの保存
解析などの実行結果を画像ファイルとして出力しましょう。今回はSTAによる受容野の推定結果を題材に開設します。
Spike Triggered Averageの実行
STAによる受容野の推定結果を計算します。
STAの基本的な考え方は、Spikeを誘発した刺激の取り出し、その平均像を可視化するといったところです。ノイズ刺激の時間軸(3次元目)に対してニューロンが発火したかどうかを表すR1
を論理インデックスとして取り出してきます。平均の計算はmean
関数を利用し、応答を誘発したノイズ刺激データの3次元目に対して適用することで各ピクセルごとに平均像を計算することになります。
subplot
を使って画像を並べることでモデルのフィルタK1
と推定結果E1
を比較してみましょう。発火数によりますが、受容野のおおよその形状は再現できていると思います。
% Estimate model receptive field by STA
E1 = mean(NS(:, :, R1), 3);
subplot(1, 2, 1);
imagesc(K1);
axis image off;
title('Model');
subplot(1, 2, 2);
imagesc(E1);
axis image off;
title('Estimation');
matlabには画像を保存する関数がいくつかあります。必要な状況に合わせて使い分けましょう。ここではその中からprint
関数とimwrite
関数の2つを紹介します。6
print
関数によるFigureの保存
print
関数は表示しているFigureの描画を画像ファイルとして保存します。
画像ファイルの形式や解像度などをオプションで細かく設定できるのでドキュメントをお読みください。
print('resultSTA_print', '-dpng');
imwrite
関数による行列データの画像化
imwrite
関数は、行列データ自体を画像として保存します。
関数が様々なデータ形式、出力ファイル形式に対応している都合上、仕様に沿った記述を行わないと思った通りの出力が得られません。とりあえず何も考えずに推定結果E1
をimwrite
の引数に入れてみましょう。7
imwrite(E1, 'resultSTA_imwrite.png');
グレースケールかつ、明るかった部分(ON領域)しか見えなくなってしまいました。
これはimwrite
関数が行列データを[0 ~ 1]
のデータであると認識しているためです。
mat2gray
関数を利用してデータの最小値を0、最大値を1とするように正規化します。
引数
imwrite(mat2gray(E1), 'resultSTA_imwrite_normalized.png');
カラーマップを反映させたいときはgray2ind
関数とind2rgb
関数を利用します。
gray2ind
関数はグレースケール画像をn段階のインデックス画像に変換します。
ind2rgb
関数はインデックス画像にカラーマップを適用し、RGB画像に変換します。
imwrite(ind2rgb(gray2ind(mat2gray(E1), 256), parula(256)), 'resultSTA_imwrite_normalized_parula.png');
imwrite
関数自体もインデックス画像を入力に受けることができるため、以下のようにしても同様の結果を得ることができます。
imwrite(gray2ind(mat2gray(E1), 256)), parula(256)), 'resultSTA_imwrite_normalized_parula.png');
動画ファイルでの保存
実行結果を動画ファイルとして保存しましょう。
時間変化の可視化
ディスプレイ上でデータを可視化する以上、可視化は2次元に制限されます。8
時間とともに表示を変化させることで、表示する軸を1つ拡張することができます。
ここでは時間とともに鮮明となるSTAの推定結果を可視化してみます。ti
の値をfor
で順に増やしていきます。9
title
を使用することで、各座標軸ごとにグラフのタイトルをつけることができます。さらにここでは時間の変化とともに、タイトルの文字列を更新しています。num2str
は数値を文字列に変換します。大かっこ[ ]
を使用することで文字列を結合することができます。ここでは['Stimulus: ', num2str(ti)]
とすることで、Stimulus: 512
のように何番目のノイズ画像を処理したのかを表示します。
各ループの最後にpause
で指定した時間(秒)だけ処理を停止します。こうすることで、描画がコマ送りに更新されます。10
実行を途中で停止したいときはCtrl + C
で処理を中断できます。
% for-loop to update STA ewsult with time
figure();
for ti = 1:2048
P1 = project(K1, NS(:, :, 1:ti));
Q1 = rectify(P1);
R1 = isfired(Q1, 2);
E1 = mean(NS(:, :, R1), 3);
subplot(1, 2, 1);
imagesc(NS(:, :, ti));
axis image off;
title(['Stimulus: ', num2str(ti)]);
subplot(1, 2, 2);
imagesc(E1);
axis image off;
title('Estimation');
pause(1/30);
end
getframe
によるFigure画像の取得
getframe
を用いることで、現在描画されているFigureをデータとして確保することができます。
とりあえず以下を実行して、各時刻ti
における刺激と、STAの結果を取得します。
% for-loop to update STA ewsult with time
figure();
Frame(2048) = struct('cdata',[],'colormap',[]);
for ti = 1:2048
P1 = project(K1, NS(:, :, 1:ti));
Q1 = rectify(P1);
R1 = isfired(Q1, 2);
E1 = mean(NS(:, :, R1), 3);
subplot(1, 2, 1);
imagesc(NS(:, :, ti));
axis image off;
title(['Stimulus: ', num2str(ti)]);
subplot(1, 2, 2);
imagesc(E1);
axis image off;
title('Estimation');
drawnow();
Frame(ti) = getframe(gcf);
end
VideoWriter
による動画の書き込み
VideoWriter
を利用することで、動画ファイルを作製することができます。
これまでと処理の書き方が少々異なりますが、以下を実行することでMP4形式の動画ファイルとして書き込むことができます。11
% Create VideoWrite Object
vidObj = VideoWriter('resultSTA.mp4', 'MPEG-4');
% Set properties bofore file opend
vidObj.FrameRate = 30;
vidObj.Quality = 100;
% Writeing video file, do not forget open and close
vidObj.open();
vidObj.writeVideo(Frame);
vidObj.close();
残念ながらQiitaではmp4ファイルを貼りつけることができないようです。
imwrite
によるgif動画の書き込み
また、imwrite
を使用することにより、gif動画ファイルを作成することもできます。12
filename = 'resultSTA.gif';
DelayTime = 1/30;
for ti = 1:2048
[A, map] = rgb2ind(frame2im(Frame(ti)), 256);
if ti == 1
% For first frame
imwrite(A, map, filename, 'gif', 'LoopCount', Inf, 'DelayTime', DelayTime);
else
imwrite(A, map, filename, 'gif', 'WriteMode','append', 'DelayTime', DelayTime);
end
end
お疲れ様です。
とりあえず今回は以上です。13
-
今回の目的はSTAの実装ではなく、matlabの練習ですのでかなり粗い記述となっています。この辺りの定義は論文によってかなり違いがありますので調べてみてください。前半でも紹介しました、Meyer AF et al. 2016 Front Syst Neurosci、辺りが参考になるかと思います。 ↩
-
ここでは$\Theta$を固定したパラメータとして扱いましたが、これは必ずしも必須ではありません。実際的には、刺激に対するスパイクの発生がポアソン分布などの何らかの確率分布に従うという仮説が置かれます。また、今回のモデルニューロンでは半波整流したのちに閾値処理をおこなっています。この場合、結果からみると半波整流は不要ですがモデルを変更するときのことを考えて分けて考えます。 ↩
-
それぞれの処理結果が変数
P1
,Q1
,R1
へと代入されています。このような変数名の付け方はよくありません、変数の名前は後から読んでもわかりやすいものにしましょう。適当なアルファベットや単語を略したものを付けがちですが、できるだけ避けましょう。コードが長くなった時に同じ名前で変数を再定義し、値を上書きしてしまうという悲劇を招きます。 ↩ -
「無名関数」は「関数ハンドル」という変数として扱われます。関数に対して関数を引数として渡したいときなどに役に立ちます。 ↩
-
スクリプト内サブ関数はR2016b以降からサポートされています。それ以前のバージョンをお使いの方は,この実装は使用できません。 ↩
-
ここでは出力された画像データを拡大して貼り付けています。上記スクリプトを実行して得られるデータは縦横16 pixなので、画像ファイルとしてはかなり小さなものになります。 ↩
-
3次元データを可視化する関数もありますが、それらはあくまで2次元に投影した図になります。 ↩
-
tiが順に増えていく際に、論理インデックス配列となる
R1
の大きさはNS
の3次元目の要素数より少ないですが、「より小さな配列の論理インデックス」はインデックス配列の欠損要素がゼロに設定されるので、エラーが出ずに実行できます。 ↩ -
ここで停止時間として、
1/30
を使用していますが、実際にはデータの処理と描画にかかる時間があるため、必ずしも描画が30 Hz
でおこなわれるということを意味しません。また、各時間における描画をギリギリまで早くするにはdrawnow
を使用します。 ↩ -
VideoWriter
はクラスという形式で、定義されています。今回はクラスについて扱わないので詳細は割愛します。 ↩ -
ここで表示しているgif動画はアップロードするファイル容量の関係で、4フレームおきに描画した結果になっているため、ここでのスクリプトの4倍のスピードで描画されています。 ↩
-
今回の記事を書くにあたり、いくつかの内容を削りました。また気が向いたら関連する内容を追加しようと思います。 ↩