卷积与互相关 对于离散信号,卷积的定义为:
其中g就是滤波器(卷积核),公式的理解就是将g先进行-τ的翻转,再进行+n的滑动以对其输入信号f,卷积操作的输出即为经过滤波器的输出信号
而在深度学习中,卷积核的翻转其实是不必要的,因此省去了翻转,直接对卷积核进行滑动的操作,也就是互相关操作
来源:【CNN】很详细的讲解什么以及为什么是卷积(Convolution)!-腾讯云开发者社区-腾讯云
卷积核根据参数的不同可以实现提取低频、高频特征(边缘特征),平滑、锐化处理等操作
参数理解 数据形状 以二维卷积为例,在pytorch中,conv2d的定义为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def __init__ ( self, in_channels: int , out_channels: int , kernel_size: _size_2_t, stride: _size_2_t = 1 , padding: Union [str , _size_2_t] = 0 , dilation: _size_2_t = 1 , groups: int = 1 , bias: bool = True , padding_mode: str = 'zeros' , device=None , dtype=None )
in_channels决定了卷积核的深度 ,每层卷积核输出的数据最终会相加,也就是说一个卷积核输出一个单通道数据
out_channels决定了卷积核的个数 ,决定生成多少个上条中说到的单通道数据
kernel_size决定了每个卷积核的长宽 (参数量)
这三个参数共同决定了该卷积层的参数量,具体图片等参考:(1 封私信 / 80 条消息) Depthwise卷积与Pointwise卷积 - 知乎
对于一维卷积,当不考虑channels的改变(且为1)时,卷积核为1*k时等价于对其进行一维卷积;例:对于形状为(b,t,f)的频谱图,直接对其进行一维卷积就是在对其frequency bin进行卷积操作
感受野大小与步长
非11卷积核:`3 3和5*5`具有较好的细粒度特征提取能力且计算开销适中,对于有特殊要求的数据也可以使用不规则形状;而使用大卷积核可以获取更大的局部感受野,对大面积的特征会更加敏感,但计算量也会变大
11卷积核:`1 1`目前我见到过的有如下几种用法:1.用于对数据进行单纯的channel维度上\下采样;2.用于Depthwise Separable Convolution中的pointwise 卷积;3.用于代替某些attention结构中qkv的linear计算;4.inception模块中的多感受野融合
对于步长,常用的就是1和2,1适合用于特征提取阶段;选择2时输出形状会缩减约一半,常用于下采样
卷积种类 常规卷积 卷积权重形状为(out_channels, inchannels, H, W),可以理解为有out_channels个(inchannels, H, W)的卷积核。
常规卷积group=1,具体看下一小节
Depthwise Separable Convolution 由pointwise conv 和 depthwise conv组合而成(先depth再pointwise)。其中,pointwise conv就是卷积核为1*1的常规卷积,在efficient net中其扩大了输出channels;而depthwise conv 则是通过调整nn.conv2d的groups选项实现:
卷积权重形状完整写法其实为(out_channels, inchannels/groups, H, W),对于depthwise conv:groups=in_channels=out_channels ,groups必须要能被in_channels整除;
举个例子,对于一个in_channels=4的数据来说,先将其分为4个channel=1的数据,因此每个卷积核的大小只能为(1,H,W);而对于卷积核的个数,每组数据分到out_channels/groups个(depthwise中为该结果为1),最后拼接起来即为out_channels
Depthwise Separable Convolution可以有效缩减参数量,以达到更深的网络结构:假设in/out_channels=4,卷积核3*3:
常规卷积参数:4*4*3*3=144
Depthwise Separable:4*4*1*1+4*1*3*3=52
转置卷积 通过对输入进行padding的方式,可以实现输入H、W维度的上采样,生成更高分辨率的数据
搭建EfficientNet_v2 本节以efficientnetv2_s为例
主模型类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 class EfficientNetV2 (nn.Module): def __init__ (self, model_cnf: list , num_classes: int = 1000 , num_features: int = 1280 , dropout_rate: float = 0.2 , drop_connect_rate: float = 0.2 ): super (EfficientNetV2, self).__init__() for cnf in model_cnf: assert len (cnf) == 8 、 norm_layer = partial(nn.BatchNorm2d, eps=1e-3 , momentum=0.1 ) stem_filter_num = model_cnf[0 ][4 ] self.stem = ConvBNAct(3 , stem_filter_num, kernel_size=3 , stride=2 , norm_layer=norm_layer) total_blocks = sum ([i[0 ] for i in model_cnf]) block_id = 0 blocks = [] for cnf in model_cnf: repeats = cnf[0 ] op = FusedMBConv if cnf[-2 ] == 0 else MBConv for i in range (repeats): blocks.append(op(kernel_size=cnf[1 ], input_c=cnf[4 ] if i == 0 else cnf[5 ], out_c=cnf[5 ], expand_ratio=cnf[3 ], stride=cnf[2 ] if i == 0 else 1 , se_ratio=cnf[-1 ], drop_rate=drop_connect_rate * block_id / total_blocks, norm_layer=norm_layer)) block_id += 1 self.blocks = nn.Sequential(*blocks) head_input_c = model_cnf[-1 ][-3 ] head = OrderedDict() head.update({"project_conv" : ConvBNAct(head_input_c, num_features, kernel_size=1 , norm_layer=norm_layer)}) head.update({"avgpool" : nn.AdaptiveAvgPool2d(1 )}) head.update({"flatten" : nn.Flatten()}) if dropout_rate > 0 : head.update({"dropout" : nn.Dropout(p=dropout_rate, inplace=True )}) head.update({"classifier" : nn.Linear(num_features, num_classes)}) self.head = nn.Sequential(head) for m in self.modules(): if isinstance (m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode="fan_out" ) if m.bias is not None : nn.init.zeros_(m.bias) elif isinstance (m, nn.BatchNorm2d): nn.init.ones_(m.weight) nn.init.zeros_(m.bias) elif isinstance (m, nn.Linear): nn.init.normal_(m.weight, 0 , 0.01 ) nn.init.zeros_(m.bias) def forward (self, x: Tensor ) -> Tensor: x = self.stem(x) x = self.blocks(x) x = self.head(x) return x
MBConv 对于v2版本,其前半部分网络使用的fuse-MBConv将原1*1conv+depthwise替换为了一个33conv,不再额外说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 class MBConv (nn.Module): def __init__ (self, kernel_size: int , input_c: int , out_c: int , expand_ratio: int , stride: int , se_ratio: float , drop_rate: float , norm_layer: Callable [..., nn.Module] ): super (MBConv, self).__init__() if stride not in [1 , 2 ]: raise ValueError("illegal stride value." ) self.has_shortcut = (stride == 1 and input_c == out_c) activation_layer = nn.SiLU expanded_c = input_c * expand_ratio assert expand_ratio != 1 self.expand_conv = ConvBNAct(input_c, expanded_c, kernel_size=1 , norm_layer=norm_layer, activation_layer=activation_layer) self.dwconv = ConvBNAct(expanded_c, expanded_c, kernel_size=kernel_size, stride=stride, groups=expanded_c, norm_layer=norm_layer, activation_layer=activation_layer) self.se = SqueezeExcite(input_c, expanded_c, se_ratio) if se_ratio > 0 else nn.Identity() self.project_conv = ConvBNAct(expanded_c, out_planes=out_c, kernel_size=1 , norm_layer=norm_layer, activation_layer=nn.Identity) self.out_channels = out_c self.drop_rate = drop_rate if self.has_shortcut and drop_rate > 0 : self.dropout = DropPath(drop_rate) def forward (self, x: Tensor ) -> Tensor: result = self.expand_conv(x) result = self.dwconv(result) result = self.se(result) result = self.project_conv(result) if self.has_shortcut: if self.drop_rate > 0 : result = self.dropout(result) result += x return result
SE模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class SqueezeExcite (nn.Module): def __init__ (self, input_c: int , expand_c: int , se_ratio: float = 0.25 ): super (SqueezeExcite, self).__init__() squeeze_c = int (input_c * se_ratio) self.conv_reduce = nn.Conv2d(expand_c, squeeze_c, 1 ) self.act1 = nn.SiLU() self.conv_expand = nn.Conv2d(squeeze_c, expand_c, 1 ) self.act2 = nn.Sigmoid() def forward (self, x: Tensor ) -> Tensor: scale = x.mean((2 , 3 ), keepdim=True ) scale = self.conv_reduce(scale) scale = self.act1(scale) scale = self.conv_expand(scale) scale = self.act2(scale) return scale * x