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
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.
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 torchname = '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?
Mientras lee «[[«comodoscorcheteseltokenizadorlosconvierteenunaúnicaIDqueelclasificadordetokenstratarácomounaclasecompletamentedistintadelcorcheteúnicoEstohacequetodalalógicaqueelmodelodebeaprenderseamáscomplicada(porejemplorecordarcuántoscorchetescerrar)DemanerasimilaragregarunespacioantesdelaspalabraspuedecambiarsutokenizaciónysuIDdeclasePorejemplo:[[“astwosquarebracketsthetokenizerconvertsthemintoasingleIDwhichwillbetreatedasacompletelydistinctclassfromthesinglebracketbythetokenclassifierThismakestheentirelogicthatthemodelmustlearn—morecomplicated(forexamplerememberinghowmanybracketstoclose)SimilarlyaddingaspacebeforewordsmaychangetheirtokenizationandtheirclassIDForinstance:
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.
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.