Schritt 4: Modell erstellen, trainieren und bewerten

In diesem Abschnitt werden wir an der Erstellung, dem Training und der Bewertung unseres Modells arbeiten. In Schritt 3 haben wir uns für die Verwendung eines N-Gramm-Modells oder eines Sequenzmodells mit dem Seitenverhältnis S/W entschieden. Jetzt ist es an der Zeit, unseren Klassifizierungsalgorithmus zu schreiben und zu trainieren. Wir verwenden hierfür TensorFlow mit der tf.keras API.

Beim Erstellen von Modellen für maschinelles Lernen mit Keras geht es um das Zusammensetzen von Ebenen und Bausteinen zur Datenverarbeitung, ähnlich wie beim Bau von Legosteinen. Mit diesen Ebenen können wir die Sequenz der Transformationen angeben, die wir für unsere Eingabe ausführen möchten. Da unser Lernalgorithmus eine einzelne Texteingabe akzeptiert und eine einzelne Klassifizierung ausgibt, können wir mit der Sequential Model API einen linearen Stapel von Ebenen erstellen.

Linearer Stapel von Ebenen

Abbildung 9: Linearer Stapel von Ebenen

Die Eingabeebene und die Zwischenebenen werden unterschiedlich konstruiert, je nachdem, ob wir ein N-Gramm- oder ein Sequenzmodell erstellen. Aber unabhängig vom Modelltyp ist die letzte Ebene für ein bestimmtes Problem dieselbe.

Letzte Ebene konstruieren

Wenn wir nur zwei Klassen haben (binäre Klassifizierung), sollte unser Modell eine einzelne Wahrscheinlichkeitswertung ausgeben. Die Ausgabe von 0.2 für ein bestimmtes Eingabebeispiel bedeutet beispielsweise „20% Zuverlässigkeit, dass dieses Beispiel in der ersten Klasse (Klasse 1) enthalten ist, 80 %, dass es sich in der zweiten Klasse befindet (Klasse 0).“ Zum Ausgeben eines solchen Wahrscheinlichkeitswerts sollte die Aktivierungsfunktion der letzten Ebene eine Sigmoid-Funktion sein und die Verlustfunktion – 1 nutzen1.

Wenn es mehr als zwei Klassen gibt (Klassifizierung mit mehreren Klassen), sollte unser Modell einen Wahrscheinlichkeitswert pro Klasse ausgeben. Die Summe dieser Werte sollte 1 sein. Die Ausgabe von {0: 0.2, 1: 0.7, 2: 0.1} bedeutet beispielsweise: „20% ist zuverlässig, dass dieses Beispiel in Klasse 0 enthalten ist, 70 %, dass es sich in Klasse 1 befindet und 10 %, dass es sich in Klasse 2 befindet.“ Um diese Punktzahlen auszugeben, sollte die Aktivierungsfunktion der letzten Ebene Softmax sein. Die Verlustfunktion, die zum Trainieren des Modells verwendet wird, sollte kategorische Kreuzentropie haben. (Siehe Abbildung 10 rechts).

Letzte Ebene

Abbildung 10: Letzte Ebene

Der folgende Code definiert eine Funktion, die die Anzahl der Klassen als Eingabe verwendet und die entsprechende Anzahl von Ebeneneinheiten (1 Einheit für die binäre Klassifizierung, andernfalls 1 Einheit für jede Klasse) und die entsprechende Aktivierungsfunktion ausgibt:

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

In den folgenden beiden Abschnitten wird die Erstellung der verbleibenden Modellebenen für N-Gramm- und Sequenzmodelle beschrieben.

Wenn das Verhältnis S/W klein ist, haben wir festgestellt, dass N-Gramm-Modelle besser funktionieren als Sequenzmodelle. Sequenzmodelle sind besser, wenn es eine große Anzahl kleiner, dichter Vektoren gibt. Das liegt daran, dass das Einbetten von Beziehungen in einem kompakten Raum erfolgt, was am besten bei vielen Stichproben geschieht.

N-Gramm-Modell erstellen [Option A]

Wir bezeichnen Modelle, die die Tokens unabhängig voneinander verarbeiten (ohne die Wortfolge berücksichtigen) als N-Gramm-Modelle. In diese Kategorie fallen einfache mehrschichtige Perceptronmodelle (einschließlich logischer Regression), Gradienten-Boosting-Maschinen und Support-Vektormaschinen. Sie können keine Informationen zur Textreihenfolge nutzen.

Wir haben die Leistung einiger der oben genannten N-Gramm-Modelle verglichen und festgestellt, dass Mehrschicht-Perceptrone (MLPs) normalerweise eine bessere Leistung erzielen als andere Optionen. MLPs sind einfach zu definieren und zu verstehen, bieten eine gute Genauigkeit und erfordern relativ wenige Berechnungen.

Mit dem folgenden Code wird ein zweischichtiges MLP-Modell in tf.keras definiert und es werden einige Dropout-Ebenen für die Normalisierung hinzugefügt, um eine Überanpassung der Trainingsbeispiele zu verhindern.

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

Build-Sequenzmodell [Option B]

Wir bezeichnen Modelle, die aus der Nähe von Tokens lernen können, als Sequenzmodelle. Dazu gehören CNN- und RNN-Modellmodelle. Die Daten werden für diese Modelle als Sequenzvektoren vorverarbeitet.

Sequenzmodelle haben in der Regel eine größere Anzahl von Parametern, die sie lernen müssen. Die erste Ebene in diesen Modellen ist eine Einbettungsschicht, die die Beziehung zwischen den Wörtern in einem dichten Vektorraum erlernt. Wortbeziehungen funktionieren am besten bei vielen Stichproben.

Wörter in einem bestimmten Dataset sind höchstwahrscheinlich nicht eindeutig für dieses Dataset. Auf diese Weise können wir die Beziehung zwischen den Wörtern in unserem Dataset mithilfe anderer Datasets nachvollziehen. Dazu können wir eine Einbettung, die aus einem anderen Dataset gelernt wurde, in unsere Einbettungsebene übertragen. Diese Einbettungen werden als vortrainierte Einbettungen bezeichnet. Mit einer vortrainierten Einbettung hat das Modell einen Vorsprung im Lernprozess.

Es gibt vortrainierte Einbettungen, die mit großen Korpora, z. B. GloVe, trainiert wurden. GloVe wurde in mehreren Korpora geschult (hauptsächlich Wikipedia). Wir haben das Training unserer Sequenzmodelle mit einer Version von GloVe-Einbettungen getestet und festgestellt, dass die Modelle keine gute Leistung erzielen, wenn wir die Gewichtungen der vortrainierten Einbettungen einfrieren und nur den Rest des Netzwerks trainieren. Dies kann daran liegen, dass sich der Kontext, in dem die Einbettungsebene trainiert wurde, von dem Kontext unterscheidet, in dem wir sie verwendet haben.

GloVe-Einbettungen, die mit Wikipedia-Daten trainiert wurden, stimmen möglicherweise nicht mit den Sprachmustern in unserem IMDb-Dataset überein. Die abgeleiteten Beziehungen müssen möglicherweise aktualisiert werden, d.h., die Einbettungsgewichtungen müssen möglicherweise an den Kontext angepasst werden. Das erfolgt in zwei Phasen:

  1. Bei der ersten Ausführung, bei der die Einbettungsschichten eingefroren sind, erlauben wir dem Rest des Netzwerks, zu lernen. Am Ende dieser Ausführung erreichen die Modellgewichtungen einen Status, der viel besser als ihre nicht initialisierten Werte ist. Bei der zweiten Ausführung lernt die Einbettungsebene, die Gewichtungen im Netzwerk genau anzupassen. Wir bezeichnen diesen Prozess als fein abgestimmte Einbettung.

  2. Feine Einbettungen liefern eine höhere Genauigkeit. Dies führt jedoch zu einer erhöhten Rechenleistung, die zum Trainieren des Netzwerks erforderlich ist. Bei einer ausreichenden Anzahl von Proben könnten wir ebenso gut eine Einbettung von Grund auf lernen. Wir haben festgestellt, dass S/W > 15K von Grund auf ungefähr die gleiche Genauigkeit wie mit fein abgestimmter Einbettung hat.

Wir haben verschiedene Sequenzmodelle wie CNN, sepCNN, RNN (LSTM & GRU), CNN-RNN und gestapelte RNN verglichen, die die Modellarchitekturen variieren. Wir haben festgestellt, dass sepCNNs, eine Convolutional-Netzwerkvariante, die oft dateneffizienter und recheneffizienter ist, eine bessere Leistung als die anderen Modelle bieten.

Mit dem folgenden Code wird ein sepCNN-Modell mit vier Ebenen erstellt:

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

Modell trainieren

Nachdem Sie nun die Modellarchitektur erstellt haben, müssen Sie das Modell trainieren. Das Training umfasst eine Vorhersage anhand des aktuellen Status des Modells, die Berechnung der Genauigkeit der Vorhersage und die Aktualisierung der Gewichtungen oder Parameter des Netzwerks, um diesen Fehler zu minimieren und die Modellvorhersage zu verbessern. Wir wiederholen diesen Vorgang, bis das Modell angelaufen ist und nicht mehr lernen kann. Für diesen Prozess müssen drei wichtige Parameter ausgewählt werden (siehe Tabelle 2).

  • Messwert: So messen Sie die Leistung des Modells mit einem Messwert. In unseren Tests haben wir als Messwert accuracy verwendet.
  • Verlustfunktion: Eine Funktion, mit der ein Verlustwert berechnet wird, den der Trainingsprozess dann durch Anpassen der Netzwerkgewichtungen minimiert. Bei Klassifizierungsproblemen funktioniert der Kreuzentropieverlust gut.
  • Optimierungstool: Eine Funktion, mit der bestimmt wird, wie die Netzwerkgewichtungen basierend auf der Ausgabe der Verlustfunktion aktualisiert werden. Wir haben in unseren Tests das beliebte Adam-Optimierungstool verwendet.

In Keras können wir diese Lernparameter mit der Methode kompilieren an ein Modell übergeben.

Lernparameter Wert
Messwert Genauigkeit
Verlustfunktion – binäre Klassifizierung binäre Kreuzentropie
Verlustfunktion – Klassifizierung mehrerer Klassen sparse_categorical_crossentropy
Optimierung Adam

Tabelle 2: Lernparameter

Das eigentliche Training erfolgt mit der fit-Methode. Abhängig von der Größe Ihres Datasets ist dies die Methode, mit der die meisten Computing-Zyklen verwendet werden. In jeder Trainingsausführung werden zum Berechnen des Verlusts batch_size Stichproben aus Ihren Trainingsdaten verwendet und die Gewichtungen anhand dieses Werts einmal aktualisiert. Sobald das Modell das gesamte Trainings-Dataset gesehen hat, wird eine epoch abgeschlossen. Am Ende jeder Epoche verwenden wir das Validierungs-Dataset, um zu bewerten, wie gut das Modell lernt. Wir wiederholen das Training mit dem Dataset für eine vorab festgelegte Anzahl von Epochen. Wir können dies optimieren, indem wir frühzeitig beenden, wenn sich die Validierungsgenauigkeit zwischen aufeinanderfolgenden Epochen stabilisiert, was zeigt, dass das Modell nicht mehr trainiert wird.

Hyperparameter für Training Wert
Lernrate 1e-3
Epochen 1.000
Batchgröße 512
Vorzeitiges Beenden Parameter: val_loss, Geduld: 1

Tabelle 3: Hyperparameter trainieren

Mit dem folgenden Keras-Code wird der Trainingsprozess mit den in den obigen Tabellen 2 und 3 ausgewählten Parametern implementiert:

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]

Codebeispiele zum Trainieren des Sequenzmodells finden Sie hier.