Resumen NLTK: Acceso a corpus de texto y recursos léxicos

Este resumen se corresponde con el capítulo 2 del NLTK Book Accessing Text Corpora and Lexical Resources. La lectura del capítulo es muy recomendable.

Corpus no anotados: el Proyecto Gutenberg

NLTK no da acceso directo a varias colecciones de textos. Para empezar, vamos a juguetear un poco con los libros del Proyecto Gutenberg, un repositorio público de libros libres y/o sin derechos de copyright en vigor. Antes de nada, necesitamos importar el módulo gutenberg que está en la librería nltk.corpus.


In [1]:
from nltk.corpus import gutenberg

Podemos listar el catálogo de libros del Proyecto Gutenberg disponibles desde NLTK a través del método nltk.corpus.gutenberg.fileids


In [2]:
print(gutenberg.fileids())


['austen-emma.txt', 'austen-persuasion.txt', 'austen-sense.txt', 'bible-kjv.txt', 'blake-poems.txt', 'bryant-stories.txt', 'burgess-busterbrown.txt', 'carroll-alice.txt', 'chesterton-ball.txt', 'chesterton-brown.txt', 'chesterton-thursday.txt', 'edgeworth-parents.txt', 'melville-moby_dick.txt', 'milton-paradise.txt', 'shakespeare-caesar.txt', 'shakespeare-hamlet.txt', 'shakespeare-macbeth.txt', 'whitman-leaves.txt']

Para cargar alguno de estos libros en variables y poder manipularlos directamente, podemos utilizar varios métodos.

  • gutenberg.raw recupera el texto como una única cadena de caracteres.
  • gutenberg.words recupera el texto tokenizado en palabras. El método devuelve una lista palabras.
  • gutenberg.sents recupera el texto segmentado por oraciones. El método devuelve una lista de oraciones. Cada oración es a su vez una lista de palabras.
  • gutenberg.paras recupera el texto segmentado por párrafos. El método devuelve una lista de párrafos. Cada párrafo es una lista de oraciones, cada oración es a su vez una lista de palabras.

In [3]:
# cargo la vesión 'cruda' de un par de libros. Como son libros del Proyecto Gutenberg, se trata de ficheros en texto plano
alice = gutenberg.raw("carroll-alice.txt")
print(alice[:200]) # imprimo los primeros 200 caracteres del libro de Alicia

bible = gutenberg.raw("bible-kjv.txt")
print(bible[:200]) # imprimo los primeros 200 caracteres de la Biblia


[Alice's Adventures in Wonderland by Lewis Carroll 1865]

CHAPTER I. Down the Rabbit-Hole

Alice was beginning to get very tired of sitting by her sister on the
bank, and of having nothing to do: once
[The King James Bible]

The Old Testament of the King James Bible

The First Book of Moses:  Called Genesis


1:1 In the beginning God created the heaven and the earth.

1:2 And the earth was without 

In [4]:
# segmentamos el texto en palabras teniendo en cuenta los espacios 
bible_tokens = bible.split()
# cargamos la versión de la Biblia segmentado en palabras
bible_words = gutenberg.words("bible-kjv.txt")

# no da el mismo número de tokens
print(len(bible_tokens), len(bible_words))

print(bible_tokens[:100])
print("-----------------------------------------------")
print(bible_words[:100])


821133 1010654
['[The', 'King', 'James', 'Bible]', 'The', 'Old', 'Testament', 'of', 'the', 'King', 'James', 'Bible', 'The', 'First', 'Book', 'of', 'Moses:', 'Called', 'Genesis', '1:1', 'In', 'the', 'beginning', 'God', 'created', 'the', 'heaven', 'and', 'the', 'earth.', '1:2', 'And', 'the', 'earth', 'was', 'without', 'form,', 'and', 'void;', 'and', 'darkness', 'was', 'upon', 'the', 'face', 'of', 'the', 'deep.', 'And', 'the', 'Spirit', 'of', 'God', 'moved', 'upon', 'the', 'face', 'of', 'the', 'waters.', '1:3', 'And', 'God', 'said,', 'Let', 'there', 'be', 'light:', 'and', 'there', 'was', 'light.', '1:4', 'And', 'God', 'saw', 'the', 'light,', 'that', 'it', 'was', 'good:', 'and', 'God', 'divided', 'the', 'light', 'from', 'the', 'darkness.', '1:5', 'And', 'God', 'called', 'the', 'light', 'Day,', 'and', 'the', 'darkness']
-----------------------------------------------
['[', 'The', 'King', 'James', 'Bible', ']', 'The', 'Old', 'Testament', 'of', 'the', 'King', 'James', 'Bible', 'The', 'First', 'Book', 'of', 'Moses', ':', 'Called', 'Genesis', '1', ':', '1', 'In', 'the', 'beginning', 'God', 'created', 'the', 'heaven', 'and', 'the', 'earth', '.', '1', ':', '2', 'And', 'the', 'earth', 'was', 'without', 'form', ',', 'and', 'void', ';', 'and', 'darkness', 'was', 'upon', 'the', 'face', 'of', 'the', 'deep', '.', 'And', 'the', 'Spirit', 'of', 'God', 'moved', 'upon', 'the', 'face', 'of', 'the', 'waters', '.', '1', ':', '3', 'And', 'God', 'said', ',', 'Let', 'there', 'be', 'light', ':', 'and', 'there', 'was', 'light', '.', '1', ':', '4', 'And', 'God', 'saw', 'the', 'light', ',', 'that', 'it']

In [5]:
# cargo la versión de Alicia segmentada en palabras
alice_words = gutenberg.words("carroll-alice.txt")
print(alice_words[:20]) # imprimo las primeros 20 palabras
print(len(alice_words))


['[', 'Alice', "'", 's', 'Adventures', 'in', 'Wonderland', 'by', 'Lewis', 'Carroll', '1865', ']', 'CHAPTER', 'I', '.', 'Down', 'the', 'Rabbit', '-', 'Hole']
34110

In [6]:
# cargo la versión de Alicia segmentada en oraciones
alice_sents = gutenberg.sents("carroll-alice.txt")
print("Alice tiene", len(alice_sents), "oraciones")
print(alice_sents[2:5]) # imprimo la tercera, cuarta y quinta oración


Alice tiene 1703 oraciones
[['Down', 'the', 'Rabbit', '-', 'Hole'], ['Alice', 'was', 'beginning', 'to', 'get', 'very', 'tired', 'of', 'sitting', 'by', 'her', 'sister', 'on', 'the', 'bank', ',', 'and', 'of', 'having', 'nothing', 'to', 'do', ':', 'once', 'or', 'twice', 'she', 'had', 'peeped', 'into', 'the', 'book', 'her', 'sister', 'was', 'reading', ',', 'but', 'it', 'had', 'no', 'pictures', 'or', 'conversations', 'in', 'it', ',', "'", 'and', 'what', 'is', 'the', 'use', 'of', 'a', 'book', ",'", 'thought', 'Alice', "'", 'without', 'pictures', 'or', 'conversation', "?'"], ['So', 'she', 'was', 'considering', 'in', 'her', 'own', 'mind', '(', 'as', 'well', 'as', 'she', 'could', ',', 'for', 'the', 'hot', 'day', 'made', 'her', 'feel', 'very', 'sleepy', 'and', 'stupid', '),', 'whether', 'the', 'pleasure', 'of', 'making', 'a', 'daisy', '-', 'chain', 'would', 'be', 'worth', 'the', 'trouble', 'of', 'getting', 'up', 'and', 'picking', 'the', 'daisies', ',', 'when', 'suddenly', 'a', 'White', 'Rabbit', 'with', 'pink', 'eyes', 'ran', 'close', 'by', 'her', '.']]

In [7]:
# cargo la versión de Alicia segmentada en párrafos
alice_paras = gutenberg.paras("carroll-alice.txt")
print("Alice tiene", len(alice_paras), "párrafos")

# imprimo los cinco primeros
for para in alice_paras[:5]:
    print(para)
    print("-------------------")


Alice tiene 817 párrafos
[['[', 'Alice', "'", 's', 'Adventures', 'in', 'Wonderland', 'by', 'Lewis', 'Carroll', '1865', ']']]
-------------------
[['CHAPTER', 'I', '.'], ['Down', 'the', 'Rabbit', '-', 'Hole']]
-------------------
[['Alice', 'was', 'beginning', 'to', 'get', 'very', 'tired', 'of', 'sitting', 'by', 'her', 'sister', 'on', 'the', 'bank', ',', 'and', 'of', 'having', 'nothing', 'to', 'do', ':', 'once', 'or', 'twice', 'she', 'had', 'peeped', 'into', 'the', 'book', 'her', 'sister', 'was', 'reading', ',', 'but', 'it', 'had', 'no', 'pictures', 'or', 'conversations', 'in', 'it', ',', "'", 'and', 'what', 'is', 'the', 'use', 'of', 'a', 'book', ",'", 'thought', 'Alice', "'", 'without', 'pictures', 'or', 'conversation', "?'"]]
-------------------
[['So', 'she', 'was', 'considering', 'in', 'her', 'own', 'mind', '(', 'as', 'well', 'as', 'she', 'could', ',', 'for', 'the', 'hot', 'day', 'made', 'her', 'feel', 'very', 'sleepy', 'and', 'stupid', '),', 'whether', 'the', 'pleasure', 'of', 'making', 'a', 'daisy', '-', 'chain', 'would', 'be', 'worth', 'the', 'trouble', 'of', 'getting', 'up', 'and', 'picking', 'the', 'daisies', ',', 'when', 'suddenly', 'a', 'White', 'Rabbit', 'with', 'pink', 'eyes', 'ran', 'close', 'by', 'her', '.']]
-------------------
[['There', 'was', 'nothing', 'so', 'VERY', 'remarkable', 'in', 'that', ';', 'nor', 'did', 'Alice', 'think', 'it', 'so', 'VERY', 'much', 'out', 'of', 'the', 'way', 'to', 'hear', 'the', 'Rabbit', 'say', 'to', 'itself', ',', "'", 'Oh', 'dear', '!'], ['Oh', 'dear', '!'], ['I', 'shall', 'be', 'late', "!'"], ['(', 'when', 'she', 'thought', 'it', 'over', 'afterwards', ',', 'it', 'occurred', 'to', 'her', 'that', 'she', 'ought', 'to', 'have', 'wondered', 'at', 'this', ',', 'but', 'at', 'the', 'time', 'it', 'all', 'seemed', 'quite', 'natural', ');', 'but', 'when', 'the', 'Rabbit', 'actually', 'TOOK', 'A', 'WATCH', 'OUT', 'OF', 'ITS', 'WAISTCOAT', '-', 'POCKET', ',', 'and', 'looked', 'at', 'it', ',', 'and', 'then', 'hurried', 'on', ',', 'Alice', 'started', 'to', 'her', 'feet', ',', 'for', 'it', 'flashed', 'across', 'her', 'mind', 'that', 'she', 'had', 'never', 'before', 'seen', 'a', 'rabbit', 'with', 'either', 'a', 'waistcoat', '-', 'pocket', ',', 'or', 'a', 'watch', 'to', 'take', 'out', 'of', 'it', ',', 'and', 'burning', 'with', 'curiosity', ',', 'she', 'ran', 'across', 'the', 'field', 'after', 'it', ',', 'and', 'fortunately', 'was', 'just', 'in', 'time', 'to', 'see', 'it', 'pop', 'down', 'a', 'large', 'rabbit', '-', 'hole', 'under', 'the', 'hedge', '.']]
-------------------

Fíjate en que cada método devuelve una estructura de datos diferente: desde una única cadena a listas de listas anidadas. Para que tengas claro las dimensiones de cada uno, podemos imprimir el número de caracteres, palabras, oraciones y párrafos del libro.


In [8]:
print(len(alice), "caracteres") 
print(len(alice_words), "palabras")
print(len(alice_sents), "oraciones")
print(len(alice_paras), "párrafos")


144395 caracteres
34110 palabras
1703 oraciones
817 párrafos

Vamos a imprimir algunas estadísticas para todos los libros del Proyecto Gutenberg disponibles. Para cada libro, impriremos por pantalla el promedio de caracteres por palabra, el promedio de palabras por oración y el promedio de oraciones por párrafo.


In [9]:
# para cada libro que está disponible en el objeto gutenberg
for libro in gutenberg.fileids():
    caracteres = len(gutenberg.raw(libro))
    palabras = len(gutenberg.words(libro))
    oraciones = len(gutenberg.sents(libro))
    parrafos = len(gutenberg.paras(libro))
    print(libro[:-4], "\t", round(caracteres/palabras, 2), "\t", round(palabras/oraciones, 2), "\t", round(oraciones/parrafos, 2))


austen-emma 	 4.61 	 24.82 	 3.27
austen-persuasion 	 4.75 	 26.2 	 3.63
austen-sense 	 4.75 	 28.32 	 2.68
bible-kjv 	 4.29 	 33.57 	 1.22
blake-poems 	 4.57 	 19.07 	 1.54
bryant-stories 	 4.49 	 19.41 	 2.4
burgess-busterbrown 	 4.46 	 17.99 	 3.96
carroll-alice 	 4.23 	 20.03 	 2.08
chesterton-ball 	 4.72 	 20.3 	 2.98
chesterton-brown 	 4.72 	 22.61 	 3.28
chesterton-thursday 	 4.63 	 18.5 	 2.91
edgeworth-parents 	 4.44 	 20.59 	 2.75
melville-moby_dick 	 4.77 	 25.93 	 3.6
milton-paradise 	 4.84 	 52.31 	 63.83
shakespeare-caesar 	 4.35 	 11.94 	 2.91
shakespeare-hamlet 	 4.36 	 12.03 	 3.27
shakespeare-macbeth 	 4.34 	 12.13 	 2.81
whitman-leaves 	 4.59 	 36.44 	 1.72

El módulo nltk.corpus permite acceder a otras colecciones de textos en otras lenguas (lista completa aquí). Vamos a probar con un corpus de noticias en castellano llamado cess_esp que incluye anotación morfo-sintáctica.


In [10]:
from nltk.corpus import cess_esp

# la versión en crudo de este corpus contiene información morfosintáctica con un formato que todavía no conocemos.
# en este caso, pasamos directamente a trabajar con los textos segmentados

# cargo el primer documento del corpus segmentado en palabras
palabras = cess_esp.words(cess_esp.fileids()[0])
print(palabras[:50])

print("----------------------")

# y segmentado en oraciones
oraciones = cess_esp.sents(cess_esp.fileids()[0])
print(oraciones[:5])


['El', 'grupo', 'estatal', 'Electricité_de_France', '-Fpa-', 'EDF', '-Fpt-', 'anunció', 'hoy', ',', 'jueves', ',', 'la', 'compra', 'del', '51_por_ciento', 'de', 'la', 'empresa', 'mexicana', 'Electricidad_Águila_de_Altamira', '-Fpa-', 'EAA', '-Fpt-', ',', 'creada', 'por', 'el', 'japonés', 'Mitsubishi_Corporation', 'para', 'poner_en_marcha', 'una', 'central', 'de', 'gas', 'de', '495', 'megavatios', '.', 'Una', 'portavoz', 'de', 'EDF', 'explicó', 'a', 'EFE', 'que', 'el', 'proyecto']
----------------------
[['El', 'grupo', 'estatal', 'Electricité_de_France', '-Fpa-', 'EDF', '-Fpt-', 'anunció', 'hoy', ',', 'jueves', ',', 'la', 'compra', 'del', '51_por_ciento', 'de', 'la', 'empresa', 'mexicana', 'Electricidad_Águila_de_Altamira', '-Fpa-', 'EAA', '-Fpt-', ',', 'creada', 'por', 'el', 'japonés', 'Mitsubishi_Corporation', 'para', 'poner_en_marcha', 'una', 'central', 'de', 'gas', 'de', '495', 'megavatios', '.'], ['Una', 'portavoz', 'de', 'EDF', 'explicó', 'a', 'EFE', 'que', 'el', 'proyecto', 'para', 'la', 'construcción', 'de', 'Altamira_2', ',', 'al', 'norte', 'de', 'Tampico', ',', 'prevé', 'la', 'utilización', 'de', 'gas', 'natural', 'como', 'combustible', 'principal', 'en', 'una', 'central', 'de', 'ciclo', 'combinado', 'que', 'debe', 'empezar', 'a', 'funcionar', 'en', 'mayo_del_2002', '.'], ['La', 'electricidad', 'producida', 'pasará', 'a', 'la', 'red', 'eléctrica', 'pública', 'de', 'México', 'en_virtud_de', 'un', 'acuerdo', 'de', 'venta', 'de', 'energía', 'de', 'EAA', 'con', 'la', 'Comisión_Federal_de_Electricidad', '-Fpa-', 'CFE', '-Fpt-', 'por', 'una', 'duración', 'de', '25', 'años', '.'], ['EDF', ',', 'que', 'no', 'quiso', 'revelar', 'cuánto', '*0*', 'pagó', 'por', 'su', 'participación', 'mayoritaria', 'en', 'EAA', ',', 'intervendrá', 'como', 'asistente', 'en', 'la', 'construcción', 'de', 'Altamira_2', 'y', ',', 'posteriormente', ',', '*0*', 'se', 'encargará', 'de', 'explotarla', 'como', 'principal', 'accionista', '.'], ['EDF', 'y', 'Mitsubishi', 'participaron', 'en', '1998', 'en', 'la', 'licitación', 'de', 'licencias', 'para', 'construir', 'centrales', 'eléctricas', 'en', 'México', 'y', '*0*', 'se', 'quedaron', 'con', 'dos', 'cada', 'una', ':', 'Río_Bravo', 'y', 'Saltillo', 'para', 'la', 'compañía', 'francesa', 'y', 'Altamira', 'y', 'Tuxpán', 'para', 'la', 'japonesa', '.']]

De manera similar a como hemos hecho sacando estadísticias de las obras disponibles en el corpus gutenberg, vamos a calcular la longitud promedio de palabras y el número de palabras promedio por oración, para los diez primeros documentos de este corpus cess_esp.

Fíjate en la estructura de este ejemplo: contiene bucles anidados.


In [11]:
# para cada documento que está entre los 10 primeros del corpus
for documento in cess_esp.fileids()[:10]:    
    # carga el texto segmentado en palabras
    palabras = cess_esp.words(documento)
    # y en oraciones
    oraciones = cess_esp.sents(documento)
    
    # pon el contador de caracteres a 0
    caracteres = 0    
    # para cada palabra dentro de la lista de palabras del documento
    for palabra in palabras:
        # ve sumando al contador el número de caracteres que tiene la palabra en cuestión
        caracteres = caracteres + len(palabra)
    
    # cuando hayas terminado, divide la longitud total del texto entre el número de palabras
    longitud_promedio = caracteres / len(palabras)
    
    # imprime el nombre del documento, la longitud de la palabra y el número de palabras por oración
    print(documento[:-4], "\t", round(longitud_promedio, 2), "\t", round(len(palabras)/len(oraciones), 2))


10017_20000413 	 5.04 	 42.17
10044_20000313 	 4.27 	 47.62
10049_20001114 	 5.84 	 31.11
10055_20000713 	 4.98 	 30.89
10080_20000914 	 4.6 	 34.0
10084_20000313_1 	 4.54 	 42.36
10084_20000313_2 	 4.71 	 24.0
10127_20001013_1 	 4.63 	 40.1
10127_20001013_2 	 4.71 	 32.1
10127_20001013_3 	 4.72 	 35.29

Los libros del Proyecto Gutenberg constituyen el tipo de corpus más sencillo: no está anotado (no incluye ningún tipo de información lingüística) ni categorizado.

Corpus categorizados y anotados: el Corpus de Brown

El Corpus de Brown fue el primer gran corpus orientado a tareas de PLN. Desarrollado en la Universidad de Brown, contiene más de un millón de palabras provenientes de 500 fuentes. La principal catacterística de este corpus es que sus textos están categorizados por género.


In [12]:
from nltk.corpus import brown

Como en los libros del Proyecto Gutenberg, aquí también podemos imprimir los nombres de los ficheros. En este caso son poco significativos, nos nos dicen nada del contenido.


In [13]:
# Brown está formado por 500 documentos
print(len(brown.fileids()))
# imprimimos solos los 10 primeros
print(brown.fileids()[:10])


500
['ca01', 'ca02', 'ca03', 'ca04', 'ca05', 'ca06', 'ca07', 'ca08', 'ca09', 'ca10']

El corpus de Brown está categorizado por géneros

Una de las principales diferencias con otros corpus vistos anteriormente es que el de Brown está categorizado: los textos están agrupados según su género o temática. Y en este caso, los nombres de las categorías sí nos permiten intuir el contenido de los textos.


In [14]:
print(brown.categories())


['adventure', 'belles_lettres', 'editorial', 'fiction', 'government', 'hobbies', 'humor', 'learned', 'lore', 'mystery', 'news', 'religion', 'reviews', 'romance', 'science_fiction']

De manera similar a los libros del Proyecto Gutenberg, podemos acceder a los textos de este corpus a través de los métodos brown.raw, brown.words, brown.sents y brown.paras. Además, podemos acceder a una categoría de textos concretas si lo especificamos como argumento.


In [15]:
news_words = brown.words(categories="news")
scifi_sents = brown.sents(categories="science_fiction")

In [16]:
print(news_words[:50])
print("-----------------------")
print(scifi_sents[:3])


['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', 'Friday', 'an', 'investigation', 'of', "Atlanta's", 'recent', 'primary', 'election', 'produced', '``', 'no', 'evidence', "''", 'that', 'any', 'irregularities', 'took', 'place', '.', 'The', 'jury', 'further', 'said', 'in', 'term-end', 'presentments', 'that', 'the', 'City', 'Executive', 'Committee', ',', 'which', 'had', 'over-all', 'charge', 'of', 'the', 'election', ',', '``', 'deserves', 'the', 'praise']
-----------------------
[['Now', 'that', 'he', 'knew', 'himself', 'to', 'be', 'self', 'he', 'was', 'free', 'to', 'grok', 'ever', 'closer', 'to', 'his', 'brothers', ',', 'merge', 'without', 'let', '.'], ["Self's", 'integrity', 'was', 'and', 'is', 'and', 'ever', 'had', 'been', '.'], ['Mike', 'stopped', 'to', 'cherish', 'all', 'his', 'brother', 'selves', ',', 'the', 'many', 'threes-fulfilled', 'on', 'Mars', ',', 'corporate', 'and', 'discorporate', ',', 'the', 'precious', 'few', 'on', 'Earth', '--', 'the', 'unknown', 'powers', 'of', 'three', 'on', 'Earth', 'that', 'would', 'be', 'his', 'to', 'merge', 'with', 'and', 'cherish', 'now', 'that', 'at', 'last', 'long', 'waiting', 'he', 'grokked', 'and', 'cherished', 'himself', '.']]

Vamos a sacar provecho de la categorización de los textos de este corpus. Para ello, vamos a calcular la frecuencia de distribución de distintos verbos modales para cada categoría. Para ello vamos a calcular una distribución de frecuencia condicional que calcule la frecuencia de cada palabra para cada categoría.

No te preocupes si no entiendes la sintaxis para crear tablas de frecuencias condicionales a través del objeto ConditionalFreqDist. Créeme, ese objeto calcula frecuencias de palabras atendiendo a la categoría en la que aparecen y crea una especie de diccionario de diccionarios.


In [17]:
from nltk import ConditionalFreqDist
modals = "can could would should must may might".split()
modals_cfd = ConditionalFreqDist(
                          (category, word) 
                          for category in brown.categories() 
                          for word in brown.words(categories=category)
                        )

# la sintaxis anterior de anidar bucles for es un poco compleja y no la hemos visto
# sin necesidad de profundizar más, las últimas tres líneas son equivalentes a:
# for category in brown.categories():
#     for word in brown.words(categories=category):
#         ConditionalFreqDist(category, word)

Una vez tenemos calculada la frecuencia de distribución condicional, podemos pintar los valores fácilmente en forma de tabla a través del método .tabulate, especificando como condiciones cada una de las categorías, y como muestras los verbos modales del inglés que hemos definido.


In [18]:
modals_cfd.tabulate(conditions=brown.categories(), samples=modals)

# imprimo solo algunos verbos modales para la categoría fiction
modals_cfd.tabulate(conditions=["fiction"], samples=["can", "should", "would"])


                 can could would should must  may might 
      adventure   46  151  191   15   27    5   58 
 belles_lettres  246  213  392  102  170  207  113 
      editorial  121   56  180   88   53   74   39 
        fiction   37  166  287   35   55    8   44 
     government  117   38  120  112  102  153   13 
        hobbies  268   58   78   73   83  131   22 
          humor   16   30   56    7    9    8    8 
        learned  365  159  319  171  202  324  128 
           lore  170  141  186   76   96  165   49 
        mystery   42  141  186   29   30   13   57 
           news   93   86  244   59   50   66   38 
       religion   82   59   68   45   54   78   12 
        reviews   45   40   47   18   19   45   26 
        romance   74  193  244   32   45   11   51 
science_fiction   16   49   79    3    8    4   12 
         can should would 
fiction   37   35  287 

Las cifras que hemos mostrado en las tablas anteriores se refieren a las frecuencias absolutas de cada verbo modal en cada categoría. Realizar comparaciones así no es acertado, porque es posible que cada categoría tenga un número de documentos (y de palabras) diferente.

Vamos a comprobar si esto es cierto. ¿Está equilibrada la colección o tenemos algunos géneros sobrerrepresentados?


In [19]:
for categoria in brown.categories():
    print(categoria, len(brown.words(categories=categoria)))


adventure 69342
belles_lettres 173096
editorial 61604
fiction 68488
government 70117
hobbies 82345
humor 21695
learned 181888
lore 110299
mystery 57169
news 100554
religion 39399
reviews 40704
romance 70022
science_fiction 14470

Como vemos, el número de palabras no está equilibado. Tenemos muchos más datos en las categorías belles_lettres y learned que en science_fiction o humor, por ejemplo.

Calculemos a continuación la frecuencia relativa de estos verbos modales, atendiendo al género. Para ello, necesitamos dividir la frecuencia absoluta de cada modal entre el número de palabras total de cada categoría.


In [20]:
for categoria in brown.categories():
    # ¿cuántas palabras tenemos en cada categoría?
    longitud = len(brown.words(categories=categoria))
    print("\n", categoria)
    print("----------------------")
    for palabra in modals:
        print(palabra, "->", modals_cfd[categoria][palabra]/longitud)


 adventure
----------------------
can -> 0.0006633786161345216
could -> 0.002177612413832886
would -> 0.0027544633843846443
should -> 0.00021631911395690923
must -> 0.00038937440512243663
may -> 7.210637131896974e-05
might -> 0.000836433907300049

 belles_lettres
----------------------
can -> 0.0014211766880806026
could -> 0.0012305310348014974
would -> 0.002264639275315432
should -> 0.0005892683828626889
must -> 0.000982113971437815
may -> 0.001195868188750751
might -> 0.000652816933955724

 editorial
----------------------
can -> 0.001964158171547302
could -> 0.0009090318810466853
would -> 0.0029218881890786313
should -> 0.0014284786702162197
must -> 0.0008603337445620414
may -> 0.0012012206999545483
might -> 0.0006330757743003701

 fiction
----------------------
can -> 0.0005402406260950823
could -> 0.002423782268426586
would -> 0.004190515126737531
should -> 0.0005110384300899427
must -> 0.0008030603901413386
may -> 0.00011680878402055835
might -> 0.000642448312113071

 government
----------------------
can -> 0.0016686395595932513
could -> 0.0005419513099533637
would -> 0.0017114251893264115
should -> 0.0015973301767046508
must -> 0.0014547114109274499
may -> 0.0021820671163911747
might -> 0.00018540439551036124

 hobbies
----------------------
can -> 0.0032545995506709576
could -> 0.0007043536341004311
would -> 0.0009472341975833384
should -> 0.0008865140567126115
must -> 0.0010079543384540653
may -> 0.0015908676908130428
might -> 0.000267168619831198

 humor
----------------------
can -> 0.0007374971191518783
could -> 0.0013828070984097719
would -> 0.0025812399170315743
should -> 0.0003226549896289468
must -> 0.00041484212952293156
may -> 0.00036874855957593915
might -> 0.00036874855957593915

 learned
----------------------
can -> 0.002006729415904293
could -> 0.0008741643209007741
would -> 0.001753826530612245
should -> 0.0009401389866291344
must -> 0.0011105735397607319
may -> 0.0017813159746657284
might -> 0.0007037297677691766

 lore
----------------------
can -> 0.0015412651066646116
could -> 0.0012783434119982956
would -> 0.0016863253519977515
should -> 0.0006890361653324146
must -> 0.0008703614719988395
may -> 0.0014959337799980055
might -> 0.000444247001332741

 mystery
----------------------
can -> 0.0007346638912697441
could -> 0.002466371634976998
would -> 0.0032535115184802953
should -> 0.0005072679249243471
must -> 0.0005247599223355315
may -> 0.00022739596634539699
might -> 0.0009970438524375099

 news
----------------------
can -> 0.0009248761859299481
could -> 0.0008552618493545757
would -> 0.002426556874912982
should -> 0.0005867494082781391
must -> 0.0004972452612526602
may -> 0.0006563637448535115
might -> 0.0003779063985520218

 religion
----------------------
can -> 0.002081271098251225
could -> 0.0014974999365466128
would -> 0.0017259321302571132
should -> 0.0011421609685525014
must -> 0.0013705931622630017
may -> 0.0019797456788243355
might -> 0.000304576258280667

 reviews
----------------------
can -> 0.0011055424528301887
could -> 0.0009827044025157233
would -> 0.0011546776729559748
should -> 0.0004422169811320755
must -> 0.00046678459119496856
may -> 0.0011055424528301887
might -> 0.0006387578616352201

 romance
----------------------
can -> 0.0010568107166319157
could -> 0.0027562765987832393
would -> 0.0034846191197052353
should -> 0.0004569992288138014
must -> 0.0006426551655194082
may -> 0.00015709348490474423
might -> 0.000728342520921996

 science_fiction
----------------------
can -> 0.0011057360055286801
could -> 0.0033863165169315825
would -> 0.005459571527297858
should -> 0.0002073255010366275
must -> 0.0005528680027643401
may -> 0.00027643400138217003
might -> 0.00082930200414651

Vamos a repetir la operación de cálculo de frecuencias relativas reasignando estos valores en el propio objeto modals_cfd, con el objetivo de utilizar el método tabulate para poder impirmir la tabla con los valors relativos.


In [21]:
# lo primero, realizo una copia de mi distribución de frecuencias 
import copy
modals_cfd_rel = copy.deepcopy(modals_cfd)

In [22]:
# sustituyo los conteos de la tabla por sus frecuencias relativas (ojo, en tantos por 10.000)
for categoria in brown.categories():
    longitud = len(brown.words(categories=categoria))
    for palabra in modals:
        modals_cfd_rel[categoria][palabra] = (modals_cfd[categoria][palabra]/longitud)*10000

In [23]:
# imprimo la tabla        
modals_cfd_rel.tabulate(conditions=brown.categories(), samples=modals)


                 can could would should must  may might 
      adventure    6   21   27    2    3    0    8 
 belles_lettres   14   12   22    5    9   11    6 
      editorial   19    9   29   14    8   12    6 
        fiction    5   24   41    5    8    1    6 
     government   16    5   17   15   14   21    1 
        hobbies   32    7    9    8   10   15    2 
          humor    7   13   25    3    4    3    3 
        learned   20    8   17    9   11   17    7 
           lore   15   12   16    6    8   14    4 
        mystery    7   24   32    5    5    2    9 
           news    9    8   24    5    4    6    3 
       religion   20   14   17   11   13   19    3 
        reviews   11    9   11    4    4   11    6 
        romance   10   27   34    4    6    1    7 
science_fiction   11   33   54    2    5    2    8 

Brown es también un corpus anotado con información morfológica

El corpus de Brown no solo está categorizado, también está anotado con información morfológica. Para acceder a la versión anotada del corpus, podemos utilizar los métodos: brown.tagged_words, brown.tagged_sents y brown.tagged_ paras


In [24]:
scifi_tagged_words = brown.tagged_words(categories="science_fiction")
print(scifi_tagged_words[:20])


[('Now', 'RB'), ('that', 'CS'), ('he', 'PPS'), ('knew', 'VBD'), ('himself', 'PPL'), ('to', 'TO'), ('be', 'BE'), ('self', 'NN'), ('he', 'PPS'), ('was', 'BEDZ'), ('free', 'JJ'), ('to', 'TO'), ('grok', 'VB'), ('ever', 'QL'), ('closer', 'RBR'), ('to', 'IN'), ('his', 'PP$'), ('brothers', 'NNS'), (',', ','), ('merge', 'VB')]

Fíjate que cuando accedemos a la versión etiquetada del corpus, no obtenemos una simple lista de palabras sino una lista de tuplas, donde el primer elemento es la palabra en cuestión u el segundo es la etiqueta que indica la categoría gramatical de la palabra.

Este conjunto etiquetas se ha convertido en casi un estándar para el inglés y se utilizan habitualmente para anotar cualquier recurso lingüístico en esa lengua.

Vamos a crear una nueva frecuencia de distribución condicional para calcular la frecuencia de aparición de las etiquetas, teniendo en cuenta la categoría.


In [25]:
tags_cfd = ConditionalFreqDist(
                                (category, item[1])
                                for category in brown.categories()
                                for item in brown.tagged_words(categories=category)
                              )

Y ahora vamos a imprimir la tabla de frecuencias para cada categoría y para algunas de las etiquetas morfológicas: sustantivos en singular NN, verbos en presente VB, verbos en pasado simple VBD, participios pasados VBN, adjetivos JJ, preposiciones IN, y artículos AT.

Recuerda: estas cifras no son directamente comparables entre categorías ya que éstas no están equilibradas. Hay categorías con más textos que otras.


In [26]:
tags_cfd.tabulate(conditions=brown.categories(), 
                  samples="NN VB VBD VBN JJ IN AT".split())


                  NN   VB  VBD  VBN   JJ   IN   AT 
      adventure 8051 2170 3702 1276 2687 5908 5531 
 belles_lettres 21800 4829 3501 4223 10414 19083 14898 
      editorial 7675 2129  700 1491 3593 6204 5311 
        fiction 7815 2173 3027 1497 2958 6012 5439 
     government 9877 1833  405 2190 4173 8596 5716 
        hobbies 12465 2966  617 2252 4883 8591 6946 
          humor 2567  656  699  478 1078 1926 1655 
        learned 29194 4342 1481 6044 12294 21757 16828 
           lore 14707 3083 2272 2822 6475 12074 9936 
        mystery 6461 2026 2645 1161 2109 4692 4321 
           news 13162 2440 2524 2269 4392 10616 8893 
       religion 4923 1275  511  931 2327 4266 3327 
        reviews 5066  872  504  875 2742 4040 3447 
        romance 7166 2404 3048 1359 3180 5616 4671 
science_fiction 1541  495  531  318  723 1176 1040 

Veamos otro ejemplo: creamos una lista de adjetivos (etiquetados como JJ) que aparezcan en la colección de textos sobre hobbies, e imprimirmos los 50 primeros que encontramos


In [27]:
# creo una lista vacía de adjetivos
adjetivos = []

# itero sobre las tuplas de la categorías hobbies
for tupla in brown.tagged_words(categories="hobbies"):
    # compruebo que son adjetivos
    if tupla[1] == "JJ":
        # guardo la palabra en cuestión en mis lista de adjetivos
        adjetivos.append(tupla[0])
        
# hay bastantes 
print(len(adjetivos))  
# así que, solo imprimo solo los 50 primeros
print(adjetivos[:50])


4883
['old', 'childish', 'genuine', 'happy', 'unkind', 'happy', 'sunny', 'new', 'fast', 'fast', 'good', 'famous', 'prize-winning', 'tall', 'astounding', 'famous', 'fast', 'wonder-working', 'crazy', 'vigorous', 'solid', 'skinny', 'shapely', 'upper', 'muscular', 'symmetrical', 'real', 'now-famous', 'specific', 'famous', 'upper', 'collar-to-collar', 'wide', 'Reeves-type', 'upper', 'frontal', 'entire', 'chest-back-shoulder', 'alternate', 'alternate', 'complete', 'complete', 'five-minute', 'complete', 'similar', 'flat', 'downward', 'possible', 'upper', 'true']

Otro ejempo: Para cada categoría del corpus de Brown, imprimimos solo aquellos adjetivos que tengan una longitud de al menos 15 caracteres y que no sean palabras compuestas escritas con guiones ortográficos.


In [28]:
# en este caso, creo un diccionario vacío
adjetivos = {}

# itero sobre las categorías
for categoria in brown.categories():
    for elemento in brown.tagged_words():
        if elemento[1] == "JJ":
            if len(elemento[0]) >= 15:
                if "-" not in elemento[0]:
                    # cuando encuentro una palabra que cumple las tres condiciones, la almacenos en mi diccionario de adjetivos
                    adjetivos[elemento[0]] = 1

# por último, imprimo las claves de mi diccionario
print(adjetivos.keys())


dict_keys(['Crystallographic', 'inconsequential', 'distinguishable', 'impressionistic', 'anthropomorphic', 'polycrystalline', 'unsophisticated', 'internationalist', 'interchangeable', 'intercollegiate', 'neuropsychiatric', 'glottochronological', 'unconstitutional', 'communicational', 'micrometeoritic', 'Thermogravimetric', 'extraterrestrial', 'cathodoluminescent', 'pharmacological', 'multidimensional', 'semiquantitative', 'intergovernmental', 'autobiographical', 'lexicostatistic', 'intraepithelial', 'interdenominational', 'intradepartmental', 'traditionalistic', 'intercontinental', 'phenomenological', 'underprivileged', 'chromatographic', 'uncommunicative', 'bibliographical', 'anthropological', 'nonmythological', 'particularistic', 'nondiscriminatory', 'straightforward', 'incomprehensible', 'psychotherapeutic', 'crystallographic', 'disproportionate', 'psychopharmacological', 'noncommissioned', 'encephalographic', 'indistinguishable', 'photoelectronic', 'unreconstructed', 'undifferentiated', 'cathodophoretic', 'substitutionary', 'substerilization', 'undistinguished', 'incontrovertible', 'trichloroacetic', 'indiscriminating', 'expressionistic', 'macropathological', 'spectrophotometric', 'interdepartmental', 'nonagricultural', 'Physicochemical', 'parasympathetic', 'individualistic', 'gastrointestinal'])

Recursos Léxicos: WordNet

Wordnet es una red semántica para el inglés. En esencia, es similar a un diccionario pero está organizado por synsets (conjunto de palabras sinónimas) y no por lemas.

Podemos acceder a WordNet a través de NLTK, de manera similar a como accedemos desde el interfaz web:


In [29]:
from nltk.corpus import wordnet as wn

Para consultar los synsets en los que aparece una determinada palabra, podemos utilizar el método .synsets como se muestra en el ejemplo. Como resultado obtenemos una lista con todos los synsets en los que aparece la palabra.


In [30]:
# buscamos los synsets en los que aparece la palabra sword
print(wn.synsets("sword"))

# y buscamos car
print(wn.synsets("car"))


[Synset('sword.n.01')]
[Synset('car.n.01'), Synset('car.n.02'), Synset('car.n.03'), Synset('car.n.04'), Synset('cable_car.n.01')]

En este caso, la palabra sword solo aparece en un synset, lo que implica que solo tiene un sentido. Además, sabemos que es un sustantivo, porque el nombre de synset está etiquetado como n.

Por su parte, la palabra car es polisémica y aparece en cinco sentidos, toso ellos sustantivos.

Si guardo el synset en cuestión en una variable (fíjate que me quedo con el primer elemento de la lista que me devuelve el método wn.synsets), podemos acceder a distintos métodos:


In [31]:
sword = wn.synsets("sword")[0]
print(sword.lemma_names()) # imprime los lemas del synset => sinónimos
print(sword.definition()) # imprime la definición del synset

# hacemos lo mismo con car
car = wn.synsets("car")
cable_car = car[-1]
print(cable_car.lemma_names(), cable_car.definition())
print(car[-1].lemma_names(), car[-1].definition()) # esta línea es equivalente a la anterior. ¿Ves por qué?

# imprimo las oraciones de ejemplo
print(cable_car.examples())


['sword', 'blade', 'brand', 'steel']
a cutting or thrusting weapon that has a long metal blade and a hilt with a hand guard
['cable_car', 'car'] a conveyance for passengers or freight on a cable railway
['cable_car', 'car'] a conveyance for passengers or freight on a cable railway
['they took a cable car to the top of the mountain']

Si escribes sword. y pulsas el tabulador podrás visualizar todos los métodos accesibles desde un objeto synset. Son muchos: si tienes interés en alguno que no se menciona en este resumen, pregúntame o consulta el libro de NLTK.

Entre las cosas que sí nos interesan está el poder acceder a relaciones como hiponimia, meronimia, etc. Por ejemplo, para acceder a todos los hipónimos de sword con el sentido de espada, es decir, a todos los tipos de espada y a sus definiciones.


In [32]:
print(sword.hyponyms())

for element in sword.hyponyms():
    print(element.lemma_names())
    print(element.definition())


[Synset('backsword.n.02'), Synset('broadsword.n.01'), Synset('cavalry_sword.n.01'), Synset('cutlas.n.01'), Synset('falchion.n.01'), Synset('fencing_sword.n.01'), Synset('rapier.n.01')]
['backsword']
a sword with only one cutting edge
['broadsword']
a sword with a broad blade and (usually) two cutting edges; used to cut rather than stab
['cavalry_sword', 'saber', 'sabre']
a stout sword with a curved blade and thick back
['cutlas', 'cutlass']
a short heavy curved sword with one edge; formerly used by sailors
['falchion']
a short broad slightly convex medieval sword with a sharp point
['fencing_sword']
a sword used in the sport of fencing
['rapier', 'tuck']
a straight sword with a narrow blade and two edges

En lugar de bajar hasta los elementos más específicos, podemos navegar en la jerarquía de sentidos hasta los synsets más generales. Por ejemplo, podemos acceder a los hiperónimos inmediatos de un synset a través del método .hypernyms():


In [33]:
for element in sword.hypernyms():
    print(element.lemma_names())
    print(element.definition())


['weapon', 'arm', 'weapon_system']
any instrument or instrumentality used in fighting or hunting

Fíjate que con este método sólo subimos un nivel hacia el synset más general. En este caso, comprobamos que sword es un tipo de weapon o arm. Si, por el contrario, lo que nos interesa es acceder a todos los hiperónimos de sword, navegando hasta el elemento raíz de la jerarquía de WordNet (que siempre es entity), podemos utilizar el método .hypernym_paths():


In [34]:
for path in sword.hypernym_paths():
    for element in path:
        print(element.lemma_names())
        print(element.definition())


['entity']
that which is perceived or known or inferred to have its own distinct existence (living or nonliving)
['physical_entity']
an entity that has physical existence
['object', 'physical_object']
a tangible and visible entity; an entity that can cast a shadow
['whole', 'unit']
an assemblage of parts that is regarded as a single entity
['artifact', 'artefact']
a man-made object taken as a whole
['instrumentality', 'instrumentation']
an artifact (or system of artifacts) that is instrumental in accomplishing some end
['device']
an instrumentality invented for a particular purpose
['instrument']
a device that requires skill for proper use
['weapon', 'arm', 'weapon_system']
any instrument or instrumentality used in fighting or hunting
['sword', 'blade', 'brand', 'steel']
a cutting or thrusting weapon that has a long metal blade and a hilt with a hand guard

Fíjate que .hypernym_paths() me devuelve una lista de caminos posibles desde el synset en cuestión hasta el elemento entity. Por eso itero sobre los elementos path que me devuelve .hypernym_paths(). En el ejemplo de sword, da la casualidad de que solo hay un camino posible. Cada path es una lista de synsets, e itero sobre ellos. Por eso utilizo un bucle dentro de otro.

Para acceder a los merónimos, es decir, a las partes o elementos constitutivos de sword, podemos utilizar el método .part_meronyms(), como se muestra a continuación.


In [35]:
for element in sword.part_meronyms():
    print(element.lemma_names())
    print(element.definition())


['blade']
the flat part of a tool or weapon that (usually) has a cutting edge
['foible']
the weaker part of a sword's blade from the forte to the tip
['forte']
the stronger part of a sword blade between the hilt and the foible
['haft', 'helve']
the handle of a weapon or tool
['hilt']
the handle of a sword or dagger
['point', 'tip', 'peak']
a V shape

De manera similar, podemos acceder a los holónimos de un synset, es decir, a los elementos de los que espada forma parte, a través del método .part_holonyms(). El synset que estamos utilizando no tiene definidos holónimos, así que el ejemplo devuelve una lista vacía.


In [36]:
print(sword.part_holonyms())


[]

Busquemos ahora algún ejemplo que tenga otros tipos de merónimos.


In [37]:
# en cuántos synsets aparece la palabra water?
water = wn.synsets("water")
for synset in water:
    print(synset.lemma_names())

# me quedo con el primero
agua = water[0]
# que tiene unos cuantos merónimos de sustancia
print(agua.substance_meronyms())

# ídem para air
air = wn.synsets("air")
for synset in air:
    print(synset.lemma_names(), synset.definition())

aire = air[0]
print(aire.substance_meronyms())


['water', 'H2O']
['body_of_water', 'water']
['water']
['water_system', 'water_supply', 'water']
['urine', 'piss', 'pee', 'piddle', 'weewee', 'water']
['water']
['water', 'irrigate']
['water']
['water']
['water']
[Synset('hydrogen.n.01'), Synset('oxygen.n.01')]
['air'] a mixture of gases (especially oxygen) required for breathing; the stuff that the wind consists of
['air'] the region above the ground
['air', 'aura', 'atmosphere'] a distinctive but intangible quality surrounding a person or thing
['breeze', 'zephyr', 'gentle_wind', 'air'] a slight wind (usually refreshing)
['atmosphere', 'air'] the mass of air surrounding the Earth
['air'] once thought to be one of four elements composing the universe (Empedocles)
['tune', 'melody', 'air', 'strain', 'melodic_line', 'line', 'melodic_phrase'] a succession of notes forming a distinctive sequence
['air', 'airwave'] medium for radio and television broadcasting
['air_travel', 'aviation', 'air'] travel via aircraft
['air_out', 'air', 'aerate'] expose to fresh air
['air'] be broadcast
['air', 'send', 'broadcast', 'beam', 'transmit'] broadcast over the airwaves, as in radio or television
['publicize', 'publicise', 'air', 'bare'] make public
['air'] expose to warm or heated air, so as to dry
['vent', 'ventilate', 'air_out', 'air'] expose to cool or cold air so as to cool or freshen
[Synset('argon.n.01'), Synset('krypton.n.01'), Synset('neon.n.01'), Synset('nitrogen.n.01'), Synset('oxygen.n.01'), Synset('xenon.n.01')]

Hay varios métodos para acceder a distintos tipos de merónimos y holónimos, aunque no siempre están definidas estas relaciones. Cuando no están definidas, los métodos no dan error, simplemente devuelven listas vacías.

Los nombres de estos métodos tratan de ser autoexplicativos: por un lado, tenemos .part_holonyms(), .member_holonyms(), .substance_holonyms(), y por otro, .part_meronyms(), .member_meronyms(), .substance_meronyms().

Pequeño ejercicio

  • Busca los sentidos en los que aparece la palabra bike.
  • Identifica el que hace referencia a bicicleta: vehículo de dos ruedas a pedales
  • Imprime los merónimos, las partes que conforman una bicicleta

In [38]:
bike_synsets = wn.synsets("bike")
for s in bike_synsets:
    print(s.pos(), ":", s.definition())
    
print("merónimos de", bike_synsets[1].definition()) 
for meronym in bike_synsets[1].part_meronyms():
    print(meronym.lemma_names())

print("----------------------------")
    
print("hipónimos de", bike_synsets[1].definition())
for hipo in bike_synsets[1].hyponyms():
    print(hipo.lemma_names())


n : a motor vehicle with two wheels and a strong frame
n : a wheeled vehicle that has two wheels and is moved by foot pedals
v : ride a bicycle
merónimos de a wheeled vehicle that has two wheels and is moved by foot pedals
['bicycle_seat', 'saddle']
['bicycle_wheel']
['chain']
['coaster_brake']
['handlebar']
['kickstand']
['mudguard', 'splash_guard', 'splash-guard']
['pedal', 'treadle', 'foot_pedal', 'foot_lever']
['sprocket', 'sprocket_wheel']
----------------------------
hipónimos de a wheeled vehicle that has two wheels and is moved by foot pedals
['bicycle-built-for-two', 'tandem_bicycle', 'tandem']
['mountain_bike', 'all-terrain_bike', 'off-roader']
['ordinary', 'ordinary_bicycle']
['push-bike']
['safety_bicycle', 'safety_bike']
['velocipede']