1mtnqjisddrrb7vv7fdd3pa.png

Cómo restringir su modelo para generar formatos definidos

En esta publicación explicaré y demostraré el concepto de «IA generativa estructurada»: IA generativa limitada a formatos definidos. Al final de la publicación, comprenderá dónde y cuándo se puede usar y cómo implementarlo, ya sea que esté creando un modelo de transformador desde cero o utilizando los modelos de Hugging Face. Además, cubriremos un consejo importante para la tokenización que es especialmente relevante para lenguajes estructurados.

Uno de los muchos usos de la IA generativa es como herramienta de traducción. Esto a menudo implica traducir entre dos idiomas humanos, pero también puede incluir lenguajes o formatos informáticos. Por ejemplo, es posible que su aplicación necesite traducir el lenguaje natural (humano) a SQL:

Natural language: “Get customer names and emails of customers from the US”

SQL: "SELECT name, email FROM customers WHERE country = 'USA'"

O para convertir datos de texto a formato JSON:

Natural language: “I am John Doe, phone number is 555–123–4567,
my friends are Anna and Sara”

JSON: {name: "John Doe",
phone_number: "555–123–5678",
friends: {
name: [["Anna", "Sara"]]}
}

Naturalmente, son posibles muchas más aplicaciones para otros lenguajes estructurados. El proceso de capacitación para tales tareas implica alimentar ejemplos de lenguaje natural junto con formatos estructurados a un modelo codificador-decodificador. Alternativamente, puede ser suficiente aprovechar un modelo de lenguaje (LLM) previamente entrenado.

Si bien es inalcanzable lograr una precisión del 100%, hay una clase de errores que podemos eliminar: los errores de sintaxis. Estas son violaciones del formato del lenguaje, como reemplazar comas con puntos, usar nombres de tablas que no están presentes en el esquema SQL u omitir cierres de corchetes, lo que hace que SQL o JSON no sean ejecutables.

El hecho de que estemos traduciendo a un lenguaje estructurado significa que la lista de tokens legítimos en cada paso de generación es limitada y predeterminada. Si pudiéramos insertar este conocimiento en el proceso de IA generativa, podríamos evitar una amplia gama de resultados incorrectos. Ésta es la idea detrás de la IA generativa estructurada: limitarla a una lista de tokens legítimos.

Un recordatorio rápido sobre cómo se generan los tokens

Ya sea que se emplee un codificador-decodificador o una arquitectura GPT, la generación de tokens opera de forma secuencial. La selección de cada token se basa tanto en los tokens de entrada como en los generados previamente, y continúa hasta que se genera un token , lo que significa la finalización de la secuencia. En cada paso, un clasificador asigna valores logit a todos los tokens del vocabulario, representando la probabilidad de cada token como la siguiente selección. El siguiente token se muestrea en función de esos logits.

El clasificador del decodificador asigna un logit a cada token del vocabulario (Imagen del autor)

Limitar la generación de tokens

Para limitar la generación de tokens, incorporamos conocimiento de la estructura del lenguaje de salida. Los tokens ilegítimos tienen sus logits configurados en -inf, lo que garantiza su exclusión de la selección. Por ejemplo, si solo una coma o «DE» es válida después de «Seleccionar nombre», todos los demás logits de token se configuran en -inf.

Si está utilizando Hugging Face, esto se puede implementar utilizando un «procesador logits». Para usarlo necesitas implementar una clase con un método __call__, que será llamado después de que se calculen los logits, pero antes del muestreo. Este método recibe todos los logits de tokens y los ID de entrada generados, y devuelve logits modificados para todos los tokens.

Los logits devueltos por el procesador logits: todos los tokens ilegítimos obtienen un valor de -inf (Imagen del autor)

Demostraré el código con un ejemplo simplificado. Primero, inicializamos el modelo, usaremos Bart en este caso, pero esto puede funcionar con cualquier modelo.

from transformers import BartForConditionalGeneration, BartTokenizerFast, PreTrainedTokenizer
from transformers.generation.logits_process import LogitsProcessorList, LogitsProcessor
import torch

name = 'facebook/bart-large'
tokenizer = BartTokenizerFast.from_pretrained(name, add_prefix_space=True)
pretrained_model = BartForConditionalGeneration.from_pretrained(name)

Si queremos generar una traducción del lenguaje natural a SQL, podemos ejecutar:

to_translate = 'customers emails from the us'
words = to_translate.split()
tokenized_text = tokenizer([words], is_split_into_words=True)

out = pretrained_model.generate(
torch.tensor(tokenized_text["input_ids"]),
max_new_tokens=20,
)
print(tokenizer.convert_tokens_to_string(
tokenizer.convert_ids_to_tokens(
out[0], skip_special_tokens=True)))

Regresando

'More emails from the us'

Como no ajustamos el modelo para tareas de texto a SQL, el resultado no se parece a SQL. No entrenaremos el modelo en este tutorial, pero lo guiaremos para generar una consulta SQL. Lo lograremos empleando una función que asigna cada token generado a una lista de los siguientes tokens permitidos. Para simplificar, nos centraremos sólo en el token inmediatamente anterior, pero los mecanismos más complicados son fáciles de implementar. Usaremos un diccionario que define para cada token qué tokens pueden seguirlo. Por ejemplo, la consulta debe comenzar con «SELECCIONAR» o «ELIMINAR», y después de «SELECCIONAR» solo se permiten «nombre», «correo electrónico» o «id», ya que esas son las columnas de nuestro esquema.

rules = {'<s>': ['SELECT', 'DELETE'], # beginning of the generation
'SELECT': ['name', 'email', 'id'], # names of columns in our schema
'DELETE': ['name', 'email', 'id'],
'name': [',', 'FROM'],
'email': [',', 'FROM'],
'id': [',', 'FROM'],
',': ['name', 'email', 'id'],
'FROM': ['customers', 'vendors'], # names of tables in our schema
'customers': ['</s>'],
'vendors': ['</s>'], # end of the generation
}

Ahora necesitamos convertir estos tokens a los ID utilizados por el modelo. Esto sucederá dentro de una clase heredada de LogitsProcessor.

def convert_token_to_id(token):
return tokenizer(token, add_special_tokens=False)['input_ids'][0]

class SQLLogitsProcessor(LogitsProcessor):
def __init__(self, tokenizer: PreTrainedTokenizer):
self.tokenizer = tokenizer
self.rules = {convert_token_to_id(k): [convert_token_to_id(v0) for v0 in v] for k,v in rules.items()}

Finalmente, implementaremos la función __call__, que se llama después de calcular los logits. La función crea un nuevo tensor de -infs, verifica qué ID son legítimos según las reglas (el diccionario) y coloca sus puntuaciones en el nuevo tensor. El resultado es un tensor que sólo tiene valores válidos para los tokens válidos.

class SQLLogitsProcessor(LogitsProcessor):
def __init__(self, tokenizer: PreTrainedTokenizer):
self.tokenizer = tokenizer
self.rules = {convert_token_to_id(k): [convert_token_to_id(v0) for v0 in v] for k,v in rules.items()}

def __call__(self, input_ids: torch.LongTensor, scores: torch.LongTensor):
if not (input_ids == self.tokenizer.bos_token_id).any():
# we must allow the start token to appear before we start processing
return scores
# create a new tensor of -inf
new_scores = torch.full((1, self.tokenizer.vocab_size), float('-inf'))
# ids of legitimate tokens
legit_ids = self.rules[int(input_ids[0, -1])]
# place their values in the new tensor
new_scores[:, legit_ids] = scores[0, legit_ids]
return new_scores

¡Y eso es! Ahora podemos ejecutar una generación con el procesador logits:

to_translate = 'customers emails from the us'
words = to_translate.split()
tokenized_text = tokenizer([words], is_split_into_words=True, return_offsets_mapping=True)

logits_processor = LogitsProcessorList([SQLLogitsProcessor(tokenizer)])

out = pretrained_model.generate(
torch.tensor(tokenized_text["input_ids"]),
max_new_tokens=20,
logits_processor=logits_processor
)
print(tokenizer.convert_tokens_to_string(
tokenizer.convert_ids_to_tokens(
out[0], skip_special_tokens=True)))

Regresando

 SELECT email , email , id , email FROM customers

El resultado es un poco extraño, pero recuerda: ¡Ni siquiera entrenamos el modelo! Solo aplicamos la generación de tokens según reglas específicas. En particular, restringir la generación no interfiere con la formación; Las restricciones solo se aplican durante la generación posterior al entrenamiento. Por lo tanto, cuando se implementan adecuadamente, estas restricciones sólo pueden mejorar la precisión de la generación.

Nuestra implementación simplista no llega a cubrir toda la sintaxis SQL. Una implementación real debe admitir más sintaxis, considerando potencialmente no solo el último token sino varios, y permitir la generación por lotes. Una vez implementadas estas mejoras, nuestro modelo entrenado puede generar de manera confiable consultas SQL ejecutables, restringidas a nombres válidos de tablas y columnas del esquema. Un enfoque similar puede imponer restricciones en la generación de JSON, asegurando la presencia de claves y el cierre de corchetes.

Tenga cuidado con la tokenización

A menudo se pasa por alto la tokenización, pero la tokenización correcta es crucial cuando se utiliza IA generativa para resultados estructurados. Sin embargo, en el fondo, la tokenización puede tener un impacto en el entrenamiento de su modelo. Por ejemplo, puede ajustar un modelo para traducir texto a JSON. Como parte del proceso de ajuste, usted proporciona al modelo ejemplos de pares texto-JSON, que tokeniza. ¿Cómo será esta tokenización?

(Imagen del autor)

Mientras lee «[[«comodoscorcheteseltokenizadorlosconvierteenunaúnicaIDqueelclasificadordetokenstratarácomounaclasecompletamentedistintadelcorcheteúnicoEstohacequetodalalógicaqueelmodelodebeaprenderseamáscomplicada(porejemplorecordarcuántoscorchetescerrar)DemanerasimilaragregarunespacioantesdelaspalabraspuedecambiarsutokenizaciónysuIDdeclasePorejemplo:[[“astwosquarebracketsthetokenizerconvertsthemintoasingleIDwhichwillbetreatedasacompletelydistinctclassfromthesinglebracketbythetokenclassifierThismakestheentirelogicthatthemodelmustlearn—morecomplicated(forexamplerememberinghowmanybracketstoclose)SimilarlyaddingaspacebeforewordsmaychangetheirtokenizationandtheirclassIDForinstance:

(Imagen del autor)

Nuevamente, esto complica la lógica que el modelo tendrá que aprender, ya que los pesos conectados a cada una de estas ID deberán aprenderse por separado, para casos ligeramente diferentes.

Para un aprendizaje más sencillo, asegúrese de que cada concepto y puntuación se convierta consistentemente al mismo token, agregando espacios antes de las palabras y los caracteres.

Las palabras espaciadas conducen a una tokenización más consistente (Imagen del autor)

Introducir ejemplos espaciados durante el ajuste fino simplifica los patrones que el modelo debe aprender, lo que mejora la precisión del modelo. Durante la predicción, el modelo generará el JSON con espacios, que luego podrá eliminar antes de analizar.

Resumen

La IA generativa ofrece un enfoque valioso para traducir a un lenguaje formateado. Al aprovechar el conocimiento de la estructura de salida, podemos restringir el proceso generativo, eliminando una clase de errores y garantizando la ejecutabilidad de las consultas y la capacidad de análisis de las estructuras de datos.

Además, estos formatos pueden utilizar puntuación y palabras clave para indicar ciertos significados. Asegurarse de que la tokenización de estas palabras clave sea consistente puede reducir drásticamente la complejidad de los patrones que el modelo tiene que aprender, reduciendo así el tamaño requerido del modelo y su tiempo de entrenamiento, al tiempo que aumenta su precisión.

La IA generativa estructurada puede traducir eficazmente el lenguaje natural a cualquier formato estructurado. Estas traducciones permiten la extracción de información del texto o la generación de consultas, lo que constituye una poderosa herramienta para numerosas aplicaciones.