Reemplazar caracteres

Una de la funciones más habituales de analizar texto como lenguaje natural es el reemplazar caracteres: quitar tildes, cambiar mayúsculas por minúsculas, reemplazar signos de puntuación, etc. Su utilidad es simplificar el texto para facilitar el análisis, la búsqueda en diccionarios o la separación en tokens.

Hay muchas funciones para reemplazar caracteres, desde el remplace más simple hasta cosas más complejas como expresiones regulares. ¿Para qué inventar uno nuevo?. Por sencillez y velocidad. El método que utilizamos aquí es muy simple, no permite reemplazos complicados como las expresiones regulares, solo remplazar un carácter por otro, pero a cambio es capaz de realizar todos los reemplazos en una sola iteración al texto.

Su funcionamiento es sencillo, se crea un hashmap donde el carácter a remplazar es la clave y el carácter que lo reemplaza el valor. Luego se recorre el texto caracter a caracter y se consulta en el hashmap si existe un valor para el carácter actual, si es así se remplaza, si no se deja el carácter tal cual.

Para usarlo basta con añadir los caracteres, se pueden añadir de uno en uno o varios a la vez.

replace.add(«A»,»a»);

replace.add(«Aåáäâ», «a»);

También se puede reemplazar un carácter por varios.

replace.add(«/»,» dividido por «);

No así al revés, no se pueden reemplazar varios caracteres.

Si se reemplaza por una cadena vacía será como borrar el carácter del texto

replace.add(«:», «»);

Una vez configurado el diccionario basta con llamar al método replace con el texto a tratar, como resultado devolverá el texto con los reemplazos realizados.

text = replace.replace(text);

En el repositorio de la libreria se puede ver un ejemplo de funcionamiento. En él se crea un conversor a «ninini» (léase con ritmo de «chincha pincha»). Para ello se transforman todas las vocales a letras «i».

En el ejemplo se puede ver que no bastaria solo con reemplazar los caracteres hay casos como «qii» y «gii» que necesitan un tratamiento posterior. Por eso es importante elegir bien que métodos de reemplazo a usar y el coste en tiempo de los mismos.

Finalmente hay una funcionalidad más que aporta esta librería. Si se fija un valor para la propiedad default cuando no se encuentre un carácter en el hashmap, en lugar de dejarlo igual, lo reemplazará por el valor de default. Por ejemplo en este caso eliminará todos los caracteres no

replace.setDefault(«»);

Arquitectura de un bot que interactúa por voz

Vamos a ver como crear nuestro propio bot que interactue usando la voz. Algo, salvando las distancias, al estilo Siri, Alexa o Cortana. En esta entrada voy a hablar solo de la arquitectura y no del procesamiento del lenguaje natural o de la síntesis del mismo.

Lo primero que necesita todo bot es estar escuchando a la espera de oír su nombre o alguna frase que pueda reconocer como su orden. Aunque hay implementaciones por hardware que esperan oír el nombre del bot para activar la captura de audio, en nuestro caso deberemos estar escuchando constantemente. Así que nuestro primer componente va ser un proceso que escucha constantemente y transforma el audio en texto.

Posteriormente ese texto ha de ser analizado por un sistema de reglas que determine si el audio contiene algún comando valido, en nuestro caso el sistema de reglas estará basado en expresiones regulares.

Una vez tengamos una coincidencia valida se tienen que extraer los datos del comando y ejecutar el código asociado.

El resultado de la ejecución de ese código ha de ser transformado a una frase de texto que sea entendible por el usuario.

Finalmente ese texto será leído en voz alta por nuestro bot. Por lo que necesitaremos un sistema que sintetice la voz a partir del texto.

Los navegadores modernos incluyen una API tanto de reconocimiento del habla como de síntesis del habla . Aunque la verdad que el único que les da un soporte decente es Chrome, de hecho es el único en que logré que funcionara en español.  Con eso tenemos parte de lo que necesita un bot, falta una forma de definir los comando validos y otra de componer las frases que el bot necesite decir. Para ello, y alguna cosa más, se ha creado la librería jsBotVoice

En resumen:

  1. Captura del audio
  2. Conversión a texto
  3. Análisis del texto en busca de coincidencias con los comandos de texto definidos
  4. Extracción de datos y ejecución del código asociado al comando
  5. Creación del texto en respuesta
  6. Síntesis de voz a partir del texto
botVoice

Diagrama de funcionamiento de un bot que se comunica por voz

 

Los puntos del 1 al 4 están explicados en esta entrada del blog y del 5 al 6 en esta otra.

Síntesis de voz y lenguaje natural en un bot

En una entrada anterior veíamos como interpretar los comandos de voz. Ahora vamos a ver hacer que el bot de respuesta lo más naturales posibles en lenguaje hablado.

La forma más sencilla y rápida es tener respuestas predefinidas. Lo malo de este sistema es que la respuesta es siempre igual y queda muy poco natural. Usando la librería jsBotVoice  es tan sencillo como usar el modificador * delante de la cadena y esa cadena será enunciada literalmente.

voice.talk("*Este texto se lee literal");

Para aportar algo de variedad a la respuesta vamos a utilizar un modelo muy simple basado en diccionarios. La idea es que cada palabra de la frase pueda reemplazarse por varias expresiones similares, eligiéndose una al azar lo cual dota al bot de variedad en las respuestas. Lo primero será definir el diccionario.

var spanish_dictionary = {
  "hi": ["hola", "saludos"],
  "person": ["humano","ser"],
  "yes": ["si","afirmativo"],
  "no": ["no","negativo"],
  "ok": ["vale", "de acuerdo", "hecho"]
};

Una vez definido para usarlo solo hemos de crear una frase con esos tokens

voice.talk("hi person");

Esta linea pueda dar varios posibles resultados:

hola humano
saludos humano
hola ser
saludos ser

Si queremos aladir alguna palabra que se lea de forma literal en lugar de interpretarla como un token podemos ponerle delante el modificador #

voice.talk("#Buenas person");

Posibles resultados:
buenas humano
buenas ser

Por otro lado podemos personalizar aun más las respuestas usando variables, para ello usamos el nombre de la variable precedido de un $ . Las variables han de ir almacenadas en el mapa voice.data

voice.data["name"] = "Cubiwan";
voice.talk("hi $name");

Posibles resultados:
hola cubiwan
saludos cubiwan

Más documentación y ejemplos en jsBotVoice .

Interpretar lenguaje natural en un bot usando expresiones regulares

En una entrada anterior hemos visto como capturar voz en el navegador y convertirla en mensajes de texto. Ahora nos toca tratar esos mensajes para poder lanzar el comando adecuado. Cómo realmente lo único que necesitamos entender son comandos muy simples y predefinidos el sistema va a ser relativamente sencillo. Para procesar textos más complejos exiten sistemas más avanzados que requiren procesamiento del lenguaje natural. Estos sistemas requieren un conocimiento previo del lenguaje que van a procesar tanto por parte de la libreria como por parte del programador. Las expresiones regulares son sencillas de definir por el programador y para un sistema tan simple funcionan perfectamente.

En esta entrada usaremos la librería jsBotVoice que ayuda a usar expresiones regulares para interpretar los comandos.

Por lo general los bots habitualmente funcionan definiendo varias combinaciones de frases por cada comando precedidas del nombre del bot, «chispas» en nuestro caso. Por ejemplo las frases para preguntarnos la hora pueden ser:

«¿Qué hora es?»
«Dime la hora»
«Di la hora»
«¿Me dices la hora?»
«Hora»

Sin contar que puede ir precedida de cosas como. «Por favor».

Para facilitar el trabajo la mayoría de las librerías llevan herramientas para reducir el número de reglas necesarias permitiendo agrupar varias expresiones bajo una misma regla. En este caso vamos a usar expresiones regulares. Para ello dividiremos cada expresión en tokens que van asociados a expresiones regulares que describen su parte de la frase.

En nuestro caso el modelo sería:

bot please# tell time any

Cada token se traduciría en las siguientes expresiones regulares:

bot: chispa|chispas
please:por favor|porfa
tell: que|di[a-z]|me dices
time: la hora|hora
any: .

El problema es que al usar expresiones regulares tan poco concretas reconoceremos como válidas expresiones incorrectas como:

«chispas divaga hora y media»

En este caso podriamos añadir di|dim|dinos en lugar de di[a-z]*.

Hay que buscar un equilibrio entre flexibilidad de las expresiones y concretar los resultados.

Hemos logrado traducir todas las maneras de decirlo en una sola expresión pero si necesitáramos más no habría problema. Por ejemplo supongamos que queremos añadir:

«¿En que punto de la corriente temporal me encuentro?»

Podemos añadirla directamente por ejemplo:

bot: chispa|chispas|chispita
please:por favor|porfa
tell: en que|que|di[a-z]|me dices
time: la hora|hora
any: .

temporalPoint: punto de la corriente temporal

Que se traduciría en un nuevo comando de voz:

bot please# tell temporalPoint any

Vamos a ponernos con el código, primero definir los tokens:

var tokens = {
  'bot': 'chispa|chispas|chispita',
  'please': 'por favor|porfa',
  'tell': 'que|di[a-z]*|me dices',
  'time': 'la hora|hora',
  'any': '.*',
  'temporalPoint': 'punto de la corriente temporal'
}

Ahora con esos tokens podemos definir las expresiones que el bot va a entender


var vc1 = new VoiceCommand();
vc1.name="tell me time";
vc1.expressions[0] = "bot please# tell time any";
vc1.expressions[1] = "bot please# tell TemporalPoint any";
vc1.execute = function(exp, m, voice) {
 sayTime();
};

Cuando nuestro bot oiga un comando de voz que encaje con las expresiones definidas ejecutara el comando definido en execute al que se le pasa como parámetros:

  • exp: el indice de la expresión que ha coincidido
  • m: array con la coincidencia de texto de cada token de la expresión. m[0] incluye todo el texto
  • voice: referencia al al objeto Voice

Para iniciar el bot a escuchar basta con ejecutar init.

voice.init();

Pero si se quiere probar como reconoce las frases sin iniciar la parte de reconocer el audio puede usarse la función analyze pasando el texto a analizar.

voice.analyze(text);

Hay que tener en cuenta que hay que realizar pruebas reales para detectar errores habituales en el reconocimiento del habla. Por ejemplo en el caso del nombre del bot (bot: chispa|chispas) en se ha incluido chispa porque una gran cantidad de veces se reconocia la palabra «chispas» como si fuera «chispa».

En la web de la librería se puede encontrar más información.

Diccionarios vs algoritmos generadores

El mundo está lleno de grandes enfrentamientos. Espacios vs tabuladores, Android vs iOs, …. Hoy toca diccionarios vs algoritmos generadores. El motivo del enfrentamiento es que una palabra se puede tener diversas formas, por ejemplo los verbos tienen prácticamente una forma distinta dependiendo de la persona, el tiempo o el modo. Un nombre tiene masculino y femenino y sus respectivos plurales. ¿Como recopilamos todas esas variaciones? Hay dos opciones principales: diccionarios y algoritmos generadores.

Diccionarios:

Es el más simple de entender. Almacenamos un listado con cada palabra y sus posibles variaciones.

Ventajas:

  • Es fácil de entender y de modificar.
  • No tiene problemas con las excepciones.

Inconvenientes:

  • Ocupa mucho espacio.
  • No sabe qué hacer con las palabras nuevas que no están en el diccionario.
  • Es costoso y difícil recopilar todos los casos.

Algoritmos generadores:

Se basan en usar un algoritmo que a partir de una forma de la palabra genera todas las demás.

Ventajas:

  • Ocupa poco espacio.
  • Pueden trabajar con palabras nuevas sin problemas.

Inconvenientes:

  • No pueden trabajar con excepciones.
  • Son difíciles de modificar.
  • A veces tienes que aplicar varios algoritmos para ir de una forma a otra ya que no hay uno directo.

Algoritmos generadores con diccionarios de excepciones:

Siempre hay una tercera opción que es mezclar las otras dos. Se usan algoritmos generadores para generar la nueva forma de la palabra, y solo se almacenan en el diccionario las excepciones a estos algoritmos. Ocupa menos espacio y es capaz de tener en cuenta las excepciones.

Por ejemplo, podríamos tener un algoritmo generador realmente simple que tuviera esta regla:

«Si la palabra femenina acaba en ‘a’ el masculino remplaza la ‘a’ por ‘o'»

Funciona bien la mayoría de los casos, pero sería necesario tener un diccionario que indique que el femenino de ‘vaca’ es ‘toro’ y no ‘vaco’.

Pero no todo es alegría y regocijo. Aún con este sistema es difícil ser exhaustivo y tener en cuenta todos los posibles casos. Además de que el lenguaje es algo vivo que siempre está creciendo y cambiando por lo que hay que realizar un esfuerzo por mantenerlo actualizado.

Encontrando la sílaba tónica

La sílaba tónica es aquella que se pronuncia con más énfasis en la frase. Encontrarla es útil para saber el significado de las palabras ya que muchas pueden cambiar con la pronunciación. Por ejemplo: médico, medico y medicó.

El algoritmo para encontrarla en español es muy simple. Vamos a suponer que ya tenemos la palabra dividida en sílabas. Si es monosílaba, no  y hay mucho donde elegir, está claro que esa sílaba es la tónica. Si tiene más sílabas hay que buscar cual tiene la tilde. Si no la tiene ninguna solo puede ser llana o aguda. Si termina en «n»,»s» o vocal y no tiene tilde es llana. Si termina en cualquier otra letra es aguda.

Y ya está, simple y rápido.

Este algoritmo está incluido en la librería jsESsyllable .


var syllable = new jsESsyllable();

var word = "...";
var syllables = syllable.divide(word);
var strong = syllable.stress(syllables);

Puedes probarlo online.

Separar palabras en sílabas

Este algoritmo esta inspirado en el paper «A Syllabification Algorithm for Spanish» de Heriberto Cuayáhuitl. Pero realizando algunos cambios.

Para separar las palabras en sílabas usamos un algoritmo de dos pasos.

Paso 1

La clave del primer paso está en analizar las consonantes entre vocales. Podemos tener los siguientes casos: VCV, VCCV, VCCCV, VCCCCV. Siempre dividimos por delante de la última consonante: V|CV, VC|CV, VCC|CV, VCCC|CV . Sin embargo si la última consonante es una ‘r’, ‘l’ o ‘h’. Estamos ante una estructura de dos consonantes como cr, fr, rr, ll, ch,… Por lo que desplazaremos el corte a la consonante anterior. Hay que tener en cuenta algún detalle más. Cuando hay varias vocales juntas actúan como si fueran una sola. Así que realmente las V pueden representar a varias vocales.

El algoritmo necesita una variable temporal donde se almacena lo queda a la derecha del punto de corte. Pero antes de actualizarla esa variable hay que tomar el valor almacenado en ella, añadirle lo que queda a la izquierda del punto de corte y almacenarlo como una de las silabas. Puede que algunas de la silabas no sean correctas, de ellas se ocupará el paso 2.

El algoritmo elimina la parte analizada de la palabra y es como si siempre estuviera al principio de la misma. Para ello el algoritmo al cortar un bloque de vocales y consonantes toma el lado de la izquierda, lo junta con lo que haya almacenado en la variable temporal (el lado de la derecha del anterior corte), eso forma una silaba y la guarda. Luego almacena en la variable la parte de la derecha del corte hasta poder unirlo con la parte izquierda del siguiente corte (si no hubiera más cortes lo guarda y finaliza).

Al final las reglas que quedan son:

La palabra comienza por….GuardarVariable Temporal (T)
V -> | VV
CV -> | CVTCV
C[rlh]V -> | C[rlh]VTC[rlh]V
CCV -> C | CVT+CCV
CC[rlh]V -> C | C[rlh]VT+CC[rlh]V
CCCV -> CC | CVT+CCCV
CCC[rlh]V -> CC | C[rlh]VT+CCC[rlh]V
CCCCV -> CCC | CVT+CCCCV
C* -> C* |T+C*

Siendo: V = vocal/es, C = Consonante, [rlh] = consonante que sea r, l, h, T = variable temporal, | = punto de corte

La última regla hace referencia a que solo quedan caracteres y ninguna vocal, en ese caso se unen todo a lo que haya en la memoria temporal.

Ejemplos:

PalabraReglaBloqueGuardarTemporalSílabas
instrumentoV -> |V| i i
nstrumentoCCC[rlh]V -> CC | C[rlh]Vns | trui+nstruins,
mentoCV -> | CV| metrumeins, tru
ntoCCV -> C | CVn | tome+ntoins, tru, men
  to ins, tru, men, to
PalabraReglaBloqueGuardarTemporalSílabas
trompetaC[rlh]V -> | C[rlh]V| tro tro
mpetaCCV -> C | CVm | petro+mpetrom,
taCV -> | CV| tapetatrom, pe
tatrom, pe, ta
PalabraReglaBloqueGuardarTemporalSílabas
aviónV -> |V| a a
viónCV -> | CV| vióavióa,
nC* -> C* |n | vió + na, vión

 Paso 2

En el segundo paso repasamos cada uno de los grupos creados en el paso anterior y vamos a dividir los grupos de vocales cuando sea necesario. Separaremos las vocales en dos grupos: débiles {i,u} y fuerte {a,e,o}. Denotaremos el grupo de débiles con d y el de fuertes con f. A su vez pueden estar acentuadas o no, lo indicaremos como D {í,ú} y F {á,é,ó}.

Para dos vocales juntas las reglas son:

  • Permanecen juntas: dd, df, dF, Fd
  • Se separan:  ff, Ff, fF, Df, fD

En el caso de tres vocales solo se separan si hay dos fuertes juntas.

  • {iuüíú}{aeoáéó}{aeoáéó} -> {iuüíú}{aeoáéó} | {aeoáéó}
  • {aeoáéó}{aeoáéó}{iuüíú} -> {aeoáéó} | {aeoáéó}{iuüíú}

Ejemplo:

«avión» tras el paso uno queda dividida en «a»,»vión». Al ser «vión» un caso dF las vocales no se dividen, el paso dos devolverá «a»,»vión»

«rocíen» se separará en el paso uno en «ro»,»cíen». Como íe es un caso de Df se separara «ro»,»cí»,»en»

En cambio «ca»,»ca»,»hue»,»te» en el caso de «hue» al ser df seguirán juntas.


Todo esto está recopilado en la librería jsESsyllable.

var syllable = new jsESsyllable();

var word = "...";
var syllables = syllable.divide(word);

Puedes usar la demo online.

Tienes un vídeo explicando este mismo algoritmo en mi canal de youtube:

Haz click para ver el vídeo en mi canal de youtube

Flexiones y derivaciones de las palabras.

Uno de los mayores problemas que tenemos para conseguir que nuestro agente pueda tanto interpretar como generar con más naturalidad las frases ha de ser capaz de manejar las flexiones y derivaciones de nuestro lenguaje, al menos de una forma básica.

Eso le permite usar e interpretar correctamente el género, número, conjugaciones y tiempos verbales. Incluso aumentativos y diminutivos.

Reconozcamos que conversaciones como:

– Chispas Hola, soy Lorena

– Hola señor Lorena

– Chispas enciende las luces

– La luces está encendido

Quedan muy absurdas.

Desgraciadamente el lenguaje no se construye pensando en que sea fácil procesarlo por un algoritmo y al final el código acaba siendo un montón de reglas y excepciones. Hay que asumir que no son 100% correctos y que van a fallar en casos concretos que se salten las reglas. Hay mecanismos para incluir estás excepciones. Aún así hay que tratar de controlar el léxico usado. Como siempre es más fácil controlarlo si reducimos el lenguaje a un contexto concreto.

Pero es que hay que tener en cuenta que  todo puede funcionar bien y una palabra puede estar correctamente cambiada de género y número y aún así ser incorrecto su uso. Por ejemplo: «El manzano está en flor» si lo pasamos a femenino «La manzana está en flor» aunque manzana es correcto cambia el sentido de la frase. Ejemplos hay a montones «caso, casa», «paso, pasa»… Desgraciadamente es difícil de controlar ya que no solo depende de la palabra en sí, si no también del contexto y de su significado.

Además a veces el paso de una forma otra otro no es sencillo ya que se pierde información de la palabra original. Por ejemplo del plural al singular. En el caso de la palabras acabadas en es no queda claro cuáles tienes que quitar la terminación -es y cuales solo la -s.

Meses -> Mes

Ceses -> Cese

A todos estos problemas hay que contarle varios tipos de excepciones:

  • Verbos: llevan sus propias reglas (conjugaciones verbales) y varían según el género, número y tiempo.
  • Nombres propios: estos son un drama, los hay indistinguibles de nombres comunes (Rosa, Mar, Pilar). Los hay invariantes, que no varían con el número o el género: Sócrates y otros que si (María,Mario) y los que varían a veces no siguen las reglas habituales (Carlos, Carla).
  • Invariantes: palabras que no cambian su forma para los distintos géneros y/o números (Gafas).
  • Excepciones: palabras que no cumplen la reglas para cambiar el género o el número (Hombre/Mujer)

Los algoritmos has sido recogidos en el proyecto jsESinflection que está divido en cinco ficheros JS.

Plurales y singulares jsESnumber.js

Masculinos y femeninos jsESgender.js

Aumentativos jsESaugmentative.js

Diminutivos jsESdiminutive.js

Despreciativos jsESdespective.js

Para cargarlas se puede usar el siguite codigo:






var number = new jsESnumber();
var gender = new jsESgender();
var despective = new jsESdespective();
var augmentative = new jsESaugmentative();
var diminutive = new jsESdiminutive();

Las llamadas para transformar las palabras son sencillas:

number.pluralOf(word);
number.singularOf(word);
number.singularOrPlural(word);
gender.feminineOf(word);
gender.masculineOf(word);
gender.masculineOrFeminine(word);
despective.despectiveOf(word);
augmentative.augmentativeOf(word);
diminutive.diminutiveOf(word);

También permite añadir excepciones si las reglas que aplican las funciones no dan el resultado correcto:

number.addException(singular, plural);
gender.addException(masculine, feminine);
augmentative.addException(normal, augmentative);
diminutive.addException(normal, diminutive)
diminutive.addException(normal, despective);

La librería ya incluye las excepciones más comunes pero para declarar las propias es tan sencillo como pasarle el par de palabras. Por ejemplo:

//realmente es innecesario ya esta incluido en la librería
gender.addException("hombre", "mujer");

Me gustaría decir que funciona a la perfección en el 100% de los casos, pero el leguaje tiene demasiadas excepciones y peculiaridades apra que así sea, sin embargo da un buen resultado la mayoría de las veces.

Extraer lemas de un texto

Una vez visto como lematizar palabras vamos a ver una utilidad de ello. Sacar de que va un texto. En realidad sacar los lemas principales de un texto y así poder compararlos. ¿Por qué convertir las palabras de un texto en lemas? Simplemente porque en un texto el mismo términos puede aparecer varias veces en distintas formas (masculino, femenino, plural, singular,…) y la única forma de saber que es el mismo termino es lematizarlos o en nuestro caso usar un stemmer, en concreto jsEStemmer

No todos los lemas van a ser importantes. En un texto largo puedes tener montón de palabras que no estén relacionadas con el tema principal del texto. La idea es que cuantas más veces salga repetido el mismo lema más probable es que el texto trate sobre el. Así una vez extraídos todos los lemas habría que contarlos y agruparlos para quedarnos solo con los más repetidos.
Pero antes de poder extraer los lemas hay que eliminar las stopwords que son palabras que no aportan nada y son tan comunes que casi siempre saldrían como lemas principales. Al final tendrías que casi todos los textos hablan de los lemas «un», «el», «de»,.. palabras que son muy comunes y no nos aportan nada sobre el texto.

Así que el proceso sería:

  1. Eliminar los caracteres no alfabéticos y convertir las mayúsculas a minúsculas. Asi como eliminar tildes, diéresis, etc.
  2. Eliminar stopwords
  3. Lematización la palabras restantes.
  4. Agrupar lemas similares por distancia
  5. Filtrar los lemas y dejar solo los más importantes.

En el caso de nuestra librería primero obtendríamos todos los lemmas y luego los filtraríamos.

var lemmas = stemmer.stemText(text, distance);
var lemmas = stemmer.filterLemmas(lemmas, max, number, percentage);

Se pueden filtrar por:

  • max – Número máximo de lemas que debe contener la lista
  • number – Número mínimo de veces que aparece el lemma en el texto
  • percentage – Porcentaje mínimo de veces que aparece el lemma en el texto

El resultado obtenido​ estará en forma de una estructura con tres datos:

  • Un listado de los lemas «similares»
  • El número de veces que aparecen estos lemas en el texto.
  • El porcentaje que representa respecto al total de palabras del texto sin stopwords.
function Lemma(){
  this.lemmas = []; //lista de lemmas similares
  this.number = 0; //numero de repeticiones en el texto
  this.percentage = 0; //porcentaje de repeticiones en el texto

  this.merge = function(lemma){...} //permite unir dos lemmas
}

¿Como sabemos si un lema es «similar» a otro?. Calculando la distancia entre lo dos. Si son menor que un valor umbral se consideran «idénticos». Esto es necesario porque el proceso de lematización no es tan perfecto como nos gustaría y dos palabras que deberían tener  y el mismo lema pueden generar lemas muy parecidos pero no identificos. Para ello se fija un valor de corte por debajo del cual dos lemas se consideran el mismo y se agrupan bajo la misma estructura. Se suman el número de veces que aparecen y el porcentaje.

Ahora teniendo los lemas principales de un texto podemos compararlo con los lemas de cualquier otro texto, palabra o frase para ver si están relacionados.

//Calcula lo similares que son dos listados de lemas
//El resultado se calcula entre 0 y 1.
var result = stemmer.compareArrayOfLemmas((a, b, threshold));

Medir distancia entre lemas

Ya hemos visto como medir la distancia entre palabras, pero el medir distancia entre lemas plantea un problema diferente a medir el de palabras ya que solo tienen en común el texto que vaya desde el principio del lema hasta la primera letra diferente. Por ejemplo, aunque cos y cas tienen la mayor parte de las letras en común su distancia debería ser alta ya que son lemas de palabras muy diferentes. La distancia que vamos a usar es el número de caracteres en común del principio al primer carácter diferente por dos (son caracteres en común en ambos lemas) dividido entre la suma del número de caracteres de cada palabra.


function distanceLemmas(a,b){
  var shorterLength;
  var totalLength = a.length + b.length;
  if(a.length > b.length){
    shorterLength = b.length;
  } else {
    shorterLength = a.length;
  }

  var i = 0
  for(; i < shorterLength; ++i){
    if(a[i] != b[i])
      break;
  }

  if(i == 0) {
    return 1;
  } else {
    return 1-((i*2)/totalLength);
  }
};

La distancia será un resultado entre 0 y 1. 0 si todos los caracteres coinciden 1 si no coincide ninguno.

Uno de los principales motivos para calcular esta distancia es agrupar lemas similares, ya que tras al lematización los lemas obtenidos para palabras de la misma familia no siempre son el exactamente el mismo, Tras realizar varias pruebas recomendaría que se consideren similares los lemas con una distancia de hasta 0.20-0.25.

Usando la librería jsEStemmer:


var stemmer = new jsEStemmer.stemmer();
var lemma1 = stemmer.stemWord(word1);
var lemma2 = stemmer.stemWord(word2);
var distance = stemmer.distance(lemma1, lemma2);