步驟 4:建構、訓練及評估模型

本節將致力於建構、訓練及評估模型。在步驟 3 中,我們使用 S/W 比例選擇使用 n 元語法模型或序列模型。接著,請編寫並訓練分類演算法。為此,我們將使用 TensorFlow 搭配 tf.keras API。

使用 Keras 建構機器學習模型的重點在於組合層、資料處理建構模塊,就像在組合樂高積木一樣。這些層可讓我們指定要對輸入執行的轉換序列。由於我們的學習演算法採用單一文字輸入內容並輸出單一分類,因此我們可以使用依序模型 API 建立線性圖層堆疊。

圖層的線性堆疊

圖 9:圖層的線性堆疊

視要建構的是 n 元語法或序列模型而定,輸入層和中繼層的建構方式會有所不同。但無論模型類型為何,指定問題的最後一個層會相同。

建構最後一層

當我們只有 2 個類別 (二元分類) 時,模型應輸出單一機率分數。舉例來說,針對特定輸入樣本輸出 0.2 代表「有 20% 的信賴度,表示這個樣本位於第一個類別 (類別 1) 中,80% 位於第二個類別 (類別 0)」。如要輸出這種機率分數,最後一個層的啟用函式應是 S 函數函式,而 交叉函數則應用於訓練特徵碼模型。(請見左側的圖 10)。

如果類別超過 2 個 (多元分類),我們的模型應每個類別輸出一個機率分數。這些分數的總和應為 1。舉例來說,輸出 {0: 0.2, 1: 0.7, 2: 0.1} 表示「樣本落在類別 0 的 20% 信賴區間,70% 來自類別 1,10% 位於類別 2」。如要輸出這些分數,最後一層的活化函式應為 softmax,而用來訓練模型的損失函式應為類別式交叉熵。(請參閱圖 10)。

最後一層

圖 10:最後一層

以下程式碼定義了一個函式,該函式會將類別數量做為輸入內容,然後輸出適當的圖層單元數量 (二元分類為 1 個單位;否則每個類別各 1 個單位),以及適當的活化函式:

def _get_last_layer_units_and_activation(num_classes):
    """Gets the # units and activation function for the last network layer.

    # Arguments
        num_classes: int, number of classes.

    # Returns
        units, activation values.
    """
    if num_classes == 2:
        activation = 'sigmoid'
        units = 1
    else:
        activation = 'softmax'
        units = num_classes
    return units, activation

以下兩節將逐步說明如何為 n 元語法模型和序列模型建立其餘的模型層。

如果 S/W 比率偏低,我們發現 n 元語法模型的成效優於序列模型。如果向量數量較多、密集的向量,序列模型就會比較好。這是因為嵌入關係是在稠密空間中學習,而在許多樣本中效果最好。

建構 n 元語法模型 [選項 A]

我們將獨立處理符記的模型 (不考量字詞順序) 做為 n 元語法模型。簡單的多層感知 (包括邏輯迴歸 梯度增強機器以及支援向量機器模型) 都屬於這個類別;無法使用任何文字排序相關資訊。

我們比較上述某些 n 元語法模型的效能,發現多層感知 (MLP) 的效能通常優於其他選項。MLP 易於定義和理解、提供良好的準確率,且運算能力相對較低。

以下程式碼在 tf.keras 中定義雙層 MLP 模型,新增幾個用於正規化的 Dropout 層,以避免訓練樣本過度配適

from tensorflow.python.keras import models
from tensorflow.python.keras.layers import Dense
from tensorflow.python.keras.layers import Dropout

def mlp_model(layers, units, dropout_rate, input_shape, num_classes):
    """Creates an instance of a multi-layer perceptron model.

    # Arguments
        layers: int, number of `Dense` layers in the model.
        units: int, output dimension of the layers.
        dropout_rate: float, percentage of input to drop at Dropout layers.
        input_shape: tuple, shape of input to the model.
        num_classes: int, number of output classes.

    # Returns
        An MLP model instance.
    """
    op_units, op_activation = _get_last_layer_units_and_activation(num_classes)
    model = models.Sequential()
    model.add(Dropout(rate=dropout_rate, input_shape=input_shape))

    for _ in range(layers-1):
        model.add(Dense(units=units, activation='relu'))
        model.add(Dropout(rate=dropout_rate))

    model.add(Dense(units=op_units, activation=op_activation))
    return model

建構序列模型 [選項 B]

我們將可透過符記的相鄰性學習的模型為序列模型。這包括模型的 CNN 和 RNN 類別。系統會預先處理資料做為這些模型的序列向量。

序列模型通常需要學習的參數數量較多。這類模型的第一層是嵌入層,會學習密集向量空間中字詞之間的關係。學習字詞關係最適合用於許多範例

指定資料集內的字詞不太可能專屬於該資料集。因此,我們可以使用其他資料集,瞭解資料集內字詞之間的關係。為此,我們可以將從其他資料集學到的嵌入轉移至嵌入層。這些嵌入稱為「預先訓練的嵌入」。使用預先訓練的嵌入,可讓模型在學習過程中搶先體驗。

許多預先訓練的嵌入已使用大型語料庫訓練,例如 GloVe。GloVe 曾在多個語料庫 (主要為維基百科) 上訓練。我們使用 GloVe 嵌入版本測試序列模型,並發現如果凍結預先訓練嵌入的權重,並僅訓練網路其他部分,模型的表現就會不佳。這可能是因為訓練嵌入層的情境可能與當時的情境不同。

使用 Wikipedia 資料訓練的 GloVe 嵌入,可能與 IMDb 資料集的語言模式不一致。推論的關係可能需要經過一些更新,也就是說,嵌入權重可能需要調整內容。我們會分兩個階段來執行這項作業:

  1. 在第一次執行時,嵌入層權重凍結,我們允許網路其他部分學習。在此執行作業結束時,模型權重達到的狀態會優於未初始化的值。在第二次執行時,我們允許嵌入層也學習,並微調網路中的所有權重。我們將這個過程稱為使用經過微調的嵌入。

  2. 經過微調的嵌入可提高準確度。然而,這會導致訓練網路所需的運算能力增加。如果樣本數量充足,我們就能從零開始學習嵌入。我們發現 S/W > 15K 從零開始,實際上就與使用微調的嵌入相同,兩者的準確率差不多。

我們比較了不同的模型架構,例如 CNN、sepCNN、RNN (LSTM 和 GRU)、CNN-RNN 和堆疊的 RNN。我們發現 sepCNNs 是卷積網路變體,通常其資料效率和運算效率較高,其效能優於其他模型。

下列程式碼建構了一個四層 sepCNN 模型:

from tensorflow.python.keras import models
from tensorflow.python.keras import initializers
from tensorflow.python.keras import regularizers

from tensorflow.python.keras.layers import Dense
from tensorflow.python.keras.layers import Dropout
from tensorflow.python.keras.layers import Embedding
from tensorflow.python.keras.layers import SeparableConv1D
from tensorflow.python.keras.layers import MaxPooling1D
from tensorflow.python.keras.layers import GlobalAveragePooling1D

def sepcnn_model(blocks,
                 filters,
                 kernel_size,
                 embedding_dim,
                 dropout_rate,
                 pool_size,
                 input_shape,
                 num_classes,
                 num_features,
                 use_pretrained_embedding=False,
                 is_embedding_trainable=False,
                 embedding_matrix=None):
    """Creates an instance of a separable CNN model.

    # Arguments
        blocks: int, number of pairs of sepCNN and pooling blocks in the model.
        filters: int, output dimension of the layers.
        kernel_size: int, length of the convolution window.
        embedding_dim: int, dimension of the embedding vectors.
        dropout_rate: float, percentage of input to drop at Dropout layers.
        pool_size: int, factor by which to downscale input at MaxPooling layer.
        input_shape: tuple, shape of input to the model.
        num_classes: int, number of output classes.
        num_features: int, number of words (embedding input dimension).
        use_pretrained_embedding: bool, true if pre-trained embedding is on.
        is_embedding_trainable: bool, true if embedding layer is trainable.
        embedding_matrix: dict, dictionary with embedding coefficients.

    # Returns
        A sepCNN model instance.
    """
    op_units, op_activation = _get_last_layer_units_and_activation(num_classes)
    model = models.Sequential()

    # Add embedding layer. If pre-trained embedding is used add weights to the
    # embeddings layer and set trainable to input is_embedding_trainable flag.
    if use_pretrained_embedding:
        model.add(Embedding(input_dim=num_features,
                            output_dim=embedding_dim,
                            input_length=input_shape[0],
                            weights=[embedding_matrix],
                            trainable=is_embedding_trainable))
    else:
        model.add(Embedding(input_dim=num_features,
                            output_dim=embedding_dim,
                            input_length=input_shape[0]))

    for _ in range(blocks-1):
        model.add(Dropout(rate=dropout_rate))
        model.add(SeparableConv1D(filters=filters,
                                  kernel_size=kernel_size,
                                  activation='relu',
                                  bias_initializer='random_uniform',
                                  depthwise_initializer='random_uniform',
                                  padding='same'))
        model.add(SeparableConv1D(filters=filters,
                                  kernel_size=kernel_size,
                                  activation='relu',
                                  bias_initializer='random_uniform',
                                  depthwise_initializer='random_uniform',
                                  padding='same'))
        model.add(MaxPooling1D(pool_size=pool_size))

    model.add(SeparableConv1D(filters=filters * 2,
                              kernel_size=kernel_size,
                              activation='relu',
                              bias_initializer='random_uniform',
                              depthwise_initializer='random_uniform',
                              padding='same'))
    model.add(SeparableConv1D(filters=filters * 2,
                              kernel_size=kernel_size,
                              activation='relu',
                              bias_initializer='random_uniform',
                              depthwise_initializer='random_uniform',
                              padding='same'))
    model.add(GlobalAveragePooling1D())
    model.add(Dropout(rate=dropout_rate))
    model.add(Dense(op_units, activation=op_activation))
    return model

訓練模型

現在我們已建構了模型架構,接著需要訓練模型,訓練時,您需要根據模型的目前狀態進行預測、計算預測的準確度,並更新網路的權重或參數,藉此將這項錯誤降到最低,並提高模型的預測品質。我們會重複這個流程,直到模型收尾且無法再學習為止。為此程序選擇三個重要參數 (請參閱表 2)。

  • 指標:如何使用指標評估模型的效能。我們使用「準確率」做為實驗中的指標。
  • 損失函式:此函式是用來計算訓練程序之後,會嘗試藉由調整網路權重來最小化的損失值。如果是分類問題,跨熵損失的效果相當不錯。
  • 最佳化工具:這個函式可決定如何根據損失函式的輸出結果更新網路權重。我們在實驗中使用了熱門的 Adam 最佳化工具。

在 Keras 中,我們可以使用 compile 方法,將這些學習參數傳送至模型。

表 2:學習參數

學習參數 價值
指標 精確度
損失函式 - 二元分類 binary_crossentropy
損失函式 - 多類別分類 sparse_categorical_crossentropy
最佳化器 adam

實際的訓練是使用 fit 方法進行。此方法將用於大部分運算週期,視您的資料集大小而定。在每個訓練疊代中,系統會使用訓練資料中的 batch_size 樣本計算損失,並根據這個值更新一次權重。一旦模型看到整個訓練資料集,訓練程序就會完成 epoch。在每個訓練週期結束時,我們會使用驗證資料集來評估模型的學習成效。針對預先定義的訓練週期數 我們會使用該資料集重複進行訓練當驗證準確率介於連續週期之間,顯示模型已不再訓練時,我們可能會提早停止最佳化作業。

訓練超參數 價值
學習率 1e-3
訓練週期 1000
批量 512
提早中止訓練 參數:val_loss、可用性:1

表 3:訓練超參數

下列 Keras 程式碼使用上方表 2 和表 3 中選擇的參數,實作訓練程序:

def train_ngram_model(data,
                      learning_rate=1e-3,
                      epochs=1000,
                      batch_size=128,
                      layers=2,
                      units=64,
                      dropout_rate=0.2):
    """Trains n-gram model on the given dataset.

    # Arguments
        data: tuples of training and test texts and labels.
        learning_rate: float, learning rate for training model.
        epochs: int, number of epochs.
        batch_size: int, number of samples per batch.
        layers: int, number of `Dense` layers in the model.
        units: int, output dimension of Dense layers in the model.
        dropout_rate: float: percentage of input to drop at Dropout layers.

    # Raises
        ValueError: If validation data has label values which were not seen
            in the training data.
    """
    # Get the data.
    (train_texts, train_labels), (val_texts, val_labels) = data

    # Verify that validation labels are in the same range as training labels.
    num_classes = explore_data.get_num_classes(train_labels)
    unexpected_labels = [v for v in val_labels if v not in range(num_classes)]
    if len(unexpected_labels):
        raise ValueError('Unexpected label values found in the validation set:'
                         ' {unexpected_labels}. Please make sure that the '
                         'labels in the validation set are in the same range '
                         'as training labels.'.format(
                             unexpected_labels=unexpected_labels))

    # Vectorize texts.
    x_train, x_val = vectorize_data.ngram_vectorize(
        train_texts, train_labels, val_texts)

    # Create model instance.
    model = build_model.mlp_model(layers=layers,
                                  units=units,
                                  dropout_rate=dropout_rate,
                                  input_shape=x_train.shape[1:],
                                  num_classes=num_classes)

    # Compile model with learning parameters.
    if num_classes == 2:
        loss = 'binary_crossentropy'
    else:
        loss = 'sparse_categorical_crossentropy'
    optimizer = tf.keras.optimizers.Adam(lr=learning_rate)
    model.compile(optimizer=optimizer, loss=loss, metrics=['acc'])

    # Create callback for early stopping on validation loss. If the loss does
    # not decrease in two consecutive tries, stop training.
    callbacks = [tf.keras.callbacks.EarlyStopping(
        monitor='val_loss', patience=2)]

    # Train and validate model.
    history = model.fit(
            x_train,
            train_labels,
            epochs=epochs,
            callbacks=callbacks,
            validation_data=(x_val, val_labels),
            verbose=2,  # Logs once per epoch.
            batch_size=batch_size)

    # Print results.
    history = history.history
    print('Validation accuracy: {acc}, loss: {loss}'.format(
            acc=history['val_acc'][-1], loss=history['val_loss'][-1]))

    # Save model.
    model.save('IMDb_mlp_model.h5')
    return history['val_acc'][-1], history['val_loss'][-1]

請前往這裡查看訓練序列模型的程式碼範例。