
简介这份资源是一篇发表于《河北省科学院学报》2020年第3期的中文综述文章系统梳理了基于深度学习的医疗影像识别技术适合医疗AI研究人员、深度学习初学者及智能医学诊断方向的学生阅读。全文从二维与高维医疗影像两个维度回顾技术发展历程重点讲解卷积神经网络、全卷积神经网络、深度对抗网络、RGCNN及三维卷积神经网络等典型模型并结合影像增强、检测、分割与识别四类应用场景给出案例和准确率数据。资源为PDF格式共1个文件压缩包大小约1.21MB内容精炼且便于检索目前已有213人学习。文中还分析了医疗数据标注困难、模型解释性不足、数据不平衡等挑战并对多模态融合、可解释性提升和隐私保护等未来方向做出展望可帮助读者快速建立该领域从模型原理到落地问题的整体认知。1. 这份综述在讲什么把「识别」拆开才有阅读价值有次一位做影像组学的同事发来一份 PDF标题写着《基于深度学习的医疗影像识别技术研究综述.pdf》。这类综述通常从 CNN 胜出讲起把分类、分割、检测模型依次扫一遍读完感觉理论很完整动手却不知道先跑通哪一步。我习惯把标题里的“识别”拆成三个递进的问题病灶有没有、病灶在哪里、病灶是哪一类。拆完之后综述的结构变成三个工具箱每个都能给出可操作的最小实验路径。这篇博文就按这个顺序展开从任务界定、模型选型、训练调参到评估验收与落地边界。适合有 Python 和 PyTorch 基础、打算复现影像识别实验的研究生或算法工程师。2. 医疗影像识别不是单一任务分类、分割与检测的选型逻辑医疗影像与自然影像最大的差异是病灶往往只占整幅图像的很小比例而且 CT 的密度值、MR 的相对信号、病理切片的超大分辨率各自有完全不同的数据分布。所以综述里如果只写一句「深度学习在医疗影像识别中表现优越」那等于什么都没说。动手之前先把任务类型定死再谈网络结构和损失函数。2.1 三类任务决定三类评测口径分类任务关心「有没有」或「到什么程度」输出是一个概率向量。最常见做法是拿 2D 切片输入 ResNet/EfficientNet 系列的卷积网络用交叉熵训练处理 CT 序列时则把相邻切片叠成 3D 体积后交给 3D-CNN。分割任务把每一个像素标成背景或结构输出和输入同尺寸的 maskU-Net 家族的编码器-解码器结构因此成为绝对主线。检测任务要在全景影像里同时回答「病灶在哪」和「病灶多大」RetinaNet、Faster R-CNN 的变体在肺结节和骨折检测里更常见。这三类任务不能互相替代。分类网络能给出「有结节」的结论但回答不了边界和形态分割网络在没有候选区域时也不知道该把注意力放在哪检测网络虽然找到 ROI却常常不提供精细边缘。因此我拿到一份医疗影像识别综述时第一件事不是看模型而是看实验里到底在哪个任务上做了评估。任务一变损失函数、数据划分方式和指标口径都要整套换掉。2.2 CNN 为什么仍是主干ViT 与 EfficientNetV2 只能补位不少综述会把 Vision Transformer 列成重要分支但医疗影像能拿到的标注样本通常只有几千到几万例远低于 ViT 需要的大规模预训练数据量。CNN 的局部连接和平移等变性在这种小数据场景下更可靠用 2D U-Net 直接训练 CT 切片经常能打过用 ImageNet 预训练的 ViT。ViT 更适合「大批量 X 光片 大规模公开数据」的场景比如胸部平片的多标签分类EfficientNetV2 则适合作为分割模型的编码器后端用复合缩放平衡分辨率、深度和宽度。选择模型时不要看谁的名字新而要看预训练权重能否覆盖目标模态X 光、CT、MRI 的图像特征差异很大用自然图像权重冷启动通常需要更长的微调。我一般固定两个候选网络起步一个是轻量 ResNet-18/34 做二分类可行性验证一个是带预训练编码器的 U-Net 做分割主实验。流程跑通之后再换高精度模型避免一开始就被显存和调参拖住。2.3 数据链路是绕不过去的第一个坑DICOM 如何变成可训练矩阵2.3.1 最小 DICOM 序列加载脚本按物理位置排序而不是按文件名from pathlib import Path import numpy as np import pydicom def load_dicom_series(dicom_dir: Path) - np.ndarray: files [ f for f in Path(dicom_dir).iterdir() if f.suffix.lower() in {.dcm, .ima, } and f.is_file() ] slices [] for f in files: dcm pydicom.dcmread(str(f)) if not hasattr(dcm, SliceLocation): continue # CT 原始像素值需要按斜率与截距转成 HU亨氏单位 slope float(getattr(dcm, RescaleSlope, 1.0)) intercept float(getattr(dcm, RescaleIntercept, 0.0)) arr dcm.pixel_array.astype(np.float32) * slope intercept slices.append((float(dcm.SliceLocation), arr)) slices.sort(keylambda item: item[0]) if not slices: raise ValueError(f{dicom_dir} 下没有可读 DICOM 序列) return np.stack([item[1] for item in slices])这段代码先收集目录下所有候选文件逐个读取后跳过缺少SliceLocation的定位像RescaleSlope和RescaleIntercept是把 CT 存储值映射到真实密度单位的两个 tag缺失时用 1 和 0 兜底可以兼容超声等无标定模态。最后按物理位置排序并堆叠成(D, H, W)的 3D 数组这个数组才是后续裁剪 patch、做增强和输入模型的最小单元。需要注意一个目录里可能混有多个扫描序列仅按文件名或SliceLocation排序会串层。正确做法是先按SeriesInstanceUID分组再对组内按SliceLocation排序。提示如果同一病例有平扫和增强两套序列务必先分开加载否则训练时会混入两种完全不连续的密度分布。3. 深度学习训练与调参把医疗影像识别跑成可复现实验医疗影像实验最值的投入不在网络结构而在超参和损失函数。常见错误是只记最终准确率不记归一化窗口、patch 尺寸和早停策略结果换一台机器或换一个数据集分数立刻漂移。下面按超参模板、分割损失、训练循环三步展开。3.1 超参模板先有一张能复现的表参数推荐起点调整信号输入 patch2D 用 256×2563D 用 128×128×32显存不足先降 batch再考虑降 patch体素归一化HU 裁剪到 [-1000, 400] 后缩放至 [-1,1]边缘不清晰时适当放宽窗宽batch size2D 8163D 24过拟合时减小震荡时加大优化器AdamW(lr1e-4) 或 SGD(lr0.01)曲线长时间不降时降 lr 十倍warmup510 个 epoch前段 loss 爆炸时延长weight decay1e-4训练 Dice 与验证 Dice 落差大时调大混合精度开启 AMP出现 NaN 时关闭再定位问题早停 patience20 个 epoch验证曲线仍上升时改为 30这张表对分类和分割都适用唯一要替换的是损失函数。分类任务可以继续用交叉熵分割任务必须在交叉熵之外引入区域层面的约束否则模型会把大量背景像素学得很好病灶区域反而被平滑掉。3.2 Dice Loss 为什么是分割默认损失代码级拆解import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): # pred 是网络未过 sigmoid 的 logits输出 shape (N, 1, H, W) pred torch.sigmoid(pred) N pred.size(0) pred_flat pred.view(N, -1) target_flat target.view(N, -1) intersect (pred_flat * target_flat).sum(dim1) union pred_flat.sum(dim1) target_flat.sum(dim1) per_sample_score (2 * intersect smooth) / (union smooth) return 1 - per_sample_score.mean()pred在进入损失函数前不套sigmoid让数值计算交给更稳定的实现smooth防止背景完全为 0 时除零。按样本计算 Dice 再取平均比把所有 batch 元素拼起来算一个整体 Dice 更稳定否则大尺寸图像会主导梯度方向。当病灶只占图像 1% 时交叉熵的梯度几乎全部来自背景Dice Loss 则直接优化前景区域的匹配程度所以分割任务默认从它起步。实际训练里我更常用0.5 * dice_loss 0.5 * BCEWithLogits让像素级语义信息不被完全丢弃。3.3 训练循环早停与最佳模型快照怎么配合best_val -1.0 patience 20 wait 0 for epoch in range(max_epochs): model.train() for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss dice_loss(out, y) 0.5 * F.binary_cross_entropy_with_logits(out, y) loss.backward() # 3D U-Net 参数多梯度裁剪能防偶发 NaN torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() val_score evaluate(model, val_loader, metricdice) if val_score best_val: best_val val_score torch.save(model.state_dict(), best.pt) wait 0 else: wait 1 if wait patience: breakclip_grad_norm_把梯度范数限制在 1.0避免 CT 图像中极端像素值引发梯度爆炸。验证指标用 Dice 而不是组合 loss因为早停要盯最终目标不是盯中间数值。另一个常见问题是保存最后一个 epoch 的权重而不是best.pt在早停结束后验证集 Dice 通常已回落一到三个点。4. 复现综述实验的坑评估口径比模型结构更影响结论综述里的对比表看着差距很大但如果不问清数据划分维度这些数字完全不能直接采信。医疗影像数据天然具有「同一患者多张图像」的结构评估时最容易被整体准确率误导。4.1 patient-level split同一患者的切片不能同时出现在训练和测试一个胸部 CT 病例往往有上百张切片相邻层之间高度相似。如果按切片随机划分训练集和测试集同一患者的不同层会同时出现在两边相当于让模型见过测试目标的近亲分割指标虚高是必然的。这也是很多综述里分数很高、换到外部数据立刻掉下来的主要原因。import random def split_records_by_patient(records, val_fraction0.15, seed0): # records 结构: [(patient_id, file_path, label), ...] patients sorted({r[0] for r in records}) rng random.Random(seed) rng.shuffle(patients) cut int(len(patients) * val_fraction) val_ids set(patients[:cut]) train, val [], [] for r in records: if r[0] in val_ids: val.append(r) else: train.append(r) return train, valval_fraction按患者数量计算而不是按切片数量计算。病例有 100 张切片和 20 张切片两种规模时按病例划分能保证不同长度的样本不会带来分组偏差。固定seed后每次划分结果一致报告里可以注明这一行读者才能复现你的实验。4.2 指标组合Dice、Sensitivity 与 Hausdorff 距离分别回答什么问题指标作用容易被误读的场景Dice / IoU衡量分割区域重叠程度病灶体积大时明显偏移仍可能拿到高分Sensitivity / Recall表示漏检比例调高它可能带来大量假阳性需要配合 Precision 看95% Hausdorff Distance捕捉分割边缘的最大偏差对孤立噪声点敏感95% 分位数更稳AUC分类阈值无关的排序能力类别极不均衡时不能替代校准曲线医疗场景最怕漏掉病灶于是很多人只盯 Sensitivity结果模型变得「宁可多画也不漏」Dice 立刻下滑。一个可接受的分割实验通常同时报告 Dice 和 95% HausdorffDice 反映整体重叠Hausdorff 反映最大边界误差两个指标组合起来才能描述「这张 mask 到底贴不贴边」。分类任务不要只报 Accuracy病灶占比 1% 的数据集里全预测阴性也能有 99% 的准确率。4.3 多随机种子报告是底线复现综述实验时固定 3 到 5 个随机种子训练全套模型报告均值和标准差不要只挑最好的一次展示。不同算法之间的分数差距如果小于标准差基本可以判定两者没有显著差异。我还会把训练集、验证集、测试集的 patient ID 列表存成 JSON嵌进实验目录这样后续任何一次代码改动都能追溯到是哪份数据划分产生了当前结果。5. 进阶应用用 Grad-CAM 验收模型再把模型装回真实影像流程5.1 Grad-CAM 可以当「验尸工具」用训练完分类模型后第一件事不是看 AUC而是看热图是否落在病灶位置。如果模型依赖图像角落的水印、扫描仪伪影或者被固定的边缘区域那它学到的不是医学特征只是数据集偏置。import torch import torch.nn.functional as F def gradcam_2d(model, x, target_layer): act, grad {}, {} def forward_hook(module, inp, out): act[value] out def backward_hook(module, grad_in, grad_out): grad[value] grad_out[0] h1 target_layer.register_forward_hook(forward_hook) h2 target_layer.register_full_backward_hook(backward_hook) logits model(x) pred logits.argmax(dim1) model.zero_grad() logits[0, pred].backward() h1.remove() h2.remove() a act[value].detach() g grad[value].detach() weights g.mean(dim(2, 3), keepdimTrue) cam (weights * a).sum(dim1, keepdimTrue) cam F.relu(cam) cam F.interpolate(cam, sizex.shape[-2:], modebilinear, align_cornersFalse) cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam.squeeze().cpu().numpy()target_layer通常选最后一个卷积模块比如 ResNet 的layer4[-1]。weights是每个通道梯度对空间维度求平均得到的权重再和正向激活加权求和得到类别对应的空间位置热度。把热图叠回原图后如果高亮区域始终偏离肉眼可辨的病灶就要回去检查数据标注或者预处理流程。对分割模型来说这个思路同样有效只是可视化对象从分类激活变成预测 mask 与 ground truth 的差异区域。5.2 从综述到真实影像流程工程边界在哪里综述里网络输出直接展示成彩图真实环境里还有三件事要做。第一推理阶段必须使用与训练完全一致的归一化参数CT 的窗宽窗位一旦变化模型分数可能断崖式下跌。第二分割输出要做连通域后处理把小于设定体素阈值的孤立块过滤掉避免把噪声当成病灶。第三部署时用 ONNX 或 TensorRT 导出模型用 GPU 做推理同时保留 CPU 兜底路径。还有一点必须遵守病人影像不能随意拷出院内环境科研项目应先确认数据使用授权和伦理审批。整个项目推进顺序应当是先在一个二十例的小验证集上做 Grad-CAM 检查确认模型看过真正病灶再进入大规模训练与多中心验证。这一步的取舍往往比再调三个月学习率更接近临床可用。本文还有配套的精品资源点击获取