明月深度學習實踐004:ResNet網絡結構學習

2021-02-20 紅樓明月

ResNet可謂大名鼎鼎了,一直遵循拿來主義,沒有好好去學習它,當然,作為一個提出來快五年的網絡結構,已經有太多人寫過它了,不好下筆。

趁著假期好好梳理一遍,相關資源:

原論文:https://arxiv.org/pdf/1512.03385.pdf

Pytorch實現:https://github.com/pytorch/vision/blob/master/torchvision/models/resnet.py

本文將從源碼出發,進行學習。

1. ResNet總體介紹

在ResNet的原始論文裡,共介紹了幾種形式:

如無特殊說明,截圖均來自原始論文

作者根據網絡深度不同,一共定義了5種ResNet結構,從18層到152層,每種網絡結構都包含五個部分的卷積層,從conv1, conv2_x到conv5_x。這些卷積層我們拆解一下,其實就三種類型:

1.1 普通卷積conv1

conv1是一種普通的卷積,卷積核是7*7,輸出64通道,步長2,輸出size是112*112。圖中並沒有說padding值是多少,在Pytorch的官方實現中,該值為3:

self.inplanes = 64self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)self.bn1 = nn.BatchNorm2d(self.inplanes)self.relu = nn.ReLU(inplace=True)

輸入size在論文中有寫,是224*224.

在這後面還有一個最大池化層:

self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

至此,輸出就變成了56*56。

為什麼輸入是224*224,池化之後是56*56?

我的理解是,這更多是一種實驗發現的,對作者使用的數據集效果比較好的參數(作者使用的數據集當然是比較流行的開放數據集),但是是否在特定場景下是否就是最優呢?我決定並不見得,特定場景下,輸入size可能往往是比較規範的,那就完全有可能產生更好的參數。

1.2 BaseBlock殘差塊結構

就是ResNet18和ResNet34中的3*3卷積核的部分:

def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):    """3x3 convolution with padding"""    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,                     padding=dilation, groups=groups, bias=False, dilation=dilation)
class BasicBlock(nn.Module): expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1, base_width=64, dilation=1, norm_layer=None): super(BasicBlock, self).__init__() if norm_layer is None: norm_layer = nn.BatchNorm2d if groups != 1 or base_width != 64: raise ValueError('BasicBlock only supports groups=1 and base_width=64') if dilation > 1: raise NotImplementedError("Dilation > 1 not supported in BasicBlock") self.conv1 = conv3x3(inplanes, planes, stride) self.bn1 = norm_layer(planes) self.relu = nn.ReLU(inplace=True) self.conv2 = conv3x3(planes, planes) self.bn2 = norm_layer(planes) self.downsample = downsample self.stride = stride
def forward(self, x): identity = 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: identity = self.downsample(x)
out += identity        out = self.relu(out) return out

所謂殘差塊,就是兩個3*3的卷積層堆疊在一起:

例如,對於ResNet18的殘差塊conv2_x結構:

而所謂殘差連結,其實關鍵就一句:

     out += identity

就是跳層將輸入和輸出進行相加,以解決深層網絡在訓練時容易導致的梯度消失或者爆炸問題。作者論文對此有過實驗:

網絡從20層增加到56層的時候,訓練誤差和測試誤差都顯著增加了。這也是比較好理解吧,經過深層網絡,原有的特徵可能都已經消失了。

為了殘差能正常連接在一起,殘差塊的輸入輸出需要是一致的。

看源碼,還有一個downsample,這個後面再說。

1.2 Bottleneck殘差塊

除了ResNet18和34,其他幾個用的都是Bottleneck殘差塊,還是一樣,我們先看代碼:

def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):    """3x3 convolution with padding"""    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,                     padding=dilation, groups=groups, bias=False, dilation=dilation)
def conv1x1(in_planes, out_planes, stride=1): """1x1 convolution""" return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False) class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1, base_width=64, dilation=1, norm_layer=None): super(Bottleneck, self).__init__() if norm_layer is None: norm_layer = nn.BatchNorm2d width = int(planes * (base_width / 64.)) * groups self.conv1 = conv1x1(inplanes, width) self.bn1 = norm_layer(width) self.conv2 = conv3x3(width, width, stride, groups, dilation) self.bn2 = norm_layer(width) self.conv3 = conv1x1(width, planes * self.expansion) self.bn3 = norm_layer(planes * self.expansion) self.relu = nn.ReLU(inplace=True) self.downsample = downsample self.stride = stride
def forward(self, x): identity = 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: identity = self.downsample(x)
out += identity        out = self.relu(out) return out

圖示大概如下:

核心思想就是利用1*1的卷積核來靈活控制通道數,使得殘差能順利連接在一起。具體做法就是使用一個1*1卷積核將通道數降為原來的1/4(降維),在進行3*3的卷積運算之後(保持通道數不變),又使用一個1*1卷積核將通道數提升為跟輸入一樣的通道數(升維)。

這樣做的好處是大大減少了參數的數量,如上圖的參數量是:1x1x256x64 + 3x3x64x64 + 1x1x64x256 = 69632,而如果這使用BaseBlock來實現的話,參數量變為:3x3x256x256x2 = 1179648,約為前者的17倍。我們看從ResNet34到ResNet50,深度增加了差不多三分之二,但是計算量卻只是增加5%左右(當然這樣比較有點作弊的嫌疑,畢竟ResNet50不少的層都是1*1的卷積運算,用於升降維)。

注意這裡的降維時的通道數計算:

     width = int(planes * (base_width / 64.)) * groups

這裡的base_width對應的,就是訓練時的width_per_group參數,在默認值的情況下,width值就等於planes,顯然可以通過改變width_per_group和groups參數,來改變降維後的通道數。

不過,如果說這樣降維沒有特徵損失,我個人是不太相信的,只是說可能這種損失比較小。

2. ResNet的實現

講完了ResNet的基礎介紹,我們就可以講其實現了:

class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000, zero_init_residual=False, groups=1, width_per_group=64, replace_stride_with_dilation=None, norm_layer=None): super(ResNet, self).__init__() if norm_layer is None: norm_layer = nn.BatchNorm2d self._norm_layer = norm_layer
self.inplanes = 64 self.dilation = 1 if replace_stride_with_dilation is None: replace_stride_with_dilation = [False, False, False] if len(replace_stride_with_dilation) != 3: raise ValueError("replace_stride_with_dilation should be None " "or a 3-element tuple, got {}".format(replace_stride_with_dilation)) self.groups = groups self.base_width = width_per_group self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False) self.bn1 = norm_layer(self.inplanes) self.relu = nn.ReLU(inplace=True) self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) self.layer1 = self._make_layer(block, 64, layers[0]) self.layer2 = self._make_layer(block, 128, layers[1], stride=2, dilate=replace_stride_with_dilation[0]) self.layer3 = self._make_layer(block, 256, layers[2], stride=2, dilate=replace_stride_with_dilation[1]) self.layer4 = self._make_layer(block, 512, layers[3], stride=2, dilate=replace_stride_with_dilation[2]) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(512 * block.expansion, num_classes)
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, nn.GroupNorm)): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0)
if zero_init_residual: for m in self.modules(): if isinstance(m, Bottleneck): nn.init.constant_(m.bn3.weight, 0) elif isinstance(m, BasicBlock): nn.init.constant_(m.bn2.weight, 0)
def _make_layer(self, block, planes, blocks, stride=1, dilate=False): norm_layer = self._norm_layer downsample = None previous_dilation = self.dilation if dilate: self.dilation *= stride stride = 1 if stride != 1 or self.inplanes != planes * block.expansion: downsample = nn.Sequential( conv1x1(self.inplanes, planes * block.expansion, stride), norm_layer(planes * block.expansion), )
layers = [] layers.append(block(self.inplanes, planes, stride, downsample, self.groups, self.base_width, previous_dilation, norm_layer)) self.inplanes = planes * block.expansion for _ in range(1, blocks): layers.append(block(self.inplanes, planes, groups=self.groups, base_width=self.base_width, dilation=self.dilation, norm_layer=norm_layer))
return nn.Sequential(*layers)
def _forward_impl(self, x): x = self.conv1(x) x = self.bn1(x) x = self.relu(x) x = self.maxpool(x)
x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x)
x = self.avgpool(x) x = torch.flatten(x, 1)        x = self.fc(x) return x
def forward(self, x): return self._forward_impl(x)

其中關鍵的方法就是_make_layer,這是用來生成殘差塊的。現在我們可以看一下這個downsample了,它的觸發條件是,如果卷積步長不等於1或者輸入通道數不滿足對應的關係,則1*1的卷積運算,調整輸入通道數,而這個downsample會在模塊的第一個殘差塊進行。

這裡實現中,看不出來_forward_impl這個為什麼單獨寫成了也給函數,貌似在forward中實現並無不妥。

對於訓練時,ResNet18執行的大概是:

ResNet(BasicBlock, [2, 2, 2, 2])

其中第二個參數是conv2_x到conv5_x這四個部分的殘差塊的數量。

再看一下ResNet50的執行:

ResNet(Bottleneck, [3, 4, 6, 3])

其他的都類似了。

另外,Pytorch實現的ResNet還有一些技巧,如:zero_init_residual等。

3. ResNet的變體

ResNet發布已經四五年了,相關的研究很多,Pytorch也實現了兩種變體:

3.1 ResNeXT

這是16年底提出的結構,在Pytorch的實現中,其實只是訓練時的參數不同,增加了分組卷積的參數:

# ResNeXt-101 32x8d modelkwargs['groups'] = 32kwargs['width_per_group'] = 8ResNet(Bottleneck, [3, 4, 23, 3],**kwargs)

3.2 Wide ResNet

這是16年中提出的結構,在Pytorch的實現差異也不大:

kwargs['width_per_group'] = 64 * 2ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)

這個參數的作用其實就是增加了降維之後的通道數為標準ResNet的兩倍,也就是原來降維為1/4改為降維為1/2。

ResNet還有一些其他的升級版本,待續。

相關焦點

  • 深度學習第19講:CNN經典論文研讀之殘差網絡ResNet及其keras實現
    作者:魯偉一個數據科學踐行者的學習日記。
  • 重讀經典:完全解析特徵學習大殺器ResNet
    >>加入極市CV技術交流群,走在計算機視覺的最前沿論文標題:Deep Residual Learning for Image Recognition通過總結前人的經驗,我們常會得出這樣的結論:通過堆疊神經網絡層數(增加深度)可以非常有效地增強表徵,提升特徵學習效果。為什麼深度的網絡表徵效果會好?
  • 如果我們想要更好的目標分割,我們最好使用resnet-50的網絡結構
    卷積網絡是研究圖像分類的非常有效的方法,它能夠產生豐富的學習框架,能夠做很多複雜的預測。但是,在什麼情況下它非常有效呢?解決不同任務需要不同的方法。我們已經研究過人臉識別(single-identitydetection)。
  • ResNet——CNN經典網絡模型詳解(pytorch實現)
    2、ResNet詳解在ResNet網絡中有如下幾個亮點:提出residual結構(殘差結構),並搭建超深的網絡結構(突破1000層)使用Batch Normalization加速訓練(丟棄dropout)在ResNet網絡提出之前,傳統的卷積神經網絡都是通過將一系列卷積層與下採樣層進行堆疊得到的。但是當堆疊到一定網絡深度時,就會出現兩個問題。
  • 圖像分割中的深度學習:U-Net 體系結構
    同時,目前也出現了很多利用卷積神經網絡進行分割的方法,這些方法已成為解決圖像分割中更高級任務中不可或缺的方法。在這篇文章中,我們將仔細看看一個這樣的架構:u-net。深度學習是需要數據集來訓練模型的。但是對於數據的獲取有一定難度。對於某一個對象而言,我們並沒有足夠的數據進行訓練。在這種情況下,往往需要花費時間、金錢,最重要的是硬體設備。
  • 【新書推薦】TensorFlow深度學習及實踐
    TensorFlow是2015年年底開源的一套深度學習框架,也是目前最活躍的深度學習框架之一。《TensorFlow深度學習及實踐》從深度學習的基礎講起,深入TensorFlow的基本框架、原理、原始碼和實現等各個方面,其目的在於降低學習門檻,為讀者解決問題提供詳細的方法和指導。
  • 新書推薦:TensorFlow深度學習及實踐
    自從深度學習在語音識別和圖像識別任務中取得突破性成果後,使用深度學習的應用數量開始呈爆炸式增加。深度學習方法被大量應用在身份識別、無人駕駛、癌症檢測、遊戲AI等方面,甚至在許多領域,深度神經網絡的準確度已經超過人類自身的操作。深度學習的數學原理並不複雜,但它的一些設計思想很巧妙。入門深度學習,在數學方面只要知道如何對函數求導以及知道與矩陣相乘相關的知識即可。
  • 深度學習筆記 | 第9講:遷移學習——理論與實踐
    又到了每周一狗熊會的深度學習時間了。在前幾期的分享中,小編和大家將深度學習計算機視覺的三大任務,圖像分類、目標檢測和圖形分割的發展歷程和經典網絡進行了一個概述性的了解。本講中小編將與大家分享一個新的主題——遷移學習(Transfer Learning)。
  • 【深度學習系列】用PaddlePaddle和Tensorflow實現經典CNN網絡GoogLeNet
    點擊上圖,立即開啟AI急速修煉作者:Charlotte    高級算法工程師 ,博客專家;擅長用通俗易懂的方式講解深度學習和機器學習算法,熟悉Tensorflow,PaddlePaddle等深度學習框架,負責過多個機器學習落地項目,如垃圾評論自動過濾,用戶分級精準營銷,分布式深度學習平臺搭建等,都取了的不錯的效果。
  • 遷移學習理論與實踐
    本文闡述了遷移學習的理論知識、基於ResNet的遷移學習實驗以及基於resnet50的遷移學習模型。 >>加入極市CV技術交流群,走在計算機視覺的最前沿在深度學習模型日益龐大的今天,並非所有人都能滿足從頭開始訓練一個模型的軟硬體條件,稀缺的數據和昂貴的計算資源都是我們需要面對的難題。遷移學習(Transfer Learning)可以幫助我們緩解在數據和計算資源上的尷尬。
  • ResNet50網絡結構圖及結構詳解
    引言之前我讀了ResNet的論文Deep Residual Learning for Image Recognition,也做了論文筆記[1],筆記裡記錄了ResNet的理論基礎(核心思想、基本Block結構、Bottleneck結構、ResNet多個版本的大致結構等等),看本文之間可以先看看打打理論基礎。
  • 零基礎入門深度學習(七):圖像分類任務之VGG、GoogLeNet和ResNet
    從本課程中,你將學習到:本周為開講第四周,百度深度學習技術平臺部資深研發工程師孫高峰,開始講解計算機視覺中圖像分類任務。在上一節課中,我們為大家介紹了經典的LeNet和AlexNet神經網絡結構在眼疾識別任務中的應用,本節將繼續為大家帶來更多精彩內容。
  • 【乾貨】深度學習實驗流程及 PyTorch 提供的解決方案
    新智元推薦 來源:專知【新智元導讀】在研究深度學習的過程中,當你腦中突然迸發出一個靈感,你是否發現沒有趁手的工具可以快速實現你的想法?看完本文之後,你可能會多出一個選擇。本文簡要分析了研究深度學習問題時常見的工作流, 並介紹了怎麼使用 PyTorch 來快速構建你的實驗。
  • 神經網絡與深度學習
    深度學習筆記深度學習作為當前機器學習一個最熱門的發展方向,仍然保持著傳統機器學習方法的理念與特徵,從監督學習的角度看,深度學習與機器學習並無本質上的差異。講了這麼多,無非就是想讓大家知道,以神經網絡為核心的深度學習是機器學習的一個領域分支,所以深度學習在其本質上也必須遵循機器學習的基本要義和法則。在傳統的機器學習中,我們需要訓練的是結構化的數值數據,比如預測銷售量、預測某人是否按時還款,等等。但在深度學習中,其訓練輸入就不再是常規的數值數據了,它可能是一張圖片、一段語言、一段對話語音或一段視頻。
  • 《神經網絡和深度學習》系列文章四:神經網絡的結構
    本節譯者:哈工大SCIR碩士生 徐偉 (https://github.com/memeda)聲明:我們將在每周一,周四,周日定期連載該書的中文翻譯,如需轉載請聯繫wechat_editors@ir.hit.edu.cn,未經授權不得轉載。
  • 快速訓練殘差網絡 ResNet-101,完成圖像分類與預測,精度高達 98%|...
    出於這個原因,這一次,我將採用一種巧妙的方法——遷移學習來實現。即在預訓練模型的基礎上,採用101層的深度殘差網絡ResNet-101,對如下圖所示的花數據集進行訓練,快速實現了對原始圖像的分類和預測,最終預測精確度達到了驚人的98%。
  • VGGNet、ResNet、Inception和Xception圖像分類及對比
    圖像分類任務是一個典型的深度學習應用。
  • 深度學習三人行(第12期)----CNN經典網絡之ResNet
    上一期,我們一起學習了深度學習卷積神經網絡中的經典網絡之GoogLeNet,深度學習三人行(第11期)----CNN經典網絡之GoogLeNet
  • ResNeXt:何愷明 Facebook 升級 ResNet,提出神經網絡新維度
    論文地址:https://arxiv.org/pdf/1611.05431.pdf實例:ResNeXt 算法圖像分類 Torch 實現這個庫包含了 ResNeXt採用這種簡單設計的結果,是實現了一種多分支的同構結構,只需要設定很少的一些超參數新的維度,我們將其稱之為「基數」(cardinality,transformation 集合的大小),這是衡量神經網絡在深度(depth)和廣度(width)之外的另一個重要因素。
  • 深度學習的學習歷程
    alexnet、vgg、googlenet、resnet等網絡就像樂高一樣,把這些模塊當積木一樣組合起來,好像也沒啥特別的。又好像什麼都不懂,學會這些模塊的公式就算會深度學習了嗎?整個深度學習的學習周期是怎樣的,我下一步應該幹啥?這些模塊看起來平平無奇,為什麼組合在一起就能發揮這麼大威力?為什麼drop out能起到正則作用?L1正則和L2正則有什麼區別?