ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VisionTransformer做CT病灶定位的实战与避坑指南

VisionTransformer做CT病灶定位的实战与避坑指南 简介面向医疗影像与深度学习交叉领域的学习者和研究者这份PDF系统讲解VisionTransformer在CT扫描病灶定位中的完整落地流程。文档共34页从医疗影像诊断现状与挑战切入先回顾Transformer架构与多头自注意力、前馈网络等基础再展开ViT的图像分块、位置编码与分类令牌原理针对CT数据详细覆盖读取加载、归一化、裁剪填充、噪声去除、标注管理及数据增强等预处理环节。模型设计部分给出输入层适配、编码器改进、定位信息提取与损失函数选择并包含训练环境搭建、参数调优、正则化与评估分析最后通过实战案例对比传统诊断方法总结应用前景与局限。资源为单个PDF文件压缩包约2MB支持目录章节跳转与左侧大纲定位便于按需查阅。已有55人学习使用可用于课题研究、课程设计或技术预研参考。1. 为什么拿VisionTransformer做CT病灶定位从“标注贵”到“定位准”CT扫描是临床诊断流程里最绕不开的检查但做CT影像的人都有一个共同痛点病灶标注太贵。与其说难在显卡不如说难在拿到一批带坐标的CT数据。VisionTransformer简称ViT这两年在这类任务里频繁出现因为它把整张CT切片拆成patch序列做全局注意力能同时看到全图上下文来定位病灶。这篇文章不吹ViT能替代医生只把一套能从零跑起来的CT病灶定位方案拆开讲DICOM怎么处理、病灶坐标怎么对齐、ViT怎么训练成能报坐标的工具以及不跑一遍根本发现不了的坑。适合正在做影像组学、AI辅助诊断或毕设里要“在CT上找病灶”的同行。2. VisionTransformer在CT上的原理与选型先清楚它到底在看什么2.1 它把CT切片看成一组patch而不是一堆像素传统CNN用卷积核在图上滑动一个卷积层的感受野只有那么点大。CT上的病灶比如一个5毫米的肺结节周围全是正常组织甚至和血管截面长得一模一样。CNN想判断是不是真结节需要堆很多层才能让后面的层“看”到足够大的上下文。VisionTransformer的机制完全不同它把一张224×224的CT切片切成14×14个16×16的patch以ViT-B/16为例每个patch拉平后做线性投影再按顺序送入Transformer encoder。Transformer的多头注意力会在任意两个patch之间计算相关性所以第3层的输出就已经能同时看到左上角和右下角的patch。对CT这种背景单一、病灶和周围组织对比又明显的灰度图来说这种全局上下文天然合适。但这并不表示ViT是万能药。它的注意力矩阵大小和patch数量成平方关系patch数量越多显存涨得越快。做CT病灶定位时我一般不会直接上3D ViT参数太多标注本来就少。更稳的第一步是把每张CT切片当成2D图像先用2D ViT找出“哪几层切片、哪几个位置有病灶”再做坐标还原。这也是下面整套方案的前提。2.2 选哪个ViT变体patch尺寸、分辨率与预训练现在说到VisionTransformer选择很多。ViT-S、ViT-B、ViT-L是规模区别patch size有16、8输入分辨率有224、384。用于CT病灶定位我不是按“模型越大越好”的逻辑来选而是看病灶在图像里占多大。如果你处理的是肺结节这类小目标优先选patch size较小的变体比如ViT-S/16或ViT-S/8。patch越小每个patch对应的物理区域越小定位分辨率越高。常见做法是先用16×16的patch跑通pipeline确认loss下降到合理区间后再换8×8的patch做细定位。还有一个很多人忽略的点CT是单通道灰度图ViT默认输入是RGB三通道。加载预训练权重时要么把第一个线性投影层的权重在通道维度上平均要么把同一张灰度图复制成3个通道送进去。复制成3通道的做法我踩过坑效果倒也行但显存多占三分之一推理也慢把权重平均则省内存但一开始收敛会慢一些需要更多预热轮次。另一个现实问题是预训练权重能不能用。ImageNet预训练肯定覆盖不了CT的灰度分布但ViT前面几层学到的基本是边缘、纹理、形状这类通用结构迁移过来仍然有价值。实际训练CT病灶定位时不建议从零训练ViT除非你有上万级别的CT切片。常见配置加载ImageNet预训练权重冻结前6层其余层微调。预训练权重这步不是玄学它能帮你把训练轮数从50轮压到15轮以内。下面用一张表总结我常用的选型参考参数按实际任务调整场景推荐配置理由小病灶肺结节、微小结石ViT-S/16输入224patch16显存友好定位精度够用中等病灶肝占位、炎症区域ViT-B/16输入384特征容量大能处理多种形态只分slice有无病灶粗筛ViT-B/16输入224分类任务不需要太高空间分辨率高分辨率密集定位科研场景ViT-B/8输入384定位误差小显存建议12GB以上注意上表的显存是batch size为16的参考值实际要看你是用整张2D切片还是滑窗patch。CT的一个slice通常是512×512直接resize到224会损失细节。我一般先裁到320或384再resize到224比直接512拉伸到224保留更多有效信息。2.3 用一个最小脚本验证ViT确实能读CT切片先别急着训练。很多项目死在第一步预处理出来的CT切片进了模型就全黑或者全白。我先给一个最小验证脚本确认ViT能正常吃进CT切片并给出输出再谈训练。import torch import timm import numpy as np from PIL import Image # 读入一张预处理好的CT切片假设已经是224x224的灰度图 img Image.open(slice_102.png).convert(L) img img.resize((224, 224)) # 转成tensor: [1, 1, 224, 224]单通道灰度 x torch.from_numpy(np.asarray(img)).float().unsqueeze(0).unsqueeze(0) x (x - x.mean()) / (x.std() 1e-6) # 创建ViT-B/16in_chans1适配单通道 model timm.create_model( vit_base_patch16_224, pretrainedTrue, in_chans1, num_classes2 ) model.eval() with torch.no_grad(): logits model(x) # 输出 [1, 2] print(output shape:, logits.shape) print(pred class:, logits.argmax(dim1).item())逻辑说明timm.create_model返回ViT主干并换掉最后的分类头in_chans1会重新初始化patch embedding的输入投影层所以模型不会因为通道数对不上而报错。pretrainedTrue只加载主干部分可复用的权重分类头因为是新加的权重是随机初始化。最后的输出是[1, 2]两个logit分别对应“无病灶”和“有病灶”。参数说明in_chans1是应对CT灰度图的关键参数不写的话模型默认收3通道CT图必须复制三遍。num_classes2表示二分类如果你的标注有“背景/结节/其他病变”三类改成3。归一化用了最朴素的z-score原因是CT的HU值分布不稳定固定到[0,1]有时会把肺窗下的细节压缩掉。完整训练时归一化参数建议由整个数据集统计得出而不是靠一张切片现算。运行这个脚本如果能看到输出shape正常说明你手上的CT切片和模型已经成功对接。接下来要做数据预处理和训练这也是整篇文章真正的重头戏。3. CT数据预处理把DICOM切片和病灶坐标对齐喂给ViT3.1 从DICOM到NIfTI再到2D切片复用成熟pipelineCT设备导出的原始格式是DICOM一个病人的一次胸部CT扫描往往有200到600个独立的dcm文件代表不同层面的断层图像。直接拿DICOM当训练数据不是不行但DICOM里还夹着大量设备信息、患者信息、重建参数解析成本高。做CT病灶定位我一般先用dcm2niix把整套DICOM转成NIfTI再按轴位切成2D PNG或JPG。这一步很短但很多翻车事故就发生在文件命名和坐标系没对齐上。dcm2niix -z y -o ./output_dir -f case_%d ./input_dicom_dir参数说明-z y表示把NIfTI压成.nii.gz-f case_%d指定输出文件名前缀%d会被扫描序号替代最后的路径是存放DICOM的目录。转换后除了.nii.gz还会生成一个.json文件里面保存了spacing、slice thickness、series description等关键信息建议保留。为什么先转NIfTI而不是直接读DICOM因为后面做坐标换算时NIfTI的origin、spacing、direction通过SimpleITK几个API就能拿到而DICOM里这些信息分散在多个tag里处理起来容易乱。CT病灶定位的后续步骤里坐标换算准不准直接影响定位精度宁可多花这一步。然后是把NIfTI转成2D切片import SimpleITK as sitk import numpy as np from PIL import Image img sitk.ReadImage(case_1.nii.gz) data sitk.GetArrayFromImage(img) # shape: [num_slices, height, width] # 以HU值为例先裁剪到肺窗范围再做归一化 lung_min, lung_max -1000, 400 data np.clip(data, lung_min, lung_max) data (data - lung_min) / (lung_max - lung_min) data (data * 255).astype(np.uint8) for i in range(data.shape[0]): Image.fromarray(data[i, :, :]).save(fct_case1_slice_{i:04d}.png)逻辑说明sitk.GetArrayFromImage返回的数组轴顺序是(z, y, x)第一维是切片序号这一点要记住后面坐标换算会用到。窗宽窗位直接用了固定肺窗[-1000, 400]覆盖空气到骨头的常见范围。如果你处理的是肝或肾这类腹部器官把窗调整到[0, 300]更合适。参数说明切片序号和原始扫描的z坐标并不是一回事因为NIfTI的origin不一定是0。这个细节很容易被忽略我见过好几次把训练得到的切片编号直接当z坐标和标注做比较结果误差达到几十毫米。正确做法是坐标换算时始终回到世界坐标见下一节。3.2 病灶坐标与切片的对齐核心公式与代码医院或标注工具给出的病灶中心坐标通常是一个世界坐标(x, y, z)单位毫米物理含义是“在扫描的人体坐标系里的位置”。我们要把它换算成上面切片数组中的索引核心公式是slice_idx round((z_world - origin_z) / spacing_z)其中origin_z是第一个切片的z坐标spacing_z是层间距都来自NIfTI头信息。x和y方向同理可以换算到像素索引。很多定位算法最后的误差不是模型的错而是这一层换算错了一个符号或者忽略了方向矩阵。import SimpleITK as sitk import numpy as np img sitk.ReadImage(case_1.nii.gz) spacing img.GetSpacing() # [x_spacing, y_spacing, z_spacing] origin img.GetOrigin() # [x_origin, y_origin, z_origin] direction img.GetDirection() # 9个float构成旋转矩阵 # 假设标注给出的病灶中心是 (x_world, y_world, z_world)单位mm lesion (12.5, -30.2, -108.7) # 先做方向校正再换算到体素坐标 direction_mat np.asarray(direction).reshape(3, 3) voxel np.linalg.solve(direction_mat, np.asarray(lesion) - np.asarray(origin)) slice_idx int(round(voxel[2] / spacing[2])) print(病灶所在切片序号:, slice_idx)逻辑说明先用方向矩阵做逆变换把世界坐标转到体素空间坐标再除以spacing得到体素索引。这一步如果不做direction校正在扫描方向不是标准轴向的CT上定位会整体偏移好几个切片。大部分公开CT数据集是轴向扫描direction矩阵接近单位矩阵但你自己从医院拿的数据不一定。参数说明slice_idx对应3.1节数组里的data[slice_idx]这一张图。如果病灶正好在两个切片之间round会取较近一层。对直径小于层间距的病灶建议把相邻两层也都纳入候选不要锁单层。注意预处理函数里的窗宽窗位和归一化参数必须全程一致。训练时用的是裁剪后的灰度图推理时如果直接喂原始HU值模型输出会完全乱掉。这个坑低级但特别常见。3.3 训练集划分与增强防止数据泄漏比模型更重要数据划分是另一个容易被坑的点。一个病人一次CT扫描有几百张切片相邻切片之间高度相似。如果按“切片”为单位随机划分训练集和验证集同一个病人的前后两张切片很可能一张在训练集、一张在验证集模型实际上记住了病人的特异性纹理验证集指标虚高换一个新病人立刻掉点。正确做法是必须按病人或扫描序列为单位划分保证同一次扫描的所有切片都只出现在一个集合里。增强策略也跟自然图像不一样。CT切片里病灶方向不固定随机旋转15度不会造成医学上的荒谬结果所以随机旋转、随机翻转是安全的。但不建议用随机缩放因为CT的像素间距是物理单位缩放拉伸会改变病灶的真实尺寸影响最后的定位评估。import torchio as tio train_transforms tio.Compose([ tio.RandomAffine(degrees15, translate5, scaleFalse), tio.RandomFlip(axes(1,)), tio.RandomNoise(std0.02), ])参数说明translate单位是体素数旋转15度不会让解剖结构倒置。但如果做的是肋骨骨折这类带绝对方向的任务旋转角度要降到5度以内。RandomNoise(std0.02)模拟不同设备间的噪声差异能提升泛化能力但太强会把小结节淹没。到这里数据已经能进模型。下一章进入训练和定位输出的环节这才是VisionTransformer真正发挥“定位”作用的地方。4. 病灶定位实战用ViT做patch级分类并推算病灶坐标4.1 最小训练脚本ViT把切片判成病灶还是背景把定位问题变成分类问题给每张切片一个label1代表切片里包含病灶0代表不包含。这样一个标准的二分类任务就能跑起来。但这里有一个训练和推理不一致的问题训练时看到的是完整224×224切片推理时如果切成64×64的patch分别进入模型模型会对patch边界非常敏感。所以我更推荐训练时也做patch采样从原始512×512切片上随机裁64×64的patchresize到224×224作为输入。下面这个脚本就是最小训练循环。import torch import torch.nn as nn import numpy as np import timm from torch.utils.data import DataLoader, Dataset from PIL import Image class SliceDataset(Dataset): def __init__(self, img_paths, labels): self.img_paths img_paths self.labels labels def __len__(self): return len(self.img_paths) def __getitem__(self, idx): image Image.open(self.img_paths[idx]).convert(L) image image.resize((224, 224)) arr np.asarray(image, dtypenp.float32) arr (arr - arr.mean()) / (arr.std() 1e-6) return torch.from_numpy(arr).unsqueeze(0), self.labels[idx] model timm.create_model(vit_base_patch16_224, pretrainedTrue, in_chans1, num_classes2) model.cuda() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-2) criterion nn.CrossEntropyLoss() dataloader DataLoader(SliceDataset(train_paths, train_labels), batch_size32, shuffleTrue) for epoch in range(15): model.train() for imgs, labels in dataloader: imgs, labels imgs.cuda(), labels.cuda() logits model(imgs) loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() print(fepoch {epoch:02d} loss: {loss.item():.4f})逻辑说明这个脚本把一张224×224的切片或patch当作输入ViT的class token最终编码了整张图的信息输出一个“有无病灶”的二分类概率。虽然是整图二分类但我们通过在原始大图上滑窗推理把它变成真正的定位工具。参数说明lr1e-4是微调ViT时比较安全的起点直接上1e-3很容易出现loss震荡。weight_decay1e-2可以抑制过拟合但数据量极小时建议降到1e-4。batch size 32适合12GB显存显存不够就减半同时适当降低学习率。4.2 把分类输出变成病灶坐标滑窗、热力图到中心点用训练好的ViT做定位时常见做法是滑窗分类在512×512的切片上以64×64大小的窗口滑动步长32每个窗口单独送进ViT记录它是病灶的概率。把所有窗口的概率填回对应位置得到一个热力图再经过阈值化和连通域分析算出病灶中心。import cv2 import numpy as np import torch from scipy import ndimage def infer_slide(model, image_512, window64, stride32): model.eval() h, w image_512.shape heatmap np.zeros((h, w), dtypenp.float32) count np.zeros((h, w), dtypenp.float32) for y in range(0, h - window 1, stride): for x in range(0, w - window 1, stride): patch image_512[y:ywindow, x:xwindow] patch cv2.resize(patch, (224, 224)) patch (patch - patch.mean()) / (patch.std() 1e-6) input_tensor torch.from_numpy(patch).float().unsqueeze(0).unsqueeze(0).cuda() with torch.no_grad(): prob torch.softmax(model(input_tensor), dim1)[0, 1].item() heatmap[y:ywindow, x:xwindow] prob count[y:ywindow, x:xwindow] 1 return heatmap / np.maximum(count, 1.0) def loc_from_heatmap(heatmap, threshold0.5): mask heatmap threshold labels, n ndimage.label(mask) if n 0: return None, None sizes ndimage.sum(mask, labels, range(1, n1)) largest int(np.argmax(sizes)) 1 ys, xs np.where(labels largest) return (int(xs.mean()), int(ys.mean())), mask逻辑说明count矩阵的作用是对重叠区域取平均因为相邻窗口会有重叠如果不累计次数后面的窗口会把前面的概率覆盖掉。最后heatmap / count得到平滑的概率图。loc_from_heatmap取最大连通域的质心作为病灶中心点。参数说明window64对应原始512×512切片上的物理范围。窗口太大定位模糊窗口太小每个patch信息太少模型拿不准。stride32是window的一半保证每个像素至少被覆盖两次。阈值不要一上来就调0.9先在验证集上画PR曲线找到最优阈值。4.3 推理时最容易忽略的坑CT的灰度分布在训练和推理时不一致滑窗推理中我最常踩的坑就是预处理不一致。训练时把每张图的CT值clip到[-1000,400]推理时如果直接读DICOM原始数值、没做同样的clip和归一化模型几乎都会偏到“无病灶”。原因是CT的HU值范围很大软组织在-100到100骨骼可以到1000以上如果不统一窗宽窗位模型看到的灰度分布完全不同。还有一个容易忽略的细节滑窗推理应该逐层对整组CT所有slice执行因为病灶经常跨越多个连续切片。如果只对单张切片做定位中心点的z坐标会不稳定。常见做法是先把每张切片热力图的最大响应记录下来形成z轴方向的响应曲线取曲线峰值对应的切片作为病灶中心层再在该切片上精确求x、y坐标。slice_responses [] for slice_idx in range(data.shape[0]): heatmap infer_slide(model, data[slice_idx]) slice_responses.append(heatmap.max()) peak_slice int(np.argmax(slice_responses))参数说明如果病灶直径跨5个切片直接取argmax可能略偏。可以用“以最大值所在层为中心前后各取1层再对三层的热力图取平均”来稳定z轴定位这个技巧在验证阶段很实用。5. 病灶定位避坑记录从“定位漂移”到“训练崩了”的排查路径这一章专门写我在这类任务里真实踩过的坑每条都按“现象→原因→解决”写。5.1 训练loss卡在0.69不下降现象开始训练ViT二分类epoch到5轮时loss还在0.69附近准确率一直在51%到55%之间徘徊和随机猜差不多。原因0.69是二分类交叉熵的初始熵值对应模型输出概率接近0.5。这通常说明类别极度不平衡CT切片里包含病灶的patch数量远少于背景。模型发现一直预测“无病灶”就能拿到90%以上的准确率于是不再学习。解决不要用裸的CrossEntropyLoss改成带类别权重的版本。最常见做法是用sklearn算平衡权重传给CrossEntropyLoss的weight参数from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( balanced, classesnp.array([0, 1]), ynp.array(all_labels) ) criterion nn.CrossEntropyLoss( weighttorch.tensor(class_weights, dtypetorch.float32).cuda() )参数说明compute_class_weight会根据正负样本数量自动算权重让少数类的loss贡献变大。如果正样本占比低于5%光加权重可能还不够建议对正样本做上采样或者用Focal Loss并设gamma2压低大量易分类样本的loss。5.2 定位中心漂移模型预测和标注差5个切片现象在102例验证数据上模型预测的病灶中心z坐标整体比标注偏移3到5层x和y方向误差不大。原因这是最典型的坐标换算问题。我用dcm2niix生成的NIfTI它的origin取的是第一个切片的中心而标注工具记录的是病灶中心的物理坐标。如果在坐标换算公式里把origin当成0就会出现整体偏移而3到5层的偏移量恰好是spacing乘以层数。解决回到标注工具里查病灶中心坐标的记录方式。多数标注系统用的是DICOM坐标系和NIfTI的默认坐标系一致但有的重建算法会做方向转换。保险做法是先用SimpleITK读取一例数据的origin对照一个已标注病例把换算后的切片序号显示在原图上确认一次确认病灶确实在那个位置再批量跑所有病例。5.3 换了一台CT设备的效果马上崩现象在A院的CT数据上训练验证集敏感度0.92拿到B院同样扫描条件下采集的测试集上敏感度掉到0.61假阳性率翻倍。原因核心是扫描协议差异。A院扫描层厚1.5mmB院是5mmA院重建用肺算法B院用标准算法图像纹理细节差很多。ViT对全局纹理非常敏感但这些纹理并不真正与病灶绑定于是新设备上特征分布发生漂移。解决训练时把层厚、重建核当成数据差异来处理。常见做法有三种一是按层厚分组训练模型只处理训练时见过的层厚范围二是在预处理阶段做各向同性重采样先把所有CT的spacing统一到相同分辨率再训练三是对输入做轻度高斯模糊模拟厚层CT的纹理。我自己习惯用第二种先重采样到1.5mm各向同性保证不同中心的数据分布更接近代价是多出一些重采样的计算时间。5.4 只微调分类头导致指标震荡现象只微调ViT最后三层loss降得很快但验证集Dice忽高忽低用验证集最好的一轮参数去推理定位中心仍然跳动很大。原因ViT的预训练权重来自ImageNet前面的patch embedding和attention层已经学到很强的通用纹理特征。当训练数据只有几千张切片时微调最后三层会让分类头快速拟合但中间的attention层几乎没有更新模型在大数据上的全局上下文能力没有被真正迁移过来。另一个隐患是学习率过大1e-4在数据量少时也会让分类头震荡。解决把backbone和分类头拆成两套学习率backbone用1e-5分类头用1e-4。用timm按参数名拆分参数组即可base_params [p for n, p in model.named_parameters() if head not in n] head_params [p for n, p in model.named_parameters() if head in n] optimizer torch.optim.AdamW([ {params: base_params, lr: 1e-5}, {params: head_params, lr: 1e-4}, ], weight_decay1e-2)逻辑说明这个写法让主干以较慢速度更新分类头以较快速度学习。换掉学习率策略后loss曲线会更平坦Dice不再大幅跳动。5.5 指标高但医生不认现象算法报告说定位精确率高但医生看热力图病灶明明在右肺上叶的尖端模型却把响应最大的区域标在贴着胸膜的位置。虽然不是误检但定位偏了半个肺。原因病灶和胸膜在CT上的灰度特征非常接近尤其是小结节粘连胸膜时模型容易把胸膜当成病灶区域的一部分。ViT的注意力是全局的它更倾向于把响应集中在对比度最强的地方而医生认定的病灶中心是解剖学意义上的“生长原点”两者并不完全一致。解决不要只看中心坐标重合率要和医生一起定义“可接受的定位误差”。一个常用标准是病灶标注周围设一个10mm的容许半径模型预测中心落在这个半径内就算命中。评价指标用“容差命中率”而不是精确的欧氏距离这样既贴近临床习惯也能过滤掉模型卡在胸膜边缘这种对诊断没帮助的输出。6. 让定位结果能交付后处理、阈值选择与效果验证6.1 形态学后处理从热力图到稳定标记热力图出来不能直接当临床结果用先做形态学后处理。滑窗热力图会产生很多孤立小点它们往往是胸膜边缘、血管截面的假阳性。用开运算把这些小点抹掉再用连通域分析保留最大区域最后把质心坐标换算回世界坐标。这个坐标换算就是第3.2节的逆过程很多人卡在这一步导致医生在阅片系统上看到的标记位置对不上。from scipy import ndimage mask heatmap best_t mask ndimage.binary_opening(mask, structurenp.ones((3, 3))) labels, n ndimage.label(mask)参数说明3×3的结构元素能去掉单点噪声又不会把小结节的热力图一起抹掉。如果开运算后最大连通域面积小于某个物理阈值比如小于3mm直径对应的像素数直接判为无病灶。6.2 阈值选择先定漏检容忍度再挑精确率阈值别拍脑袋。做法是把验证集所有滑窗概率收集起来画出精确率-召回率曲线选召回率不低于0.90时精确率最高的点作为阈值。诊断辅助场景里漏检比假阳性严重所以阈值要偏向召回率一端。thresholds np.arange(0.3, 0.9, 0.05) best_t, best_diff 0.3, 0.0 for t in thresholds: pred (pred_scores t).astype(int) recall (pred * y_true).sum() / max(y_true.sum(), 1) precision (pred * y_true).sum() / max(pred.sum(), 1) if recall 0.90 and precision best_diff: best_diff precision best_t t print(best threshold:, best_t)参数说明这段代码遍历0.3到0.85的阈值找出满足召回率大于等于0.9时精确率最高的阈值。如果数据量太少建议用交叉验证把阈值取均值防止调阈值过拟合验证集。最后一件事是可视化检查。随机抽10例验证集病例把模型预测的中心点和标注中心点在原始CT切片上画出来先让经验不足的实习生看再由医生复核。这一步不是走流程它经常能暴露“中心点偏移到胸膜边缘”这类纯指标看不出的问题。做完上面几步这套ViT定位方案才算真正跑完从DICOM进来到坐标输出量化指标和人工复核都过了。我现在每次换数据集做医疗影像定位都会先按这三条走一遍再回头改模型结构。先治好数据、后处理和验证指标的病再折腾网络结构能让你的VisionTransformer少背很多锅。希望帮到你。本文还有配套的精品资源点击获取
返回列表