學習筆記-ResNet網絡


    

ResNet網絡

  1. ResNet原理和實現
  2. 總結

一、ResNet原理和實現

  神經網絡第一次出現在1998年,當時用5層的全連接網絡LetNet實現了手寫數字識別,現在這個模型已經是神經網絡界的“helloworld”,一些能夠構建神經網絡的庫比如TensorFlow、keras等等會把這個模型當成第一個入門例程。后來卷積神經網絡(Convolutional Neural Networks, CNN)一出現就秒殺了全連接神經網絡,用卷積核代替全連接,大大降低了參數個數,網絡因此也能延伸到十幾層到二十幾層,2012年的8層AlexNet獲得ILSVRC 2012比賽冠軍,2014年22層的GooLeNet獲得ILSVRC 2014亞軍,19層VGG獲得亞軍,但層數也只能到這了,因為研究者們發現:隨着網絡層級的不斷增加,模型精度不斷得到提升,而當網絡層級增加到一定的數目以后,訓練精度和測試精度迅速下降,這說明當網絡變得很深以后,深度網絡就變得更加難以訓練了。為什么?

  這得從神經網絡的傳播算法開始說起。神經網絡用反向傳播算法通過計算參數的梯度,從而將總誤差E分配到各個待調整的參數w上,以此來指導參數的更新。但如果層數過高(比如二十層),梯度變化值會逐漸衰減,以sigmoid函數為例,由於其求導結果等於f(x) *(1-f(x)),且其值域為(0,1),這意味着導數值小於等於0.25,而每經過一層神經元,都要再上一個偏導數的基礎上乘以這個求導結果,因此每經過一層,誤差就會衰減為原來的至少0.25倍,顯然只需經過十幾層二十幾層,誤差就會變的極小以至於趨近於0,這就是限制網絡深度的問題之一:梯度消失。而在另一種極端情況下,每層計算出的梯度值即使乘以f(x) *(1-f(x))后仍然大於1,梯度值以指數級增加最終溢出,這是梯度爆炸。正是這兩個問題導致梯度變得不穩定,網絡難以訓練了。

  梯度消失和梯度爆炸都有一定的緩解方法,比如換成使用ReLU函數作為激活函數,或者是在每層輸入之后添加正則化層。正則化可以使數據分布重新回到標准正態分布,在數學上解釋為“防止輸入分布變動”,一定程度上解決了梯度消失問題,也提高了訓練速度和收斂速度,網絡因此能訓練到幾十層。

  但是即使用了正則化等手段,隨着層數加深,但神經網絡在訓練集的准確度仍然會發生飽和甚至精度下降的問題。這個問題無法解釋為過擬合,因為過擬合是在訓練集上的准確率很高,在測試集上要低。而現在是神經網絡在訓練集上的准確率都下降了。研究者把這種現象稱之為網絡退化

  如何解決退化問題?Kaiming He等人做了實驗:假設現有一個淺層網絡已達到了飽和的准確率,這時在它后面再加上幾個恆等映射層(輸入=輸出的層),這樣可以增加網絡的深度,並且誤差不應該增加。而實驗結果不是這樣,這說明現有的神經網絡似乎很難學習恆等映射函數,也就是去擬合y=x。殘差網絡就是為了解決這個問題而誕生的:讓神經網絡具有學習恆等映射的能力。如果神經網絡的某一層被訓練為恆等映射層,那么這層是否存在對結果不會有影響。換句話說,如果神經網絡能學習恆等映射,那么網絡就具有了自行選擇是否需要某層神經的能力。(思路上似乎與dropout層有一定的共性,只不過dropout層更笨一些:隨機使得一定比例的神經元失效來減小過擬合效果。)

  那么殘差網絡是如何實現學習恆等映射的能力呢?目標函數是H(x)=x,其中H(x)是神經網絡。如果把目標函數設計為H(x)=F(x) + x,當F(x)=0時就能構成一個恆等映射。轉換一下我們需要學F(x) = H(x) - x。按照論文中給出的圖,具體的殘差層可以這樣設計:(這個圖不夠直觀,下面我會畫一個更直觀的)

 

  舉個例子:輸入 x = 2.9  , 經過擬合后的輸出為 H(x)=3.0,那么殘差就是 F(x)=H(X)-x=0.1

       假設 x 從 2.9 經過兩層卷積層之后變為 3.1 ,普通網絡模塊相比較於之前的輸出,其變化率為 \Delta=\frac{3.1-3.0}{3.0}=3.3\text{%} ,而殘差模塊的變化率為 \Delta=\frac{0.2-0.1}{0.1}=100\text{%}

          放大變化有助於網絡更加敏感,學得更快。如何放大變化?減去基數x,只讓網絡學習在x上所疊加的波動,也就是殘差。如果把殘差層畫的更清晰一點,應該是這樣的:

  通過上圖我們更容易理解,殘差網絡將要學習疊加在輸入x上的波動信息,這也是殘差網絡為什么稱之為殘差網絡:殘差就是波動。將波動與輸入x本身分割開來,這能使得殘差網絡更容易學習與輸入本身無關的、更一般的特征。

 

  這個圖更直觀的表現為什么殘差是“輸入本身無關的、更一般的特征”。普通的網絡只能直接學習橙色虛線(神經網絡的輸出結果),但如果在輸出結果中減去輸入x得到殘差曲線(藍色實線)后再學習,可以預見網絡將更快的收斂,精度也會更高。

     回憶前面所說的“學習恆等映射”,在殘差網絡結構中,這個問題就轉換為“學習獲得0輸出”,神經網絡更擅長擬合這類非線性問題。

  這部分的實現體現在下面的代碼中,其中考慮了一個細節問題:如果輸入與輸出的通道數相同,則將輸入的x與輸出結果進行相加作為下一層的輸出;如果輸入與輸出的圖像的通道數不同,那么輸入將會經過一個分支層(conv->bn)轉換其通道數,再與輸出相加。

     downsample = None
        # 在前后通道數不一樣的時候,對輸入進行conv->bn來變換通道數,與輸出相加,實現殘差結構
        if (stride != 1) or (self.in_channels != out_channels):
            downsample = nn.Sequential(nn.Conv2d(self.in_channels, out_channels,kernel_size=3, stride=stride, padding=1,bias=False),
                                       nn.BatchNorm2d(out_channels))
    def forward(self, input):
        # input->conv1->bn1->relu->conv2->bn2=out->out+input->out
        x = input
        out = self.conv1(input)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        #  F(x)+ x 部分,當輸入輸出通道數一樣,則直接與輸入相加,不一樣則經過conv->bn來變換通道數再相加。
        if self.downsample:
            x = self.downsample(input)
        out = out + x  # F(x)+ x
        out = self.relu(out)  # 經過relu
        return out

  為什么加x而不是x/2等其他形式?這個問題再何凱明大神的另一篇文章里提到了,如果x前有系數λ會導致梯度里多一項,反向傳播的時候碰上極端情況(all λ>1)會導致梯度爆炸,而(all λ<1)會使得網絡退化為普通網絡,所以只能是1,更詳細的解釋和實驗都在鏈接里。

  到這里,殘差網絡的學習就差不多了,我借用兩個大神的代碼實現了18層的殘差網絡,結構圖如下:

  最后附上全部代碼(使用Pytorch框架,強烈推薦):

import torch    # 1.0.1
import torch.nn as nn
import torchvision  # 0.2.2
import torchvision.transforms as transforms

# 圖像預處理模塊
transform = transforms.Compose([transforms.Pad(4),
                                transforms.RandomHorizontalFlip(),
                                transforms.RandomCrop(32),
                                transforms.ToTensor()])

# CIFAR-10 dataset
train_dataset = torchvision.datasets.CIFAR10(root='./data',
                                             train=True,
                                             transform=transform,
                                             download=True)

test_dataset = torchvision.datasets.CIFAR10(root='./data',
                                            train=False,
                                            transform=transforms.ToTensor())

# Data loader
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                           batch_size=100,
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                          batch_size=100,
                                          shuffle=False)

# 殘差塊  conv->BN->relu->conv->BN
class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(ResidualBlock, self).__init__()   # 繼承父類init()
        # print("---------------殘差塊開始-------------------------")
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample
        # print("---------------殘差塊結束-------------------------")

    def forward(self, input):
        # input->conv1->bn1->relu->conv2->bn2=out->out+input->out
        x = input
        out = self.conv1(input)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        #  F(x)+ x 部分,當輸入輸出通道數一樣,則直接與輸入相加,不一樣則經過conv->bn來變換通道數再相加。
        if self.downsample:
            x = self.downsample(input)
        out = out + x  # F(x)+ x
        out = self.relu(out)  # 經過relu
        return out

# ResNet
class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=10):
        super(ResNet, self).__init__()
        self.in_channels = 64                                   # 輸入通道
        self.conv = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn = nn.BatchNorm2d(64)                            # BN
        self.relu = nn.ReLU(inplace=True)                       # relu

        self.layer1 = self.make_layer(block, 64, layers[0], 1)
        self.layer2 = self.make_layer(block, 128, layers[1], 2)
        self.layer3 = self.make_layer(block, 256, layers[2], 2)
        self.layer4 = self.make_layer(block, 512, layers[3], 2)
        self.avg_pool = nn.AvgPool2d(4)                         # 平均池化
        self.fc = nn.Linear(512, num_classes)

    def make_layer(self, block, out_channels, blocks, stride=1):
        downsample = None
        # 在前后通道數不一樣的時候,對輸入進行conv->bn來變換通道數,與輸出相加,實現殘差結構
        if (stride != 1) or (self.in_channels != out_channels):
            downsample = nn.Sequential(nn.Conv2d(self.in_channels, out_channels,
                                                 kernel_size=3, stride=stride, padding=1,bias=False),
                                        nn.BatchNorm2d(out_channels))
        layers = []  # 聲明list
        layers.append(block(self.in_channels, out_channels, stride, downsample))  # 構建殘差模塊
        # print(self.in_channels, out_channels, stride)
        self.in_channels = out_channels  # 把輸出通道設為輸入通道
        for i in range(1, blocks):       # 只運行了一次 blocks=2
            layers.append(block(out_channels, out_channels))  # 構建16輸入 16輸出的殘差模塊
        return nn.Sequential(*layers)  # 把網絡層按序添加到模型

    def forward(self, input):
        out = self.conv(input)
        out = self.bn(out)
        out = self.relu(out)
        out = self.layer1(out)
        out = self.layer2(out)
        # print(out.shape)
        out = self.layer3(out)
        out = self.layer4(out)
        out = self.avg_pool(out)
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out


if __name__ == '__main__':
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')# 設置GPU
    torch.set_num_threads(8)  # 設定用於並行化CPU操作的OpenMP線程數  windows環境下必須在main函數下運行,不然報錯
    # 0.定義參數
    num_epochs = 1
    learning_rate = 0.001

    # 1.搭建網絡
    model = ResNet(ResidualBlock, [2, 2, 2, 2]).to(device)  # 模型遷移到GPU運行
    # #     查看每層參數
    # for name, parameters in model.named_parameters():
    #     print(name, ':', parameters.size())

    # #     tensorboard 查看模型結構
    # rand_input = torch.rand(100, 3, 32, 32)
    # with SummaryWriter(comment="network") as w:
    #     w.add_graph(model,rand_input.to(device))

    # 2.定義損失和優化器
    criterion = nn.CrossEntropyLoss()   # 交叉熵損失
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)  # adam優化

    # 3.訓練模型
    total_step = len(train_loader)  # 訓練步長
    curr_lr = learning_rate
    for epoch in range(num_epochs):
        for i, (images, labels) in enumerate(train_loader):
            images = images.to(device)
            labels = labels.to(device)
            # print(images.shape)
            # print(labels)
            # exit()
            outputs = model(images)             # 前向傳播
            loss = criterion(outputs, labels)   # 計算損失
            optimizer.zero_grad()               # 梯度清零
            loss.backward()                     # 后向傳播
            optimizer.step()                    # 參數更新
            if (i + 1) % 100 == 0:
                print("Epoch [{}/{}], Step [{}/{}] Loss: {:.4f}"
                      .format(epoch + 1, num_epochs, i + 1, total_step, loss.item()))

        # 每20周期 學習率下降為原來的三分之一
        if (epoch + 1) % 20 == 0:
            curr_lr /= 3
            for param_group in optimizer.param_groups:
                param_group['lr'] = curr_lr

    # 4.測試模型
    model.eval()    # 測試模式
    # eval()時,pytorch會自動把BN和DropOut固定住,不會取平均,而是用訓練好的值。
    # 不然的話,一旦test的batch_size過小,很容易就會被BN層導致生成圖片顏色失真極大。
    with torch.no_grad():   # 無需計算梯度
        correct = 0
        total = 0
        for images, labels in test_loader:
            images = images.to(device)
            labels = labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

        print('Accuracy of the model on the test images: {} %'.format(100 * correct / total))

    # 5.保存模型及參數
    torch.save(model.state_dict(), 'resnet.ckpt')
完整代碼

 

   代碼里含有tensorboard的相關函數,用於網絡結構的可視化,不需要可以注釋掉。

二、總結

   殘差網絡的設計簡潔漂亮,把輸出分解為輸入+殘差,將神經網絡的學習重心全部轉移到殘差上,使得網絡更專注的學習與輸入無關的、更一般的特征。這就是殘差網絡的成功原因。

參考文獻

  梯度消失、爆炸 https://blog.csdn.net/u011734144/article/details/80164927

  Batch Normalization 批標准化 https://www.cnblogs.com/guoyaohua/p/8724433.html

  理解ResNet:  https://www.cnblogs.com/alanma/p/6877166.html

        https://blog.csdn.net/bryant_meng/article/details/81187434

   代碼實現參考:https://github.com/yunjey/pytorch-tutorial/blob/master/tutorials/02-intermediate/deep_residual_network/main.py#L93

             https://blog.csdn.net/sunqiande88/article/details/80100891

  使用TensorFlow、keras、theano、pytorch框架搭建神經網絡:莫煩老師的神經網絡教程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM