【UNet 改进 | 注意机制篇】UNet引入ECA注意力机制(CVPR 2020),极轻量级跨通道交互,二次创新

本文教的是方法,也给出几种改进方法,二次创新结构,百变不离其宗,一文带你改进自己模型,科研路上少走弯路。
前言
在医学图像分割任务中,病灶区域往往形态各异、边界模糊,且经常与周围组织的对比度较低,这要求模型具备极强的特征提取和细节辨别能力。传统的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 机制通过以下创新实现破局:
- 无降维的全局信息聚合:ECA 首先对输入的特征图进行全局平均池化(GAP),获取每个通道的全局空间统计信息,但不进行任何降维处理,从而保留了完整的通道维度特征。
- 自适应一维卷积交互:在不改变通道数的情况下,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=1∑NTPi+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 模型改进算法内容持续更新中,记得点赞收藏起来,以免想看时候找不到。
更多推荐
所有评论(0)