存在的疑问
开端于MobileNetV3
最开始于使用MobileNetv3轻量化网络训练模型,但是效果并不好,于是修改了网络输出添加了dropout层,以及学习率使用了余弦退火衰减,参考上一篇文章。
Shuffle参数量
相对于普通卷积,ShuffleNet采用了分组卷积,但是查阅资料结合验算,发现一些博文提到的参数计算公式有问题,这里贴出经过我计算后的参数量。
普通卷积
输入特征图:
h
∗
w
∗
c
i
n
h*w*c_{in}
h∗w∗cin(其中h为图高,w为图宽,cin为特征图个数,即通道数)
卷积核:
c
o
u
t
∗
k
∗
k
c_{out}*k*k
cout∗k∗k(cout为滤波器个数,即通道数)
输出特征图:
h
∗
w
∗
c
o
u
t
h*w*c_{out}
h∗w∗cout(默认进行了填充)
参数量:
k
∗
k
∗
c
i
n
∗
c
o
u
t
k*k*c_{in}*c_{out}
k∗k∗cin∗cout
计算量:
h
∗
w
∗
k
∗
k
∗
c
i
n
∗
c
o
u
t
h*w*k*k*c_{in}*c_{out}
h∗w∗k∗k∗cin∗cout(其中,h,w我认为是输出特征图的尺寸)
分组卷积
输入特征图:
[
h
∗
w
∗
c
i
n
/
n
]
∗
n
[h*w*c_{in}/n]*n
[h∗w∗cin/n]∗n(其中h为图高,w为图宽,cin为特征图个数,即通道数,n为分组)
卷积核:
[
c
o
u
t
∗
k
∗
k
/
n
]
∗
n
[c_{out}*k*k/n]*n
[cout∗k∗k/n]∗n(cout为滤波器个数,即通道数,n为分组)
输出特征图:
h
∗
w
∗
c
o
u
t
h*w*c_{out}
h∗w∗cout(默认进行了填充)
参数量:
[
k
∗
k
∗
c
i
n
/
n
∗
c
o
u
t
/
n
]
∗
n
[k*k*c_{in}/n*c_{out}/n]*n
[k∗k∗cin/n∗cout/n]∗n
输入通道被分作n组,所以单个卷积核通道为 c i n / n c_{in}/n cin/n,同时输出cout也被分作n份,因此一共有n组卷积核合集,每组有 c o u t / n c_{out}/n cout/n个卷积核。
计算量:.
(
h
∗
w
∗
k
∗
k
∗
c
i
n
/
n
∗
c
o
u
t
/
n
)
∗
n
(h*w*k*k*c_{in}/n*c_{out}/n)*n
(h∗w∗k∗k∗cin/n∗cout/n)∗n
可见,参数量为普通卷积的
1
/
n
1/n
1/n。
通道重排
具体算法网直接搜会有很多,具体的这里就不加赘述了,这里只记录我认为值得注意的。
分组卷积生成的三组特征图,第一组1~ 4;第二组5~ 8;第三组9~12。先将特征图重塑,为三行N列的矩形。然后进行转置,变成N行三列。最后压平,从二维tensor变成一维tensor,每一组的特征图交叉组合在一起。实现各组之间的信息交融。
具体就是因为最后直接拼接起来得到的特征图每个分组之间没有什么联系,通过通道重排,增加每一层之间的联系。
上面这个张图很形象!配合上面这张图会很容易理解
def channel_shuffle(input_tensor, num=2): # 默认时2组特征:shortcut和卷积后的x
# 先得到输入特征图的shape,b:batch size,h,w:一张图的size,c:通道数
b, h, w, c = input_tensor.shape
# 确定shape = [b, h, w, num, c//num]。通道维度原来是一个长为c的一维tensor,变成2行n列的矩阵
# 在通道维度上将特征图reshape为2行n列的矩阵。
x_reshaped = tf.reshape(input_tensor, [-1, h, w, num, c//num])
# 确定转置的矩形的shape = [b, h, w, c//num, num]
# 矩阵转置,最后两个维度从2行n列变成n行2列
x_transposed = tf.transpose(x_reshaped, [0,1,2,4,3])
# 重新排列,shotcut和x的通道像素交叉排列,通道维度重新变成一维tensor
output = tf.reshape(x_transposed, [-1, h, w, c])
return output # 返回通道维度交叉排序后的tensor
其他模块
还有基本模块和下采样模块,合并到后面的总和中
总体代码
# -*- coding: UTF-8 -*-
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Model
#标准卷积
def conv_block(input_tensor,filters,kernel_size,stride=1,pad='same'):
'''
(class) Conv2D(filters: Any, kernel_size: Any, strides: Any = (1, 1), padding: str = 'valid', \
data_format: Any | None = None, dilation_rate: Any = (1, 1), groups: int = 1, activation: Any | None = None, \
use_bias: bool = True, kernel_initializer: str = 'glorot_uniform', bias_initializer: str = 'zeros', \
kernel_regularizer: Any | None = None, bias_regularizer: Any | None = None, activity_regularizer: Any | None = None, \
kernel_constraint: Any | None = None, bias_constraint: Any | None = None, **kwargs: Any)
'''
#注意groups,分组,但是后面分组没有用这个参数
x = layers.Conv2D(filters,kernel_size,
strides=stride,
padding=pad,
use_bias=False)(input_tensor)
x = layers.BatchNormalization()(x) # 批标准化
x = layers.ReLU()(x) # relu激活
return x # 返回一次标准卷积后的tensor
#(2)深度可分离卷积块
def depthwise_conv_block(input_tensor, stride=1):
# 深度可分离卷积+批标准化
# 不需要传入卷积核个数,输入有几个通道,就有几个卷积核,每个卷积核负责一个通道
x = layers.DepthwiseConv2D(kernel_size = (3,3), # 深度卷积核size默认3*3
strides = stride, # 步长
padding = 'same', # strides=1卷积过程中特征图size不变,strides=2卷积过程中size减半
use_bias = False)(input_tensor) # 有BN层就不需要偏置
x = layers.BatchNormalization()(x) # 批标准化
return x # 返回深度可分离卷积后的tensor
#(3)通道重排,跨组信息交融
def channel_shuffle(input_tensor, num=2): # 默认时2组特征:shortcut和卷积后的x
# 先得到输入特征图的shape,b:batch size,h,w:一张图的size,c:通道数
b, h, w, c = input_tensor.shape
# 确定shape = [b, h, w, num, c//num]。通道维度原来是一个长为c的一维tensor,变成2行n列的矩阵
# 在通道维度上将特征图reshape为2行n列的矩阵。
x_reshaped = tf.reshape(input_tensor, [-1, h, w, num, c//num])
# 确定转置的矩形的shape = [b, h, w, c//num, num]
# 矩阵转置,最后两个维度从2行n列变成n行2列
x_transposed = tf.transpose(x_reshaped, [0,1,2,4,3])
# 重新排列,shotcut和x的通道像素交叉排列,通道维度重新变成一维tensor
output = tf.reshape(x_transposed, [-1, h, w, c])
return output # 返回通道维度交叉排序后的tensor
#(4)步长=1时的卷积块
def shufflent_unit_1(input_tensor, filters):
# 首先将输入特征图在通道维度上平均分成两份:一部分用于残差连接,一部分卷积提取特征
shortcut, x = tf.split(input_tensor, 2, axis=-1) # axis指定轴
# 现在shotcut和x的通道数都只有原来的二分之一
# 1*1卷积+3*3深度卷积+1*1卷积
x = conv_block(x, filters//2, kernel_size=(1,1), stride=1) # 1*1卷积,通道数保持不变
x = depthwise_conv_block(x, stride=1) # 3*3深度卷积
x = conv_block(x, filters//2, kernel_size=(1,1), stride=1) # 1*1卷积跨通道信息融合
# 堆叠shoutcut和x,要求两个tensor的size相同
x = tf.concat([shortcut, x], axis=-1) # 在通道维度上堆叠
# 将堆叠后2组特征图,在通道维度上重新排列
x = channel_shuffle(x)
return x # 返回步长为1时的卷积块输出的tensor
#(5)步长=2时(下采样)的卷积块
def shufflenet_unit_2(input_tensor, out_channel):
# 输入特征图的通道数
in_channel = input_tensor.shape[-1]
# 首先将输入特征图复制一份,分别用于左右两个分支的卷积
shortcut = input_tensor
# ① 左分支的卷积部分==深度卷积+逐点卷积,输出特征图通道数等于原通道数
shortcut = depthwise_conv_block(shortcut, stride=2) # 特征图size减半
shortcut = conv_block(shortcut, filters=in_channel, kernel_size=(1,1), stride=1) # 输出特征图个数不变
# ② 右分支==1*1卷积下降通道数+3*3深度卷积+1*1卷积上升通道数
x = conv_block(input_tensor, in_channel//2, kernel_size=(1,1), stride=1)
x = depthwise_conv_block(x, stride=2)
# 右分支的通道数和左分支的通道数叠加==输出特征图的通道数out_channel
x = conv_block(x, out_channel-in_channel, kernel_size=(1,1), stride=1)
# ③ 左右分支的输出特征图在通道维度上堆叠,并且output.shape[-1]==out_channel
output = tf.concat([shortcut, x], axis=-1)
# ④ 堆叠后的2组特征在通道维度上重新排列
output = channel_shuffle(output)
return output # 返回步长=2时的输出结果
#(6)构建shufflenet卷积块
# 一个shuffle卷积块是由一个shufflenet_unit_2下采样单元,和若干个shufflenet_unit_1特征传递单元构成
def stage(input_tensor, filters, n): # filters代表输出通道数
# 下采样单元
x = shufflenet_unit_2(input_tensor, out_channel=filters)
# 特征传递单元循环n次
for i in range(n):
x = shufflent_unit_1(x, filters=filters)
return x # 返回一个shufflenet卷积结果
#(7)构建网络模型
def ShuffleNet(input_shape, classes):
# 构建网络输入的tensor
inputs = keras.Input(shape=input_shape)
# [224,224,3]==>[112,112,24]
x = layers.Conv2D(filters=24, kernel_size=(3,3), strides=2, padding='same')(inputs) # 普通卷积
# [112,112,24]==>[56,56,24]
x = layers.MaxPooling2D(pool_size=(3,3), strides=2, padding='same')(x) # 最大池化
# [56,56,24]==>[28,28,116]
x = stage(x, filters=116, n=3)
# [28,28,116]==>[14,14,232]
x = stage(x, filters=232, n=7)
# [14,14,232]==>[7,7,464]
x = stage(x, filters=464, n=3)
# [7,7,464]==>[7,7,1024]
x = layers.Conv2D(filters=1024, kernel_size=(1,1), strides=1, padding='same')(x) # 1*1普通卷积
# [7,7,1024]==>[None,1024]
x = layers.GlobalAveragePooling2D()(x) # 在通道维度上全局平均池化
#防止过拟合
x = tf.keras.layers.Dropout(rate=0.5)(x)
# 按论文输出层使用全连接层,也可改为卷积层再Reshape
logits = layers.Dense(classes)(x) # 为了网络稳定,训练时再使用Softmax函数
# 完成网络架构
model = Model(inputs, logits)
return model # 返回网络模型
#(8)接收网络模型
if __name__ == '__main__':
model = ShuffleNet(input_shape=[224,224,3], # 输入图像的shape
classes=1000) # 图像分类类别
model.summary() # 查看网络结构
相对原模型,这个模型添加了dropout算法
但是效果并没有特别好,后续会继续改进。
贴一些模型部署后的实际运用的混淆矩阵
第一次实验
第二次实验
可以看出来结果很不如意
当然,也许和我数据集太小有关,准备扩充数据集后再试一次。