1. Análisis no supervisado: Métodos de clustering

1.1. Definiciones necesarias para el análisis

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

1.2. Introducción al modelado de tópicos

Dentro del PLN, un modelo de tópicos es un algoritmo de aprendizaje del lenguaje que identifica tópicos o temáticas en las cuales las palabras que comparten un contexto similar aparecen juntas. Es muy similar a agrupar palabras similares, salvo que estamos utilizando una técnica de agrupación difusa, es decir, cada palabra va a tener una probabilidad determinada de clasificar en una temática, al igual que los documentos.

Una de las técnicas más comunes para el modelado de tópicos es la Asignación Latente de Dirichlet o LDA (aunque existen otros métodos como los modelos de tópicos correlacionados). El modelo LDA utiliza como principal insumo las matrices documento término con medidas tales como la frecuencia de palabras, idf, tf-idf, etc. Para más información puedes consultar el Capítulo 4 del curso.

Específicamente, el algoritmo de Asignación Latente de Dirichlet es un modelo generativo que permite que conjuntos de observaciones puedan ser explicados por grupos no observados que explican porqué algunas partes de los datos son similares. Por ejemplo, si las observaciones son palabras en documentos, presupone que cada documento es una mezcla de un pequeño número de categorías (también denominados como tópicos) y la aparición de cada palabra en un documento se debe a una de las categorías a las que el documento pertenece.

Para optimizar el número de tópicos creados por esta técnica se utilizan cuatro métricas desarrolladas por Arun y otros (2010), Juan y otros (2009), Deveaud, SanJuan y Bellot (2014) y Griffiths y Steyvers (2004), dos de las cuales deben ser minimizadas, y las otras dos, maximizadas, a través del entrenamiento de varios modelos de tópicos.

1.3. Caso de estudio: Agrupación de tópicos en la literatura de Howard Phillips Lovecraft

Al analizar datos provenientes de la literatura podremos descubrir que esta, a pesar de pertenecer a un género, puede realcionarse a una gran variedad de temáticas. En este caso de estudio analizaremos los datos procesados en el Capítulo 3 para obtener las principales temáticas incluidas en las obras de uno de los mejores escritores de terror del siglo XX: Howard Phillips Lovecraft, la cual ha inspirado un sinúmero de obras, incluyendo series de televisión, películas y videojuegos.

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(tm)
library(tidyverse)
library(tidytext)
library(ggrepel)
library(topicmodels)
library(ldatuning)
library(stopwords)
library(udpipe)

# Carga de datos
necronomicon_df = readRDS("Caso2_Literatura/NecronomiconDF.RDS")

2. Modelo de agrupación de tópicos

2.1. Anotación y procesamiento de las historias

Para comenzar el análisis, revisemos una de las historias disponibles:

necronomicon_df %>% filter(Titulo=="Nyarlathotep") %>% pull(Historia) %>% str(nchar.max = 500)
 chr "Y al fin vino del interior de Egipto El extraño Oscuro ante el que se inclinaban los fellás; Silencioso, descarnado, enigmáticamente altivo Y envuelto en telas rojas como las llamas del sol poniente. A su alrededor se apretaban las masas, ansiosas de sus órdenes, Pero al marcharse no podían repetir lo que habían oído; Mientras por las naciones se propagaba la pavorosa noticia De que las bestias salvajes le seguían lamiéndole las manos. Pronto comenzó en el mar un nacimiento pern"| __truncated__
Error in gregexpr(calltext, singleline, fixed = TRUE) : 
  regular expression is invalid UTF-8

Luego, es necesario que procesemos los datos de texto cargados, realizando las siguientes tareas:

  • Etiquetado morfosintáctico
  • Lematización
  • Remoción de stopwords

Esto lo logramos a través del siguiente código (guardando los datos, dado que la tarea es computacionalmente pesada).

# Eliminación de caracteres particulares
necronomicon_df = necronomicon_df %>% 
  mutate(Historia = str_squish(str_replace_all(Historia,"—"," ")))

# Anotación de datos
model_spanish = udpipe_load_model("spanish-gsd-ud-2.5-191206.udpipe")
necronomicon_tokenizado_df = udpipe_annotate(model_spanish, necronomicon_df$Historia, 
                                             doc_id = necronomicon_df$Titulo)
necronomicon_tokenizado_df = necronomicon_tokenizado_df %>% 
  as_tibble() %>% mutate(lemma=tolower(lemma))

# Filtrado de stopwords
necronomicon_tokenizado_df = necronomicon_tokenizado_df %>% 
  filter(!lemma %in% stopwords(language = "es", source = "nltk"),
         !token %in% stopwords(language = "es", source = "nltk"))

# Guardado de datos procesados
saveRDS(necronomicon_tokenizado_df, "Caso2_Literatura/necronomicon_tokenizado_df.RDS")

Observemos un ejemplo del resultado de este procesamiento.

necronomicon_tokenizado_df %>% 
  filter(doc_id=="Nyarlathotep") %>% 
  head(10)

2.2. Creación de la matriz documento-término

Una vez que los datos han sido tokenizados y anotados, podemos crear el principal insumo del análisis LDA: la matriz documento término, para ello utilizaremos únicamente sustantivos, verbos, adjetivos y nombres propios con el objetivo de eliminar del análisis todas las posibles palabras que puedan causarnos ruido.

Para lograrlo usaremos el siguiente código.

# Conteo de lemmas y tf-idf por cada documento
necronomicon_tfidf_df = necronomicon_tokenizado_df %>% 
  filter(!is.na(lemma)) %>% 
  filter(upos %in% c("VERB","ADJ","NOUN","PROPN")) %>% 
  count(doc_id, lemma) %>% 
  bind_tf_idf(lemma, doc_id, n)
  
# Creación de matriz documento término
necronomicon_dtm = necronomicon_tfidf_df %>%
  cast_dtm(doc_id, lemma, n)

necronomicon_dtm
<<DocumentTermMatrix (documents: 76, terms: 19586)>>
Non-/sparse entries: 90409/1398127
Sparsity           : 94%
Maximal term length: 35
Weighting          : term frequency (tf)

Observemos cómo se ha construido esta matriz.

as.matrix(necronomicon_dtm)[1:6,1:6]
                                              Terms
Docs                                           abalanzar abandonado abandonar abdul abertura abierto
  A Través De Las Puertas De La Llave De Plata         1          2         3     1        1       2
  Aire Frío                                            0          0         2     0        0       2
  Al Otro Lado De La Barrera Del Sueño                 0          0         2     0        0       2
  Arthur Jermyn                                        0          0         2     0        0       0
  Azathoth                                             1          0         0     0        0       0
  Celephaïs                                            0          0         0     0        0       0

2.3. Selección del número de tópicos

Una vez construida la matriz documento término, utilizaremos las cuatro medidas especificadas anteriormente para seleccionar el número óptimo de tópicos. Esto lo haremos gracias a la librería ldatuning. Cabe notar en este punto que de las medidas especificadas, dos deben ser minimizadas, mientras que las otras dos deben ser maximizadas, acorde al gráfico ejecutado.

# Selección del número óptimo de tópicos
necronomicon_ntopics = FindTopicsNumber(necronomicon_dtm,
                              topics = seq(from = 2, to = 20, by = 1),
                              metrics = c("Griffiths2004", "CaoJuan2009", "Arun2010", "Deveaud2014"),
                              method = "Gibbs",
                              control = list(seed = 123),
                              mc.cores = 4,
                              verbose = TRUE)
fit models... done.
calculate metrics:
  Griffiths2004... done.
  CaoJuan2009... done.
  Arun2010... done.
  Deveaud2014... done.
gc()
           used  (Mb) gc trigger  (Mb) max used  (Mb)
Ncells  2844982 152.0    6441458 344.1  6441458 344.1
Vcells 11174100  85.3   35197756 268.6 43997194 335.7
FindTopicsNumber_plot(necronomicon_ntopics)

Como se puede notar, para 9 tópicos dos de las medidas alcanzan casi su nivel mínimo, mientras que las otras dos casi se maximizan.

2.4. Modelado de tópicos

Una vez que hayamos seleccionado el número adecuado de tópicos, estimamos el modelo seleccionado.

# Modelo LDA final
necronomicon_lda = LDA(necronomicon_dtm, k = 9, method = "Gibbs", control = list(seed=123))

Una vez que el modelo haya sido estimado, este nos arrojará dos matrices: Beta y Gamma. La matriz Beta denota la probabilidad de cada palabra de pertenecer a cada tópico y la matriz Beta muestra la probabilidad de cada documento de pertenecer a cada tópico. Para entenderlo de mejor manera, realicemos un gráfico de cada matriz.

# Obtención de matriz beta
necronomicon_beta_lda = tidy(necronomicon_lda, matrix = "beta")

# Gráfico de palabras más probables por tópico
necronomicon_beta_lda %>%
  group_by(topic) %>%
  slice_max(order_by = beta, n = 10) %>%
  ggplot(aes(x=reorder_within(term, beta, topic), beta, fill = as.factor(topic))) +
  scale_x_reordered()+
  geom_col(show.legend = FALSE) +
  facet_wrap(vars(topic), scales = "free") +
  coord_flip()+
  labs(x="Palabra")+
  theme(text=element_text(size=11))

# Obtención de matriz beta
necronomicon_gamma_lda = tidy(necronomicon_lda, matrix = "gamma")

# Gráfico de documentos más probables por tópico
necronomicon_gamma_lda %>%
  group_by(topic) %>%
  slice_max(order_by = gamma, n = 3) %>%
  ggplot(aes(x=reorder_within(document, gamma, topic), gamma, fill = as.factor(topic))) +
  scale_x_reordered()+
  geom_col(show.legend = FALSE) +
  facet_wrap(vars(topic), scales = "free", ncol = 2) +
  coord_flip()+
  labs(x="Documento")+
  theme(text=element_text(size=11))

Como podemos observar (salvo un mejor criterio), los cuentos se encuentran agrupados dentro de temáticas similares.

Finalmente, guardaremos los resultados de las matrices Beta y Gamma con la finalidad de volverlas a visualizar (con herramientas como Word Embeddings y t-SNE) en el próximo capítulo

saveRDS(necronomicon_gamma_lda, "Caso2_Literatura/necronomicon_gamma_lda.RDS")
saveRDS(necronomicon_beta_lda, "Caso2_Literatura/necronomicon_beta_lda.RDS")

3. Bibliografía

Arun, R. y otros (2010), «On finding the natural number of topics with latent dirichlet allocation: Some observations», Advances in knowledge discovery and data mining.
Deveaud, R., SanJuan, É. & Bellot, P. (2014), «Accurate and effective latent concept modeling for ad hoc information retrieval».
Griffiths, T. L. & Steyvers, M. (2004), «Finding scientific topics», Proceedings of the National Academy of Sciences.
Juan, C. y otros (2009), «A density-based method for adaptive lda model selection», Neurocomputing.
LS0tDQp0aXRsZTogIkFuw6FsaXNpcyBkZSBjb21wb3J0YW1pZW50byBlbiByZWRlcyBzb2NpYWxlcyB1c2FuZG8gUHJvY2VzYW1pZW50byBkZWwgTGVuZ3VhamUgTmF0dXJhbCINCnN1YnRpdGxlOiAnQ2Fww610dWxvIDg6IEFwcmVuZGl6YWplIG5vIHN1cGVydmlzYWRvLCBDbHVzdGVyaW5nJw0KYXV0aG9yOiBIdWdvIFBvcnJhcw0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOg0KICAgIGNzczogRXN0aWxvcy5jc3MNCiAgICB0b2M6IHRydWUNCiAgICB0b2NfZGVwdGg6IDINCiAgICB0b2NfZmxvYXQ6DQogICAgICBjb2xsYXBzZWQ6IHRydWUNCiAgICAgIHNtb290aF9zY3JvbGw6IGZhbHNlDQpiaWJsaW9ncmFwaHk6IEJpYmxpb2dyYWZpYS5iaWINCmNzbDogY2VwYWwueG1sDQotLS0NCg0KYGBge3IgZWNobz1GLCBtZXNzYWdlPUYsIHdhcm5pbmc9RiwgZXJyb3I9RkFMU0V9DQpsaWJyYXJ5KGthYmxlRXh0cmEpDQpgYGANCg0KDQojICoqMS4gQW7DoWxpc2lzIG5vIHN1cGVydmlzYWRvOiBNw6l0b2RvcyBkZSBjbHVzdGVyaW5nKioNCg0KIyMgKioxLjEuIERlZmluaWNpb25lcyBuZWNlc2FyaWFzIHBhcmEgZWwgYW7DoWxpc2lzKioNCg0KKipBcHJlbmRpemFqZSBubyBzdXBlcnZpc2FkbzoqKiBUZW5lbW9zIHVuIGNvbmp1bnRvIHZhcmlhYmxlIGNvbnRpbnVhcyBvIGNhdGVnw7NyaWNhcyBzb2JyZSBsYXMgY3VhbGVzIGRlc2VhbW9zIGFwcmVuZGVyIG8gZGVzY3J1YnJpciB1biBwYXRyw7NuIGRlIGNvbXBvcnRhbWllbnRvLiBUYWxlcyBwYXRyb25lcyBwdWVkZW4gc2VyIGRlc2N1YmllcnRvcyBhIHRyYXbDqXMgZGUgdMOpY25pY2FzIGRlIHJlZHVjY2nDs24gZGUgZGltZW5zaW9uYWxpZGFkIG8gdMOpY25pY2FzIGRlIGFncnVwYW1pZW50bywgbGFzIGN1YWxlcyBzb24gZGVmaW5pZGFzIGJyZXZlbWVudGUgYSBjb250aW51YWNpw7NuLg0KDQohW10oZmlncy8wOF9VbnN1cGVydmlzZWRMZWFybmluZy5wbmcpDQoNCioqUmVkdWNjacOzbiBkZSBkaW1lbnNpb25hbGlkYWQ6KiogTG9zIG3DqXRvZG9zIGRlIHJlZHVjY2nDs24gZGUgZGltZW5zaW9uYWxpZGFkIGNvbnNpc3RlbiBlbiByZXN1bWlyIHkgdmlzdWFsaXphciBsYSBpbmZvcm1hY2nDs24gbcOhcyBpbXBvcnRhbnRlIGNvbnRlbmlkYSBlbiB1biBkYXRhc2V0LiBFeGlzdGVuIHZhcmlhcyB0w6ljbmljYXMgYWxyZWRlZG9yIGRlIGVzdGEgdGVtw6F0aWNhLCB0YW50byBzaSBhc3VtaW1vcyBxdWUgZXhpc3RlbiBwYXRyb25lcyBsaW5lYWxlcyBjb21vIG5vIGxpbmVhbGVzIGVuIGxvcyBkYXRvcy4gQSBjb250aW51YWNpw7NuIHNlIGVudW1lcmFuIGFsZ3VuYXMgZGUgZXN0YXMgdMOpY25pY2FzLCBjbGFzaWZpY8OhbmRvbGFzIGFjb3JkZSBhbCB0aXBvIGRlIGRhdG9zIGNvbiBlbCBxdWUgZXN0ZW1vcyB0cmFiYWphbmRvIHkgYWxndW5vcyBkZSBsb3MgcGFxdWV0ZXMgZXhpc3RlbnRlcyBwYXJhIHN1IHVzbzoNCg0KYGBge3IsIGVjaG89RkFMU0V9DQprYWJsZV9zdHlsaW5nKGtibChkYXRhLmZyYW1lKCJUaXBvVmFyaWFibGVzIj1jKCJOdW3DqXJpY29zIiwiQ2F0ZWfDs3JpY29zIiwiTWl4dG9zIiksDQogICAgICAgICAgICAgICAiVGVjbmljYSI9YygiQW7DoWxpc2lzIGRlIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzLCB0LVNORSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAiQW7DoWxpc2lzIGRlIGNvcnJlc3BvbmRlbmNpYXMgbcO6bHRpcGxlcyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAiQW7DoWxpc2lzIGZhY3RvcmlhbCBtaXh0byIpLA0KICAgICAgICAgICAgICAgIkxpYnJlcmlhcyIgPSBjKCJiYXNlLCBGYWN0b01pbmVSLCBSdHNuZSIsICJGYWN0b01pbmVyLCBhZGU0LCBlcE1DQSIsICJGYWN0b01pbmVyIiksIA0KICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpKSkNCmBgYA0KDQoqKkNsdXN0ZXJpbmc6KiogTGFzIHTDqWNuaWNhcyBkZSBhZ3J1cGFtaWVudG8gbyBjbHVzdGVyaW5nIG5vcyBwZXJtaXRlbiBvYnRlbmVyIGNvbm9jaW1pZW50byBhIHBhcnRpciBkZWwgZGVzY3VicmltaWVudG8gZGUgcGF0cm9uZXMgZXhpc3RlbnRlcyBlbiBsb3MgZGF0b3MuIEVzcGVjw61maWNhbWVudGUsIGVsIG9iamV0aXZvIGRlIGxvcyBtw6l0b2RvcyBkZSBjbHVzdGVyaW5nIHlhY2VuIGVuIGxhIGlkZW50aWZpY2FjacOzbiBkZSBncnVwb3MgZGUgb2JqZXRvcyBzaW1pbGFyZXMgZW4gdW4gY29uanVudG8gZGUgZGF0b3MgZGUgaW50ZXLDqXMgYSB0cmF2w6lzIGRlIHVuYSBtZWRpZGEgZGUgc2ltaWxhcmlkYWQgZW50cmUgcHVudG9zIChlLmcgbGEgZGlzdGFuY2lhIGV1Y2xpZGVhbmEpLiBBIGNvbnRpbnVhY2nDs24gc2UgZW51bWVyYW4gYWxndW5hcyBkZSBlc3RhcyB0w6ljbmljYXMsIGNsYXNpZmljw6FuZG9sYXMgYWNvcmRlIGFsIHRpcG8gZGUgZGF0b3MgY29uIGVsIHF1ZSBlc3RlbW9zIHRyYWJhamFuZG8geSBhbGd1bm9zIGRlIGxvcyBwYXF1ZXRlcyBleGlzdGVudGVzIHBhcmEgc3UgdXNvOg0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCmxpYnJhcnkoa2FibGVFeHRyYSkNCmthYmxlX3N0eWxpbmcoa2JsKGRhdGEuZnJhbWUoIlRpcG9WYXJpYWJsZXMiPWMoIk51bcOpcmljb3MiLCJDYXRlZ8Ozcmljb3MiLCJNaXh0b3MiKSwNCiAgICAgICAgICAgICAgICJUZWNuaWNhIj1jKCJrLW1lZGlhcywgR01NLCBDTEFSQSwgQXNpZ25hY2nDs24gTGF0ZW50ZSBkZSBEaXJpY2hsZXQgTERBIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJrLW1vZGFzLCBvdHJhcyBtZWRpZGFzIGRlIGRpc3RhbmNpYSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAiay1wcm90b3R5cGVzLCBvdHJhcyBtZWRpZGFzIGRlIGRpc3RhbmNpYSIpLA0KICAgICAgICAgICAgICAgIkxpYnJlcmlhcyIgPSBjKCJjbHVzdGVyLCBGYWN0b01pbmVSLCBtY2x1c3QsIHRvcGljbW9kZWxzIiwgImtsYVIiLCAiY2x1c3RNaXhUeXBlIiksIA0KICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYpKSkNCmBgYA0KDQojIyAqKjEuMi4gSW50cm9kdWNjacOzbiBhbCBtb2RlbGFkbyBkZSB0w7NwaWNvcyoqDQoNCkRlbnRybyBkZWwgUExOLCB1biBtb2RlbG8gZGUgdMOzcGljb3MgZXMgdW4gYWxnb3JpdG1vIGRlIGFwcmVuZGl6YWplIGRlbCBsZW5ndWFqZSBxdWUgaWRlbnRpZmljYSB0w7NwaWNvcyBvIHRlbcOhdGljYXMgZW4gbGFzIGN1YWxlcyBsYXMgcGFsYWJyYXMgcXVlIGNvbXBhcnRlbiB1biBjb250ZXh0byBzaW1pbGFyIGFwYXJlY2VuIGp1bnRhcy4gRXMgbXV5IHNpbWlsYXIgYSBhZ3J1cGFyIHBhbGFicmFzIHNpbWlsYXJlcywgc2Fsdm8gcXVlIGVzdGFtb3MgdXRpbGl6YW5kbyB1bmEgdMOpY25pY2EgZGUgYWdydXBhY2nDs24gZGlmdXNhLCBlcyBkZWNpciwgY2FkYSBwYWxhYnJhIHZhIGEgdGVuZXIgdW5hIHByb2JhYmlsaWRhZCBkZXRlcm1pbmFkYSBkZSBjbGFzaWZpY2FyIGVuIHVuYSB0ZW3DoXRpY2EsIGFsIGlndWFsIHF1ZSBsb3MgZG9jdW1lbnRvcy4NCg0KVW5hIGRlIGxhcyB0w6ljbmljYXMgbcOhcyBjb211bmVzIHBhcmEgZWwgbW9kZWxhZG8gZGUgdMOzcGljb3MgZXMgbGEgQXNpZ25hY2nDs24gTGF0ZW50ZSBkZSBEaXJpY2hsZXQgbyBMREEgKGF1bnF1ZSBleGlzdGVuIG90cm9zIG3DqXRvZG9zIGNvbW8gbG9zIG1vZGVsb3MgZGUgdMOzcGljb3MgY29ycmVsYWNpb25hZG9zKS4gRWwgbW9kZWxvIExEQSB1dGlsaXphIGNvbW8gcHJpbmNpcGFsIGluc3VtbyBsYXMgbWF0cmljZXMgZG9jdW1lbnRvIHTDqXJtaW5vIGNvbiBtZWRpZGFzIHRhbGVzIGNvbW8gbGEgZnJlY3VlbmNpYSBkZSBwYWxhYnJhcywgaWRmLCB0Zi1pZGYsIGV0Yy4gUGFyYSBtw6FzIGluZm9ybWFjacOzbiBwdWVkZXMgY29uc3VsdGFyIGVsIFtDYXDDrXR1bG8gNF0oaHR0cHM6Ly9ycHVicy5jb20vaHVnb3BvcnJhcy9ubHBfY2FwaXR1bG80KSBkZWwgY3Vyc28uDQoNCkVzcGVjw61maWNhbWVudGUsIGVsIGFsZ29yaXRtbyBkZSBBc2lnbmFjacOzbiBMYXRlbnRlIGRlIERpcmljaGxldCBlcyB1biBtb2RlbG8gZ2VuZXJhdGl2byBxdWUgcGVybWl0ZSBxdWUgY29uanVudG9zIGRlIG9ic2VydmFjaW9uZXMgcHVlZGFuIHNlciBleHBsaWNhZG9zIHBvciBncnVwb3Mgbm8gb2JzZXJ2YWRvcyBxdWUgZXhwbGljYW4gcG9ycXXDqSBhbGd1bmFzIHBhcnRlcyBkZSBsb3MgZGF0b3Mgc29uIHNpbWlsYXJlcy4gUG9yIGVqZW1wbG8sIHNpIGxhcyBvYnNlcnZhY2lvbmVzIHNvbiBwYWxhYnJhcyBlbiBkb2N1bWVudG9zLCBwcmVzdXBvbmUgcXVlIGNhZGEgZG9jdW1lbnRvIGVzIHVuYSBtZXpjbGEgZGUgdW4gcGVxdWXDsW8gbsO6bWVybyBkZSBjYXRlZ29yw61hcyAodGFtYmnDqW4gZGVub21pbmFkb3MgY29tbyB0w7NwaWNvcykgeSBsYSBhcGFyaWNpw7NuIGRlIGNhZGEgcGFsYWJyYSBlbiB1biBkb2N1bWVudG8gc2UgZGViZSBhIHVuYSBkZSBsYXMgY2F0ZWdvcsOtYXMgYSBsYXMgcXVlIGVsIGRvY3VtZW50byBwZXJ0ZW5lY2UuIA0KDQpQYXJhIG9wdGltaXphciBlbCBuw7ptZXJvIGRlIHTDs3BpY29zIGNyZWFkb3MgcG9yIGVzdGEgdMOpY25pY2Egc2UgdXRpbGl6YW4gY3VhdHJvIG3DqXRyaWNhcyBkZXNhcnJvbGxhZGFzIHBvciBAUmFqa3VtYXIyMDEwLCBASnVhbjIwMDksIEBEZXZlYXVkMjAxNCB5IEBHcmlmZml0aHMyMDA0LCBkb3MgZGUgbGFzIGN1YWxlcyBkZWJlbiBzZXIgbWluaW1pemFkYXMsIHkgbGFzIG90cmFzIGRvcywgbWF4aW1pemFkYXMsIGEgdHJhdsOpcyBkZWwgZW50cmVuYW1pZW50byBkZSB2YXJpb3MgbW9kZWxvcyBkZSB0w7NwaWNvcy4NCg0KIVtdKGZpZ3MvMDhfdG9waWNfbW9kZWwucG5nKQ0KDQojIyAqKjEuMy4gQ2FzbyBkZSBlc3R1ZGlvOiBBZ3J1cGFjacOzbiBkZSB0w7NwaWNvcyBlbiBsYSBsaXRlcmF0dXJhIGRlIEhvd2FyZCBQaGlsbGlwcyBMb3ZlY3JhZnQqKg0KDQpBbCBhbmFsaXphciBkYXRvcyBwcm92ZW5pZW50ZXMgZGUgbGEgbGl0ZXJhdHVyYSBwb2RyZW1vcyBkZXNjdWJyaXIgcXVlIGVzdGEsIGEgcGVzYXIgZGUgcGVydGVuZWNlciBhIHVuIGfDqW5lcm8sIHB1ZWRlIHJlYWxjaW9uYXJzZSBhIHVuYSBncmFuIHZhcmllZGFkIGRlIHRlbcOhdGljYXMuIEVuIGVzdGUgY2FzbyBkZSBlc3R1ZGlvIGFuYWxpemFyZW1vcyBsb3MgZGF0b3MgcHJvY2VzYWRvcyBlbiBlbCBbQ2Fww610dWxvIDNdKGh0dHBzOi8vcnB1YnMuY29tL2h1Z29wb3JyYXMvbmxwX2NhcGl0dWxvMykgcGFyYSBvYnRlbmVyIGxhcyBwcmluY2lwYWxlcyB0ZW3DoXRpY2FzIGluY2x1aWRhcyBlbiBsYXMgb2JyYXMgZGUgdW5vIGRlIGxvcyBtZWpvcmVzIGVzY3JpdG9yZXMgZGUgdGVycm9yIGRlbCBzaWdsbyBYWDogSG93YXJkIFBoaWxsaXBzIExvdmVjcmFmdCwgbGEgY3VhbCBoYSBpbnNwaXJhZG8gdW4gc2luw7ptZXJvIGRlIG9icmFzLCBpbmNsdXllbmRvIHNlcmllcyBkZSB0ZWxldmlzacOzbiwgcGVsw61jdWxhcyB5IHZpZGVvanVlZ29zLg0KDQohW10oZmlncy8wOF9sb3ZlY3JhZnRfY291bnRyeS5qcGVnKQ0KDQpDb24gZXN0YXMgZGVmaW5pY2lvbmVzIGVuIG1lbnRlLCB5IGhhYmllbmRvIGRlZmluaWRvIGVsIGNhc28gZW50cmUgbWFub3MsIHBvZGVtb3MgcHJlcGFyYXIgZWwgdGVycmVubyBwYXJhIHJlYWxpemFyIGVsIG1vZGVsYWRvIGNhcmdhbmRvIGxhcyBsaWJyZXLDrWFzIHkgbG9zIGRhdG9zIGEgc2VyIHVzYWRvcy4NCg0KYGBge3IgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQojIENhcmdhIGRlIGxpYnJlcsOtYXMNCmxpYnJhcnkodG0pDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkodGlkeXRleHQpDQpsaWJyYXJ5KGdncmVwZWwpDQpsaWJyYXJ5KHRvcGljbW9kZWxzKQ0KbGlicmFyeShsZGF0dW5pbmcpDQpsaWJyYXJ5KHN0b3B3b3JkcykNCmxpYnJhcnkodWRwaXBlKQ0KDQojIENhcmdhIGRlIGRhdG9zDQpuZWNyb25vbWljb25fZGYgPSByZWFkUkRTKCJDYXNvMl9MaXRlcmF0dXJhL05lY3Jvbm9taWNvbkRGLlJEUyIpDQpgYGANCg0KIyAqKjIuIE1vZGVsbyBkZSBhZ3J1cGFjacOzbiBkZSB0w7NwaWNvcyoqDQoNCiMjICoqMi4xLiBBbm90YWNpw7NuIHkgcHJvY2VzYW1pZW50byBkZSBsYXMgaGlzdG9yaWFzKioNCg0KUGFyYSBjb21lbnphciBlbCBhbsOhbGlzaXMsIHJldmlzZW1vcyB1bmEgZGUgbGFzIGhpc3RvcmlhcyBkaXNwb25pYmxlczoNCg0KYGBge3J9DQpuZWNyb25vbWljb25fZGYgJT4lIGZpbHRlcihUaXR1bG89PSJOeWFybGF0aG90ZXAiKSAlPiUgcHVsbChIaXN0b3JpYSkgJT4lIHN0cihuY2hhci5tYXggPSA1MDApDQpgYGANCg0KIVtdKGZpZ3MvMDhfbnlhcmxhdGhvdGVwLmpwZykNCg0KTHVlZ28sIGVzIG5lY2VzYXJpbyBxdWUgcHJvY2VzZW1vcyBsb3MgZGF0b3MgZGUgdGV4dG8gY2FyZ2Fkb3MsIHJlYWxpemFuZG8gbGFzIHNpZ3VpZW50ZXMgdGFyZWFzOg0KDQorIEV0aXF1ZXRhZG8gbW9yZm9zaW50w6FjdGljbw0KKyBMZW1hdGl6YWNpw7NuDQorIFJlbW9jacOzbiBkZSBzdG9wd29yZHMNCg0KRXN0byBsbyBsb2dyYW1vcyBhIHRyYXbDqXMgZGVsIHNpZ3VpZW50ZSBjw7NkaWdvIChndWFyZGFuZG8gbG9zIGRhdG9zLCBkYWRvIHF1ZSBsYSB0YXJlYSBlcyBjb21wdXRhY2lvbmFsbWVudGUgcGVzYWRhKS4NCg0KYGBge3J9DQojIEVsaW1pbmFjacOzbiBkZSBjYXJhY3RlcmVzIHBhcnRpY3VsYXJlcw0KbmVjcm9ub21pY29uX2RmID0gbmVjcm9ub21pY29uX2RmICU+JSANCiAgbXV0YXRlKEhpc3RvcmlhID0gc3RyX3NxdWlzaChzdHJfcmVwbGFjZV9hbGwoSGlzdG9yaWEsIuKAlCIsIiAiKSkpDQoNCiMgQW5vdGFjacOzbiBkZSBkYXRvcw0KbW9kZWxfc3BhbmlzaCA9IHVkcGlwZV9sb2FkX21vZGVsKCJzcGFuaXNoLWdzZC11ZC0yLjUtMTkxMjA2LnVkcGlwZSIpDQpuZWNyb25vbWljb25fdG9rZW5pemFkb19kZiA9IHVkcGlwZV9hbm5vdGF0ZShtb2RlbF9zcGFuaXNoLCBuZWNyb25vbWljb25fZGYkSGlzdG9yaWEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZG9jX2lkID0gbmVjcm9ub21pY29uX2RmJFRpdHVsbykNCm5lY3Jvbm9taWNvbl90b2tlbml6YWRvX2RmID0gbmVjcm9ub21pY29uX3Rva2VuaXphZG9fZGYgJT4lIA0KICBhc190aWJibGUoKSAlPiUgbXV0YXRlKGxlbW1hPXRvbG93ZXIobGVtbWEpKQ0KDQojIEZpbHRyYWRvIGRlIHN0b3B3b3Jkcw0KbmVjcm9ub21pY29uX3Rva2VuaXphZG9fZGYgPSBuZWNyb25vbWljb25fdG9rZW5pemFkb19kZiAlPiUgDQogIGZpbHRlcighbGVtbWEgJWluJSBzdG9wd29yZHMobGFuZ3VhZ2UgPSAiZXMiLCBzb3VyY2UgPSAibmx0ayIpLA0KICAgICAgICAgIXRva2VuICVpbiUgc3RvcHdvcmRzKGxhbmd1YWdlID0gImVzIiwgc291cmNlID0gIm5sdGsiKSkNCg0KIyBHdWFyZGFkbyBkZSBkYXRvcyBwcm9jZXNhZG9zDQpzYXZlUkRTKG5lY3Jvbm9taWNvbl90b2tlbml6YWRvX2RmLCAiQ2FzbzJfTGl0ZXJhdHVyYS9uZWNyb25vbWljb25fdG9rZW5pemFkb19kZi5SRFMiKQ0KYGBgDQoNCk9ic2VydmVtb3MgdW4gZWplbXBsbyBkZWwgcmVzdWx0YWRvIGRlIGVzdGUgcHJvY2VzYW1pZW50by4NCiANCmBgYHtyfQ0KbmVjcm9ub21pY29uX3Rva2VuaXphZG9fZGYgJT4lIA0KICBmaWx0ZXIoZG9jX2lkPT0iTnlhcmxhdGhvdGVwIikgJT4lIA0KICBoZWFkKDEwKQ0KYGBgDQoNCiMjICoqMi4yLiBDcmVhY2nDs24gZGUgbGEgbWF0cml6IGRvY3VtZW50by10w6lybWlubyoqDQoNClVuYSB2ZXogcXVlIGxvcyBkYXRvcyBoYW4gc2lkbyB0b2tlbml6YWRvcyB5IGFub3RhZG9zLCBwb2RlbW9zIGNyZWFyIGVsIHByaW5jaXBhbCBpbnN1bW8gZGVsIGFuw6FsaXNpcyBMREE6IGxhIG1hdHJpeiBkb2N1bWVudG8gdMOpcm1pbm8sIHBhcmEgZWxsbyB1dGlsaXphcmVtb3Mgw7puaWNhbWVudGUgc3VzdGFudGl2b3MsIHZlcmJvcywgYWRqZXRpdm9zIHkgbm9tYnJlcyBwcm9waW9zIGNvbiBlbCBvYmpldGl2byBkZSBlbGltaW5hciBkZWwgYW7DoWxpc2lzIHRvZGFzIGxhcyBwb3NpYmxlcyBwYWxhYnJhcyBxdWUgcHVlZGFuIGNhdXNhcm5vcyBydWlkby4gDQoNClBhcmEgbG9ncmFybG8gdXNhcmVtb3MgZWwgc2lndWllbnRlIGPDs2RpZ28uDQogDQpgYGB7cn0NCiMgQ29udGVvIGRlIGxlbW1hcyB5IHRmLWlkZiBwb3IgY2FkYSBkb2N1bWVudG8NCm5lY3Jvbm9taWNvbl90ZmlkZl9kZiA9IG5lY3Jvbm9taWNvbl90b2tlbml6YWRvX2RmICU+JSANCiAgZmlsdGVyKCFpcy5uYShsZW1tYSkpICU+JSANCiAgZmlsdGVyKHVwb3MgJWluJSBjKCJWRVJCIiwiQURKIiwiTk9VTiIsIlBST1BOIikpICU+JSANCiAgY291bnQoZG9jX2lkLCBsZW1tYSkgJT4lIA0KICBiaW5kX3RmX2lkZihsZW1tYSwgZG9jX2lkLCBuKQ0KICANCiMgQ3JlYWNpw7NuIGRlIG1hdHJpeiBkb2N1bWVudG8gdMOpcm1pbm8NCm5lY3Jvbm9taWNvbl9kdG0gPSBuZWNyb25vbWljb25fdGZpZGZfZGYgJT4lDQogIGNhc3RfZHRtKGRvY19pZCwgbGVtbWEsIG4pDQoNCm5lY3Jvbm9taWNvbl9kdG0NCmBgYA0KDQpPYnNlcnZlbW9zIGPDs21vIHNlIGhhIGNvbnN0cnVpZG8gZXN0YSBtYXRyaXouDQoNCmBgYHtyfQ0KYXMubWF0cml4KG5lY3Jvbm9taWNvbl9kdG0pWzE6NiwxOjZdDQpgYGANCg0KIyMgKioyLjMuIFNlbGVjY2nDs24gZGVsIG7Dum1lcm8gZGUgdMOzcGljb3MqKg0KDQpVbmEgdmV6IGNvbnN0cnVpZGEgbGEgbWF0cml6IGRvY3VtZW50byB0w6lybWlubywgdXRpbGl6YXJlbW9zIGxhcyBjdWF0cm8gbWVkaWRhcyBlc3BlY2lmaWNhZGFzIGFudGVyaW9ybWVudGUgcGFyYSBzZWxlY2Npb25hciBlbCBuw7ptZXJvIMOzcHRpbW8gZGUgdMOzcGljb3MuIEVzdG8gbG8gaGFyZW1vcyBncmFjaWFzIGEgbGEgbGlicmVyw61hICpsZGF0dW5pbmcqLiBDYWJlIG5vdGFyIGVuIGVzdGUgcHVudG8gcXVlIGRlIGxhcyBtZWRpZGFzIGVzcGVjaWZpY2FkYXMsIGRvcyBkZWJlbiBzZXIgbWluaW1pemFkYXMsIG1pZW50cmFzIHF1ZSBsYXMgb3RyYXMgZG9zIGRlYmVuIHNlciBtYXhpbWl6YWRhcywgYWNvcmRlIGFsIGdyw6FmaWNvIGVqZWN1dGFkby4NCg0KYGBge3J9DQojIFNlbGVjY2nDs24gZGVsIG7Dum1lcm8gw7NwdGltbyBkZSB0w7NwaWNvcw0KbmVjcm9ub21pY29uX250b3BpY3MgPSBGaW5kVG9waWNzTnVtYmVyKG5lY3Jvbm9taWNvbl9kdG0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0b3BpY3MgPSBzZXEoZnJvbSA9IDIsIHRvID0gMjAsIGJ5ID0gMSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRyaWNzID0gYygiR3JpZmZpdGhzMjAwNCIsICJDYW9KdWFuMjAwOSIsICJBcnVuMjAxMCIsICJEZXZlYXVkMjAxNCIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gIkdpYmJzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbnRyb2wgPSBsaXN0KHNlZWQgPSAxMjMpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWMuY29yZXMgPSA0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmVyYm9zZSA9IFRSVUUpDQpnYygpDQpGaW5kVG9waWNzTnVtYmVyX3Bsb3QobmVjcm9ub21pY29uX250b3BpY3MpDQpgYGANCg0KQ29tbyBzZSBwdWVkZSBub3RhciwgcGFyYSA5IHTDs3BpY29zIGRvcyBkZSAgbGFzIG1lZGlkYXMgYWxjYW56YW4gY2FzaSBzdSBuaXZlbCBtw61uaW1vLCBtaWVudHJhcyBxdWUgbGFzIG90cmFzIGRvcyBjYXNpIHNlIG1heGltaXphbi4NCg0KIyMgKioyLjQuIE1vZGVsYWRvIGRlIHTDs3BpY29zKioNCg0KVW5hIHZleiBxdWUgaGF5YW1vcyBzZWxlY2Npb25hZG8gZWwgbsO6bWVybyBhZGVjdWFkbyBkZSB0w7NwaWNvcywgZXN0aW1hbW9zIGVsIG1vZGVsbyBzZWxlY2Npb25hZG8uDQoNCmBgYHtyfQ0KIyBNb2RlbG8gTERBIGZpbmFsDQpuZWNyb25vbWljb25fbGRhID0gTERBKG5lY3Jvbm9taWNvbl9kdG0sIGsgPSA5LCBtZXRob2QgPSAiR2liYnMiLCBjb250cm9sID0gbGlzdChzZWVkPTEyMykpDQpgYGANCg0KVW5hIHZleiBxdWUgZWwgbW9kZWxvIGhheWEgc2lkbyBlc3RpbWFkbywgZXN0ZSBub3MgYXJyb2phcsOhIGRvcyBtYXRyaWNlczogQmV0YSB5IEdhbW1hLiBMYSBtYXRyaXogQmV0YSBkZW5vdGEgbGEgcHJvYmFiaWxpZGFkIGRlIGNhZGEgcGFsYWJyYSBkZSBwZXJ0ZW5lY2VyIGEgY2FkYSB0w7NwaWNvIHkgbGEgbWF0cml6IEJldGEgbXVlc3RyYSBsYSBwcm9iYWJpbGlkYWQgZGUgY2FkYSBkb2N1bWVudG8gZGUgcGVydGVuZWNlciBhIGNhZGEgdMOzcGljby4gUGFyYSBlbnRlbmRlcmxvIGRlIG1lam9yIG1hbmVyYSwgcmVhbGljZW1vcyB1biBncsOhZmljbyBkZSBjYWRhIG1hdHJpei4NCg0KYGBge3J9DQojIE9idGVuY2nDs24gZGUgbWF0cml6IGJldGENCm5lY3Jvbm9taWNvbl9iZXRhX2xkYSA9IHRpZHkobmVjcm9ub21pY29uX2xkYSwgbWF0cml4ID0gImJldGEiKQ0KDQojIEdyw6FmaWNvIGRlIHBhbGFicmFzIG3DoXMgcHJvYmFibGVzIHBvciB0w7NwaWNvDQpuZWNyb25vbWljb25fYmV0YV9sZGEgJT4lDQogIGdyb3VwX2J5KHRvcGljKSAlPiUNCiAgc2xpY2VfbWF4KG9yZGVyX2J5ID0gYmV0YSwgbiA9IDEwKSAlPiUNCiAgZ2dwbG90KGFlcyh4PXJlb3JkZXJfd2l0aGluKHRlcm0sIGJldGEsIHRvcGljKSwgYmV0YSwgZmlsbCA9IGFzLmZhY3Rvcih0b3BpYykpKSArDQogIHNjYWxlX3hfcmVvcmRlcmVkKCkrDQogIGdlb21fY29sKHNob3cubGVnZW5kID0gRkFMU0UpICsNCiAgZmFjZXRfd3JhcCh2YXJzKHRvcGljKSwgc2NhbGVzID0gImZyZWUiKSArDQogIGNvb3JkX2ZsaXAoKSsNCiAgbGFicyh4PSJQYWxhYnJhIikrDQogIHRoZW1lKHRleHQ9ZWxlbWVudF90ZXh0KHNpemU9MTEpKQ0KYGBgDQoNCmBgYHtyfQ0KIyBPYnRlbmNpw7NuIGRlIG1hdHJpeiBiZXRhDQpuZWNyb25vbWljb25fZ2FtbWFfbGRhID0gdGlkeShuZWNyb25vbWljb25fbGRhLCBtYXRyaXggPSAiZ2FtbWEiKQ0KDQojIEdyw6FmaWNvIGRlIGRvY3VtZW50b3MgbcOhcyBwcm9iYWJsZXMgcG9yIHTDs3BpY28NCm5lY3Jvbm9taWNvbl9nYW1tYV9sZGEgJT4lDQogIGdyb3VwX2J5KHRvcGljKSAlPiUNCiAgc2xpY2VfbWF4KG9yZGVyX2J5ID0gZ2FtbWEsIG4gPSAzKSAlPiUNCiAgZ2dwbG90KGFlcyh4PXJlb3JkZXJfd2l0aGluKGRvY3VtZW50LCBnYW1tYSwgdG9waWMpLCBnYW1tYSwgZmlsbCA9IGFzLmZhY3Rvcih0b3BpYykpKSArDQogIHNjYWxlX3hfcmVvcmRlcmVkKCkrDQogIGdlb21fY29sKHNob3cubGVnZW5kID0gRkFMU0UpICsNCiAgZmFjZXRfd3JhcCh2YXJzKHRvcGljKSwgc2NhbGVzID0gImZyZWUiLCBuY29sID0gMikgKw0KICBjb29yZF9mbGlwKCkrDQogIGxhYnMoeD0iRG9jdW1lbnRvIikrDQogIHRoZW1lKHRleHQ9ZWxlbWVudF90ZXh0KHNpemU9MTEpKQ0KYGBgDQoNCkNvbW8gcG9kZW1vcyBvYnNlcnZhciAoc2Fsdm8gdW4gbWVqb3IgY3JpdGVyaW8pLCBsb3MgY3VlbnRvcyBzZSBlbmN1ZW50cmFuIGFncnVwYWRvcyBkZW50cm8gZGUgdGVtw6F0aWNhcyBzaW1pbGFyZXMuIA0KDQpGaW5hbG1lbnRlLCBndWFyZGFyZW1vcyBsb3MgcmVzdWx0YWRvcyBkZSBsYXMgbWF0cmljZXMgQmV0YSB5IEdhbW1hIGNvbiBsYSBmaW5hbGlkYWQgZGUgdm9sdmVybGFzIGEgdmlzdWFsaXphciAoY29uIGhlcnJhbWllbnRhcyBjb21vIFdvcmQgRW1iZWRkaW5ncyB5IHQtU05FKSBlbiBlbCBwcsOzeGltbyBjYXDDrXR1bG8NCg0KYGBge3J9DQpzYXZlUkRTKG5lY3Jvbm9taWNvbl9nYW1tYV9sZGEsICJDYXNvMl9MaXRlcmF0dXJhL25lY3Jvbm9taWNvbl9nYW1tYV9sZGEuUkRTIikNCnNhdmVSRFMobmVjcm9ub21pY29uX2JldGFfbGRhLCAiQ2FzbzJfTGl0ZXJhdHVyYS9uZWNyb25vbWljb25fYmV0YV9sZGEuUkRTIikNCmBgYA0KDQoNCiMgKiozLiBCaWJsaW9ncmFmw61hKioNCg==