Running PyNN Simulations on SpiNNaker
author: Raul (Zhou Muyu)
date: 2020.07.20
Introduction
本手册介绍使用python上的PyNN库对基于SpiNNaker的神经网络芯片进行开发的基础。
除了基本网络的搭建和介绍,还结合个人学习经历,尽量帮大家避免踩坑。文中个别不确定的翻译,选择用英文原文+个人理解翻译结合的方式呈现,如果错误,还请指正。
Installation
PyNN是一种可在多simulator(如SpiNNaker、BrainScaleS、NEURON、NEST等)上进行神经网络模型搭建的python库。而sPyNNaker是在PyNN基础上,针对SpiNNaker板专门进行封装的python库,其内的大多数功能是对PyNN的调用,并将适用范围缩小到SpiNNaker。
安装可在spynnaker的GitHub官网可以查看到相关信息(传送门),库的安装指导在这。
本手册安装spynnaker8,过程简述如下:
1.spynnaker只在PyNN的0.9.0-0.9.4版本间运行,所以如果电脑里有安装过PyNN或者直接pip安装的话,目前版本为0.9.5,需要卸载重装。
pip install pynn==0.9.4
还需要注意的事,spynnaker在python2.7版本运行。2.创建虚拟环境(如果有,或者直接在主机上装,跳过此步)
可以使用Anaconda或Pycharm进行环境创建,而Mac用户请注意,由于Apple自带了python2.7,并将其作为framework,所以建议重新创建一个python2.7的环境。
conda create --name my_name python=2.7
3.在新建环境中,依次安装如下:
pip install pynn==0.9.4
pip install matplotlib
pip install sPyNNaker8
python -m spynnaker8.setup_pynn
最后一步是安装pyNN-SpiNNaker,在这里如果报错framework问题,则使用
pythonw -m spynnaker8.setup_pynn
解决。4.安装完成后,需要对配置文件进行设置,SpyNNaker提供了3中连接simulation的方法:直接连板子、IP连接、虚拟机模拟。这三种方法的选择,是根据您根目录下的“.spynnaker.cfg”文件中的"[Machine]"下的内容决定的。
a.如果是直接连接SpiNNaker,设置如下两项:
其中,machineName填写SpiNNaker的IP地址或hostname;version根据板子芯片数不同命名不同,例如48芯的对应version为5,machineName默认为192.168.240.1。
b.如果是IP连接的方法,设置如下:
这里为还没有尝试过,读者可自行在文档中阅读。
c.虚拟机连接,设置如下:
首先需将virtual_board设置为True(三种方式必选其一,否则error),然后对板子的规格进行设定(这里width,height是否是表示一块板子上的芯片排列,比如(2,2)就是4个芯片排正方形,关于这个我也不清楚)。
需要注意的事,模拟器方法无法返回数据,适用于DEBUG
到此安装结束,可以到这里用个例子测试一下。
Spiking Neural Networks
生物神经元会突然产生电压的升高,而Spike就是指这种生物行为,又称为脉冲或尖峰(下文统称为脉冲)。脉冲导致了电荷在神经元之间通过突触进行转移。突触连接着发送电荷的突触前神经元和接受电荷的突触后神经元,当突触后神经元累积的电荷超过一定阈值,则会产生一次脉冲。该脉冲沿着神经元的轴突进行传递,经过一段延迟delay后到达神经元的突触,使电荷传递到下个神经元,以此循环。
脉冲会导致神经元刺激(膜电位升高)或抑制(膜电位降低)。我们需要选择适当的模型来模拟膜电位在一段时间内的响应,这是突触模型的选择。
关于SNN
尽管目前而言,非脉冲的DNN使用可微的非线性激活函数进行堆叠,并由反向传播进行基于梯度的参数优化,先进的正则化方法的发明,再加之大型带标签数据集的发展和GPU的开发,DNN已拥有十分强大的泛化能力。但其与大脑最显著的差别在于——信息在单元间传播的方式,这便催生出了SNN。
在大脑中,神经元之间的通信是通过传播动作电位序列(脉冲序列)来完成的。单独脉冲在时间上是稀疏饿的,因此每个脉冲都有很高的信息量,并近似拥有相同均匀的幅值(100mV,脉宽1ms)。因此SNN中的信息是通过脉冲时许川大的,包括延迟和脉冲速率。
SNN在生物学上更加现实直观、更类脑;对于硬件有良好的适用性;节约能源提高效率,但由其传递函数不可微而无法实现反向学习,举步维艰。
科学动机
大脑在嘈杂的环境中识别复杂的视觉模式或听觉目标的能力是深层脉冲网络中嵌入多个处理阶段和多种学习机制的结果。
工程动机
DNN需要耗能大的高端图形卡,脉冲在时间上稀疏的特性可制造低功耗硬件。
一般过程
1.确定编码方式,将样本数据编码为脉冲序列
2.将脉冲序列Si(t)输入SNN得到输出脉冲序列So(t)
3.将期望输出序列Sd(t)与So(t)对比,根据误差调整权重w
难点
1.编码方式,如何将样本信息合理地转化为脉冲序列进行训练
2.如何设计脉冲神经元模型,如何模拟脉冲神经网络
3.如何对序列误差合理定义
部分解决方案
1.延迟编码、相位编码、Time-To-First、Spike编码、BsA编码
2.IF模型、LIF模型、IM模型、HH模型
一些算法
SpikeProp算法用于脉冲网络误差反向学习,为了克服神经元内部状态变量由于脉冲发放而导致的不连续性,限制网络中所有层神经元只能发放一个脉冲。
Tempotron算法认为神经元突触后膜电位是所有与之相连突触前神经元脉冲输入的加权和。
突触可塑性认为,如果两个神经元同时兴奋,则它们之间的突触得以增强,即在一个时间窗口内,当突触后神经元的脉冲出现在突触前神经元之后(突触前神经元发放脉冲后,突触后神经元在一定时间内也发放脉冲),那么就会因此长时程的增强(该突触权重增加),反之则引起长时程抑制(突触权重降低)。
脉冲时序依赖可塑性STDP,强调发放时序不对称的重要性,突触权值自适应调整。
监督Hebbian算法
远程监督学习ReSuMe算法
基于脉冲序列卷积的监督学习算法,由于脉冲序列是由神经元发放脉冲时间所构成的离散事件集合,而导致函数不连续,无法求导,因此引入核函数应用卷积将序列唯一的转换为一个连续函数:
The PyNN Neural Network Description Language
PyNN是python上搭建SNN的建模库,它可以通过将代码微调,让网络可以在不同的simulator上运行。基本的网络创建步骤如下:
1.设置simulator
2.创建神经元集群Population
3.创建Polulation之间的投影projection
4.设置需要记录的数据
5.运行simulator
6.获取并处理记录的数据
下面通过一个简单的示例来描述pynn创建网络的过程:
import pyNN.spiNNaker as sim
import pyNN.utility.plotting as plot
import matplotlib as plt
sim.setup(timestep = 1.0)
sim.set_number_of_neurons_per_core(sim.IF_curr_exp,100)
pop_1 = sim.Population(1,sim.IF_curr_exp(),label='pop_1')
input = sim.Population(1,sim.SpikeSourceArray(spike_times=[0]),label='input')
input_proj = sim.Projection(input,pop_1,sim.OneToOneConnector(),synapse_type=sim.StaticSynapse(weight=5,delay=1))
pop_1.record(["spikes","v"])
simtime = 10
sim.run(simtime)
neo = pop_1.get_data(variables=["spikes","v"])
spikes = neo.segments[0].spiketrains
print spikes
v = neo.segments[0].filter(name='v')[0]
print v
sim.end()
plot.Figure(plot.Panel(v,ylabel="Membrane potential (mV)",
data_labels=[pop_1.label],yticks=True,xlim=(0,simtime)),
plot.Panel(spikes,yticks=True,markersize=5,xlim=(0,simtime)),title="Simple Example",annotations = "Simulated with {}".format(sim.name()))
plt.show()
简单来说,该网络使用的timestep为1ms,创建了一个单输入源(SpikeSourceArray)在0时刻向一个单神经元(IF_curr_exp)发放一个脉冲的网络。这个连接是weighted的,从突触前神经元向突触后神经元的兴奋型突触发放一个大小为5nA的固定电流(脉冲),延迟1ms。记录脉冲和膜电位。使用simulator运行10ms,绘制膜电位和产生的脉冲图。
PoPulation
在PyNN中,神经元都是以Population定义的,一个Population中定义一簇具有相同属性的神经元。PyNN提供了大量的标准神经元模型,最常用的就是Leaky Integrate and Fire model(LIF),在程序中使用IF_curr_exp来表示。
参数 | 默认值 | 含义 |
---|---|---|
tau_m | 20.0 ms | 膜时间常数;RC电路时间常数 |
tau_refrac | 0.1 ms | 不应期 |
v_reset | -65.0 mV | 重置膜电位 |
v_rest | -65.0 mV | 静息电位 |
v_thresh | -50.0 mV | 触发阈值电压 |
tau_syn_E | 5.0 ms | 兴奋型突触时间常数 |
tau_syn_I | 5.0 ms | 抑制型突触时间常数 |
i_offset | 0.0 nA | 偏置电流;每个时间步加入的基本输入电流 |
cm | 1.0 nF | 膜电容 |
PyNN支持既支持基于电流的模型,也支持基于电导的模型。电导模型在此暂不展开。神经元模型的默认值通过initialize函数即可修改,输入状态变量的名称和修改值即可。例如(v是膜电位):
pop_1.initialize(v=-70.0)
该模型将神经元建模为并联的电阻和电容器,当电荷被接收时,会在电容器中积累,但同时会通过电阻泄漏。如果没有累积达到阈值发放脉冲,膜电位则会在一定时间后重归静息电位;如果累积的膜电位达到阈值,则发放一个脉冲,当脉冲发放后,该神经元进入不应期refractory period,一段时间内无法再发送脉冲,不应期过后,恢复正常神经元活动。此外,使用接收的输入电流(上例为5nA)的指数衰变对突出进行建模,即在多个时间步内对输入电流(如果有)进行累加,但同时在每一个时间步之间(无论有无输入电流),电流呈指数衰减。输入电流序列相同,衰减率越大,则突触累积的电荷量越多,这相当于减缓了突触中电荷的泄漏。
除了神经元模型,PyNN还有提供了很多可以用来在网络中模拟输入的实用模型,常见如下两类:
SpikeSourceArray - 以spike_times为时间间隔发放脉冲
PyNN让模型设为SpikeSourceArray的Population中的每一个神经元都在相同的时间发放脉冲,所以spike_times可以是一组关于时间的向量。同时,PyNN也支持实用矩阵(array of arrays)来描述spike_times,每个矩阵定义了每个神经元应该在哪些时刻发放脉冲,比如:spike_times = [[0,2,4],[1,3,5]]表示第一个神经元分别在第0、2、4时刻发放脉冲,第二个神经元在第1、3、5时刻发放脉冲。SpikeSourcePoisson - 在随机时刻以频率rate(每个时间单位发放脉冲数)发放脉冲。
Projection
Projection用来连接Population中的神经元。这是一个定向连接,脉冲从源神经元(突触前神经元)发放到目标神经元(突触后神经元)。Projection连接的可以是两个相同的Population(内连),或者是不同的(外连)。
创建Projection时需要设置connector参数,这是用来具体决定神经元之间的连接有哪些。可以自己设置connector的规则,指定Population_1中的某些神经元与Population_2中的某些神经元的连接,也可以将其设置为库里提供的几种常用连接方式如下:
- OneToOneConnector - neuron_1_i --> neuron_2_i
- AllToAllConnector - neuron_1_i --> neuron_2_all
- FixedProbabilityConnector - 每一个突触前神经元都以p_connect的概率连接每一个突触后神经元
- FromListConnector - 用conn_list(pre_synaptic_neuron_id,post_synaptic_neuron_id,weight,delay)或(pre_synaptic_neuron_id,post_synaptic_neuron_id)来精确连接。需要注意conn_list的维度必须一致,不能出现比如有的带weight有的不带这种情况。如果包含了weight和delay参数,那么将忽略通过Projection的synapse_type提供的参数。
- FixedTotalNumberConnector - 固定神经元连接数量n_synapses,并从可能的连接中随机抽取,并进行替换。值得注意的是这里的连接是可以被替代的。
除此之外,在Projection中还必须定义突触类型,这决定了突触接收/发放脉冲的机制。有三种突触类型(SynapseType),分别是:
Fixed synaptic weight
1.固定突触权重weight和延时delay
synapse_type = pyNN.spiNNaker.StaticSynapse(weight = 0.75, delay = 1.0)
2.用一个分布替代权值
w = pyNN.random.RandomDistribution('gamma',[10, 0.004],rng = pyNN.random.NumpyRNG(seed = 1314)) ,synapse_type = pyNN.spiNNaker.StaticSynapse(weight = w, delay = 0.5)
3.根据突触前后神经元之间的距离来指定延迟
synapse_type = pyNN.spiNNaker.StaticSynapse(weight = w ,delay = "0.2 + 0.01 * d")
- Short-term synaptic plasticity
pyNN目前针对此类型突触(促进和抑制)提供一种标准模型:depressing_synapse = pyNN.spiNNaker.TsodyksMarkramSynapse(weight = w, dealy = 0.2, U = 0.5, tau_rec = 800.0, tau_facil = 0.0) tau_rec = pyNN.random.RandomDistribution('normal',[100.0,10.0]) facilitating_synapse = pyNN.spiNNaker.TsodyksMarkramSynapse(weight = w, dealy = 0.5, U = 0.04, tau_rec = tau_rec
- Spike-timing-dependent plasticity
STDP是一个依赖于突触连接的两个神经元的脉冲时间的学习方式,它的指定方式与其他模型略有不同,STDP突触类型由单独的权重和时间依赖组件构成,如:stdp = pyNN.spiNNaker.STDPMechanism(weight = 0.02,delay = "0.2 + 0.01*d"\ ,timing_dependence = pyNN.spiNNaker.SpikePairRule(tau_plus=20.0, tau_minus=20.0, A_plus = 0.01, A_minus= 0.012)\ ,weight_dependence = pyNN.spiNNaker.AdditiveWeightDependence(w_min = 0, w_max = 0.04))
当一个突触后脉冲紧跟一个突触前脉冲发放时,就假设突触前神经元导致了突触后神经元发放脉冲,连接两个神经元的突触权值变大,这称为“刺激”。
如果一个突触后神经元在突出前神经元发放脉冲之前发放脉冲,那么该突触前神经元不可能引发突触后神经元发放脉冲,所以此时连接这两个神经元的突触权值降低,这被称为“抑制”。
权值改变的大小取决于突触前后神经元发放脉冲的时间,当两个脉冲之间时间间隔变大时,权值呈指数倍下降,如下图所示:
然而,不同的实验会根据情况突出表现不同的突触行为。有些学者还提出,突触前和突触后脉冲的三重态和四重态之间的相关性会导致突出的刺激或抑制。
Random Parameters
通常而言,使用随机权重和延迟,突触类型权重和延迟的值被设定为RandomDistribution类,之后需要指定FromListConnector为(pre_synaptic_neuron_id,post_synaptic_neuron_id)样式的元组。具体函数指定分布的参数与该分布本身的参数一致。RandomDistribution还可以用来指定神经元参数,初始化状态变量等。
Recording Data
在模拟中的所有Population都可以被记录,可以被记录的数据取决于模拟模型。总体上,PyNN允许记录每个神经元发放脉冲的时刻spikes和膜电位v。相反,输入模型SpikeSourceArray、SpikeSourcePoisson只允许记录spikes。在SpiNNaker中,模型还额外允许使用gsyn记录神经元输入。从技术上说,PyNN允许将其保留为在支持该功能的模型(如IF_cond_exp)中记录突触电导,同样也允许在IF_curr_exp之类的模型中记录突触电流。
Running the Simulation
当模型搭建完成并选择好需要记录的数据后,可以使用run函数来开始模拟。run函数可以依次调用多次来继续运行,在每一次run的间隔中,可以更改网络参数。目前,SpiNNaker只支持更改Population的参数如更改i_offset来调整输入的神经元。同时也可以取出运行之间记录的数据。
还可以调用reset函数来将模拟重调回0时刻,之后可以在SpiNNaker模拟中进行更多的更改如增加Population和Prijection等。需要注意的是,这些改动会导致网络重新进行一遍完整的映射,这将比修改参数花费更多时间。
Retrieving and Plotting Data
一旦运行模拟,Population的get_data方法就可以用来取回记录的数据,这些数据会以一个Neo对象的形式保存。每个Neo对象都会有一个重置段(segment)列表,每个重置运行周期一个segment,也就是说如果程序没有调用reset函数,则Neo段列表只有一个segment。脉冲数据spikes可以通过.segents[i].piketrains属性来获取。在Population的每一个神经元中都有一个SpikeTrain,每一个SpikeTrain可以是一个描述神经元在整个模拟过程中的哪些时刻发放脉冲的numpy向量。
cell.record("spikes") # record the spikes data
cell_neo = cell.get_data("spikes") # return a Neo object
cell_neo.segments[0].spiketrains # spikes data
其他数据可以通过调用segments[0].filter(name =
Neo.segments[0].spiketrains和Neo.segments[0].filter(name = < >)[0]的结果都可以被传递给pyNN.utility.plotting.Panel来做图。spynnaker8.spynakker_plotting包含了一个SpynakkerPanel对象,它还可以以稍微快一些的速度绘制脉冲时序图,显示模拟信号数据的heatmap。
Using PyNN with SpiNNaker
当使用SpiNNaker时,延迟的范围在1个时间步(timesteps)到144个时间步之间,因此当时间步为1.0ms时,delay的范围是1.0ms到144.0ms;当时间步为0.1ms时,delay的范围时0.1ms到14.4ms。
SpiNNaker板子上每个核默认的神经元大小为255个,当超过这个大小时,板子会通过软件自动将其分为255个神经元一组的块。注意核还会用来做其他事,比如输入资源、延迟拓展(当有任意一个delay超过16个时间步)、减少神经元可用的核数。
STDP in PyNN
使用STDP创建网络的步骤和上文描述的基本相同,最主要的区别在于这些Projection需要使用STDPMechanism来描述突触可塑性。下面是一个使用STDP创建Projection的例子:
timing_rule = pynn.spiNNaker.SpikePairRule(tau_plus = 20.0, tau_minus = 20.0, A_plus = 0.5, A_minus = 0.5)
weight_rule = pynn.spiNNaker.AdditiveWeightDependence(w_max = 5.0, w_min = 0.0)
stdp_model = pynn.spiNNaker.STDPMechanism(timing_dependence = timing_rule, weight_dependence = weight_rule, weight = 0.0, delay = 5.0)
stdp_projection = pynn.spiNNaker.Projection(pre_pop, post_pop, pynn.spiNNaker.OneToOneConnector(), synapse_type = stdp_model)
首先创建时间规则,本例中采用SpikePairRule规则,它根据突触前后一对神经元发放脉冲的相对时间来更新突触权重,它有四个参数,tau_plus和tau_minus描述权重的大小随突触前后神经元发放脉冲的时间的推移呈指数衰减(这两个参数是描述该指数分布的简化参数应该),刺激时延迟为tau_plus,抑制时延迟为tau_minus。参数A_plus和A_minus分别定义了在刺激过程中增加的最大权重和在抑制过程中减去的最大权重(描述权重变化的大小)。
接下来创建权重更新规则,本例中采用AdditiveWeightDependence,它通过简单的增加电流权重来更新突触权值。w_max和w_min分别定义了突触权重的最大值和最小值(描述权重上下限)。注意,实际增加或减少的大小是在时间规则中定义的,因为这要根据脉冲之间的时间间隔来决定变化量的大小。
除此之外,还支持MultiplicativeWeightDependence,突触权值的改变取决于当前突触权重和突触权重极限值的差,和w_max的差别用于刺激,和w_min的差别用于抑制,接着用A_plue和A_minus的值分别乘上该差值即可得出最大的权重变化量。同样,实际权值大小取决于时间规则和脉冲之间的时间。
时间与权重规则和延迟和时间结合在一起,形成一个描述整个所需机制的单个STDPMechanism对象。Projection仍需要指定connector策略。该connector还用于描述突触前后神经元之间的整体连通性。初始权重最好设置在w_min和w_max之间,如果不在也不会报错,但当第一次权重更新时,权值会自动调整到该范围之内。
需要注意,尽管在SpiNNaker上可以使用STDP让很多的Projection指向同一个目标Population,但也有一些限制,所有的Projection都必须拥有相同的规则(参数一致)。这是由于在每个核上的本地可用限制,从而减少了可为参数保留的数据量。
还需要注意的是,在SpiNNaker上使用STDP,可塑性机制当且仅当突触后神经元收到第二个突触前神经元发放的脉冲时才生效。因此至少拥有两个突触前(神经元,这里不确定是两个需要两个神经元,还是一个神经元发放两次脉冲)脉冲才可以激活该机制。
Getting Synaptic Data
一个Projection中声明的权重和延迟可以通过使用Projection的get方法获取,指定要获取的数据项,包括权重weight、延迟delay或STDP机制包括的参数和它们被使用的格式信息等。支持list格式,返回值由一个被选中值的元祖列表组成;array格式,每个值都以二维矩阵返回,该矩阵由发送端Population中的源神经元和接受端Population中的目标神经元索引而来。在list的结果中,每个元组还包含源神经元ID和目标神经元ID,作为元组的第0个和第1个值。在array结果中,缺失的连接用“NaN”代表,而有多个连接的位置的值则累加表示。
在SpiNNaker上,可以在调用PyNN的run函数前检索投影数据,但直到调用完run函数后才能检查此数据。这是因为在调用run函数前,不会生成单个连接数据。