Passaggio 3: prepara i dati

Prima di poter essere inviati a un modello, i dati devono essere trasformati in un formato comprensibile.

Per prima cosa, i campioni di dati che abbiamo raccolto potrebbero trovarsi in un ordine specifico. Non vogliamo che le informazioni associate all'ordine dei campioni influenzino la relazione tra testi ed etichette. Ad esempio, se un set di dati viene ordinato per classe e poi suddiviso in set di addestramento/convalida, questi set non rappresentano la distribuzione complessiva dei dati.

Una semplice best practice per assicurarsi che il modello non sia influenzato dall'ordine dei dati è lo shuffling dei dati sempre prima di fare qualsiasi altra cosa. Se i dati sono già suddivisi in set di addestramento e convalida, assicurati di trasformare i dati di convalida nello stesso modo in cui li trasformi. Se non hai già set di addestramento e convalida separati, puoi suddividere i campioni dopo aver eseguito lo shuffling; è normale utilizzare l'80% dei campioni per l'addestramento e il 20% per la convalida.

In secondo luogo, gli algoritmi di machine learning prendono i numeri come input. Ciò significa che dovremo convertire i testi in vettori numerici. Questa procedura è composta da due passaggi:

  1. Tokenizzazione: dividi i testi in parole o sottotesti più piccoli, in modo da una buona generalizzazione della relazione tra i testi e le etichette. Questo determina il "vocabolario" del set di dati (insieme di token univoci presenti nei dati).

  2. Vettoretizzazione: definisci una buona misura numerica per caratterizzare questi testi.

Vediamo come eseguire questi due passaggi sia per i vettori di n-grammi sia per i vettori di sequenza, nonché come ottimizzare le rappresentazioni vettoriali utilizzando tecniche di selezione delle caratteristiche e normalizzazione.

Vettori di n-grammi [Opzione A]

Nei paragrafi successivi, vedremo come eseguire la tokenizzazione e la vettorizzazione per i modelli n-gram. Vedremo anche come ottimizzare la rappresentazione n-gram utilizzando tecniche di normalizzazione e selezione delle caratteristiche.

In un vettore di n-grammi, il testo è rappresentato come una raccolta di n-grammi univoci: gruppi di n token adiacenti (in genere, parole). Considera il testo The mouse ran up the clock. Qui:

  • La parola unigrammi (n = 1) sono ['the', 'mouse', 'ran', 'up', 'clock'].
  • La parola bigram (n = 2) è ['the mouse', 'mouse ran', 'ran up', 'up the', 'the clock']
  • e così via.

Tokenizzazione

Abbiamo scoperto che la tokenizzazione in unigrammi + bigram fornisce una buona accuratezza e richiede meno tempo di calcolo.

Vettoriazione

Dopo aver suddiviso i campioni di testo in n-grammi, dobbiamo trasformare questi n-grammi in vettori numerici che i nostri modelli di machine learning sono in grado di elaborare. L'esempio seguente mostra gli indici assegnati agli unigrammi e ai bigram generati per due testi.

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'the': 7, 'mouse': 2, 'ran': 4, 'up': 10,
  'clock': 0, 'the mouse': 9, 'mouse ran': 3, 'ran up': 6, 'up the': 11, 'the
clock': 8, 'down': 1, 'ran down': 5}

Una volta assegnati gli indici agli n-grammi, in genere la vettorializziamo utilizzando una delle seguenti opzioni.

Codifica one-hot: ogni testo di esempio è rappresentato come un vettore che indica la presenza o l'assenza di un token nel testo.

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1]

Codifica del conteggio: ogni testo di esempio è rappresentato come un vettore che indica il conteggio di un token nel testo. Tieni presente che l'elemento corrispondente all'unigrammo "the" è ora rappresentato come 2 perché la parola "the" compare due volte nel testo.

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1]

Codifica TF-idf: il problema con i due approcci precedenti è che le parole comuni che si ripetono con frequenze simili in tutti i documenti (ovvero le parole che non sono particolarmente univoche per gli esempi di testo nel set di dati) non vengono penalizzate. Ad esempio, parole come "a" ricorreranno molto spesso in tutti i testi. Pertanto, un conteggio dei token più alto per "il" non è molto utile.

'The mouse ran up the clock' = [0.33, 0, 0.23, 0.23, 0.23, 0, 0.33, 0.47, 0.33, 0.23, 0.33, 0.33]

(vedi TfidfTransformer di Scikit-learn)

Esistono molte altre rappresentazioni vettoriali, ma le tre precedenti sono le più usate.

Abbiamo osservato che la codifica tf-idf è leggermente migliore rispetto alle altre due in termini di accuratezza (in media: 0,25-15% in più) e consigliamo di utilizzare questo metodo per vettore di n-grammi. Tuttavia, tieni presente che occupa più memoria (perché utilizza la rappresentazione in virgola mobile) e richiede più tempo per il calcolo, soprattutto per i set di dati di grandi dimensioni (in alcuni casi può richiedere il doppio del tempo).

Selezione delle caratteristiche

Quando convertiamo tutti i testi di un set di dati in token uni+bigrammi di parole, potremmo avere decine di migliaia di token. Non tutti questi token/funzionalità contribuiscono alla previsione delle etichette. Possiamo quindi rilasciare determinati token, ad esempio quelli che si verificano molto raramente nel set di dati. Possiamo anche misurare l'importanza delle caratteristiche (in che misura ogni token contribuisce alle previsioni delle etichette) e includere solo i token più informativi.

Esistono molte funzioni statistiche che prendono le caratteristiche e le etichette corrispondenti e restituiscono il punteggio di importanza delle caratteristiche. Due funzioni di uso comune sono f_classif e chi2. I nostri esperimenti mostrano che entrambe le funzioni hanno lo stesso rendimento.

Cosa più importante, abbiamo visto che l'accuratezza raggiunge il picco di circa 20.000 caratteristiche per molti set di dati (vedi la Figura 6). L'aggiunta di altre funzionalità al di sopra di questa soglia contribuisce molto a pochissimo e, a volte, porta anche all'overfitting e a peggiorare le prestazioni.

Top-k e precisione

Figura 6: confronto tra funzionalità Top-K e accuratezza. Nei set di dati, si registrano altissimi livelli di accuratezza intorno alle 20.000 caratteristiche principali.

Normalizzazione

La normalizzazione converte tutti i valori delle caratteristiche/dei campioni in valori piccoli e simili. Ciò semplifica la convergenza della discesa del gradiente negli algoritmi di apprendimento. Da quanto abbiamo visto, la normalizzazione durante la pre-elaborazione dei dati non sembra aggiungere molto valore ai problemi di classificazione del testo. Consigliamo di saltare questo passaggio.

Il seguente codice include tutti i passaggi precedenti:

  • tokenizza campioni di testo in uni+bigrammi di parole,
  • Vettorizza utilizzando la codifica tf-idf,
  • Seleziona solo le 20.000 caratteristiche principali dal vettore di token ignorando i token che appaiono meno di 2 volte e utilizzando f_classif per calcolare l'importanza delle caratteristiche.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import f_classif

# Vectorization parameters
# Range (inclusive) of n-gram sizes for tokenizing text.
NGRAM_RANGE = (1, 2)

# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Whether text should be split into word or character n-grams.
# One of 'word', 'char'.
TOKEN_MODE = 'word'

# Minimum document/corpus frequency below which a token will be discarded.
MIN_DOCUMENT_FREQUENCY = 2

def ngram_vectorize(train_texts, train_labels, val_texts):
    """Vectorizes texts as n-gram vectors.

    1 text = 1 tf-idf vector the length of vocabulary of unigrams + bigrams.

    # Arguments
        train_texts: list, training text strings.
        train_labels: np.ndarray, training labels.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val: vectorized training and validation texts
    """
    # Create keyword arguments to pass to the 'tf-idf' vectorizer.
    kwargs = {
            'ngram_range': NGRAM_RANGE,  # Use 1-grams + 2-grams.
            'dtype': 'int32',
            'strip_accents': 'unicode',
            'decode_error': 'replace',
            'analyzer': TOKEN_MODE,  # Split text into word tokens.
            'min_df': MIN_DOCUMENT_FREQUENCY,
    }
    vectorizer = TfidfVectorizer(**kwargs)

    # Learn vocabulary from training texts and vectorize training texts.
    x_train = vectorizer.fit_transform(train_texts)

    # Vectorize validation texts.
    x_val = vectorizer.transform(val_texts)

    # Select top 'k' of the vectorized features.
    selector = SelectKBest(f_classif, k=min(TOP_K, x_train.shape[1]))
    selector.fit(x_train, train_labels)
    x_train = selector.transform(x_train).astype('float32')
    x_val = selector.transform(x_val).astype('float32')
    return x_train, x_val

Con la rappresentazione vettoriale n-gram, scartiamo molte informazioni sull'ordine delle parole e sulla grammatica (nella migliore delle ipotesi, possiamo mantenere alcune informazioni di ordinamento parziali quando n > 1). Questo approccio è chiamato "bag-of-words". Questa rappresentazione viene utilizzata in combinazione con modelli che non tengono conto dell'ordinamento, come regressione logistica, percetroni multi-livello, macchine per il gradiente di intensità, supporto di macchine vettoriali.

Vettori di sequenza [Opzione B]

Nei paragrafi successivi vedremo come eseguire la tokenizzazione e la vettorizzazione per i modelli di sequenza. Vedremo anche come ottimizzare la rappresentazione della sequenza usando tecniche di normalizzazione e selezione delle caratteristiche.

Per alcuni esempi di testo, l'ordine delle parole è fondamentale per il significato del testo. Ad esempio, le frasi: "Odiavo il mio tragitto giornaliero. La mia nuova bici è cambiata così completamente" può essere interpretata solo se letta in ordine. Modelli come CNN/RNN possono dedurre il significato dall'ordine delle parole in un campione. Per questi modelli, rappresentiamo il testo come una sequenza di token, mantenendo l'ordine.

Tokenizzazione

Il testo può essere rappresentato come una sequenza di caratteri o una sequenza di parole. Abbiamo scoperto che l'uso della rappresentazione a livello di parola offre prestazioni migliori rispetto ai token di carattere. Questa è anche la norma generale seguita dal settore. L'uso di token di carattere ha senso solo se i testi contengono molti errori di battitura, cosa che di solito non accade.

Vettorizzazione

Dopo aver convertito i campioni di testo in sequenze di parole, dobbiamo trasformare queste sequenze in vettori numerici. L'esempio seguente mostra gli indici assegnati agli unigrammi generati per due testi e quindi la sequenza di indici di token in cui viene convertito il primo testo.

Texts: 'The mouse ran up the clock' and 'The mouse ran down'

Indice assegnato a ogni token:

{'clock': 5, 'ran': 3, 'up': 4, 'down': 6, 'the': 1, 'mouse': 2}

NOTA: la parola "il" è presente più di frequente, quindi il valore di indice 1 vi viene assegnato. Alcune librerie prenotano l'indice 0 per i token sconosciuti, come in questo caso.

Sequenza degli indici di token:

'The mouse ran up the clock' = [1, 2, 3, 4, 1, 5]

Sono disponibili due opzioni per vettorializzare le sequenze di token:

Codifica one-hot: le sequenze sono rappresentate utilizzando vettori di parole nello spazio n-dimensionale dove n = dimensione del vocabolario. Questa rappresentazione funziona molto bene quando la tokenizzazione viene applicata come caratteri e il vocabolario è quindi ridotto. Quando viene tokenizzata come parole, il vocabolario di solito ha decine di migliaia di token, il che rende i vettori one-hot molto sparsi e inefficienti. Esempio:

'The mouse ran up the clock' = [
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 1, 0]
]

Incorporamenti di parole: alle parole sono associati significati. Di conseguenza, possiamo rappresentare i token di parole in uno spazio vettoriale denso (circa alcune centinaia di numeri reali), in cui la posizione e la distanza tra le parole indicano la loro somiglianza semantica (vedi Figura 7). Questa rappresentazione è chiamata incorporazione di parole.

Incorporamenti di parole

Figura 7: incorporamenti di parole

I modelli di sequenza spesso hanno uno strato di incorporamento come primo strato. Questo strato impara a trasformare le sequenze dell'indice di parole in vettori di incorporamento di parole durante il processo di addestramento, in modo che ogni indice di parola venga mappato su un vettore denso di valori reali che rappresentano la posizione di quella parola nello spazio semantico (vedi Figura 8).

Strato di incorporamento

Figura 8: strato di incorporamento

Selezione delle caratteristiche

Non tutte le parole nei nostri dati contribuiscono alle previsioni delle etichette. Possiamo ottimizzare il nostro processo di apprendimento eliminando le parole rare o irrilevanti dal nostro vocabolario. Infatti, notiamo che utilizzare le 20.000 caratteristiche più frequenti è in genere sufficiente. Questo vale anche per i modelli n-grammi (vedi Figura 6).

Mettiamo insieme tutti i passaggi precedenti nella vettore di sequenza. Il codice seguente esegue queste attività:

  • Tokenizza i testi in parole
  • Crea un vocabolario utilizzando i primi 20.000 token
  • Converte i token in vettori di sequenza
  • Applica un pad alle sequenze fino a una lunghezza fissa
from tensorflow.python.keras.preprocessing import sequence
from tensorflow.python.keras.preprocessing import text

# Vectorization parameters
# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Limit on the length of text sequences. Sequences longer than this
# will be truncated.
MAX_SEQUENCE_LENGTH = 500

def sequence_vectorize(train_texts, val_texts):
    """Vectorizes texts as sequence vectors.

    1 text = 1 sequence vector with fixed length.

    # Arguments
        train_texts: list, training text strings.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val, word_index: vectorized training and validation
            texts and word index dictionary.
    """
    # Create vocabulary with training texts.
    tokenizer = text.Tokenizer(num_words=TOP_K)
    tokenizer.fit_on_texts(train_texts)

    # Vectorize training and validation texts.
    x_train = tokenizer.texts_to_sequences(train_texts)
    x_val = tokenizer.texts_to_sequences(val_texts)

    # Get max sequence length.
    max_length = len(max(x_train, key=len))
    if max_length > MAX_SEQUENCE_LENGTH:
        max_length = MAX_SEQUENCE_LENGTH

    # Fix sequence length to max value. Sequences shorter than the length are
    # padded in the beginning and sequences longer are truncated
    # at the beginning.
    x_train = sequence.pad_sequences(x_train, maxlen=max_length)
    x_val = sequence.pad_sequences(x_val, maxlen=max_length)
    return x_train, x_val, tokenizer.word_index

Vettoriazione delle etichette

Abbiamo visto come convertire dati di testo di esempio in vettori numerici. È necessario applicare una procedura simile alle etichette. Possiamo semplicemente convertire le etichette in valori nell'intervallo [0, num_classes - 1]. Ad esempio, se ci sono 3 classi, possiamo usare i valori 0, 1 e 2 per rappresentarle. Internamente, la rete utilizzerà vettori one-hot per rappresentare questi valori (per evitare di dedurre una relazione errata tra le etichette). Questa rappresentazione dipende dalla funzione di perdita e dalla funzione di attivazione dell'ultimo strato che utilizziamo nella rete neurale. Approfondiremo l'argomento nella prossima sezione.