news 2026/10/11 17:20:37

Unet3+与自适应多尺度训练:ISIC皮肤病多类别分割实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unet3+与自适应多尺度训练:ISIC皮肤病多类别分割实战

简介:一套基于Unet3+架构的ISIC皮肤病语义分割完整项目,面向医学图像分割初学者与算法工程师,针对皮肤病灶多类别分割中的训练不稳定与目标尺度差异问题,给出完整解决思路。项目融入自适应多尺度训练策略,提供数据加载、模型训练、评估全流程Python脚本,原始JPG皮肤影像与PNG标注掩码一一对应,按README说明即可从零跑通实验。包内共2000个文件,以1279个PNG图像、712个JPG图像为主,另有5个Py脚本、3个TXT配置文件和1个README说明文档,整体为192.71MB的7z压缩包,目录结构清晰,便于按需查阅。目前已有516人学习下载,训练100轮后全局准确率约0.95,多类别平均Dice约0.94,皮肤病变类别IoU达0.84,这些指标可直接作为后续研究的基线参考。配套文档还整理了各阶段训练结果与复现步骤,适合课题研究、课程设计或算法对比,尤其对想快速上手Unet3+医学分割的开发者十分友好。

1. 从 ISIC 到 Unet3+:为什么皮肤病分割要上多尺度多类别方案

ISIC 皮肤镜图像里,一个病灶常常只占整张图的 2%,却可能是黑色素瘤、痣、脂溢性角化病这些病理完全不同的东西;用 Unet3+ 做这类 ISIC 皮肤病语义分割,目标不只是把病变轮廓抠出来,还要让每个像素带上类别信息。实际问题在于:ISIC 公开任务里默认给的是二值掩码,两千多张图又不平衡,固定 512×512 的输入会让大病灶丢边缘、小病灶变成噪点。这篇文章按我实际做这个方向的经验展开讲两件事:多类别分割标签怎么造、自适应多尺度训练怎么做,再给出 Unet3+ 的关键参数、避坑和验证技巧,覆盖从零跑通到调优验证的全过程。

2. Unet3+ 的骨架:全尺度跳跃、深度监督与多类别分割适配

2.1 从 U-Net 到 Unet3+:跳连从“同层”变成“全尺度”

U-Net 在语义分割里的地位不用多说,它的经典设计是编码器下采样、解码器上采样,同时把编码器同一尺度的特征原样拼到解码器对应层。这个设计对大多数自然图像够用,但对 ISIC 这种病灶尺度跨度极大的皮肤镜数据,问题就暴露出来了:如果病变只占图像 2%,经过五次下采样后,深层特征图上对应区域只剩下几个像素,编码器第 5 层的语义信息对小病灶几乎无效,而同层跳跃只能把高分辨率细节传回来,无法补充深层的语义判断。

Unet3+ 的核心改动正是冲着这一点去的。它的每个解码器层不再只拼接“编码器同层”的特征,而是拼接三类信息:上一层解码器上采样后的特征、所有更浅解码器上采样到当前分辨率的特征、以及所有编码器层变换到当前分辨率的特征。简单说,第 k 层解码器看到的输入里同时包含了从原始分辨率到最深下采样层的全部语义尺度,这就是“全尺度跳跃连接”的由来。

这个设计对 ISIC 的皮肤病分割意义非常直接。皮肤镜图像里,早期小痣可能只有 30 像素直径,黑色素瘤却可能占据大半幅画面;Unet3+ 让浅层编码器的原始尺度特征直接短路到浅层解码器,小病灶的边缘不需要经过层层下采样再放大的过程,深层语义信息又可以通过更深编码器分支补充类别判断,大小病灶都能拿到适合自己的信息组合。

与 U-Net++ 的嵌套密集跳跃相比,Unet3+ 少了很多中间堆叠模块,参数增加不明显,但信息通路更短、更直接。在实际工程里,我见过不少项目把 Unet3+ 当成“U-Net++ 的简化版”,这是理解偏差;U-Net++ 是在通道维度上做逐级密集连接,Unet3+ 是在尺度维度上做全尺度并行连接,两者解决的问题不同。如果你评估的 ISIC 模型老是卡在病灶边缘召回率上,先检查是不是把 Unet3+ 实现成了普通 U-Net,只接了同层跳跃。

2.2 深度监督和分类引导模块:在 ISIC 小目标上的意义

Unet3+ 另一条容易被忽略的设计是深度监督。每个解码器分支都会输出一个预测图,并参与损失计算,而不是只有最后一个解码器输出算 loss。对 ISIC 2018 这类只有 2594 张训练图的场景,深度监督等于让浅层网络结构也直接收到梯度信号,不依赖梯度从最深层一路传回,训练前期收敛明显更快,类别不平衡带来的梯度噪声也会被多个监督头摊薄。

在二值分割任务里,Unet3+ 原论文还在深度监督头上接了分类引导模块,用来判断“图像里到底有没有病变”。这个模块的作用相当于一个全局先验开关:如果某个解码器分支在背景区域产生大块高置信度假阳性,而全局分类头判断这张图大概率没有对应病灶,分类反馈就会压低这个分支的像素损失,让解码器整体更保守。皮肤镜图像里,黑色素瘤和脂溢性角化病在颜色、纹理上都高度相似,纯像素级监督很容易让模型在健康皮肤上产生散布的假阳性,CGM 恰好补上了这一环。

但我必须提醒:直接把 CGM 抄到多类别分割里会出问题。原论文的分类头只输出“有病变/无病变”一个概率,多类别分割里不同病变类别互相排斥,如果沿用二值版本,模型会把 MEL 和 NV 混在一起统计,全局先验反而变成噪声。我一般会改三处:分类头的输出通道改成类别数,激活函数换成 sigmoid 做多标签分类,损失函数用 multi-label 版本。改造之后,CGM 对每一类病变都提供一个独立的全局存在性判断,像素损失和全局损失才能在同一语义空间里对齐。

2.3 把全尺度连接封装进 PyTorch:一段可直接改的骨架代码

理解了原理,落地时最需要中小心的就是全尺度连接本身的实现。我通常在项目里把一个解码器层封装成独立模块,输入的是所有编码器特征、所有上方解码器特征,输出当前分辨率的融合特征。下面这个模块就是骨架,可以根据显存和任务需要去调整中间通道数。

import torch import torch.nn as nn import torch.nn.functional as F class FullScaleSkipDecoder(nn.Module): def __init__(self, enc_chs, dec_chs, mid_chs=64, out_chs=1): super().__init__() self.enc_project = nn.ModuleList( [nn.Conv2d(c, mid_chs, 1) for c in enc_chs] ) self.dec_project = nn.ModuleList( [nn.Conv2d(c, mid_chs, 1) for c in dec_chs] ) self.fuse = nn.Conv2d( mid_chs * (len(enc_chs) + len(dec_chs)), out_chs, 3, padding=1 ) def forward(self, enc_feats, dec_feats, H, W): # enc_feats 是编码器 5 个尺度的输出列表 # dec_feats 是当前层上方已经解码出来的 4 个特征 branches = [] for i, f in enumerate(enc_feats): x = F.interpolate(f, size=(H, W), mode="bilinear", align_corners=False) branches.append(self.enc_project[i](x)) for j, f in enumerate(dec_feats): x = F.interpolate(f, size=(H, W), mode="bilinear", align_corners=False) branches.append(self.dec_project[j](x)) return self.fuse(torch.cat(branches, dim=1))

代码逻辑很简单:先用插值把所有输入统一到目标分辨率,再用 1×1 卷积统一通道数,最后拼接融合。这里dec_feats的长度是变化的,越靠近输出端的解码器能拿到越多的上方特征,第一次调用时dec_feats为空列表,模块会自动跳过解码器分支,只融合编码器全尺度特征。

参数方面,mid_chs我一般设为 64,ISIC 这种单模态小数据集不需要太大,放到 128 不会带来明显精度收益,但参数量和显存都会涨。out_chs在多类别分割里等于类别数,背景加 7 类病变就是 8。还有一个细节:编码器特征用bilinear上采样比最近邻更平滑,但会在病变边缘引入轻微模糊,如果发现边界细节不够好,可以在后期微调时把靠近输出的两层换成nearest插值,只牺牲一点梯度稳定性换更锐利的边缘。

3. 数据与标签:把 ISIC 二值掩码改造成多类别分割掩码

3.1 数据集下载与目录结构约定

ISIC Archive 公开的数据集里,ISIC 2018 是最适合做多类别分割任务的版本。它有独立的 Task1 训练集,包含 2594 张皮肤镜图像和对应的二值分割掩码;同时 Task3 提供每张图像的疾病诊断标签。官方把分割和分类分成两个任务,但做语义分割项目时,最常见做法是把两者拼起来:用 Task1 的图像和掩码作为空间标注,用 Task3 的 dx 列作为类别来源。

下载完成后,我习惯把数据整理成统一的目录结构,避免脚本里到处写绝对路径:

ISIC2018/ ├── ISIC2018_Task1_Training_Input/ │ ├── ISIC_0000000.jpg │ ├── ISIC_0000001.jpg │ └── ... ├── ISIC2018_Task1_Training_GroundTruth/ │ ├── ISIC_0000000_segmentation.png │ └── ... └── ISIC2018_Task3_Training_GroundTruth.csv

Task1 原始图像大多是 1024×768 或接近的高分辨率图,掩码和原图尺寸一致,都是单通道 PNG,背景为 0、病变为 255。Task3 的 CSV 是一个表格,每一行对应一张图,dx列是主要诊断,取值包括 MEL、NV、BKL、BCC、AKIEC、DF、VASC,这七类就是多类别分割的目标类别。

需要注意两个数据坑。第一,Task1 训练集和 Task3 标签存在少量不重合的图像,需要先做交集过滤。第二,个别图像在 CSV 里对应多行,同一张图可能出现多个诊断标签;官方任务里这类样本较少,简单处理是取第一诊断,但如果你的验证集正好抽到这些样本,单类 IoU 会被拉低,建议提前把它们单独标记。

3.2 从二值掩码和 dx 列生成多类别掩码的脚本

拿到数据后,第一步就是把二值掩码重新编码成多类别掩码。下面这个脚本是项目里最基础的一段代码,读入掩码、查表映射类别、保存新的 PNG,整个过程用 Pandas 批量处理。

import numpy as np import pandas as pd from pathlib import Path from PIL import Image root = Path("ISIC2018") src_mask = root / "ISIC2018_Task1_Training_GroundTruth" csv_dx = root / "ISIC2018_Task3_Training_GroundTruth.csv" out_mask = root / "ISIC2018_Task1_MultiClass_Mask" out_mask.mkdir(exist_ok=True) df = pd.read_csv(csv_dx) # 只保留有分割掩码且有诊断标签的图像 mask_ids = {p.name.replace("_segmentation.png", "") for p in src_mask.glob("*.png")} df = df[df["image"].isin(mask_ids)] CLASS_NAMES = ["NV", "MEL", "BKL", "BCC", "AKIEC", "DF", "VASC"] class2id = {name: i + 1 for i, name in enumerate(CLASS_NAMES)} for image_id, dx in zip(df["image"], df["dx"]): mask_path = src_mask / f"{image_id}_segmentation.png" mask = np.array(Image.open(mask_path).convert("L")) binary = (mask > 128).astype(np.uint8) cls_id = class2id[dx] multi = np.zeros_like(binary) multi[binary == 1] = cls_id # 病变区标成类别 id,背景保持 0 Image.fromarray(multi.astype(np.uint8)).save(out_mask / f"{image_id}_multiclass.png")

这段脚本的关键有两个。一是binary > 128而不是== 255,因为很多掩码 PNG 的边缘存在抗锯齿灰度值,直接用 255 会把一圈真实边界判成背景,生成的训练标签边缘全部少一两个像素,模型学出来的边界也会系统性内缩。二是class2id的排序,我习惯把 NV 放在第一位、MEL 放第二位,这样后面打印每类 IoU 时,最先看到的就是占比最高和最需要关注的类,不用每次去翻类名表。

生成完掩码,我还会跑一条快速巡检命令,确认每个类的像素数量分布没有异常:

python -c " from PIL import Image import numpy as np from pathlib import Path ids = ['100','5665','2776'] for i in ids: a = np.array(Image.open(Path('ISIC2018/ISIC2018_Task1_MultiClass_Mask') / f'{i}_multiclass.png')) print(i, np.unique(a), (a>0).mean()) "

如果某个图像打印出来的类别值超过两个,说明它存在多个诊断标签,脚本默认取了 CSV 里的第一行;想要严谨可以在生成时对重复图像单独做区域合并,把多标签区域按面积最大或优先级合并成一个类。ISIC 里这种情况占比不高,直接忽略一般只损失零点几个 mIoU,不影响复现,但别让它在避坑排查时迷惑你。

3.3 三种增强策略和它们的副作用

多类别分割的数据增强,不能只考虑“涨点”,还要考虑多类别掩码的插值特性。我项目里用的增强分三组:几何变换、颜色扰动、随机裁剪缩放。几何变换用随机旋转 0~15 度、水平翻转和垂直翻转,对 ISIC 相对安全,因为皮肤镜拍摄时病灶方向不固定,这些变换等价于增加拍摄角度多样性。颜色扰动用亮度 ±10%、对比度 ±10% 和轻微 HSV 抖动,皮肤镜图像色彩偏红且光照相对一致,扰动太大会让模型学到伪颜色关系。

弹性形变要慎重。有些增强库默认自带很强的弹性形变,把病灶边缘扭曲成不规则锯齿,这对病理结构是有害的:黑色素瘤的边界不规则度本身就是诊断特征,形变相当于把不同类别的边缘特征混在一起。我的经验是弹性形变幅度调小,网格间距拉大,让形变主要作用在皮肤纹理上而不是病变边界上,或者干脆不用。

随机裁剪缩放这组增强直接关系到多尺度训练的效果。我一般配置三档:全局缩放系数 0.75~1.25、以病变为中心 80%~110% 的随机裁剪、最终固定到 512×512。顺序必须是先缩放再裁剪,如果先裁剪再缩放,多尺度训练就退化成固定尺寸,因为裁剪窗口已经把尺度信息消掉了。另一个更隐蔽的坑是颜色增强放在裁剪之后:裁剪出来是 512×512 的局部图,再做颜色扰动,会只扰动局部区域的色彩分布,产生不自然的样本,正确顺序是几何变换完成后立刻做颜色扰动,最后才裁剪。

4. 自适应多尺度训练:根据病灶尺寸动态决定缩放与裁剪

4.1 固定尺寸训练为什么让大病灶丢边缘、小病灶成噪点

绝大多数分割项目会把训练图缩放到固定尺寸,比如 512×512,然后直接用。这在 Cityscapes 这种图像内容相对均匀的数据集上没问题,但 ISIC 的病灶尺寸分布极不均匀:有的病灶直径小于 40 像素,有的超过 1024 像素。统一缩放到 512×512 后,小病灶在深层特征图里只占几个像素,几乎拿不到有效梯度;大病灶则是被硬性压缩,黑色素瘤最关键的边缘不规则细节被缩放平滑掉了。

这种问题在训练曲线上不一定会暴露,因为整体 mIoU 会被大样本平均拉高,但仔细看每类指标就会发现,小病灶类的召回率特别低,大病灶类的边界 F1 分数特别差。我之前在一个项目里试过固定 512×512 输入训练,MEL 类的 IoU 只有 0.51,换成自适应尺度后涨到 0.57,提升全来自边缘像素被正确召回。

自适应多尺度训练的核心思路是:采样器根据每个样本的病灶占比动态决定缩放系数,目标是让病灶在训练画面里的占比落在 20%~60% 之间。这个区间是我在实践中调出来的,区间太大会让模型只关注局部细节、丢失上下文;太小则退化回固定尺寸训练。对于偏早期筛查的场景,可以放大到 30%~70%;偏宏观统计的场景缩到 10%~40% 更合适。

4.2 自适应尺度采样器的完整实现

采样器的输入是原图和对应的多类别掩码,输出是缩放后的图像和掩码,以及最终 512×512 的裁剪窗口。实现里最关键的一步是计算病灶像素占比,再反向推导缩放系数。下面这个类可以直接放进数据管线里调用。

import numpy as np import cv2 class AdaptiveScaleSampler: def __init__(self, out_size=512, size_range=(0.75, 1.6), lower=0.2, upper=0.6): self.out_size = out_size self.size_range = size_range self.lower = lower self.upper = upper def __call__(self, img, mask): h, w = mask.shape[:2] ys, xs = np.where(mask > 0) if len(ys) < 1: scale = np.random.uniform(0.9, 1.1) else: fg = len(ys) / (h * w) # 病灶像素占比 clip_fg = np.clip(fg, self.lower, self.upper) scale = float(np.sqrt(clip_fg / fg)) # 把占比反推回边长尺度 scale = float(np.clip(scale, *self.size_range)) scale *= np.random.uniform(0.9, 1.1) new_h, new_w = int(round(h * scale)), int(round(w * scale)) img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) mask = cv2.resize(mask, (new_w, new_h), interpolation=cv2.INTER_NEAREST) y0 = np.random.randint(0, max(1, new_h - self.out_size + 1)) x0 = np.random.randint(0, max(1, new_w - self.out_size + 1)) img_crop = img[y0:y0 + self.out_size, x0:x0 + self.out_size] mask_crop = mask[y0:y0 + self.out_size, x0:x0 + self.out_size] return img_crop, mask_crop

核心逻辑在fg和scale两行。假设一张 1024×1024 的图,病灶只占 2%,clip_fg被下限 0.2 夹住,sqrt(0.2 / 0.02) ≈ 3.16,再被size_range上限 1.6 夹住,最终缩放到约 1.6 倍;如果病灶占 70%,clip_fg取 0.6,sqrt(0.6 / 0.7) ≈ 0.93,基本保持原尺寸。随机抖动0.9~1.1是为了防止模型只在特定尺度上过拟合,同时让相邻 epoch 的样本尺度略有差异。

这里有两个容易踩的细节。第一个是掩码缩放必须用INTER_NEAREST,不能用INTER_LINEAR,否则类别边界会出现 0.7、1.4 这类小数灰度值,CrossEntropyLoss 会把它们当成合法类别索引去查,轻则边界被训练成错误类别,重则直接下标越界。第二个是np.where(mask > 0)里的判断条件,在多类别任务里掩码背景是 0、各类别从 1 开始,这个条件天然正确;如果你准备把背景也当成一个要学习的类,就得改成mask > -1,但那样每个样本的“病灶占比”失去意义,所以保留背景 0、参与损失但不参与尺度计算更合理。

4.3 多类别损失组合与评估口径,参数怎么给

尺度采样解决的是输入分布,损失函数要解决类别不平衡。ISIC 七类病变里,NV 占比远超 MEL 和 DF,直接用标准 CrossEntropy 会被 NV 主导。我项目里用的损失是 CrossEntropy 加权加多类别 Dice,下面是具体的实现代码。

import torch import torch.nn.functional as F class MultiScaleLoss(torch.nn.Module): def __init__(self, num_classes=8, ce_weight=0.6, dice_weight=0.4): super().__init__() self.num_classes = num_classes self.ce_weight = ce_weight self.dice_weight = dice_weight def forward(self, logits, target): b, c, h, w = logits.shape ce = F.cross_entropy(logits, target.long(), ignore_index=255) pred = F.softmax(logits, dim=1) target_onehot = F.one_hot(target.long(), num_classes=self.num_classes) target_onehot = target_onehot.permute(0, 3, 1, 2).float() target_onehot[target == 255] = 0 inter = (pred * target_onehot).sum(dim=(2, 3)) union = pred.sum(dim=(2, 3)) + target_onehot.sum(dim=(2, 3)) dice = (2 * inter + 1e-6) / (union + 1e-6) dice = dice.mean(dim=(0, 1)) return self.ce_weight * ce + self.dice_weight * (1.0 - dice)

target_onehot[target == 255] = 0这行是给未来留的口子:当某张图像只有部分区域被标注时,可以用 255 表示“不参与监督”,这样不必为少样本重新设计损失函数。dice.mean(dim=(0, 1))是同时对类别和样本做平均,而不是先按类别平均再按样本平均,后者会让某个小类别恒为 0 时拉低整体数值,导致 loss 曲线抖动剧烈。

评估指标建议用多类别 mIoU 加每类 IoU,不要只盯整体 Dice。训练日志里我会同时记录 NV-IoU、MEL-IoU、mIoU 三项,前两项直接反映临床最关心的类。自适应多尺度训练对 MEL 的影响通常比 NV 更明显,因为 MEL 边缘不规则度更高,缩放过小会把边界锯齿抹平;如果日志里 MEL-IoU 一直起不来,优先怀疑 4.2 节的size_range上界开小了,再考虑采样器是不是被随机抖动覆盖得太严重。

5. Unet3+ 在 ISIC 上稳定收敛:训练参数、评估指标与避坑清单

5.1 多卡训练参数表与四条必填配置

我在 24GB 单卡上跑过完整实验,也用多卡并行训练过。512×512 输入、单卡 batch 8、AdamW 优化器在 24GB 显存下约占 14GB;显存不够时把 batch 降到 4,学习率同步减半即可。下表是项目里最常用的一组参数,可以直接当初始配置。

参数建议值说明
输入尺寸512×512与自适应采样器输出对齐
batch size单卡 8,多卡总 64有效 batch 维持 64 左右
优化器AdamW,lr=1e-4,wd=1e-4比 Adam 稳,适合分割微调
学习率调度poly,power=0.9 或余弦余弦在 80 epoch 内更稳
epoch80~120120 之后基本过拟合
损失权重CE 0.6 + Dice 0.4对应 4.3 节代码
深度监督权重输出层 1.0,浅层 0.4浅层监督噪声更大
混合精度AMP,损失计算走 FP32见避坑第 1 条

深度监督权重这条是最容易被忽略的。Unet3+ 默认给每个解码器分支相同的监督权重,但 ISIC 里背景占比大,浅层分支的梯度大部分来自背景区域,权重太高会让模型过早进入“只输出背景”的局部最优。我在项目里把最浅两层解码器的监督权重降到 0.4,让它们只起辅助引导作用,不主导梯度方向,训练曲线会平稳很多。

多卡训练时,学习率要根据总 batch 线性缩放。单卡 batch 8、lr 1e-4,八卡总 batch 64 时 lr 提到 2e-4~3e-4 通常可行;再往上提就要小心 AdamW 的二阶动量累积,否则前期会出现 loss 反复横跳。数据加载的 num_workers 在 Windows 下最好设成 0,Linux 下可以按核数减半配置,ISIC 图像解压成本不高,瓶颈大多在中断和随机裁剪上。

5.2 训练结果怎么看:五个必经的踩坑记录

第一条,混合精度训练 loss 变成 NaN。现象是训练正常跑到第 3、4 个 epoch,loss 突然跳成 NaN,回看日志发现前面几个 epoch 一切正常。原因大多出在 AMP 的 autocast 里,深度监督头的 sigmoid 加 BCE 在小 logits 下容易溢出,CGM 的分类输出也参与自动混合精度计算,几个极小值链式相乘就变成 inf。解决方法是把深度监督和 CGM 的损失计算全部固定走 FP32,代码里在调用损失函数前显式开启with torch.cuda.amp.autocast(enabled=False);同时给每个监督分支的 logits 做一次clamp(-100, 100),双保险。

第二条,mIoU 正常但单类 IoU 差异巨大。现象是 NV-IoU 到 0.84,MEL-IoU 只有 0.52,整体 mIoU 看起来还不错。原因是 MEL 样本数量远少于 NV,模型把大量 MEL 像素预测成了背景或 NV,整体指标被 NV 拉高,掩盖了 MEL 的失败。解决方法是给类别权重做差异化配置,MEL 拉到 1.5、BKL 拉到 1.2、其他保持 1.0,同时坚持用 MEL-IoU 做早停依据,而不是用 mIoU。

第三条,自适应尺度退化成固定尺度,训练曲线和固定尺寸几乎一样。原因是采样器里size_range被夹得太紧,比如设成 0.75~1.25,小病灶的放大倍数被上限压死,等效于每张图都只做了轻微缩放。解决方法是把size_range上界至少放到 1.6,并且每隔 10 个 epoch 打印一次平均 scale 和病灶占比的散点图,确认小病灶确实得到了更大的 scale,而不是随机抖动全落在 1.0 附近。

第四条,验证时边缘出现断断续续的伪影,像被剪刀剪过。原因是推理阶段直接沿用了训练时的随机裁剪,或者测试时把大图缩小到 512 就送进网络,边界上低置信度像素被 argmax 压掉。解决方法是推理时使用 4.2 节采样器的固定版本,关闭随机抖动,对整图做一次等比缩放,再用滑动窗口推理,把相邻窗口重叠区域的预测按平均置信度合并,边缘伪影会显著减少。

第五条,多类别掩码里混进异常类别值,训练时 loss 报错或打印出现奇怪类号。原因是某个增强库在掩码缩放时用了线性插值,产生非整数灰度值,阈值化后又归到其他类别 ID。解决方法是所有掩码操作一律INTER_NEAREST,并在进入损失函数前加一句mask[mask > num_classes - 1] = 255兜底,能挡掉八成玄学报错。

6. 验证进阶:用测试时增强把每类 IoU 再抬两个点

多尺度训练完成后,想在不改网络、不加数据的条件下再提高分数,最直接的手段是测试时增强。我在 ISIC 项目里用四种变换:原图、水平翻转、垂直翻转、水平垂直都翻转。每张图前向四次,对四个 softmax 输出取平均再 argmax,通常能让边缘更稳,MEL 和 BKL 这类样本多的类别能涨 1~2 个点的 IoU,代价只是推理时间翻四倍。

import numpy as np import torch def predict_tta(model, img, flips=(False, True)): votes = [] for flip_x in flips: for flip_y in flips: x = img if flip_x: x = np.flip(x, axis=1) if flip_y: x = np.flip(x, axis=0) with torch.inference_mode(): pred = torch.softmax( model(torch.from_numpy(x).unsqueeze(0).cuda())[0], dim=1 ).cpu().numpy() if flip_y: pred = np.flip(pred, axis=2) if flip_x: pred = np.flip(pred, axis=1) votes.append(pred) return np.argmax(np.mean(votes, axis=0), axis=0)

这里最容易翻车的是翻转恢复:如果只把输入翻转,不把预测 flip 回原方向,等于把几个不同坐标系的预测平均在一起,结果反而变差。代码里已经在两次 flip 之后恢复预测坐标,再取平均才是安全的。如果显存紧张,也可以只跑水平和垂直两种翻转,涨点幅度大约是全组合的六成,但推理时间减半。

除了 TTA,另一个我每次项目都会做的验证动作是保存每张验证图像的预测概率热力图,叠加到原图上逐张看。哪些样本的 MEL 边界和背景 BKL 区分不开,哪些样本的真阳性集中在病灶中心而边缘全是低置信度,这些都是模型最典型的失败模式。根据失败模式再回去调自适应尺度上界或类别权重,比盲目调学习率有用得多。我的习惯是每次调完只改一个变量,跑一轮验证集,只看每类 IoU 的三行日志变化,而不是盯整体 mIoU 一个大数。希望这个工作流能帮你在 ISIC 这类不平衡分割数据上少走弯路,一次跑通并真正用得上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 17:15:48

腾讯云快速搭建OpenClaw:AI自动化服务框架部署实战指南

1. 上手前先搞清楚&#xff1a;OpenClaw 到底是什么&#xff0c;能解决什么问题这几年 AI 圈子里的项目一个比一个火&#xff0c;但真正能落到日常使用、帮你省时间的其实没几个。OpenClaw&#xff08;社区里也叫 Clawdbot&#xff09;算是一个比较特别的存在——它不是那种聊聊…

作者头像 李华
网站建设 2026/10/11 17:15:45

Linux命令行垃圾箱Trash-Cli:告别rm误删,安全删除与恢复指南

1. 为什么Linux用户需要命令行垃圾箱 1.1 被rm支配的恐惧&#xff0c;每个老用户都懂 在Linux环境下待久了&#xff0c;几乎每个人都有一段关于 rm -rf 的惨痛回忆。我印象最深的一次&#xff0c;是某公司一位A同学在部署脚本里写了一个变量拼接路径的清理逻辑&#xff0c;结…

作者头像 李华
网站建设 2026/10/11 17:12:35

Python职位推荐系统实战:从数据清洗到FastAPI服务化落地

简介&#xff1a;这份资源面向具备一定Python基础、希望了解推荐系统落地流程的开发者与在校学生&#xff0c;围绕职位推荐场景&#xff0c;提供一套可运行的完整项目代码与配套说明。压缩包共79个文件&#xff0c;约942KB&#xff0c;其中47个py脚本承担数据读取、协同过滤、冷…

作者头像 李华
网站建设 2026/10/11 17:10:00

Ollama模型存储路径迁移:修改OLLAMA_MODELS环境变量释放系统盘

1. 为什么非动不可&#xff1a;默认路径的痛点与适用场景先聊聊背景。Ollama 这个工具&#xff0c;用过的都知道&#xff0c;本地跑大模型的体验做得相当干净&#xff1a;一条命令拉模型&#xff0c;一条命令进对话&#xff0c;API 也有&#xff0c;配合各种前端项目特别方便。…

作者头像 李华
网站建设 2026/10/11 17:09:32

买海尔家电哪个平台评价好?用户反馈与服务承接解析

准备下单海尔家电的人&#xff0c;大多会先翻一翻评价。评分高低只是一方面&#xff0c;用户更在意的是送货是否按时、安装有没有额外收费、使用几年后出现问题能否找到对接方。这些细节拼起来&#xff0c;才是一个平台在用户口中的真实样子。而各渠道在这些环节的承接方式本身…

作者头像 李华
网站建设 2026/10/11 17:05:53

Minari 远程数据集托管实战:HuggingFace Hub 与 GCP 接入完整指南

【免费下载链接】Minari A standard format for offline reinforcement learning datasets, with popular reference datasets and related utilities 项目地址&#xff1a; https://gitcode.com/gh_mirrors/mi/Minari 点击查看 免费下载 Minari 是离线强化学习&#xff08;Of…

作者头像 李华