{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "provenance": [], "collapsed_sections": [ "FtgdalArNvHn", "gCpCYmSrN6oK" ] }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "language_info": { "name": "python" } }, "cells": [ { "cell_type": "markdown", "source": [ "Redes Neuronales para Lenguaje Natural, 2024\n", "\n", "---\n", "# Laboratorio 2\n", "\n", "En este laboratorio construiremos un sistema de Question Answering (QA) utilizando el método de Retrieval-augmented Generation (RAG), que implica el uso de un paso de recuperación de información y un paso de generación de respuesta con LLM.\n", "\n", "**Entrega: viernes 22 de noviembre**\n", "\n", "**Se debe entregar un archivo zip que contenga:**\n", "* Este notebook de Python (.ipynb) completo.\n", "* Los documentos obtenidos y utilizados como fuentes de información según se explica en la parte 1 (opcionalmente se puede entregar un archivo CSV con los textos de cada documento).\n", "* Archivo CSV con el conjunto de preguntas y respuestas como se explica en la parte 5.\n", "\n", "**No olvidar mantener todas las salidas de cada región de código en el notebook!**\n", "\n", "---\n", "\n" ], "metadata": { "id": "t9ZNGOygrFsT" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "lJHOJf9OVMci", "cellView": "form" }, "outputs": [], "source": [ "#@title Instalar librerias\n", "!pip install transformers\n", "!pip install bitsandbytes\n", "!pip install accelerate\n", "!pip install sentence-transformers\n", "!pip install evaluate\n", "!pip install bert_score\n", "!pip install wikipedia-api" ] }, { "cell_type": "code", "source": [ "#@title Estilo de salida de colab\n", "from IPython.display import HTML, display\n", "pre_run_cell_fn = lambda: display(HTML(''''''))\n", "get_ipython().events.register('pre_run_cell', pre_run_cell_fn)" ], "metadata": { "id": "hKuaFtkc5Esq", "cellView": "form" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## Parte 1: Procesamiento de los documentos\n", "\n", "En esta parte, cada grupo deberá construir y procesar su conjunto de documentos. Esto consiste de los siguientes pasos:\n", "\n", "* Elegir un tema dentro de un dominio específico sobre el que trabajar.\n", "* Obtener al menos 5 documentos en español que contengan información sobre el tema elegido.\n", "* Procesar cada documento para extraer el texto del formato original a un string en Python (por ejemplo, extraer el texto de un PDF).\n", "\n", "El resultado de esta parte debe ser una lista cargada en memoria que contenga el texto (string) de cada uno de los documentos elegidos.\n", "\n", "**Sugerencias:**\n", "* Se recomienda utilizar artículos de wikipedia para simplificar la etapa de extracción del texto (ver la librería [wikipedia-api](https://github.com/martin-majlis/Wikipedia-API/)).\n", "* Opcionalmente puede utilizar documentos PDF, páginas web u otros formatos. En estos casos se sugiere:\n", " * Utilizar la librería PyPDF2 para procesar documentos PDF.\n", " * Utilizar la librería LangChain para procesar páginas web, en particular la clase Html2TextTransformer, que convierte HTML a Markdown ([ejemplo de uso](https://python.langchain.com/v0.2/docs/integrations/document_transformers/html2text/)).\n", "* Puede ser conveniente guardar el resultado del procesamiento de los documentos en un archivo CSV (donde cada fila corresponde al texto de un documento) para no tener que repetir este proceso cada vez que se ejecuta el notebook, y en su lugar cargar el archivo CSV." ], "metadata": { "id": "FtgdalArNvHn" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "XuotA7OMHahn" }, "outputs": [], "source": [ "# Su código aquí\n" ] }, { "cell_type": "code", "source": [ "# Ejemplo de uso de la libraría wikipedia-api\n", "import wikipediaapi\n", "\n", "wiki_wiki = wikipediaapi.Wikipedia('RNLN2024', 'es')\n", "\n", "page = wiki_wiki.page('Alan Turing')\n", "print(page.text)" ], "metadata": { "id": "d9tdJ9160C7q" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Los textos resultantes deben estar almacenados en la variable `documents`:" ], "metadata": { "id": "2qtOV9zSSRul" } }, { "cell_type": "code", "source": [ "documents = [] # Lista de strings con el texto de cada documento" ], "metadata": { "id": "iGkUEe10SQrT" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## Parte 2: Chunking\n", "\n", "Una vez que se obtiene el texto de cada documento, se debe realizar la etapa de _chunking_. Esta etapa consiste en dividir cada texto en segmentos más chicos a los que llamamos _chunks_.\n", "\n", "Realizar la etapa de _chunking_ de forma automática utilizando un método simple que permita obtener _chunks_ de un largo aproximado de 500 caracteres.\n", "\n", "Puede probar con dividir a nivel de caracteres, palabras o incluso párrafos, teniendo en cuenta que el largo de cada _chunk_ no debería exceder demasiado los 500 caracteres.\n", "\n", "**Sugerencias:**\n", "* Puede utilizar los splitters disponibles en LangChain ([documentación](https://python.langchain.com/v0.1/docs/modules/data_connection/document_transformers/)) como RecursiveCharacterTextSplitter, aunque no es obligatorio y también es correcto hacer una implementación propia.\n", "* Tener en cuenta que esta etapa es crucial en el resultado final. Cuanto más contextualizados queden los *chunks*, mejor será el rendimiento de la etapa de recuperación de información. Es conveniente minimizar la división de palabras (o párrafos) por la mitad." ], "metadata": { "id": "gCpCYmSrN6oK" } }, { "cell_type": "code", "source": [ "def chunk_text(text):\n", " # Su código aquí\n", "\n", " return chunks # Lista de strings con los chunks del texto" ], "metadata": { "id": "6PgtMbHjVLEj" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "chunks = []\n", "for document in documents:\n", " chunks += chunk_text(document)" ], "metadata": { "id": "74-bH4j_VrRT" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## Parte 3: Recuperación de información\n", "\n", "En esta parte vamos a implementar el método de recuperación de información que nos permitirá obtener los _chunks_ más relevantes para la pregunta.\n", "\n", "En primer lugar, cargamos el modelo Bi-Encoder que utilizaremos para generar los embeddings utilizando la librería sentence_transformers.\n", "\n", "Se utiliza el modelo multilingüe [intfloat/multilingual-e5-large](https://huggingface.co/intfloat/multilingual-e5-large), fine-tuning del modelo `xlm-roberta-large` para la tarea de generación de embeddings.\n", "\n", "Se pueden explorar otros modelos Bi-Encoder, e incluso modelos Cross-Encoder o del tipo ColBERT. En HuggingFace se puede consultar el siguiente [leaderboard](https://huggingface.co/spaces/mteb/leaderboard) que compara varios modelos de este tipo en diferentes tareas." ], "metadata": { "id": "XJ2wgEx3N-O3" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "4Fozn2H7vZsj" }, "outputs": [], "source": [ "from sentence_transformers import SentenceTransformer\n", "\n", "model_emb = SentenceTransformer(\"intfloat/multilingual-e5-large\")" ] }, { "cell_type": "markdown", "source": [ "A continuación se debe generar las representaciones vectoriales para todos los _chunks_ ([ejemplo de uso](https://huggingface.co/intfloat/multilingual-e5-large#support-for-sentence-transformers)).\n", "\n", "**Observación:** El modelo que estamos usando espera que los _chunks_ comiencen con el prefijo `passage: ` por lo que será necesario agregarlo al inicio de todos los _chunks_." ], "metadata": { "id": "OwuNrG2djc3f" } }, { "cell_type": "code", "source": [ "# Su código aquí" ], "metadata": { "id": "WeBfJ_Ubjb3k" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Por último, se debe implementar el algoritmo de búsqueda de los embeddings más cercanos para un embedding dado.\n", "\n", "**Sugerencias:**\n", "* Utilizar la clase NearestNeighbors de sklearn ([documentación](https://scikit-learn.org/dev/modules/generated/sklearn.neighbors.NearestNeighbors.html#sklearn.neighbors.NearestNeighbors))." ], "metadata": { "id": "N62kl88KwZ-u" } }, { "cell_type": "code", "source": [ "# Su código aquí" ], "metadata": { "id": "x5bGq2Etxkj-" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## Parte 4: Generación de respuestas" ], "metadata": { "id": "qwMSqjQUOEzQ" } }, { "cell_type": "markdown", "metadata": { "id": "ZyPn_Cwtq81r" }, "source": [ "### Configuración de LLM\n", "\n", "Utilizaremos el modelo **Llama 3.1** 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 el modelo en HuggingFace, que aparecen en el siguiente enlace: https://huggingface.co/meta-llama/Meta-Llama-3.1-8B-Instruct\n", "- Crear un token de HuggingFace con permiso de lectura 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", "source": [ "A continuación se inicializan el tokenizer y el modelo cuantizado a 4 bits." ], "metadata": { "id": "_fPqX6DH1Gzn" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "LVPu5MUiTBog" }, "outputs": [], "source": [ "from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig\n", "import torch\n", "\n", "# Inicializar el tokenizer\n", "tokenizer = AutoTokenizer.from_pretrained(\n", " \"meta-llama/Meta-Llama-3.1-8B-Instruct\"\n", ")\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", "# Inicializar el modelo\n", "model = AutoModelForCausalLM.from_pretrained(\n", " \"meta-llama/Meta-Llama-3.1-8B-Instruct\",\n", " quantization_config=bnb_config,\n", " device_map=\"auto\",\n", ")" ] }, { "cell_type": "markdown", "source": [ "Creamos ahora dos funciones auxiliares que usaremos para la generación de las respuestas." ], "metadata": { "id": "rgGESsQX1k5_" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Jyf3eQPB4zQz" }, "outputs": [], "source": [ "# Generar respuesta\n", "from transformers import GenerationConfig, pipeline\n", "\n", "def get_response(prompt, temp=0.0, max_tok=500):\n", " # Configuración de temperatura\n", " generation_config = GenerationConfig(\n", " temperature = temp if temp > 0 else None,\n", " do_sample = temp > 0\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\n", " output = pipe(\n", " prompt,\n", " return_full_text=False,\n", " max_new_tokens=max_tok\n", " )\n", "\n", " return output[0]['generated_text']" ] }, { "cell_type": "markdown", "source": [ "### Crear prompt y generar respuesta\n", "\n", "Escribir la función `create_prompt(question)` que dada una pregunta, genere la prompt que se utilizará para generar la respuesta. Tener en cuenta que se debe realizar la búsqueda semántica de los _chunks_ más cercanos a la pregunta utilizando lo implementado en la parte 3.\n", "\n", "**Observación:** Al igual que para los _chunks_, el modelo Bi-Encoder espera que la pregunta comience con un prefijo especial: `query: ` por lo que será necesario agregarlo al inicio de la pregunta para generar el embedding.\n", "\n", "**Sugerencias:**\n", "* Puede probar con distintas cantidades de _chunks_ recuperados, pero se sugiere comenzar con 3. Tener en cuenta que más _chunks_ recuperados y agregados en la prompt implica mayor uso de memoria en inferencia.\n", "* Utilizar la función `apply_chat_template` del tokenizer para aplicar el template correcto del modelo Llama 3.1." ], "metadata": { "id": "PN9wyhSm5Mtj" } }, { "cell_type": "code", "source": [ "def create_prompt(question):\n", " # Su código aquí\n", "\n", " return prompt" ], "metadata": { "id": "xQtVcehH-nQJ" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Probar la prompt anterior con un ejemplo." ], "metadata": { "id": "ZfYv8FZ2-0fR" } }, { "cell_type": "code", "source": [ "question = \"\" # Completar con una pregunta adecuada al contexto\n", "prompt = create_prompt(question)\n", "print(\"PROMPT:\")\n", "print(prompt)\n", "\n", "print(\"\\nRESPUESTA:\")\n", "print(get_response(prompt))" ], "metadata": { "id": "ZqESfBkq-wF6" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## Parte 5: Evaluación\n", "A continuación vamos a evaluar la solución construida. Para ello, se deben seguir los siguientes pasos:\n", "\n", "* Construir un conjunto de evaluación de forma manual que contenga al menos 12 preguntas y respuestas con las siguientes características:\n", " * Al menos 3 preguntas deben necesitar información presente en más de un _chunk_ para ser respondidas correctamente.\n", " * Al menos 3 preguntas no deben estar relacionadas con el dominio, y su respuesta de referencia debe ser algo similar a: \"Lo siento, no cuento con información para responder esa pregunta.\"\n", "* El conjunto debe estar en un archivo CSV llamado testset.csv, con las columnas \"question\" y \"answer\".\n", "\n", "Se deberá realizar al menos tres experimentos diferentes y evaluar sobre el mismo conjunto de test con la métrica BERTScore. Los experimentos deben variar en al menos uno de los siguientes elementos:\n", "* Método de chunking\n", "* Modelo (o método) de retrieval\n", "* Modelo de generación (LLM)\n", "* Método de prompting (se puede probar con few-shot, chain of thought, etc)\n", "* Otros aspectos que considere relevantes a probar\n", "\n", "A continuación se definen funciones auxiliares para la evaluación.\n" ], "metadata": { "id": "LYOvIEHAOJSc" } }, { "cell_type": "code", "source": [ "import evaluate\n", "import numpy as np\n", "from tqdm.notebook import tqdm\n", "\n", "def generate_predictions(questions):\n", " prompts = [create_prompt(question) for question in questions]\n", " predictions = [get_response(prompt) for prompt in tqdm(prompts)]\n", " return predictions\n", "\n", "def evaluate_predictions(predictions, references):\n", " bertscore = evaluate.load(\"bertscore\")\n", " results = bertscore.compute(predictions=predictions, references=references, lang='es')\n", "\n", " print(f\"BERTScore P: {np.array(results['precision']).mean():.3f}\")\n", " print(f\"BERTScore R: {np.array(results['recall']).mean():.3f}\")\n", " print(f\"BERTScore F1: {np.array(results['f1']).mean():.3f}\")" ], "metadata": { "id": "JkFBE_iEKkkm" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "import pandas as pd\n", "\n", "# Leer el conjunto de evaluación\n", "df = pd.read_csv(\"testset.csv\")\n", "\n", "# Obtener preguntas y respuestas\n", "questions = df[\"question\"].tolist()\n", "references = df[\"answer\"].tolist()" ], "metadata": { "id": "F-wIITRgKKoE" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Evalúe los experimentos realizados." ], "metadata": { "id": "i8quL9-CVKEB" } }, { "cell_type": "code", "source": [ "# su código aquí\n" ], "metadata": { "id": "jUnRnoksVOv-" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Reportar los resultados obtenidos en los experimentos realizados completando la siguiente tabla:\n", "\n", "| Exp | Descripción | P BERTScore | R BERTScore | F BERTScore |\n", "|-----|-------------|-------------|-------------|-------------|\n", "| 1 | | | | |\n", "| 2 | | | | |\n", "| 3 | | | | |\n", "\n", "Responda las siguientes preguntas:\n", "\n", "1. Explique brevemente las diferencias en los experimentos realizados, ¿Qué aspectos se varió en el pipeline de RAG?\n", "\n", "2. ¿Son consistentes los resultados obtenidos con lo que esperaba?\n", "\n", "3. ¿Le parece que la métrica BERTScore está capturando correctamente las diferencias de los distintos experimentos realizados?" ], "metadata": { "id": "t1GKFIKIVk42" } }, { "cell_type": "markdown", "source": [ "(sus respuestas aquí)\n" ], "metadata": { "id": "peHQat3XWzX2" } } ] }