【深度学习项目】语义分割-U2Net模型(介绍、原理、代码实现)

时间:2025-01-20 11:47:32

文章目录

  • 介绍
    • 深度学习语义分割的关键特点
    • 主要架构和技术
    • 数据集和评价指标
    • 总结
  • U2Net(Unified Network for Multi-Level Feature Aggregation and Segmentation)
    • U2Net 的核心特点
    • U2Net 的工作原理
    • 应用与优势
    • 网络结构
    • 损失计算
    • 评价指标
    • 项目代码
      • DUTS数据集介绍
      • 官方权重
      • 训练记录(`u2net_full`)
      • 训练方法
      • src文件目录
      • 根目录

个人主页:道友老李
欢迎加入社区:道友老李的学习社区

介绍

深度学习语义分割(Semantic Segmentation)是一种计算机视觉任务,它旨在将图像中的每个像素分类为预定义类别之一。与物体检测不同,后者通常只识别和定位图像中的目标对象边界框,语义分割要求对图像的每一个像素进行分类,以实现更精细的理解。这项技术在自动驾驶、医学影像分析、机器人视觉等领域有着广泛的应用。

深度学习语义分割的关键特点

  • 像素级分类:对于输入图像的每一个像素点,模型都需要预测其属于哪个类别。
  • 全局上下文理解:为了正确地分割复杂场景,模型需要考虑整个图像的内容及其上下文信息。
  • 多尺度处理:由于目标可能出现在不同的尺度上,有效的语义分割方法通常会处理多种分辨率下的特征。

主要架构和技术

  1. 全卷积网络 (FCN)

    • FCN是最早的端到端训练的语义分割模型之一,它移除了传统CNN中的全连接层,并用卷积层替代,从而能够接受任意大小的输入并输出相同空间维度的概率图。
  2. 跳跃连接 (Skip Connections)

    • 为了更好地保留原始图像的空间细节,一些模型引入了跳跃连接,即从编码器部分直接传递特征到解码器部分,这有助于恢复细粒度的结构信息。
  3. U-Net

    • U-Net是一个专为生物医学图像分割设计的网络架构,它使用了对称的收缩路径(下采样)和扩展路径(上采样),以及丰富的跳跃连接来捕捉局部和全局信息。
  4. DeepLab系列

    • DeepLab采用了空洞/膨胀卷积(Atrous Convolution)来增加感受野而不减少特征图分辨率,并通过多尺度推理和ASPP模块(Atrous Spatial Pyramid Pooling)增强了对不同尺度物体的捕捉能力。
  5. PSPNet (Pyramid Scene Parsing Network)

    • PSPNet利用金字塔池化机制收集不同尺度的上下文信息,然后将其融合用于最终的预测。
  6. RefineNet

    • RefineNet强调了高分辨率特征的重要性,并通过一系列细化单元逐步恢复细节,确保输出高质量的分割结果。
  7. HRNet (High-Resolution Network)

    • HRNet在整个网络中保持了高分辨率的表示,同时通过多尺度融合策略有效地整合了低分辨率但富含语义的信息。

数据集和评价指标

常用的语义分割数据集包括PASCAL VOC、COCO、Cityscapes等。这些数据集提供了标注好的图像,用于训练和评估模型性能。

评价语义分割模型的标准通常包括:

  • 像素准确率 (Pixel Accuracy):所有正确分类的像素占总像素的比例。
  • 平均交并比 (Mean Intersection over Union, mIoU):这是最常用的评价指标之一,计算每个类别的IoU(交集除以并集),然后取平均值。
  • 频率加权交并比 (Frequency Weighted IoU):考虑每个类别的出现频率,对mIoU进行加权。

总结

随着硬件性能的提升和算法的进步,深度学习语义分割已经取得了显著的进展。现代模型不仅能在速度上满足实时应用的需求,还能提供非常精确的分割结果。未来的研究可能会集中在提高模型效率、增强跨域泛化能力以及探索无监督或弱监督的学习方法等方面。

U2Net(Unified Network for Multi-Level Feature Aggregation and Segmentation)

U2Net(Unified Network for Multi-Level Feature Aggregation and Segmentation)是一种先进的语义分割网络架构,由中国科学院自动化研究所的研究人员提出。它在传统的 U-Net 基础上进行了多项创新,旨在解决多尺度特征聚合和细粒度结构分割的问题。U2Net 的设计特别适用于资源受限的环境,如移动设备或嵌入式系统,因为它不仅具有高精度,而且模型非常轻量化。

U2Net 的核心特点

  1. 双重编码器-解码器结构

    • U2Net 采用了两套平行的编码器-解码器路径,一套用于捕捉全局上下文信息(Global Context),另一套则专注于局部细节(Local Details)。这样的设计可以更全面地理解图像内容,同时保持对细小物体边界的敏感性。
  2. RSU(Recurrent Squeeze Unit)模块

    • RSU 是 U2Net 的关键组件之一,它通过递归的方式多次应用挤压操作(squeeze operation),从而增强特征表示能力。每个 RSU 包含多个卷积层,它们之间存在内部跳跃连接,有助于缓解梯度消失问题,并促进不同层次特征之间的交流。
  3. 渐进式上采样策略

    • 在解码阶段,U2Net 使用了一种渐进式的上采样方法,逐步恢复空间分辨率。与一次性大幅上采样的方法相比,这种方法可以在每一级都融合来自编码器的多尺度特征,确保了输出结果的空间一致性。
  4. 多层级特征融合

    • U2Net 强调从低层到高层的多层次特征融合,利用了丰富的上下文信息来改进最终的分割效果。这种跨层级的信息交互使得模型能够更好地处理复杂场景中的各种对象。
  5. 轻量化设计

    • 尽管性能强大,U2Net 却是一个极其紧凑的模型,参数量较少且计算成本低。这使得它非常适合部署在移动端或其他计算资源有限的平台上。
  6. 端到端训练

    • 整个 U2Net 模型可以作为一个整体进行端到端的训练,无需预训练或者分阶段训练,简化了开发流程并提高了适应特定任务的能力。

U2Net 的工作原理

  • 输入层:接收任意大小的输入图像。
  • 主干网络(Backbone):基于 ResNet 或其他高效的基础架构,负责初步特征提取。
  • 双重编码器路径
    • 全局编码器:逐渐降低空间分辨率,提取高层次语义特征。
    • 局部编码器:保留较高分辨率,强调细节捕捉。
  • RSU 模块:分布在编码器和解码器中,强化特征表达。
  • 渐进式解码器路径:逐步恢复空间分辨率,每一步都结合来自两个编码器路径的特征。
  • 多层级特征融合:在不同的解码阶段整合来自各个层级的信息。
  • 输出层:生成每个像素点的类别预测值,其通道数等于类别的数量。

应用与优势

U2Net 已经被广泛应用于多种计算机视觉任务,包括但不限于:

  • 医学图像分割:如肿瘤、器官等的精确分割。
  • 自然场景分割:例如道路、行人、车辆等元素的识别。
  • 遥感图像分析:土地覆盖分类、变化检测等领域。
  • 实时视频处理:由于其高效的特性,U2Net 可以实现实时的帧间分割。

总之,U2Net 以其独特的双重编码器-解码器结构、RSU 模块以及渐进式上采样策略,为语义分割任务提供了新颖而有效的解决方案,尤其是在需要兼顾精度和效率的情况下。

SOD任务是将图片中最吸引人的目标和区域分割出来,只分前景和背景,简单来说是个二分类任务。
在这里插入图片描述

在这里插入图片描述

在Encoder阶段, 每通过一个block都会下采样2倍(maxpool), 在Decoder阶段,每通过一个block都会上采样2倍(bilinear)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

网络结构

在这里插入图片描述

损失计算

在这里插入图片描述

  • l:代表二值交叉熵损失
  • w:代表每个损失的权重

评价指标

F-measure
在这里插入图片描述

MAE
在这里插入图片描述

项目代码

DUTS数据集介绍

DUTS数据集官方下载地址:http://saliencydetection.net/duts/

如果下载不了,可以通过我提供的百度云下载,链接: https://pan.baidu.com/s/1nBI6GTN0ZilqH4Tvu18dow 密码: r7k6

其中DUTS-TR为训练集,DUTS-TE是测试(验证)集,数据集解压后目录结构如下:
在这里插入图片描述

  • 注意训练或者验证过程中,将--data-path指向DUTS-TR所在根目录

官方权重

从官方转换得到的权重:

  • u2net_full.pth下载链接: https://pan.baidu.com/s/1ojJZS8v3F_eFKkF3DEdEXA 密码: fh1v
  • u2net_lite.pth下载链接: https://pan.baidu.com/s/1TIWoiuEz9qRvTX9quDqQHg 密码: 5stj

u2net_full在DUTS-TE上的验证结果(使用validation.py进行验证):

MAE: 0.044
maxF1: 0.868

注:

  • 这里的maxF1和原论文中的结果有些差异,经过对比发现差异主要来自post_norm,原仓库中会对预测结果进行post_norm,但在本仓库中将post_norm给移除了。
    如果加上post_norm这里的maxF1为0.872,如果需要做该后处理可自行添加,post_norm流程如下,其中output为验证时网络预测的输出:
ma = torch.max(output)
mi = torch.min(output)
output = (output - mi) / (ma - mi)
  • 如果要载入官方提供的权重,需要将src/model.pyConvBNReLU类里卷积的bias设置成True,因为官方代码里没有进行设置(Conv2d的bias默认为True)。
    因为卷积后跟了BN,所以bias是起不到作用的,所以在项目中默认将bias设置为False。

训练记录(u2net_full)

训练最终在DUTS-TE上的验证结果:

MAE: 0.047
maxF1: 0.859

训练过程详情可见results.txt文件,训练权重下载链接: https://pan.baidu.com/s/1df2jMkrjbgEv-r1NMaZCZg 密码: n4l6

训练方法

  • 确保提前准备好数据集
  • 若要使用单GPU或者CPU训练,直接使用train.py训练脚本
  • 若要使用多GPU训练,使用torchrun --nproc_per_node=8 train_multi_GPU.py指令,nproc_per_node参数为使用GPU数量
  • 如果想指定使用哪些GPU设备可在指令前加上CUDA_VISIBLE_DEVICES=0,3(例如我只要使用设备中的第1块和第4块GPU设备)
  • CUDA_VISIBLE_DEVICES=0,3 torchrun --nproc_per_node=2 train_multi_GPU.py

src文件目录

  • model.py
from typing import Union, List
import torch
import torch.nn as nn
import torch.nn.functional as F


class ConvBNReLU(nn.Module):
    def __init__(self, in_ch: int, out_ch: int, kernel_size: int = 3, dilation: int = 1):
        super().__init__()

        padding = kernel_size // 2 if dilation == 1 else dilation
        self.conv = nn.Conv2d(in_ch, out_ch, kernel_size, padding=padding, dilation=dilation, bias=False)
        self.bn = nn.BatchNorm2d(out_ch)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.relu(self.bn(self.conv(x)))


class DownConvBNReLU(ConvBNReLU):
    def __init__(self, in_ch: int, out_ch: int, kernel_size: int = 3, dilation: int = 1, flag: bool = True):
        super().__init__(in_ch, out_ch, kernel_size, dilation)
        self.down_flag = flag

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        if self.down_flag:
            x = F.max_pool2d(x, kernel_size=2, stride=2, ceil_mode=True)

        return self.relu(self.bn(self.conv(x)))


class UpConvBNReLU(ConvBNReLU):
    def __init__(self, in_ch: int, out_ch: int, kernel_size: int = 3, dilation: int = 1, flag: bool = True):
        super().__init__(in_ch, out_ch, kernel_size, dilation)
        self.up_flag = flag

    def forward(self, x1: torch.Tensor, x2: torch.Tensor) -> torch.Tensor:
        if self.up_flag:
            x1 = F.interpolate(x1, size=x2.shape[2:], mode='bilinear', align_corners=False)
        return self.relu(self.bn(self.conv(torch.cat([x1, x2], dim=1))))


class RSU(nn.Module):
    def __init__(self, height: int, in_ch: int, mid_ch: int, out_ch: int):
        super().__init__()

        assert height >= 2
        self.conv_in = ConvBNReLU(in_ch, out_ch)

        encode_list = [DownConvBNReLU(out_ch, mid_ch, flag=False)]
        decode_list = [UpConvBNReLU(mid_ch * 2, mid_ch, flag=False)]
        for i in range(height - 2):
            encode_list.append(DownConvBNReLU(mid_ch, mid_ch))
            decode_list.append(UpConvBNReLU(mid_ch * 2, mid_ch if i < height - 3 else out_ch))

        encode_list.append(ConvBNReLU(mid_ch, mid_ch, dilation=2))
        self.encode_modules = nn.ModuleList(encode_list)
        self.decode_modules = nn.ModuleList(decode_list)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x_in = self.conv_in(x)

        x = x_in
        encode_outputs = []
        for m in self.encode_modules:
            x = m(x)
            encode_outputs.append(x)

        x = encode_outputs.pop()
        for m in self.decode_modules:
            x2 = encode_outputs.pop()
            x = m(x, x2)

        return x + x_in


class RSU4F(nn.Module):
    def __init__(self, in_ch: int, mid_ch: int, out_ch: int):
        super().__init__()
        self.conv_in = ConvBNReLU(in_ch, out_ch)
        self.encode_modules = nn.ModuleList([ConvBNReLU(out_ch, mid_ch),
                                             ConvBNReLU(mid_ch, mid_ch, dilation=2),
                                             ConvBNReLU(mid_ch, mid_ch, dilation=4),
                                             ConvBNReLU(mid_ch, mid_ch, dilation=8)])

        self.decode_modules = nn.ModuleList([ConvBNReLU(mid_ch * 2, mid_ch, dilation=4),
                                             ConvBNReLU(mid_ch * 2, mid_ch, dilation=2),
                                             ConvBNReLU(mid_ch * 2, out_ch)])

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x_in = self.conv_in(x)

        x = x_in
        encode_outputs = []
        for m in self.encode_modules:
            x = m(x)
            encode_outputs.append(x)

        x = encode_outputs.pop()
        for m in self.decode_modules:
            x2 = encode_outputs.pop()
            x = m(torch.cat([x, x2], dim=1))

        return x + x_in


class U2Net(nn.Module):
    def __init__(self, cfg: dict, out_ch: int = 1):
        super().__init__()
        assert "encode" in cfg
        assert "decode" in cfg
        self.encode_num = len(cfg["encode"])

        encode_list = []
        side_list = []
        for c in cfg["encode"]:
            # c: [height, in_ch, mid_ch, out_ch, RSU4F, side]
            assert len(c) == 6
            encode_list.append(RSU(*c[:4]) if c[4] is False else RSU4F(*c[1:4]))

            if c[5] is True:
                side_list.append(nn.Conv2d(c[3], out_ch, kernel_size=3, padding=1))
        self.encode_modules = nn.ModuleList(encode_list)

        decode_list = []
        for c in cfg["decode"]:
            # c: [height, in_ch, mid_ch, out_ch, RSU4F, side]
            assert len(c) == 6
            decode_list.append(RSU(*c[:4]) if c[4] is False else RSU4F(*c[1:4]))

            if c[5] is True:
                side_list.append(nn.Conv2d(c[3], out_ch, kernel_size=3, padding=1))
        self.decode_modules = nn.ModuleList(decode_list)
        self.side_modules = nn.ModuleList(side_list)
        self.out_conv = nn.Conv2d(self.encode_num * out_ch, out_ch, kernel_size=1)

    def forward(self, x: torch.Tensor) -> Union[torch.Tensor, List[torch.Tensor]]:
        _, _, h, w = x.shape

        # collect encode outputs
        encode_outputs = []
        for i, m in enumerate(self.encode_modules):
            x = m(x)
            encode_outputs.append(x)
            if i != self.encode_num - 1:
                x = F.max_pool2d(x, kernel_size=2, stride=2, ceil_mode=True)

        # collect decode outputs
        x = encode_outputs.pop()
        decode_outputs = [x]
        for m in self.decode_modules:
            x2 = encode_outputs.pop()
            x = F.interpolate(x, size=x2.shape[2:], mode='bilinear', align_corners=False)
            x = m(torch.concat([x, x2], dim=1))
            decode_outputs.insert(0, x)

        # collect side outputs
        side_outputs = []
        for m in self.side_modules:
            x = decode_outputs.pop()
            x = F.interpolate(m(x), size=[h, w], mode='bilinear', align_corners=False)
            side_outputs.insert(0, x)

        x = self.out_conv(torch.concat(side_outputs, dim=1))

        if self.training:
            # do not use torch.sigmoid for amp safe
            return [x] + side_outputs
        else:
            return torch.sigmoid(x)


def u2net_full(out_ch: int = 1):
    cfg = {
        # height, in_ch, mid_ch, out_ch, RSU4F, side
        "encode": [[7, 3, 32, 64, False, False],      # En1
                   [6, 64, 32, 128, False, False],    # En2
                   [5, 128, 64, 256, False, False],   # En3
                   [4, 256, 128, 512, False, False],  # En4
                   [4, 512, 256, 512, True, False],   # En5
                   [4, 512, 256, 512, True, True]],   # En6
        # height, in_ch, mid_ch, out_ch, RSU4F, side
        "decode": [[4, 1024, 256, 512, True, True],   # De5
                   [4, 1024, 128, 256, False, True],  # De4
                   [5, 512, 64, 128, False, True],    # De3
                   [6, 256, 32, 64, False, True],     # De2
                   [7, 128, 16, 64, False, True]]     # De1
    }

    return U2Net(cfg, out_ch)


def u2net_lite(out_ch: int = 1):
    cfg = {
        # height, in_ch, mid_ch, out_ch, RSU4F, side
        "encode": [