人工神经网络(ANN)训练的高能耗和计算成本往往令人望而却步。此外,它们在学习简单时序任务方面的困难和无能似乎困扰着研究社区。然而,我们可以观察到自然智能以微小的能量消耗实现创造力、问题解决和多任务处理。生物系统似乎通过自然进化掌握了信息处理和响应。理解它们如此有效的原因并适应这些发现的需求催生了脉冲神经网络(SNN)。
在本文中,我们将涵盖SNN的理论和在PyTorch中的简化实现。
生物神经元细胞的行为不像我们在ANN中使用的神经元。但是什么使它们不同呢?一个主要区别是生物神经元可以处理的输入和输出信号。生物神经元的输出不是要传播的浮点数。相反,输出是由细胞中离子运动引起的电流。这些电流通过突触向前移动到连接的细胞。当传播的电流到达下一个细胞时,会增加它们的膜电位,这是这些离子浓度不平衡的结果。当神经元的膜电位超过某个阈值时,细胞会发出一个脉冲,即要传递给前向细胞的电流。为此,利用我们对典型ANN的理解,我们可以将神经元的输出映射为二进制值,其中1表示脉冲在时间上的存在,而突触作为连接两个神经元的权重。
从这种映射中产生的生物神经元的另一个特征是异步通信和信息处理。典型的ANN同步传输信息,其中在一个步骤中,一层读取输入,计算输出,并将其前馈。另一方面,生物神经元依赖于时间维度。在任何时刻,它们都可以接收输入信号并产生输出,而不管其他神经元的行为如何。
总之,生物神经元具有内部动力学,导致它们随时间变化。随着时间的推移,它们倾向于放电并降低其膜电位。因此,稀疏的输入脉冲不会导致神经元放电。
为了进一步理解生物神经元的行为,我们将看一个例子。我们将使用Leaky Integrate-and-Fire模型对神经元进行建模,这是用于SNN的最简单模型之一。
Leaky Integrate-and-Fire(LIF)模型可以表示为电阻-电容电路(RC电路),具有向电路提供脉冲的输入和导致...好吧,我们将看到它做什么的阈值开关。
简而言之,电路分析的解导致随时间指数衰减,并伴随值的突然增加。更详细地说,让我们将稳态电压设置为0。当输入电流以值V到达时,电压将增加V伏特。然后,该电压将开始指数衰减,直到第二个脉冲到达。当输入脉冲导致电压超过阈值(即1伏特)时,神经元自身发出脉冲并重置到初始状态。
τₘdVₘ(t)/dt = -(Vₘ - Eₘ) + i_inject/g_leak
在这个方程中,项τₘ = C/g_leak表示一个时间常数,即所谓的膜时间常数,它控制神经元的衰减率。Vₘ和Eₘ分别表示膜的电压和神经元的初始状态电压。输入信号通过项i_inject馈入。在下图中,我们看到当值为0.8的输入脉冲到达时神经元的响应。指数衰减可以容易地显示,但这里我们没有超过阈值1。因此神经元既不重置其电压也不发出脉冲。
LIF神经元模型允许我们通过操纵其超参数(如衰减率或阈值)来控制其动力学。让我们考虑控制衰减率的情况。增加衰减率将导致神经元在时间上更稀疏地放电,或在时间上关联事件。在下图中,我们看到时间维度上的输入脉冲。脉冲在时间0.075s、0.125s、0.2s等到达神经元。
之后,我们观察到三个神经元的响应。第一个神经元的衰减率为0.05(1/200),输入脉冲的值为0.5(突触的权重为0.5)。因此,神经元没有达到阈值,因为其电压迅速下降,所以神经元不发出任何脉冲。将衰减率降低到0.025(1/400)和突触权重降低到0.35导致神经元在时间0.635s放电一次。然而,我们可能希望神经元在"打包"事件时放电,如我们的例子中,在区间(0, 0.2)和(0.6, 0.8)。我们看到通过将突触权重调整回0.5来实现期望的结果。
神经元的这种内部动态和网络的异步行为一直是SNN的关键。在困难的时空任务和神经形态硬件研究中已经获得了值得注意的结果。后者以低能耗实现SNN。有人可能会问复杂数据(如图像)如何馈送到网络。像MNIST这样的图像数据集包含每个像素具有单组值(RGB、灰度等)的图像,而正如我们讨论的,SNN可以处理和处理脉冲序列,即所谓的脉冲序列。脉冲序列是时间窗口上的一系列脉冲。
但是你如何将输入转换为脉冲序列?可以使用两种方法:编码算法和事件相机(用于视觉处理)。
信息编码是获取输入信号并将其转换为脉冲序列的过程。一种这样简单的编码方法是泊松编码,其中要作为输入给神经元的信号的值在最小值和最大值之间归一化。归一化的值表示概率r。在时间窗口T和小时间步长dt上,编码信号的所得脉冲序列在每个时间步长dt具有概率rdt包含脉冲。这样,概率r越高,所得脉冲序列将具有的脉冲越多。因此信息将被更准确地编码。
让我们看一个例子。想象一个大小为32x32像素的灰度图像。位置(1,1)处的像素值为32。归一化该值我们得到r = 32/256 => r = 0.125。在1秒的时间窗口和dt = 0.001秒上,所得脉冲序列将具有大约r*T/dt = 125个脉冲,其总数遵循高斯分布。在每个时间步长0.001,我们期望找到脉冲的概率为0.125。
P(在dt期间1个脉冲) ≈ rdt
P(在Δt期间n个脉冲) = e^(-rΔt) * (rΔt)^n / n!
这种方法显示出高度的生物合理性,因为人类视觉似乎以相同的方式存储第一层神经元的信息。然而,这忽略了在脉冲的确切时间编码的信息。人们可以容易地意识到这一点,因为两个相同的值可能导致不同的脉冲序列。
为了缓解这个问题,已经提出了各种各样的算法,例如Rank Order Coding(ROC)或Population Order Coding(POC)。ROC在给定时间窗口内脉冲到达的顺序中编码信息,第一个脉冲表示信号的最高值。这导致信息丢失。POC通过使用覆盖所有可能值的感受野在多个神经元中编码单个值来解决这个问题。
让我们看两种编码到脉冲的方法,一种基于速率(泊松编码器)和一种基于时间(类似ROC)。看下面的图。
尝试处理这个2乘2的灰度图像,我们意识到它存储为4个整数,范围从0(对于白色)到255(对于黑色)。在ANN中,这组数字是馈送到网络的输入向量。我们想将其转换为给定时间窗口中的脉冲序列;例如1ms,dt为0.02ms。这导致50个离散时间步长。泊松编码方法将产生与给定时间窗口成比例的脉冲数,如下所示。值255将充满脉冲,值207将产生较少,依此类推。另一方面,时间方法将为每个值只产生一个脉冲,但信息在这个脉冲的时间中编码,其中最高值产生较早的脉冲,而较低值产生较晚的脉冲。
两者都有其优点和缺点。即使看一眼自然界和生物大脑如何处理信息,也足以理解两种类型或它们的混合被使用。
编码算法的选择影响神经元输出的结果和网络的一般行为。此外,并非所有开发的学习方案都可以应用于每个编码方案。一些学习方法依赖于在脉冲的确切时间编码的信息,因此像泊松编码算法这样的速率编码算法将不起作用,反之亦然。已经开发了许多更多的算法,如Temporal Contrast、HSA、BSA,可以提供广泛的选择。
另一方面,事件相机,最著名的是动态视觉传感器(DVS),通过将输入(如视频)直接记录到脉冲序列中来克服整个编码过程。
在视频记录中,典型相机为每个时间步(帧)映射每个像素的值。相反,DVS专注于亮度强度差异。当亮度在连续两个时间步之间变化足够大时,相机产生一个"事件"。这个事件存储:a)关于脉冲发生的信息(简单地说,产生了一个脉冲),b)产生脉冲的像素的空间坐标X、Y形式,c)事件发生的时间戳,和d)极性。极性表示阈值是被超过还是被低于。因此,当强度增加超过阈值时,极性将是正的。另一方面,当它减少超过(负)阈值时,它将是负的。极性存储需要"负"脉冲的存在,但它也为网络增加了信息,因为它可以使用这个属性来提取更有意义的结果。
ΔL(xₖ,tₖ) = L(xₖ,tₖ) - (xₖ,tₖ - Δtₖ)
ΔL(xₖ,tₖ) = pₖC
这张图片显示了动态视觉传感器产生的数据。在左边,我们看到来自标准相机的图像。在右边,我们看到由DVS拍摄的同一图片。这里,我们看到传感器产生的上述正负脉冲。再次,DVS获取时空数据。DVS收集和比较时间上的亮度差异。上面的图片没有明确说明这一点。在下图中,我们看到所得记录的时间和空间分量。黄色脉冲表示早期产生的脉冲,而紫色表示晚期产生的脉冲。请注意,此图像不包含极性信息。
有人可能会争辩说,与标准相机相比,这种传感器产生的数据在包含的信息方面落后。但重要的是,DVS:仅使用典型视觉记录器消耗的能量的一小部分,可以操作到高达1MHz的频率,并且不会模糊所得图像或视频。这使它们对研究社区非常有吸引力,并且是能源有限的机器人系统的可能候选者。
你已经看到了如何为我们的网络建模神经元,研究了它的响应,并分析了编码方法以获得我们期望的输入。现在是最重要的过程:网络的训练。人脑如何学习,甚至意味着什么?学习是以期望的方式改变连接神经元的权重的过程。然而,生物神经元使用突触来传递信息。权重变化等同于突触的连接强度,这可以通过导致突触可塑性的过程随时间改变。突触可塑性是突触改变其强度的能力。
"一起放电的神经元,连接在一起"。这个短语描述了众所周知的Hebbian学习和我们将讨论的突触时间依赖性可塑性(STDP)学习方法。STDP是一种无监督方法,基于以下原则。神经元调整其前突触输入脉冲(先前神经元的输入权重)以匹配输入脉冲与输出脉冲的时间。
数学表达式将帮助我们理解。
Δwⱼ = ∑∑W(tᵢⁿ - tⱼᶠ)
让我们考虑一个输入权重w₁。如果来自w₁的脉冲在神经元发出脉冲之后到达,权重减少,因为输入脉冲对输出脉冲没有影响。另一方面,在神经元放电之前到达的脉冲强烈影响神经元脉冲的时间,因此其权重增加以通过这个突触(权重w₁)在时间上连接两个神经元。
由于这个过程,模式出现在神经元的连接中,并且已经表明实现了学习。但STDP不是唯一的学习方法。
SpikeProp是最早使用的学习方法之一。它可以被认为是一种监督STDP学习方法。核心原则是相同的:它基于脉冲时序改变突触权重。与STDP的区别在于,虽然STDP测量前突触和后突触脉冲时序的差异,但这里我们只关注神经元的所得脉冲(后突触)及其时序。由于它是一种监督学习方法,我们需要一个目标值,即输出脉冲的期望放电时间。损失函数和突触的比例权重变化取决于所得脉冲时序与期望脉冲时序的差异。换句话说,我们尝试以这样的方式改变输入突触权重,即输出脉冲的时序与所得脉冲的期望时序匹配。
如果我们看一下损失函数的方程,我们注意到它与STDP权重更新规则的相似性。
E = 1/2 ∑(tⱼᵃ - tⱼᵈ)²
我们考虑输出与期望脉冲时序的差异的事实允许我们:a)创建类来分类数据和b)使用损失函数与反向传播类似的规则来更新神经元的输入权重。
Δwᵢⱼᵏ = -η ∂E/∂wᵢⱼᵏ
或
Δwᵢⱼᵏ = -η yᵢᵏ(tⱼᵃ) δⱼ
项δⱼ是权重更新的关键项,等于以下方程:
δⱼ = (tⱼᵈ - tⱼᵃ) / ∑∑wᵢⱼˡ(∂yᵢˡ(tⱼᵃ)/∂tⱼᵃ)
δ项的适应可以为我们提供具有多个层的SpikeProp的反向传播规则。这种方法说明了SNN的力量。使用SpikeProp,我们可以编码单个神经元来分类给定问题的多个类。例如,如果我们为第一个类设置期望的输出时序为50毫秒,为第二个类设置为75毫秒,单个神经元可以区分多个类。这样做的缺点是神经元将只能访问前50毫秒的信息来决定输入是否属于第一个类。
对脉冲神经网络的稳定增长的兴趣导致了许多尝试开发用于Python的SNN库。仅举几例,Norse、PySNN和snnTorch在简化使用脉冲神经网络的深度学习过程方面做了惊人的工作。请注意,它们还包含完整的文档和教程。
现在,让我们看看如何为我们著名的MNIST数据集创建我们自己的分类器。我们将使用Jason Eshraghian的snnTorch库,因为它使理解网络的架构变得容易。SnnTorch可以被认为是扩展PyTorch库的库。如果你想自己执行代码,你可以从我们的Google colab笔记本中这样做。
让我们使用pip install安装snntorch并导入必要的库
$ pip install snntorchimport snntorch as snn
import torch首先,我们必须加载我们的数据集。如你所知,MNIST数据集是静态的。因此,我们必须将数据编码为脉冲。下面的代码使用速率编码方法通过spikegen.rate()函数产生期望的结果,尽管延迟(时间)和增量调制编码方法已经被实现。你可以自由尝试使用函数spikegen.latency()和spikegen.delta()来查看差异。
我们使用torchvision进行数据的转换和加载。
from torchvision import datasets, transforms
from snntorch import utils
from torch.utils.data import DataLoader
from snntorch import spikegen
# Training Parameters
batch_size=128
data_path='/data/mnist'
num_classes = 10 # MNIST has 10 output classes
# Torch Variables
dtype = torch.float
# Define a transform
transform = transforms.Compose([
transforms.Resize((28,28)),
transforms.Grayscale(),
transforms.ToTensor(),
transforms.Normalize((0,), (1,))])
mnist_train = datasets.MNIST(data_path, train=True, download=True, transform=transform)
mnist_test = datasets.MNIST(data_path, train=False, download=True, transform=transform)
train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=True)
num_steps = 100
# Iterate through minibatches
data = iter(train_loader)
data_it, targets_it = next(data)
# Spiking Data
spike_data = spikegen.rate(data_it, num_steps=num_steps)num_steps变量设置来自静态输入图像的脉冲序列的持续时间。如果我们增加步数,我们看到平均脉冲数趋向于像素的值。你可以尝试用不同的num_steps值运行下面的代码,看看预期的行为是否遵循。
num_steps = 1000
# Iterate through minibatches
data = iter(train_loader)
data_it, targets_it = next(data)
# Spiking Data
spike_data = spikegen.rate(data_it, num_steps=num_steps)
img = 13
x, y = 12, 12
print(float(sum(spike_data[:,img,0,x,y])/num_steps), data_it[img,0,x,y])现在,是时候创建神经元了。最简单编码的是Leaky Integrate and Fire神经元模型(LIF)。代码如下所示:
def leaky_integrate_and_fire(mem, x, w, beta, threshold=1):
spk = (mem > threshold) # if membrane exceeds threshold, spk=1, else, 0
mem = beta * mem + w*x - spk*threshold
return spk, memmem变量保持内部状态,神经元的膜电位,而当电位超过阈值时产生脉冲。我们可以手动实现和运行神经元的模拟(如snnTorch教程中所做的那样编码每一步)。然而,神经元的实例可以用snnTorch实现的一行代码设置:
lif1 = snn.Leaky(beta=0.8)为了开发一层LIF神经元,库使用上述命令,并添加典型的torch线性或卷积层。使用下面的代码,我们设置网络的架构:
# layer parameters
num_inputs = 784
num_hidden = 1000
num_outputs = 10
beta = 0.99
# initialize layers
fc1 = nn.Linear(num_inputs, num_hidden)
lif1 = snn.Leaky(beta=beta)
fc2 = nn.Linear(num_hidden, num_outputs)
lif2 = snn.Leaky(beta=beta)我们现在有足够的来运行我们网络的前向迭代。然而,这不是这里要走的路。如果我们继续这样,我们失去PyTorch提供的广泛多样性,如内置优化器和方法。
遵循PyTorch设计模式,我们编码一个类来实现我们的网络,其中forward函数为每一层产生所得的脉冲序列。函数self.lif.init_leaky()初始化层的参数。
import torch.nn as nn
# Network Architecture
num_inputs = 28*28
num_hidden = 1000
num_outputs = 10
# Temporal Dynamics
num_steps = 25
beta = 0.95
# Define Network
class Net(nn.Module):
def __init__(self):
super().__init__()
# Initialize layers
self.fc1 = nn.Linear(num_inputs, num_hidden)
self.lif1 = snn.Leaky(beta=beta)
self.fc2 = nn.Linear(num_hidden, num_outputs)
self.lif2 = snn.Leaky(beta=beta)
def forward(self, x):
# Initialize hidden states at t=0
mem1 = self.lif1.init_leaky()
mem2 = self.lif2.init_leaky()
# Record the final layer
spk2_rec = []
mem2_rec = []
for step in range(num_steps):
cur1 = self.fc1(x)
spk1, mem1 = self.lif1(cur1, mem1)
cur2 = self.fc2(spk1)
spk2, mem2 = self.lif2(cur2, mem2)
spk2_rec.append(spk2)
mem2_rec.append(mem2)
return torch.stack(spk2_rec, dim=0), torch.stack(mem2_rec, dim=0)
# Load the network onto CUDA if available
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
net = Net().to(device)为了训练网络,我们将使用通过时间的反向传播方法(BPTT)。这种方法基本上是脉冲序列每个时间步长的反向传播。这意味着我们可以使用Adam优化器来训练我们的网络。我们设置我们的损失函数和优化器:
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=5e-4, betas=(0.9, 0.999))现在,剩下的就是我们网络的训练循环。这个循环的实现类似于其他ANN,不同之处在于运行期间神经元动力学的存储。让我们为一个时期编码训练迭代:
num_epochs = 1
loss_hist = []
test_loss_hist = []
counter = 0
# Outer training loop
for epoch in range(num_epochs):
train_batch = iter(train_loader)
# Minibatch training loop
for data, targets in train_batch:
data = data.to(device)
targets = targets.to(device)
# forward pass
net.train()
spk_rec, mem_rec = net(data.view(batch_size, -1))
# initialize the loss & sum over time
loss_val = torch.zeros((1), dtype=dtype, device=device)
for step in range(num_steps):
loss_val += loss(mem_rec[step], targets)
# Gradient calculation + weight update
optimizer.zero_grad()
loss_val.backward()
optimizer.step()
# Store loss history for future plotting
loss_hist.append(loss_val.item())
# Test set
with torch.no_grad():
net.eval()
test_data, test_targets = next(iter(test_loader))
test_data = test_data.to(device)
test_targets = test_targets.to(device)
# Test set forward pass
test_spk, test_mem = net(test_data.view(batch_size, -1))
# Test set loss
test_loss = torch.zeros((1), dtype=dtype, device=device)
for step in range(num_steps):
test_loss += loss(test_mem[step], test_targets)
test_loss_hist.append(test_loss.item())
# Print test loss
if counter % 50 == 0:
print("Test loss: ", float(test_loss))
counter += 1完成了!随着网络的训练,我们看到测试损失值减少,表明网络在给定数据集上训练的能力。我们必须提到,上述网络的设置不需要输入作为脉冲序列传递,而是处理MNIST数据集的每个像素的数值。
在本文中,我们只触及了SNN训练算法的表面。STDP的属性可以在更复杂的设置中实现,即使是监督学习算法,而SpikeProp似乎不是多感知器层的合适选择。基于生物过程或借用ANN学习算法思想的广泛算法已经被实现。这样有趣的想法是常见的通过时间的反向传播(BPTT),其最近继承者E-prop,基于本地学习的DECOLLE,和基于内核的SuperSpike算法。
到现在,我希望你已经掌握了SNN的核心思想。什么是SNN的"脉冲"以及它是如何编码的。神经元的"时间动力学"如何影响网络的行为。我们强烈建议研究snnTorch的文档齐全的教程,并尽可能多地试验超参数,以获得对内部结构发生的情况的更好理解。
@article{korakovounis2021spiking,
title = "Spiking Neural Networks: where neuroscience meets artificial intelligence",
author = "Korakovounis, Dimitrios",
journal = "https://theaisummer.com/",
year = "2021",
url = "https://theaisummer.com/spiking-neural-networks/"
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。