經典的卷積神經網絡
在本次筆記中主要介紹一些經典的卷積神經網絡模型,主要包含以下:
- LeNet:最早發布的卷積神經網絡之一,目的是識別圖像中的手寫數字;
- AlexNet: 是第一個在大規模視覺競賽中擊敗傳統計算機視覺模型的大型神經網絡;
- 使用重復塊的網絡(VGG):利用許多重復的神經網絡塊;
- 網絡中的網絡(NiN):重復使用由卷積層和1×1卷積層(用來代替全連接層)來構建深層網絡;
- 含并行連結的網絡(GoogLeNet):使用并行連結的網絡,通過不同窗口大小的卷積層和最大匯聚層來并行抽取信息;
- 殘差網絡(ResNet):它通過殘差塊構建跨層的數據通道,是計算機視覺中最流行的體系架構;
- 稠密連接網絡(DenseNet):它的計算成本很高,但能帶來了更好的效果。
- 輕量化CNN(mobileNet):旨在優化移動和嵌入式設備上的性能
LeNet
介紹
最早發布的卷積神經網絡之一,因其在計算機視覺任務中的高效性能而受到廣泛關注。 該模型是由AT&T貝爾實驗室的研究員Yann LeCun在1989年提出的(并以其命名),目的是識別圖像中的手寫數字(應用與美國郵政服務)。
LeNet(LeNet-5)由兩個部分組成:
- 卷積編碼器:由兩個卷積層組成;
- 全連接層密集塊:由三個全連接層組成。
在卷積層塊中,每個卷積層都使用5 × 5 的窗口,并在輸出上使用sigmoid激活函數。第一個卷積層輸出通道數為6,第二個卷積層輸出通道數則增加到16。
在圖中網絡結構中匯聚層就是應用最大池化完成的。其中,最大池化的窗口大小為2 × 2,且步幅為2。由于池化窗口與步幅形狀相同,池化窗口在輸入上每次滑動所覆蓋的區域互不重疊。
該網絡使用數據集:MNIST數據集
包含50000個訓練數據、10000個測試數據;樣本均為灰度圖像:28*28;輸出:10類(0-9)
總結
- LeNet是早期成功的神經網絡
- 先使用卷積層來學習圖片空間信息
- 然后使用全連接層來轉換到類別空間
代碼實現
# lenet模型的實現import torch
from torch import nn
from d2l import torch as d2lnet = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Flatten(),nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),nn.Linear(120, 84), nn.Sigmoid(),nn.Linear(84, 10)
)# 我們對原始模型做了一點小改動,去掉了最后一層的高斯激活。除此之外,這個網絡與最初的LeNet-5一致。# 檢查模型
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:X = layer(X)print(layer.__class__.__name__,'output shape: \t',X.shape)# 模型訓練,上面已經實現了LeNet,接下來讓我們看看LeNet在Fashion-MNIST數據集上的表現
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)# 評估模型
def evaluate_accuracy_gpu(net, data_iter, device=None):if isinstance(net, nn.Module):net.eval() # 設置為評估模式# 如果 device 參數未提供,則通過 next(iter(net.parameters())).device 獲取模型第一個參數所在的設備,將其作為后續計算的設備。if not device:device = next(iter(net.parameters())).device# 正常預測的數量,總預測的數量metric = d2l.Accumulator(2)# 在評估階段,不需要計算梯度with torch.no_grad():for X, y in data_iter:# 如果輸入數據 X 是列表類型(通常在 BERT 微調等場景中會遇到),將列表中的每個張量遷移到指定設備上。if isinstance(X, list):# BERT 微調所需的(之后講)X = [x.to(device) for x in X]else:# 否則直接將輸入數據 X 遷移到指定設備上X = X.to(device)# 標簽 y 遷移到指定設備上 y = y.to(device)# 使用 d2l 庫中的 accuracy 函數計算當前批次的預測準確率,即正確預測的樣本數量。metric.add(d2l.accuracy(net(X), y), y.numel())return metric[0] / metric[1]# 訓練函數def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):"""用GPU訓練模型"""# 1. 初始化模型的權重,使用 Xavier 均勻分布初始化線性層和卷積層的權重。def init_weight(m):if type(m) == nn.Linear or type(m) == nn.Conv2d:nn.init.xavier_uniform_(m.weight)net.apply(init_weight)print('training on', device)# 2. 將模型移動到指定的設備(通常是 GPU)上進行訓練。net.to(device)# 3.定義優化器(隨機梯度下降 SGD)和損失函數(交叉熵損失)。optimizer = torch.optim.SGD(net.parameters(), lr=lr)loss = nn.CrossEntropyLoss()# 4. 創建一個動畫繪制器 Animator 用于實時可視化訓練過程中的損失和準確率變化。animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],legend=['train loss', 'train acc', 'test acc'])timer, num_batches = d2l.Timer(), len(train_iter)# 5. 開始多個輪次的訓練,在每個輪次中遍歷訓練數據集,計算損失、反向傳播并更新模型參數。for epoch in range(num_epochs):# 訓練損失之和,訓練準確率之和,樣本數metric = d2l.Accumulator(3)net.train()for i, (X, y) in enumerate(train_iter):timer.start()optimizer.zero_grad()X, y = X.to(device), y.to(device)y_hat = net(X)l = loss(y_hat, y)l.backward()optimizer.step()with torch.no_grad():metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])timer.stop()train_l = metric[0] / metric[2]train_acc = metric[1] / metric[2]if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:animator.add(epoch + (i + 1) / num_batches,(train_l, train_acc ,None))# 6.每個輪次結束后,評估模型在測試數據集上的準確率。test_acc = evaluate_accuracy_gpu(net, test_iter)animator.add(epoch + 1, (None, None, test_acc))# 7.最后輸出訓練結束后的訓練損失、訓練準確率、測試準確率以及訓練速度。print(f'loss{train_l:.3f}, train acc {train_acc:.3f},'f'test acc {test_acc:.3f}')print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec'f'on {str(device)}')# 接下來訓練和評估LeNet-5模型
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
得到結果:
AlexNet
先說點其他的
-
背景
當時關于特征提取有兩種思想:- 觀察圖像特征的提取方法:2012年 前,圖像特征都是機械地計算出來的。
- 認為特征本身應該被學習
-
AlexNet 誕生的關鍵因素
- 數據層面:深度模型需大量有標簽數據才能優于傳統方法。早期受存儲和預算限制,研究多基于小數據集。2009 年 ImageNet 數據集發布并舉辦挑戰賽,提供百萬樣本、千種類別,推動了研究發展。
- 硬件層面:深度學習計算需求高,早期優化凸目標的簡單算法因計算資源限制受青睞。GPU 最初用于圖形處理,其運算與卷積層計算相似,英偉達和 ATI 將其優化用于通用計算。GPU 由眾多小型處理單元組成,雖單個核心性能弱,但數量龐大,在浮點性能、功耗和內存帶寬方面優于 CPU。2012 年,Alex Krizhevsky 和 Ilya Sutskever 利用 GPU 實現快速卷積運算,推動了深度學習發展。
介紹
-
網絡結構
使用了8層卷積神經網絡,前5層是卷積層,剩下的3層是全連接層,具體如下所示。
(由當時GPU內存的限制引起的,作者使用兩塊GPU進行計算,因此分為了上下兩部分) -
與LeNet結構的差異:
-
具體細節:
- 卷積層C1:使用96個核對224×224×3的輸入圖像進行濾波,卷積核大小為11×11×3,步長為4。將一對55×55×48的特征圖分別放入ReLU激活函數,生成激活圖。激活后的圖像進行最大池化,size為3×3,stride為2,池化后的特征圖size為27×27×48(一對)。池化后進行LRN處理。
- 卷積層C2:使用卷積層C1的輸出(響應歸一化和池化)作為輸入,并使用256個卷積核進行濾波,核大小為5 × 5 × 48。
- 卷積層C3:有384個核,核大小為3 × 3 × 256,與卷積層C2的輸出(歸一化的,池化的)相連。
- 卷積層C4:有384個核,核大小為3 × 3 × 192。
- 卷積層C5:有256個核,核大小為3 × 3 × 192。卷積層C5與C3、C4層相比多了個池化,池化核size同樣為3×3,stride為2。
- 全連接F6:此層的全連接實際上是通過卷積進行的,輸入6×6×256,4096個6×6×256的卷積核,擴充邊緣padding = 0, 步長stride = 1, 因此其FeatureMap大小為(6-6+0×2+1)/1 = 1,即1×1×4096;
-
創新特點:
-
更深的神經網絡結構
- AlexNet 是首個真正意義上的深度卷積神經網絡,它的深度達到了當時先前神經網絡的數倍。通過增加網絡深度,AlexNet 能夠更好地學習數據集的特征,從而提高了圖像分類的精度。
-
ReLU激活函數的使用
- AlexNet 首次使用了修正線性單元ReLU這一非線性激活函數。相比于傳統的 sigmoid 和 tanh 函數,ReLU 能夠在保持計算速度的同時,有效地解決了梯度消失問題,從而使得訓練更加高效。
-
ReLU激活函數的使用
- 對于每個特征圖上的每個位置,計算該位置周圍的像素的平方和,然后將當前位置的像素值除以這個和。
- LRN是在卷積層和池化層之間添加的一種歸一化操作。在卷積層中,每個卷積核都對應一個特征圖(feature map),LRN就是對這些特征圖進行歸一化。具體來說,對于每個特征圖上的每個位置,計算該位置周圍的像素的平方和,然后將當前位置的像素值除以這個和。
- 對于每個特征圖上的每個位置,計算該位置周圍的像素的平方和,然后將當前位置的像素值除以這個和。
-
數據增強和Dropout
- 數據增強:通過隨機裁剪、水平翻轉圖像以及對 RGB 通道強度進行 PCA 變換來擴大數據集,減少過擬合,降低錯誤率。
- Dropout:在全連接層使用 Dropout 技術,以 0.5 概率隨機失活神經元,減少神經元間復雜的協同適應,防止過擬合,測試時將神經元輸出乘以 0.5。
-
大規模分布式訓練
- AlexNet在使用GPU進行訓練時,可將卷積層和全連接層分別放到不同的GPU上進行并行計算,從而大大加快了訓練速度。像這種大規模 GPU 集群進行分布式訓練的方法在后來的深度學習中也得到了廣泛的應用。
-
-
分布式GPU的使用方式
-
將網絡分布在兩個 GTX 580 GPU 上進行訓練。兩個 GPU 之間能夠直接讀寫彼此內存,無需通過主機內存,這為跨 GPU 并行化提供了便利。
在分布式訓練時,各層在兩個 GPU 上有不同的分工,如下圖:
-
對模型壓縮的啟發:
-
模型分割與并行化
-
減少冗余與參數共享
如:C4、C5卷積層僅與同GPU的特征圖相連。(參數共享、剪枝) -
優化訓練策略(調整學習率和優化器參數)
-
利用硬件特性(使用低精度計算、設計輕量級網絡結構)
-
-
總結
-
AlexNet的架構與LeNet相似,但使用了更多的卷積層和更多的參數來擬合大規模的ImageNet數據集。
-
現在,AlexNet已經被更有效的架構所超越,但它是從淺層網絡到深層網絡的關鍵一步。
-
盡管AlexNet的代碼只比LeNet多出幾行,但學術界花了很多年才接受深度學習這一概念,并應用其出色的實驗結果。這也是由于缺乏有效的計算工具。
-
Dropout、ReLU和預處理是提升計算機視覺任務性能的其他關鍵步驟。
代碼實現
import torch
from torch import nn
from d2l import torch as d2lnet = nn.Sequential(# 這里使用一個11*11的更大窗口來捕捉對象。# 同時,步幅為4,以減少輸出的高度和寬度。# 另外,輸出通道的數目遠大于LeNetnn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2),# 減小卷積窗口,使用填充為2來使得輸入與輸出的高和寬一致,且增大輸出通道數nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2),# 使用三個連續的卷積層和較小的卷積窗口。# 除了最后的卷積層,輸出通道的數量進一步增加。# 在前兩個卷積層之后,匯聚層不用于減少輸入的高度和寬度nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2),nn.Flatten(),# 這里,全連接層的輸出數量是LeNet中的好幾倍。使用dropout層來減輕過擬合nn.Linear(6400, 4096),nn.ReLU(),nn.Dropout(p=0.5),nn.Linear(4096, 4096), nn.ReLU(),nn.Dropout(p=0.5),# 最后是輸出層。由于這里使用Fashion-MNIST,所以用類別數為10,而非論文中的1000nn.Linear(4096, 10)
)# 構造一個高度和寬度都為224的(單通道數據,來觀察每一層輸出的形狀
X = torch.randn(1, 1, 224, 224)
for layer in net:X = layer(X)print(layer.__class__.__name__,'output shape:\t', X.shape)# 讀取數據集
batch_size = 128
# Fashion-MNIST圖像的分辨率 低于ImageNet圖像。:我們將它們增加到224 x224;這里是為了在AlexNet上使用該數據集;一般不會使用resize=224
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
結果:
可以看到損失比在LeNet上降了很多,訓練精度和測試精度都大幅提高.
這里是在Kaggle上跑的,自己電腦根本跑不動(/(ㄒoㄒ)/~~)
VGG
- 設計背景:AlexNet 雖證明深層神經網絡有效,但未提供通用設計模板。隨著神經網絡架構設計的抽象化發展,研究人員轉向以塊為單位設計網絡,VGG 網絡便是使用塊進行設計的典型代表。
- 經典卷積神經網絡的基本組成部分:
- 帶填充以保持分辨率的卷積層;
- 非線性激活函數,如ReLU;
- 匯聚(池化)層,如最大匯聚(池化)層。
一個VGG塊與之類似,由一系列的卷積層組成,后面再加上用于空間下采樣的最大池化層。
介紹
概念說明:卷積層全部為3*3的卷積核,用conv3-xxx來表示,xxx表示通道數。
在VGG論文中展示了六次實驗的結果:
其中,詳細為:
A:簡單的卷積神經網絡結構
A-LRN:在A的基礎上加了LRN
B:在A的基礎上加了兩個conv3,即多加了兩個33卷積核
C:B的基礎上加了三個conv1,即多加了三個11卷積核
D:C的基礎上把三個conv1換成了三個33卷積核
E:D的基礎上加了三個conv3,即多加了三個33卷積核
- 從以上的實驗中可以得到:
- A和A-LRN對比,LRN并沒有得到好的效果,可去除
- C和D對比:Conv3 比Conv1 更有效
- 統籌看這六個實驗,可發現:隨著網絡層數的加深,模型的表現會越來越好
總結
- VGG使用 可復用的卷積塊構造網絡。不同的VGG模型可通過每個塊中卷積層數量和輸出通道數量的 差異來定義。
- 塊的使用導致網絡定義的非常簡潔,使用塊可以有效地設計復雜的網絡。
- 在VGG論文中,Simonyan和Ziserman嘗試了各種架構。特別是他們發現 深層且窄的卷積(即3 × 3)比 較淺層且寬的卷積更有效。
代碼實現
import torch
from torch import nn
from d2l import torch as d2l# 實現 VGG塊
def vgg_block(num_convs, in_channels, out_channels):layers = []for _ in range(num_convs):# 根據需要幾個添加 卷積 和 激活函數layers.append(nn.Conv2d(in_channels, out_channels,kernel_size=3, padding=1))layers.append(nn.ReLU())in_channels = out_channelslayers.append(nn.MaxPool2d(kernel_size=2, stride=2))return nn.Sequential(*layers)# VGG 網絡: 主要由卷積層和匯聚層組成,第二部分由全連接層組成
# 原始VGG網絡有5個卷積塊,其中前兩個塊各有一個卷積層,后三個塊各包含兩個卷積層。
# 第一個模塊有64個輸出通道,每個后續模塊將輸出通道數量翻倍,直到該數字達到512。
# 由于該網絡使用8個卷積層和3個全連接層,因此它通常被稱為VGG-11。
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))# 下面代碼實現了VGG-11
def vgg(conv_arch):conv_blks = []in_channels = 1# 卷積層部分for (num_convs, out_channels) in conv_arch:conv_blks.append(vgg_block(num_convs, in_channels, out_channels))in_channels = out_channelsreturn nn.Sequential(*conv_blks, nn.Flatten(),# 全連接層部分nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),nn.Linear(4096, 10))net = vgg(conv_arch)# 接下來,構建一個高度和寬度為224的單通道數據樣本,來觀察每個層輸出的形狀
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:X = blk(X)print(blk.__class__.__name__,'output shape:\t',X.shape)# 訓練模型
# [由于VGG-11比AlexNet計算量更大,因此我們構建了一個通道數較少的網絡,足夠用于訓練Fashion-MNIST
ratio = 4
# 這行代碼的主要目的是對 conv_arch 列表中的每個元素進行處理,將每個元素(一個二元組)的第二個值除以 ratio 后得到一個新的二元組,然后把這些新的二元組組合成一個新的列表 small_conv_arch。
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch)lr, num_epochs, batch_size = 0.05, 10, 28
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
結果:
也可以看到訓練精度和測試精度的大幅提升。
NiN
共同的設計模式(LeNet、AlexNet和VGG):通過一系列的卷積層與池化層來提取空間結構特征;然后通過全連接層對特征的表征進行處理。(AlexNet和VGG對LeNet的改進主要在于如何擴大和加深這兩個模塊)
帶來的問題:使用了全連接層,可能會完全放棄表征的空間結構。
解決:網絡中的網絡(NiN)提供了一個非常簡單的解決方案:在每個像素的通道上分別使用多層感知機(具體實現:使用多個連續的1 × 1 卷積層(被視為 MLP),對每個像素點進行多層的非線性特征變換)。
介紹
- NiN塊
卷積層輸入和輸出:由四維張量組成,每個軸對應樣本、通道、高度、寬度。
全連接層輸入和輸出:二維張量,對應樣本、特征。
NiN的想法:在每個像素位置(針對每個高度和寬度)應用一個全連接層。
1×1 卷積層可看作逐像素的全連接層,增
加每像素的非線性變換,還能調整通道數,
在減少參數的同時豐富特征表達。
總結
-
NiN使用由一個卷積層和多個1 x 1卷積層組成的塊。該塊可以在卷積神經網絡中使用,以允許更多的每像素非線性。
-
NiN去除了容易造成過擬合的全連接層,將它們替換為全局平均匯聚層(即在所有位置上進行求和)。該匯聚層通道數量為所需的輸出數量(例如,Fashion-MNIST的輸出為10)。
-
移除全連接層可減少過擬合,同時顯著減少NiN的參數。
-
NiN的設計影響了許多后續卷積神經網絡的設計。
代碼實現
import torch
from torch import nn
from d2l import torch as d2l# NiN塊
# NiN的想法是在每個像素位置(針對每個高度和寬度)應用一個全連接層def nin_block(in_channels, out_channels, kernel_size, strides, padding):return nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),nn.ReLU(),nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())# NiN 模型
# NiN使用窗口形狀為 11×11 、 5×5 和 3×3 的卷積層,輸出通道數量與AlexNet中的相同。
# 每個NiN塊后有一個最大匯聚層,匯聚窗口形狀為 3×3 ,步幅為2。net = nn.Sequential(nin_block(1, 96, kernel_size=11, strides=4, padding=0),nn.MaxPool2d(3, stride=2),nin_block(96, 256, kernel_size=5, strides=1, padding=2),nn.MaxPool2d(3, stride=2),nin_block(256, 384, kernel_size=3, strides=1, padding=1),nn.MaxPool2d(3, stride=2),nn.Dropout(0.5),# 標簽類別數是10nin_block(384, 10, kernel_size=3, strides=1, padding=1),nn.AdaptiveAvgPool2d((1, 1)),# 將四維的輸出轉為二維的輸出,其形狀為(批量大小,10)nn.Flatten()
)# 創建一個數據樣本來[查看每個塊的輸出形狀]
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:X = layer(X)print(layer.__class__.__name__,'output shape: \t', X.shape)# 使用Fashion-MNIST來訓練模型
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
結果:
GoogleNet
設計思想:吸收了NiN中串聯網絡的思想,并在此基礎上做了改進。
重點:解決了什么樣大小的卷積核最合適的問題
介紹
- Inception塊是 GoogLeNet 的基本卷積塊。
- 由四條并行路徑構成,前三條路徑分別使用 1×1、3×3 和 5×5 的卷積層提取不同空間大小的信息,中間兩條路徑先通過 1×1 卷積層減少通道數,以降低模型復雜度。第四條路徑先使用 3×3 最大匯聚層,再用 1×1 卷積層改變通道數。
- 四條路徑都使用合適的填充來使輸入與輸出的高和寬一致,最后將每條線路的輸出在通道維度上連結構成輸出。
- 在Inception塊中,通常調整的超參數是每層輸出通道數。
- 模型框架
一共使用9個Inception塊和全局平均匯聚層的堆疊來生成其估計值.
總結
-
Inception塊相當于一個有4條路徑的子網絡。它通過不同窗口形狀的卷積層和最大匯聚層來并行抽取信息,并使用1 X 1卷積層減少每像素級別上的通道維數從而降低模型復雜度。
-
GoogLeNet將多個設計精細的Inception塊與其他層(卷積層、全連接層)串聯起來。其中Inception塊的通道數分配之比是在ImageNet數據集上通過大量的實驗得來的。
-
GoogLeNet和它的后繼者們一度是ImageNet上最有效的模型之一:它以較低的計算復雜度提供了類似的測試精度。
GoogLeNet有一些后續版本:
-
添加批量規范化層 (Ioffe and Szegedy, 2015)(batch normalization)
-
對Inception模塊進行調整 (Szegedy et al., 2016);
-
使用標簽平滑(label smoothing)進行模型正則化 (Szegedy et al., 2016);
-
加入殘差連接 (Szegedy et al., 2017)。( 下節將介紹)。
代碼實現
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l# (Inception塊)
# Inception塊由四條并行路徑組成。
# 前三條路徑使用窗口大小為 1×1 、 3×3 和 5×5 的卷積層,從不同空間大小中提取信息class Inception(nn.Module):# c1--c4是每條路徑的輸出通道數def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):super(Inception, self).__init__(**kwargs)# 線路1:單1 x 1 卷積層self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)# 線路2:1x1卷積后接 3x3卷積層self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)# 線路3:1x1卷積層后接 5 x 5卷積層self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)# 線路4:3x3最大匯聚層后接 1x1卷積層self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)def forward(self, x):p1 = F.relu(self.p1_1(x))p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))p4 = F.relu(self.p4_2(self.p4_1(x)))# 在通道連結輸出return torch.cat((p1, p2, p3, p4), dim=1)# 濾波器(filter)的組合,它們可以用各種濾波器尺寸探索圖像,這意味著不同大小的濾波器可以有效地識別不同范圍的圖像細節。
# 同時,我們可以為不同的濾波器分配不同數量的參數。
# GoogLeNet一共使用9個Inception塊和全局平均匯聚層的堆疊來生成其估計值。# 逐一來實現GoogleNet 的每個模塊,第一個模塊使用64個通道、7 x 7 卷積層
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)# 第二個模塊:使用兩個卷積層,第一個卷積層是64個通道、1 * 1 卷積層;
# 第二個人卷積層使用將通道數量增加三倍的3 * 3 卷積層, 對應于Inception塊中的第二天路徑
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),nn.ReLU(),nn.Conv2d(64, 192, kernel_size=3, padding=1),nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)# 第三個模塊串聯兩個完整的Inception塊
# 第三個模塊串聯兩個完整的Inception塊。
# 第一個Inception塊的輸出通道數為 64+128+32+32=256 ,四個路徑之間的輸出通道數量比為 64:128:32:32=2:4:1:1 。
# 第二個和第三個路徑首先將輸入通道的數量分別減少到 96/192=1/2 和 16/192=1/12 然后連接第二個卷積層。
# 第二個Inception塊的輸出通道數增加到 128+192+96+64=480 ,四個路徑之間的輸出通道數量比為 128:192:96:64=4:6:3:2 。
# 第二條和第三條路徑首先將輸入通道的數量分別減少到 128/256=1/2 和 32/256=1/8 。b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),Inception(256, 128, (128, 192), (32, 96), 64),nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)# 第4個模塊,串聯了五個Inception塊
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),Inception(512, 160, (112, 224), (24, 64), 64),Inception(512, 128, (128, 256), (24, 64), 64),Inception(512, 112, (144, 288), (32, 64), 64),Inception(528, 256, (160, 320), (32, 128), 128),nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
# 第五模塊包含輸出通道數為 256+320+128+128=832 和 384+384+128+128=1024 的兩個Inception塊
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),Inception(832, 384, (192, 384), (48, 128), 128),nn.AdaptiveAvgPool2d((1,1)),nn.Flatten()
)net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))# 模型計算復雜,不如VGG那樣便于修改通道數
# 為了使Fashion-MNIST上的訓練短小精悍,我們將輸入的高和寬從224降到96]
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:X = layer(X)print(layer.__class__.__name__, 'output shape:\t', X.shape)# 訓練模型
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
結果:
批量規范化
批量規范化是在卷積層或全連接層之后、相應的激活函數之前應用的。
-
先計算小批量的均值和標準差:
-
BN公式: 輸入x∈B
其中, 可學習參數:拉伸參數(scale)γ和偏移參數(shift)β;
應用標準化后,生成的小批量的平均值為0和單位方差為1。 -
標準化的作用:
- 標準化輸入特征可統一參數量級,有助于優化器工作;
- 中間層變量變化范圍大,其分布偏移會阻礙網絡收斂。不同層變量值相差較大時,需針對性的調整學習率;
- 深層網絡結構復雜,容易出現過擬合現象。
ResNet
-
函數類角度
-
具有X特性和y標簽的數據集:
F: 神經網絡架構(包含學習率及其他超參數設置)
-
當我們想要訓練一個更強大的F’,需要滿足 F ∈ F’,即較復雜函數類包含較小函數類。
-
也就是說: 新模型的函數類包含了原模型的函數,新模型具備原模型的所有能力,在處理相同任務時,至少能達到與原模型相同的效果。
-
-
針對深度神經網絡, 將新添加層訓練成恒等映射:
殘差網絡的核心思想:每個附加層都應更容易地包含原始函數作為其元素之一。 于是,殘差塊(residual blocks)便誕生了。
介紹
-
殘差塊
- 對于深度神經網絡,學習恒等映射相對容易。以殘差塊為例,在殘差塊中擬合 f(x) - x 殘差映射比直接擬合f(x)理想映射更簡單。當理想映射f(x)極接近于恒等映射時,殘差映射更易于捕捉恒等映射的細微波動。
- 將新添加層訓練為恒等映射,其權重和偏置參數可近似為零,從優化的角度,這種簡單的映射關系使得訓練過程更穩定,不易出現梯度消失或爆炸等問題,從而保證新模型能達到與原模型相同的性能水平。
-
模型框架
- 前兩層與GoogleNet一樣,不同之處在于ResNet每個卷積層后增加了批量規范化層。
- 每個模塊有4個卷積層(不包括恒等映射的1 * 1 卷積層)。 加上第一個卷積層和最后一個全連接層,共有18層。 因此,這種模型通常被稱為ResNet-18。 (通過配置不同的通道數和模塊里的殘差塊數可以得到不同的ResNet模型,例如更深的含152層的ResNet-152。)
- 雖然ResNet的主體架構跟GoogLeNet類似,但ResNet架構更簡單,修改也更方便.
總結
-
學習嵌套函數(nested function)是訓練神經網絡的理想情況。在深層神經網絡中,學習另一層作為恒等映射(identity function)較容易(盡管這是一個極端情況)。
-
殘差映射可以更容易地學習同一函數,例如將權重層中的參數近似為零。
-
利用殘差塊(residual blocks)可以訓練出一個有效的深層神經網絡:輸入可以通過層間的殘余連接更快地向前傳播。
代碼實現
!pip install d2l
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l# 殘差塊定義
# 殘差塊里首先有2個有相同輸出通道數的 3×3 卷積層。 每個卷積層后接一個批量規范化層和ReLU激活函數。
# 然后我們通過跨層數據通路,跳過這2個卷積運算,將輸入直接加在最后的ReLU激活函數前。
# 這樣的設計要求2個卷積層的輸出與輸入形狀一樣,從而使它們可以相加# 實現如下:
class Residual(nn.Module):def __init__(self, input_channels, num_channels,use_1x1conv=False, strides=1):super().__init__()self.conv1 = nn.Conv2d(input_channels, num_channels,kernel_size=3, padding=1, stride=strides)self.conv2 = nn.Conv2d(num_channels, num_channels,kernel_size=3, padding=1)if use_1x1conv:self.conv3 = nn.Conv2d(input_channels, num_channels,kernel_size=1, stride=strides)else:self.conv3 = Noneself.bn1 = nn.BatchNorm2d(num_channels)self.bn2 = nn.BatchNorm2d(num_channels)def forward(self, X):Y = F.relu(self.bn1(self.conv1(X)))Y = self.bn2(self.conv2(Y))if self.conv3:X = self.conv3(X)Y += Xreturn F.relu(Y)# 查看[輸入和輸出形狀一致]
blk = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
Y.shape# 增加輸出通道數的同時,減半輸出的高和寬]。
blk = Residual(3,6, use_1x1conv=True, strides=2)
blk(X).shape# ResNet模型
# 前兩層跟之前介紹的GoogLeNet中的一樣: 在輸出通道數為64、步幅為2的 7×7 卷積層后,接步幅為2的 3×3 的最大匯聚層
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),nn.BatchNorm2d(64), nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)# ResNet則使用4個由殘差塊組成的模塊,每個模塊使用若干個同樣輸出通道數的殘差塊。
# 第一個模塊的通道數同輸入通道數一致
# 注意:對第一個模塊做了特別處理。
def resnet_block(input_channels, num_channels, num_residuals,first_block=False):blk = []for i in range(num_residuals):if i == 0 and not first_block:blk.append(Residual(input_channels, num_channels,use_1x1conv=True, strides=2))else:blk.append(Residual(num_channels, num_channels))return blk# 接著在ResNet加入所有殘差塊,這里每個模塊使用2個殘差塊。
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))# 最后,在ResNet中加入全局平均匯聚層,以及全連接層輸出。
net = nn.Sequential(b1,b2,b3,b4,b5,nn.AdaptiveAvgPool2d((1,1)),nn.Flatten(), nn.Linear(512, 10)
)# 觀察一下ResNet中不同模塊的輸入形狀是如何變化的
# X = torch.rand(size=(1, 1, 224, 224))
# for layer in net:
# X = layer(X)
# print(layer.__class__.__name__,'output shape:\t', X.shape)# 訓練模型
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
結果:
使用殘差網絡可以看出訓練損失降了很多,并且訓練精度和測試精度有大幅提高;不過兩者存在一定的gap。
DenseNet
-
從 ResNet 到 DenseNet:
-
繼續拓展:
-
稠密連接:
稠密網絡由2部分構成:稠密塊(dense block)和過渡層(transition layer)。
前者定義如何連接輸入和輸出,而后者則控制通道數量。
介紹
-
稠密塊(dense block)
-
由多個使用相同數量輸出通道的卷積塊組成
-
卷積塊架構: BN - ReLU - Conv (bottleneck)
-
主要作用:實現特征重用(即對不同“級別”的特征——不同表征進行總體性地再探索)
-
-
過渡層(transition layer)
- 主要用于連接兩個相鄰的DenseBlock。
- 包括一個1x1卷積(用于調整通道數)和2x2AvgPooling(用于降低特征圖大小),結構為BN+ReLU+1x1 Conv+2x2 AvgPooling。
- 作用:Transition層可以起到壓縮模型的作用。
-
超參數調節
- θ取值(0,1],當θ=1時,feature維度不變,即無壓縮;而θ<1時,這種結構稱為DenseNet-C(文中使用θ=0.5);對于使用bottleneck層的DenseBlock結構和壓縮系數小于1的Transition組合結構稱為DenseNet-BC。
-
網絡結構
DenseNet的網絡結構主要由DenseBlock和Transition組成,一個DenseNet中有3個或4個DenseBlock。而一個DenseBlock中也會有多個Bottleneck layers。最后的DenseBlock之后是一個global AvgPooling層,然后送入一個softmax分類器,得到每個類別所屬分數。
總結
代碼實現
import torch
from torch import nn
from d2l import torch as d2l# 使用 ResNet 改良版中 "BN、ReLU、Conv"架構def conv_block(input_channels, num_channels):return nn.Sequential(nn.BatchNorm2d(input_channels), nn.ReLU(),nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1))# 一個稠密塊由多個卷積塊組成,每個卷積塊使用相同數量的輸出通道。
# 然而,在前向傳播中,我們將每個卷積塊的輸入和輸出在通道維上連結。
class DenseBlock(nn.Module):def __init__(self, num_convs, input_channels, num_channels):super(DenseBlock, self).__init__()layer = []for i in range(num_convs):layer.append(conv_block(num_channels * i + input_channels, num_channels))self.net = nn.Sequential(*layer)def forward(self, X):for blk in self.net:Y = blk(X)# 連接通道維度上每個塊的輸入和輸出X = torch.cat((X, Y), dim=1)return X# 定義一個有2個輸出通道數為10的(DenseBlock)
# 使用通道數為3的輸入時,我們會得到通道數為 3+2×10=23 的輸出blk = DenseBlock(2, 3, 10)
X = torch.randn(4, 3, 8, 8)
Y = blk(X)
Y.shape# 過渡層
# 由于每個稠密塊都會帶來通道數的增加,使用過多則會過于復雜化模型。
# 而過渡層可以用來控制模型復雜度。
# 它通過 1×1 卷積層來減小通道數,并使用步幅為2的平均匯聚層減半高和寬,從而進一步降低模型復雜度。
def transition_block(input_channels, num_channels):return nn.Sequential(nn.BatchNorm2d(input_channels), nn.ReLU(),nn.Conv2d(input_channels, num_channels, kernel_size=1),nn.AvgPool2d(kernel_size=2, stride=2))# 使用 通道數為10的[過渡層]。 此時輸出的通道數減為10,高和寬均減半。
blk = transition_block(23, 10)
blk(Y).shape# 構造 DenseNet模型
# DenseNet首先使用同ResNet一樣的單卷積層和最大匯聚層。
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),nn.BatchNorm2d(64), nn.ReLU(),nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)# DenseNet使用的是4個稠密塊
# num_channels為當前的通道數
num_channels, growth_rate = 64, 32
num_convs_in_dense_blocks = [4, 4, 4, 4]
blks = []
for i, num_convs in enumerate(num_convs_in_dense_blocks):blks.append(DenseBlock(num_convs, num_channels, growth_rate))# 上一個稠密塊的輸出通道數num_channels += num_convs * growth_rate# 在稠密塊之間添加一個轉換層,使通道數量減半if i != len(num_convs_in_dense_blocks) - 1:blks.append(transition_block(num_channels, num_channels // 2))num_channels = num_channels // 2# 最后接上全局匯聚層和全連接層來輸出結果
net = nn.Sequential(b1, *blks,nn.BatchNorm2d(num_channels), nn.ReLU(),nn.AdaptiveAvgPool2d((1, 1)),nn.Flatten(),nn.Linear(num_channels, 10))# 訓練模型lr, num_epochs, batch_size = 0.1, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
結果:
MobileNet
-
研究小而高效的CNN模型(思想):
- 一是對訓練好的復雜模型進行壓縮得到小模型;
- 二是直接設計小模型并進行訓練。
-
核心目標:在保持模型性能(accuracy)的前提下降低模型大小(parameters size),同時提升模型速度(speed, low latency)
MobileNet 模型是 Google 針對手機等嵌入式設備提出的一種輕量級的深層神經網絡.
V1 介紹
主要操作:
(1)將普通卷積換成了深度可分離卷積;
(2)引入了兩個超參數使得可以根據資源來更加靈活的控制自己模型的大小。
-
深度可分離卷積(Depthwise separable convolution)
(追溯歷史,可分為空間可分離卷積和深度可分離卷積)-
分解為兩個操作: 深度卷積(depthwise convolution) 和點卷積( pointwise convolution)
-
回憶下標準卷積:
-
深度卷積(depthwise convolution)
- 對輸入特征圖的每個通道分別應用一個濾波器
- 對輸入特征圖的每個通道分別應用一個濾波器
-
點卷積( pointwise convolution)
- 用 1*1 卷積進行升維和降維
- 用 1*1 卷積進行升維和降維
-
-
與傳統卷積的不同:
-
為什么使用ReLU6?
- 與relu函數區別: 對輸入的最大輸出值做了約束.
- 主要原因解釋: 為了在移動端float16/int8等低精度設備時,也能夠有很好的數值分辨率。如果對Relu的值沒有加以任何限制,則輸出范圍可以從0到無限大,這就使得激活值很大,分布在一個很大的范圍內,而低精度的float16等嵌入式設備就無法很好的精確描述如此大的范圍,從而帶來精度損失。(創新點1)
-
創新點2:雖然MobileNet網絡結構和延遲已經比較小了,但是很多時候在特定應用下還是需要更小更快的模型,為此引入了寬度因子α,為了控制模型大小,引入了分辨因子ρ。
- 寬度因子α(Width Mutiplier)
- 通常α在(0, 1] 之間,比較典型的值由1, 0.75, 0.5, 0.25
- 在每一層對網絡的輸入輸出通道數進行縮減,輸出通道數由 M 到 αM,輸出通道數由 N 到 αN,變換后的計算量為:
- 分辨因子ρ(resolution multiplier)
- 通常ρ在(0, 1] 之間,比較典型的輸入分辨為 224, 192, 160, 128;
- 用于控制輸入和內部層表示,即用分辨率因子控制輸入的分辨率,深度卷積和逐點卷積的計算量為:
- 寬度因子α(Width Mutiplier)
在MobileNetV1中,深度卷積網絡的每個輸入通道都應用了單個濾波器。然后,逐點卷積應用 1*1 卷積網絡來合并深度卷積的輸出。這種標準卷積方法既能濾波,又能一步將輸入合并成一組新的輸出。
- 網絡結構
- 一共由 28層構成(不包括AvgPool 和 FC 層,且把深度卷積和逐點卷積分開算)
其除了第一層采用的是標準卷積核之外,剩下的卷積層都是用Depth Wise Separable Convolution。
- 一共由 28層構成(不包括AvgPool 和 FC 層,且把深度卷積和逐點卷積分開算)
下面簡要對 V2 和 V3 進行一些介紹
-
MobileNet V2
- 在V1的基礎上,引入了倒殘差塊(Inverted Residual Block)和線性激活函數(Linear Activation)。這些改進使得V2在保持輕量級特性的同時,實現了更高的準確性和更低的延遲。倒殘差塊的設計有助于保留和增強特征信息,改善了模型在低資源環境中的表現。
-
MobileNet V3
- 進一步對V2進行了全面改進,采用了HardSwish激活函數、擠壓勵磁模塊(Squeeze-and-Excitation Block),以及MnasNet和NetAdapt等**網絡架構搜索(NAS)**技術。這些技術使得V3在保持高性能的同時,實現了更快的推理速度和更小的模型尺寸。
MobileNet V1代碼實現
import torch
from torch import nn
from d2l import torch as d2l# 定義深度可分離卷積層
class DepthwiseSeparableConv(nn.Module):def __init__(self, in_channels, out_channels, stride=1):super(DepthwiseSeparableConv, self).__init__()# 深度卷積self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=stride, padding=1, groups=in_channels, bias=False)self.bn1 = nn.BatchNorm2d(in_channels)self.relu1 = nn.ReLU(inplace=True)# 逐點卷積self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)self.bn2 = nn.BatchNorm2d(out_channels)self.relu2 = nn.ReLU(inplace=True)def forward(self, x):x = self.relu1(self.bn1(self.depthwise(x)))x = self.relu2(self.bn2(self.pointwise(x)))return x# 定義 MobileNet V1 網絡
class MobileNetV1(nn.Module):def __init__(self, num_classes=10):super(MobileNetV1, self).__init__()# 初始卷積層self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1, bias=False)self.bn1 = nn.BatchNorm2d(32)self.relu1 = nn.ReLU(inplace=True)# 深度可分離卷積層self.layers = nn.Sequential(DepthwiseSeparableConv(32, 64, stride=1),DepthwiseSeparableConv(64, 128, stride=2),DepthwiseSeparableConv(128, 128, stride=1),DepthwiseSeparableConv(128, 256, stride=2),DepthwiseSeparableConv(256, 256, stride=1),DepthwiseSeparableConv(256, 512, stride=2),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 512, stride=1),DepthwiseSeparableConv(512, 1024, stride=2),DepthwiseSeparableConv(1024, 1024, stride=1))# 全局平均池化層self.avgpool = nn.AdaptiveAvgPool2d((1, 1))# 全連接層self.fc = nn.Linear(1024, num_classes)def forward(self, x):x = self.relu1(self.bn1(self.conv1(x)))x = self.layers(x)x = self.avgpool(x)x = x.view(x.size(0), -1)x = self.fc(x)return x# 初始化模型
net = MobileNetV1()# 訓練模型
lr, num_epochs, batch_size = 0.1, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
結果:
總結
- AlexNet :首次運用大型神經網絡在大規模視覺競賽中戰勝傳統計算機視覺模型,開啟深度卷積神經網絡應用的新時代。
- VGG :利用大量重復的神經網絡塊構建模型,探索出一種規整化的網絡構建方式,提升了網絡的可擴展性和性能。
- NiN :創新性地用卷積層和 1×1 卷積層替代全連接層構建深層網絡,減少模型參數數量,提升計算效率。
- GoogLeNet :通過設計 Inception 塊,運用不同窗口大小的卷積層和最大匯聚層并行抽取信息,有效提高模型對圖像特征的提取能力。(寬度方向)
- ResNet :引入殘差塊構建跨層數據通道,解決了深層網絡訓練中的梯度消失等問題,極大地加深了網絡深度,提升模型性能。(深度方向)
- DenseNet :采用稠密連接方式,在通道維上連結各層輸入與輸出,增強了特征傳播與復用能力,在一定程度上提升了模型效果,但計算成本較高。(從feature上入手)
- MobileNet 階段:針對移動端等資源受限場景,提出深度可分離卷積,將普通卷積分解為深度卷積和逐點卷積,在大幅降低計算量與模型大小的同時,保持了較好的分類精度,推動了卷積神經網絡在移動設備等領域的廣泛應用.