From e424de89ca9b2ad87856c80637688329712a2ab2 Mon Sep 17 00:00:00 2001 From: rlin50 Date: Fri, 22 Dec 2023 15:53:31 -0800 Subject: [PATCH 1/4] t2 3 4 5 uploaded --- docs/tutorials/zh-cn/tutorial_2_cn.rst | 888 +++++++++++++++++++++++++ docs/tutorials/zh-cn/tutorial_3_cn.rst | 432 ++++++++++++ docs/tutorials/zh-cn/tutorial_4_cn.rst | 351 ++++++++++ docs/tutorials/zh-cn/tutorial_5_cn.rst | 718 ++++++++++++++++++++ 4 files changed, 2389 insertions(+) create mode 100644 docs/tutorials/zh-cn/tutorial_2_cn.rst create mode 100644 docs/tutorials/zh-cn/tutorial_3_cn.rst create mode 100644 docs/tutorials/zh-cn/tutorial_4_cn.rst create mode 100644 docs/tutorials/zh-cn/tutorial_5_cn.rst diff --git a/docs/tutorials/zh-cn/tutorial_2_cn.rst b/docs/tutorials/zh-cn/tutorial_2_cn.rst new file mode 100644 index 00000000..1fae6db6 --- /dev/null +++ b/docs/tutorials/zh-cn/tutorial_2_cn.rst @@ -0,0 +1,888 @@ +====================================================== +教程(二) - Leaky Integrate-and-Fire(LIF)神经元 +====================================================== + +本教程出自 Jason K. Eshraghian (`www.ncg.ucsc.edu `_) + + `English `_ + +.. image:: https://colab.research.google.com/assets/colab-badge.svg + :alt: Open In Colab + :target: https://colab.research.google.com/github/jeshraghian/snntorch/blob/master/examples/tutorial_2_lif_neuron.ipynb + +snnTorch 教程系列基于以下论文。如果您发现这些资源或代码对您的工作有用, 请考虑引用以下来源: + + `Jason K. Eshraghian, Max Ward, Emre Neftci, Xinxin Wang, Gregor Lenz, Girish + Dwivedi, Mohammed Bennamoun, Doo Seok Jeong, and Wei D. Lu. “Training + Spiking Neural Networks Using Lessons From Deep Learning”. arXiv preprint arXiv:2109.12894, + September 2021. `_ + +.. note:: + 本教程是不可编辑的静态版本。交互式可编辑版本可通过以下链接获取: + * `Google Colab `_ + * `Local Notebook (download via GitHub) `_ + + +简介 +------------- + +在本教程中, 你将: + +* 学习leaky integrate-and-fire (LIF) 神经元模型的基础知识 +* 使用snntorch实现一阶LIF神经元 + +安装 snnTorch 的最新 PyPi 发行版: + +:: + + $ pip install snntorch + +:: + + # imports + import snntorch as snn + from snntorch import spikeplot as splt + from snntorch import spikegen + + import torch + import torch.nn as nn + + import numpy as np + import matplotlib.pyplot as plt + + +1. 神经元模型的分类 +--------------------------------------- + +神经元模型种类繁多, 从精确的生物物理模型(比如说Hodgkin-Huxley模型) +到极其简单的人工神经元, 它们遍及现代深度学习的所有方面。 + +**Hodgkin-Huxley Neuron Models**\ :math:`-`\ 虽然生物物理模型可以高度准确地 +再现电生理结果, 但其复杂性使其目前难以使用。 + +**Artificial Neuron Model**\ :math:`-`\ 人工神经元则是另一方面。 +输入乘以相应的权重, 然后通过激活函数。这种简化使深度学习研究人员在计算机视觉、 +自然语言处理和许多其他机器学习领域的任务中取得了令人难以置信的成就。 + +**Leaky Integrate-and-Fire Neuron Models**\ :math:`-`\ Leaky Integrate-and-Fire(LIF) +神经元模型处于两者之间的中间位置。它接收加权输入的总和, 与人工神经元非常相似。 +但它并不直接将输入传递给激活函数, 而是在一段时间内通过泄漏对输入进行累积, +这与 RC 电路非常相似。如果累积值超过阈值, 那么 LIF 神经元就会发出电压脉冲。 +LIF 神经元会提取出输出脉冲的形状和轮廓;它只是将其视为一个离散事件。 +因此, 信息并不是存储在脉冲中, 而是存储在脉冲的时长(或频率)中。 +简单的脉冲神经元模型为神经代码、记忆、网络动力学以及最近的深度学习提供了很多启示。 +LIF 神经元介于生物合理性和实用性之间。 + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_1_neuronmodels.png?raw=true + :align: center + :width: 1000 + +不同版本的 LIF 模型都有各自的动态特性和用途。snnTorch 目前支持以下 LIF 神经元: + +* Lapicque’s RC 模型: ``snntorch.Lapicque`` +* 一阶模型: ``snntorch.Leaky`` +* 基于突触电导的神经元模型: ``snntorch.Synaptic`` +* 递归一阶模型: ``snntorch.RLeaky`` +* 基于递归突触电导的神经元模型: ``snntorch.RSynaptic`` +* Alpha神经元模型: ``snntorch.Alpha`` + +当然也包含一些非LIF脉冲神经元。 +本教程主要介绍其中的第一个模型。它将被用来建立 `以下其他模型 `_. + +2. Leaky Integrate-and-Fire(LIF) 神经元模型 +-------------------------------------------------- + +2.1 脉冲神经元: 灵感 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +在我们的大脑中, 一个神经元可能与1000 - 10000个其他神经元相连。 +如果一个神经元脉冲, 所有下坡神经元都可能感受到。但是, 是什么决定了 +神经元是否会出现峰值呢?过去一个世纪的实验表明, 如果神经元在输入时受到 +*足够的* 刺激, 那么它可能会变得兴奋, 并发出自己的脉冲。 + +这种刺激从何而来?它可以来自: + +* 外围感官, +* 一种侵入性的电极人工地刺激神经元, 或者在多数情况下, +* 来自突触前神经元。 + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_2_intuition.png?raw=true + :align: center + :width: 600 + +考虑到这些脉冲电位是非常短的电位爆发, +不太可能所有输入尖峰电位都精确一致地到达神经元体。这表明有时间动态在 +‘维持’ 输入脉冲, 就像是延迟. + +2.2 被动细胞膜 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +与所有细胞一样, 神经元周围也有一层薄薄的膜。这层膜是一层脂质双分子层, +将神经元内的导电生理盐水, 与细胞外介质隔离开来。 +在电学上, 被绝缘体隔开的两种导电溶液就像一个电容器。 + +这层膜的另一个作用是控制进出细胞的物质 (比如说钠离子Na\ :math:`^+`). +神经元膜通常不让离子渗透过去, 这就阻止了离子进出神经元体。但是, +膜上有一些特定的通道, 当电流注入神经元时, 这些通道就会被触发打开。 +这种电荷移动用电阻器来模拟。 + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_3_passivemembrane.png?raw=true + :align: center + :width: 450 + +下面的代码块将从头开始推导LIF神经元的行为。如果你想跳过数学, 那请继续往下翻; +在推导之后, 我们将采用更实际的方法来理解LIF神经元动力学。 + +------------------------ + +**选读: LIF神经元模型的推导** + +现在假设一些任意的时变电流 :math:`I_{\rm in}(t)` 注入了神经元, +可能是通过电刺激, 也可能是来自其他神经元。 电路中的总电流是守恒的, 所以: + +.. math:: I_{\rm in}(t) = I_{R} + I_{C} + +根据欧姆定律, 神经元内外测得的膜电位 :math:`U_{\rm mem}` 与通过电阻的电流成正比: + +.. math:: I_{R}(t) = \frac{U_{\rm mem}(t)}{R} + +电容是神经元上存储的电荷 :math:`Q` 与 :math:`U_{\rm mem}(t)`之间的比例常数: + +.. math:: Q = CU_{\rm mem}(t) + +电荷变化率给出通过电容的电流: + +.. math:: \frac{dQ}{dt}=I_C(t) = C\frac{dU_{\rm mem}(t)}{dt} + +因此: + +.. math:: I_{\rm in}(t) = \frac{U_{\rm mem}(t)}{R} + C\frac{dU_{\rm mem}(t)}{dt} + +.. math:: \implies RC \frac{dU_{\rm mem}(t)}{dt} = -U_{\rm mem}(t) + RI_{\rm in}(t) + +等式右边的单位是电压 **\[Voltage]**。在等式的左边, :math:`\frac{dU_{\rm mem}(t)}{dt}` 这一项的单位是 **\[Voltage/Time]**. 为了让等式的两边的单位相等 (都为电压), +:math:`RC` 的单位必须是 **\[Time]**. 我们称 :math:`\tau = RC` 为电路的时间常数: + +.. math:: \tau \frac{dU_{\rm mem}(t)}{dt} = -U_{\rm mem}(t) + RI_{\rm in}(t) + +被动细胞膜此时成为了一个线性微分方程。 + +函数的导数要与原函数的形式相同, 即, :math:`\frac{dU_{\rm mem}(t)}{dt} \propto U_{\rm mem}(t)`, +这意味着方程的解是带有时间常数 :math:`\tau`的指数函数。 + +假设神经元从某个值 :math:`U_{0}` 开始, 也没什么进一步的输入, +即 :math:`I_{\rm in}(t)=0.` 其线性微分方程的解最终是: + +.. math:: U_{\rm mem}(t) = U_0e^{-\frac{t}{\tau}} + +整体解法如下所示: + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_4_RCmembrane.png?raw=true + :align: center + :width: 450 + +------------------------ + + +**选读: 前向欧拉法解LIF神经元模型** + +我们设法找到了 LIF 神经元的解析解, 但还不清楚这在神经网络中会有什么用处。 +这一次, 让我们改用前向欧拉法来求解之前的线性常微分方程(ODE)。 +这种方法看似繁琐, 但却能为我们提供 LIF 神经元的离散、递归形式。 +一旦我们得到这种解法, 它就可以直接应用于神经网络。与之前一样, 描述 RC 电路的线性 ODE 为: + +.. math:: \tau \frac{dU(t)}{dt} = -U(t) + RI_{\rm in}(t) + +:math:`U(t)` 的下标从简省略。 + +首先让我们来在不求极限的情况下解这个导数 +:math:`\Delta t \rightarrow 0`: + +.. math:: \tau \frac{U(t+\Delta t)-U(t)}{\Delta t} = -U(t) + RI_{\rm in}(t) + +对于足够小的 :math:`\Delta t`, 这给出了连续时间积分的一个足够好的近似值。 +在下一时间段隔离膜, 得出 + +.. math:: U(t+\Delta t) = U(t) + \frac{\Delta t}{\tau}\big(-U(t) + RI_{\rm in}(t)\big) + +下面的函数表示了这个等式: + +:: + + def leaky_integrate_neuron(U, time_step=1e-3, I=0, R=5e7, C=1e-10): + tau = R*C + U = U + (time_step/tau)*(-U + I*R) + return U + +默认参数设置为 :math:`R=50 M\Omega` 与 +:math:`C=100pF` (i.e., :math:`\tau=5ms`). 这与真实的生物神经元相差无几。 + +现在循环这个函数, 每次迭代一个时间段。 +膜电位初始化为 :math:`U=0.9 V`, 也假设没有任何注入电流 :math:`I_{\rm in}=0 A`. +在以毫秒 :math:`\Delta t=1\times 10^{-3}`\ s 为精度的条件下执行模拟。 + + +:: + + num_steps = 100 + U = 0.9 + U_trace = [] # keeps a record of U for plotting + + for step in range(num_steps): + U_trace.append(U) + U = leaky_integrate_neuron(U) # solve next step of U + + plot_mem(U_trace, "Leaky Neuron Model") + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/leaky1.png?raw=true + :align: center + :width: 300 + +这种指数衰减看起来与我们的预期相符! + +3 Lapicque’s LIF Neuron Model +-------------------------------- + +`路易-拉皮克(Louis Lapicque)在 1907 年 `__ +观察到神经膜和 RC 电路之间的这种相似性。他用短暂的电脉冲刺激青蛙的神经纤维, +发现神经元膜可以近似为具有漏电的电容器。我们以他的名字命名 snnTorch 中的基本 LIF 神经元模型, +以此向他的发现表示敬意。 + +Lapicque 模型中的大多数概念都可以应用到其他 LIF 神经元模型中。 +现在是使用 snnTorch 模拟这个神经元的时候了。 + +3.1 Lapicque: 无人工刺激 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +使用下面的代码实现Lapicque的神经元。R & C改为更简单的值, +同时保持之前的时间常数 :math:`\tau=5\times10^{-3}`\ s. + +:: + + time_step = 1e-3 + R = 5 + C = 1e-3 + + # leaky integrate and fire neuron, tau=5e-3 + lif1 = snn.Lapicque(R=R, C=C, time_step=time_step) + +神经元模型现在储存在 ``lif1`` 中。要使用这个神经元: + +**输入** + +* ``spk_in``: :math:`I_{\rm in}` 中的每个元素依次作为输入传递 (现在是0) +* ``mem``: 代表膜电位, 之前写作 :math:`U[t]`, 也作为输入传递。随便将其初始化为 :math:`U[0] = 0.9~V`. + +**输出** + +* ``spk_out``: 下一个时间段的输出脉冲 :math:`S_{\rm out}[t+\Delta t]` (如果产生脉冲则为 ‘1’ ; 如果没有则为 ‘0’ ) +* ``mem``: 下一个时间段的膜电位 :math:`U_{\rm mem}[t+\Delta t]` + +这些都必须是 ``torch.Tensor`` 类型。 + +:: + + # Initialize membrane, input, and output + mem = torch.ones(1) * 0.9 # U=0.9 at t=0 + cur_in = torch.zeros(num_steps) # I=0 for all t + spk_out = torch.zeros(1) # initialize output spikes + +这些值只针对初始时间段 :math:`t=0`. +要分析 ``mem`` 值随着时间的迭代, 我们可以创建一个 ``mem_rec`` 来记录这些值。 + +:: + + # A list to store a recording of membrane potential + mem_rec = [mem] + +是时候运行模拟了! 在每个时间段, ``mem`` 都会被更新并保存在 ``mem_rec`` 中: + +:: + + # pass updated value of mem and cur_in[step]=0 at every time step + for step in range(num_steps): + spk_out, mem = lif1(cur_in[step], mem) + + # Store recordings of membrane potential + mem_rec.append(mem) + + # convert the list of tensors into one tensor + mem_rec = torch.stack(mem_rec) + + # pre-defined plotting function + plot_mem(mem_rec, "Lapicque's Neuron Model Without Stimulus") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/lapicque.png?raw=true + :align: center + :width: 300 + +在没有任何输入刺激的情况下, 膜电位会随时间衰减。 + +3.2 Lapicque: 阶跃输入 +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +现在应用一个在 :math:`t=t_0` 时切换的阶跃电流 :math:`I_{\rm in}(t)`。 +根据线性一阶微分方程: + +.. math:: \tau \frac{dU_{\rm mem}}{dt} = -U_{\rm mem} + RI_{\rm in}(t), + +一般解为: + +.. math:: U_{\rm mem}=I_{\rm in}(t)R + [U_0 - I_{\rm in}(t)R]e^{-\frac{t}{\tau}} + +如果膜电位初始化为 :math:`U_{\rm mem}(t=0) = 0 V`, 那么: + +.. math:: U_{\rm mem}(t)=I_{\rm in}(t)R [1 - e^{-\frac{t}{\tau}}] + +基于这个明确的时间依赖形式, 我们期望 :math:`U_{\rm mem}` 会指数级地 +向 :math:`I_{\rm in}R` 收敛。让我们通过在 :math:`t_0 = 10ms` 时 +触发电流脉冲来可视化这是什么样子。 + +:: + + # 初始化输入电流脉冲 + cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.1), 0) # 输入电流在 t=10 时打开 + + # 初始化膜、输出和记录 + mem = torch.zeros(1) # t=0 时膜电位为0 + spk_out = torch.zeros(1) # 神经元需要一个地方顺序存储输出的脉冲 + mem_rec = [mem] + +这一次, 新的 ``cur_in`` 值传递给了神经元: + +:: + + num_steps = 200 + + # 在每个时间步骤中传递 mem 和 cur_in[step] 的更新值 + for step in range(num_steps): + spk_out, mem = lif1(cur_in[step], mem) + mem_rec.append(mem) + + # 将张量列表合并成一个张量 + mem_rec = torch.stack(mem_rec) + + plot_step_current_response(cur_in, mem_rec, 10) + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/lapicque_step.png?raw=true + :align: center + :width: 450 + +当 :math:`t\rightarrow \infty` 时, 膜电位 :math:`U_{\rm mem}` 指数级地收敛到 :math:`I_{\rm in}R`: + +:: + + >>> print(f"计算得到的输入脉冲 [A] x 电阻 [Ω] 的值为: {cur_in[11]*lif1.R} V") + >>> print(f"模拟得到的稳态膜电位值为: {mem_rec[200][0]} V") + + 计算得到的输入脉冲 [A] x 电阻 [Ω] 的值为: 0.5 V + 模拟得到的稳态膜电位值为: 0.4999999403953552 V + +足够接近! + +3.3 Lapicque: 冲激输入 +~~~~~~~~~~~~~~~~~~~~~~ + +那么如果阶跃输入在 :math:`t=30ms` 处被截断会怎么样呢? + +:: + + # 初始化电流脉冲、膜电位和输出 + cur_in1 = torch.cat((torch.zeros(10), torch.ones(20)*(0.1), torch.zeros(170)), 0) # 输入在 t=10 开始, t=30 结束 + mem = torch.zeros(1) + spk_out = torch.zeros(1) + mem_rec1 = [mem] + +:: + + # 神经元模拟 + for step in range(num_steps): + spk_out, mem = lif1(cur_in1[step], mem) + mem_rec1.append(mem) + mem_rec1 = torch.stack(mem_rec1) + + plot_current_pulse_response(cur_in1, mem_rec1, "Lapicque神经元模型的输入脉冲", + vline1=10, vline2=30) + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/lapicque_pulse1.png?raw=true + :align: center + :width: 450 + +:math:`U_{\rm mem}` 就像对于阶跃输入一样上升, +但现在它会像在我们的第一个模拟中那样以 :math:`\tau` 的时间常数下降。 + +让我们在半个时间内提供大致相同的电荷 :math:`Q = I \times t` 给电路。 +这意味着必须稍微增加输入电流的幅度, 缩小时间窗口。 + +:: + + # 增加电流脉冲的幅度;时间减半。 + cur_in2 = torch.cat((torch.zeros(10), torch.ones(10)*0.111, torch.zeros(180)), 0) # 输入在 t=10 开始, t=20 结束 + mem = torch.zeros(1) + spk_out = torch.zeros(1) + mem_rec2 = [mem] + + # 神经元模拟 + for step in range(num_steps): + spk_out, mem = lif1(cur_in2[step], mem) + mem_rec2.append(mem) + mem_rec2 = torch.stack(mem_rec2) + + plot_current_pulse_response(cur_in2, mem_rec2, "Lapicque神经元模型的输入脉冲:x1/2 脉宽", + vline1=10, vline2=20) + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/lapicque_pulse2.png?raw=true + :align: center + :width: 450 + + +让我们再来一次, 但使用更快的输入脉冲和更大的幅度: + +:: + + # 增加电流脉冲的幅度;时间缩短四分之一。 + cur_in3 = torch.cat((torch.zeros(10), torch.ones(5)*0.147, torch.zeros(185)), 0) # 输入在 t=10 开始, t=15 结束 + mem = torch.zeros(1) + spk_out = torch.zeros(1) + mem_rec3 = [mem] + + # 神经元模拟 + for step in range(num_steps): + spk_out, mem = lif1(cur_in3[step], mem) + mem_rec3.append(mem) + mem_rec3 = torch.stack(mem_rec3) + + plot_current_pulse_response(cur_in3, mem_rec3, "Lapicque神经元模型的输入脉冲:x1/4 脉宽", + vline1=10, vline2=15) + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/lapicque_pulse3.png?raw=true + :align: center + :width: 450 + + +现在将所有三个实验在同一图上进行比较: + +:: + + compare_plots(cur_in1, cur_in2, cur_in3, mem_rec1, mem_rec2, mem_rec3, 10, 15, + 20, 30, "Lapicque神经元模型的输入脉冲:不同的输入") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/compare_pulse.png?raw=true + :align: center + :width: 450 + +随着输入电流脉冲幅度的增加, 膜电位的上升时间加快。 +当输入电流脉冲的宽度趋于无穷小时, :math:`T_W \rightarrow 0s`, +膜电位将在几乎零上升时间内迅速上升: + +:: + + # 当前脉冲输入 + cur_in4 = torch.cat((torch.zeros(10), torch.ones(1)*0.5, torch.zeros(189)), 0) # 输入仅在1个时间步上打开 + mem = torch.zeros(1) + spk_out = torch.zeros(1) + mem_rec4 = [mem] + + # 神经元模拟 + for step in range(num_steps): + spk_out, mem = lif1(cur_in4[step], mem) + mem_rec4.append(mem) + mem_rec4 = torch.stack(mem_rec4) + + plot_current_pulse_response(cur_in4, mem_rec4, "Lapicque神经元模型的输入脉冲", + vline1=10, ylim_max1=0.6) + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/lapicque_spike.png?raw=true + :align: center + :width: 450 + + +当前脉冲的宽度现在如此短, 实际上看起来像脉冲。 +也就是说, 电荷在无限短的时间内传递, :math:`I_{\rm in}(t) = Q/t_0`, +其中 :math:`t_0 \rightarrow 0`。 +更正式地: + +.. math:: I_{\rm in}(t) = Q \delta (t-t_0), + +其中 :math:`\delta (t-t_0)` 是狄拉克-δ函数。从物理角度来看, 不可能“瞬间”存放电荷。 +但积分 :math:`I_{\rm in}` 给出了一个在物理上有意义的结果, +因为我们可以得到传递的电荷: + +.. math:: 1 = \int^{t_0 + a}_{t_0 - a}\delta(t-t_0)dt + +.. math:: f(t_0) = \int^{t_0 + a}_{t_0 - a}f(t)\delta(t-t_0)dt + +在这里, +:math:`f(t_0) = I_{\rm in}(t_0=10) = 0.5A \implies f(t) = Q = 0.5C`。 + +希望您对膜电位在静息状态下泄漏并积分输入电流有了一个很好的感觉。 +这涵盖了神经元的“泄漏(Leaky)”和“累积(Integrate)”部分。那么如何引发“放电(Fire)”呢? + +3.4 Lapicque: 放电 +~~~~~~~~~~~~~~~~~~~~~~ + +到目前为止, 我们只看到神经元对输入的脉冲作出反应。 +要使神经元在输出端产生并发出自己的脉冲, 必须将被动膜模型与阈值结合起来。 + +如果膜电位超过此阈值, 则会在被动膜模型外部生成一个电压脉冲。 + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_4_spiking.png?raw=true + :align: center + :width: 400 + +修改之前的 ``leaky_integrate_neuron`` 函数以添加脉冲响应。 + +:: + + # 用于说明的 R=5.1, C=5e-3 + def leaky_integrate_and_fire(mem, cur=0, threshold=1, time_step=1e-3, R=5.1, C=5e-3): + tau_mem = R*C + spk = (mem > threshold) # 如果膜超过阈值, 则 spk=1, 否则为0 + mem = mem + (time_step/tau_mem)*(-mem + cur*R) + return mem, spk + +设置 ``threshold=1``, 并应用阶跃电流以使该神经元发放脉冲。 + +:: + + # 小步电流输入 + cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.2), 0) + mem = torch.zeros(1) + mem_rec = [] + spk_rec = [] + + # 神经元模拟 + for step in range(num_steps): + mem, spk = leaky_integrate_and_fire(mem, cur_in[step]) + mem_rec.append(mem) + spk_rec.append(spk) + + # 将列表转换为张量 + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + plot_cur_mem_spk(cur_in, mem_rec, spk_rec, thr_line=1, vline=109, ylim_max2=1.3, + title="带无控制放电的LIF神经元模型") + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/lif_uncontrolled.png?raw=true + :align: center + :width: 450 + + +哎呀 - 输出脉冲失控了!这是因为我们忘记了添加复位机制。 +实际上, 每当神经元放电时, 膜电位都应该超极化(hyperpolarizes)回到其静息电位。 + +将此复位机制实施到我们的神经元中: + +:: + + # 带复位机制的LIF + def leaky_integrate_and_fire(mem, cur=0, threshold=1, time_step=1e-3, R=5.1, C=5e-3): + tau_mem = R*C + spk = (mem > threshold) + mem = mem + (time_step/tau_mem)*(-mem + cur*R) - spk*threshold # 每次 spk=1 时, 减去阈值 + return mem, spk + +:: + + # 小步电流输入 + cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.2), 0) + mem = torch.zeros(1) + mem_rec = [] + spk_rec = [] + + # 神经元模拟 + for step in range(num_steps): + mem, spk = leaky_integrate_and_fire(mem, cur_in[step]) + mem_rec.append(mem) + spk_rec.append(spk) + + # 将列表转换为张量 + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + plot_cur_mem_spk(cur_in, mem_rec, spk_rec, thr_line=1, vline=109, ylim_max2=1.3, + title="带复位的LIF神经元模型") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/reset_2.png?raw=true + :align: center + :width: 450 + +现在我们有了一个功能完善的LIF神经元模型, 好耶! + +请注意, 如果 :math:`I_{\rm in}=0.2 A` 并且 :math:`R<5 \Omega`, 那么 :math:`I\times R < 1 V`。如果 ``threshold=1``, 则不会发生放电。请随意返回到上面, 更改值并测试。 + +与之前一样, 通过调用内置的snntorch中的Lapicque神经元模型, 所有这些代码都被压缩: + +:: + + # 使用snntorch创建与之前相同的神经元 + lif2 = snn.Lapicque(R=5.1, C=5e-3, time_step=1e-3) + + >>> print(f"膜电位时间常数: {lif2.R * lif2.C:.3f}s") + "膜电位时间常数: 0.025s" + +:: + + # 初始化输入和输出 + cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.2), 0) + mem = torch.zeros(1) + spk_out = torch.zeros(1) + mem_rec = [mem] + spk_rec = [spk_out] + + # 在100个时间步骤内进行模拟运行。 + for step in range(num_steps): + spk_out, mem = lif2(cur_in[step], mem) + mem_rec.append(mem) + spk_rec.append(spk_out) + + # 将列表转换为张量 + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + plot_cur_mem_spk(cur_in, mem_rec, spk_rec, thr_line=1, vline=109, ylim_max2=1.3, + title="带阶跃输入的Lapicque神经元模型") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/lapicque_reset.png?raw=true + :align: center + :width: 450 + +膜电位呈指数上升, 然后达到阈值, 此时膜电位复位。我们大致可以看到这发生在 :math:`105ms < t_{\rm spk} < 115ms` 之间。出于好奇, 让我们看看脉冲记录实际包括什么内容: + +:: + + >>> print(spk_rec[105:115].view(-1)) + tensor([0., 0., 0., 0., 1., 0., 0., 0., 0., 0.]) + +脉冲的缺失由 :math:`S_{\rm out}=0` 表示, +而脉冲的发生由 :math:`S_{\rm out}=1` 表示。在这里, +脉冲发生在 :math:`S_{\rm out}[t=109]=1`。 +如果您想知道为什么每个这些条目都被存储为张量, 那是因为在未来的教程中, +我们将模拟大规模的神经网络。每个条目将包含许多神经元的脉冲响应, +并且可以将张量加载到GPU内存以加速训练过程。 + +如果增加 :math:`I_{\rm in}`, 则膜电位会更快地接近阈值 :math:`U_{\rm thr}`: + +:: + + # 初始化输入和输出 + cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.3), 0) # 增加电流 + mem = torch.zeros(1) + spk_out = torch.zeros(1) + mem_rec = [mem] + spk_rec = [spk_out] + + # 神经元模拟 + for step in range(num_steps): + spk_out, mem = lif2(cur_in[step], mem) + mem_rec.append(mem) + spk_rec.append(spk_out) + + # 将列表转换为张量 + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + + plot_cur_mem_spk(cur_in, mem_rec, spk_rec, thr_line=1, ylim_max2=1.3, + title="带周期性放电的Lapicque神经元模型") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/periodic.png?raw=true + :align: center + :width: 450 + +通过降低阈值也可以诱发类似的放电频率增加。这需要初始化一个新的神经元模型, 但上面的代码块的其余部分完全相同: + +:: + + # 阈值减半的神经元 + lif3 = snn.Lapicque(R=5.1, C=5e-3, time_step=1e-3, threshold=0.5) + + # 初始化输入和输出 + cur_in = torch.cat((torch.zeros(10), torch.ones(190)*0.3), 0) + mem = torch.zeros(1) + spk_out = torch.zeros(1) + mem_rec = [mem] + spk_rec = [spk_out] + + # 神经元模拟 + for step in range(num_steps): + spk_out, mem = lif3(cur_in[step], mem) + mem_rec.append(mem) + spk_rec.append(spk_out) + + # 将列表转换为张量 + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + plot_cur_mem_spk(cur_in, mem_rec, spk_rec, thr_line=0.5, ylim_max2=1.3, + title="带更低阈值的Lapicque神经元模型") + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/threshold.png?raw=true + :align: center + :width: 450 + +这是一个常数电流注入的情况。但在深度神经网络和生物大脑中, +大多数神经元都将连接到其他神经元。它们更有可能接收脉冲, 而不是持续电流的注入。 + + +3.5 Lapicque: 脉冲输入 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +让我们利用我们在 `教程(一) `_ +中学到的一些技能, 并使用 ``snntorch.spikegen`` 模块创建一些随机生成的输入脉冲。 + +:: + + # 创建一个1-D的随机脉冲序列。每个元素有40%的概率发放。 + spk_in = spikegen.rate_conv(torch.ones((num_steps)) * 0.40) + +运行以下代码块以查看生成了多少脉冲。 + +:: + + >>> print(f"在{len(spk_in)}个时间步骤中, 总共生成了{int(sum(spk_in))}个脉冲。") + There are 85 total spikes out of 200 time steps. + +:: + + fig = plt.figure(facecolor="w", figsize=(8, 1)) + ax = fig.add_subplot(111) + + splt.raster(spk_in.reshape(num_steps, -1), ax, s=100, c="black", marker="|") + plt.title("输入脉冲") + plt.xlabel("时间步骤") + plt.yticks([]) + plt.show() + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/spikes.png?raw=true + :align: center: + :width: 400 + +:: + + # 初始化输入和输出 + mem = torch.ones(1)*0.5 + spk_out = torch.zeros(1) + mem_rec = [mem] + spk_rec = [spk_out] + + # 神经元模拟 + for step in range(num_steps): + spk_out, mem = lif3(spk_in[step], mem) + spk_rec.append(spk_out) + mem_rec.append(mem) + + # 将列表转换为张量 + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + plot_spk_mem_spk(spk_in, mem_rec, spk_out, "具有输入脉冲的Lapicque神经元模型") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/spk_mem_spk.png?raw=true + :align: center: + :width: 450 + + +3.6 Lapicque: Reset Mechanisms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +我们已经从头开始实现了重置机制, 但让我们再深入一点。 +膜电位的急剧下降促进了脉冲生成的减少, 这是有关大脑如何如此高效的一部分理论的补充。 +在生物学上, 膜电位的这种下降被称为“去极化”。 +在此之后, 很短的时间内很难引发神经元的另一个脉冲。 +在这里, 我们使用重置机制来模拟去极化。 + +有两种实现重置机制的方法: + +1. *减法重置*(默认):每次生成脉冲时, 从膜电位中减去阈值; +2. *归零重置*:每次生成脉冲时, 将膜电位强制归零。 +3. *不重置*:不采取任何措施, 让脉冲潜在地不受控制。 + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_5_reset.png?raw=true + :align: center + :width: 400 + +实例化另一个神经元模型, 以演示如何在重置机制之间切换。默认情况下, +snnTorch神经元模型使用 ``reset_mechanism = "subtract"``。 +可以通过传递参数 ``reset_mechanism = "zero"`` 来明确覆盖默认设置。 + +:: + + # 重置机制设置为“zero”的神经元 + lif4 = snn.Lapicque(R=5.1, C=5e-3, time_step=1e-3, threshold=0.5, reset_mechanism="zero") + + # 初始化输入和输出 + spk_in = spikegen.rate_conv(torch.ones((num_steps)) * 0.40) + mem = torch.ones(1)*0.5 + spk_out = torch.zeros(1) + mem_rec0 = [mem] + spk_rec0 = [spk_out] + + # 神经元模拟 + for step in range(num_steps): + spk_out, mem = lif4(spk_in[step], mem) + spk_rec0.append(spk_out) + mem_rec0.append(mem) + + # 将列表转换为张量 + mem_rec0 = torch.stack(mem_rec0) + spk_rec0 = torch.stack(spk_rec0) + + plot_reset_comparison(spk_in, mem_rec, spk_rec, mem_rec0, spk_rec0) + + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/_static/comparison.png?raw=true + :align: center + :width: 550 + + +请特别关注膜电位的演变, 尤其是在它达到阈值后的瞬间。 +您可能会注意到, “重置为零”后, 膜电位被迫在每次脉冲后归零。 + +那么哪种方法更好?应用 ``"subtract"`` (重置机制的默认值)更不会丢失信息, +因为它不会忽略膜电位超过阈值的程度。 + +另一方面, 采用 ``"zero"`` 的强制重置会促进稀疏性, +并在专用的神经形态硬件上运行时可能降低功耗。您可以尝试使用这两种选项。 + +这涵盖了LIF神经元模型的基础知识! + + +Conclusion +--------------- + +实际上,我们可能不会用这个神经元模型来训练神经网络。 +Lapicque LIF 模型增加了很多需要调整的超参数::math:`R`, :math:`C`, :math:`\Delta t`, :math:`U_{\rm thr}`, +以及重置机制的选择。这一切都有点令人生畏。 +因此, `下一个教程 `_ 将取消大部分超参数, +并引入更适合大规模深度学习的神经元模型。 + +如果你喜欢这个项目,请考虑在 GitHub 上给代码仓库点亮星星⭐, +因为这是支持它的最简单的、最好的方式。 + +参考文档在 `这里 `__. + +更多阅读 +--------------- + +- `Check out the snnTorch GitHub project here. `__ +- `snnTorch + documentation `__ + of the Lapicque, Leaky, Synaptic, and Alpha models +- `Neuronal Dynamics: From single neurons to networks and models of + cognition `__ by Wulfram + Gerstner, Werner M. Kistler, Richard Naud and Liam Paninski. +- `Theoretical Neuroscience: Computational and Mathematical Modeling of + Neural + Systems `__ + by Laurence F. Abbott and Peter Dayan diff --git a/docs/tutorials/zh-cn/tutorial_3_cn.rst b/docs/tutorials/zh-cn/tutorial_3_cn.rst new file mode 100644 index 00000000..c6e05056 --- /dev/null +++ b/docs/tutorials/zh-cn/tutorial_3_cn.rst @@ -0,0 +1,432 @@ +================================================ +教程(三) - 一个前馈脉冲神经网络 +================================================ + +本教程出自 Jason K. Eshraghian (`www.ncg.ucsc.edu `_) + +.. image:: https://colab.research.google.com/assets/colab-badge.svg + :alt: Open In Colab + :target: https://colab.research.google.com/github/jeshraghian/snntorch/blob/master/examples/tutorial_3_feedforward_snn.ipynb + +snnTorch 教程系列基于以下论文。如果您发现这些资源或代码对您的工作有用, 请考虑引用以下来源: + + `Jason K. Eshraghian, Max Ward, Emre Neftci, Xinxin Wang, Gregor Lenz, Girish + Dwivedi, Mohammed Bennamoun, Doo Seok Jeong, and Wei D. Lu. “Training + Spiking Neural Networks Using Lessons From Deep Learning”. Proceedings of the IEEE, 111(9) September 2023. `_ + +.. note:: + 本教程是不可编辑的静态版本。交互式可编辑版本可通过以下链接获取: + * `Google Colab `_ + * `Local Notebook (download via GitHub) `_ + + +简介 +------------- + +在本教程中, 你将: + +* 了解如何简化LIF神经元,使其适合深度学习 +* 实现前馈脉冲神经网络(SNN) + +安装 snnTorch 的最新 PyPi 发行版: + +:: + + $ pip install snntorch + +:: + + # imports + import snntorch as snn + from snntorch import spikeplot as splt + from snntorch import spikegen + + import torch + import torch.nn as nn + import matplotlib.pyplot as plt + + +1. 简化的LIF神经元模型 +---------------------------------------------------------- + +在前一个教程中,我们设计了自己的LIF神经元模型。但它相当复杂,并添加了一系列需要调整的超参数, +包括 :math:`R`、:math:`C`、:math:`\Delta t`、:math:`U_{\rm thr}` 和复位机制的选择。 +这是很多需要跟踪的内容,在扩展到完整的SNN时会变得更加繁琐。所以让我们进行一些简化。 + + +1.1 衰减率:beta +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +在之前的教程中,我们使用欧拉方法推导出了被动膜模型的以下解: + +.. math:: U(t+\Delta t) = (1-\frac{\Delta t}{\tau})U(t) + \frac{\Delta t}{\tau} I_{\rm in}(t)R \tag{1} + +现在假设没有输入电流,即 :math:`I_{\rm in}(t)=0 A`: + +.. math:: U(t+\Delta t) = (1-\frac{\Delta t}{\tau})U(t) \tag{2} + +令 :math:`U` 的连续值之比,即 :math:`U(t+\Delta t)/U(t)`,为膜电位的衰减率,也称为逆时间常数: + +.. math:: U(t+\Delta t) = \beta U(t) \tag{3} + +根据 :math:`(1)`,这意味着: + +.. math:: \beta = (1-\frac{\Delta t}{\tau}) \tag{4} + +为了保证合理的准确性,:math:`\Delta t << \tau`。 + +1.2 加权输入电流 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +如果我们假设 :math:`t` 代表序列中的时间步长,而不是连续时间,那么我们可以设置 :math:`\Delta t = 1`。 +为了进一步减少超参数的数量,假设 :math:`R=1`。根据 :math:`(4)`,这些假设导致: + +.. math:: \beta = (1-\frac{1}{C}) \implies (1-\beta)I_{\rm in} = \frac{1}{\tau}I_{\rm in} \tag{5} + +输入电流由 :math:`(1-\beta)` 加权。通过额外假设输入电流瞬间对膜电位产生影响: + +.. math:: U[t+1] = \beta U[t] + (1-\beta)I_{\rm in}[t+1] \tag{6} + +请注意,时间的离散化意味着我们假设每个时间间隔 :math:`t` 足够短,以至于一个神经元在此区间内最多只能发射一个脉冲。 + +在深度学习中,输入的加权因子通常是一个可学习的参数。暂时抛开到目前为止所做的符合物理可行性的假设, +我们将 :math:`(6)` 中的 :math:`(1-\beta)` 效应纳入一个可学习的权重 :math:`W` 中,并相应地用输入 :math:`X[t]` 替换 :math:`I_{\rm in}[t]`: + +.. math:: WX[t] = I_{\rm in}[t] \tag{7} + +这可以这样理解。:math:`X[t]` 是一个输入电压或脉冲,通过 :math:`W` 的突触电导缩放,以产生对神经元的电流注入。这给我们以下结果: + +.. math:: U[t+1] = \beta U[t] + WX[t+1] \tag{8} + +在未来的模拟中,:math:`W` 和 :math:`\beta` 的效应是分开的。:math:`W` 是一个独立于 :math:`\beta` 更新的可学习参数。 + +1.3 脉冲发射和重置 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +我们现在引入脉冲发射和重置机制。回想一下,如果膜电位超过阈值,那么神经元将发射一个输出脉冲: + +.. math:: + + S[t] = \begin{cases} 1, &\text{if}~U[t] > U_{\rm thr} \\ + 0, &\text{otherwise} \end{cases} + +.. math:: + + \tag{9} + +如果触发了脉冲,膜电位应该被重置。 +*通过减法重置*机制可以这样建模: + +.. math:: U[t+1] = \underbrace{\beta U[t]}_\text{decay} + \underbrace{WX[t+1]}_\text{input} - \underbrace{S[t]U_{\rm thr}}_\text{reset} \tag{10} + +由于 :math:`W` 是一个可学习参数,而 :math:`U_{\rm thr}` 通常只是设为 :math:`1` (尽管也可以调整),这样就只剩下衰减率 :math:`\beta` 作为需要指定的唯一超参数。 +这就完成了本教程中繁琐的部分。 + +.. 请注意:: + + 一些实现可能会做出略有不同的假设。 + 例如,:math:`(9)` 中的:math:`S[t] \rightarrow S[t+1]` ,或 + :math:`(10)` 中的:math:`X[t] \rightarrow X[t+1]` 。以上推导是在snntorch中使用的, + 因为我们发现它直观地映射到循环神经网络的表示中,且不会影响性能。 + +1.4 代码实现 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +用 Python 实现这个神经元的代码如下所示: + +:: + + def leaky_integrate_and_fire(mem, x, w, beta, threshold=1): + spk = (mem > threshold) # 如果膜电位超过阈值,spk=1,否则为0 + mem = beta * mem + w*x - spk*threshold + return spk, mem + +为了设置 :math:`\beta`,我们可以选择使用方程 +:math:`(3)` 来定义它,或者直接硬编码。这里,我们将使用 +:math:`(3)` 作为示范,但在未来,我们将直接硬编码,因为我们更关注的是实际效果而不是生物学精度。 + +方程 :math:`(3)` 告诉我们 :math:`\beta` 是 +连续两个时间步骤中膜电位的比率。使用连续时间依赖形式的方程(假设 +没有电流注入)来解决这个问题,这在 `教程 +2 `__ 中已经推导出来了: + +.. math:: U(t) = U_0e^{-\frac{t}{\tau}} + +:math:`U_0` 是在 :math:`t=0` 时初始的膜电位。假设时间依赖方程是在 +:math:`t, (t+\Delta t), (t+2\Delta t)~...~` 的离散步骤中计算的,那么我们可以找到 +连续步骤之间的膜电位比率: + +.. math:: \beta = \frac{U_0e^{-\frac{t+\Delta t}{\tau}}}{U_0e^{-\frac{t}{\tau}}} = \frac{U_0e^{-\frac{t + 2\Delta t}{\tau}}}{U_0e^{-\frac{t+\Delta t}{\tau}}} =~~... + +.. math:: \implies \beta = e^{-\frac{\Delta t}{\tau}} + +:: + + # 设置神经元参数 + delta_t = torch.tensor(1e-3) + tau = torch.tensor(5e-3) + beta = torch.exp(-delta_t/tau) + +:: + + >>> print(f"衰减率是: {beta:.3f}") + 衰减率是: 0.819 + +运行一个快速模拟,以检查神经元对阶跃电压输入的响应是否正确: + +:: + + num_steps = 200 + + # initialize inputs/outputs + small step current input + x = torch.cat((torch.zeros(10), torch.ones(190)*0.5), 0) + mem = torch.zeros(1) + spk_out = torch.zeros(1) + mem_rec = [] + spk_rec = [] + + # neuron parameters + w = 0.4 + beta = 0.819 + + # neuron simulation + for step in range(num_steps): + spk, mem = leaky_integrate_and_fire(mem, x[step], w=w, beta=beta) + mem_rec.append(mem) + spk_rec.append(spk) + + # convert lists to tensors + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + plot_cur_mem_spk(x*w, mem_rec, spk_rec, thr_line=1,ylim_max1=0.5, + title="LIF Neuron Model With Weighted Step Voltage") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial3/_static/lif_step.png?raw=true + :align: center + :width: 400 + + +2. 在 snnTorch 中的Leaky神经元模型 +--------------------------------------- + +我们可以通过实例化 ``snn.Leaky`` 来实现相同的功能,在这方面和我们在上一个教程中使用的 ``snn.Lapicque`` 类似,但参数更少: + +:: + + lif1 = snn.Leaky(beta=0.8) + +现在神经元模型存储在 ``lif1`` 中。使用这个神经元: + +**输入** + +* ``cur_in``: :math:`W\times X[t]` 的每个元素依次作为输入传递 +* ``mem``: 之前步骤的膜电位,:math:`U[t-1]`,也作为输入传递。 + +**输出** + +* ``spk_out``: 输出脉冲 :math:`S[t]` (如果有脉冲为‘1’;没有脉冲为‘0’) +* ``mem``: 当前步骤的膜电位 :math:`U[t]` + +这些都需要是 ``torch.Tensor`` 类型。请注意,在这里,我们假设输入电流在传递到 +``snn.Leaky`` 神经元之前已经被加权。当我们构建一个网络规模模型时,这将更有意义。此外,方程 :math:`(10)` 在不失一般性的情况下向后移动了一个步骤。 + +:: + + # 小幅度电流输入 + w=0.21 + cur_in = torch.cat((torch.zeros(10), torch.ones(190)*w), 0) + mem = torch.zeros(1) + spk = torch.zeros(1) + mem_rec = [] + spk_rec = [] + + # 神经元模拟 + for step in range(num_steps): + spk, mem = lif1(cur_in[step], mem) + mem_rec.append(mem) + spk_rec.append(spk) + + # 将列表转换为张量 + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + plot_cur_mem_spk(cur_in, mem_rec, spk_rec, thr_line=1, ylim_max1=0.5, + title="snn.Leaky 神经元模型") + +将这个图表与手动推导的泄漏积分-脱火神经元进行比较。 +膜电位重置略微弱些:即,它使用了 +*软重置*。 +这样做是有意为之,因为它在一些深度学习基准测试中能够获得更好的性能。 +相反使用的方程是: + +.. math:: U[t+1] = \underbrace{\beta U[t]}_\text{衰减} + \underbrace{WX[t+1]}_\text{输入} - \underbrace{\beta S[t]U_{\rm thr}}_\text{软重置} \tag{11} + + +这个模型和 Lapicque 神经元模型一样,有相同的可选输入参数 ``reset_mechanism`` +和 ``threshold``。 + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial3/_static/snn.leaky_step.png?raw=true + :align: center + :width: 450 + + +3. 一个前馈脉冲神经网络 +--------------------------------------------- + +到目前为止,我们只考虑了单个神经元对输入刺激的响应。snnTorch使将其扩展为深度神经网络变得简单。在本节中,我们将创建一个3层全连接神经网络,维度为784-1000-10。 +与迄今为止的模拟相比,每个神经元现在将整合更多的输入脉冲。 + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_8_fcn.png?raw=true + :align: center + :width: 600 + +PyTorch用于形成神经元之间的连接,snnTorch用于创建神经元。首先,初始化所有层。 + +:: + + # 层参数 + num_inputs = 784 + num_hidden = 1000 + num_outputs = 10 + beta = 0.99 + + # 初始化层 + fc1 = nn.Linear(num_inputs, num_hidden) + lif1 = snn.Leaky(beta=beta) + fc2 = nn.Linear(num_hidden, num_outputs) + lif2 = snn.Leaky(beta=beta) + +接下来,初始化每个脉冲神经元的隐藏变量和输出。随着网络规模的增加,这变得更加繁琐。可以使用静态方法 ``init_leaky()`` 来处理这个问题。 +snnTorch中的所有神经元都有自己的初始化方法,遵循相同的语法,例如 ``init_lapicque()``。隐藏状态的形状会在第一次前向传递期间根据输入数据的维度自动初始化。 + +:: + + # 初始化隐藏状态 + mem1 = lif1.init_leaky() + mem2 = lif2.init_leaky() + + # 记录输出 + mem2_rec = [] + spk1_rec = [] + spk2_rec = [] + +创建一个输入脉冲列以传递给网络。需要模拟784个输入神经元的200个时间步骤,即原始输入的维度为 :math:`200 \times 784`。 +然而,神经网络通常以小批量方式处理数据。snnTorch使用时间优先的维度: + +[:math:`时间 \times 批次大小 \times 特征维度`] + +因此,将输入沿着 ``dim=1`` 进行“unsqueeze”以指示“一个批次”的数据。这个输入张量的维度必须是 200 :math:`\times` 1 :math:`\times` 784: + +:: + + spk_in = spikegen.rate_conv(torch.rand((200, 784))).unsqueeze(1) + >>> print(f"spk_in的维度: {spk_in.size()}") + "spk_in的维度: torch.Size([200, 1, 784])" + +现在终于是时候运行完整的模拟了。将PyTorch和snnTorch协同工作的直观方式是,PyTorch将神经元连接在一起,而snnTorch将结果加载到脉冲神经元模型中。 +从编写网络的角度来看,这些脉冲神经元可以像时变激活函数一样处理。 + +以下是正在发生的事情的顺序说明: + +- 从 ``spk_in`` 的第 :math:`i^{th}` 输入到第 :math:`j^{th}` 神经元的权重由 ``nn.Linear`` 中初始化的参数加权: + :math:`X_{i} \times W_{ij}` +- 这生成了方程 :math:`(10)` 中输入电流项的输入,贡献给脉冲神经元的 :math:`U[t+1]` +- 如果 :math:`U[t+1] > U_{\rm thr}`,则从该神经元触发一个脉冲 +- 这个脉冲由第二层权重加权,然后对所有输入、权重和神经元重复上述过程。 +- 如果没有脉冲,那么不会传递任何东西给 postsynaptic 神经元。 + +与迄今为止的模拟唯一的区别是,现在我们使用由 ``nn.Linear`` 生成的权重来缩放输入电流,而不是手动设置 :math:`W`。 + +:: + + # network simulation + for step in range(num_steps): + cur1 = fc1(spk_in[step]) # post-synaptic current <-- spk_in x weight + spk1, mem1 = lif1(cur1, mem1) # mem[t+1] <--post-syn current + decayed membrane + cur2 = fc2(spk1) + spk2, mem2 = lif2(cur2, mem2) + + mem2_rec.append(mem2) + spk1_rec.append(spk1) + spk2_rec.append(spk2) + + # convert lists to tensors + mem2_rec = torch.stack(mem2_rec) + spk1_rec = torch.stack(spk1_rec) + spk2_rec = torch.stack(spk2_rec) + + plot_snn_spikes(spk_in, spk1_rec, spk2_rec, "Fully Connected Spiking Neural Network") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial3/_static/mlp_raster.png?raw=true + :align: center + :width: 450 + +在这个阶段,脉冲还没有任何实际意义。输入和权重都是随机初始化的,还没有进行任何训练。但是脉冲应该从第一层传播到输出。 +如果您没有看到任何脉冲,那么您可能在权重初始化方面运气不佳 - 您可以尝试重新运行最后四个代码块。 + +``spikeplot.spike_count`` 可以创建输出层的脉冲计数器。以下动画将需要一些时间来生成。 + + 注意:如果您在本地桌面上运行代码,请取消下面的行的注释,并修改路径以指向您的 ffmpeg.exe + +:: + + from IPython.display import HTML + + fig, ax = plt.subplots(facecolor='w', figsize=(12, 7)) + labels=['0', '1', '2', '3', '4', '5', '6', '7', '8','9'] + spk2_rec = spk2_rec.squeeze(1).detach().cpu() + + # plt.rcParams['animation.ffmpeg_path'] = 'C:\\path\\to\\your\\ffmpeg.exe' + + # 绘制脉冲计数直方图 + anim = splt.spike_count(spk2_rec, fig, ax, labels=labels, animate=True) + HTML(anim.to_html5_video()) + # anim.save("spike_bar.mp4") + +.. raw:: html + +
+ +
+ +``spikeplot.traces`` 让您可以可视化膜电位轨迹。我们将绘制10个输出神经元中的9个。将其与上面的动画和 raster 图进行比较,看看是否可以将轨迹与神经元匹配。 + +:: + + # 绘制膜电位轨迹 + splt.traces(mem2_rec.squeeze(1), spk=spk2_rec.squeeze(1)) + fig = plt.gcf() + fig.set_size_inches(8, 6) + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial3/_static/traces.png?raw=true + :align: center + :width: 450 + +一些神经元在发放脉冲,而其他神经元则完全不发放脉冲是相当正常的。再次强调,直到权重被训练之前,这些脉冲都没有任何实际意义。 + +结论 +----------- + +这涵盖了如何简化漏电积分-放电神经元模型,然后使用它构建脉冲神经网络。在实践中,我们几乎总是倾向于在训练网络时使用 ``snn.Leaky`` 而不是 ``snn.Lapicque``,因为后者的超参数搜索空间更小。 + +`教程(四) `__ +详细介绍了2阶 ``snn.Synaptic`` 和 ``snn.Alpha`` 模型。如果您希望直接进入使用snnTorch进行深度学习, +那么可以跳转到 `教程(五) `__。 + +如果您喜欢这个项目,请考虑在GitHub上为仓库加星⭐,因为这是支持它的最简单和最好的方式。 + +参考文档 `可以在这里找到 +`__。 + +更多阅读 +--------------- + +- `在这里查看 snnTorch GitHub 项目。 `__ +- `snnTorch + 文档 `__ + 的 Lapicque、Leaky、Synaptic 和 Alpha 模型 +- 由 Wulfram Gerstner、Werner M. Kistler、Richard Naud 和 Liam Paninski 编写的 `神经元动力学:从单个神经元到认知网络和模型 + `__。 +- 由 Laurence F. Abbott 和 Peter Dayan 编写的 `理论神经科学:神经系统的计算和数学建模 + `__。 diff --git a/docs/tutorials/zh-cn/tutorial_4_cn.rst b/docs/tutorials/zh-cn/tutorial_4_cn.rst new file mode 100644 index 00000000..1bf7aec0 --- /dev/null +++ b/docs/tutorials/zh-cn/tutorial_4_cn.rst @@ -0,0 +1,351 @@ +=========================== +教程(四) - 二阶脉冲神经元模型 +=========================== + +本教程出自 Jason K. Eshraghian (`www.ncg.ucsc.edu `_) + +.. image:: https://colab.research.google.com/assets/colab-badge.svg + :alt: Open In Colab + :target: https://colab.research.google.com/github/jeshraghian/snntorch/blob/master/examples/tutorial_4_advanced_neurons.ipynb + +snnTorch 教程系列基于以下论文。如果您发现这些资源或代码对您的工作有用, 请考虑引用以下来源: + + `Jason K. Eshraghian, Max Ward, Emre Neftci, Xinxin Wang, Gregor Lenz, Girish + Dwivedi, Mohammed Bennamoun, Doo Seok Jeong, and Wei D. Lu. “Training + Spiking Neural Networks Using Lessons From Deep Learning”. Proceedings of the IEEE, 111(9) September 2023. `_ + +.. note:: + 本教程是不可编辑的静态版本。交互式可编辑版本可通过以下链接获取: + * `Google Colab `_ + * `Local Notebook (download via GitHub) `_ + + + +简介 +------------- + +在本教程中, 你将: + +* 了解更先进的LIF神经元模型: ``Synaptic(突触传导)`` 和 ``Alpha`` + +安装 snnTorch 的最新 PyPi 发行版。 + +:: + + $ pip install snntorch + +:: + + # imports + import snntorch as snn + from snntorch import spikeplot as splt + from snntorch import spikegen + + import torch + import torch.nn as nn + import matplotlib.pyplot as plt + + +1. 基于突触传导的LIF神经元模型 +------------------------------------------------ + +在前几个教程中探讨的神经元模型中,我们假设输入电压脉冲会导致突触电流瞬间跃升,然后对膜电位产生影响。 +但实际上,一个脉冲将导致神经递质从前突触神经元(pre-synaptic neuron)逐渐释放到后突触神经元(post-synaptic neuron)。基于突触(synapse)传导的LIF模型考虑了输入电流的渐变时间动态。 + +1.1 建模突触电流 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +从生物学角度讲,如果前突触神经元发放脉冲,电压脉冲将传递到神经元的轴突(axon)。 +它触发囊泡释放神经递质到突触间隙。这些神经递质激活后突触受体,直接影响流入后突触神经元的有效电流。 +下面显示了两种兴奋性受体,AMPA和NMDA。 + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_6_synaptic.png?raw=true + :align: center + :width: 600 + +最简单的突触电流模型假定电流在极快的时间尺度上不断增加,随后出现相对缓慢的指数衰减,正如上文 AMPA 受体反应所示。这与 Lapicque 模型中的膜电位动态非常相似。 + +突触模型有两个指数衰减项::math:`I_{\rm syn}(t)` 和 :math:`U_{\rm mem}(t)`。 :math:`I_{\rm syn}(t)` 的后续项之间的比例(即衰减率)设置为 :math:`\alpha`,:math:`U(t)` 的比例设置为 :math:`\beta`: + +.. math:: \alpha = e^{-\Delta t/\tau_{\rm syn}} + +.. math:: \beta = e^{-\Delta t/\tau_{\rm mem}} + +其中单个时间步长的持续时间规范化为 :math:`\Delta t = 1`。 :math:`\tau_{\rm syn}` 以类似的方式模拟突触电流的时间常数,就像 :math:`\tau_{\rm mem}` 模拟膜电位的时间常数一样。 :math:`\beta` 以与前一个教程相同的方式派生,对 :math:`\alpha` 采用类似的方法: + +.. math:: I_{\rm syn}[t+1]=\underbrace{\alpha I_{\rm syn}[t]}_\text{衰减} + \underbrace{WX[t+1]}_\text{输入} + +.. math:: U[t+1] = \underbrace{\beta U[t]}_\text{衰减} + \underbrace{I_{\rm syn}[t+1]}_\text{输入} - \underbrace{R[t]}_\text{复位} + +与之前的LIF神经元一样,触发脉冲的条件仍然成立: + +.. math:: + + S_{\rm out}[t] = \begin{cases} 1, &\text{如果}~U[t] > U_{\rm thr} \\ + 0, &\text{否则}\end{cases} + +1.2 snnTorch中的突触神经元模型 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +突触传导模型将突触电流动力学与被动膜结合在一起。它必须使用两个输入参数实例化: + +* :math:`\alpha`:突触电流的衰减率 +* :math:`\beta`:膜电位的衰减率(与Lapicque模型相同) + +:: + + # 时间动态 + alpha = 0.9 + beta = 0.8 + num_steps = 200 + + # 初始化2阶LIF神经元 + lif1 = snn.Synaptic(alpha=alpha, beta=beta) + +使用这个神经元与之前的LIF神经元完全相同,但现在加入了突触电流``syn``作为输入和输出: + +**输入** + +* ``spk_in``:每个加权输入电压脉冲 :math:`WX[t]` 被顺序传递 +* ``syn``:上一时间步的突触电流 :math:`I_{\rm syn}[t-1]` +* ``mem``:上一时间步的膜电位 :math:`U[t-1]` + +**输出** + +* ``spk_out``:输出脉冲 :math:`S[t]`(如果有脉冲则为'1';如果没有脉冲则为'0') +* ``syn``:当前时间步的突触电流 :math:`I_{\rm syn}[t]` +* ``mem``:当前时间步的膜电位 :math:`U[t]` + +这些都需要是 ``torch.Tensor`` 类型。请注意,神经元模型已经向后移动了一步,不过无所谓。 + +应用周期性的脉冲输入,观察电流和膜随时间的演变: + +:: + + # 周期性脉冲输入,spk_in = 0.2 V + w = 0.2 + spk_period = torch.cat((torch.ones(1)*w, torch.zeros(9)), 0) + spk_in = spk_period.repeat(20) + + # 初始化隐藏状态和输出 + syn, mem = lif1.init_synaptic() + spk_out = torch.zeros(1) + syn_rec = [] + mem_rec = [] + spk_rec = [] + + # 模拟神经元 + for step in range(num_steps): + spk_out, syn, mem = lif1(spk_in[step], syn, mem) + spk_rec.append(spk_out) + syn_rec.append(syn) + mem_rec.append(mem) + + # 将列表转换为张量 + spk_rec = torch.stack(spk_rec) + syn_rec = torch.stack(syn_rec) + mem_rec = torch.stack(mem_rec) + + plot_spk_cur_mem_spk(spk_in, syn_rec, mem_rec, spk_rec, + "带输入脉冲的突触传导型神经元模型") + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial4/_static/syn_cond_spk.png?raw=true + :align: center + :width: 450 + +该模型还具有可选的输入参数 ``reset_mechanism`` 和 ``threshold`` ,如Lapicque的神经元模型所述。 +总之,每个脉冲都会对突触电流 :math:`I_{\rm syn}` 产生一个平移的指数衰减,然后将它们全部相加。 +然后,这个电流由在 `教程(二) `_ 中导出的被动膜方程进行积分,从而生成输出脉冲。下图示意了这个过程。 + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_7_stein.png?raw=true + :align: center + :width: 450 + +1.3 一阶神经元与二阶神经元 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +一个自然而然的问题是 - *我什么时候应该使用一阶LIF神经元,什么时候应该使用这种二阶LIF神经元?* 虽然这个问题还没有真正解决,但我的实验给了我一些可能有用的灵感。 + +**二阶神经元更好的情况** + +* 如果你的输入数据的时间关系发生在长时间尺度上, +* 或者如果输入的脉冲模式是稀疏的 + +通过有两个循环方程和两个衰减项(:math:`\alpha` 和 :math:`\beta`),这种神经元模型能够在更长的时间内“维持”输入脉冲。这对于保持长期关系是有益的。 + +另一种可能的用例是: + +- 当时间编码很重要时 + +如果你关心一个脉冲的精确时间,对于二阶神经元来说,控制起来似乎更容易。 +在 ``Leaky`` 模型中,一个脉冲将直接与输入同步触发。 +对于二阶模型,膜电位被“平滑处理”(即,突触电流模型对膜电位进行低通滤波),这意味着可以为 :math:`U[t]` 使用有限的上升时间。 +这在之前的模拟中很明显,其中输出脉冲相对于输入脉冲有所延迟。 + +**一阶神经元更好的情况** + +* 任何不属于上述情况的情况,有时,甚至包括上述情况。 + +一阶神经元模型(如 ``Leaky``)只有一个方程,使得反向传播过程稍微简单一些。 +尽管如此, ``Synaptic`` 模型在 :math:`\alpha=0.` 时功能上等同于 ``Leaky`` 模型。 +在我对简单数据集进行的超参数扫描中,最佳结果似乎将 :math:`\alpha` 尽可能接近 0。 +随着数据复杂性的增加,:math:`\alpha` 可能会变大。 + + +1.3 一阶神经元与二阶神经元 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +一个自然而然的问题是 - *我什么时候应该使用一阶LIF神经元,什么时候应该使用这种二阶LIF神经元?* +虽然这个问题还没有真正解决,但我的实验给了我一些可能有用的直觉。 + +**二阶神经元更好的情况** + +* 如果你的输入数据的时间关系发生在长时间尺度上, +* 或者如果输入的脉冲模式是稀疏的 + +通过有两个循环方程和两个衰减项(:math:`\alpha` 和 :math:`\beta`), +这种神经元模型能够在更长的时间内“维持”输入脉冲。这对于保持长期关系是有益的。 + +另一种可能的用例是: + +- 当时间编码很重要时 + +如果你关心一个脉冲的精确时间,对于二阶神经元来说,控制起来似乎更容易。在 ``Leaky`` 模型中, +一个脉冲将直接与输入同步触发。对于二阶模型,膜电位被“平滑处理”(即,突触电流模型对膜电位进行低通滤波), +这意味着可以为 :math:`U[t]` 使用有限的上升时间。这在之前的模拟中很明显,其中输出脉冲相对于输入脉冲有所延迟。 + +**一阶神经元更好的情况** + +* 任何不属于上述情况的情况,有时,甚至包括上述情况。 + +一阶神经元模型(如 ``Leaky``)只有一个方程,使得反向传播过程稍微简单一些。 +尽管如此,``Synaptic`` 模型在 :math:`\alpha=0.` 时功能上等同于 ``Leaky`` 模型。 +在我对简单数据集进行的超参数扫描中,最佳结果似乎将 :math:`\alpha` 尽可能接近 0。随着数据复杂性的增加,:math:`\alpha` 可能会变大。 + + +2.1 建模 Alpha 神经元模型 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +正式一点,这个过程由下式表示: + +.. math:: U_{\rm mem}(t) = \sum_i W(\epsilon * S_{\rm in})(t) + +其中,进入的脉冲 :math:`S_{\rm in}` 与脉冲响应核 :math:`\epsilon( \cdot )` 进行卷积。脉冲响应通过突触权重 :math:`W` 进行缩放。 +在顶部的图形中,核是一个指数衰减函数,相当于Lapicque的一阶神经元模型。在底部,核是一个alpha函数: + +.. math:: \epsilon(t) = \frac{t}{\tau}e^{1-t/\tau}\Theta(t) + +其中 :math:`\tau` 是 alpha 核的时间常数,:math:`\Theta` 是 Heaviside 阶跃函数。大多数基于核的方法采用 alpha 函数,因为它提供了对于关心指定神经元精确脉冲时间的时间编码很有用的时间延迟。 + +在 snnTorch 中,脉冲响应模型不是直接作为滤波器实现的。相反,它被重构成递归形式,这样只需要前一个时间步的值就可以计算下一组值。这减少了所需的内存。 + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial2/2_9_alpha.png?raw=true + :align: center + :width: 550 + +由于膜电位现在由两个指数之和决定,因此这些指数每个都有自己的独立衰减率。:math:`\alpha` 定义正指数的衰减率,:math:`\beta` 定义负指数的衰减率。 + +:: + + alpha = 0.8 + beta = 0.7 + + # 初始化神经元 + lif2 = snn.Alpha(alpha=alpha, beta=beta, threshold=0.5) + +使用这种神经元与之前的神经元相同,但是两个指数函数之和要求将突触电流 ``syn`` 分成 ``syn_exc`` 和 ``syn_inh`` 两个部分: + +**输入** + +* ``spk_in``:每个加权输入电压脉冲 :math:`WX[t]` 依次传入 +* ``syn_exc``:前一个时间步的兴奋性突触后电流 :math:`I_{\rm syn-exc}[t-1]` +* ``syn_inh``:前一个时间步的抑制性突触后电流 :math:`I_{\rm syn-inh}[t-1]` +* ``mem``:当前时间 :math:`t` 前一个时间步的膜电位 :math:`U_{\rm mem}[t-1]` + +**输出** + +* ``spk_out``:当前时间步的输出脉冲 :math:`S_{\rm out}[t]`(如果有脉冲则为‘1’;如果没有脉冲则为‘0’) +* ``syn_exc``:当前时间步 :math:`t` 的兴奋性突触后电流 :math:`I_{\rm syn-exc}[t]` +* ``syn_inh``:当前时间步 :math:`t` 的抑制性突触后电流 :math:`I_{\rm syn-inh}[t]` +* ``mem``:当前时间步的膜电位 :math:`U_{\rm mem}[t]` + +与所有其他神经元模型一样,这些必须是 ``torch.Tensor`` 类型。 + +:: + + # 输入脉冲:初始脉冲,然后是周期性脉冲 + w = 0.85 + spk_in = (torch.cat((torch.zeros(10), torch.ones(1), torch.zeros(89), + (torch.cat((torch.ones(1), torch.zeros(9)),0).repeat(10))), 0) * w).unsqueeze(1) + + # 初始化参数 + syn_exc, syn_inh, mem = lif2.init_alpha() + mem_rec = [] + spk_rec = [] + + # 运行模拟 + for step in range(num_steps): + spk_out, syn_exc, syn_inh, mem = lif2(spk_in[step], syn_exc, syn_inh, mem) + mem_rec.append(mem.squeeze(0)) + spk_rec.append(spk_out.squeeze(0)) + + # 将列表转换为张量 + mem_rec = torch.stack(mem_rec) + spk_rec = torch.stack(spk_rec) + + plot_spk_mem_spk(spk_in, mem_rec, spk_rec, "Alpha 神经元模型带输入脉冲") + + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial4/_static/alpha.png?raw=true + :align: center + :width: 500 + +与 Lapicque 和 Synaptic 模型一样,Alpha 模型也有修改阈值和重置机制的选项。 + +2.2 实际考虑 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +如同前面对突触神经元的讨论,模型越复杂,训练过程中的反向传播过程也越复杂。 +在我自己的实验中,我还没有发现 Alpha 神经元在性能上超越突触和Leaky神经元模型的案例。 +通过正负指数进行学习似乎只会增加梯度计算过程的难度,抵消更复杂的神经元动力学可能带来的好处。 + +然而,当SRM模型被表示为时变核(而不是像这里这样的递归模型)时,它似乎与简单的神经元模型表现得一样好。例如,参见以下论文: + + `Sumit Bam Shrestha 和 Garrick Orchard, “SLAYER: Spike layer error + reassignment in time”, Proceedings of the 32nd International + Conference on Neural Information Processing Systems, pp. 1419-1328, + 2018. `__ + +加入 Alpha 神经元的目的是为将基于 SRM 的模型移植到 snnTorch 提供一个选项,尽管在 snnTorch 中对它们进行本机训练似乎不太有效。 + +结论 +------------ + +我们已经覆盖了 snnTorch 中可用的所有LIF神经元模型。简要总结一下: + +- **Lapicque**:基于 RC-电路参数的物理精确模型 +- **Leaky**:简化的一阶模型 +- **Synaptic**:考虑突触电流演变的二阶模型 +- **Alpha**:膜电位跟踪 alpha 函数的二阶模型 + +一般来说, ``Leaky`` 和 ``Synaptic`` 似乎对于训练网络最有用。 ``Lapicque`` 适用于演示物理精确模型,而 ``Alpha`` 只旨在捕捉SRM神经元的行为。 + +使用这些稍微高级一些的神经元构建网络的过程与 `教程3 `_ 中的过程完全相同。 + +如果您喜欢这个项目,请考虑在 GitHub 上给仓库点赞⭐,这是支持它的最简单也是最好的方式。 + +参考文献,可在 `这里找到 +`__。 + +进一步阅读 +--------------- + +- `在这里查看 snnTorch GitHub 项目。 `__ +- 关于 Lapicque, Leaky, Synaptic, 和 Alpha 模型的 `snnTorch文档 `__ +- `神经动力学:从单个神经元到网络和认知模型 + `__ ,由 Wulfram + Gerstner, Werner M. Kistler, Richard Naud 和 Liam Paninski 著。 +- `理论神经科学:计算和数学建模的神经 + 系统 `__ + ,由 Laurence F. Abbott 和 Peter Dayan 著。 + diff --git a/docs/tutorials/zh-cn/tutorial_5_cn.rst b/docs/tutorials/zh-cn/tutorial_5_cn.rst new file mode 100644 index 00000000..6fde9d8f --- /dev/null +++ b/docs/tutorials/zh-cn/tutorial_5_cn.rst @@ -0,0 +1,718 @@ +=========================================================== +教程(五) - 使用snnTorch训练脉冲神经网络 +=========================================================== + +本教程出自 Jason K. Eshraghian (`www.ncg.ucsc.edu `_) + +.. image:: https://colab.research.google.com/assets/colab-badge.svg + :alt: Open In Colab + :target: https://colab.research.google.com/github/jeshraghian/snntorch/blob/master/examples/tutorial_5_FCN.ipynb + +snnTorch 教程系列基于以下论文。如果您发现这些资源或代码对您的工作有用,请考虑引用以下来源: + + `Jason K. Eshraghian, Max Ward, Emre Neftci, Xinxin Wang, Gregor Lenz, Girish + Dwivedi, Mohammed Bennamoun, Doo Seok Jeong, and Wei D. Lu. “Training + Spiking Neural Networks Using Lessons From Deep Learning”. arXiv preprint arXiv:2109.12894, + September 2021. `_ + +.. note:: + 本教程是不可编辑的静态版本。交互式可编辑版本可通过以下链接获取: + * `Google Colab `_ + * `Local Notebook (download via GitHub) `_ + + +简介 +--------------- + +在本教程中,你将: + +* 了解脉冲神经元如何作为递归网络实现 +* 通过时间了解反向传播,以及 SNN 中的相关挑战,如脉冲的不可微分性 +* 在静态 MNIST 数据集上训练全连接网络 + + +.. + +本教程的部分灵感来自 Friedemann Zenke 在 SNN 方面的大量工作。 +请在 `这里 `_ 查看他关于替代梯度的资料库, +以及我最喜欢的一篇论文: E. O. Neftci, H. Mostafa, F. Zenke, + `SNN中的替代梯度学习: 将基于梯度的优化功能引入SNN。 `_ IEEE Signal Processing Magazine 36, 51-63. + +在教程的最后,我们将实施一种基本的监督学习算法。 +我们将使用原始静态 MNIST 数据集,并使用梯度下降法训练 +多层 全连接 脉冲神经网络 来执行图像分类。 + +安装 snnTorch 的最新 PyPi 发行版: + +:: + + $ pip install snntorch + +:: + + # imports + import snntorch as snn + from snntorch import spikeplot as splt + from snntorch import spikegen + + import torch + import torch.nn as nn + from torch.utils.data import DataLoader + from torchvision import datasets, transforms + + import matplotlib.pyplot as plt + import numpy as np + import itertools + +1. 脉冲神经网络的递归表示 +---------------------------------------- + +在 `教程(三) `_ 中, +我们推导出了泄漏整合-发射(LIF)神经元的递归表示: + +.. math:: U[t+1] = \underbrace{\beta U[t]}_\text{decay} + \underbrace{WX[t+1]}_\text{input} - \underbrace{R[t]}_\text{reset} \tag{1} + +其中,输入突触电流解释为 :math:`I_{\rm in}[t] = WX[t]`, +而 :math:`X[t]` 可以是任意输入的脉冲、 +阶跃/时变电压或非加权阶跃/时变电流。 +脉冲用下式表示,如果膜电位超过阈值,就会发出一个脉冲: + +.. math:: + + S[t] = \begin{cases} 1, &\text{if}~U[t] > U_{\rm thr} \\ + 0, &\text{otherwise}\end{cases} + +.. math:: + \tag{2} + +这种离散递归形式的脉冲神经元表述几乎可以完美利用训练递归神经网络(RNN) +和基于序列模型的发展。我们使用一个*隐式*递归连接来说明膜电位的衰减, +并将其与*显式*递归区分开来,在*显式*递归中, +输出脉冲 :math:`S_{\rm out}`被反馈回输入。 +在下图中, 权重为 :math:`U_{\rm thr}`的连接代表着复位机制:math:`R[t]`。 + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial5/unrolled_2.png?raw=true + :align: center + :width: 600 + +展开图的好处在于,它明确描述了计算是如何进行的。 +展开过程说明了信息流在时间上的前向(从左到右),以计算输出和损失, +以及在时间上的后向,以计算梯度。模拟的时间步数越多,图形就越深。 + +传统的 RNN 将 :math:`\beta` 视为可学习的参数。 +这对 SNN 也是可行的, 不过默认情况下, 它们被视为超参数(hyperparameters)。 +这就用超参数搜索取代了梯度消失和梯度爆炸问题。 +未来的教程将介绍如何使 :math:`\beta` 成为可学习参数。 + +2. 脉冲的不可微分性 +----------------------------------------- + +2.1 使用反向传播算法进行训练 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +表示 :math:`S` 和 :math:`U` 之间关系的另一种方法是: + +.. math:: S[t] = \Theta(U[t] - U_{\rm thr}) \tag{3} + +其中 :math:`\Theta(\cdot)` 是 Heaviside 阶跃函数(其实就是在原点发生阶跃的函数): + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial3/3_2_spike_descrip.png?raw=true + :align: center + :width: 600 + +以这种形式训练网络会带来一些严峻的挑战。 +考虑上图中题为 *"脉冲神经元的递归表示"* 的计算图的一个单独的时间步, +如下图 *前向传递* 所示: + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial5/non-diff.png?raw=true + :align: center + :width: 400 + +我们的目标是利用损失相对于权重的梯度来训练网络,从而更新权重,使损失最小化。 +反向传播算法利用链式规则实现了这一目标: + +.. math:: + + \frac{\partial \mathcal{L}}{\partial W} = + \frac{\partial \mathcal{L}}{\partial S} + \underbrace{\frac{\partial S}{\partial U}}_{\{0, \infty\}} + \frac{\partial U}{\partial I}\ + \frac{\partial I}{\partial W}\ \tag{4} + +从 :math:`(1)`, :math:`/partial I//partial W=X`, +以及 :math:`partial U//partial I=1`。 +虽然没定义损失函数, 我们还是可以假设 :math:`\partial \mathcal{L}/\partial S` +有一个解析解,有一个类似于交叉熵或均方误差损失(稍后会详细介绍)的解析解。 + +我们真正要处理的项是 :math:`\partial S/\partial U`。 +(3)中的Heaviside阶跃函数的导数是狄拉克-德尔塔函数, +它在任何地方都求值为 :math:`0`, +但在阈值处除外 :math:`U_{\rm thr} = \theta`, +在这里它趋于无穷大。这意味着 梯度几乎总是归零 +(如果 :math:`U` 恰好位于阈值处,则为饱和而不是归零), +无法进行学习。这被称为 **死神经元问题** 。 + +2.2 克服死神经元问题 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +解决死神经元问题的最常见方法是在前向传递过程中保持Heaviside函数的原样, +但将导数项 :math:`\partial S/\partial U` +换成在后向传递过程中不会扼杀学习过程的导数项, +即 :math:`\partial \tilde{S}/\partial U`。这听起来可能有些奇怪, +但事实证明,神经网络对这种近似是相当稳健的。这就是通常所说的 *替代梯度* 方法。 + +使用替代梯度有多种选择, +我们将在 `教程(六) `_" 中详细介绍这些方法。 +snnTorch 的默认方法(截至 v0.6.0)是用反正切函数平滑 Heaviside 函数。 +使用的后向导数为 + + +.. math:: + + \frac{\partial \tilde{S}}{\partial U} \leftarrow \frac{1}{\pi}\frac{1}{(1+[U\pi]^2)} + + +其中左箭头表示替换。 + +下面用 PyTorch 实现了 :math:`(1)-(2)` 中描述的同一个神经元模型 +(又名教程 3 中的 `snn.Leaky` 神经元)。如果您不理解,请不要担心。 +稍后我们将使用 snnTorch 将其浓缩为一行代码: + +:: + + # Leaky neuron model, overriding the backward pass with a custom function + class LeakySurrogate(nn.Module): + def __init__(self, beta, threshold=1.0): + super(LeakySurrogate, self).__init__() + + # initialize decay rate beta and threshold + self.beta = beta + self.threshold = threshold + self.spike_gradient = self.ATan.apply + + # the forward function is called each time we call Leaky + def forward(self, input_, mem): + spk = self.spike_gradient((mem-self.threshold)) # call the Heaviside function + reset = (self.beta * spk * self.threshold).detach() # remove reset from computational graph + mem = self.beta * mem + input_ - reset # Eq (1) + return spk, mem + + # Forward pass: Heaviside function + # Backward pass: Override Dirac Delta with the derivative of the ArcTan function + @staticmethod + class ATan(torch.autograd.Function): + @staticmethod + def forward(ctx, mem): + spk = (mem > 0).float() # Heaviside on the forward pass: Eq(2) + ctx.save_for_backward(mem) # store the membrane for use in the backward pass + return spk + + @staticmethod + def backward(ctx, grad_output): + (spk,) = ctx.saved_tensors # retrieve the membrane potential + grad = 1 / (1 + (np.pi * mem).pow_(2)) * grad_output # Eqn 5 + return grad + +请注意,重置机制是与计算图分离的,因为替代梯度只应用于 :math:`\partial S/\partial U` 而不是 :math:`\partial R/\partial U`。 + +以上神经元可以这样实现: + +:: + + lif1 = LeakySurrogate(beta=0.9) + +这个神经元可以用 for 循环来模拟,就像之前的教程一样。 +PyTorch 的自动差异化(autodiff)机制会在后台跟踪梯度。 + +调用 ``snn.Leaky`` 神经元也能实现同样的效果。 +事实上,每次从 snnTorch 调用任何神经元模型时, +*ATan* 替代梯度都会默认应用于该神经元: + +:: + + lif1 = snn.Leaky(beta=0.9) + +如果您想了解该神经元的行为,请参阅 +`教程(三) `__. + +3. 通过时间反向传播(BPTT) +---------------------- + +方程 :math:`(4)` 仅计算一个单一时间步的梯度(在下图中称为 *即时影响*), +但是通过时间反向传播(BPTT)算法计算 从损失到 *所有* 后代(descendants)的梯度并将它们相加。 + +权重 :math:`W` 在每个时间步都应用,因此可以想象在每个时间步也计算了损失。 +权重对当前和历史损失的影响必须相加在一起以定义全局梯度: + +.. math:: + + \frac{\partial \mathcal{L}}{\partial W}=\sum_t \frac{\partial\mathcal{L}[t]}{\partial W} = + \sum_t \sum_{s\leq t} \frac{\partial\mathcal{L}[t]}{\partial W[s]}\frac{\partial W[s]}{\partial W} \tag{5} + +方程 :math:`(5)` 的目的是确保因果关系: +通过限制 :math:`s\leq t`,我们只考虑了权重 :math:`W` 对损失的即时和先前影响的贡献。 +循环系统将权重限制为在所有步骤中共享::math:`W[0]=W[1] =~... ~ = W`。 +因此,对于所有的 :math:`W`,改变 :math:`W[s]` 将对所有 :math:`W` 产生相同的影响, +这意味着 :math:`\partial W[s]/\partial W=1`: + +.. math:: + + \frac{\partial \mathcal{L}}{\partial W}= + \sum_t \sum_{s\leq t} \frac{\partial\mathcal{L}[t]}{\partial W[s]} \tag{6} + +举个例子,隔离由于 :math:`s = t-1` *仅* 导致的先前影响; +这意味着反向传递必须回溯一步。可以将 :math:`W[t-1]` 对损失的影响写成: + +.. math:: + + \frac{\partial \mathcal{L}[t]}{\partial W[t-1]} = + \frac{\partial \mathcal{L}[t]}{\partial S[t]} + \underbrace{\frac{\partial \tilde{S}[t]}{\partial U[t]}}_{方程~(5)} + \underbrace{\frac{\partial U[t]}{\partial U[t-1]}}_\beta + \underbrace{\frac{\partial U[t-1]}{\partial I[t-1]}}_1 + \underbrace{\frac{\partial I[t-1]}{\partial W[t-1]}}_{X[t-1]} \tag{7} + +我们已经处理了来自方程 :math:`(4)` 的所有这些项, +除了 :math:`\partial U[t]/\partial U[t-1]`。 +根据方程 :math:`(1)`,这个时间导数项简单地等于 :math:`\beta`。 +因此,如果我们真的想,我们现在已经知道足够的信息来手动(且痛苦地) +计算每个时间步的每个权重的导数,对于单个神经元,它会看起来像这样: + +.. image:: https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/examples/tutorial5/bptt.png?raw=true + :align: center + :width: 600 + +但幸运的是,PyTorch 的自动微分在后台为我们处理这些。 + +.. note:: + 以上图中省略了重置机制。在 snnTorch 中,重置包含在前向传递中,但与反向传递分离。 + + +4. 设置损失函数 / 输出解码 +------------------------------------------ + +在传统的非脉冲神经网络中,有监督的多类分类问题会选取 +激活度最高的神经元,并将其作为预测类别。 + +在脉冲神经网络中,有多种解释输出脉冲的方式。最常见的方法包括: + +* **脉冲率编码:** 选择具有最高脉冲率(或脉冲计数)的神经元作为预测类别 +* **延迟编码:** 选择首先发放脉冲的神经元作为预测类别 + +这可能会让你联想到关于 `教程(一)神经编码 `__。不同之处在于,在这里,我们是在解释(解码)输出脉冲,而不是将原始输入数据编码/转换成脉冲。 + +让我们专注于脉冲率编码。当输入数据传递到网络时, +我们希望正确的神经元类别在仿真运行的过程中发射最多的脉冲。 +这对应于最高的平均脉冲频率。实现这一目标的一种方法是增加正确类别的膜电位至 :math:`U>U_{\rm thr}`, +并将不正确类别的膜电位设置为 :math:`U