在这里插入图片描述

本文教的是方法,也给出几种改进方法,二次创新结构,百变不离其宗一文带你改进自己模型,科研路上少走弯路。


前言

在医学图像分割任务中,病灶区域往往形态各异、边界模糊,且经常与周围组织的对比度较低,这要求模型具备极强的特征提取和细节辨别能力。传统的U-Net网络虽然通过跳跃连接融合了多尺度特征,但在处理复杂语义和精细边缘时仍有不足,尤其是在捕获跨通道的特征依赖时,要么缺乏注意力机制导致关键特征不够突出,要么采用类似 SE(Squeeze-and-Excitation)机制却因为降维操作丢失了通道间的原始空间关联。为了解决这些痛点,本文引入了在计算机视觉领域表现出色的 ECA(Efficient Channel Attention,高效通道注意力)机制。ECA 巧妙地抛弃了传统的降维全连接层,通过自适应核大小的一维卷积直接实现局部跨通道交互。这种创新设计不仅极大地降低了模型复杂度(仅需少量参数),而且有效保留了通道特征的完整性,使得网络能够更加精准、高效地聚焦病灶区域,让 U-Net 模型在医学图像病灶分割上取得显著突破!

论文发表信息

  • 论文标题ECA-Net: Efficient Channel Attention for Deep Convolutional Neural Networks
  • 发表会议/期刊CVPR 2020(计算机视觉顶会)
  • 作者单位:天津大学、大连理工大学等
  • 核心贡献:提出了一种极其轻量且高效的通道注意力模块(ECA),通过避免降维操作并采用自适应一维卷积来实现局部跨通道交互,在几乎不增加模型参数和计算量的前提下显著提升了模型性能。
  • 论文地址https://arxiv.org/abs/1910.03151
  • 代码地址https://github.com/BangguWu/ECANet

ECA 核心模块理论

ECA(高效通道注意力)机制的核心思想在于避免通道降维,并通过高效的一维卷积捕获局部跨通道的交互信息。传统的 SE 注意力机制虽然有效,但其为了降低计算量采用了“降维-升维”的瓶颈结构,这不可避免地破坏了通道间的直接对应关系。而 ECA 机制通过以下创新实现破局:

  1. 无降维的全局信息聚合:ECA 首先对输入的特征图进行全局平均池化(GAP),获取每个通道的全局空间统计信息,但不进行任何降维处理,从而保留了完整的通道维度特征。
  2. 自适应一维卷积交互:在不改变通道数的情况下,ECA 使用一个轻量级的一维卷积(1D Convolution)来实现局部跨通道交互。卷积核的大小 k k k 根据通道数 C C C 自适应计算得出,通道数越多,交互范围越大。随后通过 Sigmoid 激活函数生成每个通道的注意力权重。

作为一个极度轻量化且高效的注意力模块,ECA 能够让模型在几乎不增加任何计算负担的情况下,深度挖掘通道间的关键依赖关系并精确定位病灶特征,非常适合应用于计算资源敏感且对精度要求极高的医学图像分割。

训练代码参考和下载:手把手带你完成基于Unet的医学图像分割系统设计与实现,Unet网络架构讲解,Unet复现,使用vibe coding工具,开发一个完整系统,包含完整源码



🐴一、实战细节

⚡⚡实验结果画图

画图效果如下,代码可一键运行

在这里插入图片描述

画图代码:

# -*- coding: utf-8 -*-
"""
@Auth :落花不写码
@File :画图.py
@IDE :PyCharm
@Motto :学习新思想,争做新青年
"""
import matplotlib.pyplot as plt
import pandas as pd
import os


plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams.update({'font.size': 14})


def smooth_curve(points, factor=None):
    smoothed_points = []
    for point in points:
        if smoothed_points:
            previous = smoothed_points[-1]
            smoothed_points.append(previous * factor + point * (1 - factor))
        else:
            smoothed_points.append(point)
    return smoothed_points


"""
想对比几个就写几个
"""
csv_configs = [
    {'name': 'U-Net', 'path': r'runs/train/exp/results.csv', 'color': '#4C72B0'},  # 蓝色
    {'name': '本文算法', 'path': 'runs/train/exp3/results.csv', 'color': '#DD8452'},  # 红色
]
max_compare_epochs = None  # 对比轮数
factor = 0.5


# 保存结果路径
save_dir = 'result'
os.makedirs(save_dir, exist_ok=True)


data_frames = []
for config in csv_configs:
    if os.path.exists(config['path']):
        df = pd.read_csv(config['path'])
        df['epoch'] = df['epoch'] + 1
        df.columns = df.columns.str.strip()

        if max_compare_epochs is not None:
            df = df[df['epoch'] <= max_compare_epochs]

        data_frames.append({
            'name': config['name'],
            'df': df,
            'color': config['color']
        })
    else:
        print(f"找不到文件 {config['path']}")

if not data_frames:
    raise ValueError("没有找到任何 CSV 文件,请检查路径!")

fig, ax = plt.subplots(figsize=(10, 6), dpi=300)

for item in data_frames:
    name = item['name']
    df = item['df']
    color = item['color']
    epochs = df['epoch'].values
    vals = df['val_loss'].values

    # ax.plot(epochs, vals, alpha=0.3, color=color, linewidth=1.5)

    # 画平滑后的曲线
    smoothed_vals = smooth_curve(vals, factor=factor)
    ax.plot(epochs, smoothed_vals, alpha=1.0, color=color, linewidth=2.5, label=name)

ax.set_title('损失对比 (Val Loss)', pad=15, fontweight='bold')
ax.set_xlabel('训练轮次 (Epochs)', labelpad=10)
ax.set_ylabel('损失值 (Loss)', labelpad=10)

# 去除右侧和上方的黑框
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# 左侧和下方的坐标轴
ax.spines['bottom'].set_linewidth(1.5)
ax.spines['left'].set_linewidth(1.5)

ax.grid(axis='y', linestyle='--', alpha=0.5)
ax.legend(frameon=False, loc='upper right')

plt.tight_layout()
loss_save_path = os.path.join(save_dir, 'val_loss_comparison.png')
plt.savefig(loss_save_path, format='png', dpi=300)
print(f"Loss对比图已保存至: {loss_save_path}")
plt.show()

# 绘制 mIoU 图
fig, ax = plt.subplots(figsize=(10, 6), dpi=300)

for item in data_frames:
    name = item['name']
    df = item['df']
    color = item['color']
    epochs = df['epoch'].values
    vals = df['val_miou'].values

    # ax.plot(epochs, vals, alpha=0.3, color=color, linewidth=1.5)
    smoothed_vals = smooth_curve(vals, factor=factor)
    ax.plot(epochs, smoothed_vals, alpha=1.0, color=color, linewidth=2.5, label=name)

ax.set_title('mIoU对比 (Val mIoU)', pad=15, fontweight='bold')
ax.set_xlabel('训练轮次 (Epochs)', labelpad=10)
ax.set_ylabel('mIoU', labelpad=10)

ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_linewidth(1.5)
ax.spines['left'].set_linewidth(1.5)

ax.grid(axis='y', linestyle='--', alpha=0.5)
ax.legend(frameon=False, loc='lower right')

plt.tight_layout()
acc_save_path = os.path.join(save_dir, 'val_miou_comparison.png')
plt.savefig(acc_save_path, format='png', dpi=300)
print(f"mIoU对比图已保存至: {acc_save_path}")
plt.show()



⚡⚡ECA模块代码

# -*- coding: utf-8 -*-
"""
@Auth :落花不写码
@File :eca.py
@Motto :学习新思想,争做新青年
"""
import torch
from torch import nn


class ECA(nn.Module):
    """Constructs a ECA module.
    Args:
        channel: Number of channels of the input feature map
        k_size: Adaptive selection of kernel size
    """

    def __init__(self, channel, k_size=3):
        super(ECA, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.conv = nn.Conv1d(1, 1, kernel_size=k_size, padding=(k_size - 1) // 2, bias=False)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        # feature descriptor on the global spatial information
        y = self.avg_pool(x)

        # Two different branches of ECA module
        y = self.conv(y.squeeze(-1).transpose(-1, -2)).transpose(-1, -2).unsqueeze(-1)

        # Multi-scale information fusion
        y = self.sigmoid(y)

        return x * y.expand_as(x)


if __name__ == "__main__":

    model = ECA(64)

    # 输入张量 (B, C, H, W)
    input_tensor = torch.randn(2, 64, 32, 32)

    print(f"输入维度: {input_tensor.shape}")

    output = model(input_tensor)

    print(f"输出维度: {output.shape}")

⚡⚡使用教程

在源码项目下,创建 nn 目录,之后按照以下结构创建文件夹和文件就行,后续还有其他,我也会分享整个源码给大家

nn/
├── __init__.py          # 用于导出模块
├── registry.py          #  用于模块注册器
├── models/              # 放具体模块实现
│   ├── __init__.py      # 用于导出模块
│   ├── attention/       #  放注意力机制模块文件夹
│   │   ├── __init__.py  # 导出注意力模块
│   │   ├── xx.py        #  注意力机制模块代码
│   └── blocks/          #  基础块模块,放自定义卷积、自定义损失函数等文件
│       ├── __init__.py  # 导出模块

☑️步骤1

下面所说的文件如果已经有了,或者下载的源码有这个文件就不用下载了

registry.py 用于注册模块的。
builder.py 用于修改原始模型结构的,主要修改方法就在里面,核心部分,看懂网络结构,基本可以自己自定义魔改原始网络结构了。

☑️步骤2

在 nn/models/attention 路径下创建一个 py 文件,文件名一般以模块名命名,这个文件名命名问题不大,之后把上面的改进模块代码复制粘贴进去

☑️步骤3

导出模块这里只讲一次,以后有新的模块就按照下面的方法导出就行,百变不离其宗

(1) 在 nn/models/attention/__init__.py 中导出模块

from .eca import ECA

(2) 将该模块名字加入到 __all__ 列表中

__all__ = [
    # ... 其他原有模块 ...
    "ECA",
]

☑️步骤4

在 nn/models/unet.py,添加下面的代码,给你的代码应该已经添加了:

class EnhancedConvBlock(ConvBlock):
    def __init__(self, dim, ch_out, module_class, **kwargs):
        super().__init__(dim, ch_out)

        self.module = module_class(dim, **kwargs)

    def forward(self, x):

        # 添加新的模块(如注意机制、即插即用模块)
        x = self.module(x)
        x = self.conv(x)

        return x

在这里插入图片描述

🐴二、模型结构分析

⚡⚡ 注意机制结构分析

在魔改网络时,常常会出现张量维度(Shape)不匹配的问题,那么这时候就要分析改进的模型结构了,要是维度一样,可以方便我们的模块即插即用,要是维度不一样也是有方法解决滴,后面改进遇到这个问题的话我在提一嘴吧。那么我们这里先打印结构和输出的维度信息,直接运行上面我给的改进模块代码即可。

input_tensor = torch.randn(2, 64, 32, 32) # 输入维度

从下图的结果来看, Input Shape(输入维度)和 Output Shape(输出维度)是一致的,基于该模块的维度不变性,你可以理解为一个即插即用的模块,这也是比较重要的一个点!

在这里插入图片描述

⚡⚡二次创新实战

通过上面的例子我们大概知道怎么修改了,接下来开始进行缝合手法,教会你缝合手法,相信大家会想出很多种缝合手法

在 configs 目录下,复制一份 default.yaml 文件,然后取名为 xxx.yaml,xxx 一般取改进模块名字,之后在这个文件进行修改,改进时候修改 modifications 参数即可,之后训练时候使用改进的 yaml 文件就行,以下是例子:
在这里插入图片描述

☑️第一种改进手法

(1)添加注意机制、即插即用模块、卷积模块等

yaml 配置文件参数如下所示:

modifications:
  - type: 'replace'                 # 改进方法
    stage_idx: [1,3]                #  修改多个结构时候可以写 [1, 3]
    module_class: 'ECA'            # 模块名称
    kwargs: {}

type 参数就是调用 nn/builder.py 里面的方法,具体参数可以去看看。需要注意的是:目前使用

stage_idx 参数就是模型层数的编号,以 ConvNeXt 模型为例, features 层编号为 1 的结构如下图红色框圈起来的部分(其他层也是这样数滴,其他模型大概也是这样分析):

module_class 参数就是使用什么模块,大家应该看得懂的,如 module_class: ‘ECA’、module_class: ‘SSPCAB’ 、module_class: ‘LWGA_Block’

kwargs 参数就是填该模块的其他参数(输入通道数不用填),该模块是指 module_class 填的模块,假设 module_class 填 LWGA_Block 模块,那么你就要看一下 LWGA_Block 需要哪些参数,那么接下来手把手教你分析,落花不写码本人的口头禅就是:教的是方法。

📐模块的传参分析教程

打开该模块的代码看一下开头的 __init__ 函数头,如下所示:

在这里插入图片描述

(1)看第一个参数,这里是 dim,几乎所有模块都需要输入通道数,一般第一个参数就是输入通道数,不同模块的命名方式不一样(如 dim, in_channels, c1),你大概知道就行,这个参数没有赋值,那么就要给它传参,我代码已经处理好了,所以输入通道数参数不用管了

(2)看第二个参数,这里是 stage,如果没有等号,这是必传参,必须写进 kwargs 里面,不过你也可以在这里手动填一下,比如stage=3 等,从该模块的代码来看 stage 参数意思是走那条分支的。
(3)看第三个参数,这里是 att_kernel,从字面意思看出该参数意思是注意力的卷积核大小,代码已经有默认值,可以不写进 kwargs 里面;如果想修改值,可以在代码直接修改或者就写进 kwargs 把新的值赋值给它就行。类似如下

module_class: 'LWGA_Block'
    kwargs:
      stage: 3
      att_kernel: 12  # 可选填
      drop_rate: 0.1  # 可选填

下面继续拿其他模块分析一下,看到除了第一个输入通道数参数不用管,其他参数都赋值了,所以 kwargs: {} ,保持空字典即可。

在这里插入图片描述

module_class: 'LSKA'
    kwargs: {}  # 保持空字典即可,使用代码默认设置即可

下面的改进手法都是教的方法,灵活变动就行,大家应该都懂的

小结:想做更复杂的改进,比如加入注意力机制、多尺度特征融合也是可以使用我说的方法,大家可以自己探索探索。

可以修改 builder.py 文件进行测试,查看是否改进成功,如果没有报错,训练时候基本没有问题了

在这里插入图片描述

🐴三、论文常用的评估指标

图像分割任务本质上是对图像中的每一个像素点进行分类。评估指标主要使用:像素准确率(PA)、精确度(Precision)、召回率(Recall)、交并比(IoU)、平均交并比(mIoU) 和 Dice系数(Dice Coefficient) 来评估模型预测掩码(Mask)与真实掩码的重合度与精准度。

在混淆矩阵中,基本单位由“样本数”变为了像素点数

  • T P TP TP (True Positive,真正类):真实为某类别,模型预测也为该类别的像素点数
  • T N TN TN (True Negative,真负类):真实为其他类别,模型预测也为其他类别的像素点数
  • F P FP FP (False Positive,假正类):真实为其他类别,却被模型错误预测为该类别的像素点数(误分割/多抠了)。
  • F N FN FN (False Negative,假负类):真实为该类别,却被模型错误预测为其他类别的像素点数(漏分割/没抠全)。

☑️像素准确率 (Pixel Accuracy, PA)

像素准确率是指分类正确的像素点占图像总像素点的比例。它是最直观的评估指标,但在 U-Net 常用的医学图像分割中(例如病灶极小、背景极大),严重的类别不平衡会使 PA 产生“虚假的高分”,单独使用存在误导性。

公式:

P A = T P + T N T P + T N + F P + F N PA = \frac{TP + TN}{TP + TN + FP + FN} PA=TP+TN+FP+FNTP+TN

☑️精确率 (Precision) 与 召回率 (Recall)

在分割任务中,精确率和召回率往往是评估边缘和病灶捕捉能力的重要依据:

  • 精确率(Precision):在模型预测为目标区域的所有像素中,真正属于目标的像素比例。衡量模型分割的“精准程度”(有没有错杀)。
  • 召回率(Recall,在医学中也称灵敏度 Sensitivity):在所有真实的目标像素中,被模型成功提取出来的比例。衡量模型“捕捉目标”的能力(有没有漏网之鱼)。

公式:

P r e c i s i o n = T P T P + F P Precision = \frac{TP}{TP + FP} Precision=TP+FPTP

R e c a l l = T P T P + F N Recall = \frac{TP}{TP + FN} Recall=TP+FNTP

☑️交并比 (Intersection over Union, IoU)

交并比是语义分割领域最核心、最标准的绝对评价指标。它计算的是模型预测的目标区域(预测掩码)与真实目标区域(真实掩码)的交集面积与并集面积之比。它能极其严苛地同时惩罚误分割和漏分割行为。

公式:

I o U = T P T P + F P + F N IoU = \frac{TP}{TP + FP + FN} IoU=TP+FP+FNTP

☑️平均交并比 (Mean Intersection over Union, mIoU)

在多类别图像分割任务中,我们通常不仅关注单一(前景)类别的分割效果。mIoU 会分别计算出每一个类别(包括背景类和各个前景类)的 IoU,然后将所有类别的 IoU 累加起来求平均值。它能够全局且客观地评价模型在所有类别上的综合分割性能,是衡量多类语义分割模型最权威、也是发论文必带的指标。

公式:

m I o U = 1 N ∑ i = 1 N T P i T P i + F P i + F N i mIoU = \frac{1}{N} \sum_{i=1}^{N} \frac{TP_i}{TP_i + FP_i + FN_i} mIoU=N1i=1NTPi+FPi+FNiTPi

(注:其中 N N N 为数据集中包含背景在内的总类别数量, i i i 代表第 i i i 个类别)

☑️Dice系数 (Dice Coefficient / F1 Score)

Dice 系数(在分类任务中数学等价于 F1 Score)是 U-Net 原论文及医学图像分割中最权威、最常作为 Loss 优化的评估指标。它同样衡量预测掩码与真实掩码的空间重合度。由于公式中赋予了 T P TP TP 两倍的权重,它比 IoU 更加关注对目标前景的挖掘,是精确率和召回率的调和平均数。

公式:

D i c e = 2 × P r e c i s i o n × R e c a l l P r e c i s i o n + R e c a l l Dice = \frac{2 \times Precision \times Recall}{Precision + Recall} Dice=Precision+Recall2×Precision×Recall

将其展开为混淆矩阵像素点的形式为:

D i c e = 2 × T P 2 × T P + F P + F N Dice = \frac{2 \times TP}{2 \times TP + FP + FN} Dice=2×TP+FP+FN2×TP


总结

unet 模型改进算法内容持续更新中,记得点赞收藏起来,以免想看时候找不到。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐