对应实验:lessons/04_inputs.py
神经元很少在完全没有输入的情况下工作。输入可能来自:
- 一个随时间变化的外部刺激。
- 大量未显式建模的随机背景神经元。
- 已知精确时刻的实验提示。
- 另一组真正参与网络的脉冲神经元。
Brian2 提供不同对象,不是为了让 API 显得丰富,而是因为这些输入在建模含义和计算代价上不同。
本章比较:
| 机制 | 核心含义 |
|---|---|
TimedArray |
已知的时间函数 |
PoissonInput |
高效聚合的随机背景输入 |
SpikeGeneratorGroup |
精确指定的脉冲序列 |
PoissonGroup + Synapses |
可作为网络节点的随机脉冲源 |
方程:
dv/dt = (-v + stimulus(t)) / (10*ms)
它表示 v 以 10 ms 时间常数追踪当前刺激值 stimulus(t)。
如果刺激在一段时间内固定为 S:
dv/dt = (S-v)/tau
那么 v 会指数靠近 S。
但本章还有三类脉冲输入会瞬时增加 v。因此完整轨迹由两种机制叠加:
- 连续追踪时间刺激。
- 离散脉冲造成瞬时跳变。
代码:
stimulus = TimedArray(
[0.1, 0.9, 0.2, 1.1, 0.1],
dt=20 * ms,
)它定义一个分段时间序列:
| 时间区间 | stimulus(t) |
|---|---|
| 0 至 20 ms | 0.1 |
| 20 至 40 ms | 0.9 |
| 40 至 60 ms | 0.2 |
| 60 至 80 ms | 1.1 |
| 80 至 100 ms | 0.1 |
因此:
stimulus(45 * ms)返回 0.2,因为 45 ms 落在第三个区间。
- 实验测得的刺激波形。
- 分阶段任务信号。
- 已知采样率的外部输入。
- 多次实验中完全相同的时间协议。
TimedArray 不会自己产生脉冲,也不是一个神经元群。它只是一个可在方程中按时间查询的函数。
dv/dt = (-v + stimulus(t))/(10*ms) : 1
t 是 Brian2 提供的当前仿真时间。
每次更新状态时,方程会查询当前时间对应的刺激值。
如果写成普通 Python:
current = stimulus(45*ms)只会在 Python 执行到这一行时得到一个固定值。把 stimulus(t) 放在方程中,才会让它随仿真时间变化。
后三种输入都涉及脉冲。先理解泊松脉冲的含义。
对频率 r 的齐次泊松过程,在很短的时间 dt 内:
发生一次事件的概率约为 r * dt
例如 r=10 Hz、dt=0.1 ms:
r * dt
= 10 / second * 0.0001 second
= 0.001
即每个时间步约 0.1% 概率发生事件。
长期平均频率接近 10 Hz,但:
- 事件间隔不固定。
- 短时间内可能一个也没有。
- 也可能偶然连续出现。
泊松输入常用作“缺乏详细结构的随机背景”,不是说真实神经元一定严格服从泊松过程。
代码:
poisson_input = PoissonInput(
neurons,
target_var="v",
N=20,
rate=10 * Hz,
weight=0.04,
)含义是每个靶神经元接收:
- 20 路独立泊松输入。
- 每一路平均频率 10 Hz。
- 每个到达事件让 v 增加 0.04。
单个靶神经元每秒期望接收的总事件数约为:
20 * 10 Hz = 200 events/s
若忽略阈值、衰减和事件重叠,期望注入总量尺度约为:
200 * 0.04 = 8 / second
这只是理解驱动强度的粗略尺度,不等于 v 每秒净增加 8,因为 v 同时在衰减并被 reset。
如果背景神经元本身没有需要记录或连接的状态,就没有必要显式创建:
- 20 个源神经元。
- 20 倍靶数量的突触。
PoissonInput 直接向目标变量添加统计等价的随机事件,节省对象和内存。
你不能像操作普通 Synapses 那样:
- 记录每个背景源的脉冲编号。
- 为每条背景连接设置复杂可塑性。
- 让同一个源再连接到其他网络对象。
代码:
cue = SpikeGeneratorGroup(
1,
indices=[0, 0],
times=[35, 75] * ms,
)同一个 0 号提示源在 35 ms 和 75 ms 各放电一次。
随后:
cue_synapse = Synapses(
cue,
neurons,
on_pre="v_post += 0.3",
)
cue_synapse.connect()因为 cue 有 1 个神经元,neurons 有 20 个,无参数 connect() 会建立 20 条连接:
cue[0] -> neurons[0]
cue[0] -> neurons[1]
...
cue[0] -> neurons[19]
所以两个提示时刻会同时推动全部 20 个神经元。
- 重放实验记录的脉冲。
- 精确提示或同步信号。
- 单元测试突触响应。
- 构造已知输入输出关系。
代码:
background = PoissonGroup(
5,
rates="5*Hz + i*2*Hz",
)创建 5 个源神经元。i 从 0 到 4,所以频率为:
| i | rate |
|---|---|
| 0 | 5 Hz |
| 1 | 7 Hz |
| 2 | 9 Hz |
| 3 | 11 Hz |
| 4 | 13 Hz |
这些源没有膜电位方程,但会产生可被 SpikeMonitor 记录、可通过普通 Synapses 传播的脉冲。
连接:
background_synapse.connect(p=0.25)每个可能的 source-target 对以 25% 概率建立连接。
共有:
5 * 20 = 100
个可能连接,实际连接数会随机波动,期望约 25 条。
PoissonGroup 的源是显式对象。因此你可以:
- 记录每个源的脉冲。
- 设置不同源频率。
- 使用复杂突触模型。
- 让源连接多个目标群体。
- 在突触上加入延迟或可塑性。
代价是需要创建和调度更多对象。
本例的 v 同时受到:
- TimedArray 连续驱动。
- PoissonInput 小而频繁的随机跳变。
- cue 在 35 和 75 ms 的同步大跳变。
- background 稀疏随机突触跳变。
- 自身漏衰减。
- 达到阈值后的 reset。
不要试图把图上的每一个小折点都只归因于一种机制。正确方法是逐层建立对照:
- 先只保留 TimedArray。
- 再加入 PoissonInput。
- 再加入 cue。
- 最后加入 PoissonGroup。
这叫消融实验:逐项移除或加入机制,判断它对结果的贡献。
state = StateMonitor(neurons, "v", record=[0])20 个神经元都接收相同 TimedArray 和 cue,但随机输入不同。
记录一个代表性神经元可以:
- 减少数据量。
- 让曲线容易阅读。
- 与全群体脉冲栅格图互补。
但单个神经元不能代表整个群体统计。若要判断群体差异,应增加:
- 多个代表神经元。
- 群体平均。
- 放电率分布。
TimedArray 只有 0.1,主要靠随机背景积累,活动较弱。
刺激升至 0.9,v 更接近阈值。35 ms 的 cue 再让所有神经元增加 0.3,容易形成一批同步脉冲。
刺激降至 0.2,活动应减弱。
刺激升至 1.1,本身已高于阈值平衡点;75 ms cue 进一步推动群体,活动应最强。
刺激回到 0.1,活动再次减弱。
具体脉冲数受随机种子影响,但时间结构应与刺激协议对应。
上图:
- 只显示 0 号神经元 v。
- 可观察连续追踪、随机跳变、cue 跳变和 reset。
下图:
- 每个点是任意神经元的一次脉冲。
- 可观察 35 ms 和 75 ms 附近是否形成同步活动。
- 可比较五个刺激阶段的活动密度。
把两图结合:
- 上图解释单个状态如何接近阈值。
- 下图验证这种机制是否在群体中形成脉冲。
脚本打印:
print("总脉冲数:", spikes.num_spikes)它用于快速确认网络是否有活动。
但总脉冲数把时间结构压缩成一个数字,无法回答:
- 脉冲集中在哪个刺激阶段。
- cue 是否造成同步。
- 哪些神经元最活跃。
统计量必须与问题匹配。总数适合健康检查,不足以完成机制分析。
N 是每个靶神经元接收的独立泊松输入源数量。
它们可以产生相似统计输入,但前者是显式源对象,后者是聚合输入机制。
本例按给定 dt 索引离散值,应把它理解为分段时间协议,而不是默认连续插值曲线。
25 是期望值。每次随机连接的实际数量会波动。
它们共享 TimedArray 和 cue,但随机泊松事件及随机连接不同。
依次注释掉:
- PoissonInput。
- cue 突触。
- background 突触。
每次预测图会失去什么特征。
把 cue 放在弱刺激区间和强刺激区间,比较同样 0.3 增量造成的脉冲数。
这能展示“同样输入的效果依赖当前网络状态”。
比较:
N=20, rate=10 Hz, weight=0.04
N=200, rate=1 Hz, weight=0.04
N=20, rate=20 Hz, weight=0.02
虽然某些平均量接近,波动结构未必相同。
给 background 添加 SpikeMonitor,把源脉冲与 target 响应放在同一时间轴分析。
预测不同靶神经元之间的输入差异会怎样变化。
选择输入对象前,先问:
输入是连续时间函数还是离散脉冲?
脉冲时刻已知还是随机?
是否需要记录每个源?
是否需要复杂突触和连接结构?
下一章会专门处理“怎样记录”。你会看到 StateMonitor、SpikeMonitor 和 PopulationRateMonitor 记录的是三种不同层次的数据,不能互相替代。