Aprendizaje no supervisado: Tenemos un conjunto variable continuas o categóricas sobre las cuales deseamos aprender o descrubrir un patrón de comportamiento. Tales patrones pueden ser descubiertos a través de técnicas de reducción de dimensionalidad o técnicas de agrupamiento, las cuales son definidas brevemente a continuación.
Reducción de dimensionalidad: Los métodos de reducción de dimensionalidad consisten en resumir y visualizar la información más importante contenida en un dataset. Existen varias técnicas alrededor de esta temática, tanto si asumimos que existen patrones lineales como no lineales en los datos. A continuación se enumeran algunas de estas técnicas, clasificándolas acorde al tipo de datos con el que estemos trabajando y algunos de los paquetes existentes para su uso:
| TipoVariables | Tecnica | Librerias |
|---|---|---|
| Numéricos | Análisis de componentes principales, t-SNE | base, FactoMineR, Rtsne |
| Categóricos | Análisis de correspondencias múltiples | FactoMiner, ade4, epMCA |
| Mixtos | Análisis factorial mixto | FactoMiner |
Clustering: Las técnicas de agrupamiento o clustering nos permiten obtener conocimiento a partir del descubrimiento de patrones existentes en los datos. Específicamente, el objetivo de los métodos de clustering yacen en la identificación de grupos de objetos similares en un conjunto de datos de interés a través de una medida de similaridad entre puntos (e.g la distancia euclideana). A continuación se enumeran algunas de estas técnicas, clasificándolas acorde al tipo de datos con el que estemos trabajando y algunos de los paquetes existentes para su uso:
| TipoVariables | Tecnica | Librerias |
|---|---|---|
| Numéricos | k-medias, GMM, CLARA, Asignación Latente de Dirichlet LDA | cluster, FactoMineR, mclust, topicmodels |
| Categóricos | k-modas, otras medidas de distancia | klaR |
| Mixtos | k-prototypes, otras medidas de distancia | clustMixType |
Dentro del PLN, uno de los problemas más complicados de resolver es la representación del significado de una palabra, de tal manera que un ordenador lo entienda. Una forma de solucionarlo ha sido la creación de bases de datos con palabras agrupadas por significado y reglas que definan las relaciones semánticas entre ellas, como WordNet. Pese a que este tipo de recursos funciona, existen una serie de inconvenientes tales como la pérdida de matiz, dificultad de actualización, exceso de trabajo manual y complicaciones al momento de obtener una medida numérica para medir la similitud entre palabras.
Por tal motivo se han usado a través del tiempo técnicas de representación discreta del lenguaje a través de técnicas como one hot encoding o term frequency para análisis del tipo bag of words o bolsa de palabras, causando pérdida de información. Así, se ha ido desarrollando la idea de intentar codificar toda la información de una palabra en un vector denso, llamado word vector o word embedding, lo cual nos permitiría comparar palabras y utilizar esas representaciones en modelos de aprendizaje automático.
Acorde a Chollet y Allaire (2018), los word embeddings son vectores densos (sin entradas igual a cero) de dimensión reducida que pueden ser aprendidos de los datos. Así, existen dos formas de obtener estos vectores:
Para el caso de análisis utilizaremos la segunda opción. Esta presenta opciones tales como Word2Vec y GloVe. GloVe (Global Vectors for Word Representation) es un algoritmo de aprendizaje no supervisado desarrollado por Pennington, Socher y Manning (2014) que obtiene representaciones vectoriales de las palabras que reciba como entrada. Su entrenamiento se realiza sobre medidas de co-ocurrencia de palabras que se obtienen de un corpus, y las representaciones resultantes muestran estructuras lineales del espacio vectorial de las palabras de análisis.
Para su entrenamiento, el modelo GloVe crea una matriz global de co-ocurrencia de palabras a través de la tabulación de cuán frecuentemente una palabra co-ocurre con otra en un corpus determinado. Su objetivo de entrenamiento es aprender vectores de palabras tales que su producto punto sea igual al logaritmo de la probabilidad de co-ocurrencia de las palabras. La intuición detrás del modelo yace en la idea de que la observación de ratios de co-ocurrencia de palabras tienen el potencial de codificar de alguna manera el significado de ellas.
El algoritmo t-SNE (t-distributed stochastic neighbor embedding) es un método de visualización de datos con alta dimmensionalidad, a través de la proyección de los datos originales en un espacio de 2 o 3 dimensiones. Este tiene la característica de dibujar cerca objetos similares, mientras que objetos distintos son dibujados lejos uno de otro.
El algoritmo tiene 2 etapas principales, Primero, construye una distribución de probabilidad entre pares de objetos originales de tal manera que aquellos más cercanos tengan una mayor probabilidad, mientras que los distintos tengan una baja probabilidad. Luego, se define una distribución de probabilidad similar a la observada en un espacio de pocas dimensiones minimizando la divergencia KL (Kullback-Leibler) entre ambas distribuciones, usando como medida de similaridad base la distancia euclideana.
En el Capítulo 8 obtuvimos a través del modelado de tópicos las principales temáticas incluidas en las obras de uno de los mejores escritores de terror del siglo XX: Howard Phillips Lovecraft, y en este capítulo visualizaremos la relación entre las palabras asignadas a cada temática (i.e. las más probables).
Con estas definiciones en mente, y habiendo definido el caso entre manos, podemos preparar el terreno para realizar el modelado cargando las librerías y los datos a ser usados.
# Carga de librerías
library(text2vec)
library(Rtsne)
library(scales)
library(tm)
library(tidyverse)
library(tidytext)
library(ggrepel)
# Carga de datos
necronomicon_tokenizado_df = readRDS("Caso2_Literatura/necronomicon_tokenizado_df.RDS")
necronomicon_beta_lda = readRDS("Caso2_Literatura/necronomicon_beta_lda.RDS")
necronomicon_gamma_lda = readRDS("Caso2_Literatura/necronomicon_gamma_lda.RDS")
Para comenzar el análisis, crearemos los vectores que representen cada una de las palabras a través del algoritmo GloVe, gracias a la librería text2vec. Para ello, primero crearemos un vector con todas las palabras disponibles.
# Vector de palabras
necronomicon_palabras = list(necronomicon_tokenizado_df %>%
filter(upos %in% c("VERB","ADJ","NOUN","PROPN")) %>%
pull(lemma) %>% tolower())
necronomicon_palabras[[1]] %>% head()
[1] "poned" "nave" "pairo" "flotar" "sotavento" "hablar"
Luego, crearemos un diccionario de las palabras usadas y escogeremos aquellas con una frecuencia superior a 5 (el valor puede cambiar acorde a un proceso de prueba y error).
# Creamos un objeto iterable
i_token = itoken(iterable = necronomicon_palabras, progressbar = FALSE)
# Creamos el vocabulario a partir de la frecuencia de cada palabra
necronomicon_vocabulario = create_vocabulary(i_token)
# Podamos el vocabulario, quedándonos con aquellos términos que tengan una frecuencia igual o superior a 5
necronomicon_vocabulario = prune_vocabulary(necronomicon_vocabulario, term_count_min = 5)
necronomicon_vocabulario %>% head()
Number of docs: 1
0 stopwords: ...
ngram_min = 1; ngram_max = 1
Vocabulary:
Adicionalmente, para la ejecución del algoritmo GloVe crearemos la matriz de co-ocurrencia de palabras a través del siguiente código.
# Vectorizamos el vocabulario creado
v_vectorizer = vocab_vectorizer(necronomicon_vocabulario)
# Creamos la matriz de co-ocurrencia de términos, definiendo una ventana de contexto
necronomicon_tcm = create_tcm(i_token, v_vectorizer, skip_grams_window = 10)
necronomicon_tcm %>% head()
6 x 6406 sparse Matrix of class "dgTMatrix"
[[ suppressing 60 column names 㤼㸱abandoner㤼㸲, 㤼㸱abater㤼㸲, 㤼㸱abeja㤼㸲 ... ]]
abandoner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
abater . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
abeja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
abriéndo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
absorbente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 0.25 . . . . . . . . . . . . . . . . . . . .
absorbido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
abandoner . ......
abater . ......
abeja . ......
abriéndo . ......
absorbente . ......
absorbido . ......
.....suppressing 6346 columns in show(); maybe adjust 'options(max.print= *, width = *)'
..............................
Una vez creada la matriz, entrenamos el modelo GloVe. Cabe notar que en realidad estamos tomando un modelo pre-entrenado en un contexto distinto (GloVe se entrena sobre datos de la Wikipedia y otras fuentes), y estamos ‘’terminando’’ de entrenarlo en nuestro contexto para obtener representaciones vectoriales densas de las palabras en nuestro vocabulario.
# Inicializamos el modelo glove definiendo la dimensión del vector (50) y el tamaño del contexto (10)
glove = GlobalVectors$new(rank = 50, x_max = 10)
# Terminamos de entrenar el modelo con 100 iteraciones y un parámetro de tolerancia de 0.00001, paralelizando con los núcleos disponibles
necronomicon_wv_main = glove$fit_transform(necronomicon_tcm, n_iter = 100, convergence_tol = 0.00001, n_threads = 4)
INFO [18:27:01.254] epoch 1, loss 0.0994
INFO [18:27:02.855] epoch 2, loss 0.0688
INFO [18:27:04.080] epoch 3, loss 0.0615
INFO [18:27:05.944] epoch 4, loss 0.0569
INFO [18:27:07.593] epoch 5, loss 0.0536
INFO [18:27:09.656] epoch 6, loss 0.0511
INFO [18:27:11.490] epoch 7, loss 0.0491
INFO [18:27:13.720] epoch 8, loss 0.0475
INFO [18:27:15.968] epoch 9, loss 0.0462
INFO [18:27:18.188] epoch 10, loss 0.0451
INFO [18:27:20.588] epoch 11, loss 0.0441
INFO [18:27:22.790] epoch 12, loss 0.0433
INFO [18:27:25.212] epoch 13, loss 0.0426
INFO [18:27:27.476] epoch 14, loss 0.0419
INFO [18:27:29.906] epoch 15, loss 0.0414
INFO [18:27:32.153] epoch 16, loss 0.0409
INFO [18:27:34.501] epoch 17, loss 0.0404
INFO [18:27:36.766] epoch 18, loss 0.0400
INFO [18:27:39.099] epoch 19, loss 0.0396
INFO [18:27:41.484] epoch 20, loss 0.0393
INFO [18:27:43.790] epoch 21, loss 0.0389
INFO [18:27:45.978] epoch 22, loss 0.0386
INFO [18:27:48.159] epoch 23, loss 0.0384
INFO [18:27:50.382] epoch 24, loss 0.0381
INFO [18:27:52.565] epoch 25, loss 0.0379
INFO [18:27:54.768] epoch 26, loss 0.0377
INFO [18:27:57.131] epoch 27, loss 0.0375
INFO [18:27:59.302] epoch 28, loss 0.0373
INFO [18:28:01.474] epoch 29, loss 0.0371
INFO [18:28:03.869] epoch 30, loss 0.0369
INFO [18:28:06.175] epoch 31, loss 0.0368
INFO [18:28:08.485] epoch 32, loss 0.0366
INFO [18:28:10.806] epoch 33, loss 0.0365
INFO [18:28:13.162] epoch 34, loss 0.0363
INFO [18:28:15.613] epoch 35, loss 0.0362
INFO [18:28:18.337] epoch 36, loss 0.0361
INFO [18:28:20.978] epoch 37, loss 0.0359
INFO [18:28:23.317] epoch 38, loss 0.0358
INFO [18:28:25.565] epoch 39, loss 0.0357
INFO [18:28:27.564] epoch 40, loss 0.0356
INFO [18:28:29.348] epoch 41, loss 0.0355
INFO [18:28:31.416] epoch 42, loss 0.0354
INFO [18:28:33.373] epoch 43, loss 0.0353
INFO [18:28:35.233] epoch 44, loss 0.0353
INFO [18:28:37.530] epoch 45, loss 0.0352
INFO [18:28:39.206] epoch 46, loss 0.0351
INFO [18:28:40.975] epoch 47, loss 0.0350
INFO [18:28:42.747] epoch 48, loss 0.0349
INFO [18:28:44.614] epoch 49, loss 0.0349
INFO [18:28:46.379] epoch 50, loss 0.0348
INFO [18:28:48.211] epoch 51, loss 0.0347
INFO [18:28:50.013] epoch 52, loss 0.0347
INFO [18:28:51.788] epoch 53, loss 0.0346
INFO [18:28:53.627] epoch 54, loss 0.0345
INFO [18:28:55.410] epoch 55, loss 0.0345
INFO [18:28:57.106] epoch 56, loss 0.0344
INFO [18:28:58.902] epoch 57, loss 0.0344
INFO [18:29:00.729] epoch 58, loss 0.0343
INFO [18:29:02.519] epoch 59, loss 0.0343
INFO [18:29:04.236] epoch 60, loss 0.0342
INFO [18:29:06.071] epoch 61, loss 0.0342
INFO [18:29:07.922] epoch 62, loss 0.0341
INFO [18:29:09.743] epoch 63, loss 0.0341
INFO [18:29:11.601] epoch 64, loss 0.0340
INFO [18:29:13.335] epoch 65, loss 0.0340
INFO [18:29:15.329] epoch 66, loss 0.0339
INFO [18:29:17.135] epoch 67, loss 0.0339
INFO [18:29:18.908] epoch 68, loss 0.0338
INFO [18:29:20.782] epoch 69, loss 0.0338
INFO [18:29:22.703] epoch 70, loss 0.0338
INFO [18:29:24.433] epoch 71, loss 0.0337
INFO [18:29:26.282] epoch 72, loss 0.0337
INFO [18:29:28.166] epoch 73, loss 0.0337
INFO [18:29:29.885] epoch 74, loss 0.0336
INFO [18:29:31.645] epoch 75, loss 0.0336
INFO [18:29:33.496] epoch 76, loss 0.0336
INFO [18:29:35.312] epoch 77, loss 0.0335
INFO [18:29:37.151] epoch 78, loss 0.0335
INFO [18:29:38.930] epoch 79, loss 0.0335
INFO [18:29:40.744] epoch 80, loss 0.0334
INFO [18:29:42.614] epoch 81, loss 0.0334
INFO [18:29:44.491] epoch 82, loss 0.0334
INFO [18:29:46.363] epoch 83, loss 0.0333
INFO [18:29:48.238] epoch 84, loss 0.0333
INFO [18:29:49.906] epoch 85, loss 0.0333
INFO [18:29:51.593] epoch 86, loss 0.0333
INFO [18:29:53.341] epoch 87, loss 0.0332
INFO [18:29:55.223] epoch 88, loss 0.0332
INFO [18:29:57.041] epoch 89, loss 0.0332
INFO [18:29:58.787] epoch 90, loss 0.0332
INFO [18:30:00.598] epoch 91, loss 0.0331
INFO [18:30:02.454] epoch 92, loss 0.0331
INFO [18:30:04.175] epoch 93, loss 0.0331
INFO [18:30:06.175] epoch 94, loss 0.0331
INFO [18:30:07.885] epoch 95, loss 0.0330
INFO [18:30:09.687] epoch 96, loss 0.0330
INFO [18:30:11.448] epoch 97, loss 0.0330
INFO [18:30:13.319] epoch 98, loss 0.0330
INFO [18:30:15.127] epoch 99, loss 0.0329
INFO [18:30:16.896] epoch 100, loss 0.0329
Finalmente, creamos los vectores de palabras sumando dos componentes: un vector de significados principales y un vector de contexto.
# Asignación del vector de contexto
necronomicon_wv_context = glove$components
# Creación de los vectores de palabras
necronomicon_word_vectors = necronomicon_wv_main + t(necronomicon_wv_context)
necronomicon_word_vectors[1:6, 1:6]
[,1] [,2] [,3] [,4] [,5] [,6]
abandoner -0.31848269 0.07243172 -0.11872560 -0.6632458 0.003957836 0.48662676
abater -0.01169542 0.13845203 -0.72439489 -0.1483219 -0.022623716 0.92812916
abeja 0.08270150 0.37814797 -0.12440411 1.0096737 0.167521339 -0.63285243
abriéndo -0.08676527 -0.51560951 0.29158454 0.4849325 0.348935537 -1.56050584
absorbente -0.45029276 -0.45168083 0.56228256 -0.7140288 0.160443237 -0.03535428
absorbido -0.53519803 0.34017082 -0.01328539 -0.3326715 -1.167520354 0.59233555
Para comprobar que el procedimiento seguido tenga resultados válidos, revisemos los vectores más cercanos a 3 ‘’Grandes’’ o ‘’Antiguos’’ y un personaje clave dentro de la literatura de Lovecraft: Cthulhu (el que vive en las profundidades de R’lyeh), Nyarlathotep (el caos reptante), Azatoth (el gran demonio lobotomizado) y Randolph Carter. Esto lo lograremos a través del cálculo de la similaridad del coseno.
cthulhu = necronomicon_word_vectors["cthulhu", , drop = F]
cos_sim_cthulhu = sim2(x = necronomicon_word_vectors, y = cthulhu, method = "cosine", norm = "l2")
head(sort(cos_sim_cthulhu[,1], decreasing = T), 20)
cthulhu progenie grandes difunto dioses educar desenlace niñez calvo arcano
1.0000000 0.4900879 0.4810451 0.4549651 0.4535383 0.4422686 0.4378781 0.4363859 0.4349288 0.4309892
boreal contár transilvania agudeza averno infundir amueblado denys r'lyeh petición
0.4172085 0.4159479 0.4104915 0.4101301 0.4080863 0.4047745 0.4022618 0.3958539 0.3913419 0.3878506
nyarla = necronomicon_word_vectors["nyarlathotep", , drop = F]
cos_sim_nyarla = sim2(x = necronomicon_word_vectors, y = nyarla, method = "cosine", norm = "l2")
head(sort(cos_sim_nyarla[,1], decreasing = T), 20)
nyarlathotep caos reptante mensajero dioses espíritu aprender palacio necronomicon reliquia
1.0000000 0.7338404 0.6706194 0.5793857 0.5786593 0.5715900 0.5064829 0.4776228 0.4643920 0.4446749
posesión alhazred poderoso encontrado discuter rendir conservación sona-nyl plantear destino
0.4397264 0.4349346 0.4265074 0.4228891 0.4150610 0.4143902 0.4070119 0.4038883 0.4032745 0.3999672
azathoth = necronomicon_word_vectors["azathoth", , drop = F]
cos_sim_azathoth = sim2(x = necronomicon_word_vectors, y = azathoth, method = "cosine", norm = "l2")
head(sort(cos_sim_azathoth[,1], decreasing = T), 20)
azathoth descubertar aconsejable demonio caverno ominoso alienista compromiso sonreer insufrible diverso
1.0000000 0.5084301 0.4766871 0.4659447 0.4558213 0.4431632 0.4369158 0.4355890 0.4350156 0.4240887 0.4231161
sorcier congo colega referencia entonar repasar noyes bisabuela acusación
0.4229815 0.4126149 0.4079624 0.4069512 0.4059271 0.4014247 0.4002436 0.3965964 0.3927615
randolph = necronomicon_word_vectors["randolph", , drop = F]
cos_sim_randolph = sim2(x = necronomicon_word_vectors, y = randolph, method = "cosine", norm = "l2")
head(sort(cos_sim_randolph[,1], decreasing = T), 20)
randolph carter comprender fragmento pormenor capitán gul amar llave bien asellius
1.0000000 0.8005259 0.5270819 0.5172530 0.4852574 0.4836598 0.4775300 0.4653673 0.4505735 0.4488241 0.4477838
entidad yog-sothoth significar antepasado traspasar atraído soñador caos alegrar
0.4376358 0.4309294 0.4147632 0.4142185 0.4108100 0.4101656 0.4034271 0.4033007 0.3977327
Como se puede notar, las palabras más cercanas a los términos analizados tienen bastante relación dentro de la literatura lovecraftiana.
Una vez que hemos creado los vectores de palabras, visualizaremos su cercanía a través de su proyección en dos dimensiones gracias al algoritmo t-SNE, disponible en la librería Rtsne. Para ello, no hacen falta más que dos líneas de código.
# Datos de entrenamiento como dataframe
necronomicon_train_df = necronomicon_word_vectors %>%
as.data.frame() %>%
rownames_to_column("word")
# Entrenamiento de t-SNE
necronomico_tsne = Rtsne(necronomicon_train_df[,-1], dims = 2, perplexity = 50, verbose=TRUE, max_iter = 500)
Performing PCA
Read the 6406 x 50 data matrix successfully!
OpenMP is working. 1 threads.
Using no_dims = 2, perplexity = 50.000000, and theta = 0.500000
Computing input similarities...
Building tree...
Done in 9.77 seconds (sparsity = 0.040333)!
Learning embedding...
Iteration 50: error is 82.519176 (50 iterations in 2.53 seconds)
Iteration 100: error is 82.519175 (50 iterations in 2.80 seconds)
Iteration 150: error is 82.519177 (50 iterations in 3.61 seconds)
Iteration 200: error is 82.519177 (50 iterations in 6.20 seconds)
Iteration 250: error is 82.519174 (50 iterations in 4.35 seconds)
Iteration 300: error is 4.391691 (50 iterations in 4.49 seconds)
Iteration 350: error is 4.391691 (50 iterations in 3.72 seconds)
Iteration 400: error is 3.921769 (50 iterations in 5.28 seconds)
Iteration 450: error is 3.868845 (50 iterations in 2.96 seconds)
Iteration 500: error is 3.850003 (50 iterations in 2.95 seconds)
Fitting performed in 38.88 seconds.
Para visualizar los resultados utilizaremos el siguiente código.
# Creación de un vector de palabras a visualizar
palabras = c(sort(cos_sim_cthulhu[,1], decreasing = T) %>% head(20) %>% names(),
sort(cos_sim_nyarla[,1], decreasing = T) %>% head(20) %>% names())
# Definición de colores
colores = rainbow(length(unique(necronomicon_train_df$word)))
names(colores) = unique(necronomicon_train_df$word)
# Creación de dataframe con los resultados del algoritmo
necronomico_tsne_df = data.frame(necronomico_tsne$Y) %>%
mutate(word = necronomicon_train_df$word,
col = colores[necronomicon_train_df$word]) %>%
left_join(necronomicon_vocabulario, by = c("word" = "term"))
# Gráfico
necronomico_tsne_df %>%
filter(word %in% palabras) %>%
ggplot(aes(X1, X2)) +
geom_label_repel(aes(X1, X2, label = word, color = col), size = 7, max.overlaps = 20) +
xlab("") + ylab("") +
theme(legend.position = "none")
Una vez construida la representación de los word embeddings en dos dimensiones, visualizaremos la matriz betta a través de la técnica recién revisada. Esto lo logramos a través del siguiente código.
# Unión del resultado de t-SNE y la matriz beta tópicos
necronomicon_beta_tsne_df = necronomico_tsne_df %>%
left_join(necronomicon_beta_lda, by = c("word" = "term")) %>%
filter(beta > .005)
# Creación del gráfico
necronomicon_beta_tsne_df %>%
ggplot(aes(X1, X2, label = word, fill=word)) +
geom_label_repel(size = 7, max.overlaps = 15) +
facet_wrap(~topic, scales= "free")+
theme(legend.position = "none")
A partir de este gráfico podrá mejorar nuestra interpretación de cada tópico.