Classificazione di testo con testo pre-elaborato: Recensioni di film

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza il sorgente su GitHub Scarica il notebook

Questo notebook classifica recensioni di film come positive o negative usando il testo delle revisioni. Questo è un esempio di classificazione binaria—o a due classi, un importante tipo di problema di machine learning largamente applicabile.

Useremo il dataset IMDB che contiene il testo di 50.000 recensioni di film dall'Internet Movie Database. Esse sono divise in 25,000 recensioni per l'addestramento e 25,000 revisioni per la verifica. Gli insiemi di addestramento e verifica sono bilanciati, nel senso che essi contengono un eguale numero di recensioni positive e negative.

Questo notebook usa tf.keras, una API di alto livello per costruire e addestrare modelli in TensorFlow. Per un tutorial più avanzato di classificazione del testo che usa tf.keras, vedere la MLCC Text Classification Guide.

Setup

from __future__ import absolute_import, division, print_function, unicode_literals
try:
  # %tensorflow_version only exists in Colab.
  %tensorflow_version 2.x
  !pip install -q tf-nightly
except Exception:
  pass
import tensorflow as tf
from tensorflow import keras

!pip install -q tensorflow-datasets
import tensorflow_datasets as tfds
tfds.disable_progress_bar()

import numpy as np

print(tf.__version__)
WARNING: You are using pip version 20.2.2; however, version 20.2.3 is available.
You should consider upgrading via the '/tmpfs/src/tf_docs_env/bin/python -m pip install --upgrade pip' command.
2.3.0

Scarichiamo il dataset IMDB

Il dataset di recensioni di film IMDB viene compattato in tfds. Esso è stato già pre-elaborato in modo che le recensioni (sequenze di parole) sono state convertite in sequenze di interi, ove ciascun intero rappresenta una particolare parola in un vocabolario.

Il codice che segue scarica il dataset IMDB sulla vostra macchina (o usa una copia locale se lo avete scaricato in precedenza):

Per codificare il vostro testo vedere il tutorial sul caricamento di testo

(train_data, test_data), info = tfds.load(
    # Use the version pre-encoded with an ~8k vocabulary.
    'imdb_reviews/subwords8k', 
    # Return the train/test datasets as a tuple.
    split = (tfds.Split.TRAIN, tfds.Split.TEST),
    # Return (example, label) pairs from the dataset (instead of a dictionary).
    as_supervised=True,
    # Also return the `info` structure. 
    with_info=True)
WARNING:absl:TFDS datasets with text encoding are deprecated and will be removed in a future version. Instead, you should use the plain text version and tokenize the text using `tensorflow_text` (See: https://www.tensorflow.org/tutorials/tensorflow_text/intro#tfdata_example)

Downloading and preparing dataset imdb_reviews/subwords8k/1.0.0 (download: 80.23 MiB, generated: Unknown size, total: 80.23 MiB) to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0...
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incomplete4M39KW/imdb_reviews-train.tfrecord
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incomplete4M39KW/imdb_reviews-test.tfrecord
Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0.incomplete4M39KW/imdb_reviews-unsupervised.tfrecord
Dataset imdb_reviews downloaded and prepared to /home/kbuilder/tensorflow_datasets/imdb_reviews/subwords8k/1.0.0. Subsequent calls will reuse this data.

Proviamo il codificatore

Il dataset info include il codificatore di testo (un tfds.features.text.SubwordTextEncoder).

encoder = info.features['text'].encoder
print ('Vocabulary size: {}'.format(encoder.vocab_size))
Vocabulary size: 8185

Questo codificatore di testo codifica reversibilmente ogni stringa:

sample_string = 'Hello TensorFlow.'

encoded_string = encoder.encode(sample_string)
print ('Encoded string is {}'.format(encoded_string))

original_string = encoder.decode(encoded_string)
print ('The original string: "{}"'.format(original_string))

assert original_string == sample_string
Encoded string is [4025, 222, 6307, 2327, 4043, 2120, 7975]
The original string: "Hello TensorFlow."

Il codificatore codifica la stringa spezzandola in sotto-parole o caratteri se la parola non è presente nel suo vocabolario. In questo modo, più una stringa somiglia al dataset, più corta sarà la rappresentazione codificata.

for ts in encoded_string:
  print ('{} ----> {}'.format(ts, encoder.decode([ts])))
4025 ----> Hell
222 ----> o 
6307 ----> Ten
2327 ----> sor
4043 ----> Fl
2120 ----> ow
7975 ----> .

Esploriamo i dati

Prendiamoci un momento per capire il formato dei dati. Il dataset è pre-elaborato: ogni esempio è un vettore di interi che rappresenta le parole della recensione del film.

I testi delle recensioni sono stati convertiti in interi, dove ciascun intero rappresenta un particolare frammento di parola nel vocabolario.

Ogni etichetta è un valore intero tra 0 e 1, dove 0 è una recensione negativa, e 1 una recensione positiva.

Qui ciò a cui somiglia la prima recensione:

for train_example, train_label in train_data.take(1):
  print('Encoded text:', train_example[:10].numpy())
  print('Label:', train_label.numpy())
Encoded text: [  62   18   41  604  927   65    3  644 7968   21]
Label: 0

La struttura info contiene il codificatore/decodificatore. Il decodificatore può essere usato per recuperare il testo originale:

encoder.decode(train_example)
"This was an absolutely terrible movie. Don't be lured in by Christopher Walken or Michael Ironside. Both are great actors, but this must simply be their worst role in history. Even their great acting could not redeem this movie's ridiculous storyline. This movie is an early nineties US propaganda piece. The most pathetic scenes were those when the Columbian rebels were making their cases for revolutions. Maria Conchita Alonso appeared phony, and her pseudo-love affair with Walken was nothing but a pathetic emotional plug in a movie that was devoid of any real meaning. I am disappointed that there are movies like this, ruining actor's like Christopher Walken's good name. I could barely sit through it."

Prepariamo i dati per l'addestramento

Vorrete creare lotti di dati di addestramento per il vostro modello. Le recensioni sono tutte di lunghezza diversa, così usiamo padded_batch per riempire di zeri le sequenze durante la suddivisione in lotti:

BUFFER_SIZE = 1000

train_batches = (
    train_data
    .shuffle(BUFFER_SIZE)
    .padded_batch(32))

test_batches = (
    test_data
    .padded_batch(32))

Ogni lotto avrà una forma del tipo (batch_size, sequence_length) e dato che il riempimento è dinamico, ogni lotto avrà una lunghezza diversa:

for example_batch, label_batch in train_batches.take(2):
  print("Batch shape:", example_batch.shape)
  print("label shape:", label_batch.shape)

Batch shape: (32, 974)
label shape: (32,)
Batch shape: (32, 1039)
label shape: (32,)

Costruiamo il modello

La rete neurale viene creata impilando livelli—ciò richiede due decisioni architetturali principali:

  • Quanti livelli usare nel modello?
  • Quante unità nascoste usare in ciascun livello?

In questo esempio, i dati di input sono costituiti da un vettore di parole-indici. Le etichette da prevedere sono 0 oppure 1. Costruiamo un modello in stile "Continuous bag-of-words" per questo problema:

Attenzione: Questo modello non usa la mascheratura, così il riempimento di zeri viene utilizzato come parte dell'input, così la lunghezza del riempimento può influire sull'output. Per evitare ciò, vedere la guida al riempimento e mascheramento.

model = keras.Sequential([
  keras.layers.Embedding(encoder.vocab_size, 16),
  keras.layers.GlobalAveragePooling1D(),
  keras.layers.Dense(1)])

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, None, 16)          130960    
_________________________________________________________________
global_average_pooling1d (Gl (None, 16)                0         
_________________________________________________________________
dense (Dense)                (None, 1)                 17        
=================================================================
Total params: 130,977
Trainable params: 130,977
Non-trainable params: 0
_________________________________________________________________

I livelli sono impilati in sequenza per implementare il classificatore:

  1. Il primo livello è un livello Incorporamento. Questo livello prende il vocabolario codificato in interi e guarda il vettore di incorporamento per ogni parola-indice. Questi vettori sono assimilati durante l'addestramento del modello. I vettori aggiungono una dimensione al vettore di output. Le dimensioni risultanti sono: (batch, sequence, embedding).
  2. Successivamente, un livello GlobalAveragePooling1D restituisce in output un vettore di lunghezza fissa per ogni esempio mediando sulle dimensioni della sequenza. Ciò permette al modello di gestire input di lunghezza variabile, nel modo più semplice possibile.
  3. Questo vettore di output a lunghezza fissa viene passato attraverso un livello completamente connesso (Denso) con 16 unità nascoste.
  4. L'ultimo livello è connesso densamente ed ha un solo nodo di output. Usando la funzione di attivazione sigmoid, questo valore è un decimale tra 0 e 1, che rappresenta una probabilità, o un livello di confidenza.

Unità nascoste

Il modello di cui sopra ha due livelli intermedi o "nascosti", tra l'input e l'output. Il numero di output (unità, nodi o neuroni) è la dimensione dello spazio di rappresentazione del livello. In altre parole, l'ammontare della libertà di cui dispone la rete quando durante l'apprendimento di una rappresentazione interna.

Se un modello ha più di un'unità nascosta (uno spazio di rappresentazione dimensionale più grande), e/o più livelli, allora la rete può apprendere rappresentazioni più complesse. Comunque, ciò rende la rete computazionalmente più costosa e può condurre all'apprendimento di pattern indesiderati—pattern che aumentano le prestazioni sui dati di addestramento ma non sui dati di test. Questo (fenomeno n.d.r.) viene chiamato overfitting (sovradattamento n.d.t.), e verrà esplorato in seguito.

Funzione obiettivo e ottimizzatore

Un modello, per l'addestramento, ha bisogno di una funzione obiettivo e di un ottimizzatore. Essendo questo un problema di classificazione binaria e l'output del modello una probabilità (un livello a unità singola con un'attivazione sigmoid), useremo la funzione obiettivo binary_crossentropy.

Questa non è l'unica scelta possibile per una funzione obiettivo, potreste, per esempio, scegliere la mean_squared_error. In generale, però, binary_crossentropy è migliore per gestire probabilità—essa misura la "distanza" tra distribuzioni di probabilità o, nel nostro caso, tra la distribuzione dei dati reali e le previsioni.

Nel seguito, quando esploreremo i problemi di regressione (diciamo, per prevedere il prezzo di una casa), vedremo come usare un'altra funzione obiettivo chiamata scarto quadratico medio.

Adesso, configuriamo il modello per usare un ottimizzatore ed una funzione obiettivo:

model.compile(optimizer='adam',
              loss=tf.losses.BinaryCrossentropy(from_logits=True),
              metrics=['accuracy'])

Addestriamo il modello

Addestrare il modello passando l'oggetto Dataset alla funzione di allenamento del modello. Impostare il numero di epoche.

history = model.fit(train_batches,
                    epochs=10,
                    validation_data=test_batches,
                    validation_steps=30)
Epoch 1/10
782/782 [==============================] - 4s 5ms/step - loss: 0.6838 - accuracy: 0.5002 - val_loss: 0.6688 - val_accuracy: 0.5021
Epoch 2/10
782/782 [==============================] - 4s 5ms/step - loss: 0.6249 - accuracy: 0.5490 - val_loss: 0.5985 - val_accuracy: 0.5927
Epoch 3/10
782/782 [==============================] - 4s 5ms/step - loss: 0.5454 - accuracy: 0.6602 - val_loss: 0.5349 - val_accuracy: 0.7021
Epoch 4/10
782/782 [==============================] - 4s 5ms/step - loss: 0.4780 - accuracy: 0.7471 - val_loss: 0.4845 - val_accuracy: 0.7615
Epoch 5/10
782/782 [==============================] - 4s 5ms/step - loss: 0.4240 - accuracy: 0.8005 - val_loss: 0.4473 - val_accuracy: 0.8052
Epoch 6/10
782/782 [==============================] - 4s 5ms/step - loss: 0.3830 - accuracy: 0.8302 - val_loss: 0.4202 - val_accuracy: 0.7906
Epoch 7/10
782/782 [==============================] - 4s 5ms/step - loss: 0.3501 - accuracy: 0.8545 - val_loss: 0.3976 - val_accuracy: 0.8406
Epoch 8/10
782/782 [==============================] - 4s 5ms/step - loss: 0.3270 - accuracy: 0.8670 - val_loss: 0.3830 - val_accuracy: 0.8438
Epoch 9/10
782/782 [==============================] - 4s 5ms/step - loss: 0.3043 - accuracy: 0.8757 - val_loss: 0.3712 - val_accuracy: 0.8510
Epoch 10/10
782/782 [==============================] - 4s 5ms/step - loss: 0.2887 - accuracy: 0.8849 - val_loss: 0.3632 - val_accuracy: 0.8552

Valutiamo il modello

E andiamo a vedere come si comporta il modello. Saranno restituiti due valori. loss (Perdita n.d.t.) (un numero che rappresenta il nostro errore, per cui valori piccoli sono migliori), e accuracy (accuratezza n.d.t).

loss, accuracy = model.evaluate(test_batches)

print("Loss: ", loss)
print("Accuracy: ", accuracy)
782/782 [==============================] - 2s 2ms/step - loss: 0.3328 - accuracy: 0.8598
Loss:  0.3327641487121582
Accuracy:  0.8597599864006042

Questo approccio abbastanza ingenuo raggiunge un'accuratezza di circa l'87%. Con approcci più avanzati, il modello potrebbe avvicinarsi al 95%.

Creiamo un grafico di accuratezza e obiettivo nel tempo

model.fit() restituisce un oggetto History che contiene un registro con tutto ciò che è accaduto durante l'addestramento:

history_dict = history.history
history_dict.keys()
dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])

Ci sono quattro sezioni: una per ogni metrica monitorata durante l'addestramento e la validazione. Possiamo usare queste per tracciare il confronto tra l'obiettivo in addestramento e in validazione, così come l'accuratezza in addestramento e validazione:

import matplotlib.pyplot as plt

acc = history_dict['accuracy']
val_acc = history_dict['val_accuracy']
loss = history_dict['loss']
val_loss = history_dict['val_loss']

epochs = range(1, len(acc) + 1)

# "bo" is for "blue dot"
plt.plot(epochs, loss, 'bo', label='Training loss')
# b is for "solid blue line"
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.show()

png

plt.clf()   # clear figure

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')

plt.show()

png

In questo grafico, i punti rappresentano la perita e l'accuratezza in addestramento, mentre le linee continue sono l'obiettivo e l'accuratezza in validazione.

Notate che l'obiettivo in addestramento decresce con le epoche e l'accuratezza cresce con le epoche. Questo è quello che ci si attende quando si usa un'ottimizzazione a gradiente discendente—esso dovrebbe minimizzare la quantità obiettivo ad ogni iterazione.

Questo non accade per obiettivo e accuratezza in validazione—esse sembrano avere un picco dopo circa venti epoche. Questo è un esempio di sovradattamento: il modello ha prestazioni migliori sui dati di addestramento che su dati che non ha mai visto prima. Dopo questo punto, il modello sovra-ottimizza ed impara rappresentazioni specifiche dei dati di addestramento che non generalizzano sui dati di test.

Per questo caso particolare, non possiamo prevenire il sovradattamento fermando semplicemente l'addestramento dopo più o meno venti epoche. Nel seguito, vedremo come farlo automaticamente con una callback.

# MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.