En este notebook se muestra cómo crear un modelo de clasificación de texto usando la aproximación ULMFit.

En primer lugar es necesario disponer de un modelo de lenguaje entrenado con un dataset de tamaño considerable. En nuestro caso la Wikipedia. Este modelo sirve para conocer los fundamentos del lenguaje con el cual se está trabajando. Sin embargo, a la hora de construir un modelo de clasificación es conveniente que el modelo comprenda el estilo que se usa para escribir esos textos. Dicho estilo puede ser más informal o más técnico que el contenido de la wikipedia. En el caso del dataset IMDb, este va a contener una gran cantidad de nombres de directores, actores, y además el estilo de redacción es más informal que los textos que aparecen en la Wikipedia. Por ello, a partir del modelo de lenguaje de Wikipedia entrenaremos un modelo de lenguaje para IMDb, y a partir de ese modelo de lenguaje construiremos nuestro clasificador.

Afortunadamente la librería FastAI ya proporciona un modelo de lenguaje para la Wikipedia (construir este tipo de modelos puede llevar días), por lo que nos podemos centrar en los otros dos pasos.

Para esta práctica es necesario el uso de GPU, así que recuerda activar esta opción en Colab.

Librerías

Comenzamos actualizando la librería FastAI y descargando la librería datasets de HuggingFace. Al finalizar la instalación deberás reiniciar el kernel (menú Entorno de ejecución -> Reiniciar Entorno de ejecución).

!pip install fastai -Uqq
!pip install datasets -Uqq
     |████████████████████████████████| 189 kB 4.3 MB/s 
     |████████████████████████████████| 55 kB 2.0 MB/s 
     |████████████████████████████████| 325 kB 4.3 MB/s 
     |████████████████████████████████| 134 kB 34.0 MB/s 
     |████████████████████████████████| 67 kB 1.5 MB/s 
     |████████████████████████████████| 212 kB 38.3 MB/s 
     |████████████████████████████████| 1.1 MB 26.4 MB/s 
     |████████████████████████████████| 127 kB 39.1 MB/s 
     |████████████████████████████████| 271 kB 35.1 MB/s 
     |████████████████████████████████| 144 kB 47.7 MB/s 
     |████████████████████████████████| 94 kB 1.3 MB/s 
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
datascience 0.10.6 requires folium==0.2.1, but you have folium 0.8.3 which is incompatible.

Cargamos a continuación las librerías que necesitaremos en esta práctica que son la parte de procesado de lenguaje natural de la librería fastAI, la librería pandas, y la funcionalidad para cargar datasets de HuggingFace.

import pandas as pd
from fastai.text.all import *
from datasets import load_dataset

Dataset

Para este ejemplo vamos a usar el dataset Gutenberg Poem Dataset, un dataset para detectar sentimientos en poemas (negativos, positivos, sin impacto, mezcla de positivo y negativo).

Descarga el dataset usando el siguiente comando.

poem_sentiment_dataset = load_dataset("poem_sentiment")
Using custom data configuration default
Downloading and preparing dataset poem_sentiment/default (download: 48.70 KiB, generated: 58.53 KiB, post-processed: Unknown size, total: 107.23 KiB) to /root/.cache/huggingface/datasets/poem_sentiment/default/1.0.0/4e44428256d42cdde0be6b3db1baa587195e91847adabf976e4f9454f6a82099...
Dataset poem_sentiment downloaded and prepared to /root/.cache/huggingface/datasets/poem_sentiment/default/1.0.0/4e44428256d42cdde0be6b3db1baa587195e91847adabf976e4f9454f6a82099. Subsequent calls will reuse this data.

Carga de datos

Cargamos a continuación el dataset en distintos dataframes de pandas (el formato que puede leer la librería de FastAI).

train_df = poem_sentiment_dataset["train"].to_pandas()
valid_df = poem_sentiment_dataset["validation"].to_pandas()
test_df = poem_sentiment_dataset["test"].to_pandas()

A continuación procesamos el dataset como vimos en la práctica anterior para tenerlo en el formato adecuado.

train_df['set']=False
valid_df['set']=True
train_df = train_df.drop(columns=['id'],axis=1)
valid_df = valid_df.drop(columns=['id'],axis=1)
train_valid_df = pd.concat([train_df,valid_df])
train_valid_df = train_valid_df.rename(columns={"verse_text": "text"})

Modelo de lenguaje

El proceso a seguir para hacer fine-tuning sobre el modelo de lenguaje de FastAI es análogo al visto en prácticas anteriores. Comenzamos creando un DataBlock a partir de nuestro dataframe.

db_lm = DataBlock(
    blocks=TextBlock.from_df('text', is_lm=True,max_vocab=100000), # Indicamos que vamos a trabajar con un modelo de lenguaje
    get_items=ColReader('text'), # Indicamos donde estará el texto dentro del dataframe
    splitter=RandomSplitter(0.1) # Partimos el dataset en entrenamiento y validación
)

Creamos ahora nuestro dataloader (esto puede llevar varios segundos).

dls_lm = db_lm.dataloaders(train_valid_df, bs=128, seq_len=80)

Podemos ahora mostrar un batch de este dataloader. Como podemos apreciar, la entrada del modelo es una frase, y la salida es dicha frase desplazada una posición a la derecha.

dls_lm.show_batch(max_n=2)
text text_
0 xxbos before the xxunk soul , whose xxunk will xxbos and not be xxunk xxunk to the moon , xxbos so xxunk to me . xxunk , friend , since friend xxbos and _ xxunk _ , with his xxunk , xxunk look , xxbos nor is he , as some xxunk xxunk , xxbos the days pass over me xxbos a hundred xxunk , and xxunk before the xxunk soul , whose xxunk will xxbos and not be xxunk xxunk to the moon , xxbos so xxunk to me . xxunk , friend , since friend xxbos and _ xxunk _ , with his xxunk , xxunk look , xxbos nor is he , as some xxunk xxunk , xxbos the days pass over me xxbos a hundred xxunk , and xxunk more
1 more , had xxunk their leaves and xxunk , xxbos of xxunk 's love and the xxunk of xxunk . xxbos and xxunk are xxunk , and xxunk flame . xxbos " i xxunk xxunk has always held the xxunk . " xxbos all along down " xxunk xxunk ? " xxbos who xxunk in the xxunk - xxunk still ? xxbos xxunk xxunk had xxunk the , had xxunk their leaves and xxunk , xxbos of xxunk 's love and the xxunk of xxunk . xxbos and xxunk are xxunk , and xxunk flame . xxbos " i xxunk xxunk has always held the xxunk . " xxbos all along down " xxunk xxunk ? " xxbos who xxunk in the xxunk - xxunk still ? xxbos xxunk xxunk had xxunk the xxunk

Creamos ahora nuestro Learner.

learn = language_model_learner(
    dls_lm, # El dataloader que usamos
    AWD_LSTM, # La arquitectura que es la misma usada en la práctica anterior
    drop_mult=0.3, # Aplicamos dropout para evitar el sobreajuste
    metrics=[accuracy, Perplexity()] # Como métricas usamos la accuracy y la perplexity.
).to_fp16()

Y por último entrenamos el modelo.

learn.fine_tune(10,base_lr=2e-2)
epoch train_loss valid_loss accuracy perplexity time
0 5.325081 5.248910 0.093750 190.358749 00:02
epoch train_loss valid_loss accuracy perplexity time
0 5.301597 4.874115 0.142857 130.858353 00:02
1 5.048782 4.420505 0.228795 83.138260 00:02
2 4.785182 4.009061 0.245536 55.095131 00:02
3 4.613196 3.922916 0.281250 50.547634 00:02
4 4.439808 3.853290 0.283482 47.147942 00:02
5 4.305257 3.711589 0.292411 40.918758 00:01
6 4.179667 3.591018 0.308036 36.270988 00:01
7 4.068951 3.525345 0.323661 33.965485 00:02
8 3.980177 3.500305 0.327009 33.125561 00:02
9 3.906196 3.494886 0.339286 32.946529 00:02

Una vez entrenado el modelo guardamos el encoder que usaremos luego para nuestro modelo de clasificación (esto es análogo a lo que vimos para los modelos de clasificación de imágenes).

learn.save_encoder('finetuned')

Entrenando un modelo de clasificación

Pasamos ahora a crear nuestro modelo de clasificación de texto. El proceso será el mismo que el que vimos en la práctica anterior con la diferencia de que antes de empezar a entrenar el modelo cargaremos el encoder guardado en el paso anterior.

Comenzamos definiendo un DataBlock que se creará a partir de nuestro dataframe df.

sentiment_clas = DataBlock(
    blocks=(TextBlock.from_df('text', vocab=dls_lm.vocab), # La entrada del modelo es texto usando el mismo 
                                                           # vocabulario que en el modelo de lenguaje 
            CategoryBlock), #, y la salida una clase 
    get_x=ColReader('text'),  # Indicamos donde estará el texto dentro del dataframe
    get_y=ColReader('label'), # Indicamos cómo extraer la clase del dataframe
    splitter=ColSplitter('set') # Partimos el dataset en entrenamiento y validación
)

Ahora definimos nuestro dataloader a partir del DataBlock que acabamos de crear.

dls = sentiment_clas.dataloaders(train_valid_df, bs=64)

Podemos mostrar un batch de nuestro dataloader.

dls.show_batch(max_n=2)
text category
0 xxbos when i xxunk the xxunk xxunk of xxunk , and the xxunk of mighty xxunk , i do not envy the xxunk , 3
1 xxbos till , xxunk ' xxunk i know , there xxunk xxunk an xxunk xxunk i could lay my xxunk ' on , 2

Pasamos ahora a crear nuestro learner usando el método text_classifier_learner al que pasamos como arquitectura de red la arquitectura AWD_LSTM, además aplicamos dropout a nuestro modelo.

callbacks = [ShowGraphCallback(),
             SaveModelCallback()]

learnClass = text_classifier_learner(dls, AWD_LSTM, drop_mult=0.5, metrics=accuracy,cbs=callbacks).to_fp16()

Cargamos a continuación el encoder del modelo de lenguaje.

learnClass = learnClass.load_encoder('finetuned')

Ahora podemos utilizar toda la funcionalidad que ya vimos para clasificación de imágenes. Por ejemplo, podemos buscar un learning rate adecuado para entrenar nuestro modelo.

learnClass.lr_find()
SuggestedLRs(valley=0.005248074419796467)

Y a continuación aplicar fine tuning.

learnClass.fine_tune(10, 6e-2)
epoch train_loss valid_loss accuracy time
0 1.356869 1.046609 0.657143 00:02
Better model found at epoch 0 with valid_loss value: 1.0466091632843018.
epoch train_loss valid_loss accuracy time
0 1.072497 0.954486 0.657143 00:02
1 0.982236 0.909238 0.657143 00:02
2 0.933094 0.953875 0.685714 00:03
3 0.846433 1.242218 0.542857 00:02
4 0.762326 1.367901 0.552381 00:02
5 0.654452 1.145903 0.647619 00:02
6 0.537358 1.472025 0.590476 00:02
7 0.447860 1.408006 0.609524 00:02
8 0.364009 1.425131 0.619048 00:02
9 0.294759 1.411824 0.628571 00:02
Better model found at epoch 0 with valid_loss value: 0.9544857740402222.
Better model found at epoch 1 with valid_loss value: 0.9092381000518799.

Ahora podemos usar nuestro modelo para predecir la clase de una nueva frase.

learnClass.predict('with pale blue berries. in these peaceful shades--.')
('2', TensorText(2), TensorText([0.1746, 0.1666, 0.5807, 0.0781]))

Por último, podemos validar nuestro modelo en el conjunto de test, para lo cuál hay que combinar los dataframes y construir un nuevo dataloader.

test_df['set']=True
test_df = test_df.drop(columns=['id'],axis=1)
train_test_df = pd.concat([train_df,test_df])
train_test_df = train_test_df.rename(columns={"verse_text": "text"})
dls_test = sentiment_clas.dataloaders(train_test_df, bs=64)

Modificamos ahora el dataloader de nuestro learner, y procedemos a validar.

learnClass.dls = dls_test
learnClass.validate()
Better model found at epoch 0 with valid_loss value: 0.692307710647583.
(#2) [0.8739429712295532,0.692307710647583]

Hemos obtenido un modelo con una accuracy del 69% (un 3% mejor que sin aplicar la técnica de self-supervised learning).