JuliaでIzhikevichモデル
- モデルの定義
- 変数の更新式 (forループを使う場合)
- シミュレーションの実行
- 変数の更新式 (ベクトルを使う場合)
- frequency-current (F-I) curve
- 記事を書いた動機
- 関連資料
JuliaでIzhikevichモデルの実装をする。JuliaによるSNNの実装はSpikingNeuralNetworks.jlが既にある。この記事の実装ではJuliaの書き方を勉強すべく大幅に参考にした。以下はJulia 1.4.0で実行。ブログでは何故かJuliaのSyntax highlightがされてない。
モデルの定義
まず必要なパッケージを読み込む。Parameters.jlはstruct
のデフォルト値の設定のために必要。struct
の前に@with_kw
というmacroを書くことでstruct
をデコレートできる。ただ、この記事を書いた後に@bicycle1885氏に以下のように教えていただいたので、Base.@kwdef
を用いることにした。感謝…!
> この辺、デフォルトの機能であってもいいと思いましたが。
— (「・ω・)「ガオー (@bicycle1885) July 14, 2020
実はBaseに@ kwdefマクロがありますよ。using Base: @ kwdef すれば多分Parameters.jlとほとんど同じように使えると思います。
using Plots
using Base: @kwdef
using Parameters: @unpack # or using UnPack
変更しない定数を保持するstruct
のIZParameter
と、変数を保持するmutable struct
のIZ
を作成する。
@kwdef struct IZParameter{FT}
a::FT = 0.02
b::FT = 0.2
c::FT = -65
d::FT = 8.0
vrest::FT = -60.0
vpeak::FT = 30.0
end
@kwdef mutable struct IZ{FT}
param::IZParameter = IZParameter{FT}()
N::Int32
v::Vector{FT} = fill(param.vrest, N)
u::Vector{FT} = param.b * v
fire::Vector{Bool} = zeros(Bool, N)
end
型をFT
で指定していますが、これはFloatTypeの略。インスタンス作成時にneurons = IZ{Float32}(N=N)
などとして型を指定する。元々Float32
と指定していたが、この記事を書いた後に黒木玄さん(@genkuroki)にFloat32
と型を限定するのは良くないと指摘していただき、次のようなnotebookを作成していただいた:https://nbviewer.jupyter.org/gist/genkuroki/41824601d6905cdab159e0c88e925ff8
Float32やFloat64などの型を決めうちせずに書く方法をhttps://t.co/sLuUpKotUK
— 黒木玄 Gen Kuroki (@genkuroki) July 14, 2020
で紹介しておきました。
struct Foo{T, U}
p::T
A::U
end
まで略して書いても十分実用的です。 pic.twitter.com/vYgKr7mnO6
function updateIZ!(variable::IZ, param::IZParameter, I::Vector, dt)
@unpack N, v, u, fire = variable
@unpack a, b, c, d, vrest, vpeak = param
@inbounds for i = 1:N
v[i] += dt * (0.04f0v[i]^2 + 5f0v[i] + 140f0 - u[i] + I[i])
u[i] += dt * (a * (b * v[i] - u[i]))
end
@inbounds for i = 1:N
fire[i] = v[i] >= vpeak
v[i] = ifelse(fire[i], c, v[i])
u[i] += ifelse(fire[i], d, 0)
end
end
for
ループを用いて1つのニューロンごとに膜電位v
や回復変数u
を更新する。SNN本ではPythonを使ったので後述するベクトル表記を使用したが、Juliaなら問題なし。macro @inbounds
を使うと配列の境界確認が無くなり高速化されるらしい (Bounds checking · The Julia Language)。
T = 400 # ms
dt = 0.01f0 # ms
nt = Int32(T/dt) # number of timesteps
N = 1 # ニューロンの数
# 入力刺激
t = Array{Float32}(1:nt)*dt
I = repeat(10f0 * ((t .> 50) - (t .> 350)), 1, N) # injection current
# 記録用
varr = zeros(Float32, nt, N)
uarr = zeros(Float32, nt, N)
# modelの定義
neurons = IZ{Float32}(N=N)
# simulation
@time for i = 1:nt
updateIZ!(neurons, neurons.param, I[i, :], dt)
varr[i, :] = neurons.v
uarr[i, :] = neurons.u
end
ニューロンの膜電位v
、回復変数u
、刺激電流I
の描画をする。
p1 = plot(t, varr[:, 1])
p2 = plot(t, uarr[:, 1])
p3 = plot(t, I[:, 1])
plot(p1, p2, p3,
xlabel = ["" "" "Times (ms)"],
ylabel= ["Membrane\n potential (mV)" "Recovery\n current (pA)" "Injection\n current (pA)"],
layout = grid(3, 1, heights=[0.5, 0.25, 0.25]), legend = false)
function update_vecIZ!(variable::IZ, param::IZParameter, I::Vector, dt)
@unpack N, v, u, fire = variable
@unpack a, b, c, d, vrest, vpeak = param
v[:] += dt * (0.04f0v.^2 + 5f0v .+ 140f0 - u + I)
u[:] += dt * (a * (b * v - u))
fire = v .>= vpeak
u[:] += d * fire
v[:] = v .* (1 .- fire) + c * fire
end
なお、上の関数においてv[:] += ...
ではなくv += ...
としていたために変数の更新が行われず嵌った。Mutating function in Julia (function that modifies its arguments) - Stack Overflow
によると
function right1_fill_with_twos!(v::Vector{Int64})
v[:]=[2 for ii in 1:length(v)]
end
は機能して
function wrong1_fill_with_twos!(v::Vector{Int64})
v=[2 for ii in 1:length(v)]
end
はダメらしい。
同様にシミュレーションを実行。Pythonと異なり、forループを用いた更新の方が速い。
# 記録用
varr2 = zeros(Float32, nt, N)
uarr2 = zeros(Float32, nt, N)
neurons = IZ{Float32}(N=N)
@time for i = 1:nt
update_vecIZ!(neurons, neurons.param, I[i, :], dt)
varr2[i, :] = neurons.v
uarr2[i, :] = neurons.u
end
結果を表示する。
p1 = plot(t, varr2[:, 1])
p2 = plot(t, uarr2[:, 1])
p3 = plot(t, I[:, 1])
plot(p1, p2, p3,
xlabel = ["" "" "Times (ms)"],
ylabel= ["Membrane\n potential (mV)" "Recovery\n current (pA)" "Injection\n current (pA)"],
layout = grid(3, 1, heights=[0.5, 0.25, 0.25]), legend = false)
frequency-current (F-I) curve
入力を一定電流とし、その強さを1からmaxcurrent
まで変更したときの発火率の変化を見る。入力電流はrange
で作成する。linspace
は廃止されたらしい(cf. Julia 0.6 から 1.x への移植)。
T = 5000 # ms
dt = 0.01f0 # ms
nt = Int32(T/dt) # number of timesteps
N = 200 # ニューロンの数
# 入力刺激
maxcurrent = 10
t = Array{Float32}(1:nt)*dt
I = Array{Float32}(range(1,maxcurrent,length=N)) # injection current
# 記録用
spikearr = zeros(Int32, nt, N)
# modelの定義
neurons = IZ{Float32}(N=N)
# simulation
@time for i = 1:nt
updateIZ!(neurons, neurons.param, I[:], dt)
spikearr[i, :] = neurons.fire
end
発火率を計算する。
fr = sum(spikearr, dims=1) / (T*1e-3)
結果を描画する。
plot(I, fr[:],
xlabel="Injection current (pA)",
ylabel="Firing rate (Hz)",
legend=false)
記事を書いた動機
『Juliaで学ぶ計算論的神経科学』的な本を2~5年後ぐらいに何らかの形式で出したいと思っています。とりあえずSNN本(ゼロから作るSpiking Neural Networks)のJulia版をJupyter Notebook形式で公開して既存の内容についてコメントを貰いたいと思っています。Juliaに強い方、この記事のコードでもっと良い書き方があればお教えいただきたく存じます。
- 勉強や研究に支障がない程度
— ymtk (@tak_yamm) June 16, 2020
- 完成は2~3年後ぐらい
- 色んな分野 (SNN本の内容を完全に含む)
- コード付き (重要)
でやろうと思ってます。とりあえずは今までに書いたブログと同人誌をベースに広げていく感じで…
関連資料
Twitterで検索したらDifferentialEquations.jl
を用いた実装を公開されている方がいた。
DifferentialEquations.jl で Izhikevich モデル、以前やったものを発掘した。微分方程式でのイベント処理の例としても。 #Julia言語https://t.co/OAzFT0Rtkq pic.twitter.com/zJKjN1Q5e0
— Lirimy (@LirimyDh) July 13, 2020