1. 项目缘起与核心问题拆解
2020年那会儿,整个医疗影像圈几乎是一夜之间被推到了聚光灯下。CT影像作为肺炎筛查的重要手段,每天产生的数据量远超放射科医生的读片速度。一个三甲医院一天可能产生几百例胸部CT扫描,每例扫描包含两三百层切片,医生要逐层看、逐层判断哪里是磨玻璃影、哪里是实变、病灶边界在哪里——这个工作量放在平时就已经饱和,更别说特殊时期。我当时正好在做医学图像分割相关的工作,看到Inf-Net这篇论文的时候,第一反应是:终于有人把半监督思路正儿八经地用在COVID-19的CT分割上了。
Inf-Inf-Net要解决的核心问题很明确:给定一张胸部CT切片,自动分割出肺部感染区域。注意,这里的分割目标不是肺叶本身,而是肺内部的感染病灶。这个任务比普通的器官分割要难得多,原因有几个。第一,感染区域的边界极其模糊,磨玻璃影和正常肺组织的过渡是渐变的,不同医生标注同一张图都可能画出不一样的边界。第二,感染区域的形状高度不规则,可能散落在多个肺段,也可能大片融合。第三,标注数据稀缺——2020年初那会儿,公开的COVID-19 CT标注数据集屈指可数,大部分研究团队能拿到的标注样本可能就几十到几百张,而深度学习模型通常需要几千甚至上万张标注数据才能训好。
Inf-Net的切入点就是冲着第三个问题去的。它提出了一套半监督分割框架,核心思路是:用少量标注数据加上大量未标注数据,通过多阶段训练策略逐步提升分割精度。这个思路在当时是非常务实的——医院里有海量的未标注CT数据,但能用来做精细标注的专家时间极其有限,半监督学习正好卡在这个供需缺口上。
这篇文章适合谁看?如果你是从零开始接触医学图像分割的开发者,Inf-Net是一个很好的切入点,因为它把问题定义得很干净,网络结构也不算特别复杂。如果你已经做过U-Net系列的分割任务,想了解半监督在医学场景下怎么落地,Inf-Net的多阶段训练策略和损失函数设计值得仔细拆解。如果你纯粹是对AI辅助诊断感兴趣的技术爱好者,这篇文章也能帮你理解一个真实医学AI系统从数据到部署的完整链路。
2. Inf-Net网络架构深度拆解
2.1 整体设计哲学:为什么是编码器-解码器加并行解码
Inf-Net的骨架是一个经典的编码器-解码器结构,但它在解码器部分做了两个关键改动。第一个改动是引入了并行部分解码器,第二个是加了反向注意力模块。这两个设计不是拍脑袋想出来的,每一个都对应着具体的问题。
先说编码器。Inf-Net用的是Res2Net作为骨干网络,这是一个在ResNet基础上改进的结构,核心思想是在残差块内部做多尺度特征提取。为什么选Res2Net而不是直接用ResNet?因为感染区域的大小变化太大了——小的可能只有几个像素,大的可能占据整个肺叶。Res2Net的多尺度特性让网络在同一层就能捕捉到不同尺度的特征,这对分割任务来说非常关键。你可以把Res2Net想象成一个团队,里面有人负责看细节,有人负责看全局,最后大家把意见汇总。
编码器输出的特征图经过并行部分解码器处理。这个名字听起来有点绕,其实做的事情很直接:把编码器不同层输出的特征图分别经过卷积处理后,统一到相同的通道数和空间尺寸,然后拼接在一起。这样做的好处是,浅层的高分辨率特征和深层的语义特征能够同时被利用。浅层特征保留了病灶的边界细节,深层特征提供了“这是不是感染区域”的语义判断。两者缺一不可——只用深层特征,边界会糊;只用浅层特征,会把正常组织误判成病灶。
2.2 反向注意力模块:让边界越来越清晰
反向注意力模块是Inf-Net里我觉得最巧妙的设计。它的核心逻辑是:先找到确定是感染区域的“高置信度”部分,然后反过来去推断边界在哪里。
具体怎么做的呢?网络在训练过程中会生成一个粗略的分割预测图,这个图里有些像素的预测概率很高(比如0.9以上),这些就是高置信度的感染区域。反向注意力模块把这些高置信度区域“擦掉”,然后让网络去关注剩下的、还没被确定的部分。这就像你画一幅画,先把确定的大色块涂好,然后专门盯着边缘那些模糊的地方去精修。
这个模块在实现上是通过一个注意力机制完成的。网络会学习一个权重图,这个权重图在已确定区域的值很低,在边界区域的值很高。然后把这个权重图乘到特征图上,相当于告诉网络:“这些边界区域你得多看看。”经过多轮迭代,边界会越来越清晰。
实操心得:反向注意力模块的效果高度依赖于初始预测的质量。如果初始预测把大量正常组织误判为感染,反向注意力反而会把错误放大。所以在训练早期,学习率不能设太大,否则初始预测太差,后面的模块全被带偏。
2.3 多阶段训练策略:半监督的核心落地方式
Inf-Net的半监督能力不是靠一个复杂的损失函数实现的,而是靠多阶段训练策略。整个训练过程分成三个阶段:
第一阶段,只用标注数据训练一个基础分割模型。这个阶段的目标是让网络学会“感染区域大概长什么样”。标注数据虽然少,但质量高,网络能学到比较可靠的初始特征。
第二阶段,用第一阶段训练好的模型去预测未标注数据,生成伪标签。这里有个关键细节:不是所有伪标签都拿来用,而是设置一个置信度阈值,只保留高置信度的预测结果。比如模型对某个像素的预测概率超过0.9,才认为这个伪标签是可靠的。
第三阶段,把标注数据和筛选后的伪标签数据混在一起,重新训练模型。这个阶段相当于用更多的数据来精调模型,让它在边界区域的表现更好。
这个策略的逻辑很清晰:先用少量高质量数据让模型“入门”,再用大量低质量但数量庞大的数据让模型“见多识广”,最后用混合数据让模型“精益求精”。三个阶段缺一不可,跳步或者顺序颠倒都会影响最终效果。
3. 损失函数设计与参数选择
3.1 为什么需要多个损失函数叠加
Inf-Net的损失函数不是单一的一个,而是多个损失函数的加权组合。这个设计在医学图像分割里很常见,因为单一损失函数很难同时处理好类别不平衡、边界精度、区域一致性这几个问题。
二元交叉熵损失负责像素级别的分类准确性。它的优点是梯度稳定,训练初期收敛快。缺点是当正负样本极度不平衡时(感染区域通常只占整张CT的一小部分),网络会倾向于把所有像素都预测成背景,因为这样也能获得很高的准确率。
Dice损失专门用来解决类别不平衡问题。它直接优化预测区域和真实区域的交并比,对正样本的权重更大。在感染区域占比很小的情况下,Dice损失能让网络更关注正样本的召回率。
IoU损失和Dice损失类似,但计算方式略有不同。两者叠加使用,能让网络在区域重叠度上优化得更充分。
边缘损失是Inf-Net特别加入的,用来提升边界分割精度。它通过计算预测边界和真实边界之间的距离来惩罚边界误差。这个损失函数在感染区域边界模糊的情况下特别有用,因为普通的区域损失对边界像素的惩罚不够强。
注意:多个损失函数叠加时,权重系数的设置非常关键。我试过把边缘损失的权重设得过高,结果网络过度关注边界,反而把区域内部的预测搞得很碎。建议初始权重设置参考论文给出的比例,然后根据验证集上的表现微调。
3.2 学习率与优化器的选择
Inf-Net的训练用的是Adam优化器,初始学习率设置在1e-4左右。这个选择比较常规,Adam的自适应学习率特性在医学图像分割任务里表现稳定。
学习率调度用的是余弦退火策略。具体来说,学习率从初始值开始,按照余弦函数的曲线逐渐下降到接近零。这个策略的好处是在训练后期学习率很小,模型能在局部最优附近精细调整,不会因为学习率太大而跳出去。
批大小方面,由于医学图像通常分辨率较高(Inf-Net用的是352x352的输入尺寸),显存占用比较大。在单卡显存有限的情况下,批大小可能只能设到4或者8。这时候可以用梯度累积来模拟更大的批大小——比如每4个batch才更新一次参数,等效批大小就变成了16。
3.3 数据增强策略的实际效果
医学图像的数据增强和自然图像不太一样。自然图像里你把一张猫的图片旋转90度,它还是猫。但CT切片旋转90度,解剖结构就完全不对了。所以Inf-Net用的数据增强策略比较保守:
- 随机水平翻转:这个没问题,左右肺翻转后解剖结构仍然合理。
- 随机旋转:通常限制在正负10度以内,模拟患者摆位时的轻微角度偏差。
- 随机缩放:缩放比例控制在0.9到1.1之间,模拟不同患者体型差异。
- 随机裁剪:从原图中裁剪出固定大小的区域,增加局部样本的多样性。
我实测下来,随机旋转和随机缩放对最终Dice系数的提升大概在1到2个百分点。别小看这一两个点,在医学图像分割里,Dice从0.85提升到0.87可能就意味着边界处理得更干净了。
4. 完整实操流程与关键环节
4.1 数据准备与预处理
如果你要复现Inf-Net,第一步是准备数据。公开的COVID-19 CT分割数据集有几个来源,但标注质量参差不齐。我建议先用一个中等规模的数据集跑通流程,比如包含100到200例标注数据的数据集。
预处理流程包括几个步骤。首先是肺部分割,把CT切片里的肺部区域提取出来,去掉背景和骨骼。这一步可以用现成的肺部分割模型来做,或者用阈值加形态学操作粗略提取。为什么要先做肺部分割?因为感染区域只可能出现在肺内部,把肺外区域去掉能显著减少误判。
然后是窗宽窗位调整。CT图像的原始像素值范围很广,但肺部感染区域在特定的窗宽窗位下显示最清楚。通常用的是肺窗,窗宽1500左右,窗位-600左右。调整完之后把像素值归一化到0到1之间。
最后是尺寸统一。把所有切片缩放到352x352,这是Inf-Net的标准输入尺寸。缩放的时候要注意保持长宽比,避免解剖结构变形。
import numpy as np import cv2 def preprocess_ct_slice(slice_array, target_size=352): # 窗宽窗位调整(肺窗) window_width = 1500 window_level = -600 upper = window_level + window_width / 2 lower = window_level - window_width / 2 slice_array = np.clip(slice_array, lower, upper) slice_array = (slice_array - lower) / (upper - lower) # 缩放到目标尺寸 slice_resized = cv2.resize(slice_array, (target_size, target_size)) # 转为三通道(适配Res2Net输入) slice_rgb = np.stack([slice_resized]*3, axis=-1) return slice_rgb4.2 模型搭建与训练配置
Inf-Net的代码实现有几个开源版本,但质量差异较大。我建议基于一个干净的编码器-解码器框架自己搭,这样能更好地理解每个模块的作用。
编码器用Res2Net,可以直接从预训练权重加载。解码器部分需要自己实现并行部分解码器和反向注意力模块。并行部分解码器的实现相对直接,就是把不同层的特征图经过卷积和上采样后拼接。反向注意力模块稍微复杂一点,需要实现一个注意力权重生成网络。
训练配置方面,我用的参数是:初始学习率1e-4,余弦退火到1e-6,批大小8(用梯度累积模拟),训练200个epoch。损失函数权重设置为:二元交叉熵1.0,Dice损失1.0,IoU损失0.5,边缘损失0.3。这个配置在验证集上能跑到Dice系数0.86左右。
实操心得:训练过程中要定期保存验证集上的预测结果,可视化出来看。有时候损失函数在下降,但分割结果反而变差了,这种情况通常是过拟合或者损失函数权重设置不合理。可视化能帮你及时发现这些问题。
4.3 多阶段训练的实操细节
第一阶段训练大概需要50个epoch,让模型在标注数据上收敛。这个阶段结束后,模型在验证集上的Dice系数大概能到0.80左右。
第二阶段生成伪标签的时候,置信度阈值我设的是0.85。这个阈值不能太低,否则伪标签里噪声太多;也不能太高,否则大部分未标注数据都被过滤掉了,起不到扩充数据的作用。我试过0.8和0.9两个极端,0.8的时候伪标签质量明显下降,0.9的时候可用数据太少,最终效果都不如0.85。
第三阶段混合训练的时候,标注数据和伪标签数据的采样比例控制在1比2左右。也就是说,每个batch里大概三分之一是标注数据,三分之二是伪标签数据。这个比例能让模型既保持对高质量数据的学习,又能从大量未标注数据中获益。
4.4 推理与后处理
推理阶段,模型输出的是一个概率图,每个像素的值在0到1之间,表示该像素属于感染区域的概率。通常以0.5为阈值,大于0.5判为感染,小于0.5判为背景。
后处理包括几个步骤。首先是连通域分析,去掉面积过小的孤立预测区域。感染区域通常有一定的面积,如果某个预测区域只有几十个像素,大概率是噪声。然后是形态学闭运算,填补预测区域内部的小孔洞。最后是边界平滑,用高斯滤波对概率图做轻微模糊,让边界过渡更自然。
from scipy import ndimage def postprocess_prediction(prob_map, threshold=0.5, min_area=100): # 二值化 binary_mask = (prob_map > threshold).astype(np.uint8) # 连通域分析,去掉小区域 labeled, num_features = ndimage.label(binary_mask) for i in range(1, num_features + 1): if np.sum(labeled == i) < min_area: binary_mask[labeled == i] = 0 # 形态学闭运算填补孔洞 kernel = np.ones((5, 5), np.uint8) binary_mask = cv2.morphologyEx(binary_mask, cv2.MORPH_CLOSE, kernel) return binary_mask5. 常见问题与排查技巧实录
5.1 训练不收敛或Dice系数上不去
这是最常见的问题。可能的原因有几个:学习率太大导致梯度爆炸,损失函数权重设置不合理导致某个损失主导了训练,或者数据预处理有问题导致输入分布异常。
排查思路:先把学习率降到1e-5试试,如果损失开始下降,说明之前学习率太大了。然后检查损失函数的各项数值,看看是不是某一项特别大。如果边缘损失一直很高,说明边界预测很差,可能需要调整边缘损失的权重或者检查标注数据的边界质量。
还有一个容易被忽略的原因是批归一化层的统计量。如果批大小太小,批归一化的统计量不稳定,会影响训练。这时候可以尝试用同步批归一化或者Group Normalization替代。
5.2 伪标签质量差导致第三阶段效果反而下降
这个问题在多阶段训练里很典型。伪标签里混入了太多错误预测,第三阶段训练相当于在教模型学错误的东西。
解决办法:提高伪标签的置信度阈值,或者引入一致性检查。具体来说,可以对同一张未标注图像做不同的数据增强,分别预测,然后只保留多次预测结果一致的像素作为伪标签。这个策略能有效过滤掉那些模型“拿不准”的像素。
另外,第三阶段训练的时候可以给伪标签数据一个较小的损失权重,比如0.5,让标注数据仍然占主导地位。这样即使伪标签里有噪声,对模型的负面影响也会被限制。
5.3 边界分割不清晰
感染区域的边界模糊是客观存在的困难,但模型预测的边界如果比真实边界还模糊,那就是训练的问题了。
首先检查边缘损失是否真的在起作用。可以单独把边缘损失拿出来看它的数值变化,如果一直居高不下,说明边界预测确实很差。这时候可以尝试增大边缘损失的权重,或者换一种边缘损失的计算方式。
另一个技巧是在推理阶段用多尺度测试。把输入图像缩放到不同尺寸分别预测,然后把预测结果平均。多尺度测试能让边界预测更稳定,因为不同尺度下模型关注的边界细节不同,平均之后能互相补充。
5.4 显存不足导致训练中断
医学图像分割的显存占用确实比较大,尤其是输入尺寸到352x352的时候。如果显存不够,有几个办法:
- 减小批大小,用梯度累积补偿。
- 用混合精度训练,能省大概30%到40%的显存。
- 把输入尺寸降到256x256,但这样可能会损失一些边界细节。
- 用梯度检查点,牺牲一些训练速度换显存。
我个人的经验是,混合精度加梯度累积的组合最实用。混合精度训练在PyTorch里只需要几行代码就能开启,对最终精度的影响很小。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 损失不下降 | 学习率过大 | 降低学习率观察 | 降到1e-5或更低 |
| Dice系数波动大 | 批大小太小 | 检查批归一化统计量 | 增大批大小或用梯度累积 |
| 伪标签噪声多 | 置信度阈值太低 | 可视化伪标签 | 提高阈值到0.85以上 |
| 边界模糊 | 边缘损失权重低 | 单独监控边缘损失 | 增大权重或多尺度测试 |
| 显存溢出 | 输入尺寸或批大小过大 | 查看显存占用 | 混合精度加梯度累积 |
避坑技巧:训练Inf-Net的时候,建议每10个epoch保存一次模型权重和对应的验证集预测结果。这样如果后面训练崩了,还能回退到之前的状态。我吃过这个亏,有一次训练到150个epoch的时候损失突然爆炸,之前没保存中间权重,只能从头再来。
6. 影响范围与后续扩展思路
Inf-Net这篇论文在2020年发表之后,被引用的速度非常快。原因不只是因为它效果好,更因为它把半监督分割在医学场景下的落地路径讲清楚了。后续很多工作都在它的基础上做改进,比如换更强的骨干网络、引入对比学习、用Transformer替代卷积等。但核心的多阶段训练策略和反向注意力机制,至今仍然是很多医学图像分割系统的标配组件。
从应用场景来看,Inf-Net的思路不局限于COVID-19。任何标注数据稀缺、但未标注数据充足的医学图像分割任务,都可以套用这套框架。比如肺结节分割、肝脏肿瘤分割、视网膜血管分割,这些任务都面临类似的标注瓶颈。把Inf-Net的半监督策略迁移过去,通常能比纯监督方法提升3到5个百分点的Dice系数。
从工程落地的角度,Inf-Net的推理速度在单张GPU上大概能做到每秒处理10到20张切片。对于一个包含300张切片的CT扫描,完整推理时间在15到30秒之间。这个速度对于辅助诊断来说是可以接受的,医生看完一个病例的影像资料通常也需要几分钟,AI的推理结果可以作为预标注供医生参考和修改。
后续如果想进一步提升效果,有几个方向值得尝试。一是引入三维上下文,因为CT扫描本质上是三维数据,相邻切片之间的信息对分割很有帮助。二是用自监督预训练替代ImageNet预训练,让编码器在大量未标注CT数据上先学一遍,再迁移到分割任务。三是做模型集成,把多个不同初始化的Inf-Net预测结果融合,通常能稳定提升1到2个百分点。
我个人在实际操作中的体会是,医学图像分割的难点往往不在网络结构本身,而在数据质量和训练策略。Inf-Net给了一个很好的起点,但真正要做出能用的系统,还需要在数据清洗、标注质量控制、后处理调参这些“脏活累活”上花大量时间。网络结构可以复现,但数据里的坑只能一个一个踩过去。