☰
标注受限下的医学图像高效学习:自监督预训练、课程学习与半监督伪标签
2026/11/3 15:31:55 网站建设 项目流程

简介:《MICCAI 2024》会议论文集第 XI 分册(LNCS 15011)完整 PDF,收录 2024 年 10 月在马拉喀什举办的第 27 届医学图像计算与计算机辅助介入大会论文,面向医学影像研究者、计算机视觉工程师及跨学科临床团队。内容聚焦自监督机制下的医疗影像分析精度提升,涵盖渐进式自适应步调机制、脑图谱与大脑成像效果改善、微型注释样本训练模型、病变区域检测与分类定位等方向,并给出基于神经网络框架的实验验证与阿尔茨海默病神经成像数据库支持下的可靠性结论,可用于选题调研、方法复现与算法对比。压缩包共 1 个 PDF 文件,约 109.88MB,为 Springer 原版排版,含编者前言、目录与完整论文正文,便于按主题检索。目前已有 449 人学习下载。

1. 医学图像计算与计算机辅助介入:为什么标注翻倍,不如换掉预训练目标

在医学图像计算与计算机辅助介入(MICCAI)这个圈子里,有一个反直觉的结论:把一个数据集的像素级标注量翻一倍,模型性能的提升往往比不上把预训练目标从“监督分类”换成“自监督重建加对比”。原因并不复杂——医学图像的标注从来都不是真值,它只是某位观察者在特定协议下做出的判断。同一张脑部磁共振,两位医生勾出的病灶边界 Dice 可能只有 0.7 到 0.8,而监督学习的拟合上限已经被这个一致性水平固定住了。相比之下,像素、体素、切片之间的解剖结构约束是免费的、近乎无限的监督信号,不需要任何额外的人工成本。

这也是那卷围绕高效学习策略的会议论文集值得细读的原因:该届投稿约 2800 篇、接收 857 篇、接收率约 30%,能被选进来的工作大多不是在比拼谁的数据更多,而是在标注、算力和样本量同时受限的前提下,把“模型能学到什么”这件事重新设计了一遍。自我监督学习、微型注释样本训练、脑图谱分析这类题目反复出现,正是因为临床场景里标注永远是稀缺资源。

2. 渐进式自适应步调:课程学习的难度曲线为什么不能写死

课程学习的基本直觉是“先学简单的,再学难的”。这个说法听起来无懈可击,但凡是在真实医学数据上跑过的人都知道,难度曲线一旦写死,训练就会开始变得难以预测。问题出在“难度”这个词本身:它不是样本的固有属性,而是样本与当前模型之间的一种关系。今天模型觉得这张片子难,是因为它还没学会识别低对比度边界;训了二十个 epoch 之后,这张片子可能已经是最容易的一批,可固定阈值的调度器依然把它挡在门外。

自适应步调机制要解决的就是这件事——把“何时让一个样本进入训练”从静态配置变成随模型状态变化的动态决策。这卷论文集里那些带“渐进式”字样的工作,核心思想基本都是给每个样本算一个随训练进度移动的进入权重,而不是一个二值的开关。

2.1 从固定阈值到样本级步调打分

固定阈值方案的流程通常是:先算一遍全量损失,按分位数切出“简单集”,前若干 epoch 只用简单集,之后逐步放开。它有三个不易察觉的缺陷。

第一个是样本永久丢弃。硬阈值意味着被挡住的样本梯度恒为零,如果一个样本在早期因为伪影被判定为“难”,后面即使模型已经能处理它,它也不会再回来,相当于白白浪费了数据。

第二个是捷径特征放大。医学图像里“简单”的样本往往不是解剖结构清晰的样本,而是带有明显捷径线索的样本——比如某个中心的扫描仪有特定的灰度分布、某个体位有固定的标记物。先训这些样本,模型会先学会认设备,而不是认病灶。

第三个是阈值不可迁移。在 A 数据集上标定好的分位数,换到 B 数据集上直接失效,因为损失的量纲和分布都变了。

样本级步调打分的做法是把难度定义成一个连续的、随进度退火的量。常见形式是:

import torch def pacing_weight(per_sample_loss, progress, tau_lo=0.15, tau_hi=0.85, gamma=8.0, eps=1e-6): """ per_sample_loss: [B],当前模型对每个样本的损失(越小代表当前越"容易") progress: 0~1 的训练进度 tau_lo/tau_hi: 阈值退火区间,从宽松逐步收紧 gamma: 软化系数,越大越接近硬阈值 返回: [B] 的损失权重,值域 (0, 1) """ # 用 batch 内的均值做归一化,抵消不同阶段损失量纲的漂移 norm_loss = per_sample_loss.detach() / (per_sample_loss.detach().mean() + eps) # 阈值随训练推进上移:早期几乎全放开,后期只保留"当前确实学得动"的样本 tau = tau_lo + (tau_hi - tau_lo) * progress # 软权重,保证被压低的样本仍有非零梯度,可被后续重新激活 w = torch.sigmoid(gamma * (tau - norm_loss)) return w

这里有几个参数值得单独说。tau_lo到tau_hi的退火区间决定了课程收紧的速度,实测中把tau_hi设得过高会让后期有效 batch 缩水严重,反而拖慢收敛;gamma控制软硬程度,取 5 到 10 之间的值能保证权重曲线既有区分度又不至于退化成阶跃函数。真正关键的一行是sigmoid而不是(norm_loss < tau):软权重让被压制的样本保留非零梯度,模型能力上来之后这些样本的权重会自然回升,这是避免“样本永久丢弃”的唯一手段。

2.2 把步调放进自监督预训练:掩码比例也是一种课程

自适应步调的思路并不只适用于有监督的样本筛选。在自监督预训练里,同样的机制可以作用在掩码比例上。掩码图像建模的经典设定是固定掩掉 75% 的 patch,但在医学三维数据上这个数字相当玄学:早期掩得太狠,模型只能靠邻域的低频强度去猜,学到的全是模糊的平均纹理;掩得太少,任务又退化成一个简单的插值。

渐进式的做法是让掩码比例随训练进度从低到高走,同时用重建损失的下降速度作为反馈来调节爬升斜率。如果重建损失下降停滞,就暂停提高掩码比例,退回去把当前难度训透,再继续往上推。这在实现上就是一个带反馈的调度器:

class AdaptiveMaskScheduler: def __init__(self, ratio_lo=0.30, ratio_hi=0.80, patience=3): self.ratio = ratio_lo self.ratio_lo, self.ratio_hi = ratio_lo, ratio_hi self.patience = patience self.best = float("inf") self.bad_epochs = 0 def step(self, recon_loss): # 重建损失没有改善就暂停提高难度,先让模型在当前掩码比例上收敛 if recon_loss < self.best - 1e-4: self.best = recon_loss self.bad_epochs = 0 self.ratio = min(self.ratio_hi, self.ratio + 0.05) else: self.bad_epochs += 1 if self.bad_epochs >= self.patience: self.ratio = max(self.ratio_lo, self.ratio - 0.02) self.bad_epochs = 0 return self.ratio

这段代码的机制点在“双向调节”:只升不降的调度器在噪声大的医学数据上很容易把难度推到一个模型永远追不上的位置,然后就一直卡在那里,而回退一步往往能把训练重新带回可控区间。patience设 3 是经验值,太小会因为损失的正常波动频繁回退,太大又失去了反馈的意义。

2.3 梯度信噪比:为什么自适应比“由易到难”更稳

从优化角度看,课程学习有效的原因不是“简单样本更好学”,而是简单样本的梯度方向更一致。在训练早期,模型对难样本的梯度方向与整体下降方向的夹角很大,这些梯度混进来之后,等效于给参数更新注入了一大份噪声,有效学习率被拉低。渐进式步调做的事情,本质是在控制每个阶段参与训练的梯度信噪比。

这也解释了为什么硬阈值方案在中期会突然失效:当简单样本被训到接近饱和,它们的梯度幅值趋于零,而难样本又还没放开,整个 batch 的有效梯度几乎消失,损失曲线就会出现那种一段平、一段陡的锯齿。软权重方案天然避免了这一点——权重是连续变化的,饱和样本的权重会被其他样本的损失均值自然稀释,不会出现整批梯度同时归零的情况。

提示:调节步调权重时,比起盯总损失,更该盯的是每个 epoch 里“权重接近 0 的样本占比”。这个比例长期高于 30%,说明退火曲线走得太快,模型还没跟上。

3. 微型注释样本训练模型:几十例标注怎么撑起一个分割器

在一个中心只拿得到几十例带像素级标注的数据,这在临床上太常见了。这种情况下直接训一个 U-Net 类的分割网络,结果基本是模型把训练集背下来,换一批扫描参数就崩。微型注释样本训练的核心思路,是把“标注”从唯一的监督来源降级成众多约束之一,剩下的约束来自未标注数据的一致性、来自解剖先验、来自教师模型的稳定输出。

3.1 教师-学生与一致性正则:稳定性比精度更值钱

半监督分割里最常被复现的框架是教师-学生结构:学生吃强增强的输入,教师吃弱增强的输入并产出伪标签,学生在伪标签和监督标注上同时算损失。这个框架能 work 的关键不在伪标签有多准,而在于伪标签有多稳。教师参数由学生参数的指数滑动平均得到,相当于对训练轨迹做了一次低通滤波,滤掉了 Batch 级别的抖动。

import torch @torch.no_grad() def ema_update(teacher, student, momentum=0.999): # 教师的参数是学生参数的历史滑动平均,相当于一个低通滤波器 for tp, sp in zip(teacher.parameters(), student.parameters()): tp.mul_(momentum).add_(sp.detach(), alpha=1.0 - momentum) @torch.no_grad() def select_pseudo(teacher_logits, fg_prior=0.03, topk_ratio=0.5): """ teacher_logits: [B, C, H, W] fg_prior: 前景体素的先验占比,用来对抗医学数据里的极端类别不平衡 topk_ratio: 只保留置信度最高的这一部分前景体素参与损失 """ prob = torch.softmax(teacher_logits, dim=1) conf, pred = prob.max(dim=1) # 关键:不用全局单一阈值。前景体素极少,统一阈值会把前景全滤掉 fg_mask = pred > 0 if fg_mask.any(): thr_fg = torch.quantile(conf[fg_mask], 1.0 - topk_ratio) else: thr_fg = 1.0 thr_bg = 0.95 # 背景容易判,阈值可以卡得更死 keep = torch.where(fg_mask, conf >= thr_fg, conf >= thr_bg) return pred, keep

select_pseudo里那个按前景、背景分别取阈值的分支,是这类方法从论文走向可用的分水岭。医学分割的前景占比经常是 1% 到 5%,如果统一用一个 0.9 的置信度阈值,模型早期几乎不可能在前景上产出高置信预测,于是所有伪标签都是背景,学生很快学会“全预测背景”这个退化解,损失还降得挺好看。分开取阈值,本质上是在伪标签层面做了一次类别平衡。

momentum这个参数也值得留意:0.999 是常见起点,但如果每个 epoch 的迭代步数很少(小数据集常见),0.999 意味着教师几乎不更新,学生会追着一个几乎静止的目标跑,学不到东西。这种情况下把 momentum 降到 0.99 更合理,判断依据是“教师更新一次的时间常数大约等于几个 epoch”。

3.2 脑图谱分析:把解剖先验当成软标签用

脑图谱分析这条路提供的是另一种约束。把标准脑图谱配准到个体空间之后,得到的不只是一张分区标签图,还有每个体素属于某个解剖结构的先验概率。这个东西的价值在于它是连续的、有空间结构的,可以直接当成软标签或者空间注意力先验塞进损失函数。

常见的做法是在损失里加一项先验加权:预测的分割边界如果越过了图谱给出的高概率区域边界,就施加额外惩罚。这在只有几十例标注的情况下特别有用,因为它约束的是模型不要在小样本上“乱扩张”——分割网络在标注极少时的典型失败模式不是漏分割,而是把病灶周边的高亮组织一起吞进来,而图谱先验恰好能挡住这类越界。

实际实现时有个细节容易被忽略:配准误差本身就有好几个体素,先验图要做一次形态学膨胀再当约束,否则约束会卡得模型连正确边界都过不去,表现为整体欠分割。膨胀半径按目标体素间距换算,别写死成固定的体素数。

3.3 未标注数据的用法:一致性比伪标签更耐噪

在标注极少时,未标注数据的一致性约束往往比伪标签更可靠。伪标签的错误是会被学生原样学走的,而一致性正则约束的是“同一张图在两种扰动下输出应当接近”,它不指定输出应该是什么,只要求输出稳定。在解剖结构本身是确定的情况下,这个假设比伪标签的假设更弱、也更安全。

具体形式可以是对同一张图的两次不同增强做输出一致性,也可以是对同一病人的相邻切片做预测平滑性约束。后者在三维数据上尤其有效,因为相邻切片之间的解剖结构变化是连续的,模型如果在这上面给出跳变很大的预测,几乎必然是错的。

4. 分类与定位的耦合:为什么诊断模型会盯着扫描仪水印

临床上要的从来不是一句“有病/没病”。一个只输出二分类概率的模型,即使测试集 AUC 很漂亮,也很难被真正用起来,因为医生需要知道模型看的是哪里。而纯分类训练出来的模型,注意力落点经常让人哭笑不得——它盯的可能是图像角落的强度标记,也可能是不同设备留下的重建伪影差异。这类捷径特征在单中心数据上表现完美,多中心一测就崩,算是这个方向最常见的失败情况。

4.1 弱监督定位的失效模式

用分类标签做弱监督定位,主流手段是类激活图那一类方法:拿分类分数对最后一层特征图求梯度,梯度大的位置就认为是判别区域。它在自然图像上效果尚可,搬到医学图像上有两个典型失效。

一是只激活最具判别性的一小块。比如肺结节,模型可能只高亮结节里最亮的那几个体素,而临床关心的是完整边界。原因是分类损失只要求“有一个强证据就够了”,它没有任何动力去覆盖整个病灶。

二是激活区域漂移到病灶之外。当病灶很小而周围组织有强对比时,分类模型很容易抓住周围组织,因为那更容易区分正负样本。这个现象在多中心数据上尤其明显,不同中心的对比剂方案不一样,模型学到的可能就是这个方案差异。

缓解手段有两类。一类是在特征层面做文章,比如多实例学习里的 top-k pooling,用前 k 个高分实例的均值代替最大值,逼模型不要只依赖单个最强响应。另一类是加辅助任务,用一个分割辅助头强迫共享编码器保留空间信息,分类头再从保留了空间结构的特征上做判断。

4.2 多任务损失的梯度冲突

分类头和分割头共享编码器时,两个任务的梯度方向不一定一致。分类希望特征对类别有判别力、对空间位置不敏感,分割希望特征保留精细的空间细节。在编码器深层,这两个诉求会打架,表现为训练后期分类指标上去了、分割 Dice 掉下来,或者反过来。

常见的缓解方式是对冲突梯度做投影:如果两个梯度夹角大于 90 度,就把其中一个投影到另一个的法平面上,去掉互相抵消的分量。

def project_conflicting(grad_a, grad_b): """当两个任务梯度夹角为钝角时,把 grad_b 中与 grad_a 冲突的分量投影掉""" dot = torch.dot(grad_a, grad_b) if dot < 0: # 方向相反,存在冲突 norm_a = torch.dot(grad_a, grad_a) + 1e-8 grad_b = grad_b - (dot / norm_a) * grad_a return grad_a, grad_b

这段逻辑要作用在参数级的梯度上,通常按层做,把每层的梯度展平成一维再算内积。逐层投影比全局投影稳定,因为不同层的冲突程度差别很大——浅层往往一致,深层才是主战场。计算开销上,投影本身不贵,贵的是要为两个任务各算一次反向传播,所以实践中通常隔几个 step 才做一次投影,或者只在检测到指标此消彼长时才启用。

注意:多任务损失权重的固定配比几乎必然不是最优的。更实用的做法是监控两个任务在验证集上的相对提升速率,谁停滞就给谁加权,但加权要有上限,否则会把另一个任务彻底压死。

5. 避坑与排查:复现高效学习策略时最容易翻车的五件事

5.1 验证集指标虚高,上线就崩

现象:验证集 Dice 到 0.9,换一批数据直接掉到 0.6 以下。

原因:按切片而不是按病人划分数据集。同一个病人的相邻切片高度相似,切到训练集和验证集里,等于让模型在验证时见到了训练样本的近亲。医学数据的样本独立性单位是病人,不是切片,也不是检查。

解决:按病人 ID 分组划分,做分组交叉验证。划分完之后再检查一遍两个集合的病人 ID 有没有交集,这一步别嫌麻烦,我见过太多项目栽在这里。

5.2 训练震荡、损失突然飙升

现象:教师-学生框架跑到中期,损失周期性大幅波动。

原因:强弱增强用反了。教师在吃强增强的输入,输出本身就在抖,伪标签跟着抖,学生再去拟合一个抖动的目标,整个系统进入正反馈。

解决:教师永远只吃弱增强(或者原始输入),强增强只给学生。另外教师侧的前向要包在torch.no_grad()里,别让伪标签的梯度回传到教师网络上。

5.3 伪标签全变成背景

现象:半监督训练几个 epoch 后,学生输出几乎全是背景,但损失还在降。

原因:前景占比极低时,统一置信度阈值把前景全滤掉了,模型找到了“全预测背景”这个退化解。

解决:按前景、背景分别设阈值,或者用每个类别的预测占比做约束,强制伪标签里的前景比例不低于一个下限。同时监控每个 epoch 保留下来的前景体素数,这个数字掉到 0 就是明确的告警信号。

5.4 重采样顺序导致分布漂移

现象:训练正常,推理时图像看起来发灰或者过曝。

原因:归一化和重采样的顺序不一致。先做强度归一化再重采样,插值会在归一化后的空间里引入非物理的中间值;先重采样再归一化,不同 spacing 的样本归一化后的统计量对不上。

解决:固定顺序为“重采样 → 强度归一化 → 裁剪/填充”,并且把目标 spacing 作为一个显式配置项记录下来,训练和推理走同一套预处理代码路径。别在两个地方各写一份预处理。

5.5 断点续训后模型性能回退

现象:从 checkpoint 恢复训练,前几个 epoch 指标明显掉一截,之后才慢慢爬回来。

原因:只存了模型参数和优化器状态,没存课程/步调调度器的内部状态。步调的退火进度、掩码比例的当前值、教师的 EMA 参数全被重置到初始值,模型被迫从头走一遍课程。

解决:把调度器状态、EMA 教师参数、随机数种子状态一起打进 checkpoint。写一个统一的state_dict()聚合函数,别指望每个组件都记得自己存。

6. 设计取舍:标注预算、算力与泛化的三角怎么划线

把这几条技术路线放在一起看,会发现它们其实是在三个约束之间做不同的交换。自我监督预训练用算力换标注,半监督用调参复杂度和训练不稳定性换标注,微型注释样本方法用先验知识和架构约束换标注。没有哪条路是普遍更优的,选择取决于你手上最不缺什么。

方案标注需求算力开销主要风险适用判断
全监督高低标注一致性封顶,跨中心泛化差单中心、标注充足、任务固定
自监督预训练 + 微调低高(预训练阶段)代理任务与下游不匹配,重建学到的多是小纹理有多中心无标注数据、算力可租
教师-学生半监督中中伪标签退化、训练不稳定有大量未标注同分布数据
微型注释 + 图谱先验极低低先验失配、配准误差传播解剖结构稳定、图谱可得

验证方法上,我最推荐的是画标注预算曲线,而不是只报一个数字。取标注量的 10%、25%、50%、100% 各训一遍,看性能随标注量的增长斜率。如果斜率在 50% 之后就基本平了,说明瓶颈不在标注量,继续标是浪费;如果到 100% 还在陡升,说明模型容量或者训练策略还有空间,此时上自监督预训练的收益会比继续标注大得多。这个曲线每次跑至少三个随机种子,报均值和置信区间——医学数据集的规模决定了单次实验的方差经常大到你无法区分两个方法。

还有一个容易被忽略的取舍是评测的方差本身。微型注释场景下,验证集可能只有十几个病人,指标的标准误大到能盖住方法差异。这种情况下与其纠结模型结构,不如先把评测协议做扎实:病人级分组、多中心留一验证、固定随机种子集合。我吃过一次亏,两个方法比了两周,最后发现差异完全落在种子波动范围内,那两周基本白干。从那以后我每次开新项目,都强制先把评测协议和数据划分脚本写完再碰模型代码。

希望这些能帮你在自己的数据上少走几步弯路,也值得翻翻这卷论文集里那些具体的方法细节——真正决定成败的往往是伪标签阈值怎么取、EMA 动量设多少这类不起眼的地方。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询