5.lab 2. Deep Learning para Texto
5.lab 2. Deep Learning para Texto
5.lab 2. Deep Learning para Texto
Curso 2021/22
Deep Learning
para texto
1
Deep Learning para texto
1. Objetivos
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.
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
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()
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á.
# ejemplo de vectorización
sentences = ['John likes ice cream', 'John hates chocolate.']
from sklearn.feature_extraction.text import CountVectorizer
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:
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)
# 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?
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”).
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)
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.
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.
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()
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'))
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'))
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
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()
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 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
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.
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:
11
Y ya sólo queda invocarlo con varios ejemplos:
Como se puede observar, las frases más cercanas del corpus son las más parecidas
semánticamente a la consulta proporcionada.
4. Evaluación
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