課程中認識許多CNN架構。首先是經典網絡:
- LeNet-5
- AlexNet
- VGG
之后是近年來的一些網絡:
- ResNet
- Inception
- MobileNet
經典網絡
LeNet-5
LeNet-5是用于手寫數字識別(識別0~9的阿拉伯數字)的網絡。它的結構如下:
網絡是輸入是一張[32, 32, 1]的灰度圖像,輸入經過4個卷積+池化層,再經過兩個全連接層,輸出一個0~9的數字。這個網絡和我們上周見過的網絡十分相似,數據體的寬和高在不斷變小,而通道數在不斷變多。
這篇工作是1998年發表的,當時的神經網絡架構和現在我們學的有不少區別:
- 當時padding還沒有得到廣泛使用,數據體的分辨率會越降越小。
- 當時主要使用平均池化,而現在最大池化更常見。
- 網絡只輸出一個值,表示識別出來的數字。而現在的多分類任務一般會輸出10個值并使用softmax激活函數。
- 當時激活函數只用sigmoid和tanh,沒有人用ReLU。
- 當時的算力沒有現在這么強,原工作在計算每個通道卷積時使用了很多復雜的小技巧。而現在我們直接算就行了。
LeNet-5只有6萬個參數。隨著算力的增長,后來的網絡越來越大了。
AlexNet
AlexNet是2012年發表的有關圖像分類的CNN結構。它的輸入是[227, 227, 3]的圖像,輸出是一個1000類的分類結果。
原論文里寫的是輸入形狀是[224, 224, 3],但實際上這個分辨率是有問題的,按照這個分辨率是算不出后續結果的分辨率的。但現在一些框架對AlexNet的復現中,還是會令輸入的分辨率是224。這是因為框架在第一層卷積中加了一個padding的操作,強行讓后續數據的分辨率和原論文對上了。
AlexNet和LeNet-5在架構上十分接近。但是,AlexNet做出了以下改進:
- AlexNet用了更多的參數,一共有約6000萬個參數。
- 使用ReLU作為激活函數。
AlexNet還提出了其他一些創新,但與我們要學的知識沒有那么多關系:
- 當時算力還是比較緊張,AlexNet用了雙GPU訓練。論文里寫了很多相關的工程細節。
- 使用了Local Response Normalization這種歸一化層。現在幾乎沒人用這種歸一化。
VGG-16
VGG-16也是一個圖像分類網絡。VGG的出發點是:為了簡化網絡結構,只用3x3等長(same)卷積和2x2最大池化。
可以看出,VGG也是經過了一系列的卷積和池化層,最后使用全連接層和softmax輸出結果。順帶一提,VGG-16里的16表示有16個帶參數的層。
VGG非常龐大,有138M(1.38億)個參數。但是它簡潔的結構吸引了很多人的關注。
吳恩達老師鼓勵大家去讀一讀這三篇論文。可以先看AlexNet,再看VGG。LeNet有點難讀,可以放到最后去讀。
ResNets(基于殘差的網絡)
非常非常深的神經網絡是很難訓練的,這主要是由梯度爆炸/彌散問題導致的。在這一節中,我們要學一種叫做“跳連(skip connection)”的網絡模塊連接方式。使用跳連,我們能讓淺層模塊的輸出直接對接到深層模塊的輸入上,進而搭建基于殘差的網絡,解決梯度爆炸/彌散問題,訓練深達100層的網絡。
殘差塊
回憶一下,在全連接網絡中,假如我們有中間層的輸出,是怎么由算出來的呢?我們之前用的公式如下:
也就是說,要經過一個線性層、一個激活函數、一個線性層、一個激活函數,才能傳遞到
,這條路徑非常長:
而在殘差塊(Residual block)中,我們使用了一種新的連接方法:
的值被直接加到了第二個ReLU層之前的線性輸出上,這是一種類似電路中短路的連接方法(又稱跳連)。這樣,淺層的信息能更好地傳到深層了。
使用這種方法后,計算公式變更為:
殘差塊中還有一個要注意的細節。這個式子能夠成立,實際上是默認了
,
的維度相同。而一旦
的維度發生了變化,就需要用下面這種方式來調整了。
我們可以用一個W'來完成維度的轉換。為了方便理解,我們先讓所有a都是一維向量,W'是矩陣。這樣,假設的長度是256,的長度是128,則W'的形狀就是256*128。
但實際上,a是一個三維的圖像張量,三個維度的長度都可能發生變化。因此,對于圖像,上式中的W'應該表示的是一個卷積操作。通過卷積操作,我們能夠減小圖像的寬高,調整圖像的通道數,使得和
的維度完全相同。
殘差網絡
在構建殘差網絡ResNet時,只要把這種殘差塊一個一個拼接起來即可。或者從另一個角度來看,對于一個“平坦網絡”("plain network", ResNet論文中用的詞,用于表示非殘差網絡),我們只要把線性層兩兩打包,添加跳連即可。
殘差塊起到了什么作用呢?讓我們看看在網絡層數變多時,平坦網絡和殘差網絡訓練誤差的變化趨勢:
理論上來說,層數越深,訓練誤差應該越低。但在實際中,對平坦網絡增加深度,反而會讓誤差變高。而使用ResNet后,隨著深度增加,訓練誤差起碼不會降低了。
正是有這樣的特性,我們可以用ResNet架構去訓練非常深的網絡。
為什么ResNet是有這樣的特性呢?我們還是從剛剛那個ResNet的公式里找答案。
假設我們設計好了一個網絡,又給它新加了一個殘差塊,即多加了兩個卷積層,那么最后的輸出可以寫成:
即?
由于正則化的存在,所有W和b都傾向于變的更小,極端情況下,W,b都變為0。那么,
再不妨設,則
也是ReLU的輸出,有:
這其實是一個恒等映射,也就是說,新加的殘差塊對之前的輸出沒有任何影響。網絡非常容易學習到恒等映射。這樣,最起碼能夠保證較深的網絡不比淺的網絡差。
準備好了所有基礎知識,我們來看看完整的ResNet長什么樣。
ResNet有幾個參數量不同的版本。這里展示的叫做ResNet-34。完整的網絡很長,我們只用關注其中一小部分就行了。
一開始,網絡還是用一個大卷積核大步幅的卷積以及一個池化操作快速降低圖像的寬度,再把數據傳入殘差塊中。和我們剛剛學的一樣,殘差塊有兩種,一種是維度相同可以直接相加的(實線),一種是要調整維度的(虛線)。整個網絡就是由這若干個這樣的殘差塊組構成。經過所有殘差塊后,還是和經典的網絡一樣,用全連接層輸出結果。
這里,我們只學習了殘差連接的基本原理。ResNet的論文里還有更多有關網絡結構、實驗的細節。最好能讀一讀論文。當然,這周的編程實戰里我們也會復現ResNet,以加深對其的理解。
Inception 網絡
有了之前的知識,我們可以看Inception模塊的完整結構了。1x1卷積沒有什么特別的。為了減少3x3卷積和5x5卷積的計算量,做這兩種卷積之前都會用1x1卷積減少通道數。而為了改變池化結果的通道數,池化后接了一個1x1卷積操作。
實際上,理解了Inception塊,也就能看懂Inception網絡了。如下圖所示,紅框內的模塊都是Inception塊。而這個網絡還有一些小細節:除了和普通網絡一樣在網絡的最后使用softmax輸出結果外,這個網絡還根據中間結果也輸出了幾個結果。當然,這些都是早期網絡的設計技巧了。
MobileNet
MobileNet,顧名思義,這是一種適用于移動(mobile)設備的神經網絡。移動設備的計算資源通常十分緊缺,因此,MobileNet對網絡的計算量進行了極致的壓縮。
再回顧一下:一次卷積操作中主要的計算量如下:
計算量這么大,主要問題出在每一個輸出通道都要與每一個輸入通道“全連接”上。為此,我們可以考慮讓輸出通道只由部分的輸入通道決定。這樣一種卷積的策略叫逐深度可分卷積(Depthwise Separable Convolution)。
這里的depthwise是“逐深度”的意思,但我覺得“逐通道”這個稱呼會更容易理解一點。?
逐深度可分卷積分為兩步:逐深度卷積(depthwise convolution),逐點卷積(pointwise convolution)。逐深度卷積生成新的通道,逐點卷積把各通道的信息關聯起來。
之前,要對下圖中的三通道圖片做卷積,需要3個卷積核分別處理3個通道。而在逐深度卷積中,我們只要1個卷積核。這個卷積核會把輸入圖像當成三個單通道圖像來看待,分別對原圖像的各個通道進行卷積,并生成3個單通道圖像,最后把3個單通道圖像拼回一個三通道圖像。也就是說,逐深度卷積只能生成一幅通道數相同的新圖像。
下一步,是逐點卷積,也就是1x1卷積。它用來改變圖片的通道數。
之前的卷積有2160次乘法,現在只有432+240=672次,計算量確實減少了不少。實際上,優化后計算量占原計算量的比例是:
其中是輸出通道數,f是卷積核邊長。一般來說計算量都會少10倍。
網絡結構
知道了MobileNet的基本思想,我們來看幾個不同版本的MobileNet。
MobileNet v1
MobileNet v2
兩個改進:
- 殘差連接
- 擴張(expansion)操作
殘差連接和ResNet一樣。這里我們關注一下第二個改進。
在MobileNet v2中,先做一個擴張維度的1x1卷積,再做逐深度卷積,最后做之前的逐點1x1卷積。由于最后的逐點卷積起到的是減小維度的作用,所以最后一步操作也叫做投影。
這種架構很好地解決了性能和效果之間的矛盾:在模塊之間,數據的通道數只有3,占用內存少;在模塊之內,更高通道的數據能擬合更復雜的函數。
EfficientNet
EfficientNet能根據設備的計算能力,自動調整網絡占用的資源。
讓我們想想,哪些因素決定了一個網絡占用的運算資源?我們很快能想到下面這些因素:
- 圖像分辨率
- 網絡深度
- 特征的長度(即卷積核數量或神經元數量)
在EfficientNet中,我們可以在這三個維度上縮放網絡,動態改變網絡的計算量。EfficientNet的開源實現中,一般會提供各設備下的最優參數。
卷積網絡實現細節
使用開源實現
由于深度學習項目涉及很多訓練上的細節,想復現一個前人的工作是很耗時的。最好的學習方法是找到別人的開源代碼,在現有代碼的基礎上學習。
使用遷移學習
如第三門課第二周所學,我們可以用遷移學習,導入別人訓練好的模型里的權重為初始權重,加速我們自己模型的訓練。
還是以多分類任務的遷移學習為例(比如把一個1000分類的分類器遷移到一個貓、狗、其他的三分類模型上)。遷移后,新的網絡至少要刪除輸出層,并按照新的多分類個數,重新初始化一個輸出層。之后,根據新任務的數據集大小,凍結網絡的部分參數,從導入的權重開始重新訓練網絡的其他部分:
當然,可以多刪除幾個較深的層,也可以多加入幾個除了輸出層以外的隱藏層。
數據增強
由于CV任務總是缺少數據,數據增強是一種常見的提升網絡性能的手段。
常見的改變形狀的數據增強手段有:
- 鏡像
- 裁剪
- 旋轉
- 扭曲
此外,還可以改變圖像的顏色。比如對三個顏色通道都隨機加一個偏移量。
數據增強有一些實現上的細節:數據的讀取及增強是放在CPU上運行的,訓練是放在CPU或GPU上運行的。這兩步其實是獨立的,可以并行完成。最常見的做法是,在CPU上用多進程(發揮多核的優勢)讀取數據并進行數據增強,之后把數據搬到GPU上訓練。
Pytorch實現ResNet
用到的pytorch基礎知識
1.?TensorDataset
db = TensorDataset(x, y)
-
TensorDataset:PyTorch 中的工具類,將?
x
?和?y
?包裝成一個數據集對象。 -
作用:確保?
x[i]
?和?y[i]
?一一對應(類似?(輸入, 標簽)
?的配對)。
2.?DataLoader
DataLoader(db, batch_size, shuffle=True)
-
DataLoader:PyTorch 的核心工具,用于按批次加載數據。
-
db
:上一步創建的數據集。 -
batch_size
:每個批次的大小(如32)。 -
shuffle=True
:是否打亂數據順序(每個epoch重新隨機排序,防止模型記住數據順序)
-
所以完整代碼是:
-
import torch from torch.utils.data import TensorDataset, DataLoader# 假設有輸入數據 x 和標簽 y(假設是張量) x = torch.randn(100, 3, 224, 224) # 100張224x224的RGB圖像 y = torch.randint(0, 10, (100,)) # 100個標簽(0~9的整數)# 創建 TensorDataset 和 DataLoader dataset = TensorDataset(x, y) dataloader = DataLoader(dataset, batch_size=32, shuffle=True)# 使用示例 for batch_x, batch_y in dataloader:print(batch_x.shape, batch_y.shape) # (32,3,224,224) 和 (32,)
3. torch.max
????????torch.max(out, dim=1)[0]
:獲取最大值(例如?[2.5, 3.2, 4.6]
)。
-
?????torch.max(out, dim=1)[1]
:獲取最大值對應的索引(即預測的類別編號)。 -
predictions = torch.max(out, dim=1)[1] print(predictions) # 輸出:tensor([1, 0, 2])
代碼實現:
此次作業的主要目的是使用殘差網絡實現深層卷積神經網絡完成分類問題。
1. ResNets介紹:
1.1 - 深度神經網絡的問題
我們知道深度神經網絡可以表達出更加復雜的非線性函數,這就可以實現從輸入中提取更多不同的特征。但是隨著網絡層數的加多,梯度消失(vanishing gradient)的效應將被放大,這將導致算法在反向傳播時從最后一層傳播到第一層的過程中,算法乘了每一層的權重矩陣,因此梯度會很快地下降到接近0(或者很快地增加到一個很大的值)。
具體來說,在訓練是你會看到前面層的梯度會非常迅速地降為零:
1.2 - 殘差網絡
殘差網絡可以很好解決深度神經網絡的上訴問題,主要就是使用跳躍連接(skip connection)讓梯度可以直接反向傳遞給前面的層(earlier layers):
殘差塊主要有兩種,根據輸入輸出的維度是否相同劃分為對等塊(identity block)和卷積塊(convolutional block)。
?1.2.1-對等塊
ResNets中的對等塊表示輸入激活值的維度?和輸出激活值的維度
?相同的情況。
圖中上部的路徑表示跳躍連接,下部的路徑表示主路徑。為了加速訓練過程,并在每一層添加了BatchNorm的步驟。
在本次試驗中你將實現一個更加有效的ResNets的對等塊,即進行跨越3個隱藏層的跳躍連接而非2個:
1.2.2 - 卷積塊
ResNets中的卷積塊表示輸入激活值的維度?和輸出激活值的維度
不相同的情況。對于不相同的情況我們對跳躍連接的部分再次應用一個卷積層(CONV2D)以此達到輸入輸出維度相同的目的。
?
這個應用到跳躍連接的卷積層和視頻中所說的矩陣?擁有相同的作用,不過注意這個卷積層不會應用任何的非線性函數,因為這個路徑的作用僅僅是更改輸入層
的維度以便和輸出層
?的維度相匹配。
?1.2.3?- 模型架構
殘差塊用的卷積核為kernel_size=3.模型的conv3_1,conv4_1,conv5_1之前做了寬高減半的downsample.conv2_x是通過maxpool(stride=2)完成的下采樣.其余的是通過conv2d(stride=2)完成的.
2. 構建ResNets模型
2.1 構建殘差塊?
class Residual(nn.Module):def __init__(self, in_channels, out_channels,stride = 1)->None:super().__init__()self.conv1 = nn.Conv2d(in_channels, out_channels, stride = stride, kernel_size =3, padding =1)self.bn1 = nn.BatchNorm2d(out_channels)self.relu = nn.ReLU()self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size =3, padding =1)self.bn2 = nn.BatchNorm2d(out_channels)if in_channels != out_channels:self.conv1x1 = nn.Conv2d(in_channels, out_channels,kernel_size=1,stride=stride)else:self.conv1x1 = Nonedef forward(self, x):o1 = self.relu(self.bn1(self.conv1(x)))o2 = self.bn2(self.conv2(o1))#print("o2 shape",o2.shape)#print("x:",x.shape)if self.conv1x1:x = self.conv1x1(x)return self.relu(o2+x)
2.2 殘差網絡
class ResNet(nn.Module):def __init__(self, in_channels, num_classes) -> None:super().__init__()self.model = nn.Sequential(nn.Conv2d(in_channels=in_channels,out_channels=64,stride = 2,kernel_size=7,padding = 3),#其中 padding=3 是 kernel_size//2,確保 padding='same' 的效果。nn.BatchNorm2d(64),nn.ReLU(),nn.MaxPool2d(kernel_size=3,stride=2,padding=1),Residual(64,64),Residual(64,64),Residual(64,64),Residual(64,128,stride=2),Residual(128,128),Residual(128,128),Residual(128,128),Residual(128,256,stride=2),Residual(256,256),Residual(256,256),Residual(256,256),Residual(256,256),Residual(256,256),Residual(256,512,stride=2),Residual(512,512),Residual(512,512),nn.AdaptiveAvgPool2d(output_size=1)## 自適應平均池化,指定輸出(H,W))self.fc = nn.Linear(512, num_classes)self.softmax = nn.Softmax(dim=1)#print(self.model)def forward(self, x):out = self.model(x)#print("out.shape:",out.shape)out = out.reshape(x.shape[0], -1)#print("out.shape:",out.shape)self.fc(out)return outdef predict(self, x):out = self.forward(x)out = self.softmax(out)return torch.max(out, dim=1)[1]
3. 數據預處理
def load_dataset():train_dataset = h5py.File('datasets/train_signs.h5','r')test_dataset = h5py.File('datasets/test_signs.h5','r')# 直接從把h5數組轉化為tensor太慢,先轉成numpy再轉到tensor更快train_set_x = torch.from_numpy(np.array(train_dataset['train_set_x']))train_set_y = torch.from_numpy(np.array(train_dataset['train_set_y']))test_set_x = torch.from_numpy(np.array(test_dataset['test_set_x']))test_set_y = torch.from_numpy(np.array(test_dataset['test_set_y']))classes = torch.tensor(test_dataset['list_classes'])train_set_x = train_set_x.permute(0,3,1,2) /255test_set_x = test_set_x.permute(0,3,1,2) /255return train_set_x,train_set_y,test_set_x,test_set_y,classesdef data_loader(x, y, batch_size = 32):db = TensorDataset(x, y)return DataLoader(db, batch_size, shuffle=True)train_X,train_Y,test_X,test_Y,classes = load_dataset()
print(f'The num of train set:{train_X.shape[0]}')
print(f'The num of test set:{test_X.shape[0]}')
print(f'The shape of train set(x): {train_X.shape}')
print(f'The shape of train set(y): {train_Y.shape}')
print(f'The number of class: {classes.shape[0]}')
4. 訓練模型
def train(train_X: np.ndarray,train_Y: np.ndarray,num_classes:int,batch_size=32,num_epoch=5):in_channels = train_X.shape[1]net = ResNet(in_channels, num_classes)loss_fn = torch.nn.CrossEntropyLoss()train_loader = data_loader(train_X, train_Y, batch_size)optimizer = torch.optim.Adam(net.parameters(), 5e-4)for e in range(num_epoch):for step, (batch_x, batch_y) in enumerate(train_loader):output = net.forward(batch_x)loss = loss_fn(output, batch_y)optimizer.zero_grad()loss.backward()optimizer.step()print(f'Epoch {e}. loss: {loss}')return net
net = train(train_X, train_Y, classes.shape[0])train_pred = net.predict(train_X)
print(f'Train accuracy: {torch.sum(train_pred == train_Y)/train_Y.shape[0]*100:.2f}%')
test_pred = net.predict(test_X)
print(f'Test accuracy: {torch.sum(test_pred == test_Y) / test_Y.shape[0] * 100:.2f}%')
?
clear memory?
%%javascript
IPython.notebook.save_checkpoint();
if (confirm("Clear memory?") == true)
{IPython.notebook.kernel.restart();
}
?
參考了
吳恩達深度學習C4W2殘差網絡[Pytorch實現]_吳恩達 殘差網絡 練習 pytorch-CSDN博客
https://zhuanlan.zhihu.com/p/544917913