Curso de Inteligencia Artificial y Aprendizaje Profundo
and their Compositionality](https://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf), 3. Xin Rong, 2016, word2vec Parameter Learning Explained, 4. Mikolov et al. 2013b, Google, Efficient Estimation of Word Representations in Vector Space.
Trabajar con datos de texto no estructurados es difícil, especialmente cuando se intenta construir un sistema inteligente que interprete y comprenda el lenguaje natural que fluye libremente al igual que los humanos.
Debe poder procesar y transformar datos textuales no estructurados y ruidosos en algunos formatos estructurados y vectorizados que puedan ser entendidos por cualquier algoritmo de aprendizaje automático.
Los principios del procesamiento del lenguaje natural, el aprendizaje automático o el aprendizaje profundo, todos los cuales caen bajo el amplio paraguas de la inteligencia artificial, son herramientas eficaces del oficio.
Un punto importante a recordar aquí es que cualquier algoritmo de aprendizaje automático se basa en principios de estadística, matemáticas y optimización.
Por lo tanto, no son lo suficientemente inteligentes como para comenzar a procesar texto en su forma original y sin procesar.
En esta lección, analizaremos funciones que a menudo aprovechan los modelos de aprendizaje profundo. Más específicamente, cubriremos los modelos Word2Vec, GloVe y FastText.
Con respecto a los sistemas de reconocimiento de voz o imágenes, toda la información ya está presente en forma de vectores de características ricos y densos incrustados en conjuntos de datos de alta dimensión como espectrogramas de audio e intensidades de píxeles de imagen, como hemos estudiado en otras lecciones.
Sin embargo, cuando se trata de datos de texto sin procesar, especialmente modelos basados en conteo como la bolsa de palabras (bag of words), estamos tratando con palabras individuales que pueden tener sus propios identificadores y no capturan la relación semántica entre palabras.
En lecciones anteriores trabajamos con la técnica de bolsa de palabras en la tećnica Lattent Dirichlet Allocation (LDA).
Esto conduce a enormes vectores de palabras dispersas para datos textuales y, por lo tanto, si no tenemos suficientes datos, podemos terminar obteniendo modelos deficientes o incluso sobreajustando los datos debido a la maldición de la dimensionalidad.
Para superar las deficiencias de perder la semántica y la escasez de características basadas en el modelo de bolsa de palabras, necesitamos hacer uso de los modelos de espacio vectorial - Vector Space Models(VSM) de tal manera que podamos incrustar vectores de palabras en este espacio vectorial continuo basado en semánticas y similitud contextual.
De hecho, la hipótesis distributiva en el campo de la semántica distributiva nos dice que las palabras que ocurren y se usan en el mismo contexto, son semánticamente similares entre sí y tienen significados similares.
En términos simples, una palabra se caracteriza por la compañía que mantiene. Uno de los artículos famosos que habla en detalle sobre estos vectores de palabras semánticas y varios tipos es Don’t count, predict! A systematic comparison of context-counting vs. context-predicting semantic vectors’ by Baroni et al, de by Baroni et al.
No profundizaremos mucho, pero en resumen, hay dos tipos principales de métodos para los vectores de palabras contextuales.
Nos centraremos en estos métodos predictivos en esta lección.
import tensorflow as tf
import pandas as pd
import numpy as np
import re
import nltk
import matplotlib.pyplot as plt
pd.options.display.max_colwidth = 200
%matplotlib inline
NLTK incluye una pequeña selección de textos del archivo de texto electrónico del Proyecto Gutenberg, que contiene unos 25.000 libros electrónicos gratuitos, alojados en Gutenberg project
Tokenizador de frases Punkt
Este tokenizador divide un texto en una lista de oraciones mediante el uso de un algoritmo no supervisado para construir un modelo de abreviaturas, colocaciones y palabras que inician oraciones. Debe entrenarse en una gran colección de texto sin formato en el idioma de destino antes de que pueda usarse.
El paquete de datos NLTK incluye un tokenizador Punkt previamente entrenado para inglés.
Para empezar usaremos el siguiente toy corpus
Nuestro corpus de juguetes consta de documentos pertenecientes a varias categorías.
Otro corpus que usaremos en esta lección es la versión King James de la Biblia disponible gratuitamente en Project Gutenberg a través del módulo de corpus en nltk.
Lo cargaremos en breve, en la siguiente sección. Antes de los análisis necesitamos preprocesar y normalizar este texto.
corpus = ['The sky is blue and beautiful.',
'Love this blue and beautiful sky!',
'The quick brown fox jumps over the lazy dog.',
"A king's breakfast has sausages, ham, bacon, eggs, toast and beans",
'I love green eggs, ham, sausages and bacon!',
'The brown fox is quick and the blue dog is lazy!',
'The sky is very blue and the sky is very beautiful today',
'The dog is lazy but the brown fox is quick!'
]
labels = ['weather', 'weather', 'animals', 'food', 'food', 'animals', 'weather', 'animals']
corpus = np.array(corpus)
corpus_df = pd.DataFrame({'Document': corpus,
'Category': labels})
corpus_df = corpus_df[['Document', 'Category']]
corpus_df
Document | Category | |
---|---|---|
0 | The sky is blue and beautiful. | weather |
1 | Love this blue and beautiful sky! | weather |
2 | The quick brown fox jumps over the lazy dog. | animals |
3 | A king's breakfast has sausages, ham, bacon, eggs, toast and beans | food |
4 | I love green eggs, ham, sausages and bacon! | food |
5 | The brown fox is quick and the blue dog is lazy! | animals |
6 | The sky is very blue and the sky is very beautiful today | weather |
7 | The dog is lazy but the brown fox is quick! | animals |
wpt = nltk.WordPunctTokenizer()
stop_words = nltk.corpus.stopwords.words('english')
def normalize_document(doc):
# remove special characters:
doc = re.sub(r'[^a-zA-Z\s]', '', doc, re.I|re.A)
# transform to lower case
doc = doc.lower()
# remove \whitespaces
doc = doc.strip()
# tokenize document
tokens = wpt.tokenize(doc)
# filter stopwords out of document
filtered_tokens = [token for token in tokens if token not in stop_words]
# re-create document from filtered tokens
doc = ' '.join(filtered_tokens)
return doc
# crea una función vectorizada para que actué sobre múltiples textos
normalize_corpus = np.vectorize(normalize_document)
#normalize_corpus
norm_corpus = normalize_corpus(corpus)
norm_corpus
array(['sky blue beautiful', 'love blue beautiful sky', 'quick brown fox jumps lazy dog', 'kings breakfast sausages ham bacon eggs toast beans', 'love green eggs ham sausages bacon', 'brown fox quick blue dog lazy', 'sky blue sky beautiful today', 'dog lazy brown fox quick'], dtype='<U51')
from nltk.corpus import gutenberg
from string import punctuation
bible = gutenberg.sents('bible-kjv.txt') # tokeniza por sentencias
remove_terms = punctuation + '0123456789'
norm_bible = [[word.lower() for word in sent if word not in remove_terms] for sent in bible]
norm_bible = [' '.join(tok_sent) for tok_sent in norm_bible]
norm_bible = filter(None, normalize_corpus(norm_bible))
norm_bible = [tok_sent for tok_sent in norm_bible if len(tok_sent.split()) > 2]
print('Total lines:', len(bible))
print('\nSample line:', bible[10])
print('\nProcessed line:', norm_bible[10])
Total lines: 30103 Sample line: ['1', ':', '6', 'And', 'God', 'said', ',', 'Let', 'there', 'be', 'a', 'firmament', 'in', 'the', 'midst', 'of', 'the', 'waters', ',', 'and', 'let', 'it', 'divide', 'the', 'waters', 'from', 'the', 'waters', '.'] Processed line: god said let firmament midst waters let divide waters waters
Este modelo fue creado por Google en 2013. Para los detalles consulte Distributed Representations of Words and Phrases and their Compositionality y Efficient Estimation of Word Representations in Vector Space.
Una explicación sencilla puede encontrarse en word2vec Parameter Learning Explained,
Word2vec es un modelo de aprendizaje profundo para generar distribuciones vectoriales continuas de palabras, con una alta calidad, que capturan similitudes contextuales y semánticas.
Básicamente, se trata de modelos no supervisados (en el fondo son autoencoders) que pueden incluir corpus textuales masivos, crear un vocabulario de palabras posibles y generar incrustaciones (embbedings) para cada palabra en el espacio vectorial que representa ese vocabulario.
Por lo general, se puede especificar el tamaño de los vectores de sumergimiento de palabras y el número total de vectores es esencialmente el tamaño del vocabulario.
Esto hace que la dimensionalidad de este espacio vectorial denso sea mucho menor que el espacio vectorial disperso de alta dimensión construido con los modelos tradicionales de Bag of Words.
Hay dos arquitecturas de modelos diferentes que Word2Vec puede aprovechar para crear estas representaciones de incrustación de palabras. Éstos incluyen,
CBOW = Continuous Bag of Words
The CBOW model architecture tries to predict the current target word (the center word) based on the source context words (surrounding words). Considering a simple sentence, “the quick brown fox jumps over the lazy dog”, this can be pairs of (context_window, target_word) where if we consider a context window of size 2, we have examples like ([quick, fox], brown), ([the, brown], quick), ([the, dog], lazy) and so on. Thus the model tries to predict the target_word based on the context_window words.
Con la arquitectura del modelo CBOW se intenta predecir la palabra objetivo actual- target word- (la palabra central) basándose en las palabras del contexto de origen (palabras circundantes). Consideremos una oración simple en inglés: “the quick brown fox jumps over the lazy dog”
Aquí se consideran pares de (context_window, target_word) donde si consideramos una ventana de contexto de tamaño 2, tenemos ejemplos como ([quick, fox], brown), ([the, brown], quick), ([the, dog], lazy) y así sucesivamente.
Por lo tanto, el modelo intenta predecir la palabra_destino (target_word) basándose en las palabras de la ventana de contexto (context_window).
El siguiente gráfico ilustra la arquitectuta CBW. Cada plabra en la entrada está codificada usanod la codificación one-hot.
Para lograr esto, el diccionario de palabras es construído previamente y en consecuencia, cada palabra es representada por un vector binario de tamaño igual al tamaño del diccionario.
Fuente: Alvaro Montenegro
La siguiente imagen es la arquitectura general CBOW.
La familia de modelos Word2Vec no es supervisada, lo que esto significa es que puede simplemente darle un corpus sin etiquetas o información adicionales y puede construir incrustaciones densas de palabras a partir del corpus.
Pero aún necesitará aprovechar una metodología de clasificación supervisada una vez que tenga este corpus para acceder a estas incorporaciones.
Haremos esto desde el propio corpus, sin ninguna información auxiliar. Podemos modelar esta arquitectura CBOW ahora como un modelo de clasificación de aprendizaje profundo de modo que tomemos en las palabras de contexto como nuestra entrada, $X$ e intentemos predecir la palabra objetivo, $Y$.
from tensorflow.keras.preprocessing import text
#from tensorflow.keras.utils import np_utils
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.preprocessing import sequence
tokenizer = text.Tokenizer()
tokenizer.fit_on_texts(norm_bible)
word2id = tokenizer.word_index
# build vocabulary of unique words
word2id['PAD'] = 0
id2word = {v:k for k, v in word2id.items()}
# docs as nested lists of id's of words
wids = [[word2id[w] for w in text.text_to_word_sequence(doc)] for doc in norm_bible]
vocab_size = len(word2id)
embed_size = 100
window_size = 2 # context window size
print('Vocabulary Size:', vocab_size)
print('Vocabulary Sample:', list(word2id.items())[:10])
print('list of numeric sequences: ',wids[:2])
Vocabulary Size: 12425 Vocabulary Sample: [('shall', 1), ('unto', 2), ('lord', 3), ('thou', 4), ('thy', 5), ('god', 6), ('ye', 7), ('said', 8), ('thee', 9), ('upon', 10)] list of numeric sequences: [[13, 1154, 5766], [154, 2450, 13, 1154, 5766]]
Por lo tanto, puede ver que hemos creado un vocabulario de palabras únicas en nuestro corpus y también formas de asignar una palabra a su identificador único y viceversa. El término PAD se usa típicamente para rellenar palabras de contexto a una longitud fija si es necesario.
Necesitamos pares que consistan en una palabra central objetivo y palabras de contexto circundantes.
En nuestra implementación, una palabra de destino tiene una longitud de 1 y el contexto circundante tiene una longitud de 2 x tamaño de ventana, donde tomamos las palabras de tamaño de ventana antes y después de la palabra de destino en nuestro corpus.
Esto quedará más claro con el siguiente ejemplo.
def generate_context_word_pairs(corpus, window_size, vocab_size):
context_length = window_size*2
for words in corpus:
sentence_length = len(words)
for index, word in enumerate(words):
context_words = []
label_word = []
start = index - window_size
end = index + window_size + 1
context_words.append([words[i]
for i in range(start, end)
if 0 <= i < sentence_length
and i != index])
label_word.append(word)
x = sequence.pad_sequences(context_words, maxlen=context_length)
y = to_categorical(label_word, vocab_size)
yield (x, y)
# Test this out for some samples
i = 0
for x, y in generate_context_word_pairs(corpus=wids, window_size=window_size, vocab_size=vocab_size):
if 0 not in x[0]:
print('Context (X):', [id2word[w] for w in x[0]], '-> Target (Y):', id2word[np.argwhere(y[0])[0][0]])
if i == 10:
break
i += 1
Context (X): ['old', 'testament', 'james', 'bible'] -> Target (Y): king Context (X): ['first', 'book', 'called', 'genesis'] -> Target (Y): moses Context (X): ['beginning', 'god', 'heaven', 'earth'] -> Target (Y): created Context (X): ['earth', 'without', 'void', 'darkness'] -> Target (Y): form Context (X): ['without', 'form', 'darkness', 'upon'] -> Target (Y): void Context (X): ['form', 'void', 'upon', 'face'] -> Target (Y): darkness Context (X): ['void', 'darkness', 'face', 'deep'] -> Target (Y): upon Context (X): ['spirit', 'god', 'upon', 'face'] -> Target (Y): moved Context (X): ['god', 'moved', 'face', 'waters'] -> Target (Y): upon Context (X): ['god', 'said', 'light', 'light'] -> Target (Y): let Context (X): ['god', 'saw', 'good', 'god'] -> Target (Y): light
Las entradas serán nuestras palabras de contexto que se pasan a una capa de incrustación (inicializada con pesos aleatorios).
Las incrustaciones de palabras se propagan a una capa lambda donde promediamos las incrustaciones de palabras (por esta razón, se llama CBOW porque realmente no consideramos el orden o la secuencia en las palabras de contexto cuando se promedian) y luego pasamos esta incrustación de contexto promediada a una capa denssa con activación que predice nuestra palabra objetivo (taget).
Observe que al promediar es como si calcularamos una palabra promedio ques pasa la capa densa para que la compare con la palabra target $Y$.
Calculamos la pérdida usando la entropía cruzada.
import tensorflow as tf
from tensorflow.keras.layers import Input, Dense, Embedding, Lambda
from tensorflow.keras.models import Model
from tensorflow.keras.utils import plot_model
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.losses import MeanSquaredError
#cbow
inputs = Input(shape=(window_size*2,),name='cbow_input')
x = Embedding(input_dim= vocab_size, output_dim=embed_size, input_length=window_size*2)(inputs)
x = Lambda(lambda x: tf.reduce_mean(x, axis=1), output_shape=(embed_size,))(x)
outputs = Dense(vocab_size, activation='softmax')(x)
cbow = Model(inputs=inputs,outputs=outputs, name='cbow_model')
cbow.summary()
plot_model(cbow, to_file='../Imagenes/cbow.png',
show_shapes=True)
Model: "cbow_model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= cbow_input (InputLayer) [(None, 4)] 0 _________________________________________________________________ embedding_1 (Embedding) (None, 4, 100) 1242500 _________________________________________________________________ lambda_1 (Lambda) (None, 100) 0 _________________________________________________________________ dense_1 (Dense) (None, 12425) 1254925 ================================================================= Total params: 2,497,425 Trainable params: 2,497,425 Non-trainable params: 0 _________________________________________________________________
# compile
cbow.compile(loss='categorical_crossentropy', optimizer='adam')
La gráfica muestra la arquitectura neuronal CBOW
Fuente: Dipanjan (DJ) Sarkar
wids[0:2]
[[13, 1154, 5766], [154, 2450, 13, 1154, 5766]]
# train: time expensive: AWS,..., colab...
for epoch in range(1, 6):
loss = 0.
i = 0
for x, y in generate_context_word_pairs(corpus=wids, window_size=window_size, vocab_size=vocab_size):
i += 1
loss += cbow.train_on_batch(x, y)
if i % 100000 == 0:
print('Processed {} (context, word) pairs'.format(i))
print('Epoch:', epoch, '\tLoss:', loss)
print()
# recuperando el incrustamiento (embedding)
weights = cbow.get_weights()[0]
weights = weights[1:]
print(weights.shape)
pd.DataFrame(weights, index=list(id2word.values())[1:]).head()
Fuente: Dipanjan (DJ) Sarkar
from sklearn.metrics.pairwise import euclidean_distances
# compute pairwise distance matrix
distance_matrix = euclidean_distances(weights)
print(distance_matrix.shape)
# view contextually similar words
similar_words = {search_term: [id2word[idx] for idx in distance_matrix[word2id[search_term]-1].argsort()[1:6]+1]
for search_term in ['god', 'jesus', 'noah', 'egypt', 'john', 'gospel', 'moses','famine']}
similar_words
La arquitectura del modelo Skip-gram generalmente intenta lograr lo contrario de lo que hace el modelo CBOW.
Intenta predecir las palabras de contexto de origen (palabras circundantes) dada una palabra de destino (la palabra central). Teniendo en cuenta nuestra simple oración de antes, "the quick brown fox jumps over the lazy dog”.
Si usamos el modelo CBOW, obtenemos pares de (context_window, target_word) donde si consideramos una ventana de contexto de tamaño 2, tenemos ejemplos como ([quick, fox], brown), ([the, brown], quick) , ([the, dog], lazy) y así sucesivamente.
Ahora, teniendo en cuenta que el objetivo del modelo skip-gram es predecir el contexto a partir de la palabra objetivo, el modelo normalmente invierte los contextos y objetivos e intenta predecir cada palabra de contexto a partir de su palabra objetivo.
Por lo tanto, la tarea se convierte en predecir el contexto [quick, fox] dada la palabra objetivo brown o [the brown] dada la palabra objetivo quick y así sucesivamente.
Por lo tanto, el modelo intenta predecir las palabras de la ventana context_window basándose en target_word.
La figura ilustra la qrtuitectuta skip-gram
Fuente: Dipanjan (DJ) Sarkar
Para esto, alimentamos nuestros pares de modelos de skip-gram son $(X, Y)$ donde $X$ es nuestra entrada e $Y$ es nuestra etiqueta.
Hacemos esto usando los pares [(objetivo, contexto), 1] como muestras de entrada positivas donde objetivo es nuestra palabra de interés y contexto es una palabra de contexto que aparece cerca de la palabra objetivo y la etiqueta positiva 1 indica que este es un par contextualmente relevante.
También introducimos pares [(objetivo, aleatorio), 0] como muestras de entrada negativa donde objetivo es nuevamente nuestra palabra de interés, pero aleatorio significa que es solo una palabra seleccionada al azar de nuestro vocabulario que no tiene contexto o asociación con nuestra palabra objetivo.
Por lo tanto, la etiqueta negativa 0 indica que este es un par contextualmente irrelevante. Hacemos esto para que el modelo pueda aprender qué pares de palabras son contextualmente relevantes y cuáles no y generar incrustaciones similares para palabras semánticamente similares.
from tensorflow.keras.preprocessing import text
tokenizer = text.Tokenizer()
tokenizer.fit_on_texts(norm_bible)
word2id = tokenizer.word_index
id2word = {v:k for k, v in word2id.items()}
vocab_size = len(word2id) + 1
embed_size = 100
wids = [[word2id[w] for w in text.text_to_word_sequence(doc)] for doc in norm_bible]
print('Vocabulary Size:', vocab_size)
print('Vocabulary Sample:', list(word2id.items())[:10])
Vocabulary Size: 12425 Vocabulary Sample: [('shall', 1), ('unto', 2), ('lord', 3), ('thou', 4), ('thy', 5), ('god', 6), ('ye', 7), ('said', 8), ('thee', 9), ('upon', 10)]
[(target, context), relevancy]
Afortunadamente, keras tiene una ingeniosa utilidad de skipgrams que se puede usar y no tenemos que implementar manualmente este generador como lo hicimos en CBOW.
Esta función transforma una secuencia de índices de palabras (lista de números enteros) en tuplas de palabras de la forma:
from tensorflow.keras.preprocessing.sequence import skipgrams
# generate skip-grams
skip_grams = [skipgrams(wid, vocabulary_size=vocab_size, window_size=10) for wid in wids]
# view sample skip-grams
pairs, labels = skip_grams[0][0], skip_grams[0][1]
for i in range(10):
print("({:s} ({:d}), {:s} ({:d})) -> {:d}".format(
id2word[pairs[i][0]], pairs[i][0],
id2word[pairs[i][1]], pairs[i][1],
labels[i]))
(bible (5766), james (1154)) -> 1 (james (1154), bible (5766)) -> 1 (king (13), silvanus (5751)) -> 0 (king (13), bible (5766)) -> 1 (bible (5766), discontinue (10966)) -> 0 (king (13), james (1154)) -> 1 (king (13), undressed (6985)) -> 0 (bible (5766), king (13)) -> 1 (james (1154), king (13)) -> 1 (james (1154), stout (5534)) -> 0
from tensorflow.keras.layers import Dot
from tensorflow.keras.layers import Dense, Reshape, Activation
from tensorflow.keras.layers import Embedding
from tensorflow.keras.models import Sequential
from tensorflow.keras.utils import plot_model
# inputs
w_inputs = Input(shape=(1, ), dtype='int32', name='inputs')
w = Embedding(vocab_size, embed_size)(w_inputs)
# context
c_inputs = Input(shape=(1, ), dtype='int32', name='context')
c = Embedding(vocab_size, embed_size)(c_inputs)
o = Dot(axes=2)([w, c])
o = Reshape((1,), input_shape=(1, 1))(o)
o = Activation('sigmoid')(o)
SkipGram = Model(inputs=[w_inputs, c_inputs], outputs=o)
SkipGram.summary()
SkipGram.compile(loss='binary_crossentropy', optimizer='adam')
plot_model(SkipGram, to_file='../Imagenes/model_skip_gram.png', show_shapes=True)
Model: "model_2" __________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ================================================================================================== inputs (InputLayer) [(None, 1)] 0 __________________________________________________________________________________________________ context (InputLayer) [(None, 1)] 0 __________________________________________________________________________________________________ embedding_14 (Embedding) (None, 1, 100) 1242500 inputs[0][0] __________________________________________________________________________________________________ embedding_15 (Embedding) (None, 1, 100) 1242500 context[0][0] __________________________________________________________________________________________________ dot_4 (Dot) (None, 1, 1) 0 embedding_14[0][0] embedding_15[0][0] __________________________________________________________________________________________________ reshape_9 (Reshape) (None, 1) 0 dot_4[0][0] __________________________________________________________________________________________________ activation_2 (Activation) (None, 1) 0 reshape_9[0][0] ================================================================================================== Total params: 2,485,000 Trainable params: 2,485,000 Non-trainable params: 0 __________________________________________________________________________________________________
Comprender el modelo de aprendizaje profundo anterior es bastante sencillo. Sin embargo, intentaremos resumir los conceptos centrales de este modelo en términos simples para facilitar la comprensión.
Tenemos un par de palabras de entrada para cada ejemplo de entrenamiento que consta de una palabra objetivo de entrada que tiene un identificador numérico único y una palabra de contexto que tiene un identificador numérico único.
Si es una muestra positiva, la palabra tiene un significado contextual, es una palabra de contexto y nuestra etiqueta Y = 1, de lo contrario, si es una muestra negativa, la palabra no tiene significado contextual, es solo una palabra aleatoria y nuestra etiqueta Y = 0.
Pasaremos cada uno de ellos a una capa de incrustación propia, que tiene un tamaño (vocab_size x embed_size) que nos dará densas incrustaciones de palabras para cada una de estas dos palabras (1 x embed_size para cada palabra).
A continuación, usamos una capa de combinación para calcular el producto escalar de estas dos incrustaciones y obtener el valor del producto escalar.
Esto luego se envía a la capa sigmoidea densa que genera un 1 o un 0. Comparamos esto con la etiqueta real Y (1 o 0), calculamos la pérdida, retropropagamos los errores para ajustar los pesos (en la capa de incrustación) y repetimos este proceso para todos los pares (objetivo, contexto) para múltiples épocas.
La siguiente figura intenta explicar lo mismo.
Fuente: Dipanjan (DJ) Sarkar
for epoch in range(1, 6):
loss = 0
for i, elem in enumerate(skip_grams):
pair_first_elem = np.array(list(zip(*elem[0]))[0], dtype='int32')
pair_second_elem = np.array(list(zip(*elem[0]))[1], dtype='int32')
labels = np.array(elem[1], dtype='int32')
X = [pair_first_elem, pair_second_elem]
Y = labels
if i % 10000 == 0:
print('Processed {} (skip_first, skip_second, relevance) pairs'.format(i))
loss += SkipGram.train_on_batch(X,Y)
print('Epoch:', epoch, 'Loss:', loss)
## Recupera incrustaciones
merge_layer = model.layers[0]
word_model = merge_layer.layers[0]
word_embed_layer = word_model.layers[0]
weights = word_embed_layer.get_weights()[0][1:]
print(weights.shape)
pd.DataFrame(weights, index=id2word.values()).head()
Fuente: Dipanjan (DJ) Sarkar
from sklearn.metrics.pairwise import euclidean_distances
distance_matrix = euclidean_distances(weights)
print(distance_matrix.shape)
similar_words = {search_term: [id2word[idx] for idx in distance_matrix[word2id[search_term]-1].argsort()[1:6]+1]
for search_term in ['god', 'jesus', 'noah', 'egypt', 'john', 'gospel', 'moses','famine']}
similar_words
from sklearn.manifold import TSNE
words = sum([[k] + v for k, v in similar_words.items()], [])
words_ids = [word2id[w] for w in words]
word_vectors = np.array([weights[idx] for idx in words_ids])
print('Total words:', len(words), '\tWord Embedding shapes:', word_vectors.shape)
tsne = TSNE(n_components=2, random_state=0, n_iter=10000, perplexity=3)
np.set_printoptions(suppress=True)
T = tsne.fit_transform(word_vectors)
labels = words
plt.figure(figsize=(14, 8))
plt.scatter(T[:, 0], T[:, 1], c='steelblue', edgecolors='k')
for label, x, y in zip(labels, T[:, 0], T[:, 1]):
plt.annotate(label, xy=(x+1, y+1), xytext=(0, 0), textcoords='offset points')
Fuente: Dipanjan (DJ) Sarkar
from gensim.models import word2vec
# tokenize sentences in corpus
wpt = nltk.WordPunctTokenizer()
tokenized_corpus = [wpt.tokenize(document) for document in norm_bible]
# Set values for various parameters
feature_size = 100 # Word vector dimensionality
window_context = 30 # Context window size
min_word_count = 1 # Minimum word count
sample = 1e-3 # Downsample setting for frequent words
w2v_model = word2vec.Word2Vec(tokenized_corpus, size=feature_size,
window=window_context, min_count=min_word_count,
sample=sample, iter=50)
# view similar words based on gensim's model
similar_words = {search_term: [item[0] for item in w2v_model.wv.most_similar([search_term], topn=5)]
for search_term in ['god', 'jesus', 'noah', 'egypt', 'john', 'gospel', 'moses','famine']}
similar_words
{'god': ['worldly', 'soberly', 'lord', 'godly', 'ever'], 'jesus': ['messias', 'peter', 'cross', 'james', 'apelles'], 'noah': ['shem', 'japheth', 'ham', 'enosh', 'kenan'], 'egypt': ['egyptians', 'pharaoh', 'bondage', 'rid', 'rod'], 'john': ['james', 'baptist', 'peter', 'devine', 'tetrarch'], 'gospel': ['christ', 'faith', 'afflictions', 'repentance', 'persecutions'], 'moses': ['children', 'congregation', 'aaron', 'joshua', 'ordinance'], 'famine': ['pestilence', 'peril', 'blasting', 'sword', 'mildew']}
from sklearn.manifold import TSNE
words = sum([[k] + v for k, v in similar_words.items()], [])
wvs = w2v_model.wv[words]
tsne = TSNE(n_components=2, random_state=0, n_iter=10000, perplexity=2)
np.set_printoptions(suppress=True)
T = tsne.fit_transform(wvs)
labels = words
plt.figure(figsize=(14, 8))
plt.scatter(T[:, 0], T[:, 1], c='orange', edgecolors='r')
for label, x, y in zip(labels, T[:, 0], T[:, 1]):
plt.annotate(label, xy=(x+1, y+1), xytext=(0, 0), textcoords='offset points')
Para esta aplicación vamos a usar nuestro ejemplo de juguete.
# build word2vec model
wpt = nltk.WordPunctTokenizer()
tokenized_corpus = [wpt.tokenize(document) for document in norm_corpus]
# Set values for various parameters
feature_size = 10 # Word vector dimensionality
window_context = 10 # Context window size
min_word_count = 1 # Minimum word count
sample = 1e-3 # Downsample setting for frequent words
w2v_model = word2vec.Word2Vec(tokenized_corpus, size=feature_size,
window=window_context, min_count = min_word_count,
sample=sample, iter=100)
# visualize embeddings
from sklearn.manifold import TSNE
words = w2v_model.wv.index2word
wvs = w2v_model.wv[words]
tsne = TSNE(n_components=2, random_state=0, n_iter=5000, perplexity=2)
np.set_printoptions(suppress=True)
T = tsne.fit_transform(wvs)
labels = words
plt.figure(figsize=(12, 6))
plt.scatter(T[:, 0], T[:, 1], c='orange', edgecolors='r')
for label, x, y in zip(labels, T[:, 0], T[:, 1]):
plt.annotate(label, xy=(x+1, y+1), xytext=(0, 0), textcoords='offset points')
Recuerde que nuestro corpus es extremadamente pequeño, por lo que para obtener incrustaciones de palabras significativas y para que el modelo obtenga más contexto y semántica, más datos ayudan.
Ahora bien, ¿qué es una palabra incrustada en este escenario?
Por lo general, es un vector denso para cada palabra, como se muestra en el siguiente ejemplo para la palabra *sky.
w2v_model.wv['sky']
array([-0.02261146, 0.02863239, -0.04796803, -0.04033024, 0.04963837, 0.00975511, 0.00527302, -0.04298129, 0.02490452, 0.02038221], dtype=float32)
Ahora suponga que quisiéramos agrupar los ocho documentos de nuestro corpus de juguetes, necesitaríamos obtener las incrustaciones de nivel de documento de cada una de las palabras presentes en cada documento.
Una estrategia sería promediar las incrustaciones de palabras para cada palabra en un documento.
Esta es una estrategia extremadamente útil y puede adoptar la misma para sus propios problemas. Apliquemos esto ahora en nuestro corpus para obtener características para cada documento.
def average_word_vectors(words, model, vocabulary, num_features):
feature_vector = np.zeros((num_features,),dtype="float64")
nwords = 0.
for word in words:
if word in vocabulary:
nwords = nwords + 1.
feature_vector = np.add(feature_vector, model[word])
if nwords:
feature_vector = np.divide(feature_vector, nwords)
return feature_vector
def averaged_word_vectorizer(corpus, model, num_features):
vocabulary = set(model.wv.index2word)
features = [average_word_vectors(tokenized_sentence, model, vocabulary, num_features)
for tokenized_sentence in corpus]
return np.array(features)
# get document level embeddings
w2v_feature_array = averaged_word_vectorizer(corpus=tokenized_corpus, model=w2v_model, num_features=feature_size)
pp = pd.DataFrame(w2v_feature_array)
pp
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0.003410 | 0.000640 | -0.033249 | -0.027940 | 0.009086 | 0.016739 | -0.001130 | -0.003688 | 0.003148 | 0.012419 |
1 | 0.007855 | -0.009855 | -0.020123 | -0.010149 | 0.001781 | 0.016522 | -0.006216 | -0.003472 | -0.007690 | 0.008691 |
2 | 0.003381 | -0.001723 | -0.015952 | -0.008342 | 0.004348 | -0.001745 | 0.023056 | 0.012036 | 0.015293 | 0.006381 |
3 | 0.007724 | -0.004753 | 0.014649 | -0.024434 | 0.010517 | 0.011342 | 0.011578 | -0.031183 | 0.006504 | 0.010732 |
4 | 0.001606 | -0.010687 | 0.011966 | -0.003939 | 0.009683 | 0.013014 | 0.000181 | -0.017332 | 0.006653 | 0.006743 |
5 | 0.002404 | 0.002423 | -0.023853 | -0.003063 | 0.007681 | 0.003784 | 0.025750 | 0.011553 | 0.016425 | 0.006381 |
6 | 0.006311 | 0.013330 | -0.028168 | -0.028394 | 0.023893 | 0.013247 | 0.007039 | -0.005283 | 0.013501 | 0.012522 |
7 | 0.003561 | 0.006017 | -0.020257 | -0.003702 | 0.015033 | -0.000151 | 0.032216 | 0.004933 | 0.021022 | 0.001736 |
Ahora que tenemos nuestras características para cada documento, agrupemos estos documentos utilizando el algoritmo de propagación de afinidad, que es un algoritmo de agrupación basado en el concepto de "paso de mensajes" entre puntos de datos y no necesita el número de agrupaciones como una entrada explícita que a menudo es requerido por algoritmos de agrupación en clústeres basadosen particiones.
from sklearn.cluster import AffinityPropagation
ap = AffinityPropagation()
ap.fit(w2v_feature_array)
cluster_labels = ap.labels_
cluster_labels = pd.DataFrame(cluster_labels, columns=['ClusterLabel'])
pd.concat([corpus_df, cluster_labels], axis=1)
Document | Category | ClusterLabel | |
---|---|---|---|
0 | The sky is blue and beautiful. | weather | 0 |
1 | Love this blue and beautiful sky! | weather | 0 |
2 | The quick brown fox jumps over the lazy dog. | animals | 2 |
3 | A king's breakfast has sausages, ham, bacon, eggs, toast and beans | food | 1 |
4 | I love green eggs, ham, sausages and bacon! | food | 1 |
5 | The brown fox is quick and the blue dog is lazy! | animals | 2 |
6 | The sky is very blue and the sky is very beautiful today | weather | 0 |
7 | The dog is lazy but the brown fox is quick! | animals | 2 |
Finalmente hagamos un plot de los documentos usando un análisis de componentes principales (ACP).
from sklearn.decomposition import PCA
pca = PCA(n_components=2, random_state=0)
pcs = pca.fit_transform(w2v_feature_array)
labels = ap.labels_
categories = list(corpus_df['Category'])
plt.figure(figsize=(8, 6))
for i in range(len(labels)):
label = labels[i]
color = 'orange' if label == 0 else 'blue' if label == 1 else 'green'
annotation_label = categories[i]
x, y = pcs[i]
plt.scatter(x, y, c=color, edgecolors='k')
plt.annotate(annotation_label, xy=(x+1e-4, y+1e-3), xytext=(0, 0), textcoords='offset points')