Una gramática libre de contexto nos va ayudar a capturar ciertos patrones en la estructura sintáctica de una oración. Con ella podremos hacer que la computadora pueda determinar si un elemento es un sujeto, un objeto directo, un objeto indirecto, un verbo, etc.
Para esto, la librería NLTK nos provee de librerías que nos permitirán hacer este análisis.
from nltk import PCFG
Supóngamos que tenemos una oración y que la hemos tokenizado.
sentence = 'Juan come unos tacos'
tokens = sentence.split()
print(tokens)
['Juan', 'come', 'unos', 'tacos']
Hemos conservado el orden de las palabras y en este caso no hicimos un processo de stemming. El proceso de stemming debe hacerse si tal proceso está definido en la gramática. Como ahora no lo hemos hecho, nuestra gramática trabajará con los tokens y no con los stems de éstos.
Definamos nuestra gramática con base en esto.
grammar = PCFG.fromstring('''
O -> FN FV [0.7]
O -> FV FN [0.3]
FN -> Sust [0.6]
FN -> Det Sust [0.4]
FV -> V FN [0.8]
FV -> FN V [0.2]
Sust -> 'Juan' [0.5]
Sust -> 'tacos' [0.5]
Det -> 'unos' [1.0]
V -> 'come' [1.0]
''')
print(grammar)
Grammar with 10 productions (start state = O) O -> FN FV [0.7] O -> FV FN [0.3] FN -> Sust [0.6] FN -> Det Sust [0.4] FV -> V FN [0.8] FV -> FN V [0.2] Sust -> 'Juan' [0.5] Sust -> 'tacos' [0.5] Det -> 'unos' [1.0] V -> 'come' [1.0]
En este caso hemos definido nuestra gramática y podemos, entonces parsear las oraciones que queramos analizar. Nótese aquí que distinguimos los símobolos terminales con comillas simples '', mientras que los símbolos no terminales no se marcan de esta forma.
Para este parser a partir de la gramática usaremos el algoritmo de Viterbi, el cual revisaremos más adelante.
from nltk import ViterbiParser
parser = ViterbiParser(grammar)
for t in parser.parse(tokens):
print(t)
(O (FN (Sust Juan)) (FV (V come) (FN (Det unos) (Sust tacos)))) (p=0.0336)
En la forma en que hemos definido la gramática podemos realizar otras construcciones para determinar posibles árboles.
for t in parser.parse(['come', 'unos','tacos', 'Juan']):
print(t)
(O (FV (V come) (FN (Det unos) (Sust tacos))) (FN (Sust Juan))) (p=0.0144)
Dada esta gramática una construcción más probable es "Juan come unos tacos", mientras que si bien "unos tacos come Juan" es aceptable, su probabilidad es menor.
Por último, si queremos ver la versión gráfica de este árbol. Podremos usar la siguiente orden:
for t in parser.parse(tokens):
t.draw()