paper地址: https://arxiv.org/abs/1911.11907
pytorch:https://github.com/iamhankai/ghostnet.pytorch
如果想看原文翻譯,請跳轉:https://blog.csdn.net/qq_38316300/article/details/104602071
這篇博客主要是對2020CVPR論文GhostNet: More Features from Cheap Operations的源碼部分進行賞析,瞭解GhostNet網絡的構建思路,並且使用PyTorch構建整體的網絡架構。
目錄
第二部分:構建inverted residual blocks模塊
前沿知識:
- 你已經讀過了論文,瞭解了Ghost模塊提出的必要性和重要性
- 能夠使用PyTorch構建簡單的卷積神經網絡
創建GhostNet網絡
GhostNet網絡主要由四個部分組成:
- 第一部分:使用3*3的卷積核,對原始圖像進行下采樣。
- 第二部分:使用16個GhostBottleneck模塊,代替卷積核操作,進行圖像的高維特徵提取
- 第三部分:使用全局平均池化,對特徵圖進行壓縮得到更強的表徵信息
- 第四部分:構建網絡的分類器,進行圖像分類
第一部分:構建網絡的第一層
網絡的第一層主要是由一個卷積核尺寸爲3*3,步長爲2的下采樣層構成。我們把第一層放在layers元組中,layers能夠存儲第一層以及inverted residual blocks層。將layers通過變換之後能夠變成一個有順序的Sequential網絡層。
def __init__(self, cfgs, num_classes=1000, width_mult=1.):
super(GhostNet, self).__init__()
# setting of inverted residual blocks
self.cfgs = cfgs
# building first layer
output_channel = _make_divisible(16 * width_mult, 4)
layers = [nn.Sequential(
nn.Conv2d(3, output_channel, 3, 2, 1, bias=False),
nn.BatchNorm2d(output_channel),
nn.ReLU(inplace=True)
)]
input_channel = output_channel
第二部分:構建inverted residual blocks模塊
在這一模塊中,我們的核心是要構建一個GhostBottleneck模塊,因爲inverted residual blocks模塊的主要組成部門就是GhostBottleneck模塊。
# building inverted residual blocks
block = GhostBottleneck
for k, exp_size, c, use_se, s in self.cfgs:
output_channel = _make_divisible(c * width_mult, 4)
hidden_channel = _make_divisible(exp_size * width_mult, 4)
layers.append(block(input_channel, hidden_channel, output_channel, k, s,
use_se))
input_channel = output_channel
self.features = nn.Sequential(*layers)
其中,_make_divisible函數能夠保證輸出通道的數目能夠被4整除,代碼具體如下:
def _make_divisible(v, divisor, min_value=None):
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
# Make sure that round down does not go down by more than 10%.
if new_v < 0.9 * v:
new_v += divisor
return new_v
下面,我們構建核心模塊GhostBottleneck模塊,它主要由四個子模塊構成,其分別是:
- GhostModule:該模塊只能夠增加特徵圖的通道數,不能夠改變其次尺寸
- depthwise convolution:能夠改變特徵圖的尺寸,不改變通道數(這兩個模塊實現了一個卷積核的改變尺寸和通道數的作用)
- SEModule:能夠從全局感受野出發,增加重要的特徵圖的權重,削弱不重要的特徵圖的權重
- Identity mapping:類似殘差網絡中的恆等映射,能夠保留更多的信息,使得更深的網絡可以比較容易的訓練
2.1 GhostModule模塊
GhostModule主要有兩步操作:原始的卷積操作,生成一定量m個特徵圖;廉價線性變換得到一定量s個冗餘特診圖。
在開始,我們需要得到m和n的具體數值,在GhostModule類中加上下面代碼行:
def __init__(self, inp, oup, kernel_size=1, ratio=2, dw_size=3, stride=1, relu=True):
super(GhostModule, self).__init__()
self.oup = oup
init_channels = math.ceil(oup / ratio)
new_channels = init_channels*(ratio-1)
GhostModule類傳入的主要參數有輸入通道數inp,輸出通道數oup以及relu參數。說是主要參數是因爲GhostModule中無論是原始卷積還是線性運算所使用的卷積核大小以及步長都是確定的,唯一不定的就是輸入輸出通道數目以及是否使用激活函數。
確定了兩者的輸入輸出通道數之後,我們在__init__函數中加入原始卷積核以及廉價線性操作的代碼
self.primary_conv = nn.Sequential(nn.Conv2d(inp, init_channels, kernel_size, stride,
kernel_size//2, bias=False),
nn.BatchNorm2d(init_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential())
self.cheap_operation = nn.Sequential(nn.Conv2d(init_channels, new_channels, dw_size, 1,
dw_size//2, groups=init_channels, bias=False),
nn.BatchNorm2d(new_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential())
從中我們可以看到,Sequential中既包括了卷積操作/線性變化,也包括了BN層以及ReLU層。這是因爲在PyTorch,更偏向於將它們當做是一個網絡層進行統一處理。在源碼中,原始卷積的卷積核尺寸爲1,步長爲1;廉價線性變換的卷積核尺寸爲3,步長爲1,只不過增加了groups=inp這一個屬性來表示線性變化。
編寫了網絡層之後,我們需要指定數據在網絡層中傳遞的順序,在forwad函數中添加下面的代碼:
def forward(self, x):
x1 = self.primary_conv(x)
x2 = self.cheap_operation(x1)
out = torch.cat([x1,x2], dim=1)
return out[:,:self.oup,:,:]
下面,我們編寫一下depthwise卷積操作,在PyTorch中,這個卷積操作主要在Conv2d上添加了groups屬性:
def depthwise_conv(inp, oup, kernel_size=3, stride=1, relu=False):
return nn.Sequential(nn.Conv2d(inp, oup, kernel_size, stride, kernel_size//2,
groups=inp, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU(inplace=True) if relu else nn.Sequential())
下面,我們編寫一下SEModule操作,該操作主要就是先進行池化操作,然後再進行全連接操作。
class SELayer(nn.Module):
def __init__(self, channel, reduction=4):
super(SELayer, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel), )
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
y = torch.clamp(y, 0, 1)
return x * y
需要注意一下,在池化層到全連接層轉換過程中需要需要對數據的尺寸進行轉換,將四維數據轉換成二維數據,這是因爲全連接層只能接收二維數據。全連接之後,需要將權重數據轉換成原來四維數據,對原始特徵圖進行加權處理。
2.2 GhostBottleneck模塊
在GhostBottleneck模塊中,子模塊的主要順序就是 GhostModule --> depthwise --> SEModule --> GhostModule -->Identity mapping
第一個GhostModul是增加特徵圖通道數,在該模塊中包含更多的信息;第二個GhostModul模塊讓特徵圖的通道數恢復到期望的通道數。當特徵圖的尺寸改變的時候,我們需要對捷徑層也進行相應的操作,使得兩個連接的特徵圖尺寸和通道數都要相同。
class GhostBottleneck(nn.Module):
def __init__(self, inp, hidden_dim, oup, kernel_size, stride, use_se):
super(GhostBottleneck, self).__init__()
assert stride in [1, 2]
self.conv = nn.Sequential(
# pw
GhostModule(inp, hidden_dim, kernel_size=1, relu=True),
# dw
depthwise_conv(hidden_dim, hidden_dim, kernel_size, stride, relu=False)
if stride==2 else nn.Sequential(),
# Squeeze-and-Excite
SELayer(hidden_dim) if use_se else nn.Sequential(),
# pw-linear
GhostModule(hidden_dim, oup, kernel_size=1, relu=False),
)
if stride == 1 and inp == oup:
self.shortcut = nn.Sequential()
else:
self.shortcut = nn.Sequential(
depthwise_conv(inp, inp, 3, stride, relu=True),
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
def forward(self, x):
return self.conv(x) + self.shortcut(x)
GhostBottleneck模塊構建之後,我們就可以構建整個GhostNet的網絡架構了,首先創建一個GhostNet類,然後依次構建第一部分、inverted residual blocks、構建squeeze模塊以及classifier模塊。
第三部分:構建squeeze層
# building last several layers
output_channel = _make_divisible(exp_size * width_mult, 4)
self.squeeze = nn.Sequential(
nn.Conv2d(input_channel, output_channel, 1, 1, 0, bias=False),
nn.BatchNorm2d(output_channel),
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d((1, 1)))
input_channel = output_channel
第四部分:構建分類器
output_channel = 1280
self.classifier = nn.Sequential(
nn.Linear(input_channel, output_channel, bias=False),
nn.BatchNorm1d(output_channel),
nn.ReLU(inplace=True),
nn.Dropout(0.2),
nn.Linear(output_channel, num_classes))
最後指定數據在GhostNet網絡層之間的傳輸順序
def forward(self, x):
x = self.features(x)
x = self.squeeze(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x
這樣一個GhostNet網絡就已經構建完畢了,我們可以給網絡加載預訓練模型參數,在__init__函數的最後加上
self._initialize_weights()
然後實例化這個函數:
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
我們可以創建一個函數,讓這個函數實現GhostNet的一個對象
def ghost_net(**kwargs):
"""
Constructs a MobileNetV3-Large model
"""
cfgs = [
# k, t, c, SE, s
[3, 16, 16, 0, 1],
[3, 48, 24, 0, 2],
[3, 72, 24, 0, 1],
[5, 72, 40, 1, 2],
[5, 120, 40, 1, 1],
[3, 240, 80, 0, 2],
[3, 200, 80, 0, 1],
[3, 184, 80, 0, 1],
[3, 184, 80, 0, 1],
[3, 480, 112, 1, 1],
[3, 672, 112, 1, 1],
[5, 672, 160, 1, 2],
[5, 960, 160, 0, 1],
[5, 960, 160, 1, 1],
[5, 960, 160, 0, 1],
[5, 960, 160, 1, 1]
]
return GhostNet(cfgs, **kwargs)
這樣一個GhostNet就完全構建好了,我們可以稍微測試一下:
if __name__=='__main__':
model = ghost_net()
model.eval()
print(model)
input = torch.randn(32,3,224,224)
y = model(input)
print(y)
歡迎和我交流~