PyTorch對ResNet網絡的實現解析
1.首先導入需要使用的包
import torch.nn as nn
import torch.utils.model_zoo as model_zoo
# 默認的resnet網絡,已預訓練
model_urls = {
'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
}
2.定義一個3*3的卷積層
def conv3x3(in_channels,out_channels,stride=1):
return nn.Conv2d(
in_channels, # 輸入深度(通道)
out_channels, # 輸出深度
kernel_size=3,# 濾波器(過濾器)大小為3*3
stride=stride,# 步長,默認為1
padding=1, # 0填充一層
bias=False # 不設偏置
)
下面會重復使用到這個3*3卷積層,雖然只使用了幾次...
這里為什么用深度而不用通道,是因為我覺得深度相比通道更有數量上感覺,其實都一樣。
3.定義最重要的殘差模塊
這個是基礎塊,由兩個疊加的3*3卷積組成
class BasicBlock(nn.Module):
expansion = 1 # 是對輸出深度的倍乘,在這里等同於忽略
def __init__(self,in_channels,out_channels,stride=1,downsample=None):
super(BasicBlock,self).__init__()
self.conv1 = conv3x3(in_channels,out_channels,stride) # 3*3卷積層
self.bn1 = nn.BatchNorm2d(out_channels) # 批標准化層
self.relu = nn.ReLU(True) # 激活函數
self.conv2 = conv3x3(out_channels,out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
self.downsample = downsample # 這個是shortcut的操作
self.stride = stride # 得到步長
def forward(self,x):
residual = x # 獲得上一層的輸出
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None: # 當shortcut存在的時候
residual = self.downsample(x)
# 我們將上一層的輸出x輸入進這個downsample所擁有一些操作(卷積等),將結果賦給residual
# 簡單說,這個目的就是為了應對上下層輸出輸入深度不一致問題
out += residual # 將bn2的輸出和shortcut過來加在一起
out = self.relu(out)
return out
瓶頸塊,有三個卷積層分別是1x1,3x3,1x1,分別用來降低維度,卷積處理,升高維度
class Bottleneck(nn.Module): # 由於bottleneck譯意為瓶頸,我這里就稱它為瓶頸塊
expansion = 4 # 若我們輸入深度為64,那么擴張4倍后就變為了256
# 其目的在於使得當前塊的輸出深度與下一個塊的輸入深度保持一致
# 而為什么是4,這是因為在設計網絡的時候就規定了的
# 我想應該可以在保證各層之間的輸入輸出一致的情況下修改擴張的倍數
def __init__(self,in_channels,out_channels,stride=1,downsample=None):
super(Bottleneck,self).__init__()
self.conv1 = nn.Conv2d(in_channels,out_channels,kernel_size=1,bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
# 這層1*1卷積層,是為了降維,把輸出深度降到與3*3卷積層的輸入深度一致
self.conv2 = nn.conv3x3(out_channels,out_channels) # 3*3卷積操作
self.bn2 = nn. BatchNorm2d(out_channels)
# 這層3*3卷積層的channels是下面_make_layer中的第二個參數規定的
self.conv3 = nn.Conv2d(out_channels,out_channels*self.expansion,kernel_size=1,bias=False)
self.bn3 = nn.BatchNorm2d(out_channels*self.expansion)
# 這層1*1卷積層,是在升維,四倍的升
self.relu = nn.ReLU(True) # 激活函數
self.downsample = downsample # shortcut信號
self.stride = stride # 獲取步長
def forward(self,x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out) # 連接一個激活函數
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x) # 目的同上
out += residual
out = self.relu(out)
return out
注意:降維只發生在當1*1卷積層的輸出深度大於輸入深度的時候,當輸入輸出深度一樣時是沒有降維的。Resnet中沒有降維的情況只發生在剛開始第一個殘差塊那。
引入Bottleneck的目的是,減少參數的數目,Bottleneck相比較BasicBlock在參數的數目上少了許多,但是精度上卻差不多。減少參數同時還會減少計算量,使模型更快的收斂。
4.ResNet主體部分的實現
class ResNet(nn.Module):
def __init__(self,block,layers,num_classes=10):
# block:為上邊的基礎塊BasicBlock或瓶頸塊Bottleneck,它其實就是一個對象
# layers:每個大layer中的block個數,設為blocks更好,但每一個block實際上也很是一些小layer
# num_classes:表示最終分類的種類數
super(ResNet,self).__init__()
self.in_channels = 64 # 輸入深度為64,我認為把這個理解為每一個殘差塊塊輸入深度最好
self.conv1 = nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3,bias=False)
# 輸入深度為3(正好是彩色圖片的3個通道),輸出深度為64,濾波器為7*7,步長為2,填充3層,特征圖縮小1/2
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(True) # 激活函數
self.maxpool = nn.MaxPool2d(kernel_size=3,stride=2,padding=1) # 最大池化,濾波器為3*3,步長為2,填充1層,特征圖又縮小1/2
# 此時,特征圖的尺寸已成為輸入的1/4
# 下面的每一個layer都是一個大layer
# 第二個參數是殘差塊中3*3卷積層的輸入輸出深度
self.layer1 = self._make_layer(block,64,layers[0]) # 特征圖大小不變
self.layer2 = self._make_layer(block,128,layers[1],stride=2) # 特征圖縮小1/2
self.layer3 = self._make_layer(block,256,layers[2],stride=2) # 特征圖縮小1/2
self.layer4 = self._make_layer(block,512,layers[3],stride=2) # 特征圖縮小1/2
# 這里只設置了4個大layer是設計網絡時規定的,我們也可以視情況自己往上加
# 這里可以把4個大layer和上邊的一起看成是五個階段
self.avgpool = nn.AvgPool2d(7,stride=1) # 平均池化,濾波器為7*7,步長為1,特征圖大小變為1*1
self.fc = nn.Linear(512*block.expansion,num_classes) # 全連接層
# 這里進行的是網絡的參數初始化,可以看出卷積層和批標准化層的初始化方法是不一樣的
for m in self.modules():
# self.modules()采取深度優先遍歷的方式,存儲了網絡的所有模塊,包括本身和兒子
if isinstance(m,nn.Conv2d): # isinstance()判斷一個對象是否是一個已知的類型
nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')
# 9. kaiming_normal 初始化 (這里是nn.init初始化函數的源碼,有好幾種初始化方法)
# torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
# nn.init.kaiming_normal_(w, mode='fan_out', nonlinearity='relu')
# tensor([[ 0.2530, -0.4382, 1.5995],
# [ 0.0544, 1.6392, -2.0752]])
elif isinstance(m,nn.BatchNorm2d):
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias,0)
# 3. 常數 - 固定值 val
# torch.nn.init.constant_(tensor, val)
# nn.init.constant_(w, 0.3)
# tensor([[ 0.3000, 0.3000, 0.3000],
# [ 0.3000, 0.3000, 0.3000]])
def _make_layer(self,block,out_channels,blocks,stride=1):
# 這里的blocks就是該大layer中的殘差塊數
# out_channels表示的是這個塊中3*3卷積層的輸入輸出深度
downsample = None # shortcut內部的跨層實現
if stride != 1 or self.in_channels != out_channels*block.expansion:
# 判斷步長是否為1,判斷當前塊的輸入深度和當前塊卷積層深度乘於殘差塊的擴張
# 為何用步長來判斷,我現在還不明白,感覺沒有也行
downsample = nn.Sequential(
nn.Conv2d(self.in_channels,out_channels*block.expansion,kernel_size=1,stride=stride,bias=False),
nn.BatchNorm2d(out_channels*block.expansion)
)
# 一旦判斷條件成立,那么給downsample賦予一層1*1卷積層和一層批標准化層。並且這一步將伴隨這特征圖縮小1/2
# 而為何要在shortcut中再進行卷積操作?是因為在殘差塊之間,比如當要從64深度的3*3卷積層階段過渡到128深度的3*3卷積層階段,主分支為64深度的輸入已經通過128深度的3*3卷積層變成了128深度的輸出,而shortcut分支中x的深度仍為64,而主分支和shortcut分支相加的時候,深度不一致會報錯。這就需要進行升維操作,使得shortcut分支中的x從64深度升到128深度。
# 而且需要這樣操作的其實只是在基礎塊BasicBlock中,在瓶頸塊Bottleneck中主分支中自己就存在升維操作,那么Bottleneck還在shortcut中引入卷積層的目的是什么?能帶來什么幫助?
layers = []
layers.append(block(self.in_channels,out_channels,stride,downsample))
# block()生成上面定義的基礎塊和瓶頸塊的對象,並將dowsample傳遞給block
self.in_channels = out_channels*block.expansion # 改變下面的殘差塊的輸入深度
# 這使得該階段下面blocks-1個block,即下面循環內構造的block與下一階段的第一個block的在輸入深度上是相同的。
for i in range(1,blocks): # 這里面所有的block
layers.append(block(self.in_channels,out_channels))
#一定要注意,out_channels一直都是3*3卷積層的深度
return nn.Sequential(*layers) # 這里表示將layers中的所有block按順序接在一起
def forward(self,x):
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.maxpool(out) # 寫代碼時一定要仔細,別把out寫成x了,我在這里吃了好大的虧
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.avgpool(out)
out = out.view(out.size(0),-1) # 將原有的多維輸出拉回一維
out = self.fc(out)
return out
5.定義各種ResNet網絡
resnet18,共有18層卷積層
def resnet18(pretrained=False,**kwargs):
'''
pretrained:若為True,則返回在ImageNet數據集上預先訓練的模型
**kwargs:應該只包括兩個參數,一個是輸入x,一個是輸出分類個數num_classes
'''
model = ResNet(BasicBlock,[2,2,2,2],**kwargs)
# block對象為 基礎塊BasicBlock
# layers列表為 [2,2,2,2],這表示網絡中每個大layer階段都是由兩個BasicBlock組成
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))
return model
resnet34,共有34層卷積層
def resnet34(pretrained=False,**kwargs):
model = ResNet(BasicBlock,[3,4,6,3],**kwargs)
# block對象為 基礎塊BasicBlock
# layers列表 [3,4,6,3]
# 這表示layer1、layer2、layer3、layer4分別由3、4、6、3個BasicBlock組成
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))
return model
resnet50,共有50層卷積層
def resnet50(pretrained=False,**kwargs):
model = ResNet(Bottleneck,[3,4,6,3],**kwargs)
# block對象為 瓶頸塊Bottleneck
# layers列表 [3,4,6,3]
# 這表示layer1、layer2、layer3、layer4分別由3、4、6、3個Bottleneck組成
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))
return model
resnet101,共有101層卷積層
def resnet101(pretrained=False,**kwargs):
model = ResNet(Bottleneck,[3,4,23,3],**kwargs)
# block對象為 瓶頸塊Bottleneck
# layers列表 [3,4,23,3]
# 這表示layer1、layer2、layer3、layer4分別由3、4、23、3個Bottleneck組成
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))
return model
resnet152,共有152層卷積層
def resnet152(pretrained=False,**kwargs):
model = ResNet(Bottleneck,[3,8,36,3],**kwargs)
# block對象為 瓶頸塊Bottleneck
# layers列表 [3,8,36,3]
# 這表示layer1、layer2、layer3、layer4分別由3、8、36、3個Bottleneck組成
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))
return model
6.總結
我們可以從上面看出:
- resnet18和resnet34只用到了簡單的BasicBlock,resnet50、resnet101和resnet152用的是Bottleneck。
- Bottleneck相比較BasicBlock在參數量上減少了16.94倍。
- resnet50、resnet101和resnet152三個網絡輸入輸出大小都一樣,只是中間的參數個數不一樣。
- resnet網絡中第一個殘差塊的輸入深度都為64,其他的為殘差塊中3*3卷積層的深度乘以block.expansion。
- 從每一個layer階段到下一個layer階段都伴隨着特征圖縮小1/2,特征圖深度加深1/2。這發生在除第一個layer外的每個layer中的第一個殘差塊中。
- resnet網絡的四個layer前后的操作都是一樣,因此resnet網絡輸入的圖片尺寸固定為224*224(還不確定)。
- 在理解網絡的時候最好結合resnet18、resnet50的結構圖。