ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

半监督学习在医学图像分类中的实战应用

半监督学习在医学图像分类中的实战应用

1. 项目概述:半监督图像分类实战解析

在计算机视觉领域,图像分类一直是基础但至关重要的任务。传统监督学习需要大量标注数据,但在医疗、工业质检等专业领域,获取标注样本的成本极高。这时半监督学习(Semi-Supervised Learning)展现出独特价值——它能同时利用少量标注数据和大量未标注数据提升模型性能。最近我在一个舌苔分类项目中实践了半监督方案,仅用30%的标注数据就达到了全监督90%的准确率。

这个实战案例涉及三个技术关键点:首先是如何设计有效的半监督训练流程,其次是标注数据与未标注数据的协同利用策略,最后是针对医学图像的特性优化方案。下面我将结合代码和实验数据,拆解整个实现过程。如果你正在处理标注成本高的分类任务,这种方案能帮你节省至少50%的标注预算。

2. 半监督学习框架设计

2.1 基础模型选型

经过对比实验,我们选择EfficientNet-B4作为基础架构。相比ResNet,它在计算效率和准确率之间取得了更好平衡。更重要的是,其复合缩放系数(compound scaling)机制能灵活适配不同规模的数据:

from efficientnet_pytorch import EfficientNet model = EfficientNet.from_pretrained('efficientnet-b4', num_classes=10)

对于半监督场景,模型需要额外输出未标注数据的预测置信度。我们在分类层后添加dropout和temperature scaling:

class SemiSupHead(nn.Module): def __init__(self, in_features, num_classes): super().__init__() self.dropout = nn.Dropout(0.5) self.temp = nn.Parameter(torch.ones(1)*1.5) # 可学习温度系数 self.fc = nn.Linear(in_features, num_classes) def forward(self, x): x = self.dropout(x) return self.fc(x) / self.temp

2.2 数据准备策略

我们使用公开的TongueSet舌苔数据集,包含8类舌苔图像。原始数据集中仅30%有标注,其余作为未标注数据。关键预处理步骤:

  1. 对标注数据:应用标准增强(旋转、翻转、颜色抖动)
  2. 对未标注数据:采用强增强组合(RandAugment + CutOut)
  3. 创建两个数据加载器:
    labeled_loader = DataLoader(labeled_dataset, batch_size=32, shuffle=True) unlabeled_loader = DataLoader(unlabeled_dataset, batch_size=96, shuffle=True)

注意:未标注数据的batch size通常设为标注数据的3倍,这是半监督学习的经验法则

3. 核心训练算法实现

3.1 一致性正则化(Consistency Regularization)

这是半监督的核心技术,强制模型对同一图像的不同增强版本给出相似预测。我们采用Mean Teacher框架:

teacher_model = deepcopy(student_model) # 教师模型初始化为学生模型副本 optimizer = torch.optim.AdamW(student_model.parameters(), lr=3e-4) for epoch in range(100): # 教师模型参数指数移动平均更新 with torch.no_grad(): for t_param, s_param in zip(teacher_model.parameters(), student_model.parameters()): t_param.data.mul_(0.99).add_(s_param.data, alpha=0.01) # 处理标注数据 labeled_loss = F.cross_entropy(student_model(labeled_images), labels) # 处理未标注数据 with torch.no_grad(): teacher_logits = teacher_model(unlabeled_images_weak) student_logits = student_model(unlabeled_images_strong) consistency_loss = F.mse_loss( teacher_logits.softmax(dim=1), student_logits.softmax(dim=1) ) # 组合损失 total_loss = labeled_loss + 10.0 * consistency_loss # 加权系数需调优

3.2 伪标签(Pseudo Labeling)优化

我们采用动态阈值策略生成伪标签:

  1. 计算每个类别的平均预测置信度作为类别阈值
  2. 只保留高于阈值的预测作为伪标签
  3. 每周期间隔重新计算阈值

实现代码如下:

def generate_pseudo_labels(model, unlabeled_loader): model.eval() all_probs = [] with torch.no_grad(): for images, _ in unlabeled_loader: logits = model(images) probs = logits.softmax(dim=1) all_probs.append(probs) all_probs = torch.cat(all_probs) class_thresholds = all_probs.mean(dim=0) # 每类平均置信度 # 筛选高置信度样本 mask = (all_probs > class_thresholds).any(dim=1) pseudo_labels = all_probs.argmax(dim=1) return pseudo_labels[mask], unlabeled_images[mask]

4. 医学图像特化处理

4.1 领域自适应增强

舌苔图像需要特殊的数据增强:

  • 颜色空间变换:在HSV空间调整色调(H),模拟不同光照条件
  • 局部对比度增强:使用CLAHE算法突出纹理特征
  • 区域遮挡:随机遮挡非舌体区域,增强模型对关键部位的关注
class MedicalAugment: def __call__(self, img): # HSV色调扰动 hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) hsv[...,0] = (hsv[...,0] + random.randint(-10,10)) % 180 img = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB) # CLAHE增强 lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) lab[...,0] = clahe.apply(lab[...,0]) img = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB) return img

4.2 类别不平衡处理

舌苔数据常呈现长尾分布,我们采用:

  1. 标注数据:类别加权采样
  2. 未标注数据:伪标签的类别平衡筛选
  3. 损失函数:引入Focal Loss
class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, inputs, targets): BCE_loss = F.cross_entropy(inputs, targets, reduction='none') pt = torch.exp(-BCE_loss) loss = self.alpha * (1-pt)**self.gamma * BCE_loss return loss.mean()

5. 实验效果与调优记录

5.1 性能对比

在保留的测试集上,不同方法的表现:

方法准确率所需标注数据量
全监督训练92.3%100%
基础半监督87.1%30%
本文方案90.8%30%
仅使用标注数据76.2%30%

5.2 关键调参经验

  1. 一致性损失权重:从1.0开始,每10个epoch增加2.0,最终稳定在10.0
  2. 教师模型EMA系数:0.99效果最好,0.995会导致更新过慢
  3. 温度系数初始值:1.5比常见的1.0更适合医学图像
  4. 强增强组合:RandAugment的magnitude设为12,CutOut面积比例20%

实测发现:未标注数据量达到标注数据的5倍时,收益趋于平缓

6. 典型问题排查指南

6.1 伪标签质量差

症状:验证集准确率波动大 解决方法:

  • 检查类别阈值是否合理:打印各个类别的平均置信度
  • 降低初始阶段的伪标签权重
  • 增加强增强的强度

6.2 模型崩溃(预测退化为单一类别)

症状:某个类别的预测占比突然超过90% 解决方法:

  • 在损失函数中加入类别平衡约束
  • 暂时停用伪标签,只用一致性正则
  • 检查数据增强是否破坏了图像语义

6.3 训练不稳定

症状:损失值剧烈震荡 解决方法:

  • 减小教师模型的EMA系数(如0.95→0.99)
  • 使用更小的学习率(3e-4→1e-4)
  • 梯度裁剪(max_norm=1.0)

7. 工程部署建议

7.1 推理优化

将Mean Teacher转换为单模型推理:

  1. 训练结束后,直接使用教师模型
  2. 应用知识蒸馏将教师模型知识迁移到更小的学生模型
  3. 使用TensorRT加速:
# 转换ONNX格式 torch.onnx.export(teacher_model, dummy_input, "model.onnx") # TensorRT优化 trt_engine = onnx2trt(onnx_model, fp16_mode=True)

7.2 持续学习策略

当获得新标注数据时:

  1. 用原有模型生成新数据的伪标签
  2. 混合新旧数据重新训练
  3. 逐步降低旧数据的采样权重

这种方案在我参与的工业质检项目中,将模型迭代周期从2周缩短到3天。关键是要维护一个动态更新的样本库,记录每个样本的预测置信度和历史准确率。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进