por Bruno Dias
Gracias a la generosidad de patrocinadores de PROCJAM en Kickstarter, puedo estar aquí para enseñarte los conceptos básicos sobre Improv. Este es un tutorial semiavanzado; se espera que te sientas al menos un poco cómodo con los editores de texto, el lenguaje de programación Javascript y las herramientas de línea de comandos, pero no voy a entrar en nada demasiado complicado (o eso espero).
Improv es una herramienta para generar texto procesalmente a partir de una gramática. Una gramática es un grupo de reglas que definen cómo escribir un texto construyéndolo a partir de partes más pequeñas. La principal desventaja de las gramáticas es que llevan mucho trabajo; tienen que estar escritas a mano o ser generadas con algún otro método, quizás a partir de datos estructurados. Pero las gramáticas pueden generar texto estructurado y consistente, y son más accesibles para usar (y obtener buenos resultados) que las cadenas de Markov o el texto predictivo o las redes neuronales, u otras técnicas más complicadas de machine learning (aprendizaje automático).
La estructura básica de Improv se debe a Tracery de Kate Compton, y a la metodología que Emily Short desarrolló para escribir The Annals of the Parrigues. Improv fue diseñado para Voyageur, y como tal, tenía que ser capaz de generar descripciones de párrafos y bastante variadas de lugares y cosas autoconsistentes y que describían algo con atributos variables y complejos. Voyageur es un juego de exploración espacial y comercio en el que los jugadores saltan de planeta en planeta. En Voyageur, un planeta tiene factores independientes como la ecología, la ideología y la economía; y se le debe dar una descripción que mencione todos esos factores.
Necesitamos un entorno donde podamos ejecutar JavaScript. En la práctica, la mayoría de las cosas para las que podrías usar Improv, se ejecutarían en el navegador, o en un entorno parecido a un navegador como Electron o Cordova. Pero Improv también puede ejecutarse en Node.js, que es lo que haré en este tutorial para mantener las cosas simples; de todos modos, necesitamos Node para descargar una copia de Improv a través de npm, la utilidad del administrador de paquetes de Node. Así que, en una máquina con Node.js instalado (y utilizando el shell bash predeterminado en Linux / MacOS o Powershell en Windows), puedes configurar un entorno de Improv simple con esta fórmula:
mkdir improv-tutorial
cd improv-tutorial
npm install fs-jetpack js-yaml improv
npm se quejará bastante de no tener un archivo package.json en su proyecto, pero instalará los paquetes. Querrás crear un archivo improv-tutorial.js aquí y también un archivo grammar.yaml.
Aquí está nuestro sencillo script para ejecutar Improv:
// Require libraries
const Improv = require('improv');
const yaml = require('js-yaml');
const fs = require('fs-jetpack');
// Load our data from a file
const grammarData = yaml.load(fs.read('grammar.yaml'));
// Create a generator object from this data
const generator = new Improv(grammarData, {
filters: [Improv.filters.mismatchFilter()],
reincorporate: true
});
// Generate text and print it out
console.log(generator.gen('root', {}));
Improv
, importado de la biblioteca, es un constructor (así que utilízalo con new
) que crea y devuelve un objeto generador. Se necesitan dos parámetros, una gramática y otro objeto para configurar el generador. Tienes muchas opciones, pero para este tutorial solo usaremos filters
(filtros) y reincorporate
(reincorporar); Volveré a lo que esto significa en un momento.
Puedes ejecutar esto solo con el node improv-tutorial.js
, pero creará un error; grammar.yaml todavía no existe, así que vamos a crearlo.
Improv es una biblioteca de JavaScript, por lo que las gramáticas en Improv son también objetos de javascript, pero tienen una estructura bastante anidada. Para facilitar la lectura y para no volverme loco, me gusta escribirlos en YAML. Una gramática básica es algo como esto:
root:
groups:
-
tags: []
phrases:
- "The [:prefix] [:name] is a [:class] commissioned in 1888"
prefix:
groups:
-
tags: []
phrases:
- "HMS"
name:
groups:
-
tags: []
phrases:
- "Invincible"
class:
groups:
-
tags: []
phrases:
- "cutter"
Guarda esto como grammar.yaml
, ejecuta el script de nuevo, y deberías obtener exactamente este resultado: "The HMS Invincible is a cutter commissioned in 1888."
Hay bastantes cosas aquí así que voy a ir paso a paso: Cada clave en este objeto (como root
, prefix
) (regla, prefijo) es una regla (rule). Cada regla contiene grupos (groups) (en este caso, solo uno), y cada grupo tiene una lista de etiquetas (tags) y una lista de frases (phrases). Cada frase es un fragmento individual de texto que puede contener directivas (por ejemplo [:prefix]
) que apuntan a otras reglas.
Improv funciona así: Te pedimos que generes la regla root
(generator.gen('root', {})
de vuelta en el archivo de JavaScript. Improv revisa todos los grupos de esa regla; claro que en este caso solo hay uno en este archivo. Primero hay un paso de filtrado, del que hablaremos con más detalle más adelante, y éste selecciona qué grupos usará. Después, recopila todas las frases de los grupos y elige una al azar.
Más tarde, se rellenan los vacíos en cada frase; Improv busca instrucciones que aparezcan entre [paréntesis]. Las instrucciones que comienzan con dos puntos, como [:prefix]
, son la base de la generación de texto Improv; son referencias a otras reglas, que luego se insertan en el lugar apropiado. Cuando Improv encuentra una de estas, recurre, generando esa regla.
En este momento el resultado no es muy impresionante; obtenemos el mismo cada vez, ya que todas nuestras reglas solo contienen una frase. Podemos agregar variaciones agregando más frases:
root:
groups:
-
tags: []
phrases:
- "The [:prefix] [:name] is a [:class] commissioned in [#1880-1910]"
prefix:
groups:
-
tags: []
phrases:
- "HMS"
name:
groups:
-
tags: []
phrases:
- "Invincible"
- "Unstoppable"
- "Indefatigable"
- "Inevitable"
- "Inerrant"
class:
groups:
-
tags: []
phrases:
- "cutter"
- "torpedo boat"
- "light cruiser"
- "heavy cruiser"
- "battleship"
Y haciéndolo así, ahora tenemos variaciones:
The HMS Unstoppable is a battleship commissioned in 1898
The HMS Invincible is a cutter commissioned in 1891
The HMS Indefatigable is a light cruiser commissioned in 1906
Ten en cuenta la [# 1880-1910]
; es una instrucción especial que produce un número aleatorio entre 1880 y 1910.
Digamos que queremos incluir buques civiles y militares en nuestra gramática. Podemos añadir variantes civiles, con sus propios nombres, clases y prefijo:
root:
groups:
-
tags: []
phrases:
- "The [:prefix] [:name] is a [:class] commissioned in [#1880-1910]"
prefix:
groups:
-
tags:
- ['type', 'military']
phrases:
- "HMS"
-
tags:
- ['type', 'civilian']
phrases:
- "SS"
name:
groups:
-
tags:
- ['type', 'military']
phrases:
- "Invincible"
- "Unstoppable"
- "Indefatigable"
- "Inevitable"
- "Inerrant"
-
tags:
- ['type', 'civilian']
phrases:
- "Dromedary"
- "Mule"
- "Camel"
- "Ox"
class:
groups:
-
tags:
- ['type', 'military']
phrases:
- "cutter"
- "torpedo boat"
- "light cruiser"
- "heavy cruiser"
- "battleship"
-
tags:
- ['type', 'civilian']
phrases:
- "steamship"
- "cargo ship"
- "ferry"
Ahora tenemos grupos de frases para barcos civiles y militares; y estamos usando la propiedad de etiquetas. Lo importante que tener en cuenta aquí es que las tags
son una lista de listas (list of lists). Cada etiqueta individual es una lista, como ['type', 'military']
(tipo, militar); eso es una etiqueta, no dos. Esto es importante; significa que las etiquetas forman una jerarquía y que serán relevantes para el filtrado más adelante.
La primera forma en que se usan las etiquetas es en la reincorporación (reincorporation), que declaramos como verdadera antes. Ten en cuenta que cuando utilizamos Improv.gen()
, le dimos dos parámetros; el primero, 'root'
, es el nombre de la regla desde la que queremos partir. El segundo solo era {}
, un objeto nuevo y vacío. Este es nuestro modelo (model), un objeto que contiene datos sobre el texto que estamos generando. Cada vez que Improv selecciona y usa una frase, suponiendo que la reincorporación esté activada, ésta se agrega al modelo. Podemos hacer un pequeño cambio en nuestro script para poder inspeccionar el modelo más tarde:
const model = {};
// Generate text and print it out
console.log(generator.gen('root', model));
console.log(model);
Ejemplo de output:
The HMS Inevitable is a light cruiser commissioned in 1894
{ tags: [ [ 'type', 'military' ] ] }
Podemos ver que la etiqueta que usamos se añadió a nuestro objeto vacío. Puedes generar un modelo con anticipación usando otro método y pasándolo a Improv, y puedes guardar un modelo después de generar texto (como variable) para reutilizarlo, generando diferentes grupos de texto con las mismas etiquetas.
Hasta ahora, no hemos hablado de la característica más importante de Improv, el filtrado. Recuerda filters: [Improv.filters.mismatchFilter()]
? Lo que esa línea hace es configurar el generador para usar Improv.filters.mismatchFilter()
como su único filtro. Un filtro es solo una función utilizada para ayudar a Improv a seleccionar qué grupos debe usar al seguir una regla. Improv.filters
contiene un conjunto de funciones integradas que devuelven filtros listos para que Improv los utilice, pero nada te impide escribir los tuyos propios. La documentación de Improv tiene una lista de filtros incorporados junto con información sobre su función, pero para nuestros fines, solo vamos a ver el filtro mismatch filter (filtro de desajuste).
Lo que hace el filtro de desajuste es buscar etiquetas que contradigan las etiquetas del modelo, y descarta los grupos que contienen esas etiquetas. Es el filtro más básico y más útil; impide que Improv se contradiga, suponiendo que el texto en la gramática haya sido etiquetado para evitar eso.
La definición de "contradecir" en este sentido es:
- El modelo y el grupo tienen una etiqueta con el mismo primer elemento; - Pero no todos los elementos son idénticos.Con lo cual, 'type', 'military'
es un desajuste con 'type', 'civilian'
. Pero permite coincidencias exactas ('type', 'military'
) (tipo, militar), además de etiquetas que son totalmente diferentes ('propulsion', 'sail'
, (propulsión', vela) , por ejemplo).
Esta es la razón por la que las etiquetas son listas; el primer elemento es un tipo de categoría, y los elementos siguientes definen subcategorías. Las etiquetas indican qué aspecto del objeto representan, para poder identificar las contradicciones; un barco no puede ser militar y civil ni grande y pequeño; pero puede ser militar y tener velas, o puede ser civil y ser grande.
Puedes agregar más etiquetas y variaciones para expandir esta gramática y crear descripciones cada vez más complejas; la demostración en el repositorio Improv github contiene un ejemplo muy detallado de generación de naves militares imaginarias.
Esto solo cubre una pequeña parte de la funcionalidad de Improv. La mayoría de los filtros no descarta grupos sin más; sino que devuelven un número (negativo o positivo), y los números devueltos de todos los filtros se suman para dar a cada grupo un puntaje de relevancia. Improv luego elige qué debe usar basándose en este puntaje de relevancia. Esto puede utilizarse para más que evitar que el texto se contradiga. Voyageur usa una compleja «receta secreta» de métodos de filtrado para producir su texto. Una de las cosas que utiliza para la selección es la amplitud de la descripción (breadth of description), tratando de usar etiquetas que no ha utilizado ya para producir un párrafo que abarque la mayoría de los aspectos de lo que está describiendo. También trata de buscar especificidad (specificity), valorando fragmentos de texto asociados con condiciones muy específicas, para que aparezcan en las pocas ocasiones en las que son apropiados.