una simulación de ti mismo mediante LLMs

Ajustar un LLM con tus textos: una simulación de ti mismo (parte 5)

Juan Gabriel Gomila Juan Gabriel Gomila
12 minutos

Leer el artículo
Audio generated by DropInBlog's Blog Voice AI™ may have slight pronunciation nuances. Learn more

Esta es la conclusión de mi guía para ajustar un modelo de lenguaje grande (LLM) usando tu historial de mensajes de texto para crear una simulación de ti mismo. 📱 Es hora de generar texto. Durante las vacaciones, lo hice con mi historial de 240,000 mensajes, ¡y los resultados fueron bastante entretenidos! 😄


El recorrido hasta ahora:

1️⃣ En la parte 1, configuramos el entorno y descargamos el historial de textos / WhatsApp.
2️⃣ En la parte 2, organizamos y filtramos los textos.
3️⃣ En la parte 3, curamos, encriptamos y subimos los conjuntos de datos.
4️⃣ En la última entrega, la parte 4, realizamos el ajuste fino con QLoRA en un modelo base de Llama 2.

Recuerda que el post original de Edward Donner, creador del curso en inglés lo puedes consultar directamente en su blog.

¿Aún no estás inscrito en el curso completo?

Curso Ingeniería de LLMs

Accede a todo el contenido del curso Ingeniería de LLMs e IA Generativa y domina las técnicas más avanzadas de IA generativa. ¡No pierdas esta oportunidad para transformar tu carrera profesional! 🎓

Apúntate al curso ahora

Prepárate para la inferencia

Usé un cuaderno nuevo de Jupyter para experimentar con la generación de texto en paralelo al entrenamiento. 🧪 Comienza configurando las constantes, asegurándote de que coincidan con las usadas durante el entrenamiento: 💻

BASE_MODEL_NAME = "meta-llama/Llama-2-7b-chat-hf"
PROJECT_NAME = 'messages'
RUN_NAME = 'v1'
MODEL_NAME = f"tu-usuario-de-hf/{PROJECT_NAME}-{RUN_NAME}"
MAX_LENGTH = 200
ME = "Edward" # tu nombre va aquí

Ahora, las instalaciones e importaciones necesarias. Si no estás familiarizado con las bibliotecas que estamos usando, consúltalas en las páginas de HF como los tutoriales o las API docs.

# installs
!pip install -q  torch peft bitsandbytes transformers trl accelerate sentencepiece

# imports
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, PeftModel, PeftConfig
from IPython.display import clear_output

Nos logeamos en Hugging Face como anteriormente para poder crear una simulación de ti mismo:

from huggingface_hub import notebook_login
notebook_login()


Carga tu modelo ajustado con fine-tuning

Es hora de descargar tu modelo personalizado y elaborado con cariño.Llama 2 desde Hugging Face para poder crear una simulación de ti mismo:

tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

quant_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

base_model = AutoModelForCausalLM.from_pretrained(
    BASE_MODEL_NAME,
    quantization_config=quant_config,
    device_map="auto",
)

base_model.config.use_cache = False
base_model.config.pretraining_tp = 1

model = PeftModel.from_pretrained(
    base_model,
    MODEL_NAME,
    tokenizer=tokenizer,
    max_seq_length=MAX_LENGTH,
)


Generación de texto

Recordarás de la Parte 1 que Hugging Face ofrece pipelines súper convenientes para la generación. Sin embargo, descubrí rápidamente que necesitaba un control más detallado sobre el proceso de generación. Fue este trabajo, más que cualquier otra cosa, lo que marcó la mayor diferencia en la calidad de los resultados.

Comencé con un método auxiliar:

SUPPRESS_TOKENS = [26308, 243, 162,   155,   149, 160, 47, 18610]
BAD_WORDS = [[26308], [243], [162], [155], [149], [160], [47], [18610], [229,159,171]]


def generate_next(text, min_tokens, max_tokens):
  inputs, outputs = [], []
  attempt = ""
  final_tokens = []
  try:
    inputs = tokenizer(text, return_tensors="pt").to('cuda')
    outputs = model.generate(**inputs,
                                    max_new_tokens=max_tokens,
                                    min_new_tokens=min_tokens,
                                    return_dict_in_generate=True,
                                    output_scores=False,
                                    no_repeat_ngram_size=6,
                                    suppress_tokens = SUPPRESS_TOKENS,
                                    bad_words_ids = BAD_WORDS)
    sequence = outputs['sequences'][0]
    attempt = tokenizer.decode(sequence, skip_special_tokens=True)
    final_tokens = sequence[:-10]
  finally:
    del inputs
    del outputs
    torch.cuda.empty_cache()
  return attempt, final_tokens

Estoy utilizando SUPPRESS_TOKENS y BAD_WORDS para evitar que el modelo genere algunos tokens al establecer sus probabilidades en 0 antes de realizar el muestreo. El token 18610 es "***", el cual sustituimos por una imagen. Descubrí que era mejor suprimir estos tokens, ya que de lo contrario el modelo a veces quedaba atrapado en un bucle intercambiando imágenes furiosamente consigo mismo.

También notarás que incluí un proceso de limpieza después de la generación para evitar que la GPU se quede sin memoria. 🖥️💡


Un último, último ajuste de datos

Entiendo que este próximo fragmento de código puede parecer mucho para procesar, pero prometo que valdrá la pena. 😅 Frecuentemente encontré que la generación de conversaciones podía desordenarse. Por ejemplo, el modelo a veces comenzaba a alucinar, y el formato se desviaba, necesitando ser reparado. Esto me recordó un poco a ese icónico momento de Arnie:

 


Así que decidí trabajar con mucho cuidado, generando unos pocos tokens a la vez y reconstruyendo la conversación en cada paso. Creé dos clases para ayudar en este proceso: Message y Conversation.

Además de reparar la conversación, este código trunca a los últimos 10 mensajes (ver NLI_MAX_MESSAGES), lo cual parece ser suficiente para mantener el contexto de la conversación. Si intentas incluir demasiados mensajes, el LLM empieza a perder el hilo... ¡prueba aumentando ese número para experimentar tu propio colapso al estilo TWO WEEKS! 😅

class Message:
  def __init__(self, sender=None, message=None, text=None):
    self.is_complete = True
    if message is not None:
      self.sender = ME if sender is None else sender
      self.message = message
    else:
      if ':' not in text and ';' in text:
        text = text.replace(';',':')
      if ':' not in text:
        self.sender = text
        self.message = ''
        self.is_complete = False
      else:
        beginning, ending = text.split(':')
        self.sender = beginning.replace('###', '').strip()
        self.message = ending.strip()

  def __repr__(self):
    if self.is_complete:
      return f'### {self.sender}: {self.message}'
    else:
      return f'### {self.sender}'


class Conversation:

  NLI_MAX_MESSAGES = 10

  def __init__(self, who):
    self.who = who
    self.messages = []
    self.nli_message_count = 0
    self.current_sender = ME

  def prefix(self):
    result = f"<<SYS>>Write a realistic text message chat. Avoid repetition.<</SYS>>\n"
    result += f"[INST]Write a chat between {ME} and {self.who}[/INST]\n"
    return result

  def next_sender(self):
    self.current_sender = self.who if self.current_sender == ME else ME

  def add(self, message_contents):
    self.add_message(Message(message=message_contents, sender=self.current_sender))

  def add_message(self, message):
    self.messages.append(message)

  def add_prompt(self):
    self.add('')

  def nli(self):
    result = self.prefix()
    nlis = [message.__repr__() for message in self.messages[-Conversation.NLI_MAX_MESSAGES:]]
    self.nli_message_count = len(nlis)
    result += ' '.join(nlis)
    return result

  def __repr__(self):
    result = ""
    for message in self.messages:
      result += message.__repr__() + '\n'
    return result

  def process(self, language):
    language = language.replace('?:',':').replace('::',':')
    incoming = language.replace(' ###','###').split('### ')[1:]
    self.messages = self.messages[:-1] # remove the last message
    new_messages = incoming[self.nli_message_count-1:]
    for index, new_message in enumerate(incoming[self.nli_message_count-1:]):
      message = Message(text=new_message)
      if message.sender != self.current_sender and index != 0:
        return True
      else:
        self.add_message(message)
    return False


Hora de jugar con la simulación de ti mismo

Después de haber curado cuidadosamente tu dataset, afinado tu modelo Llama y gastado $100, finalmente estás listo para simularte a ti mismo. Ejecuta este código y presiona Enter cada vez que quieras que el LLM genere una respuesta. El modelo puede actuar como tú, como la otra persona en el chat, o incluso como... ¡ambos! 🤖💬

¿Listo para ver cómo se desenvuelve tu propio clon digital, una simulación de ti mismo en tus conversaciones? 😄

print('Who is the conversation with?')
who = input()

conversation = Conversation(who)
while True:
  print(f'{conversation.current_sender}: ')
  reply = input()
  if reply == 'stop':
    break
  elif reply != '':
    conversation.add(reply)
  else:
    conversation.add_prompt()
    ready = False
    while not ready:
      language, final_tokens = generate_next(conversation.nli(), 3, 8)
      ready = conversation.process(language)
      clear_output(wait=True)
      print(conversation)
      # print(final_tokens)
  conversation.next_sender()

He comentado la línea print(final_tokens) — esta imprime las últimas líneas de la conversación como tokens. Si tu modelo comienza a generar caracteres, palabras o frases espurias o repetitivas, puedes usar esta función para identificar los tokens problemáticos y agregarlos a las listas de supresión. 🛠️


Tu recompensa: una simulación de ti mismo 🏆

¡Ahora puedes disfrutar probando las conversaciones con la simulación de ti mismo! 🎉 Al principio, los temas pueden parecer algo simples, pero basta con provocar al LLM con preguntas interesantes o "picantes" para sacar su lado más intrigante. 😏 Por ejemplo, ¡intenta que la versión simulada de ti mismo revele tus secretos más profundos!

Lo que me pareció más inquietante fue lo bien que el modelo adoptó el tono y las características de mis amigos, incluso en temas cotidianos. Aquí tienes un ejemplo con mi amigo “James,” quien siempre es excelente organizando su tiempo y cumpliendo con los planes.

🧠✨ ¿Listo para asombrarte con lo que tu modelo puede lograr creando una simulación de ti mismo?

### Edward: Hey James
### James: Hey Ed. How are you?
### Edward: I'm great thanks. What are you up to
### James: Just in town for a few days. I'm going to be here until the 27th.
### James: I'm actually in the city now. Just at the gym
### Edward: Free to hang out while you're here?
### James: Sure. What are you doing?
### Edward: Just working on a blog post
### James: What time are you done?
### Edward: I'll be finished at 6pm - it would be great to see you tonight if you're free
### James: 6pm is perfect.

Y aquí está “John”, que tiene un estilo más apresurado, usa demasiado las comas y es bastante imposible lograr que se comprometa con algo:

### Edward: Hey John
### John: Hey, how are u?
### Edward: I'm great thanks. What are you up to
### John: I'm at work, waiting for a client, what's new?
### Edward: Not much, I'm working on a blog post
### John: Oh, that's cool, what's the topic?
### Edward: it's about generating text chats
### John: I'm interested, I've been working on my own web site, I have a lot of pics to post, but I need to figure out how to do it
### Edward: you know I'm good at things like that, I'd love to help
### John: I know, u have a lot of knowledge in this field, I'll ask u for help, I don't want to make it too hard to navigate
### John: I'll let u know when I'm ready
### Edward: I'd love that.. and are you free to hang out some time?
### John: I'm not sure, I'm trying to get back into work, I'll let u no when I can
### Edward: How about this weekend? It would be great to catch up
### John: I'll let u now, I'm trying not to be so flaky


Puede que no parezca mucha cosa al principio

Pero, por favor, no te desanimes. Comienza con un poco de depuración; revisa tres veces que los datasets de entrenamiento estén bien estructurados. Sumérgete en los gráficos de Weights & Biases y confirma que las métricas se están moviendo en la dirección correcta. Cambia los hiperparámetros, uno a la vez al principio. Y lo más importante: profundiza en el proceso de generación de texto y busca mejoras, como suprimir más tokens.

Puedes probar más opciones tras crear esta simulación de ti mismo en la llamada a model.generate(), consultando la documentación. Yo experimenté con la búsqueda en haz (beam search), pero obtuve los mejores resultados con la configuración predeterminada mencionada arriba. Tuve algunos problemas con un valor bajo en no_repeat_ngram_size, lo que causaba que conversaciones más largas se descontrolaran... pero deberías probarlo.

num_beams=8,
num_return_sequences=1,
no_repeat_ngram_size=6,
repetition_penalty=1.0, # default
temperature = 1.0, # default
do_sample=True, # default

Y finalmente, cuando empieces a ver buenos resultados, cambia a usar la variante de 13B de Llama 2. El proceso de fine-tuning tomará más tiempo, por supuesto, pero encontré que la calidad de los resultados mejoró sustancialmente.


¿Qué sigue tras crear una simulación de ti mismo?

¡Tengo tantas ideas para probar más cosas! He estado explorando modelos en la cima del ranking de Hugging Face, pero hasta ahora la versión de Llama 2 es la mejor. Estoy considerando entrenar otro modelo con mi historial de Slack — me intriga ver una conversación entre mi yo de trabajo y mi yo en casa...

Espero que hayas disfrutado este viaje y aprendido algo en el camino. Si has encontrado errores o mejoras en mi código, o mejores técnicas o hiperparámetros, por favor, ponte en contacto. Me encantaría probar tus ideas y publicaré cualquier actualización aquí.

Solo tengo una solicitud. Si me contactas por mensaje de texto — asegúrate de no pedir mis secretos más profundos 😂

¡Nos vemos en clase!

« Volver al Blog

Obtener mi regalo ahora