糖尿病视网膜病变视力丧失预测:贝叶斯估计与威布尔分布
副标题:当我们说"预测失明"时,到底在预测什么?------一场从临床困惑到概率模型的层层拆解
0. 从一个无法回答的问题开始
想象你是一名内分泌科医生。一位 58 岁的 2 型糖尿病患者坐在你面前,眼底照相显示轻度非增殖性糖尿病视网膜病变(NPDR)。他抬起头问你:"医生,我大概还有多久会看不见?"
别急。这个问题看似简单,实则藏着几个认知陷阱:
- 不是"会不会",而是"什么时候":这不是二分类问题,不是贴个"高风险/低风险"标签就能打发的。患者想知道的是时间------一种连续变量的预测。
- 不是所有人同步出发:有的患者确诊 DR 时已经 60 岁,有的才 40 岁;有人糖化血红蛋白长期 9%,有人控制在 6.5%。他们的"时钟"走得不一样快。
- 我们永远看不到完整剧本:很多患者在中途失访、搬家、或者死于其他并发症。他们的"失明时间"是一个被截断(censored)的未知数。
听起来抽象对吧?没关系。接下来,我们将像剥洋葱一样,从"这个临床问题该用什么工具"开始,一层层剥到"计算机如何在参数空间里做随机漫步"。每一层只解决一个让你夜不能寐的疑问。
如果画成图,整篇文章的旅程大概是这样的------一条从临床问题出发,经过威布尔分布、贝叶斯更新、层次建模,最终抵达个体化生存预测的流水线:
临床问题:
患者何时失明?
生存分析框架:
处理时间与截断
威布尔分布:
时间的弹性模具
贝叶斯估计:
先验 + 数据 → 后验
层次贝叶斯:
人群→个体信息流动
MCMC采样:
后验分布的数值逼近
个体化预测:
S(t|x,θ) 与置信带
现在我们已经了解了整趟旅程的路线图,接下来看看第一站:为什么传统的分类模型在这里会失灵?
1. 为什么不是分类?------生存分析的直觉
1.1 被浪费的信息
假设我们把问题粗暴地改成二分类:"5 年内会失明吗?"然后训练一个 Random Forest 或 ResNet。听起来合理,但等等------我们浪费了什么?
想象两个患者:
- 患者 A:在第 4 年失明(事件发生在观察期内)。
- 患者 B:随访了 4 年还没失明,但第 5 年搬走了(右截断,right-censored)。
在分类器的标签体系里,A 是"阳性",B 是"阴性"。但直觉告诉我们,B 的信息和 A 完全不同:B 其实告诉我们"至少能撑 4 年",这是一个不等式,而不是一个确定的终点。分类器把这两种信息压扁成同一个 0/1 空间,相当于把一杯红酒和一杯白开水都标记为"液体",然后问为什么品不出区别。
如果画成图,数据在时间里长这样------有些到达终点(红点),有些被截断(蓝箭头),还有些在中间区间被观察到(区间截断):
第2年失明
随访到第3年
未失明
第1-2年间
区间截断
时间轴 t(年)
0
1
2
3
4
5
患者A
●
事件
患者B
○
右截断
患者C
◎
区间截断
1.2 生存函数:概率的另一种形状
现在我们已经了解了分类框架的浪费,接下来看看生存分析如何优雅地接住这些信息。
在生存分析里,我们不预测一个硬标签,而是预测一个生存函数 S(t):给定患者已经挺到时间 t,他继续挺下去的概率。数学上,S(t) = P(T > t),其中 T 是事件发生时间(比如视力丧失)。
这就像一个递减的阶梯或曲线,从 1.0(100% 还没失明)开始,随着时间慢慢下滑。曲线下降越快,说明疾病进展越凶猛;下降越平缓,说明预后越好。
如果画成图,三个不同严重程度的患者群体,他们的生存曲线大概是这样分叉的:
S(t)高
S(t)中
S(t)低
时间 t
0
2
4
6
8
生存概率 S(t)
1.0
0.8
0.6
0.4
0.2
0.0
轻度NPDR:
平缓下降
→
中度NPDR:
中等斜率
→
重度NPDR:
陡峭下降
→
锚定已知:如果你熟悉 RNN 里的"遗忘门",你可以把 S(t) 想象成一个随时间衰减的"记忆保留概率"------只不过这里遗忘的不是信息,而是视力。
在继续之前,让我们把 toy example 再缩小一点。假设我们只有 5 个患者,随访 10 年:
| 患者 | 事件时间 | 状态 | 含义 |
|---|---|---|---|
| 1 | 3.2 | 事件 | 第 3.2 年失明 |
| 2 | 5.1 | 事件 | 第 5.1 年失明 |
| 3 | 7.8 | 截断 | 随访到第 7.8 年,未失明 |
| 4 | 2.5 | 事件 | 第 2.5 年失明 |
| 5 | 10.0 | 截断 | 完整随访 10 年,未失明 |
第一步,我们先算经验生存函数(Kaplan-Meier 估计)。这不需要任何分布假设,就像先做一个"直方图草稿"看看数据的轮廓。KM 曲线会在每个事件时间点往下跳一步,截断数据不引起跳跃,但减少分母。
但 KM 曲线是阶梯状的、非参数的。它描述了过去,却不方便预测未来------尤其当我们想回答"一位新患者,糖化 8.5%,病程 12 年,他的 S(t) 长什么样?"时。我们需要一个参数化的、带协变量的模具来浇筑这些曲线。
这就是威布尔分布登场的时刻。
2. 威布尔分布------时间的弹性模具
2.1 为什么选威布尔?
听起来抽象对吧?我们不是在选最美味的蛋糕,而是在选一个能弯折成各种形状的橡皮泥模具。
指数分布只有一个速率参数 λ,它的危险率(hazard rate)是恒定的------就像一台老化程度不变的机器,每小时坏掉的概率都一样。但糖尿病视网膜病变不是这么回事:随着病程延长,微血管损伤累积,失明的瞬时风险通常是随时间上升的(如果未治疗),或者至少不是恒定的。
威布尔分布有两个参数:
- 形状参数 k(或写作 α、ρ):决定曲线的"脾气"。k > 1 时危险率递增(越拖越危险);k = 1 时退化为指数分布(恒定危险);k < 1 时危险率递减(早期高危,后期趋稳)。
- 尺度参数 λ(或写作 σ、η):决定时间的"缩放比例"。λ 越大,事件越晚发生。
如果画成图,威布尔的概率密度函数就像一根可以被掰弯的香蕉:
形状参数 k 对危险率 h(t) 的影响
k < 1:
递减危险
早期爆发型
→
k = 1:
恒定危险
指数分布
→
k > 1:
递增危险
累积损伤型
→
在 DR 的场景中,大量研究表明 k 往往大于 1。比如 DeepDR Plus 系统就使用固定大小的威布尔混合模型来模拟个体生存分布,在预测 DR 进展时取得了 0.754--0.846 的 concordance index。这说明疾病进展符合"越往后越危险"的累积损伤逻辑。
2.2 从向量到矩阵:威布尔的数学骨架
别急,我们先看单个患者的 toy example,再推广到矩阵实现。
威布尔分布的生存函数(survival function)和概率密度函数(PDF)长这样:
S(t | k, λ) = exp[ -(t/λ)^k ]
f(t | k, λ) = (k/λ) * (t/λ)^(k-1) * exp[ -(t/λ)^k ]
在 Mermaid 里,如果我们要画一个节点来表达这个公式,绝对不能写 $...$ 或者 \lambda。正确的写法是用引号包裹,内部用 HTML 标签:
S(t | k, λ) = exp[ -(t/λ)k ]
f(t | k, λ) = (k/λ)(t/λ)k-1exp[ -(t/λ)k ]
现在,假设我们有一个小数据集(还是刚才那 5 个患者)。在频率学派的框架下,我们会写似然函数:
对于观察到事件 的患者(患者 1、2、4),他们对似然的贡献是 PDF 在事件时间的值。
对于右截断的患者(患者 3、5),他们的贡献是生存函数在截断时间的值------因为"挺到这时还没失明"的概率就是 S(t)。
所以总似然是:
L(k, λ | data) = f(3.2) * f(5.1) * S(7.8) * f(2.5) * S(10.0)
取对数后,我们得到一个关于 k 和 λ 的二维函数。频率学派会找让这个函数最大化的 (k, λ) 对,也就是最大似然估计(MLE)。
但如果画成图,这个对数似然表面在参数空间里长什么样?它像一座山,山顶就是 MLE。可问题是------这座山的形状可能很怪,山顶可能很尖,也可能是个平缓的高原。更麻烦的是,当我们引入协变量(比如糖化血红蛋白、病程年限)时,λ 不再是一个固定数,而是随患者变化的 λi = exp(βT xi)。这时候参数空间从 2 维膨胀到 2 + p 维(p 是协变量个数),山的维度骤增,MLE 的寻找变得像在迷雾中攀岩。
对数似然表面(概念图)
贝叶斯方法:
不只找山顶
而是画等高线
平坦区域:
信息匮乏
山顶:
MLE估计
山谷:
低概率区域
山脊:
参数相关性高
后验分布
现在我们已经了解了威布尔分布为什么比指数分布更适合 DR,接下来看看贝叶斯估计如何把这个"找山顶"的游戏升级为"画地图"的艺术。
3. 贝叶斯估计------从"点估计"到"信念分布"
3.1 先验:在数据到来之前的"偏见"
频率学派说:"让数据自己说话。"贝叶斯学派回答:"但如果我们已经知道一点医学常识,为什么要假装自己是婴儿?"
贝叶斯的核心公式,我们在高中就见过:
后验 ∝ 先验 × 似然
P(θ | data) ∝ P(θ) × P(data | θ)
在 Mermaid 里表达这个关系,我们要避免 LaTeX:
先验 P(θ)
医学常识 / 历史数据
×
似然 P(data|θ)
当前患者数据
后验 P(θ|data)
更新后的信念
具体到 DR 的威布尔模型,θ = (k, λ, β1, ..., βp)。我们需要为每个参数选一个先验分布。
锚定已知 :如果你调过神经网络的超参数,先验就像是权重衰减(weight decay)------一种在数据不足时把参数往合理区间拉的软性约束。
- k(形状参数):我们知道 DR 的危险率通常是递增的,所以 k 应该大于 1。可以选一个 Gamma(2, 1) 或 Uniform(0.5, 5) 作为弱信息先验。
- λ(尺度参数):取决于时间单位。如果是"年",可以先验认为中位生存时间在 5--20 年之间,对应一个宽泛的 Half-Normal 或 Gamma 先验。
- β(回归系数):对每个协变量(如 HbA1c、病程),我们假设它们对风险的影响不会极端到 100 倍,所以选 Normal(0, 1) 或 Normal(0, 2.5) 是标准做法。
如果画成图,先验就像一团云雾笼罩在参数空间上。数据到来后,这团云雾被"挤压"到似然高的区域,形成更紧致的后验云雾:
参数空间 (k, λ)
数据注入
贝叶斯更新
先验:
宽大云雾
覆盖广阔区域
似然:
聚光灯区域
数据支持的位置
后验:
紧致云雾
不确定性缩小
3.2 为什么贝叶斯对 DR 特别友好?
别急,这里有个临床痛点。在 DR 研究中,进展到失明的事件是稀少的。你可能随访了 1000 名患者,10 年后只有 80 人失明。小样本 + 截断数据 = 频率学派的噩梦。MLE 的方差估计会很大,置信区间宽得像大海。
贝叶斯方法通过先验引入历史研究的信息,相当于"借来"其他数据集的力量。比如,UK Prospective Diabetes Study (UKPDS) 曾对 3642 名受试者使用威布尔分布评估糖尿病并发症(包括失明)的发生概率。我们可以把 UKPDS 的 k 和 λ 的后验当作新研究的先验------这就是贝叶斯更新的美妙之处:昨天的后验,是今天的先验。
此外,贝叶斯直接给出后验预测分布。当我们想知道"一位新患者 5 年内失明的概率"时,我们不是只插一个点估计进 S(t),而是把后验里所有可能的 (k, λ, β) 都试一遍,加权平均。结果是一个自带置信带的预测:"中位数估计是 12%,但 95% 可信区间是 3%--28%"。这种不确定性量化对临床决策至关重要------医生可以根据区间宽窄决定是否加强随访。
新患者特征 x
后验样本 θ(1),...,θ(M)
对每个样本:
计算 S(t|x,θ(m))
聚合:
中位数 + 95% CrI
临床决策:
区间宽→加强随访
区间窄→常规筛查
在继续之前,让我们把 toy example 再推进一步。假设我们只有 5 个患者,但想估计 k 和 λ。由于样本极小,MLE 可能给出一个很离谱的 k(比如 0.3,意味着危险率递减,与医学常识相悖)。但如果我们在 k 上放一个 Gamma(2, 1) 先验(均值 2,偏向递增危险),后验就会被拉回合理区间。这就像在 RNN 里用梯度裁剪防止梯度爆炸------先验是一种"常识正则化"。
现在我们已经了解了贝叶斯估计如何把点变成云,接下来看看当患者之间存在层次结构时(比如同一医院的患者、双眼数据),信息如何在不同层级间流动。
4. 层次贝叶斯------把人群和个体用管道连起来
4.1 为什么需要层次结构?
想象一个场景:你收集了 5 家医院共 2000 名 DR 患者的数据。现在问题来了------
- 患者 A 和 B 在同一家医院,由同一台眼底照相机拍摄。他们的测量误差可能相关。
- 患者 C 只有左眼数据,患者 D 有双眼数据,且双眼不是独立的(一个人全身的微血管环境共享)。
- 有些医院随访勤,有些医院随访稀疏。数据质量层次不齐。
如果用一个"大平层"模型把所有患者扔在一起,假设他们独立同分布,就像假设所有 RNN 时间步共享同一组权重而不考虑时序依赖一样荒谬。
层次贝叶斯模型(Hierarchical Bayesian Model, HBM)说:我们不把参数看成固定常数,而是看成从更高层分布中抽出来的样本。
如果画成图,数据的层级像一栋三层小楼:
层次贝叶斯的三层信息流
N(μ, Σ)
Weibull(ki, λi)
人群层 Population
μ, Σ: 超参数
所有患者共享
个体层 Individual
τi = (ki, λi)
从人群分布抽取
观测层 Observation
tij: 第i人第j次随访
从个体分布抽取
4.2 信息流动的直觉
这栋楼的管道设计非常精妙。让我们用一个 toy example 来感受信息如何流动。
假设我们只有 3 名患者:
- 患者 1:随访 10 年,事件丰富,数据很"吵"但信息量大。
- 患者 2:随访 2 年,早期截断,数据很少。
- 患者 3:随访 8 年,中期截断。
在普通模型里,患者 2 的 (k, λ) 估计会非常不稳定,因为数据太少。但在 HBM 里,患者 2 的个体参数 τ2 是从人群分布 N(μ, Σ) 中抽取的。人群分布的均值 μ 由所有患者共同塑造,尤其是信息丰富的患者 1。所以患者 2 虽然自己的数据少,但通过"人群层"这个中央水池,借到了患者 1 和患者 3 的信息。
这有点像全连接层里的权重共享------不是每个神经元独立学习,而是整个层通过反向传播共享梯度信息。在 HBM 里,"反向传播"就是贝叶斯更新,"梯度"就是后验概率的流向。
如果画成信息流动的管道图:
患者3
数据中等
患者2
数据稀缺
患者1
数据丰富
塑造
塑造
反哺
t1, t2, t3, ...
t1 (截断)
t1, t2
人群层
共享超参数
μ, Σ
Montesano 等人在分析青光眼视野进展时,正是采用了这种三层 HBM:人群层设定超参数先验,个体层为每个患者抽取参数,观测层处理截断和异方差。他们发现,层次模型将进展检测的中位时间缩短了 37%,同时大幅降低了估计偏差。虽然他们的直接对象是青光眼,但视觉功能进展的统计结构与 DR 视力丧失高度同源------都是随时间衰减的敏感度量,都面临截断和测量噪声。
4.3 双眼数据的依赖结构
DR 研究还有一个特殊之处:很多患者有双眼数据。一个人的左眼和右眼不是独立的------它们共享相同的血糖环境、相同的全身微血管病变程度。
在层次模型里,我们可以再加一层:
人群层 → 个体层(患者) → 眼睛层(左眼/右眼) → 观测层(每次随访)
Sarhan 等人提出的 Bivariate Burr XII Inverse Weibull (BBXII-IW) 分布正是为了建模这种双眼依赖的竞争风险。虽然他们的模型更复杂(涉及 Marshall-Olkin copula 构造),但核心直觉是一致的:不把双眼当作两个独立样本,而是用一个共享的脆弱项(frailty)或相关结构把它们绑在一起。
如果画成图,双眼数据的相关性就像两根被橡皮筋绑在一起的绳子:
患者X
共同拉高
或拉低风险
共同拉高
或拉低风险
相关≠独立
左眼
Tleft
共享脆弱项
νi ~ Gamma
右眼
Tright
现在我们已经了解了层次结构如何让信息像水一样在患者之间流动,接下来看看当后验分布没有解析解时,我们如何用 MCMC 在参数空间里做随机漫步来逼近答案。
5. MCMC------在参数空间里的随机漫步
5.1 为什么需要采样?
听起来抽象对吧?让我们回到那座"似然山"。在 toy example 里(5 个患者,2 个参数),我们或许可以 brute-force:把 k 和 λ 切成 100×100 的网格,每个格子算一个后验概率,最后归一化。这就是 Allen Downey 在《Think Bayes》里展示的网格法------直观、精确,但维度一高就爆炸。
如果引入 10 个协变量,参数空间变成 12 维。10012 个网格点?别说你的笔记本,超算也得跪。
MCMC(Markov Chain Monte Carlo)说:我们不需要看清整座山的每块石头,只需要在山里散步,按"后验概率高低"决定下一步往哪走。待得越久的地方,说明后验概率越高。最后把脚印收集起来,这些脚印的分布就是后验分布的近似。
如果画成图,网格法 vs MCMC 就像普查 vs 抽样调查:
MCMC(高维必需)
起点
迈步
接受/拒绝
新位置
只在高概率区
密集停留
网格法(低维可行)
k=0.5
λ=5
k=0.5
λ=6
k=0.6
λ=5
k=0.6
λ=6
5.2 Gibbs 采样与 NUTS
在贝叶斯威布尔生存模型中,最常用的两种采样器是 Gibbs 采样和 Hamiltonian Monte Carlo(HMC,及其自动调参版本 NUTS)。
Gibbs 采样像是一个轮流值班的人:今天固定 k 和 β,只更新 λ;明天固定 λ 和 β,只更新 k。每次只在一个维度上动,条件分布往往比联合分布好算。Kumar 等人和 Lin 等人的早期工作就使用 Gibbs 采样在 WinBUGS 里实现威布尔共享脆弱模型。
**NUTS(No-U-Turn Sampler)**则像是一个会利用梯度信息的山地车手:它不仅知道哪里高,还知道坡往哪倾斜(通过似然函数对参数的梯度)。于是它能沿着后验分布的"等高线"高效滑行,而不是像 Gibbs 那样一步一步蹭。PyMC 和 Stan 都默认使用 NUTS,因为它在高维空间(比如 50+ 个参数)里比 Gibbs 快几个数量级。
如果画成图,两种采样策略的步态差异如下:
NUTS / HMC
计算梯度
∇log P(θ|data)
沿等高线
滑翔一大步
U-Turn检测
自动停止
Gibbs 采样
沿 λ 轴移动
沿 k 轴移动
沿 β1 轴移动
5.3 诊断:你的链收敛了吗?
别急,采样完了不代表万事大吉。MCMC 有一个经典问题:如果采样器被困在后验分布的某个局部高地里,它给出的"脚印"就不能代表整座山。
标准诊断工具是 Gelman-Rubin R̂ 统计量(Potential Scale Reduction Factor)。基本思想是:同时跑 4 条独立的链,如果它们都探索到了相同的区域,链内方差和链间方差应该差不多,R̂ ≈ 1.0。如果某条链还在半山腰另一条已到山顶,R̂ 会远大于 1,说明还没收敛。
另一个直观检查是迹线图(trace plot):把参数值随迭代次数画出来。好的迹线应该像一条"毛茸茸的毛毛虫",没有明显的趋势或周期,几条链交织在一起。如果画成图:
未收敛
参数值
有明显漂移
链间分离
周期性波动
收敛良好
参数值
毛茸茸毛毛虫状
多条链重叠
无趋势
现在我们已经了解了 MCMC 如何在参数空间里用脚印丈量后验分布,接下来看看这一切如何落地为可执行的算法,以及在实际代码中数据是如何流转的。
6. 结构化伪代码:从数据到后验预测的完整流水线
6.1 整体算法架构
让我们把之前所有的直觉压缩成一份类 Pascal/Algol 风格的结构化伪代码。这段伪代码不绑定任何具体编程语言(Python/R/Stan),但足够详细到可以直接翻译成实现。
锚定已知:如果你读过 Transformer 的原始论文,你会记得那种"带缩进、混排数学符号"的伪代码风格。我们这里遵循同样的学术传统,但把生存分析的特有结构(截断处理、层次先验、MCMC 更新)显式展开。
Algorithm 1: Hierarchical Bayesian Weibull Survival for DR Vision Loss
Input:
D = {(t<sub>i</sub>, δ<sub>i</sub>, x<sub>i</sub>, h<sub>i</sub>, e<sub>i</sub>) | i = 1,...,N} // 观测数据
// t<sub>i</sub>: 随访时间
// δ<sub>i</sub>: 事件指示 (1=失明, 0=截断)
// x<sub>i</sub>: 协变量向量 [HbA1c, duration, age, ...]
// h<sub>i</sub>: 医院/中心标识
// e<sub>i</sub>: 眼睛标识 (1=左眼, 2=右眼, 0=单眼)
PriorSpec: 超先验参数
MCMCSpec: {chains, iterations, burn_in, thinning}
Output:
{θ<sup>(m)</sup> | m = 1,...,M} // 后验样本
{S<sub>pred</sub>(t | x<sub>new</sub>)<sup>(m)</sup>} // 新患者的预测生存曲线
// ============================================================
// 第一步: 模型规格化 (Model Specification)
// ============================================================
procedure SpecifyModel(D, PriorSpec):
// 人群层超先验
μ<sub>k</sub> ~ Normal(0, 2.5) // 形状参数对数的均值
σ<sub>k</sub> ~ Half-Cauchy(0, 2.5) // 形状参数对数的标准差
μ<sub>λ</sub> ~ Normal(0, 2.5) // 尺度参数对数的均值
σ<sub>λ</sub> ~ Half-Cauchy(0, 2.5) // 尺度参数对数的标准差
Σ<sub>β</sub> ~ LKJCholesky(2) // 协变量系数的相关结构
τ<sub>β</sub> ~ Half-Cauchy(0, 2.5) // 协变量系数的全局尺度
// 医院层随机效应
σ<sub>hosp</sub> ~ Half-Cauchy(0, 1) // 医院间异质性
// 共享脆弱项 (双眼相关)
ν<sub>i</sub> ~ Gamma(1/θ<sub>frail</sub>, θ<sub>frail</sub>) // 患者级脆弱
θ<sub>frail</sub> ~ Exponential(1)
return ModelGraph
end procedure
// ============================================================
// 第二步: 层次参数生成 (Hierarchical Parameter Generation)
// ============================================================
procedure GenerateIndividualParameters(ModelGraph, i):
// 从人群池抽取个体参数
log(k<sub>i</sub>) ~ Normal(μ<sub>k</sub>, σ<sub>k</sub>)
log(λ<sub>base,i</sub>) ~ Normal(μ<sub>λ</sub>, σ<sub>λ</sub>)
// 医院随机效应
η<sub>h<sub>i</sub></sub> ~ Normal(0, σ<sub>hosp</sub>)
// 协变量效应 (加速失效时间模型)
β ~ Normal(0, diag(τ<sub>β</sub>) × Σ<sub>β</sub> × diag(τ<sub>β</sub>))
// 个体尺度参数 (AFT 形式)
λ<sub>i</sub> = λ<sub>base,i</sub> × exp(β<sup>T</sup> x<sub>i</sub> + η<sub>h<sub>i</sub></sub>)
// 应用脆弱项 (乘法修正危险率)
λ<sub>frail,i</sub> = λ<sub>i</sub> × ν<sub>i</sub><sup>-1/k<sub>i</sub></sup>
return θ<sub>i</sub> = (k<sub>i</sub>, λ<sub>frail,i</sub>, β, η<sub>h<sub>i</sub></sub>, ν<sub>i</sub>)
end procedure
// ============================================================
// 第三步: 对数似然计算 (Log-Likelihood Evaluation)
// ============================================================
function ComputeLogLikelihood(D, {θ<sub>i</sub>}):
logL = 0
for each observation i in D do:
k = k<sub>i</sub>
λ = λ<sub>frail,i</sub>
t = t<sub>i</sub>
δ = δ<sub>i</sub>
if δ == 1 then
// 观察到事件: 使用威布尔 PDF
logL += log(k) - log(λ) + (k-1)×[log(t) - log(λ)] - (t/λ)<sup>k</sup>
else if δ == 0 then
// 右截断: 使用生存函数
logL += -(t/λ)<sup>k</sup>
else if δ == -1 then
// 区间截断 (interval-censored)
// t = (L, R): 事件发生在 (L, R] 之间
L = t.lower; R = t.upper
logL += log[ exp(-(L/λ)<sup>k</sup>) - exp(-(R/λ)<sup>k</sup>) ]
end if
// 双眼相关性修正 (如果 e<sub>i</sub> ≠ 0)
if patient(i) has two eyes then
// 通过共享 ν<sub>i</sub> 已在 λ<sub>frail</sub> 中体现相关
// 此处无额外项,因为脆弱项已耦合
end if
end for
return logL
end function
// ============================================================
// 第四步: 对数先验计算 (Log-Prior Evaluation)
// ============================================================
function ComputeLogPrior(ModelGraph, {θ<sub>i</sub>}):
logP = 0
// 超参数先验
logP += logNormal(μ<sub>k</sub> | 0, 2.5) + logHalfCauchy(σ<sub>k</sub> | 0, 2.5)
logP += logNormal(μ<sub>λ</sub> | 0, 2.5) + logHalfCauchy(σ<sub>λ</sub> | 0, 2.5)
logP += logLKJ(Σ<sub>β</sub> | 2) + sum(logHalfCauchy(τ<sub>β,j</sub> | 0, 2.5))
// 层次参数的先验已在 GenerateIndividualParameters 中隐含
// 此处累加随机效应和脆弱项
for each hospital h do:
logP += logNormal(η<sub>h</sub> | 0, σ<sub>hosp</sub>)
end for
for each patient p do:
logP += logGamma(ν<sub>p</sub> | 1/θ<sub>frail</sub>, θ<sub>frail</sub>)
end for
return logP
end function
// ============================================================
// 第五步: MCMC 采样核心 (NUTS Sampler Kernel)
// ============================================================
procedure NUTSSampler(θ<sub>current</sub>, logPost<sub>current</sub>, ε, L_max):
// 计算对数后验梯度
g = ∇<sub>θ</sub> [ ComputeLogLikelihood(D, θ) + ComputeLogPrior(ModelGraph, θ) ]
// 初始化动量 (辅助变量)
r<sub>0</sub> ~ Normal(0, I)
// leapfrog 积分: 沿后验等高线模拟哈密顿动力学
θ<sub>prop</sub> = θ<sub>current</sub>
r<sub>prop</sub> = r<sub>0</sub> + 0.5 × ε × g
for step = 1 to L_max do:
θ<sub>prop</sub> += ε × r<sub>prop</sub>
g<sub>new</sub> = ∇<sub>θ</sub> logPost(θ<sub>prop</sub>)
r<sub>prop</sub> += ε × g<sub>new</sub>
// U-Turn 检测: 如果动量翻转,停止积分
if (θ<sub>prop</sub> - θ<sub>current</sub>)<sup>T</sup> × r<sub>prop</sub> < 0 then
break
end if
end for
// Metropolis-Hastings 接受/拒绝
logα = logPost(θ<sub>prop</sub>) - 0.5×r<sub>prop</sub><sup>T</sup>r<sub>prop</sub>
- logPost(θ<sub>current</sub>) + 0.5×r<sub>0</sub><sup>T</sup>r<sub>0</sub>
u ~ Uniform(0, 1)
if log(u) < logα then
return θ<sub>prop</sub> // 接受
else
return θ<sub>current</sub> // 拒绝
end if
end procedure
// ============================================================
// 第六步: 主循环 (Main MCMC Loop)
// ============================================================
procedure RunMCMC(D, ModelGraph, MCMCSpec):
// 初始化多条链
for c = 1 to MCMCSpec.chains do:
θ<sub>c,0</sub> = InitializeFromPrior(ModelGraph) // 从先验抽样
while ComputeLogPost(θ<sub>c,0</sub>) is -∞ do:
θ<sub>c,0</sub> = InitializeFromPrior(ModelGraph) // 确保有限
end while
end for
// 迭代采样
for iter = 1 to MCMCSpec.iterations do:
for c = 1 to MCMCSpec.chains do:
θ<sub>c,iter</sub> = NUTSSampler(θ<sub>c,iter-1</sub>, ε<sub>adapt</sub>, L<sub>max</sub>)
// 自适应步长 (前 burn_in 期)
if iter ≤ MCMCSpec.burn_in then
ε<sub>adapt</sub> = AdaptStepSize(acceptance_rate)
end if
end for
// 每 thinning 步保存一次
if iter > MCMCSpec.burn_in && iter mod MCMCSpec.thinning == 0 then
for c = 1 to MCMCSpec.chains do:
Samples.append(θ<sub>c,iter</sub>)
end for
end if
end for
// 收敛诊断
R̂ = GelmanRubinDiagnostic(Samples_by_chain)
if max(R̂) > 1.01 then
Warning("Chains may not have converged. Increase iterations.")
end if
return Samples // M 个后验样本
end procedure
// ============================================================
// 第七步: 后验预测 (Posterior Prediction)
// ============================================================
procedure PredictSurvival(x<sub>new</sub>, h<sub>new</sub>, Samples, t_grid):
// t_grid = [0, 0.5, 1.0, ..., T_max]
S<sub>matrix</sub> = zeros(M, length(t_grid))
for m = 1 to M do:
θ<sup>(m)</sup> = Samples[m]
// 抽取新患者的个体参数 (从人群后验预测)
log(k<sub>new</sub>) ~ Normal(μ<sub>k</sub><sup>(m)</sup>, σ<sub>k</sub><sup>(m)</sup>)
log(λ<sub>base,new</sub>) ~ Normal(μ<sub>λ</sub><sup>(m)</sup>, σ<sub>λ</sub><sup>(m)</sup>)
η<sub>h<sub>new</sub></sub><sup>(m)</sup> ~ Normal(0, σ<sub>hosp</sub><sup>(m)</sup>)
ν<sub>new</sub><sup>(m)</sup> ~ Gamma(1/θ<sub>frail</sub><sup>(m)</sup>, θ<sub>frail</sub><sup>(m)</sup>)
λ<sub>new</sub><sup>(m)</sup> = λ<sub>base,new</sub> × exp(β<sup>(m)T</sup> x<sub>new</sub> + η<sub>h<sub>new</sub></sub><sup>(m)</sup>) × ν<sub>new</sub><sup>(m)-1/k<sub>new</sub></sup>
for each t in t_grid do:
S<sub>matrix</sub>[m, t_idx] = exp( -(t / λ<sub>new</sub><sup>(m)</sup>)<sup>k<sub>new</sub></sup> )
end for
end for
// 聚合: 中位数和 95% 可信区间
S<sub>median</sub>(t) = median(S<sub>matrix</sub>[:, t_idx]) for each t_idx
S<sub>lower</sub>(t) = quantile(S<sub>matrix</sub>[:, t_idx], 0.025)
S<sub>upper</sub>(t) = quantile(S<sub>matrix</sub>[:, t_idx], 0.975)
return {S<sub>median</sub>, S<sub>lower</sub>, S<sub>upper</sub>, S<sub>matrix</sub>}
end procedure
// ============================================================
// 第八步: 模型更新 (Bayesian Updating)
// ============================================================
procedure UpdateModel(OldSamples, D<sub>new</sub>):
// 旧后验作为新先验
for each hyperparameter η in {μ<sub>k</sub>, σ<sub>k</sub>, μ<sub>λ</sub>, σ<sub>λ</sub>, ...} do:
// 用旧后验的均值和标准差构造新先验
μ<sub>η</sub><sup>prior</sup> = mean(OldSamples.η)
σ<sub>η</sub><sup>prior</sup> = std(OldSamples.η) × inflation_factor // 适度放宽
η ~ Normal(μ<sub>η</sub><sup>prior</sup>, σ<sub>η</sub><sup>prior</sup>)
end for
// 用 D<sub>new</sub> 重新运行 RunMCMC
NewSamples = RunMCMC(D<sub>old</sub> ∪ D<sub>new</sub>, UpdatedModelGraph, MCMCSpec)
return NewSamples
end procedure
6.2 伪代码的数据流图解
如果画成图,上面八步的流水线是这样的:
输出层
计算层
模型层
输入层
临床数据 D
t, δ, x, h, e
先验规格
PriorSpec
MCMC配置
MCMCSpec
SpecifyModel
超先验设定
GenerateIndividualParameters
层次参数生成
ComputeLogLikelihood
截断-aware 似然
ComputeLogPrior
层次先验
NUTSSampler
梯度驱动采样
后验样本
θ(1..M)
预测生存曲线
S(t) ± 95% CrI
收敛诊断
R̂, trace plot
现在我们已经了解了从数据输入到后验预测的完整算法流水线,接下来看看这在训练和实际临床使用中到底意味着什么。
7. 闭环:训练、部署与临床意义
7.1 训练阶段的关键抉择
协变量选择 :在 DR 视力丧失预测中,哪些变量该进模型?根据 UCSF 和 ZSFG 医院的大规模 EHR 研究,NPDR 严重程度、年龄、保险类型、糖尿病神经病变、卒中次数、平均 HbA1c、住院次数都是关键预测因子。但要注意,Dynamic 模型(纳入变量随时间的变化)并未一致性地提升预测性能,可能是因为 6 个月的随访窗口太短,不足以让时变变量显露出预测力。这提示我们:不是变量越多越好,而是时间尺度要匹配疾病的自然史。
截断处理策略:真实世界的 DR 数据充满各种截断。右截断最常见(患者还在随访中没失明),区间截断次之(在两次筛查之间发生了事件,只知道发生在某个区间内),左截断较少见。我们的伪代码在 ComputeLogLikelihood 里显式处理了这三种情况。忽略区间截断会导致事件时间被系统性低估------就像把"在 2--3 年之间失明"粗暴记成"3 年失明",会人为拉长生存时间。
先验敏感度分析 :一个好的贝叶斯建模者不会只跑一组先验。他会问:如果我把 σk 的先验从 Half-Cauchy(0, 2.5) 换成 Half-Normal(0, 1),后验变吗?如果答案变了太多,说明数据信息量不足,先验在主导结论。这在样本量小的亚组(比如青少年 1 型糖尿病患者)中尤其重要。
7.2 部署阶段的临床翻译
模型训练完了,怎么给医生看?一张满是 MCMC 迹线的图对临床毫无意义。我们需要的是决策支持界面:
临床决策支持界面
输入:
患者ID + 眼底图像 + HbA1c
后台推断:
PyMC/Stan
2秒采样
输出:
5年生存概率 = 78%
95% CrI: [65%, 87%]
建议筛查间隔: 18个月
DeepDR Plus 系统的真实世界验证给了我们一个标杆:通过个性化筛查间隔,平均随访间隔可以从固定的 12 个月延长到 31.97 个月,同时将漏检率压到 0.18%。这就是贝叶斯生存模型的力量------它不仅告诉你"有风险",还告诉你"什么时候该回来复查"。
7.3 不确定性即特征,不是缺陷
在传统的深度学习分类模型里,一个"95% 概率进展"的输出可能让医生过度紧张。但贝叶斯生存模型给出的 95% 可信区间如果很宽(比如 5 年失明概率的 CrI 是 10%--60%),这本身就是一个临床信号:"这位患者的轨迹高度不确定,建议缩短随访、加强代谢控制。"不确定性没有被掩盖,而是被显式地翻译为临床行动。
如果画成图,窄区间和宽区间对应不同的临床路径:
宽可信区间
预测不确定
5年失明概率: 35%
CrI: [10%, 65%]
行动:
6个月复查 + 强化降糖
窄可信区间
预测精确
5年失明概率: 15%
CrI: [12%, 18%]
行动:
常规年度筛查
8. 延伸阅读与资源
如果你想把这趟旅程从"听懂"推进到"动手",以下资源是按难度递进排列的:
入门(建立直觉)
- Downey, A. B. Think Bayes (2nd ed.), Chapter 14: Survival Analysis. citeweb_search:2#8 这是一本免费开源的贝叶斯入门书,第 14 章用网格法手把手教你威布尔分布的贝叶斯更新,非常适合 toy example 起步。
进阶(医学应用)
- Montesano et al. "Hierarchical Censored Bayesian Analysis of Visual Field Progression." Translational Vision Science & Technology, 2021. citeweb_search:2#0 虽然针对青光眼,但层次结构、截断处理和异方差建模对 DR 完全适用。
- Rodrigues et al. "Developing and Validating Models to Predict Progression to Proliferative Diabetic Retinopathy." Ophthalmology Science, 2023. citeweb_search:1#1 展示了 Cox 模型和 Random Survival Forest 在 DR 进展预测中的基准表现,可作为威布尔贝叶斯模型的对比基线。
高阶(概率编程实现)
- Martin et al. "Bayesian parametric models for survival prediction in medical applications." BMC Medical Research Methodology, 2023. citeweb_search:2#1 提供了基于 PyMC 的完整实现,包括 NUTS 采样、后验预测检查、模型更新流程。
- Sarhan et al. "A Dependent Bivariate Burr XII Inverse Weibull Model: Application to Diabetic Retinopathy." Mathematics, 2025. citeweb_search:1#2 如果你想处理双眼依赖的竞争风险数据,这是目前文献中针对 DR 最专门的生存模型之一。
工具链
- PyMC (Python): 工业级概率编程,默认 NUTS,文档对生存分析有专门教程。
- Stan (R/Python): 语法更数学化,适合习惯写声明式模型的研究者。
- WinBUGS/OpenBUGS: 经典工具,大量早期医学贝叶斯文献使用,适合理解 Gibbs 采样的历史脉络。
结语:从预测到对话
回到文章开头那位 58 岁患者的问题:"我大概还有多久会看不见?"
现在我们已经了解了整套技术栈,可以重新组织语言来回答:
"根据您的眼底图像、糖化血红蛋白 8.2%、12 年病程,以及我们医院过去 2000 多例患者的数据,模型估计您 5 年内视力严重丧失的概率约为 22% 。但这个数字有个范围------考虑到个体差异和医学固有的不确定性,95% 的情况下这个概率落在 12% 到 34% 之间。因为区间不算太宽,我们建议保持目前的降糖方案,每 12--15 个月复查一次眼底。如果糖化能降到 7% 以下,这个数字还会往下走。"
这就是贝叶斯威布尔生存模型在 DR 视力丧失预测中的终极意义:它不是取代医生的判断,而是把"时间"和"不确定性"这两个原本模糊的临床直觉,翻译成可计算、可更新、可解释的概率语言。在这场从数据到决策的旅程中,威布尔分布给了我们描述时间弹性的模具,贝叶斯估计给了我们量化不确定性的语法,而层次结构则让个体患者的声音在人群合唱中既不被淹没,也不被错配。
希望这趟层层剥开的旅程,让你下次面对生存分析代码时,看到的不再是一堆希腊字母和采样器参数,而是一座清晰可见的三层小楼,里面住着患者、医生和概率。
本文图表均使用 Mermaid 语法绘制,可直接在 GitHub、Notion、Obsidian 等支持 Mermaid 的平台上渲染。