Universidad Internacional de La Rioja (UNIR) - Máster Universitario en Inteligencia Artificial - Procesamiento del Lenguaje Natural
Datos del alumno (Cristian Camilo Garzon Tamayo):
Fecha: 04-May-2022
Trabajo: Etiquetado morfosintáctico
Objetivos
Con esta actividad se tratará de que el alumno consiga aplicar un método basado en modelos ocultos de Markov (HMM) para realizar el etiquetado morfosintáctico de una oración.
Descripción
En esta actividad debes implementar en Python un etiquetador morfosintáctico basado en modelos ocultos de Markov (HMM) y realizar el etiquetado morfosintáctico de la oración:
Habla con el enfermo grave de trasplantes.
Implementando también en Python el algoritmo de Viterbi.
Parte 1: Construir el etiquetador morfosintáctico
En esta primera parte de la actividad tienes que implementar en Python el etiquetador morfosintáctico basado en un HMM bigrama a partir de un corpus etiquetado.
Para ello debes utilizar el corpus mia07_t3_tra_Corpus-tagged, que se encuentra disponible en el aula virtual.
El corpus se compone de frases en español etiquetadas con conocimiento sobre las partes de la oración (categorías gramaticales o POS tags). Estas frases etiquetadas han sido extraídas de algunos documentos que forman parte de Wikicorpus, un corpus trilingüe (español, catalán e inglés) compuesto por más de 750 millones de palabras. Wikicorpus fue creado por investigadores de la Universitat Politèncnica de Catalunya a partir de documentos de la Wikipedia que fueron anotados con la librería opensource FreeLing.
La tabla 1 muestra en formato de texto plano y sin etiquetar algunos ejemplos de frases que componen el corpus. De hecho, también se indica el identificador del documento del cual han sido extraídas las frases etiquetadas.
La versión anotada la conforma el corpus anotado proporcionado para realizar esta actividad. El formato del fichero de texto que contiene el corpus es el mismo que el utilizado en Wikicorpus. Por lo tanto, cada uno de los documentos de Wikipedia se identifica con el tag XML
Además, cada una de las frases en el documento viene separada por una línea en blanco. La información relativa a cada palabra de la frase se representa en una nueva línea del fichero. Para cada palabra, es decir, en cada línea del fichero, se proporciona —además del token que representa a la propia palabra— su lema, la etiqueta gramatical (POS tag) asociada a la palabra y el sentido de esta.
La figura 1 muestra una captura del corpus anotado, donde se observa la frase «Tristana es una película del director español nacionalizado mexicano Luis Buñuel.» perteneciente al documento de Wikicorpus con identificador 27315 y titulado Tristana.
Si se analizan las anotaciones para la palabra «es», se observa que su lema es «ser», que la categoría gramatical a la que pertenece esa palabra es la identificada por la etiqueta gramatical «VSIP3S0» y que el sentido de la palabra es el identificado por el código «01775973175».
También se observa que la palabra «del» en la frase se representa en dos líneas y se anota con dos tokens, el primero «de» y el segundo «el». Esto se debe a que la palabra «del» es la contracción de la preposición «de» y el artículo «el». Por el contrario, el nombre propio «Luis Buñuel», que está formado por dos palabras (el nombre «Luis» y el apellido «Buñuel»), se anota como un único token «luis_buñuel». Además, se observa que el punto final de la frase también viene anotado como un token «.».
Aunque el corpus anotado proporciona más información (ver figura 1), es importante tener en cuenta de que para realizar esta actividad solo será necesario el token y la etiqueta gramatical (POS tag) de cada palabra; es decir, la información contenida en la primera y la tercera cadena de cada línea que representa una palabra en el corpus anotado.
Las etiquetas gramaticales (POS tags) utilizadas para anotar la información morfosintáctica del corpus son las definidas en FreeLing y se basan en EAGLES, una recomendación para la anotación de la mayoría de las lenguas europeas. La definición del conjunto de etiquetas gramaticales (POS tags) utilizadas por FreeLing en el etiquetado de un corpus en español se puede consultar en la web.
Accede al recurso a través del aula virtual o desde la siguiente dirección web: https://freeling-user-manual.readthedocs.io/en/v4.1/tagsets/tagset-es/
Las etiquetas gramaticales de EAGLES utilizadas por FreeLing son de longitud variable, donde cada carácter corresponde a una característica morfosintáctica. El primer carácter en la etiqueta es siempre la categoría gramatical o parte de la oración. Esa categoría gramatical determina la longitud de la etiqueta y la interpretación de cada uno del resto de caracteres en la misma.
La definición de la etiqueta para la categoría gramatical «verbo» se muestra en la tabla 2. Entonces, la etiqueta «VSIP3S0», con la que ha sido etiquetada la palabra «es» en la frase que se presentó anteriormente, se interpreta de la siguiente forma: se refiere a un verbo (V) de tipo semiauxiliar (S) en modo indicativo (I) y en tiempo presente (P) para la tercera persona (3) de (número) singular (S). Asimismo, el carácter «0» al final de la etiqueta indica que esta forma verbal no tiene género.
Es importante destacar que para realizar la actividad se deben utilizar las etiquetas con las que se anota el corpus en formato EAGLES; por ejemplo, «VSIP3S0».
Importante: Si se utilizan otras etiquetas la actividad será considerada incorrecta y puntuada con cero puntos.
Para construir el etiquetador morfosintáctico a partir del corpus etiquetado con los datos de entrenamiento, deberás seguir los siguientes pasos:
Nota: Presenta en el envío de la actividad la tabla (guardada en formato de hoja de cálculo de Microsoft Excel (.xlsx) o equivalente) con las probabilidades de emisión y las de transición, calculadas para todas las etiquetas y tokens (palabras) que aparecen en el corpus.
En primer lugar se va a cargar el corpus leyendo el archivo y recuperando la información de la primera y tercera columna de cada registro que continen el token de la palabra y la etiqueta, respectivamente.
Estos valores se almacenarán en objetos de la clase Palabra
.
Esta clase permitirá recuperar el Token()
y el Tag()
fácilmente para cada registro.
class Palabra:
'''
Clase para guardar el token y la etiqueta de una palabra de un corpus
'''
def __init__(self, token: str, tag: str):
'''
Constructor de la clase
token : str
Token de la palabra
tag : str
Etiqueta de la palabra
'''
self._token = token
self._tag = tag
def Token(self):
'''
Método para acceder al token de la palabra
'''
return self._token
def Tag(self):
'''
Método par acceder a la etiqueta de la palabra
'''
return self._tag
El corpus se guardará como una lista que a su vez contiene una serie de listas de objetos del tipo Palabra
. Cada una de las listas de objetos del tipo Palabra
guarda una oración.
archivo = open('mia07_t3_tra_Corpus-tagged.txt', "r")
corpus = list()
oracion_actual = list()
for entrada in archivo.readlines():
entrada = entrada.split()
if len(entrada) == 0:
# Puede ser la primera oración del documento
# O que termina la oración
if len(oracion_actual) > 0:
# Fin de la oración
corpus.append(oracion_actual)
oracion_actual = list()
continue
elif entrada[0] == '<doc':
# Inicio de documento. No se hace nada
continue
elif entrada[0] == '</doc>':
# Fin del documento. No se hace nada
continue
oracion_actual.append(Palabra(token=entrada[0], tag=entrada[2]))
archivo.close()
corpus
El siguiente código te permite imprimir el corpus:
for oracion in corpus:
for palabra in oracion:
print(palabra.Token(), palabra.Tag())
Una vez se dispone del corpus
correctamente cargado se creará un objeto, hmmbigrama
de la clase HMMBigrama
.
hmmbigrama
permitirá hacer el cálculo de las tablas de probabilidades de transición y de emisión.
#Se usa pandas para crear las tablas.
import pandas as pd
class HMMBigrama:
'''
Clase para obtener las matrices de probabilidad HMM Bigrama a partir de un corpus
'''
def __init__(self, corpus: [[Palabra]]):
'''
Constructor de la clase para calcular el Modelo Oculto de Markov Bigrama
'''
self._corpus = corpus
self._estados = dict()
self._tokens = dict()
self._q0 = 'q0'
self._qF = 'qF'
self._prob_trans = pd.DataFrame()
self._prob_obs = pd.DataFrame()
def Corpus(self):
return self._corpus.copy()
def EstadoInicial(self):
return self._q0
def EstadoFinal(self):
return self._qF
def _ProcesarCorpus(self):
'''
Método para contar el número de ocurrencias de estados y tokens
'''
for oracion in self._corpus:
for palabra in oracion:
# Se recorren todas las palabras de todas las oraciones del corpus recuperando las etiquetas (estados)
estado = palabra.Tag()
estados = self._estados
estados[estado] = estados[estado] + 1 if estado in estados else 1
# Se recorren todas las palabras de todas las oraciones del corpus recuperando los tokens
token = palabra.Token()
tokens = self._tokens
tokens[token] = tokens[token] + 1 if token in tokens else 1
def Estados(self, incluir_inicial: bool = False, incluir_final: bool = False):
'''
Devuelve los estados del bigrama en base al corpus proporcionado al constructor
incluir_inicial : bool (False)
Flag para indicar si se quiere recuperar el estado inicial
incluir_final : bool (False)
Flag para indicar si se quiere recuperar el estado final
return
Diccionario de estados con el número de ocurrencias de cada estado en el corpus
'''
if len(self._estados) == 0:
self._ProcesarCorpus()
copia_estados = dict()
if incluir_inicial:
# Hay tantos estados como oraciones en el corpus
copia_estados[self._q0] = len(self._corpus)
copia_estados.update(self._estados)
if incluir_final:
# Hay tantos estados como oraciones en el corpus
copia_estados[self._qF] = len(self._corpus)
return copia_estados
def Tokens(self):
'''
Devuelve los tokens del bigrama en base al corpus proporcionado al constructor
return
Diccionario de tokens con el número de ocurrencias de cada token en el corpus
'''
if len(self._tokens) == 0:
self._ProcesarCorpus()
return self._tokens.copy()
def ProbabilidadesDeTransicion(self):
'''
Método para calcular las probabilidades de transición bigrama
a partir del corpus proporcionado a la clase
'''
# Si ya se ha calculado se devuelve
if len(self._prob_trans) != 0:
return self._prob_trans.copy()
'''
En esta parte del código se calcula el número de
transiciones bigrama, es decir, en el diccionario
'contador_transiciones' se almacenarán los contadores
de las transiciones t-1 -> t
Las claves del diccionario serán los estados de partida
mientras que los valores de cada clave serán los estados
de destino y el número de veces que transitan a cada estado
'''
q0 = self._q0
qF = self._qF
contador_transiciones = {q0: dict()}
for oracion in self._corpus:
# Contador de transición q0 a estado q1
q1 = oracion[0].Tag()
if q1 not in contador_transiciones[q0]:
contador_transiciones[q0][q1] = 0
contador_transiciones[q0][q1] += 1
# Contador de transiciones entre palabras de la oración
for it in range(0, len(oracion) - 1):
##################################################
########## Aquí debes incluir tu código ##########
##################################################
# Para el cálculo de las probabilidades en la matriz de transición, Se incluye el conteo de los bigramas para cada tag 'categoría gramatical'cgarz
q_1 = oracion[it].Tag()
q = oracion[it+1].Tag()
if(q_1 not in contador_transiciones):
contador_transiciones[q_1] = {}
if(q not in contador_transiciones[q_1]):
contador_transiciones[q_1][q] = 0
contador_transiciones[q_1][q] += 1
##################################################
# Contador de transición qF_1 a qF
qF_1 = oracion[-1].Tag()
if qF_1 not in contador_transiciones:
contador_transiciones[qF_1] = dict()
if qF not in contador_transiciones[qF_1]:
contador_transiciones[qF_1][qF] = 0
contador_transiciones[qF_1][qF] += 1
'''
Cálculo de la tabla de probabilidades de transición.
Se calculan ahora las probabilidades de transición
siguiendo la relación: P(T|T-1) = C(T-1, T) / C(T-1).
En 'contador_transiciones' se han acumulado la coincidencias C(T-1, T)
y en 'estados' se tiene disponible C(T-1) por lo que es posible
calcular la tabla de probabilidades de transiciones con estos elementos.
'''
tags_estados_iniciales = list(
self.Estados(incluir_inicial=True).keys())
tags_estados_finales = list(self.Estados(incluir_final=True).keys())
estados_totales = self.Estados(
incluir_inicial=True, incluir_final=True)
prob_trans = {qt_1: {qt: 0 for qt in tags_estados_finales}
for qt_1 in tags_estados_iniciales}
for qt_1 in tags_estados_iniciales:
for qt in tags_estados_finales:
prob = 0
if qt_1 in contador_transiciones and qt in contador_transiciones[qt_1]:
##################################################
########## Aquí debes incluir tu código ##########
##################################################
# Para calcular la probabilidad de transiciones, se incluye para cada posición de la matriz de transiciones, y valida el conteo de las categorías y los bigramas gramaticales.
cont_qt_1_qt = int(contador_transiciones[qt_1][qt])
cont_qt_1 = int(estados_totales[qt_1])
if(cont_qt_1 != 0):
prob = cont_qt_1_qt / cont_qt_1
##################################################
prob_trans[qt_1][qt] = prob
self._prob_trans = pd.DataFrame.from_dict(prob_trans, orient='index')
return self._prob_trans.copy()
def ProbabilidadesDeEmision(self):
'''
Método para calcular las probabilidades de emisión
a partir del corpus proporcionado a la clase
'''
if len(self._prob_obs) != 0:
return self._prob_obs.copy()
'''
En esta parte del código se calculan el número de
ocurrencias de la palabra Wi para la etiqueta Ti
'''
estados = self.Estados()
contador_observaciones = {key: dict() for key in estados.keys()}
for oracion in self._corpus:
for palabra in oracion:
token = palabra.Token()
etiqueta = palabra.Tag()
if token not in contador_observaciones[etiqueta]:
##################################################
########## Aquí debes incluir tu código ##########
##################################################
# Se inicializa el contador en 0
contador_observaciones[etiqueta][token] = 0
##################################################
contador_observaciones[etiqueta][token] += 1
'''
Cálculo de la tabla de probabilidades de emisión.
Se calculan ahora las probabilidades de emisión
siguiendo la relación: P(Wi|Ti) = C(Ti,Wi) / C(Ti).
En 'contador_observaciones' se han acumulado la coincidencias C(Ti, Wi)
y en 'estados' se tiene disponible C(Ti) por lo que es posible
calcular la tabla de probabilidad de emisión con estos elementos.
'''
tokens = self.Tokens()
prob_obs = {Ti: {Wi: 0 for Wi in tokens} for Ti in estados}
for Ti in estados:
for Wi in tokens:
prob = 0
if Ti in contador_observaciones and Wi in contador_observaciones[Ti]:
##################################################
########## Aquí debes incluir tu código ##########
##################################################
# Por cada categoría, se realiza el conteo de observaciones de las palabras y se divide por el conteo por categoría para calcular, posterior asignar las probabilidades de emisión.
cont_tiwi = int(contador_observaciones[Ti][Wi])
cont_ti = int(estados[Ti])
if(cont_ti != 0):
prob = cont_tiwi / cont_ti
##################################################
prob_obs[Ti][Wi] = prob
self._prob_obs = pd.DataFrame.from_dict(prob_obs, orient='index')
return self._prob_obs
El siguiente código te permite crear el HMM Bigrama y obtener información relevante:
hmmbigrama = HMMBigrama(corpus)
hmmbigrama.Tokens()
len(hmmbigrama.Tokens())
hmmbigrama.Estados()
len(hmmbigrama.Estados())
El método ProbabilidadesDeTransición()
de la clase HMMBigrama
devuelve la tabla de probabilidades de transición.
def non_zero_green(val):
'''
Función para resaltar en verde las probabilidades que no sean 0
'''
return 'background-color: Aquamarine' if val > 0 else ''
prob_transicion = hmmbigrama.ProbabilidadesDeTransicion()
'''Matriz de probabilidades de transmisión para las categorías gramaticales del corpus.'''
prob_transicion.style.applymap(non_zero_green)
prob_transicion.to_excel('mia07_t3_tra_resultados_trans.xlsx', sheet_name='prob_trans')
El método ProbabilidadesDeEmision()
de la clase HMMBigrama
devuelve la tabla de probabilidades de emisión.
prob_emision = hmmbigrama.ProbabilidadesDeEmision()
'''Matriz de probabilidades de emisión (Probabilidades de que las palabras pertenezcan a una categoría gramatical'''
prob_emision.style.applymap(non_zero_green)
prob_emision = hmmbigrama.ProbabilidadesDeEmision()
'''Matriz de probabilidades de emisión (Probabilidades de que las palabras pertenezcan a una categoría gramatical'''
prob_emision.style.applymap(non_zero_green)
prob_emision.to_excel('mia07_t3_tra_resultados_emision.xlsx', sheet_name='prob_emision')
Parte 2: Etiquetar morfosintácticamente una oración
En esta segunda parte de la actividad tienes que implementar en Python un programa que permita calcular la mejor secuencia de etiquetas para una oración, dicho de otro modo, realizar el etiquetado morfosintáctico de la oración: «Habla con el enfermo grave de trasplantes. ».
Para ello debes utilizar el etiquetador que has construido en la parte 1 de esta actividad, es decir las tablas de probabilidades calculadas, y aplicar el algoritmo de Viterbi.
Para aplicar el algoritmo de Viterbi, se deben seguir los siguientes pasos:
Calcular la matriz de probabilidades de la ruta se Viterbi (matriz con los valores de Viterbi) donde se representen claramente las observaciones y los estados de la máquina de estados finitos. Calcula el valor de Viterbi para cada celda de la matriz e indica claramente los valores obtenidos. Nota: Para simplificar, puedes eliminar todos aquellos estados asociados a etiquetas que no aparezcan en el posible análisis de la oración y sólo quedarte con los estados relevantes. Además, debes tener en cuenta la transición al estado final representado por el punto al final de la oración a analizar.
Obtener la ruta con máxima probabilidad, es decir, traza la ruta inversa para obtener la mejor secuencia de etiquetas.
Mostrar la oración etiquetada. Debes indicar claramente el resultado obtenido del etiquetado morfosintáctico de la oración estudiada.
Nota: Presenta en el envío de la actividad la tabla (guardada en formato de hoja de cálculo de Microsoft Excel (.xlsx) o equivalente) con la matriz de probabilidades de la ruta Viterbi para el etiquetado morfosintáctico de la oración «Habla con el enfermo grave de trasplantes. ».
La clase Viterbi
permitirá realizar el cálculo de la matriz de probabilidades de la ruta de Viterbi y la posterior decodificación de la secuencia óptima de etiquetado para una oración a analizar.
El etiquetado morfosintáctico creado en la Parte 1, es decir el objeto hmmbigrama
de la clase HMMBigrama
, será proporcionado al objeto viterbi
de la clase Viterbi
para poder aplicar el Algoritmo de Viterbi.
El cálculo de los valores de Viterbi se realiza en el método Probabilidades()
de la clase Viterbi
.
El método DecodificacionSecuenciaOptima()
de la clase Viterbi
permite obtener la secuencia de etiquetas más probables para la oración a analizar.
class Viterbi:
'''
Algoritmo de Viterbi para obtener las mejores
etiquetas de las palabras de una oración
'''
def __init__(self, hmmbigrama: HMMBigrama, oracion: str):
self._hmmbigrama = hmmbigrama
self._oracion = oracion
self._estados_relevantes = None
self._prob_viterbi = pd.DataFrame()
self._estado_max_anterior = None
def _CalculoEstadosRelevantes(self):
self._estados_relevantes = set()
for palabra_analizar in [x.lower() for x in self._oracion.split()]:
# Búsqueda de estados
for oracion in self._hmmbigrama.Corpus():
for palabra_corpus in oracion:
if palabra_corpus.Token() == palabra_analizar:
self._estados_relevantes.add(palabra_corpus.Tag())
def Probabilidades(self):
if len(self._prob_viterbi) != 0:
return self._prob_viterbi.copy()
if not self._estados_relevantes:
self._CalculoEstadosRelevantes()
estados_relevantes = self._estados_relevantes
'''
Matriz en la que se guardan los valores de Viterbi
'''
matriz_viterbi = {q: dict() for q in estados_relevantes}
'''
Matriz asociada a la matriz de Viterbi en la que se almacena
el estado de origen que maximiza cada probabilidad
'''
self._estado_max_anterior = {q: dict() for q in estados_relevantes}
q0 = self._hmmbigrama.EstadoInicial()
prob_trans = self._hmmbigrama.ProbabilidadesDeTransicion()
prob_obs = self._hmmbigrama.ProbabilidadesDeEmision()
token_anterior = None
for token in [x.lower() for x in self._oracion.split()]:
for qDestino in estados_relevantes:
prob_max = 0
if not token_anterior:
# Estado q0
prob_max = prob_trans[qDestino][q0]
else:
# Resto de estados
for qOrigen in estados_relevantes:
##################################################
########## Aquí debes incluir tu código ##########
##################################################
# Para la segunda columna en adelante se realiza el cálculo de probabilidades de Viterbi. Se obtiene el valor de Viterbi de la columna anterior, posteriormente se multiplica el valor de Viterbi (v_viterbi) por la probabilidad de transmisión
v_viterbi = matriz_viterbi[qOrigen][token_anterior]
prob_qOrigen = prob_trans[qDestino][qOrigen] * v_viterbi
##################################################
if prob_qOrigen > prob_max:
##################################################
########## Aquí debes incluir tu código ##########
##################################################
# Una vez identificado el valor máximo de Viterbi, y se almacena en self._estado_max_anterior para obtener la probabilidad en el cálculo de la mejor ruta
prob_max = prob_qOrigen
self._estado_max_anterior[token_anterior] = [qDestino,qOrigen]
##################################################
matriz_viterbi[qDestino][token] = prob_max * prob_obs[token][qDestino]
token_anterior = token
self._prob_viterbi = pd.DataFrame.from_dict(matriz_viterbi, orient='index')
return self._prob_viterbi.copy()
def DecodificacionSecuenciaOptima(self):
# Decodificación de la secuencia óptima
oracion_invertida = [x.lower() for x in self._oracion.split()]
oracion_invertida.reverse()
prob_viterbi = self.Probabilidades()
oracion_etiquetada = []
# Se busca la probablidad máxima de Viterbi asociada a la última palabra de la oración
palabra = oracion_invertida[0]
etiqueta = prob_viterbi[palabra].idxmax()
oracion_etiquetada.append({'token': palabra, 'tag': etiqueta, 'prob': prob_viterbi[palabra].max()})
# Ahora se usa la tabla auxiliar de Viterbi que contiene
# el estado de origen que maximiza cada probabilidad Viterbi
palabra_anterior = palabra
for palabra in oracion_invertida[1:]:
##################################################
########## Aquí debes incluir tu código ##########
##################################################
# Con self._estado_max_anterior Para la oración en orden invertido e identificar la etiqueta que valida (valor_viterbi * prob_transición).
etiqueta = self._estado_max_anterior[palabra][1]
probabilidad = prob_viterbi[palabra][etiqueta]
oracion_etiquetada.append({'token': palabra, 'tag': etiqueta, 'prob': probabilidad})
##################################################
# Se recupera el orden de la oración con las palabras ya etiquetadas
oracion_etiquetada.reverse()
return oracion_etiquetada
El siguiente código te permite realizar el análisis de la oración: "Habla con el enfermo grave de trasplantes."
viterbi = Viterbi(hmmbigrama=hmmbigrama, oracion='Habla con el enfermo grave de trasplantes .')
El siguiente código te permite mostrar la matriz de probabilidades de la ruta de Viterbi (solo se presentan aquellas etiquetas que tienen algún valor no nulo para alguna de las palabras de la oración analizada).
matriz_prob_viterbi = viterbi.Probabilidades()
'''Matriz de probabilidades de Viterbi'''
matriz_prob_viterbi.style.applymap(non_zero_green)
matriz_prob_viterbi.to_excel('mia07_t3_tra_resultados_viterbi.xlsx', sheet_name='viterbi')
El siguiente código te permite mostrar la ruta de Viterbi con máxima probabilidad
oracion_etiquetada = viterbi.DecodificacionSecuenciaOptima()
oracion_etiquetada
El siguiente código te permite mostrar la oración etiquetada
for palabra in oracion_etiquetada:
print('{} / {}'.format(palabra['token'], palabra['tag']))
Parte 3: Analizar el etiquetador morfosintáctico
Una vez hayas creado el etiquetador morfosintáctico y lo hayas utilizado para etiquetar la oración «Habla con el enfermo grave de trasplantes.», reflexiona sobre los resultados obtenidos, interprétalos y analiza el rendimiento del etiquetador creado y sus limitaciones. Para ello responde de forma razonada a las siguientes preguntas:
Respecto a la oración «Habla con el enfermo grave de trasplantes» se obtuvo el etiquetado automático:
A pesar de que en el corpus la palabra 'habla' tiene una mayor ocurrencia como verbo, el etiquetador morfosintáctico no realizó correctamente la clasificación gramatical debido a que lo clasificó como un 'sustantivo'
Para la oración «El enfermo grave habla de trasplantes.» se obtuvo el etiquetado automático: