Procesamiento del Lenguaje Natural con Python

Esta notebook fue creada originalmente como un blog post por Raúl E. López Briega en Matemáticas, Analisis de datos y Python. El contenido esta bajo la licencia BSD.

"El lenguaje sirve no sólo para expresar el pensamiento, sino para hacer posibles pensamientos que no podrían existir sin él."

Bertrand Russell

Introducción

El lenguaje es una de las herramientas centrales en nuestra vida social y profesional. Entre otras cosas, actúa como un medio para transmitir ideas, información, opiniones y sentimientos; así como para persuadir, pedir información, o dar ordenes. Asimismo, el lenguaje humano es algo que esta en constante cambio y evolución; y que puede llegar a ser muy ambiguo y variable. Tomemos por ejemplo la frase "comí una pizza con amigos" comparada con "comí una pizza con aceitunas"; su estructura es la misma, pero su significado es totalmente distinto. De la misma manera, un mismo mensaje puede ser expresado de formas diferentes; "comí una pizza con amigos" puede también ser expresado como "compartí una pizza con amigos".

Los seres humanos somos muy buenos a la hora de producir e interpretar el lenguaje, podemos expresar, percibir e interpretar significados muy elaborados en fracción de segundos casi sin dificultades; pero al mismo tiempo, somos también muy malos a la hora de entender y describir formalmente las reglas que lo gobiernan. Por este motivo, entender y producir el lenguaje por medio de una computadora es un problema muy difícil de resolver. Éste problema, es el campo de estudio de lo que en inteligencia artificial se conoce como Procesamiento del Lenguaje Natural o NLP por sus siglas en inglés.

¿Qué es el Procesamiento del Lenguaje Natural?

El Procesamiento del Lenguaje Natural o NLP es una disciplina que se encuentra en la intersección de varias ciencias, tales como las Ciencias de la Computación, la Inteligencia Artificial y Psicología Cognitiva. Su idea central es la de darle a las máquinas la capacidad de leer y comprender los idiomas que hablamos los humanos. La investigación del Procesamiento del Lenguaje Natural tiene como objetivo responder a la pregunta de cómo las personas son capaces de comprender el significado de una oración oral / escrita y cómo las personas entienden lo que sucedió, cuándo y dónde sucedió; y las diferencias entre una suposición, una creencia o un hecho.

En general, en Procesamiento del Lenguaje Natural se utilizan seis niveles de comprensión con el objetivo de descubrir el significado del discurso. Estos niveles son:

  • Nivel fonético: Aquí se presta atención a la fonética, la forma en que las palabras son pronunciadas. Este nivel es importante cuando procesamos la palabra hablada, no así cuando trabajamos con texto escrito.

  • Nivel morfológico: Aquí nos interesa realizar un análisis morfológico del discurso; estudiar la estructura de las palabras para delimitarlas y clasificarlas.

  • Nivel sintáctico: Aquí se realiza un análisis de sintaxis, el cual incluye la acción de dividir una oración en cada uno de sus componentes.

  • Nivel semántico: Este nivel es un complemente del anterior, en el análisis semántico se busca entender el significado de la oración. Las palabras pueden tener múltiples significados, la idea es identificar el significado apropiado por medio del contexto de la oración.

  • Nivel discursivo: El nivel discursivo examina el significado de la oración en relación a otra oración en el texto o párrafo del mismo documento.

  • Nivel pragmático: Este nivel se ocupa del análisis de oraciones y cómo se usan en diferentes situaciones. Además, también cómo su significado cambia dependiendo de la situación.

Todos los niveles descritos aquí son inseparables y se complementan entre sí. El objetivo de los sistemas de NLP es incluir estas definiciones en una computadora y luego usarlas para crear una oración estructurada y sin ambigüedades con un significado bien definido.

Aplicaciones del Procesamiento del Lenguaje Natural

Los algoritmos de Procesamiento del Lenguaje Natural suelen basarse en algoritmos de aprendizaje automático. En lugar de codificar manualmente grandes conjuntos de reglas, el NLP puede confiar en el aprendizaje automático para aprender estas reglas automáticamente analizando un conjunto de ejemplos y haciendo una inferencia estadística. En general, cuanto más datos analizados, más preciso será el modelo. Estos algoritmos pueden ser utilizados en algunas de las siguientes aplicaciones:

  • Resumir texto: Podemos utilizar los modelos de NLP para extraer las ideas más importantes y centrales mientras ignoramos la información irrelevante.

  • Crear chatbots: Podemos utilizar las técnicas de NLP para crear chatbots que puedan interactuar con las personas.

  • Generar automáticamente etiquetas de palabras clave: Con NLP también podemos realizar un análisis de contenido aprovechando el algoritmo de LDA para asignar palabras claves a párrafos del texto.

  • Reconocer entidades: Con NLP podemos identificar a las distintas entidades del texto como ser una persona, lugar u organización.

  • Análisis de sentimiento: También podemos utilizar NLP para identificar el sentimiento de una cadena de texto, desde muy negativo a neutral y a muy positivo.

Librerías de Python para Procesamiento del Lenguaje Natural

Actualmente, Python es uno de los lenguajes más populares para trabajar en el campo la Inteligencia Artificial. Para abordar los problemas relacionados con el Procesamiento del Lenguaje Natural Python nos proporciona las siguientes librerías:

  • NLTK: Es la librería líder para el Procesamiento del Lenguaje Natural. Proporciona interfaces fáciles de usar a más de 50 corpus y recursos léxicos, junto con un conjunto de bibliotecas de procesamiento de texto para la clasificación, tokenización, el etiquetado, el análisis y el razonamiento semántico.
  • TextBlob: TextBlob simplifica el procesamiento de texto proporcionando una interfaz intuitiva a NLTK. Posee una suave curva de aprendizaje al mismo tiempo que cuenta con una sorprendente cantidad de funcionalidades.
  • Spacy: Es una librería relativamente nueva que sobresale por su facilidad de uso y su velocidad a la hora de realizar el procesamiento de texto.
  • Gensim: Es una librería diseñada para extraer automáticamente los temas semánticos de los documentos de la forma más eficiente y con menos complicaciones posible.
  • pyLDAvis: Esta librería está diseñado para ayudar a los usuarios a interpretar los temas que surgen de un análisis de tópicos. Nos permite visualizar en forma muy sencilla cada uno de los temas incluidos en el texto.

Como veremos, el NLP también se está sumando a la popularidad del Deep Learning, por lo que muchos de los frameworks que se utilizan en Deep Learning pueden ser aplicados para realizar modelos de NLP.

Corpus lingüístico

Hoy en día, es indispensable el uso de buenos recursos lingüísticos para el desarrollo de los sistemas de NLP. Estos recursos son esenciales para la creación de gramáticas, en el marco de aproximaciones simbólicas; o para llevar a cabo la formación de módulos basados en el aprendizaje automático.

Un corpus lingüístico es un conjunto amplio y estructurado de ejemplos reales de uso de la lengua. Estos ejemplos pueden ser textos (los más comunes), o muestras orales (generalmente transcritas). Un corpus lingüístico es un conjunto de textos relativamente grande, creado independientemente de sus posibles formas o usos. Es decir, en cuanto a su estructura, variedad y complejidad, un corpus debe reflejar una lengua, o su modalidad, de la forma más exacta posible; en cuanto a su uso, preocuparse de que su representación sea real. La idea es que representen al lenguaje de la mejor forma posible para que los modelos de NLP puedan aprender los patrones necesarios para entender el lenguaje. Encontrar un buen corpus sobre el cual trabajar no suele ser una tarea sencilla; un corpus que se suele utilizar para entrenar modelos es el que incluye la información extraída de wikipedia.

Procesamiento del Lenguaje Natural con Python

Hasta aquí la introducción, ahora llegó el momento de ensuciarse un poco las manos y comenzar a explorar algunos ejemplos de las herramientas que nos ofrece Python para trabajar con problemas de Procesamiento del Lenguaje Natural. Comencemos por ejemplo, descargando el corpus de wikipedia en español. Esto lo podemos hacer fácilmente utilizando Textacy.


In [1]:
# <!-- collapse=True -->
# Importando las librerías que vamos a utilizar
import pandas as pd
import numpy as np 
import matplotlib.pyplot as plt 
import textacy
from textacy.datasets import Wikipedia
from collections import Counter, defaultdict
import warnings; warnings.simplefilter('ignore')

# graficos incrustados
%matplotlib inline

# función auxiliar
def leer_texto(texto):
    """Funcion auxiliar para leer un archivo de texto"""
    with open(texto, 'r') as text:
        return text.read()

In [2]:
# Descargando copus de wikipedia
wp = Wikipedia(data_dir='/home/raul/Documents/data', lang='es', version='latest')
wp.download()

In [3]:
# Chequeando la información descargada
wp.info


Out[3]:
{'data_dir': '/home/raul/Documents/data',
 'description': 'All articles for a given language- and version-specific Wikipedia site snapshot.',
 'name': 'wikipedia',
 'site_url': 'https://meta.wikimedia.org/wiki/Data_dumps'}

In [4]:
for text in wp.texts(min_len=1000, limit=2):
    print(text[:375], "\n")


Andorra

Andorra, oficialmente Principado de Andorra , es un pequeño país soberano del suroeste de Europa. Constituido en Estado independiente, de derecho, democrático y social, cuya forma de gobierno es el coprincipado parlamentario. Su territorio está organizado en siete parroquias, con una población total en 2016 de 78.264 habitantes. Su capital es Andorra la Vieja.

Ti 

Argentina

La República Argentina, conocida simplemente como Argentina, es un país soberano de América del Sur, ubicado en el extremo sur y sudeste de dicho subcontinente. Adopta la forma de gobierno republicana, representativa y federal.

El Estado argentino es un Estado federal descentralizado, integrado por un Estado nacional y veintitrés estados provinciales autónomos  

Como podemos ver, con la ayuda de Textacy es muy fácil descargar la información de wikipedia para luego poder utilizarla de base para nuestro corpus. Veamos otros problemas que también podemos resolver con la ayuda de Textacy; como ser los casos de detectar el idioma o de procesar todo un texto y analizarlo.


In [5]:
# Detectando el idioma con taxtacy
saludos = ["Hola", "Hello", "Bonjour", "Guten Tag", "Buon giorno", "Bom dia"]
for saludo in saludos:
    print(textacy.text_utils.detect_language(saludo))


es
en
fr
de
it
pt

In [6]:
# Cargando el modelo en español de spacy
nlp = textacy.data.spacy.load('es_core_web_md')

In [7]:
# detalle de stop words
# las stop words son las palabras más comunes de un corpus que en general
# queremos eliminar porque no son significativas para un análisis.
# Ocurren muchas veces, pero aportan muy poca información
stop = list(textacy.data.spacy.es.STOP_WORDS)
stop[:15]


Out[7]:
['cuanto',
 'sera',
 'trabajo',
 'tan',
 'ya',
 'claro',
 'encuentra',
 'arriba',
 'despacio',
 'primero',
 'pocas',
 'tiempo',
 'aquéllos',
 'días',
 'enseguida']

In [8]:
# Procesando un texto 
# Procesando 1984 de George Orwell - mi novela favorita
texto = leer_texto('ORWELL_1984.txt')
texto_procesado = nlp(texto)

In [9]:
# Cuántas sentencias hay en el texto?
sentencias = [s for s in texto_procesado.sents]
print(len(sentencias))


8114

In [10]:
# imprimir las primeras 10 sentencias para verificar el texto
print(sentencias[1:11])


[Winston Smith, con la barbilla clavada en el pecho en su esfuerzo por burlar el molestísimo viento, se deslizó rápidamente por entre las puertas de cristal de las Casas de la Victoria, aunque, no con la suficiente rapidez para evitar que una ráfaga polvorienta se colara con él.
, El vestíbulo olía a legumbres cocidas y a esteras viejas., Al fondo, un cartel de colores, demasiado grande para hallarse en un interior, estaba pegado a la pared., Representaba sólo un enorme rostro de más de un metro de anchura: la cara de un hombre de unos cuarenta y cinco años con un gran bigote negro y facciones hermosas y endurecidas., Winston se dirigió hacia las escaleras., Era inútil intentar subir en el ascensor., No funcionaba con frecuencia y en esta época la corriente se cortaba durante las horas de día., Esto era parte de las restricciones con que se preparaba la Semana del Odio., Winston tenía que subir a un séptimo piso.]

In [11]:
# sentencias con las que aparece el Gran Hermano
[sent for sent in texto_procesado.sents if 'Gran Hermano' in sent.string][-10:]


Out[11]:
[—¿Morirá el Gran Hermano?,
 ael Gran Hermano.,
 Pensó en el Gran Hermano.,
 ¿Cuáles eran sus verdaderos sentimientos hacia el Gran Hermano?,
 Dime: ¿cuáles son los verdaderos sentimientos que te inspira el Gran Hermano?,
 Tienes que amar ael Gran Hermano.,
 l Gran Hermano.,
 l Gran Hermano.,
 Amaba ael Gran Hermano.
 
 
 
 
 ,
 Hubiera sido posible, por ejemplo, decir el «Gran Hermano inbueno».]

In [12]:
# <!-- collapse=True -->
def encontrar_personajes(doc):
    """
    Devuelve una lista de los personajes de un `doc` con su cantidad de
    ocurrencias
    
    :param doc: NLP documento parseado por Spacy
    :return: Lista de Tuplas con la forma
        [('winston', 686), ("o'brien", 135), ('julia', 85),]
    """
    
    personajes = Counter()
    for ent in doc.ents:
        if ent.label_ == 'PERSON':
            personajes[ent.lemma_] += 1
            
    return personajes.most_common()

In [13]:
# Extrayendo los personajes principales del texto y contando cuantas veces
# son nombrados.
print(encontrar_personajes(texto_procesado)[:20])


[('winston', 686), ("o'brien", 135), ('julia', 85), ('partido', 85), ('parsons', 36), ('syme', 29), ('goldstein', 29), ('pensamiento', 22), ('odio', 13), ('ministerio', 13), ('', 11), ('katharine', 11), ('winston ?', 10), ('rutherford', 9), ('ogilvy', 8), ('aaronson', 8), ('charrington', 8), ('—la', 7), ('withers', 6), ('ingsoc', 6)]

In [14]:
# <!-- collapse=True -->
def obtener_adj_pers(doc, personaje):
    """
    Encontrar todos los adjetivos relacionados a un personaje en un `doc`
    
    :param doc: NLP documento parseado por Spacy
    :param personaje: un objeto String 
    :return: lista de adjetivos relacionados a un `personaje`
    """
    
    adjetivos = []
    for ent in doc.ents:
        if ent.lemma_ == personaje:
            for token in ent.subtree:
                if token.pos_ == 'ADJ':
                    adjetivos.append(token.lemma_)
    
    for ent in doc.ents:
        if ent.lemma_ == personaje:
            if ent.root.dep_ == 'nsubj':
                for child in ent.root.head.children:
                    if child.dep_ == 'acomp':
                        adjetivos.append(child.lemma_)
    
    return adjetivos

In [15]:
# Encontrar adjetivos que describen a algún personaje.
print(obtener_adj_pers(texto_procesado, "winston"))


['superior', 'separado', 'fuertes', 'abierto', 'oscura', 'dirigiéndose', 'negro', 'saltones', 'tristes', 'burlones', 'solo', 'humano', 'sostenemos', 'dubitativo', 'completa', 'definitiva', 'sobresaltado', 'fascinado', 'extraña', 'sobrado', 'propio', 'solos', 'joven', 'sorprendido', 'sorprendido', 'hermosa', 'breve', 'cortante', 'primera', 'junto', 'obediente', 'metafísico', 'blanca', 'sonriente', 'sentado', 'irresoluto', 'sumergido', 'feliz']

In [16]:
# <!-- collapse=True -->
def personaje_verbo(doc, verbo):
    """
    Encontrar los personajes que utilizan determinado `verbo` en `doc`
    
    :param doc: NLP documento parseado por Spacy
    :param verbo: un objeto String 
    :return: lista de personajes que utilizan `verbo`
    """
    contar_verbo = Counter()
    for ent in doc.ents:
        if ent.label_ == 'PERSON' and ent.root.head.lemma_ == verbo:
            contar_verbo[ent.text] += 1
    
    return contar_verbo.most_common(10)

In [17]:
# Encontrar personajes que utilizan determinado verbo
personaje_verbo(texto_procesado, "dijo")


Out[17]:
[('Winston', 7),
 ('Julia', 4),
 ('Syme', 2),
 ('Julia—. Espera', 1),
 ('Parsons', 1),
 ('—le', 1)]

In [18]:
# Trabajando con las entidades del texto
# Una entidad nombrada es cualquier objeto del mundo real como una persona,
# ubicación, organización o producto con un nombre propio.

# tipos de entidades del texto
set(ent.label_ for ent in texto_procesado.ents)


Out[18]:
{'CARDINAL', 'LOC', 'MISC', 'ORDINAL', 'ORG', 'PERSON'}

In [19]:
# Entidades nombradas de tipo ORG
[ent for ent in texto_procesado.ents if ent.label_ == 'ORG'][:10]


Out[19]:
[INGSOC,
 Partido:,
 ES LA,
 Departamento de Registro,
 Dos Minutos de Odio,
 Departamento de Novela,
 Partido Interior,
 Partido Interior,
 Departamento de Registro,
 Dos Minutos]

In [20]:
# Partes de la oración (POS)
# En las partes de la oración se etiquetan las palabras de acuerdo a lo 
# que significan segun su contexto. Algunas de estas etiquetas pueden
# ser: Adjetivos, verbos, adverbios, conjunciones, pronombres, sustantivos.

# Etiquetas del texto
set(token.pos_ for token in texto_procesado)


Out[20]:
{'ADJ',
 'ADP',
 'ADV',
 'AUX',
 'CONJ',
 'DET',
 'INTJ',
 'NOUN',
 'NUM',
 'PART',
 'PRON',
 'PROPN',
 'PUNCT',
 'SCONJ',
 'SPACE',
 'SYM',
 'VERB'}

In [21]:
# Etiquetas de tipo ADJ
[token.orth_ for token in texto_procesado if token.pos_ == 'ADJ'][1:11]


Out[21]:
['frío',
 'clavada',
 'molestísimo',
 'suficiente',
 'polvorienta',
 'cocidas',
 'viejas',
 'grande',
 'pegado',
 'enorme']

In [22]:
# Etiquetas de tipo PROPN
[token.orth_ for token in texto_procesado if token.pos_ == 'PROPN'][1:11]


Out[22]:
['GEORGE',
 'ORWELL',
 'PARTE',
 'CAPITULO',
 'Winston',
 'Smith',
 'Casas',
 'Victoria',
 'Winston',
 'Semana']

Como demuestran estos ejemplos Textacy / Spacy son herramientas muy poderosas que nos pueden ayudar a analizar y obtener información valiosa de un texto en forma rápida y sencilla.

Deep Learning y Procesamiento del Lenguaje Natural

Durante mucho tiempo, las técnicas principales de Procesamiento del Lenguaje Natural fueron dominadas por métodos de aprendizaje automático que utilizaron modelos lineales como las máquinas de vectores de soporte o la regresión logística, entrenados sobre vectores de características de muy alta dimensional pero muy escasos. Recientemente, el campo ha tenido cierto éxito en el cambio hacia modelos de deep learning sobre entradas más densas.

Las redes neuronales proporcionan una poderosa maquina de aprendizaje que es muy atractiva para su uso en problemas de lenguaje natural. Un componente importante en las redes neuronales para el lenguaje es el uso de una capa de word embedding, una asignación de símbolos discretos a vectores continuos en un espacio dimensional relativamente bajo. Cuando se utiliza word embedding, se transforman los distintos símbolos en objetos matemáticos sobre los que se pueden realizar operaciones. En particular, la distancia entre vectores puede equipararse a la distancia entre palabras, facilitando la generalización del comportamiento de una palabra sobre otra. Esta representación de palabras como vectores es aprendida por la red como parte del proceso de entrenamiento. Subiendo en la jerarquía, la red también aprende a combinar los vectores de palabras de una manera que es útil para la predicción. Esta capacidad alivia en cierta medida los problemas de dispersión de los datos. Hay dos tipos principales de arquitecturas de redes neuronales que resultan muy útiles en los problemas de Procesamiento del Lenguaje Natural: las Redes neuronales prealimentadas y las Redes neuronales recurrentes.

Para ejemplificar, veamos como podemos utilizar word embedding utilizando el modelo word2vec de Gensim.


In [23]:
# <!-- collapse=True -->
# importando gensim y TSNE para graficar
import gensim
from sklearn.manifold import TSNE


Using TensorFlow backend.

In [24]:
# transformando el texto para pasarlo al modelo de gensim
texto = [[str(palabra).lower() for palabra in sent if str(palabra) not in stop ]
         for sent in sentencias]
# generando el diccionario
diccionario = gensim.corpora.Dictionary(texto)

In [25]:
# creando el modelo
modelo = gensim.models.Word2Vec(texto, workers=4, size=100, 
                                min_count=5, window=10, sample=1e-3)

In [26]:
# representación de la palabra hermano como vector.
modelo['hermano']


Out[26]:
array([-0.22692642, -0.08890257, -0.12868501, -0.17392403,  0.22627664,
        0.10127033, -0.09027202,  0.10692301, -0.30289358,  0.06429829,
        0.17862263,  0.20448232, -0.54694331, -0.40681064,  0.61438572,
        0.0217872 ,  0.080202  ,  0.46306548,  0.09076022, -0.02869571,
       -0.46194851,  0.28670114,  0.38570273,  0.32555154,  0.13098474,
       -0.03134775, -0.09577781,  0.06859019, -0.15935177,  0.61558241,
        0.07509102, -0.24245416, -0.44668666, -0.77279037,  0.84581488,
       -0.54047441, -0.18756895, -0.12506978, -0.52870399,  0.1898849 ,
       -0.00930689,  0.36932173,  0.22370262, -0.67407966, -0.45509291,
       -0.00848365,  0.62967575,  0.16172817,  0.09978516,  0.15064637,
       -0.34957823,  0.20686783,  0.1038606 , -0.09155462,  0.08276461,
        0.31154567, -0.3129864 , -0.45181432, -0.12060832,  0.30541465,
       -0.37994722,  0.13566031,  0.16380484,  0.32732216,  0.15746659,
        0.69340295, -0.25527388,  0.37333885,  0.23317885, -0.4710786 ,
       -0.22506852,  0.14103019, -0.30253953,  0.00573605, -0.14745024,
       -0.50815731, -0.37789851, -0.3400358 ,  0.62753612,  0.04747195,
       -0.07443633,  0.4276363 , -0.28931141,  0.29784235, -0.07251735,
       -0.07709371, -0.1003265 , -0.29098341,  0.47159177,  0.41372281,
       -0.10831725, -0.04670507,  0.07489309,  0.00146162, -0.02867368,
       -0.2771121 ,  0.37281424, -0.53325164,  0.19094327,  0.51455575], dtype=float32)

In [27]:
# como convertimos las palabras en vectores y los vectores capturan muchas 
# regularidades lingüísticas, podemos aplicar operaciones vectoriales para
# extraer muchas propiedades interesantes. 

# palabras similares a persona
modelo.most_similar_cosmul('persona')


Out[27]:
[('mano', 0.9999627470970154),
 ('se', 0.9999592304229736),
 ('mesa', 0.9999556541442871),
 ('lo', 0.999954879283905),
 ('podía', 0.9999546408653259),
 ('personas', 0.9999545812606812),
 ('habría', 0.9999533891677856),
 ('sitio', 0.9999532103538513),
 ('no', 0.999953031539917),
 ('pero', 0.999951958656311)]

In [28]:
# por último podemos graficar el modelo
vocab = list(modelo.wv.vocab)
X = modelo[vocab]

# aplicamos TSNE
tsne = TSNE(n_components=2)
X_tsne = tsne.fit_transform(X)

# transformamos en DataFrame
df = pd.concat([pd.DataFrame(X_tsne),
                pd.Series(vocab)],
               axis=1)

df.columns = ['x', 'y', 'palabra']

In [29]:
# creamos el gráfico
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(1, 1, 1)

# reducimos el dataset a 15 palabras para el ejemplo
df_e = df.head(15)

ax.scatter(df_e['x'], df_e['y'])

for i, txt in enumerate(df_e['palabra']):
    ax.annotate(txt, (df_e['x'].iloc[i], df_e['y'].iloc[i]))
    
plt.show()


Aquí termina este artículo, obviamente el Procesamiento del Lenguaje Natural es un campo muy amplio y quedaron muchas cosas sin desarrollar. Espero que esta introducción les haya sido de utilidad.

Saludos!

Este post fue escrito por Raúl e. López Briega utilizando Jupyter notebook. Pueden descargar este notebook o ver su version estática en nbviewer.