卷积神经网络的演化(4)-- 轻量化

MobileNet是为移动端和嵌入式端深度学习应用而设计的网络,使得在cpu上也能达到理想的速度要求。

MobileNet v1

MobileNet v1的结构非常简单,是一个复古的直筒结构,类似于VGG一样。除了第一层采用标准的卷积层之外,其他的层都为深度可分离卷积。

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
import torch
import torch.nn as nn

#========================================================
# 网络框架
#========================================================

class MobileNet_v1(nn.Module):
def __init__(self):
super(MobileNet_v1, self).__init__()
def conv_bn(in_channels, out_channels, stride):
# 传统的卷积:conv3*3+BN+ReLU
conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(in_channelslace=True)
)
return conv

def conv_dw(in_channels, out_channels, stride):
# 深度可分离卷积
return nn.Sequential(
# 深度卷积(depthwise convolution)
nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=stride, padding=1, grout_channelss=in_channels, bias=False),
nn.BatchNorm2d(in_channels),
nn.ReLU(in_channelslace=True),

# 逐点卷积(pointwise convolution)
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(in_channelslace=True)
)

self.features = nn.Sequential(
conv_bn(3, 32, 2),
conv_dw(32, 64, 1),
conv_dw(64, 128, 2),
conv_dw(128, 128, 1),
conv_dw(128, 256, 2),
conv_dw(256, 256, 1),
conv_dw(256, 512, 2),
conv_dw(512, 512, 1),
conv_dw(512, 512, 1),
conv_dw(512, 512, 1),
conv_dw(512, 512, 1),
conv_dw(512, 512, 1),
conv_dw(512, 1024, 2),
conv_dw(1024, 1024, 1),
nn.AvgPool2d(7)
)
self.classifier = nn.Linear(1024, 1000)

def forward(self, x):
x = self.features(x)
x = x.view(-1, 1024)
x = self.classifier(x)
return x

#========================================================
# 主程序
#========================================================

model = MobileNet_v1()
print(model)

in_channelsut_tensor = torch.randn((1, 3, 224, 224))
in_channelsut_var = torch.autograd.Variable(in_channelsut_tensor)
out = model(in_channelsut_var)

MobileNet v2

MobileNet v2是对MobileNet v1的改进,同样是一个轻量级卷积神经网络,使用了线性瓶颈层。

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import torch
import torch.nn as nn

#========================================================
# 瓶颈结构
#========================================================

class Bottleneck(nn.Module):
def __init__(self, in_channels, expansion, out_channels, repeat_times, stride):
super(Bottleneck, self).__init__()
inner_channels = in_channels*expansion

self.conv1 = nn.Sequential(
nn.Conv2d(in_channels, inner_channels, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(inner_channels),
nn.ReLU(inplace=True),

nn.Conv2d(inner_channels, inner_channels, kernel_size=3, stride=stride, padding=1, groups=inner_channels, bias=False),
nn.BatchNorm2d(inner_channels),
nn.ReLU(inplace=True),

nn.Conv2d(inner_channels, out_channels, kernel_size=1, stride=1, groups=1, bias=False),
nn.BatchNorm2d(out_channels)
)

self.conv2 = nn.Sequential(
nn.Conv2d(out_channels, out_channels*expansion, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(out_channels*expansion),
nn.ReLU(inplace=True),

nn.Conv2d(out_channels*expansion, out_channels*expansion, kernel_size=3, stride=1, padding=1, groups=out_channels, bias=False),
nn.BatchNorm2d(out_channels*expansion),
nn.ReLU(inplace=True),

nn.Conv2d(out_channels*expansion, out_channels, kernel_size=1, stride=1, groups=1, bias=False),
nn.BatchNorm2d(out_channels)
)
self.n = repeat_times

def forward(self, x):
out = self.conv1(x)
count = 2
while(count <= self.n):
temp = out
temp = self.conv2(temp)
out = out + temp
count = count+1
return out

#========================================================
# 网络框架
#========================================================

class MobileNet_v2(nn.Module):
def __init__(self):
super(MobileNet_v2, self).__init__()
def head():
# 首层
conv = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True)
)
return conv

def tail():
# 尾层
conv = nn.Sequential(
nn.Conv2d(320, 1280, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(1280),
nn.ReLU(inplace=True),

nn.AvgPool2d(kernel_size=7)
)
return conv

self.features = nn.Sequential(
head(),
Bottleneck(32, 1, 16, 1, 1),
Bottleneck(16, 6, 24, 2, 2),
Bottleneck(24, 6, 32, 3, 2),
Bottleneck(32, 6, 64, 4, 2),
Bottleneck(64, 6, 96, 3, 1),
Bottleneck(96, 6, 160, 3, 2),
Bottleneck(160, 6, 320, 1, 1),
tail()
)
self.classifier = nn.Conv2d(1280, 1000, kernel_size=1, stride=1, bias=False)

def forward(self, x):
x = self.features(x)
out = self.classifier(x)
return out.view(-1, 1000)

#========================================================
# 主程序
#========================================================

model = MobileNet_v2()
print(model)

input_tensor = torch.randn((1, 3, 224, 224))
input_var = torch.autograd.Variable(input_tensor)
out = model(input_var)

MobileNet v3

2019年,Google发布了第三代MobileNet,即MobileNetV3。MobileNetV3主要的贡献就是使用科学的理论重新设计了网络结构,并且引入了H-Swish激活函数与Relu搭配使用。此外在网络中还加入了Squeeze-And-Excite模块。
MobileNetV3根据硬件条件的不同又分出了两套网络,分别是MobileNetV3_large和MobileNetV3_small,前者适合硬件条件较好的设备,后者适合硬件条件局限性较大的设备,精度略有下降。

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import torch
import torch.nn as nn
import torch.nn.functional as F

#========================================================
# 特殊结构
#========================================================

def Hswish(x, inplace=True):
return x * F.relu6(x + 3., inplace=inplace) / 6.

def Hsigmoid(x, inplace=True):
return F.relu6(x + 3., inplace=inplace) / 6.

# Squeeze-And-Excite模块
class SEModule(nn.Module):
def __init__(self, channel, reduction=4):
super(SEModule, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.se = nn.Sequential(
nn.Linear(channel, channel // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel, bias=False),
)

def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.se(y)
y = Hsigmoid(y).view(b, c, 1, 1)
return x * y.expand_as(x)

class Bottleneck(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, exp_channels, stride, se='True', nl='HS'):
super(Bottleneck, self).__init__()
padding = (kernel_size - 1) // 2
if nl == 'RE':
self.nlin_layer = F.relu6
elif nl == 'HS':
self.nlin_layer = Hswish
self.stride=stride
if se:
self.se = SEModule(exp_channels)
else:
self.se = None
self.conv1 = nn.Conv2d(in_channels, exp_channels, kernel_size=1, stride=1, padding=0, bias=False)
self.bn1 = nn.BatchNorm2d(exp_channels)
self.conv2=nn.Conv2d(exp_channels, exp_channels, kernel_size=kernel_size, stride=stride, padding=padding, groups=exp_channels, bias=False)
self.bn2 = nn.BatchNorm2d(exp_channels)
self.conv3 = nn.Conv2d(exp_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels)
# 先初始化一个空序列,之后改造其成为残差链接
self.shortcut = nn.Sequential()
# 只有步长为1且输入输出通道不相同时才采用跳跃连接(想一下跳跃链接的过程,输入输出通道相同这个跳跃连接就没意义了)
if stride == 1 and in_channels != out_channels:
self.shortcut = nn.Sequential(
# 下面的操作卷积不改变尺寸,仅匹配通道数
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(out_channels)
)

def forward(self, x):
out = self.nlin_layer(self.bn1(self.conv1(x)))
if self.se is not None:
out = self.bn2(self.conv2(out))
out = self.nlin_layer(self.se(out))
else:
out = self.nlin_layer(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out = out + self.shortcut(x) if self.stride == 1 else out
return out

#========================================================
# 网络框架
#========================================================

class MobileNet_v3_large(nn.Module):
# (out_channels, kernel_size, exp_channels, stride, se, nl)
cfg = [
(16, 3, 16, 1, False, 'RE'),
(24, 3, 64, 2, False, 'RE'),
(24, 3, 72, 1, False, 'RE'),
(40, 5, 72, 2, True, 'RE'),
(40, 5, 120, 1, True, 'RE'),
(40, 5, 120, 1, True, 'RE'),
(80, 3, 240, 2, False, 'HS'),
(80, 3, 200, 1, False, 'HS'),
(80, 3, 184, 1, False, 'HS'),
(80, 3, 184, 1, False, 'HS'),
(112, 3, 480, 1, True, 'HS'),
(112, 3, 672, 1, True, 'HS'),
(160, 5, 672, 2, True, 'HS'),
(160, 5, 960, 1, True, 'HS'),
(160, 5, 960, 1, True, 'HS')
]
def __init__(self, num_classes=17):
super(MobileNetV3_large, self).__init__()
self.conv1 = nn.Conv2d(3, 16, 3, 2, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(16)
# 根据cfg数组自动生成所有的Bottleneck层
self.layers = self._make_layers(in_channels=16)
self.conv2 = nn.Conv2d(160, 960, 1, stride=1, bias=False)
self.bn2 = nn.BatchNorm2d(960)
# 卷积后不跟BN,就应该把bias设置为True
self.conv3 = nn.Conv2d(960, 1280, 1, 1, padding=0, bias=True)
self.conv4 = nn.Conv2d(1280, num_classes, 1, stride=1, padding=0, bias=True)

def _make_layers(self, in_channels):
layers=[]
for out_channels, kernel_size, exp_channels, stride, se, nl in self.cfg:
layers.append(
Bottleneck(in_channels, out_channels, kernel_size, exp_channels, stride, se, nl)
)
in_channels = out_channels
return nn.Sequential(*layers)

def forward(self, x):
out = Hswish(self.bn1(self.conv1(x)))
out = self.layers(out)
out = Hswish(self.bn2(self.conv2(out)))
out = F.avg_pool2d(out, 7)
out = Hswish(self.conv3(out))
out = self.conv4(out)
# 因为原论文中最后一层是卷积层来实现全连接的效果,维度是四维的,后两维是1,在计算损失函数的时候要求二维,因此在这里需要做一个resize
a, b = out.size(0), out.size(1)
out = out.view(a, b)
return out

class MobileNet_v3_small(nn.Module):
# (out_channels, kernel_size, exp_channels, stride, se, nl)
cfg = [
(16, 3, 16, 2, True, 'RE'),
(24, 3, 72, 2, False, 'RE'),
(24, 3, 88, 1, False, 'RE'),
(40, 5, 96, 2, True, 'HS'),
(40, 5, 240, 1, True, 'HS'),
(40, 5, 240, 1, True, 'HS'),
(48, 5, 120, 1, True, 'HS'),
(48, 5, 144, 1, True, 'HS'),
(96, 5, 288, 2, True, 'HS'),
(96, 5, 576, 1, True, 'HS'),
(96, 5, 576, 1, True, 'HS')
]
def __init__(self, num_classes=17):
super(MobileNet_v3_small, self).__init__()
self.conv1 = nn.Conv2d(3, 16, 3, 2, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(16)
# 根据cfg数组自动生成所有的Bottleneck层
self.layers = self._make_layers(in_channels=16)
self.conv2 = nn.Conv2d(96, 576, 1, stride=1, bias=False)
self.bn2 = nn.BatchNorm2d(576)
# 卷积后不跟BN,就应该把bias设置为True
self.conv3 = nn.Conv2d(576, 1280, 1, 1, padding=0, bias=True)
self.conv4 = nn.Conv2d(1280, num_classes, 1, stride=1, padding=0, bias=True)

def _make_layers(self, in_channels):
layers = []
for out_channels, kernel_size, exp_channels, stride, se, nl in self.cfg:
layers.append(
Bottleneck(in_channels, out_channels, kernel_size, exp_channels, stride, se, nl)
)
in_channels=out_channels
return nn.Sequential(*layers)

def forward(self, x):
out = Hswish(self.bn1(self.conv1(x)))
out = self.layers(out)
out = self.bn2(self.conv2(out))
se = SEModule(out.size(1))
out = Hswish(se(out))
out = F.avg_pool2d(out, 7)
out = Hswish(self.conv3(out))
out = self.conv4(out)
# 因为原论文中最后一层是卷积层来实现全连接的效果,维度是四维的,后两维是1,在计算损失函数的时候要求二维,因此在这里需要做一个resize
a, b = out.size(0), out.size(1)
out = out.view(a, b)
return out

#========================================================
# 主程序
#========================================================

model = MobileNet_v3_small()
print(model)

input_tensor = torch.randn((1, 3, 224, 224))
input_var = torch.autograd.Variable(input_tensor)
out = model(input_var)
print(out)
0%