论文地址:
《CondConv: Conditionally Parameterized Convolutions for Efficient Inference》
问题
常规卷积具有权值共享的特点,即所有的样本共享卷积网络中的卷积参数,因此,若想提升模型的容量,就需要增加网络的深度或者宽度,这会导致模型的计算量和参数量增加,模型部署难度大;然而,在某些cv应用中,要求模型的实时性高,及需要模型拥有较低的参数量和计算量。
思想
作者提出一种条件参数卷积用于解决上述问题,它通过输入计算卷积核参数打破了传统的静态卷积特性。作者将CondConv中的卷积核参数化为多个专家知识的线性组(a1,a2,a3,...an是通过梯度下降法学习的加权系数,x为输入tensor):
(a1W1+a2W2+...+anWn)*x
为更有效的提升模型容量,在网络设计过程中可以提升专家数量,这比提升卷积核尺寸更为高效,同时专家知识只需要进行一次组合,这就可以在提升模型容量的同时保持高效推理。
结构
(1)采用更细粒度的集成方式,每一个卷积层都拥有多套权重,卷积层的输入分别经过不同的权重卷积之后组合输出,缺点是计算量依旧很大;
(2)作者提出既然输入相同,卷积是一种线性计算,COMBINE也是一个线性计算(比如加权求和),作者将多套权重加权组合之后,只做一次卷积就能完成相当的效果,计算量相比(1)大大降低.;
CondConv在卷积层设置多套卷积核,在推断时对卷积核施加SE模块,根据卷积核的输入决定各套卷积核的权重,最终加权求和得到一个为输入特定的卷积核组,最后执行一次卷积即可。
原理
常规卷积的卷积核参数经训练确定且对所有输入样本权值共享;而在CondConv
中,卷积核参数通过对输入进行变换得到 :
其中一个样本依赖加权参数。在CondConv
中,每个卷积核具有与标准卷积核参数相同的维度。
常规卷积的容量提升依赖于卷积核尺寸与通道数的提升,会进一步提升的网络的整体计算;
CondConv
则只需要在执行卷积计算之前通过多个专家知识对输入样本计算加权卷积核,并且每个卷积核只需计算一次并作用于不同位置即可,这意味着:通过提升专家知识数据量达到提升网络容量的目的,而代码仅仅是很小的推理耗时,每个额外参数仅需一次乘加。
CondConv
等价于多个静态卷积的线性组合,因此它具有与n个专家知识同等的维度,但计算更为高效。而这其中最关键是:加权参数。它必须具有数据依赖,否则CondConv
将等价于静态卷积。那么如何设计该数据依赖型参数呢?作者采用三个步骤计算该参数:
全局池化GAP、全连接FC、激活函数sigmoid:
CondConv
可以对现有网络中的标准卷积进行替换,它同时适用于深度卷积与全连接层。
实验
CondConv与Conv在不同网络结构上的对比:
对比提升专家数量对模型性能的影响:
在MobileNetV1上进行实验,0.25、0.5、0.75、1.0表示分别使用1、2、4、8个experts
实现(tensorflow)
输入是X,大小是(N,H,W,C),其中(h,w,cin,cout)表示卷积核大小、h和w分别表示卷积核的高和宽、cin,cout分别表示卷积核的输入和输出通道数;
CondConv:(右边)计算输入样本对各自的卷积核的权重,具体流程如下:
- 对输入X,进行GAP操作(GlobalAveragePooling2D),即改变输入feature map的宽高,输出(N,C);
- 之后经过FC层,学习不同输入样本对用num_experts个卷积各自的权重系数,输出为(N,num_experts);
- 采用Sigmoid归一化到(0,1)之间,输出为(N,num_experts);
- 将上述输出权重系数和num_experts个卷积核权重通过矩阵的相乘,赋予到相应的卷积上,输出各个样本对应加权后的卷积核权重,输出大小为(N,h*w*cin,cout);
- 将输出在N维度进行Split操作,得到各个样本对应加权后卷积核的权重。
完成CondConv:(左边)对输入X依次通过对应加权输出的卷积核权重:
- 将X在N维度进行split操作;
- 将上述输出结果和右边线输出对应卷积权重进行卷积操作,之后进行Concat,完成CondConv操作。
代码:
import torch
import torch.nn.functional as F
import torch.nn as nn
from torch import Tensor
import functools
from torch.nn.modules.conv import _ConvNd
from torch.nn.modules.utils import _pair
from torch.nn.parameter import Parameter
class _routing(nn.Module):
def __init__(self, in_channels, num_experts, dropout_rate):
super(_routing, self).__init__()
self.dropout = nn.Dropout(dropout_rate)
self.fc = nn.Linear(in_channels, num_experts)
def forward(self, x):
x = torch.flatten(x)
x = self.dropout(x)
x = self.fc(x)
return F.sigmoid(x)
class CondConv2D(_ConvNd):
def __init__(self, in_channels, out_channels, kernel_size,
stride=1, padding=0, dilation=1, groups=1,
bias=True, padding_mode='zeros',
num_experts=3, dropout_rate=0.2):
# tuple
kernel_size = _pair(kernel_size)
stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
super(CondConv2D, self).__init__(
in_channels, out_channels, kernel_size, stride, padding, dilation,
False, _pair(0), groups, bias, padding_mode)
self._avg_pooling = functools.partial(F.adaptive_avg_pool2d, output_size=(1, 1))
self._routing_fn = _routing(in_channels, num_experts, dropout_rate)
self.weight = Parameter(torch.Tensor(
num_experts, out_channels, in_channels // groups, *kernel_size))
self.reset_parameters()
def _conv_forward(self, input, weight):
if self.padding_mode != 'zeros':
return F.conv2d(F.pad(input, self._padding_repeated_twice, mode=self.padding_mode),
weight, self.bias, self.stride,
_pair(0), self.dilation, self.groups)
return F.conv2d(input, weight, self.bias, self.stride,
self.padding, self.dilation, self.groups)
def forward(self, inputs):
b, _, _, _ = inputs.size()
res = []
for input in inputs:
input = input.unsqueeze(0)
pooled_inputs = self._avg_pooling(input)
routing_weights = self._routing_fn(pooled_inputs)
kernels = torch.sum(routing_weights[: ,None, None, None, None] * self.weight, 0)
out = self._conv_forward(input, kernels)
res.append(out)
return torch.cat(res, dim=0)
总结
不同输入具有不同特征,采用不同的卷积核参数进行调整。
参考:
仅为学习记录,侵删!