2022年4月6日 星期三

機器學習筆記 : 深度學習的 16 堂課 (十)

本篇繼續整理 "深度學習的16堂課" 這本書的讀後筆記, 本系列之前的文章參考 : 

  1. 卷積神經網路 CNN (Convolution Neural Network) 是含有一個以上卷積層的神經網路, 通常用於機器視覺領域. 整個 CNN 結構是由卷積層 + 池化層 + 密集層組成

  2. 使用密集神經網路辨識圖片有兩個問題 :
    (1). 須先將 2D 影像拉平為 1D 陣列, 因為密集層的輸入是 1D 形式, 但硬將二軸影像展平一軸會破壞像素與像素之間的關係 (人類之所以能識別眼前景物, 是因為大腦能將影像裡的各種形狀與特徵串聯起來). CNN 網路不需要將輸入展平, 因此能保持 2D 圖片中像素之間的空間關係
    (2). 用密集神經網路處理影像會使網路太複雜, 例如 MNIST 手寫資料集的 28*28 灰階圖片在輸入層就要用掉 784 個神經元, 如果尺寸更大, 或加上彩色 (RGB 三通道), 神經元與參數數量會飆升, 例如 200*200 全彩圖片, 第一個隱藏層若使用密集層每個神經元需要 200*200*3 + 1(偏值)=120001 個參數, 第一層若只用 64 個神經元就有 64*120001=7680064 超過 768 萬個參數. 當權重參數太多, 而訓練資料又不夠多時就會出現過度適配 (overfitting) 問題. CNN 的目的就是希望能降低神經網路的權重數量又能保持一定的學習能力

  3. CNN 的卷積層主要負責影像的特徵擷取 (feature extraction), 亦即從原始影像中萃取出足以辨識內容的特徵, 如此即可不須將整張圖片的特徵值 (即像素) 餵給密集神經網路做分類, 可以避免輸入特徵過量問題.  

  4. CNN 的核心-卷積核 (convolutional kernel) : 
    卷積層使用卷積核掃描圖片, 卷積核是一個權重方陣, 常用的尺寸是 3*3 或 5*5 (比這大的很少見), 卷積核是一個滑動窗口 (sliding window), 把它放在圖片上依序向右向下以固定步長 (stride) 滑動掃描圖片時與窗口下的重疊的圖片像素進行卷積運算而取得其特徵, 故卷積核又稱為濾鏡或過濾器 (filter). 濾鏡透過滑動掃描圖片可在任何局部區域檢查是否有其鎖定之特徵
    卷積核內的權重值就如同密集層的權重參數一樣要透過訓練來求得最佳的配置. 以 3*3 濾鏡來處理 MNIST 圖片就有 3*3*1 (通道) 個權重, 加上一個偏值共 10 個參數要訓練, 若是處理全彩圖片則共有 3*3*3 (通道) + 1 (偏值) = 28 個參數要訓練. 與密集層相比, 卷積層的設計大大減少了參數的數量, 也有效避免了過度適配問題
    卷積核在圖片中滑動時進行卷積運算的方式與也是與密集層一樣計算 wx + b 得到 z 後再經過激活函數算出 a, 這就是圖片在該區域之特徵值, 3*3 卷積核會將原本 9 個像素萃取成單一的特徵值, 當卷積核掃過整張圖片後每次滑動得到的特徵值會組成一個 2D 陣列, 稱為元使圖片的特徵圖 (feature map) 或激活圖 (activation map)
    事實上在做卷積運算之前還會添加填補 (padding) 動作, 即在原圖周圍添加 0 擴增尺寸 (例如 MNIST 的 28*28 圖片在上下左右都做 0 填補後變成 30*30), 目的是讓卷積運算後輸出的特徵圖尺寸與輸入圖片相同 (仍然得到 28*28). 卷積運算的結果保有與原影像同尺寸的 2D 結構, 因此保留了像素之間的空間關係 (不像密集網路因須展平為 1D 結構而被破壞).
    關於 CNN 的卷積運算細節參考 :
    使用 Keras 卷積神經網路 (CNN) 辨識手寫數字

  5. CNN 與密集神經網路的唯一差別是密集神經網路是為每一個輸入配置獨立的權重, 而 CNN 的濾鏡 (卷積核) 則是只用一組權重來應付所有輸入, 濾鏡中的權重值不會隨滑動窗口移動而改變, 因此 CNN 的卷積層權重數量就比密集層少很多

  6. 每一個卷積層不是只有一個濾鏡, 而是一層配置了多個濾鏡, 目的是希望每個濾鏡負責萃取不同的特徵, 例如有的濾鏡對垂直線敏銳, 有的則對水平線或顏色變化較靈敏, 當它們掃過同一張圖片時, 卷積運算就會對各自專精的特徵輸出較大的值. 一個卷積層的每個濾鏡都會輸出一個 2D 陣列的特徵圖 (稱為 slice), 把這些濾鏡產生的特徵圖疊起來就會多出一個軸, 其維度就是濾鏡的數量, 稱為特徵圖的深度 (depth). 因此卷積層輸出的特徵圖是個 3D 陣列 (寬*高*深度)

  7. 前面的卷積層萃取出來的簡單特徵圖 (線條與色彩) 會傳遞給後面的卷積層, 經過層層濾鏡的重組與詮釋就會萃取出複雜的特徵圖 (紋理與形狀), 層數越多萃取出來的特徵圖越抽象, 使得較後面的神經層具有是別影像的能力. 簡言之, 在 CNN 中, 越前面的卷積層萃取簡單特徵, 越後面的層萃取複雜特徵. Jason Yosinski 製作了一個四分鐘影片來說明卷積層的濾鏡如何識別影像中的特徵 :
    https://www.youtube.com/watch?v=AgkfIQ4IGaM


  8. 每個卷積層的濾鏡數量與密集層要設幾個神經元一樣都是超參數 (super parameter), 可依下列經驗來選定 :
    (1). 濾鏡數量越多越能識別複雜特徵, 但需要的計算量也越大. 
    (2). 若神經網路含有多個卷積層, 原則是越後面的卷積層所用的濾鏡要越多. 
    (3). 濾鏡越多計算量越大, 故濾鏡數量是能少就少. 若損失值不會因濾鏡減半而增加, 那就沒必要用這麼多濾鏡. 

  9. 卷積核的超參數有下列三個 :
    (1). 濾鏡的尺寸 (通常是 3*3 或 5*5)
    (2). 滑動窗口的步長 (stride, 通常是 1 或 2 個像素)
    (3). 是否要做填補 (padding)
    實務上超過 5*5 的濾鏡或超過 2 的步長很少見. 濾鏡設太大會納入太多特徵而難以有效學習; 但設太小 (例如 2*2) 又不容易鎖定特徵來優化學習, 故一般選用 3*3 較常見. 大的步長可以減少所需之計算量, 但可能會剛好跳過重要特徵所在之區域, 故步長不宜設太大. 
    步長與填補這兩個參數必須妥善搭配才能讓卷積運算順利進行 (例如因為輸出特徵圖因不整除變成浮點數而發生錯誤), 否則會造成輸出的特徵圖尺寸與原始圖片不同 (變小). 

  10. 特徵圖的尺寸計算公式 :
    (D-F+2P)/S + 1
    其中 D=輸入圖片邊長, F=濾鏡邊長, P=填補數, S=步長
    例如 MNIST 圖片 28*28, 若採用 3*3 濾鏡, 上下左右各填補一個像素 (填 0), 步長為 1 (每次滑動一個像素), 則輸出特徵圖的邊長為 (28-3+2*1)/1 + 1=28, 與輸入圖片尺寸相同, 都是 28*28. 若將步長改為 2 會得到 12.5 的邊長, 這會造成運算錯誤. 

  11. CNN 神經網路在卷積層後面還會搭配池化層 (pooling layer), 其功用是負責對卷積層輸出的特徵圖作 "重點挑選" 的動作, 這樣會使特徵圖的尺寸縮小但保留了深度 (也就是只將 3D 特徵圖的邊長縮減而已, 深度不變). 池化層的運作原理與卷積層類似, 它也有一個滑動窗口, 其尺寸是一個超參數, 稱為池化尺寸 (pooling size). 不過, 與卷積層不同的是, 池化層沒有權重與偏值參數. 池化窗口跟濾鏡一樣以固定步長向右向下滑動, 不同之處在於所作的運算是池化運算, 通常有下列三種 :
    (1). 最大池化 (max pooling)
    (2). 平均池化 (average pooling)
    (3). L2 範數池化 (L2-norm pooling)
    最大池化是挑池化窗口中的最大值, 平均池化是計算窗口內的平均值, 而 L2 範數池化則是計算窗口內數值的 L2 範數 (簡單說就是平方和再開根號). "
    關於 L2 範數參考 :
    理解L1,L2 範數在機器學習中應用
    How to implement L2-norm pooling in Keras?
    CNN 最常用的是最大池化. 池化層通常使用 2*2 的池化窗口, 步長通常採用 2, 這樣的配置會讓特徵圖尺寸寬高各縮小一半, 但深度不變. 以 MNIST 圖片經過 16 個濾鏡輸出的 28*28*16 的特徵圖為例, 經 2*2 窗口步長為 2 池化後輸出變成 14*14*16. 

  12. 以 tf.keras 建立仿 LeNet-5 神經網路 : 
    LeNet-5 是 Yoshua Bengio 與 Yann LeCun 等人提出的深度學習模型, 也是史上第一個卷積神經網路.  LeNet 具有兩個卷積層, 分別有 6 與 16 個濾鏡; 池化層亦有兩個. 以下於 Colab 上實作仿 LeNet 來辨識 MNIST 手寫數字, 但結構做了一些修改 : 兩個卷積層的濾鏡增加為 32 與 64 個, 以及只用一個池化層, 少用池化層是深度學習的趨勢, 另外還使用了 LeNet 那時還沒有的 ReLu 激活函數. 

    (1). 匯入模組 : 
    from tensorflow.keras.datasets import mnist
    from tensorflow.keras.models import Sequential
    from tensorflow.keras.layers import Dense, Dropout
    from tensorflow.keras.utils import to_categorical
    from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten
    (2). 載入資料集 :
    (X_train, y_train), (X_test, y_test)=mnist.load_data()
    (3). 資料集預處理 :
    X_train=X_train.reshape(6000028, 28, 1).astype('float32'
    X_test=X_test.reshape(1000028, 28, 1).astype('float32')  
    X_train /= 255                    
    X_test /= 255                    
    y_train=to_categorical(y_train, 10)          
    y_test=to_categorical(y_test, 10注意此處與感知器所用的密集層做法不同, 不需要把 2D 的輸入圖片展平為 1D, 而是維持 28*28 尺寸 (單一 channel), 其餘處理方式與密集層相同. 

    (4). 建構仿 LeNet 的 CNN 神經網路 : 
    model=Sequential()
    model.add(Conv2D(32, kernel_size=(33), activation='relu',
             input_shape=(28281))) # 第一個卷積層 (32 個濾鏡)
    model.add(Conv2D(64, kernel_size=(33), activation='relu',
             input_shape=(28281))) # 第二個卷積層 (64 個濾鏡)
    model.add(MaxPooling2D(pool_size=(22))) # 池化層 (stride=2)
    model.add(Dropout(0.25)) # 第一個丟棄層 (25%)
    model.add(Flatten()) # 展平為 1D 陣列
    model.add(Dense(128, activation='relu')) # 密集層 (128 個神經元)
    model.add(Dropout(0.5)) # 第二個丟棄層 (50%)
    model.add(Dense(10, activation='softmax')) # 輸出層 (10) 個神經元) 此處使用了兩個卷積層與一個池化層, 輸出經過一個丟棄層丟掉 25% 的特徵後將 2D 陣列展平, 然後餵給一個密集層, 再經過一個丟棄層丟掉 50% 特徵後送至輸出層. Conv2D() 是對 2D 陣列作卷積, 也有對 1D 作卷積的函數 Conv1D(), 亦有 3D 卷積函數 Conv3D(), 例如用在醫學影像. Conv2D() 沒有指定 stride 步長參數的話預設值為 1, 填補參數 padding 未指定的話預設值為 'valid' (不填補), 若要填補要設為 'same', 故此處第一個卷積層特徵圖邊長為 (28-3+0)/1+1=26, 即輸出之特徵圖尺寸為 26*26*32. 同理第二個卷積層特徵圖邊長為 (26-3+0)/1+1=24, 即輸出之特徵圖尺寸為 24*24*64.

    (5). 檢視神經網路模型之摘要 :
    model.summary()
    結果如下 : 



    可見第一個卷積層輸出的特徵圖尺寸是 (26, 26, 32), 共有 320 個參數, 其中權重數目有 288 個=32 個濾鏡 * 每個濾鏡 9 個參數 (3x3 單一通道), 偏值 32 個 (每個濾鏡一個), 288+32=320. 第二個卷積層參數共 18496, 其中權重有 18432 個=64 個濾鏡 * 每個濾鏡 9 個參數 (3x3 單色通道) * 深度 32 (前一層有 32 個濾鏡), 偏值 64 個 (每個濾鏡一個), 18432+64=18496. 
    池化層窗口 2x2 步長為 1, 會使特徵圖尺寸縮小為 1/4, 故其尺寸為 (12, 12, 64), 深度不變仍是. 展平層會將 3D 特徵圖展成 1D, 故有 12*12*64=9216 個神經元. 第一個密集層有 128 個神經元, 與前一層展平層的 9216 個神經元相連, 故密集層有 9216*128=1179776 個參數 (只要用到密集層, 參數就會暴增). 第二個密集層就是有 10 個神經元的輸出層, 共有參數 1290=128*10+10=1290. 整個 LeNet 參數總數 1199882=320+18496+1179776+1290, 就是將每一層參數數目加起來. 注意, 池化, 丟棄, 與展平這三層不需要參數

    (6). 編譯模型 :
    model.compile(loss='categorical_crossentropy', optimizer='adam',
                  metrics=['accuracy'])
    此處使用了 adam 優化器, 其餘與密集層的設定一樣. 

    (7). 訓練模型 : 
    model.fit(X_train, y_train, batch_size=128epochs=10, verbose=1,
              validation_data=(X_test, y_test))
    因 CNN 參數比密集層少很多, 因此訓練週期不用太多, 此處設為 10, 結果如下 :



    可見在第四輪準確率就達到 90%, 最終於第十輪來到 99.27%! 密集層 MLP 的 86% 根本無法看到它的車尾燈! 不過計算過程較久, 花了 25 分鐘 (未使用 GPU).
    以上原始碼參考 :
    https://github.com/tony1966/colab/blob/main/deep_learning_illustrated_ch10_LeNet.ipynb

  13. 以 tf.keras 建立仿 AlexNet 神經網路 :
    上面 LeNet 的 CNN 網路的核心是兩個卷積層與一個最大池化層, 這種組合稱為一個卷積池化區塊 (conv-pool block), 可以將更多這種區塊堆疊成更複雜的網路, 最後再接密集層與輸出層即可 (例如 2012 年的 ILSRV 電腦視覺辨識大賽冠軍 AlexNet 即是). 以下用 tf.keras 建立仿 AlexNet 的神經網路來辨識解析度為 224*224 的全彩花朵圖片, 使用來自 tflearn 套件的 oxflower17 資料集, 故要在 Colab 上安裝 tflearn 套件 : 

    (1). 安裝 tflearn :
    !pip install tflearn


    (2). 匯入模組 :
    from tensorflow.keras.models import Sequential  
    from tensorflow.keras.layers import Dense, Dropout, BatchNormalization   
    from tensorflow.keras.utils import to_categorical 
    from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten 
    import tflearn.datasets.oxflower17 as oxflower17
    與上面 LeNet 不同之處為新增匯入 BatchNormalization 與 oxflower17 資料集. 

    (3). 載入資料集 :
    X, Y=oxflower17.load_data(one_hot=True)


    (4). 建立仿 AlexNet 的 CNN 網路 :
    #第一個卷積區塊
    model=Sequential()
    model.add(Conv2D(96, kernel_size=(1111), strides=(44),
             activation='relu', input_shape=(2242243)))
    model.add(MaxPooling2D(pool_size=(33), strides=(22)))
    model.add(BatchNormalization())
    #第二個卷積區塊
    model.add(Conv2D(256, kernel_size=(55), activation='relu'))
    model.add(MaxPooling2D(pool_size=(33), strides=(22)))
    model.add(BatchNormalization())
    #第三個卷積區塊
    model.add(Conv2D(256, kernel_size=(33), activation='relu'))
    model.add(Conv2D(384, kernel_size=(33), activation='relu'))
    model.add(Conv2D(384, kernel_size=(33), activation='relu'))
    model.add(MaxPooling2D(pool_size=(33), strides=(22)))
    model.add(BatchNormalization())
    #密集層
    model.add(Flatten())       
    model.add(Dense(4096, activation='tanh'))   
    model.add(Dropout(0.5))   
    model.add(Dense(4096, activation='tanh')) 
    model.add(Dropout(0.5))
    #輸出層
    model.add(Dense(17, activation='softmax'))

    (5). 檢視模型摘要 : 
    model.summary()
    結果如下 :
     

    (6). 編譯模型 : 
    model.compile(loss='categorical_crossentropy', optimizer='adam',
                  metrics=['accuracy'])
    這與上面 LeNet 設定相同. 

    (7). 訓練模型 :
    model.fit(X, Y, batch_size=64, epochs=100, verbose=1
              validation_split=0.1, shuffle=True)

    此處 validation_split 參數用來將資料集的 90% 拆出來作為訓練用的測試集 (test set), 10% 作為驗證集 (validation set), 0.1 指的是驗證集的比例. 結果如下 : 


    (略)

    這個網路較複雜且要跑 100 輪, 所以花的時間較久, 跑了近兩小時 (未使用 GPU), 測試集準確度有到 90% 以上, 但驗證集卻步怎麼理想, 不到 70%, 似有過擬現象. 程式碼如下 :
    https://github.com/tony1966/colab/blob/main/deep_learning_illustrated_ch10_AlexNet.ipynb

  14. 以 tf.keras 建立仿 VGGNet 神經網路 :
    VGGNet 是 2014 年 ILSRV 電腦視覺辨識大賽冠軍, 它堆疊了更多層的卷積池化區塊, 但將卷積核變小, 全部使用 3x3 的濾鏡, 以下用 tf.keras 建立仿 VGGNet 的 CNN 網路, 同樣使用 tflearn 套件的 oxflower17 全彩花朵圖片資料集, 故須先安裝 tflearn :

    (1). 安裝 tflearn :
    !pip install tflearn
     
    (2). 匯入模組 : 
    from tensorflow.keras.models import Sequential  
    from tensorflow.keras.layers import Dense, Dropout, BatchNormalization   
    from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten 
    import tflearn.datasets.oxflower17 as oxflower17

    (3). 載入資料集 :
    X, Y=oxflower17.load_data(one_hot=True)

    (4). 建立仿 VGGNet 的 CNN 網路 :
    model = Sequential()
    #第一個卷積區塊
    model.add(Conv2D(643, activation='relu', input_shape=(2242243)))
    model.add(Conv2D(643, activation='relu'))
    model.add(MaxPooling2D(22))
    model.add(BatchNormalization())
    #第二個卷積區塊
    model.add(Conv2D(1283, activation='relu'))
    model.add(Conv2D(1283, activation='relu'))
    model.add(MaxPooling2D(22))
    model.add(BatchNormalization())
    #第三個卷積區塊
    model.add(Conv2D(2563, activation='relu'))
    model.add(Conv2D(2563, activation='relu'))
    model.add(Conv2D(2563, activation='relu'))
    model.add(MaxPooling2D(22))
    model.add(BatchNormalization())
    #第四個卷積區塊
    model.add(Conv2D(5123, activation='relu'))
    model.add(Conv2D(5123, activation='relu'))
    model.add(Conv2D(5123, activation='relu'))
    model.add(MaxPooling2D(22))
    model.add(BatchNormalization())
    #第五個卷積區塊
    model.add(Conv2D(5123, activation='relu'))
    model.add(Conv2D(5123, activation='relu'))
    model.add(Conv2D(5123, activation='relu'))
    model.add(MaxPooling2D(22))
    model.add(BatchNormalization())
    #密集層
    model.add(Flatten())
    model.add(Dense(4096, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(4096, activation='relu'))
    model.add(Dropout(0.5))
    #輸出層
    model.add(Dense(17, activation='softmax'))

    (5). 檢視模型摘要 : 
    model.summary()
    結果如下 :



    (6). 編譯模型 : 
    model.compile(loss='categorical_crossentropy', optimizer='adam',
                  metrics=['accuracy'])
    這與上面 LeNet 與 AlexNet 設定相同. 

    (7). 訓練模型 :
    model.fit(X, Y, batch_size=64, epochs=240, verbose=1
              validation_split=0.1, shuffle=True)

    這會跑很久很久 ..... 
    原始碼參考 :
    https://github.com/tony1966/colab/blob/main/deep_learning_illustrated_ch10_VGGNet.ipynb

  15. 本章後面 10.5 節還介紹殘差神經網路 (redidual network), 即使用殘差模組 (residual module 或 residual block) 所組成之網路. 所謂殘差模組就是在卷積區塊裡加上跳接 (skip connection) 的設計, 將輸入直接連到輸出的 "抄捷徑" 方式, 可以減少神經網路的損失. 2015 年微軟研究院團隊便以史上第一個殘差神經網路 ResNet 奪得 2015 年 ILSVRC 視覺辨識大賽. ResNet 不僅在影像分類奪冠, 在物件偵測與圖像分割項目也拿下冠軍. 
  16. 10.6 節介紹物件偵測, 圖像分割, 與遷移學習 (transfer learning). 物件偵測技術從 R-CNN 開始演化為 Fast R-CNN, Faster R-CNN, 與 YOLO 等演算法. YOLO (You Only Look Once) 於 2015 年提出, 它使用預訓練過的 CNN 來提取特徵, 接著將影像劃分為網格狀, 然後逐格預測內含物件的邊框位置與類別機率, 若高於某臨界值就採用其預測結果來定位影像中的物件. 
  17. 遷移學習是將已充分訓練好的模型移植到別的應用上直接使用, 不須重新訓練模型). 遷移學習有程式碼實作 VGGNet 的遷移學習, 直接載入 tf.kearas.applictions.vgg19 的 VGG19 模型來使用, 並將所有神經層的 trainable 屬性設為 False 來凍結 VGGNet 的神經層參數, 避免載入的模型參數倍改變. 篇幅太多略過 .....

沒有留言 :