Skip to content

Latest commit

 

History

History
469 lines (294 loc) · 9.82 KB

File metadata and controls

469 lines (294 loc) · 9.82 KB

第 6 章:STDP 如何把“先后顺序”变成突触权重变化

对应实验:lessons/06_stdp.py

输出图片:outputs/06_stdp.png

这一章解决什么问题

前面的突触权重由我们手动设置,仿真期间保持不变。

学习规则要回答另一类问题:

一条突触经历了哪些脉冲事件后,连接应该增强还是减弱?

本章实现经典的成对脉冲时序依赖可塑性:

STDP,Spike-Timing-Dependent Plasticity。

核心直觉:

  • 突触前神经元先放电,突触后神经元随后放电:这条连接可能促成了后神经元放电,因此增强。
  • 突触后神经元先放电,突触前神经元随后才放电:前脉冲不太可能是本次后脉冲的原因,因此减弱。

这是一种局部规则。每条突触只需要知道:

  • 最近的前脉冲留下了多少痕迹。
  • 最近的后脉冲留下了多少痕迹。
  • 当前事件是前脉冲还是后脉冲。

1. 为什么不能保存所有历史脉冲再逐对比较

最直接的 STDP 实现似乎是:

  1. 保存每个前脉冲时刻。
  2. 保存每个后脉冲时刻。
  3. 遍历所有前后脉冲对。
  4. 根据时间差更新权重。

但长仿真中脉冲数量很大。对每个事件回看全部历史会越来越慢。

更高效的方法是使用“痕迹变量”:

  • 前脉冲发生时,把 apre 增加一点。
  • apre 随时间指数衰减。
  • 后脉冲发生时,读取当前 apre
  • 后脉冲同理留下 apost

痕迹越大,说明相关事件发生得越近。

这样只需维护两个状态变量,不必保留完整事件历史。

2. 两条痕迹方程在表达什么

dapre/dt = -apre/taupre
dapost/dt = -apost/taupost

如果某次前脉冲把 apre 增加为 A,之后没有新事件:

apre(t) = A * exp(-Δt/taupre)

经过:

  • 0 ms:保留 100%。
  • 1 个 tau:保留约 36.8%。
  • 2 个 tau:保留约 13.5%。
  • 3 个 tau:保留约 5%。

因此 taupre=20 ms 表示几十毫秒内的前脉冲时序最重要。

它不是硬窗口。20 ms 之外影响不会突然归零,而是指数变小。

3. 为什么一条痕迹为正,另一条为负

代码:

Apre = 0.01
Apost = -Apre * 1.05

前脉冲留下正痕迹:

apre += +0.01

后脉冲留下负痕迹:

apost += -0.0105

更新规则设计为:

  • 前事件读取 apost
  • 后事件读取 apre

于是:

前脉冲先,后脉冲后

前脉冲留下正 apre

后脉冲发生时:

w += apre

权重增加。

后脉冲先,前脉冲后

后脉冲留下负 apost

前脉冲发生时:

w += apost

权重减少。

符号和事件顺序共同实现了 STDP。

4. 用时间差写出近似学习窗

设前脉冲时刻为 t_pre,后脉冲时刻为 t_post

定义:

Δt = t_post - t_pre

如果 Δt > 0,前脉冲先发生:

Δw ≈ Apre * exp(-Δt/taupre)

如果 Δt < 0,后脉冲先发生:

Δw ≈ Apost * exp(Δt/taupost)

因为 Apost 为负,所以第二种情况减弱权重。

这就是典型的双指数 STDP 窗。

真实实验中的 STDP 规则会随脑区、细胞类型、频率和调制状态变化。本章实现的是经典教学模型,不应被理解为唯一生物定律。

5. 为什么痕迹声明为 (event-driven)

代码:

dapre/dt = -apre/taupre : 1 (event-driven)
dapost/dt = -apost/taupost : 1 (event-driven)

这两条方程:

  • 线性。
  • 只依赖自身。
  • 在事件之间具有解析解。

Brian2 可以不在每个 dt 更新它们,而是在下一次相关事件发生时,根据距离上次更新的时间一次性算出衰减后的值。

例如 50 ms 内没有事件,不必计算 500 次 0.1 ms 更新,只需在下次事件到达时计算:

old_value * exp(-50ms/tau)

这就是 event-driven 的性能优势。

并非所有方程都能标记为 event-driven。变量之间复杂耦合或需要持续影响其他状态时,可能必须 clock-driven 更新。

6. 网络结构是什么

inputs = PoissonGroup(100, rates=15 * Hz)
target = NeuronGroup(1, ...)

有:

  • 100 个独立泊松输入神经元。
  • 1 个目标神经元。
synapses.connect()

建立 100 条连接:

inputs[0] -> target[0]
inputs[1] -> target[0]
...
inputs[99] -> target[0]

每条突触拥有独立的:

  • w。
  • apre。
  • apost。

后神经元放电时,连接到它的所有突触都收到 on_post 事件,但它们的 apre 可能不同,因为各自前脉冲历史不同。

7. 逐行拆解 on_pre

on_pre="""
v_post += w
apre += Apre
w = clip(w + apost, 0, wmax)
"""

执行顺序:

1. 推动后神经元

v_post += w

当前权重越大,这次输入对后神经元影响越强。

2. 留下前脉冲痕迹

apre += Apre

告诉突触:“刚刚发生过一次前脉冲。”

3. 根据此前后脉冲痕迹更新权重

w = clip(w + apost, 0, wmax)

如果最近发生过后脉冲,apost 为负,因此权重减小。

这里读取的是此前留下并已衰减的 apost。

8. 逐行拆解 on_post

on_post="""
apost += Apost
w = clip(w + apre, 0, wmax)
"""

1. 留下后脉冲痕迹

apost += Apost

Apost 为负,所以 apost 变得更负。

2. 读取前痕迹增强权重

w = clip(w + apre, 0, wmax)

如果近期有前脉冲,apre 为正,权重增加。

代码顺序意味着当前后脉冲不会通过刚加入的 apost 直接影响这一行的增强;apost 主要供未来前脉冲读取。

9. 为什么必须限制权重

wmax = 0.08
w = clip(w + ..., 0, wmax)

没有边界时,重复事件可能让权重:

  • 变成负数。
  • 不断增大。
  • 把目标神经元推入非预期高频状态。

clip(x, 0, wmax) 表示:

x < 0      -> 0
0 <= x <= wmax -> x
x > wmax   -> wmax

硬边界是模型假设,不是 Brian2 必需规则。

其他可塑性模型可能使用:

  • 软边界。
  • 权重依赖更新。
  • 归一化。
  • 稳态调节。

10. 初始化权重为何要保存副本

synapses.w = "rand()*wmax"
initial_weights = synapses.w[:].copy()

synapses.w[:] 取出当前权重数组。

.copy() 很重要,因为我们需要保存运行前快照。若只是保留某种指向内部数据的视图,运行后可能无法可靠比较旧值。

初始化范围是:

0 <= w < wmax

随机初始化使不同突触从不同起点开始。

11. 为什么目标神经元是否放电很关键

只有前脉冲:

  • 会推动 v。
  • 会留下 apre。
  • 可能读取负 apost。

但如果目标神经元从不放电:

  • on_post 永远不执行。
  • apost 不会被创建为负痕迹。
  • 正向增强事件也不会发生。

因此打印:

target_spikes.num_spikes

不是附带信息,而是检查学习规则是否获得后事件的必要诊断。

12. 怎样解释权重直方图

脚本比较学习前后权重分布。

直方图回答:

  • 是否有更多权重靠近 0?
  • 是否有更多权重靠近 wmax?
  • 分布是变宽、变窄还是整体移动?

它不回答:

  • 哪一条具体突触变强。
  • 权重变化发生在什么时刻。
  • 变化是否与某次前后脉冲配对一致。

若要验证机制,应再加入:

  • StateMonitor(synapses, "w", record=[...])
  • 前后 SpikeMonitor。
  • 单条突触的事件时序分析。

13. 为什么平均权重变化很小也可能正常

输出示例:

平均权重: 0.0423 -> 0.0434

平均值变化小不代表没有学习。

可能发生:

  • 一些突触增强。
  • 一些突触减弱。
  • 两者在平均值中部分抵消。

因此至少应同时查看:

  • 平均值。
  • 分布。
  • 单条轨迹。
  • 与脉冲时序的关系。

14. 常见误区

误区一:STDP 等于“同时放电就增强”

核心是相对时序,不只是共同活跃。

误区二:apre 是突触前神经元变量

本例中 apre 是每条突触的变量。同一个前神经元若连接多个目标,每条连接可有独立痕迹。

误区三:event-driven 表示只有事件时变量才衰减

数学上它在事件间持续指数衰减,只是 Brian2 延迟到事件时用解析式一次算出当前值。

误区四:权重上升一定说明模型学到了有用任务

权重改变只说明学习规则运行了。是否学到任务,需要独立行为指标。

误区五:一秒仿真足以得出稳定统计结论

本例只用于展示机制。稳定分布可能需要更长时间、多次重复和参数分析。

15. 动手实验

实验 A:确定性单突触 STDP

使用两个 SpikeGeneratorGroup,精确设置:

pre = 10 ms
post = 15 ms

再交换顺序,直接验证权重增减方向。

实验 B:扫描时间差

令 Δt 从 -50 ms 到 +50 ms,画出最终 Δw,重建 STDP 学习窗。

实验 C:改变 tau

把 taupre、taupost 改为 5、20、50 ms,观察学习窗宽度。

实验 D:去掉 clip

只在短安全实验中尝试,观察权重是否越界。理解边界为何属于模型设计。

实验 E:记录三条权重轨迹

为三条突触增加 StateMonitor,并同步画出输入和目标脉冲。

本章小结

STDP 的实现逻辑可以压缩为:

前脉冲 -> 留下正痕迹
后脉冲 -> 读取正痕迹并增强

后脉冲 -> 留下负痕迹
前脉冲 -> 读取负痕迹并减弱

event-driven 让痕迹在数学上连续衰减、计算上只在事件时更新。

下一章会处理实验控制:怎样保存同一个起点、恢复状态、使用相同随机序列做公平对照,并查看 Brian2 的调度和性能。

官方参考