深度學習——手動實現殘差網絡 辛普森一家人物識別
目標
通過深度學習,訓練模型識別辛普森一家人動畫中的14個角色
最終實現92%-94%的識別准確率。
數據
ResNet介紹
論文地址 https://arxiv.org/pdf/1512.03385.pdf
殘差網絡(ResNet)是微軟亞洲研究院的何愷明、孫劍等人2015年提出的,它解決了深層網絡訓練困難的問題。利用這樣的結構我們很容易訓練出上百層甚至上千層的網絡。
殘差網絡的提出,有效地緩解了深度學習兩個大問題
-
梯度消失:當使用深層的網絡時(例如> 100)反向傳播時會產生梯度消。由於參數初始化一般更靠近0,這樣在訓練的過程中更新淺層網絡的參數時,很容易隨着網絡的深入而導致梯度消失,淺層的參數無法更新。
-
退化問題:層數越多,訓練錯誤率與測試錯誤率反而升高。舉個例子,假設已經有了一個最優化的網絡結構,是18層。當我們設計網絡結構的時候,我們並不知道具體多少層次的網絡時最優化的網絡結構,假設設計了34層網絡結構。那么多出來的16層其實是冗余的,我們希望訓練網絡的過程中,模型能夠自己訓練這16層為恆等映射,也就是經過這層時的輸入與輸出完全一樣,這樣子最終的結果和18層是一致的最優的。但是往往模型很難將這16層恆等映射的參數學習正確,那么就一定會不比最優化的18層網絡結構性能好,所以隨着網絡深度增加,模型會產生退化現象。它不是由過擬合產生的,而是由冗余的網絡層學習了不是恆等映射的參數造成的。
ResNet使用了一個新的思想,ResNet的思想是假設我們的網絡,存在最優化的網絡層次,那么往往我們設計的深層次網絡是有很多網絡層為冗余層的。那么我們希望這些冗余層能夠完成恆等映射,保證經過該恆等層的輸入和輸出完全相同。
這是殘差網絡的基本單元,和普通的網絡結構多了一個直接到達輸出前的連線(shortcut)。開始輸入的X是這個殘差塊的輸入,F(X)是經過第一層線性變化並激活后的輸出。在第二層輸出值激活前加入X,這條路徑稱作shortcut連接,然后再進行激活后輸出。
這個殘差怎么理解呢?大家可以這樣理解在線性擬合中的殘差說的是數據點距離擬合直線的函數值的差,那么這里我們可以類比,這里的X就是我們的擬合的函數,而H(x)的就是具體的數據點,那么我通過訓練使的擬合的值加上F(x)的就得到具體數據點的值,因此這 F(x)的就是殘差了,還是畫個圖吧,如下圖:
引用:https://blog.csdn.net/weixin_42398658/article/details/84627628
ResNet就是在網絡中添加shortcut,來構成一個個的殘差塊,從而解決梯度爆炸和網絡退化。
ResNet18網絡結構
這是一個ResNet18的網絡結構,在我的實現中,我根據這個結構搭建網絡,並根據自己的實際情況進行調整。
引用:https://www.researchgate.net/figure/ResNet-18-model-architecture-10_fig2_342828449
項目思路
首先,ResNets利用恆等映射幫助解決漸變消失的問題。我開始嘗試使用簡單的MLP模型,例如一個輸入層、一個輸出層和三個卷積層。但是它的訓練表現很差,只有45%的准確率。所以我需要更多的神經網絡層,但是如果太多的神經網絡層會導致梯度消失的問題。最后我想到了ResNet。
其次,網絡深度決定了savedModel.pth(訓練好的模型)的文件大小,該數據集總共有15,000張圖像和14個類別,並不是很多。所以我選擇了ResNet18,因為我們的數據集不是特別大。
而且不需要更深層次的網絡模型。
ResNet18是一個卷積神經網絡。它的架構有18層。它在圖像分類中是非常有用和有效的。首先是一個卷積層,內核大小為3x3,步幅為1。
在標准的ResNet18模型中,這一層使用7*7的內核大小和步幅2。我在這里做了一些改變。
根據實際情況,因為原始模型中輸入的文件大小是224 * 224,而我們的圖像大小是64 * 64,7*7的內核大小對於這個任務來說太大了。標准ResNet18模型的精度為大約89%,而我修改的模型的准確率大約為94%。
輸入層之后是由剩余塊組成的四個中間層。ResNet的殘余塊是由兩個33卷積層,包括一個shortcut,使用11卷積層直接添加的輸入前一層到另一層的輸出。最后,average pooling應用於的輸出,將最終的殘塊和接收到的特征圖賦給全連通層。
此外,模型中的卷積結果采用ReLu激活函數和歸一化處理。
Data Transform:
為了減少過擬合,我使用圖像變換進行數據增強。並對輸入圖像進行歸一化處理。這樣可以保證所有圖像的分布是相似的,即在訓練過程中更容易收斂,訓練速度更快,效果更好。
我還嘗試將圖像的大小調整為224224,並在輸入層使用77的卷積核,但我發現圖像放大得太多,導致特征模糊,模型性能變差。
歸一化:我使用下面的腳本來計算所有數據的均值和標准差。
data = torchvision.datasets.ImageFolder(root=student.dataset,
transform=student.transform('train'))
trainloader = torch.utils.data.DataLoader(data,
batch_size=1, shuffle=True)
mean = torch.zeros(3)
std = torch.zeros(3)
for batch in trainloader:
images, labels = batch
for d in range(3):
mean[d] += images[:, d, :, :].mean()
std[d] += images[:, d, :, :].std()
mean.div_(len(data))
std.div_(len(data))
print(list(mean.numpy()), list(std.numpy()))
超參數及其他設定
Epochs, batch_size, learning rate:
epochs = 120
, 如果太小,收斂可能不會結束。
batch_size = 256
, 如果batch_size太小,可能會導致收斂速度過慢或損失不會減少
lr = 0.001
,當學習率設置過大時,梯度可能會圍繞最小值振盪,甚至無法收斂
Loss function:
torch.nn.CrossEntropyLoss()
是很適合圖像作業的
Aptimiser:
我嘗試過SGD
, RMSprop
, Adadelta
等,但Adam
是最適合我的。
Dropout and weight_decay: not use them
當我試圖設置它們來減少過擬合問題時,效果並不好。這種設置使loss無法減少或精度降低。
代碼
圖像增強代碼
def transform(mode):
"""
Called when loading the data. Visit this URL for more information:
https://pytorch.org/vision/stable/transforms.html
You may specify different transforms for training and testing
"""
if mode == 'train':
return transforms.Compose([
# transforms.Grayscale(num_output_channels=1),
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.42988312, 0.42988312, 0.42988312],
[0.17416202, 0.17416202, 0.17416202])
])
elif mode == 'test':
return transforms.Compose([
# transforms.Grayscale(num_output_channels=1),
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.42988312, 0.42988312, 0.42988312],
[0.17416202, 0.17416202, 0.17416202])
])
ResNet18手工搭建
class Network(nn.Module):
def __init__(self, num_classes=14):
super().__init__()
self.inchannel = 64
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU()
# nn.MaxPool2d(3, 1, 1)
)
self.layer1 = self.make_layer(ResBlock, 64, 2, stride=1)
self.layer2 = self.make_layer(ResBlock, 128, 2, stride=2)
self.layer3 = self.make_layer(ResBlock, 256, 2, stride=2)
self.layer4 = self.make_layer(ResBlock, 512, 2, stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, num_classes)
def make_layer(self, block, channels, num_blocks, stride):
layers = []
for i in range(num_blocks):
if i == 0:
layers.append(block(self.inchannel, channels, stride))
else:
layers.append(block(channels, channels, 1))
self.inchannel = channels
return nn.Sequential(*layers)
def forward(self, x):
out = self.conv1(x)
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.avgpool(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
return out
class ResBlock(nn.Module):
def __init__(self, inchannel, outchannel, stride=1):
super(ResBlock, self).__init__()
# two 3*3 kenerl size conv layers
self.left = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(outchannel),
nn.ReLU(inplace=True),
nn.Conv2d(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(outchannel)
)
self.shortcut = nn.Sequential()
if stride != 1 or inchannel != outchannel:
# shortcut,1*1 kenerl size
# shortcut,這里為了跟2個卷積層的結果結構一致,要做處理
self.shortcut = nn.Sequential(
nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(outchannel)
)
def forward(self, x):
out = self.left(x)
# 將2個卷積層的輸出跟處理過的x相加,實現ResNet的基本結構
out = out + self.shortcut(x)
out = F.relu(out)
return out
參考
論文:
https://arxiv.org/pdf/1512.03385.pdf
參考博客:
https://www.cnblogs.com/gczr/p/10127723.html
https://blog.csdn.net/weixin_42398658/article/details/84627628