Natural Language Processing

Marco teórico

El procesamiento de lenguaje natural, mencionado como NLP a partir de ahora por sus siglas en inglés (Natural Language Processing), Es una rama de la inteligencia artficial que busca realizar análisis y transformaciones a cuerpos de texto para encontrar patrones, inferir significados y relaciones entre elementos (palabras, párrafos, oraciones, etc.) o, en general, servir en un modelo con algún propósito de toma de decisiones, inferencia o predicción.

Aquí veremos los básicos de esta área utilizando el paquete TextAnalysis en Julia.

Documentos

Los documentos se definen como cualquier cuerpo de texto que puede ser representado de alguna manera específica en el disco de la computadora. Existen los siguientes tipos:

  • Tipo archivo (FileDocument): Un documento representado como texto plano en el disco.

  • Tipo string (StringDocument): Un documento representado como un string codificado en UTF8 en memoria RAM

  • Tipo Token: (TokenDocument): Un documento representado como una sucesión de tokens UTF8, es decir, palabras o símbolos individuales (strings tras ser “tokenizados”).

  • Tipo N-grama: (NGramDocument): Un documento representado como una colección de pares donde un elemento es un token y el otro es un entero que representa el una frecuencia de ocurrencia de dicho string.

Observemos los tipos continuación:

using TextAnalysis

Documentos de tipo string/cadena

Los documentos de tipo cadena suelen ser oraciones individuales, párrafos o textos más completos. No obstante, parte de tener una conjunto de datos limpio yace en organizar los textos en archivo o más pequeño posible.

Además, éstos se guardan en memoria ram, representados como una cadena de bits en codificación UTF-8. Por ello, tener textos muy grandes localizados en una sola variable identificadora puede dificultar su manipulación.

str = "Este es un texto de prueba. Este es la segunda oración"
"Este es un texto de prueba. Este es la segunda oración"
sd = StringDocument(str)
A StringDocument{String}
 * Language: Languages.English()
 * Title: Untitled Document
 * Author: Unknown Author
 * Timestamp: Unknown Time
 * Snippet: Este es un texto de prueba. Este es la segunda ora

Documentos de tipo archivo

Los documentos de tipo archivo se utilizan cuando tenemos un archivo contenedor del texto que queremos analizar. Para generarlo en julia basta con poner la ruta hacia el archivo:

pathname = "./nlp/archivo.txt"
"./nlp/archivo.txt"
fd = FileDocument(pathname)
Can't find file: ./nlp/archivo.txt

Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] text at /home/runner/.julia/packages/TextAnalysis/32jBX/src/document.jl:214 [inlined]
 [3] summary(::FileDocument) at /home/runner/.julia/packages/TextAnalysis/32jBX/src/show.jl:16
 [4] show at /home/runner/.julia/packages/TextAnalysis/32jBX/src/show.jl:45 [inlined]
 [5] limitstringmime(::MIME{Symbol("text/plain")}, ::FileDocument) at /home/runner/.julia/packages/IJulia/IDNmS/src/inline.jl:43
 [6] display_mimestring at /home/runner/.julia/packages/IJulia/IDNmS/src/display.jl:71 [inlined]
 [7] display_dict(::FileDocument) at /home/runner/.julia/packages/IJulia/IDNmS/src/display.jl:102
 [8] #invokelatest#1 at ./essentials.jl:710 [inlined]
 [9] invokelatest at ./essentials.jl:709 [inlined]
 [10] execute_request(::ZMQ.Socket, ::IJulia.Msg) at /home/runner/.julia/packages/IJulia/IDNmS/src/execute_request.jl:112
 [11] #invokelatest#1 at ./essentials.jl:710 [inlined]
 [12] invokelatest at ./essentials.jl:709 [inlined]
 [13] eventloop(::ZMQ.Socket) at /home/runner/.julia/packages/IJulia/IDNmS/src/eventloop.jl:8
 [14] (::IJulia.var"#15#18")() at ./task.jl:356

Documentos de tipo token

Los token en el contexto de procesamiento de lenguaje natural son elementos individuales y uniformes que yacen en una colección. Se habla de token usualmente para referirse a palabras individuales cuando tenemos un alfabtero latino, no obstante, para otros alfabetos puede que el concepto cambie ligeramente.

Además, los tokens podrían referirse a letras individuales, oraciones o cualquier forma de agrupar el texto que posea información estructural. No obstante, esas dos agrupaciones mencionadas no suelen poseer información útil al ser respectivamente muy pequeñas y muy grandes.

Para crearlas en Julia podemos crear un arreglo de ellas:

mis_tokens = String["Esta", "es", "una", "oración", "de", "prueba"]
6-element Array{String,1}:
 "Esta"
 "es"
 "una"
 "oración"
 "de"
 "prueba"

Aquí aprovechamos a mostrar además que para crear arreglos de un tipo uniforme en Julia, podemos anteponer el nombre del tipo y el compilador sabrá que debe esperar y forzar dicho tipo (ejemplo, Int32 en un arreglo forzaría a todos los Integer dentro a que sean representado por 32 bits)

typeof(mis_tokens)
Array{String,1}
td = TokenDocument(mis_tokens)
A TokenDocument{String}
 * Language: Languages.English()
 * Title: Untitled Document
 * Author: Unknown Author
 * Timestamp: Unknown Time
 * Snippet: ***SAMPLE TEXT NOT AVAILABLE***

Documento de tipo N-grama

Podemos pensar en que el documento de tipo token puede ser generado a partir de un documento de tipo string, y así también el de tipo string ser generado a partir de uno de tipo archivo. Esto es correcto y existen métodos específicos para ello.

Por una parte, para pasar de un string a una lista de tokens podemos utilizar el paquete WordTokenizer, el cual tiene múltiples métodos de tokenización de alto rendimiento para procesar grandes cuerpos de texto y reducirlos a tokens listos para el análisis.

Ahora, el documento de tipo N-grama tiene más sentido pensarlo viniendo de uno de tipo token, pues al tokenizar un texto grande, es muy probable que tengamos palabras repetidas y obtener la frecuencia en la que éstas palabras ocurren a lo largo de dicho texto juega un rol en el análisis exploratorio inicial.

Los documento de tipo N-grama son precisamente pares de tokens con un número entero que cuenta las ocurrencias de dicho token en algún texto. Aquí podemos generarlo de las siguientes maneras:

dict_ocurrencias = Dict("hola" => 1, "mundo" => 1)
Dict{String,Int64} with 2 entries:
  "hola"  => 1
  "mundo" => 1
ngd = NGramDocument(dict_ocurrencias)
A NGramDocument{AbstractString}
 * Language: Languages.English()
 * Title: Untitled Document
 * Author: Unknown Author
 * Timestamp: Unknown Time
 * Snippet: ***SAMPLE TEXT NOT AVAILABLE***

Procesamiento

Funciones

La siguiente es una exploración a algunas funciones ya definidas (para todos los tipos anteriores mediante multiple dispatch) en el paquete.

Texto

text(sd), text(fd), text(td)
Can't find file: ./nlp/archivo.txt

Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] text(::FileDocument) at /home/runner/.julia/packages/TextAnalysis/32jBX/src/document.jl:214
 [3] top-level scope at In[11]:1
 [4] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1091

Tokens

tokens(sd)
11-element Array{String,1}:
 "Este"
 "es"
 "un"
 "texto"
 "de"
 "prueba."
 "Este"
 "es"
 "la"
 "segunda"
 "oración"
 tokens(fd)
Can't find file: ./nlp/archivo.txt

Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] text at /home/runner/.julia/packages/TextAnalysis/32jBX/src/document.jl:214 [inlined]
 [3] tokens(::FileDocument) at /home/runner/.julia/packages/TextAnalysis/32jBX/src/document.jl:264
 [4] top-level scope at In[13]:1
 [5] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1091

N-gramas

ngrams(sd)
Dict{String,Int64} with 9 entries:
  "la"      => 1
  "un"      => 1
  "prueba." => 1
  "oración" => 1
  "segunda" => 1
  "texto"   => 1
  "Este"    => 2
  "es"      => 2
  "de"      => 1
ngrams(fd)
Can't find file: ./nlp/archivo.txt

Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] text at /home/runner/.julia/packages/TextAnalysis/32jBX/src/document.jl:214 [inlined]
 [3] tokens(::FileDocument) at /home/runner/.julia/packages/TextAnalysis/32jBX/src/document.jl:264
 [4] ngrams(::FileDocument, ::Int64) at /home/runner/.julia/packages/TextAnalysis/32jBX/src/document.jl:312
 [5] ngrams(::FileDocument) at /home/runner/.julia/packages/TextAnalysis/32jBX/src/document.jl:314
 [6] top-level scope at In[15]:1
 [7] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1091
ngrams(sd, 2)
Dict{AbstractString,Int64} with 9 entries:
  "prueba. Este"    => 1
  "un texto"        => 1
  "Este es"         => 2
  "texto de"        => 1
  "es la"           => 1
  "la segunda"      => 1
  "es un"           => 1
  "de prueba."      => 1
  "segunda oración" => 1
ngrams(sd, 2, 3)
Dict{AbstractString,Int64} with 18 entries:
  "texto de"           => 1
  "la segunda"         => 1
  "es un texto"        => 1
  "es la segunda"      => 1
  "prueba. Este"       => 1
  "es la"              => 1
  "Este es un"         => 1
  "texto de prueba."   => 1
  "segunda oración"    => 1
  "prueba. Este es"    => 1
  "Este es la"         => 1
  "la segunda oración" => 1
  "Este es"            => 2
  "es un"              => 1
  "un texto de"        => 1
  "de prueba. Este"    => 1
  "un texto"           => 1
  "de prueba."         => 1

Podemos también extraer la estructura de un documento de N-gramas para entender si tiene bigramas o trigramas, etc.

ngram_complexity(NGramDocument(ngrams(sd), 2))
2

Metadata

language(sd) ## El lenguaje por defecto es inglés...
Languages.English()

Podemos cambiarlo utilizando la versión “mutadora” de la función language:

language!(sd, TextAnalysis.Languages.Spanish())
Languages.Spanish()

Así igual los demás elementos de la metadata…

title(sd), author(sd), timestamp(sd)
("Untitled Document", "Unknown Author", "Unknown Time")
title!(sd, "Mi título"), author!(sd, "Yo"), timestamp!(sd, "Desconocido")
("Mi título", "Yo", "Desconocido")
sd
A StringDocument{String}
 * Language: Languages.Spanish()
 * Title: Mi título
 * Author: Yo
 * Timestamp: Desconocido
 * Snippet: Este es un texto de prueba. Este es la segunda ora

Procesamiento de documentos

Antes de comenzar a hacer transformaciones y análisis a los documentos, nos puede interesar hacer una limpieza en caso de tener caracteres corruptos o pequeñas molestias de formato que nos haría más limpio nuestro trabajo si no estuvieran

remove_corrupt_utf8!(sd) ## <- ejemplo de ello
str_2 = StringDocument("HolA!!!,. Soy un teXto, que No está mUy Bien escrito..")
A StringDocument{String}
 * Language: Languages.English()
 * Title: Untitled Document
 * Author: Unknown Author
 * Timestamp: Unknown Time
 * Snippet: HolA!!!,. Soy un teXto, que No está mUy Bien escr
prepare!(str_2, strip_punctuation)
text(str_2)
"HolA Soy un teXto que No está mUy Bien escrito"

Removiendo las mayúsculas…

remove_case!(str_2)
text(str_2)
"hola soy un texto que no está muy bien escrito"

Podemos además remover ciertas palabras..

remove_words!(str_2, [" no"])
text(str_2)
"hola soy un texto que está muy bien escrito"

Otras posibilidades son:

- prepare!(sd, strip_articles)
- prepare!(sd, strip_indefinite_articles)
- prepare!(sd, strip_definite_articles)
- prepare!(sd, strip_preposition)
- prepare!(sd, strip_pronouns)
- prepare!(sd, strip_stopwords)
- prepare!(sd, strip_numbers)
- prepare!(sd, strip_non_letters)
- prepare!(sd, strip_spares_terms)
- prepare!(sd, strip_frequent_terms)
- prepare!(sd, strip_html_tags)

Además de poder ser utilizadas juntas:

prepare!(sd, strip_articles| strip_numbers| strip_html_tags)