拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

肺部CT多病种智能诊断实战:天池数字人体赛题全流程复现

肺部CT多病种智能诊断实战:天池数字人体赛题全流程复现 简介这是一份面向AI医疗影像初学者与竞赛入门者的天池比赛baseline代码包对应2019年全球数据智能大赛“数字人体”赛场一的肺部CT多病种智能诊断任务。方案使用YOLOv3完成病灶目标检测再以ResNet网络进行假阳性衰减整个过程简单直接、依赖较少在Windows环境即可运行非常适合新人快速跑通赛题流程并在此基础上进行后续优化。压缩包内共27个文件以Python脚本为主涵盖训练、测试、数据预处理、标注转换、锚框聚类等关键模块另有YOLOv3配置文件、类别文件与说明文档整体压缩后仅147KB代码量精简便于阅读和二次修改。目前已有58人学习下载此代码包。随包附有清晰的README与目录结构说明可帮助理解data、code、model_data等模块的用途虽然从成绩看初赛B榜41/1635、复赛35准确率中等但作为一套容易上手的baseline能够完整展示CT多病种智能诊断中目标检测与假阳性抑制的技术链路适合用于学习、实验与参赛起步。1. 天池的“数字人体”赛场一在解决什么问题先给肺部CT智能诊断定个位天池的“数字人体”赛场一核心任务是肺部CT多病种智能诊断给定一套胸部CT断层序列判断患者是否存在多个预定义疾病标签中的若干项。这类任务在医疗AI落地中的典型价值是把影像科医生日常阅片时的判断逻辑拆成可量化、可离线验证的模型而不是做一个单一病种检测Demo。这个压缩包本身提供的是一条完整数据链路从zip内的DICOM序列、标注信息到最终提交标准都包含在内。适合两类人复现想用公开赛题建立医学影像工程经验的同学和正在评估CT智能诊断方案可行性的从业者。复现时的关键判断集中在数据解析、多标签训练和推理落地上。2. 把zip变成可训练的CT序列数据解析与预处理的完整链路拿到这个压缩包第一件要做的事不是解压后急着写训练脚本而是先确认数据在磁盘上长什么样。这类赛事的数据组织通常按患者ID建目录目录下放若干DICOM序列但也出现过把全部文件平铺、靠文件名序号区分的版本。先做文件体检能省掉后面不少排查时间。2.1 解压前先做文件体检zip结构、伪加密与DICOM读取在服务器上我习惯先用unzip -l列出压缩包内容而不是直接unzip -o到磁盘。这样可以提前看到顶层文件夹命名、文件数量级判断这次要处理的是DICOM原始序列还是已经转换成npy/nii的中间格式。下面的命令会把清单前50行打出来并保留一份完整清单到文件中。unzip -l 天池比赛-肺部CT多病种智能诊断-全球数据智能大赛\(2019\)“数字人体”赛场一.zip | head -50 unzip -l 天池比赛-肺部CT多病种智能诊断-全球数据智能大赛\(2019\)“数字人体”赛场一.zip filelist.txt第一段代码用于浏览目录结构第二段把完整清单写到本地后面做数据检查时可以直接grep文件名。压缩包文件名里带中文、括号和引号命令行里要用反斜杠转义或者用Tab自动补全让shell处理。如果文件路径里有空格整条路径要加引号。解压时有个容易绕弯的坑叫zip伪加密压缩包里的文件实际没有加密但zip头部的一个flag bit被置位unzip会提示输入密码或报CRC错误。这不是真正的密码保护只是文件打包工具留下了错误的加密标志。用Python zipfile读一下每个文件的flag_bits就能判定import zipfile archive_path 天池比赛-肺部CT多病种智能诊断-全球数据智能大赛(2019)“数字人体”赛场一.zip z zipfile.ZipFile(archive_path) for info in z.infolist(): # bit 0 是加密标志位伪加密文件该位为 1但实际并未加密 if info.flag_bits 0x1: print(pseudo-encrypted:, info.filename)这段代码遍历压缩包所有成员把加密标志位为1的条目列出来。如果列出的文件能直接通过z.open(info)读出内容且无需密码就是伪加密。遇到这种情况不需要去猜密码直接用Python的zipfile逐个提取或者用7za按普通压缩包处理都能避开错误的标志位。接下来是DICOM读取。DICOM序列不是单张图片而是一个文件夹里几十到几百张.dcm文件。常见正确做法是用glob递归找全所有dcm文件按DICOM标签里的图像位置排序而不是按文件名排序。import glob import os import pydicom import numpy as np def load_series_as_volume(series_dir): dcm_paths glob.glob(os.path.join(series_dir, **, *.dcm), recursiveTrue) if not dcm_paths: raise RuntimeError(fno dcm files: {series_dir}) slices [pydicom.dcmread(p) for p in dcm_paths] slices.sort(keylambda s: float(s.ImagePositionPatient[-1])) # 按z坐标排序 vol np.stack( [s.pixel_array.astype(np.float32) for s in slices], axis-1 ) # 得到 (H, W, slices) slope getattr(slices[0], RescaleSlope, 1.0) intercept getattr(slices[0], RescaleIntercept, 0.0) vol vol * float(slope) float(intercept) return vol, slices[0]这段代码的关键有三个。一是glob的recursiveTrue保证不管dcm埋在几层子目录下都能找到二是按ImagePositionPatient的z轴排序文件名里虽然有slice序号但有的扫描序列在采集时会出现无序写入不能只信文件名三是RescaleSlope和RescaleIntercept这两个标签DICOM里存储的像素值是设备原始灰度要转成临床通用的HU亨斯菲尔德单位必须做这个线性变换。如果把slope不是1的序列不转换直接送入网络所谓“同一窗口”就失去意义了。2.2 CT值的获取和重采样从像素灰阶到统一体素间距前面加载函数已经把每个序列转成了(H, W, D)的HU体数据但不同CT机的扫描参数不一样像素间距往往不同。有的在xy方向是0.6mm有的是1.2mm层间距从0.5mm到5mm都有。CNN输入需要固定尺寸重采样是绕不过去的步骤。常见做法是把所有序列重采样到统一物理间距我一般用(1.5, 1.5, 3.0)或者(1.5, 1.5, 1.5)。轴内1.5mm对肺结节来说足够层间3.0mm则直接压缩z方向深度减少显存压力。对多病种判别任务层间分辨率降低对分类效果影响不大但能明显提升训练速度。from scipy.ndimage import zoom def resample_volume(vol, src_spacing, dst_spacing(1.5, 1.5, 3.0)): # src_spacing: (spacing_x, spacing_y, spacing_z) factor ( src_spacing[0] / dst_spacing[0], src_spacing[1] / dst_spacing[1], src_spacing[2] / dst_spacing[2], ) vol_r zoom(vol, factor, order1) # 线性插值速度和精度平衡 return vol_r.astype(np.float32)一次完整的重采样分三步先读物理间距再计算缩放因子最后执行插值。缩放因子是“原始间距除以目标间距”。例如原始层间距5mm、目标3mm时factor为5/31.67表示z方向会被放大1.67倍。order1的线性插值在前后处理上够用order3的插值边缘更平滑但三维体数据上耗时差5到10倍样本上千之后基本等不起。如果确实需要更高精度也只对z方向用order3xy保持线性。重采样后通常还要统一深度。网络输入固定的128层时超过部分从中间裁不足部分两侧补零。def crop_or_pad_depth(vol, target_depth128): depth vol.shape[-1] if depth target_depth: start (depth - target_depth) // 2 return vol[..., start:start target_depth] pad_left (target_depth - depth) // 2 pad_right target_depth - depth - pad_left return np.pad(vol, ((0, 0), (0, 0), (pad_left, pad_right)), modeconstant)裁切和补零的逻辑完全一致都是把z方向正中间的一段保留下来。之所以取中间是因为肺部CT扫描的开始和结束位置会带很多空层中间部分的解剖结构最稳定体表外的空气区域和体腔外的背景对病种判别没有贡献还容易干扰归一化统计。提示如果显存只够3D输入支撑到32层深度不要直接把网络输入改成32层硬跑。优先把重采样的z方向间距调到4~5mm让同样深度覆盖更多的解剖范围再不行就切2.5D方案。2.3 窗宽窗位与归一化把数据切到模型熟悉的灰度范围CT值是绝对物理量同一个像素位置在不同机器上的HU值基本一致但不同疾病的最佳显示窗不一样。肺实质在肺窗下对比度最好纵隔淋巴结在纵隔窗下才清晰。常见做法是直接把网络输入做成多窗叠加比如把肺窗和纵隔窗分别归一化后拼成两个通道。def get_windowed_volume(vol, center, width): vmin center - width / 2.0 vmax center width / 2.0 vol np.clip(vol, vmin, vmax) vol (vol - vmin) / (vmax - vmin) return vol.astype(np.float32) # 肺窗与纵隔窗分别得到两个0~1的输入通道 lung_w get_windowed_volume(vol_hu, center-600, width1800) medi_w get_windowed_volume(vol_hu, center50, width400) # 与原始HU归一化后concat可以得到三通道输入具体看网络输入层设计这里面的参数直接来自临床窗口约定肺窗是中心-600、窗宽1800对应截断区间[-1500, 300]纵隔窗是中心50、窗宽400对应截断区间[-150, 250]。映射后数据范围被压到0~1不同病例之间不会因为扫描机器差异产生灰度偏移。为什么不直接用全局min-max归一化因为只要有一个序列包含高密度金属植入物或扫描床边缘全局最大值会被拉得很远正常肺实质的对比度会被压缩窗口截断天然不受这种极端值影响。参数说明center和width这两个超参是我在肺CT定位任务中调得最多的。只做肺结节分类时只保留肺窗也能跑病种多且涉及胸膜、纵隔、淋巴结时叠加纵隔窗能把这部分特征补上。多窗输入会增加网络第一层的输入通道数但不会显著增加计算量因为3D卷积后续层参数量不变。另外重采样和窗口处理后一定要做一次数值审查打印每个volume的min/max/mean把全零volume和含NaN的序列找出来。这步虽然看起来基础但DICOM解析错误和重采样边界错误都会在这一步暴露比训练到一半再排查要快得多。2.4 按患者划分训练与验证集防止数据泄漏的硬规则数据划分在复现里最容易被忽略但它对结果可靠性的影响比换模型大得多。同一个患者的多次CT序列在内容和诊断标签上高度相关如果把同一个患者的一部分序列放进训练集、另一部分放进验证集验证集指标会虚高。这属于典型的数据泄漏不是玄学是划分逻辑问题。正确的划分级别是患者级别不是序列级别。先拿到每个序列对应的PatientID用一个字典把序列映射到患者再对患者列表做划分。分层的目的是让训练集中不同病种的比例不要偏离原始分布太多。import random from collections import defaultdict patient_to_series defaultdict(list) for series_id, patient_id in series_meta: patient_to_series[patient_id].append(series_id) patient_ids list(patient_to_series.keys()) random.seed(2024) random.shuffle(patient_ids) train_ratio 0.8 split_idx int(len(patient_ids) * train_ratio) train_patients set(patient_ids[:split_idx]) val_patients set(patient_ids[split_idx:]) train_series [sid for pid in train_patients for sid in patient_to_series[pid]] val_series [sid for pid in val_patients for sid in patient_to_series[pid]]这段代码按“患者ID到序列ID”的两级映射组织数据。如果原始数据里没有显式PatientID就用DICOM的PatientID tag再退一步用文件夹名。注意random.shuffle之前必须设固定seed否则每次跑划分结果不同模型对比就没有意义。分层抽样在这里没有写复杂逻辑因为多标签分层实现比较繁琐常见做法是先用患者级标签向量做一次粗略分桶确保每种疾病在两个集合中都出现再在桶内随机抽取。注意DICOM里的PatientID可能为空或用研究ID代替遇到多中心数据时先统计每个文件夹下不同PatientID的数量再做患者分组。调参过程中不要在训练集上验证模型。验证集一旦用作阈值选择或早停依据就不要再回灌到训练过程里做二次调参否则它也会“泄漏”。多轮试验后建议重新划分一次数据或做3折交叉验证用折间波动判断某个改进是否稳定。3. 模型骨架与训练策略怎么定3D网络、损失函数和类别不平衡的取舍数据准备好之后才是模型。赛题叫“多病种智能诊断”不是“单病种识别”这一点决定架构选型和损失函数。如果一开始把它当成某一种疾病的二分类任务去调后面换病种时要改动的地方就很多。3.1 任务定界多标签分类、疾病共现与检测的取舍肺部CT诊断在临床上天然是多标签问题一个患者可能同时有肺结节和肺炎也可能一个标签都没有。网络输出的每个病种概率不再是互斥的softmax而是每个标签独立sigmoid。这样做的好处是训练时每个标签的梯度互不压制模型可以学到病种之间的共现关系坏处是类别不平衡的影响会被放大后面需要专门处理。第一步应该先做成多标签图像分类输入重采样后的3D volume输出C个0~1概率值。如果赛题提供的标注还包含病灶坐标框可以扩展出检测分支但不要直接上3D目标检测因为CT的bbox标注质量和数量通常不足以稳定训练anchor网络。先跑通多标签baseline把数据管线、验证框架和推理流程全部打通再考虑在baseline基础上加定位分支或先检测后分类的两阶段方案。常见做法还有一个用滑窗或多尺度裁剪把大体积拆成多个patch每个patch做多标签分类再对patch概率做max或mean pooling。这相当于增加空间分辨率但会成倍增加推理时间只在病种目标小且数据量足够时推荐。3.2 3D卷积与2.5D方案怎么选显存、预训练与收敛三角多标签CT分类的核心网络有两条路线。第一条是纯3D卷积常见backbone为3D ResNet、3D DenseNet第二条是2.5D方案常见做法是先从volume的z方向采样若干层对每层用2D CNN提特征再做时序汇聚或直接对2D特征做池化。两条路线在这个任务上都跑得通但适用条件不同。3D卷积的好处是能直接利用三维空间结构肺结节、条索影这类具有强空间连续性的病灶更容易学。代价是显存和算力。以3D ResNet-18为例输入(1, 128, 192, 192)在V100 16GB上batch size取4到6比较安全。2.5D方案可以用ImageNet预训练权重初值质量高收敛快但对z方向的空间上下文利用不充分病灶重叠或跨层分布时容易混。我的选型建议是资源紧张时跑2.5D ResNet-50建立基线资源允许时切换到3D ResNet-18。基线代码只需要把输入从“每个样本一个volume”改成“每个样本取9到15层切片”其余训练逻辑不用大改。def sample_2d5d_slices(vol_resampled, num_slices9): depth vol_resampled.shape[-1] # 等间隔取中心附近的切片避免只取固定位置 inds np.linspace(depth * 0.2, depth * 0.8, num_slices).astype(int) slices [vol_resampled[..., i] for i in inds] return np.stack(slices, axis0) # (num_slices, H, W)这里用等间隔采样覆盖范围是深度的20%到80%相当于避开胸腔上下界外的空层。每个切片是(H, W)的单通道灰度图后续可以当作2D单通道输入也可以把相邻切片堆成三通道输入。堆三通道相当于用2D卷积的通道维度模拟z方向的小范围上下文ImageNet预训练权重可以加载前三通道的初始值。第一阶段建议输入的xy分辨率保持192不要轻易降到128。肺结节可能只有几毫米在1.5mm间距下只占两三个像素再降分辨率小病灶的信息就彻底丢了。3.3 损失函数与类别不平衡Focal Loss的落地方案损失函数是这一节的核心。多标签分类默认用Binary Cross Entropy这个损失在类别严重不平衡时会被负样本主导。每个患者大部分标签都是0负样本梯度数量可能是正样本的十倍以上模型容易倾向于输出全零。Focal Loss对置信度高的样本降权让模型把注意力放在难样本上缓解了这个问题。PyTorch的BCEWithLogitsLoss可以直接加pos_weight这是最简单有效的加权方案如果想进一步抑制易分负样本用Focal Loss。import torch import torch.nn as nn import torch.nn.functional as F class MultiLabelFocalLoss(nn.Module): def __init__(self, gamma2.0, alpha0.75): super().__init__() self.gamma gamma self.alpha alpha def forward(self, logits, targets): bce F.binary_cross_entropy_with_logits(logits, targets, reductionnone) pt torch.exp(-bce) # 从bce反推sigmoid概率数值更稳 loss self.alpha * (1 - pt) ** self.gamma * bce # 先对标签维度平均再对所有样本求和 return loss.mean(dim0).sum()这个实现里有个细节先用BCE算出逐点损失再通过exp反向求pt。如果直接调用sigmoid(logits)算ptlogits绝对值很大时容易出现浮点饱和反向推导则稳定得多。alpha取0.75表示正类的缩放权重相当于负类权重为0.25。gamma控制困难样本权重gamma0时退化为普通CEgamma2时易分负样本的梯度被压得很低。在肺CT多标签问题上alpha不建议机械地用Focal论文里的0.25。病种间阳性频率差异很大固定标量只解决“正负样本数”的宏观失衡解决不了“不同病种阳性频率相差50倍”的问题。如果想让每个病种的权重更合理把alpha扩展成向量比如第i个标签的频率为f_ialpha_i1/sqrt(f_i)然后对每个标签分别乘。代码里没有做这个展开但实现思路是保留一个可学习的alpha参数或预计算权重向量。参数使用心得固定标量时gamma1.5到2.0、alpha0.6到0.8这个区间对肺CT多标签比较稳。gamma太高会把难样本权重抬得过大导致少数病种在训练后期震荡。3.4 训练循环与早停用病种AUC而不是整体准确率做判断网络和损失定义好之后训练策略还要做两个决策学习率制度和验证指标。医学影像数据量比ImageNet小很多批量大数据集常用的“从1e-3开始、按step阶梯衰减”在这里不够稳我一般用warmup加cosine总共50个epoch左右。warmup的前3个epoch让BN统计量先稳定避免一开始就把模型推到陡峭的损失曲面。具体学习率可以这样给3D ResNet-18随机初始化时基础lr上限控制在1e-4到5e-4warmup后cosine降到接近0。2.5D ResNet-50因为有ImageNet预训练可以用2e-3起步但前几个epoch如果loss出现明显上升不要硬扛马上把lr减半再试。验证指标不该用整体准确率。类别不平衡时把全部样本预测为阴性准确率也有90%以上。我会在验证集上按每个标签计算ROC-AUC再把所有标签的AUC做平均作为早停依据。这个指标对阈值不敏感能反映排序质量适合判断模型到底学得怎么样。from sklearn.metrics import roc_auc_score def mean_label_auc(y_true, y_prob): aucs [] for i in range(y_true.shape[1]): if len(set(y_true[:, i])) 1: aucs.append(roc_auc_score(y_true[:, i], y_prob[:, i])) return float(np.mean(aucs)), aucs这个函数给出所有标签的平均AUC和单标签AUC列表训练时每2个epoch打印一次。如果某个标签的阳性样本数小于5AUC本身方差会很大出现剧烈抖动是数据量问题而不是模型问题。遇到这类标签不要为了提升它把整个训练节奏带偏先把其他高频病种稳住再用伪标签或数据增强去补低频项。早停的细节模型状态dict建议保存“最优”和“最后一个epoch”两份类似后悔药。训练后期如果AUC波动可以回退到最优权重做推理而不是拿最后一步的权重硬跑。4. 复现过程避坑指南五个最容易让结果翻车的环节赛题复现掉链子的地方往往不在模型结构而在数据链路里那些一眼看不出来的错位。我按踩过的先后顺序把最值得提的五条写下来。每条都按现象、原因、解决三个步骤展开。4.1 文件结构不一致glob递归读取比固定路径可靠现象按固定路径拼接dcm文件训练不到一半报FileNotFoundError检查后发现有部分病例的dcm文件不在预期目录下有的直接放在病例根目录有的嵌套到双层子目录。原因数据生产方打包时没有统一目录层级文件夹命名也五花八门用固定字符串拼接路径必然漏文件。漏掉的序列会让标签和数据失配轻则损失样本重则训练分布偏移。解决不依赖固定相对路径用glob配合recursiveTrue按文件名后缀递归收集所有文件再按DICOM内部的SeriesInstanceUID聚合到序列。每个患者目录下可能有多组序列比如平扫和增强不能把不同序列混成一个volume。通用逻辑是先递归收集所有dcm再按SeriesInstanceUID分桶取层数最多的一组作为主序列。这样即使目录结构不统一数据加载也稳定。4.2 显存溢出先把深度裁剪到64层再谈batch size现象3D网络首次训练输入128×256×256batch_size写到8几轮迭代后直接CUDA out of memory。原因3D卷积显存占用随输入尺寸成三次方增长宽高从224提到256显存增长不只是百分之几而是立体的增长。很多人先resize到256再调batch路径反了。解决先降深度再降空间分辨率最后才降batch size。重采样时直接把z方向间距设为3mm后深度大约在90到120层再统一裁剪到64层轴面分辨率降到192×192。这样输入体量从128×256×256降到64×192×192显存占用约为原来的三分之一batch size 8基本可以在16GB卡上跑起来。如果还溢出把batch降到4而不是继续削分辨率因为分辨率下降会直接影响小病灶的区分度。4.3 全零预测Bias初始化、正负样本比与Warmup配合现象训练多轮后推理输出几乎全是0.1以下的概率所有预测标签都落在阴性侧训练loss还在缓慢下降验证AUC却接近0.5。原因负样本占绝对多数。网络最后一层bias从零初始化梯度在负样本方向堆积让sigmoid输出被压到零。没有warmup时学习率前几百步过大参数直接走进“全零”的局部极小。解决数据层面用Focal Loss或对阳性样本做小幅重采样抑制负样本模型层面把最后一层bias初始化成正样本先验概率的对数几率训练层面加3个epoch左右的线性warmup。三个操作要一起做只靠一个往往压不住。import numpy as np prior_pos 0.05 # 先验阳性率由标注统计得到 bias_init np.log(prior_pos / (1 - prior_pos)) # log(0.05/0.95) ≈ -2.94 model.cls_head.bias.data.fill_(float(bias_init))这段代码的意义是让模型在初始状态就输出约0.05的概率而不是0.001这样第一轮梯度不会全被负样本淹没。先验阳性率直接从标注统计得到不依赖任何外部数据。配合Focal Loss通常在30个epoch内能看到标签AUC持续上升。4.4 验证集划分泄漏按患者聚合是“后悔药”最容易漏掉的现象线下验证AUC能到0.97测试结果却大幅缩水。原因划分数据集时用了序列级别随机同一个患者的序列同时出现在训练集和验证集里。相邻序列是同一个病人同一个时间点的扫描模型不需要泛化就能记住患者特征验证集变成了记忆库。解决先按PatientID聚合确保每个患者只出现在一个集合。标签分布可以做一次粗略检查训练集每种病种比例与全量数据的比例偏差在5%以内说明划分基本可信。做交叉验证时每折都要重新按患者划分不能复用上一次划分结果。4.5 推理阶段内存堆积把中间数组显式释放现象推理阶段跑到一半内存不足尤其当序列包含几百层薄层CT时单个case处理完内存占用不降反升。原因代码里同时保留了原始HU体数据、重采样体数据、窗口截断体数据三个副本。每个副本如果是256×256×300的float32就是约80MB三个副本240MB原以为不大但PyTorch显存、数据加载worker、多个副本叠加内存就被吃满。解决在推理循环里用函数作用域封装单case流程退出时把体积变量del掉并调用gc.collect()同时把DataLoader的num_workers控制在合理范围。CPU预处理和GPU前向尽量解耦避免单进程内同时压两份大数组。提交格式也要在推理前确认。先构造一个空的CSV模板确认每一行的patient_id和预测概率列名与要求一致。列名不一致时即使模型跑出好数字也会在提交环节扣分。格式确认后再跑全量推理避免白跑几小时。5. 从能跑到跑赢伪标签、模型融合与验证阈值调整基线模型稳定之后提升方向有三个数据广度、模型多样性和阈值精度。这三个方向排序很明确先扩数据再融合模型最后调阈值。5.1 伪标签扩数据高置信度样本的第二轮训练赛题只提供有限标注数据但未标注的CT序列往往很多。常见做法是拿第一轮模型对未标注数据做预测把置信度高的样本当作伪标签加入训练集再跑第二轮。伪标签容易引入噪声我的习惯是只加入两类所有标签概率都低于0.1的确定性阴性样本或者只有一个标签概率大于0.9且其余均低于0.2的强阳性单病种样本。中间带混淆的样本不加因为那是模型自己都拿不准的部分硬加进去只会放大噪声。5.2 模型融合3D与2.5D的分数级加权基线用3D ResNet-18第二路可以换成2.5D ResNet-50或其他DenseNet。两个模型结构差异大错误分布相关性低融合后比单独调三个月参更稳。加权系数不需要复杂寻优在验证集上把权重从0.1到0.9扫描一遍取平均AUC最高的一组。alpha 0.6 # 3D模型权重 pred_blend alpha * pred_3d (1 - alpha) * pred_2d5d这行代码就是分数级融合。不用做stacking层医学影像分类任务里决策边界相对简单线性融合已经能拿到大部分收益。如果两个模型的指标差距过大优先把弱模型权重调低而不是硬拉平。5.3 按标签独立调阈值验证集上找最优切点模型输出的是概率提交或判断阴阳性需要阈值。不同病种的敏感度和特异度偏好不同一个统一0.5阈值会让正样本稀少的病种漏掉太多阳性。按标签扫描F1最大值是常见做法。import numpy as np from sklearn.metrics import f1_score best_thr [] for i in range(y_val.shape[1]): best_t, best_f1 0.5, -1 for t in np.arange(0.15, 0.85, 0.05): f1 f1_score(y_val[:, i], (y_prob[:, i] t).astype(int)) if f1 best_f1: best_t, best_f1 t, f1 best_thr.append(best_t)这段代码逐个标签扫阈值最后得到每个标签的最优阈值列表推理时按标签取用。需要注意阈值是跟着模型权重走的换模型或换数据分布后要重新选择不能一路用到底。如果某标签的最优阈值低于0.2或高于0.8说明概率分布极不健康优先查数据和标签质量而不是硬套阈值。我现在的习惯是每次跑完一组实验先看标签AUC分布再做阈值扫描。阈值扫描这类后处理放在最后一步避免模型迭代过程中被验证集阈值带来虚假的“好”。在复现这类肺部CT多病种智能诊断任务时我在预处理上花的时间往往比改网络多一倍。数据链路不牢靠时模型的任何改进都难判断是真提升还是噪声。希望帮到你先把数据管线和验证划分钉死再开始调模型能少走很多弯路。本文还有配套的精品资源点击获取
返回列表