简介:本资源面向计算机视觉方向的毕业设计学生与深度学习入门者,提供一套基于Transformer的皮肤病变图像语义分割完整方案,帮助解决医学图像中细微差异与复杂纹理难以精确分割的问题。压缩包共约2000个文件,整体59.39MB,以5447张jpg皮肤病变图像及对应标注为主,辅以20个py训练与推理脚本、2个pth预训练权重、yml配置文件、m与txt说明文档等,覆盖数据预处理、模型搭建、训练调优到结果可视化的完整链路。项目在编码器-解码器结构中融合CNN与分层注意力机制,可输出IoU、Precision、Recall等评估指标,并对比真实标注与预测结果。目前已有1055人学习下载,适合希望深入掌握Transformer在图像语义分割领域应用、快速完成毕业设计或医学图像分析实践的读者参考。
1. 从一堆皮肤镜图片说起:这套 Transformer 语义分割毕设到底能跑出什么
如果你手里正躺着一批皮肤镜图像,文件名是 0103.jpg、0437.jpg、0116.jpg 这种纯数字编号,同时导师催着要一份能演示、能写论文、能答辩的语义分割毕设,那这套「基于 Transformer 的皮肤病变分割」资源就是冲这个场景来的。它把医学图像里最典型的像素级二分类任务——把病灶区域从正常皮肤里抠出来——用 Transformer 架构做了一遍完整实现,压缩包名叫 Medical-Transformer-Improvement-master,里面按摘要描述应该包含源码、数据集、配置文件、评估脚本和可视化结果。适合谁?适合已经会 PyTorch 基础、想找一个能直接改、能复现指标、能往论文里塞对比实验的毕业设计选题的人。不适合谁?完全没碰过深度学习、指望双击就跑出论文的人,这套东西你得先补编码器-解码器结构和注意力机制的基本概念。
2. Transformer 做分割的选型逻辑:为什么不用纯 U-Net 而要上注意力
2.1 卷积的局部性与皮肤病变的边界模糊问题
皮肤病变分割和遥感、街景分割最大的区别在于:病灶边界不是硬边缘,而是从色素沉着到正常皮肤的渐变过渡。纯 CNN 分割网络(比如经典 U-Net)靠堆叠 3×3 卷积扩大感受野,但卷积核天生只看局部邻域,要覆盖整张 512×512 皮肤镜图像的全域上下文,得下采样到 1/16 甚至 1/32 分辨率,这时候小病灶的边界信息已经丢得差不多了。我实际跑过几组对比,U-Net 在 ISIC 类数据集上 Dice 能到 0.85 左右,但边界区域的假阳性明显偏多,尤其是病灶边缘有毛发遮挡或者颜色过渡带的时候。
Transformer 的自注意力机制不一样,它从第一层就能让任意两个像素建立关联,全局感受野是天然属性,不需要靠深度堆叠去换。这就是为什么摘要里提到「捕捉更丰富的上下文信息」——对于皮肤病变这种边界依赖全局纹理判断的任务,注意力图能同时看到病灶中心和周围正常皮肤的对比关系,边界回归会更稳。
2.2 编码器-解码器里 CNN 和 Transformer 怎么混
纯 ViT 做分割有个硬伤:patch embedding 把图像切成 16×16 的块,块内细节直接丢了,而医学图像恰恰对像素级细节敏感。所以这套资源大概率采用的是混合结构——编码器前端用几层卷积做浅层特征提取和降采样,中间插入 Transformer block 做全局建模,解码器再用转置卷积或者插值上采样恢复分辨率。常见做法是参考 TransUNet 或 Swin-UNet 的骨架,前者是 CNN 提特征 + Transformer 编码 + CNN 解码,后者是纯 Swin Transformer 加 U 形结构。
你拿到代码后第一件事是看 model 定义文件里 forward 函数的张量流向,确认三件事:patch size 是多少(常见 16 或 4)、注意力是全局还是窗口(Swin 是窗口)、跳跃连接从哪几层引到解码器。这三个参数直接决定显存占用和最终指标。
2.3 环境搭建与依赖确认
先别急着训练,把环境跑通。我一般用 conda 建独立环境,避免和系统里的 CUDA 版本打架。
conda create -n medseg python=3.8 -y conda activate medseg # PyTorch 版本要和你的 CUDA 驱动匹配,30 系卡建议 1.12+ pip install torch==1.12.1 torchvision==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu113 pip install numpy opencv-python pillow matplotlib scikit-learn tensorboard tqdm装完之后跑一句python -c "import torch; print(torch.cuda.is_available())",返回 True 才算 GPU 可用。如果返回 False,先查驱动版本和 CUDA 版本对不对得上,别硬跑 CPU,皮肤病变数据集虽然不大,但 Transformer 在 CPU 上训一个 epoch 能让你等到怀疑人生。
2.4 数据集的目录结构与标注格式核对
摘要里说数据集分训练/验证/测试,图像是 jpg,标注大概率是 png 掩码(0 背景、255 病灶)或者 npy 数组。你拿到后先写个脚本统计一下图像尺寸和掩码像素分布,确认没有全黑或者全白的脏标注。
import os import cv2 import numpy as np img_dir = "data/train/images" mask_dir = "data/train/masks" for name in sorted(os.listdir(img_dir))[:5]: img = cv2.imread(os.path.join(img_dir, name)) mask = cv2.imread(os.path.join(mask_dir, name.replace(".jpg", ".png")), 0) # 统计掩码中前景像素占比,低于 1% 或高于 90% 的要警惕 fg_ratio = (mask > 127).sum() / mask.size print(f"{name} | img shape: {img.shape} | fg ratio: {fg_ratio:.4f}")这段代码的作用是抽样检查图像和掩码是否对齐、前景占比是否合理。参数说明:mask > 127是二值化阈值,如果你的标注是 0/1 存储就改成> 0.5。前景占比低于 1% 说明病灶极小,训练时正负样本严重失衡,后面损失函数得用 Dice Loss 或者 Focal Loss 来压;高于 90% 可能是标注反了或者图像本身有问题。
3. 训练脚本拆解:从数据增强到损失函数怎么配
3.1 皮肤镜图像的数据增强策略
医学图像数据量通常不大,皮肤病变公开数据集也就几千张,不做增强很容易过拟合。但皮肤镜图像的增强有讲究——水平翻转、垂直翻转、90 度旋转是安全的,因为病灶没有方向性;颜色抖动要谨慎,因为颜色是诊断依据之一,抖动幅度大了会改变病灶的色素特征。我一般用 albumentations 库,配置如下:
import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.Resize(256, 256), # 统一分辨率,Transformer 对输入尺寸敏感 A.HorizontalFlip(p=0.5), # 水平翻转,安全 A.VerticalFlip(p=0.5), # 垂直翻转,安全 A.RandomRotate90(p=0.5), # 90 度旋转,安全 A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=15, p=0.5), A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.3), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2(), ])逻辑说明:Resize 到 256 是平衡显存和精度的常见选择,如果你的卡显存够(12G 以上),可以上 384 或 512,Transformer 对分辨率更敏感。ShiftScaleRotate 的幅度我压得比较小,因为皮肤镜图像通常病灶居中,大幅平移旋转会引入无意义的黑色填充区域。Normalize 用的是 ImageNet 统计量,如果你从零训练可以用数据集自身的均值和方差,但用预训练权重的话必须和预训练时一致。
3.2 损失函数:Dice + BCE 的组合逻辑
皮肤病变分割是典型的类别不平衡任务,背景像素远多于病灶像素。纯 BCE 会让模型倾向于全预测背景,Dice 能直接优化重叠度但对小目标梯度不稳定。常见做法是两者加权相加:
import torch import torch.nn as nn class DiceBCELoss(nn.Module): def __init__(self, weight=0.5): super().__init__() self.weight = weight self.bce = nn.BCEWithLogitsLoss() def forward(self, pred, target): # pred: (B, 1, H, W) logits, target: (B, 1, H, W) 0/1 bce_loss = self.bce(pred, target) pred_sigmoid = torch.sigmoid(pred) intersection = (pred_sigmoid * target).sum(dim=(2, 3)) union = pred_sigmoid.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) dice_loss = 1 - (2 * intersection + 1e-6) / (union + 1e-6) return self.weight * bce_loss + (1 - self.weight) * dice_loss.mean()参数说明:weight=0.5是 BCE 和 Dice 的平衡系数,如果验证集上边界假阳性多,把 weight 降到 0.3 让 Dice 主导;如果训练初期 loss 震荡大,把 weight 提到 0.7 让 BCE 稳住梯度。1e-6是平滑项,防止分母为零。注意 pred 是 logits,不要在外面再过一遍 sigmoid,否则 BCEWithLogitsLoss 内部会重复计算导致数值不稳定。
3.3 学习率调度与优化器选择
Transformer 类模型对学习率很敏感,太大直接发散,太小收敛慢。我一般用 AdamW 配余弦退火,初始学习率 1e-4,weight decay 1e-4。如果加载了预训练编码器,编码器部分学习率设小 10 倍,解码器用正常学习率,这叫分层学习率,能防止预训练权重被快速破坏。
from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR # 假设 model 有 encoder 和 decoder 两个属性 optimizer = AdamW([ {"params": model.encoder.parameters(), "lr": 1e-5}, {"params": model.decoder.parameters(), "lr": 1e-4}, ], weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6)T_max 设成总 epoch 数,eta_min 是学习率下限。如果你发现训练到 30 epoch 后验证指标不升反降,大概率是过拟合了,早点停或者加 dropout。
3.4 训练循环里必须监控的三个量
别只盯着 loss 看,我习惯每个 epoch 记录 train loss、val Dice、val IoU 三个量。Dice 和 IoU 的关系是 Dice = 2*IoU/(1+IoU),监控一个就行,但两个都打出来方便和论文里的对比方法对齐口径。
def compute_dice(pred, target, threshold=0.5): pred_bin = (torch.sigmoid(pred) > threshold).float() intersection = (pred_bin * target).sum() return (2 * intersection + 1e-6) / (pred_bin.sum() + target.sum() + 1e-6) # 训练循环片段 for epoch in range(num_epochs): model.train() for img, mask in train_loader: img, mask = img.cuda(), mask.cuda() optimizer.zero_grad() out = model(img) loss = criterion(out, mask) loss.backward() optimizer.step() scheduler.step() # 验证阶段 model.eval() with torch.no_grad(): val_dice = compute_dice(model(val_img), val_mask) print(f"Epoch {epoch} | Loss {loss.item():.4f} | Val Dice {val_dice:.4f}")threshold=0.5 是二值化阈值,如果验证时发现预测掩码偏小,降到 0.4;偏大就升到 0.6。这个阈值在测试阶段可以调,但论文里报告指标时要用固定值,不能针对测试集调参。
4. 避坑与排查:这套代码跑不起来时先查这五处
4.1 显存溢出但 batch size 已经降到 1
现象:RuntimeError: CUDA out of memory,即使 batch_size=1 也爆。原因:Transformer 的注意力矩阵是 O(N²) 复杂度,输入分辨率 512×512 时 patch 数量是 1024,注意力矩阵就是 1024×1024,显存占用随分辨率平方增长。解决:先把输入降到 256×256 跑通,确认模型能训之后再逐步升分辨率;或者改用 Swin Transformer 的窗口注意力,复杂度降到线性。另外检查有没有在验证阶段忘了torch.no_grad(),验证图不反传但前向激活值照样占显存。
4.2 损失降到 0.1 以下但 Dice 只有 0.3
现象:训练 loss 看着很低,验证 Dice 惨不忍睹。原因:类别极度不平衡时 BCE 会被背景像素主导,模型学会全预测背景就能把 loss 压得很低。解决:确认损失函数里 Dice 的权重够不够,把 weight 调到 0.3 让 Dice 主导;同时检查数据加载时掩码有没有被错误归一化到 0-1 之外,或者图像和掩码没对齐(比如一个做了 resize 另一个没做)。
4.3 验证集指标比训练集还高
现象:val Dice 0.88,train Dice 0.82。原因:常见于数据增强只加在训练集、验证集用原图,而训练集增强后的图像分布和验证集差异大,模型在验证集上反而「见得多」。另一个可能是验证集太小,几千张里抽 200 张,随机波动大。解决:把验证集扩到至少 500 张,或者做 5 折交叉验证取平均;检查增强 pipeline 里有没有 Normalize 参数不一致的情况。
4.4 预测结果全是灰色或者全黑
现象:可视化出来的掩码要么全 0 要么全 1。原因:模型输出 logits 没经过 sigmoid 就直接二值化了,或者可视化时把 0-1 浮点当 0-255 显示。解决:可视化前先torch.sigmoid(out)再乘 255 转 uint8;检查测试脚本里有没有漏掉 sigmoid。如果是全黑,看模型最后一层有没有加激活函数,有些实现把 sigmoid 放在损失函数里,推理时忘了补。
4.5 加载预训练权重报 key 不匹配
现象:load_state_dict 报 Missing keys 或 Unexpected keys。原因:预训练模型的主干命名和你当前模型定义不一致,比如预训练用backbone.layer1,你的代码写的是encoder.conv1。解决:打印两边 state_dict 的 key 列表对比,用strict=False先加载能匹配的部分,再手动映射剩余层。如果差异太大,考虑用 timm 库加载标准预训练权重,它的命名规范比较统一。
5. 进阶技巧:用测试时增强把 Dice 再抬两个点
训练完模型别急着写论文,测试时增强(TTA)是一个几乎零成本涨点的技巧。原理很简单:对同一张测试图做多次几何变换(水平翻转、垂直翻转、旋转),分别推理后再把结果逆变换回原空间取平均。因为模型对翻转后的图像预测会有细微差异,平均能抵消一部分随机误差。
def tta_predict(model, img_tensor): """img_tensor: (1, 3, H, W) 已归一化""" model.eval() preds = [] with torch.no_grad(): # 原始 preds.append(torch.sigmoid(model(img_tensor))) # 水平翻转 preds.append(torch.flip(torch.sigmoid(model(torch.flip(img_tensor, [3]))), [3])) # 垂直翻转 preds.append(torch.flip(torch.sigmoid(model(torch.flip(img_tensor, [2]))), [2])) # 旋转 90 度 rotated = torch.rot90(img_tensor, 1, [2, 3]) pred_rot = torch.sigmoid(model(rotated)) preds.append(torch.rot90(pred_rot, -1, [2, 3])) return torch.stack(preds, dim=0).mean(dim=0)逻辑说明:每次变换后推理,再把预测结果做逆变换回原坐标系,最后在 batch 维度取平均。参数说明:torch.flip的 dims 参数 [3] 是宽方向,[2] 是高方向;torch.rot90的 k=1 是逆时针 90 度,逆变换用 k=-1。注意 TTA 只用在测试阶段,训练时不要用,否则显存和时间开销翻几倍。
我实测下来,在皮肤病变分割任务上 TTA 一般能涨 1.5 到 2.5 个 Dice 点,代价是推理时间变成 4 倍。如果答辩演示要求实时性,可以只保留水平翻转这一路,涨点约 1 个点,时间只多一倍。
还有一个容易被忽略的点:推理分辨率。训练时用 256×256,测试时可以用 320×320 或者 384×384,Transformer 对分辨率变化的鲁棒性比 CNN 好,适当提高推理分辨率往往能改善小病灶的边界。但别直接上 512,除非你确认显存够且训练时也用过类似尺度。
从那以后我每次交毕设代码前,都会强制走一遍「换机器复现」流程——把代码拷到另一台没配过环境的机器上,从建 conda 环境开始跑通训练和推理,确认没有隐式的本地路径依赖和版本锁定。这套 Medical-Transformer-Improvement 的资源结构比较完整,但医学图像分割的坑往往不在模型本身,而在数据管线和评估口径上,把这两块对齐了,论文里的对比实验才站得住。希望帮到你。
本文还有配套的精品资源,点击获取