Skip to content

English · Español

Lab 01 — Constrained decoding por JSON Schema

Objetivo: generalizar la máscara de calentamiento a un JSON Schema real; producir salidas {verb, tense, person} que parseen el 100% de las veces.

Tiempo estimado: 4–6 horas.

Prerrequisito: lab 00 (máscara regex) commiteado.


Lo que produces

Un directorio experiments/30-conjugation-schema/ que contiene:

  • conjugation_schema.json — el esquema formal al que se ajusta tu máscara (una copia del esquema canónico para mantener registro; la fuente canónica vive en src/ministruct/schemas.py).
  • mask_driver.py — ejecuta MiniGPT sobre el conjunto eval probe con JSONSchemaMask activado; recolecta salidas.
  • results.json{n_samples, n_parsed_ok, n_schema_valid, kl_per_step_avg}.
  • outputs.jsonl — cada string generado, uno por línea.
  • parse_failures.md — debería estar vacío. Si no lo está, son bugs a arreglar.
  • manifest.json.

Más, en src/ministruct/:

  • schemas.py — el esquema canónico de conjugación como una spec tipo dataclass de Python.
  • dfa.py — compilador esquema → máquina de estados.
  • mask.py — extendido con JSONSchemaMask.

El esquema

{
  "type": "object",
  "additionalProperties": false,
  "required": ["verb", "tense", "person"],
  "properties": {
    "verb": {
      "type": "string",
      "enum": ["work", "play", "walk", "talk", "listen", "watch", "study",
               "finish", "start", "look", "want", "like",
               "be", "have", "do", "go", "come", "see", "eat", "write"]
    },
    "tense": {
      "type": "string",
      "enum": ["infinitive", "present_simple", "past_simple",
               "past_participle", "simple_future"]
    },
    "person": {
      "type": "string",
      "enum": ["1sg", "2sg", "3sg"]
    },
    "spanish": {
      "type": "string",
      "maxLength": 30
    }
  }
}

Los enums sobre verb, tense, person son el alcance canónico de la gramática de verbos en inglés (según LYNX_CORTEX_ADDENDUM.md §A13). spanish es la traducción opcional al español de la forma conjugada resultante (p. ej., para {verb: "eat", tense: "past_simple", person: "3sg"} el valor es "comió").

TODOs

Bloque A — esquema → estados

En src/ministruct/dfa.py:

  • Parsea el esquema (el json de stdlib está bien; NO uses la librería jsonschema para la máscara misma).
  • Construye una máquina de estados. Los estados están documentados en theory/02-logit-masks.md §"Computar la máscara".
  • Cada estado guarda: (a) la posición del parser en el esqueleto JSON, (b) qué claves se han emitido hasta ahora, © qué clave se está valorando actualmente, (d) progreso dentro del valor.
  • Implementa transition(state, char) -> state | None (None = ilegal). Testéalo exhaustivamente sobre un ejemplo válido escrito a mano.

Bloque B — máscara a nivel de token

En src/ministruct/mask.py, implementa JSONSchemaMask:

  • Constructor: JSONSchemaMask(tokenizer, schema_dict).
  • En step(last_token_id):
  • Si last_token_id no es None, decodifícalo a caracteres, avanza la máquina de estados por cada carácter. Si cualquier carácter es rechazado, el parser está en un estado inválido — marca esto como bug (la máscara del paso anterior estaba equivocada; nunca debería dispararse si la implementación es correcta).
  • Computa la máscara: para cada token del vocabulario, decodifícalo, simula la máquina de estados hacia adelante, acepta si la simulación nunca llega a un estado ilegal.
  • Devuelve el array de máscara.
  • En is_done(): True si y solo si la máquina de estados alcanzó el estado terminal DONE.

Bloque C — tokens multi-carácter

Esta es la parte sutil. Un token como "," cruza múltiples caracteres JSON. Tu transition debe llamarse por carácter, no por token, durante el bucle de construcción de máscara. Referencia: theory/03-grammar-as-dfa.md §"A nivel de token vs a nivel de carácter".

  • Verifica con un test escrito a mano: un token " seguido del token verb seguido del token " produce la secuencia correcta de transiciones de estado al decodificarse un carácter a la vez.
  • Verifica con un test: un token ,"tense":" (un blob BPE multi-carácter, si está presente) recorre 4 transiciones de estado en un paso y es aceptado si y solo si el estado final es legal Y cada estado intermedio es legal.

Bloque D — cablear en el decoder + eval

  • mask_driver.py: carga MiniGPT, carga el conjunto eval probe (data/eval/conjugation_probes.jsonl — el conjunto probe §A13 de la Fase 20), para cada probe construye un prompt pidiendo la tripla de conjugación, genera con JSONSchemaMask, recolecta la salida.
  • Valida cada salida: json.loads(output) tiene éxito Y jsonschema.validate(parsed, schema) tiene éxito. Solo para validación, la librería jsonschema SÍ está permitida — no es parte de la máscara.
  • Computa KL por paso (usando \(Z = \sum_{t \in \mathcal{L}_i} p(t)\) de theory/02-logit-masks.md).

Bloque E — tests

En tests/test_ministruct_mask.py:

  • test_schema_first_token_is_open_brace — en el paso 0 de JSONSchemaMask, el único token legal es uno cuyo string decodificado empieza con {.
  • test_verb_enum_after_verb_key — tras emitir {"verb":", solo los tokens que son prefijos de uno de los 20 strings del enum de verbos son legales.
  • test_tense_enum_after_tense_key — misma comprobación para los 5 valores de tense.
  • test_person_enum_after_person_key — misma comprobación para los 3 valores de person.
  • test_done_after_close_brace — tras un objeto válido completo, is_done() es True.
  • test_reset_between_requests — genera dos salidas distintas desde la misma instancia de máscara tras llamar a reset(). Ambas deben parsear.
  • test_no_extra_keys_admitted — la máscara debe rechazar cualquier clave no en {verb, tense, person, spanish}.
  • test_no_repeat_keys — la máscara debe rechazar una clave que ya se ha emitido en el objeto actual.
  • test_spanish_optional — salida sin la clave spanish pasa; salida con la clave spanish también pasa.

Restricciones

  • Solo parseo de esquema. No uses jsonschema para la lógica de enmascarado. La librería jsonschema puede usarse para validación de salidas en el Bloque D (a posteriori, no como parte de la máscara).
  • Un archivo por preocupación. Constantes de esquema en schemas.py; FSM en dfa.py; máscara en mask.py; tests en tests/test_ministruct_mask.py.
  • Determinismo. La máscara dada (estado, vocab, esquema) es determinista. Sin aleatoriedad en la construcción de la máscara.

Condiciones de parada

Terminado cuando:

  1. Todos los tests del Bloque E pasan.
  2. results.json reporta n_parsed_ok == n_samples Y n_schema_valid == n_samples. Contrato fuerte.
  3. parse_failures.md está vacío.
  4. README.md incluye una discusión breve del KL medio por paso. ¿Es pequeño (el modelo ya conocía el formato)? ¿Grande (al modelo se le estaba coaccionando)? ¿Qué implica eso para el ajuste fino (fine-tuning) en la Fase 28?

Trampas

  • Espacios en blanco. JSON permite espacios en blanco arbitrarios entre elementos. Lo más fácil es prohibir espacios en blanco en tu máscara (requerir la forma canónica minificada). Documenta esto en el README.
  • Caracteres de escape en strings. Una " dentro de una traducción al español (p. ej., la palabra española para alguna forma que contenga comillas) es rara en el alcance de §A13 pero posible. O prohíbe secuencias de escape en spanish (más simple), o implementa la lógica de escape con cuidado.
  • Orden de claves. JSON permite cualquier orden de claves; tu máscara o (a) impone un orden canónico (más simple, menos estados), o (b) permite cualquier orden (más estados, más bugs). Recomendado: impón el orden canónico verb → tense → person → spanish?. Documéntalo.
  • Sorpresas del token-trie. Un token como "verb": puede ser un solo token en tu BPE. La máscara debe recorrer los 7 caracteres a través de la máquina de estados en un paso. Si tu test nunca ve un token así, simúlalo (mock) para forzar el camino de código.
  • spanish opcional, pero no "saltable en mitad del stream". Si empezaste a emitir "spanish":", debes terminarlo. La máscara debe imponer eso.

Cuándo consultar solutions/

Después de que se alcance el 100% de tasa de parseo sobre el conjunto probe. La solución cruzará tu estructura de máquina de estados y probablemente tu interpretación del diagnóstico KL.


Siguiente lab: lab/02-end-to-end-conjugate.md.