經典卷積神經網路演算法(5):ResNet

 

1 引言

神經網路演算法有時候又被稱為深度學習,究其原因就是因為神經網路模型可以通過添加網路層數來擴展網路的深度以獲得更優越的性能。以CNN網路為例,卷積層數越多,模型越能夠提取到更高層次的特徵,資訊更加豐富。所以,我們不禁要猜想,是不是網路的深度越深,模型的性能越好。如果真是這樣,那神經網路就是近乎無所不能的演算法,沒有什麼是添加一層網路不能解決的,如果有,那就添加兩層。

在前面幾篇部落格中,我們實現了LeNet、AlexNet、VGG、GoogLeNet等幾個景點的卷積神經網路演算法,這些演算法層數對比如下所示:

 

 

初步一看,可以發現網路的層數確實是在增加的,但如果這就認為網路層數越多,模型性能越高,那就太天真了(如果真這麼簡單,當時的這些學者直接將網路層數提升到成千上萬層,性能不比上表中一二十次高得多?還有這幾個演算法什麼事)。事實確實也是如此,經過反覆驗證,人們發現,當網路層數到達一定數量時,繼續添加網路層數,模型性能不但得不到提升,反而下降。

這是為什麼呢?

針對這一問題進行探索、優化,最終就有了本文的主角——殘差網路(ResNet)的誕生。

 

2 退化現象

 

ResNet是中國華人學者何凱明團隊在其論文《Deep Residual Learning for Image Recognition》中提出。在改論文中,作者先對網路深度與模型性能的關係進行探索,如下所示:

 

(註:圖來自論文)</cneter>
 

對比上面兩圖紅黃兩線,可以發現,網路層數多的(56層)訓練效果遠比網路層數少(20層)模型差。其實,在其他諸多實驗也發現,隨著網路層級的不斷增加,模型精度不斷得到提升,而當網路層級增加到一定的數目以後,訓練精度和測試精度迅速下降,這說明當網路變得很深以後,深度網路就變得更加難以訓練了。

為什麼會出現這種現象呢?

VGGNet和GoogLeNet等網路都表明有足夠的深度是模型表現良好的前提,但是在網路深度增加到一定程度時,更深的網路意味著更高的訓練誤差。 神經網路在反向傳播過程中通過鏈式法則不斷地傳播梯度,而當網路層數加深時,梯度在傳播過程中會逐漸消失(假如採用Sigmoid函數,對於幅度為1的訊號,每向後傳遞一層,梯度就衰減為原來的0.25,層數越多,衰減越厲害),導致無法對前面網路層的權重進行有效的調整,導致訓練和測試效果變差,這一現象稱為退化。

那麼,該如何解決退化問題,使得在加深網路層數的同時,又能解決梯度消失問題,從而提升模型精度呢?

 

3 殘差網路(ResNet)

 

通過前文分析我們知道了,網路模型的深度到達一定量時,模型的精度就不再隨著深度的提升而增加,更有可能會跟著降低。這裡,我們有一個設想,假設模型總共有100層,但是模型在第10層的時候精度就達到了巔峰,那麼,後續的90層都是多餘的,我們不需要它對輸出結果進行任何改變,這90層只需要保證第10層網路的輸出能夠原模原樣映射到最後一層即可——這就是本文的主角——殘差網路(ResNet)演算法靈感的來源。在論文中,作者將後續90層這種將輸入原樣輸出的映射層為恆等映射。

我們回到ResNet演算法來看看恆等映射具體是如何發揮作用的。如下圖所示,假設$x$為淺層網路的輸出,在原始的網路中,$x$會繼續前向傳播,經過至少一層網路後輸出為$F(x)$,在ResNet網路中,從淺層玩那個羅的輸出$x$到$F(x)$之間添加了一條捷徑,這條捷徑不會對$x$做任何改變,保留原樣輸出,所以,在圖中捷徑與原始網路的交匯處將獲得的輸出變為$H(x)=F(x)+x$此時,$F(x)=H(x)-x$,淺層網路後續的更深層次網路只需要對殘差$H(x)-x$進行擬合,這也是為什麼作者將這一模型稱為殘差網路的原因。如果淺層網路的輸出,也就是$x$的進度已經到達最優,那麼,$H(x)=x$將是最優的輸出,$F(x)$只需要對$0$進行擬合,這樣的擬合會比沒有殘差時的擬合簡單得多。另外,也因為有恆等映射的捷徑存在,因此在反向傳播過程中梯度的傳導也多了更簡便的路徑,僅僅經過一個relu就可以把梯度傳達給上一個模組。

 

 

這種殘差跳躍式的結構,打破了傳統的神經網路n-1層的輸出只能給n層作為輸入的慣例,使某一層的輸出可以直接跨過幾層作為後面某一層的輸入,其意義在於為疊加多層網路而使得整個學習模型的錯誤率不降反升的難題提供了新的方向。

 

當然,在卷積網路的殘差塊設計中,並不都是設計成恆等映射,這也是允許的,如下圖所示。

  • 實線連接部分表示通道數不變,如圖中的第二個矩形和第三個矩形,都是3x3x64的特徵圖,由於通道相同,所以採用計算方式為$H(x)=F(x)+x$;
  • 虛線連接部分表示通道發生變化,如上圖的第一個綠色矩形和第三個綠色矩形,分別是3x3x64和3x3x128的特徵圖,通道不同,採用的計算方式為$H(x)=F(x)+Wx$,其中$W$是卷積操作,用來調整$x$的通道數。
 

 

在論文中,作者進一步證明了深度殘差網路的確解決了退化問題,如下圖所示,左圖為平原網路(plain network)網路層次越深(34層)比網路層次淺的(18層)的誤差率更高;右圖為殘差網路ResNet的網路層次越深(34層)比網路層次淺的(18層)的誤差率更低。

 

 

至此,神經網路的層數可以超越之前的約束,達到幾十層、上百層甚至千層,為更高層次特徵提取和分類提供了可行性。

 

4 程式碼實現

In [1]:
import os
import tensorflow as tf
from tensorflow import keras 
from tensorflow.keras import layers, Sequential, datasets, optimizers
In [2]:
os.environ["TF_CPP_MIN_LOG_LEVEL"]='2'
tf.random.set_seed(2345)
In [3]:
# 使用cifar100數據集
(x_train, y_train), (x_test, y_test) = datasets.cifar100.load_data()
In [4]:
def preprocess(x, y):
    x = tf.cast(x, dtype=tf.float32) / 255.  # 將每個像素值映射到[0, 1]內
    y = tf.cast(y, dtype=tf.float32)
    return x, y
In [5]:
y_train = tf.squeeze(y_train, axis=1)
y_test = tf.squeeze(y_test, axis=1)
train_db = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_db = train_db.shuffle(1000).map(preprocess).batch(256)
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.shuffle(1000).map(preprocess).batch(256)
In [6]:
class BasicBlock(layers.Layer):
    def __init__(self,filter_num,stride=1):
        super(BasicBlock, self).__init__()
        self.conv1=layers.Conv2D(filter_num,(3,3),strides=stride,padding='same')
        self.bn1=layers.BatchNormalization()
        self.relu=layers.Activation('relu')

        self.conv2=layers.Conv2D(filter_num,(3,3),strides=1,padding='same')
        self.bn2 = layers.BatchNormalization()

        if stride!=1:
            self.downsample=Sequential()
            self.downsample.add(layers.Conv2D(filter_num,(1,1),strides=stride))
        else:
            self.downsample=lambda x:x
    def call(self,input,training=None):
        out=self.conv1(input)
        out=self.bn1(out)
        out=self.relu(out)

        out=self.conv2(out)
        out=self.bn2(out)

        identity=self.downsample(input)
        output=layers.add([out,identity])
        output=tf.nn.relu(output)
        return output
In [7]:
class ResNet(keras.Model):
    def __init__(self,layer_dims,num_classes=100):
        super(ResNet, self).__init__()
        # 預處理層
        self.stem=Sequential([
            layers.Conv2D(64,(3,3),strides=(1,1)),
            layers.BatchNormalization(),
            layers.Activation('relu'),
            layers.MaxPool2D(pool_size=(2,2),strides=(1,1),padding='same')
        ])
        # resblock
        self.layer1 = self.build_resblock(64,layer_dims[0])
        self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
        self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
        self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)

        # there are [b,512,h,w]
        # 自適應
        self.avgpool=layers.GlobalAveragePooling2D()
        self.fc=layers.Dense(num_classes)



    def call(self,input,training=None):
        x=self.stem(input)
        x=self.layer1(x)
        x=self.layer2(x)
        x=self.layer3(x)
        x=self.layer4(x)
        # [b,c]
        x=self.avgpool(x)
        x=self.fc(x)
        return x

    def build_resblock(self,filter_num,blocks, stride=1):
        res_blocks= Sequential()
        # may down sample
        res_blocks.add(BasicBlock(filter_num, stride))
        # just down sample one time
        for pre in range(1, blocks):
            res_blocks.add(BasicBlock(filter_num, stride=1))
        return res_blocks
In [8]:
def resnet18():
    return ResNet([2, 2, 2, 2])
In [13]:
def main():
    model=resnet18()
    model.build(input_shape=(None,32,32,3))
    model.summary()
    optimizer=optimizers.Adam(lr=1e-3)
    for epoch in range(50):
        for step,(x,y) in enumerate(train_db):
            with tf.GradientTape() as tape:
                logits=model(x)
                y = tf.cast(y, tf.int32)
                y_onehot=tf.one_hot(y,depth=100)
                loss=tf.losses.categorical_crossentropy(y_onehot,logits,from_logits=True)
                loss=tf.reduce_mean(loss)
            grads=tape.gradient(loss,model.trainable_variables)
            optimizer.apply_gradients(zip(grads,model.trainable_variables))
            if step%100==0:
                print(epoch,step,'loss',float(loss))
        total_num=0
        total_correct=0
        for x,y in test_db:
            logits=model(x)
            prob=tf.nn.softmax(logits,axis=1)
            pred=tf.argmax(prob,axis=1)
            pred=tf.cast(pred,dtype=tf.int32)
            correct=tf.cast(tf.equal(pred,y),dtype=tf.int32)
            correct=tf.reduce_sum(correct)
            total_num+=x.shape[0]
            total_correct+=int(correct)
        acc=total_correct/total_num
        print(epoch,'acc:',acc)
In [ ]:
main()
 
Model: "res_net_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
sequential_16 (Sequential)   multiple                  2048      
_________________________________________________________________
sequential_17 (Sequential)   multiple                  148736    
_________________________________________________________________
sequential_18 (Sequential)   multiple                  526976    
_________________________________________________________________
sequential_20 (Sequential)   multiple                  2102528   
_________________________________________________________________
sequential_22 (Sequential)   multiple                  8399360   
_________________________________________________________________
global_average_pooling2d_2 ( multiple                  0         
_________________________________________________________________
dense_2 (Dense)              multiple                  51300     
=================================================================
Total params: 11,230,948
Trainable params: 11,223,140
Non-trainable params: 7,808
_________________________________________________________________
0 0 loss 4.609687805175781