对应实验:lessons/06_stdp.py
输出图片:outputs/06_stdp.png
前面的突触权重由我们手动设置,仿真期间保持不变。
学习规则要回答另一类问题:
一条突触经历了哪些脉冲事件后,连接应该增强还是减弱?
本章实现经典的成对脉冲时序依赖可塑性:
STDP,Spike-Timing-Dependent Plasticity。
核心直觉:
- 突触前神经元先放电,突触后神经元随后放电:这条连接可能促成了后神经元放电,因此增强。
- 突触后神经元先放电,突触前神经元随后才放电:前脉冲不太可能是本次后脉冲的原因,因此减弱。
这是一种局部规则。每条突触只需要知道:
- 最近的前脉冲留下了多少痕迹。
- 最近的后脉冲留下了多少痕迹。
- 当前事件是前脉冲还是后脉冲。
最直接的 STDP 实现似乎是:
- 保存每个前脉冲时刻。
- 保存每个后脉冲时刻。
- 遍历所有前后脉冲对。
- 根据时间差更新权重。
但长仿真中脉冲数量很大。对每个事件回看全部历史会越来越慢。
更高效的方法是使用“痕迹变量”:
- 前脉冲发生时,把
apre增加一点。 apre随时间指数衰减。- 后脉冲发生时,读取当前
apre。 - 后脉冲同理留下
apost。
痕迹越大,说明相关事件发生得越近。
这样只需维护两个状态变量,不必保留完整事件历史。
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 之外影响不会突然归零,而是指数变小。
代码:
Apre = 0.01
Apost = -Apre * 1.05前脉冲留下正痕迹:
apre += +0.01
后脉冲留下负痕迹:
apost += -0.0105
更新规则设计为:
- 前事件读取
apost。 - 后事件读取
apre。
于是:
前脉冲留下正 apre。
后脉冲发生时:
w += apre
权重增加。
后脉冲留下负 apost。
前脉冲发生时:
w += apost
权重减少。
符号和事件顺序共同实现了 STDP。
设前脉冲时刻为 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 规则会随脑区、细胞类型、频率和调制状态变化。本章实现的是经典教学模型,不应被理解为唯一生物定律。
代码:
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 更新。
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 可能不同,因为各自前脉冲历史不同。
on_pre="""
v_post += w
apre += Apre
w = clip(w + apost, 0, wmax)
"""执行顺序:
v_post += w
当前权重越大,这次输入对后神经元影响越强。
apre += Apre
告诉突触:“刚刚发生过一次前脉冲。”
w = clip(w + apost, 0, wmax)
如果最近发生过后脉冲,apost 为负,因此权重减小。
这里读取的是此前留下并已衰减的 apost。
on_post="""
apost += Apost
w = clip(w + apre, 0, wmax)
"""apost += Apost
Apost 为负,所以 apost 变得更负。
w = clip(w + apre, 0, wmax)
如果近期有前脉冲,apre 为正,权重增加。
代码顺序意味着当前后脉冲不会通过刚加入的 apost 直接影响这一行的增强;apost 主要供未来前脉冲读取。
wmax = 0.08
w = clip(w + ..., 0, wmax)没有边界时,重复事件可能让权重:
- 变成负数。
- 不断增大。
- 把目标神经元推入非预期高频状态。
clip(x, 0, wmax) 表示:
x < 0 -> 0
0 <= x <= wmax -> x
x > wmax -> wmax
硬边界是模型假设,不是 Brian2 必需规则。
其他可塑性模型可能使用:
- 软边界。
- 权重依赖更新。
- 归一化。
- 稳态调节。
synapses.w = "rand()*wmax"
initial_weights = synapses.w[:].copy()synapses.w[:] 取出当前权重数组。
.copy() 很重要,因为我们需要保存运行前快照。若只是保留某种指向内部数据的视图,运行后可能无法可靠比较旧值。
初始化范围是:
0 <= w < wmax
随机初始化使不同突触从不同起点开始。
只有前脉冲:
- 会推动 v。
- 会留下 apre。
- 可能读取负 apost。
但如果目标神经元从不放电:
on_post永远不执行。- apost 不会被创建为负痕迹。
- 正向增强事件也不会发生。
因此打印:
target_spikes.num_spikes不是附带信息,而是检查学习规则是否获得后事件的必要诊断。
脚本比较学习前后权重分布。
直方图回答:
- 是否有更多权重靠近 0?
- 是否有更多权重靠近 wmax?
- 分布是变宽、变窄还是整体移动?
它不回答:
- 哪一条具体突触变强。
- 权重变化发生在什么时刻。
- 变化是否与某次前后脉冲配对一致。
若要验证机制,应再加入:
StateMonitor(synapses, "w", record=[...])。- 前后 SpikeMonitor。
- 单条突触的事件时序分析。
输出示例:
平均权重: 0.0423 -> 0.0434
平均值变化小不代表没有学习。
可能发生:
- 一些突触增强。
- 一些突触减弱。
- 两者在平均值中部分抵消。
因此至少应同时查看:
- 平均值。
- 分布。
- 单条轨迹。
- 与脉冲时序的关系。
核心是相对时序,不只是共同活跃。
本例中 apre 是每条突触的变量。同一个前神经元若连接多个目标,每条连接可有独立痕迹。
数学上它在事件间持续指数衰减,只是 Brian2 延迟到事件时用解析式一次算出当前值。
权重改变只说明学习规则运行了。是否学到任务,需要独立行为指标。
本例只用于展示机制。稳定分布可能需要更长时间、多次重复和参数分析。
使用两个 SpikeGeneratorGroup,精确设置:
pre = 10 ms
post = 15 ms
再交换顺序,直接验证权重增减方向。
令 Δt 从 -50 ms 到 +50 ms,画出最终 Δw,重建 STDP 学习窗。
把 taupre、taupost 改为 5、20、50 ms,观察学习窗宽度。
只在短安全实验中尝试,观察权重是否越界。理解边界为何属于模型设计。
为三条突触增加 StateMonitor,并同步画出输入和目标脉冲。
STDP 的实现逻辑可以压缩为:
前脉冲 -> 留下正痕迹
后脉冲 -> 读取正痕迹并增强
后脉冲 -> 留下负痕迹
前脉冲 -> 读取负痕迹并减弱
event-driven 让痕迹在数学上连续衰减、计算上只在事件时更新。
下一章会处理实验控制:怎样保存同一个起点、恢复状态、使用相同随机序列做公平对照,并查看 Brian2 的调度和性能。