Skip to content

Latest commit

 

History

History
508 lines (316 loc) · 11.6 KB

File metadata and controls

508 lines (316 loc) · 11.6 KB

第 1 章:从“会漏电的容器”理解第一个脉冲神经元

对应实验:lessons/01_first_neuron.py

输出图片:outputs/01_first_neuron.png

这一章解决什么问题

我们要建立一个最小但完整的脉冲神经元。它必须同时包含:

  1. 一个连续变化的内部状态。
  2. 一个判断“是否放电”的阈值。
  3. 一个放电后的重置动作。
  4. 一个暂时限制再次放电的不应期。
  5. 对连续状态和离散脉冲的记录。

这五件事合在一起,才构成一次完整的 Brian2 神经元仿真。

1. 为什么神经元可以先抽象成一个变量

真实神经元非常复杂:

  • 细胞膜上有多种离子通道。
  • 不同位置的电压可以不同。
  • 通道开闭具有非线性和随机性。
  • 突触输入在时间和空间上不断变化。

但学习建模时,不应该第一步就复制全部细节。我们先保留最关键的计算结构:

输入使内部状态逐渐升高;状态达到阈值时产生脉冲;脉冲后状态被重置。

本章把内部状态记为 v。它可以类比膜电位,但本例故意使用无量纲变量,避免第一章同时承担完整生物物理单位。

2. “漏积分”这个名字从哪里来

想象一个有小孔的水桶:

  • 外部水流不断把水位推向某个目标高度。
  • 小孔同时让水位不会无限积累。
  • 当水位超过刻度线,就记一次“事件”并把桶倒空。

这就是漏积分放电模型的直觉:

  • 积分:输入会累积到状态变量中。
  • :当前状态会向某个平衡值回落或靠近。
  • 放电:超过阈值时产生离散事件。

本章方程是:

dv/dt = (1.1 - v) / tau

右边有两部分:

  • 1.1:状态最终想靠近的平衡值。
  • -v:当前状态越高,继续上升的速度越慢。

3. 方程逐项拆解

dv/dt

dv/dt 不是“v 除以 t”,而是 v 对时间的变化率。

如果某一刻:

dv/dt = 0.05 / ms

直觉上表示接下来很短的 1 ms 内,v 大约增加 0.05。严格结果还取决于变化率是否随 v 改变。

1.1 - v

v = 0

1.1 - v = 1.1

上升最快。

v = 1.0

1.1 - v = 0.1

上升已经很慢。

v = 1.1

1.1 - v = 0

状态不再变化,因此 1.1 是稳定平衡点。

如果没有阈值和重置,v 会越来越接近 1.1,但不会在有限时间内精确到达它。

tau

tau = 10*ms 是时间常数。它决定靠近平衡点的速度。

线性方程的解析解为:

v(t) = 1.1 + (v(0) - 1.1) * exp(-t/tau)

如果初始值 v(0)=0,则:

v(t) = 1.1 * (1 - exp(-t/tau))

经过一个时间常数 t=tau

v(tau) = 1.1 * (1 - exp(-1))
       ≈ 1.1 * 0.632
       ≈ 0.695

因此“时间常数”不是“到终点所需时间”,而是描述指数过程速度的尺度。

4. 我们甚至可以预测第一次放电时间

阈值是 v > 1。令解析解等于 1:

1 = 1.1 * (1 - exp(-t/tau))

整理:

exp(-t/tau) = 1 - 1/1.1 = 1/11
t = tau * ln(11)

代入 tau = 10 ms

t ≈ 10 ms * 2.3979
  ≈ 23.98 ms

实验输出第一次脉冲约为:

23.9 ms

理论与仿真并非神秘地“差了一点”,原因是仿真只在离散时间步上检查阈值。本例时间步是 0.1 ms,所以事件时刻会落在 0.1 ms 的网格上。

这正是数学模型与数值仿真的第一次重要区别:

方程描述连续时间;计算机只能在有限精度和离散时间点上推进与检查。

5. Brian2 如何表示这个方程

代码:

tau = 10 * ms
neurons = NeuronGroup(
    1,
    "dv/dt = (1.1 - v)/tau : 1",
    threshold="v > 1",
    reset="v = 0",
    refractory=2 * ms,
    method="exact",
)

第一个参数 1

表示创建 1 个神经元。

NeuronGroup 即使只创建一个神经元,也仍然使用“群体”接口。以后把 1 改成 1000 时,大部分模型代码不需要重写。

方程末尾的 : 1

Brian2 方程必须声明变量单位。

dv/dt = ... : 1

这里的 1 表示 v 无量纲。

如果 v 是真实电压,可能写成:

dv/dt = ... : volt

不要把 : 1 理解成赋值。它是变量声明的一部分。

外部变量 tau

方程字符串里没有定义 tau,Brian2 会在创建和运行对象的命名空间中寻找同名 Python 变量。

因此:

tau = 10 * ms

必须在方程可访问的范围中存在,而且单位必须与方程一致。

6. 阈值、重置和方程不是同一种机制

连续动力学

"dv/dt = (1.1 - v)/tau : 1"

它描述 v 在时间中的连续演化。

离散事件条件

threshold="v > 1"

Brian2 在时间步中检查条件。条件从假变为满足时,该神经元产生一个 spike 事件。

事件动作

reset="v = 0"

发生 spike 后执行。它不是微分方程,而是一次瞬时赋值。

所以完整模型是一套“混合动力系统”:

  • 微分方程负责连续变化。
  • threshold 负责检测离散事件。
  • reset 负责事件发生后的跳变。

脉冲神经网络的核心正是这种连续状态与离散事件的结合。

7. 不应期到底做了什么

代码:

refractory=2 * ms

表示一次放电后的 2 ms 内,该神经元不会再次触发阈值。

要注意两件事:

  1. 不应期默认阻止再次放电。
  2. 状态变量是否停止更新,要看方程有没有 (unless refractory) 标记。

本章方程没有该标记,所以 v 在不应期内仍然按方程变化。

但从 0 再次达到阈值需要约 24 ms,远大于 2 ms,因此本章的不应期并不会改变脉冲间隔。它主要用于展示 API。

你可以把平衡点改得非常高,使 v 很快重新到达阈值,再观察不应期是否开始限制最大放电频率。

8. 为什么选择 method="exact"

Brian2 必须把连续方程变成离散计算。

常见方法:

  • euler:用当前斜率近似下一个时间步。
  • exponential_euler:适合一类指数形式方程。
  • exact:对支持的线性方程使用解析更新。

本章方程是线性常微分方程,Brian2 可以精确写出一个时间步后的状态,因此使用 exact

这里的“exact”并不表示整个实验在所有意义上绝对精确:

  • 状态更新可以使用解析式。
  • 阈值仍然在离散时间步检查。
  • 浮点运算仍有有限精度。

所以输出脉冲时刻仍受 defaultclock.dt 影响。

9. 时间步 dt 为什么重要

代码:

defaultclock.dt = 0.1 * ms

仿真 100 ms 时,大约会经历:

100 ms / 0.1 ms = 1000 个时间步

减小 dt:

  • 阈值时刻定位通常更细。
  • 快速动力学更容易被捕捉。
  • 计算步骤更多。
  • Monitor 数据更多。

增大 dt:

  • 运行更快。
  • 可能错过快速变化。
  • 事件时刻更粗糙。
  • 数值积分误差可能增大。

dt 不是越小越“科学”。合理做法是进行收敛检查:把 dt 减半,观察关键结论是否稳定。

10. 为什么要调用 start_scope()

Brian2 在简单脚本中会自动收集当前作用域创建的对象,这常被称为 magic network。

在交互环境或重复运行脚本片段时,旧对象可能仍被收集。start_scope() 表示:

从这里开始,把后续创建的对象视为一个新的实验作用域。

独立 Python 进程每次本来就很干净,但保留它有两个好处:

  • 把脚本复制到 Notebook 后更不容易混入旧对象。
  • 明确标出“实验从这里开始”。

11. 两种 Monitor 记录两种世界

StateMonitor

state = StateMonitor(neurons, "v", record=True)

它记录连续变量 v 随时间的轨迹。

结果主要在:

  • state.t:记录时刻。
  • state.v:每个被记录神经元的 v。

单神经元时:

state.v[0]

表示 0 号神经元的完整轨迹。

SpikeMonitor

spikes = SpikeMonitor(neurons)

它记录离散脉冲事件。

常用属性:

  • spikes.t:每次脉冲的时间。
  • spikes.i:对应神经元编号。
  • spikes.count:每个神经元一共放了多少次。
  • spikes.num_spikes:整个群体的总脉冲数。

同一个实验通常既要记录状态,也要记录事件,因为它们回答的问题不同:

  • StateMonitor:状态怎样接近阈值?
  • SpikeMonitor:阈值事件何时真正发生?

12. run() 才真正推进时间

在调用:

run(100 * ms)

之前,代码只是在定义:

  • 神经元。
  • 方程。
  • 阈值和重置。
  • 监视器。

run() 才会:

  1. 检查和准备网络。
  2. 生成状态更新代码。
  3. 推进仿真时钟。
  4. 执行阈值与重置。
  5. 让 Monitor 记录数据。

这也是为什么你可以先创建整张网络,再一次运行。

13. 怎样读结果图

曲线会反复出现锯齿:

  1. v 从 0 开始上升。
  2. 越接近 1.1,上升越慢。
  3. 到达阈值 1 附近时产生脉冲。
  4. reset 把 v 瞬间设回 0。
  5. 同一过程重新开始。

输出脉冲时刻:

[23.9, 47.9, 71.9, 95.9] ms

相邻脉冲相差约 24 ms,与前面的解析预测一致。

图中的竖直下降不是漏电造成的,而是离散 reset 动作造成的。

14. 常见误区

误区一:v 就是真实膜电位

本章 v 无量纲,只保留了膜电位模型的形状。不要把 v=1 直接解释为 1 伏。

误区二:设置阈值后 Brian2 会自动重置

不会。thresholdreset 是两个独立规则。

误区三:exact 意味着 dt 不重要

状态更新可以解析完成,但事件检查仍发生在离散时间网格中。

误区四:画出的曲线就是神经元的“真实动作电位”

这里的 spike 是一个事件标记,不是完整的动作电位波形。LIF 模型通常不模拟尖峰波形本身。

15. 动手实验

每次只改一个变量,并在运行前写下预测。

实验 A:改变时间常数

把:

tau = 10 * ms

改成 5*ms20*ms

预测:

  • tau 变小,v 更快靠近平衡点,脉冲更密。
  • tau 变大,脉冲更稀。

尝试用 t = tau*ln(11) 预测第一次脉冲时间。

实验 B:让平衡点低于阈值

把方程中的 1.1 改成 0.9。

预测:v 永远不会越过 1,因此没有脉冲。

这个实验说明“运行成功但没有脉冲”不一定是程序错误,也可能是模型动力学决定的。

实验 C:改变时间步

分别设置:

defaultclock.dt = 1*ms
defaultclock.dt = 0.1*ms
defaultclock.dt = 0.01*ms

比较第一次脉冲时刻、运行速度和记录点数量。

实验 D:冻结不应期状态

把方程改为:

dv/dt = (1.1 - v)/tau : 1 (unless refractory)

然后提高平衡点或延长不应期,观察冻结与不冻结状态的差别。

本章小结

第一个神经元可以用四条规则概括:

连续状态:dv/dt = (1.1-v)/tau
离散条件:v > 1
离散动作:v = 0
时间限制:放电后 2 ms 内不能再次放电

Brian2 的关键价值不是替你决定模型,而是让你把这四条规则清楚地写出来,并负责单位检查、数值更新和事件调度。

下一章会把一个神经元扩展成 100 个,并引入两个现实问题:个体差异与随机波动。

官方参考