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

在本節中,我們會努力建構、訓練和評估模型。在步驟 3,我們會選擇使用 S/W 比率使用 n-gram 模型或序列模型。現在,我們要編寫分類演算法並進行訓練。為此,我們使用 TensorFlow 搭配 tf.keras API。

使用 Keras 建構機器學習模型,主要是將圖層和資料處理構成要素組合在一起,就像我們組合 Lego 磚塊一樣。這些層能讓我們指定您希望在輸入上執行的轉換順序。由於我們的學習演算法會採用單一文字輸入並輸出單一分類,因此可以使用序列模型 API 建立線性線性線性堆疊。

圖層的線性堆疊

圖 9:圖層的線性堆疊

輸入層和中間層的建構方式會因我們建構的是 N 公元或序列模型而定。但無論模型類型為何,特定問題的最後一層都會是相同的。

建構最後一個圖層

如果僅有 2 個類別 (二進位分類),則模型應會輸出單一機率分數。例如,針對特定輸入樣本輸出 0.2 表示「20% 的信心樣本位於第一個類別 (類別 1)」中,80% 表示在第二個類別 (類別 0) 中)。若要輸出這類機率分數,最後一個圖層的「啟用函式」應為 sigmoid 函式0.2

如果有超過 2 個類別 (多類別分類),則模型應該為每個類別輸出一個機率分數。這些分數的總和應為 1。例如,輸出 {0: 0.2, 1: 0.7, 2: 0.1} 表示「20% 的信心樣本在這個類別 0 和 70% 的類別中為 10%,在 2 類別中則為 10%。」如要輸出這些分數,最後一個圖層的啟用函式應為 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-gram 模型和序列模型的其餘模型層。

S/W 比率較小時,我們發現 N 元模型的效能優於序列模型。大量小型向量時,序列模型會更好。這是因為嵌入關係是在密閉空間中學習的,而這對於許多範例來說都是最佳。

建立 N 元模型 [Option A]

我們將模型視為獨立處理符記 (不考慮字詞順序) 的模型。簡易的多層感知器 (包括邏輯迴歸)、漸層增強機器支援向量機器模型都屬於這個類別;無法運用文字排序的任何資訊。

我們比較了上述部分 N 元模型的效能,並發現多層感知器 (MLP) 通常比其他選項更好。MLP 易於定義及理解、提供準確的準確率,且需要相對較低的運算。

以下程式碼在 tf.keras 中定義雙層 MLP 模型,並新增幾項正規化層 (以防止過度訓練為訓練範例)。

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

建構序列模型 [Option B]

我們將模型的權杖從序列中學習。包括 CNN 和 RNN 類別。資料會預先處理為這些模型的序列向量。

序列模型通常有大量可供學習的參數。這些模型中的第一層是嵌入層,用於學習茂密向量空間中字詞之間的關係。對於許多範例,學習文字關聯性最適合。

特定資料集中的字詞在資料集中可能最不重複。因此,我們可以透過其他資料集瞭解資料集中字詞之間的關係。為此,我們可以將從其他資料集學到的嵌入內容轉移至嵌入層。這類嵌入稱為「預先訓練的嵌入。使用預先訓練的嵌入,可讓模型在學習過程中脫穎而出。

已有預先訓練的嵌入項目,可以使用大型公司 (例如 GloVe) 來訓練。GloVe 已受過多個主體獲得訓練 (主要為維基百科)。我們使用 GloVe 嵌入版本測試序列模型,並發現如果針對預先訓練的嵌入權重進行訓練,並僅訓練網路的其餘部分,模型的效能就會不佳。這可能是因為內嵌層的訓練結構定義可能與先前使用嵌入層的結構定義不同。

根據維基百科資料訓練的 GloVe 嵌入內容可能與 IMDb 資料集的語言模式不一致。推測推論出可能需要更新的部分,也就是說,嵌入的權重可能需要內容微調設定。做法分為兩個階段:

  1. 第一次執行時,嵌入層權重凍結時,系統會讓網路的其餘部分學習。在此執行作業結束時,模型權重會達到比未初始化值遠大的狀態。第二次執行時,我們也能允許嵌入層學習學習,並對網路中的所有權重進行微調。這項程序稱為微調的嵌入內容。

  2. 微調嵌入內容會更準確。不過,這需要訓練網路所需的運算能力提高。有了足夠的樣本,我們可以不從頭開始學習嵌入。我們發現,從 S/W > 15K 開始,使用微調微調的嵌入功能來獲得正確的準確率。

我們會比較不同的序列模型,例如 CNN、sepCNN、RNN (LSTM & GRU)、CNN-RNN 和堆疊式 RNN,各種不同的模型架構。我們發現 sepCNN 是卷積網路變化版本 (通常較具有資料效率和運算效率),成效通常優於其他模型。

下列程式碼可用來建構四層 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)

  • 指標:如何使用指標評估模型的效能。我們在實驗中使用了準確率指標。
  • Loss 函式:這個函式是用來計算訓練程序稍後會因為調整網路權重而盡可能減少的損失值。進行分類問題時,跨選擇的損耗效果相當良好。
  • 最佳化工具:用於根據遺失函式輸出結果,更新網路權重的函式。我們在實驗中使用熱門的 Adam 最佳化工具。

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

學習參數
指標 準確率
損失函式 - 二元分類 bincross_crossentropy
損失函式 - 多類別分類 sparse_categorical_crossentropy
最佳化工具 Adam

表 2:學習參數

實際訓練作業會使用 fit 方法進行。視資料集大小而定,這是大部分運算週期的支出方法。在每個訓練疊代作業中,系統會使用 batch_size 份訓練資料的樣本來計算損失,並根據這個值更新權重一次。模型看過整個訓練資料集後,訓練程序就會完成 epoch。在每個週期中,我們都會使用驗證資料集來評估模型的學習程度。我們會使用資料集重複訓練訓練的訓練週期數量。如果驗證的準確率在各個週期週期之間穩定,我們可能會提前停止,從而表明該模型不再受到訓練。

訓練超參數
學習率 1 到 3
Epoch 紀元時間 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]

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