El parseo morfológico consiste en analizar la estructura de una palabra para obtener de ella información semántica. En particular, se busca obtener información morfológica; es decir, queremos describir una palabra a partir de los morfemas que la conforman. En este sentido, buscamos etiquetar los morfemas con la descripción de éstos. Así, si pensamos en español, tenemos morfemas de género (masculino y femenino) o de número (singular y plural) para los sustantivos. En el caso de los verbos, podemos tener morfemas de tiempo (presente, pasado, futuro, imperfecto, etc.) de número y persona, entre otros.
La dificultad de realizar este etiquetado a partir de transductores finitos es que cada tipo de palabra (sustantivo, verbo, adverbio, artículo, conjunción, etc.) tiene una estructura particular. Además de que debemos definir una serie de reglas para cada tipo de palabras, debemos contar con los conocimientos suficientes sobre la estructura de las palabras.
Por ejemplo, en español, notamos que la mayoría de los fenómenos morfológicos están presentes en la derecha de la cadena; esto es, se trata de sufijos. Así, podemos pensar que, en general, las palabras en español son de la forma:
$$base + sufijos$$Sin embargo, existen lenguas en donde predominan los prefijos e, incluso, aquellas en las que existen tanto prefijos como sufijos de forma equivalente. En este sentido, este tipo de métodos son dependientes del lenguaje.
import re
A continuación, creamos un pequeño transductor para estudiar los sustantivos en español. Para esto, tomamoss algunas palabras como ejemplos. A partir de estas palabras podemos deducir patrones que nos ayuden a crear el transductor.
#Lista de palabras
words = ['niño','niños', 'niñas','niñitos', 'gato', 'gatos', 'gatitos', 'paloma','palomita','palomas',
'flores','flor','florecita','lápiz','lápices']
Si observamos detalladamente, y a partir de nuestro conocimiento del español, podemos ver que estos sustantivos cuentan con categorías de género (GEN) y número (NUM), pero además algunos tienen una lectura de diminutivo (DIM). Estas categorías se presentan en la palabra a partir de subcadenas. Estas subcadenas además presentan un orden establecido. Así, analizando cada palabra podemos ver que se tiene una estructura de la forma:
$$Base + DIM + GEN + NUM$$La base es la parte de la palabra que aporta el significado central, por lo que buscamos conservarla. Algo que es importante notar es que el diminutivo (DIM) puede presentarse de varias formas: como la subcadena $it$, como la subcadena $ecit$ o como el elemento nulo $\epsilon$ (en este caso, diremos que no hay diminutivo). El género (GEN) también puede presentarse de distintas formas, pero cada una de estas formas conlleva un cmabio de significado; así la subcadena $a$ implica género femenino (FEM), mientras que la subcadena $o$ implica género masculino (MSC). De igual forma, puede darse el caso donde haya un elemento $\epsilon$ (en estos casos, es difícil determminar el género). Finalmente, el número (NUM) puede mostrar otras variaciones. Si se tiene un elemento $\epsilon$ podemos pensar que hay número singular. Por su parte, el número plural (PL) se da a partir de la presencia de una subcadena $s$ o $es$.
En este sentido, podemos definir una lista de sufijos de este lenguaje $L$. Esta lista está dada como:
$$L/S = \{\epsilon, ecit, it, a, o, es, s\}$$Un esbozo del transductor que se puede definir para parsear este lenguaje es el siguiente:
$$(q_0, [\^ \_]+, [\^ \_ ]+) = q_1 \\ (q_1, [ecit|it], DIM) = q_2 \\ (q_2, o, MSC) = q_3 \\ (q_2,a,FEM) = q_3 \\ (q_3, [s|es],PL) = q_4$$Este transductor no es del todo preciso, pero da una idea de lo que buscamos, a continuación definimos un transductor que pueda parsear este lenguaje.
#Lista para guardar las palabras parseadas
morph_parsing = []
for w in words+['perritos']:
#ecit -> DIM
R0 = re.sub(r'([^ ]+)ecit([a|o|as|os])',r'\1-DIM\2',w)
#it -> DIM
R1 = re.sub(r'([^ ]+)it([a|o|as|os])',r'\1-DIM\2',R0)
#a(s) -> FEM
R2 = re.sub(r'([^ ]+)a(s)',r'\1-FEM\2',R1)
#a -> FEM
R3 = re.sub(r'([^ ]+)a\b',r'\1-FEM',R2)
#o(s) -> MSC
R4 = re.sub(r'([^ ]+)o(s)',r'\1-MSC\2',R3)
#o .> MSC
R5 = re.sub(r'([^ ]+)o\b',r'\1-MSC',R4)
#es -> PL
R6 = re.sub(r'([^ ]+)es\b',r'\1-PL',R5)
#s -> PL
R7 = re.sub(r'([^ ]+)s\b',r'\1-PL',R6)
#Sustituye la c por z cuando es necesario
parse = re.sub(r'c-',r'z-',R7)
#Guarda los parseos
morph_parsing.append(parse)
print(w,'-->',parse)
niño --> niñ-MSC niños --> niñ-MSC-PL niñas --> niñ-FEM-PL niñitos --> niñ-DIM-MSC-PL gato --> gat-MSC gatos --> gat-MSC-PL gatitos --> gat-DIM-MSC-PL paloma --> palom-FEM palomita --> palom-DIM-FEM palomas --> palom-FEM-PL flores --> flor-PL flor --> flor florecita --> flor-DIM-FEM lápiz --> lápiz lápices --> lápiz-PL perritos --> perr-DIM-MSC-PL
El stemming puede realizarse a partir de transducir los patrones hacia elementos vacios $\epsilon$. Del ejemplo anterior, por ejemplo, podemos eliminar las etiquetas para generar un stemmer de este tipo de sustantivos
#Lista para guardar las palabras stemizadas
stems = []
for w in words+['perritos']:
#ecit -> e
R0 = re.sub(r'([^ ]+)ecit([a|o|as|os])',r'\1\2',w)
#it -> e
R1 = re.sub(r'([^ ]+)it([a|o|as|os])',r'\1\2',R0)
#a(s) -> e
R2 = re.sub(r'([^ ]+)a(s)',r'\1\2',R1)
#a -> e
R3 = re.sub(r'([^ ]+)a\b',r'\1',R2)
#o(s) -> e
R4 = re.sub(r'([^ ]+)o(s)',r'\1\2',R3)
#o .> e
R5 = re.sub(r'([^ ]+)o\b',r'\1',R4)
#es -> e
R6 = re.sub(r'([^ ]+)es\b',r'\1',R5)
#s -> e
R7 = re.sub(r'([^ ]+)s\b',r'\1',R6)
#Sustituye la c por z cuando es necesario
stem = R7
#Guarda los stems
stems.append(stem)
print(w,'-->',stem)
niño --> niñ niños --> niñ niñas --> niñ niñitos --> niñ gato --> gat gatos --> gat gatitos --> gat paloma --> palom palomita --> palom palomas --> palom flores --> flor flor --> flor florecita --> flor lápiz --> lápiz lápices --> lápic perritos --> perr
La lematización es un proceso difícil de realizar pues la mayoría de las veces se realiza a partir de diccionarios. En estos diccionarios no se guardan todas los tipos, sino sólo los stems. Así, reducimos las posibiles formas que puede tomar un mismo tipo. Por tanto, la mayoría de las veces será necesario stemmizar (o parsear) antes de realizar una lematización.
Como con el transductor anterior hemos obtenido los stems de las palabras (además, de sus etiquetas morfológicas) podemos aplicar un proceso de lematización; para esto, definimos el siguiente diccionario.
lemas = {'gat':'gato','niñ':'niño', 'palom':'paloma'}
Finalmente, sustituimos el stem por el lema cuando es necesario:
#lista para guardar lemas parseados
lema_parsing = []
for p in morph_parsing:
try:
#Busca el stem en la cadena parseada
stem = re.match(r'[^-]+',p).group(0)
#stem -> lema
lema = p.replace(stem, lemas[stem])
except:
#Si la palabra no tiene tags, se deja tal como está
lema = p
#Guarda los lemas
lema_parsing.append(lema)
print(lema)
niño-MSC niño-MSC-PL niño-FEM-PL niño-DIM-MSC-PL gato-MSC gato-MSC-PL gato-DIM-MSC-PL paloma-FEM paloma-DIM-FEM paloma-FEM-PL flor-PL flor flor-DIM-FEM lápiz lápiz-PL perr-DIM-MSC-PL
Además de servir para parsear morfológicamente las palabras, los transductores finitos (en tanto gramáticas) nos pueden servir para generar lenguaje. Así, dada una forma "profunda" (es decir, un lema o stem con las etiqutas de los morfemas que bsucamos) se puede obtener una palabra con sus morfemas correspondientes. Esto parecería fácil, pues sabemos que los afijos están relacionados a las etiquetas. Sin embargo, la dificultad viene cuando una misma etiqueta puede presentar diferentes afijos. Las lenguas naturales, empero, cuentan con patrones que nos ayudan a predecir cuando se presenta un afijo u otro. Así, sabemos que en español por ejemplo, el plural (PL) se presenta con un sufijo $s$ cuando el lema termina en vocal, y se presenta un sufijo $es$ cuando termina en alguna consonante o en $í$.
De esta forma, podemos realizar un transductor que genere lenguaje. En este caso, es un lenguaje muy sencillo, el de los sustantivos. Queremos que dado un lema más los morfemas que queremos que realice la palabra correspondiente con sus sufijos adecuados. En este caso, contamos con un número de etiquetas morfológicas bien definidas (DIM, FEM, MSC y PL).
def generate_noun(l):
#Dim -> ecit
C1 = re.sub(r'([^-]+[r|n])-DIM(.+)',r'\1cit\2',l)
#DIM -> it
C2 = re.sub(r'([^-]+)[a|e|i|o|u]-DIM(.+)',r'\1it\2',C1)
#C9 = re.sub(r'([^-]+l)-DIM',r'\1ito',l)
#FEM -> a
C3 = re.sub(r'([^-]+t)-FEM(.*)',r'\1a\2',C2)
C4 = re.sub(r'([^-]+)[o|a]-FEM(.*)',r'\1a\2',C3)
#MSC -> o
C5 = re.sub(r'([^-]+t)-MSC(.*)',r'\1o\2',C4)
C6 = re.sub(r'([^-]+)[o|a]-MSC(.*)',r'\1o\2',C5)
#PL -> es
C7 = re.sub(r'([^-]+[d|j|l|r|n|z|í])-PL',r'\1es',C6)
#PL -> s
C8 = re.sub(r'([^-]+[a|e|i|o|u])-PL',r'\1s',C7)
#zPL -> cPL
word = re.sub(r'(.+)z(es)\b',r'\1c\2',C8)
return word
Ahora podemos poner a prueba el transductor que hemos definido con la lista de formas parseadas.
for lema in lema_parsing:
reinflection = generate_noun(lema)
print(lema,'-->',reinflection)
niño-MSC --> niño niño-MSC-PL --> niños niño-FEM-PL --> niñas niño-DIM-MSC-PL --> niñitos gato-MSC --> gato gato-MSC-PL --> gatos gato-DIM-MSC-PL --> gatitos paloma-FEM --> paloma paloma-DIM-FEM --> palomita paloma-FEM-PL --> palomas flor-PL --> flores flor --> flor flor-DIM-FEM --> florcita lápiz --> lápiz lápiz-PL --> lápices perr-DIM-MSC-PL --> perrcitos
Además, podemos poner a prueba su capacidad de generalizar utilizando otras palabras.
print(generate_noun('rubí-PL'))
print(generate_noun('callo-DIM-MSC-PL'))
print(generate_noun('pan-DIM-MSC-PL'))
print(generate_noun('árbol-PL'))
print(generate_noun('carcaj-PL'))
rubíes callitos pancitos árboles carcajes
print(generate_noun('pan-DIM-MSC'))
pancito
from nltk.stem import SnowBallStemmer
--------------------------------------------------------------------------- ImportError Traceback (most recent call last) <ipython-input-52-a41934a43157> in <module> ----> 1 from nltk.stem import SnowBallStemmer ImportError: cannot import name 'SnowBallStemmer' from 'nltk.stem' (/home/mijangos/.local/lib/python3.8/site-packages/nltk/stem/__init__.py)
Claramente, se trata de un transductor muy básicos. Varios problemas surgen: por ejemplo, la forma 'pan-DIM-MSC' puede ser 'panecito', pero tmabién 'pancito'. Este transductor, además, dejaría de lado caso como 'árbol-DIM-MSC' o similares. ¿Qué reglas se podrían agregar para hacer este transductor más completo? ¿Cómo sería un transductor que generara conjugaciones de verbos?