<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Resumen de NLTK
Step2:  Gramáticas Independientes del Contexto (CFG)
Noam Chmosky definió una jerarquía de lenguajes y gramáticas que se utiliza habitualmente en Lingüística e Informática para clasificar lenguajes y gramáticas formales. Cuando queremos modelar fenómenos lingüísticos de las lenguas naturales, el tipo de gramática más adeacuado es el conocido como Tipo 2 o Gramáticas Independientes del Contexto o Context-Free Grammars (CFG) en inglés.
Vamos a definir una gramática simplemente como un conjunto de reglas de reescritura o transformación. Sin entrar en muchos detalles sobre las restricciones que tienen que cumplir las reglas de las gramáticas de Tipo 2, es importante que tengamos en cuenta lo siguiente
Step3:  Fíjate cómo hemos definido nuestra gramática
Step4:  Con el objeto grammar1 ya creado, creamos el analizador con el método nltk.ChatParser.
Step5:  Una vez creado nuestro analizador ya lo podemos utilizar. Tenemos a nuestro alcance el método .parse para analizar sintácticamente cualquier oración que se especifique como una cadena de palabras. Nuestra gramática es bastante limitada, pero podemos utilizarla para analizar la oración I shot an elephant in my pajamas. Si imprimimos el resultado del método, obtenemos el árbol sintáctico.
Step6:  Por si no te has dado cuenta, la oración I shot an elephant in my pajamas es ambigua en inglés
Step7:  Recuerda que para imprimir el árbol sintáctico hay que iterar (con un bucle for, por ejemplo) sobre el objeto que devuelve el método parse() y utilizar la función print.
Step9:  A continuación modifico ligeramente mi gramática g1 para incluir una nueva categoría gramatical PRO y añadir algo de volcabulario nuevo. Compara ambos ejemplos
Step10:  NOTA IMPORTANTE sobre errores y el comportamiento de parse()
Cuando un analizador reconoce todo el vocabulario de una oración de entrada pero es incapaz de analizarla, el método parse() no da error pero devuelve un objeto vacío. En este caso, la oración es agramatical según nuestra gramática.
Step11:  Sin embargo, cuando el analizador no reconoce todo el vocabulario (porque utilizamos una palabra no definida dentro del léxico), el método parse() falla y muestra un mensaje de error de tipo ValueError como el siguiente. Fíjate solo en la última línea
Step13:  Tenlo en cuenta a la hora de detectar errores en tu código. 
Gramáticas en español
Visto un primer ejemplo de CFG, vamos a cambiar de lengua y crear un analizador para oraciones sencillas en español. El procedimiento es el mismo, definimos nuestra gramática en formato de Chomsky en un fichero aparte o en una cadena de texto, la parseamos con el método nltk.CFG.fromstring() y creamos un analizador con el método nltk.ChartParser()
Step15:  Vamos a probar si es capaz de analizar distintas oraciones es español. Para hacerlo más divertido, vamos a guardar varias oraciones separadas por un intro (simbolizado por el metacarácter \n) en una lista de cadenas llamda oraciones. Iteramos sobre esas oraciones, las imprimimos, después las rompemos en listas de palabras (con el método .split()) e imprimimos el resultado de analizarlas con nuestro analizador.
Step18:  Vamos a aumentar la cobertura de nuestra gramática de modo que sea capaz de reconocer y analizar oraciones coordinadas. Para ello, modificamos la regla en la que definimos la oración añadiendo una definición recursivaque defina oración como la secuencia de una oración (O) seguida de una conjunción (Conj) y de otra oración (O). Por último añadimos también algo de léxico nuevo
Step21:  Recuerda que una gramática no es un programa
Step24:  ¿Podemos extender g4 para que reconozca oraciones subordinadas introducidas con verbos de lengua o de pensamiento. Me refiero a oraciones del tipo
<ASSISTANT_TASK:>
Python Code:
from __future__ import print_function
from __future__ import division
import nltk
Explanation: Resumen de NLTK: Análisis sintáctico
Este resumen se corresponde con el capítulo 8 del NLTK Book Analyzing Sentence Structure. La lectura del capítulo es muy recomendable.
En este resumen vamos a repasar cómo crear gramáticas con NLTK y cómo crear herramientas que nos permitan analizar sintácticamente oraciones sencillas.
Para empezar, necesitamos importar el módulo nltk que nos da acceso a todas las funcionalidades:
End of explanation
g1 = 
S -> NP VP
NP -> Det N | Det N PP | 'I'
VP -> V NP | VP PP
PP -> P NP
Det -> 'an' | 'my'
N -> 'elephant' | 'pajamas'
V -> 'shot'
P -> 'in'
Explanation: Gramáticas Independientes del Contexto (CFG)
Noam Chmosky definió una jerarquía de lenguajes y gramáticas que se utiliza habitualmente en Lingüística e Informática para clasificar lenguajes y gramáticas formales. Cuando queremos modelar fenómenos lingüísticos de las lenguas naturales, el tipo de gramática más adeacuado es el conocido como Tipo 2 o Gramáticas Independientes del Contexto o Context-Free Grammars (CFG) en inglés.
Vamos a definir una gramática simplemente como un conjunto de reglas de reescritura o transformación. Sin entrar en muchos detalles sobre las restricciones que tienen que cumplir las reglas de las gramáticas de Tipo 2, es importante que tengamos en cuenta lo siguiente:
Las gramáticas formales manejan dos tipos de alfabetos.
Los símbolos no terminales son los componentes intermedios que utilizamos en las reglas. Todo símbolo no terminal tiene que ser definido como una secuenca de otros símbolos. En nuestro caso, los no terminales van a ser las categorías sintácticas. 
Los símbolos terminales son los componentes finales reconocidos por la gramática. En nuestro caso, los terminales van a ser las palabras de las oraciones que queremos analizar sintácticamente.
Todas las reglas de una gramática formal tienen la forma Símbolo1 -&gt; Símbolo2, Símbolo3... SímboloN y se leen como el Símbolo1 se define/está formado/se reescribe como una secuencia formada por Símbolo2, Símbolo3, etc.
En las gramáticas independientes del contexto, la parte situada a la izquierda de la flecha -&gt; es siempre un único símbolo no terminal.
Gramáticas Generativas en NLTK
Pues bien, para definir nuestras gramáticas en NLTK podemos escribirlas en un fichero aparte o como una cadena de texto siguiendo el formalismo de las gramaticas generativas de Chomsky. Vamos a definir una sencilla gramática capaz de reconocer la famosa frase de los hermanos Marx I shot an elephant in my pajamas, y la vamos a guardar como una cadena de texto en la variable g1.
End of explanation
grammar1 = nltk.CFG.fromstring(g1)
Explanation: Fíjate cómo hemos definido nuestra gramática:
Hemos encerrado todo entre triples comillas dobles. Recuerda que esta sintaxis de Python permite crear cadenas que contengan retornos de carro y ocupen más de una línea de longitud.
Para los no terminales utilizamos las convenciones habituales para las estructuras sintácticas y las categorías de palabras y los escribimos en mayúsculas. Las etiquetas son autoexplicativas, aunque estén en inglés.
Lo no terminales van escritos entre comillas simples.
Cuando un no terminal se puede definir de más de una forma, marcamos la disyunción con la barra vertical |. 
Tenemos reglas que se interpretan de la siguiente manera: una oración se define como una sintagma nominal y un sintagma verbal; un sintagma nominal se define como un determinante y un nombre, o un determinante, un nombre y un sintagma preposicional, o la palabra I, etc.
A partir de nuestra gramática en una cadena de texto, necesitamos crear un analizador que podamos utilizar posterioremente. Para ello, es imprescindible parsearla antes con el método nltk.CFG.fromstring().
End of explanation
analyzer = nltk.ChartParser(grammar1)
Explanation: Con el objeto grammar1 ya creado, creamos el analizador con el método nltk.ChatParser.
End of explanation
oracion = "I shot an elephant in my pajamas".split()
# guardamos todos los posibles análisis sintácticos en trees
trees = analyzer.parse(oracion)
for tree in trees:
    print(tree)
Explanation: Una vez creado nuestro analizador ya lo podemos utilizar. Tenemos a nuestro alcance el método .parse para analizar sintácticamente cualquier oración que se especifique como una cadena de palabras. Nuestra gramática es bastante limitada, pero podemos utilizarla para analizar la oración I shot an elephant in my pajamas. Si imprimimos el resultado del método, obtenemos el árbol sintáctico.
End of explanation
print(analyzer.parse_one(oracion))
Explanation: Por si no te has dado cuenta, la oración I shot an elephant in my pajamas es ambigua en inglés: se trata del típico ejemplo de PP attachment (saber exactamente a qué nodo está modificando un sintagma preposicional). Existe una doble interpretación para el sintagma preposicional in my pajamas: En el momento del disparo, ¿quién llevaba puesto el pijama? ¿El elefante o yo? Pues bien, nuestra gramática recoge esta ambigüedad y sería capaz de analizarla de dos maneras diferentes, tal y como se muestra en la celda anterior. 
En el caso de que nos interese solo generar uno de los posibles análisis, podemos utilizar el método parse_one(), como se muestra a continuación.
End of explanation
print(analyzer.parse(oracion))
Explanation: Recuerda que para imprimir el árbol sintáctico hay que iterar (con un bucle for, por ejemplo) sobre el objeto que devuelve el método parse() y utilizar la función print.
End of explanation
g1v2 = 
S -> NP VP
NP -> Det N | Det N PP | PRO
VP -> V NP | VP PP
PP -> P NP
Det -> 'an' | 'my'
PRO -> 'I' | 'you'
N -> 'elephant' | 'pajamas'
V -> 'shot'
P -> 'in'
grammar1v2 = nltk.CFG.fromstring(g1v2)
analyzer1v2 = nltk.ChartParser(grammar1v2)
# itero sobre la estructura que devuelve parse()
for tree in analyzer1v2.parse(oracion):
    print(tree)
    
print("\n", "-------------------------------", "\n")
for tree in analyzer1v2.parse("you shot my elephant".split()):
    print(tree)
    
Explanation: A continuación modifico ligeramente mi gramática g1 para incluir una nueva categoría gramatical PRO y añadir algo de volcabulario nuevo. Compara ambos ejemplos:
End of explanation
for tree in analyzer.parse("shot an pajamas elephant my I".split()):
    print("El análisis sintáctico es el siguiente")
    print(tree)
Explanation: NOTA IMPORTANTE sobre errores y el comportamiento de parse()
Cuando un analizador reconoce todo el vocabulario de una oración de entrada pero es incapaz de analizarla, el método parse() no da error pero devuelve un objeto vacío. En este caso, la oración es agramatical según nuestra gramática.
End of explanation
for tree in analyzer.parse("our time is running out".split()):
    print("El análisis sintáctico es el siguiente")
    print(tree)
Explanation: Sin embargo, cuando el analizador no reconoce todo el vocabulario (porque utilizamos una palabra no definida dentro del léxico), el método parse() falla y muestra un mensaje de error de tipo ValueError como el siguiente. Fíjate solo en la última línea:
End of explanation
g2 = u
O -> SN SV
SN -> Det N | Det N Adj | Det Adj N | NProp | SN SP
SV -> V | V SN | V SP | V SN SP
SP -> Prep SN
Det -> 'el' | 'la' | 'un' | 'una' 
N -> 'niño' | 'niña' | 'manzana' | 'pera' | 'cuchillo'
NProp -> 'Juan' | 'Ana' | 'Perico' 
Adj -> 'bonito' | 'pequeña' | 'verde'
V -> 'come' | 'salta' | 'pela' | 'persigue'
Prep -> 'de' | 'con' | 'desde' | 'a'
grammar2 = nltk.CFG.fromstring(g2)
analizador2 = nltk.ChartParser(grammar2)
Explanation: Tenlo en cuenta a la hora de detectar errores en tu código. 
Gramáticas en español
Visto un primer ejemplo de CFG, vamos a cambiar de lengua y crear un analizador para oraciones sencillas en español. El procedimiento es el mismo, definimos nuestra gramática en formato de Chomsky en un fichero aparte o en una cadena de texto, la parseamos con el método nltk.CFG.fromstring() y creamos un analizador con el método nltk.ChartParser():
End of explanation
oraciones = uAna salta
la niña pela una manzana verde con el cuchillo
Juan come un cuchillo bonito desde el niño
un manzana bonito salta el cuchillo desde el niño verde
el cuchillo verde persigue a la pequeña manzana de Ana
el cuchillo verde persigue a Ana.split("\n")
for oracion in oraciones:
    print(oracion)
    for tree in analizador2.parse(oracion.split()):
        print(tree, "\n")        
Explanation: Vamos a probar si es capaz de analizar distintas oraciones es español. Para hacerlo más divertido, vamos a guardar varias oraciones separadas por un intro (simbolizado por el metacarácter \n) en una lista de cadenas llamda oraciones. Iteramos sobre esas oraciones, las imprimimos, después las rompemos en listas de palabras (con el método .split()) e imprimimos el resultado de analizarlas con nuestro analizador.
End of explanation
g3 = u
O -> SN SV | O Conj O
SN -> Det N | Det N Adj | Det Adj N | NProp | SN SP
SV -> V | V SN | V SP | V SN SP
SP -> Prep SN
Det -> 'el' | 'la' | 'un' | 'una' 
N -> 'niño' | 'niña' | 'manzana' | 'pera' | 'cuchillo'
NProp -> 'Juan' | 'Ana' | 'Perico' 
Adj -> 'bonito' | 'pequeña' | 'verde'
V -> 'come' | 'salta' | 'pela' | 'persigue'
Prep -> 'de' | 'con' | 'desde' | 'a'
Conj -> 'y' | 'pero'
# Ahora fijate cómo creamos en analizador en un solo paso
# compáralo con los ejemplos anteriores
analizador3 = nltk.ChartParser(nltk.CFG.fromstring(g3))
for tree in analizador3.parse(ula manzana salta y el niño come pero el cuchillo 
verde persigue a la pequeña manzana de Ana.split()):
    print(tree)
Explanation: Vamos a aumentar la cobertura de nuestra gramática de modo que sea capaz de reconocer y analizar oraciones coordinadas. Para ello, modificamos la regla en la que definimos la oración añadiendo una definición recursivaque defina oración como la secuencia de una oración (O) seguida de una conjunción (Conj) y de otra oración (O). Por último añadimos también algo de léxico nuevo: un par de conjunciones.
End of explanation
# ojo, son sencillas, pero contienen oraciones impersonales, verbos copulativos, sujetos elípticos
oraciones = umañana es viernes
hoy es jueves
tenéis sueño
hace frío
Pepe hace sueño.split("\n")
# escribe tu gramática en esta celda
g4 = 
analyzer4 = nltk.ChartParser(nltk.CFG.fromtring(g4))
# ¿qué tal funciona?
for oracion in oraciones:
    print(oracion)
    for tree in analyzer4.parse(oracion.split()):
        print(tree, "\n")
Explanation: Recuerda que una gramática no es un programa: es una simple descripción que permite establecer qué estructuras sintácticas están bien formadas (la oraciones gramaticales) y cuáles no (las oraciones agramaticales). Cuando una oración es reconocida por una gramática (y en consecuencia, está bien formada), el analizador puede representar la estructura en forma de árbol.
NLTK proporciona acceso a distintos tipo de analizadores (árboles de dependencias, gramáticas probabilísticas, etc), aunque nosotros solo hemos utilizado el más sencillo de ellos: nltk.ChartParser(). Estos analizadores sí son programitas que permiten leer una gramática y analizar las oraciones que proporcionemos como entrada del método parse().
Otro ejemplo
En clase improvisamos un poco y proponemos el siguiente ejemplo de gramática. Vamos a ir complicándola de manera incremental. Comencemos con unas cuantas oraciones de ejemplo.
End of explanation
oraciones = uPepe cree que mañana es viernes
María dice que Pepe cree que mañana es viernes.split()
# escribe la extensión de tu gramática en esta celda
g5 = 
analyzer5 = nltk.ChartParser(nltk.CFG.fromstring(g5))
# ¿qué tal funciona?
for oracion in oraciones:
    print(oracion)
    for tree in analyzer5.parse(oracion.split()):
        print(tree, "\n")
Explanation: ¿Podemos extender g4 para que reconozca oraciones subordinadas introducidas con verbos de lengua o de pensamiento. Me refiero a oraciones del tipo: Pepe cree que mañana es viernes, María dice que Pepe cree que mañana es viernes, etc.
Aumenta tu vocabulario añadiendo tantos terminales como te haga falta.
End of explanation
<END_TASK>