ステップ 4: モデルを構築、トレーニング、評価する

このセクションでは、モデルの構築、トレーニング、評価について説明します。ステップ 3 では、S/W 比率を使用して、n グラムモデルまたはシーケンス モデルを使用します。次に、分類アルゴリズムを記述してトレーニングします。そのために、TensorFlowtf.keras API を使用します。

Keras を使用して機械学習モデルを構築するには、レゴブロックを組み合わせる場合と同様に、レイヤ、データ処理ビルディング ブロックの組み立てを行います。これらのレイヤを使用すると、入力に対して実行する変換の順序を指定できます。学習アルゴリズムは、単一のテキスト入力を受け取り、単一の分類を出力するため、Sequential モデル API を使用して線形レイヤのレイヤ スタックを作成できます。

レイヤの線形スタック

図 9: レイヤの線形スタック

入力層と中間層は、n グラムモデルとシーケンス モデルのどちらを作成するかによって、構成が異なります。ただし、モデルタイプに関係なく、特定の問題に対する最後のレイヤは同じになります。

最後のレイヤの構築

2 つのクラス(バイナリ分類)しかない場合、モデルは単一の確率スコアを出力します。たとえば、特定の入力サンプルに対して 0.2 を出力すると、「このサンプルが 1 つ目のクラス(クラス 1)にあることが 20% の信頼度、2 つ目のクラス(クラス 0)にあるのが 80% の信頼度」になります。このような確率スコアを出力するには、最後のレイヤの活性化関数sigmoid 関数で、bin0} を 1 番目に使用してください。

3 つ以上のクラス(マルチクラス分類)がある場合、モデルはクラスごとに 1 つの確率スコアを出力します。これらのスコアの合計は 1 になります。たとえば、{0: 0.2, 1: 0.7, 2: 0.1} を出力すると、「このサンプルがクラス 0 にあるという信頼度が 20%、クラス 1 にあることが 70%、クラス 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

次の 2 つのセクションでは、n グラム モデルとシーケンス モデルの残りのモデルレイヤの作成について説明します。

S/W の比率が小さい場合、n グラムモデルはシーケンス モデルよりもパフォーマンスが高いことがわかっています。密集したベクトルが数多くある場合には、シーケンス モデルが最適です。これは、埋め込み関係が高密度空間で学習され、多くのサンプルで最適に生成されるためです。

n-gram モデルの構築 [オプション A]

トークンを単独で処理する(単語の順番は考慮しない)モデルを、n グラムモデルと呼びます。シンプルな多層認識(ロジスティック回帰を含む)、勾配ブースティング マシンサポート ベクターマシンモデルはいずれもこのカテゴリに分類されます。テキストの順序付けに関する情報は活用できません。

上記の n-gram モデルのパフォーマンスを比較すると、通常、マルチレイヤ パーセプトロン(MLP)のパフォーマンスが他のオプションよりも優れていることがわかりました。MLP は、定義と理解が簡単で、精度が高く、計算量も比較的少なくて済みます。

次のコードでは、tf.keras で 2 層の MLP モデルを定義し、正則化用のドロップアウト レイヤを 2 つ追加します(トレーニング サンプルの過学習を防ぎます)。

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 は、複数のコーパス(主に Wikipedia)でトレーニングを受けています。GloVe エンベディングのバージョンを使用してシーケンス モデルのトレーニングをテストしたところ、事前トレーニング済みのエンベディングの重みを凍結し、残りのネットワークのみをトレーニングした場合、モデルのパフォーマンスは向上しませんでした。これは、エンベディング レイヤがトレーニングされたコンテキストが、このコンテキストを使用しているコンテキストと異なる可能性があるためです。

Wikipedia のデータでトレーニングされた GloVe のエンベディングは、IMDb データセットの言語パターンと一致しない場合があります。推測される関係は、なんらかの更新を必要とする場合があります。つまり、エンベディングの重み付けがコンテキストの調整を必要とする場合があります。これは次の 2 段階で行われます。

  1. 最初の実行では、エンベディング レイヤの重みが固定され、ネットワークの残りの部分も学習できるようになります。この実行の終了時に、モデルの重みは初期化されていない値よりもはるかに優れた状態に達します。2 回目の実行では、埋め込みレイヤも学習して、ネットワーク内のすべての重みを微調整します。このプロセスは、微調整されたエンベディングの使用と呼びます。

  2. エンベディングを微調整すると、精度が向上します。ただし、ネットワークのトレーニングに必要な演算能力が増大します。サンプルの数が十分であれば、エンベディングをゼロから学習することもできます。S/W > 15K について、ゼロから始めると、微調整されたエンベディングを使用する場合とほぼ同じ精度が得られることがわかりました。

CNN、sepCNN、RNN(LSTM & GRU)、CNN-RNN、スタック RNN などのさまざまなシーケンス モデルを比較して、モデル アーキテクチャを変えました。sepCNN は、畳み込みネットワーク バリアントであり、データ効率とコンピューティング効率が高いことが多いため、他のモデルよりも優れたパフォーマンスを発揮します。

次のコードは、4 層の 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

モデルのトレーニング

モデル アーキテクチャの構築が完了したので、モデルをトレーニングする必要があります。トレーニングでは、モデルの現在の状態に基づいて予測を行い、予測の誤りを正確に計算し、このエラーを最小限に抑えてモデルの予測精度を向上させるために、ネットワークの重みまたはパラメータを更新します。モデルが収束し、学習できなくなるまで、このプロセスを繰り返します。このプロセスでは、3 つの主要なパラメータを選択する必要があります(表 2 を参照)。

  • 指標: 指標を使用してモデルのパフォーマンスを測定する方法。テストの指標には精度を使用しました。
  • 損失関数: トレーニング プロセスで損失の値を計算するために使用され、ネットワークの重みを調整することによって最小化を試みる関数。分類の問題については、交差エントロピー損失が有効です。
  • オプティマイザー: 損失関数の出力に基づいてネットワークの重みの更新方法を決定する関数。テストでは、一般的な Adam オプティマイザーを使用しました。

Keras では、compile メソッドを使用して、これらの学習パラメータをモデルに渡すことができます。

学習パラメータ
指標 accuracy
損失関数 - バイナリ分類 Binary_Crossentropy
損失関数 - マルチクラス分類 sparse_categorical_crossentropy
オプティマイザー Adam

表 2: 学習パラメータ

実際のトレーニングは、fit メソッドを使用して行われます。データセットのサイズに応じて、ほとんどのコンピューティング サイクルを使用します。各トレーニング イテレーションでは、トレーニング データから batch_size のサンプル数を損失の計算に使用し、その値に基づいて重みを 1 回更新します。モデルがトレーニング データセット全体を認識すると、トレーニング プロセスは epoch を完了します。各エポックの最後に、検証データセットを使用して、モデルがどの程度適切に学習しているかを評価します。データセットを使用して、事前に決められたエポック数でトレーニングを繰り返します。Google は、連続したエポック間で検証の精度が安定したときに早期に停止することで、これを最適化できます。これは、モデルのトレーニングが不要になったことを示しています。

ハイパーパラメータのトレーニング
学習率 1e-3
エポック 1000
バッチサイズ 512
早期停止 パラメータ: val_loss、rapes: 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]

シーケンス モデルをトレーニングするコードサンプルについては、こちらをご覧ください。