回覆列表
  • 1 # 機器之心Pro

    在統計學和機器學習領域,整合方法(ensemble method)使用多種學習演算法以獲得更好的預測效能(相比單獨使用其中任何一種演算法)。和統計力學中的統計整合(通常是無窮集合)不同,一個機器學習整合僅由一個離散的可選模型的離散集合組成,但通常擁有更加靈活的結構 [1]。

    GitHub 地址:https://github.com/LawnboyMax/keras_ensemblng

    使用整合的主要動機是在發現新的假設,該假設不一定存在於構成模型的假設空間中。從經驗的角度看,當模型具有顯著的多樣性時,整合方法傾向於得到更好的結果 [2]。

    動機

    在一個大型機器學習競賽的比賽結果中,最好的結果通常是由模型的整合而不是由單個模型得到的。例如,ILSVRC2015 的得分最高的單個模型架構得到了第 13 名的成績。而第 1 到 12 名都使用了不同型別的模型整合。

    我目前並沒有發現有任何的教程或文件教人們如何在一個整合中使用多種模型,因此我決定自己做一個這方面的使用嚮導。

    我將使用 Keras,具體來說是它的功能性 API,以從相對知名的論文中重建三種小型 CNN(相較於 ResNet50、Inception 等而言)。我將在 CIFAR-10 資料集上獨立地訓練每個模型 [3]。然後使用測試集評估每個模型。之後,我會將所有三個模型組成一個集合,並進行評估。通常按照預期,這個整合相比單獨使用其中任何一個模型,在測試集上能獲得更好的效能。

    有很多種不同型別的整合:其中一種是堆疊(stacking)。這種型別更加通用並且在理論上可以表徵任何其它的整合技術。堆疊涉及訓練一個學習演算法結合多種其它學習演算法的預測 [1]。對於這個示例,我將使用堆疊的最簡單的一種形式,其中涉及對整合的模型輸出取平均值。由於取平均過程不包含任何引數,這種整合不需要訓練(只需要訓練模型)。

    本文介紹的整合的簡要結構

    準備資料

    首先,匯入類和函式:

    from keras.models import Model, Input

    from keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D, Activation, Average, Dropout

    from keras.utils import to_categorical

    from keras.losses import categorical_crossentropy

    from keras.callbacks import ModelCheckpoint, TensorBoard

    from keras.optimizers import Adam

    from keras.datasets import cifar10

    import numpy as np

    我使用的資料集是 CIFAR-10,因為很容易找到在這個資料集上工作得很好的架構的相關論文。使用一個流行的資料集還可以令這個案例容易復現。

    以下是資料集的匯入程式碼。訓練資料和測試資料都已經歸一化。訓練標籤向量被轉換成一個 one-hot 矩陣。不需要轉換測試標籤向量,因為它不會在訓練中使用。

    (x_train, y_train), (x_test, y_test) = cifar10.load_data()

    x_train = x_train / 255.

    x_test = x_test / 255.

    y_train = to_categorical(y_train, num_classes=10)

    資料集由 6 萬張 10 個類別的 32x32 的 RGB 影象組成。其中 5 萬張用於訓練/驗證,其它 1 萬張用於測試。

    print("x_train shape: {} | y_train shape: {}\nx_test shape : {} | y_test shape : {}".format(x_train.shape, y_train.shape, x_test.shape, y_test.shape))

    >>> x_train shape: (50000, 32, 32, 3) | y_train shape: (50000, 10)

    >>> x_test shape : (10000, 32, 32, 3) | y_test shape : (10000, 1)

    由於三個模型使用的是相同型別的資料,定義單個用於所有模型的輸入層是合理的。

    input_shape = x_train[0,:,:,:].shape

    model_input = Input(shape=input_shape)

    第一個模型:ConvPool-CNN-C

    第一個將要訓練的模型是 ConvPool-CNN-C[4]。它使用了常見的模式,即每個卷積層連線一個池化層。唯一一個對一些人來說可能不熟悉的細節是其最後的層。它使用的並不是多個全連線層,而是一個全域性平均池化層(global average pooling layer)。

    以下是關於全域性池化層的工作方式的簡介。最後的卷積層 Conv2D(10,(1,1)) 輸出和 10 個輸出類別相關的 10 個特徵圖。然後 GlobalAveragePooling2D() 層計算這 10 個特徵圖的空間平均(spatial average),意味著其輸出是一個維度為 10 的向量。之後,對這個向量應用一個 softmax 啟用函式。如你所見,這個方法在某種程度上類似於在模型的頂部使用全連線層。可以在這篇論文 [5] 中檢視更多關於全域性池化層的內容。

    重要事項:不要對最後的 Conv2D(10,(1,1)) 層的輸出直接應用啟用函式,因為這個層的輸出需要先輸入 GlobalAveragePooling2D()。

    def conv_pool_cnn(model_input):

    x = Conv2D(96, kernel_size=(3, 3), activation="relu", padding = "same")(model_input)

    x = Conv2D(96, (3, 3), activation="relu", padding = "same")(x)

    x = Conv2D(96, (3, 3), activation="relu", padding = "same")(x)

    x = MaxPooling2D(pool_size=(3, 3), strides = 2)(x)

    x = Conv2D(192, (3, 3), activation="relu", padding = "same")(x)

    x = Conv2D(192, (3, 3), activation="relu", padding = "same")(x)

    x = Conv2D(192, (3, 3), activation="relu", padding = "same")(x)

    x = MaxPooling2D(pool_size=(3, 3), strides = 2)(x)

    x = Conv2D(192, (3, 3), activation="relu", padding = "same")(x)

    x = Conv2D(192, (1, 1), activation="relu")(x)

    x = Conv2D(10, (1, 1))(x)

    x = GlobalAveragePooling2D()(x)

    x = Activation(activation="softmax")(x)

    model = Model(model_input, x, name="conv_pool_cnn")

    return model

    Instantiate the model.

    用具體例子解釋該模型

    conv_pool_cnn_model = conv_pool_cnn(model_input)

    為了簡單起見,每個模型都使用相同的引數進行編譯和訓練。其中,epoch 數等於 20、批尺寸等於 32(每個 epoch 進行 1250 次迭代)的引數設定能使三個模型都找到區域性極小值。隨機選擇訓練集的 20% 作為驗證集。

    def compile_and_train(model, num_epochs):

    model.compile(loss=categorical_crossentropy, optimizer=Adam(), metrics=["acc"])

    filepath = "weights/" + model.name + ".{epoch:02d}-{loss:.2f}.hdf5"

    checkpoint = ModelCheckpoint(filepath, monitor="loss", verbose=0, save_weights_only=True, save_best_only=True, mode="auto", period=1)

    tensor_board = TensorBoard(log_dir="logs/", histogram_freq=0, batch_size=32)

    history = model.fit(x=x_train, y=y_train, batch_size=32, epochs=num_epochs, verbose=1, callbacks=[checkpoint, tensor_board], validation_split=0.2)

    return history

    大約需要每 epoch1 分鐘的時間訓練這個(以及下一個)模型,我們使用了單個 Tesla K80 GPU。如果你使用的是 CPU,可能需要花較多的時間。

    _ = compile_and_train(conv_pool_cnn_model, num_epochs=20)

    該模型達到了大約 79% 的驗證準確率。

    ConvPool-CNN-C 驗證準確率和損失

    透過計算測試集的誤差率對模型進行評估。

    def evaluate_error(model):

    pred = model.predict(x_test, batch_size = 32)

    pred = np.argmax(pred, axis=1)

    pred = np.expand_dims(pred, axis=1) # make same shape as y_test

    error = np.sum(np.not_equal(pred, y_test)) / y_test.shape[0]

    return error

    evaluate_error(conv_pool_cnn_model)

    >>> 0.2414

    第二個模型:ALL-CNN-C

    下一個模型,ALL-CNN-C,來自同一篇論文 [4]。這個模型和上一個很類似。唯一的區別是用步幅為 2 的卷積層取代了最大池化層。再次,需要注意,在 Conv2D(10,(1,1)) 層之後不要立刻應用啟用函式,如果在該層之後應用了 ReLU 啟用函式,會導致訓練失敗。

    def all_cnn(model_input):

    x = Conv2D(96, kernel_size=(3, 3), activation="relu", padding = "same")(model_input)

    x = Conv2D(96, (3, 3), activation="relu", padding = "same")(x)

    x = Conv2D(96, (3, 3), activation="relu", padding = "same", strides = 2)(x)

    x = Conv2D(192, (3, 3), activation="relu", padding = "same")(x)

    x = Conv2D(192, (3, 3), activation="relu", padding = "same")(x)

    x = Conv2D(192, (3, 3), activation="relu", padding = "same", strides = 2)(x)

    x = Conv2D(192, (3, 3), activation="relu", padding = "same")(x)

    x = Conv2D(192, (1, 1), activation="relu")(x)

    x = Conv2D(10, (1, 1))(x)

    x = GlobalAveragePooling2D()(x)

    x = Activation(activation="softmax")(x)

    model = Model(model_input, x, name="all_cnn")

    return model

    all_cnn_model = all_cnn(model_input)

    _ = compile_and_train(all_cnn_model, num_epochs=20)

    The model converges to ~75% validation accuracy.

    該模型收斂到了大約 75% 的驗證準確率。

    ConvPool-CNN-C 驗證準確率和損失

    由於這兩個模型很相似,誤差率差別不大。

    evaluate_error(all_cnn_model)

    >>> 0.26090000000000002

    第三個模型:Network In Network CNN

    第三個 CNN 是 Network In Network CNN[5]。這個模型來自引入了全域性池化層的論文。它比之前的兩個模型更小,因此其訓練速度更快。(再提醒一次,不要在最後的卷積層之後使用 ReLU 函式!)

    相較於在 MLP 卷積層中使用多層感知機,我使用的是 1x1 卷積核的卷積層。從而需要最佳化的引數變得更少,訓練速度進一步加快,並且還獲得了更好的結果(當使用全連線層的時候無法獲得高於 50% 的驗證準確率)。該論文中稱,MLP 卷積層中應用的函式等價於在普通卷積層上的級聯跨通道引數化池化(cascaded cross channel parametric pooling),其中依次等價於一個 1x1 卷積核的卷積層。如果這個結論有錯誤,歡迎指正。

    def nin_cnn(model_input):

    #mlpconv block 1

    x = Conv2D(32, (5, 5), activation="relu",padding="valid")(model_input)

    x = Conv2D(32, (1, 1), activation="relu")(x)

    x = Conv2D(32, (1, 1), activation="relu")(x)

    x = MaxPooling2D((2, 2))(x)

    x = Dropout(0.5)(x)

    #mlpconv block2

    x = Conv2D(64, (3, 3), activation="relu",padding="valid")(x)

    x = Conv2D(64, (1, 1), activation="relu")(x)

    x = Conv2D(64, (1, 1), activation="relu")(x)

    x = MaxPooling2D((2, 2))(x)

    x = Dropout(0.5)(x)

    #mlpconv block3

    x = Conv2D(128, (3, 3), activation="relu",padding="valid")(x)

    x = Conv2D(32, (1, 1), activation="relu")(x)

    x = Conv2D(10, (1, 1))(x)

    x = GlobalAveragePooling2D()(x)

    x = Activation(activation="softmax")(x)

    model = Model(model_input, x, name="nin_cnn")

    return model

    nin_cnn_model = nin_cnn(model_input)

    This model trains much faster—15 seconds per epoch on my machine.

    這個模型的訓練速度快得多,在我的機器上每個 epoch 只要 15 秒就能完成。

    _ = compile_and_train(nin_cnn_model, num_epochs=20)

    The model achieves ~65% validation accuracy.

    該模型達到了大約 65% 的驗證準確率。

    NIN-CNN 驗證準確率和損失

    這個模型比之前的兩個模型簡單得多,因此其誤差率要高一點。

    evaluate_error(nin_cnn_model)

    >>> 0. 0.31640000000000001

    三個模型的整合

    現在將這三個模型組合成一個整合。

    所有三個模型都被重新例項化並載入了最佳的已儲存權重。

    conv_pool_cnn_model = conv_pool_cnn(model_input)

    all_cnn_model = all_cnn(model_input)

    nin_cnn_model = nin_cnn(model_input)

    conv_pool_cnn_model.load_weights("weights/conv_pool_cnn.29-0.10.hdf5")

    all_cnn_model.load_weights("weights/all_cnn.30-0.08.hdf5")

    nin_cnn_model.load_weights("weights/nin_cnn.30-0.93.hdf5")

    models = [conv_pool_cnn_model, all_cnn_model, nin_cnn_model]

    整合模型的定義是很直接的。它使用了所有模型共享的輸入層。在頂部的層中,該整合透過使用 Average() 合併層計算三個模型輸出的平均值。

    def ensemble(models, model_input):

    outputs = [model.outputs[0] for model in models]

    y = Average()(outputs)

    model = Model(model_input, y, name="ensemble")

    return model

    ensemble_model = ensemble(models, model_input)

    不出所料,相比於任何單一模型,整合有著更低的誤差率。

    evaluate_error(ensemble_model)

    >>> 0.2049

    其他可能的整合

    為了完整性,我們可以檢視由兩個模型組合組成的整合的效能。相比於單一模型,前者有更低的誤差率。

    pair_A = [conv_pool_cnn_model, all_cnn_model]

    pair_B = [conv_pool_cnn_model, nin_cnn_model]

    pair_C = [all_cnn_model, nin_cnn_model]

    pair_A_ensemble_model = ensemble(pair_A, model_input)

    evaluate_error(pair_A_ensemble_model)

    >>> 0.21199999999999999

    pair_B_ensemble_model = ensemble(pair_B, model_input)

    evaluate_error(pair_B_ensemble_model)

    >>> 0.22819999999999999

    pair_C_ensemble_model = ensemble(pair_C, model_input)

    evaluate_error(pair_C_ensemble_model)

    >>>0.2447

    結論

    重申一下介紹中的內容:每個模型有其自身的缺陷。使用整合背後的原因是透過堆疊表徵了關於資料的不同假設的不同模型,我們可以找到一個更好的假設,它不在一個從其構建整合的模型的假設空間之中。

    與在大多數情況下使用單個模型相比,使用一個非常基礎的整合實現了更低的誤差率。這證明了整合的有效性。

    當然,在使用整合處理你的機器學習任務時,需要牢記一些實際的考慮。由於整合意味著同時堆疊多個模型,這也意味著輸入資料需要前向傳播到每個模型。這增加了需要被執行的計算量,以及最終的評估(預測)時間。如果你在研究或 Kaggle 競賽中使用整合,增加的評估時間並不重要,但是在設計一個商業化產品時卻非常關鍵。另一個考慮是最後的模型增加的大小,它會再次成為商業化產品中整合使用的限制性因素。

    參考文獻

    1. Ensemble Learning. (n.d.). In Wikipedia. Retrieved December 12, 2017, from https://en.wikipedia.org/wiki/Ensemble_learning

    2. D. Opitz and R. Maclin (1999)「Popular Ensemble Methods: An Empirical Study」, Volume 11, pages 169–198 (http://jair.org/papers/paper614.html)

    3. Learning Multiple Layers of Features from Tiny Images, Alex Krizhevsky, 2009.

    4. Striving for Simplicity: The All Convolutional Net:arXiv:1412.6806v3 [cs.LG]

    5. Network In Network:arXiv:1312.4400v3 [cs.NE]

  • 中秋節和大豐收的關聯?
  • 你們遇到過什麼樣的“熊大人”?