Krok 4. Tworzenie, trenowanie i ocena modelu

W tej sekcji postaramy się stworzyć, wytrenować i ocenić nasz model. W kroku 3 wybierzmy model n-gramowy lub sekwencyjny ze współczynnikiem S/W. Czas napisać nasz algorytm klasyfikacji i wytrenować interfejs. Do tego celu użyjemy interfejsu TensorFlow i interfejsu tf.keras.

Tworzenie modeli systemów uczących się w Keras polega na składaniu warstw, czyli blokach przetwarzania danych, tak samo jak w przypadku klocków Lego. Te warstwy umożliwiają określenie sekwencji przekształceń, które mają być stosowane przez nas. Gdy nasz algorytm nauczania wbierze pojedynczy tekst i określa pojedynczą klasyfikację, możemy utworzyć liniowy układ warstw za pomocą interfejsu API modelu sekwencyjnego.

Liniowy układ warstw

Rysunek 9. Liniowy stos warstw

Warstwa wejściowa i warstwy pośrednie będą konstruowane w różny sposób – zależnie od tego, czy tworzymy model n-gram czy sekwencja. Niezależnie od typu modelu ostatnia warstwa dla danego problemu będzie taka sama.

Tworzenie ostatniej warstwy

Jeśli mamy tylko 2 klasy (klasyfikacja binarna), nasz model powinien podawać jeden wynik prawdopodobieństwa. Na przykład wynik 0.2 w przypadku danej próbki wejściowej oznacza „20% ufności, że ta próbka jest w pierwszej klasie (klasa 1), 80% w drugiej klasie (klasa 0)”. Aby uzyskać wynik punktowy, funkcja aktywacji ostatniej warstwy powinna być funkcją sigmoidową, a funkcja funkcji straty służy do trenowania modelu1.

Jeśli istnieją więcej niż 2 klasy (klasyfikacja wieloklasowa), nasz model powinien podawać jeden wynik prawdopodobieństwa na klasę. Suma tych wyników powinna wynosić 1. Na przykład wynik {0: 0.2, 1: 0.7, 2: 0.1} oznacza „20% ufności, że ta próbka jest w klasie 0, 70% w klasie 1 i 10% w klasie 2”. Aby uzyskać wyniki, funkcja aktywacji ostatniej warstwy powinna być metodą miękką, a funkcja straty wykorzystywana do trenowania modelu powinna być kategoryczna entropia kategoryzacyjna. (zobacz Rysunek 10, prawy).

Ostatnia warstwa

Rysunek 10. Ostatnia warstwa

Poniższy kod definiuje funkcję, która przyjmuje liczbę klas jako dane wejściowe, i zwraca odpowiednią liczbę jednostek warstw (1 jednostkę na potrzeby klasyfikacji binarnej, w przeciwnym razie 1 na każdą klasę) i odpowiednią funkcję aktywacji:

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 kolejnych 2 sekcjach omówiono tworzenie pozostałych warstw modeli dla modeli n g i modeli sekwencji.

Gdy współczynnik S/W jest mały, odkryliśmy, że modele n-gramów są skuteczniejsze niż modele sekwencyjne. Modele sekwencji są lepsze, gdy występuje duża liczba małych, gęstych wektorów. Dzieje się tak, ponieważ relacje uczą się w gęstej przestrzeni, co sprawdza się najlepiej w przypadku wielu próbek.

Model n-gramów [opcja A]

Modele, które przetwarzają tokeny niezależnie (nie uwzględniają kolejności słów), nazywamy modelami n-gramami. Do tej kategorii należą proste wielowarstwowe percepola (w tym regresja logistyczna), maszyny wznoszące gradienty i obsługujące maszyny wektorowe. Nie mogą też korzystać z informacji o kolejności tekstu.

Porównaliśmy skuteczność niektórych z powyższych modeli n-gramów i zauważyliśmy, że wielowarstwowe perceptery (MLP) mają zazwyczaj lepsze wyniki niż inne opcje. LP-y są łatwe do zdefiniowania i zrozumienia, wymagają dużej dokładności i nie wymagają dużo obliczeń.

Poniższy kod definiuje dwuwarstwowy model MLP w lokalizacji tf.keras, dodając kilka warstw do ujednolicania (aby zapobiec nadmiarowi w przykładach 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

Model sekwencji kompilacji [opcja B]

Modele uczą się od sąsiednich tokenów jako modeli sekwencji. Obejmuje to modele modeli CNN i RNN. Dane te są wstępnie przetwarzane jako wektory sekwencji.

Modele sekwencji mają zwykle większą liczbę parametrów do nauki. Pierwsza warstwa tych modeli to warstwa osadzająca, która uczy się zależności między słowami w gęstej przestrzeni wektorowej. Nauczanie związków słów sprawdza się najlepiej w wielu próbach.

Słowa w danym zbiorze danych prawdopodobnie nie są unikalne dla tego zbioru danych. 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ść umieszczone zasoby z innego zbioru danych do naszej warstwy. Takie umieszczanie są nazywane wytrenowanymi miejscami docelowymi. Korzystanie z już wytrenowanego osadzenia umożliwia modelowi rozpoczęcie procesu nauki.

Dostępne są już wytrenowane umieszczanie, które zostały wytrenowane z wykorzystaniem dużego korpusu, takiego jak GloVe. GloVe przeszkolono z wielu zbiorów (głównie Wikipedii). Przetestowaliśmy nasze modele sekwencjonowania za pomocą wersji osadzonego GloVe i zauważyliśmy, że po zamrożeniu wagi wytrenowanych osadzeń i wytrenowaniu tylko pozostałej sieci modele nie osiągnęły dobrych wyników. Może to być spowodowane tym, że kontekst, w którym trenowano warstwę osadzania, mógł różnić się od kontekstu, w którym go używamy.

Umieszczanie elementów GloVe wytrenowanych na danych z Wikipedii może nie być zgodne ze wzorcem języka w zbiorze danych IMDb. Ustalone powiązania mogą wymagać aktualizacji. Na przykład wagi do umieszczenia mogą wymagać dostrajania kontekstowego. Robimy to na 2 etapach:

  1. Przy pierwszym uruchomieniu, gdy warstwy osadzone są zablokowane, zezwalamy pozostałej sieci na naukę. Na koniec tego modelu wagi osiągają stan, który jest znacznie lepszy niż ich niezainicjowane wartości. Przy drugim uruchomieniu uczymy się też warstwy umieszczonej na stronie, więc możemy ją dostosować do wszystkich wag w sieci. Proces ten nazywamy umieszczaniem w dobrze dostosowanym formacie.

  2. Precyzyjne dostosowywanie osadzeń zwiększa dokładność. Wiąże się to jednak z większą mocą obliczeniową wymaganą do trenowania sieci. Przy wystarczającej liczbie prób udało nam się stworzyć taką samą mapę. Zauważyliśmy, że od S/W > 15K skutecznie rozpoczynasz korzystanie z aplikacji od zera dzięki takiej samej dokładności, jak w przypadku dokładnego osadzania.

Porównaliśmy różne modele sekwencji sekwencji, takie jak CNN, sepCNN, RNN (LSTM & GRU), CNN-RNN i skumulowane RNN, w różnych architekturach modeli. Odkryliśmy, że epepCNNs to awangardowy wariant sieci, który często jest bardziej wydajny i wydajny niż dane, co zapewnia lepszą skuteczność niż pozostałe modele.

Ten kod tworzy 4-warstwowy 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

Trenowanie modelu

Po skompilowaniu architektury modelu musimy go wytrenować. Trenowanie polega na wykonaniu prognozy na podstawie bieżącego stanu modelu, obliczeniu nieprawidłowej prognozy i zaktualizowaniu wag lub parametrów sieci, aby zminimalizować ten błąd i zwiększyć dokładność prognozy. Powtarzamy ten proces, aż nasz model się połączy i nie będzie w stanie się uczyć. W tym procesie musisz wybrać 3 kluczowe parametry (patrz tabela 2).

  • Dane: jak mierzyć skuteczność naszego modelu za pomocą danych. Użyliśmy dokładności jako danych eksperymentalnych.
  • Utracona funkcja: funkcja, która służy do obliczania wartości straty, która jest optymalizowana przez proces trenowania w celu zminimalizowania wagi sieci. W przypadku problemów z klasyfikacją dobrze sprawdza się utrata entropii.
  • Optymalizator: funkcja określająca sposób aktualizowania wag sieci na podstawie danych wyjściowych funkcji utraty. W swoich eksperymentach korzystaliśmy z popularnego optymalizatora Adam.

W Keras można przekazywać te parametry nauczania do modelu, korzystając z metody kompilacji.

Parametr szkoleniowy Wartość
Dane dokładność
Funkcja utraty – klasyfikacja binarna binarna_binarna
Funkcja utraty – klasyfikacja wieloklasowa spadkowa_kategoria_krótka
Optymalizator adam

Tabela 2. Parametry edukacyjne

Faktycznie trenowanie odbywa się metodą fit. W zależności od rozmiaru zbioru danych w ten sposób będzie wykorzystywana większość cykli obliczeniowych. W każdej iteracji trenowania do obliczania wartości utraty używana jest batch_size próbka z danych treningowych, a wagi są aktualizowane raz na podstawie tej wartości. Trenowanie kończy epoch, gdy model widzi cały zbiór danych treningowych. Na końcu każdej epoki używamy zbioru danych do weryfikacji, aby ocenić skuteczność nauki modelu. Powtarzamy trenowanie przy użyciu zbioru danych z góry określonej liczby epoki. Możemy ją optymalizować, zatrzymując się wcześniej, gdy dokładność weryfikacji ustabilizuje się między kolejnymi epokami, co pokazuje, że model nie jest już trenowany.

Hiperparametr Wartość
Wskaźnik uczenia się 1e-3
Epoki 1000
Wielkość wsadu 512
Wczesne zatrzymanie parametr: val_loss, cierpliwość: 1

Tabela 3. Trenowanie hiperparametrów

Ten kod Keras implementuje proces trenowania z wykorzystaniem parametrów wybranych w tabelach 2 i 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]

Przykłady trenowania modelu sekwencji znajdziesz tutaj.