神经元 SpikingFlow.neuron

本教程作者: fangwei123456

本节教程主要关注 SpikingFlow.neuron,包括如何使用已有神经元、如何定义新的神经元。

LIF神经元仿真

我们使用一个LIF神经元进行仿真,代码如下:

import SpikingFlow
import SpikingFlow.neuron as neuron
# 导入绘图模块
from matplotlib import pyplot
import torch

# 新建一个LIF神经元
lif_node = neuron.LIFNode([1], r=9.0, v_threshold=1.0, tau=20.0)
# 新建一个空list,保存仿真过程中神经元的电压值
v_list = []
# 新建一个空list,保存神经元的输出脉冲
spike_list = []

T = 200
# 运行200次
for t in range(T):
    # 前150次,输入电流都是0.1
    if t < 150:
        spike_list.append(lif_node(0.1).float().item())
    # 后50次,不输入,也就是输入0
    else:
        spike_list.append(lif_node(0).float().item())

    # 记录每一次输入后,神经元的电压
    v_list.append(lif_node.v.item())

# 画出电压的变化
pyplot.subplot(2, 1, 1)
pyplot.plot(v_list, label='v')
pyplot.xlabel('t')
pyplot.ylabel('voltage')
pyplot.legend()

# 画出脉冲
pyplot.subplot(2, 1, 2)
pyplot.bar(torch.arange(0, T).tolist(), spike_list, label='spike')
pyplot.xlabel('t')
pyplot.ylabel('spike')
pyplot.legend()
pyplot.show()

print('t', 'v', 'spike')
for t in range(T):
    print(t, v_list[t], spike_list[t])

运行后得到的电压和脉冲如下:

_images/1-1.png

你会发现,LIF神经元在有恒定输入电流时,电压会不断增大,但增速越来越慢。

如果输入电流不是足够大,最终在每个dt内,LIF神经元的电压衰减值会恰好等于输入电流造成的电压增加值,电压不再增大,导致无法充电到过阈值、发放脉冲。

当停止输入后,LIF神经元的电压会指数衰减,从图中500个dt后的曲线可以看出。

我们修改代码,给予更大的电流输入:

...
for t in range(T):
    # 前150次,输入电流都是0.12
    if t < 150:
        spike_list.append(lif_node(0.12).float().item())
...

运行后得到的电压和脉冲如下(需要说明的是,脉冲是以pyplot柱状图的形式画出,当柱状图的横轴,也就是时间太长时,而图像的宽度又不够大,一些“落单”的脉冲在图像上会无法画出,因为宽度小于一个像素点):

_images/1-2.png

可以发现,LIF神经元已经开始发放脉冲了:

定义新的神经元

在SNN中,不同的神经元模型,区别往往体现在描述神经元的微分方程。上文所使用的LIF神经元,描述其动态特性的微分方程为:

\[\tau_{m} \frac{\mathrm{d}V(t)}{\mathrm{d}t} = -(V(t) - V_{reset}) + R_{m}I(t)\]

其中 \(\tau_{m}\) 是细胞膜的时间常数, \(V(t)\) 是膜电位, \(V_{reset}\) 是静息电压, \(R_{m}\) 是膜电阻, \(I(t)\) 是输入电流

SpikingFlow是时间驱动(time-driven)的框架,即将微分方程视为差分方程,通过逐步仿真来进行计算。例如LIF神经元,代码位于 SpikingFlow.neuron.LIFNode,参考它的实现:

def forward(self, i):
    '''
    :param i: 当前时刻的输入电流,可以是一个float,也可以是tensor
    :return: out_spike: shape与self.shape相同,输出脉冲
    '''
    out_spike = self.next_out_spike

    # 将上一个dt内过阈值的神经元重置
    if isinstance(self.v_reset, torch.Tensor):
        self.v[out_spike] = self.v_reset[out_spike]
    else:
        self.v[out_spike] = self.v_reset

    v_decay = -(self.v - self.v_reset)
    self.v += (self.r * i + v_decay) / self.tau
    self.next_out_spike = (self.v >= self.v_threshold)
    self.v[self.next_out_spike] = self.v_threshold
    self.v[self.v < self.v_reset] = self.v_reset

    return out_spike

从代码中可以发现,t-dt时刻电压没有达到阈值,t时刻电压达到了阈值,则到t+dt时刻才会放出脉冲。这是为了方便查看波形图,如果不这样设计,若t-dt时刻电压为0.1,v_threshold=1.0,v_reset=0.0, t时刻增加了0.9,直接在t时刻发放脉冲,则从波形图上看,电压从0.1直接跳变到了0.0,不利于进行数据分析。

此外,“脉冲”被定义为“torch.bool”类型的变量。SNN中的神经元,输出的应该是脉冲而不是电压之类的其他值。

如果想自行实现其他类型的神经元,只需要继承 SpikingFlow.neuron.BaseNode,并实现 __init__(), forward(), reset() 函数即可。