Passaggio 4: crea, addestra e valuta il modello

In questa sezione ci impegneremo per creare, addestrare e valutare il nostro modello. Nel Passaggio 3, scegliamo di utilizzare un modello n-gram o un modello di sequenza, utilizzando il nostro rapporto S/W. Ora è il momento di scrivere il nostro algoritmo di classificazione e formarlo. A questo scopo, utilizzeremo TensorFlow con l'API tf.keras.

La creazione di modelli di machine learning con Keras è incentrata sull'assemblaggio di livelli, componenti di base per l'elaborazione dei dati, come per l'assemblaggio di mattoni Lego. Questi livelli ci consentono di specificare la sequenza di trasformazioni che vogliamo eseguire sulla base del nostro contributo. Poiché il nostro algoritmo di apprendimento acquisisce un singolo input di testo e genera una singola classificazione, possiamo creare uno stack lineare di livelli utilizzando l'API Modello sequenziale.

Stack lineare di livelli

Figura 9: stack lineare di livelli

Il livello di input e i livelli intermedi verranno creati in modo diverso, a seconda che stiamo creando un modello n-gram o un modello di sequenza. Indipendentemente dal tipo di modello, l'ultimo livello sarà sempre lo stesso per un determinato problema.

Creazione dell'ultimo livello

Quando abbiamo solo 2 classi (classificazione binaria), il nostro modello dovrebbe generare un singolo punteggio di probabilità. Ad esempio, l'output di 0.2 per un dato campione di input significa "20% di certezza che questo campione è nella prima classe (classe 1), 80% che è nella seconda classe (classe 0"). Per ottenere un punteggio di probabilità di questo tipo, la funzione di attivazione dell'ultimo livello dovrebbe essere una funzione sigmoida e la funzione di perdita utilizzata per addestrare il modello 1.

Se ci sono più di due classi (classificazione multi-classe), il nostro modello dovrebbe restituire un punteggio di probabilità per classe. La somma di questi punteggi dovrebbe essere 1. Ad esempio, generare {0: 0.2, 1: 0.7, 2: 0.1} significa "20% di certezza che questo campione è in classe 0, 70% che è in classe 1 e 10% che è in classe 2". Per ottenere questi punteggi, la funzione di attivazione dell'ultimo strato deve essere softmax e la funzione di perdita utilizzata per addestrare il modello dovrebbe essere interentropia categoriale. (vedi la Figura 10 a destra).

Ultimo livello

Figura 10: ultimo livello

Il codice seguente definisce una funzione che accetta il numero di classi come input e restituisce il numero appropriato di unità di livello (1 unità per la classificazione binaria; altrimenti 1 unità per ogni classe) e la funzione di attivazione appropriata:

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

Le due sezioni seguenti illustrano la creazione dei restanti livelli modello per i modelli n-gram e per i modelli di sequenza.

Quando il rapporto S/W è piccolo, abbiamo riscontrato che i modelli n-gram hanno un rendimento migliore rispetto ai modelli sequenza. I modelli di sequenza sono migliori quando esiste un numero elevato di vettori piccoli e densi. Questo perché le relazioni di incorporamento vengono apprese in uno spazio denso e questo accade meglio su molti esempi.

Modello n-gram build [Opzione A]

I modelli che elaborano i token in modo indipendente (senza tenere conto dell'ordine delle parole) vengono definiti modelli n-gram. Semplici perceptron multistrato (incluse le regressioni logistiche), grandi macchine per il gradiente e supporta i modelli di macchine virtuali rientrano in questa categoria; non possono utilizzare nessuna informazione sull'ordinamento dei testi.

Abbiamo confrontato le prestazioni di alcuni dei modelli n-gram sopra menzionati e abbiamo osservato che i percettori multilivello (MLP) in genere hanno un rendimento migliore rispetto ad altre opzioni. I machine learning sono semplici da definire e comprendere, offrono una buona precisione e richiedono un calcolo relativamente ridotto.

Il codice seguente definisce un modello MLP a due livelli in tf.keras, aggiungendo un paio di livelli di abbandono per la regolarizzazione (per evitare l'overfitting dei campioni di addestramento).

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

Modello sequenza di build [Opzione B]

Facciamo riferimento ai modelli che possono apprendere dall'adiacenza dei token come modelli di sequenza. Sono incluse le classi di modelli per CNN e RNN. I dati vengono pre-elaborati come vettori di sequenza per questi modelli.

In genere i modelli di sequenza hanno un numero maggiore di parametri da apprendere. Il primo livello in questi modelli è un livello di incorporamento, che apprende la relazione tra le parole in uno spazio vettoriale denso. Imparare le relazioni di parola funziona meglio.

È molto probabile che le parole in un determinato set di dati non siano univoche per il set di dati. Possiamo quindi imparare la relazione tra le parole nel nostro set di dati utilizzando altri set di dati. Per fare ciò, possiamo trasferire un incorporamento appreso da un altro set di dati nel nostro livello di incorporamento. Questi incorporamenti sono denominati incorporamenti preaddestrati. L'utilizzo di un incorporamento preaddestrato offre al modello un vantaggio iniziale nel processo di apprendimento.

Sono disponibili incorporamenti preaddestrati che sono stati addestrati utilizzando grandi corpora, come GloVe. GloVe ha completato un corso di formazione su più corpora (principalmente Wikipedia). Abbiamo testato l'addestramento dei nostri modelli di sequenza utilizzando una versione degli incorporamenti GloVe e abbiamo osservato che, se abbiamo congelato le ponderazioni degli incorporamenti preaddestrati e abbiamo addestrato solo il resto della rete, i modelli non hanno avuto un buon rendimento. Ciò potrebbe essere dovuto al fatto che il contesto in cui è stato addestrato il livello di incorporamento potrebbe essere stato diverso dal contesto in cui lo utilizzavamo.

Gli incorporamenti GloVe addestrati sui dati di Wikipedia potrebbero non corrispondere ai pattern linguistici del nostro set di dati IMDb. Le relazioni dedotte potrebbero richiedere un aggiornamento, ovvero le ponderazioni di incorporamento potrebbero richiedere l'ottimizzazione contestuale. Per farlo:

  1. Nella prima esecuzione, con le ponderazioni del livello di incorporamento bloccate, consentiamo al resto della rete di apprendere. Alla fine di questa esecuzione, le ponderazioni del modello raggiungono uno stato migliore dei loro valori non inizializzati. Per la seconda esecuzione, permettiamo di imparare anche il livello di incorporamento, apportando piccole modifiche a tutte le ponderazioni nella rete. Definiamo questo processo come l'utilizzo di un incorporamento ottimizzato.

  2. Gli incorporamenti ottimizzati ottimizzano la precisione. Tuttavia, si tratta di un aumento della potenza di calcolo richiesta per addestrare la rete. Dato un numero sufficiente di esempi, potremmo fare lo stesso con l'apprendimento anche dell'incorporamento da zero. Abbiamo osservato che per S/W > 15K, la riprogettazione da zero garantisce in modo efficace la stessa precisione dell'utilizzo dell'incorporamento ottimizzato.

Abbiamo confrontato diversi modelli di sequenza, come CNN, sepCNN, RNN (LSTM & GRU), CNN-RNN e RNN impilati, variando le architetture dei modelli. Abbiamo scoperto che i sepCNN, una variante di rete convoluzionale, spesso più efficiente in termini di dati e di calcolo, hanno un rendimento migliore rispetto agli altri modelli.

Il seguente codice crea un modello sepCNN a quattro livelli:

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

Addestra il tuo modello

Ora che abbiamo costruito l'architettura del modello, dobbiamo addestrare il modello. L'addestramento prevede che si faccia una previsione basata sullo stato attuale del modello, calcolando il grado di errore della previsione e aggiornando le ponderazioni o i parametri della rete per ridurre al minimo questo errore e migliorare la previsione del modello. Ripetiamo questa procedura finché il nostro modello non converge e non potrà più apprendere. Esistono tre parametri chiave tra cui scegliere per questo processo (vedi Tabella 2).

  • Metrica: come misurare le prestazioni del nostro modello utilizzando una metrica. Abbiamo usato accuratezza come metrica nei nostri esperimenti.
  • Funzione di perdita: una funzione utilizzata per calcolare un valore di perdita che il processo di addestramento cerca di ridurre al minimo ottimizzando i pesi della rete. Per i problemi di classificazione, la perdita interentropia funziona bene.
  • Ottimizzatore: una funzione che determina la modalità di aggiornamento delle ponderazioni di rete in base all'output della funzione di perdita. Negli esperimenti abbiamo utilizzato il popolare strumento di ottimizzazione Adam.

In Keras, possiamo trasmettere questi parametri di apprendimento a un modello utilizzando il metodo di compilazione.

Parametro di apprendimento Valore
Metrica accuratezza
Perdita funzione - classificazione binaria binari_crossentropy
Perdita funzione - classificazione con più classi sparse_categoryical_crossentropy
Ottimizzatore amam

Tabella 2: parametri di apprendimento

L'addestramento effettivo viene eseguito utilizzando il metodo fit. A seconda delle dimensioni del set di dati, questo è il metodo in cui verrà spesa la maggior parte dei cicli di computing. In ogni iterazione dell'addestramento, viene utilizzato un numero di campioni dei dati di addestramento batch_size per calcolare la perdita, e le ponderazioni vengono aggiornate una volta, in base a questo valore. Il processo di addestramento completa un epoch dopo che il modello ha rilevato l'intero set di dati di addestramento. Alla fine di ogni periodo, utilizziamo il set di dati di convalida per valutare l'efficacia del modello. Ripetiamo l'addestramento utilizzando il set di dati per un numero predeterminato di periodi. Possiamo ottimizzarlo interrompendo in anticipo il momento in cui l'accuratezza della convalida si stabilizza tra i periodi consecutivi, a indicare che il modello non è più in fase di addestramento.

Iperparametro di addestramento Valore
Tasso di apprendimento 1e-3
Epoche 1000
Dimensione batch 512
Interruzione anticipata parametro: val_loss, pazienza: 1

Tabella 3: iperparametri di addestramento

Il seguente codice Keras implementa il processo di addestramento utilizzando i parametri scelti nelle Tabelle 2 e 3 sopra:

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]

Esempi di codici per l'addestramento del modello di sequenza sono disponibili qui.