ARTICLE DETAIL

资讯详情

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

基于Transformer的皮肤病变分割毕业设计:Swin-UNet实战与优化

基于Transformer的皮肤病变分割毕业设计:Swin-UNet实战与优化 简介本资源面向计算机视觉方向的毕业设计学生与深度学习入门者提供一套基于Transformer的语义分割完整实现方案重点解决皮肤病变区域的像素级分割问题适用于医学图像分析场景。压缩包共约2000个文件整体59.39MB以5447张jpg皮肤病变图像及对应标注为主辅以20个py训练与推理脚本、2个pth预训练权重、yml配置文件、m文件及说明文档覆盖数据预处理、模型搭建、训练调参与结果可视化全流程。项目在编码器-解码器结构中融合卷积神经网络与分层注意力机制用于捕捉皮肤图像中的细微纹理与多尺度上下文信息并配有IoU、Precision、Recall等评估代码。目前已有1055人学习下载读者可据此掌握Transformer在医学语义分割中的落地方法完成从数据组织到模型评估的完整实践适合作为毕业设计参考与二次开发基础。1. 皮肤病变分割这个选题为什么Transformer比U-Net更值得做毕业设计如果你正在找毕业设计题目看到“基于Transformer实现语义分割”这个方向大概率已经翻过不少U-Net的教程了。皮肤病变分割是医学图像分割里最经典的入门场景之一——ISIC数据集公开可用任务定义清晰把病灶区域从皮肤背景里抠出来评价指标成熟Dice、IoU。但问题在于如果你只拿U-Net跑一遍ISIC答辩时老师一句“创新点在哪”就能把你问住。Transformer架构引入语义分割之后这个局面变了。Vision Transformer和Swin Transformer把自注意力机制带进了密集预测任务在皮肤病变这种边界模糊、形状不规则、对比度低的场景里全局建模能力确实比纯卷积网络有优势。更关键的是这套方案有完整的代码路径可以复现数据集也是现成的不需要你自己标注。适合的人群很明确有一定PyTorch基础、想做医学图像方向、希望毕业设计有技术纵深而不是调包了事的本科生或低年级研究生。2. 从ISIC数据集到Transformer分割网络选型与预处理2.1 为什么皮肤病变分割适合用Transformer打底皮肤病变分割的核心难点不在“分割”本身而在病灶边界的模糊性。黑色素瘤的边缘往往和周围正常皮肤渐变过渡卷积核的局部感受野在这种场景下容易丢失全局上下文。U-Net通过跳跃连接缓解了一部分问题但本质上仍然是在局部窗口内做特征提取。Transformer的自注意力机制天然适合处理这种“需要看全局才能判断局部”的任务。每一层注意力都在计算所有patch之间的关系病灶区域和远端的皮肤纹理可以建立直接关联。Swin Transformer进一步引入了窗口注意力和移位窗口机制把计算复杂度从平方级降到线性级同时保留了跨窗口的信息交换能力。这就是为什么近两年的医学图像分割论文里Swin-UNet、TransUNet这类混合架构频繁出现。对于毕业设计来说选Swin Transformer作为编码器、配合一个轻量解码器是比较稳妥的方案。纯ViT做分割需要大量数据增强和长训练周期Swin的归纳偏置更强收敛更快。2.2 ISIC数据集下载与目录结构整理ISICInternational Skin Imaging Collaboration数据集是皮肤病变分割的标准公开数据。常见做法是从ISIC Archive下载2016到2018年的任务数据包含输入图像和对应的二值mask。原始数据解压后通常是这样的结构ISIC2018/ ├── ISIC2018_Task1-2_Training_Input/ │ ├── ISIC_0000000.jpg │ ├── ISIC_0000001.jpg │ └── ... ├── ISIC2018_Task1_Training_GroundTruth/ │ ├── ISIC_0000000_segmentation.png │ ├── ISIC_0000001_segmentation.png │ └── ... └── ISIC2018_Task1-2_Validation_Input/ └── ...我一般会先写一个脚本把数据整理成统一的训练/验证划分同时检查图像和mask的配对关系。下面这段代码做三件事遍历输入目录、匹配对应的分割mask、按8:2划分训练集和验证集并写入txt文件。import os import random from pathlib import Path # 配置路径 data_root Path(ISIC2018) image_dir data_root / ISIC2018_Task1-2_Training_Input mask_dir data_root / ISIC2018_Task1_Training_GroundTruth output_dir Path(splits) output_dir.mkdir(exist_okTrue) # 收集所有图像文件名不含扩展名 image_files sorted([f.stem for f in image_dir.glob(*.jpg)]) # 检查每个图像是否有对应的mask paired [] for name in image_files: mask_path mask_dir / f{name}_segmentation.png if mask_path.exists(): paired.append(name) else: print(f警告{name} 没有对应的分割mask已跳过) print(f有效配对数量{len(paired)}) # 随机打乱后按8:2划分 random.seed(42) random.shuffle(paired) split_idx int(len(paired) * 0.8) train_list paired[:split_idx] val_list paired[split_idx:] # 写入文件 with open(output_dir / train.txt, w) as f: f.write(\n.join(train_list)) with open(output_dir / val.txt, w) as f: f.write(\n.join(val_list)) print(f训练集{len(train_list)}验证集{len(val_list)})这段代码的关键参数是random.seed(42)固定随机种子保证每次划分结果一致方便复现。split_idx控制划分比例8:2是医学图像分割里比较常用的配置数据量少的时候可以调到7:3。输出的是纯文件名列表后续Dataset类直接读取这个txt来加载数据。注意ISIC数据集的mask是PNG格式像素值只有0和255加载后需要除以255归一化到0-1之间。图像本身是RGB三通道尺寸不统一需要在Dataset里统一resize。2.3 数据增强策略皮肤病变场景下哪些增强真正有用皮肤病变图像的数据增强不能照搬自然图像那套。颜色抖动要慎用因为病灶的颜色本身就是重要特征随机裁剪可以用但要注意保持病灶在裁剪窗口内的比例。我一般会用这几类增强水平翻转和垂直翻转是安全的皮肤病变没有固定的方向性。随机旋转90度也常用。颜色方面轻微的亮度对比度调整可以但幅度要小。Cutout和GridMask这类遮挡增强在皮肤病变上效果不稳定因为病灶区域被遮挡后模型可能学到错误的边界信息。下面是一个基于Albumentations的增强管道配置import albumentations as A from albumentations.pytorch import ToTensorV2 # 训练集增强 train_transform A.Compose([ A.Resize(256, 256), # 统一尺寸 A.HorizontalFlip(p0.5), # 水平翻转 A.VerticalFlip(p0.5), # 垂直翻转 A.RandomRotate90(p0.5), # 90度旋转 A.ShiftScaleRotate( # 平移缩放旋转 shift_limit0.1, scale_limit0.1, rotate_limit30, p0.5 ), A.RandomBrightnessContrast( # 轻微亮度对比度调整 brightness_limit0.1, contrast_limit0.1, p0.3 ), A.Normalize( # 标准化 mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ), ToTensorV2() ]) # 验证集只做resize和标准化 val_transform A.Compose([ A.Resize(256, 256), A.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ), ToTensorV2() ])Resize(256, 256)是权衡显存和精度的结果显存够可以上384。ShiftScaleRotate的shift_limit0.1表示最多平移10%的边长scale_limit0.1表示缩放范围是0.9到1.1倍。RandomBrightnessContrast的p0.3控制触发概率不要设太高。Normalize用的ImageNet均值方差如果从头训练可以改成ISIC数据集自己的统计值。3. Swin Transformer编码器加轻量解码器的完整实现3.1 模型整体结构编码器-解码器怎么搭Swin-UNet的结构思路很直接用Swin Transformer的四个stage作为编码器每个stage输出不同分辨率的特征图然后用一个对称的解码器逐步上采样并融合跳跃连接。和U-Net的区别在于编码器不再是卷积堆叠而是窗口注意力和移位窗口注意力交替的Transformer块。我一般会直接用timm库里的Swin Transformer预训练权重作为编码器初始化这样收敛快很多。解码器部分自己写保持轻量避免参数量爆炸。整体结构是这样的编码器Swin-Tiny或Swin-Small输出四个stage的特征通道数分别是96、192、384、768瓶颈层一个卷积块进一步压缩解码器四个上采样阶段每个阶段用双线性插值放大特征图然后和编码器对应stage的特征做通道拼接再经过两个3x3卷积输出层1x1卷积把通道数降到1接Sigmoid得到分割概率图下面给出编码器加载和解码器定义的代码import torch import torch.nn as nn import timm class SwinUNet(nn.Module): def __init__(self, num_classes1, pretrainedTrue): super().__init__() # 加载Swin Transformer编码器features_only模式返回四个stage的输出 self.encoder timm.create_model( swin_tiny_patch4_window7_224, pretrainedpretrained, features_onlyTrue, out_indices(0, 1, 2, 3) ) # 编码器各stage输出通道数 encoder_channels [96, 192, 384, 768] # 解码器从最深特征开始逐步上采样 self.decoder4 self._make_decoder_block(768, 384) self.decoder3 self._make_decoder_block(384, 192) self.decoder2 self._make_decoder_block(192, 96) self.decoder1 self._make_decoder_block(96, 48) # 最终输出层 self.final_conv nn.Sequential( nn.Conv2d(48, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, num_classes, kernel_size1) ) def _make_decoder_block(self, in_ch, out_ch): return nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, kernel_size3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): # 编码器前向 features self.encoder(x) # features是一个列表包含四个stage的输出 # 形状分别为 [B, 96, H/4, W/4], [B, 192, H/8, W/8], # [B, 384, H/16, W/16], [B, 768, H/32, W/32] f1, f2, f3, f4 features # 解码器逐级上采样 d4 self.decoder4(f4) d4 nn.functional.interpolate(d4, sizef3.shape[2:], modebilinear, align_cornersFalse) d4 d4 f3 # 残差连接 d3 self.decoder3(d4) d3 nn.functional.interpolate(d3, sizef2.shape[2:], modebilinear, align_cornersFalse) d3 d3 f2 d2 self.decoder2(d3) d2 nn.functional.interpolate(d2, sizef1.shape[2:], modebilinear, align_cornersFalse) d2 d2 f1 d1 self.decoder1(d2) # 最终上采样回原图尺寸 out nn.functional.interpolate(d1, sizex.shape[2:], modebilinear, align_cornersFalse) out self.final_conv(out) return outtimm.create_model的features_onlyTrue让Swin只返回特征图而不是分类logitsout_indices(0,1,2,3)指定返回四个stage的输出。解码器里用的是加法残差连接而不是通道拼接这样参数量更小显存占用低。interpolate的align_cornersFalse是PyTorch里做双线性上采样的标准配置避免边缘像素偏移。注意Swin Transformer的输入尺寸需要是patch_size的整数倍patch4_window7要求输入能被4整除。256x256是安全的如果改成其他尺寸要检查一下。3.2 损失函数选择Dice Loss加BCE的组合逻辑皮肤病变分割里正负样本极度不平衡病灶区域通常只占整张图的10%到30%。只用BCE Loss会导致模型倾向于预测全背景Dice系数上不去。常见做法是Dice Loss和BCE Loss加权组合Dice负责优化重叠区域BCE负责稳定训练梯度。class DiceBCELoss(nn.Module): def __init__(self, dice_weight0.5, bce_weight0.5): super().__init__() self.dice_weight dice_weight self.bce_weight bce_weight self.bce nn.BCEWithLogitsLoss() def forward(self, pred, target): # pred是logitstarget是0/1标签 bce_loss self.bce(pred, target) # Dice Loss计算 pred_sigmoid torch.sigmoid(pred) pred_flat pred_sigmoid.view(-1) target_flat target.view(-1) intersection (pred_flat * target_flat).sum() dice_loss 1 - (2. * intersection 1e-6) / (pred_flat.sum() target_flat.sum() 1e-6) return self.dice_weight * dice_loss self.bce_weight * bce_lossdice_weight和bce_weight各0.5是起点如果训练初期loss震荡大可以把bce_weight调到0.7。1e-6是平滑项防止分母为零。注意BCEWithLogitsLoss内部已经做了Sigmoid所以Dice部分要手动加Sigmoid两者不能混用同一个输入。3.3 训练循环与学习率调度训练配置上我一般用AdamW优化器初始学习率1e-4weight decay 1e-4。学习率调度用CosineAnnealingLR配合 warmup 前5个epoch。Batch size根据显存来256x256输入下8GB显存大概能跑8到12。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0 for images, masks in dataloader: images images.to(device) masks masks.to(device).unsqueeze(1).float() optimizer.zero_grad() outputs model(images) loss criterion(outputs, masks) loss.backward() # 梯度裁剪防止Transformer训练不稳定 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / len(dataloader) # 优化器和调度器配置 model SwinUNet(num_classes1, pretrainedTrue).to(device) optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) # Warmup 5个epoch然后Cosine退火 warmup LinearLR(optimizer, start_factor0.01, total_iters5) cosine CosineAnnealingLR(optimizer, T_max95, eta_min1e-6) scheduler SequentialLR(optimizer, schedulers[warmup, cosine], milestones[5])clip_grad_norm_的max_norm1.0是Transformer训练的常规操作防止梯度爆炸。LinearLR的start_factor0.01表示从初始学习率的1%开始线性增加到1e-4total_iters5表示5个epoch完成warmup。CosineAnnealingLR的T_max95对应剩余95个epoch总训练100轮。4. 评估指标与推理可视化Dice和IoU到底怎么看4.1 Dice、IoU、敏感度和特异度的计算方式皮肤病变分割的评估不能只看一个指标。Dice系数衡量重叠度IoU是交并比敏感度反映病灶被正确检出的比例特异度反映背景被正确排除的比例。这四个指标一起看才能判断模型是真的学到了分割能力还是只是偏向预测背景。import numpy as np def compute_metrics(pred_mask, gt_mask, threshold0.5): pred_mask: 模型输出的概率图形状[H, W] gt_mask: 真实标签形状[H, W]值为0或1 threshold: 二值化阈值 pred_binary (pred_mask threshold).astype(np.uint8) gt_binary gt_mask.astype(np.uint8) # 计算TP, FP, FN, TN tp np.sum((pred_binary 1) (gt_binary 1)) fp np.sum((pred_binary 1) (gt_binary 0)) fn np.sum((pred_binary 0) (gt_binary 1)) tn np.sum((pred_binary 0) (gt_binary 0)) # Dice dice (2 * tp) / (2 * tp fp fn 1e-6) # IoU iou tp / (tp fp fn 1e-6) # 敏感度 sensitivity tp / (tp fn 1e-6) # 特异度 specificity tn / (tn fp 1e-6) return { dice: dice, iou: iou, sensitivity: sensitivity, specificity: specificity }threshold0.5是默认二值化阈值实际推理时可以在验证集上扫一遍0.3到0.7选Dice最高的那个。1e-6同样是防止除零。这四个指标里敏感度低说明漏检多特异度低说明误检多Dice和IoU综合反映分割质量。4.2 推理结果可视化叠加原图和预测mask答辩的时候一张清晰的可视化图比一堆数字更有说服力。我一般会把原图、真实mask、预测mask和叠加结果拼成一张四格图。import matplotlib.pyplot as plt def visualize_prediction(image, gt_mask, pred_mask, save_pathNone): image: 原始RGB图像形状[H, W, 3]值范围0-255 gt_mask: 真实mask形状[H, W] pred_mask: 预测概率图形状[H, W] fig, axes plt.subplots(1, 4, figsize(16, 4)) axes[0].imshow(image) axes[0].set_title(Original Image) axes[0].axis(off) axes[1].imshow(gt_mask, cmapgray) axes[1].set_title(Ground Truth) axes[1].axis(off) axes[2].imshow(pred_mask, cmapjet) axes[2].set_title(Prediction Heatmap) axes[2].axis(off) # 叠加显示原图 预测轮廓 overlay image.copy() pred_binary (pred_mask 0.5).astype(np.uint8) contours pred_binary - np.roll(pred_binary, 1, axis0) overlay[contours 1] [255, 0, 0] axes[3].imshow(overlay) axes[3].set_title(Overlay) axes[3].axis(off) plt.tight_layout() if save_path: plt.savefig(save_path, dpi150, bbox_inchestight) plt.close()cmapjet让概率图从蓝色到红色渐变红色区域是模型认为最可能是病灶的地方。叠加图里用红色轮廓标出预测边界方便和原图对比。dpi150保证保存的图片足够清晰。5. 训练过程中最容易翻车的五个地方5.1 现象loss不下降Dice一直在0.3左右原因通常是学习率太大或者数据归一化没做对。Swin Transformer对输入分布敏感如果图像没有用ImageNet均值方差标准化预训练权重相当于白加载。解决方法是检查Normalize的参数确认输入范围在0-1之间再减均值除方差。另外把学习率从1e-4降到5e-5试试。5.2 现象验证集Dice比训练集高很多这是典型的过拟合信号但皮肤病变数据量小的时候反而常见。原因是验证集没有做数据增强分布更“干净”模型在验证集上表现反而好。解决方法是增加训练集增强强度或者加Dropout和Weight Decay。如果训练集Dice已经到0.95但验证集只有0.7那就是过拟合没跑了。5.3 现象预测mask全是背景或者全是病灶全背景说明正样本权重太低把Dice Loss的权重从0.5提到0.7。全病灶说明BCE的pos_weight没设对可以在BCEWithLogitsLoss里加pos_weight参数值设为负样本数除以正样本数。另一个可能是学习率太大导致模型在初期就崩了检查前几个epoch的loss曲线。5.4 现象训练到一半loss突然变成NaNTransformer训练里梯度爆炸是常见问题。检查clip_grad_norm_有没有加max_norm是不是设得太大。另外AdamW的eps默认是1e-8如果loss还是NaN可以调到1e-6。混合精度训练也会导致NaN如果用了amp先关掉试试。5.5 现象推理时显存不够batch size只能设1Swin Transformer的显存占用和输入尺寸的平方成正比。256x256输入下Swin-Tiny大概需要4GB显存做推理Swin-Small要6GB以上。解决办法是推理时用torch.no_grad()上下文并且把输入尺寸降到224x224。如果还是不够可以把编码器换成Swin-Tiny的蒸馏版本参数量更小。6. 把Dice从0.85推到0.90的三个进阶技巧第一个技巧是多尺度推理。训练时用256x256推理时把原图缩放到256、320、384三个尺度分别预测然后把概率图缩回原尺寸取平均。这个操作不需要重新训练推理时间增加三倍但Dice通常能涨1到2个点。代码实现上就是写一个循环对每个尺度调用模型收集输出后做torch.mean。第二个技巧是后处理里的条件随机场。CRF对边界像素的优化效果在皮肤病变上比较明显尤其是病灶边缘模糊的样本。用pydensecrf库把模型输出的概率图作为一元势原图颜色作为二元势迭代5次。注意CRF比较慢一张图大概0.5秒适合答辩演示但不适合大规模推理。第三个技巧是测试时增强。对验证集的每张图做水平翻转、垂直翻转、90度旋转每个变换都跑一次模型把预测结果逆变换后平均。这个和第一个技巧可以叠加使用但推理次数会变成12次需要权衡时间。def tta_predict(model, image, device): 测试时增强推理 model.eval() preds [] with torch.no_grad(): # 原始 preds.append(model(image.unsqueeze(0).to(device)).squeeze()) # 水平翻转 preds.append(torch.flip(model(torch.flip(image, [2]).unsqueeze(0).to(device)).squeeze(), [1])) # 垂直翻转 preds.append(torch.flip(model(torch.flip(image, [1]).unsqueeze(0).to(device)).squeeze(), [0])) # 旋转90度 rotated torch.rot90(image, 1, [1, 2]) pred_rot model(rotated.unsqueeze(0).to(device)).squeeze() preds.append(torch.rot90(pred_rot, -1, [0, 1])) return torch.stack(preds).mean(dim0)这段代码里torch.flip和torch.rot90的维度参数要对应好图像是[C, H, W]翻转H轴用[1]翻转W轴用[2]。逆变换的时候旋转方向要反过来torch.rot90的-1表示逆时针转回去。最后torch.stack把四个预测叠起来取平均。我自己的习惯是训练完先跑一遍基础推理记录Dice然后逐个加技巧看每个技巧的实际增益。多尺度推理和TTA的增益比较稳定CRF有时候会掉点因为皮肤病变的颜色分布差异大CRF的二元势不一定能正确引导边界。答辩前留一天时间专门调这些后处理参数别等到最后一刻才发现CRF把结果搞砸了。希望帮到你。本文还有配套的精品资源点击获取
返回列表