PyTorch對ResNet網絡的實現解析

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的結構圖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章