基于深度学习的皮肤烧伤检测项目复现与避坑指南 简介一份围绕皮肤烧伤检测的深度学习实战项目基于卷积神经网络自动识别烧伤范围、深度与愈合阶段利用深度学习提升烧伤诊断的准确性与效率主要面向医疗AI研究者、算法工程师与计算机视觉学习者既适合算法验证也便于二次开发。压缩包共有168个文件以43个Python脚本、38个YAML配置、16个Jupyter Notebook及多格式图片、CSV记录文件为主其中py为训练与推理工具、yaml用于模型参数配置、ipynb保留实验分析过程另有Dockerfile、Shell脚本辅助环境部署覆盖数据预处理、模型搭建、训练调参、日志输出与结果可视化等完整环节整体大小仅41.69MB目录组织清晰可直接进入训练流程。目前已有105人学习下载资源额外配套了从数据收集、预处理、模型设计、训练测试到部署验证的流程教程帮助初学者理解烧伤识别任务中CNN如何提取关键特征也可基于源码快速复现实验结果或迁移到其他医学图像分类场景。整体是一套端到端的优质实战案例能提供可复用框架与系统化工程思路。1. 皮肤烧伤检测为什么选深度学习先把“测什么”和“能测准多少”对齐“皮肤烧伤检测”这个标题拆到底其实是一件事让深度学习算法在创面图片上给出烧伤深度分级而不是做“有伤/无伤”的二分类。医生在处理早期烧伤时最吃不准的就是浅Ⅱ度还是深Ⅱ度这直接决定保守换药还是手术切痂而肉眼判断本来就有明显的主观性。正因为这个痛点“基于深度学习的烧伤检测”才成为医疗 AI 项目里既现实又有动手空间的课题。适合谁看想用一份带源码和流程教程的项目入门医学图像分类的开发者或者手里只有几百张标注图、想在本地跑通训练和验证的工程人员。下文按我复现这类项目的习惯拆开讲任务定义、数据集组织、训练参数再讲四个复现时最常见的坑。2. 复现这套皮肤烧伤检测项目任务拆分、数据集来源与最小依赖环境2.1 烧伤检测不等于一块模型分类做分级分割做定位拿到“基于深度学习实现的皮肤烧伤检测算法”这类项目时先别急着解压训练得确定你要交付的是“整张图的烧伤等级”还是“图上哪一块是几度烧伤”。这两个问题对应的模型完全不同。常见做法是先把任务分成两类。第一类是分类输入一张裁剪好的创面局部图输出四个类别——I 度、浅Ⅱ度、深Ⅱ度、Ⅲ度。这类模型实现简单一个 ResNet 加一个分类头就能跑训练脚本干净验证时看混淆矩阵也直观。第二类是分割逐像素输出每个位置的烧伤深度适合面积大、同时存在多种深度的创面术前规划更需要这种但标注成本高模型出来也重。我的建议是如果这个 zip 里的流程教程只给了 train.py 和 infer.py默认它大概率是分类任务。先按分类把它跑通得到 baseline 之后再考虑给模型加分割分支。烧伤创面往往混合了红、白、黄灰和焦痂分类模型对这个信号的抓取比想象中更直接而分割网络在小数据集上反而容易学到错误的边界。源码项目复现的第一步不是追求“模型高级”而是把任务边界先立住。2.2 数据怎么攒成标准结构目录划分与标签映射复现时最容易卡住的是数据。烧伤图像不像 ImageNet 一条命令就能下来源一般三块公开医学图像库、医院脱敏数据、从公开竞赛和教科书资料中人工整理。量通常不大几百到两三千张都算正常。你手里的 zip 里如果带了数据优先看它的目录结构如果没带自己得先按下面的方式搭一份。data/ train/ superficial/ # 浅Ⅱ度 deep/ # 深Ⅱ度 full/ # Ⅲ度 / 全层 val/ test/这种按类别分文件夹的结构配合 torchvision 的ImageFolder直接能读是“源码流程教程”类项目最常见的格式。我一般会在数据目录里再放一个label_map.json把文件夹名映射到整数避免训练时类别顺序被 filesystem 顺序搞乱{ first_degree: 0, superficial: 1, deep: 2, full: 3 }划分数据集时有一个关键原则按病人划分而不是按图片划分。同一患者的多张创面照片高度相似如果同一患者照片同时出现在 train 和 val验证集分数会被数据泄漏虚高等部署到新病人身上就直接“翻车”。我经历过一次看起来 val acc 95%、换新数据掉到 60% 的情况后来反复查才发现是划分时没按patient_id做 group split。这是烧伤检测这类小样本医学项目最典型的坑。2.3 环境固定用尽量少的依赖先把训练脚本跑起来不要一上来就装一堆深度学习全家桶。复现源码项目的核心是先让入口脚本能跑依赖越少越好排查。我通常会先把 requirements 钉成下面这几行torch2.0.0 torchvision0.15.0 opencv-python4.5.0 tqdm scikit-learn pandas然后新建虚拟环境安装python -m venv .venv source .venv/bin/activate pip install -r requirements.txt逻辑说明torch 和 torchvision 必须保持主版本对应否则加载预训练权重时容易报size mismatch或直接ImportError。opencv 用于读图和导出scikit-learn 用来算混淆矩阵和分类报告这几样够覆盖整个流程。如果你机器没有 GPUpip 会自动装 CPU 版还是 CUDA 版取决于 PyTorch 官方给出的安装命令这一步在第五章会展开这里只要保证 import 不出错就行。还有一条血泪经验解压路径不要带中文和空格。很多 zip 里的脚本用的是相对路径路径里一旦出现非 ASCII 字符opencv 读图和 torch 保存权重都可能直接报错。这种错误表面上看是环境问题实际上改一个文件夹名字就好了。3. 把训练脚本改造成能收敛的闭环从过拟合单batch到参数稳定区3.1 第一口饭过拟合四十张图先让代码“能学”拿到源码后的第一个动作不是完整训练而是先在几十张图上把 loss 打下来。这一步的目的很简单确认数据加载、模型前向、损失计算、梯度回传这条链路是通的而不是花两小时跑完一个 epoch 才发现 loss 压根没动。# 固定取一个 batch看模型能不能记住它 sample_images, sample_labels next(iter(train_loader)) model.train() for step in range(200): logits model(sample_images.to(device)) loss criterion(logits, sample_labels.to(device)) optimizer.zero_grad() loss.backward() optimizer.step() if step % 20 0: print(fstep {step:3d} | loss {loss.item():.4f})这个脚本里只用一个 batch比如 16 张图跑 200 步。如果代码没问题loss 应当在前几十步内明显下降最后能逼近 0.1 以下。如果 loss 降不下去先查三件事标签是否传错、模型参数是否被requires_gradFalse钉死、数据读取后像素值是否异常。这一步是动手深度学习里最廉价的“体检”比看任何教程都管用。3.2 模型结构选型预训练 ResNet 替换分类头烧伤图像在 ImageNet 里几乎不存在但预训练权重依然值得用。原因在于烧伤深度判级依赖创面的颜色纹理和表皮结构特征而这些低级视觉特征——边缘、颜色渐变、组织块状纹理——和 ImageNet 上学到的通用表征是共享的。直接用随机初始化在小数据集上从零训基本会被 sample size 打回原形。import torchvision from torch import nn model torchvision.models.resnet50( weightstorchvision.models.ResNet50_Weights.IMAGENET1K_V1 ) model.fc nn.Linear(model.fc.in_features, num_classes4)替换掉最后的fc是因为原来的输出是 1000 类我们的分类数是 4。in_features从model.fc.in_features拿不要手写 2048这样以后换 resnet18 时不用改第二行。选定这个结构之后我建议先全冻结骨干只训练分类头跑几个 epoch 看到 val 能到基线后再解冻最后一段进行微调。常见的误用是一上来就全参数requires_gradTrue加上大学习率后面 4.2 会专门说这个。3.3 三个开箱参数图像尺寸、学习率与早停训练参数直接决定你这个源码项目能跑出多少分。下面这组是烧伤分类项目比较省事的起点我复现同类项目时基本沿用这个底座再按数据量微调参数起点值调整策略输入尺寸224 x 224创面纹理细节不足时升到 384但训练时间翻倍batch size16显存不够降到 8同时学习率减半优化器AdamWweight_decay 建议 0.01比 SGD 稳学习率分类头 3e-4骨干 1e-5解冻骨干后一定要降学习率epoch30 起加到 val loss 不再下降为止训练循环里最关键的是“按 val loss 存模型而不是按 val acc”。best_val_loss float(inf) no_improve_epochs 0 for epoch in range(max_epochs): train_loss train_one_epoch(model, train_loader, optimizer, criterion) val_loss evaluate(model, val_loader, criterion) if val_loss best_val_loss: best_val_loss val_loss no_improve_epochs 0 torch.save(model.state_dict(), best.pt) else: no_improve_epochs 1 if no_improve_epochs 5: print(fearly stop at epoch {epoch}) break逻辑说明evaluate只返回 val loss 不返回 acc是故意的。烧伤数据类别不均衡准确率在“全猜浅Ⅱ度”时也能到 70% 以上用它判断是否保留模型会把最需要关注的深Ⅱ度和Ⅲ度样本带偏。val loss 不降反升持续 5 个 epoch就加载历史最优权重这是最常见也最有效的早停方案。3.4 多任务和分割分支先别贪有些“优质项目实战”会附一套 U-Net 风格的分割分支同时输出分类和像素级掩膜。做法通常是在 ResNet 骨干后并行接一个解码器总 loss 设为0.8 * DiceLoss 0.2 * CrossEntropyLoss。原理上合理但第一次复现时我不建议开双头训练。原因很实际两个任务的收敛速度不同尤其在几十个 epoch 内联合训练很容易出现分类没学好、分割也没学好的两头空局面。4. 烧伤检测复现避坑四个高频问题和排查线索4.1 类不均衡伪装成高准确率现象训练集和验证集准确率都到了 90% 以上看起来“项目跑通了”但一打印分类报告深Ⅱ度和Ⅲ度的 recall 只有 20%。原因烧伤样本里浅Ⅱ度占大头深Ⅱ度和全层烧伤样本少模型偏向学大类的统计规律。解决方法先看混淆矩阵再给损失函数加权。from sklearn.metrics import classification_report, confusion_matrix y_true, y_pred [], [] for images, labels in val_loader: logits model(images.to(device)) y_pred.extend(logits.argmax(1).cpu().tolist()) y_true.extend(labels.tolist()) print(classification_report(y_true, y_pred, digits3))加权方法是给CrossEntropyLoss传weight权重按“类别样本数的倒数”做平滑import torch # 手动给一个类的权重顺序与 label_map 一致 class_weights torch.tensor([1.0, 1.5, 3.0, 8.0]) criterion torch.nn.CrossEntropyLoss(weightclass_weights)权重大小不是越大越好。我试过按严格样本数反比把全层烧伤权重拉到 20 倍结果模型反而把大量浅Ⅱ度误判成Ⅲ度。一般先给 24 倍梯度观察 confusion matrix 里低频类的 recall 是否抬升再逐步加大。医学判级场景里模型宁可过度倾向严重类别也不该把深Ⅱ度漏成浅Ⅱ度。4.2 迁移学习反而掉点预训练特征被大学习率打散现象训练脚本如果直接全量微调学习率设成标准的 1e-3loss 第一个 epoch 就震荡val loss 远高于随机初始化。原因ImageNet 预训练的骨干特征已经被训练得很好新换上的分类头是随机权重。梯度刚开始主要来自随机头大学习率回传到骨干把预训练学到的纹理和颜色过滤器毁得七七八八。解决分两步。第一步分类头单独训几个 epoch第二步解冻骨干把学习率降到 1e-5 左右继续微调。# 第一阶段只训练分类头 for name, param in model.named_parameters(): param.requires_grad name.startswith(fc) optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr3e-4, weight_decay0.01 ) # 第二阶段解冻全部骨干 for param in model.parameters(): param.requires_grad True optimizer torch.optim.AdamW( model.parameters(), lr1e-5, weight_decay0.01 )这套“先头后干”的顺序基本适用于所有医疗图像小数据集的迁移学习场景。如果你发现某个“源码包”里直接写了lr1e-3且没有任何冻结逻辑大概率作者只在小规模 demo 上跑通并没有仔细考虑过泛化。4.3 颜色增广把烧伤判据“洗掉”现象训练时加入了RandomBrightnessContrast、RandomSaturation甚至随机色调偏移然后发现验证集崩溃。原因烧伤深度分级的核心判据就是创面颜色——红色区域多为浅Ⅱ度苍白或蜡白是深Ⅱ度焦黑坏死则是Ⅲ度。一旦颜色增广过猛模型只能退而寻找纹理和形状信息而这些信号在真实照片里稳定性远不如颜色。我一般把颜色增广限制在极窄范围from torchvision import transforms as T train_transform T.Compose([ T.RandomResizedCrop(224, scale(0.8, 1.0)), T.RandomHorizontalFlip(), T.ColorJitter(brightness0.1, contrast0.1, saturation0, hue0), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])饱和度和色调直接设为 0亮度对比度各只给 0.1这个幅度足够应付拍摄环境差异又不会把“红”变成“白”。如果项目自带的增广里有RandomGrayscale建议直接删掉灰度图对烧伤判级基本没有可用信息。4.4 zip 源码跑不通的“三件套”排查现象解压后运行python train.py报错要么是ModuleNotFoundError要么是加载预训练权重时size mismatch。原因大概是三类requirements 没装全但不报依赖错、预训练权重文件缺失或路径不对、Python 版本与 torch 安装版本错位。排查顺序固定成“三件套”。先看入口文件最前面的 import凡是第三方库先 pip 补上再找模型加载那一段如果代码写了torch.load(weights/resnet50.pth)而项目目录里没有这个文件那这就是第一问题最后用一个短命令确认 torch 和 torchvision 能同时导入python -c import torch, torchvision; print(torch.__version__, torchvision.__version__)如果还出现size mismatch多半是加载的权重来自另一种模型架构比如代码期望 ResNet50 你却在 torchvision 里取了 ResNet101。不要试图硬改state_dict把加载方式改成 torchvision 自带的weights...IMAGENET1K_V1最省事。5. 没有GPU怎么办用CPU把烧伤检测源码跑出有效结果5.1 思路转变别用“训练”的思路用“特征再利用”很多人的困惑是手里只有一台普通笔记本CPU 上跑 ResNet50 全量微调一个 epoch 几十分钟看起来完全做不下去。解决办法不是去买 GPU而是把任务降维成“线性探测”linear probe用预训练模型把图像转成特征向量然后用一个线性层做分类。这么做的道理在于烧伤分级数据量本来就少模型真正需要学习的是一个“如何在预训练特征空间里划界”的浅层问题没必要重新训练卷积核。CPU 上跑 ResNet 推理一张 224 的图确实要时间但如果只提取一次特征保存到内存成本是一次性的。这比硬上剪枝算法实在得多——剪枝算法是同一模型反复迭代时用的加速手段第一次复现动剪枝很容易把精度压垮。5.2 一个能下班的 CPU 训练脚本import torch import torchvision from torch import nn # 主干提取特征但不做分类 base torchvision.models.resnet18( weightstorchvision.models.ResNet18_Weights.IMAGENET1K_V1 ) base.fc nn.Identity() # 去掉分类头输出变成 512 维特征 for param in base.parameters(): param.requires_grad False head nn.Linear(512, 4)这个base一旦加载完毕训练阶段它完全处于“数据搬运”状态不参与梯度计算。整个模型的待训练参数只有head的权重和偏置也就 2048 个参数CPU 上跑十来个 epoch 几分钟就能完成。配合前面的数据集划分效果虽然不如全量微调但对几百张图的烧伤分类项目已经够用。# 把特征放进线性头里训练 features base(images) # [batch, 512] logits head(features) # [batch, 4] loss criterion(logits, labels)如果嫌 ResNet18 还慢可以把输入尺寸从 224 降到 160ResNet18 的瓶颈在最后几个 stage降分辨率带来的速度收益远大于精度损失。160 尺寸配合随机裁剪缩放基本不影响读色块。5.3 CPU 推理出口导出 ONNX 做轻量验证训练完成后建议导出 ONNX。这样可以在不依赖 torch 的 Python 进程里做推理也方便你把它接到 Web 端或手机端做后续项目演示。model.eval() dummy torch.randn(1, 3, 160, 160) torch.onnx.export( model, (dummy,), burn_classifier.onnx, input_names[image], output_names[logits], opset_version12 )导出后可以用 onnxruntime 加载输入直接传 NCHW 的 float32 张量不需要再引入 torch。逻辑说明这个藏在“最后一章”的操作往往是整个源码项目最容易被忽略的部分但实际部署价值很高因为医生端目标机器通常没有 GPUONNX 推理一次只需一次前向。6. 把“大概率对”变成“可交付”验证模型与不确定性设计模型在验证集上跑出 85% 准确率不等于可以直接交给医生。烧伤检测的代价是非对称的漏掉一个深Ⅱ度比多报一个浅Ⅱ度危险得多。所以我最后都会再加一道不确定性过滤当模型对某个样本的预测概率低于阈值时输出“无法判断”。这个设计在医疗场景中叫“拒判”比硬着头皮猜一个结果更安全。probs torch.softmax(logits, dim1) conf, pred torch.max(probs, dim1) # 低于 0.7 直接交给人工复核 if conf.item() 0.7: pred -1这个技巧对深Ⅱ度和Ⅲ度的边界尤其有用因为这两类的视觉相似度天然高强行细分不如主动承认不确定。阈值 0.7 是我常用起点你可以画一条“阈值—准确率”曲线来决定让漏诊率足够低时保留多少样本给人工。我现在的习惯是接触任何一份皮肤烧伤检测源码到手第一件事不是看模型结构而是跑两个 20 行以内的脚本一个确认过拟合单个 batch另一个打印验证集的 confusion matrix。两个都不超过十分钟但能挡掉后面大半的无效调试。这个顺序帮我躲过不止一次返工希望你也能在复现这个项目时少走一段弯路希望帮到你。本文还有配套的精品资源点击获取