前阵子我处理一个只有五个物种的化学主方程(Chemical Master Equation,CME)模型时,一脚踩进“似然算不出、MCMC跑不动”的经典坑里。观测数据是某信号通路在抑制前后几个时间点的分子数测量,波峰、时滞、随机涨落全都在,可参数后验就是怎么都收敛不了。后来我把整条推断链路换成神经网络后验估计(Neural Posterior Estimation,NPE),再配上一个同时处理模型选择(Model Selection)的联合框架,原本几天都出不来的结果,在GPU上几小时就能拿到像样的后验分布,还能顺手回答“哪个反应机制更合理”这种模型选择问题。
这篇文章是我对这套做法的一次完整复盘。核心内容是:当化学反应网络层面的离散随机性让似然函数变得不可处理时,如何用模拟器加神经网络做贝叶斯推断;以及在多个候选反应网络之间做选择时,怎么把“选模型”和“估参数”两件事合成一个端到端框架。适合正在做系统生物学建模、计算生物学的朋友参考,也适合对模拟型推断(Simulation-Based Inference,SBI)感兴趣、但还没实际跑通流程的算法方向读者。
1. 化学主方程下的推断困局:为什么MCMC会跑不动
先解决第一个问题:化学反应网络的参数推断为什么比其他模型难。很多人第一次接触生化系统建模时,第一反应是写一组常微分方程(ODE),然后拟合曲线。但现实中的分子反应有很强的离散随机性,ODE只描述了平均行为,一旦系统处于低分子数区间或者存在多稳态,平均值根本不足以刻画系统状态。
1.1 从ODE到主方程:随机性才是麻烦的根源
假设一个最简单的基因表达模块,mRNA转录出蛋白,蛋白再发生降解。ODE写法通常是:
d[mRNA]/dt = k_transcription - d_mRNA * [mRNA] d[Protein]/dt = k_translation * [mRNA] - d_Protein * [Protein]这种写法隐含了一个假设:系统内分子数足够大,涨落可以忽略。但单细胞实验里,一个细胞里某类蛋白可能只有几十个甚至十几个分子。分子数少的时候,系统状态必须用“每个物种当前的分子数”这种离散向量来描述,演化规律也要换成化学主方程:
dP(x, t)/dt = Σ ( W(x | x') * P(x', t) - W(x' | x) * P(x, t) )其中 x 表示分子数状态,W 是反应转移率。这个方程本身是线性但维数极高的,状态数会随物种数指数增长。比如五个物种、每个物种分子数在 0 到 100 之间变化,理论状态数就是 101 的 5 次方量级,几千万甚至上亿个状态。想直接数值求解主方程,绝大多数系统的规模都撑不住。
所以行业里更常用的做法是走随机模拟路线,通过大量轨迹样本去近似系统的分布。这类算法在原理上模拟的是“下一个反应是什么、什么时候发生”,生成的轨迹天然包含随机涨落。问题是,模拟器虽然能生成数据,但正向生成容易、反向推断困难。
1.2 似然不可计算,贝叶斯推断就被卡死
贝叶斯推断的核心是后验分布:
p(θ | X) ∝ p(X | θ) * p(θ)其中 p(X | θ) 是似然。对于 CME 这类随机过程,似然函数通常没有闭式表达式,也不可能直接枚举状态空间去求和。于是经典 MCMC 就卡死了:MCMC 每次迭代都要计算一次似然,而这里的似然恰恰算不出来或者算起来成本极高。
有人会用线性噪声近似(Linear Noise Approximation)或者矩闭合方法去逼近似然,但这类方法对强非线性、双稳态、振荡系统经常失真。如果模型本身就是用来解释实验里的异常涨落或开关行为,那近似误差往往就落在你最关心的现象上,显然不可接受。
所以需要一条新路:既然我拿不到似然函数,但我能拿模拟器,能不能直接从模拟器生成的大量参数-数据配对里学会后验分布?这就是 SBI 的基本思想。
1.3 模型选择被卡住的真正瓶颈
参数推断还不是全部,实际项目中更难的是模型选择。我见过太多人把候选网络结构都建好,却不知道该怎么比。经典的信息准则,比如 AIC、BIC,都需要先算出一个靠谱的对数似然值。但在 CME 系统里这一步就不成立。
就算退一步,走严格贝叶斯路线去算模型证据:
p(X | M) = ∫ p(X | θ, M) * p(θ | M) dθ这里要把所有参数都积分掉,而 p(X | θ, M) 依然是一个无法计算的似然。结果就是,模型选择被卡在比参数推断更深的泥潭里。
现实中大部分人的应对方式很粗暴:用 ODE 拟合好坏去判断反应机制,或者干脆只看拟合曲线的均方误差。这样做的风险是,随机波动较大的数据会让几个候选模型都拟合得很像,均方误差根本区分不了;即便能区分,也谈不上不确定性量化。真正可靠的模型选择,必须把“观测分布本身的差异”作为依据,而且最好给出概率化的置信程度。
2. 核心思路:用模拟器代替似然,用神经网络逼近后验
既然正向模拟可行,逆向推断就可以交给神经网络。这里的关键并不是“深度学习怎么拟合数据”,而是如何设计一个端到端框架,让网络直接输出后验分布。同时,模型选择也要融进同一套流程里。
2.1 NPE:一种不需要似然的贝叶斯推断方法
NPE 的全称是 Neural Posterior Estimation,核心做法非常直接。
我先从先验分布 p(θ) 里采样一批参数,把每个参数丢进化学反应模拟器,生成对应的观测轨迹 x,于是得到大量配对样本:
(θ_1, x_1), (θ_2, x_2), ..., (θ_N, x_N)然后训练一个条件密度估计器 q_φ(θ | x)。训练目标不是最小化预测误差,而是最大化这批联合样本上的条件对数似然:
L = E_{θ, x ~ p(θ, x)} [ log q_φ(θ | x) ]这里微妙的点在于,因为样本来自联合分布 p(θ, x),理论上这个目标会让 q_φ(θ | x) 逐渐逼近真实后验 p(θ | x)。整个训练过程从头到尾没有调用一次似然函数,唯一需要的就是“能从参数生成数据”的模拟器。
密度估计器通常用归一化流(Normalizing Flow)实现。通俗解释,归一化流就是把一个简单分布比如标准高斯,通过一系列可逆的变换,慢慢揉捏成任意复杂分布。每一层变换都是可逆且有解析雅可比行列式的,所以网络既能灵活拟合复杂后验,又能直接给出密度值,还能从拟合分布里快速采样。做贝叶斯推断时,这个“能采样”的能力很重要,因为后验均值、分位数区间、后验预测检查都需要从后验里抽样本。
NPE 还有两个重要变体。非序列版本直接对全量先验范围训练,适合先验本身和真实后验差距不大的场景。序列版本则会把模拟逐步聚焦到高后验区域,降低模拟器计算压力。我在实际项目中通常先用非序列版本跑通全流程,确认摘要统计没问题,再考虑是否上序列版。
2.2 观测数据编码:摘要统计和embedding网络不能省
化学反应网络的观测往往是一组时间序列,例如多个时间点上的蛋白丰度测量。原始数据能否直接送进归一化流?理论上可以,但实践效果通常不好。
原因是原始时间序列维度高,而且不同候选模型的区分信息往往集中在少数特征里:峰值时间、上升斜率、稳态水平、波动幅度。所以一般要先把原始轨迹压缩成低维表示,再输入到网络。
有两种主流做法。第一种是手工设计摘要统计量,比如取每个观测物种的对数均值、对数方差、特定时间窗口的变化量、峰值时间。这个做法透明可解释,但需要反复验证摘要统计是否包含了足够的模型区分信息。第二种是用一个可训练的 embedding 网络自动从原始轨迹里学表示,常见结构是 1D CNN 加双向 LSTM 或者小型 Transformer。我把两种方法都试过,经验是:自动 embedding 通常更稳,因为它能发现我没想到的特征,但代价是训练不稳定、需要调参。
比较稳妥的组合方式是:先用一组基础摘要统计做兜底,再让 embedding 网络从原始轨迹里抽取额外特征,两者拼接后送到条件密度估计器。这样即便 embedding 暂时学不好,基础摘要也能保住大部分信息。
2.3 模型选择方案:把模型索引当成离散参数一起学
单纯的参数后验估计只能解决“给定反应网络结构,推测参数”的问题。但标题还强调模型选择,也就是并回答“多个候选反应机制里,哪个更可能产生这批观测数据”。
经典做法是分别对每个模型跑一遍 NPE,再尝试计算模型证据,但这样做有两个麻烦:一是多次训练成本高;二是用 NPE 后验再去估计模型证据时,重要性权重通常方差很大,结果不稳定。
更优雅的策略是把模型索引 m 当作一个额外的离散随机变量。整个联合先验写成:
p(θ, m) = p(θ | m) * p(m)每个模型 m 有自己的参数空间和模拟器。训练数据生成时先按先验概率抽一个模型,再从该模型的参数先验里抽参数,然后模拟得到观测。网络在同一个 embedding 表示之上分出两个头:
- 一个分类头输出 p_φ(m | x),给出模型后验概率;
- 一个密度头输出 q_φ(θ | x, m),给出模型条件参数后验。
两部分的损失合并起来一起优化。这就是“联合”的真正含义:参数后验和模型后验共享一套观测表示,互相受益。实际测试里,这个方案比“分别训练三个 NPE”要稳定得多,训练数据利用效率更高,得到的模型选择概率也自然是一个可解释的贝叶斯结果。
3. 从零搭一个化学网络推断和模型选择框架
这部分我完全按实操流程来讲。为了让步骤可执行,我用一个虚构的四物种信号转导模块为例,候选模型有三个:M1 是线性产生和降解过程,M2 增加自激活正反馈,M3 增加前馈负反馈通路。目标是用模拟的观测数据同时完成参数后验估计和机制选择。
3.1 建立候选模型集合与随机模拟器
第一步是明确每个候选模型的具体反应方程。例如 M1 可能只有两条反应:
∅ → A (合成速率 k1) A → ∅ (降解速率 k2)M2 加一条自催化反应:
A + A → 2A (自激活速率 k3)M3 则加入中间物种 B,形成负反馈:
A → A + B (联产速率 k4) B → ∅ (B 降解速率 k5) A + B → ∅ (A 被 B 抑制清除,速率 k6)每个模型的物种集合、反应网络都不同,参数维度也不同。M1 有两个参数,M2 有三个,M3 有五个。这里要特别强调:不同模型的参数空间维度不同完全没问题,因为密度头是条件在模型索引上的,每个模型使用自己的参数维度和先验。
模拟器我建议使用 Gillespie 类随机模拟算法。核心原理是:每个反应都有当前状态下的发生率,两个均匀随机数分别决定下一步反应发生的时间和反应类型。这个算法在物种数少的时候非常可靠,但速度偏慢。如果训练数据量很大,可以先用优化过的 tau-leaping 近似模拟器粗跑,再用精确算法微调部分数据,这个后文会展开。
代码逻辑大致如下:
def simulate_trajectory(model_index, theta, x0, t_obs): # model_index: 0 / 1 / 2 对应 M1 / M2 / M3 # theta: 该模型的参数向量,例如 log 空间速率常数 # t_obs: 需要观测的时间点列表 rates = build_reaction_rates(model_index, theta) trajectory = ssa_simulate(rates, x0, t_obs[-1]) return trajectory[t_obs] + noise这里一个重要细节是,参数最好在 log 空间内模拟。因为反应速率常数只能为正,在 log 空间采样天然满足约束,网络也更容易学习连续分布。
3.2 训练数据生产线的参数与规模
训练数据规模直接影响后验精度。我通常的做法是:每个候选模型生成 6 到 8 万条轨迹,三个模型合计 20 万条左右作为训练集;再额外生成 5 万条作为验证集。数量太少时归一化流容易出现某个区域拟合不足、某个区域过拟合的情况。
先验设置要围绕生物学可行区间来定。比如速率常数在 log 空间服从均匀分布,上下界保守地覆盖实验观测到的动力学范围;初始分子数可以固定在一个合理值,也可以在某个窄区间内随机化,避免网络将初始条件混淆为反应速率效应。
观测噪声也要模拟。实验里的单细胞测量通常带有较大的技术噪声,我喜欢在模拟轨迹上叠加对数正态噪声。噪声幅度按真实数据的变异系数去设置,比如变异系数 0.2 到 0.5。这一步如果省掉,训练出来的后验会对真实数据过度自信,因为网络没有见过“脏数据”。
每个模型的训练数据要按先验频率等量生成,避免网络因为某个模型样本多就产生模型偏见。生成好的 (m, θ, x) 三元组可以全部存入磁盘,训练时随机读取。数据生成是计算密集型环节,建议用多进程并行,每个进程负责一批独立的轨道。
3.3 网络结构、训练损失和关键超参数
网络结构方面,我推荐这样一个组合:
- embedding 层:输入是原始观测时间序列和手工摘要统计的拼接。原始序列先过三层一维卷积,每层输出通道 64,再进双向 LSTM,隐藏维 128;手工摘要统计通过一个两层的 MLP 映射到 128 维;最后拼接成一个约 256 维的表示向量。
- 模型索引层:模型索引先做 embedding 查表,比如每个模型对应一个 32 维向量,再复制或加性融合进网络。
- 归一化流部分:使用五层自回归掩码流,每个自回归层内部是两个 256 维的隐藏层。这个深度和宽度组合在大多数中小规模化学网络上够用,太深反而容易训练不稳定。
- 输出头:分类头是一个映射到 3 类的 MLP,输出模型后验 logits;密度头输出 q_φ(θ | x, m) 的参数和 log 密度值。
训练损失是两部分之和。密度头用负条件对数似然,分类头用交叉熵:
loss = -mean(log_q_phi(theta | x, model_index)) + alpha * cross_entropy(model_logits, model_index)这里 alpha 通常取 1.0,保持两个任务同等权重。如果模型选择任务特别难,可以加大 alpha 让网络优先学会分辨模型。训练使用 Adam 优化器,学习率 1e-3,加一个两轮 warmup 后余弦退火。批次大小 128,训练轮数 150 到 200 轮,用验证集上的总损失做早停。
训练好后,从后验里抽取样本非常简单:给定观测 x,把 x 编码成表示向量,再在归一化流里输入随机高斯噪声,反变换后得到参数样本;模型后验则直接从分类头取 softmax 概率。
3.4 评估阶段:后验校准与模型选择可靠性
训练完成后不能只看损失曲线下降就收工。我至少要跑三类评估。
第一类是单模型参数后验评估。拿一个已知真实参数的测试轨迹,把模型索引固定,让网络生成 1 万个后验样本,检查真实参数是否落在 90% 后验区间里。重复几百次后统计覆盖率。如果覆盖率明显低于 90%,说明网络过度自信,需要检查摘要统计或增加训练数据。
第二类是全局校准检验,也就是仿真校准检查(Simulation-Based Calibration)。做法是:从先验里取参数,模拟观测,用网络做后验推断,记录真实参数在后验分布中的百分位。理想情况下,这些百分位应该服从均匀分布。如果分布集中在低百分位,说明后验整体偏移,通常暗示先验与模拟器设置有问题。
第三类是模型选择评估。在测试集上计算模型选择准确率和混淆矩阵,同时看每个模型的预测概率是否校准。例如模型 M1 在 1000 条轨迹中预测概率平均 0.8,那么实际上这 1000 条轨迹里确实应该有大约 800 条来自 M1。校准性能可以通过期望校准误差来量化,如果偏差过大,需要检查模型间观测分布是否真的可区分。
4. 实操踩坑实录:常见问题与排查技巧
这段是我最想写的部分。NPE 和模型选择框架跑通不难,难在让它稳定工作。我把自己踩过的坑和排查方法整理成下面几组。
4.1 模拟器慢到劝退怎么办
化学网络模拟器是整套流程的性能瓶颈。生成 20 万条轨迹如果一条一条跑,有些复杂网络要跑好几小时甚至数天。我第一次跑带双稳态的候选模型时,数据生成就花了一天半。
提高模拟速度有三个有效手段。
第一是向量化模拟器。很多随机模拟算法是逐事件推进的,很难直接并行,但可以把初始条件、参数批次做成数组,在每一步同时推进多条独立轨迹,只要保证它们互不依赖即可。这样做能在保持精确性的前提下提升数据吞吐量。
第二是先用 tau-leaping 做粗粒度预训练。tau-leaping 是把一段时间内多个反应合并推进的近似方法,速度快很多。先让它生成数据训练一版模型,让网络先学到大致结构;最后再用精确模拟器生成一部分边界样本去修正,能省大量时间。
第三是缓存机制。如果某些参数组合对应轨迹在训练中被反复使用,可以按参数分区缓存模拟结果。NPE 训练如果每个 epoch 都重新模拟,那是灾难;正确做法是一次性生成全部数据并落盘,后续训练只是随机读取。
4.2 摘要统计丢了关键信息,模型自信得过头
NPE 一个非常隐蔽的问题是过度自信。如果观测数据没有携带足够多关于参数的信息,而流量模型又足够灵活,它可能学出一个窄而错的后验分布。最大来源通常是摘要统计设计不当。
比如区分双稳态模型和单调模型,如果摘要统计只用了均值,两条候选模型的均值轨迹可能几乎一样,网络就只能靠先验瞎猜。但峰值时刻附近的方差、切换概率这类特征会完全不同。所以摘要统计的设计必须和候选模型的动力学特征强绑定。
排查方法很简单:做 SBC 百分位分布检验。如果发现校准严重不足,大概率是摘要统计信息量不够。可以逐步往摘要里加特征,比如各时间点的分位数、相邻时间点的涨落方差、频谱密度,每加一个就重新评估覆盖率和校准误差。也可以直接用端到端 embedding 网络替代手工摘要,让网络自己决定看什么。但要提醒,embedding 网络也需要足够训练数据才能学出好表示,数据量小时手工摘要反而更稳。
4.3 模型选择的不稳定与先验效应
模型选择部分最常见的问题是结果对先验非常敏感。贝叶斯框架下这本身合理,模型后验本来就依赖模型先验 p(m),但很多人会忽略“参数先验也会影响模型选择”。
举例说明:三个候选模型里,M2 的参数维度更高,如果速率参数的先验范围设得过大,M2 的先验分布会非常分散,后验里它得到支持的概率就会被稀释。即便 M2 能完美解释数据,模型证据也会偏低。这不是框架错误,而是先验敏感性。
解决办法是报告不同先验设置下的模型后验结果,而不是只给一个数字。比如跑两组先验,一组宽先验、一组紧先验,如果模型选择排序翻转,说明候选模型之间的观测分布差异不足以可靠区分,此时应该直接告诉用户“数据在当前观测条件下无法区分这些机制”。这种诚实结论比强行给一个高概率更有价值。
另外,候选模型之间可能存在近似不可区分的情形。比如 M2 的自激活速率非常低时,行为退化成 M1 的线性增长。测试集上模型选择矩阵会显示 M1 和 M2 相互混淆。这种时候我建议计算候选模型的先验预测分布之间的散度,提前识别哪些模型对“可区分”,哪些根本不可能区分,避免浪费算力去硬学。
| 问题 | 常见原因 | 排查与解决 |
|---|---|---|
| 模型选择准确率忽高忽低 | 摘要统计对模型特征不够敏感 | 增加时间点分位数、局部方差、频谱特征 |
| 训练损失低但覆盖率差 | 流量模型过拟合模拟分布 | 扩大训练集、替换更大容量embedding、调整正则 |
| 模型后验概率被先验过多主导 | 参数先验范围过宽 | 根据实验背景收紧先验,做敏感性分析 |
| 测试集模型混淆集中在某对模型 | 两个机制在观测尺度上近似不可区分 | 增加观测频率或报告不可区分性结论 |
| 训练初期损失波动大 | 归一化流初始化不稳定 | 降低学习率、增加warmup轮数、固定随机种子 |
我实际做项目的体会是,NPE 联合模型选择这个框架真正厉害的地方不是“深度学习黑箱”,而是它把贝叶斯推断里最麻烦的似然计算替换成了可验证的模拟和密度估计。只要你把摘要统计校准好、先验设置合理,它能在比 MCMC 快几个数量级的情况下给出可靠性相当的结果。但也不能完全放手不管,覆盖率测试和模型选择校准一定要跑,这两步能挡住九成以上的隐性错误。
最后再给一个小技巧:如果你手头有一批真实观测数据,但真实机制未知,训练完框架后别急着看模型后验,先做一组后验预测检查。用模型后验概率最高的模型采样参数,预测出若干合成轨迹,把合成轨迹和真实轨迹画在一起对比。这个直观检查能快速暴露模拟器和真实系统之间的系统性偏差,比任何数值指标都灵敏。我第一次做时就发现模拟器缺少一个“短暂滞后期”的细节,加进模拟器后再跑模型选择,结论直接反转。这个细节,常规文档里基本不会写。