1. 程式人生 > >深度學習與計算機視覺(PB-06)-模型整合

深度學習與計算機視覺(PB-06)-模型整合

在本章中,我們將探討整合方法的概念——多個分類器合併成一個大型元分類器的過程。將多個模型的平均結果最為最終結果,可以比隨機的單一模型獲得更高的效能(比如準確度)。事實上,幾乎你所看到的在ImageNet資料挑戰賽上獲得最佳的結果都是通過整合多個卷積神經網路結果得到的。

首先,我們將討論下Jensen不等式,這是整合方法的關鍵。然後,我們相互獨立地訓練多個CNN模型,並對每個CNN模型進行評估,最後,將多個訓練好的cnn模型融合成一個元分類器,並評估元分類器的效能。

整合方法

整合方法通常指的是訓練“大量”的模型(其中‘量’的確切值取決於分類任務),然後通過投票或平均方法將多個模型的輸出結果合併成一個結果,以提高分類的準確性。事實上,整合方法並不是專門針對深度學習和卷積神經網路的。一直以來我們使用了很多整合方法,比如AdaBoost[18]和Random forest[19]等技術都是整合方法的典型例子。

比如在隨機森林中,我們獨立地訓練多棵決策樹[20,21],並由這些決策樹組成一個大森林,最後利用組合得到的森林進行預測。如圖6.1所示,隨機森林由多棵決策樹組成,每棵決策樹都會返回一個自身的預測結果。這些結果將組成一個元分類器結果表,通過投票方法從結果表中返回出現最多的標籤作為最後的預測結果。

圖6.1 隨機森林模型結構圖

同樣的概念也適用於深度學習和卷積神經網路。我們可以訓練多個網路,且每個網路都會返回每一個標籤的概率(如圖6.2左)。然後,計算多個概率值的平均值,該平均值就是最後的預測結果。

為什麼對多個模型進行平均預測是有效的?我們首先了解下Jensen不等式,然後通過實踐表明整合方法的有效性。

圖6.2 左:整合網路結構, 右:糖果罐

Jensen 不等式

一般來說,整合方法是通過融合一個有限的模型集合的結果獲得比集合中單模型更好的準確度的過程。Dietterich[23]的開創性研究詳細闡述了為什麼整合方法通常比單個模型會獲得更高的準確度。

Dietterich研究的成功主要取決於Jensen不等式,即機器學習文獻中的“多樣性”或“奇異值分解”。Jensen不等式的定義表明,凸函式(average)整合的誤差小於或等於各模型的平均誤差,當然這可能是由於其中一個單模型的誤差低於所有模型的平均誤差,但是,總的來說,可以確信的是平均所有模型的結果不比隨機單模型差。

通過一個簡單例子來解釋Jensen不等式和模型平均的概念,比如圖6.2右,讓你猜猜罐子裡面有多少糖果。

能猜出有多少糖果嗎?100個?200個?500個?在沒有參考的情況下,只能憑藉自己的感覺。

但是,這個遊戲有一個小技巧——基於Jensen不等式。如果你問我罐子裡有多少糖果,我就會去問你和每個購買了書籍的人,問他們每個人認為糖果的數量是多少。然後我把所有的猜測都加起來,取平均值作為我的最終預測。

也許,其中有幾個人的結果很接近真實的數量,也有幾個人的結果偏離真實的數量,但是,在未知的情況下,我們無法根據任何的準則來評估哪個人的結果較接近真實的數量。所以我取每個人的結果平均值,至少平均而言,我的結果不比你隨機選擇一個結果差。本質上,這就是Jensen不等式。

隨機猜測糖果數量遊戲和深度學習模型的區別在於,假設CNNs訓練很好[具有一定的準確度],這時預測結果就有一定的可靠性,而不是隨機猜測。因此,取多個cnn模型結果的平均值,往往分類準確度會得到一定的提高。所以在深度學習一些公開比賽中,排名靠前的結果基本都是訓練了多種模型,然後對多個模型進行整合得到最後的結果,準確率往往會提高。

CNN整合

在構建一個CNNs整合模型之前,我們需要獨立訓練多個CNN模型。在之前章節中,我們已經看到了許多訓練單個CNN的例子——但是我們如何訓練多個網路呢?總的來說,我們有兩個選擇:

  • 1.執行多次訓練單個網路的指令碼,將每次輸出的模型權重進行儲存。

  • 2.建立一個單獨的Python指令碼,該指令碼使用for迴圈來訓練N個網路,並在每次迭代結束時輸出序列化的模型。

這兩種方法都是完全可以接受的。第一種方法比較簡單,多次執行命令就可以生成多個cnn模型。下面我們嘗試第二個方法,新建一個檔案,命名為train_models.py,寫入以下程式碼:

#encoding:utf-8
#設定影象的背景
import matplotlib
matplotlib.use('ggplot')

# 載入模組
from sklearn.preprocessing import LabelBinarizer
from sklearn.metrics import classification_report
from pyimagesearch.nn.conv import MiniVGGNet as MVN
from keras.preprocessing.image import ImageDataGenerator
from keras.optimizers import SGD
from keras.datasets import cifar10
import matplotlib.pyplot as plt
import numpy as np
import argparse
import os

說明:從載入的模組中可以看到

  • 1.主要訓練多個MiniVGGNet模型進行融合
  • 2.對CIFAR-10資料進行了資料增強處理
  • 3.使用隨機梯度下降法進行優化

定義命令列引數:

# 解析命令列引數
ap = argparse.ArgumentParser()
ap.add_argument('-o','--output',required=True,help='path to output directory')
ap.add_argument('-m','--models',required=True,help='path to output models directory')
ap.add_argument('-n','--num_models',type = int,default=5,help='# of models to train')
args = vars(ap.parse_args())

其中:

  • –output: 輸出結果儲存目錄。
  • –models: 儲存模型權重目錄
  • –num_models: 訓練模型個數,預設值為5

一般傳統的整合模型都是有多個[幾十或者上百]個基礎模型組成,比如隨機森林由超過30棵決策樹組成(大部分情況下都是超過100棵)。而CNNs整合主要由幾個卷積神經網路[一般是5-10個]組成,因為訓練CNN模型比較耗時且計算昂貴。

在這節實驗中,我們主要使用的CIFAR-10資料集進行實驗,首先,從磁碟中載入資料到記憶體中,並對記憶體中的資料進行預處理,如下程式碼:

# 載入資料集,並劃分為train和test資料
# 影象資料預處理:歸一化
((trainX,trainY),(testX,testY)) = cifar10.load_data()
trainX  = trainX.astype('float') / 255.0
testX = testX.astype('float') / 255.0

# 標籤進行編碼化處理

lb = LabelBinarizer()
trainY = lb.fit_transform(trainY)
testY = lb.fit_transform(testY)

# 初始化標籤名陳
labelNames = ["airplane", "automobile", "bird", "cat", "deer",
"dog", "frog", "horse", "ship", "truck"]

進行資料增強處理:

# 初始化資料增強模組
aug = ImageDataGenerator(rotation_range=10,width_shift_range=0.1,
                         height_shift_range=0.1,horizontal_flip=True,
                         fill_mode='nearest')

主要對初始影象進行隨機旋轉,平移等變換操作,下面,針對資料集單獨訓練多個MiniVGGNet模型:

# 遍歷模型訓練個數
for i in np.arange(0,args['num_models']):
    # 初始化優化器和模型
    print("[INFO] training model {}/{}".format(i+1,args['num_models']))
    opt = SGD(lr = 0.01,decay=0.01/ 40,momentum=0.9,
              nesterov=True)
    model = MVN.MiniVGGNet.build(width=32,height=32,depth=3,
                                 classes = 10)
    model.compile(loss = 'categorical_crossentropy',optimizer=opt,metrics = ['accuracy'])

其中,在優化器SGD中,學習率lr= 0.01,動量g = 0.9,並使用Nesterov進行加速——Nesterov是Momentum的變種,與Momentum唯一區別就是,計算梯度的不同,Nesterov先用當前的速度v更新一遍引數,在用更新的臨時引數計算梯度,具體的演算法可以參考如下圖所示:

Nesterov

下面,我們開始訓練模型,並將模型權重儲存到磁碟中。

    # 訓練網路
    H = model.fit_generator(aug.flow(trainX,trainY,batch_size=64),
                            validation_data=(testX,testY),epochs=40,
                            steps_per_epoch=len(trainX) // 64,verbose = 1)
    # 將模型儲存到磁碟中
    p = [args['models'],"model_{}.model".format(i)]
    model.save(os.path.sep.join(p))

在網路完成訓練之後,儲存模型的評估結果:

    # 評估模型
    predictions = model.predict(testX,batch_size=64)
    report = classification_report(testY.argmax(axis =1),
                                   predictions.argmax(axis =1),target_names=labelNames)
    # 將模型結果儲存到檔案中
    p = [args['output'],'model_{}.text'.format(i)]
    with open(os.path.sep.join(p),'w') as fw:
        fw.write(report)

繪製模型的損失和精度,檢視單個模型的效能:

    # loss函式視覺化
    p = [args['output'],'model_{}.png'.format(i)]
    plt.style.use('ggplot')
    plt.figure()
    plt.plot(np.arange(0,40),H.history['loss'],
             label = 'train_loss')
    plt.plot(np.arange(0,40),H.history['val_loss'],
             label = 'val_loss')
    plt.plot(np.arange(0,40),H.history['acc'],
             label = 'train-acc')
    plt.plot(np.arange(0,40),H.history['val_acc'],
             label = 'val-acc')
    plt.title("Training Loss and Accuracy for model {}".format(i))
    plt.xlabel("Epoch #")
    plt.ylabel("Loss/Accuracy")
    plt.legend()
    plt.savefig(os.path.sep.join(p))
    plt.close()

需要注意的是,我們永遠不會直接進行整合訓練——我們首先要進行一系列實驗,以確定模型架構、優化器和超引數的具體資訊,保證在給定的資料集中能夠產生最高的精度。一旦確定了具體資訊之後,我們就可以訓練多個模型來形成一個模型集合。所以,直接訓練整合方法是不合理的,因為我們還不知道什麼樣的體系結構、哪種優化器和具體的超引數對給定的資料集最有效。從之前的內容中,我們知道使用SGD訓練的MiniVGGNet的準確度約為83%,下面通過應用整合方法,我們希望能夠增加準確度。

只需執行以下命令,就可以開始訓練模型:

$ python train_models.py --output output --models models

訓練了5個模型,檢視輸出路徑,如:

$ ls output/
model_0.png model_1.png model_2.png model_3.png model_4.png
model_0.txt model_1.txt model_2.txt model_3.txt model_4.txt

使用grep,我們可以輕鬆提取每個網路的分類結果:

$ grep ’avg / total’ output/*.txt
output/model_0.txt:avg / total 0.83 0.83 0.82 10000
output/model_1.txt:avg / total 0.83 0.83 0.82 10000
output/model_2.txt:avg / total 0.83 0.83 0.82 10000
output/model_3.txt:avg / total 0.82 0.82 0.81 10000
output/model_4.txt:avg / total 0.83 0.83 0.82 10000

從結果中可以看到,5個模型中有4個獲得82%的分類準確率,而其餘的模型只有81%的準確率。並從圖6.3中可以看到每組學習曲線都有些相似,但是仔細看又有一些不同,這表明每個MiniVGGNet模型以不同的方式進行“學習”。

圖6.3 訓練結果

既然我們已經單獨訓練了5個模型,下面將它們進行結合進行預測結果,看看分類精度是否有提高。

整合評估

新一個名為test_ensemble.py檔案,寫入以下程式碼:

#encoding:utf-8
# 載入所需模組
from sklearn.preprocessing import LabelBinarizer
from sklearn.metrics import classification_report
from keras.models import load_model
from keras.datasets import cifar10
import numpy as np
import argparse
import glob
import os

#命令列引數
ap = argpase.ArgumentParser()
ap.add_argument('-m','--models',required=True,help='path to models directory')
args = vars(ap.parse_args())

其中,命令列引數中:

  • –models: 模型權重儲存路徑,這裡載入權重時使用

載入CIFAR-10資料集,這裡只保留test資料集進行預測:

# 載入資料集,並進行預處理
(testX,testY) = cifar10.load_data()[1]
testX = testX.astype('float') / 255.0
# 初始化標籤名稱
labelNames = ["airplane", "automobile", "bird", "cat", "deer",
              "dog", "frog", "horse", "ship", "truck"]
# 類別one-hot編碼
lb = LabelBinarizer()
testY = lb.fit_transform(testY)

載入訓練好的模型權重:

# 遍歷模型
modelPaths = os.path.sep.join([args['models'],"*.model"])
modelPaths = list(glob.glob(modelPaths))
models = []

儲存的模型路徑結構:

[’models/model_0.model’, ’models/model_1.model’, ’models/model_2.model’,
’models/model_3.model’, ’models/model_4.model’]

從磁碟中載入每一個模型:

for (i,modelPath) in enumerate(modelPaths):
    print("[INFO] loading model {}/{}".format(i+1,len(modelPaths)))
    models.append(load_model(modelPath))

構建一個整合模型結果,並進行評估:

# 初始化
print("[INFO] evaluating ensemble...")
predictions = []
#遍歷模型
for model in models:
    # 模型預測
    predictions.append(model.predict(testX,batch_size=64))

# 平均所有模型結果
predictions = np.average(predictions,axis = 0)
# 模型結果
print(classification_report(testY.argmax(axis =1),
                            predictions.argmax(axis=1),target_names=labelNames))

首先,我們會初始化一個預測列表,該列表主要儲存每一個模型對test資料的預測結果。CIFAR-10資料集總共有10000張影象,10種類別,因此,每一個模型會產生一個(10000,10)的陣列。遍歷完5個模型預測之後,我們會得到一個shape為(5,10000,10)的predictions列表。而我們的真實資料shape為(10000,1),因此,我們需要對多個結果進行處理,常見的處理方法為取多個模型的平均值,如第6行程式碼所示,對得到的5個模型結果進行平均處理,將得到shape為(10000,10)的predictions列表,將predictions列表結果作為最後的輸出。

為了確定我們的MiniVGGNet模型集合是否提高了分類精度,執行以下命令:

$ python test_ensemble.py --models models

結果如下;

             precision    recall  f1-score   support

   airplane       0.87      0.85      0.86      1000
 automobile       0.91      0.94      0.93      1000
       bird       0.82      0.68      0.75      1000
        cat       0.76      0.55      0.64      1000
       deer       0.78      0.78      0.78      1000
        dog       0.84      0.66      0.74      1000
       frog       0.61      0.97      0.75      1000
      horse       0.87      0.89      0.88      1000
       ship       0.93      0.90      0.92      1000
      truck       0.86      0.93      0.89      1000

avg / total       0.83      0.81      0.81     10000

從結果中可以看到準確度從82%提高到834%,而我們僅僅只是對多個模型的結果進行平均計算。通常,對卷積神經網路使用整合方法進行預測,準確率可以提高1-5%。

總結

在本章中,我們回顧了機器學習中的集合技術,以及如何訓練多個獨立的模型,然後取多個模型結果的平均值作為最後的結果,以提高分類的準確性。通過回顧Jensen不等式,我們可以找到整合方法的理論依據。Jensen不等式指出,總體而言,多個模型的融合結果不比隨機單模型的結果差。

事實上,你在最新的論文(包括Inception[17]、ResNet[24]等)中看到的最好的結果都是多個模型進行整合得到的結果(通常是3-5個模型)。針對實際的資料集,合理進行使用整合方法。一般可以將準確度增加1-5%。

雖然,整合方法可以提高分類結果的準確度,但是,計算的代價也是很大的,因為本身訓練一個卷積神經網路既耗時又計算昂貴,利用整合方法之後,我們不是隻訓練一個網路,而是訓練多個CNN模型。所以一般使用差不多5-10個模型進行整合。

為了減輕訓練多個模型的計算負擔,Huang等人在2017年的一篇論文《Snapshot Ensembles:Train 1,get M for free》中提出了在單一訓練過程中使用迴圈學習速率計劃來訓練多個模型的想法。

該方法的工作原理是:

  • 1.以較高的學習速率開始訓練,然後快速降低學習速率,使得模型快速收斂到一個區域性最小值,儲存模型權重。
  • 2.接著使用較大的學習率進行優化,使得模型能夠跳出上一次的最小值
  • 3.不斷地重複M次

關於該論文的具體內容以及實現程式碼可以看到該篇文章

本文完整程式碼可在: github上獲取