#!/usr/bin/env python # coding: utf-8 # # Resumen NLTK: Etiquetado morfológico (*part-of-speech tagging*) # # Este resumen se corresponde con el capítulo 5 del NLTK Book [Categorizing and Tagging Words](http://www.nltk.org/book/ch05.html). La lectura del capítulo es muy recomendable. # # # ## Etiquetado morfológico con NLTK # # NLTK propociona varias herramientas para poder crear fácilmente etiquetadores morfológicos (*part-of-speech taggers*). Veamos algunos ejemplos. # # Para empezar, necesitamos importar el módulo `nltk` que nos da acceso a todas las funcionalidades: # In[1]: import nltk # Como primer ejemplo, podemos utilizar la función `nltk.pos_tag` para etiquetar morfológicamente una oración en inglés, siempre que la especifiquemos como una lista de palabras o tokens. # In[2]: oracion1 = "This is the lost dog I found at the park".split() oracion2 = "The progress of the humankind as I progress".split() print(nltk.pos_tag(oracion1)) print(nltk.pos_tag(oracion2)) # El etiquetador funciona bastante bien aunque comete errores, obviamente. Si probamos con la famosa frase de Chomksy detectamos palabras mal etiquetadas. # In[3]: oracion3 = "Green colorless ideas sleep furiously".split() print(nltk.pos_tag(oracion3)) print(nltk.pos_tag(["My", "name", "is", "Prince"])) print(nltk.pos_tag('He was born during the summer of 1988'.split())) print(nltk.pos_tag("She's Tony's sister".split())) print(nltk.pos_tag('''My name is Xrtwewvdk'''.split())) # ¿Cómo funciona este etiquetador? `nltk.pos_tag` es un etiquetador morfológico basado en aprendizaje automático. A partir de miles de ejemplos de oraciones etiquetadas manualmente, el sistema *ha aprendido*, calculando frecuencias y generalizando cuál es la categoría gramatical más probable para cada token. # # Como sabes, desde NLTK podemos acceder a corpus que ya están etiquetados. Vamos a utilizar alguno de los que ya conoces, el corpus de Brown, para entrenar nuestros propios etiquetadores. # # Para ello importamos el corpus de Brown y almacenamos en un par de variables las noticias de este corpus en su versión etiquetada morfológicamente y sin etiquetar. # In[4]: from nltk.corpus import brown brown_sents = brown.sents(categories="news") brown_tagged_sents = brown.tagged_sents(categories="news") # Para comparar ambas versiones, podemos imprimir la primera oración de este corpus en su versión sin etiquetas (fíjate que se trata de una lista de tokens, sin más) y en su versión etiquetada (se trata de una lista de tuplas donde el primer elemento es el token y el segundo es la etiqueta morfológica). # In[5]: # imprimimos la primera oración de las noticias de Brown print(brown_sents[0]) # sin anotar print(brown_tagged_sents[0]) # etiquetada morfológicamente # ## Etiquetado morfológico automático # # ### Etiquetador por defecto # # NLTK nos da acceso a tipos de etiquetadores morfológicos. Veamos cómo utilizar algunos de ellos. # # A la hora de enfrentarnos al etiquetado morfológico de un texto, podemos adoptar una estrategia sencilla consistente en etiquetar por defecto todas las palabras con la misma categoría gramatical. Con NLTK podemos utilizar un `DefaultTagger` que etiquete todos los tokens como sustantivo. Las categoría **sustantivo singular** (`NN` en el esquema de etiquetas de Treebank) suele ser la más frecuente. Veamos qué tal funciona. # In[6]: defaultTagger = nltk.DefaultTagger('NN') print(oracion1) print(defaultTagger.tag(oracion1)) print(defaultTagger.tag(oracion2)) # Obviamente no funciona bien, pero ojo, en el ejemplo anterior con `oracion1` hemos etiquetado correctamente 2 de 10 tokens. Si lo evaluamos con un corpus más grande, como el conjunto de oraciones de Brown que ya tenemos, obtenemos una precisión superior al 13%: # In[7]: defaultTagger.evaluate(brown_tagged_sents) # el método `.evaluate` que podemos ejecutar con cualquier etiquetador si especificamos como argumento una colección de referencia que ya esté etiquetada, nos devuelve un número: la **precisión**. Esta precisión se calcula como el porcentaje de tokens correctamente etiquetados por el *tagger*, teniendo en cuenta el corpus especificado como referencia. # Obviamente, los resultados son malos. Probemos con otras opciones más sofisticadas. # # ### Etiquetador basado en Expresiones Regulares # # Las expresiones regulares consisten en un lenguaje formal que nos permite especificar cadenas de texto. Ya las hemos utilizado en ocasiones anteriores. Pues bien, ahora podemos probar a definir distintas categorías morfológicas a partir de patrones, al menos para fenómenos morfológicos regulares. # # A continuación definimos la variable `patrones` como una lista de tuplas, cuyo primer elemento se corresponde con la expresion regular que queremos capturar y el segundo elemento como la categoría gramatical. Y a partir de estas expresiones regulares creamos un `RegexpTagger`. # In[8]: patrones = [ (r'[Aa]m$', 'VBP'), # irregular forms of 'to be' (r'[Aa]re$', 'VBP'), # (r'[Ii]s$', 'VBZ'), # (r'[Ww]as$', 'VBD'), # (r'[Ww]ere$', 'VBD'), # (r'[Bb]een$', 'VBN'), # (r'[Hh]ave$', 'VBP'), # irregular forms of 'to be' (r'[Hh]as$', 'VBZ'), # (r'[Hh]ad$', 'VBD'), # (r'^I$', 'PRP'), # personal pronouns (r'[Yy]ou$', 'PRP'), # (r'[Hh]e$', 'PRP'), # (r'[Ss]he$', 'PRP'), # (r'[Ii]t$', 'PRP'), # (r'[Tt]hey$', 'PRP'), # (r'[Aa]n?$', 'AT'), # (r'[Tt]he$', 'AT'), # (r'[Ww]h.+$', 'WP'), # wh- pronoun (r'.*ing$', 'VBG'), # gerunds (r'.*ed$', 'VBD'), # simple past (r'.*es$', 'VBZ'), # 3rd singular present (r'[Cc]an(not|n\'t)?$', 'MD'), # modals (r'[Mm]ight$', 'MD'), # (r'[Mm]ay$', 'MD'), # (r'.+ould$', 'MD'), # modals: could, should, would (r'.*ly$', 'RB'), # adverbs (r'.*\'s$', 'NN$'), # possessive nouns (r'.*s$', 'NNS'), # plural nouns (r'-?[0-9]+(.[0-9]+)?$', 'CD'), # cardinal numbers (r'^to$', 'TO'), # to (r'^in$', 'IN'), # in prep (r'^[A-Z]+([a-z])*$', 'NNP'), # proper nouns (r'.*', 'NN') # nouns (default) ] regexTagger = nltk.RegexpTagger(patrones) # In[9]: print(regexTagger.tag(u"I was taking a sunbath in Alpedrete".split())) print(regexTagger.tag(u"She would have found 100 dollars in the bag".split())) print(regexTagger.tag(u"DSFdfdsfsd 1852 to dgdfgould fXXXdg in XXXfdg".split())) # Cuando probamos a evaluarlo con un corpus de oraciones más grande, vemos que nuestra precisión sube por encima del 32%. # In[10]: regexTagger.evaluate(brown_tagged_sents) # ### Etiquetado basado en ngramas # # Antes hemos dicho que la función `nltk.pos_tag` tenía un etiquetador morfológico que funcionaba con información estadística. A continuación vamos a reproducir, a pequeña escala, el proceso de entrenamiento de un etiquetador morfológico basado en aprendizaje automático. # # En general, los sistemas de aprendizaje automático funcionan del siguiente modo: # # * Se etiqueta manualmente una colección de datos. Cuanto más grande y más representativa sea la colección, mejores datos podremos extraer. # * El sistema de aprendizaje procesa la colección de entrenamiento y "aprende" a partir de los ejemplos observados. En el caso del etiquetado morfológico, el sistema calcula frecuencias y generaliza (de distintas maneras) cuáles son las categorías gramaticales más probables para cada palabra. # * Una vez que el sistema ha sido entrenado, se evalúa su rendimiento sobre datos no observados. # # Nosotros tenemos un pequeño corpus de ejemplos etiquetados: las oraciones del corpus de Brown de la categoría "noticias". Lo primero que necesitamos hacer es separar nuestros corpus de entrenamiento y test. En este caso, vamos a reservar el primer 90% de las oraciones para el entrenamiento (serán los ejemplos observados a partir de los cuales nuestro etiquetador aprenderá) y vamos a dejar el 10% restante para comprobar qué tal funciona. # In[11]: print(len(brown_tagged_sents)) print((len(brown_tagged_sents) * 90) / 100) # In[12]: size = int(len(brown_tagged_sents) * 0.9) corpusEntrenamiento = brown_tagged_sents[:size] corpusTest = brown_tagged_sents[size:] print(corpusEntrenamiento[0]) print(corpusTest[0]) # A continuación vamos a crear un etiquetador basado en unigramas (secuencias de una palabra o palabras sueltas) a través de la clase `nltk.UnigramTagger`, proporcionando nuestro `corpusEntrenamiento` para que aprenda. Una vez entrenado, vamos a evaluar su rendimiento sobre `corpusTest`. # In[13]: unigramTagger = nltk.UnigramTagger(corpusEntrenamiento) print(unigramTagger.evaluate(corpusTest)) # In[14]: # ¿qué tal se etiquetan nuestras oraciones de ejemplo? print(unigramTagger.tag(oracion1)) print(unigramTagger.tag(oracion2)) print(unigramTagger.tag(oracion3)) # Los etiquetadores basados en unigramas se construyen a partir del simple cálculo de una distribución de frecuencia para cada token (palabra) y asignan siempre la etiqueta morfológica más probable. En nuestro caso, esta estrategia funciona relativamente bien: el tagger supera el 81% de precisión. Sin embargo, esta aproximación presenta numerosos problemas a la hora de etiquetar palabras homógrafas (un mismo token funcionando con más de una categoría gramatical). Si probamos con nuestra `oracion2`, comprobamos que la segunda aparición de *progress* no es etiquetada correctamente. # # Intuitivamente, podemos pensar que sabríamos distinguir ambas categorías si tuviéramos en cuenta algo del contexto de aparición de las palabras: *progress* es un sustantivo cuando aparece después del artículo *the* y es verbo cuando aparece tras un pronombre personal como *I*. Si en lugar de calcular frecuencias de unigramas, extendiéramos los cálculos a secuencias de dos o tres palabras, podríamos tener mejores resultados. Y precisamente por eso vamos a calcular distribuciones de frecuencias condicionales: asignaremos a cada token la categoría gramatical más frecuente teniendo en cuenta la categoría gramatical de la(s) palabra(s) inmediatamente anterior(es). # # Creamos un par de etiquetadores basado en bigramas (secuencias de dos palabras) o trigramas (secuencias de tres palabras) a través de las clases `nltk.BigramTagger` y `nltk.TrigramTagger`. Y los probamos con nuestra `oracion2`. # In[15]: bigramTagger = nltk.BigramTagger(corpusEntrenamiento) trigramTagger = nltk.TrigramTagger(corpusEntrenamiento) # In[16]: print(bigramTagger.tag(oracion2)) print(trigramTagger.tag(oracion2)) # Como se ve en los ejemplos, los resultados son desastrosos. La mayoría de los tokens se quedan sin etiqueta y se muestran como `None`. # # Si los evaluamos con nuestra colección de test, vemos que apenas superan el 10% de precisión. Peores resultados que nuestro `DefaultTagger`. # In[17]: print(bigramTagger.evaluate(corpusTest)) print(trigramTagger.evaluate(corpusTest)) # ¿Por qué ocurre esto? La intuición no nos engaña, y es verdad que si calculamos distribuciones de frecuencia condicionales teniendo en cuenta secuencias de palabras más largas, nuestros datos serán más finos. Sin embargo, cuando consideramos secuencias de tokens más largos nos arriesgamos a que dichas secuencias no aparezcan como tales en el corpus de entrenamiento. # # En el ejemplo de `oracion2`, nuestro `bigramTagger` es incapaz de etiquetar la palabra *progress* porque no ha encontrado en el corpus de entrenamiento ni el bigrama (**The, progress**) ni (**I, progress**). Obviamente, nuestro `trigramTagger` tampoco ha encontrado los trigramas (**`INICIO_DE_ORACIÓN`, The, progress**) o (**as, I, progress**). Si esas secuencias no aparecen en el corpus de entrenamiento, no hay nada que aprender. # # # # ### Combinamos etiquetadores # # En estos casos, la solución más satisfactoria consiste en combinar de manera incremental la potencia de todos nuestros etiquetadores. Vamos a crear nuevos taggers que utilicen otros como respaldo. # # Utilizaremos un tagger de trigramas que, cuando no tenga respuesta para etiquetar un determinado token, utilizará como respaldo el tagger de bigramas. A su vez, el tagger de bigramas tirará del de unigramas cuando no tenga respuesta. Por último, el de unigramas utilizará como respaldo el tagger de expresiones regulares que definimos antes. De esta manera aumentamos la precisión hasta casi el 87%. # In[18]: unigramTagger = nltk.UnigramTagger(corpusEntrenamiento, backoff=regexTagger) bigramTagger = nltk.BigramTagger(corpusEntrenamiento, backoff=unigramTagger) trigramTagger = nltk.TrigramTagger(corpusEntrenamiento, backoff=bigramTagger) trigramTagger.evaluate(corpusTest) # In[19]: print(trigramTagger.tag(oracion1)) print(trigramTagger.tag(oracion2)) print(trigramTagger.tag(oracion3)) # In[20]: print(nltk.pos_tag(oracion2))