ARTICLE DETAIL

资讯详情

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

Transformer语义分割实战:皮肤病变分割毕设全流程与代码

Transformer语义分割实战:皮肤病变分割毕设全流程与代码 简介本资源面向计算机视觉方向的本科与硕士毕业生提供一套基于Transformer的皮肤病变图像语义分割完整毕业设计实现方案帮助解决医学图像中细微差异与复杂纹理难以精确分割的问题。压缩包共约2000个文件整体59.39MB以5447张jpg皮肤病变图像及像素级标注为主体辅以20个py训练与推理脚本、2个pth预训练权重、yml配置文件、m与txt说明文档并附有评估指标与结果可视化代码覆盖数据预处理、模型搭建、训练调优到性能评估的完整链路。项目在编码器-解码器结构中融合CNN与分层注意力机制可捕捉更丰富的上下文信息提升IoU、Precision、Recall等指标表现。目前已有1055人学习下载适合希望深入掌握Transformer在医学图像分割中应用、需要完整代码与数据集快速开展课题的读者参考。1. 从一张皮肤镜照片说起Transformer 语义分割到底在解决什么问题皮肤镜拍下一张痣的照片医生要判断的是「这块色素区域边界在哪、是不是恶性」。传统做法靠肉眼勾边同一张图两个医生能画出两条不一样的线。语义分割要干的事就是让模型对每个像素打标签把病灶区域从背景皮肤里抠出来。而基于 Transformer 的语义分割是这两年被反复提起的路线——它不再只靠卷积核一层层堆局部感受野而是用自注意力直接建模任意两个像素之间的关系边界贴合度往往比纯 CNN 更稳。这个标题对应的是一套完整的毕业设计级方案用 Transformer 系语义分割网络在皮肤病变数据集上训练、评估、出图附带可运行代码和数据集组织方式。适合两类人一是要交毕设、需要跑通全流程的学生二是想从 U-Net 切到 Transformer 分割、但不知道从哪下手的一线开发者。下面按「数据怎么摆 → 模型怎么搭 → 训练怎么调 → 坑在哪 → 怎么验证」推一遍。2. 皮肤病变数据集怎么组织从 ISIC 原始图到可训练掩码2.1 为什么皮肤病变分割对数据格式特别敏感皮肤病变分割的公开数据里ISIC 系列是最常被拿来用的。原始压缩包里通常是三样东西ISIC_xxxxxxx.jpg原图、ISIC_xxxxxxx_segmentation.png二值掩码、一个metadata.csv记录病灶类型。掩码里病灶是白色像素值 255背景是黑色0。很多人第一次跑翻车不是模型问题是掩码读进来之后没做二值化——PNG 带抗锯齿边缘像素值可能是 254、128 这种灰阶直接当类别标签用loss 会算得莫名其妙。常见做法是统一转成 0/1 的uint8并且把原图和掩码按同一随机种子做同步增强。这里给一个最小可用的数据组织脚本把散落的原图和掩码整理成images/和masks/两个平行目录import os import shutil import numpy as np from PIL import Image RAW_DIR ./raw_isic # 解压后的原始目录 OUT_IMG ./dataset/images OUT_MSK ./dataset/masks os.makedirs(OUT_IMG, exist_okTrue) os.makedirs(OUT_MSK, exist_okTrue) for name in os.listdir(RAW_DIR): if name.endswith(.jpg): stem name[:-4] mask_name stem _segmentation.png mask_path os.path.join(RAW_DIR, mask_name) if not os.path.exists(mask_path): continue # 没有对应掩码的图直接跳过 # 掩码二值化127 视为病灶 m np.array(Image.open(mask_path).convert(L)) m (m 127).astype(np.uint8) * 255 Image.fromarray(m).save(os.path.join(OUT_MSK, stem .png)) shutil.copy(os.path.join(RAW_DIR, name), os.path.join(OUT_IMG, stem .jpg)) print(done)逻辑说明遍历原图找同名掩码读成灰度图后按 127 阈值二值化再乘 255 存回 PNG保证掩码只有 0 和 255 两个值。参数上阈值 127 是经验值如果掩码边缘毛刺多可以调到 100 让病灶区域略大一点宁可多包一点也别漏掉边界。原图和掩码用同一个stem命名是为了后面 Dataset 里能靠文件名直接配对省掉维护一张映射表的麻烦。2.2 划分训练验证集时最容易忽略的类别泄漏皮肤病变数据有个隐蔽问题同一个人可能有多张不同角度的照片如果随机按图划分同一个病灶的图可能同时进训练集和验证集验证指标会虚高。稳妥做法是按病灶 ID 或患者 ID 分组划分而不是按图片随机划分。metadata 里一般有lesion_id字段用它做 group split。import pandas as pd from sklearn.model_selection import GroupShuffleSplit meta pd.read_csv(./raw_isic/metadata.csv) # 只保留有掩码的样本 meta meta[meta[image].apply( lambda x: os.path.exists(f./dataset/masks/{x[:-4]}.png))] gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(meta, groupsmeta[lesion_id])) meta.iloc[train_idx][[image]].to_csv(./dataset/train.csv, indexFalse) meta.iloc[val_idx][[image]].to_csv(./dataset/val.csv, indexFalse)参数说明test_size0.2是验证集比例毕设场景数据量通常几千张0.2 够评估稳定性random_state固定住保证每次划分一致方便复现。groups传lesion_id同一个病灶的所有图只会落在一边。这一步不做后面 Dice 冲到 0.95 也别高兴大概率是泄漏。3. Transformer 分割网络怎么搭以 SegFormer 思路写一个最小实现3.1 为什么选分层 Transformer 而不是原版 ViT原版 ViT 把图切成 16×16 的 patch全程保持同一分辨率做分割时上采样回来边界很糊。语义分割要的是多尺度特征所以主流做法是分层结构像 Swin、SegFormer 那样stage 越深 patch 越大、通道越多最后把四个 stage 的特征统一上采样融合。皮肤病变目标尺度变化大小痣和大面积病灶都有分层结构比单尺度更扛得住。下面写一个精简版的分层 Transformer 编码器用 PyTorch 实现重点是讲清 patch embedding 和自注意力的形状变化不追求堆到 SOTA。import torch import torch.nn as nn class PatchEmbed(nn.Module): 把 HxW 图切成 patch映射到 embed_dim def __init__(self, in_ch3, embed_dim64, patch4): super().__init__() self.proj nn.Conv2d(in_ch, embed_dim, kernel_sizepatch, stridepatch) self.norm nn.LayerNorm(embed_dim) def forward(self, x): x self.proj(x) # B, C, H/p, W/p B, C, H, W x.shape x x.flatten(2).transpose(1, 2) # B, N, C return self.norm(x), (H, W) class SelfAttention(nn.Module): def __init__(self, dim, heads4): super().__init__() self.heads heads self.scale (dim // heads) ** -0.5 self.qkv nn.Linear(dim, dim * 3) self.proj nn.Linear(dim, dim) def forward(self, x): B, N, C x.shape qkv self.qkv(x).reshape(B, N, 3, self.heads, C // self.heads) q, k, v qkv.permute(2, 0, 3, 1, 4) # 各 B, heads, N, C/heads attn (q k.transpose(-2, -1)) * self.scale attn attn.softmax(dim-1) out (attn v).transpose(1, 2).reshape(B, N, C) return self.proj(out)逻辑说明PatchEmbed用 stride 等于 kernel 的卷积完成切块和线性映射比手动 unfold 快输出 reshape 成B, N, C才能喂给注意力。SelfAttention里scale是 1/sqrt(head_dim)防止点积过大导致 softmax 梯度消失这是 Transformer 的标配。heads4是毕设显存下的稳妥值显存够可以上 8。注意qkv一次线性出三份再切分比写三个 Linear 省显存。3.2 解码器怎么把 Transformer 特征还原成像素级掩码编码器给的是序列特征解码器要把它变回 H×W 的分割图。最省事的做法是把序列 reshape 回二维过几层卷积上采样最后 1×1 卷积输出单通道 logits。如果做多 stage 融合就把每个 stage 的特征都上采样到原图 1/4 大小再拼接。class SegHead(nn.Module): def __init__(self, dim64, num_classes1): super().__init__() self.up nn.Sequential( nn.ConvTranspose2d(dim, dim // 2, 2, stride2), nn.BatchNorm2d(dim // 2), nn.ReLU(inplaceTrue), nn.ConvTranspose2d(dim // 2, dim // 4, 2, stride2), nn.BatchNorm2d(dim // 4), nn.ReLU(inplaceTrue), ) self.cls nn.Conv2d(dim // 4, num_classes, 1) def forward(self, x, hw): B, N, C x.shape H, W hw x x.transpose(1, 2).reshape(B, C, H, W) x self.up(x) # 上采样 4 倍回到原图分辨率 return self.cls(x) # B, 1, H*4, W*4参数说明两次ConvTranspose2dstride2正好把 patch4 带来的 1/4 分辨率还原回原图。num_classes1是二分类分割输出 logits 配BCEWithLogitsLoss如果做多类病灶改成类别数并换CrossEntropyLoss。上采样用转置卷积而不是直接插值是为了让网络自己学怎么补边界细节代价是可能产生棋盘格伪影介意的话换成nn.Upsample加普通卷积。4. 训练参数怎么设皮肤病变分割的 loss、学习率与显存权衡4.1 Dice BCE 组合 loss 为什么比单 BCE 稳皮肤病变区域通常只占整图一小块背景像素远多于病灶纯 BCE 会让模型倾向于全预测背景也能拿到很低的 loss结果就是掩码全黑。Dice loss 直接优化预测和真值的重叠度对类别不平衡不敏感。常见做法是两者加权相加BCE 管像素级稳定Dice 管区域重叠。class DiceBCELoss(nn.Module): def __init__(self, bce_weight0.5): super().__init__() self.bce nn.BCEWithLogitsLoss() self.bce_weight bce_weight def forward(self, logits, targets): bce self.bce(logits, targets) probs torch.sigmoid(logits) probs probs.flatten(1) targets targets.flatten(1) inter (probs * targets).sum(dim1) dice 1 - (2 * inter 1e-6) / (probs.sum(1) targets.sum(1) 1e-6) return self.bce_weight * bce (1 - self.bce_weight) * dice.mean()参数说明bce_weight0.5是起点如果训练早期掩码大面积塌成全黑把它降到 0.3 让 Dice 主导如果边界抖动厉害升到 0.7 让 BCE 稳住像素。1e-6是防止分母为零的平滑项别省。flatten(1)把空间维压平按样本算 Dice 再平均比整批一起算更公平。4.2 学习率、batch size 与显存的实际取值Transformer 分割网络比同规模 CNN 更吃显存因为自注意力是 N² 复杂度。毕设常见的单卡 8G 或 12G 显存下输入 256×256、patch4 时 N4096注意力矩阵 4096×4096 已经不小。稳妥配置是 batch size 4 到 8学习率用 AdamW 从 6e-5 起配 cosine 退火。参数推荐值调整方向输入尺寸256×256显存不够降到 224别低于 192batch size4~8显存爆了先降它别先降分辨率初始学习率6e-5不收敛降到 3e-5震荡升到 1e-4weight decay0.01Transformer 标配别设 0训练轮数80~120看验证 Dice 平台期别死磕预热轮数5前几轮线性升温防早期发散学习率用 warmup 很关键Transformer 对初始学习率敏感前 5 轮从 0 线性升到 6e-5能明显减少早期 loss 爆炸。优化器选 AdamW 而不是 Adam是因为权重衰减解耦后对 Transformer 更友好这是踩过坑才知道的。5. 训练与推理常见问题排查皮肤病变分割的 5 个血泪坑5.1 掩码全黑或全白现象训练几个 epoch 后验证集预测出来的掩码要么整张黑要么整张白Dice 卡在 0.1 以下不动。原因类别极度不平衡加上 loss 权重没调好模型找到了「全预测背景」这个局部最优。解决把 Dice loss 权重提上来bce_weight降到 0.3同时检查掩码二值化是不是反了——有的数据集病灶是黑底白字有的是白底黑字读进来先np.unique看一眼像素值分布。5.2 验证 Dice 高得离谱现象验证集 Dice 0.97但拿新图一测惨不忍睹。原因按图随机划分导致同一病灶泄漏或者验证集做了和训练集一样的强增强比如随机翻转后掩码没同步。解决按lesion_id分组划分验证集只做 resize 和归一化不做随机翻转旋转增强时原图和掩码必须用同一个随机参数。5.3 显存溢出但 batch 已经很小现象batch 降到 2 还是 OOM。原因注意力矩阵随分辨率平方增长输入 512×512 时 N16384注意力矩阵直接爆。解决先把输入降到 256或者改用带窗口注意力的结构把全局注意力限制在局部窗口内再不行就上梯度累积用 batch 1 累积 8 步模拟大 batch。5.4 边界毛刺、分割区域不贴合现象病灶主体分对了但边缘一圈锯齿Dice 上不去。原因上采样用最近邻插值或者解码器太浅没学到边界细节。解决上采样换成转置卷积或双线性插值加卷积在 loss 里加边界加权对掩码边缘像素给更高权重训练后期把学习率降下来精修边界。5.5 推理速度慢到没法演示现象单张图推理要好几秒答辩演示卡顿。原因没切 eval 模式、没关梯度或者模型还在 GPU 上做无用计算。解决推理时model.eval()加torch.no_grad()把模型转成半精度model.half()输入尺寸按需降演示用 256 足够。这几步下来单张通常能压到百毫秒级。6. 怎么验证这套方案真的能用Dice、IoU 与可视化对照训练跑完不能只看一个 loss 数字得用分割指标加可视化双重验证。Dice 和 IoU 是最常用的两个Dice 对重叠更敏感IoU 更严格。下面这段评估代码按图算指标再平均避免整批混算带来的偏差。torch.no_grad() def evaluate(model, loader, device): model.eval() dice_list, iou_list [], [] for imgs, masks in loader: imgs imgs.to(device) logits model(imgs) pred (torch.sigmoid(logits) 0.5).float() for p, m in zip(pred, masks): p, m p.flatten(), m.flatten().to(device) inter (p * m).sum() dice (2 * inter 1e-6) / (p.sum() m.sum() 1e-6) iou (inter 1e-6) / (p.sum() m.sum() - inter 1e-6) dice_list.append(dice.item()) iou_list.append(iou.item()) return sum(dice_list) / len(dice_list), sum(iou_list) / len(iou_list)参数说明阈值 0.5 是二分类默认如果发现漏检多就降到 0.4误检多就升到 0.6这个阈值要在验证集上试出来别拍脑袋。1e-6同样防除零。按图循环而不是整批算是因为每张图病灶大小差异大整批混算会被大病灶主导。可视化对照更直观把原图、真值掩码、预测掩码三张并排存成一张图肉眼扫一遍就能发现模型是不是在「猜」。我一般会挑 Dice 最低的 10 张单独看翻车往往集中在毛发遮挡、低对比度、病灶贴边这几种情况。如果这几类占比高说明数据增强里该加毛发模拟和对比度扰动而不是继续调学习率。最后说个习惯每次改完数据划分或 loss先把随机种子固定跑一遍小规模过拟合测试——拿 20 张图训练看能不能把训练 Dice 干到 0.99。过拟合都上不去说明网络或数据管道有 bug别急着上全量数据烧卡。这套流程走下来毕设的完整代码和数据集组织基本就立住了剩下的就是耐心调。希望帮到你。本文还有配套的精品资源点击获取
返回列表