Python para NLP: creación de modelos de clasificación de tipos de datos múltiples con Keras

Este es el artículo número 18 de mi serie de artículos sobre Python para PNL. En mi artículo anterior, expliqué cómo crear un análisis de sentimiento de película basado en aprendizaje profundo...

Este es el artículo número 18 de mi serie de artículos sobre Python para PNL. En mi Artículo anterior, expliqué cómo crear un sentimiento cinematográfico basado en el aprendizaje profundo modelo de análisis utilizando la biblioteca Keras de Python. En ese artículo, vimos cómo podemos realizar un análisis de opinión de las reseñas de los usuarios con respecto a diferentes películas en IMDB. Usamos el texto de la reseña para predecir el sentimiento.

Sin embargo, en tareas de clasificación de texto, también podemos hacer uso de la información no textual para clasificar el texto. Por ejemplo, el género puede tener un impacto en el sentimiento de la reseña. Además, las nacionalidades pueden afectar la opinión pública sobre una película en particular. Por lo tanto, esta información asociada, también conocida como metadatos, también se puede utilizar para mejorar la precisión del modelo estadístico.

En este artículo, nos basaremos en los conceptos que estudiamos en los dos últimos artículos y veremos cómo crear un sistema de clasificación de texto que clasifique las opiniones de los usuarios sobre diferentes negocios, en una de las tres categorías predefinidas, es decir, "bueno", "malo", y "promedio". Sin embargo, además del texto de la revisión, utilizaremos los metadatos asociados de la revisión para realizar la clasificación. Dado que tenemos dos tipos diferentes de entradas, es decir, entrada textual y entrada numérica, necesitamos crear un modelo de entradas múltiples. Usaremos la API funcional de Keras, ya que admite múltiples entradas y múltiples modelos de salida.

Después de leer este artículo, podrá crear un modelo de aprendizaje profundo en Keras que sea capaz de aceptar múltiples entradas, concatenar las dos salidas y luego realizar una clasificación o regresión usando la entrada agregada.

Antes de sumergirnos en los detalles de la creación de dicho modelo, primero revisemos brevemente el conjunto de datos que vamos a utilizar.

El conjunto de datos

El conjunto de datos para este artículo se puede descargar desde este Enlace Kaggle. El conjunto de datos contiene varios archivos, pero solo nos interesa el archivo yelp_review.csv. El archivo contiene más de 5,2 millones de reseñas sobre diferentes negocios, incluidos restaurantes, bares, dentistas, médicos, salones de belleza, etc. Para nuestros propósitos, solo utilizaremos los primeros 50 000 registros para entrenar nuestro modelo. Descargue el conjunto de datos a su máquina local.

Primero importemos todas las bibliotecas que usaremos en este artículo antes de importar el conjunto de datos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from numpy import array
from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers.core import Activation, Dropout, Dense
from keras.layers import Flatten, LSTM
from keras.layers import GlobalMaxPooling1D
from keras.models import Model
from keras.layers.embeddings import Embedding
from sklearn.model_selection import train_test_split
from keras.preprocessing.text import Tokenizer
from keras.layers import Input
from keras.layers.merge import Concatenate

import pandas as pd
import numpy as np
import re

Como primer paso, necesitamos cargar el conjunto de datos. El siguiente script hace eso:

1
yelp_reviews = pd.read_csv("/content/drive/My Drive/yelp_review_short.csv")

El conjunto de datos contiene una columna Estrellas que contiene calificaciones para diferentes negocios. La columna "Estrellas" puede tener valores entre 1 y 5. Simplificaremos nuestro problema convirtiendo los valores numéricos de las reseñas en valores categóricos. Agregaremos una nueva columna reviews_score a nuestro conjunto de datos. Si la reseña del usuario tiene un valor de 1 en la columna Estrellas, la columna reviews_score tendrá un valor de cadena malo. Si la calificación es 2 o 3 en la columna Estrellas, la columna reviews_score contendrá un valor promedio. Finalmente, la calificación de revisión de 4 o 5 tendrá un valor correspondiente de bueno en la columna reviews_score.

El siguiente script realiza este preprocesamiento:

1
2
3
bins = [0,1,3,5]
review_names = ['bad', 'average', 'good']
yelp_reviews['reviews_score'] = pd.cut(yelp_reviews['stars'], bins, labels=review_names)

A continuación, eliminaremos todos los valores NULL de nuestro marco de datos e imprimiremos la forma y el encabezado del conjunto de datos.

1
2
3
4
5
yelp_reviews.isnull().values.any()

print(yelp_reviews.shape)

yelp_reviews.head()

En el resultado verá (50000,10), lo que significa que nuestro conjunto de datos contiene 50 000 registros con 10 columnas. El encabezado del marco de datos yelp_reviews se ve así:

head{.img-responsive}

Puede ver las 10 columnas que contiene nuestro marco de datos, incluida la columna reviews_score recientemente agregada. La columna “texto” contiene el texto de la reseña, mientras que la columna “útil” contiene un valor numérico que representa el número de personas que encontraron útil la reseña. Del mismo modo, las columnas “gracioso” y “genial” contienen los recuentos de personas que encontraron reseñas “divertidas” o “geniales”, respectivamente.

Elijamos al azar una reseña. Si observa la cuarta revisión (revisión con índice 3), tiene 4 estrellas y, por lo tanto, está marcada como “buena”. Veamos el texto completo de esta reseña:

1
print(yelp_reviews["text"][3])

La salida se ve así:

1
2
3
4
5
Love coming here. Yes the place always needs the floor swept but when you give out  peanuts in the shell how won't it always be a bit dirty.

The food speaks for itself, so good. Burgers are made to order and the meat is put on the grill when you order your sandwich. Getting the small burger just means 1 patty, the regular is a 2 patty burger which is twice the deliciousness.

Getting the Cajun fries adds a bit of spice to them and whatever size you order they always throw more fries (a lot more fries) into the bag.

Se puede ver claramente que esta es una crítica positiva.

Tracemos ahora el número de reseñas buenas, promedio y malas.

1
2
3
import seaborn as sns

sns.countplot(x='reviews_score', data=yelp_reviews)

head{.img-responsive}

Es evidente a partir del gráfico anterior que la mayoría de las críticas son buenas, seguidas por las críticas promedio. El número de críticas negativas es muy pequeño.

Hemos preprocesado nuestros datos y ahora crearemos tres modelos en este artículo. El primer modelo solo utilizará entradas de texto para predecir si una reseña es “buena”, “promedio” o “mala”. En el segundo modelo, no usaremos texto. Solo utilizaremos la metainformación como “útil”, “divertido” y “genial” para predecir el sentimiento de la reseña. Finalmente, crearemos un modelo que acepte múltiples entradas, es decir, texto y metainformación para la clasificación de texto.

Crear un modelo solo con entradas de texto {#crear un modelo solo con entradas de texto}

El primer paso es definir una función que limpie los datos textuales.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def preprocess_text(sen):

    # Remove punctuations and numbers
    sentence = re.sub('[^a-zA-Z]', ' ', sen)

    # Single character removal
    sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)

    # Removing multiple spaces
    sentence = re.sub(r'\s+', ' ', sentence)

    return sentence

Dado que solo usamos texto en este modelo, filtraremos todas las revisiones de texto y las almacenaremos en la lista. Las revisiones de texto se limpiarán con la función preprocess_text, que elimina los signos de puntuación y los números del texto.

1
2
3
4
5
6
X = []
sentences = list(yelp_reviews["text"])
for sen in sentences:
    X.append(preprocess_text(sen))

y = yelp_reviews['reviews_score']

Nuestra variable X aquí contiene las reseñas de texto, mientras que la variable y contiene los valores correspondientes de reviews_score. La columna reviews_score tiene datos en formato de texto. Necesitamos convertir el texto en un vector codificado one-hot. Podemos usar el método to_categorical del módulo keras.utils. Sin embargo, primero tenemos que convertir el texto en etiquetas enteras usando la función LabelEncoder del módulo sklearn.preprocessing.

1
2
3
4
5
6
7
from sklearn import preprocessing

# label_encoder object knows how to understand word labels.
label_encoder = preprocessing.LabelEncoder()

# Encode labels in column 'species'.
y = label_encoder.fit_transform(y)

Ahora dividamos nuestros datos en conjuntos de prueba y entrenamiento:

1
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)

Ahora podemos convertir tanto las etiquetas de entrenamiento como las de prueba en vectores codificados en caliente:

1
2
3
from keras.utils import to_categorical
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

Expliqué en mi artículo sobre incrustación de palabras que los datos textuales deben convertirse en algún tipo de forma numérica antes de que puedan ser utilizado por algoritmos estadísticos como máquinas y modelos de aprendizaje profundo. Una forma de convertir texto en números es a través de incrustaciones de palabras. Si no sabe cómo implementar incrustaciones de palabras a través de Keras, le recomiendo que lea Este artículo antes de pasar a las siguientes secciones del código.

El primer paso en la incrustación de palabras es convertir las palabras en sus índices numéricos correspondientes. Para hacerlo, podemos usar la clase Tokenizer del módulo Keras.preprocessing.text.

1
2
3
4
5
tokenizer = Tokenizer(num_words=5000)
tokenizer.fit_on_texts(X_train)

X_train = tokenizer.texts_to_sequences(X_train)
X_test = tokenizer.texts_to_sequences(X_test)

Las oraciones pueden tener diferentes longitudes y, por lo tanto, las secuencias devueltas por la clase Tokenizer también consisten en longitudes variables. Especificamos que la longitud máxima de la secuencia será de 200 (aunque puedes probar con cualquier número). Para las oraciones que tengan una longitud inferior a 200, los índices restantes se rellenarán con ceros. Para las oraciones que tengan una extensión superior a 200, se truncarán los índices restantes.

Mira el siguiente guión:

1
2
3
4
5
6
vocab_size = len(tokenizer.word_index) + 1

maxlen = 200

X_train = pad_sequences(X_train, padding='post', maxlen=maxlen)
X_test = pad_sequences(X_test, padding='post', maxlen=maxlen)

A continuación, debemos cargar las incrustaciones de palabras Guante integradas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from numpy import array
from numpy import asarray
from numpy import zeros

embeddings_dictionary = dict()

for line in glove_file:
    records = line.split()
    word = records[0]
    vector_dimensions = asarray(records[1:], dtype='float32')
    embeddings_dictionary [word] = vector_dimensions

glove_file.close()

Finalmente, crearemos una matriz de incrustación donde las filas serán iguales al número de palabras en el vocabulario (más 1). El número de columnas será 100 ya que cada palabra en las incrustaciones de palabras GloVe que cargamos se representa como un vector de 100 dimensiones.

1
2
3
4
5
embedding_matrix = zeros((vocab_size, 100))
for word, index in tokenizer.word_index.items():
    embedding_vector = embeddings_dictionary.get(word)
    if embedding_vector is not None:
        embedding_matrix[index] = embedding_vector

Una vez que se completa el paso de incrustación de palabras, estamos listos para crear nuestro modelo. Usaremos la API funcional de Keras para crear nuestro modelo. Aunque los modelos de entrada única como el que estamos creando ahora también se pueden desarrollar usando una API secuencial, pero dado que en la siguiente sección vamos a desarrollar un modelo de entrada múltiple que solo se puede desarrollar usando la API funcional de Keras, nos ceñiremos a la API funcional. API en esta sección también.

Crearemos un modelo muy simple con una capa de entrada (capa incrustada), una capa LSTM con 128 neuronas y una capa densa que también actuará como capa de salida. Como tenemos 3 salidas posibles, el número de neuronas será 3 y la función de activación será softmax. Usaremos categorical_crossentropy como nuestra función de pérdida y adam como función de optimización.

1
2
3
4
5
6
7
deep_inputs = Input(shape=(maxlen,))
embedding_layer = Embedding(vocab_size, 100, weights=[embedding_matrix], trainable=False)(deep_inputs)
LSTM_Layer_1 = LSTM(128)(embedding_layer)
dense_layer_1 = Dense(3, activation='softmax')(LSTM_Layer_1)
model = Model(inputs=deep_inputs, outputs=dense_layer_1)

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

Imprimamos el resumen de nuestro modelo:

1
print(model.summary())
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 200)               0
_________________________________________________________________
embedding_1 (Embedding)      (None, 200, 100)          5572900
_________________________________________________________________
lstm_1 (LSTM)                (None, 128)               117248
_________________________________________________________________
dense_1 (Dense)              (None, 3)                 387
=================================================================
Total params: 5,690,535
Trainable params: 117,635
Non-trainable params: 5,572,900

Finalmente, imprimamos el diagrama de bloques de nuestra red neuronal:

1
2
from keras.utils import plot_model
plot_model(model, to_file='model_plot1.png', show_shapes=True, show_layer_names=True)

El archivo model_plot1.png se creará en su ruta de archivo local. Si abres la imagen, se verá así:

head{.img-responsive}

Puede ver que el modelo tiene 1 capa de entrada, 1 capa de incrustación, 1 LSTM y una capa densa que también sirve como capa de salida.

Ahora entrenemos nuestro modelo:

1
history = model.fit(X_train, y_train, batch_size=128, epochs=10, verbose=1, validation_split=0.2)

El modelo se entrenará con el 80 % de los datos del tren y se validará con el 20 % de los datos del tren. Los resultados para las 10 épocas son los siguientes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Train on 32000 samples, validate on 8000 samples
Epoch 1/10
32000/32000 [==============================] - 81s 3ms/step - loss: 0.8640 - acc: 0.6623 - val_loss: 0.8356 - val_acc: 0.6730
Epoch 2/10
32000/32000 [==============================] - 80s 3ms/step - loss: 0.8508 - acc: 0.6618 - val_loss: 0.8399 - val_acc: 0.6690
Epoch 3/10
32000/32000 [==============================] - 84s 3ms/step - loss: 0.8461 - acc: 0.6647 - val_loss: 0.8374 - val_acc: 0.6726
Epoch 4/10
32000/32000 [==============================] - 82s 3ms/step - loss: 0.8288 - acc: 0.6709 - val_loss: 0.7392 - val_acc: 0.6861
Epoch 5/10
32000/32000 [==============================] - 82s 3ms/step - loss: 0.7444 - acc: 0.6804 - val_loss: 0.6371 - val_acc: 0.7311
Epoch 6/10
32000/32000 [==============================] - 83s 3ms/step - loss: 0.5969 - acc: 0.7484 - val_loss: 0.5602 - val_acc: 0.7682
Epoch 7/10
32000/32000 [==============================] - 82s 3ms/step - loss: 0.5484 - acc: 0.7623 - val_loss: 0.5244 - val_acc: 0.7814
Epoch 8/10
32000/32000 [==============================] - 86s 3ms/step - loss: 0.5052 - acc: 0.7866 - val_loss: 0.4971 - val_acc: 0.7950
Epoch 9/10
32000/32000 [==============================] - 84s 3ms/step - loss: 0.4753 - acc: 0.8032 - val_loss: 0.4839 - val_acc: 0.7965
Epoch 10/10
32000/32000 [==============================] - 82s 3ms/step - loss: 0.4539 - acc: 0.8110 - val_loss: 0.4622 - val_acc: 0.8046

Puede ver que la precisión final del entrenamiento del modelo es del 81,10 %, mientras que la precisión de la validación es del 80,46. La diferencia es muy pequeña y, por lo tanto, asumimos que nuestro modelo no se sobreajusta a los datos de entrenamiento.

Ahora vamos a evaluar el rendimiento de nuestro modelo en el conjunto de prueba:

1
2
3
4
score = model.evaluate(X_test, y_test, verbose=1)

print("Test Score:", score[0])
print("Test Accuracy:", score[1])

La salida se ve así:

1
2
3
10000/10000 [==============================] - 37s 4ms/step
Test Score: 0.4592904740810394
Test Accuracy: 0.8101

Finalmente, grafiquemos los valores de pérdida y precisión para los conjuntos de entrenamiento y prueba:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import matplotlib.pyplot as plt

plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])

plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])

plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()

Debería ver los siguientes dos gráficos:

head{.img-responsive}

Puede ver que las líneas para las precisiones de entrenamiento y prueba y las pérdidas están bastante cerca una de la otra, lo que significa que el modelo no se está sobreajustando.

Crear un modelo solo con metainformación {#crear un modelo solo con metainformación}

En esta sección, crearemos un modelo de clasificación que utiliza información de las columnas “útil”, “divertida” y “genial” de las reseñas de Yelp. Dado que los datos de estas columnas están bien estructurados y no contienen ningún patrón secuencial o espacial, podemos usar redes neuronales simples densamente conectadas para hacer predicciones.

Grafiquemos los recuentos promedio de las reseñas “útiles”, “divertidas” y “geniales” frente a la puntuación de las reseñas.

1
2
import seaborn as sns
sns.barplot(x='reviews_score', y='useful', data=yelp_reviews)

head{.img-responsive}

A partir del resultado, puede ver que el recuento promedio de reseñas marcadas como “útiles” es el más alto para las reseñas malas, seguido por las reseñas promedio y las reseñas buenas.

Tracemos ahora el recuento promedio de reseñas “divertidas”:

1
sns.barplot(x='reviews_score', y='funny', data=yelp_reviews)

head{.img-responsive}

El resultado muestra que, nuevamente, el recuento promedio de reseñas marcadas como “divertidas” es más alto para las malas reseñas.

Finalmente, tracemos el valor promedio de la columna cool contra la columna reviews_score. Esperamos que el recuento promedio de la columna “genial” sea el más alto para las buenas reseñas, ya que las personas a menudo marcan las reseñas positivas o buenas como geniales:

1
sns.barplot(x='reviews_score', y='cool', data=yelp_reviews)

head{.img-responsive}

Como era de esperar, el recuento promedio de buenas críticas es el más alto. A partir de esta información, podemos suponer con seguridad que los valores de conteo para las columnas “útil”, “divertida” y “genial” tienen cierta correlación con las columnas “reviews_score”. Por lo tanto, intentaremos usar los datos de estas tres columnas para entrenar nuestro algoritmo que predice el valor de la columna reviews_score.

Filtremos estas tres columnas de nuestro conjunto de datos:

1
2
3
4
5
yelp_reviews_meta = yelp_reviews[['useful', 'funny', 'cool']]

X = yelp_reviews_meta.values

y = yelp_reviews['reviews_score']

A continuación, convertiremos nuestras etiquetas en valores codificados en caliente y luego dividiremos nuestros datos en conjuntos de entrenamiento y prueba:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from sklearn import preprocessing

# label_encoder object knows how to understand word labels.
label_encoder = preprocessing.LabelEncoder()

# Encode labels in column 'species'.
y = label_encoder.fit_transform(y)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)

from keras.utils import to_categorical
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

El siguiente paso es crear nuestro modelo. Nuestro modelo constará de cuatro capas (puede probar cualquier número): la capa de entrada, dos capas ocultas densas con 10 neuronas y funciones de activación relu y, finalmente, una capa densa de salida con 3 neuronas y función de activación softmax. La función de pérdida y el optimizador serán categorical_crossentropy y adam, respectivamente.

El siguiente script define el modelo:

1
2
3
4
5
6
7
input2 = Input(shape=(3,))
dense_layer_1 = Dense(10, activation='relu')(input2)
dense_layer_2 = Dense(10, activation='relu')(dense_layer_1)
output = Dense(3, activation='softmax')(dense_layer_2)

model = Model(inputs=input2, outputs=output)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

Imprimimos el resumen del modelo:

1
print(model.summary())
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 3)                 0
_________________________________________________________________
dense_1 (Dense)              (None, 10)                40
_________________________________________________________________
dense_2 (Dense)              (None, 10)                110
_________________________________________________________________
dense_3 (Dense)              (None, 3)                 33
=================================================================
Total params: 183
Trainable params: 183
Non-trainable params: 0

Finalmente, el diagrama de bloques para el modelo se puede crear mediante el siguiente script:

1
2
from keras.utils import plot_model
plot_model(model, to_file='model_plot2.png', show_shapes=True, show_layer_names=True)

Ahora, si abre el archivo model_plot2.png desde su ruta de archivo local, se ve así:

head{.img-responsive}

Ahora entrenemos el modelo e imprimamos los valores de precisión y pérdida para cada época:

1
history = model.fit(X_train, y_train, batch_size=16, epochs=10, verbose=1, validation_split=0.2)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Train on 32000 samples, validate on 8000 samples
Epoch 1/10
32000/32000 [==============================] - 8s 260us/step - loss: 0.8429 - acc: 0.6649 - val_loss: 0.8166 - val_acc: 0.6734
Epoch 2/10
32000/32000 [==============================] - 7s 214us/step - loss: 0.8203 - acc: 0.6685 - val_loss: 0.8156 - val_acc: 0.6737
Epoch 3/10
32000/32000 [==============================] - 7s 217us/step - loss: 0.8187 - acc: 0.6685 - val_loss: 0.8150 - val_acc: 0.6736
Epoch 4/10
32000/32000 [==============================] - 7s 220us/step - loss: 0.8183 - acc: 0.6695 - val_loss: 0.8160 - val_acc: 0.6740
Epoch 5/10
32000/32000 [==============================] - 7s 227us/step - loss: 0.8177 - acc: 0.6686 - val_loss: 0.8149 - val_acc: 0.6751
Epoch 6/10
32000/32000 [==============================] - 7s 219us/step - loss: 0.8175 - acc: 0.6686 - val_loss: 0.8157 - val_acc: 0.6744
Epoch 7/10
32000/32000 [==============================] - 7s 216us/step - loss: 0.8172 - acc: 0.6696 - val_loss: 0.8145 - val_acc: 0.6733
Epoch 8/10
32000/32000 [==============================] - 7s 214us/step - loss: 0.8175 - acc: 0.6689 - val_loss: 0.8139 - val_acc: 0.6734
Epoch 9/10
32000/32000 [==============================] - 7s 215us/step - loss: 0.8169 - acc: 0.6691 - val_loss: 0.8160 - val_acc: 0.6744
Epoch 10/10
32000/32000 [==============================] - 7s 216us/step - loss: 0.8167 - acc: 0.6694 - val_loss: 0.8138 - val_acc: 0.6736

A partir de la salida, puede ver que nuestro modelo no converge y los valores de precisión permanecen entre 66 y 67 en todas las épocas.

Veamos cómo se comporta el modelo en el conjunto de prueba:

1
2
3
4
score = model.evaluate(X_test, y_test, verbose=1)

print("Test Score:", score[0])
print("Test Accuracy:", score[1])
1
2
3
10000/10000 [==============================] - 0s 34us/step
Test Score: 0.8206425309181213
Test Accuracy: 0.6669

Podemos imprimir los valores de pérdida y precisión para los conjuntos de entrenamiento y prueba a través del siguiente script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import matplotlib.pyplot as plt

plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])

plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])

plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()

head{.img-responsive}

A partir de la salida, puede ver que los valores de precisión son relativamente más bajos. Por lo tanto, podemos decir que nuestro modelo no se ajusta correctamente. La precisión se puede aumentar aumentando el número de capas densas o aumentando el número de épocas, sin embargo, eso se lo dejo a usted.

Pasemos a la sección final y más importante de este artículo, donde usaremos múltiples entradas de diferentes tipos para entrenar nuestro modelo.

Creación de un modelo con múltiples entradas {#creación de un modelo con múltiples entradas}

En las secciones anteriores, vimos cómo entrenar modelos de aprendizaje profundo utilizando datos textuales o metainformación. ¿Qué sucede si queremos combinar información textual con metainformación y usarla como entrada para nuestro modelo? Podemos hacerlo usando la API funcional de Keras. En esta sección crearemos dos submodelos.

El primer submodelo aceptará entradas de texto en forma de revisiones de texto. Este submodelo constará de una capa de forma de entrada, una capa de incrustación y una capa LSTM de 128 neuronas. El segundo submodelo aceptará entradas en forma de metainformación de las columnas útil, divertida y genial. El segundo submodelo también consta de tres capas. Una capa de entrada y dos capas densas.

La salida de la capa LSTM del primer submodelo y la salida de la segunda capa densa del segundo submodelo se concatenarán juntas y se usarán como entrada concatenada a otra capa densa con 10 neuronas. Finalmente, la capa densa de salida tendrá tres neuronas correspondientes a cada tipo de revisión.

Veamos cómo podemos crear un modelo concatenado de este tipo.

Primero tenemos que crear dos tipos diferentes de entradas. Para hacerlo, dividiremos nuestros datos en un conjunto de características y un conjunto de etiquetas, como se muestra a continuación:

1
2
3
X = yelp_reviews.drop('reviews_score', axis=1)

y = yelp_reviews['reviews_score']

La variable X contiene el conjunto de funciones, mientras que la variable y contiene el conjunto de etiquetas. Necesitamos convertir nuestras etiquetas en vectores codificados one-hot. Podemos hacerlo usando el codificador de etiquetas y la función to_categorical del módulo keras.utils. También dividiremos nuestros datos en entrenamiento y conjunto de características.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from sklearn import preprocessing

# label_encoder object knows how to understand word labels.
label_encoder = preprocessing.LabelEncoder()

# Encode labels in column 'species'.
y = label_encoder.fit_transform(y)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)

from keras.utils import to_categorical
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

Ahora nuestro conjunto de etiquetas tiene la forma requerida. Dado que solo habrá una salida, no necesitamos procesar nuestro conjunto de etiquetas. Sin embargo, habrá múltiples entradas al modelo. Por lo tanto, necesitamos preprocesar nuestro conjunto de funciones.

Primero creemos la función preproces_text que se usará para preprocesar nuestro conjunto de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def preprocess_text(sen):

    # Remove punctuations and numbers
    sentence = re.sub('[^a-zA-Z]', ' ', sen)

    # Single character removal
    sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)

    # Removing multiple spaces
    sentence = re.sub(r'\s+', ' ', sentence)

    return sentence

Como primer paso, crearemos una entrada de texto para el conjunto de entrenamiento y prueba. Mira el siguiente guión:

1
2
3
4
X1_train = []
sentences = list(X_train["text"])
for sen in sentences:
    X1_train.append(preprocess_text(sen))

Ahora X1_train contiene la entrada textual para el conjunto de entrenamiento. De manera similar, el siguiente script preprocesa los datos de entrada textuales para el conjunto de prueba:

1
2
3
4
X1_test = []
sentences = list(X_test["text"])
for sen in sentences:
    X1_test.append(preprocess_text(sen))

Ahora necesitamos convertir la entrada de texto para los conjuntos de entrenamiento y prueba en forma numérica usando incrustaciones de palabras. El siguiente script hace eso:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
tokenizer = Tokenizer(num_words=5000)
tokenizer.fit_on_texts(X1_train)

X1_train = tokenizer.texts_to_sequences(X1_train)
X1_test = tokenizer.texts_to_sequences(X1_test)

vocab_size = len(tokenizer.word_index) + 1

maxlen = 200

X1_train = pad_sequences(X1_train, padding='post', maxlen=maxlen)
X1_test = pad_sequences(X1_test, padding='post', maxlen=maxlen)

Nuevamente usaremos incrustaciones de palabras GloVe para crear vectores de palabras:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from numpy import array
from numpy import asarray
from numpy import zeros

embeddings_dictionary = dict()

glove_file = open('/content/drive/My Drive/glove.6B.100d.txt', encoding="utf8")

for line in glove_file:
    records = line.split()
    word = records[0]
    vector_dimensions = asarray(records[1:], dtype='float32')
    embeddings_dictionary[word] = vector_dimensions

glove_file.close()

embedding_matrix = zeros((vocab_size, 100))
for word, index in tokenizer.word_index.items():
    embedding_vector = embeddings_dictionary.get(word)
    if embedding_vector is not None:
        embedding_matrix[index] = embedding_vector

Hemos preprocesado nuestra entrada de texto. El segundo tipo de entrada es la metainformación en las columnas útil, divertida y genial. Filtraremos estas columnas del conjunto de características para crear una meta entrada para entrenar los algoritmos. Mira el siguiente guión:

1
2
X2_train = X_train[['useful', 'funny', 'cool']].values
X2_test = X_test[['useful', 'funny', 'cool']].values

Ahora vamos a crear nuestras dos capas de entrada. La primera capa de entrada se usará para ingresar la entrada de texto y la segunda capa de entrada se usará para ingresar metainformación de las tres columnas.

1
2
3
input_1 = Input(shape=(maxlen,))

input_2 = Input(shape=(3,))

Puede ver que la primera capa de entrada input_1 se usa para la entrada de texto. El tamaño de la forma se ha ajustado a la forma de la oración de entrada. Para la segunda capa de entrada, la forma corresponde a tres columnas.

Ahora vamos a crear el primer submodelo que acepte datos de la primera capa de entrada:

1
2
embedding_layer = Embedding(vocab_size, 100, weights=[embedding_matrix], trainable=False)(input_1)
LSTM_Layer_1 = LSTM(128)(embedding_layer)

De manera similar, la siguiente secuencia de comandos crea un segundo submodelo que acepta la entrada de la segunda capa de entrada:

1
2
dense_layer_1 = Dense(10, activation='relu')(input_2)
dense_layer_2 = Dense(10, activation='relu')(dense_layer_1)

Ahora tenemos dos submodelos. Lo que queremos hacer es concatenar la salida del primer submodelo con la salida del segundo submodelo. La salida del primer submodelo es la salida de LSTM_Layer_1 y, de manera similar, la salida del segundo submodelo es la salida de dense_layer_2. Podemos usar la clase Concatenar del módulo keras.layers.merge para concatenar dos entradas.

El siguiente script crea nuestro modelo final:

1
2
3
4
concat_layer = Concatenate()([LSTM_Layer_1, dense_layer_2])
dense_layer_3 = Dense(10, activation='relu')(concat_layer)
output = Dense(3, activation='softmax')(dense_layer_3)
model = Model(inputs=[input_1, input_2], outputs=output)

Puede ver que ahora nuestro modelo tiene una lista de entradas con dos elementos. El siguiente script compila el modelo e imprime su resumen:

1
2
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])
print(model.summary())

El resumen del modelo es el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            (None, 200)          0
__________________________________________________________________________________________________
input_2 (InputLayer)            (None, 3)            0
__________________________________________________________________________________________________
embedding_1 (Embedding)         (None, 200, 100)     5572900     input_1[0][0]
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 10)           40          input_2[0][0]
__________________________________________________________________________________________________
lstm_1 (LSTM)                   (None, 128)          117248      embedding_1[0][0]
__________________________________________________________________________________________________
dense_2 (Dense)                 (None, 10)           110         dense_1[0][0]
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 138)          0           lstm_1[0][0]
                                                                 dense_2[0][0]
__________________________________________________________________________________________________
dense_3 (Dense)                 (None, 10)           1390        concatenate_1[0][0]
__________________________________________________________________________________________________
dense_4 (Dense)                 (None, 3)            33          dense_3[0][0]
==================================================================================================
Total params: 5,691,721
Trainable params: 118,821
Non-trainable params: 5,572,900

Finalmente, podemos trazar el modelo de red completo usando el siguiente script:

1
2
from keras.utils import plot_model
plot_model(model, to_file='model_plot3.png', show_shapes=True, show_layer_names=True)

Si abre el archivo model_plot3.png, debería ver el siguiente diagrama de red:

head{.img-responsive}

La figura anterior explica claramente cómo hemos concatenado múltiples entradas en una entrada para crear nuestro modelo.

Ahora entrenemos nuestro modelo y veamos los resultados:

1
history = model.fit(x=[X1_train, X2_train], y=y_train, batch_size=128, epochs=10, verbose=1, validation_split=0.2)

Aquí está el resultado para las 10 épocas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Train on 32000 samples, validate on 8000 samples
Epoch 1/10
32000/32000 [==============================] - 155s 5ms/step - loss: 0.9006 - acc: 0.6509 - val_loss: 0.8233 - val_acc: 0.6704
Epoch 2/10
32000/32000 [==============================] - 154s 5ms/step - loss: 0.8212 - acc: 0.6670 - val_loss: 0.8141 - val_acc: 0.6745
Epoch 3/10
32000/32000 [==============================] - 154s 5ms/step - loss: 0.8151 - acc: 0.6691 - val_loss: 0.8086 - val_acc: 0.6740
Epoch 4/10
32000/32000 [==============================] - 155s 5ms/step - loss: 0.8121 - acc: 0.6701 - val_loss: 0.8039 - val_acc: 0.6776
Epoch 5/10
32000/32000 [==============================] - 154s 5ms/step - loss: 0.8027 - acc: 0.6740 - val_loss: 0.7467 - val_acc: 0.6854
Epoch 6/10
32000/32000 [==============================] - 155s 5ms/step - loss: 0.6791 - acc: 0.7158 - val_loss: 0.5764 - val_acc: 0.7560
Epoch 7/10
32000/32000 [==============================] - 154s 5ms/step - loss: 0.5333 - acc: 0.7744 - val_loss: 0.5076 - val_acc: 0.7881
Epoch 8/10
32000/32000 [==============================] - 154s 5ms/step - loss: 0.4857 - acc: 0.7973 - val_loss: 0.4849 - val_acc: 0.7970
Epoch 9/10
32000/32000 [==============================] - 154s 5ms/step - loss: 0.4697 - acc: 0.8034 - val_loss: 0.4709 - val_acc: 0.8024
Epoch 10/10
32000/32000 [==============================] - 154s 5ms/step - loss: 0.4479 - acc: 0.8123 - val_loss: 0.4592 - val_acc: 0.8079

Para evaluar nuestro modelo, tendremos que pasar ambas entradas de prueba a la función evaluar como se muestra a continuación:

1
2
3
4
score = model.evaluate(x=[X1_test, X2_test], y=y_test, verbose=1)

print("Test Score:", score[0])
print("Test Accuracy:", score[1])

Aquí está el resultado:

1
2
3
10000/10000 [==============================] - 18s 2ms/step
Test Score: 0.4576087875843048
Test Accuracy: 0.8053

La precisión de nuestra prueba es del 80,53 %, que es un poco menos que nuestro primer modelo que solo usa entrada de texto. Esto muestra que la metainformación en yelp_reviews no es muy útil para la predicción de sentimientos.

De todos modos, ¡ahora sabe cómo crear un modelo de entrada múltiple para la clasificación de texto en Keras!

Finalmente, ahora imprimamos la pérdida y la precisión para los conjuntos de entrenamiento y prueba:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import matplotlib.pyplot as plt

plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])

plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])

plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()

head{.img-responsive}

Puede ver que las diferencias en los valores de pérdida y precisión son mínimas entre los conjuntos de entrenamiento y prueba, por lo tanto, nuestro modelo no se sobreajusta.

Yendo más lejos: proyecto de extremo a extremo portátil

¿Tu naturaleza inquisitiva te hace querer ir más allá? Recomendamos consultar nuestro Proyecto guiado: ["Subtítulos de imágenes con CNN y Transformers con Keras"](https://wikihtp.com/courses/image-captioning-with-cnns-and -transformadores-con-keras/#cta){target="_blank"}.

En este proyecto guiado, aprenderá a crear un modelo de subtítulos de imágenes, que acepta una imagen como entrada y produce un subtítulo de texto como salida.

Aprenderás a:

  • Preprocesar texto
  • Vectorizar la entrada de texto fácilmente
  • Trabaje con la API tf.data y cree conjuntos de datos de alto rendimiento
  • Cree Transformers desde cero con TensorFlow/Keras y KerasNLP: la adición horizontal oficial a Keras para crear modelos NLP de última generación
  • Cree arquitecturas híbridas donde la salida de una red se codifica para otra

¿Cómo enmarcamos los subtítulos de las imágenes? La mayoría lo considera un ejemplo de aprendizaje profundo generativo, porque estamos enseñando a una red a generar descripciones. Sin embargo, me gusta verlo como una instancia de traducción automática neuronal: estamos traduciendo las características visuales de una imagen en palabras. A través de la traducción, estamos generando una nueva representación de esa imagen, en lugar de simplemente generar un nuevo significado. Verlo como traducción, y solo por generación de extensión, enfoca la tarea bajo una luz diferente y la hace un poco más intuitiva.

Enmarcar el problema como uno de traducción hace que sea más fácil averiguar qué arquitectura querremos usar. Los transformadores solo de codificador son excelentes para comprender el texto (análisis de opinión, clasificación, etc.) porque los codificadores codifican representaciones significativas. Los modelos de solo decodificador son excelentes para la generación (como GPT-3), ya que los decodificadores pueden inferir representaciones significativas en otra secuencia con el mismo significado. La traducción generalmente se realiza mediante una arquitectura de codificador-decodificador, donde los codificadores codifican una representación significativa de una oración (o imagen, en nuestro caso) y los decodificadores aprenden a convertir esta secuencia en otra representación significativa que es más interpretable para nosotros (como una oración).

Pensamientos finales y mejoras

En este artículo, construimos una red neuronal muy simple ya que el propósito del artículo es explicar cómo crear un modelo de aprendizaje profundo que acepte múltiples entradas de diferentes tipos.

Los siguientes son algunos de los consejos que puede seguir para mejorar aún más el rendimiento del modelo de clasificación de texto:

  1. Solo usamos 50 000, de los 5,2 millones de registros de este artículo, ya que teníamos restricciones de hardware. Puede intentar entrenar su modelo en una mayor cantidad de registros y ver si puede lograr un mejor rendimiento.
  2. Intente agregar más LSTM y capas densas al modelo. Si el modelo se sobreajusta, intente agregar abandono.
  3. Intente cambiar la función del optimizador y entrene el modelo con un mayor número de épocas.

Comparta sus resultados junto con la configuración de la red neuronal en la sección de comentarios. Me encantaría ver qué tan bien lo hiciste.