Schritt 4: Modell erstellen, trainieren und bewerten

In diesem Abschnitt beschäftigen wir uns mit der Erstellung, dem Training und der Bewertung unseres Modells. In Schritt 3 haben wir uns entweder für ein N-Gramm-Modell oder ein Sequenzmodell mit unserem S/W-Verhältnis entschieden. Jetzt ist es an der Zeit, unseren Klassifizierungsalgorithmus zu schreiben und zu trainieren. Dazu verwenden wir TensorFlow mit der tf.keras API.

Bei der Erstellung von Modellen für maschinelles Lernen mit Keras geht es darum, Schichten und Bausteine der Datenverarbeitung zusammenzuführen, ähnlich wie beim Zusammensetzen von Lego-Steinen. Mithilfe dieser Schichten können wir die Abfolge der Transformationen angeben, die wir für unsere Eingabe ausführen möchten. Da unser Lernalgorithmus eine einzelne Texteingabe annimmt und eine einzelne Klassifizierung ausgibt, können wir mit der Sequential Model API einen linearen Stapel von Ebenen erstellen.

Linearer Ebenenstapel

Abbildung 9: Linearer Ebenenstapel

Die Eingabe- und die Zwischenschicht sind unterschiedlich aufgebaut, je nachdem, ob wir ein N-Gramm- oder ein Sequenzmodell erstellen. Unabhängig vom Modelltyp ist die letzte Ebene für ein bestimmtes Problem jedoch gleich.

Letzte Ebene konstruieren

Wenn wir nur zwei Klassen haben (binäre Klassifizierung), sollte unser Modell einen einzigen Wahrscheinlichkeitswert ausgeben. Die Ausgabe von 0.2 für ein bestimmtes Eingabebeispiel bedeutet beispielsweise: „20% Wahrscheinlichkeit, dass dieses Beispiel in der ersten Klasse (Klasse 1) ist, und 80 %, dass es in der zweiten Klasse (Klasse 0) ist“. Für die Ausgabe eines solchen Wahrscheinlichkeitswerts sollte die Aktivierungsfunktion der letzten Schicht eine Sigmoidfunktion und die Verlustfunktion zum Trainieren des Modells sein. (siehe Abbildung 10, links).

Wenn mehr als zwei Klassen vorhanden sind (Klassifizierung mit mehreren Klassen), sollte unser Modell einen Wahrscheinlichkeitswert pro Klasse ausgeben. Die Summe dieser Punktzahlen sollte 1 sein. Die Ausgabe von {0: 0.2, 1: 0.7, 2: 0.1} bedeutet beispielsweise: „20% Wahrscheinlichkeit, dass diese Stichprobe in Klasse 0 ist, 70 %, dass sie in Klasse 1 ist, und 10 %, dass sie in Klasse 2 ist.“ Für die Ausgabe dieser Werte sollte die Aktivierungsfunktion der letzten Schicht Softmax und die zum Trainieren des Modells verwendete Verlustfunktion eine kategoriale Kreuzentropie sein. (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 Aktivierungsfunktion ausgibt, und zwar eine entsprechende Anzahl von Ebeneneinheiten (1 Einheit für die binäre Klassifizierung, andernfalls 1 Einheit für jede Klasse):

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-Modelle und Sequenzmodelle beschrieben.

Wenn das S/W-Verhältnis klein ist, haben wir festgestellt, dass N-Gramm-Modelle eine bessere Leistung erzielen als Sequenzmodelle. Sequenzmodelle sind besser, wenn es eine große Anzahl kleiner, dichter Vektoren gibt. Das liegt daran, dass Beziehungen eingebetteter Inhalte in einem dicht bebauten Raum erlernt werden und das am besten bei vielen Stichproben funktioniert.

N-Gramm-Modell erstellen [Option A]

Wir bezeichnen Modelle, die die Tokens unabhängig verarbeiten und die Wortreihenfolge nicht berücksichtigen, als N-Gramm-Modelle. Einfache mehrschichtige Perceptrons, einschließlich logistischer Regressions, Gradient-Boosting-Maschinen und Unterstützungsvektormodelle, fallen alle in diese Kategorie. Sie können keine Informationen zur Textsortierung verwenden.

Wir haben die Leistung einiger der oben genannten N-Gramm-Modelle verglichen und festgestellt, dass Multi-Layer-Perceptrons (MLPs) in der Regel eine bessere Leistung als andere Optionen erzielen. MLPs sind einfach zu definieren und zu verstehen, bieten eine gute Genauigkeit und erfordern relativ wenig Berechnung.

Der folgende Code definiert ein zweischichtiges MLP-Modell in tf.keras, wobei einige Dropout-Ebenen zur Regularisierung hinzugefügt werden, um eine Überanpassung an Trainingsbeispiele zu vermeiden.

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

Sequenzmodell erstellen [Option B]

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

Sequenzmodelle haben in der Regel eine größere Anzahl von Parametern, die erlernt werden müssen. Die erste Schicht in diesen Modellen ist eine Einbettungsschicht, die die Beziehung zwischen den Wörtern in einem dichten Vektorraum erlernt. Das Erlernen von Wortbeziehungen funktioniert am besten über viele Beispiele hinweg.

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 ermitteln. Dazu übertragen wir eine aus einem anderen Dataset erlernte Einbettung in unsere Einbettungsebene. Diese Einbettungen werden als vortrainierte Einbettungen bezeichnet. Die Verwendung einer vortrainierten Einbettung verleiht dem Modell einen Vorsprung im Lernprozess.

Es sind vortrainierte Einbettungen verfügbar, die mit großen Korpora wie GloVe trainiert wurden. GloVe wurde mit mehreren Korpora (hauptsächlich Wikipedia) trainiert. Wir haben das Training unserer Sequenzmodelle mit einer Version von GloVe-Einbettungen getestet und festgestellt, dass die Modelle nicht gut funktionieren, wenn wir die Gewichtungen der vortrainierten Einbettungen einfrieren und nur den Rest des Netzwerks trainieren. Das könnte daran liegen, dass sich der Kontext, in dem die Einbettungsebene trainiert wurde, von dem Kontext abweicht, 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 erfordern möglicherweise eine kontextabhängige Feinabstimmung. Dies geschieht in zwei Phasen:

  1. Wenn die Gewichtungen der Einbettungsschicht beim ersten Durchlauf fixiert waren, kann der Rest des Netzwerks lernen. Am Ende dieser Ausführung erreichen die Modellgewichtungen einen Zustand, der viel besser als ihre nicht initialisierten Werte ist. Beim zweiten Durchlauf kann auch die Einbettungsebene lernen und dann Feinabstimmungen an allen Gewichtungen im Netzwerk vornehmen. Wir bezeichnen diesen Prozess als eine abgestimmte Einbettung.

  2. Detaillierte Einbettungen erzielen eine höhere Genauigkeit. Dies geht jedoch auf Kosten der höheren Rechenleistung hinaus, die zum Trainieren des Netzwerks erforderlich ist. Angesichts einer ausreichenden Anzahl von Beispielen wäre es genauso gut, eine Einbettung von Grund auf neu zu erlernen. Wir haben festgestellt, dass bei S/W > 15K ein Start von vorne annähernd dieselbe Genauigkeit erzielt wie die Verwendung einer fein abgestimmten Einbettung.

Wir haben verschiedene Sequenzmodelle wie CNN, sepCNN, RNN (LSTM & GRU), CNN-RNN und gestapeltes RNN verglichen und dabei die Modellarchitekturen variiert. Wir haben festgestellt, dass sepCNNs, eine Faltungsnetzwerkvariante, die häufig dateneffizienter und recheneffizienter ist, eine bessere Leistung als die anderen Modelle erzielen.

Mit dem folgenden Code wird ein sepCNN-Modell mit vier Schichten 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 die Modellarchitektur erstellt haben, müssen Sie das Modell trainieren. Beim Training wird eine Vorhersage auf der Grundlage des aktuellen Zustands des Modells getroffen, die Fehlerwahrscheinlichkeit der Vorhersage berechnet und die Gewichtungen oder Parameter des Netzwerks aktualisiert, um diesen Fehler zu minimieren und die Modellvorhersage zu verbessern. Wir wiederholen diesen Vorgang, bis unser Modell konvergiert ist und nicht mehr lernen kann. Für diesen Vorgang müssen drei wichtige Parameter ausgewählt werden (siehe Tabelle 2).

  • Messwert: Wie Sie die Leistung unseres Modells mithilfe eines Messwerts messen. Wir haben in unseren Tests Genauigkeit als Messwert verwendet.
  • Verlustfunktion: Eine Funktion, mit der ein Verlustwert berechnet wird, den der Trainingsprozess dann durch die Feinabstimmung der Netzwerkgewichtungen zu minimieren versucht. Für Klassifizierungsprobleme eignet sich der Kreuzentropieverlust gut.
  • Optimierungstool: Eine Funktion, die entscheidet, wie die Netzwerkgewichtungen basierend auf der Ausgabe der Verlustfunktion aktualisiert werden. Wir haben in unseren Tests das beliebte Optimierungstool Adam verwendet.

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

Tabelle 2: Lernparameter

Lernparameter Wert
Messwert Genauigkeit
Verlustfunktion – binäre Klassifizierung binary_crossentropy
Verlustfunktion – Klassifizierung mit mehreren Klassifizierungen sparse_categorical_crossentropy
Optimierung adam

Das eigentliche Training erfolgt mit der fit-Methode. Je nach Größe Ihres Datasets ist dies die Methode, für die die meisten Rechenzyklen ausgegeben werden. Bei jedem Trainingsdurchlauf wird zur Berechnung des Verlusts eine batch_size-Anzahl von Stichproben aus Ihren Trainingsdaten verwendet. Die Gewichtungen werden dann einmal anhand dieses Werts aktualisiert. Der Trainingsprozess führt einen epoch durch, sobald das Modell das gesamte Trainings-Dataset gesehen hat. Am Ende jeder Epoche wird anhand des Validierungs-Datasets ermittelt, wie gut das Modell lernt. Wir wiederholen das Training mit dem Dataset für eine vorgegebene Anzahl von Epochen. Wir können dies optimieren, indem wir den Vorgang frühzeitig beenden, wenn sich die Validierungsgenauigkeit zwischen aufeinanderfolgenden Epochen stabilisiert, was zeigt, dass das Modell nicht mehr trainiert wird.

Trainings-Hyperparameter Wert
Lernrate 1e–3
Epochen 1000
Batchgröße 512
Vorzeitiges Beenden Parameter: val_loss, Geduld: 1

Tabelle 3: Hyperparameter trainieren

Mit dem folgenden Keras-Code wird der Trainingsprozess mithilfe der in den Tabellen 2 und 3 ausgewählten Parameter 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.