news 2026/9/12 22:30:24

用Res2Net改进UNet实现舌头图像语义分割实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Res2Net改进UNet实现舌头图像语义分割实战

简介:这套基于UNet与Res2Net模块改进的舌头图像语义分割项目,以PyTorch为框架,面向医学影像分析、深度学习入门及语义分割进阶的研究者与学生,提供从数据预处理到训练评估的完整流程。资源包共610个文件,约7.37MB,包括300张JPG原图、300张PNG掩码图,以及4份Python代码、1份项目说明书(DOCX)和说明文档(TXT/MD),数据集与代码一一对应。项目支持二分类与多类别分割,整合数据增强、自动标签处理、IoU/Dice评估,并可通过命令行配置数据路径、学习率与标签映射,训练后输出模型权重、曲线和指标日志,便于直接使用或二次改进。目前已有59人学习下载,适合需要快速构建分割基线或深入研究Res2Net改进策略的读者。

1. 舌头图像语义分割为什么要动 UNet 的结构

把舌头图像分割这件事做扎实,难点从来不是“跑通一个 UNet”,而是舌头本身太不配合:舌尖和舌根的色差大,舌苔边界是渐变的,裂纹和齿痕又细又浅,普通 UNet 在编码器下采样时会把小结构直接丢掉,最后出来的掩码边缘经常是“碎”的。Res2Net 模块恰好能在不加深网络的前提下,把同一层的感受野宽度拉开,让编码器在保留全局轮廓的同时不牺牲细粒度纹理,这正是舌头分割最需要的特性。

这篇文章不是给你一份现成项目说明书,而是顺着“UNet + Res2Net 模块改造 + 舌头数据集 + 完整代码”这条线,把每一层的设计意图、参数选择和落地坑位讲清楚。适合已经跑通过 UNet、想在医学或细粒度语义分割任务上做改进的工程师和研究生;看完你至少能回答三个问题:Res2Net 放在 UNet 哪个位置收益最大、舌头数据集怎么标怎么增强不翻车、训练时哪个超参数对结果影响排在第一位。

2. Res2Net 原理拆解,以及它凭什么改进 UNet

2.1 Res2Net 的多尺度粒度,和普通空洞卷积不是一回事

Res2Net 发表在 CVPR 2020,它的核心改动非常小:在残差块的内部,把经过1x1卷积压缩后的特征图按通道维度切成s份(一般s=4),从第二份开始,每一份都会经过一个3x3卷积,且输入是前一份的输出加上当前份的特征。这样从第二份往后,每一条分支的等效感受野是逐级放大的,网络在同一层里就拥有了从3x33x3*s的连续尺度覆盖。

这和 ASPP、空洞卷积系列的区别在于:ASPP 是在特征图外侧并联不同 dilation rate,权重是共享的;Res2Net 是串联式的逐步融合,更接近“特征金字塔”在单层内的微缩版,计算量增量却小得多。对舌头分割而言,舌体轮廓是大尺度目标,舌裂、齿痕是中尺度,舌乳头纹理是小尺度,三类特征如果不能在同一层同时出现,解码器后期就很难融合出干净的边界。

用公式表达一个Res2Block的前向过程,假设输入经过1x1压缩后得到x,按通道切成sx_ii=1,2,...s),定义y_i为第i份的输出:

y_1 = x_1 y_2 = conv3x3(x_2 + y_1) y_3 = conv3x3(x_3 + y_2) y_s = conv3x3(x_s + y_{s-1})

实际工程中可以做两种变体:一种让y_1也过一次3x3,所有分支都统一;另一种是y_1直接跳连,如上式。两者的 mIoU 差距在 0.5% 以内,但后者省一次卷积,训练更快。我的默认选项是后者。

2.2 UNet 中放置 Res2Net 的三个候选位置对比

把 Res2Net 模块塞进 UNet,位置选择会影响最终指标的 3~5 个百分点,这不是玄学,是不同深度对多尺度特征的需求强度不同。常见做法有三种,先看对比表:

插入位置做法收益点副作用
编码器全部卷积块把每层两个3x3卷积替换为 Res2Block各层同步获得多尺度,最稳参数量增加约 20%,显存占用上升
仅最底层(瓶颈层)第 4 层替换为 Res2Block语义信息最丰富,收益高浅层细节仍然丢失
跳跃连接处在 skip connection 前加一个 Res2Block融合浅层细节多尺度化对深层的全局尺度无能为力

我在舌头数据集上的实际体验是:三选二组合(编码器前两层 + 瓶颈层)性价比最高。第一层和第二层分辨率高,感受野小,细粒度纹理主要靠这两层保留;瓶颈层控制全局语义。如果全部替换,显存占用上涨,而第三层本身是中等语义,替换后对结果的提升和它带来的训练时间不成比例。

所以接下来给出的完整代码,采用“前两层 Res2Block + 瓶颈层 Res2Block”的改进方案,第三层保持普通卷积,解码器不动,这也是这套改进在小型医学数据集上收敛最快、最不容易过拟合的配置。

2.3 面向 UNet 改造的 Res2Block PyTorch 实现

下面这个 PyTorch 实现直接可用,不依赖任何第三方外部库,只基于torch.nn。这里用的是BasicBlock结构,适配 UNet 每层的通道数变化。

import torch import torch.nn as nn class Res2Block(nn.Module): def __init__(self, in_channels, out_channels, scale=4, stride=1): super().__init__() self.scale = scale # 1x1 降维,控制计算量;width 是每个分支的通道数 width = out_channels // scale self.conv1 = nn.Conv2d(in_channels, width * scale, kernel_size=1) self.bn1 = nn.BatchNorm2d(width * scale) # 中间的多尺度 3x3 卷积;第一分支不参与,因为 y1 = x1 self.convs = nn.ModuleList([ nn.Conv2d(width, width, kernel_size=3, padding=1, stride=stride) for _ in range(scale - 1) ]) self.bns = nn.ModuleList([ nn.BatchNorm2d(width) for _ in range(scale - 1) ]) self.conv3 = nn.Conv2d(width * scale, out_channels, kernel_size=1) self.bn3 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) def forward(self, x): identity = x out = self.relu(self.bn1(self.conv1(x))) xs = torch.chunk(out, self.scale, dim=1) ys = [] fuse = xs[0] for i in range(self.scale - 1): if i == 0: fuse = xs[0] fuse = fuse + xs[i + 1] y = self.relu(self.bns[i](self.convs[i](fuse))) ys.append(y) fuse = y # 第一分支原样保留,拼接后 1x1 恢复通道 ys = [xs[0]] + ys out = torch.cat(ys, dim=1) out = self.bn3(self.conv3(out)) if identity.shape == out.shape: out += identity return self.relu(out)

参数说明:scale=4时参数量约为普通两个3x3卷积的 1.2 倍,显存增加约 15%~20%;stride参数是给下采样层用的,stride=2时在3x3上直接降采样,能省掉一层池化。代码里的torch.chunk是按通道切分,切分维度和scale必须整除,如果out_channels是 64,scale设为 4,每个分支 16 个通道,这个取值在 UNet 第一层表现不错。

3. 舌头数据集制作:从标注到可训练的完整代码

3.1 标注类别的选择,直接决定网络学习难度

舌头分割不是“舌头一个类、背景一个类”这么简单。实际做中医舌诊辅助系统时,至少要把舌头区域拆成两类:舌体(不含舌苔的舌质部分)和舌苔。这两类的边界在很多样本里是渐变的,只标一个前景类会让网络在渐变带上产生严重的不确定预测。

类别设定建议:0=背景1=舌质2=舌苔。如果你的任务更细,比如还要分割齿痕或裂纹,单独开类会导致样本不均衡,更推荐先做二分类前景分割,再在 ROI 内部做细分类的两阶段方案。一阶段直接分 4 类以上,在几百张数据上几乎必然收敛困难。

我通常用 Labelme 标注,每张图生成一个 JSON 文件,记录多边形顶点。但 Labelme 原生的 JSON 转掩码方式速度慢且不容易做多类合并,所以我自己写了转换逻辑。

3.2 使用 labelme 半自动标注后的 JSON 转掩码脚本

import json import base64 import numpy as np import cv2 import os def labelme_json_to_mask(json_path, shape=(512, 512)): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) mask = np.zeros(shape, dtype=np.uint8) for shape_item in data["shapes"]: label = shape_item["label"] points = np.array(shape_item["points"], dtype=np.int32) if label == "tongue_body": class_id = 1 # 舌质 elif label == "tongue_coating": class_id = 2 # 舌苔 else: continue cv2.fillPoly(mask, [points], class_id) return mask def process_folder(json_dir, output_dir): os.makedirs(output_dir, exist_ok=True) for file in os.listdir(json_dir): if not file.endswith(".json"): continue mask = labelme_json_to_mask(os.path.join(json_dir, file)) out_path = os.path.join(output_dir, file.replace(".json", ".png")) cv2.imwrite(out_path, mask)

逻辑说明:cv2.fillPoly把多边形顶点填充成指定类别,后画的标注如果覆盖前一个,会直接覆盖像素值,所以在标注时舌质要最后框。这里的class_id顺序要和训练脚本里的ignore_index设置保持一致。处理完的 PNG 是单通道图像,像素值 0、1、2。注意:不要保存成三通道彩色 PNG,否则加载时必须多做一步cv2.COLOR_BGR2GRAY转换,且压缩噪声会污染类别索引。

3.3 针对舌头图像的数据增强,mIoU 能差 6 个点

舌头图像有高度统一的成像规范,白平衡、光照角度、舌头伸出的程度在不同医疗点差异很大。增强策略需要兼顾几何形变和颜色扰动,关键是不要破坏语义边界——舌体是非刚性形变,但扭曲太狠会让舌苔纹理失真。

import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.RandomResizedCrop(size=(512, 512), scale=(0.8, 1.0)), A.Rotate(limit=15, border_mode=cv2.BORDER_CONSTANT), A.HorizontalFlip(p=0.5), A.OneOf([ A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1, hue=0.02, p=1.0), A.HueSaturationValue(hue_shift_limit=5, val_shift_limit=20, p=1.0), ], p=0.8), A.RandomGamma(gamma_limit=(80, 120), p=0.3), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2(), ]) val_transform = A.Compose([ A.Resize(512, 512), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2(), ])

参数说明:Rotateborder_mode必须设为BORDER_CONSTANT,否则旋转后边缘出现的插值像素值会污染背景类,在医学分割里这是老坑。RandomResizedCropscale下限 0.8 足够,太激进会让舌体占不满整张图,模型会在背景上学习到不必要的响应。颜色增强里,hue的扰动范围控制在 0.02,舌色在中医诊断里有临床意义,色相漂移过大会让网络把“淡红舌”和“红绛舌”学成同一类。

3.4 用 PyTorch Dataset 把图像和掩码配对

class TongueDataset(torch.utils.data.Dataset): def __init__(self, image_dir, mask_dir, transform=None): self.image_paths = sorted(os.listdir(image_dir)) self.mask_dir = mask_dir self.image_dir = image_dir self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_name = self.image_paths[idx] img = cv2.imread(os.path.join(self.image_dir, img_name)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask_path = os.path.join(self.mask_dir, img_name.replace(".jpg", ".png")) mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if self.transform: aug = self.transform(image=img, mask=mask) img = aug["image"] mask = aug["mask"] mask = mask.long() return img, mask

逻辑说明:文件名用sorted()保证图像和掩码的顺序一致,这是 Dataset 实现里最常见的潜藏 bug,一旦目录里混入系统隐藏文件或同名不同扩展名的文件,顺序全部错位,训练指标看起来正常但模型学到的是噪声。如果发现训练集 loss 下降正常、验证集 mIoU 始终不涨,先检查__getitem__里返回的图像和掩码是不是同一张。

4. 改进版 UNet 训练全流程:损失函数、参数配置与模型结构

4.1 改进后的 UNet 整体结构代码

改进版 UNet 的编码器分四层,前两层使用 Res2Block,第三层普通卷积,第四层(瓶颈层)再次使用 Res2Block。解码器保持标准结构,跳跃连接不额外加注意力模块,目的是让对比实验能明确归因于 Res2Net 的贡献。

import torch.nn as nn class DownBlock(nn.Module): def __init__(self, in_ch, out_ch, use_res2=False, scale=4): super().__init__() self.use_res2 = use_res2 if use_res2: self.block = Res2Block(in_ch, out_ch, scale=scale) else: self.block = nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) self.pool = nn.MaxPool2d(2) def forward(self, x): x = self.block(x) return x, self.pool(x) class UNetRes2Net(nn.Module): def __init__(self, in_channels=3, num_classes=3): super().__init__() self.down1 = DownBlock(in_channels, 64, use_res2=True) self.down2 = DownBlock(64, 128, use_res2=True) self.down3 = DownBlock(128, 256, use_res2=False) self.down4 = DownBlock(256, 512, use_res2=True, scale=4) self.up1 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2) self.conv1 = nn.Sequential( nn.Conv2d(512, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True) ) # 后续上采样层级类似,省略拼接部分

结构说明:DownBlock返回两个值,一个是当前层输出(进跳跃连接),一个是池化后的下一层输入。use_res2标志控制哪层替换为 Res2Block。瓶颈层用scale=4,分支通道是 512/4=128,这个宽度足够让每个分支学到有区分度的特征。如果scale=8,每分支只有 64 通道,特征碎片化严重,收敛变慢,实测 mIoU 反而下降 1.2% 左右。

4.2 损失函数组合:Dice Loss 为主、Focal 补充

舌头数据集里背景占比通常超过 60%,舌苔和舌质占比加起来约 30%~40%。直接用交叉熵会让背景类主导梯度,目标类别的边界预测会非常模糊。实践中效果最稳的组合是Dice Loss + Focal Loss,权重比 7:3。

class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super().__init__() self.smooth = smooth def forward(self, pred, target): pred = torch.softmax(pred, dim=1) target_onehot = torch.nn.functional.one_hot( target, num_classes=pred.shape[1] ).permute(0, 3, 1, 2).float() intersection = (pred * target_onehot).sum(dim=(2, 3)) dice = (2.0 * intersection + self.smooth) / ( pred.sum(dim=(2, 3)) + target_onehot.sum(dim=(2, 3)) + self.smooth ) return 1.0 - dice.mean()

参数说明:smooth是平滑项,默认 1.0 是为了防止小目标区域(如个别样本里舌苔面积只有几十个像素)出现分母为零。one_hot转类别维度的顺序是(B, H, W)(B, C, H, W),这里permute的维度顺序初学者经常搞混,写错后会报维度不匹配,训练直接从第一步崩掉。Dice Loss 对前景占比不敏感,但对类间边界模糊容忍度低,所以配 Focal 来拉低易分样本的权重,强化难分的舌苔边界。

4.3 一套在 2080Ti 上能跑的训练超参配置

用 AdamW 优化器,初始学习率 3e-4,配合余弦退火。batch size 设为 8(512x512 输入),如果显存不够,优先降低输入尺寸到 448 而不是降低 batch 到 4,梯度噪声会明显增大。

python train.py \ --arch unet_res2net \ --dataset ./tongue_data \ --image_size 512 \ --batch_size 8 \ --lr 3e-4 \ --loss dice+focal \ --epochs 150 \ --scale 4 \ --seed 42

参数说明:seed固定为 42 是为了保证对比实验可复现,尤其是在验证 Res2Net 改进收益时,如果不固定种子,两次训练之间 1% 以内的 mIoU 波动会掩盖真实改进。epochs=150对小型数据集(500~1000 张)足够,舌头分割不是大模型任务,超过 200 epoch 后验证集 mIoU 基本进入平台期,继续训练只会增加过拟合风险。

评估指标上,除了 mIoU 还要专门看Dice coefficient of tongue_coating class。舌苔类别面积小,全局 mIoU 可能看起来不错,但舌苔类别单独掉到 0.5 以下,交给临床用就是废的。训练日志里每 5 个 epoch 打印分类别 IoU,这是判断模型是否真的学到了细粒度结构的关键。

5. 项目说明书编写要点与模型导出验证

一个“可交付”的分割项目,代码只占一半分量,另一半是项目说明书里的复现信息。下面这套结构是我在多次交付中沉淀下的模板,直接按目录写即可。

5.1 项目说明书的标准目录结构

  • 数据集说明:采集设备、标注标准、类别定义、数据划分比例(训练/验证/测试 = 8:1:1)
  • 环境依赖:Python 3.9、PyTorch 1.12+、Albumentations 1.3+,完整 requirements.txt
  • 训练步骤:数据预处理命令、训练命令、日志输出位置
  • 评估结果:分模型对比表,包含基线 UNet、UNet+Res2Net、UNet+Res2Net+不同 loss 的 mIoU、Dice、参数量
  • 复现验证:用checkpoint.pth跑推理的命令,以及输出结果保存路径

复现性最重要的是把随机种子和数据处理版本写清楚。我有一次交付后对方反馈“mIoU 从 84 掉到了 81”,最后排查发现是对方用的 OpenCV 版本不同,RandomResizedCrop的插值方式变了,导致增强分布不一致。项目说明书里务必写上“推荐使用 Docker 镜像”或直接锁死依赖版本大版本号。

5.2 用 ONNX 导出并验证推理完整流程

导出 ONNX 是部署验证的第一步,同时也可以用来检查模型是否在训练和推理模式下行为一致。

import torch from models.unet_res2net import UNetRes2Net model = UNetRes2Net(in_channels=3, num_classes=3) model.load_state_dict(torch.load("checkpoints/best.pth", map_location="cpu")) model.eval() dummy = torch.randn(1, 3, 512, 512) torch.onnx.export( model, dummy, "unet_res2net_tongue.onnx", opset_version=12, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}} )

逻辑说明:dynamic_axes设置动态 batch 维度,部署时可以一次推理多张图。opset_version=12兼容性好,对 BatchNorm 和 Res2Block 里的 split 操作支持稳定。导出后一定要用onnxruntime跑一次推理、对比 PyTorch 输出。

5.3 部署时最容易翻车的两个细节

第一,测试集的图像尺寸必须保持 512 的整数倍,或者至少是 16 的倍数。Res2Block 里的torch.chunk按通道切,不涉及空间维度,但 UNet 下采样四次,输入宽高不是 16 的倍数时,上采样拼接时特征图尺寸对不上,会直接报错。最好在 Dataset 的__getitem__里强制Resize((512, 512)),不管原始图多大。

第二,掩码输出从 logits 转类别时,要用torch.argmax在通道维(dim=1)取索引,而不是在 softmax 之后取最大概率再转。两者数学上等价,但后者的 softmax 计算是浪费的,而且fp16推理下 softmax 的精度损失会导致个别像素类别错位。推荐直接对 logits 用argmax

最后一个技巧:用poi式的重叠滑窗推理(patch-based inference)处理超大尺寸舌头图像时,重叠率设成 25% 是性能和精度平衡点,低于 10% 时拼接缝明显,高于 50% 时推理时间翻倍而 mIoU 提升不足 1%。在舌头这类小器官上,除非原始图像超过 2048 像素,否则直接全图缩放推理即可,不需要滑窗。

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

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

级联H桥STATCOM低频纹波抑制技术解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 22:29:38

基因疗法在罕见癫痫症治疗中的突破与应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 22:29:22

混沌时间序列预测:基于时空RBF神经网络的MATLAB实现

简介:这是一份面向本科、硕士阶段教研学习的Matlab实现资料,聚焦时空RBF神经网络(时空RBF-NN)在混沌时间序列预测中的应用。压缩包内共10个文件,包含3个.m脚本、3个.mat数据集和4个png结果图,整体仅1.32MB&…

作者头像 李华
网站建设 2026/9/12 22:27:25

骨折图像数据集构建:从DICOM清洗到模型训练全流程指南

简介:面向医学影像分析与深度学习实战,这份骨折X射线图像数据集源自孟加拉国三家主要医院,原始扫描超过1.4万张,其中4083张经两名放射科专家独立标注并由医疗官员复核,专用于骨折分类、定位与分割任务,适合…

作者头像 李华
网站建设 2026/9/12 22:24:01

实测才敢推!盘点2026年口碑爆棚的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年AI论文写作工具正以惊人的速度改变学术写作方式,覆盖选题、写作、查重、排版等核心场景,实测提速效果炸裂,助你高效搞定论文。 一、全流程王者:一站式搞定论文全链路&#xff08…

作者头像 李华