Krok 4. Tworzenie, trenowanie i ocena modelu

W tej sekcji zajmiemy się utworzeniem, trenowaniem i oceną naszego modelu. W kroku 3 wybraliśmy model n-gram lub model sekwencyjny ze współczynnikiem S/W. Teraz czas napisać i wytrenować nasz algorytm klasyfikacji. Wykorzystamy do tego TensorFlow i tf.keras API.

Tworzenie modeli systemów uczących się przy użyciu Keras polega na łączeniu warstw i przetwarzaniu danych elementów składowych, podobnie jak przy konstruowaniu klocków z klocków Lego. Te warstwy pozwalają nam określić sekwencję przekształceń, które mają zostać wykonane na danych wejściowych. Nasz algorytm uczenia się pobiera pojedynczy tekst i generuje jedną klasyfikację, dlatego możemy utworzyć liniowy stos warstw za pomocą interfejsu API modelu sekwencyjnego.

Liniowy stos warstw

Rys. 9. Liniowy stos warstw

Warstwa wejściowa i warstwy pośrednie będą skonstruowane w różny sposób w zależności od tego, czy tworzymy model n-gram czy sekwencyjny. Niezależnie od typu modelu ostatnia warstwa będzie jednakowa w przypadku danego problemu.

Tworzenie ostatniej warstwy

Gdy mamy tylko 2 klasy (klasyfikacja binarna), nasz model powinien zwrócić jeden wynik prawdopodobieństwa. Na przykład wynik 0.2 dla danej próbki wejściowej oznacza „20% pewności, że próbka należy do pierwszej klasy (klasa 1), a 80%, że znajduje się w drugiej klasie (klasa 0). Aby wygenerować taki wynik prawdopodobieństwa, funkcja aktywacji ostatniej warstwy powinna być funkcją sigmoidalną, a funkcją straty używaną do trenowania modelu krzyżowego. (zobacz Rys. 10 po lewej stronie).

Jeśli istnieje więcej niż 2 klasy (klasyfikacja wieloklasowa), nasz model powinien zwrócić 1 wynik prawdopodobieństwa na każdą z nich. Suma tych wyników powinna wynosić 1. Na przykład wynik {0: 0.2, 1: 0.7, 2: 0.1} oznacza „20% pewności, że ta próbka znajduje się w klasie 0, 70% że jest w klasie 1 i 10%, że jest w klasie 2”. Aby wyświetlić te wyniki, funkcja aktywacji ostatniej warstwy powinna być funkcją softmax, a funkcja utraty używana do trenowania modelu powinna być kategoryczną entropią krzyżową. (zobacz Rys. 10 po prawej stronie).

Ostatnia warstwa

Rysunek 10. Ostatnia warstwa

Ten kod definiuje funkcję, która pobiera odpowiednią liczbę klas jako dane wejściowe i zwraca odpowiednią liczbę jednostek warstwy (1 jednostka do klasyfikacji binarnej; w przeciwnym razie 1 jednostka na każdą klasę) i odpowiednią funkcję aktywacyjną:

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

W 2 kolejnych sekcjach opisujemy proces tworzenia pozostałych warstw modelu dla modeli n-gramów i modeli sekwencji.

Jeśli współczynnik S/W jest mały, zauważyliśmy, że modele n-gram są skuteczniejsze niż modele sekwencyjne. Modele sekwencyjne działają lepiej przy dużej liczbie małych, gęstych wektorów. Wynika to z faktu, że relacje osadzone są nauczane w gęstej przestrzeni i najlepiej sprawdza się w przypadku wielu próbek.

Tworzenie modelu n-gram [opcja A]

Nazywamy modele, które przetwarzają tokeny niezależnie (bez uwzględniania kolejności słów) jako modele n-gram. Do tej kategorii zaliczają się proste wielowarstwowe perceptrony (w tym maszyny wzmacniające gradienty oraz maszyny wektorowe z regresją logistyczną) i nie mogą korzystać z żadnych informacji o kolejności tekstu.

Porównaliśmy wydajność niektórych z wymienionych wyżej modeli n-gramów i zauważyliśmy, że perceptrony wielowarstwowe (MLP) są zwykle skuteczniejsze niż inne opcje. Modele MLP są proste do zdefiniowania i zrozumienia, zapewniają dużą dokładność i wymagają stosunkowo małej ilości obliczeń.

W poniższym kodzie zdefiniowano dwuwarstwowy model MLP w tf.keras, dodając kilka warstw porzucenia na potrzeby regularyzacji, aby zapobiec nadmiernym dopasowaniu do próbek treningowych.

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

Tworzenie modelu sekwencji [opcja B]

Modele, które mogą uczyć się na podstawie sąsiednich tokenów, to modele sekwencyjne. Obejmuje to klasy modeli CNN i RNN. Dane są w nich wstępnie przetwarzane jako wektory sekwencji.

Modele sekwencyjne zwykle mają większą liczbę parametrów, które trzeba się nauczyć. Pierwsza warstwa w tych modelach to warstwa wektorowa, która uczy się relacji między słowami w gęstej przestrzeni wektorowej. Uczenie się relacji między słowami działa najlepiej w przypadku wielu przykładów.

Słowa w danym zbiorze danych prawdopodobnie nie są w nim unikalne. Dzięki temu możemy poznać zależności między słowami w naszym zbiorze danych, korzystając z innych zbiorów danych. Aby to zrobić, możemy przenieść wektor dystrybucyjny uzyskany z innego zbioru danych do warstwy umieszczania. Takie wektory dystrybucyjne są nazywane wstępnie wytrenowanymi wektorami dystrybucyjnymi. Użycie wytrenowanego wektora dystrybucyjnego daje modelowi przewagę w procesie nauki.

Dostępne są już wytrenowane wektory dystrybucyjne wytrenowane przy użyciu dużych korpusów, takich jak GloVe. Serwis GloVe został trenowany na wielu korpusach (przede wszystkim w Wikipedii). Przetestowaliśmy nasze modele sekwencji z wykorzystaniem wersji wektorów dystrybucyjnych GloVe i zaobserwowaliśmy, że jeśli zamrożemy wagi wstępnie wytrenowanych wektorów i wytrenujemy tylko pozostałą część sieci, modele nie będą skuteczne. Może to być spowodowane tym, że kontekst, w którym trenowano warstwę wektora dystrybucyjnego, mógł się różnić od kontekstu, w którym jej używamy.

Wektory dystrybucyjne GloVe wytrenowane na podstawie danych z Wikipedii mogą nie pasować do wzorców językowych w zbiorze danych IMDb. Wywnioskowane relacje mogą wymagać aktualizacji, tj. wagi wektora dystrybucyjnego mogą wymagać dostosowania kontekstowego. Robimy to w 2 etapach:

  1. Przy pierwszym uruchomieniu przy zablokowanym wadze warstwy wektora dystrybucyjnego pozwalamy na naukę reszty sieci. Po zakończeniu tego uruchomienia wagi modelu osiągają stan znacznie lepszy niż ich niezainicjowane wartości. Drugie uruchomienie pozwala na uczenie się warstwy wektora dystrybucyjnej, wprowadzając drobne poprawki we wszystkich wagach w sieci. Ten proces określamy jako użycie dostrojonego wektora dystrybucyjnego.

  2. Dostrojone wektory dystrybucyjne zapewniają większą dokładność. Wiąże się to jednak ze zwiększoną mocą obliczeniową wymaganą do trenowania sieci. Mając wystarczającą liczbę próbek, moglibyśmy równie dobrze nauczyć się umieszczania elementów od zera. Zauważyliśmy, że w przypadku funkcji S/W > 15K rozpoczęcie od zera daje mniej więcej taką samą dokładność jak w przypadku dostrojonego wektora dystrybucyjnego.

Porównaliśmy różne modele sekwencji, takie jak CNN, sepCNN, RNN (LSTM i GRU), CNN-RNN, i skumulowane RNN, w różnym stopniu. Odkryliśmy, że sepCNN, czyli splotowy wariant sieci, który często ma większą wydajność w zakresie przetwarzania danych i przetwarzania danych, jest skuteczniejszy niż inne modele.

Ten kod tworzy czterowarstwowy model 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

Wytrenuj model

Po skonstruowaniu architektury modelu musimy go wytrenować. Trenowanie obejmuje sporządzenie prognozy na podstawie bieżącego stanu modelu, obliczenie, jaka jest nieprawidłowa, oraz zaktualizowanie wagi lub parametrów sieci w celu zminimalizowania tego błędu i ulepszenia prognozy modelu. Powtarzamy ten proces, dopóki nasz model się nie ustabilizuje i nie będzie mógł się uczyć. W przypadku tego procesu należy wybrać 3 kluczowe parametry (patrz tabela 2).

  • Dane: jak mierzyć wydajność naszego modelu za pomocą danych. W naszych eksperymentach użyliśmy dokładności.
  • Funkcja utraty: funkcja służąca do obliczania wartości utraty, którą proces trenowania próbuje następnie zminimalizować, dostrajając wagi sieci. W przypadku problemów z klasyfikacją dobrze sprawdza się utrata entropii krzyżowej.
  • Optymalizujący: funkcja, która decyduje o sposobie aktualizowania wag sieci na podstawie danych wyjściowych funkcji utraty. W eksperymentach użyliśmy popularnego optymalizatora Adam.

W Keras można przekazać te parametry uczenia się do modelu za pomocą metody compile.

Tabela 2. Parametry uczenia się

Parametr szkoleniowy Wartość
Wskaźnik dokładność
Funkcja utraty - klasyfikacja binarna binary_crossentropy
Funkcja utraty – klasyfikacja wieloklasowa sparse_categorical_crossentropy
Optymalizator adam

Faktyczne trenowanie odbywa się za pomocą metody fit. W zależności od rozmiaru zbioru danych jest używana ta metoda, w której zostanie wykorzystana większość cykli obliczeniowych. W każdej iteracji treningowej batch_size próbek z danych treningowych jest używana do obliczania straty, a wagi są aktualizowane raz na podstawie tej wartości. Proces trenowania kończy zadanie epoch, gdy model zobaczy cały zbiór danych treningowych. Na końcu każdej epoki używamy zbioru danych do weryfikacji, aby ocenić, jak dobrze model się uczy. Na podstawie zbioru danych powtarzamy trenowanie dla określonej z góry liczby epok. Możemy zoptymalizować ten proces, zatrzymując go wcześnie, gdy dokładność weryfikacji ustabilizuje się między kolejnymi okresami, co pokazuje, że model już się nie trenuje.

Hiperparametr treningowy Wartość
Tempo uczenia się 1–3
Epoki 1000
Wielkość wsadu 512
Wczesne zatrzymanie parametr: val_loss, cierpliwość: 1

Tabela 3. Trenowanie hiperparametrów

Poniższy kod Keras implementuje proces trenowania z użyciem parametrów wybranych w tabelach 2 i 3 powyżej:

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]

Przykłady kodu do trenowania modelu sekwencji znajdziesz tutaj.