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.
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
.
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
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.# 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
# 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']
# 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
# 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', '.']]
# 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.
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.
# 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.
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.
# 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.
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.
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.
# 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']
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.
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.
news_words = brown.words(categories="news")
scifi_sents = brown.sents(categories="science_fiction")
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.
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.
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?
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.
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.
# lo primero, realizo una copia de mi distribución de frecuencias
import copy
modals_cfd_rel = copy.deepcopy(modals_cfd)
# 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
# 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
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
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.
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.
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
# 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.
# 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(['straightforward', 'intergovernmental', 'indistinguishable', 'interdenominational', 'underprivileged', 'interchangeable', 'neuropsychiatric', 'Physicochemical', 'uncommunicative', 'intradepartmental', 'intercollegiate', 'anthropological', 'intraepithelial', 'incontrovertible', 'disproportionate', 'bibliographical', 'noncommissioned', 'anthropomorphic', 'communicational', 'encephalographic', 'nondiscriminatory', 'crystallographic', 'incomprehensible', 'nonagricultural', 'parasympathetic', 'photoelectronic', 'substitutionary', 'lexicostatistic', 'unreconstructed', 'interdepartmental', 'semiquantitative', 'phenomenological', 'macropathological', 'spectrophotometric', 'Crystallographic', 'Thermogravimetric', 'impressionistic', 'inconsequential', 'psychotherapeutic', 'distinguishable', 'intercontinental', 'undistinguished', 'cathodoluminescent', 'extraterrestrial', 'glottochronological', 'micrometeoritic', 'trichloroacetic', 'chromatographic', 'undifferentiated', 'internationalist', 'unconstitutional', 'traditionalistic', 'gastrointestinal', 'particularistic', 'psychopharmacological', 'pharmacological', 'indiscriminating', 'cathodophoretic', 'individualistic', 'substerilization', 'multidimensional', 'autobiographical', 'nonmythological', 'polycrystalline', 'expressionistic', 'unsophisticated'])
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:
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.
# 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:
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.
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()
:
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()
:
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.
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.
print(sword.part_holonyms())
[]
Busquemos ahora algún ejemplo que tenga otros tipos de merónimos.
# 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()
.
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']