Retos en la Generación de Paradas dentro de Llama 2

Reto en la generación de paradas en Llama 2.

Una Exploración con Posibles Soluciones

Llama: Foto de Liudmila Shuvalova

El lanzamiento de Llama 2 por Meta ha generado emoción dentro de la comunidad, marcando el amanecer de una era para modelos de lenguaje grandes y bien realizados que anteriormente solo eran accesibles a través de APIs específicas de la empresa.

Sin embargo, es importante reconocer algunas imperfecciones inherentes en estos modelos. Entre ellas, destaca especialmente el problema de generación de parada. Mis experiencias personales han demostrado que estos modelos a menudo tienen dificultades para determinar el punto adecuado de ‘parada’, dejándolos inciertos sobre cuándo finalizar una generación de texto.

En esta publicación del blog, profundizaré en el problema de las fallas en la generación de paradas en el modelo más pequeño de Llama 2, el modelo Llama 2–7b, y discutiré varios remedios potenciales. La implementación en las secciones siguientes se puede encontrar en este cuaderno de Google Colab con el tipo de tiempo de ejecución T4.

Falla en la Generación de Paradas

En esta sección, aprovecharemos el poder de un modelo Llama 2–7b utilizando una GPU T4 equipada con amplios recursos de RAM en Google Colab (2.21 créditos/hora). Es esencial tener en cuenta que la GPU T4 viene con una capacidad de VRAM de 16 GB, precisamente suficiente para almacenar los pesos de Llama 2–7b (7b × 2 bytes = 14 GB en FP16).

Para administrar eficientemente el uso de VRAM, emplearemos una técnica llamada cuantización. La cuantización es un enfoque que se centra en minimizar los requisitos computacionales y de memoria durante la inferencia mediante la representación de pesos y activaciones utilizando tipos de datos de baja precisión.

Veamos ahora el siguiente fragmento de código. Aquí, demostraremos cómo cargar el modelo “meta-llama/Llama-2–7b-chat-hf” con una configuración de Bite y Byte y configurar una canalización de generación de texto basada en este modelo cargado.

# configuración de bite y bytenb_config = transformers.BitsAndBytesConfig(    load_in_4bit=True,  # cuantización de 4 bits    bnb_4bit_quant_type='nf4',  # float normalizado 4    bnb_4bit_use_double_quant=True,  # segunda cuantización después de la primera    bnb_4bit_compute_dtype=bfloat16  # tipo de cálculo)model_id= "meta-llama/Llama-2-7b-chat-hf"device = f'cuda:{cuda.current_device()}' if cuda.is_available() else 'cpu'# Tokenizador Llama 2tokenizer =transformers.AutoTokenizer.from_pretrained(model_id)# Modelo Llama 2model = transformers.AutoModelForCausalLM.from_pretrained(    model_id,    trust_remote_code=True,    quantization_config=bnb_config,    device_map='auto',)model.eval()# Definir canalizaciónpipeline = transformers.pipeline(model=model,    tokenizer=tokenizer,    return_full_text=True,  # langchain espera el texto completo    task='text-generation',    # aquí también pasamos los parámetros del modelo    max_new_tokens=500,  # número máximo de tokens a generar en la salida    temperature=0.1,    )

Esta configuración nos permite generar texto para una consulta dada en menos de un minuto. Pongámoslo a prueba con una pregunta sencilla: “¿Qué puedo hacer en París?” A continuación, encontrarás la respuesta (Por favor, ten en cuenta que tus resultados pueden variar debido a la configuración de temperatura).

París, la capital de Francia, es conocida por su impresionante arquitectura, museos de arte, lugares históricos y ambiente romántico. Es el destino soñado de todos. Aquí tienes algunas de las principales cosas que puedes hacer en París:1. Visitar lugares emblemáticos: La Torre Eiffel, el Museo del Louvre, la Catedral de Notre-Dame, el Arco de Triunfo y los Campos Elíseos son atracciones que no debes perderte.2. Explorar museos y galerías de arte: El Louvre, el Musée d'Orsay y el Centre Pompidou son algunos de los museos más famosos del mundo.3. Realizar un crucero por el río: Un crucero por el río Sena es una excelente forma de ver los lugares emblemáticos y puentes de la ciudad mientras aprendes sobre su historia y cultura.4. Descubrir los barrios de la ciudad: Desde el elegante Campos Elíseos hasta el encantador Montmartre, cada barrio parisino tiene su propio carácter y encanto único.# He omitido parte del texto aquí debido al límite de espacio# Por supuesto, hay innumerables

Es evidente que el modelo tiene dificultades para producir una respuesta satisfactoria; parece tener dificultad para saber cuándo concluir su salida. Al tokenizar el texto generado, se hace evidente que el último token no es un 2, que representa el token eos (fin de secuencia) en el tokenizador del modelo.

Al examinar más de cerca las puntuaciones de los tokens (probabilidades) proporcionadas por el modelo, noté que el token_id 2 (eso_token_id) tiene una puntuación de “-inf”. Esto implica que no tiene posibilidad de ser generado.

Intentos de resolución del problema

En esta sección, exploraremos varias soluciones potenciales destinadas a abordar el problema en cuestión. Es esencial tener en cuenta que las soluciones aquí discutidas representan esfuerzos proactivos, pero no siempre pueden proporcionar soluciones a los problemas en cuestión.

Procesador de logits

Un modelo de lenguaje como Llama 2 procesa una secuencia de tokens de texto como entrada y produce una secuencia de probabilidades condicionales para el próximo token, basado en el contexto desde el token inicial hasta el actual. En vista de esto, vale la pena considerar ajustes manuales a estas probabilidades a medida que nos acercamos al límite máximo de tokens, con el objetivo de aumentar la probabilidad de encontrar el token eos. Lo hacemos definiendo nuestro LogitsProcessor personalizado llamado “EosTokenRewardLogitsProcessor” con dos entradas iniciales eos_token_id y max_length donde este último representa la longitud máxima en la que el modelo debe generar un token eos:

class EosTokenRewardLogitsProcessor(LogitsProcessor):  def __init__(self,  eos_token_id: int, max_length: int):            if not isinstance(eos_token_id, int) or eos_token_id < 0:            raise ValueError(f"`eos_token_id` debe ser un entero positivo, pero es {eos_token_id}")        if not isinstance(max_length, int) or max_length < 1:          raise ValueError(f"`max_length` debe ser un entero mayor que 1, pero es {max_length}")        self.eos_token_id = eos_token_id        self.max_length=max_length  def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:    cur_len = input_ids.shape[-1]    # comenzar a aumentar la recompensa del token eos desde el 80% de la longitud máxima de forma progresiva en la longitud    for cur_len in (max(0,int(self.max_length*0.8)), self.max_length ):      ratio = cur_len/self.max_length      num_tokens = scores.shape[1] # tamaño del vocabulario      scores[:, [i for i in range(num_tokens) if i != self.eos_token_id]] =\      scores[:, [i for i in range(num_tokens) if i != self.eos_token_id]]*ratio*10*torch.exp(-torch.sign(scores[:, [i for i in range(num_tokens) if i != self.eos_token_id]]))      scores[:, self.eos_token_id] = 1e2*ratio    return scores

En el método “__call__” de la clase, mejoramos la probabilidad (puntuación) del token eos en función de la longitud de la secuencia. Cuando la longitud se acerca al 80% de la longitud máxima especificada, establecemos la puntuación del eos_token_id en 1e2 multiplicado por una proporción de longitud y ajustamos las puntuaciones de los demás tokens hacia abajo en consecuencia.

Ahora declaremos el procesador de logits en la definición del pipeline:

pipe = transformers.pipeline(model=model,    tokenizer=tokenizer,    return_full_text=True,  # langchain espera el texto completo    task='text-generation',    # también pasamos parámetros del modelo aquí    #stopping_criteria=stopping_criteria,  # sin esto, el modelo divaga durante el chat    logits_processor=logits_process_list,    max_new_tokens=500,  # número máximo de tokens a generar en la salida    temperature=0.1,    )

Ejecutemos el pipeline nuevamente con la misma indicación “¿Qué puedo hacer en París?” y obtenemos:

París, la capital de Francia, es conocida por su impresionante arquitectura, museos de arte, lugares históricos y atmósfera romántica.

¡Funciona bien! Hemos obtenido una respuesta completa aunque pueda parecer breve.

Ajuste fino

Si el modelo no logra generar el token EOS, ¿por qué no considerar instruirlo para que lo haga? El concepto de mejorar el rendimiento del modelo mediante el ajuste fino con un conjunto de datos que incluye respuestas que concluyen con el token EOS es sin duda un camino prometedor para explorar.

En esta sección, utilizaré sin vergüenza el trabajo realizado en esta publicación de blog que empleó un método de ajuste fino eficiente en parámetros (PEFT), como QLoRA, para ajustar finamente el modelo Llama 2-7b. Al igual que su predecesor, LoRA, QLoRA utiliza un pequeño conjunto de parámetros entrenables (adaptadores) manteniendo los parámetros principales del modelo sin cambios. Introduce dos innovaciones notables: NormalFloat de 4 bits (NF4), un método de cuantización de datos óptimo desde el punto de vista teórico de la información para datos normales, y Doble Cuantización. Para una comprensión más profunda, consulte el artículo original si tiene algún interés adicional en este tema.

Vamos a entrenar el modelo en un conjunto de datos llamado ‘timdettmers/openassistant-guanaco’ que se puede encontrar en la base de datos de Hugging Face. Este conjunto de datos tiene el siguiente formato donde la conversación entre el humano y el asistente está separada por “###”.

Autor de la imagen: “timdettmers/openassistant-guanaco'/ dataset

Antes de entrenar, tenemos que transformar los datos en la plantilla de prompt de Llama 2:

<s>[INST] <<SYS>>{tu_mensaje_del_sistema}<</SYS>> {mensaje_del_usuario_1} [/INST]

Me saltaré el detalle de la transformación del conjunto de datos aquí. Ahora echemos un vistazo a la parte principal del entrenamiento dado por el siguiente código:

# Cargar la configuración de LoRApeft_config = LoraConfig(    lora_alpha=lora_alpha,    lora_dropout=lora_dropout,    r=lora_r,    bias="none",    task_type="CAUSAL_LM",)# Establecer los parámetros de ajuste fino supervisadotrainer = SFTTrainer(    model=model,    train_dataset=dataset,    peft_config=peft_config,    dataset_text_field="text",    max_seq_length=max_seq_length,    tokenizer=tokenizer,    args=training_arguments,    packing=packing,)# Entrenar el modelotrainer.train()

En el contexto de un conjunto de datos que comprende instrucciones y respuestas, nuestro enfoque involucró el uso de un Entrenador Supervisado (SFTainer) en conjunto con el método QLoRA para ajustar finamente los parámetros de peso dentro del Modelo de Lenguaje (LLM). Nuestro objetivo principal fue minimizar las discrepancias entre las respuestas generadas y las respuestas de referencia, que sirvieron como nuestras etiquetas de referencia.

Un parámetro significativo dentro de esta configuración es “lora r”, que representa un valor relativamente pequeño que se refiere tanto a las dimensiones segunda como primera de los pares de matrices de peso de descomposición de rango. El entrenamiento ocurrió exclusivamente en estas dos matrices, complementando los pesos existentes.

Entrenamos el modelo durante 250 pasos con la pérdida de entrenamiento dada en la siguiente gráfica:

Imagen del autor: pérdida de entrenamiento de llama 2 durante 250 pasos

Ahora ejecutemos el pipeline con el modelo ajustado finamente. Esta vez, obtenemos:

París, la capital de Francia, es conocida por su impresionante arquitectura, rica historia y atracciones culturales. Aquí hay algunas cosas populares para hacer en París:1. Visitar lugares emblemáticos: La Torre Eiffel, el Arco de Triunfo, la Catedral de Notre-Dame y el Museo del Louvre son algunos de los lugares más famosos de París.2. Explorar museos y galerías de arte: París alberga muchos museos y galerías de arte de clase mundial, incluido el Louvre, el Museo de Orsay y el Centro Pompidou.3. Disfrutar de los jardines y parques de la ciudad: París tiene varios jardines y parques hermosos, incluidos los Jardines de Luxemburgo, el Jardín de las Tullerías y el Campo de Marte.4. Tomar un crucero por el río: Un crucero por el río Sena es una excelente manera de ver los lugares de la ciudad y aprender sobre su historia.5. Ir de compras en los Campos Elíseos: La famosa avenida está llena de boutiques de alta gama, grandes almacenes y cafés.6. Asistir a un espectáculo de cabaret: El Moulin Rouge es un famoso cabaret que ofrece un espectáculo animado y entretenido.7. Visitar Montmartre: El encantador barrio de Montmartre es conocido por su ambiente artístico, artistas callejeros e impresionantes vistas de la ciudad.8. Realizar un recorrido gastronómico: París es conocida por sus delicias culinarias y un recorrido gastronómico es una excelente manera de probar la famosa cocina de la ciudad.9. Visitar el Palacio de Versalles: El Palacio de Versalles es un hermoso palacio real ubicado justo fuera de París.10. Asistir a un desfile de moda: París es la capital de la moda del mundo y asistir a un desfile de moda es una excelente manera de ver las últimas tendencias y estilos.Estas son solo algunas de las muchas cosas que se pueden hacer en París. La ciudad tiene algo para todos, desde historia y cultura hasta comida y entretenimiento.

¡Esta es realmente una respuesta hermosa!

Bono: da una indicación diferente

En última instancia, tenemos un truco sencillo pero efectivo a nuestra disposición, especialmente cuando la verbosidad del modelo no es una preocupación. Podemos mencionar explícitamente en la indicación que requerimos una respuesta concisa. Por ejemplo, cuando le pregunto al modelo: “¿Qué puedo hacer en París? Responde en cinco oraciones o menos”, proporciona lo siguiente:

¡Claro! Aquí tienes cinco cosas que puedes hacer en París: 1. Visitar la Torre Eiffel, un hito icónico que ofrece impresionantes vistas de la ciudad. 2. Explorar el Museo del Louvre y ver obras de arte famosas como la Mona Lisa. 3. Pasear por el río Sena y disfrutar de los pintorescos puentes y encantadoras calles de la ciudad. 4. Visitar la Catedral de Notre-Dame, una iglesia hermosa e histórica. 5. Disfrutar de una deliciosa comida francesa en un bistró o restaurante tradicional.

Es una respuesta corta pero clara y completa.

Criterio de detención: un intento fallido

Para aquellos interesados, Hugging Face ha introducido otra API llamada StoppingCriteria, destinada a establecer condiciones específicas que obligan a una secuencia a detenerse. Sin embargo, cuando se trata de definir un criterio personalizado que detenga el modelo al encontrar ciertos tokens (por ejemplo, ‘\n’), es posible que no proporcione una solución integral al problema. Como ejemplo, intenté crear una clase StopOnTokens:

# Definir un objeto de criterio de detención personalizadoclass StopOnTokens(StoppingCriteria):    def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool:        for stop_ids in stop_token_ids:            if torch.eq(input_ids[0][-len(stop_ids):], stop_ids).all():                return True        return Falsestopping_criteria = StoppingCriteriaList([StopOnTokens()])

Sin embargo, el modelo aún no logra dar una respuesta completa.

Conclusión

En esta publicación de blog, resalté el problema de la parada de generación en Llama 2 e introduje varias soluciones provisionales. Nuevamente, omito muchos detalles de implementación y recomiendo que eches un vistazo más profundo a mi cuaderno.

Imagen de Jose Aragones

Sin embargo, es importante tener en cuenta que estas soluciones están destinadas a mejorar la facilidad de uso de las respuestas a corto plazo, pero esperamos ansiosamente una solución permanente para abordar este problema.