5.lab 2. Deep Learning para Texto

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 12

Inteligencia en Red

Curso 2021/22

Deep Learning
para texto

Julio Villena Román


[email protected]

1
Deep Learning para texto

1. Objetivos

El objetivo de la práctica es realizar una introducción a las técnicas de Deep Learning


aplicadas en otro escenario típico: clasificación de texto.

[Esta práctica está basada en gran parte en https://realpython.com/python-keras-text-classification/]

2. Análisis de sentimiento en comentarios con Keras

2.1 Carga de los datos

Para esta práctica vamos a utilizar Google Colaboratory. Abre un cuaderno nuevo en tu
carpeta Drive, y asegúrate de configurar el entorno de ejecución para utilizar GPU.

Para empezar, activa Google Drive en el cuaderno, y ejecuta la siguiente instrucción en


una celda para cambiar el directorio de trabajo donde poder cargar ficheros, donde
quieras, por ejemplo:

cd /content/drive/My Drive/Colab InRed

Vamos a emplear un conjunto de datos clásico del UCI Machine Learning Repository
llamado “Sentiment Labelled Sentences”, que contiene comentarios en IMDb, Amazon,
y Yelp, cada uno marcado con 0 si es negativo o 1 si es positivo:

https://archive.ics.uci.edu/ml/datasets/Sentiment+Labelled+Sentences

Ve a “Data Folder”, descarga el fichero “sentiment labelled sentences.zip” y


descomprímelo dejando los ficheros en la carpeta de trabajo que has definido más arriba.
Deben quedar tres archivos: “yelp_labelled.txt”, “amazon_cells_labelled.txt” e
“imdb_labelled.txt”.

A continuación, vamos a cargar y visualizar los datos:

import pandas as pd
filepath_dict = {'yelp': 'yelp_labelled.txt',
'amazon': 'amazon_cells_labelled.txt',
'imdb': 'imdb_labelled.txt'}
df_list = []
for source, filepath in filepath_dict.items():
df = pd.read_csv(filepath, names=['sentence', 'label'], sep='\t')
df['source'] = source # Add another column filled with the source name
df_list.append(df)
df = pd.concat(df_list)
print(df.shape)
df.head()

Para representar los histogramas:

2
import seaborn as sns
sns.countplot(df['label'])

2.2 Vectorización
El primer paso del proceso es convertir el texto (cada frase) en un vector de
características con el que poder ejecutar algoritmos de aprendizaje automático. La
representación habitual se basa en el modelo de espacio de vectores, donde cada texto
se representa como un vector donde cada dimensión corresponde a una palabra, y el
valor de dicha dimensión es el peso de la palabra en el texto. El modelo más sencillo, el
booleano, contiene un 0 si la palabra no está en el texto y un 1 si sí lo está.

En scikit-learn hay funcionalidad útil para obtener esta representación. El siguiente


código imprime el vocabulario y los vectores de las frases proporcionadas como ejemplo:

# ejemplo de vectorización
sentences = ['John likes ice cream', 'John hates chocolate.']
from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(min_df=0, lowercase=False)


vectorizer.fit(sentences)
print("Vocabulary:", vectorizer.vocabulary_)
print("Vectors: ", vectorizer.transform(sentences).toarray())

Como se observa, las palabras se toman directamente del texto, sin más filtrado ni
procesamiento. En muchas ocasiones es conveniente hacer un filtrado del texto, para
tratar con signos de puntuación, conversión a minúsculas, lematización o “stemming”,
etc. Analiza el siguiente código con un ejemplo de función de procesamiento que se
ejecutaría para limpiar el texto antes de realizar la vectorización, que utiliza la biblioteca
NLTK para procesamiento de lenguaje natural:

# ejemplo de procesamiento con stemming


import re
import nltk
from nltk.stem import PorterStemmer
from nltk.corpus import stopwords

nltk.download('stopwords')
nltk.download('wordnet')

stop_words = set(stopwords.words('english'))
st = PorterStemmer()

3
def preprocess_text(sentence):
# Remove punctuations and numbers
sentence = re.sub('[^a-zA-Z]', ' ', sentence)
# Single character removal
sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)
# Removing multiple spaces
sentence = re.sub(r'\s+', ' ', sentence)
# To lowercase
sentence = sentence.lower()
sentence = " ".join([w for w in sentence.split() if w not in stop_words])
sentence = " ".join([st.stem(w) for w in sentence.split()])
return sentence

sentences_pre = []
for sentence in sentences:
sentences_pre.append(preprocess_text(sentence))
print(sentences)
print(sentences_pre)

Compara con el empleo de un lematizador en vez de un “stemmer”, cambiando


st.stem() por wordnet_lemmatizer.lemmatize():

# ejemplo de procesamiento con lematización


from nltk.stem import WordNetLemmatizer
wordnet_lemmatizer = WordNetLemmatizer()

sentence = " ".join([wordnet_lemmatizer.lemmatize(w, pos='v') for w in sen
tence.split()])

2.3 Regresión logística


Como experimento base, vamos primero a entrenar un modelo sencillo basado en
regresión logística:

# separación train/test
from sklearn.model_selection import train_test_split
sentences = df['sentence'].values
y = df['label'].values
sentences_train, sentences_test, y_train, y_test = train_test_split(sentences,
y, test_size=0.25, random_state=1000)

# vectorización
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=2)
vectorizer.fit(sentences_train)
X_train = vectorizer.transform(sentences_train)
X_test = vectorizer.transform(sentences_test)

# modelado

4
from sklearn.linear_model import LogisticRegression
classifier = LogisticRegression()
classifier.fit(X_train, y_train)
score = classifier.score(X_test, y_test)
print("Accuracy:", score)

La precisión de base es bastante alta, como se puede observar. Pero ¿es posible
aumentarla?

2.4 Perceptrón multicapa


El primer modelo de Deep Learning que vamos a implementar en Keras tiene dos
perceptrones en serie, es decir, un perceptrón multicapa, con una capa de “input_dim”
entradas, luego una capa de 10 neuronas, y finalmente una capa de salida con 1
neurona.

from keras.models import Sequential


from keras import layers

input_dim = X_train.shape[1] # Number of features

model = Sequential()
model.add(layers.Dense(10, input_dim=input_dim, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy
'])
model.summary()

La primera capa tiene 18480 parámetros: “input_dim” (el tamaño del vector de los datos
de entrada, 1847) multiplicado por 10 (un peso por cada neurona) más 10 parámetros
(el “bias”).

El proceso de entrenamiento y evaluación es el siguiente:

history = model.fit(X_train, y_train, epochs=100, validation_data=(X_test, y_t


est), batch_size=10)
loss, accuracy = model.evaluate(X_train, y_train, verbose=False)
print("Training Accuracy: {:.4f}".format(accuracy))
loss, accuracy = model.evaluate(X_test, y_test, verbose=False)
print("Testing Accuracy: {:.4f}".format(accuracy))

Tras ejecutarlo, se ve que la elevada diferencia entre “training” y “testing” indica


claramente que hay sobreentrenamiento. Este hecho se puede observar representando
la evolución del proceso de entrenamiento gráficamente, con la siguiente función
auxiliar:

import matplotlib.pyplot as plt


plt.style.use('ggplot')

def plot_history(history):
acc = history.history['accuracy']

5
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
x = range(1, len(acc) + 1)

plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(x, acc, 'b', label='Training acc')
plt.plot(x, val_acc, 'r', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(x, loss, 'b', label='Training loss')
plt.plot(x, val_loss, 'r', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plot_history(history)

Este gráfico indica claramente el sobreentrenamiento. Habría que reducir el número de


“epochs” hasta el momento que empieza a subir el error de validación.

2.5 Representación como secuencias


La representación del texto como vector tiene un problema: que se pierde el orden de
aparición de las palabras en el texto, su contexto, así que se está perdiendo significado.

Otra representación es emplear en modelo de secuencias. Primero se obtiene el


vocabulario, igual que en la vectorización, pero cada texto se representa como una
secuencia de identificadores de palabras (“tokens”). Por ejemplo, si la frase es “Definitely
worth checking out”, su representación como secuencia sería algo como [170, 116, 390,
35] donde 170 sería el identificador de “definitely”, 116 el de “worth”, etc.

La biblioteca Keras nos proporciona funcionalidad para convertir en secuencias:

from keras.preprocessing.text import Tokenizer

tokenizer = Tokenizer(num_words=5000, filters='!"#$%&()*+,-


./:;<=>?@[\\]^_`{|}~\t\n', lower=True, split=" ")
tokenizer.fit_on_texts(sentences_train)

6
vocab_size = len(tokenizer.word_index) + 1

print("Vocabulary:", tokenizer.index_word)
print("Vocabulary size:", vocab_size)

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

print("Primera frase:")
print(sentences_train[0])
print(X_train[0])

Pero esta representación también tiene el inconveniente que la secuencia de cada texto
tiene una longitud diferente, y todos los algoritmos de aprendizaje automático, incluidas
las redes neuronales que se emplean en Deep Learning, deben tener un número fijo de
atributos de entrada.

Como solución, estas secuencias se “rellenan” (“padding” en inglés) hasta una


determinada longitud (“maxlen”):

from keras.preprocessing.sequence import pad_sequences

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

print("Primera frase:")
print(X_train[0])

Así, todas las secuencias tendrían longitud fija de 50. El “padding” se puede hacer al
final (“post”) o al principio (“pre”), que en general da igual.

Compara esta representación entrenando con el mismo modelo anterior. En este caso,
las dimensiones de la entrada es la longitud de la secuencia.

# construir el modelo
model = Sequential()
model.add(layers.Dense(10, input_dim=maxlen, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy
'])
model.summary()
# entrenar
history = model.fit(X_train, y_train, epochs=100, validation_data=(X_test, y_t
est), batch_size=10)
# evaluar
loss, accuracy = model.evaluate(X_train, y_train, verbose=False)
print("Training Accuracy: {:.4f}".format(accuracy))
loss, accuracy = model.evaluate(X_test, y_test, verbose=False)
print("Testing Accuracy: {:.4f}".format(accuracy))
plot_history(history)

7
Puedes subir o bajar la longitud de la secuencia, pero los resultados del entrenamiento
son peores que con la representación anterior con vectorización. Resulta mucho más
difícil “aprender” este modelo.

2.6 Embeddings
Con la representación con secuencias, una capa inicial de “Embeddings” permite mejorar
el proceso de entrenamiento. Cada palabra de entrada se “mapea” en un vector de unas
ciertas dimensiones (“embedding_dim”), y posteriormente los vectores de todas las
palabras se concatenan con una capa “Flatten” para obtener un único vector, de mucha
más alta dimensionalidad.

El modelo sería el siguiente (ejecutar con secuencias con maxlen=100):

embedding_dim = 50

# construir modelo
model = Sequential()
model.add(layers.Embedding(input_dim=vocab_size,
output_dim=embedding_dim,
input_length=maxlen))
model.add(layers.Flatten())
model.add(layers.Dense(10, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
model.summary()

El número de parámetros de la capa de “Embeddings” es el tamaño del vocabulario por


la dimensión de los vectores de “embeddings”.

El entrenamiento y evaluación del proceso es como el anterior, siempre igual. Ejecuta


por ejemplo con 20 “epochs”. Según se ve en el gráfico de evolución del error, con estas
20 “epochs” el modelo sobreentrena. Ajusta el número de “epochs” para obtener el
máximo de precisión. Se consigue una precisión similar al modelo base con regresión
logística.

2.7 Etiquetar un texto


Para etiquetar un texto nuevo simplemente hay que obtener su representación como
secuencia y llamar a “predict”:

examples = tokenizer.texts_to_sequences(['The house was awful.',


'The house was really beautiful'])
X_examples = pad_sequences(examples, padding='post', maxlen=maxlen)
print(X_examples)
model.predict(X_examples)

Si la salida está más cerca de 0, el sentimiento será negativo, mientras que si está cerca
de 1 será positivo.

8
2.8 MaxPooling
El problema con la representación del texto en secuencias más la capa de “embeddings”
es que el número de parámetros es muy elevado, así que es fácil caer en
sobreentrenamiento y el problema del desvanecimiento del gradiente. Por ello, vamos a
sustituir la capa “Flatten” (que concatena todos los vectores de “embeddings” por una
capa de “pooling”, por ejemplo “GlobalMaxPool1D”, que hace un muestreo
(regularización), mejorando así el proceso de aprendizaje.

El modelo sería:

model = Sequential()
model.add(layers.Embedding(input_dim=vocab_size,
output_dim=embedding_dim,
input_length=maxlen))
model.add(layers.GlobalMaxPool1D())
model.add(layers.Dense(10, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

Si ejecutas con un número apropiado de “epochs” para no sobreentrenar, puedes


obtener una precisión de más del 0.83, la más alta hasta el momento.

Y añadiendo una capa de “Dropout” entre las dos capas “Dense” es posible mejorar un
poco más la precisión, hasta 0.84:

model.add(layers.Dense(10, activation='relu'))
model.add(layers.Dropout(0.7))
model.add(layers.Dense(1, activation='sigmoid'))

2.9 Embeddings pre-entrenados


En vez de partir de cero en la capa de embeddings con vectores aleatorios, es posible
utilizar unos vectores preentrenados. Existen diferentes algoritmos, los más populares
son Word2Vec de Google y GloVe de Stanford.

Aquí vamos a utilizar GloVe:


https://nlp.stanford.edu/projects/glove/

Primero hay que bajar los datos (entrenados con 6 mil millones de palabras) y
descomprimirlos (822MB):

!wget http://nlp.stanford.edu/data/glove.6B.zip
!unzip glove.6B.zip
!head -n 1 glove.6B.50d.txt | cut -c-50

Se proporcionan “embeddings” de diferentes dimensiones, aunque nosotros vamos a


usar aquí los “embeddings” de dimensión 50. Y no necesitamos todas las palabras, así
que vamos a filtrar a nuestras palabras del vocabulario:

import numpy as np
def create_embedding_matrix(filepath, word_index, embedding_dim):

9
vocab_size = len(word_index) + 1 # Adding 1 because of reserved 0 index
embedding_matrix = np.zeros((vocab_size, embedding_dim))
with open(filepath) as f:
for line in f:
word, *vector = line.split()
if word in word_index:
idx = word_index[word]
embedding_matrix[idx] = np.array(
vector, dtype=np.float32)[:embedding_dim]
return embedding_matrix

embedding_dim = 50
embedding_matrix = create_embedding_matrix('glove.6B.50d.txt', tokenizer.word_
index, embedding_dim)

Para cargar estos “embeddings” en el modelo sólo hay que cambiar la primera capa:

model.add(layers.Embedding(vocab_size, embedding_dim,
weights=[embedding_matrix],
input_length=maxlen,
trainable=True))

El parámetro “trainable” permite entrenar más los vectores utilizando los datos. Los
resultados pueden mejorar ligeramente también.

2.10 CNN
El siguiente modelo que vamos a utilizar es una capa convolucional. Sólo hay que añadir
una capa entre los “embeddings” y el “pooling”:

embedding_dim = 100

model = Sequential()
model.add(layers.Embedding(vocab_size, embedding_dim, input_length=maxlen))
model.add(layers.Conv1D(128, 5, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(10, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
model.summary()

Variando adecuamente los parámetros, es posible llegar a un 0.85 de precisión.

Un tipo especial de capa convolucional es la LSTM (Long-Short Term Memory). Podemos


sustituir la capa “Conv1D” por:

model.add(layers.LSTM(128))

10
Un inconveniente de estas capas es que necesitan mucho texto de entrenamiento, así
que los resultados no van a ser tan buenos.

3. Similitud semántica

En este ejercicio haremos uso de una biblioteca llamada “sentence-transformers”:


https://github.com/UKPLab/sentence-transformers

Lo primero que hay que hacer es instalarla:

!pip install -U sentence-transformers

En primer lugar, cargamos uno de los modelos preentrenados para similitud semántica:
“distilbert-base-nli-stsb-mean-tokens”. Hay más de 100 modelos preentrenados, y es
posible entrenar modelos propios:
https://www.sbert.net/docs/pretrained_models.html

from sentence_transformers import SentenceTransformer, util


import numpy as np

embedder = SentenceTransformer('distilbert-base-nli-stsb-mean-tokens')

Luego generamos los “sentence embeddings” del corpus de textos, es decir, los
vectores de significado de cada texto.

corpus = ['A man is eating food.',


'A man is eating a piece of bread.',
'The girl is carrying a baby.',
'A man is riding a horse.',
'A woman is playing violin.',
'Two men pushed carts through the woods.',
'A man is riding a white horse on an enclosed ground.',
'A monkey is playing drums.',
'A cheetah is running behind its prey.']
corpus_embeddings = embedder.encode(corpus, convert_to_tensor=True)

Para calcular la similitud semántica entre una consulta (“query”) y el corpus, sólo hay
que obtener el vector de la consulta y calcular la distancia con cada vector del corpus,
utilizando por ejemplo la distancia del coseno (“cosine similarity”). Definimos un
método que encapsula esta funcionalidad:

def query(embedder, corpus, query, k=2):


query_embedding = embedder.encode([query], convert_to_tensor=True)
cos_scores = util.pytorch_cos_sim(query_embedding, corpus_embeddings)[0]
cos_scores = cos_scores.cpu()
top_results = np.argpartition(-cos_scores, range(top_k))[0:top_k]
print("\nQuery:", query)
for idx in top_results[0:top_k]:
print(corpus[idx].strip(), "(Score: %.4f)" % (cos_scores[idx]))

11
Y ya sólo queda invocarlo con varios ejemplos:

query(embedder, corpus, 'A man is eating pasta.')


query(embedder, corpus, 'Someone in a gorilla costume is playing a set of drum
s.')
query(embedder, corpus, 'A cheetah chases prey on across a field.')

Como se puede observar, las frases más cercanas del corpus son las más parecidas
semánticamente a la consulta proporcionada.

El modelo empleado es para inglés. Si cambiamos a un modelo multilingüe, por ejemplo


“distiluse-base-multilingual-cased”, podemos hacer comparaciones semánticas entre
diferentes idiomas:

query(embedder, corpus, 'Una persona comiendo pasta.')


query(embedder, corpus, 'El músico toca la guitarra.')
query(embedder, corpus, 'El buitre atrapa a su presa.')

Query: Una persona comiendo pasta.


A man is eating food. (Score: 0.6918)

Query: El músico toca la guitarra.


A woman is playing violin. (Score: 0.5338)

Query: El buitre atrapa a su presa.


A cheetah is running behind its prey. (Score: 0.4382)

Se podría aplicar a cualquier ejemplo de búsqueda de similitud, por ejemplo, para


encontrar noticias relacionadas entre varios medios de comunicación, o en un sistema
de “question answering” para buscar la pregunta frecuente que corresponde mejor con
una consulta data.

4. Evaluación

Esta práctica se realiza por parejas.

Entregable: informe con una descripción del proceso que se ha desarrollado, capturas
de pantalla de su ejecución, y resumen de qué has aprendido en el proceso.

Calificación:
• 0 puntos: no llega mínimamente a los requisitos exigidos.
• 1 puntos: realización incompleta de las tareas o análisis superficial.
• 3 puntos: análisis completo y demostrando los conocimientos adquiridos.

12

También podría gustarte