深度學習——手動實現殘差網路ResNet 辛普森一家人物識別

HsinTsao發表於2021-10-17

深度學習——手動實現殘差網路 辛普森一家人物識別

目標

通過深度學習,訓練模型識別辛普森一家人動畫中的14個角色

最終實現92%-94%的識別準確率。

資料

image

ResNet介紹

論文地址 https://arxiv.org/pdf/1512.03385.pdf

殘差網路(ResNet)是微軟亞洲研究院的何愷明、孫劍等人2015年提出的,它解決了深層網路訓練困難的問題。利用這樣的結構我們很容易訓練出上百層甚至上千層的網路。
殘差網路的提出,有效地緩解了深度學習兩個大問題

  1. 梯度消失:當使用深層的網路時(例如> 100)反向傳播時會產生梯度消。由於引數初始化一般更靠近0,這樣在訓練的過程中更新淺層網路的引數時,很容易隨著網路的深入而導致梯度消失,淺層的引數無法更新。

  2. 退化問題:層數越多,訓練錯誤率與測試錯誤率反而升高。舉個例子,假設已經有了一個最優化的網路結構,是18層。當我們設計網路結構的時候,我們並不知道具體多少層次的網路時最優化的網路結構,假設設計了34層網路結構。那麼多出來的16層其實是冗餘的,我們希望訓練網路的過程中,模型能夠自己訓練這16層為恆等對映,也就是經過這層時的輸入與輸出完全一樣,這樣子最終的結果和18層是一致的最優的。但是往往模型很難將這16層恆等對映的引數學習正確,那麼就一定會不比最優化的18層網路結構效能好,所以隨著網路深度增加,模型會產生退化現象。它不是由過擬合產生的,而是由冗餘的網路層學習了不是恆等對映的引數造成的。

ResNet使用了一個新的思想,ResNet的思想是假設我們的網路,存在最優化的網路層次,那麼往往我們設計的深層次網路是有很多網路層為冗餘層的。那麼我們希望這些冗餘層能夠完成恆等對映,保證經過該恆等層的輸入和輸出完全相同。
image
這是殘差網路的基本單元,和普通的網路結構多了一個直接到達輸出前的連線(shortcut)。開始輸入的X是這個殘差塊的輸入,F(X)是經過第一層線性變化並啟用後的輸出。在第二層輸出值啟用前加入X,這條路徑稱作shortcut連線,然後再進行啟用後輸出。

這個殘差怎麼理解呢?大家可以這樣理解線上性擬閤中的殘差說的是資料點距離擬合直線的函式值的差,那麼這裡我們可以類比,這裡的X就是我們的擬合的函式,而H(x)的就是具體的資料點,那麼我通過訓練使的擬合的值加上F(x)的就得到具體資料點的值,因此這 F(x)的就是殘差了,還是畫個圖吧,如下圖:
image
引用:https://blog.csdn.net/weixin_42398658/article/details/84627628

ResNet就是在網路中新增shortcut,來構成一個個的殘差塊,從而解決梯度爆炸和網路退化。

ResNet18網路結構

這是一個ResNet18的網路結構,在我的實現中,我根據這個結構搭建網路,並根據自己的實際情況進行調整。
image
引用: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

相關文章