1. 项目定位与整体设计思路
1.1 为什么选肺部结节图像分类作为毕设方向
每年到毕设选题季,很多同学都在“做什么题目”上反复纠结。我在指导过若干届毕业设计之后,明显感觉到:太宽泛的题目做不出深度,太应用的题目又容易被质疑工作量不够。肺部结节的深度学习分类这个方向,是一个很典型的“中间态”题目——它有明确的研究边界、有标准的公开数据集、有可视化的展示效果,而且医疗影像结合AI本身就自带“价值感”,答辩时很容易让评委觉得“这个题目是有意义的”。
先说清楚这个题目的本质:它做的是二分类,或者说二阶段分类任务。输入是一张肺部CT影像中候选的结节区域(通常是一张裁剪好的图像块),模型需要判断这个区域到底是良性还是恶性。虽然标题写的是“基于深度学习的分类研究”,但实际展开之后会涉及数据预处理、模型结构设计、训练策略、评估指标、界面展示、答辩汇报等多个环节,每一个环节都能作为毕设的工作量支撑点。
这个题目适合三类人:第一类是深度学习基础一般、想找一个结构成熟不容易翻车的题目;第二类是希望简历上有一段比较完整的医疗AI项目经历的同学;第三类是打算后续读研、想往医学图像分析方向靠的。如果是这三类情况之一,这个题目就是一个性价比很高的选择。
1.2 核心需求拆解与技术选型逻辑
毕设答辩时,评委最常问的一句话是:“你为什么选这个方案?”所以选型逻辑不能只是“因为别人这么用”,而是要能够讲清楚每个环节的考虑。
先拆需求。整个项目从功能上看可以分成四块:数据获取与清洗、模型训练与验证、结果可视化与系统封装、论文与答辩材料整理。对应到技术上,分别是图像处理、卷积神经网络(CNN)训练、Web或桌面端推理展示、以及实验记录整理。
模型层面的选型核心是骨干网络。常见的候选有VGG、ResNet、DenseNet、EfficientNet,以及近两年流行的Vision Transformer。实际的建议是:优先选ResNet系列或DenseNet系列,具体型号看数据量和算力。原因很简单:VGG太深太重,训练慢且收益低;ViT在小数据集上容易过拟合,需要大规模预训练支撑;而ResNet的残差结构在医学图像这种数据量不大、纹理特征精细的场景下表现稳定,而且有成熟的预训练权重,迁移学习非常方便。
顺便说一个很多同学忽略的点:这个题目里的“大数据”其实不是指模型需要海量数据,而是指你要有一套规范化的数据处理流程,让有限的数据发挥出最大的效力。这一点在后面的数据处理环节会反复提到。
1.3 项目的工作量规划与答辩得分点
很多毕设在答辩时被质疑“工作量不足”,根源不是做得少,而是做的东西没有被系统地呈现。肺部结节分类这个题目,至少要保证下面几块工作都出现在最终交付里:
- 数据层面:完成数据筛选、格式统一、预处理流程编写,有明确的处理前后对比图;
- 模型层面:完成至少两个以上模型的对比实验(例如ResNet50和DenseNet121),有训练曲线、评估指标对比表;
- 系统层面:完成一个可操作的推理入口,能上传图像并返回分类结果和热力图;
- 展示层面:有Grad-CAM之类的可视化解释,能说明模型关注到的是结节区域。
把这几块写进论文目录,哪怕每块篇幅不长,整体的完整度也会明显高于那些只做“训练一个模型出个准确率”的题目。我的建议是,从一开始就把工作分解到这四个板块,不要先集中做模型、最后再补系统,那样很容易手忙脚乱。
2. 数据准备与预处理实操
2.1 数据集来源与标注格式处理
这个项目通常使用的是公开的肺部CT影像数据集,最常见的是包含结节标注信息的那类公开数据。不同版本之间差异不小,有的包含CT原始DICOM文件,有的已经提供了结节坐标和直径,还有的直接给出了裁剪好的图像块和良恶性标签。
拿到原始数据之后第一件事不是训练,而是把数据格式彻底统一。这里我吃过大亏:一开始抱着侥幸心理直接读原始目录,结果有的子集是DICOM序列,有的是PNG截图,有的还混着重复样本,导致后面训练时频繁报错。正确的姿势是:先写一个数据整理脚本,把原始数据全部复制到一个新的目录结构下,按照类别分子文件夹,同时生成一份CSV清单,记录每个样本的原始路径、类别标签、来源子集。
尤其是标签信息要格外小心。公开数据集中结节良恶性的标注标准并不完全一致,有的标注了“不确定”这一档,需要结合项目要求决定是剔除还是归为某一类。就这个项目而言,最稳妥的做法是只保留标注明确为良性或恶性的样本,把“不确定”的样本丢进另一个文件夹供后续分析使用,不参与训练,否则模型会被标注噪声带偏。
2.2 图像的窗口化与裁剪策略
医学图像和自然图像最大的区别在于成像方式。CT图像是灰度值以HU(亨氏单位)记录的,原始值域范围非常大,人眼能分辨的组织窗口是有限的,所以直接拿原始像素值去做训练不是不行,但效果通常不好。业界通用的做法是“窗宽窗位”调整,也就是截取一个合适的HU范围,把范围内的值线性映射到0-255。
针对肺部结节场景,常用的窗宽窗位设置是窗宽1500、窗位-600左右,这样能把肺实质的灰度层次充分展开,让结节的边缘信息更清晰。用公式表示就是:
normalized = (raw_value - (window_center - window_width / 2)) / window_width clipped = clip(normalized, 0, 1)处理完之后,还需要把图像统一缩放到网络输入尺寸。一般ResNet系列输入是224×224,原始结节图像块可能是不同尺寸的,直接拉伸会导致形状畸变。我的做法是保留原始长宽比,先做等比缩放让短边对齐224,再在长边方向做中心裁剪或边缘填充,这样能在尽可能保留结节完整性的同时满足网络输入要求。
这里有一个细节:不要盲目地把整张肺部CT都作为输入。肺部原始图像很大,真正的结节区域可能只占很小一部分,直接整图输入会让正负样本的特征严重失衡,而且计算成本很高。正确做法是围绕标注坐标裁剪出以结节为中心的局部图像块,这个裁剪方式本身就构成了一个前处理模块,写在论文里也是一个技术点。
2.3 数据增强与类别不平衡处理
医学影像数据集的共性问题是样本量少、类别分布不均匀。良性结节数量往往明显多于恶性,如果直接训练,模型会倾向于把所有样本都判为良性来压低损失,整体准确率看起来还行,到恶性类别的召回率就惨不忍睹。
处理类别不平衡有几个层次的手段,建议从轻到重依次尝试:
- 对少数类做过采样:简单复制恶性样本或使用数据增强扩展恶性样本数量,这是最直觉的方式;
- 对多数类做欠采样:随机去掉一部分良性样本,缺点是损失了信息,适合良性样本明显冗余的情况;
- 调整损失函数权重:给少数类更高的损失权重,在PyTorch里只需要给损失函数传入类别权重参数即可。
数据增强方面,常见组合包括随机水平翻转、随机旋转(10度以内)、随机平移、亮度对比度扰动。有一点必须注意:旋转和翻转要考虑医学图像的对称性。肺部的左右结构虽然不完全对称,但结节分类不是解剖定位任务,适度的翻转是安全的。但不要做垂直翻转,因为CT扫描的方向是固定的,上下翻转会产生不符合物理语义的样本,模型反而会学到错误特征。
最后提一个容易被忽视的步骤:划分数据集时要保证同一患者的多个结节样本不跨集合。公开数据集中一个患者可能有多个结节切片,如果这些切片同时出现在训练集和验证集,由于图像相似度太高,验证集指标会虚高,答辩时被问到“你的模型泛化能力怎么验证”就会露馅。所以划分时要基于患者ID分组,而不是简单按单个样本随机划分。
3. 模型构建与训练细节解析
3.1 网络结构选型与预训练权重加载
模型结构是整个项目的核心。我的推荐顺序是ResNet50首选、DenseNet121备选,两者都做实验对比。ResNet50参数量适中,在公开权重的基础上做微调,训练速度和控制过拟合都有优势。DenseNet121虽然理论上有更好的特征复用,但显存占用更高,如果训练机器只有入门级显卡,跑起来会比较吃力。
加载预训练权重的方式也有讲究。常见的做法有三种,效果差别很大:
- 直接冻结所有卷积层,只训练最后的全连接分类层,适合数据量极少或只是想快速验证流程的情况,但上限很低;
- 加载全部预训练权重,所有层都参与训练,需要数据量较大,否则容易过拟合;
- 折中方案:冻结前面几个Stage,只微调后面几个Stage和分类头,这个方案最适合本项目的样本规模。
在PyTorch里实现冻结和部分微调非常直接。以ResNet50为例,只需要遍历模型的参数,把前两层或者前几层的requires_grad设置为False,然后在构造优化器时只传入requires_grad为True的参数即可。实验经验是:冻结前两到三个Stage,从后往前微调,效果通常比全量微调更稳定,因为底层特征(边缘、纹理、形状)在ImageNet和CT图像上是通用的,而高层语义特征才需要针对结节进行适配。
3.2 损失函数与优化器选择背后的取舍
分类任务默认用交叉熵损失,这是最稳妥的选择。但前面提到了类别不平衡,所以有一个更好的方案:加权交叉熵。本质是在计算每个样本的损失时,根据类别频率乘一个系数。系数的设置不需要太复杂,简单做法是:
weight[0] = num_negative / total_samples weight[1] = num_positive / total_samplesPyTorch中直接传入torch.nn.CrossEntropyLoss(weight=torch.tensor([w0, w1]))即可。注意权重越大对梯度的影响越大,所以对于恶性样本数量较少的情况,权重自然会偏大,模型对恶性样本的错误分类惩罚也就更严格。
优化器方面,Adam和SGD之间的争论在医学图像任务中一直存在。我的实测感受是:虽然Adam收敛快,但很多情况下最终收敛点不如SGD加动量那么“深”,也就是说SGD + Momentum配合合适的学习率调度往往能拿到更高的最终精度。但考虑到毕设时间有限,AdamW其实是更实用的选择,收敛稳定、超参不那么敏感。关键是设置好学习率,初始学习率建议1e-4到3e-4之间,配合余弦退火或者StepLR衰减,避免后期震荡。
3.3 训练流程与关键监控指标
训练过程不能只盯着loss曲线。分类任务里loss下降不等于模型好用,尤其在医学诊断场景,最需要关心的指标是敏感度(召回率)和特异性,也就是恶性样本有多少被正确检出、良性样本有多少被正确放过。在答辩时,评委更愿意听到你从“诊断漏报率”的角度去分析模型表现,而不是只说准确率。
每个epoch结束之后,除了计算训练集和验证集的loss,至少还要记录这批指标:
- Accuracy(总体准确率)
- Precision(查准率)
- Recall / Sensitivity(查全率/敏感度)
- Specificity(特异性)
- AUC(ROC曲线下面积)
AUC是最有说服力的整体指标,它不受分类阈值影响,能反映模型排序能力的优劣。一个合格的模型,验证集AUC应该能做到0.85以上;如果只有0.7,说明模型基本没有学到有效的判别特征,需要回头检查数据预处理或模型配置。
训练过程中保存最优模型也很关键。注意不要只看验证集loss,而应该同时监控AUC——因为loss最低的epoch未必对应AUC最高。实际训练时最好的做法是每训完一个epoch就评估一次,把当前最佳AUC对应的模型参数保存为一个独立文件,训练结束后再用这个文件做后续系统集成,而不是用最后一轮的权重。
3.4 阈值选择与模型后处理
模型输出的原始值是sigmoid概率,默认阈值是0.5,但0.5未必是临床场景下的最优阈值。比如我们希望模型更敏感一些,宁可多判几个假阳性也不希望漏掉恶性结节,那就可以把阈值调低到0.3甚至0.2。反过来如果更看重特异性,就提高阈值。
毕设论文里如果能体现这一步,会显得思路非常完整。实际操作是用验证集跑一遍不同阈值下的敏感度和特异性,画一条ROC曲线,再结合约登指数(Youden‘s Index,即敏感度 + 特异性 - 1)选择最优切分点。这一步实现起来很简单,但答辩效果很好,因为它是从“模型怎么训练”到“模型怎么使用”的一个完整链条,很多同学恰恰缺这一环。
另外,对于一张CT片子,往往可以同时抽取多个包含结节的切片输入模型,然后把多个切片的预测概率做平均或投票作为最终结果。这种推理时段的集成策略能降低单一切片噪声带来的误判,工程上也更容易在系统里实现。
4. 系统实现与答辩展示准备
4.1 推理系统的功能设计与框架选型
毕设的系统部分是展示的手段,不需要做成生产级产品,但需要把核心流程跑通且容易演示。功能上只需要三块:上传图像、返回分类结果、显示注意力热力图。框架选用轻量级的Web方案即可,Python后端加一个简单前端页面是性价比最高的组合。
前后端交互的技术栈不用太重。后端直接用Python的轻量级Web框架,加一个简单的图片上传接口;前端用HTML加少量JavaScript,实现文件选择和结果显示。模型推理部分用PyTorch的CPU模式就可以跑,因为单张图片的推理时间通常在几百毫秒到一两秒之间,不需要GPU加速——这样答辩时的演示环境也更容易搭建,毕竟不是每台答辩电脑都配有独立显卡。
要说一下热力图是怎么生成的。热力图有专门的模型解释方法,即Grad-CAM:通过计算分类得分对最后一个卷积层特征图的梯度,得到每个空间位置的重要性权重,再和特征图加权求和,得到一张和输入尺寸相同的定位图。这张图叠加到原始图像上,就能直观看到模型是依据哪些区域做出的判断。这张图有很强的视觉说服力,答辩当场放出来,比念十个指标都管用。
4.2 后端推理接口的实现要点
后端接口处理的不仅仅是简单的“图片进、结果出”,还要考虑输入图像的预处理链路和模型权重加载生命周期。合理的做法是:模型只加载一次,放在内存里常驻,而不是每次请求都重新加载权重,否则响应时间会非常久,演示时容易卡顿。
推理流程拆分为四步:
- 接收上传的图片文件,读取为numpy数组;
- 执行和训练时完全一致的预处理操作(窗口化、缩放、归一化、维度转换);
- 送入模型得到概率输出;
- 同时记录最后一个卷积层的梯度信息,用于生成Grad-CAM热力图。
一个常见的坑是:训练时做了数据增强,推理时忘记关闭。虽然推理时没有随机增强操作,但某些框架或代码里如果在预处理阶段用了随机函数,会导致每次推理结果都不稳定,演示效果很尴尬。所以建议把推理预处理函数单独写,不放任何随机变换。
4.3 答辩展示所需的材料组织
答辩展示的核心原则是让评委在最短时间内理解你做了什么、为什么这么做、效果如何。所以材料组织要有主线,不要按时间顺序平铺直叙地讲开发历程,而应该按逻辑链路来组织。
建议PPT核心页包括:研究背景与意义、数据来源与预处理流程示意图、模型结构图、实验对比表格、可视化结果展示(热力图对比)、系统演示截图、总结与展望。其中实验对比表格不要只放一个模型的结果,最好有Baseline(比如直接用原始图像训练的模型)和优化后模型的对比,这样才能体现出“研究”的过程。
答辩时高频问题也要提前准备,主要包括:为什么选择这个网络结构、数据增强会不会造成信息失真、如何处理类别不平衡、模型的泛化能力如何验证、这个系统如果上线还需要做什么改进。每个问题都要准备简短的答案,最好结合自己的实验数据来回答,比如“我们实验中发现使用加权交叉熵后恶性类别的召回率从78%提升到了86%”,这样的表述比“我觉得应该有效果”有说服力得多。
5. 常见问题与避坑经验实录
5.1 训练损失不下降的排查思路
这个现象在初学者里面特别常见:模型跑起来了,loss一两个epoch之后就不再下降,或者一直在某个区间震荡。遇到这种情况别急着调参,先按顺序排查下面几个原因。
第一步确认数据预处理是否正确。把预处理后的图像保存出来看一眼,如果图像是反相的、全部是黑块,或者像素值分布异常,那模型根本不可能学到有效特征。第二步是确认标签有没有错位。尤其是自己写了数据加载器的情况下,图片顺序和标签顺序一旦没有对齐,训练集loss也会异常。第三步是检查学习率是否过高。学习率太高会导致loss在低位震荡,太低则下降极慢。第四步是检查是否真的在更新参数,比如冻结网络时忘记把分类头的requires_grad设为True,导致所有参数都不更新,loss不动。
如果是正常训练但最终精度上不去,则要考虑网络容量和特征提取能力。可以尝试更换更深的网络、增加训练轮次,或者检查数据增强是否过强导致模型学不到原始分布。
5.2 显存不足与训练速度过慢的应对方案
毕设场景里很多同学是用自己电脑训练的,显存并不宽裕。训练过程中报CUDA Out of Memory基本上是必经之路。最直接的办法是减小batch size,比如先从32降到16,还报错就降到8或4。batch size小会让梯度更新更频繁,训练曲线更震荡,但配上较低的学习率一般问题不大。
如果连4都跑不了,那就需要检查输入图像尺寸。把224×224的输入换成128×128或160×160,显存占用会显著下降。还要注意关闭不必要的梯度记录,比如在验证阶段使用torch.no_grad(),这个操作能省下不少显存。另外,混合精度训练也是一招,PyTorch里可以开启自动混合精度(AMP),用半精度浮点数计算,不仅能降低显存占用,而且通常还能提速。
训练速度慢的问题,常见原因是DataLoader的num_workers设置太小,或者数据读取时有大量重复的预处理计算。建议把已经完成窗口化处理的图像提前保存为numpy数组或预处理好的图像文件,训练时直接读取,省去每轮重复计算预处理的时间开销。
5.3 验证集表现虚高的假象与应对
有一种隐蔽的偏差,就是验证集准确率很高,但想象到实际临床场景就不行了。排除前面提到的患者级划分问题之外,还有两个容易产生“数据泄漏”的点需要特别警惕。
第一个是归一化统计量的泄漏。如果使用标准化归一化,用全部数据计算出均值和标准差再统一使用,那么验证集的信息已经间接参与了训练过程,验证结果会偏乐观。正确做法是只在训练集上计算均值和标准差,然后直接应用于验证集和测试集。
第二个是数据增强的泄漏。如果在划分之前就做了过采样或者重度增强,那么增强产生的样本可能和原始样本非常相似,参与验证后会让结果虚高。正确做法是先划分、后增强,并且验证集和测试集不做任何增强操作,只保留原始图像。
答辩时如果被问到泛化能力,可以说“我们使用患者级别的分组策略保证了训练集和验证集之间没有同源样本重叠,同时只在训练集上计算和保存归一化参数”,这句话能体现出你对数据泄漏问题的敏感度。
5.4 热力图可视化不理想时的调整技巧
做Grad-CAM热力图有时会遇到一个现象:高亮区域分散在整个图像上,而不是集中在结节区域。原因通常是模型的判别特征过于全局化,或者图像中背景纹理过于突出。
最常见的调整方式是把热力图的生成从最后一层卷积层往前移到倒数第二个或更早的卷积层。因为越深的层特征语义越强但空间分辨率越低,导致热力图很粗糙;而适当靠前的层有更好的空间定位能力。这个选择可以做一个简单的对比实验,展示不同层生成的热力图效果。
还有一个非常实用的小技巧:如果发现热力图中结节区域有高亮,但同时背景也有大片高亮,可以尝试在输入图像的预处理阶段强化窗宽窗位调整。肺癌CT图像的HU值范围如果没压好,背景噪声和肋骨边缘会被放大,模型自然也会关注到这些区域。把预处理参数调整后再训练,热力图的聚焦质量往往会有明显改观。
5.5 答辩时关于模型局限性的回答策略
毕设答辩有一个隐藏考点:评委想知道你对这个题目的边界是不是有清醒的认知。如果被问“你这个模型有什么局限性”,千万不要只说“没有”或者“暂时还没发现”,也不要答得太笼统。
从这类医学图像任务的共性出发,可以准备三个角度的回答。第一个是数据维度:训练数据来源单一,数据量有限,模型的跨机构泛化能力还未经充分验证。第二个是模型维度:当前模型更多是判断图像层面的良恶性,尚未结合临床特征(如患者年龄、吸烟史、结节形态学描述)进行多模态分析。第三个是安全性维度:医疗诊断系统需要极高可靠性,目前的模型输出不能直接作为临床依据,还需要更大规模的临床验证和更完善的错误分析机制。
这三个角度既诚实,又展示了你的思考深度。尤其第三个角度,让评委感觉到你不是在做一个玩具Demo,而是对医疗AI的真实落地约束有基础的认知,这在评分上是一个不小的加分项。
最后再分享一个经验:整个项目过程中,最值得投入精力的不是模型精度那零点几的提升,而是把数据预处理和评估流程做得规范。我在这个项目上踩过最大的坑就是把时间都花在调模型参数上,后来真正让评分上升的,反而是数据处理流程的可视化展示、患者级划分的严谨性、以及在答辩时对误差分析方法(即哪些样本被分错、为什么会分错)的梳理。把这些做扎实,这个毕设就算是很高质量地完成了。