
做计算机毕设的时候看到“基于Python卷积神经网络训练识别牙齿是否健康”这个题目我的第一反应是这其实是一个被低估的宝藏选题。任务本身听起来很简单——拿到一张牙齿照片判断这颗牙或这口牙是否健康但真正上手之后你会发现完整的链路一点都不少数据清洗、图像预处理、网络设计、训练调参、指标评估、模型导出、界面展示。你能把这些环节完整走一遍并且讲清楚每一步“为什么这么设计”毕业答辩基本就稳了。这篇文章我不打算写成那种干巴巴的教程我更想以“做过类似项目”的角度把整个项目的拆解思路、实操过程、踩坑记录和调参心得一次讲透。目标是让正在准备牙齿健康识别类毕设的同学或者对医学图像分类感兴趣的开发者看完之后可以直接照着做并且知道遇到问题时该从哪里下手排查。1. 项目定位与需求拆解1.1 这个毕设到底在做什么从技术角度看这个题目本质上是一个图像二分类任务输入一张牙齿图像模型输出“健康”或“不健康”两类标签。这里的“不健康”在数据层面通常包括龋齿、牙结石、牙龈红肿、牙齿变色等常见问题而“健康”则指外观上没有明显病变的牙齿。为什么说这个题目的难度设置很合理因为它比“Hello World”级别的图像分类比如识别猫狗需要你付出更多数据处理的功夫但又不至于像目标检测、语义分割那样需要用矩形框去精确定位病灶位置。你不需要告诉模型“牙齿区域的第几颗牙有龋洞”只需要让模型基于整张图给出一个判断。这种粗粒度的分类任务对计算机专业毕设来说既可以完整展示你对深度学习全流程的掌握又不会因为任务过重导致答辩时讲不清楚。业务层面这个项目其实对应着一个很实际的需求口腔健康初筛。在体检、社区筛查、基层诊所等场景中借助图像分类模型先做一轮自动筛查可以帮助医护人员快速标记出疑似存在问题的患者再安排人工复检。当然模型只能作为辅助手段不能替代专业医生的诊断这一点不仅在项目文档里要写清楚答辩时也应该主动说明。1.2 为什么选CNN而不是传统方法这是答辩时几乎必被问到的问题也是你设计项目时的核心逻辑。传统图像识别路线一般是人工提取特征颜色直方图、纹理特征、边缘统计等然后用SVM、随机森林这类分类器做判断。问题是牙齿图像中病变区域的位置、大小、颜色深浅、形状差异太大。比如龋齿有的表现为一个小黑点有的则是一大片发黑的区域牙结石可能是淡黄色也可能是深褐色牙龈炎的红肿范围和程度也各不相同。你很难手工设计出一组特征能把这些形态都稳定覆盖住。而卷积神经网络CNN的核心优势就是自动提取特征。卷积核通过局部感受野扫描图像低层网络学到边缘、纹理、颜色斑块等基础特征高层网络在底层特征的基础上组合出更抽象的区域形状和语义信息。你用CNN做牙齿图像分类相当于让模型自己决定看哪里、提取什么特征你只需要喂给它足够多的标注样本。我做这个项目时用过一个挺贴切的类比人看一张牙齿照片先看整体轮廓再逐一观察每一颗牙表面有没有黑点、黄斑、红肿区域CNN的层次化特征提取思路和这个观察顺序非常像。低层卷积关注局部细节高层卷积整合全局信息最后全连接层基于这些特征做出分类决策。1.3 技术栈与运行环境选型深度学习框架我推荐PyTorch理由有三点动态计算图调试方便报错信息对新手友好torchvision内置了大量预训练模型和标准数据集接口做迁移学习几乎零成本社区资料丰富遇到问题随便一搜就能找到答案。如果你更熟悉TensorFlow也不是不行但下面的代码示例我会基于PyTorch写。开发环境方面Python版本选3.8以上用Anaconda创建独立虚拟环境是个好习惯。核心依赖包括PyTorch和torchvision模型搭建和预训练权重OpenCV-Python和Pillow图像读取与预处理NumPy数值计算scikit-learn混淆矩阵、分类报告等评估指标Matplotlib绘制训练曲线和可视化结果GPU有没有没有也能做。因为这个项目本质上是对小规模数据集做分类你可以把图像缩放到较小的尺寸配合batch_size16、一个轻量级网络CPU上训练也能在半小时到两小时内跑完。但如果想在可接受的时间内多次调参有一块支持CUDA的NVIDIA显卡哪怕是入门级体验会好很多。我自己一开始用CPU跑实验后来借到一张显卡训练效率提升非常明显。2. 数据准备决定模型上限的关键一环2.1 数据从哪里来三种常见途径牙齿健康识别这个方向最大的难点不是模型而是拿不到高质量、带准确标注的数据。医学图像数据涉及患者隐私不可能像ImageNet那样公开发布一个几百万张的大规模牙齿数据集。我在实际调研和实践中总结出三条可走的路径第一种和口腔诊所或医院合作。这是最理想的方式拿到的是脱敏后的口内照片或X光片。注意脱敏是底线图片中不能出现患者姓名、病历号、人脸正面照等隐私信息。否则做项目会惹上麻烦。合作的方式一般是由机构提供数据你负责标注工具和模型训练成果共享。第二种在公开渠道搜集口腔图像。比如一些医学公开数据集、论文附带的数据、开放授权的图库等。关键是看清楚授权协议有的数据集不允许用做商业用途但毕设非商业性质一般问题不大。需要提醒的是网络图片质量参差不齐有的不是牙齿特写有的带着嘴唇、舌头等干扰区域整理清洗的功夫不能省。第三种自建小型模拟数据集。如果前两条路都走不通可以用仿真牙模拍摄照片或者从网络收集图片后人工筛选。注意不要直接拿患者隐私来做实验也不要为了凑数量把完全看不出牙齿区域的图片也塞进去。关于数据量要多大的问题毕设不追求刷SOTA我认为800到2000张的小规模数据集就足够了。有人可能会怀疑这么少的数据能训练出什么答案是配合数据增强和迁移学习临场效果完全可以支撑一轮演示。数据量不是首要矛盾标注质量才是。如果标注本身是乱的再大的数据集也只能训练出一个高精度的“乱猜”模型。2.2 图像预处理与数据增强策略数据准备好之后不能直接扔进网络先做标准化处理。统一尺寸我用的输入尺寸是224x224。为什么选这个数因为torchvision里预训练模型比如ResNet、MobileNet的默认输入尺寸就是224x224这样迁移学习时几乎不需要适配输入层代码保持一致省心很多。归一化像素值从0-255范围缩放到0-1范围再按通道进行标准化。PyTorch预训练模型官方文档里给的ImageNet均值mean[0.485, 0.456, 0.406]和std[0.229, 0.224, 0.225]我们用预训练权重时最好沿用这套归一化参数保证输入分布和预训练时一致。数据增强这是小样本项目的救命稻草。我常用的组合包括随机水平翻转、随机旋转10度以内、随机调整亮度对比度饱和度、随机缩放裁剪。做完增强之后一张原始图相当于制造出几十张不同变换的样本模型见过的“形态”就丰富多了。但这里有一个非常重要的区别数据增强只作用于训练集验证集和测试集只能做尺寸统一和归一化不能做随机变换。有些人图省事把增强用在所有数据上结果发现训练精度很高但测试表现却很差原因就在这里——测试时你不希望引入随机噪声模型应该看到的是接近真实使用场景的稳定输入。这个逻辑想通之后后续评估结果才站得住脚。2.3 数据集划分与DataLoader实现划分比例我习惯用60%训练、20%验证、20%测试。如果数据量很小也可以调到70%、15%、15%。关键是划分时必须保持类别比例一致性也就是健康和不健康的样本在两个集合中的比例要大致相同。我见过一个真实案例某同学的数据集有健康1200张、不健康800张他写代码时用随机数直接按70%切分结果测试集里健康样本占了90%不健康样本只有80张。模型训练完在测试集上的准确率看着很高但实际识别不健康牙齿的能力一塌糊涂。这就是没有考虑类别层次的典型坑。底层实操上最优雅的方案是先把两类图片分别放进“healthy”和“unhealthy”两个文件夹然后用PyTorch的ImageFolder接口加载并配合transforms实现预处理。加载代码大致如下from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop((224, 224), scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) valid_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(rootdata/train, transformtrain_transform) valid_dataset datasets.ImageFolder(rootdata/valid, transformvalid_transform) test_dataset datasets.ImageFolder(rootdata/test, transformvalid_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) valid_loader DataLoader(valid_dataset, batch_size32, shuffleFalse) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse)这里有个细节值得说RandomResizedCrop的比例参数scale设置为(0.8, 1.0)意思是裁剪时保留原图80%到100%的区域。为什么不是更激进的(0.4, 1.0)因为牙齿图像中病灶区域本身就可能很小裁剪太多会把关键信息裁掉。越是对细节敏感的任务增强强度越要克制。3. 模型搭建与训练配置3.1 从零写一个微型CNN到底够不够用在引入预训练模型之前我建议你先自己手写一个微型CNN。这个步骤不是为了最终成绩而是为了理解卷积层、池化层、全连接层到底在做什么。而且答辩时老师很可能会问“你了解卷积核大小、步长、池化的作用吗”如果你能对着自己敲的代码解释出来印象分会好很多。一个结构简单但有效的微型CNN可以参考下面的设计import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Dropout(0.5), nn.Linear(128 * 28 * 28, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x这个网络每经过一次下采样特征图的边长减半通道数翻倍。为什么这样设计因为卷积网络通过“空间上不断压缩、通道上不断增加”的方式把图像从像素空间映射到更高层次的语义空间。最后再接上全连接层做决策。Dropout层在训练时随机丢弃一部分神经元防止网络过于依赖某几个特征通道对防止过拟合有帮助。但这个模型在牙齿这种小数据集上的表现大概率不会特别出彩因为从头训练的参数量相对于数据量来说可能还是偏大。所以下一步我的建议是换用预训练模型做迁移学习。3.2 迁移学习路线ResNet18是稳妥主力牙齿健康识别这种小规模医学图像任务迁移学习几乎是标准解。什么意思呢就是先让模型在ImageNet这种千万级数据集上预训练过已经学会了非常通用的特征提取能力我们只把它最后一层分类头换掉再用自己的牙齿数据集做微调。我优先选用ResNet18原因很朴素网络深度适中残差连接让梯度传播更顺畅训练起来不容易出幺蛾子参数量适中CPU和GPU都能跑预训练权重在torchvision里一键下载。如果数据量特别小比如只有四五百张图就选MobileNetV3或者ResNet18这种轻量网络容量小过拟合风险更低。迁移学习有两种训练策略第一种冻结backbone只训练最后一层分类头。backbone的权重在训练时完全不动把预训练阶段学到的特征当成“现成的特征提取器”。这种策略适合数据量很少的情况通常几分钟就能训完。但如果牙齿图像和ImageNet的日常物体差异太大冻结底层可能会导致特征不匹配。第二种解冻全部层进行微调。backbone和分类头一起参与训练只是所有层的学习率统一调低。这样模型会在已有通用特征的基础上适应牙齿图像特有的分布。效果通常更好但训练时间更长对数据量和算力的要求也更高。我的做法是先冻结backbone用0.001的学习率训练5个epoch把分类头训到能基本收敛然后再解冻全部层、用0.0001的学习率微调15到20个epoch。两阶段训练的好处是前几轮分类头参数还比较随机如果backbone也跟着大幅更新容易把预训练学到的特征破坏掉。先训好分类头再让backbone微调整个过程会平稳很多。代码替换ResNet18分类头的写法也很简单from torchvision import models model models.resnet18(pretrainedTrue) num_features model.fc.in_features model.fc nn.Linear(num_features, 2) # 健康 / 不健康注意最后的num_features要读取原始全连接层的输入维度不要硬编码成2048这种数字因为不同预训练网络的这个值不一样。3.3 损失函数、优化器与超参数选择分类任务的损失函数二分类时最常用的写法有两种一种是输出2个logits配合nn.CrossEntropyLoss()模型内部会先算softmax、再计算交叉熵标签用0或1表示健康、不健康另一种是只输出1个logit配合nn.BCEWithLogitsLoss()使用sigmoid做概率化。两者在数学上对二分类是等价的但实际中我推荐第一种因为跟多分类代码逻辑统一以后扩展成“龋齿/牙周病/健康”三分类时只需要改变输出维度。优化器我会选Adam因为它在大多数任务上开箱即用对初始学习率的敏感度比SGD低。如果你熟悉SGD加Momentum理论上最终精度可能更高但调参成本也更高。毕设场景下Adam更划算。学习率是训练时最需要盯住的超参数。冻结backbone阶段可以开到0.001解冻微调阶段要降到0.0001左右。如果发现loss剧烈震荡或出现NaN优先把学习率除以10重新跑。batch_size一般取32显存紧张就降到16但太小的batch会使梯度噪声变大。epoch数量不建议一次性设死。每次训练完一个epoch后观察验证集loss如果连续5到10个epoch都没有下降就提前结束。可以设置EarlyStopping也可以自己手动记录最佳验证精度、保存最优模型这一点后面会展开讲。4. 训练实测与性能调优4.1 训练循环从头到尾跑通一个epoch写训练循环的时候我见过不少同学把代码写得特别复杂但实际上核心就那几个步骤。下面这份代码可以直接复制到你的项目里它把一次完整的训练和验证流程都包括了import torch import torch.nn as nn device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) def train_one_epoch(loader, model, criterion, optimizer): model.train() total_loss 0.0 correct 0 total 0 for inputs, labels in loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * inputs.size(0) _, preds torch.max(outputs, dim1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total def validate(loader, model, criterion): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, labels in loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) total_loss loss.item() * inputs.size(0) _, preds torch.max(outputs, dim1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total这里有一个非常典型、也非常容易翻车的点训练和验证模式的区别。model.train()和model.eval()不仅影响Dropout是否生效也影响BatchNorm层使用的统计量。在eval模式下BN层会使用训练阶段累积的全局均值方差而不是当前batch的统计量。如果你忘记切换model.eval()验证时的预测结果会受到随机Dropout影响导致同一张图每次预测结果都不一致。相反如果你在训练阶段忘开了model.train()Dropout和BN都会处于错误状态模型可能一直无法收敛。在训练过程中我建议每个epoch都打印训练集和验证集上的loss与准确率同时打印当前学习率。这样你能从损失曲线的形态快速判断训练是否健康loss一直在降说明梯度流动正常loss波动越来越大一般跟学习率偏高有关loss缓慢但持续下降通常是正常现象。4.2 过拟合验证集精度不如训练集的应对路径牙齿数据量小过拟合是大概率事件。典型表现是训练集准确率一路冲高到99%但验证集只有75%到85%且两者差距持续扩大。应对过拟合我的优先级列表是这样的数据增强优先。这是成本最低、效果也最明显的手段。如果当前增强强度已经比较大可以再增加一个随机擦除RandomErasing模拟牙齿上出现遮挡物、反光斑等情况帮助模型增强鲁棒性。Dropout和权重衰减。模型分类头部分加Dropout优化器里加weight_decay1e-4相当于给权重加了L2正则约束鼓励模型用更小的权重去拟合数据。降低模型复杂度。如果用的是自定义大网络改成预训练轻量模型通常立竿见影。预训练模型的底部特征已经在海量数据上预训练过很稳定你只需要让高层去适应牙齿数据过拟合空间会小很多。早停。记录验证集指标最好的那一个epoch把参数保存下来训练结束时再加载回这个最优状态而不是用最后一个epoch的参数。这是防止后期过拟合的兜底方案。我在实际项目里还发现有些“过拟合”其实是数据本身的问题。比如训练集里所有健康牙齿都是白平衡正常的照片而不健康牙齿全是偏黄偏暗的照片模型学到的可能是“亮度偏暗不健康”这种伪特征。因此在数据清洗时就要刻意保证不同类别的图像分布在拍摄条件上尽量接近这一步比任何调参都更管用。4.3 评估指标不要只盯着准确率训练完成后很多同学习惯把准确率往报告里一贴就结束了。但准确率在类不平衡时极具迷惑性。假设测试集中健康样本占60%不健康样本占40%模型把所有图都判为健康准确率也有60%看起来“还行”但模型实际一个病牙也找不出来。所以我在做评估时至少会看四样东西混淆矩阵、精确率、召回率、F1值。混淆矩阵能一眼看出模型把哪一类误判成了哪一类精确率衡量预测为“不健康”的样本里真的不健康的比例召回率衡量所有不健康样本里被模型找出来的比例。两者是相互制约的F1是它们的调和平均。scikit-learn里两个函数就能完成from sklearn.metrics import classification_report, confusion_matrix y_true [] y_pred [] with torch.no_grad(): for inputs, labels in test_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) _, preds torch.max(outputs, dim1) y_true.extend(labels.cpu().numpy()) y_pred.extend(preds.cpu().numpy()) print(classification_report(y_true, y_pred, target_names[healthy, unhealthy])) print(confusion_matrix(y_true, y_pred))对于二分类还可以绘制ROC曲线并计算AUC。AUC的好处是对分类阈值不敏感哪怕你默认阈值0.5设定得不好AUC也能客观反映模型的排序能力。如果答辩时被问到“模型区分好坏样本的整体能力如何”AUC是一个非常合适的回答。5. 常见问题与排查技巧实录5.1 训练不收敛或Loss为NaNLoss变成NaN是深度学习新手最容易慌的事情。常见原因按概率排序大概是学习率过大。这是头号嫌疑犯。如果初始学习率设成0.01甚至更高训练loss可能直接飙升到NaN。解决方法是把学习率下降到0.0001重新观察。输入数据含有异常值。有没有损坏的图片有没有全黑或全白的异常样本如果PIL无法正常读取某张图返回的tensor可能是空的反向传播自然就崩了。排查方法是在加载数据后写一段代码扫描所有图片的数值范围看是否存在全0或极大值。归一化参数写错。比如忘了做标准化输入像素还是0到255范围配合预训练模型很容易导致梯度爆炸。我自己遇到过这个问当时折腾了很久才发现Normalize没生效因为transforms里写错了顺序。排查顺序我的建议是先打印输入张量的shape、数值范围和标签确认数据没问题再打印第一个batch的前向输出看有没有Inf或NaN最后逐步把学习率降低一个数量级重跑。定位到具体环节后问题通常很快就能解决。5.2 分类结果总是偏向某一个类别如果测试时模型几乎把所有图都判成同一个类别比如都判成“健康”首先检查测试集本身的类别比例。如果测试集里健康样本远远多于不健康样本模型只需要倾向于预测多数类就能把总损失压得很低。更彻底的解决方案是类别均衡处理。可以用WeightedRandomSampler在每次采样时让少数类被抽到的概率更高也可以在损失函数中直接传入weight向量给少数类的loss加一个更大的权重系数。两者选一即可不必同时用。还有一种容易被忽视的情况训练集里不健康样本的形态本身就不够丰富。比如不健康样本大多是轻度的色素沉着而测试集里的不健康样本全是严重龋齿模型很难泛化。这种情况不属于调参范畴而是数据分布问题需要在数据采集阶段解决。5.3 精度一直上不去该怎么排查如果模型能正常收敛但验证集准确率始终在85%附近晃提升比较乏力我一般按下面的顺序检查先看数据集质量。标注是否干净有没有把健康牙齿标成不健康、把阴影误判成龋齿的低质量样本我建议把训练中被模型反复预测错的那批样本单独导出来用眼睛过一遍往往能发现是标注噪声还是拍摄条件过差。再看图像分辨率。如果原始图片比较模糊或者拍摄时对焦不准病灶细节丢失模型上限会被严重压制。可以尝试先用超分辨率自动修复工具做一次预处理但我个人觉得效果不够稳定不如采集阶段就把图像质量把控好。最后尝试换更强的预训练模型作为baseline对比。比如从ResNet18换成ResNet50或EfficientNet看看精度有没有提升。如果性能依然没有明显变化说明瓶颈不在模型容量上大概率还是数据问题或标注质量问题。5.4 训练速度太慢或显存不够没有GPU时CPU上跑ResNet18训练几十个epoch会很煎熬。我试过的实用方案是将输入尺寸从224降到160。对于牙齿分类这种任务160的输入已经能保留足够细节训练速度可以提升一倍。减小batch_size比如从32降到16。虽然梯度噪声变大但配合较小的学习率效果损失不大。换成MobileNetV3或SqueezeNet这种轻量网络参数量比ResNet18少一个量级。训练时开启混合精度。现在很多显卡支持AMP自动混合精度训练在几乎不影响精度的同时显著减少显存占用和加快速度。6. 部署与展示让模型在答辩现场“活起来”6.1 用Gradio三行代码搭一个Web Demo答辩时如果只是播放一段PPT里的训练曲线效果远不如现场演示一次“上传一张图模型立刻给出判断”。我推荐用Gradio它是目前学起来最轻松、演示效果又最可靠的工具。下面这个代码片段就能搭起一个简单的网页界面import gradio as gr import torch from torchvision import transforms from PIL import Image transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict(img): img_tensor transform(img).unsqueeze(0) with torch.no_grad(): outputs model(img_tensor) probs torch.softmax(outputs, dim1) healthy_prob, unhealthy_prob probs[0].tolist() return { 健康: healthy_prob, 不健康: unhealthy_prob } demo gr.Interface( fnpredict, inputsgr.Image(typepil), outputsgr.Label(num_top_classes2) ) demo.launch()这里的亮点是输出用gr.Label组件会直接以带概率的标签形式展示非常直观。运行时它会生成本地网页链接答辩现场演示的时候让老师上传图片或者从电脑里选图体验很顺畅。6.2 模型保存与导出别在最后一步掉链子训练完成后保存模型我有一个强烈建议只保存state_dict不要保存整个模型。保存state_dict的做法是torch.save(model.state_dict(), tooth_classifier.pth)加载时再重新定义网络结构然后load_state_dict。这样即使之后你改了类名或文件路径模型参数依然能独立迁移。如果你草率地直接torch.save(model, ...)一旦项目换了一台机器、Python版本或PyTorch版本有差异反序列化失败的概率会明显上升。如果要导出为ONNX服务端或移动端部署会更简单。导出时务必用torch.onnx.export指定固定的输入尺寸dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, tooth_classifier.onnx, input_names[input], output_names[output], dynamic_axesNone )dynamic_axesNone意味着模型只接受固定尺寸输入。如果你之后传入一张640x480的图片不做resize直接喂给模型会报错。所以ONNX导出后推理管线里务必保留resize和归一化步骤。7. 说几句掏心窝的经验建议做牙齿健康识别这个项目最大的感触是真正花时间的不是模型训练而是数据整理。我第一次做完整个流程后复盘发现调参只用了不到两天数据清洗、标注确认、图像标准化却花了将近三周。这个比例在医学图像类项目里是非常真实的。如果你准备用这个题目做毕设请一定提前规划好数据这块的时间别等到最后才开始催别人给你传图片。答辩的时候不要只展示一张高精度截图重点讲清楚三件事数据怎么收集和清洗的为什么选择CNN和迁移学习评估阶段用了哪些指标、这些指标分别说明什么把这些讲透了老师会觉得你是真的理解了这个项目的技术逻辑而不是只会按教程跑代码。这个项目后续还可以继续扩把二分类升级成龋齿、牙周病、健康牙的三分类或者在分类基础上再叠加一个目标检测模块把病变位置框出来形成一个更接近临床使用的系统。不过如果你是第一次做深度学习项目我建议先把分类这件事做扎实别一上来就铺太大摊子。最后也顺便说明一下这类模型即使效果再理想也仅用于辅助研究或初步筛查演示最终的判断还是要交给具备专业资质的医生。在项目文档、答辩PPT和Demo界面里都加上这样一句免责声明既是对医疗专业性的尊重也会让你的项目显得更严谨。