当前位置:   article > 正文

第三十二课.脉冲神经网络SNN_脉冲神经网络事件驱动

脉冲神经网络事件驱动

时间驱动与事件驱动

时间驱动

为了便于理解时间驱动,我们可以将SNN(spiking neuron network)看作是一种RNN,它的输入是电压增量(电流与膜电阻的乘积),隐藏状态是膜电压,输出是脉冲。这样的SNN神经元具有马尔可夫性:当前时刻的输出只与当前时刻的输入,神经元自身的状态有关。

可以用充电,放电,重置3个方程描述脉冲神经元: H ( t ) = f ( V ( t − 1 ) , X ( t ) ) H(t)=f(V(t-1),X(t)) H(t)=f(V(t1),X(t)) S ( t ) = g ( H ( t ) − V t h r e s h o l d ) = Θ ( H ( t ) − V t h r e s h o l d ) S(t)=g(H(t)-V_{threshold})=\Theta(H(t)-V_{threshold}) S(t)=g(H(t)Vthreshold)=Θ(H(t)Vthreshold) V ( t ) = H ( t ) ⋅ ( 1 − S ( t ) ) + V r e s e t ⋅ S ( t ) V(t)=H(t)\cdot(1-S(t))+V_{reset}\cdot S(t) V(t)=H(t)(1S(t))+VresetS(t)其中, V ( t ) V(t) V(t)是神经元的膜电压, X ( t ) X(t) X(t)是外源输入,即电压增量, H ( t ) H(t) H(t)是神经元的隐藏状态,可以理解为神经元还没有发脉冲前的瞬间, f ( V ( t − 1 ) , X ( t ) ) f(V(t-1),X(t)) f(V(t1),X(t))是神经元的状态更新方程,不同的神经元,区别在于更新方程不同。

对于LIF神经元,其动作的微分方程和近似差分方程分别为: τ m d V ( t ) d t = − ( V ( t ) − V r e s e t ) + X ( t ) \tau_{m}\frac{dV(t)}{dt}=-(V(t)-V_{reset})+X(t) τmdtdV(t)=(V(t)Vreset)+X(t) τ m ( V ( t ) − V ( t − 1 ) ) = − ( V ( t − 1 ) − V r e s e t ) + X ( t ) \tau_{m}(V(t)-V(t-1))=-(V(t-1)-V_{reset})+X(t) τm(V(t)V(t1))=(V(t1)Vreset)+X(t)因此,对应的充电(状态更新)方程为: f ( V ( t − 1 ) , X ( t ) ) = V ( t − 1 ) + 1 τ m ( − ( V ( t − 1 ) − V r e s e t ) + X ( t ) ) f(V(t-1),X(t))=V(t-1)+\frac{1}{\tau_{m}}(-(V(t-1)-V_{reset})+X(t)) f(V(t1),X(t))=V(t1)+τm1((V(t1)Vreset)+X(t))放电方程中的 S ( t ) S(t) S(t)是神经元发放的脉冲, g ( x ) = Θ ( x ) g(x)=\Theta(x) g(x)=Θ(x)是阶跃函数,也被称为脉冲函数,脉冲函数的输出仅为0或1。

重置表示电压重置过程:发放脉冲,则电压重置为 V r e s e t V_{reset} Vreset;如果没有发放脉冲,则电压不变。

注意到,在RNN中,使用了可微分的激活函数,例如tanh。但SNN中对应的脉冲函数 g ( x ) g(x) g(x)是不可微分的,这就导致了包含SNN层的深度学习模型不能用反向传播的方式来训练。但是我们可以用一个形状与阶跃函数相似的激活函数 σ ( x ) \sigma(x) σ(x)去代替。这被称为梯度替代法。

在前向传播时,使用 g ( x ) = Θ ( x ) g(x)=\Theta(x) g(x)=Θ(x),神经元的输出是离散的0和1,网络依然是标准的SNN,但反向传播时,使用梯度替代函数代替计算脉冲函数的梯度: g ′ ( x ) = σ ′ ( α x ) g'(x)=\sigma'(\alpha x) g(x)=σ(αx)其中, α \alpha α可以调整激活函数的平滑程度: σ ( α x ) = 1 1 + e x p ( − α x ) \sigma(\alpha x)=\frac{1}{1+exp(-\alpha x)} σ(αx)=1+exp(αx)1 α \alpha α越大, σ ( α x ) \sigma(\alpha x) σ(αx)就越接近 Θ ( x ) \Theta(x) Θ(x),但也越容易在靠近 x = 0 x=0 x=0时梯度爆炸,远离 x = 0 x=0 x=0时则容易梯度消失,导致网络难以训练。

下图显示了不同 α \alpha α时,梯度替代函数的形状:
fig1

事件驱动

对于事件驱动的SNN,不需要通过时钟驱动SNN的计算,神经元的状态更新由事件触发。

在脉冲响应模型(Spike response model,SRM)中,使用显式的 V − t V-t Vt方程来描述神经元的活动,而不是用微分方程去描述神经元的充电过程。由于 V − t V-t Vt是已知的,因此给与任何输入 X ( t ) X(t) X(t),神经元的响应 V ( t ) V(t) V(t)都可以被直接算出(不需要提供 V ( t − 1 ) V(t-1) V(t1)的信息,因此与时钟取消了关联)。

Tempotron神经元也是一种SNN神经元,其命名来源于ANN中的感知器,感知器是最简单的ANN神经元,其对输入数据加权求和,输出二值0或1来表示数据的分类结果。Tempotron神经元可以看作是SNN中的感知器,它同样对输入数据加权求和,再输出二分类结果。

Tempotron的膜电位定义为: V ( t ) = ∑ i w i ∑ t i K ( t − t i ) + V r e s e t V(t)=\sum_{i}w_{i}\sum_{t_{i}}K(t-t_{i})+V_{reset} V(t)=iwitiK(tti)+Vreset其中, w i w_{i} wi是第 i i i个输入的权重,也可以看作是所连接的突触的权重; t i t_{i} ti是第 i i i个输入的脉冲发射时刻 K ( t − t i ) K(t-t_{i}) K(tti)是由于输入脉冲引发的突触后膜电位(postsynaptic potentials,PSPs); V r e s e t V_{reset} Vreset是Tempotron的重置电位,或者称为静息电位。

其中,关于 K ( t − t i ) K(t-t_{i}) K(tti)是一个关于 t i t_{i} ti的函数(PSP Kernel),当 t ≥ t i t\geq t_{i} tti时,其函数表达为: K ( t − t i ) = V 0 ( e x p ( − t − t i τ ) − e x p ( − t − t i τ s ) ) K(t-t_{i})=V_{0}(exp(-\frac{t-t_{i}}{\tau})-exp(-\frac{t-t_{i}}{\tau_{s}})) K(tti)=V0(exp(τtti)exp(τstti)) t < t i t<t_{i} t<ti时,其函数表达为: K ( t − t i ) = 0 K(t-t_{i})=0 K(tti)=0其中, V 0 V_{0} V0是归一化系数,使得函数的最大值为1; τ \tau τ是膜电位时间常数,可以看出输入的脉冲在Tempotron上会引起瞬时的电位激增,但之后会呈指数衰减; τ s \tau_{s} τs是突触电流的时间常数,这一项的存在表示突触上传导的电流会随时间衰减。

单个的Tempotron可以作为一个二分类器,分类结果的判别,是看Tempotron的膜电位在仿真周期内是否超过阈值:
fig2
其中 t m a x = a r g m a x t { V t } t_{max}=argmax_{t}\left\{V_{t}\right\} tmax=argmaxt{Vt}。从Tempotron的输出结果也可以看出,Tempotron只能发射不超过1个脉冲。单个Tempotron只能做二分类,但多个Tempotron就可以做多分类。

关于Tempotron的训练,可以使用梯度下降法优化网络参数 w i w_{i} wi,以二分类问题为例,损失函数被定义为仅在分类错误的情况下存在。当实际类别 y y y是1而实际输出 y ^ \widehat{y} y 是0,损失为 V t h r e s h o l d − V t m a x V_{threshold}-V_{t_{max}} VthresholdVtmax;当实际类别是0而实际输出是1,损失为 V t m a x − V t h r e s h o l d V_{t_{max}}-V_{threshold} VtmaxVthreshold。可以统一写为: E = ( y − y ^ ) ( V t h r e s h o l d − V t m a x ) E=(y-\widehat{y})(V_{threshold}-V_{t_{max}}) E=(yy )(VthresholdVtmax)直接对参数求梯度,可以得到:
fig3
因为: ∂ V ( t m a x ) ∂ t m a x = 0 \frac{\partial V(t_{max})}{\partial t_{max}}=0 tmaxV(tmax)=0

基于时间驱动的脉冲神经元

spikingjelly:LIF神经元

后续内容的实验部分将基于第三方库spikingjelly进行。SpikingJelly是一个开源脉冲神经网络深度学习框架,使用PyTorch作为自动微分后端,利用C++和CUDA扩展进行性能增强,同时支持CPU和GPU。框架中包含数据集,可视化,深度学习三大模块。

安装spikingjelly:

pip install spikingjelly
  • 1

在spikingjelly中,约定只能输出脉冲,即0或1的神经元,都可以称之为"脉冲神经元",使用脉冲神经元的网络,进而也可以称之为脉冲神经网络。在spikingjelly.clock_driven.neuron中定义了各种常见的脉冲神经元模型,我们以spikingjelly.clock_driven.neuron.LIFNode为例进行了解。

首先导入相关模块:

import torch
import torch.nn as nn
import numpy as np
from spikingjelly.clock_driven import neuron
from spikingjelly import visualizing
import matplotlib.pyplot as plt
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

新建一个LIF神经元构成的网络层:

lif=neuron.LIFNode(tau=100.0)
  • 1

LIF神经元的参数有以下:

  • tau:膜电位时间常数;
  • v_threshold:神经元的阈值电压;
  • v_reset:神经元的重置电压。如果不为None,当神经元发射脉冲后,电压会被重置为v_reset;如果设置为None,则电压会被减去v_threshold;
  • surrogate_function:反向传播时,脉冲函数的替代函数。

其中 surrogate_function 参数,在前向传播时的行为与阶跃函数完全相同。

神经元的数量是在初始化或调用 reset() 函数重新初始化后,根据第一次接收输入的 shape 自动决定的。

与RNN中的神经元非常类似,脉冲神经元也是有状态的,或者说是有记忆。脉冲神经元的状态变量,一般是它的膜电位 V ( t ) V(t) V(t)(发射脉冲前),因此,spikingjelly.clock_driven.neuron中的神经元,都有对象v,可以打印刚刚新建神经元的膜电位:

print(lif.v) # 0.0
  • 1

电位是0,因为我们没有给它任何输入,我们给几个不同的输入,观察神经元的电压shape

x = torch.rand(size=[2, 3])
lif(x)
print('x.shape', x.shape, 'lif.v.shape', lif.v.shape)
# x.shape torch.Size([2, 3]) lif.v.shape torch.Size([2, 3])
lif.reset()

x = torch.rand(size=[4, 5, 6])
lif(x)
print('x.shape', x.shape, 'lif.v.shape', lif.v.shape)
# x.shape torch.Size([4, 5, 6]) lif.v.shape torch.Size([4, 5, 6])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

回顾前面的LIF神经元状态更新方程(充电): τ m d V ( t ) d t = − ( V ( t ) − V r e s e t ) + X ( t ) \tau_{m}\frac{dV(t)}{dt}=-(V(t)-V_{reset})+X(t) τmdtdV(t)=(V(t)Vreset)+X(t)其中, τ m \tau_{m} τm是膜电位时间常数, V r e s e t V_{reset} Vreset是重置电压,对于这样的微分方程,由于 X ( t ) X(t) X(t)不是常数,所以不易求出解析解,我们用差分方程近似微分: τ m ( V ( t ) − V ( t − 1 ) ) = − ( V ( t − 1 ) − V r e s e t ) + X ( t ) \tau_{m}(V(t)-V(t-1))=-(V(t-1)-V_{reset})+X(t) τm(V(t)V(t1))=(V(t1)Vreset)+X(t)因此可以得到 V ( t ) V(t) V(t)(神经元未发射脉冲前的瞬时电压)的表达式: V ( t ) = f ( V ( t − 1 ) , X ( t ) ) = V ( t − 1 ) + 1 τ m ( − ( V ( t − 1 ) − V r e s e t ) + X ( t ) ) V(t)=f(V(t-1),X(t))=V(t-1)+\frac{1}{\tau_{m}}(-(V(t-1)-V_{reset})+X(t)) V(t)=f(V(t1),X(t))=V(t1)+τm1((V(t1)Vreset)+X(t))不同的神经元,充电方程不尽相同。但膜电位超过阈值电压后,释放脉冲,以及释放脉冲后,膜电位的重置都是相同的。因此它们全部继承自 spikingjelly.clock_driven.neuron.BaseNode,共享相同的放电、重置方程。

surrogate_function() 在前向传播时是阶跃函数,只要输入大于或等于0,就会返回1,否则会返回0。我们将这种元素仅为0或1的 tensor 视为脉冲。

发射脉冲消耗了神经元累积的电荷,因此膜电位会有一个瞬时的降低,即电压重置,在spikingjelly中,设置了两种重置方式,根据spikingjelly.clock_driven.neuron 中的构造函数的参数之一 v_reset选择:

  • Hard:v_reset=1.0(默认值为1.0),释放脉冲后,膜电位直接被设置成重置电压: V ( t ) = V r e s e t V(t)=V_{reset} V(t)=Vreset
  • Soft:v_reset=None,释放脉冲后,膜电位减去阈值电压: V ( t ) = V ( t ) − V t h r e s h o l d V(t)=V(t)-V_{threshold} V(t)=V(t)Vthreshold

实验仿真

接下来,我们将逐步给与神经元输入,并查看它的膜电位和输出脉冲。

现在我们给与LIF神经元层持续的输入,并画出其放电后的膜电位和输出脉冲:

lif.reset()
x = torch.as_tensor([2.0])
T = 150
s_list = [] # 输出脉冲
v_list = [] # 隐藏状态:膜电位
for t in range(T):
    s_list.append(lif(x))
    v_list.append(lif.v)

visualizing.plot_one_neuron_v_s(np.asarray(v_list), np.asarray(s_list), v_threshold=lif.v_threshold, v_reset=lif.v_reset,
                                dpi=200)
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

fig4
我们给与的输入shape=[1](固定数值2.0),因此这个神经元层只有一个神经元。它的膜电位(Membrane Potentials)和输出脉冲随着时间变化情况如上图。

现在将该层神经元重置(重新初始化),并给与shape=[32]随机数组输入,查看这32个神经元的膜电位和输出脉冲:

lif.reset()
x = torch.rand(size=[32]) * 4
T = 50
s_list = [] # 32个神经元在50步仿真下输出的脉冲
v_list = [] # 32个神经元在50步仿真下的膜电位
for t in range(T):
    s_list.append(lif(x).unsqueeze(0)) # unsqueeze(0)增加首维度,lif(x).unsqueeze(0) shape=[1,32]
    v_list.append(lif.v.unsqueeze(0))

s_list = torch.cat(s_list) # shape=[50,32]
v_list = torch.cat(v_list) # shape=[50,32]

visualizing.plot_2d_heatmap(array=np.asarray(v_list), title='Membrane Potentials', xlabel='Simulating Step',
                            ylabel='Neuron Index', int_x_ticks=True, x_max=T, dpi=200)
visualizing.plot_1d_spikes(spikes=np.asarray(s_list), title='Spiking', xlabel='Simulating Step',
                           ylabel='Neuron Index', dpi=200)
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

fig5

fig6
从Spiking中可以看出,有部分神经元发射了脉冲,有的却没有发射。我们可以想像,在重复输入一个信息对象的过程中,该信息对象对应的某个特征持续被感知到,迫使该神经元膜电位上升而作出发射脉冲的行为。

从上面的仿真可以看出,脉冲神经元层可以接收连续数值张量,但只能输出脉冲,如果仅包含LIF脉冲神经元层,模型不需要训练(因为没有可学习参数)。

对比传统的神经元,传统神经元的激活是瞬时的,缺少时间尺度上提供的信息,从生物角度看,此刻兴奋的神经元在下一时刻应该惯性地也比较兴奋。这就引出了脉冲神经网络(SNN)的想法,神经元的兴奋度不应该是一瞬间更新的,而是具有慢慢衰减的表现,在持续输入信息的刺激下,最后激活的兴奋区才是预测判别结果。SNN更贴近生物角度,适合处理与脉冲序列有关的应用前景。

研究问题随笔
通过观察LIF神经元,发现神经元缺少知识,若想嵌入到深度学习模型,只能作为替代激活函数的一个工具。LIF神经元只是模拟了生物上信息的传递与神经元电位的改变,后续研究或许可以在LIF的机理上进行修改,为神经元增加知识。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/647555
推荐阅读
相关标签
  

闽ICP备14008679号