最近在项目开发中有需要用到轻量化相关的内容,那必定是绕不开深度可分离卷积的,这里记录自己的学习记录和实践内容。
深度可分离卷积(Depthwise Separable Convolution)是一种轻量化的卷积操作,广泛应用于移动设备和嵌入式设备上的深度学习模型(如 MobileNet 和 Xception)。它将标准卷积分解为两步:深度卷积(Depthwise Convolution, DW) 和 逐点卷积(Pointwise Convolution, PW)。
1. 深度可分离卷积的原理
(1)标准卷积
标准卷积的计算复杂度较高,尤其是当输入通道数和输出通道数较大时。
(2)深度可分离卷积
深度可分离卷积将标准卷积分解为两步:
2. 深度可分离卷积的实现
(1)深度卷积(DW)的实现
深度卷积的实现步骤:
-
对每个输入通道独立进行卷积。
-
每个输入通道的卷积结果保持独立。
(2)逐点卷积(PW)的实现
逐点卷积的实现步骤:
-
使用 1×1 的卷积核在通道维度上进行线性组合。
-
输出特征图的通道数由逐点卷积核的输出通道数决定。
3. 深度可分离卷积的优缺点
(1)优点
1. 参数量大幅减少
-
深度可分离卷积的参数量显著低于标准卷积。
-
参数量计算公式:
2. 计算量大幅减少
-
深度可分离卷积的计算量显著低于标准卷积。
-
计算量计算公式:
3. 适合轻量化模型
-
深度可分离卷积广泛应用于移动设备和嵌入式设备上的轻量化模型(如 MobileNet 和 Xception)。
-
它能够在保持较好特征提取能力的同时,大幅减少参数量和计算量。
(2)缺点
1. 特征表达能力受限
-
深度卷积(DW)对每个输入通道独立进行卷积,缺乏通道间的信息交互。
-
逐点卷积(PW)虽然能够进行通道间的信息融合,但其计算量仍然较大。
2. 不适合所有任务
-
深度可分离卷积在某些任务(如图像分类)中表现优异,但在某些任务(如图像分割)中可能表现不如标准卷积。
4. 深度可分离卷积的应用
(1)MobileNet
-
MobileNet 是深度可分离卷积的经典应用。
-
它通过深度可分离卷积大幅减少了参数量和计算量,同时保持了较好的特征提取能力。
(2)Xception
-
Xception 是另一种广泛使用深度可分离卷积的模型。
-
它通过深度可分离卷积实现了高效的特征提取。
(3)轻量化模型
-
深度可分离卷积广泛应用于移动设备和嵌入式设备上的轻量化模型。
-
它能够在保持较好特征提取能力的同时,大幅减少参数量和计算量。
5. 总结
深度可分离卷积的原理
-
将标准卷积分解为深度卷积(DW)和逐点卷积(PW)。
-
深度卷积对每个输入通道独立进行卷积,逐点卷积在通道维度上进行线性组合。
深度可分离卷积的实现
-
深度卷积(DW):对每个输入通道独立进行卷积。
-
逐点卷积(PW):使用 1×11×1 的卷积核在通道维度上进行线性组合。
深度可分离卷积的优缺点
-
优点:参数量和计算量大幅减少,适合轻量化模型。
-
缺点:特征表达能力受限,不适合所有任务。
深度可分离卷积的应用
-
广泛应用于移动设备和嵌入式设备上的轻量化模型(如 MobileNet 和 Xception)。
6. 代码实践
回顾了深度可分离卷积的相关理论内容后,接下来通过动手实践来进一步加深理解。
1、首先我们不使用任何深度学习模型的框架来实现深度可分离卷积
这里主要是包括:DW卷积实现、PW卷积实现和深度可分离卷积实现,基于Nump模块来完成对应的开发工作,代码如下:
def depthwiseConv2d(input_feature_map, depthwise_kernel, stride=1, padding=0):
"""
深度卷积(Depthwise Convolution)
:param input_feature_map: 输入特征图,形状为 (H, W, C_in)
:param depthwise_kernel: 深度卷积核,形状为 (K_h, K_w, 1, 1)
:param stride: 卷积步幅
:param padding: 填充大小
:return: 输出特征图,形状为 (H', W', C_in)
"""
H, W, C_in = input_feature_map.shape
K_h, K_w, _, _ = depthwise_kernel.shape
# 填充输入特征图
if padding > 0:
input_feature_map = np.pad(
input_feature_map,
((padding, padding), (padding, padding), (0, 0)),
mode="constant",
)
H_pad, W_pad, _ = input_feature_map.shape
else:
H_pad, W_pad = H, W
# 计算输出特征图的尺寸
H_out = (H_pad - K_h) // stride + 1
W_out = (W_pad - K_w) // stride + 1
# 初始化输出特征图
output_feature_map = np.zeros((H_out, W_out, C_in))
# 执行深度卷积
for c in range(C_in):
for i in range(0, H_pad - K_h + 1, stride):
for j in range(0, W_pad - K_w + 1, stride):
# 提取局部区域
region = input_feature_map[i : i + K_h, j : j + K_w, c]
# 计算卷积结果
output_feature_map[i // stride, j // stride, c] = np.sum(
region * depthwise_kernel[:, :, 0, 0]
)
return output_feature_map
def pointwiseConv2d(input_feature_map, pointwise_kernel):
"""
逐点卷积(Pointwise Convolution)
:param input_feature_map: 输入特征图,形状为 (H, W, C_in)
:param pointwise_kernel: 逐点卷积核,形状为 (1, 1, C_in, C_out)
:return: 输出特征图,形状为 (H, W, C_out)
"""
H, W, C_in = input_feature_map.shape
_, _, _, C_out = pointwise_kernel.shape
# 初始化输出特征图
output_feature_map = np.zeros((H, W, C_out))
# 执行逐点卷积
for c_out in range(C_out):
for c_in in range(C_in):
output_feature_map[:, :, c_out] += (
input_feature_map[:, :, c_in] * pointwise_kernel[0, 0, c_in, c_out]
)
return output_feature_map
def depthwiseSeparableConv2dNumpy(
input_feature_map, depthwise_kernel, pointwise_kernel, stride=1, padding=0
):
"""
深度可分离卷积(Depthwise Separable Convolution)
:param input_feature_map: 输入特征图,形状为 (H, W, C_in)
:param depthwise_kernel: 深度卷积核,形状为 (K_h, K_w, 1, 1)
:param pointwise_kernel: 逐点卷积核,形状为 (1, 1, C_in, C_out)
:param stride: 卷积步幅
:param padding: 填充大小
:return: 输出特征图,形状为 (H', W', C_out)
"""
# 第一步:深度卷积(DW)
depthwise_output = depthwiseConv2d(
input_feature_map, depthwise_kernel, stride, padding
)
# 第二步:逐点卷积(PW)
pointwise_output = pointwiseConv2d(depthwise_output, pointwise_kernel)
return pointwise_output
实例调用执行代码如下:
# 基于numpy实现
input_feature_map = np.random.randn(32, 32, 3) # 输入特征图,形状为 (32, 32, 3)
# 深度卷积核
depthwise_kernel = np.random.randn(3, 3, 1, 1) # 深度卷积核,形状为 (3, 3, 1, 1)
# 逐点卷积核
pointwise_kernel = np.random.randn(1, 1, 3, 16) # 逐点卷积核,形状为 (1, 1, 3, 16)
# 执行深度可分离卷积
output_feature_map = depthwiseSeparableConv2dNumpy(
input_feature_map, depthwise_kernel, pointwise_kernel, stride=1, padding=1
)
print("输出特征图的形状:", output_feature_map.shape)
# 计算参数量
depthwise_params = np.prod(depthwise_kernel.shape) # 深度卷积的参数量
pointwise_params = np.prod(pointwise_kernel.shape) # 逐点卷积的参数量
total_params = depthwise_params + pointwise_params # 总参数量
# 计算计算量
H, W, C_in = input_feature_map.shape
K_h, K_w, _, _ = depthwise_kernel.shape
_, _, _, C_out = pointwise_kernel.shape
H_out = (H + 2 * 1 - K_h) // 1 + 1 # 深度卷积的输出特征图大小
W_out = (W + 2 * 1 - K_w) // 1 + 1
depthwise_flops = H_out * W_out * K_h * K_w * C_in # 深度卷积的计算量
pointwise_flops = H_out * W_out * C_in * C_out # 逐点卷积的计算量
total_flops = depthwise_flops + pointwise_flops # 总计算量
print("深度卷积的参数量:", depthwise_params)
print("逐点卷积的参数量:", pointwise_params)
print("总参数量:", total_params)
print("深度卷积的计算量:", depthwise_flops)
print("逐点卷积的计算量:", pointwise_flops)
print("总计算量:", total_flops)
运行输出如下所示:
2、使用Pytorch框架来实现
这里主要开始借助于主流的深度学习框架比如Pytorch来进行深度可分离卷积块的实现,并分析预支对应的参数量和计算量,代码实现如下所示:
class DepthwiseSeparableConvPytorch(nn.Module):
def __init__(
self, input_channels, output_channels, kernel_size=3, stride=1, padding=1
):
super(DepthwiseSeparableConvPytorch, self).__init__()
# 深度卷积(Depthwise Convolution)
self.depthwise = nn.Conv2d(
in_channels=input_channels,
out_channels=input_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
groups=input_channels, # 分组卷积,每个通道独立卷积
bias=False,
)
# 逐点卷积(Pointwise Convolution)
self.pointwise = nn.Conv2d(
in_channels=input_channels,
out_channels=output_channels,
kernel_size=1,
stride=1,
padding=0,
bias=False,
)
# 批归一化
self.bn = nn.BatchNorm2d(output_channels)
# 激活函数
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
# 深度卷积
x = self.depthwise(x)
# 逐点卷积
x = self.pointwise(x)
# 批归一化
x = self.bn(x)
# 激活函数
x = self.relu(x)
return x
实例调用执行如下所示:
#使用Pytorch来实现
input_channels = 3 # 输入通道数
output_channels = 16 # 输出通道数
model = DepthwiseSeparableConvPytorch(input_channels, output_channels)
# 打印模型结构
print(model)
# 测试模型
input_tensor = torch.randn(1, 3, 32, 32) # 输入张量,形状为 (batch_size, C_in, H, W)
output_tensor = model(input_tensor)
print("输出张量的形状:", output_tensor.shape)
#计算参数量
summary(model, input_size=(3, 32, 32))
执行输出如下所示:
3、使用Keras框架来实现
Keras框架本身内置的有深度可分离卷积层可以直接使用,如下所示:
def depthwise_separable_conv_keras(
input_shape, output_channels, kernel_size=3, stride=1, padding="same"
):
"""
使用 Keras 实现深度可分离卷积
:param input_shape: 输入特征图的形状 (H, W, C_in)
:param output_channels: 输出通道数
:param kernel_size: 卷积核大小
:param stride: 卷积步幅
:param padding: 填充方式 ('same' 或 'valid')
:return: Keras 模型
"""
# 输入层
inputs = Input(shape=input_shape)
# 深度可分离卷积层
x = SeparableConv2D(
filters=output_channels,
kernel_size=kernel_size,
strides=stride,
padding=padding,
depth_multiplier=1, # 深度卷积的输出通道数是输入通道数的倍数
use_bias=False,
)(inputs)
# 批归一化
x = BatchNormalization()(x)
# 激活函数
x = ReLU()(x)
# 构建模型
model = Model(inputs, x)
return model
实例调用执行如下所示:
#使用Keras内置的SeparableConv2D层来实现
input_shape = (32, 32, 3) # 输入特征图的形状 (H, W, C_in)
output_channels = 16 # 输出通道数
model = depthwise_separable_conv_keras(input_shape, output_channels)
model.summary()
代码执行输出如下所示:
4、不使用Keras内置的SeparableConv2D层进行实现
这里主要是参照Pytorch的实现思路使用普通的卷积层来实现深度可分离卷积,代码实现如下所示:
class DepthwiseSeparableConvKeras(Layer):
def __init__(self, output_channels, kernel_size=3, stride=1, padding="same"):
"""
自定义深度可分离卷积层
:param output_channels: 输出通道数
:param kernel_size: 卷积核大小
:param stride: 卷积步幅
:param padding: 填充方式 ('same' 或 'valid')
"""
super(DepthwiseSeparableConvKeras, self).__init__()
self.output_channels = output_channels
self.kernel_size = kernel_size
self.stride = stride
self.padding = padding
def build(self, input_shape):
# 输入通道数
input_channels = input_shape[-1]
# 深度卷积(Depthwise Convolution)
self.depthwise_conv = Conv2D(
filters=input_channels, # 输出通道数与输入通道数相同
kernel_size=self.kernel_size,
strides=self.stride,
padding=self.padding,
groups=input_channels, # 分组卷积,每个通道独立卷积
use_bias=False,
)
# 逐点卷积(Pointwise Convolution)
self.pointwise_conv = Conv2D(
filters=self.output_channels, # 输出通道数
kernel_size=1,
strides=1,
padding="same",
use_bias=False,
)
# 批归一化
self.bn = BatchNormalization()
# 激活函数
self.relu = ReLU()
def call(self, inputs):
# 深度卷积
x = self.depthwise_conv(inputs)
# 逐点卷积
x = self.pointwise_conv(x)
# 批归一化
x = self.bn(x)
# 激活函数
x = self.relu(x)
return x
实例调用执行如下所示:
#使用Keras普通卷积Conv2D层来实现
input_shape = (32, 32, 3) # 输入特征图的形状 (H, W, C_in)
output_channels = 16 # 输出通道数
# 输入层
inputs = Input(shape=input_shape)
# 自定义深度可分离卷积层
x = DepthwiseSeparableConvKeras(output_channels)(inputs)
# 构建模型
model = Model(inputs, x)
model.summary()
执行输出如下所示:
通过对比二者结果不难发现,参数量是一致的:
SeparableConv2D层结果:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 32, 32, 3)] 0
separable_conv2d (Separabl (None, 32, 32, 16) 75
eConv2D)
batch_normalization (Batch (None, 32, 32, 16) 64
Normalization)
re_lu (ReLU) (None, 32, 32, 16) 0
=================================================================
Total params: 139 (556.00 Byte)
Trainable params: 107 (428.00 Byte)
Non-trainable params: 32 (128.00 Byte)
_________________________________________________________________
自主实现结果:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 32, 32, 3)] 0
depthwise_separable_conv_k (None, 32, 32, 16) 139
eras (DepthwiseSeparableCo
nvKeras)
=================================================================
Total params: 139 (556.00 Byte)
Trainable params: 107 (428.00 Byte)
Non-trainable params: 32 (128.00 Byte)
_________________________________________________________________
从而也能印证借助于普通的Conv2d卷积层实现的深度可分离卷积块与内置的SeparableConv2D层达到的效果是一致的。
整体回顾学习与实践记录就到这里,感兴趣的话可以参考一下!
标签:kernel,回顾,output,卷积,优缺点,channels,深度,input From: https://blog.csdn.net/Together_CZ/article/details/144581295