Esta unidad dispone de los siguientes archivos:
NLTK se distribuye con una gran cantidad de corpus. A pesar de que una gran parte de estos corpus están en inglés, hay también en otras lenguas, cómo el castellano y el catalán. La lista completa y actualizada de los corpus disponibles al NLTK se puede encontrar en el siguiente enlace: http://www.nltk.org/nltk_data/
Por ejemplo, NLTK incluye una selección de textos del Proyecto Gutenberg. Este proyecto recopila libros en formato electrónico (libros en dominio público, es decir, sin derechos de autor y por tanto legalmente descargables). Si queremos cargar este corpus y observar los archivos que contiene, podemos hacer en un intérprete interactivo:
>>> from nltk.corpus import gutenberg>>> libros=gutenberg.fileids()>>> print(libros)['austen-emma.txt', 'austen-persuasion.txt', 'austen-sin.txt', 'bible-kjv.txt', 'blake-poems.txt', 'bryant-stories.txt', 'burgess-busterbrown.txt', 'carroll-alice.txt', 'chesterton-baile.txt', 'chesterton-brown.txt', 'chesterton-thursday.txt', 'edgeworth-parientes.txt', 'melville-moby_dick.txt', 'milton-paradise.txt', 'shakespeare-caesar.txt', 'shakespeare-hamlet.txt', 'shakespeare-macbeth.txt', 'whitman-leaves.txt']Podemos acceder a las palabras de un determinado libro de la colección haciendo lo siguiente:
>>> import nltk>>> mobi=nltk.corpus.gutenberg.words("melville-moby_dick.txt")>>> print(mobi)['[', 'Moby', 'Dick', 'by', 'Herman', 'Melville', ...]>>> print(len(mobi))260819Con el método words hemos podido acceder a las palabras del libro (que he almacenado a la variable mobi). A la última línea hemos contado las posiciones de la lista mobi, es decir, el número de palabras del libro. En corpus de texto cómo este podemos acceder a:
En el programa siguiente (programa-4-1.py), podemos observar el funcionamiento de estos métodos (cuando lo ejecutes, para pasar de un nivel al siguiente, pulsa la tecla Enter):
import nltkprint("TEXTO EN BRUTO")a=input()texto_en_bruto=nltk.corpus.gutenberg.raw("melville-moby_dick.txt")print(texto_en_bruto[0:100000])print("PÁRRAFOS")a=input()parrafos=nltk.corpus.gutenberg.paras("melville-moby_dick.txt")for para in parrafos: print(para)print("ORACIONES")a=input()oraciones=nltk.corpus.gutenberg.sents("melville-moby_dick.txt")for oracion in oraciones: print(oracion)print("PALABRAS")a=input()palabras=nltk.corpus.gutenberg.words("melville-moby_dick.txt")for palabra in palabras: print(palabra)Con a=input() el sistema espera que el usuario introduzca algún valor y lo almacena en a (que no utilizamos para nada). Lo que conseguimos es que el sistema se espere hasta que el usuario pulse la tecla Enter.
Por ahora estamos trabajando con un corpus textual plano, sin ningún tipo de análisis ni anotación. Más adelante veremos que NLTK también proporciona corpus con varios niveles de análisis.
NLTK proporciona una buena colección de corpus, pero con toda seguridad necesitaremos también trabajar con corpus propios. Para hacer esto podemos utilizar las instrucciones que ya conocemos para abrir y leer archivos de texto (mirad el apartado 2.3), o bien podremos usar los diferentes lectores de corpus que proporciona NLTK.
El lector de corpus más elemental que proporciona el NLTK es el PlaintextCorpusReader, que sirve, como indica su nombre, para leer corpus que estén en formato de texto plano (es decir, solo el texto, sin ningún tipo de anotación). En el programa-4-2.py se puede observar el funcionamiento básico. En este caso también usaremos la novela Moby Dick, descargada del Proyecto Gutenberg y que podemos encontrar en los ficheros de este capítulo.
from nltk.corpus.reader.plaintext import PlaintextCorpusReadercorpus = PlaintextCorpusReader(".", 'mobi_dick.txt')for oracion in corpus.sents(): print(oracion)y a la salida tendremos las oraciones, que de hecho, si os fijáis, son listas de palabras:
...['In', 'a', 'word', ',', 'the', 'whale', 'was', 'seized', 'and', 'sold', ',', 'and', 'his', 'Grace', 'the', 'Duke', 'of', 'Wellington', 'received', 'the', 'money', '.']...El PlaintextCorpusReader carga el fichero de texto y lleva a cabo un proceso de segmentación en párrafos (a partir de saltos de párrafos), en oraciones (usando un segmentador determinado) y en palabras (o tokens, usando un tokenizador determinado). Si no indicamos nada en el momento de llamar a PlaintextCorpusReader utiliza:
Como que el tokenitzador que usa es muy simple y se basa en separar el texto en secuencias de caracteres alfabéticos y no alfabéticos usando la expresión regular \w+|[^\w\s]+ se pueden producir errores, como el siguiente:
['"', 'Won', "'", 't', 'the', 'Duke', 'be', 'content', 'with', 'a', 'quarter', 'or', 'a', 'half', '?"']dónde won't se ha separado cómo
won'tFijémonos ahora que si aplicamos este mismo programa a un corpus en castellano la tokenización no será totalmente correcta. Con este capítulo se distribuye un fragmento del Corpus del Diario Oficial de la Generalitat de Cataluña (corpus DOGC), concretamente el correspondiente a la versión castellana del año 2015. Si cargamos el fichero DOGC-2015-spa.txt, en vez de mobi_dick.txt obtenemos un resultado como el siguiente (mostramos únicamente un fragmento):
...['El', 'pleno', 'del', 'Ayuntamiento', 'de', 'Òrrius', 'en', 'la', 'sesión', 'extraordinaria', 'celebrada', 'el', 'día', '20', 'de', 'enero', 'de', '2015', 'aprobó', 'inicialmente', 'el', 'proyecto', 'de', 'obra',...…Para el castellano el segmentador funciona bastante bien. Si probamos ahora con el mismo texto en catalán (DOGC-2015-cat.txt), veremos que se producen algunos problemas:
...['El', 'ple', 'de', 'l', "'", 'Ajuntament', 'd', "'", 'Òrrius', 'a', 'la', 'sessió', 'extraordinària', 'celebrada', 'el', 'dia', '20', 'de', 'gener', 'de', '2015', 'va', 'aprovar', 'inicialment', 'el', 'projecte', 'd', "'", 'obra',......Fijémonos que, por ejemplo, los apóstrofos (') quedan como tokens aislados, separados del artículo o preposición correspondiente.
En este mismo capítulo, en los apartados 4.4 y 4.5, tratamos más a fondo la segmentación en unidades léxicas (tokenización) y la segmentación en oraciones. Lo que sí que avanzamos ahora es que al PlaintextCorpusReader se le puede indicar qué tokenizador y segmentador tiene que utilizar. Esto se puede hacer de la siguiente manera (programa-4-3.py)(necesitareis descargar el archivo catalan.pickle que tenéis en la sección de archivos de este capítulo):
import nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizersegmentador= nltk.data.load("catalan.pickle")tokenitzador=RegexpTokenizer('[ldsmLDSM]\'|\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'DOGC-2015-cat.txt',word_tokenizer=tokenitzador,sent_tokenizer=segmentador)for oracion in corpus.sents(): print(oracion)Fijaos que hemos definido un segmentador (que cargamos de catalan.pickle) y un tokenizador basado en expresiones regulares, y que usamos estos nuevos elementos cuando creamos el PlaintextCorpusReader. Ahora hemos arreglado el aspecto de los apóstrofos en la tokenización y la salida (mostramos solo un fragmento) es:
['El', 'ple', 'de', "l'", 'Ajuntament', "d'", 'Òrrius', 'a', 'la', 'sessió', 'extraordinària', 'celebrada', 'el', 'dia', '20', 'de', 'gener', 'de', '2015', 'va', 'aprovar', 'inicialment', 'el', 'projecte', "d'", 'obra', '.']En el apartado 4.4 veremos con más detalle el tema de las expresiones regulares. Comparamos ahora las expresiones regulares del tokenizador por defecto:
\w+|[^\w\s]+y el que hemos creado nosotros:
[ldsmLDSM]\'|\w+|[^\w\s]+Hemos añadido la parte [ldsmLDSM]\' que define los tokens formados por: l', d', s', m', L', D', S', M'.
Modificaremos ligeramente el programa-4-3.py, para que nos dé todas las palabras (o tokens) en vez de todas las oraciones y para que nos proporcione el número total de palabras (programa-4-4.py):
import nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizersegmentador= nltk.data.load("catalan.pickle")tokenitzador=RegexpTokenizer('[ldsmLDSM]\'|\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'DOGC-2015-cat.txt',word_tokenizer=tokenitzador,sent_tokenizer=segmentador)for paraula in corpus.words(): print(paraula)print("TOTAL PALABRAS:",len(corpus.words()))El programa se está un buen rato mostrando palabras por pantalla y después nos da el número de palabras totales:
...5524,de 11.12.2009),TOTAL PALABRAS: 2448870Si miráramos con detenimiento la lista veríamos que muchas de las palabras ocurren más de una vez (en el apartado 4.6 veremos como calcular la frecuencia y la distribución de frecuencias de las palabras). Si ahora lo que queremos es obtener una lista de palabras diferentes, podemos utilizar la instrucción set, como podemos observar en el programa-4-5.py:
import nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizersegmentador= nltk.data.load("catalan.pickle")tokenitzador=RegexpTokenizer('[ldsmLDSM]\'|\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'DOGC-2015-cat.txt',word_tokenizer=tokenitzador,sent_tokenizer=segmentador)ocurrencies=corpus.words()tipus=set(ocurrencies)print("OCURRENCIAS:",len(ocurrencies))print("TIPOS:",len(tipus))Que nos da a la salida:
OCURRENCIAS: 2448870TIPOS: 34872Para resumir podemos decir que las ocurrencias son el número total de palabras que aparecen en el corpus y los tipos el número de palabras diferentes que aparecen en el corpus.
Hay que tener en cuenta, no obstante, que no podemos hablar estrictamente de palabras, puesto que también se incluyen los signos de puntuación, las cifras, etc.
Podemos calcular un índice de riqueza léxica dividiendo el número de ocurrencias entre el número de tipos. Lo vemos al programa-4-6.py:
import nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizersegmentador= nltk.data.load("catalan.pickle")tokenitzador=RegexpTokenizer('[ldsmLDSM]\'|\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'DOGC-2015-cat.txt',word_tokenizer=tokenitzador,sent_tokenizer=segmentador)ocurrencias=corpus.words()tipos=set(ocurrencies)riquezalexica=len(ocurrencies)/len(tipus)print("OCURRENCIAS:",len(ocurrencias))print("TIPUS:",len(tipos))print("RIQUEZA LÊXICA:",round(riquezalexica,2))Que nos da a la salida:
OCURRENCIAS: 2448870TIPOS: 34872RIQUEZA LÉXICA: 70.22Lo que indica que cada palabra de media se usa 70 veces. Experimenta un poco con diferentes tipos de texto para ver cómo varía este índice de riqueza léxica.
La segmentación en unidades léxicas o tokenización, consiste en dividir el texto en unidades más pequeñas (que a menudo coinciden con palabras). A pesar de que se trata de una tarea muy básica y necesaria para poder llevar a cabo tareas de análisis más avanzadas, esta tarea presenta numerosos problemas que no son fáciles de solucionar. Hay numerosos trabajos sobre esta área entre los cuales se pueden destacar los trabajos de Grefenstette y Tapanainen (1994) y Mikheev (2002). Sea como sea, actualmente se puede considerar que esta tarea se resuelve de manera satisfactoria y no hay una investigación activa para mejorarla. En este apartado aprenderemos algunas técnicas para segmentar el texto en unidades léxicas e iremos observando los diferentes problemas que aparecen y cómo se pueden solucionar. Las pruebas de los diferentes sistemas los haremos con la oración:
El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación.
A pesar de que el resultado deseado de la tokenització puede depender de las tareas concretas, la salida deseada de nuestro sistema tendría que ser una cosa del siguiente estilo:
['El','Sr.','Martínez','llegará','mañana','de ','Alicante','con','la','R.E.N.F.E','a','las','22.30','h.','y','se ','alojará','en','el ','hotel','de',' la','estación',.']El primer tokenizador que probaremos utiliza la instrucción split(), que divide una cadena según el separador que se indique, y si no se indica nada, por espacios. Lo podemos ver al programa-4-7.py:
oracion="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación."tokens=oracion.split()print(tokens)que nos da la siguiente salida, que no es exactamente la que queríamos:
['El', 'Sr.', 'Martínez', 'llegará', 'mañana', 'de', 'Alicante', 'con', 'la', 'R.E.N.F.E.', 'a', 'las', '22.30', 'h.', 'y', 'se', 'alojará', 'en', 'el', 'hotel', 'de', 'la', 'estación.']NLTK proporciona una serie de tokenitzadores que presentamos a continuación.
Separa el texto por espacios en blanco, como lo hemos hecho en el ejemplo anterior. En este caso los espacios en blanco pueden ser los caracteres: espacio en blanco, tabulador y nueva línea. Lo podemos ver en el programa-4-8.py:
import nltkoracion="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación."tokens=nltk.tokenize.WhitespaceTokenizer().tokenize(oracion)print(tokens)Que da como salida:
['El', 'Sr.', 'Martínez', 'llegará', 'mañana', 'de', 'Alicante', 'con', 'la', 'R.E.N.F.E.', 'a', 'las', '22.30', 'h.', 'y', 'se', 'alojará', 'en', 'el', 'hotel', 'de', 'la', 'estación.']Aprovecho este ejemplo para explicar varias maneras de importar un tokenitzador, o cualquier método de una clase determinada. En el caso anterior hemos importado todo el nltk y hemos llamado al método haciendo:
tokens=nltk.tokenize.WhitespaceTokenizer().tokenize(oracion)Esto también se puede hacer de la siguiente manera alternativa (programa-4-8b.py) (fijaos como accedemos ahora al método tokenize)
from nltk.tokenize import WhitespaceTokenizeroracion="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación."tokens=WhitespaceTokenizer().tokenize(oracio)print(tokens)y también de la siguiente manera, dando un nombre cualquiera al método (programa-4-8c.py)
from nltk.tokenize import WhitespaceTokenizer as tokenitzadororacion="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación."tokens=tokenitzador().tokenize(oracio)print(tokens)Es igual que el anterior, pero en este caso el único carácter que se tiene en cuenta es el de espacio en blanco (" "). Equivale a split(" "). En nuestro ejemplo la salida seria exactamente la misma por lo que no es necesario proporcionar ni el código ni el programa.
Este tokenizador utiliza las convenciones del Penn Treebank corpus, qué es un corpus anotado del inglés creado en 1980 a partir de artículos del Wall Street Journal. Cómo que es para el inglés, no funcionará del todo bien para otras lengua y por este motivo en el ejemplo pongo una oración del inglés (programa-4-9.py):
from nltk.tokenize import TreebankWordTokenizer as tokenitzador
oracion="We need to conduct an assessment to learn whether a student's dificulties are because he or she can't or won't complete assignments."tokens=tokenitzador().tokenize(oracion)print(tokens)y la salida será:
['We', 'need', 'to', 'conduct', 'an', 'assessment', 'to', 'learn', 'whether', 'a', 'student', "'s", 'dificulties', 'are', 'because', 'he', 'or', 'she', 'ca', "n't", 'or', 'wo', "n't", 'complete', 'assignments', '.']Este tokenitzador se usa bastante para el inglés, y por este motivo se ha creado una función específica que hace de wrapper (wrapper function) para simplificar su uso (programa-4-9b.py):
from nltk.tokenize import word_tokenizeoracion="We need to conduct an assessment to learn whether a student's dificulties are because he or she can't or won't complete assignments."tokens=word_tokenize(oracion)print(tokens)que proporciona exactamente la misma salida.
En el apartado anterior (4.3. Ocurrencias (tokens) y tipos (types)) ya vimos un ejemplo del tokenizador por expresiones regulares. Este es un tipo de tokenizador que nos permite un control total sobre el proceso de tokenización. Claro que para sacar el máximo provecho hay que dominar las expresiones regulares de Python. Podéis encontrar una explicación detallada a Regular Expression HOWTO y también un buen resumen a Pyschools.com (http://doc.pyschools.com/html/regex.html). Otro buen recurso para trabajar con expresiones regulares es la web http://regexr.com/.
Veamos ahora algunos ejemplos, empezando por el programa-4-10.py.
from nltk.tokenize import RegexpTokenizeroracion="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación."tokenizador=RegexpTokenizer('\w+|[^\w\s]+')tokens=tokenizador.tokenize(oracion)print(tokens)Que nos ofrece la siguiente salida:
['El', 'Sr', '.', 'Martínez', 'llegará', 'mañana', 'de', 'Alicante', 'con', 'la', 'R', '.', 'E', '.', 'N', '.', 'F', '.', 'E', '.', 'a', 'las', '22', '.', '30', 'h', '.', 'y', 'se', 'alojará', 'en', 'el', 'hotel', 'de', 'la', 'estación', '.']Cómo podemos observar, nos separa Sr del punto (.) y realmente querríamos tener Sr. como token, y lo mismo pasa con h.. También tokeniza incorrectamente R.E.N.F.E. Podríamos solucionar esto modificando la expresión regular (programa-4-10b.py) de forma que añadimos estas dos unidades como tokens.
tokenizador=RegexpTokenizer('Sr\.|h\.|R\.E\.N.\F\.E\.|\w+|[^\w\s]+')que nos devolvería la tokenización correcta de estas dos unidades:
['El', 'Sr.', 'Martínez', 'llegará', 'mañana', 'de', 'Alicante', 'con', 'la', 'R.E.N.F.E.', 'a', 'las', '22', '.', '30', 'h.', 'y', 'se', 'alojará', 'en', 'el', 'hotel', 'de', 'la', 'estación', '.']El hecho de añadir una lista de abreviaturas frecuentes es bastante habitual, pero no podemos esperar tener una lista suficiente completa de acrónimos. Por este motivo, tenemos que intentar expresar los acrónimos de una manera más general, cómo por ejemplo la que presentamos al programa-4-10c.py
tokenizador=RegexpTokenizer('Sr\.|h\.|([A-Z]\.){1,}|\w+|[^\w\s]+')que nos ofrece la misma salida pero que ahora contempla cualquier acrónimo tipo R.E.N.F.E.. Fijémonos en la parte de la expresión regular que trata estos acrónimos:
(?:[A-Z]\.){1,}que significa cualquier combinación de uno o más {1,} letras mayúsculas [A-Z] seguidas de punto \..
Estrictamente [A-Z]contempla cualquier carácter de la A a la Z (todas las mayúsculas) pero no incluye las mayúsculas acentuadas, la ñ mayúscula, etc. puesto que no están entre la A y la Z. Podríamos cambiar la expresión regular del tokenitzador por:
tokenitzador=RegexpTokenizer('Sr\.|\h.|(?:[A-ZÀÉÈÍÏÒÓÚÜÇÑ]\.){1,}|\w+|[^\w\s]+')Por defecto, el tokenizador por expresiones regulares considera que el que expresamos son los tokens, pero podemos definir el que son los separadores de tokens con el modificador gaps=True. En el programa-4-10.py podemos ver un ejemplo simple.
import refrom nltk.tokenize import RegexpTokenizeroracion="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación."tokenizador=RegexpTokenizer('\s+',gaps=True)tokens=tokenizador.tokenize(oracion)print(tokens)Que da la siguiente salida (que coincide con la del tokenizador por espacios en blanco):
['El', 'Sr.', 'Martínez', 'llegará', 'mañana', 'de', 'Alicante', 'con', 'la', 'R.E.N.F.E.', 'a', 'las', '22.30', 'h.', 'y', 'se', 'alojará', 'en', 'el', 'hotel', 'de', 'la', 'estación.']En la sección anterior hemos visto como separar el texto en unidad léxicas, proceso que también recibe el nombre de tokenización. En esta sección veremos cómo podemos separar un párrafo en segmentos, que son unidades parecidas a oraciones. Este proceso recibe el nombre de segmentación.
En esta sección usaremos dos segmentos de prueba, uno que no presentará grandes problemas:
Hoy hace un día muy bonito. Mañana Albert irá a comer en casa.
y otro que sí que presenta numerosos problemas.
El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación. El día siguiente visitará al Dr. Rovira a la Avda. Tibidabo. La vuelta la hará en avión en el vuelo AF.352.
Este es un segmentador sencillo que básicamente segmenta por puntos. Para ver cómo funciona probamos el programa-4-11.py
from nltk.tokenize import PunktSentenceTokenizerparrafo1="Hoy hace un día muy bonito. Mañana Albert irá a comer en casa."parrafo2="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación. El día siguiente visitará al Dr. Rovira en la Avda. Tibidabo. La vuelta la hará en avión en el vuelo AF.352."segmentador=PunktSentenceTokenizer()segmentos1=segmentador.tokenize(parrafo1)print(segmentos1)segmentos2=segmentador.tokenize(parrafo2)print(segmentos2)Que proporciona la siguiente salida:
['Hoy hace un día muy bonito.', 'Mañana Albert irá a comer en casa.']['El Sr.', 'Martínez llegará mañana de Alicante con la R.E.N.F.E.', 'a las 22.30 h. y se alojará en el hotel de la estación.', 'El día siguiente visitará al Dr.', 'Rovira a la Avda.', 'Tibidabo.', 'La vuelta la hará en avión en el vuelo AF.352.']El proceso de segmentación también se puede hacer con sent_tokenizer(), que llama a una instancia especial del PunktSenteceTokenizer que ha sido entrenada y que funciona bastante bien para varias lenguas europeas. Se trata de una implementación del algoritmo de Kiss and Strunk (2006). Lo podemos ver en el programa-4-11b.py.
from nltk.tokenize import sent_tokenizeparrafo1="Hoy hace un día muy bonito. Mañana Albert irá a comer en casa."parrafo2="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación. El día siguiente visitará al Dr. Rovira en la Avda. Tibidabo. La vuelta la hará en avión en el vuelo AF.352."segmentos1=sent_tokenize(parrafo1)print(segmentos1)segmentos2=sent_tokenize(parrafo2)print(segmentos2)Que nos ofrece una salida mucho mejor, a pesar de que no perfecta:
['Hoy hace un día muy bonito.', 'Mañana Albert irá a comer en casa.']['El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E.', 'a las 22.30 h. y se alojará en el hotel de la estación.', 'El día siguiente visitará al Dr. Rovira en la Avda.', 'Tibidabo.', 'La vuelta la hará en avión en el vuelo AF.352.']Con los datos del NLTK se distribuyen una serie de segmentadores para lenguas determinadas. Concretamente se distribuyen los siguientes: checo, finlandés, noruego, español, danés, francés, polaco, sueco, holandés, alemán, portugués, turco, inglés, griego, estonio, italiano, esloveno. No se distribuye uno para el catalán, pero en esta misma sección aprenderemos a crear uno específico para el catalán. En el programa-4-12.py cargamos un de específico para el inglés:
import nltk.dataparrafo="Today Mr. Smith and Ms. Johanson will meet at St. Patrick church."segmentador=nltk.data.load("tokenizers/punkt/PY3/english.pickle")segmentos=segmentador.tokenize(parrafo)print(segmentos)Que proporciona la salida:
['Today Mr. Smith and Ms. Johanson will meet at St. Patrick church.']El algoritmo de Kiss and Strunk (2006) permite entrenar un segmentador a partir de texto sin ningún tipo de anotación. NLTK implementa este algoritmo. Vamos a entrenar un segmentador para el castellano (de hecho crear un spanish.pickle, aunque ya se distribuye uno con NLTK) a partir del corpus de DOGC correspondiendo al año 2015. El programa-4-13.py implementa este aprendizaje:
import nltk.tokenize.punktimport pickleimport codecssegmentador = nltk.tokenize.punkt.PunktSentenceTokenizer()texto = codecs.open("DOGC-2015-spa.txt","r","utf8").read()segmentador.train(texto)out = open("spanish.pickle","wb")pickle.dump(segmentador, out)out.close()El segmentador almacenado en spanish.pickle lo podemos utilizar programa-4-12b.py):
import nltk.datasegmentador=nltk.data.load("spanish.pickle")parrafo1="Hoy hace un día muy bonito. Mañana Albert irá a comer en casa."parrafo2="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación. El día siguiente visitará al Dr. Rovira en la Avda. Tibidabo. La vuelta la hará en avión en el vuelo AF.352."segmentos1=segmentador.tokenize(parrafo1)print(segmentos1)segmentos2=segmentador.tokenize(parrafo2)print(segmentos2)Que ofrece la siguiente salida:
['Hoy hace un día muy bonito.', 'Mañana Albert irá a comer en casa.']['El Sr.', 'Martínez llegará mañana de Alicante con la R.E.N.F.E.', 'a las 22.30 h. y se alojará en el hotel de la estación.', 'El día siguiente visitará al Dr.', 'Rovira en la Avda.', 'Tibidabo.', 'La vuelta la hará en avión en el vuelo AF.352.']¿Funciona del todo correctamente el segmentador que hemos entrenado?
Podemos personalizar el segmentador entrenado para añadir nuevas abreviaturas o acrónimos que no han sido detectados en el proceso de entrenamiento. Lo podemos hacer específicamente para un programa determinado, cómo en el programa-4-12c.py, donde cargamos el catalan.pickle que hemos entrenado y añadimos R.E.N.F.E. (fijaos que lo hacemos en minúsculas y sin el punto final):
import nltk.datasegmentador=nltk.data.load("spanish.pickle")abreviaturas_extra = ['r.e.n.f.e']segmentador._params.abbrev_types.update(abreviaturas_extra)parrafo1="Hoy hace un día muy bonito. Mañana Albert irá a comer en casa."parrafo2="El Sr. Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación. El día siguiente visitará al Dr. Rovira en la Avda. Tibidabo. La vuelta la hará en avión en el vuelo AF.352."segmentos1=segmentador.tokenize(parrafo1)print(segmentos1)segmentos2=segmentador.tokenize(parrafo2)print(segmentos2)y ahora la salida ya segmenta mejor:
['El Sr.', 'Martínez llegará mañana de Alicante con la R.E.N.F.E. a las 22.30 h. y se alojará en el hotel de la estación.', 'El día siguiente visitará al Dr.', 'Rovira en la Avda.', 'Tibidabo.', 'La vuelta la hará en avión en el vuelo AF.352.']En los ficheros adjuntos podéis encontrar una lista de abreviaciones. Ahora lo que haremos será cargar el spanish.pickle que hemos entrenado y modificarlo añadiendo la lista de abreviaturas y acrónimos del fichero. Grabaremos este nuevo segmentador como spanish-mod.pickle. (programa-4-14.py)
import nltk.dataimport codecsimport picklesegmentador=nltk.data.load("spanish.pickle")archivo_abreviaturas=codecs.open("abr-spa.txt","r",encoding="utf-8")abreviaturas_extra =[]for abreviatura in archivo_abreviaturas.readlines(): abreviatura=abreviatura.rstrip() abreviaturas_extra.append(abreviatura)segmentador._params.abbrev_types.update(abreviaturas_extra)out = open("spanish-mod.pickle","wb")pickle.dump(segmentador, out)out.close()Modifica el programa-4-12b.py para que cargue este nuevo segmentador y verifica si funciona correctamente.
En este apartado aprenderemos a hacer algunos cálculos sencillos sobre corpus: frecuencias absolutas y frecuencias relativas, distribuciones de frecuencia y a encontrar las colocaciones más frecuentes de un corpus.
Entendemos por frecuencia absoluta el número total de veces que aparece una determinada unidad léxica en nuestro corpus.
El cálculo de la frecuencia absoluta de una palabra es sencillo: podemos utilizar un diccionario para poner como clave las palabras e ir incrementando el valor del diccionario cada vez que aparece la palabra. En el siguiente programa (programa-4-15.py) podemos ver una implementación sencilla de esta idea:
import nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizersegmentador= nltk.data.load("spanish-mod.pickle")tokenizador=RegexpTokenizer('\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'DOGC-2015-spa.txt',word_tokenizer=tokenizador,sent_tokenizer=segmentador)frecuencia={}for palabra in corpus.words(): frecuencia[palabra]=frecuencia.get(palabra,0)+1for clave in frecuencia.keys(): print(frecuencia[clave],clave)y por pantalla nos mostrará las palabras y las frecuencias, pero de una manera desordenada, puesto que los diccionarios de Python son estructuras de datos sin un orden determinado.
NLTK proporciona una función FreqDist que facilita mucho el cálculo de frecuencias. Veamos su uso en el programa-4-16.py:
import nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizerfrom nltk import FreqDistsegmentador= nltk.data.load("spanish-mod.pickle")tokenizador=RegexpTokenizer('\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'DOGC-2015-spa.txt',word_tokenizer=tokenizador,sent_tokenizer=segmentador)frequencia=FreqDist(corpus.words())for mc in frequencia.most_common(): print(mc)Ahora sí que nos proporciona las palabras y la frecuencia de cada palabra ordenada de más frecuente a menos frecuente. Cómo que hay muchas, podemos modificar fácilmente el programa porque nos proporcione las 25 más frecuentes, cambiando la línea:
for mc in frequencia.most_common():por
for mc in frequencia.most_common(25):La frecuencia absoluta de una palabra en un determinado corpus no nos da información real sobre si la palabra es muy frecuente o no, porque esto dependerá del tamaño del corpus. Que una palabra aparezca, digamos, 22 veces en nuestro corpus, no nos dice nada, puesto que si el corpus es muy grande quizás este valor de frecuencia es pequeño.
La frecuencia relativa de una palabra en un corpus es el número a veces que aparece dividida por el número total de palabras en el corpus.
FreqDist nos facilita mucho el cálculo de la frecuencia relativa puesto que la podemos consultar con el método freq(). Lo podemos ver en el programa-4-17.py, que es una modificación del anterior. Ahora guardamos al fichero frequencias.txt las palabras ordenadas por frecuencia y mostramos la frecuencia absoluta y la relativa de cada palabra:
import nltkfrom nltk.corpus.reader.plaintext import PlaintextCorpusReaderfrom nltk.tokenize import RegexpTokenizerfrom nltk import FreqDistimport codecssegmentador= nltk.data.load("spanish-mod.pickle")tokenizador=RegexpTokenizer('\w+|[^\w\s]+')corpus = PlaintextCorpusReader(".", 'DOGC-2015-spa.txt',word_tokenizer=tokenizador,sent_tokenizer=segmentador)frecuencia=FreqDist(corpus.words())salida=codecs.open("frecuencias.txt","w",encoding="utf-8")for mc in frecuencia.most_common(): palabra=mc[0] frecuencia_absoluta=mc[1] frecuencia_relativa=frecuencia.freq(palabra) cadena=str(frecuencia_absoluta)+"\t"+str(frecuencia_relativa)+"\t"+palabra salida.write(cadena+"\n")Y la salida (muestramos solo las 25 primeras):
1779837 0.07246859684771821 de1031818 0.042011938543933466 /986574 0.040169764684317016 ,921036 0.03750129172853188 .668298 0.027210704315134695 la421836 0.017175653174899757 y394394 0.016058313084377378 :386949 0.015755179312278437 el377317 0.015362998722237202 -349183 0.014217482866732621 en302520 0.012317532402333312 del290548 0.011830075381571926 2015278339 0.011332968568468372 que242310 0.009865996550341745 a217715 0.008864576117195547 los200890 0.00817952229374831 las189023 0.007696340497442317 0181949 0.007408312518419092 se136654 0.005564062121210024 08136191 0.005545210417182917 CIR134874 0.005491586887585294 1124829 0.005082590414686186 por122338 0.004981165804034949 )119835 0.004879252596303095 2114850 0.004676281225730467 conLa Ley de Zipf afirma que dado un corpus, la frecuencia de una palabra es inversamente proporcional a su posición a la tabla de frecuencias (rank).
Con su ley, Zipf (1949) afirma que hay una constante k que se puede calcular multiplicando la frecuencia de cualquier palabra por su posición en la tabla (rank) (k = f · r).
En el programa siguiente (programa-4-18.py) evaluamos la ley de Zipf con las 50 palabras más frecuentes del corpus Cess_esp:
import nltkfrom nltk.corpus import cess_espimport reimport codecspalabras=cess_esp.words()frecdist=nltk.FreqDist(palabras)salida=codecs.open("salida.txt","w", encoding="utf-8")posicion=0p = re.compile('\w+')for mc in frecdist.most_common(50): if p.match(mc[0]): posicion+=1 frec=mc[1] fxr=posicion*frec salida.write(mc[0]+"\t"+str(frec)+"\t"+str(posicion)+"\t"+str(fxr)+"\n")En el fichero salida.txt podemos observar las palabras con su frecuencia, posición y resultado del producto de la frecuencia y la posición (que tiende a ser constante):
de 10234 1 10234la 6412 2 12824que 5552 3 16656el 5199 4 20796en 4340 5 21700y 4235 6 25410los 2963 7 20741a 2953 8 23624del 2257 9 20313se 1884 10 18840las 1832 11 20152un 1815 12 21780con 1494 13 19422por 1456 14 20384una 1396 15 20940su 1291 16 20656para 1258 17 21386no 1232 18 22176al 984 19 18696es 911 20 18220El 812 21 17052ha 705 22 15510como 696 23 16008lo 652 24 15648más 648 25 16200La 510 26 13260sus 493 27 13311o 435 28 12180pero 357 29 10353hoy 348 30 10440entre 315 31 9765dos 306 32 9792sobre 303 33 9999En 301 34 10234le 300 35 10500años 291 36 10476este 278 37 10286han 271 38 10298también 254 39 9906fue 244 40 9760si 227 41 9307Los 226 42 9492A pesar de que la ley solo muestra una tendencia y no es exacta, recalca el hecho que en un corpus hay muy pocas palabras muy frecuentes y muchas palabras poco frecuentes. En el programa siguiente (programa-4-19.py) graficamos este fenómeno con un subconjunto del corpus Cess_esp de 1.000 palabras. Cómo podemos observar al gráfico el principio mencionado se cumple, es decir, que muy pocas palabras aparecen muchas veces y que muchas aparecen pocas veces.
import nltkfrom nltk.corpus import cess_espimport pylabpalabras=cess_esp.words()palabras1=palabras[:1000]freqdist=nltk.FreqDist(palabras1)freqdist.plot()Que crea el gráfico: