WRN详述
簡介
卷積神經(jīng)網(wǎng)絡(luò)的發(fā)展經(jīng)歷了 AlexNet、VGGNet、Inception 到 ResNet 的發(fā)展過程,在如今的計算機視覺中層和上層任務(wù)中使用 ResNet 是一個很常見的選擇,一方面其效果較好(無論是速度還是精度),另一方面其泛化能力很強(適合于以遷移學習為基礎(chǔ)的計算機視覺任務(wù))。ResNet 為更深的網(wǎng)絡(luò)設(shè)計帶來了充分的先決條件,但是,隨著模型的加深,參數(shù)量迅速增加而帶來的精度收益卻比較小,Sergey Zagoruyko 等人認為過深的殘差網(wǎng)絡(luò)并不能帶來較大的性能變化,可以改用寬而淺的網(wǎng)絡(luò)來獲得更好的效果,于是提出了 Wide Residual Networks(WRN),效果顯著。
-
論文標題
Wide residual networks
-
論文地址
https://arxiv.org/abs/1605.07146
-
論文源碼
https://github.com/szagoruyko/wide-residual-networks
網(wǎng)絡(luò)設(shè)計
先前問題
網(wǎng)絡(luò)的深淺問題是一個討論已久的話題,很多研究表明,在復雜性相同的情況下,淺層網(wǎng)絡(luò)比深層網(wǎng)絡(luò)多指數(shù)倍的部件。因此何愷明等人提出了殘差網(wǎng)絡(luò),他們希望增加網(wǎng)絡(luò)層數(shù)來使得網(wǎng)絡(luò)很深并且有更少的參數(shù)。本文作者認為,包含 identity mapping 的殘差模塊允許訓練很深的網(wǎng)絡(luò)的同時,也帶來了一些問題。梯度流經(jīng)網(wǎng)絡(luò)的時候,網(wǎng)絡(luò)不會強制要求梯度流過殘差塊的權(quán)重層,這可能導致訓練中幾乎學不到什么。最后,只有少量的殘差塊能夠?qū)W到有用的表達(絕大多數(shù)殘差塊用處不大)或者很多塊分享非常少的信息,這對結(jié)果影響很小。
上述問題稱為 diminishing feature reuse(特征復用缺失),本論文作者希望使用一種淺而寬的模型,來有效提升模型性能。在 ResNetv2 的基礎(chǔ)上,以正確的方式加寬殘差塊提供了一種更加高效地提升殘差網(wǎng)絡(luò)性能的方法。提出的 Wide Residual Networks(WRN),在深度較淺,參數(shù)同等的情況下,獲得不弱于深層網(wǎng)絡(luò)的效果,且訓練更快。并且,論文提出了一種新的 Dropout 使用方法,前人將 dropout 放在 identity 連接上,性能下降,作者認為,應(yīng)該將 dropout 插入卷積層之間,并借此獲得了新的 sota 效果。
設(shè)計貢獻
WRN
殘差網(wǎng)絡(luò)中有兩種類型的模塊,分別為basic(包含兩個3x3卷積,每個卷積緊跟BN層和Relu激活,如下圖a)和bottleneck(包含被兩個1x1卷積包裹的3x3卷積,1x1卷積用來進行維度調(diào)整,如下圖b)。由于BN的位置后來實驗證明改為BN、Relu、Conv訓練得更快,所以這里不考慮原始的設(shè)計結(jié)構(gòu),而且因為bottleneck是為了加深網(wǎng)絡(luò)而提出的,這里也不考慮,只在basic結(jié)構(gòu)上修改。
作者提出有三種方法來增加殘差模塊的表示能力:
- 每個殘差塊增加更多的卷積層
- 通過增加特征面來增加卷積層寬度
- 增大卷積核尺寸
由于VGG和Inception研究表明小卷積有著更好的效果,所以對卷積核尺寸都控制在3x3以下,主要就是研究卷積層數(shù)目和卷積層寬度的影響。為此,引入了兩個系數(shù)l(深度系數(shù))和k(寬度系數(shù)),前者表示殘差模塊中的卷積層數(shù)目,后者表示特征面數(shù)(后文解釋)。
提出了上圖c和d的兩種寬殘差模塊,下表詳細描述了結(jié)構(gòu)內(nèi)部。
事實上,令k=1就會發(fā)現(xiàn)就是基礎(chǔ)的ResNet論文中所用的結(jié)構(gòu),通道分別為16、32和64,堆疊6?N+26*N+26?N+2的深度。本文作者加了個系數(shù)k從而通過增加輸出的通道數(shù)來控制較小的N從而實現(xiàn)更寬(wide)的網(wǎng)絡(luò)。 調(diào)整l和k保證網(wǎng)絡(luò)的復雜性基本不變進行了各種結(jié)構(gòu)的嘗試。由于網(wǎng)絡(luò)加寬使得參數(shù)量增加,需要更有效的正則化方法,BN雖然有效但是需要配合充分的數(shù)據(jù)增強,需要盡量避免使用,通過在每個殘差塊的卷積層之間增加dropout并在Relu后對下一個殘差塊中的BN進行擾動以防過擬合。在非常深的殘差網(wǎng)絡(luò)中,上述方法有助于解決特征重用問題。
在選擇合適的殘差塊設(shè)計后(事實上效果差別不大),與其他網(wǎng)絡(luò)在Cifar數(shù)據(jù)集上進行對比實驗,結(jié)果如下圖。WRN40-4與ResNet1001相比,參數(shù)量類似,結(jié)果類似,訓練速度卻快了很多。這表明,增加寬度對模型的性能是有提升的,但不能武斷認為哪種更好,需要尋優(yōu)配合才行,不過,同等參數(shù),寬度網(wǎng)絡(luò)比深度網(wǎng)絡(luò)容易訓練。
由于實驗進行較多,這里不具體列舉更多數(shù)據(jù)集上的實驗結(jié)果了。最后,經(jīng)過參數(shù)的不斷嘗試,獲得在各個數(shù)據(jù)集上最好的結(jié)果的模型結(jié)構(gòu)如下表。
項目實戰(zhàn)
我用Pytorch簡單實現(xiàn)了WRN的網(wǎng)絡(luò)結(jié)構(gòu)(WRN50-2)并在Caltech101上進行了訓練(原論文作者也在Github開源了代碼,感興趣可以直接訪問),具體的模型代碼如下。由于針對的是Caltech101數(shù)據(jù)集所以使用作者論文中對ImageNet的處理思路,基于Bottleneck結(jié)構(gòu)的ResNet進行修改得到widen模型。
同時,訓練過程的數(shù)據(jù)增強也采用論文里的思路:隨機翻轉(zhuǎn)、隨機裁減以及標準化。
下面是使用PyTorch構(gòu)建模型的源碼,完整的訓練代碼可以在文末Github訪問到。
import torch.nn as nn import torch.nn.init as initclass Conv(nn.Module):"""重載帶relu的卷積層"""def __init__(self, in_channels, out_channels, kernel_size, stride, padding, activation):""":param in_channels::param out_channels::param kernel_size::param stride::param padding::param activation: 是否帶激活層"""super(Conv, self).__init__()self.conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size,stride=stride, padding=padding, bias=True)self.bn = nn.BatchNorm2d(out_channels)if activation:self.f = nn.ReLU(inplace=True)else:self.f = Nonedef forward(self, x):x = self.conv(x)x = self.bn(x)if self.f:x = self.f(x)return xdef wrn_conv1x1(in_channels, out_channels, stride, activate):return Conv(in_channels=in_channels,out_channels=out_channels,kernel_size=1,stride=stride,padding=0,activation=activate)def wrn_conv3x3(in_channels, out_channels, stride, activate):return Conv(in_channels=in_channels,out_channels=out_channels,kernel_size=3,stride=stride,padding=1,activation=activate)class Bottleneck(nn.Module):def __init__(self, in_channels, out_channels, stride, widen_factor):super(Bottleneck, self).__init__()mid_channels = int(round(out_channels // 4 * widen_factor))self.conv1 = wrn_conv1x1(in_channels, mid_channels, stride=1, activate=True)self.conv2 = wrn_conv3x3(mid_channels, mid_channels, stride=stride, activate=True)self.conv3 = wrn_conv1x1(mid_channels, out_channels, stride=1, activate=False)def forward(self, x):x = self.conv1(x)x = self.conv2(x)x = self.conv3(x)return xclass Unit(nn.Module):def __init__(self, in_channels, out_channels, stride, widen_factor):super(Unit, self).__init__()self.resize_identity = (in_channels != out_channels) or (stride != 1)self.body = Bottleneck(in_channels, out_channels, stride, widen_factor)if self.resize_identity:self.identity_conv = wrn_conv1x1(in_channels, out_channels, stride, activate=False)self.activation = nn.ReLU(inplace=True)def forward(self, x):if self.resize_identity:identity = self.identity_conv(x)else:identity = xx = self.body(x)x = x + identityx = self.activation(x)return xclass InitBlock(nn.Module):def __init__(self, in_channels, out_channels):super(InitBlock, self).__init__()self.conv = Conv(in_channels, out_channels, 7, 2, 3, True)self.pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)def forward(self, x):x = self.conv(x)x = self.pool(x)return xclass WRN(nn.Module):def __init__(self, channels, init_block_channels, widen_factor, in_channels=3, img_size=(224, 224), num_classes=101):super(WRN, self).__init__()self.img_size = img_sizeself.num_classes = num_classesself.features = nn.Sequential()self.features.add_module("init_block", InitBlock(in_channels=in_channels, out_channels=init_block_channels))in_channels = init_block_channels# 結(jié)構(gòu)嵌套for i, channels_per_stage in enumerate(channels):stage = nn.Sequential()for j, out_channels in enumerate(channels_per_stage):stride = 2 if (j == 0) and (i != 0) else 1stage.add_module("unit{}".format(j + 1),Unit(in_channels, out_channels, stride, widen_factor))in_channels = out_channelsself.features.add_module("stage{}".format(i + 1), stage)# 平均池化層self.features.add_module('final_avg_pool', nn.AvgPool2d(kernel_size=7, stride=1))# 輸出分類層self.output = nn.Linear(in_features=in_channels, out_features=num_classes)self._init_params()def _init_params(self):for name, module in self.named_modules():if isinstance(module, nn.Conv2d):init.kaiming_uniform_(module.weight)if module.bias is not None:init.constant_(module.bias, 0)def forward(self, x):x = self.features(x)x = x.view(x.size(0), -1)x = self.output(x)return xdef get_wrn(blocks, widen_factor, **kwargs):if blocks == 50:layers = [3, 4, 6, 3]elif blocks == 101:layers = [3, 4, 23, 3]elif blocks == 152:layers = [3, 8, 36, 3]elif blocks == 200:layers = [3, 24, 36, 3]else:raise ValueError("Error WRN block number: {}".format(blocks))init_block_channels = 64channels_per_layers = [256, 512, 1024, 2048]channels = [[ci] * li for (ci, li) in zip(channels_per_layers, layers)]model = WRN(channels, init_block_channels, widen_factor, **kwargs)return modeldef WRN50_2():return get_wrn(50, 2.0)if __name__ == '__main__':model = WRN50_2()print(model)簡單可視化訓練過程(loss圖)如下,訓練速度確實比原始ResNet快很多(同等復雜性網(wǎng)絡(luò))。
補充說明
本文其實相對是比較簡單的論文,驗證了寬度給模型性能帶來的提升,為很多網(wǎng)絡(luò)結(jié)構(gòu)設(shè)計提供了新的思路,我在很多上層計算機視覺任務(wù)的特征提取網(wǎng)絡(luò)中都看到了WRN的影子,是非常實用的殘差網(wǎng)絡(luò)思路。篇幅限制,并沒有太過深入分析原理,感興趣可以查看原論文,并不復雜。最后的實現(xiàn)代碼可以在我的Github訪問到,歡迎star或者fork。
參考論文
Zagoruyko S, Komodakis N. Wide residual networks[J]. arXiv preprint arXiv:1605.07146, 2016.
總結(jié)
- 上一篇: YOLO目标检测算法
- 下一篇: 非极大值抑制算法