{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "cYySPW4nFUC5"
},
"source": [
"Redes Neuronales para Lenguaje Natural, 2024\n",
"\n",
"---\n",
"# **Grandes Modelos de Lenguaje (LLMs) para clasificación de emoción en tweets**\n",
"\n",
"En este notebook haremos experimentos utilizando clasificando emociones en tweets usando un LLM mediante técnicas de prompting.\n",
"\n",
"**Importante:** Se debe ejecutar usando un runtime con GPU.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "lEtiqwo1j1QR"
},
"source": [
"## Librerías, configuraciones y funciones auxiliares\n",
"\n",
"Ejecutar las siguientes celdas para instalar las librerías necesarias, configurar formato de salida de Colab (word-wrap) y definir funciones necesarias más adelante para evaluación."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "lJHOJf9OVMci"
},
"outputs": [],
"source": [
"!pip install -U transformers\n",
"!pip install -U bitsandbytes"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "zra_NaGlodhF"
},
"outputs": [],
"source": [
"import torch\n",
"import numpy as np\n",
"\n",
"DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
"\n",
"EMOTIONS_ES = [\"Alegría\", \"Tristeza\", \"Desagrado\", \"Ira\", \"Miedo\", \"Sorpresa\", \"Otras\"]\n",
"EMOTIONS_ES_DICT = { e:i for (i,e) in enumerate(EMOTIONS_ES) }\n",
"EMOTIONS_EN = [\"joy\", \"sadness\", \"disgust\", \"anger\", \"fear\", \"surprise\", \"others\"]\n",
"EMOTIONS_EN_DICT = { e:i for (i,e) in enumerate(EMOTIONS_EN) }\n",
"EMOTIONS_ES_EN = { es:en for (es,en) in zip(EMOTIONS_ES,EMOTIONS_EN) }\n",
"EMOTIONS_EN_ES = { en:es for (en,es) in zip(EMOTIONS_EN,EMOTIONS_ES) }"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "VKkmPo6bwd_0"
},
"outputs": [],
"source": [
"# Configuración de formato de salida en colab\n",
"from IPython.display import HTML, display\n",
"\n",
"def set_css():\n",
" display(HTML('''\n",
" \n",
" '''))\n",
"get_ipython().events.register('pre_run_cell', set_css)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "CQOcUS6xk8vv"
},
"outputs": [],
"source": [
"# Funciones auxiliares\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, f1_score, precision_recall_fscore_support\n",
"\n",
"def plot_confusion_matrix(reference, prediction, labels):\n",
" cm = confusion_matrix(reference, prediction, labels=labels)\n",
" disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=labels)\n",
" disp.plot()\n",
" plt.show()\n",
"\n",
"def print_fscore(reference, prediction, labels):\n",
" print(\"Macro:\", f1_score(reference, prediction, average='macro'))\n",
" scores = f1_score(reference, prediction, average=None, labels=labels)\n",
" for i in range(len(labels)):\n",
" print(f\"Clase {labels[i]}:\", + scores[i])\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "wOhveGs5CTW5"
},
"source": [
"## Carga de la información de los tweets\n",
"\n",
"Los datos que utilizaremos en este ejercicio son los de la competencia EmoEvalEs@IberLEF2021 (https://competitions.codalab.org/competitions/28682). En esta competencia se propone asignar la emoción principal de un tweet a una de 7 categorías:\n",
"* enojo / anger\n",
"* desagrado / disgust\n",
"* miedo / fear\n",
"* alegría / joy\n",
"* tristeza / sadness\n",
"* sorpresa / surprise\n",
"* otras / others\n",
"\n",
"Descargaremos y utilizaremos las particiones de train y de dev, ya que la de test no cuenta con las etiquetas reales de la emoción esperada. Recordar que en un ejemplo real deberíamos partir el corpus de train para obtener una nueva partición de dev, o utilizar otra técnica como validación cruzada.\n",
"\n",
"El corpus tiene más información, pero en este caso solo cargaremos cada ejemplo como . Se imprime una entrada aleatoria de cada partición para probar que se hayan cargado correctamente."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "0uSuPgjOBY7x"
},
"outputs": [],
"source": [
"! wget https://www.fing.edu.uy/owncloud/index.php/s/cgbdFYX2te4zLrA/download/emoeval_data.zip\n",
"! unzip emoeval_data.zip"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "dVXNwLxf8hkJ"
},
"outputs": [],
"source": [
"import csv\n",
"import random\n",
"\n",
"def load_corpus(corpusFile):\n",
" with open(corpusFile, newline='') as corpus_csv:\n",
" reader = csv.reader(corpus_csv, delimiter=',')\n",
" titles = next(reader)\n",
" corpus = [[x[2],EMOTIONS_EN_ES[x[4]]] for x in reader]\n",
" return corpus\n",
"\n",
"corpus_train = load_corpus('train.csv')\n",
"corpus_test = load_corpus('dev.csv')\n",
"\n",
"print(random.choice(corpus_train))\n",
"print(random.choice(corpus_test))"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "wj2XWl3ku3Xl"
},
"source": [
"Debido a que no contamos con las etiquetas reales de test, utilizaremos como partición de test la que originalmente se usó como dev en la competencia. Este conjunto que usaremos para evaluar las diferentes técnicas tiene **884 ejemplos**:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"id": "0DAKfxooupfX"
},
"outputs": [],
"source": [
"print(\"Tamaño de corpus de test:\", len(corpus_test))"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "6gKGmpVMvQBq"
},
"source": [
"Debido a que los grandes modelos de lenguaje requieren del uso de mucho cómputo, cada ejecución suele llevar una cantidad de tiempo no despreciable. Para que la evaluación lleve un tiempo razonable, utilizaremos **100 ejemplos aleatorios**.\n",
"\n",
"Complete la siguiente celda para reempazar `corpus_te` con una versión reducida de 100 ejemplos.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "Tjc-MdCmwgfM"
},
"outputs": [],
"source": [
"corpus_test = ... # Completar para obtener 100 ejemplos aleatorios del corpus de test\n",
"\n",
"print(\"Nuevo tamaño de corpus de test:\", len(corpus_test))\n",
"\n",
"for e in EMOTIONS_ES:\n",
" print(e,len(list(t for t in corpus_test if t[1] == e)))"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ZyPn_Cwtq81r"
},
"source": [
"## Configuración de LLM\n",
"\n",
"Utilizaremos variantes del modelo **Llama 3.2** de Meta a través de la plataforma [HuggingFace](https://huggingface.co/). Para poder usar este modelo en HuggingFace es necesario seguir los siguientes pasos:\n",
"\n",
"- Crearse una cuenta de HuggingFace (https://huggingface.co/)\n",
"- Aceptar los términos para usar los modelos Llama-3.2 en HuggingFace, que aparecen en el siguiente enlace: https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct\n",
"- Crear un token de HuggingFace siguiendo el siguiente enlace: https://huggingface.co/settings/tokens\n",
"- Ejecutar la siguiente celda e ingresar el token creado."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "dpJk4QSGAPkE"
},
"outputs": [],
"source": [
"# Ejecutar para conectarse a HuggingFace\n",
"from huggingface_hub import notebook_login\n",
"\n",
"notebook_login()"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "-JM4WlARzZju"
},
"source": [
"Para cargar el LLM, son necesarios dos componentes: el **tokenizer**, encargado de dividir las secuencias de texto en partes más pequeñas llamadas tokens que el modelo es capaz de procesar, y el **modelo**, que es la red neuronal profunda (*transformer*) con 7 mil millones de parámetros.\n",
"\n",
"Comenzaremos cargando el tokenizer ejecutando la siguiente celda.\n",
"\n"
]
},
{
"cell_type": "code",
"source": [
"from transformers import AutoTokenizer\n",
"\n",
"tokenizer = AutoTokenizer.from_pretrained(\"meta-llama/Llama-3.2-1B-Instruct\")"
],
"metadata": {
"id": "axLi3SAROWHG"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "4a1UtbCk1Tln"
},
"source": [
"A continuación veremos el proceso de tokenización realizado por el tokenizer para un tweet aleatorio del conjunto de train.\n",
"\n",
"El resultado es una lista de identificadores, donde cada identificador corresponde a un token.\n",
"\n",
"Complete la siguiente celda.\n",
"\n",
"La documentación para trabajar con el tokenizer está disponible en: https://huggingface.co/transformers/v2.11.0/main_classes/tokenizer.html. En particular, se recomienda utilizar la función `convert_ids_to_tokens` para obtener los tokens a partir de la lista de ids."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "FOCeiHr02OyK"
},
"outputs": [],
"source": [
"import random\n",
"\n",
"tweet = ... # Elegir un tuit aleatorio\n",
"tokens = ... # Aplicar tokenizer al tuit\n",
"token_ids = ... # Convertir los tokens a ids\n",
"\n",
"print(\"Tweet:\", tweet)\n",
"print(\"\\nTokens:\", tokens)\n",
"print(\"\\nIdentificadores de tokens:\", token_ids)\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "iBj6e74T2iYy"
},
"source": [
"**Responda la siguiente pregunta:** ¿Qué puede estar representando el símbolo `Ġ` que aparece al principio de algunos tokens?\n",
"\n",
"**Respuesta:**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "aSNXQfkBCNJS"
},
"source": [
"A continuación, imprima el tamaño del vocabulario. Esto es, la cantidad total de tokens.\n",
"\n",
"Para esto, se puede usar la función `get_vocab` del tokenizer, y `len` para obtener el tamaño."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "sPtDq61aCjfX"
},
"outputs": [],
"source": [
"# Imprimir tamaño del vocabulario\n",
"print(\"Tamaño del vocabulario\",len(tokenizer.get_vocab()))\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "90KmNg874h6t"
},
"source": [
"A continuación cargaremos el modelo. La ejecución de la siguiente celda puede llevar algunos minutos, ya que se descargarán los parámetros del modelo y se cargaran en la memoria de la GPU."
]
},
{
"cell_type": "code",
"source": [
"from transformers import AutoModelForCausalLM, BitsAndBytesConfig\n",
"\n",
"# Configuración de cuantización a 4 bits (para mejorar eficiencia)\n",
"bnb_config = BitsAndBytesConfig(\n",
" load_in_4bit=True,\n",
" bnb_4bit_quant_type=\"nf4\",\n",
" bnb_4bit_compute_dtype=torch.bfloat16\n",
" )\n",
"\n",
"model = AutoModelForCausalLM.from_pretrained(\n",
" \"meta-llama/Llama-3.2-1B-Instruct\",\n",
" quantization_config=bnb_config,\n",
" device_map=\"auto\",\n",
" )"
],
"metadata": {
"id": "GcPVTkMqqIsa"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "4g6ZlRqk7J2w"
},
"source": [
"La siguiente celda muestra la arquitectura del modelo."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "OCs4I-_x7XGd"
},
"outputs": [],
"source": [
"print(model)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "hrWmXZwz8Gic"
},
"source": [
"Podemos observar que el modelo consiste de las siguientes partes:\n",
"\n",
"- **embed_tokens:** La primer capa, encargada de asociar cada token a un embedding (vector de tamaño 2048).\n",
"\n",
"- **LlamaDecoderLayer:** 16 capas iguales compuestas por\n",
"\n",
" - **self_attn** (self-attention): Mecanismo atencional, caractéristico de la arquitecura *transformer*.\n",
" - **mlp** (mutilayer perceptron): Capa feed-forward, que procesa la salida del self-attention.\n",
" - Luego se aplican normalizaciones a la salida.\n",
"\n",
"- **lm_head**: Aplica una transformación lineal a la salida de la última capa, y retorna un vector de tamaño 128256."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "pXx2VGcV-2xT"
},
"source": [
"**Responda las siguientes preguntas:**\n",
"\n",
"1. ¿Cuál es la función de activación que se utiliza en los MLP?\n",
"2. Se puede observar que el tamaño de la salida de la última capa es de 128256. ¿Por qué tiene ese tamaño? ¿A qué se corresponde?\n",
"\n",
"**Respuestas:**"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "z9jJ5-X_DMS7"
},
"source": [
"## Generación de texto usando el LLM\n",
"\n",
"En la siguiente celda se define la función que utilizaremos para generar texto con el LLM.\n",
"\n",
"Se puede explorar los atributos de la clase GenerationConfig para ver más opciones que se pueden utilizar al momento de la generación:\n",
"https://huggingface.co/docs/transformers/en/main_classes/text_generation"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "Jyf3eQPB4zQz"
},
"outputs": [],
"source": [
"# Generar respuesta\n",
"from transformers import GenerationConfig, pipeline\n",
"\n",
"tokenizer.pad_token_id = model.config.eos_token_id # Para generación abierta\n",
"\n",
"def get_response(prompts, model, temp=0.0, max_tok=500):\n",
"\n",
" # Configuración de temperatura\n",
" if temp == 0:\n",
" generation_config = GenerationConfig(\n",
" do_sample=False,\n",
" num_beams=1\n",
" )\n",
" else:\n",
" generation_config = GenerationConfig(\n",
" do_sample=True,\n",
" temperature=temp\n",
" )\n",
"\n",
" # Inicializar pipeline para generación de texto\n",
" pipe = pipeline(\n",
" \"text-generation\",\n",
" model=model,\n",
" config=generation_config,\n",
" tokenizer=tokenizer,\n",
" pad_token_id=tokenizer.eos_token_id\n",
" )\n",
"\n",
" # Generar texto para cada prompt en paralelo\n",
" output = pipe(\n",
" prompts,\n",
" return_full_text=False,\n",
" max_new_tokens=max_tok\n",
" )\n",
"\n",
" return output[0]['generated_text']"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "lkCJODtdJ77D"
},
"source": [
"Probamos la función anterior con una prompt de prueba. El modelo deberá completar la frase."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "zfWk48yaHnFg"
},
"outputs": [],
"source": [
"prompt = \"Uruguay es un país\"\n",
"output = get_response(prompt, model, max_tok=100)\n",
"\n",
"print(prompt + output)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5hUd3ZLwM16w"
},
"source": [
"El modelo que estamos utilizando es un modelo de tipo \"instruct\". Esto significa que pasó por un proceso de fine-tuning para adaptarlo a las interacciones tipo chat.\n",
"\n",
"Para esto, los modelos son entrenados con un formato de prompt específico (simulando un chat). Para obtener los mejores resultados es importante respetar este formato.\n",
"\n",
"El tokenizer implementa la función `apply_chat_template`, que dado una lista de mensajes aplica el formato de prompt adecuado.\n",
"\n",
"Cada mensaje de la lista es un diccionario que tiene un rol y el contenido del mensaje. Hay tres roles posibles:\n",
"\n",
"- **system:** Este es un único mensaje que siempre debe ir primero. Se utiliza para dar instrucciones generales al modelo de lenguaje.\n",
"\n",
"- **user:** Los mensajes de tipo usuario corresponden a los mensajes ingresados por el humano en una conversación con el modelo.\n",
"\n",
"- **assistant:** Los mensajes de tipo asistente corresponden a los mensajes generados por el modelo de lenguaje.\n",
"\n",
"El modelo de lenguaje es *stateless*. Esto significa que no mantiene el estado de la conversación. Es por eso que se debe mantener un diccionario con los mensajes intercambiados hasta el momento.\n",
"\n",
"La siguiente función recibe una lista de mensajes y retorna un string que corresponde a la prompt, que luego será utilizada para generar una respuesta."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "StX1kxR-Pkaf"
},
"outputs": [],
"source": [
"def create_prompt(messages):\n",
" return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "zDoP020MQGgb"
},
"source": [
"Veamos un ejemplo de prompt que sigue el formato adecuado, dada una lista de mensajes."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "TL4VwKAqQNqg"
},
"outputs": [],
"source": [
"messages = [\n",
" {\n",
" \"role\": \"system\",\n",
" \"content\": \"Eres un asistente servicial, respetuoso y honesto.\"\n",
" },\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": \"Hola, ¿cómo estás?\"\n",
" },\n",
" {\n",
" \"role\": \"assistant\",\n",
" \"content\": \"Hola! Estoy dispuesto a ayudarte.\"\n",
" },\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": \"¿Qué me puedes decir de Uruguay?\"\n",
" }\n",
" ]\n",
"\n",
"prompt = create_prompt(messages)\n",
"print(prompt)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "iouQbArmQzgw"
},
"outputs": [],
"source": [
"print(get_response(prompt,model,max_tok=100))"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "RMw1Zm5ARVqQ"
},
"source": [
"## Experimento 1: Prompting con instrucciones\n",
"\n",
"Este experimento consiste en **diseñar una prompt de sistema** adecuada para resolver la tarea. El objetivo es dar instrucciones al modelo del comportamiento esperado en el mensaje con rol de sistema. Luego, el tweet a clasificar será un mensaje de usuario, y se espera que el modelo retorne la etiqueta en el siguiente mensaje.\n",
"\n",
"**Aspectos importantes a tener en cuenta:**\n",
"\n",
"- Es importante dar el mayor contexto posible al modelo, explicando la tarea y las etiquetas. Recordar que el modelo no tiene conocimiento previo de la tarea que queremos que resuelva.\n",
"- El objetivo es que el primer token generado por el modelo sea una de las tres etiquetas (P, N, NONE), ya que luego generaremos un solo token. Es **fundamental** hacer esto explícito en la prompt, indicando que la respuesta esperada debe comenzar con la etiqueta."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "OO3GbgjyY4MM"
},
"outputs": [],
"source": [
"# Definición de prompt\n",
"def simple_prompt(tweet):\n",
" messages = [\n",
" {\n",
" \"role\": \"system\",\n",
" \"content\": ... # Completar con la prompt de sistema diseñada\n",
" },\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": tweet\n",
" }\n",
" ]\n",
" return create_prompt(messages)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "LonYpT5Otphg"
},
"source": [
"A continuación veremos la prompt generada para algún tweet del corpus de train, y generamos la respuesta con el LLM para verificar que el comportamiento es el esperado."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "QeBeSBAyZDH8"
},
"outputs": [],
"source": [
"prompt = simple_prompt(random.choice(corpus_train)[0])\n",
"output = get_response(prompt, model, max_tok=20)\n",
"\n",
"print(\"PROMPT:\\n\")\n",
"print(prompt)\n",
"print(\"\\nRESPUESTA:\\n\")\n",
"print(output)"
]
},
{
"cell_type": "markdown",
"source": [
"Se puede ver que, a pesar de que podemos refinar el prompt, el modelo habitualmente dirá más palabras que las esperadas. Escribir un método que intente obtener la emoción predicha a partir de la respueta."
],
"metadata": {
"id": "i1DjLlFXylFT"
}
},
{
"cell_type": "code",
"source": [
"def get_emotion_simple_prompt(tweet,model):\n",
" prompt = simple_prompt(tweet)\n",
" output = get_response(prompt, model, max_tok=20).lower()\n",
" return ... # ver si el string correspondiente a la emoción está en la salida obtenida\n",
"\n",
"tweet = random.choice(corpus_train)[0]\n",
"print(tweet)\n",
"print(get_emotion_simple_prompt(tweet,model))"
],
"metadata": {
"id": "N2T5NQ-fywey"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Obtenemos métricas sobre todo el corpus de test para el caso zero-shot:"
],
"metadata": {
"id": "7LJfwxFrp5oq"
}
},
{
"cell_type": "code",
"source": [
"reference = []\n",
"predictions = []\n",
"for t,e in corpus_test:\n",
" p = get_emotion_simple_prompt(t,model)\n",
" reference.append(e)\n",
" predictions.append(p)\n",
"\n",
"print_fscore(reference,predictions,EMOTIONS_ES)\n",
"plot_confusion_matrix(reference,predictions,EMOTIONS_ES)"
],
"metadata": {
"id": "vKL_jLBuz4iE"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Como lo que hace el modelo de lenguaje es devolvernos la probabilidad del siguiente token, podemos utilizar otra técnica para predecir la emoción: miramos el siguiente token predicho y vemos a cuál de las posibles emociones se le asigna más probabilidad.\n"
],
"metadata": {
"id": "QLFA7AkU5v1R"
}
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "SH6tVG0kLlDg"
},
"outputs": [],
"source": [
"# Obtener el primer token de cada emoción\n",
"emotion_ids = tokenizer(EMOTIONS_ES, add_special_tokens=False).input_ids\n",
"emotion_tokens = [e[0] for e in emotion_ids]\n",
"emotion_tokens = torch.tensor(emotion_tokens)\n",
"print(list((e,tokenizer.decode(e)) for e in emotion_tokens))\n",
"\n",
"def get_token_probabilities(prompt, model_llm):\n",
" '''\n",
" Dada una prompt, obtiene las probabilidades de los tokens de las emociones.\n",
"\n",
" Args:\n",
" prompt (str): La prompt para el modelo de lenguaje.\n",
"\n",
" Returns:\n",
" np.array: Un array con las probabilidades de los tokens de las emociones.\n",
" '''\n",
" inputs = tokenizer(prompt, return_tensors=\"pt\").to(DEVICE)\n",
"\n",
" with torch.no_grad():\n",
" outputs = model_llm(**inputs)\n",
"\n",
" # Obtener las probabilidades de los tokens\n",
" next_token_logits = outputs.logits[:, -1, :]\n",
" next_token_probs = torch.softmax(next_token_logits, dim=-1)\n",
"\n",
" # Obtener las probabilidades de los tokens de las emociones\n",
" emotion_probs = next_token_probs[0, emotion_tokens].cpu().numpy()\n",
"\n",
" # Normalizar las probabilidades\n",
" emotion_probs /= emotion_probs.sum()\n",
"\n",
" return emotion_probs\n"
]
},
{
"cell_type": "markdown",
"source": [
"Completar el código para obtener la emoción a partir de las probabilidades del primer token generado en la salida."
],
"metadata": {
"id": "Ck3keaG0qFcG"
}
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "HUvfxTKiLyey"
},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"def get_emotion_first_token(tweet, model):\n",
" prompt = simple_prompt(tweet)\n",
" return ... # la emoción correspondiente al token más probable de los que refieren a emociones\n",
"\n",
"tweet = random.choice(corpus_train)[0]\n",
"print(tweet)\n",
"print(get_emotion_first_token(tweet, model))\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "fJpvwGzpuq_Z"
},
"source": [
"Obtenemos métricas sobre todo el corpus de test para el caso zero-shot con el primer token generado:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "fKMNwtrGGMbI"
},
"outputs": [],
"source": [
"reference = []\n",
"predictions = []\n",
"for t,e in corpus_test:\n",
" p = get_emotion_first_token(t,model)\n",
" reference.append(e)\n",
" predictions.append(p)\n",
"\n",
"print_fscore(reference,predictions,EMOTIONS_ES)\n",
"\n",
"plot_confusion_matrix(reference,predictions,EMOTIONS_ES)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5VEnpEcfKO_2"
},
"source": [
"## Experimento 2: Few-Shot prompting\n",
"\n",
"El experimento anterior es la manera más sencilla de utilizar el LLM, simplemente indicando la tarea a resolver. Una desventaja de este método es que no utiliza la información disponible en el conjunto de entrenamiento.\n",
"\n",
"Una alternativa que suele mejorar los resultados es el método de **Few-Shot learning**. Few-Shot learning consiste en agregar algunos pocos ejemplos de la tarea resuelta en la prompt. Mediante los ejemplos (que se suman a las instrucciones), el modelo puede detectar patrones más fácilmente. Este método también se conoce como **In-Context Learning**, ya que, a diferencia de métodos como fine-tuning, se espera que el modelo aprenda a resolver una tarea nueva (para la que no fue entrenado) mediante ejemplos provistos en el contexto, sin modificaciones en sus parámetros.\n",
"\n",
"El método utilizado en el experimento anterior se puede ver como el caso particular de Few-Shot donde $n = 0$. A este caso se le llama **Zero-Shot Learning**.\n",
"\n",
"Para este experimento se debe completar la función `select_few_shot`, que debe retornar una lista con $n$ ejemplos elegidos con algún criterio del dataset pasado por parámetro. Cada ejemplo debe ser un diccionario con las claves `tweet` y `label`.\n",
"\n",
"Luego, estos $n$ ejemplos serán agregados a la lista de mensajes como (falsos) mensajes anteriores entre el usuario y el asistente.\n",
"\n",
"**Aspectos importantes a tener en cuenta:**\n",
"\n",
"- Se puede diseñar cualquier método para seleccionar los ejemplos. Algunos métodos posibles son:\n",
" - Elegir los ejemplos de forma aleatoria.\n",
" - Elegir los ejemplos de forma tal que haya la misma representación para cada clase.\n",
" - Mirar el conjunto de entrenamiento y elegir a mano ejemplos que resulten característicos de la etiqueta que representan.\n",
" - Etc.\n",
"- Nuevamente es necesario definir el prompt de sistema, al igual que en el experimento anterior. No es necesario diseñar una prompt de sistema nueva, se puede usar la misma que en experimento anterior.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "086ZgqYMYE7O"
},
"outputs": [],
"source": [
"# Definición de prompt\n",
"import random\n",
"\n",
"def select_few_shot(dataset, n=5):\n",
" # Crear una lista con n ejemplos seleccionados del dataset, donde cada elemento es un diccionario con claves \"tweet\", \"label\"\n",
" return ... # la lista creada\n",
"\n",
"def few_shot_prompt(tweet):\n",
" messages = [\n",
" {\n",
" \"role\": \"system\",\n",
" \"content\": ... # Completar con la prompt de sistema diseñada\n",
" }\n",
" ]\n",
"\n",
" few_shot_examples = select_few_shot(corpus_train,10)\n",
" for example in few_shot_examples:\n",
" messages.append({\"role\": \"user\", \"content\": example[\"tweet\"]})\n",
" messages.append({\"role\": \"assistant\", \"content\": example[\"label\"]})\n",
"\n",
" messages.append({\"role\": \"user\", \"content\": tweet})\n",
"\n",
" return create_prompt(messages)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "88WFlTnh0eNW"
},
"source": [
"A continuación veremos la prompt generada para el primer tweet del corpus de test, y generamos la respuesta con el LLM para verificar que el comportamiento es el esperado. Complete la siguiente celda."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "wrl4pbUZZH5G"
},
"outputs": [],
"source": [
"prompt = ...\n",
"output = ...\n",
"\n",
"print(\"PROMPT:\\n\")\n",
"print(prompt)\n",
"print(\"\\nRESPUESTA:\\n\")\n",
"print(output)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "9lq6tXAX0q2Y"
},
"source": [
"Al igual que en el experimento 1, el modelo podrá generar palabras de más. Obtener la emoción predicha a partir de la respuesta."
]
},
{
"cell_type": "code",
"source": [
"def get_emotion_few_shot(tweet,model):\n",
" prompt = few_shot_prompt(tweet)\n",
" output = get_response(prompt, model, max_tok=20).lower()\n",
" return ... # ver si el string correspondiente a la emoción está en la salida obtenida\n",
"\n",
"tweet = random.choice(corpus_train)[0]\n",
"print(tweet)\n",
"print(get_emotion_few_shot(tweet,model))\n"
],
"metadata": {
"id": "Dk_w-L8Z9vE5"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Obtenemos métricas sobre todo el corpus de test para el caso few-shot:"
],
"metadata": {
"id": "NFN6xxdErzsc"
}
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "XhWAugfxYE7c"
},
"outputs": [],
"source": [
"reference = []\n",
"predictions = []\n",
"for t,e in corpus_test:\n",
" p = get_emotion_few_shot(t,model)\n",
" reference.append(e)\n",
" predictions.append(p)\n",
"\n",
"print_fscore(reference,predictions,EMOTIONS_ES)\n",
"\n",
"plot_confusion_matrix(reference,predictions,EMOTIONS_ES)"
]
},
{
"cell_type": "markdown",
"source": [
"Podemos volver a probar el método de obtener la emoción más probable del primer token, pero esta vez con few shot."
],
"metadata": {
"id": "uwHwLs9Y_KLV"
}
},
{
"cell_type": "code",
"source": [
"import numpy as np\n",
"\n",
"def get_emotion_first_token_few_show(tweet,model):\n",
" prompt = few_shot_prompt(tweet)\n",
" return ... # la emoción correspondiente al token más probable de los que refieren a emociones\n",
"\n",
"tweet = random.choice(corpus_train)[0]\n",
"print(tweet)\n",
"print(get_emotion_first_token_few_show(tweet,model))\n"
],
"metadata": {
"id": "aNmHURGbZ0Dp"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Obtenemos métricas sobre todo el corpus de test para el caso few-shot con el primer token generado:"
],
"metadata": {
"id": "cphgZXQ6r3Io"
}
},
{
"cell_type": "code",
"source": [
"reference = []\n",
"predictions = []\n",
"for t,e in corpus_test:\n",
" p = get_emotion_first_token_few_show(t,model)\n",
" reference.append(e)\n",
" predictions.append(p)\n",
"\n",
"print_fscore(reference,predictions,EMOTIONS_ES)\n",
"\n",
"plot_confusion_matrix(reference,predictions,EMOTIONS_ES)"
],
"metadata": {
"id": "f056pXOoZ3Au"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "IfzsTEYB5AlE"
},
"source": [
"## (Opcional) Experimento 3: Modelo con fine-tuning\n",
"\n",
"Los resultados hasta el momento han sido muy bajos, pero notar que son muchas categorías posibles, y estamos trabajando con modelos de tamaño muy pequeño que además no están ajustados para esta tarea en particular. ¿Qué tan bien funcionaría un modelo ajustado?\n",
"\n",
"En este experimento utilizaremos un modelo que está basado en Llama-3.2-1B pero finetuneado con los datos de train de EmoEvalEs. Este modelo se mostró como ejemplo en el stand del Grupo PLN-InCo en Ingeniería de Muestra 2024.\n",
"\n",
"La diferencia es que este modelo fue finetuneado utilizando estas etiquetas:\n",
"* Alegría\n",
"* Tristeza\n",
"* Ira\n",
"* Miedo\n",
"* Sorpresa\n",
"* Neutral\n",
"\n",
"Notar que la etiqueta \"Desagrado\" no se encuentra en los ejemplos usamos para finetuning, y se cambia la etiqueta \"Otras\" por \"Neutral\".\n",
"\n",
"El prompt también tiene una modicación, ya que en el finetuning se utilizó el formato `### Texto:\\n{tweet}\\n\\n### Emoción:\\n` como entrada."
]
},
{
"cell_type": "code",
"source": [
"from transformers import AutoTokenizer, AutoModelForCausalLM\n",
"\n",
"model_ft = AutoModelForCausalLM.from_pretrained(\"nsuruguay05/Llama-3.2-1B-IdM2024\", device_map=\"auto\")"
],
"metadata": {
"id": "fSyofTimBEYl"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"import numpy as np\n",
"\n",
"def get_emotion_finetuned_first_token(tweet,model):\n",
" prompt = f\"### Texto:\\n{tweet}\\n\\n### Emoción:\\n\"\n",
" return ... # la emoción correspondiente al token más probable de los que refieren a emociones\n",
"\n",
"tweet = random.choice(corpus_train)[0]\n",
"print(tweet)\n",
"print(get_emotion_finetuned_first_token(tweet,model_ft))\n"
],
"metadata": {
"id": "M_rag8cbDXHH"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Obtenemos métricas sobre todo el corpus de test para el caso finetuneado con el primer token generado:"
],
"metadata": {
"id": "aRRYMpZGslJL"
}
},
{
"cell_type": "code",
"source": [
"reference = []\n",
"predictions = []\n",
"for t,e in corpus_test:\n",
" p = get_emotion_finetuned_first_token(t,model_ft)\n",
" reference.append(e)\n",
" predictions.append(p)\n",
"\n",
"print_fscore(reference,predictions,EMOTIONS_ES)\n",
"\n",
"plot_confusion_matrix(reference,predictions,EMOTIONS_ES)"
],
"metadata": {
"id": "hTK2Z0LoDrbJ"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "9L_9oyQe06_-"
},
"source": [
"## Tabla de resultados\n",
"\n",
"Complete la siguiente tabla con los resultados obtenidos en cada experimento.\n",
"\n",
"|Experimento|F1 Avg|F1 Clase Alegría|F1 Clase Tristeza|F1 Clase Otras|\n",
"|:---------:|:--------:|:--------:|:-----------:|:----:|\n",
"|Zero-Shot |||||\n",
"|Zero-Shot (first token)|||||\n",
"|Few-Shot |||||\n",
"|Few-Shot (first token)|||||\n",
"|Fine-Tuned (first token)|||||\n"
]
}
],
"metadata": {
"colab": {
"collapsed_sections": [
"lEtiqwo1j1QR",
"wOhveGs5CTW5"
],
"provenance": [],
"gpuType": "T4"
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
},
"accelerator": "GPU"
},
"nbformat": 4,
"nbformat_minor": 0
}