1.MobileNetv3网络详解
提出了MobileNetv3-Large和MobileNetv3-Small两种不同大小的网络结构,主要的区别是通道数的变化与bneck的次数。
网络的创新点:
(1)更新Block(bneck)
(2)使用NAS搜索参数(Neural Architecture Search)
(3)重新设计耗时层结构
(1)更新Block
MobileNetv2的倒残差结构:
MobileNetv3的倒残差结构:
NL表示非线性激活函数
与MobileNetv2相比,MobileNetv3的倒残差结构加入了轻量级的注意力机制:
注意力机制的作用方式是调整每个通道的权重(对得到的特征矩阵的每一个channel进行池化处理)。
调整方式:特征矩阵的channel等于多少,得到的一维向量就有多少个元素;然后通过两个全连接层得到输出的向量。第一个全连接层,其节点个数为特征矩阵的channel的1/4;。第二个全连接层,其节点个数等于特征矩阵的channel;输出的向量可以理解为对特征矩阵的每一个channel分析出的一个权重关系,对于重要的channel就赋予一个比较大的权重。
利用h-swish代替swish函数:
(2)使用NAS搜索参数(Neural Architecture Search)
略
(3)重新设计耗时层结构
(a)减少第一个卷积层的卷积核个数(32减为16)
原论文中作者说减少卷积核后准确率不变且能够减少计算量,节省2毫秒时间
(b)精简Last Stage
使用NAS搜索出来的网络结构的最后一部分为Original Last Stage,但作者在使用过程中发现这一部分比较耗时,因此对其进行精简为Efficient Last Stage,精简后准确率不变且节省7毫秒时间。
2.MobileNetv3(large)网络结构
第一列Input代表mobilenetV3每个特征层的shape变化;
第二列Operator代表每次特征层即将经历的block结构,在MobileNetV3中,特征提取经过了许多的bneck结构,NBN为不适用BN层;
第三、四列分别代表了bneck内倒残差结构升维后的通道数、输入到bneck时特征层的通道数。
第五列SE代表了是否在这一层引入注意力机制。
第六列NL代表了激活函数的种类,HS代表h-swish,RE代表RELU。
第七列s代表了每一次block结构所用的步长。
注:112x112x16这一层,输入特征矩阵的channel等于exp size,所以在这一层的倒残差结构里不需要升维,没有1x1的卷积层
3.MobileNetv3(small)网络结构
与MobileNetV3(large)相比,bneck的次数与通道数都有一定的下降。
4.使用Pytorch搭建MobileNetv3网络
文件结构:
MobileNetv2
├── model_v2.py: MobileNetv2模型搭建
├── model_v3.py: MobileNetv3模型搭建
├── train.py: 训练脚本
└── predict.py: 图像预测脚本
(1)model_v3.py
定义卷积结构:
class ConvBNActivation(nn.Sequential): #定义结构:Conv+BN+激活函数
def __init__(self,
in_planes: int, #输入特征矩阵channel
out_planes: int, #输出特征矩阵channel
kernel_size: int = 3,
stride: int = 1,
groups: int = 1,
norm_layer: Optional[Callable[..., nn.Module]] = None, #卷积后接的BN层
activation_layer: Optional[Callable[..., nn.Module]] = None): #激活函数
padding = (kernel_size - 1) // 2
if norm_layer is None: #如果没有传入norm_layer就默认设置为BN
norm_layer = nn.BatchNorm2d
if activation_layer is None: #如果没有传入激活函数寄默认使用ReLU6
activation_layer = nn.ReLU6
super(ConvBNActivation, self).__init__(nn.Conv2d(in_channels=in_planes,
out_channels=out_planes,
kernel_size=kernel_size,
stride=stride,
padding=padding,
groups=groups,
bias=False),
norm_layer(out_planes),
activation_layer(inplace=True))
定义注意力机制模块:
class SqueezeExcitation(nn.Module): #定义注意力机制模块,此处注意对比上文讲到的注意力机制
def __init__(self, input_c: int, squeeze_factor: int = 4): #squeeze_factor为第一个全连接层的节点个数为输入特征矩阵channel的1/4,所以int=4
super(SqueezeExcitation, self).__init__()
squeeze_c = _make_divisible(input_c // squeeze_factor, 8) #计算第一个全连接层的节点个数:输出特征矩阵channel/4,计算完成后调整到8最近的整数倍
self.fc1 = nn.Conv2d(input_c, squeeze_c, 1) #全连接层1,输入输出的channel
self.fc2 = nn.Conv2d(squeeze_c, input_c, 1) #全连接层2
def forward(self, x: Tensor) -> Tensor: #定义正向传播
scale = F.adaptive_avg_pool2d(x, output_size=(1, 1)) #对输入的特征矩阵的每一个维度进行池化操作
scale = self.fc1(scale)
scale = F.relu(scale, inplace=True)
scale = self.fc2(scale)
scale = F.hardsigmoid(scale, inplace=True)
return scale * x #利用得到的权重scale与对应的channel维度的输入相乘
定义网络的参数配置:
class InvertedResidualConfig: #对应MobileNetv3中的每一个bneck结构的参数配置
def __init__(self,
input_c: int, #对应网络结构中的input
kernel: int, #对应网络结构中的Operator
expanded_c: int, #对应网络结构中的exp size
out_c: int, #对应网络结构中的#out
use_se: bool, #对应网络结构中的SE
activation: str, #对应网络结构中的NL
stride: int, #对应网络结构中的s
width_multi: float): #对应mobilenetv2中提到的α参数,调节每一个卷积层所使用channel的倍率银子
self.input_c = self.adjust_channels(input_c, width_multi) #调节每一个卷积层所使用channel
self.kernel = kernel
self.expanded_c = self.adjust_channels(expanded_c, width_multi)
self.out_c = self.adjust_channels(out_c, width_multi)
self.use_se = use_se
self.use_hs = activation == "HS"
self.stride = stride
MobileNetv3的倒残差结构:
class InvertedResidual(nn.Module): #mobilenetv3的倒残差结构
def __init__(self,
cnf: InvertedResidualConfig,
norm_layer: Callable[..., nn.Module]):
super(InvertedResidual, self).__init__()
if cnf.stride not in [1, 2]: #判断对应某一层的步长是否为1或2(网络结构中步长只有1和2)
raise ValueError("illegal stride value.")
self.use_res_connect = (cnf.stride == 1 and cnf.input_c == cnf.out_c) #判断是否使用捷径分支(当s=1且input_c=output_c时使用)
layers: List[nn.Module] = [] #定义layers空列表
activation_layer = nn.Hardswish if cnf.use_hs else nn.ReLU #判断使用哪个激活函数
# expand #倒残差结构中第一个1x1卷积层,用来升维处理
if cnf.expanded_c != cnf.input_c: #判断exp size是否等于输入特征矩阵channel
layers.append(ConvBNActivation(cnf.input_c, #两者不相等才有1x1卷积层
cnf.expanded_c,
kernel_size=1,
norm_layer=norm_layer,
activation_layer=activation_layer))
# depthwise #dw卷积
layers.append(ConvBNActivation(cnf.expanded_c, #上一层输出的特征矩阵channel
cnf.expanded_c, #dw卷积的输入输出特征矩阵channel一直
kernel_size=cnf.kernel,
stride=cnf.stride,
groups=cnf.expanded_c, #dw卷积对每一个channel都单独使用chennel为1卷积核来进行卷积处理,其group数等于channel
norm_layer=norm_layer,
activation_layer=activation_layer))
if cnf.use_se: #判断当前层结构是否使用SE模块
layers.append(SqueezeExcitation(cnf.expanded_c)) #如果使用传入SE模块中的expanded_c
# project #最后一个卷积层,降维
layers.append(ConvBNActivation(cnf.expanded_c,
cnf.out_c,
kernel_size=1,
norm_layer=norm_layer,
activation_layer=nn.Identity))
self.block = nn.Sequential(*layers)
self.out_channels = cnf.out_c
self.is_strided = cnf.stride > 1
def forward(self, x: Tensor) -> Tensor: #定义正向传播过程
result = self.block(x)
if self.use_res_connect:
result += x
return result
定义MobileNetv3(large)网络结构:
class MobileNetV3(nn.Module): #定义MobileNetv3网络结构
def __init__(self,
inverted_residual_setting: List[InvertedResidualConfig], #对应bneck结构的参数列表
last_channel: int, #对应倒数第二个全连接层
num_classes: int = 1000, #类别个数,默认1000
block: Optional[Callable[..., nn.Module]] = None, #定义的mobilenetv3的倒残差结构
norm_layer: Optional[Callable[..., nn.Module]] = None): #BN层
super(MobileNetV3, self).__init__()
if not inverted_residual_setting: #数据格式检查
raise ValueError("The inverted_residual_setting should not be empty.")
elif not (isinstance(inverted_residual_setting, List) and
all([isinstance(s, InvertedResidualConfig) for s in inverted_residual_setting])):
raise TypeError("The inverted_residual_setting should be List[InvertedResidualConfig]")
if block is None:
block = InvertedResidual
if norm_layer is None:
norm_layer = partial(nn.BatchNorm2d, eps=0.001, momentum=0.01)
layers: List[nn.Module] = [] #构建网络
# building first layer #第一个卷积层,conv2d
firstconv_output_c = inverted_residual_setting[0].input_c #获取第一个bneck结构的input-channel,对应第一个卷积层的输出channel
layers.append(ConvBNActivation(3,
firstconv_output_c,
kernel_size=3,
stride=2,
norm_layer=norm_layer,
activation_layer=nn.Hardswish))
# building inverted residual blocks
for cnf in inverted_residual_setting: #遍历每一个bneck结构
layers.append(block(cnf, norm_layer)) #将每一层的配置文件和norm_layer传给block,然后将创建好的block(即InvertedResidual模块)添加到layers中
# building last several layers
lastconv_input_c = inverted_residual_setting[-1].out_c #构建bneck之后的一层网络
lastconv_output_c = 6 * lastconv_input_c
layers.append(ConvBNActivation(lastconv_input_c,
lastconv_output_c,
kernel_size=1,
norm_layer=norm_layer,
activation_layer=nn.Hardswish))
self.features = nn.Sequential(*layers) #特征提取主干部分
self.avgpool = nn.AdaptiveAvgPool2d(1) #平均池化
self.classifier = nn.Sequential(nn.Linear(lastconv_output_c, last_channel), #全连接层,分类部分
nn.Hardswish(inplace=True),
nn.Dropout(p=0.2, inplace=True),
nn.Linear(last_channel, num_classes))
# initial weights
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.GroupNorm)):
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_impl(self, x: Tensor) -> Tensor:
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
def forward(self, x: Tensor) -> Tensor:
return self._forward_impl(x)
定义MobileNetv3(large)网络参数:
MobileNetv3(small)同理
inverted_residual_setting = [
# input_c, kernel, expanded_c, out_c, use_se, activation, stride
bneck_conf(16, 3, 16, 16, False, "RE", 1),
bneck_conf(16, 3, 64, 24, False, "RE", 2), # C1
bneck_conf(24, 3, 72, 24, False, "RE", 1),
bneck_conf(24, 5, 72, 40, True, "RE", 2), # C2
bneck_conf(40, 5, 120, 40, True, "RE", 1),
bneck_conf(40, 5, 120, 40, True, "RE", 1),
bneck_conf(40, 3, 240, 80, False, "HS", 2), # C3
bneck_conf(80, 3, 200, 80, False, "HS", 1),
bneck_conf(80, 3, 184, 80, False, "HS", 1),
bneck_conf(80, 3, 184, 80, False, "HS", 1),
bneck_conf(80, 3, 480, 112, True, "HS", 1),
bneck_conf(112, 3, 672, 112, True, "HS", 1),
bneck_conf(112, 5, 672, 160 // reduce_divider, True, "HS", 2), # C4
bneck_conf(160 // reduce_divider, 5, 960 // reduce_divider, 160 // reduce_divider, True, "HS", 1),
bneck_conf(160 // reduce_divider, 5, 960 // reduce_divider, 160 // reduce_divider, True, "HS", 1),
]
last_channel = adjust_channels(1280 // reduce_divider) # C5
return MobileNetV3(inverted_residual_setting=inverted_residual_setting,
last_channel=last_channel,
num_classes=num_classes)
MobileNetV3预训练权重下载
https://download.pytorch.org/models/mobilenet_v3_large-8738ca79.pth
下载完成后将其名称改为mobilenet_v3_large.pth,存放在当前文件夹
数据集
数据集采用花分类数据集:使用pytorch搭建AlexNet并训练花分类数据集
(2)train.py
如果要使用v3模型,要做如下更改:
导入模块的更改
from model_v2 import MobileNetV2
改为:from model_v3 import mobilenet_v3_large
实例化网络的更改
model_weight_path = "./mobilenet_v3_large.pth" #预训练模型路径
保存路径更改:
save_path = './MobileNetV3.pth'
不使用预训练权重:
# freeze features weights #冻结特征提取部分的所有权重,只训练最后两个全连接层权重
for param in net.features.parameters(): #如果训练整个网络的权重就将这两句注释掉
param.requires_grad = False
训练结果
(3)predict.py
同样更改导入模块、实例化模块和载入权重部分代码