本文详细介绍在YOLOv11中添加seaformer实现算法改进,帮助大家实现准确率提升。
论文地址:https://arxiv.org/abs/2301.13156
项目地址:https://github.com/fudan-zvg/SeaFormer
seaformer核心代码:
import torch
from torch import nn
import torch.nn.functional as F
def _make_divisible(v, divisor, min_value=None):
"""
This function is taken from the original tf repo.
It ensures that all layers have a channel number that is divisible by 8
It can be seen here:
https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py
:param v:
:param divisor:
:param min_value:
:return:
"""
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
# Make sure that round down does not go down by more than 10%.
if new_v < 0.9 * v:
new_v += divisor
return new_v
def drop_path(x, drop_prob: float = 0., training: bool = False):
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
This is the same as the DropConnect impl I created for EfficientNet, etc networks, however,
the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper...
See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for
changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use
'survival rate' as the argument.
"""
if drop_prob == 0. or not training:
return x
keep_prob = 1 - drop_prob
shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets
random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device)
random_tensor.floor_() # binarize
output = x.div(keep_prob) * random_tensor
return output
class DropPath(nn.Module):
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
"""
def __init__(self, drop_prob=None):
super(DropPath, self).__init__()
self.drop_prob = drop_prob
def forward(self, x):
return drop_path(x, self.drop_prob, self.training)
def get_shape(tensor):
shape = tensor.shape
if torch.onnx.is_in_onnx_export():
shape = [i.cpu().numpy() for i in shape]
return shape
class Conv2d_BN(nn.Sequential):
def __init__(self, a, b, ks=1, stride=1, pad=0, dilation=1,
groups=1, bn_weight_init=1, bias=False,
norm_cfg=dict(type='BN', requires_grad=True)):
super().__init__()
self.inp_channel = a
self.out_channel = b
self.ks = ks
self.pad = pad
self.stride = stride
self.dilation = dilation
self.groups = groups
# self.bias = bias
self.add_module('c', nn.Conv2d(
a, b, ks, stride, pad, dilation, groups, bias=bias))
self.add_module('bn', nn.BatchNorm2d(self.out_channel, eps=1e-5, momentum=0.1, affine=True))
torch.nn.init.constant_(self.bn.weight, bn_weight_init)
torch.nn.init.constant_(self.bn.bias, 0)
class Mlp(nn.Module):
def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.ReLU, drop=0.,
norm_cfg=dict(type='BN', requires_grad=True)):
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.fc1 = Conv2d_BN(in_features, hidden_features, norm_cfg=norm_cfg)
self.dwconv = nn.Conv2d(hidden_features, hidden_features, 3, 1, 1, bias=True, groups=hidden_features)
self.act = act_layer()
self.fc2 = Conv2d_BN(hidden_features, out_features, norm_cfg=norm_cfg)
self.drop = nn.Dropout(drop)
def forward(self, x):
x = self.fc1(x)
x = self.dwconv(x)
x = self.act(x)
x = self.drop(x)
x = self.fc2(x)
x = self.drop(x)
return x
class InvertedResidual(nn.Module):
def __init__(
self,
inp: int,
oup: int,
ks: int,
stride: int,
expand_ratio: int,
activations=None,
norm_cfg=dict(type='BN', requires_grad=True)
) -> None:
super(InvertedResidual, self).__init__()
self.stride = stride
self.expand_ratio = expand_ratio
assert stride in [1, 2]
if activations is None:
activations = nn.ReLU
hidden_dim = int(round(inp * expand_ratio)) # 四舍五入
self.use_res_connect = self.stride == 1 and inp == oup
layers = []
if expand_ratio != 1:
# pw
layers.append(Conv2d_BN(inp, hidden_dim, ks=1, norm_cfg=norm_cfg))
layers.append(activations())
layers.extend([
# dw
Conv2d_BN(hidden_dim, hidden_dim, ks=ks, stride=stride, pad=ks // 2, groups=hidden_dim, norm_cfg=norm_cfg),
activations(),
# pw-linear
Conv2d_BN(hidden_dim, oup, ks=1, norm_cfg=norm_cfg)
])
self.conv = nn.Sequential(*layers)
self.out_channels = oup
self._is_cn = stride > 1
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class StackedMV2Block(nn.Module):
def __init__(
self,
cfgs,
stem,
inp_channel=16,
activation=nn.ReLU,
norm_cfg=dict(type='BN', requires_grad=True),
width_mult=1.):
super().__init__()
self.stem = stem
if stem:
self.stem_block = nn.Sequential(
Conv2d_BN(3, inp_channel, 3, 2, 1, norm_cfg=norm_cfg),
activation()
)
self.cfgs = cfgs
self.layers = []
for i, (k, t, c, s) in enumerate(cfgs):
output_channel = _make_divisible(c * width_mult, 8)
exp_size = t * inp_channel
exp_size = _make_divisible(exp_size * width_mult, 8)
layer_name = 'layer{}'.format(i + 1)
layer = InvertedResidual(inp_channel, output_channel, ks=k, stride=s, expand_ratio=t, norm_cfg=norm_cfg,
activations=activation)
self.add_module(layer_name, layer)
inp_channel = output_channel
self.layers.append(layer_name)
def forward(self, x):
if self.stem:
x = self.stem_block(x)
for i, layer_name in enumerate(self.layers):
layer = getattr(self, layer_name)
x = layer(x)
return x
class SqueezeAxialPositionalEmbedding(nn.Module):
def __init__(self, dim, shape):
super().__init__()
self.pos_embed = nn.Parameter(torch.randn([1, dim, shape]))
def forward(self, x):
B, C, N = x.shape
x = x + F.interpolate(self.pos_embed, size=(N), mode='linear', align_corners=False)
# print(x.shape) #torch.Size([1, 1024, 20])
return x
class Sea_Attention(torch.nn.Module):
def __init__(self, dim, key_dim, num_heads,
attn_ratio=2,
activation=nn.ReLU,
norm_cfg=dict(type='BN', requires_grad=True), ):
super().__init__()
self.num_heads = num_heads
self.scale = key_dim ** -0.5
self.key_dim = key_dim
self.nh_kd = nh_kd = key_dim * num_heads # num_head key_dim
self.d = int(attn_ratio * key_dim)
self.dh = int(attn_ratio * key_dim) * num_heads
self.attn_ratio = attn_ratio
self.to_q = Conv2d_BN(dim, nh_kd, 1, norm_cfg=norm_cfg)
self.to_k = Conv2d_BN(dim, nh_kd, 1, norm_cfg=norm_cfg)
self.to_v = Conv2d_BN(dim, self.dh, 1, norm_cfg=norm_cfg)
self.proj = torch.nn.Sequential(activation(), Conv2d_BN(
self.dh, dim, bn_weight_init=0, norm_cfg=norm_cfg))
self.proj_encode_row = torch.nn.Sequential(activation(), Conv2d_BN(
self.dh, self.dh, bn_weight_init=0, norm_cfg=norm_cfg))
self.pos_emb_rowq = SqueezeAxialPositionalEmbedding(nh_kd, 16)
self.pos_emb_rowk = SqueezeAxialPositionalEmbedding(nh_kd, 16)
self.proj_encode_column = torch.nn.Sequential(activation(), Conv2d_BN(
self.dh, self.dh, bn_weight_init=0, norm_cfg=norm_cfg))
self.pos_emb_columnq = SqueezeAxialPositionalEmbedding(nh_kd, 16)
self.pos_emb_columnk = SqueezeAxialPositionalEmbedding(nh_kd, 16)
self.dwconv = Conv2d_BN(self.dh + 2 * self.nh_kd, 2 * self.nh_kd + self.dh, ks=3, stride=1, pad=1, dilation=1,
groups=2 * self.nh_kd + self.dh, norm_cfg=norm_cfg)
self.act = activation()
self.pwconv = Conv2d_BN(2 * self.nh_kd + self.dh, dim, ks=1, norm_cfg=norm_cfg)
self.sigmoid = h_sigmoid()
def forward(self, x):
B, C, H, W = x.shape
q = self.to_q(x)
k = self.to_k(x)
v = self.to_v(x)
qkv = torch.cat([q, k, v], dim=1)
qkv = self.act(self.dwconv(qkv))
qkv = self.pwconv(qkv)
qrow = self.pos_emb_rowq(q.mean(-1)).reshape(B, self.num_heads, -1, H).permute(0, 1, 3, 2)
krow = self.pos_emb_rowk(k.mean(-1)).reshape(B, self.num_heads, -1, H)
vrow = v.mean(-1).reshape(B, self.num_heads, -1, H).permute(0, 1, 3, 2)
attn_row = torch.matmul(qrow, krow) * self.scale
attn_row = attn_row.softmax(dim=-1)
xx_row = torch.matmul(attn_row, vrow) # B nH H C
xx_row = self.proj_encode_row(xx_row.permute(0, 1, 3, 2).reshape(B, self.dh, H, 1))
qcolumn = self.pos_emb_columnq(q.mean(-2)).reshape(B, self.num_heads, -1, W).permute(0, 1, 3, 2)
kcolumn = self.pos_emb_columnk(k.mean(-2)).reshape(B, self.num_heads, -1, W)
vcolumn = v.mean(-2).reshape(B, self.num_heads, -1, W).permute(0, 1, 3, 2)
attn_column = torch.matmul(qcolumn, kcolumn) * self.scale
attn_column = attn_column.softmax(dim=-1)
xx_column = torch.matmul(attn_column, vcolumn) # B nH W C
xx_column = self.proj_encode_column(xx_column.permute(0, 1, 3, 2).reshape(B, self.dh, 1, W))
xx = xx_row.add(xx_column) # 两个张量相加
xx = v.add(xx)
xx = self.proj(xx)
xx = self.sigmoid(xx) * qkv
return xx
class SeaBlock(nn.Module):
def __init__(self, dim, c2, key_dim=16, num_heads=4, mlp_ratio=4., attn_ratio=2., drop=0.,
drop_path=0., act_layer=nn.ReLU, norm_cfg=dict(type='BN2d', requires_grad=True)):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.mlp_ratio = mlp_ratio
self.attn = Sea_Attention(dim, key_dim=key_dim, num_heads=num_heads, attn_ratio=attn_ratio,
activation=act_layer, norm_cfg=norm_cfg)
# NOTE: drop path for stochastic depth, we shall see if this is better than dropout here
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
mlp_hidden_dim = int(dim * mlp_ratio)
self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop,
norm_cfg=norm_cfg)
def forward(self, x1):
x1 = x1 + self.drop_path(self.attn(x1))
x1 = x1 + self.drop_path(self.mlp(x1))
return x1
class h_sigmoid(nn.Module):
def __init__(self, inplace=True):
super(h_sigmoid, self).__init__()
self.relu = nn.ReLU6(inplace=inplace)
def forward(self, x):
return self.relu(x + 3) / 6
if __name__ == '__main__':
model = SeaBlock(256, 256)
input = torch.randn(1, 256, 20, 20)
output = model(input)
print(output.shape)
seaformer核心思想分析:
SeaFormer的核心思想在于设计一种轻量且高效的Transformer架构,特别适合于移动设备上的语义分割任务。该方法主要解决了传统Transformer在处理高分辨率图像时计算成本高和内存需求大的问题,同时保持甚至提升了模型的性能。
1. Squeeze-Enhanced Axial Attention (SEA Attention):
- SeaFormer引入了一种新颖的注意力机制——挤压增强轴向注意力(SEA Attention),旨在降低传统全局自注意力机制的计算复杂度。
- 通过将输入特征图沿水平或垂直轴压缩成紧凑的列或行来计算自注意力,从而减少计算量。
- 在进行挤压操作后,为了补偿细节信息的丢失,SeaFormer通过连接查询(Query)、键(Key)和值(Value)并经过深度卷积层来增强局部细节。
总的来说,SeaFormer通过创新性地结合挤压操作与轴向注意力机制,在不牺牲性能的前提下大幅降低了计算成本和内存需求,提供了一个新的方向来优化Transformer模型在移动设备上的部署。这一策略不仅提高了效率,而且增强了模型捕捉全局信息的能力,同时通过细节增强模块补充了局部细节,确保了高质量的语义分割结果。
在YOLOv11中添加seaformer后的配置文件:
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
# Ultralytics YOLO11 object detection model with P3/8 - P5/32 outputs
# Model docs: https://docs.ultralytics.com/models/yolo11
# Task docs: https://docs.ultralytics.com/tasks/detect
# Parameters
nc: 80 # number of classes
scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
# [depth, width, max_channels]
n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs
# YOLO11n backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 2, C3k2, [256, False, 0.25]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 2, C3k2, [512, False, 0.25]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 2, C3k2, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 2, C3k2, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
- [-1, 2, SeaBlock, [1024]] # 10
# YOLO11n head
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 2, C3k2, [512, False]] # 13
- [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 13], 1, Concat, [1]] # cat head P4
- [-1, 2, C3k2, [512, False]] # 19 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 10], 1, Concat, [1]] # cat head P5
- [-1, 2, C3k2, [1024, True]] # 22 (P5/32-large)
- [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)