对应实验:lessons/01_first_neuron.py
输出图片:outputs/01_first_neuron.png
我们要建立一个最小但完整的脉冲神经元。它必须同时包含:
- 一个连续变化的内部状态。
- 一个判断“是否放电”的阈值。
- 一个放电后的重置动作。
- 一个暂时限制再次放电的不应期。
- 对连续状态和离散脉冲的记录。
这五件事合在一起,才构成一次完整的 Brian2 神经元仿真。
真实神经元非常复杂:
- 细胞膜上有多种离子通道。
- 不同位置的电压可以不同。
- 通道开闭具有非线性和随机性。
- 突触输入在时间和空间上不断变化。
但学习建模时,不应该第一步就复制全部细节。我们先保留最关键的计算结构:
输入使内部状态逐渐升高;状态达到阈值时产生脉冲;脉冲后状态被重置。
本章把内部状态记为 v。它可以类比膜电位,但本例故意使用无量纲变量,避免第一章同时承担完整生物物理单位。
想象一个有小孔的水桶:
- 外部水流不断把水位推向某个目标高度。
- 小孔同时让水位不会无限积累。
- 当水位超过刻度线,就记一次“事件”并把桶倒空。
这就是漏积分放电模型的直觉:
- 积分:输入会累积到状态变量中。
- 漏:当前状态会向某个平衡值回落或靠近。
- 放电:超过阈值时产生离散事件。
本章方程是:
dv/dt = (1.1 - v) / tau
右边有两部分:
1.1:状态最终想靠近的平衡值。-v:当前状态越高,继续上升的速度越慢。
dv/dt 不是“v 除以 t”,而是 v 对时间的变化率。
如果某一刻:
dv/dt = 0.05 / ms
直觉上表示接下来很短的 1 ms 内,v 大约增加 0.05。严格结果还取决于变化率是否随 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 = 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
因此“时间常数”不是“到终点所需时间”,而是描述指数过程速度的尺度。
阈值是 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 的网格上。
这正是数学模型与数值仿真的第一次重要区别:
方程描述连续时间;计算机只能在有限精度和离散时间点上推进与检查。
代码:
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 个神经元。
NeuronGroup 即使只创建一个神经元,也仍然使用“群体”接口。以后把 1 改成 1000 时,大部分模型代码不需要重写。
Brian2 方程必须声明变量单位。
dv/dt = ... : 1
这里的 1 表示 v 无量纲。
如果 v 是真实电压,可能写成:
dv/dt = ... : volt
不要把 : 1 理解成赋值。它是变量声明的一部分。
方程字符串里没有定义 tau,Brian2 会在创建和运行对象的命名空间中寻找同名 Python 变量。
因此:
tau = 10 * ms必须在方程可访问的范围中存在,而且单位必须与方程一致。
"dv/dt = (1.1 - v)/tau : 1"它描述 v 在时间中的连续演化。
threshold="v > 1"Brian2 在时间步中检查条件。条件从假变为满足时,该神经元产生一个 spike 事件。
reset="v = 0"发生 spike 后执行。它不是微分方程,而是一次瞬时赋值。
所以完整模型是一套“混合动力系统”:
- 微分方程负责连续变化。
- threshold 负责检测离散事件。
- reset 负责事件发生后的跳变。
脉冲神经网络的核心正是这种连续状态与离散事件的结合。
代码:
refractory=2 * ms表示一次放电后的 2 ms 内,该神经元不会再次触发阈值。
要注意两件事:
- 不应期默认阻止再次放电。
- 状态变量是否停止更新,要看方程有没有
(unless refractory)标记。
本章方程没有该标记,所以 v 在不应期内仍然按方程变化。
但从 0 再次达到阈值需要约 24 ms,远大于 2 ms,因此本章的不应期并不会改变脉冲间隔。它主要用于展示 API。
你可以把平衡点改得非常高,使 v 很快重新到达阈值,再观察不应期是否开始限制最大放电频率。
Brian2 必须把连续方程变成离散计算。
常见方法:
euler:用当前斜率近似下一个时间步。exponential_euler:适合一类指数形式方程。exact:对支持的线性方程使用解析更新。
本章方程是线性常微分方程,Brian2 可以精确写出一个时间步后的状态,因此使用 exact。
这里的“exact”并不表示整个实验在所有意义上绝对精确:
- 状态更新可以使用解析式。
- 阈值仍然在离散时间步检查。
- 浮点运算仍有有限精度。
所以输出脉冲时刻仍受 defaultclock.dt 影响。
代码:
defaultclock.dt = 0.1 * ms仿真 100 ms 时,大约会经历:
100 ms / 0.1 ms = 1000 个时间步
减小 dt:
- 阈值时刻定位通常更细。
- 快速动力学更容易被捕捉。
- 计算步骤更多。
- Monitor 数据更多。
增大 dt:
- 运行更快。
- 可能错过快速变化。
- 事件时刻更粗糙。
- 数值积分误差可能增大。
dt 不是越小越“科学”。合理做法是进行收敛检查:把 dt 减半,观察关键结论是否稳定。
Brian2 在简单脚本中会自动收集当前作用域创建的对象,这常被称为 magic network。
在交互环境或重复运行脚本片段时,旧对象可能仍被收集。start_scope() 表示:
从这里开始,把后续创建的对象视为一个新的实验作用域。
独立 Python 进程每次本来就很干净,但保留它有两个好处:
- 把脚本复制到 Notebook 后更不容易混入旧对象。
- 明确标出“实验从这里开始”。
state = StateMonitor(neurons, "v", record=True)它记录连续变量 v 随时间的轨迹。
结果主要在:
state.t:记录时刻。state.v:每个被记录神经元的 v。
单神经元时:
state.v[0]表示 0 号神经元的完整轨迹。
spikes = SpikeMonitor(neurons)它记录离散脉冲事件。
常用属性:
spikes.t:每次脉冲的时间。spikes.i:对应神经元编号。spikes.count:每个神经元一共放了多少次。spikes.num_spikes:整个群体的总脉冲数。
同一个实验通常既要记录状态,也要记录事件,因为它们回答的问题不同:
- StateMonitor:状态怎样接近阈值?
- SpikeMonitor:阈值事件何时真正发生?
在调用:
run(100 * ms)之前,代码只是在定义:
- 神经元。
- 方程。
- 阈值和重置。
- 监视器。
run() 才会:
- 检查和准备网络。
- 生成状态更新代码。
- 推进仿真时钟。
- 执行阈值与重置。
- 让 Monitor 记录数据。
这也是为什么你可以先创建整张网络,再一次运行。
曲线会反复出现锯齿:
- v 从 0 开始上升。
- 越接近 1.1,上升越慢。
- 到达阈值 1 附近时产生脉冲。
- reset 把 v 瞬间设回 0。
- 同一过程重新开始。
输出脉冲时刻:
[23.9, 47.9, 71.9, 95.9] ms
相邻脉冲相差约 24 ms,与前面的解析预测一致。
图中的竖直下降不是漏电造成的,而是离散 reset 动作造成的。
本章 v 无量纲,只保留了膜电位模型的形状。不要把 v=1 直接解释为 1 伏。
不会。threshold 和 reset 是两个独立规则。
状态更新可以解析完成,但事件检查仍发生在离散时间网格中。
这里的 spike 是一个事件标记,不是完整的动作电位波形。LIF 模型通常不模拟尖峰波形本身。
每次只改一个变量,并在运行前写下预测。
把:
tau = 10 * ms改成 5*ms 和 20*ms。
预测:
- tau 变小,v 更快靠近平衡点,脉冲更密。
- tau 变大,脉冲更稀。
尝试用 t = tau*ln(11) 预测第一次脉冲时间。
把方程中的 1.1 改成 0.9。
预测:v 永远不会越过 1,因此没有脉冲。
这个实验说明“运行成功但没有脉冲”不一定是程序错误,也可能是模型动力学决定的。
分别设置:
defaultclock.dt = 1*ms
defaultclock.dt = 0.1*ms
defaultclock.dt = 0.01*ms比较第一次脉冲时刻、运行速度和记录点数量。
把方程改为:
dv/dt = (1.1 - v)/tau : 1 (unless refractory)
然后提高平衡点或延长不应期,观察冻结与不冻结状态的差别。
第一个神经元可以用四条规则概括:
连续状态:dv/dt = (1.1-v)/tau
离散条件:v > 1
离散动作:v = 0
时间限制:放电后 2 ms 内不能再次放电
Brian2 的关键价值不是替你决定模型,而是让你把这四条规则清楚地写出来,并负责单位检查、数值更新和事件调度。
下一章会把一个神经元扩展成 100 个,并引入两个现实问题:个体差异与随机波动。