前两章的神经元彼此独立。即使把 100 个神经元画在同一张图上,它们也没有交换信息。
从这一章开始,我们加入网络最核心的关系:
突触前神经元产生脉冲,脉冲经过一段延迟到达突触,随后按权重改变突触后神经元。
你需要理解五个角色:
source:脉冲从哪里来。target:脉冲要影响谁。Synapses:连接规则和突触状态。on_pre:前神经元脉冲到达时执行什么。delay:从源脉冲产生到突触事件执行要等待多久。
神经元内部状态通常持续变化,因此适合微分方程。
脉冲则只在少数时刻发生。如果每个时间步都检查每一条突触并计算一次完整作用,会浪费大量计算。
Brian2 的事件驱动思想是:
没有脉冲到达 -> 这条突触不执行 on_pre
脉冲到达 -> 执行一次 on_pre 中的语句
本章的突触规则是:
on_pre="v_post += w"含义是:
当前突触的前神经元脉冲到达时,把该突触的权重 w 加到后神经元的 v。
这是一种瞬时跳变,不是连续微分方程。
代码:
source = SpikeGeneratorGroup(
5,
indices=[0, 1, 2, 3, 4],
times=[5, 10, 15, 20, 25] * ms,
)SpikeGeneratorGroup 不模拟膜电位。它直接按照给定计划发出脉冲。
两个数组必须一一对应:
| 列表位置 | indices |
times |
含义 |
|---|---|---|---|
| 0 | 0 | 5 ms | 0 号源神经元在 5 ms 放电 |
| 1 | 1 | 10 ms | 1 号源神经元在 10 ms 放电 |
| 2 | 2 | 15 ms | 2 号源神经元在 15 ms 放电 |
| 3 | 3 | 20 ms | 3 号源神经元在 20 ms 放电 |
| 4 | 4 | 25 ms | 4 号源神经元在 25 ms 放电 |
为什么不让源神经元自己积分到阈值?
因为本章只想研究:
- 权重改变响应幅度。
- 延迟改变响应时刻。
使用精确已知的脉冲时刻,可以把源神经元自身动力学这个变量暂时排除。
这是一种很重要的建模方法:为了理解一个机制,先控制其他机制。
代码:
target = NeuronGroup(
5,
"dv/dt = -v/(8*ms) : 1",
method="exact",
)方程解析解是:
v(t) = v(0) * exp(-t / 8 ms)
它的平衡点是 0。
如果没有突触输入:
- 初始 v 默认是 0。
- 导数也是 0。
- v 会一直保持 0。
突触事件到达时,v 被突然抬高。之后它再按指数衰减回 0。
因此图上的每个响应都容易解释:
突然上跳:突触事件
缓慢下降:靶神经元自身的漏衰减
代码:
synapses = Synapses(
source,
target,
"w : 1",
on_pre="v_post += w",
)四个部分:
突触前群体。事件从这里发出。
突触后群体。状态在这里被改变。
声明每条突触自己的无量纲变量 w。
注意 w 属于突触,不属于 source 或 target。两条突触可以连接同一对群体,却有不同权重。
定义突触前脉冲到达时执行的动作。
此时还没有真正创建任何连接。Synapses(...) 更像是定义了一种连接模板。
代码:
synapses.connect(
i=[0, 1, 2, 3, 4],
j=[0, 1, 2, 3, 4],
)在 Synapses 中:
i表示突触前神经元编号。j表示突触后神经元编号。
两个列表按位置配对:
i=0 -> j=0
i=1 -> j=1
i=2 -> j=2
i=3 -> j=3
i=4 -> j=4
因此只建立 5 条一对一连接,不是 25 条全连接。
如果直接写:
synapses.connect()默认会连接每个 source 到每个 target,得到:
5 * 5 = 25 条连接
连接结构决定谁能影响谁,是网络模型的一部分,不能只把它当成程序配置。
代码:
on_pre="v_post += w"Brian2 用后缀区分变量属于哪一侧:
v_pre:突触前神经元的 v。v_post:突触后神经元的 v。
本章 source 是 SpikeGeneratorGroup,它没有 v;target 才有 v,所以我们使用 v_post。
如果不写 _post,Brian2 可能把名称理解为突触自身变量,或因为名称解析不清而报错。
类似地,在可塑性规则中经常会看到:
x_pre
y_post
这些后缀让一条突触规则可以同时读取两端状态。
代码:
synapses.w = "0.2 + 0.1*i"Brian2 对每条已创建的突触计算表达式。
因为本章每条突触的 i 分别是 0 到 4:
| i | w |
|---|---|
| 0 | 0.2 |
| 1 | 0.3 |
| 2 | 0.4 |
| 3 | 0.5 |
| 4 | 0.6 |
所以越靠后的 source 神经元,其突触造成的 v 跳变越大。
这里使用 i 而不是 j,在一对一连接中结果相同。但在一般网络中二者含义不同:
- 依赖 i:同一个前神经元发出的连接具有某种共同结构。
- 依赖 j:指向某个后神经元的连接具有某种共同结构。
代码:
synapses.delay = "i*ms"得到:
| i | 源脉冲时刻 | delay | on_pre 执行时刻 |
|---|---|---|---|
| 0 | 5 ms | 0 ms | 5 ms |
| 1 | 10 ms | 1 ms | 11 ms |
| 2 | 15 ms | 2 ms | 17 ms |
| 3 | 20 ms | 3 ms | 23 ms |
| 4 | 25 ms | 4 ms | 29 ms |
SpikeGeneratorGroup 仍在原定时刻发出脉冲。delay 表示突触事件排队多久后才执行。
因此:
SpikeMonitor(source)会看到原始发放时刻。StateMonitor(target)会看到延迟后的响应时刻。
这一区分在研究传播时间、同步和振荡时非常重要。
对于第 k 条一对一连接:
- 在
5 + 6kms 左右发生跳变。 - 跳变高度为
0.2 + 0.1k。 - 之后按 8 ms 时间常数衰减。
更明确地说:
| target | 响应时刻 | 瞬时增量 |
|---|---|---|
| 0 | 5 ms | 0.2 |
| 1 | 11 ms | 0.3 |
| 2 | 17 ms | 0.4 |
| 3 | 23 ms | 0.5 |
| 4 | 29 ms | 0.6 |
由于每个 target 只接收一次输入,五条曲线不会发生多次叠加。
状态只在离散调度点被记录。一次时间步中可能依次发生:
- 状态更新。
- 阈值检测。
- 突触事件。
- 状态记录。
具体记录到事件前还是事件后的值取决于对象调度位置。
所以绘图时不要过度解读一个时间步内的像素差异。真正重要的是:
- 响应顺序。
- 延迟差。
- 跳变幅度。
- 衰减时间尺度。
需要精确分析调度时,可以查看对象的 when 与 order,第 7 章会专门讨论。
len(synapses) 返回实际创建的突触数量。
它不是:
- source 神经元数量。
- target 神经元数量。
- 可能连接数量。
在随机连接中,实际数量会围绕期望值波动。记录它是检查网络结构的第一步。
本章 v 和 w 都无量纲:
w : 1
v : 1
所以:
v_post += w
单位一致。
更生物物理的模型可能使用:
- 电流型突触:事件改变突触电流。
- 电导型突触:事件改变电导,再由反转电位计算电流。
- 带上升和衰减过程的突触:事件改变中间状态,状态再连续演化。
本章选择瞬时无量纲跳变,是为了先隔离连接、权重和延迟的基本语义。
必须调用 connect() 才会真正创建连接。
w 默认是每条突触的变量。一个前神经元发出的不同突触可以有不同 w。
delay 只推迟突触事件到达,不改变源脉冲记录。
事件只瞬间增加 v,之后 v 会继续按自身方程衰减。
不一样。无参数 connect() 是全连接。
设置:
synapses.delay = 0*ms预测五条曲线的跳变时刻,再运行验证。
改为:
synapses.connect()需要同时重新思考权重表达式与延迟表达式。观察每个 target 会接收多少次输入。
synapses.w = "0.2 + 0.1*j"在一对一连接中结果不变。然后改成全连接,观察 i 依赖和 j 依赖的区别。
让多个 source 连接到同一个 target。预测在前一次响应尚未衰减完时,下一次输入如何叠加。
给 target 添加:
threshold="v > 0.7"
reset="v = 0"判断哪些权重或输入组合足以使 target 放电。
突触传播可以拆成四个问题:
结构:i 连接到哪个 j
状态:每条突触有什么 w
事件:前脉冲到达时执行什么 on_pre
时间:事件延迟多久执行
下一章不再只使用一组手工脉冲,而是比较四种常见输入:时间函数、聚合泊松输入、精确脉冲序列和显式泊松神经元群。