Abrir en Google Colab Descargar notebook

Modelos basados en secuencias con Word2Vec

Introducción

Los modelos basados en secuencias tienen la fortaleza que toman una secuencia de token (en un determinado orden) y generan una salida dependiendo del tipo de problema que se trate.

  • Seq2Class: Toman una secuencia de tokens y generan una clase

  • Seq2Seq: Toman una secuencia de token y generan otra secuencia de tokens.

Vimos que cuando aplicamos técnicas de Topic Modeling, intentamos reducir la cantidad de dimensiones de nuestras representaciones de palabras para luego utilizar un clasificador para resolver la tarea en cuestión. Sin embargo, la suponsición básica de ese tipo de modelos es que un texto no es mas que una distribución de palabras (bag of words). Sin embargo, nosotros sabemos que un texto es una secuencia de palabras donde importa el orden. Para capturar este tipo de propiedades podemos utilizar modelos basados en secuencias.

Para ejecutar este notebook

Para ejecutar este notebook, instale las siguientes librerias:

[ ]:
!wget https://raw.githubusercontent.com/santiagxf/M72109/master/NLP/Datasets/mascorpus/tweets_marketing.csv \
    --quiet --no-clobber --directory-prefix ./Datasets/mascorpus/
!wget https://raw.githubusercontent.com/santiagxf/M72109/master/m72109/nlp/normalization.py \
    --quiet --no-clobber --directory-prefix ./m72109/nlp/
!wget https://raw.githubusercontent.com/santiagxf/M72109/master/m72109/nlp/transformation.py \
    --quiet --no-clobber --directory-prefix ./m72109/nlp/

!wget https://raw.githubusercontent.com/santiagxf/M72109/master/docs/nlp/neural/sequences-word2vec.txt \
    --quiet --no-clobber
!pip install -r sequences-word2vec.txt --quiet
[1]:
import warnings
warnings.filterwarnings('ignore')

Instalamos las librerias necesarias

[2]:
!python -m spacy download es_core_news_sm 1> /dev/null

Cargamos el set de datos

[3]:
import pandas as pd

tweets = pd.read_csv('Datasets/mascorpus/tweets_marketing.csv')

Preprocesamiento de texto

Al igual que con Topic Modeling, nuestro primer paso es preprocesar el texto. Para focalizarnos en Word2Vec en este modulo, les preparé un modulo TweetTextNormalizer que hará todo el preprocesamiento por nosotros. Pueden explorar los parametros que recibe el constructor de esta clase para ver que opciones podemos configurar como Stemmer, Lemmatization, etc.

En lo particular, estamos creando un TweetTextNormalizer que:

  • Aplicará un tokenizer especifico para Twitter

  • Eliminará stop words

  • Aplicará lemmatization

  • Eliminará URLs

  • Eliminará acentos

  • Eliminará las mayusculas

Adicionalmente, el parametro text_to_sequence=True indica que la salida de este proceso no serán oraciones sino que tokens.

[4]:
from m72109.nlp.normalization import TweetTextNormalizer
[5]:
normalizer = TweetTextNormalizer(preserve_case=False, return_tokens=True)

Transformemos el texto:

[6]:
tweets_text = normalizer.transform(tweets['TEXTO'])
100%|██████████| 3763/3763 [03:13<00:00, 19.44it/s]

Vectorización de las palabras

En las actividades anteriores utilizamos siempre un TF-IDF vectorizer para generar los vectores. En esta oportunidad utilizaremos Word2Vec utilizando un modelo pre-entrenado para el idioma español.

Descargamos nuestros vectores de word2vec en español

[7]:
!mkdir -p ./Models/Word2Vec
!wget https://santiagxf.blob.core.windows.net/public/Word2Vec/model-es.bin \
    --quiet --no-clobber

Adicionalmente, vemos que este vectorizer tiene el parametro sequence_to_idx en Verdadero. Esto significa que no queremos que como salida obtengamos los vectores de Word2Vec, sino que queremos «el indice» que se corresponde a la palabra en una matriz de indice-palabra/vectores.

[8]:
from m72109.nlp.transformation import Word2VecVectorizer
[9]:
w2v = Word2VecVectorizer(model='/content/model-es.bin', sequence_to_idx=True)
[10]:
tweets_text = w2v.transform(tweets_text)
100%|██████████| 3763/3763 [00:00<00:00, 49876.49it/s]

Construirmos un modelo basado en secuencias

Ajustando la longitud de las secuencias

Los modelos basados en secuencias pueden adaptarse a cualquier longitud de secuencia, sin embargo, los parametros de nuestras redes neuronales deberan ser fijos. Para esto definiermos una longitud máxima de la secuencia que vamos analizar. Para esto podemos utilizar un valor especifico o utilizar el valor máximo de tokens que hay en nuestro corpus.

La siguiente clase PadSequenceTransformer es un modulo que les preparé para simplificar este procesamiento. El mismo se encarga de ajustar cualquier secuencia para que tenga exactamente max_seq_len. Cuando la lingitud es mejor, se completan con ceros.

[11]:
from m72109.nlp.transformation import PadSequenceTransformer
[12]:
max_seq_len = 50
[13]:
seq2seq = PadSequenceTransformer(max_len=max_seq_len)
[14]:
tweets_text = seq2seq.transform(tweets_text)

Construyendo el modelo

Para construir nuestro modelo, utilizaremos TensorFlow. En particular utilizaremos la API de Keras que nos permite componer modelos de redes neuronales como una secuencia de pasos o capas que se conectan en una dirección.

Utilizemos los siguientes tipos de capas:

  • Embedding: Esta capa transforma vectores que representan indices dentro de una matriz en representaciones vectoriales densas. Básicamente en este caso nos resolverá la busqueda de las representaciones vectoriales para nuestras palabras.

  • SpatialDropout1D: Este tipo de capas ayudan a promover la independencia entre filtros (feature maps). Funciona en forma analoga a Dropout pero en lugar de desconectar elementos individuales, desconecta el filtro completo.

  • LSTM: Long Short-Term Memory layer - Hochreiter 1997

  • Dense: Una típica capa de una red neuronal completamente conectada (fully connected)

Algunos detalles para notar:

  • loss='sparse_categorical_crossentropy', este problema de clasificación (crossentropy) de más de una clase (categorical). Sin embargo, nuestro output produce probabilidades de cada una de las clases posibles (7) en forma one-hot encoding.

  • metrics=['accuracy']: Si bien nuestra metrica es accuracy, Keras hará un promedio ponderado del accuracy de cada clase. Este es el comportamiento por defecto.

[15]:
embedding_weights = w2v.get_weights()
100%|██████████| 2656058/2656058 [00:06<00:00, 411299.01it/s]

El método get_weights() construye la matríz de indice-palabra/vector que luego será utilizado para encontrar los vectores correspondientes de cada palabra. Esta matriz tiene dimensiones m x n, donde m es la cantidad de palabras del vocabulario y n la dimensión de los vectores de word2vec. En este caso trabajamos con vectores de dimensionalidad 100.

[16]:
import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Embedding, LSTM, Dense, Input, SpatialDropout1D
[17]:
model = Sequential([
    Embedding(w2v.vocab_size, w2v.emdedding_size,
              weights=[embedding_weights],
              trainable=False,
              mask_zero=True),
    SpatialDropout1D(0.2),
    LSTM(w2v.emdedding_size),
    Dense(7, activation='softmax')
])

model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

Podemos inspeccionar el modelo:

[18]:
model.summary()
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ embedding (Embedding)           │ ?                      │   265,605,800 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ spatial_dropout1d               │ ?                      │             0 │
│ (SpatialDropout1D)              │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm (LSTM)                     │ ?                      │   0 (unbuilt) │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense)                   │ ?                      │   0 (unbuilt) │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 265,605,800 (1013.21 MB)
 Trainable params: 0 (0.00 B)
 Non-trainable params: 265,605,800 (1013.21 MB)

Antes de continuar, separemos nuestro conjunto de datos en entrenamiento y testing y codifiquemos la variable a predecir:

[19]:
from sklearn.preprocessing import LabelEncoder

encoder = LabelEncoder()
tweets_sector = encoder.fit_transform(tweets['SECTOR'])
[20]:
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(tweets_text, tweets_sector,
                                                    test_size=0.33,
                                                    stratify=tweets_sector)

Entrenamos nuestro modelo:

[21]:
history = model.fit(X_train, y_train, epochs=50)
Epoch 1/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 7s 48ms/step - accuracy: 0.4105 - loss: 1.6565
Epoch 2/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 44ms/step - accuracy: 0.7568 - loss: 0.8116
Epoch 3/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 57ms/step - accuracy: 0.8233 - loss: 0.5701
Epoch 4/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 44ms/step - accuracy: 0.8422 - loss: 0.5003
Epoch 5/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 3s 44ms/step - accuracy: 0.8665 - loss: 0.4105
Epoch 6/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 68ms/step - accuracy: 0.8870 - loss: 0.3595
Epoch 7/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 8s 44ms/step - accuracy: 0.8790 - loss: 0.3679
Epoch 8/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 56ms/step - accuracy: 0.8821 - loss: 0.3417
Epoch 9/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 49ms/step - accuracy: 0.8865 - loss: 0.3083
Epoch 10/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 45ms/step - accuracy: 0.9041 - loss: 0.2818
Epoch 11/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 56ms/step - accuracy: 0.9031 - loss: 0.2601
Epoch 12/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 3s 44ms/step - accuracy: 0.9106 - loss: 0.2574
Epoch 13/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 48ms/step - accuracy: 0.9046 - loss: 0.2618
Epoch 14/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 44ms/step - accuracy: 0.9203 - loss: 0.2319
Epoch 15/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 44ms/step - accuracy: 0.9156 - loss: 0.2395
Epoch 16/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 54ms/step - accuracy: 0.9276 - loss: 0.2093
Epoch 17/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 3s 44ms/step - accuracy: 0.9381 - loss: 0.1913
Epoch 18/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 7s 66ms/step - accuracy: 0.9354 - loss: 0.2026
Epoch 19/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 9s 44ms/step - accuracy: 0.9395 - loss: 0.1827
Epoch 20/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 56ms/step - accuracy: 0.9335 - loss: 0.1873
Epoch 21/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 44ms/step - accuracy: 0.9352 - loss: 0.1824
Epoch 22/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 50ms/step - accuracy: 0.9427 - loss: 0.1732
Epoch 23/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 50ms/step - accuracy: 0.9359 - loss: 0.1733
Epoch 24/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 45ms/step - accuracy: 0.9433 - loss: 0.1654
Epoch 25/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 53ms/step - accuracy: 0.9488 - loss: 0.1536
Epoch 26/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 44ms/step - accuracy: 0.9513 - loss: 0.1338
Epoch 27/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 3s 44ms/step - accuracy: 0.9606 - loss: 0.1313
Epoch 28/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 52ms/step - accuracy: 0.9545 - loss: 0.1292
Epoch 29/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 44ms/step - accuracy: 0.9696 - loss: 0.1005
Epoch 30/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 44ms/step - accuracy: 0.9675 - loss: 0.1018
Epoch 31/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 51ms/step - accuracy: 0.9705 - loss: 0.1082
Epoch 32/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 50ms/step - accuracy: 0.9624 - loss: 0.1085
Epoch 33/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 45ms/step - accuracy: 0.9674 - loss: 0.0999
Epoch 34/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 50ms/step - accuracy: 0.9658 - loss: 0.0990
Epoch 35/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 50ms/step - accuracy: 0.9623 - loss: 0.1044
Epoch 36/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 44ms/step - accuracy: 0.9787 - loss: 0.0766
Epoch 37/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 58ms/step - accuracy: 0.9723 - loss: 0.0851
Epoch 38/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 44ms/step - accuracy: 0.9811 - loss: 0.0759
Epoch 39/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 44ms/step - accuracy: 0.9820 - loss: 0.0585
Epoch 40/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 46ms/step - accuracy: 0.9763 - loss: 0.0763
Epoch 41/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 44ms/step - accuracy: 0.9719 - loss: 0.0863
Epoch 42/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 58ms/step - accuracy: 0.9826 - loss: 0.0669
Epoch 43/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 44ms/step - accuracy: 0.9841 - loss: 0.0537
Epoch 44/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 44ms/step - accuracy: 0.9768 - loss: 0.0753
Epoch 45/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 51ms/step - accuracy: 0.9818 - loss: 0.0550
Epoch 46/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 3s 44ms/step - accuracy: 0.9821 - loss: 0.0616
Epoch 47/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 53ms/step - accuracy: 0.9831 - loss: 0.0579
Epoch 48/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step - accuracy: 0.9885 - loss: 0.0466
Epoch 49/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 5s 44ms/step - accuracy: 0.9784 - loss: 0.0563
Epoch 50/50
79/79 ━━━━━━━━━━━━━━━━━━━━ 6s 71ms/step - accuracy: 0.9814 - loss: 0.0593

Evalución de los resultados

Probamos su performance utilizando el test set

[22]:
predictions = model.predict(X_test)
39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 20ms/step
[23]:
import numpy as np

predictions = np.argmax(predictions, axis=1)

Veamos el reporte:

[24]:
from sklearn.metrics import classification_report

print(classification_report(y_test, predictions, target_names=encoder.classes_))
              precision    recall  f1-score   support

ALIMENTACION       0.99      0.92      0.95       110
  AUTOMOCION       0.90      0.89      0.89       148
       BANCA       0.86      0.87      0.86       198
     BEBIDAS       0.92      0.88      0.90       223
    DEPORTES       0.94      0.89      0.92       216
      RETAIL       0.80      0.87      0.83       268
       TELCO       0.89      0.91      0.90        79

    accuracy                           0.89      1242
   macro avg       0.90      0.89      0.89      1242
weighted avg       0.89      0.89      0.89      1242