1. 程式人生 > >深度學習與計算機視覺(PB-05)-網路微調

深度學習與計算機視覺(PB-05)-網路微調

第3節中,我們學習瞭如何將預訓練好的卷積神經網路作為特徵提取器。通過載入預訓練好的模型,可以提取指定層的輸出作為特徵向量,並將特徵向量儲存到磁碟。有了特徵向量之後,我們就可以在特徵向量上訓練傳統的機器學習演算法(比如在第3節中我們使用的邏輯迴歸模型)。當然對於特徵向量,我們也可以使用手工提取特徵方法,比如SIFT[15],HOG[14],LBPs[16]等。

一般來說,在計算機視覺任務中,深度學習相關的遷移學習主要有兩種型別:

  • 1.將網路當作特徵提取器。
  • 2.刪除現有網路的全連線層,新增新的FC層,並微調這些權重識別新的類別資料。

第3節中,介紹了第一種方法。下面我們介紹另一種型別的遷移學習,如果有足夠的資料,它實際上可以超越特徵提取方法,這種方法稱為微調,即我們利用新的資料對網路權重進行微調。首先,我們對預訓練好的卷積神經網路(如VGG,ResNet或Inception等,一般是在大型資料集上訓練得到的)的最後一組全連線層做截斷處理(即刪除網路中的top層),然後,我們用一組新的全連線層與截斷的已訓練好的模型進行拼接,組成一個新的完整模型,並隨機初始化權重[注意

:這裡的初始化權重只初始化新增的全連線層權重],全連線層即Top部以下的所有層權重都被凍結,一般而言,訓練時候是不更新,即後向傳播是不在訓練好的模型之間進行傳播的。

在微調過程中,一般使用非常小的學習率來訓練網路,這樣新的全連線層就可以開始從已訓練好的網路中學習新的模式。當然我們也可以對其他層的權重進行訓練,這可能需要根據具體的資料來設定。利用微調技術,我們就不用重新開始訓練模型,即可以節省大量的時間和精力,又可以獲得更高的準確度。

在本章的其餘部分,我們將更詳細地討論微調方法,以及如何對網路進行處理。最後,我們將應用於Flowers-17資料集中。

遷移學習和微調

微調方法是遷移學習的另一種型別。對於新資料集,我們首先載入預先訓練好的模型,這時載入的模型一般並不適合於新資料,但是,該模型又具有良好的特徵學習能力,想要保持該模型的強大區分能力,那怎麼做呢?我們首先刪除已有模型的全連線層,凍結剩餘層的權重(包括在訓練過程),然後構建一組新的全連線層,與已有的截斷模型進行拼接,並對新的全連線層的權重進行初始化,最後,給定較小的學習率,將‘新’模型在新的資料上進行訓練。通常,使用的預訓練好的網路都是當前比較好的網路,如VGG、ResNet和Inception等,它們已經在大型資料集ImageNet上訓練過。

正如我們在第3節討論,這些預訓練好的網路結構包含大量具有判別能力的過濾器,這些過濾器可用於新的資料集[雖然過濾器無法對新資料進行預測,但是可以提取有用的特徵]。在這節,我們將對預訓練好的網路結構進行修改,這樣我們只需要訓練修改的的部分網路引數,而不是重新開始訓練整個網路。

圖5.1 左:原始的VGG16網路結構, 右:網路結構的調整

我們以圖5.1來理解微調是如何工作的。圖5.1顯示的是VGG16網路。從左圖可以看到,最後一組(即“頭”部分)主要是由全連線層和sofrmax分類器組成。使用微調方法時,我們對已有模型的網路結構進行修改,主要是刪除頭部,就像在第3節提到的提取特徵一樣操作。然而不同的是,微調過程中,我們實際上構建了一個新的由全連線層和softmax分類器組成的‘頭’部,並與原始結構進行拼接,如圖5.1右所示。

在大多數情況下,新增的頭部全連線層的引數會比原來的引數更少[當然實際上取決於你的特定資料集]。在大型資料上訓練得到的模型往往具有強大的區分能力,而新增的全連線層是全新的且完全隨機的。如果我們讓這些隨機值的梯度反向傳播到整個網路,那麼就可能破壞這些強大的特徵。為了避開這個問題,我們僅訓練頭部全連線層引數,而“凍結”網路中其他層權重,如圖5.2(左)所示。

圖5.2 左:凍結所有conv層,微調全連線層, 右:全網路進行微調(調整全連線層之後)

微調訓練過程中,前向傳播是在整個網路之間進行傳播,但是,後向傳播只在新的全連線層之間進行,這樣全連線層就可以從具有高區分能力的Conv層中學習特徵。一般情況下,整個訓練過程中,都不對全連線層之外的層進行訓練。因為新的全連線層可能已經獲得了相當好的準確度。但是,對於某些資料集,微調部分原始層引數可能會得到更好的效果,比如圖5.2右。

當前面的FC層訓練完之後,我們可以考慮微調部分原始網路的層權重,讓原始網路也對新的資料進行學習。為了不讓原始Conv層的過濾器發生明顯的變化,設定非常小的學習速率,繼續訓練,直到獲得足夠好的準確度。

可見,我們可以利用預訓練好的CNNs模型對自定義資料集經過微調得到新的影象分類器,在大多數情況下可以比提取特徵方式獲得更高的準確度。當然,微調也存在缺點,由於我們對網路結構進行了一定修改,如何確定需要修改的部分網路結構以及重新訓練模型都是需要時間投入,從實踐中得到一個相對好的“新”模型,且新的FC層引數的選擇對網路精度存在重要影響。

其次,對於小資料集,從網路頂部就開始訓練分類器可能不是最好的選擇,這包含更多的資料集特定特徵。另外,從網路前部的啟用函式開始訓練分類器可能更好一點。

索引

在對網路結構進行修改之前,我們需要了解網路的結構,即每一層對應的名字和位置。因為我們將根據網路層對應的索引對預訓練好的模型某些層的權重進行“凍結”或“解凍”操作。

接下來,我們列印VGG16中的層名和索引。新建一個名為inspect_model.py檔案,寫入以下程式碼:

#encoding:utf-8
# 載入所需要模組
from keras.applications import VGG16
import argparse
# 命令列引數設定
ap = argparse.ArgumentParser()
ap.add_argument('-i','--include_top',type = int,default=1,help='whether or not to include top of CNN')
args = vars(ap.parse_args())
# 載入VGG16模型
print('[INFO] loading network...')
model = VGG16(weights='imagenet',include_top=args['include_top']>0)
print("[INFO] showing layers...")
# 遍歷VGG16所有層結構
for (i,layer) in enumerate(model.layers):
    print("[INFO] {}\t{}".format(i,layer.__class__.__name__))

對於網路中的每一層,輸出相應的索引i。根據這些資訊,我們就會知道FC層從哪裡開始。

執行下面命令,將在輸出介面顯示VGG16的網路結構:

$ python inspect_model.py

結果如下:

[INFO] showing layers...
[INFO] 0	InputLayer
[INFO] 1	Conv2D
[INFO] 2	Conv2D
[INFO] 3	MaxPooling2D
[INFO] 4	Conv2D
[INFO] 5	Conv2D
[INFO] 6	MaxPooling2D
[INFO] 7	Conv2D
[INFO] 8	Conv2D
[INFO] 9	Conv2D
[INFO] 10	MaxPooling2D
[INFO] 11	Conv2D
[INFO] 12	Conv2D
[INFO] 13	Conv2D
[INFO] 14	MaxPooling2D
[INFO] 15	Conv2D
[INFO] 16	Conv2D
[INFO] 17	Conv2D
[INFO] 18	MaxPooling2D
[INFO] 19	Flatten
[INFO] 20	Dense
[INFO] 21	Dense
[INFO] 22	Dense

從結果中可以看到,20-22層是全連線層。接下來,將–include_top設定為-1,即不返回FC層:

$ python inspact_model.py --include_top -1

得到的結果如下:

[INFO] showing layers...
[INFO] 0	InputLayer
[INFO] 1	Conv2D
[INFO] 2	Conv2D
[INFO] 3	MaxPooling2D
[INFO] 4	Conv2D
[INFO] 5	Conv2D
[INFO] 6	MaxPooling2D
[INFO] 7	Conv2D
[INFO] 8	Conv2D
[INFO] 9	Conv2D
[INFO] 10	MaxPooling2D
[INFO] 11	Conv2D
[INFO] 12	Conv2D
[INFO] 13	Conv2D
[INFO] 14	MaxPooling2D
[INFO] 15	Conv2D
[INFO] 16	Conv2D
[INFO] 17	Conv2D
[INFO] 18	MaxPooling2D

可以看到,網路最後一層為Pool層。我們後面將使用上面的模型結構。

實驗

在替換預訓練好的模型"頭"部分之前,我們先定義一個新的‘頭’。建立一個名為fcheadnet.py檔案,放入pyimagesearch專案中的nn.conv子模組中,結構如下所示:

--- pyimagesearch
|    |--- __init__.py
|    |--- callbacks
|    |--- io
|    |--- nn
|        |--- __init__.py
|        |--- conv
|            |--- __init__.py
|            |--- lenet.py
|            |--- minivggnet.py
|            |--- fcheadnet.py
|            |--- shallownet.py
|    |--- preprocessing
|    --- utils

並寫入以下程式碼:

#encoding:utf-8
# 載入所需要的模組
from keras.layers.core import Dropout
from keras.layers.core import Flatten
from keras.layers.core import Dense

下面,定義FCHeadNet 類:

class FCHeadNet:
    @staticmethod
    def build(baseModel,classes,D):
        # 初始化top部分
        headModel = baseModel.output
        headModel = Flatten(name='flatten')(headModel)
        headModel = Dense(D,activation='relu')(headModel)
        headModel = Dropout(0.5)(headModel)
        # 增加一個softmaxc層
        headModel = Dense(classes,activation='softmax')(headModel)
        return headModel

其中:

  • baseModel:網路的主體,比如上面的VGG16(只到18層)
  • classes:資料集中的類別個數,比如Flowers-17的類別個數為17
  • D:全連線層的節點數

新構建的網路結構如下:

INPUT => FC => RELU => DO => FC => SOFTMAX

新的全連線層比原始的VGG16的全連線層相對要簡單點,原始的VGG16包含兩組4096個節點的FC層。大多數我們進行微調操作時,並不是要複製原始網路的"頭"結構,而是要簡化它,以便更容易進行微調——頭中的引數越少,我們就越有可能正確地將網路調優到適合新的分類任務。

訓練

新建一個名為finetune_flowers17.py的檔案,並寫入以下程式碼:

#encoding:utf-8
# 載入所需要模組
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
# 資料預處理
from pyimagesearch.preprocessing import ImageToArrayPreprocessor as ITAP
from pyimagesearch.preprocessing import AspectAwarePreprocessor as AAP
from pyimagesearch.datasets import SimpleDatasetLoader as SDL
from pyimagesearch.nn.conv import fcheadnet as FCN # 新的全連線層
from keras.preprocessing.image   import ImageDataGenerator # 資料增強
from keras.optimizers import RMSprop
from keras.layers import Input
from keras.models import Model
from keras .applications import VGG16
from keras.optimizers import SGD
from keras.models import Model
from imutils import paths
import numpy as np
import argparse
import os

定義命令列引數:

# 解析命令列引數
ap = argparse.ArgumentParser()
ap.add_argument("-d","--dataset",required=True,help='path to input dataset')
ap.add_argument('-m','--model',required=True,help='path to output model')
args = vars(ap.parse_args())

其中;

  • –dataset:輸入資料的目錄路徑
  • –model:模型的儲存路徑

同樣,我們對train資料進行資料增強操作:

# 資料增強
aug = ImageDataGenerator(rotation_range=30,width_shift_range=0.1,height_shift_range=0.1,shear_range=0.2,zoom_range=0.2,horizontal_flip=True,fill_mode='nearest')

正如在第2節中提到的,大多數情況我們都應該應用資料增強,因為資料增強既可以提高模型的準確度又可以避免過擬合,而且當我們沒有足夠的資料從頭開始訓練一個CNN模型時,我們更應該使用資料增強。

下面,我們從磁碟中載入資料集,並對資料進行處理:

# 從磁碟中載入圖片,並提取標籤
print("[INFO] loading images...")
imagePaths = list(paths.list_images(args['dataset']))
classNames = [pt.split(os.path.sep)[-2] for pt in imagePaths]
classNames = [str(x) for x in np.unique(classNames)]

需要注意的下,資料結構目錄服從下面格式:

dataset_name/{class_name}/example.jpg

這樣的好處就是可以方便地提取資料的類別資訊。

# 初始化影象預處理
aap = AAP.AspectAwarePreprocesser(224,224)
iap= ITAP.ImageToArrayPreprocess()
# 載入影象資料,並進行影象資料預處理
sdl = SDL.SimpleDatasetLoader(preprocessors=[aap,iap])
(data,labels)  = sdl.load(imagePaths,verbose=500)
data = data.astype("float") / 255.0

資料劃分以及標籤編碼處理:

# 資料劃分
(trainX,testX,trainY,testY) = train_test_split(data,labels,test_size=0.25,random_state=42)
# 標籤進行編碼化處理
trainY = LabelBinarizer().fit_transform(trainY)
testY = LabelBinarizer().fit_transform(testY)

接下來,我們開始構建“新”的模型。

# 載入VGG16網路,不返回原始模型的全連線層
baseModel = VGG16(weights='imagenet',include_top=False,input_tensor=Input(shape = (224,224,3)))
# 初始化新的全連線層
headModel = FCN.FCHeadNet.build(baseModel,len(classNames),256)
# 拼接模型
model = Model(inputs=baseModel.input,outputs = headModel)

首先,我們載入了預訓練好的VGG16模型,不返回全連線層[即刪除‘頭’部分],然後,載入自定義的全連線層,最後,將兩部分進行拼接,組成一個“新”的模型。

前面提到過,在訓練之前,我們需要“凍結”已有模型的權重,這樣它們在反向傳播階段就不會被更新。keras中很容易實現‘凍結’操作,我們通過對baseModel中每個層設定.trainable引數為False來實現:

# 遍歷所有層,並凍結對應層的權重
for layer in baseModel.layers:
    layer.trainable = False

接著,我們開始初始化全連線層的權重以及進行訓練:

print("[INFO] compiling model...")
opt = RMSprop(lr=0.001)
model.compile(loss="categorical_crossentropy", optimizer=opt,metrics=["accuracy"])
# 由於我們只訓練新增的全連線層,因此,我們進行少量迭代
print("[INFO] training head...")
model.fit_generator(aug.flow(trainX,trainY,batch_size = 32),
                             validation_data = (testX,testY),epochs=25,
                             steps_per_epoch = len(trainX) //32,verbose = 1)

這裡使用的是RMSprop優化器,我們將在第7節中詳細討論這個演算法。前面提到,一般微調過程使用一個很小的學習率,因此,設定lr=0.001。上面部分,我們主要的目的是訓練“頭”部分權重,而不改變網路主體的權重,因此,epochs設定為25,當然可以根據你的具體資料集進行調整,一般設定在10-30epochs左右。

檢視模型的效能結果:

# 評估模型
print("[INFO] evaluating after initialization...")
predictions = model.predict(testX,batch_size=32)
print(classification_report(testY.argmax(axis =1),
                            predictions.argmax(axis =1),target_names=classNames))

完成了全連線層的訓練之後,接下來,我們微調原有模型的部分權重,同樣,通過對baseModel中每個層設定.trainable引數為True來實現

# 對整個網路進行微調
for layer in baseModel.layers[15:]:
    layer.trainable = True

對於很深的網路比如VGG,包含許多引數,建議只微調部分層的權重,當然如果模型的準確度有提高且沒有發生過擬合問題,那麼可以微調更多層的權重。

# 從新編譯模型
print("[INFO] re-compiling model ...")
opt = SGD(lr=0.001)
# 使用很小的學習率進行微調
model.compile(loss = 'categoricla_crossentropy',optimizer = opt,
              metrics=['accuracy'])
# 對整個模型進行微調
print("[INFO] fine-tuning model...")
model.fit_generator(aug.flow(trainX,trainY,batch_size=32),
                    validation_data = (testX,testY),epochs = 100,
                    steps_per_epoch = len(trainX) // 32,verbose = 1)

儲存模型權重:

# 評估微調後的模型結果
print("[INFO] evaluating after fine-tuning...")
predictions = model.predict(testX,batch_size=32)
print(classification_report(testY.argmax(axis =1),
        predictions.argmax(axis =1),target_names=classNames))
# 將模型儲存到磁碟
print("[INFO] serializing model...")
model.save(args['model'])

接下來,在Flower-17資料集進行實驗,

$ python finetune_flowers17.py --dataset yourPath/datasets/flowers17/images --model flowers17.model

將得到以下結果;

[INFO] loading images...
[INFO] processed 500/1360
[INFO] processed 1000/1360
[INFO] compiling model...
[INFO] training head...
Epoch 1/25
10s - loss: 4.8957 - acc: 0.1510 - val_loss: 2.1650 - val_acc: 0.3618
...
Epoch 10/25
10s - loss: 1.1318 - acc: 0.6245 - val_loss: 0.5132 - val_acc: 0.8441
...
Epoch 23/25
10s - loss: 0.7203 - acc: 0.7598 - val_loss: 0.4679 - val_acc: 0.8529
Epoch 24/25
10s - loss: 0.7355 - acc: 0.7520 - val_loss: 0.4268 - val_acc: 0.8853
Epoch 25/25
10s - loss: 0.7504 - acc: 0.7598 - val_loss: 0.3981 - val_acc: 0.8971
[INFO] evaluating after initialization...
precision recall f1-score support
  bluebell 0.75 1.00 0.86 18
 buttercup 0.94 0.85 0.89 20
 coltsfoot 0.94 0.85 0.89 20
   cowslip 0.70 0.78 0.74 18
    crocus 1.00 0.80 0.89 20
  daffodil 0.87 0.96 0.91 27
     daisy 0.90 0.95 0.93 20
 dandelion 0.96 0.96 0.96 23
fritillary 1.00 0.86 0.93 22
      iris 1.00 0.95 0.98 21
lilyvalley 0.93 0.93 0.93 15
     pansy 0.83 1.00 0.90 19
  snowdrop 0.88 0.96 0.92 23
 sunflower 1.00 0.96 0.98 23
 tigerlily 0.90 1.00 0.95 19
     tulip 0.86 0.38 0.52 16
windflower 0.83 0.94 0.88 16
avg /total 0.90 0.90 0.89 340

在第一個epoch,驗證集的準確度很低(約36%),主要一開始新的全連線層的權重是隨機初始化的。隨著優化不斷進行,準確率迅速上升——到第10個epoch時,我們的分類準確率超過了80%,到第25個epoch結束時,準確率幾乎達到了90%。

僅僅微調全連層,我們的新模型達到了約90%的準確度。接下來,我們檢視微調部分原始模型的權重的結果,如下所示:

...
[INFO] re-compiling model...
[INFO] fine-tuning model...
Epoch 1/100
12s - loss: 0.5127 - acc: 0.8147 - val_loss:0.3640 - val_acc: 0.8912
...
Epoch 99/100
12s - loss: 0.1746 - acc: 0.9373 - val_loss:0.2286 - val_acc: 0.9265
Epoch 100/100
12s - loss: 0.1845 - acc: 0.9402 - val_loss:0.2019 - val_acc: 0.9412
[INFO] evaluating after fine-tuning...
             precision    recall  f1-score   support
          0       0.94      0.79      0.86        19
          1       0.93      0.87      0.90        15
         10       1.00      1.00      1.00        20
         11       0.95      0.83      0.88        23
         12       0.95      0.95      0.95        19
         13       0.82      0.86      0.84        21
         14       0.95      0.95      0.95        20
         15       1.00      0.93      0.96        27
         16       0.89      1.00      0.94        16
          2       0.90      0.95      0.93        20
          3       0.90      0.90      0.90        20
          4       1.00      0.95      0.98        22
          5       1.00      1.00      1.00        16
          6       0.90      1.00      0.95        18
          7       0.77      0.94      0.85        18
          8       0.92      0.96      0.94        23
          9       1.00      0.96      0.98        23
avg / total       0.93      0.93      0.93       340

從結果中可以看到,準確度提高到了93%,比我們之前提取特徵的方法更高。

從實驗的結果來看,微調方法可以比特徵提取方法獲得更高的準確度。它能夠將原始的網路權重對新的資料集進行學習——這是特徵提取所不允許的。因此,當給定足夠的訓練資料時,儘量使用微調,因為你可能會比僅使用簡單的特徵提取獲得更高的分類準確度。

總結

在本章中,我們討論了遷移學習的另一種型別——微調。載入訓練好的模型(一般實在大型資料集上訓練得到的),擷取全連線層以下的層作為“新”模型的主體,並在主體上連線新的全連線層。訓練過程中,我們凍結主體層的權重,只訓練新的全連線層引數。當然我們也可以微調主體的部分層權重。利用微調技術,因為我們不必從頭訓練整個網路。相反,我們可以利用已經存在的網路架構,例如在ImageNet資料集上訓練的最優秀的模型。

本章完整程式碼地址:github