Tokenización
En esta sección veremos tareas sencillas de reconocimiento de entidades. Para ello usaremos tres librerías nuevas, una de las cuales (openNLP
) es una interfaz a una librería en Java. Usaremos además modelos pre-entrenados que no están disponibles en CRAN (debido debido a la política del repositorio):
library(NLP)
library(openNLP)
## install.packages("openNLPmodels.en",
## repos="http://datacube.wu.ac.at/",
## type="source")
Vamos a hacer una tarea sencilla de separar un texto en oraciones y en palabras. En concreto empezaremos con uno de los textos que estudiaremos en la siguiente sección
text <- readLines("./dta/senate-releases/Vitter/14Jun2007Vitter110.txt")
y lo transformaremos en un formato más adecuado para trabajar con NLP
:
text <- as.String(text)
Las funciones en openNLP
funcionan siempre del mismo modo. En primer lugar, inicializamos un anotador y después aplicamos ese anotador a un texto. Por ejemplo, podemos empezar por anotar palabras y oraciones utilizando las funciones:
word_ann <- Maxent_Word_Token_Annotator()
sent_ann <- Maxent_Sent_Token_Annotator()
y a continuación anotamos el texto aplicando estos dos objetos al texto que queremos analizar:
annotated_text <- annotate(text, list(sent_ann, word_ann))
Lo que obtenemos es un objeto de clase
class(annotated_text)
que en realidad se parece mucho a un data.frame
en el que tenemos la lista de oraciones y palabras identificadas por la posición en la que empiezan y en la que acaban. Podemos, por ejemplo, recuperar el total de oraciones:
head(subset(annotated_text, type=="sentence"))
y podemos comprobar el contenido de esta oración
subset(annotated_text, type=='sentence')[[4]]$features
text[subset(annotated_text, type=='sentence')[[4]]]
También podríamos recuperar las palabras que constituyen el texto de modo similar
head(subset(annotated_text, type=="word"))
Quizás sea más fácil explorar los resultados utilizando la función AnnotatedPlainTextDocument
:
adoc <- AnnotatedPlainTextDocument(text, annotated_text)
head(sents(adoc))
head(words(adoc))
Anotador de entidades y POS
Podemos utilizar el mismo proceso para anotar entidades dentro de un texto. Por ejemplo, podemos anotar personas, localizaciones, organizaciones y fechas dentro del documento que ya hemos anotado anteriormente.
person_ann <- Maxent_Entity_Annotator(kind="person")
loc_ann <- Maxent_Entity_Annotator(kind="location")
org_ann <- Maxent_Entity_Annotator(kind="organization")
date_ann <- Maxent_Entity_Annotator(kind="date")
y ahora aplicamos estos nuevos anotadores al texto que ya tokenizamos previamente:
annotated_text <- annotate(text,
list(person_ann, loc_ann, org_ann, date_ann),
annotated_text)
podemos ver que, a la lista de tokens, se han añadido las entidades que el clasificador ha logrado identificar.
subset(annotated_text, type=="entity")$features
y ahora podemos ver las entidades que hemos recuperado
sel <- subset(annotated_text, type=="entity")
cbind(unlist(sel$features), text[sel])
La anotación de POS sigue la misma lógica. Ahora generaremos un nuevo objeto en el que inicializamos el anotador de POS y aplicamos este nuevo anotador a nuestro texto:
pos_ann <- Maxent_POS_Tag_Annotator()
pos_annotated_text <- annotate(text, pos_ann, annotated_text)
Ahora lo que tenemos en la columna de features
es una indiación de la POS de cada una de las palabras
head(subset(pos_annotated_text, type=="word"))
En este caso vemos que, por ejemplo,
text[subset(pos_annotated_text, type=="word")[55]]
subset(pos_annotated_text, type=="word")[55]
está etiquetado como NNS
que se corresponde como sustantivo plural y
text[subset(pos_annotated_text, type=="word")[100]]
subset(pos_annotated_text, type=="word")[100]
está etiquetado como JJ
que es un adjetivo. La lista completa de abreviaturas es:
Símbolo
|
Significado
|
CC
|
Coordinating conjunction
|
CD
|
Cardinal number
|
DT
|
Determiner
|
EX
|
Existential there
|
FW
|
Foreign word
|
IN
|
Preposition or subordinating conjunction
|
JJ
|
Adjective
|
JJR
|
Adjective, comparative
|
JJS
|
Adjective, superlative
|
LS
|
List item marker
|
MD
|
Modal
|
NN
|
Noun, singular or mass
|
NNS
|
Noun, plural
|
NNP
|
Proper noun, singular
|
NNPS
|
Proper noun, plural
|
PDT
|
Predeterminer
|
POS
|
Possessive ending
|
PRP
|
Personal pronoun
|
PRP$
|
Possessive pronoun
|
RB
|
Adverb
|
RBR
|
Adverb, comparative
|
RBS
|
Adverb, superlative
|
RP
|
Particle
|
SYM
|
Symbol
|
TO
|
to
|
UH
|
Interjection
|
VB
|
Verb, base form
|
VBD
|
Verb, past tense
|
VBG
|
Verb, gerund or present participle
|
VBN
|
Verb, past participle
|
VBP
|
Verb, non3rd person singular present
|
VBZ
|
Verb, 3rd person singular present
|
WDT
|
Whdeterminer
|
WP
|
Whpronoun
|
WP$
|
Possessive whpronoun
|
WRB
|
Whadverb
|
Hemos visto que las etiquetaciones son en realidad el resultado de un modelo estadístico. Podemos pedirle al anotador que nos devuelva las probabilidades asociadas a la etiqueta escogida con el fin de tener información acerca de la incertidumbre.
pos_ann <- Maxent_POS_Tag_Annotator(probs=TRUE)
pos_annotated_text <- annotate(text, pos_ann, annotated_text)
head(subset(pos_annotated_text, type=="word"))
LS0tIAp0aXRsZTogIlByb2Nlc2FtaWVudG8gZGUgbGVuZ3VhamUgbmF0dXJhbCIKZGF0ZTogImByIGZvcm1hdChTeXMudGltZSgpLCAnJUIgJWQsICVZJylgIgotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFLCBjYWNoZT1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGV2YWwgPSBGQUxTRSkgCmBgYAoKIyMgVG9rZW5pemFjacOzbgpFbiBlc3RhIHNlY2Npw7NuIHZlcmVtb3MgdGFyZWFzIHNlbmNpbGxhcyBkZSByZWNvbm9jaW1pZW50byBkZSBlbnRpZGFkZXMuIFBhcmEKZWxsbyB1c2FyZW1vcyB0cmVzIGxpYnJlcsOtYXMgbnVldmFzLCB1bmEgZGUgbGFzIGN1YWxlcyAoYG9wZW5OTFBgKSBlcyB1bmEKaW50ZXJmYXogYSB1bmEgbGlicmVyw61hIGVuIEphdmEuIFVzYXJlbW9zIGFkZW3DoXMgbW9kZWxvcyBwcmUtZW50cmVuYWRvcyBxdWUgbm8KZXN0w6FuIGRpc3BvbmlibGVzIGVuIENSQU4gKGRlYmlkbyBkZWJpZG8gYSBsYSBwb2zDrXRpY2EgZGVsIHJlcG9zaXRvcmlvKTogCgpgYGB7cn0KbGlicmFyeShOTFApCmxpYnJhcnkob3Blbk5MUCkKCiMjIGluc3RhbGwucGFja2FnZXMoIm9wZW5OTFBtb2RlbHMuZW4iLAojIyAgICAgICAgICAgICAgICAgIHJlcG9zPSJodHRwOi8vZGF0YWN1YmUud3UuYWMuYXQvIiwKIyMgICAgICAgICAgICAgICAgICB0eXBlPSJzb3VyY2UiKQpgYGAKClZhbW9zIGEgaGFjZXIgdW5hIHRhcmVhIHNlbmNpbGxhIGRlIHNlcGFyYXIgdW4gdGV4dG8gZW4gb3JhY2lvbmVzIHkgZW4gcGFsYWJyYXMuCkVuIGNvbmNyZXRvIGVtcGV6YXJlbW9zIGNvbiB1bm8gZGUgbG9zIHRleHRvcyBxdWUgZXN0dWRpYXJlbW9zIGVuIGxhIHNpZ3VpZW50ZQpzZWNjacOzbgoKYGBge3J9CnRleHQgPC0gcmVhZExpbmVzKCIuL2R0YS9zZW5hdGUtcmVsZWFzZXMvVml0dGVyLzE0SnVuMjAwN1ZpdHRlcjExMC50eHQiKQpgYGAKCnkgbG8gdHJhbnNmb3JtYXJlbW9zIGVuIHVuIGZvcm1hdG8gbcOhcyBhZGVjdWFkbyBwYXJhIHRyYWJhamFyIGNvbiBgTkxQYDoKCmBgYHtyfQp0ZXh0IDwtIGFzLlN0cmluZyh0ZXh0KQpgYGAKCkxhcyBmdW5jaW9uZXMgZW4gYG9wZW5OTFBgIGZ1bmNpb25hbiBzaWVtcHJlIGRlbCBtaXNtbyBtb2RvLiBFbiBwcmltZXIgbHVnYXIsCmluaWNpYWxpemFtb3MgdW4gX2Fub3RhZG9yXyB5IGRlc3B1w6lzIGFwbGljYW1vcyBlc2UgYW5vdGFkb3IgYSB1biB0ZXh0by4gUG9yCmVqZW1wbG8sIHBvZGVtb3MgZW1wZXphciBwb3IgYW5vdGFyIHBhbGFicmFzIHkgb3JhY2lvbmVzIHV0aWxpemFuZG8gbGFzCmZ1bmNpb25lczogCgpgYGB7cn0Kd29yZF9hbm4gPC0gTWF4ZW50X1dvcmRfVG9rZW5fQW5ub3RhdG9yKCkKc2VudF9hbm4gPC0gTWF4ZW50X1NlbnRfVG9rZW5fQW5ub3RhdG9yKCkKYGBgCgp5IGEgY29udGludWFjacOzbiBhbm90YW1vcyBlbCB0ZXh0byBhcGxpY2FuZG8gZXN0b3MgZG9zIG9iamV0b3MgYWwgdGV4dG8gcXVlCnF1ZXJlbW9zIGFuYWxpemFyOiAKCmBgYHtyfQphbm5vdGF0ZWRfdGV4dCA8LSBhbm5vdGF0ZSh0ZXh0LCBsaXN0KHNlbnRfYW5uLCB3b3JkX2FubikpCmBgYAoKTG8gcXVlIG9idGVuZW1vcyBlcyB1biBvYmpldG8gZGUgY2xhc2UKYGBge3J9CmNsYXNzKGFubm90YXRlZF90ZXh0KQpgYGAKCnF1ZSBlbiByZWFsaWRhZCBzZSBwYXJlY2UgbXVjaG8gYSB1biBgZGF0YS5mcmFtZWAgZW4gZWwgcXVlIHRlbmVtb3MgbGEgbGlzdGEgZGUKb3JhY2lvbmVzIHkgcGFsYWJyYXMgaWRlbnRpZmljYWRhcyBwb3IgbGEgcG9zaWNpw7NuIGVuIGxhIHF1ZSBlbXBpZXphbiB5IGVuIGxhCnF1ZSBhY2FiYW4uIFBvZGVtb3MsIHBvciBlamVtcGxvLCByZWN1cGVyYXIgZWwgdG90YWwgZGUgb3JhY2lvbmVzOiAKCmBgYHtyfQpoZWFkKHN1YnNldChhbm5vdGF0ZWRfdGV4dCwgdHlwZT09InNlbnRlbmNlIikpCmBgYAoKeSBwb2RlbW9zIGNvbXByb2JhciBlbCBjb250ZW5pZG8gZGUgZXN0YSBvcmFjacOzbgoKYGBge3J9CnN1YnNldChhbm5vdGF0ZWRfdGV4dCwgdHlwZT09J3NlbnRlbmNlJylbWzRdXSRmZWF0dXJlcwp0ZXh0W3N1YnNldChhbm5vdGF0ZWRfdGV4dCwgdHlwZT09J3NlbnRlbmNlJylbWzRdXV0KYGBgCgpUYW1iacOpbiBwb2Ryw61hbW9zIHJlY3VwZXJhciBsYXMgcGFsYWJyYXMgcXVlIGNvbnN0aXR1eWVuIGVsIHRleHRvIGRlIG1vZG8Kc2ltaWxhcgpgYGB7cn0KaGVhZChzdWJzZXQoYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIikpCmBgYAoKUXVpesOhcyBzZWEgbcOhcyBmw6FjaWwgZXhwbG9yYXIgbG9zIHJlc3VsdGFkb3MgdXRpbGl6YW5kbyBsYSBmdW5jacOzbiBgQW5ub3RhdGVkUGxhaW5UZXh0RG9jdW1lbnRgOgoKYGBge3J9CmFkb2MgPC0gQW5ub3RhdGVkUGxhaW5UZXh0RG9jdW1lbnQodGV4dCwgYW5ub3RhdGVkX3RleHQpCmhlYWQoc2VudHMoYWRvYykpCmhlYWQod29yZHMoYWRvYykpCmBgYAoKIyMgQW5vdGFkb3IgZGUgZW50aWRhZGVzIHkgUE9TCgpQb2RlbW9zIHV0aWxpemFyIGVsIG1pc21vIHByb2Nlc28gcGFyYSBhbm90YXIgZW50aWRhZGVzIGRlbnRybyBkZSB1biB0ZXh0by4gUG9yCmVqZW1wbG8sIHBvZGVtb3MgYW5vdGFyIHBlcnNvbmFzLCBsb2NhbGl6YWNpb25lcywgb3JnYW5pemFjaW9uZXMgeSBmZWNoYXMgZGVudHJvCmRlbCBkb2N1bWVudG8gcXVlIHlhIGhlbW9zIGFub3RhZG8gYW50ZXJpb3JtZW50ZS4gCgpgYGB7cn0KcGVyc29uX2FubiA8LSBNYXhlbnRfRW50aXR5X0Fubm90YXRvcihraW5kPSJwZXJzb24iKQpsb2NfYW5uIDwtIE1heGVudF9FbnRpdHlfQW5ub3RhdG9yKGtpbmQ9ImxvY2F0aW9uIikKb3JnX2FubiA8LSBNYXhlbnRfRW50aXR5X0Fubm90YXRvcihraW5kPSJvcmdhbml6YXRpb24iKQpkYXRlX2FubiA8LSBNYXhlbnRfRW50aXR5X0Fubm90YXRvcihraW5kPSJkYXRlIikKYGBgCgp5IGFob3JhIGFwbGljYW1vcyBlc3RvcyBudWV2b3MgYW5vdGFkb3JlcyBhbCB0ZXh0byBxdWUgeWEgdG9rZW5pemFtb3MKcHJldmlhbWVudGU6CgpgYGB7cn0KYW5ub3RhdGVkX3RleHQgPC0gYW5ub3RhdGUodGV4dCwKICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0KHBlcnNvbl9hbm4sIGxvY19hbm4sIG9yZ19hbm4sIGRhdGVfYW5uKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBhbm5vdGF0ZWRfdGV4dCkKYGBgCnBvZGVtb3MgdmVyIHF1ZSwgYSBsYSBsaXN0YSBkZSB0b2tlbnMsIHNlIGhhbiBhw7FhZGlkbyBsYXMgZW50aWRhZGVzIHF1ZSBlbApjbGFzaWZpY2Fkb3IgaGEgbG9ncmFkbyBpZGVudGlmaWNhci4gCgpgYGB7cn0Kc3Vic2V0KGFubm90YXRlZF90ZXh0LCB0eXBlPT0iZW50aXR5IikkZmVhdHVyZXMKYGBgCgp5IGFob3JhIHBvZGVtb3MgdmVyIGxhcyBlbnRpZGFkZXMgcXVlIGhlbW9zIHJlY3VwZXJhZG8gCgpgYGB7cn0Kc2VsIDwtIHN1YnNldChhbm5vdGF0ZWRfdGV4dCwgdHlwZT09ImVudGl0eSIpCmNiaW5kKHVubGlzdChzZWwkZmVhdHVyZXMpLCB0ZXh0W3NlbF0pCmBgYAoKTGEgYW5vdGFjacOzbiBkZSBQT1Mgc2lndWUgbGEgbWlzbWEgbMOzZ2ljYS4gQWhvcmEgZ2VuZXJhcmVtb3MgdW4gbnVldm8gb2JqZXRvIGVuCmVsIHF1ZSBpbmljaWFsaXphbW9zIGVsIGFub3RhZG9yIGRlIFBPUyB5IGFwbGljYW1vcyBlc3RlIG51ZXZvIGFub3RhZG9yIGEKbnVlc3RybyB0ZXh0bzogCgpgYGB7cn0KcG9zX2FubiA8LSBNYXhlbnRfUE9TX1RhZ19Bbm5vdGF0b3IoKQpwb3NfYW5ub3RhdGVkX3RleHQgPC0gYW5ub3RhdGUodGV4dCwgcG9zX2FubiwgYW5ub3RhdGVkX3RleHQpCmBgYAoKQWhvcmEgbG8gcXVlIHRlbmVtb3MgZW4gbGEgY29sdW1uYSBkZSBgZmVhdHVyZXNgIGVzIHVuYSBpbmRpYWNpw7NuIGRlIGxhIFBPUyBkZQpjYWRhIHVuYSBkZSBsYXMgcGFsYWJyYXMKCmBgYHtyfQpoZWFkKHN1YnNldChwb3NfYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIikpCmBgYAoKRW4gZXN0ZSBjYXNvIHZlbW9zIHF1ZSwgcG9yIGVqZW1wbG8sIAoKYGBge3J9CnRleHRbc3Vic2V0KHBvc19hbm5vdGF0ZWRfdGV4dCwgdHlwZT09IndvcmQiKVs1NV1dCnN1YnNldChwb3NfYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIilbNTVdCmBgYAoKZXN0w6EgZXRpcXVldGFkbyBjb21vIGBOTlNgIHF1ZSBzZSBjb3JyZXNwb25kZSBjb21vIHN1c3RhbnRpdm8gcGx1cmFsIHkgCgpgYGB7cn0KdGV4dFtzdWJzZXQocG9zX2Fubm90YXRlZF90ZXh0LCB0eXBlPT0id29yZCIpWzEwMF1dCnN1YnNldChwb3NfYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIilbMTAwXQpgYGAKZXN0w6EgZXRpcXVldGFkbyBjb21vIGBKSmAgcXVlIGVzIHVuIGFkamV0aXZvLiBMYSBsaXN0YSBjb21wbGV0YSBkZQphYnJldmlhdHVyYXMgZXM6Cgo8dGFibGU+Cjx0cj4KPHRoPlPDrW1ib2xvPC90aD48dGg+U2lnbmlmaWNhZG88L3RoPgo8L3RyPgo8dHI+PHRkPkNDPC90ZD48dGQ+Q29vcmRpbmF0aW5nIGNvbmp1bmN0aW9uPC90ZD48L3RyPgo8dHI+PHRkPkNEPC90ZD48dGQ+IENhcmRpbmFsIG51bWJlciA8L3RkPjwvdHI+Cjx0cj48dGQ+RFQ8L3RkPjx0ZD4gRGV0ZXJtaW5lcjwvdGQ+PC90cj4KPHRyPjx0ZD5FWDwvdGQ+PHRkPiBFeGlzdGVudGlhbCB0aGVyZTwvdGQ+PC90cj4KPHRyPjx0ZD5GVzwvdGQ+PHRkPiBGb3JlaWduIHdvcmQ8L3RkPjwvdHI+Cjx0cj48dGQ+SU48L3RkPjx0ZD4gUHJlcG9zaXRpb24gb3Igc3Vib3JkaW5hdGluZyBjb25qdW5jdGlvbjwvdGQ+PC90cj4KPHRyPjx0ZD5KSjwvdGQ+PHRkPiBBZGplY3RpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+SkpSPC90ZD48dGQ+IEFkamVjdGl2ZSwgY29tcGFyYXRpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+SkpTPC90ZD48dGQ+IEFkamVjdGl2ZSwgc3VwZXJsYXRpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+TFM8L3RkPjx0ZD4gTGlzdCBpdGVtIG1hcmtlcjwvdGQ+PC90cj4KPHRyPjx0ZD5NRDwvdGQ+PHRkPiBNb2RhbDwvdGQ+PC90cj4KPHRyPjx0ZD5OTjwvdGQ+PHRkPiBOb3VuLCBzaW5ndWxhciBvciBtYXNzPC90ZD48L3RyPgo8dHI+PHRkPk5OUzwvdGQ+PHRkPiBOb3VuLCBwbHVyYWw8L3RkPjwvdHI+Cjx0cj48dGQ+Tk5QPC90ZD48dGQ+IFByb3BlciBub3VuLCBzaW5ndWxhcjwvdGQ+PC90cj4KPHRyPjx0ZD5OTlBTPC90ZD48dGQ+IFByb3BlciBub3VuLCBwbHVyYWw8L3RkPjwvdHI+Cjx0cj48dGQ+UERUPC90ZD48dGQ+IFByZWRldGVybWluZXI8L3RkPjwvdHI+Cjx0cj48dGQ+UE9TPC90ZD48dGQ+IFBvc3Nlc3NpdmUgZW5kaW5nPC90ZD48L3RyPgo8dHI+PHRkPlBSUDwvdGQ+PHRkPiBQZXJzb25hbCBwcm9ub3VuPC90ZD48L3RyPgo8dHI+PHRkPlBSUFwkPC90ZD48dGQ+IFBvc3Nlc3NpdmUgcHJvbm91bjwvdGQ+PC90cj4KPHRyPjx0ZD5SQjwvdGQ+PHRkPiBBZHZlcmI8L3RkPjwvdHI+Cjx0cj48dGQ+UkJSPC90ZD48dGQ+IEFkdmVyYiwgY29tcGFyYXRpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+UkJTPC90ZD48dGQ+IEFkdmVyYiwgc3VwZXJsYXRpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+UlA8L3RkPjx0ZD4gUGFydGljbGU8L3RkPjwvdHI+Cjx0cj48dGQ+U1lNPC90ZD48dGQ+IFN5bWJvbDwvdGQ+PC90cj4KPHRyPjx0ZD5UTzwvdGQ+PHRkPiB0bzwvdGQ+PC90cj4KPHRyPjx0ZD5VSDwvdGQ+PHRkPiBJbnRlcmplY3Rpb248L3RkPjwvdHI+Cjx0cj48dGQ+VkI8L3RkPjx0ZD4gVmVyYiwgYmFzZSBmb3JtPC90ZD48L3RyPgo8dHI+PHRkPlZCRDwvdGQ+PHRkPiBWZXJiLCBwYXN0IHRlbnNlPC90ZD48L3RyPgo8dHI+PHRkPlZCRzwvdGQ+PHRkPiBWZXJiLCBnZXJ1bmQgb3IgcHJlc2VudCBwYXJ0aWNpcGxlPC90ZD48L3RyPgo8dHI+PHRkPlZCTjwvdGQ+PHRkPiBWZXJiLCBwYXN0IHBhcnRpY2lwbGU8L3RkPjwvdHI+Cjx0cj48dGQ+VkJQPC90ZD48dGQ+IFZlcmIsIG5vbsKtM3JkIHBlcnNvbiBzaW5ndWxhciBwcmVzZW50PC90ZD48L3RyPgo8dHI+PHRkPlZCWjwvdGQ+PHRkPiBWZXJiLCAzcmQgcGVyc29uIHNpbmd1bGFyIHByZXNlbnQ8L3RkPjwvdHI+Cjx0cj48dGQ+V0RUPC90ZD48dGQ+IFdowq1kZXRlcm1pbmVyPC90ZD48L3RyPgo8dHI+PHRkPldQPC90ZD48dGQ+IFdowq1wcm9ub3VuPC90ZD48L3RyPgo8dHI+PHRkPldQJDwvdGQ+PHRkPiBQb3NzZXNzaXZlIHdowq1wcm9ub3VuPC90ZD48L3RyPgo8dHI+PHRkPldSQjwvdGQ+PHRkPiBXaMKtYWR2ZXJiPC90ZD48L3RyPgo8L3RhYmxlPgoKSGVtb3MgdmlzdG8gcXVlIGxhcyBldGlxdWV0YWNpb25lcyBzb24gZW4gcmVhbGlkYWQgZWwgcmVzdWx0YWRvIGRlIHVuIG1vZGVsbwplc3RhZMOtc3RpY28uIFBvZGVtb3MgcGVkaXJsZSBhbCBhbm90YWRvciBxdWUgbm9zIGRldnVlbHZhIGxhcyBwcm9iYWJpbGlkYWRlcwphc29jaWFkYXMgYSBsYSBldGlxdWV0YSBlc2NvZ2lkYSBjb24gZWwgZmluIGRlIHRlbmVyIGluZm9ybWFjacOzbiBhY2VyY2EgZGUgbGEKaW5jZXJ0aWR1bWJyZS4gCgpgYGB7cn0KcG9zX2FubiA8LSBNYXhlbnRfUE9TX1RhZ19Bbm5vdGF0b3IocHJvYnM9VFJVRSkKcG9zX2Fubm90YXRlZF90ZXh0IDwtIGFubm90YXRlKHRleHQsIHBvc19hbm4sIGFubm90YXRlZF90ZXh0KQpoZWFkKHN1YnNldChwb3NfYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIikpCmBgYAoK