Este notebook ofrece una introducción a los Large Language Models (LLMs), centrándose en el modelo Gemini desarrollado por Google. Se exploran los conceptos básicos de los transformers, el mecanismo de self-attention, el prompt engineering y estrategias para interactuar con un LLM.
Como caso de aplicación, se retoma el dataset de reseñas de Amazon del Módulo 4 para clasificarlas en “positivo” o “negativo” utilizando Gemini. Se analizan los resultados y se realiza un análisis de errores para identificar las limitaciones del modelo.
El objetivo principal es comprender cómo funcionan los LLMs, sus potencialidades y cómo aplicarlos a tareas de procesamiento de lenguaje natural en el ámbito de las ciencias sociales.
Un transformer es un tipo de modelo de machine learning diseñado para trabajar con datos secuenciales, como texto, pero con una capacidad mucho mayor que los modelos tradicionales. Su diseño resuelve algunos de los problemas que tenían métodos previos, como los modelos recurrentes (RNNs) o las redes LSTM.
Los transformers son la base de los modelos de lenguaje más avanzados, como los LLMs (e.g., ChatGPT).
Cuando trabajamos con texto, queremos que el modelo entienda: 1. El contexto: La relación entre las palabras dentro de una frase. 2. La relevancia de cada palabra: No todas las palabras tienen el mismo peso en el significado.
Por ejemplo, en la frase:
“El perro no jugó con el niño porque él tenía pulgas.”
Hay cierta ambigüedad: ¿a quién se refiere “él”? ¿Al perro o al niño? El self-attention ayuda al modelo a desambiguar estas relaciones analizando cómo cada palabra interactúa con las demás.
Modelos previos, como las RNNs, procesaban las palabras una por una, pero olvidaban información a medida que avanzaban. Además, eran lentos y no podían mirar toda la frase al mismo tiempo.
Los transformers usan un mecanismo clave llamado “atención”, específicamente el mecanismo de atención autoconsciente (self-attention). Este mecanismo les permite:
En lugar de procesar palabra por palabra, el transformer calcula relaciones entre todas las palabras en paralelo. Imagina que tienes una matriz donde: - Cada palabra está representada como una fila y una columna. - Los números dentro de la matriz indican cuánto influye cada palabra sobre las demás.
El self-attention permite que cada palabra en una oración analice su relación con todas las demás palabras, identificando cuáles son más importantes para su significado. Esto es crucial para entender frases donde el contexto es ambiguo o complejo.
Volvamos a la frase anterior:
“El perro no jugó con el niño porque él tenía pulgas.”
En nuestro ejemplo: - “él” podría referirse al perro o al niño. - El modelo necesita decidir cuál es más probable en función del contexto.
El self-attention calcula estas relaciones y ajusta la representación de cada palabra para incluir información relevante sobre las palabras relacionadas.
1. Representación inicial: embeddings Cada palabra se convierte en un vector numérico, llamado embedding, que captura información sobre su significado. Por ejemplo: - “perro” → [0.2, 0.5, 0.8] - “niño” → [0.4, 0.1, 0.9] - “él” → [0.7, 0.3, 0.6]
Estos vectores no solo representan las palabras aisladas, sino también su posición relativa en la oración (positional encoding).
2. Generación de Query, Key y Value El modelo transforma los embeddings iniciales de cada palabra en tres nuevas representaciones: - Query (Q): ¿Qué palabras buscan relaciones? - Key (K): ¿Con qué palabras se relacionan? - Value (V): ¿Qué información aporta cada palabra?
Por ejemplo: - Para “él”, el Query puede estar orientado a encontrar qué palabras definen a “él” en el contexto. - Para “pulgas”, el Key puede relacionarse con atributos relevantes (¿Quién tiene pulgas?).
3. Cálculo de las relaciones: matriz de atención El modelo calcula una matriz de similitud entre palabras usando el producto escalar entre los Queries y los Keys.
\[ \text{Atención}_{ij} = \frac{\text{Q}_i \cdot \text{K}_j}{\sqrt{d_k}} \]
Esto da como resultado una matriz que muestra cuánto influye cada palabra en las demás. Por ejemplo:
| perro | no | jugó | con | niño | porque | él | tenía | pulgas | |
|---|---|---|---|---|---|---|---|---|---|
| perro | 1.0 | 0.3 | 0.8 | 0.2 | 0.1 | 0.0 | 0.4 | 0.3 | 0.1 |
| él | 0.4 | 0.0 | 0.2 | 0.0 | 0.9 | 0.0 | 1.0 | 0.7 | 0.3 |
4. Ponderación de los valores El modelo usa la matriz de atención para combinar la información de las palabras relacionadas. Por ejemplo, el vector final de “él” incluirá información de: - “niño” (más relevante). - “perro” (menos relevante).
El resultado es un nuevo vector para “él” que incluye contexto sobre a quién se refiere.
5. Generación de la salida Cada palabra ahora tiene una representación ajustada que incluye información contextual. Por ejemplo: - “él” → nuevo vector que indica que probablemente se refiere al “niño” por la relación fuerte con “pulgas”.
El self-attention analiza relaciones complejas en toda la oración. En nuestro ejemplo: - Decide que “él” se refiere al “niño” porque “pulgas” tiene más sentido en ese contexto (el modelo aprende esto durante el entrenamiento).
De esta forma, este mecanismo tiene varias ventajas:
Enseguida vamos a ver un ejemplo concreto de uso.
Pero antes tenemos que introducir una última cuestión. ¿Cómo interactuamos con un LLM?
Ya mencionamos que un LLM es una herramienta sumamente útil que permite realizar diversas tareas basadas en texto: desde resúmenes y clasificaciones hasta generación de contenido y análisis de sentimientos. Sin embargo, su eficacia depende de cómo interactuemos con él. Aquí entra en juego el concepto de prompt engineering. Es la tarea que consiste en diseñar instrucciones claras y efectivas para maximizar la calidad de las respuestas del modelo.
Un LLM, como Gemini, está entrenado en vastos conjuntos de datos para reconocer patrones en el lenguaje. Aunque no “piensa” como un humano, puede generar respuestas coherentes y contextuales basadas en entradas específicas. El modelo procesa instrucciones llamadas prompts, que son preguntas, textos o comandos diseñados para guiar su comportamiento.
Por ejemplo, si queremos traducir una frase: - Mal prompt: “Traducción al inglés.” - Buen prompt: “Traduce la frase ‘El perro juega en el parque’ al inglés.”
Un buen prompt debe ser claro, estructurado y, cuando sea necesario, incluir ejemplos o contexto. Para usar en LLM tenemos que seguir algunos pasos:
Definir la tarea: Antes de usar un LLM, aclarar qué se busca lograr: ¿Resumir? ¿Clasificar? ¿Analizar sentimientos? Esto determinará cómo diseñar el prompt.
Diseñar el prompt: Asegurarse de que la instrucción:
Usar la herramienta adecuada:
Revisar e iterar: Evaluar las respuestas y ajustar el prompt según sea necesario. La iteración es clave para obtener resultados óptimos.
Hay varias formas de diseñar un prompt.
Traduce al inglés: 'El perro juega en el parque
Genera una pregunta para una encuesta sobre satisfacción laboral.
Ejemplo: '¿Qué tan satisfecho está con su horario de trabajo?'
Nueva pregunta:
Clasifica las siguientes frases según la emoción:
- "Estoy feliz hoy." → Alegría
- "Estoy cansado." → Tristeza
- "Estoy ansioso por mañana." → Ansiedad
Frase: "No puedo creer que gané la lotería." → Emoción:
Pregunta: Si Juan tiene 3 manzanas y compra 5 más, ¿cuántas tiene en total? Razonamiento:
1. Juan tiene 3 manzanas.
2. Compra 5 más, así que ahora tiene 3 + 5.
Respuesta final:
```
5. **Contextual prompting**
Proporciona al modelo un contexto para que las respuestas sean más relevantes.
Eres un experto en marketing. Crea un eslogan para una marca de café sostenible. ```
Resume el siguiente texto en tres frases que incluyan las ideas principales: [texto].
Pese a lo bien que funcionan los LLMs es necesario tener en cuenta algunas limitaciones a los fines de evitar malos entendidos.
En primer lugar, más allá del marketing alrededor de estas herramientas es importante mencionar que no razonan como humanos. Se limitan a generar respuestas basadas en patrones estadísticos, de allí el término “loros aleatorios”. ¿Esto es un problema? Depende. Si los queremos usar para resolver tareas concretas como las que hemos mencionado hasta aquí como la que vamos a atacar en breve, no. En otros contextos, puede ser.
A su vez, los LLMS (al igual que cualquier otro algoritmo, modelo o método que se basa en datos de entrada y trata de aprender patrones subyacentes) tienen sesgos. Hay una amplísima bibliografía Pueden reflejar sesgos presentes en los datos de entrenamiento. Esto hace que sea necesario tener cuidado al momento de seleccionar la tarea que vamos a solicitarle. Por otro lado, tienen memoria limitada, no recuerdan interacciones previas a menos que se incluyan en el contexto.
Google DeepMind ha desarrollado Gemini, una familia de modelos de inteligencia artificial con capacidades multimodales. Esto implica que Gemini puede procesar y comprender diversos tipos de información, como texto e imágenes.
Gemini es un modelo de lenguaje grande (LLM) que utiliza una arquitectura de red neuronal Transformer. Esta arquitectura le permite procesar información de manera eficiente y comprender las relaciones entre las palabras en un texto.
Gemini fue entrenado por Google utilizando una gran cantidad de datos de texto y código. Este proceso de entrenamiento implica ajustar los parámetros de la red neuronal para que pueda predecir la siguiente palabra o token en una secuencia. El entrenamiento se realiza en varias etapas, con conjuntos de datos cada vez más grandes y complejos. Esto permite a Gemini aprender patrones y relaciones en el lenguaje, mejorando su capacidad para comprender y generar texto.
Vamos a volver sobre el dataset de reseñas de Amazon de la semana pasada y a retomar la tarea de clasificación de las mismas en “positivo” o “negativo”. En realidad, vamos a trabajar sobre una muestra de unas 1200 reseñas, dado que la API de Gemini solamente permite un número limitado de requests gratis en un día.
Primero vamos a instalar la librería que nos va a servir de interface
para trabajar con Gemini. Se llama, como no podía ser de otra forma, gemini.R
#install.packages("gemini.R")
Cargamos, ahora, los paquetes…
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.2 ✔ tibble 3.2.1
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.0.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(gemini.R)
library(jsonlite)
##
## Attaching package: 'jsonlite'
##
## The following object is masked from 'package:purrr':
##
## flatten
Como ya hemos visto a lo largo de este curso, las APIs actúan como intermediarios que permiten la comunicación e intercambio de datos entre diferentes sistemas. Por este motivo, la autenticación es crucial para asegurar la seguridad y controlar el acceso a la información.
Para usar gemini.R, debemos autenticarnos con una clave
personal que debemos crear en [https://makersuite.google.com/app/apikey]. La función
setAPI en gemini.R nos permite configurar esta
clave.
api <- "" ## Acá introducir la api key
setAPI(api)
## ✖ API key must be a non-empty string.
Si no quieren que su API key sea pública (cosa que suele ser una buena práctica, en general) hay varias formas de hacer esto un poquito más seguro. Vamos a ver una muy simple:
setAPI(read_lines("../gemini_credentials.txt"))
## ℹ API key ...T7Ts is set.
## → You may try `gemini_chat('What is CRAN?')`
Acá guardamos la key en un archivo de texto y simplemente la cargamos
mediante read_lines(). De esta forma no se hace visible en
el script. Hay otros paquetes y otras formas para hacer esto de forma
más sistemática, pero para los efectos de este ejercicio con esto nos
alcanza.
Probemos si ha funcionado:
test <- gemini("¿Cómo puedo escribir un buen prompt?")
## Gemini is answering...
cat(test)
## Escribir un buen prompt es fundamental para obtener resultados precisos y útiles de modelos de lenguaje como yo. Un prompt bien diseñado guía al modelo para entender la tarea, el contexto y el tipo de respuesta que esperas. Aquí te dejo una guía completa con consejos y ejemplos:
##
## **I. Componentes Clave de un Buen Prompt:**
##
## * **Claridad y Especificidad:**
## * **Sé preciso sobre lo que quieres:** Evita ambigüedades y lenguaje vago.
## * **Define el objetivo:** ¿Qué quieres lograr con la respuesta? (informar, persuadir, crear, etc.)
## * **Especifica el formato:** ¿Necesitas una lista, un párrafo, un código, una tabla?
##
## * **Contexto:**
## * **Proporciona la información de fondo necesaria:** Asegúrate de que el modelo entienda la situación.
## * **Define el rol del modelo (opcional):** Puedes pedirle que actúe como un experto, un personaje, etc.
##
## * **Instrucciones:**
## * **Usa verbos de acción claros:** "Escribe", "Resume", "Compara", "Traduce", "Explica", "Crea", "Analiza", etc.
## * **Establece límites y restricciones:** Define la longitud, el tono, el estilo, etc.
## * **Incluye ejemplos (si es necesario):** Un ejemplo de la respuesta deseada puede ser muy útil.
##
## * **Entrada (Input):**
## * **Proporciona la información o texto base:** Si necesitas un resumen, análisis, traducción, etc., proporciona el texto original.
## * **Utiliza marcadores claros:** Si tienes varios elementos de entrada, sepáralos con delimitadores (por ejemplo, "***" o "---").
##
## **II. Estrategias y Técnicas Avanzadas:**
##
## * **"Few-Shot Learning":**
## * **Proporciona algunos ejemplos de entrada y salida esperada.** Esto ayuda al modelo a aprender el patrón y a generalizar a nuevas entradas.
## * **Útil para tareas complejas con requisitos específicos.**
##
## * **"Chain-of-Thought Prompting":**
## * **Pide al modelo que explique su razonamiento paso a paso.** Esto puede mejorar la precisión y la transparencia, especialmente en tareas complejas.
## * **Ejemplo:** "Primero, identifica los puntos clave. Luego, analiza la relación entre ellos. Finalmente, escribe un resumen."
##
## * **"Role-Playing":**
## * **Asigna un rol al modelo.** Esto puede influir en el tono, el estilo y el enfoque de la respuesta.
## * **Ejemplo:** "Actúa como un experto en historia del arte y explica el significado del cuadro 'La noche estrellada'."
##
## * **Usa palabras clave específicas:**
## * **Palabras clave relevantes para el tema o dominio.** Ayudan a enfocar la respuesta.
##
## * **Iteración y Refinamiento:**
## * **Experimenta con diferentes prompts y evalúa los resultados.**
## * **Refina el prompt basándote en la respuesta del modelo.**
## * **Considera la temperatura (aleatoriedad) del modelo si la opción está disponible.** Una temperatura más baja generalmente produce respuestas más deterministas.
##
## **III. Ejemplos de Buenos y Malos Prompts:**
##
## * **Mal Prompt:** "Escribe sobre la Revolución Francesa." (Demasiado vago)
## * **Buen Prompt:** "Escribe un ensayo de 500 palabras sobre las causas y consecuencias de la Revolución Francesa, incluyendo un análisis del papel de la Ilustración y el impacto en la monarquía europea."
##
## * **Mal Prompt:** "Resume este texto." (Falta contexto)
## * **Buen Prompt:** "Resume el siguiente texto, enfocándote en los principales argumentos del autor y omitiendo ejemplos específicos: [TEXTO]"
##
## * **Mal Prompt:** "Explica la física cuántica." (Demasiado amplio)
## * **Buen Prompt:** "Explica el principio de superposición en la física cuántica, utilizando una analogía comprensible para alguien sin conocimientos previos en física."
##
## * **Ejemplo Few-Shot:**
## ```
## Texto: "El gato es negro." Traducción al francés: "Le chat est noir."
## Texto: "El perro es blanco." Traducción al francés: "Le chien est blanc."
## Texto: "El pájaro es azul." Traducción al francés:
## ```
##
## **IV. Errores Comunes que Debes Evitar:**
##
## * **Ambigüedad:** Usa lenguaje claro y preciso.
## * **Instrucciones contradictorias:** Asegúrate de que las instrucciones sean coherentes.
## * **Falta de contexto:** Proporciona suficiente información de fondo.
## * **Suposiciones:** No asumas que el modelo sabe algo que no le has dicho.
## * **Expectativas poco realistas:** Recuerda que el modelo tiene limitaciones.
##
## **V. Herramientas Útiles:**
##
## * **Playground de OpenAI (si usas la API de OpenAI):** Te permite experimentar con diferentes prompts y configuraciones.
## * **Otras plataformas de IA:** Muchas plataformas de IA tienen interfaces que facilitan la creación de prompts.
##
## **En resumen:**
##
## Un buen prompt es específico, claro, proporciona contexto e incluye instrucciones precisas. No tengas miedo de experimentar y refinar tus prompts hasta obtener los resultados deseados. La práctica es la clave para dominar el arte de escribir buenos prompts. Cuanto más practiques, mejor entenderás cómo funciona el modelo y cómo obtener las respuestas que necesitas.
Perfecto. Vamos ahora a diseñar un prompt simple para nuestra tarea.
prompt <- "A continuación vas a recibir un texto de una reseña de compra online.
Quisiera que la clasifiques positiva o negativa usando las siguientes categorías:
- Positiva: la reseña es positiva
- Negativa: la reseña es negativa
Quisiera que expliques paso a paso tu razonamiento.
La salida debería tener el siguiente formato:
clasif: seguido de la clasificación | expl: seguido de la explicación
Texto:
"
Cargamos el dataset. En realidad, vamos a cargar unos 1259 datos del dataset original para hacer esta prueba.
df_list <- read_rds("../data/reviews_amazon_listsplit.rds")[[1]]
head(df_list)
## # A tibble: 6 × 4
## review_id review_body stars product_category
## <chr> <chr> <chr> <chr>
## 1 es_0179810 Fácil de instalar gracias al marco de posic… Post… wireless
## 2 es_0506233 De momento y llevo ya media botellita,no he… Nega… beauty
## 3 es_0627963 El producto al primer día dio problemas ser… Nega… wireless
## 4 es_0629229 Se rompen con mirarlos Nega… sports
## 5 es_0776446 Bonitos los colores. Relación calidad preci… Post… wireless
## 6 es_0327599 Las pegatinas son mates y traslúcidas. Se v… Nega… office_product
Y vamos a iterar por cada una de las filas del dataset y para cada fila, vamos a hacer una request a la API de Gemini con dos inputs:
prompt que diseñamos más arribareview_body)rtas <- list()
for (i in 1:nrow(df_list)){
id <- df_list$review_id[i]
Sys.sleep(4.5)
cat("Procesando comentario", i, "de", nrow(df_list), "\n")
rta <- gemini(paste0(prompt, df_list$review_body[i]))
cat(rta)
rtas[[id]] <- rta
}
El código anterior itera sobre el dataframe de reseñas de productos
(almacenadas en df_list), hace la consulta a la API de
Gemini, usando el prompt
Vamos paso a paso:
Inicialización: Se crea una lista vacía llamada rtas que almacenará las respuestas de Gemini.
Bucle for: Se itera a través de las dos primeras reseñas en df_list (índice i de 1 a 2).
Extracción de ID: En cada iteración, se extrae el ID de la reseña actual (review_id) y se almacena en la variable id.
Pausa: Se introduce una pausa de 4.5 segundos utilizando Sys.sleep(4.5) para evitar sobrecargar la API de Gemini.
Mensaje de progreso: Se imprime un mensaje en la consola indicando el progreso del procesamiento: “Procesando comentario [i] de [total de reseñas]”.
Llamada a Gemini: Se utiliza la función gemini() para enviar una consulta al modelo de lenguaje.
Impresión de la respuesta: Se imprime la respuesta de Gemini en la consola utilizando cat(rta).
Almacenamiento de la respuesta: La respuesta de Gemini (rta) se almacena en la lista rtas, utilizando el ID de la reseña como clave.
Fin del bucle: El proceso se repite para las dos primeras reseñas.
Para ganaer tiempo, ya hemos corrido las consultas al LLM. Vamos a cargar los resultados.
rtas_llms <- read_rds('../data/1_reviews_llms_prompt1.rds')
head(rtas_llms)
## $es_0179810
## text
## "clasif: Positiva | expl: La reseña destaca aspectos positivos como la facilidad de instalación, el centrado perfecto y la similitud con el producto original de Apple. Aunque menciona la falta de información sobre la durabilidad, no la considera un aspecto negativo. \n"
##
## $es_0506233
## text
## "clasif: Negativa | expl: La reseña menciona que \"no he notado ningún cambio\" después de usar media botellita del producto. Esto indica que el producto no ha cumplido con las expectativas del usuario, lo que sugiere una experiencia negativa. \n"
##
## $es_0627963
## text
## "clasif: Negativa | expl: La reseña comienza describiendo un problema serio con el producto al primer día de uso, lo que indica una experiencia negativa. Aunque el vendedor ofreció un reembolso, la reseña enfatiza la mala señal que representa un fallo tan temprano en un producto nuevo. \n"
##
## $es_0629229
## text
## "clasif: Negativa | expl: La frase \"Se rompen con mirarlos\" es una expresión negativa que implica fragilidad y falta de calidad. Sugiere que el producto es tan débil que se rompe con solo mirarlo, lo que indica una experiencia negativa con el producto. \n"
##
## $es_0776446
## text
## "clasif: Positiva | expl: La reseña destaca aspectos positivos como la estética (\"Bonitos los colores\"), la relación calidad-precio (\"Relación calidad precio adecuada\"), la rapidez del envío (\"Fue muy rápido en envío\") y la utilidad del producto (\"se adapta a lo que necesitaba\"). La frase final \"Contenta con la compra\" confirma la satisfacción general con el producto y la experiencia de compra. \n"
##
## $es_0327599
## text
## "clasif: Negativa | expl: La reseña menciona que las pegatinas son \"blanquecinas\" y que no eran lo que el usuario buscaba. Esto indica que el producto no cumplió con las expectativas del comprador, por lo que la reseña es negativa. \n"
Como habrán notado en la salida anterior, las respuestas del modelo
son básicamente un largo character. Toda la respuesta (la
etiqueta y la explicación) están juntas. No obstante, hemos diseñado el
prompt para que aparezca con algunos delimitadores que nos hagan más
fácil la extracció de la información. Para ello vamos a definir dos
funciones:
parse_clasif <- function(string){
clasif <- str_extract(string, "(?<=clasif: ).*?(?= \\|)")
return(clasif)
}
La función parse_clasif está diseñada para extraer la
clasificación de una reseña de un texto, específicamente la parte que
indica si la reseña es “Positiva” o “Negativa”.
El objetivo principal de la función es identificar y extraer la clasificación (“Positiva” o “Negativa”) de una cadena de texto que contiene la respuesta de un modelo de lenguaje.
¿Cómo funciona?
1. Entrada: Recibe una cadena de texto
(string) que se espera que contenga la clasificación de una
reseña en un formato específico, por ejemplo:
clasif: Positiva | expl: El usuario expresó satisfacción con el producto.
2. str_extract: Utiliza la función
str_extract de la librería stringr para extraer la parte de la cadena
que coincide con un patrón específico.
3. Patrón: El patrón utilizado es una expresión
regular: (?<=clasif: ).*?(?= \\|). Vamos a
desglosarlo:
(?<=clasif: ): Esta parte busca la cadena “clasif:”
y asegura que la coincidencia comience justo después de ella. Se conoce
como una “aserción lookbehind positiva”..*?: Esta parte coincide con cualquier caracter
(.) cero o más veces (*), pero de la manera
más perezosa posible (?). Esto significa que intentará
encontrar la coincidencia más corta posible.(?= \\|): Esta parte busca el caracter “|” (que debe
escaparse con una barra invertida \\ en la expresión
regular) y asegura que la coincidencia termine justo antes de él. Se
conoce como una “aserción lookahead positiva”.4. Extracción: str_extract devuelve la
parte de la cadena que coincide con el patrón completo. En este caso,
sería “Positiva” o “Negativa”.
5. Retorno: La función devuelve el valor extraído, que representa la clasificación de la reseña.
parse_expl <- function(string){
expl <- str_trim(str_replace_all(str_extract(string, "(?<=expl).*"), ":|'", ""))
return(expl)
}
Ahora bien, parse_expl está diseñada para extraer la
explicación de una reseña de un texto, específicamente la parte que
explica por qué la reseña fue clasificada como “Positiva” o
“Negativa”.
1. Entrada: Recibe una cadena de texto (string) que
se espera que contenga la explicación de una reseña en un formato
específico, por ejemplo: “clasif: Positiva | expl: El usuario expresó
satisfacción con el producto”. 2.
str_extract: Similar a parse_clasif,
utiliza str_extract de la librería stringr para extraer la parte de la
cadena que coincide con un patrón específico. 3.
Patrón: El patrón utilizado es (?<=expl).*. -
(?<=expl): Es una aserción “lookbehind positiva” que
busca la cadena “expl” y asegura que la coincidencia comience justo
después de ella. - .*: Esta parte coincide con cualquier caracter
(.) cero o más veces (*), lo que significa que
selecciona todo el texto después de “expl:”.
4. str_replace_all: Se utiliza para
limpiar la explicación extraída. Reemplaza todos los caracteres “:” y
“|” por un espacio vacío. Esto ayuda a eliminar cualquier rastro del
formato original. 5. str_trim: Se aplica
para eliminar cualquier espacio en blanco al principio y al final de la
explicación, asegurando que la salida esté limpia. 6.
Retorno: La función devuelve la explicación extraída y limpia,
que representa el razonamiento detrás de la clasificación de la
reseña.
Ejemplo:
Si la entrada es “clasif: Positiva | expl: El usuario expresó satisfacción con el producto.”, la función parse_expl devolverá “El usuario expresó satisfacción con el producto”.
rtas_df <- rtas_llms %>%
enframe(name = "review_id", value = "resp") %>%
unnest(cols = c(resp)) %>%
mutate(llm_expl = parse_expl(resp),
llm_stars = parse_clasif(resp))
rtas_df
## # A tibble: 1,248 × 4
## review_id resp llm_expl llm_stars
## <chr> <chr> <chr> <chr>
## 1 es_0179810 "clasif: Positiva | expl: La reseña destaca as… "La res… Positiva
## 2 es_0506233 "clasif: Negativa | expl: La reseña menciona q… "La res… Negativa
## 3 es_0627963 "clasif: Negativa | expl: La reseña comienza d… "La res… Negativa
## 4 es_0629229 "clasif: Negativa | expl: La frase \"Se rompen… "La fra… Negativa
## 5 es_0776446 "clasif: Positiva | expl: La reseña destaca as… "La res… Positiva
## 6 es_0327599 "clasif: Negativa | expl: La reseña menciona q… "La res… Negativa
## 7 es_0481564 "clasif: Negativa | expl: La reseña es negativ… "La res… Negativa
## 8 es_0842471 "clasif: Positiva | expl: El usuario destaca l… "El usu… Positiva
## 9 es_0240232 "clasif: Negativa | expl: El texto menciona qu… "El tex… Negativa
## 10 es_0774932 "clasif: Negativa | expl: La reseña menciona q… "La res… Negativa
## # ℹ 1,238 more rows
Este código toma una lista de respuestas, la transforma en un dataframe estructurado y utiliza las funciones parse_expl y parse_clasif para extraer la explicación y la clasificación de cada reseña, almacenando esta información en nuevas columnas del dataframe. El resultado final es un dataframe llamado rtas_df que contiene la información de las reseñas de forma organizada.
1. rtas %>%: Esto inicia una
secuencia de operaciones con el operador %>% (pipe) de la librería
magrittr. El operador %>% toma el resultado de la expresión anterior
y lo pasa como primer argumento a la siguiente función. En este caso,
rtas es la lista de respuestas que se procesará.
2.
enframe(name = "review_id", value = "resp"): Esta
función de la librería tibble transforma la lista rtas en un dataframe.
Cada elemento de la lista se convierte en una fila del dataframe. Los
nombres de los elementos de la lista se almacenan en una columna llamada
“review_id”, y los valores de los elementos se almacenan en una columna
llamada “resp”.
3. unnest(cols = c(resp)): Esta función
de la librería tidyr se utiliza para “desanidar” la columna “resp”, que
presumiblemente contiene una lista de respuestas para cada “review_id”.
unnest expande la columna “resp” de modo que cada elemento de la lista
se convierta en una fila separada en el dataframe.
**4. mutate(...):** Esta función de la librería dplyr
crea nuevas columnas en el dataframe basándose en los valores de las
columnas existentes. Dentro de mutate se realizan dos operaciones:
llm_expl = parse_expl(resp): Se crea una nueva columna
llamada “llm_expl” aplicando la función parse_expl a la columna “resp”.
Esto extrae la explicación de la reseña del texto de la respuesta y la
almacena en la nueva columna.llm_stars = parse_clasif(resp): Se crea otra nueva
columna llamada “llm_stars” aplicando la función parse_clasif a la
columna “resp”. Esto extrae la clasificación de la reseña (Positiva o
Negativa) del texto de la respuesta y la almacena en la nueva
columna.rtas_df <- rtas_df %>%
left_join(df_list) %>%
mutate(stars = case_when(
stars == "Postiva" ~ "Positiva",
TRUE ~ stars
)) %>%
mutate(across(c(stars, llm_stars), as.factor))
## Joining with `by = join_by(review_id)`
Este código combina el dataframe rtas_df con información adicional del dataframe df_list a través de un left_join. Además, corrige un posible error tipográfico en la columna “stars”, cambiando “Postiva” por “Positiva”. El resultado es un dataframe rtas_df actualizado y enriquecido con información de df_list y con una corrección en la columna “stars”.
1. left_join(df_list): - Esta función,
proveniente de la librería dplyr, realiza una unión
(join) entre el dataframe rtas_df y el
dataframe df_list. - Se trata de un left_join,
lo que significa que todas las filas de rtas_df se
conservarán en el resultado final. - La unión se realiza buscando
coincidencias entre las columnas que tienen el mismo nombre en ambos
dataframes. Si hay una columna llamada, por ejemplo, “review_id” en
ambos dataframes, se utilizará para unir las filas correspondientes. -
El resultado de esta operación es un nuevo dataframe que contiene todas
las columnas de rtas_df y todas las columnas de
df_list, combinadas según las coincidencias
encontradas.
**2. mutate(stars =
case_when(…)):** - Esta función, también dedplyr, se utiliza para modificar o crear nuevas columnas en el dataframe. - En este caso, se está modificando la columna "stars". - La funcióncase_whense utiliza para realizar una corrección en los valores de la columna "stars". - Específicamente, si el valor de "stars" es igual a "Postiva" (con una 'v' en lugar de una 'i'), se cambiará a "Positiva" (con una 'i'). - La condiciónTRUE
~ stars` indica que si ninguna de las condiciones anteriores se cumple
(es decir, si “stars” no es igual a “Postiva”), se mantendrá el valor
original de “stars”.
rtas_df
## # A tibble: 1,248 × 7
## review_id resp llm_expl llm_stars review_body stars product_category
## <chr> <chr> <chr> <fct> <chr> <fct> <chr>
## 1 es_0179810 "clasif: Po… "La res… Positiva Fácil de i… Posi… wireless
## 2 es_0506233 "clasif: Ne… "La res… Negativa De momento… Nega… beauty
## 3 es_0627963 "clasif: Ne… "La res… Negativa El product… Nega… wireless
## 4 es_0629229 "clasif: Ne… "La fra… Negativa Se rompen … Nega… sports
## 5 es_0776446 "clasif: Po… "La res… Positiva Bonitos lo… Posi… wireless
## 6 es_0327599 "clasif: Ne… "La res… Negativa Las pegati… Nega… office_product
## 7 es_0481564 "clasif: Ne… "La res… Negativa Compré est… Nega… kitchen
## 8 es_0842471 "clasif: Po… "El usu… Positiva Con estas … Nega… home
## 9 es_0240232 "clasif: Ne… "El tex… Negativa mantiene l… Nega… home
## 10 es_0774932 "clasif: Ne… "La res… Negativa La histori… Nega… book
## # ℹ 1,238 more rows
Muchas veces, una forma más consistente de trabajar con LLMs es
requerir outputs estructurados para las respuestas. En el caso de
gemini.r, esto se logra mediante la función
gemini_structured(). La misma es prácticamente igual a la
que usamos antes, pero tiene un argumento más: un schema,
es decir, un formato de salida estructurado.
Veamos como se usa. Primero generamos un objeto schema
que, a los efectos, de R es una lista; indica al modelo que debe
devolver una lista de objetos con dos propiedades de tipo texto.
schema <- list(
type = "ARRAY",
items = list(
type = "OBJECT",
properties = list(
clasif = list(type = "STRING"),
expl = list(type = "STRING")
),
propertyOrdering = c("clasif", "expl")
)
)
Luego, hacemos las consultas, al LLM usando la función
gemini_structured:
rtas <- list()
for (i in 1:5){
id <- df_list$review_id[i]
Sys.sleep(4.5) # pausa para evitar límite de tasa
cat("Procesando comentario", i, "de", nrow(df_list), "\n")
rta <- gemini_structured(
paste0(prompt, df_list$review_body[i]),
schema = schema
)
cat(rta) # muestra el JSON crudo
rtas[[id]] <- fromJSON(rta)
}
## Procesando comentario 1 de 1259
## Gemini is generating a structured response...
## [
## {
## "clasif": "Positiva",
## "expl": "Paso 1: La reseña destaca la facilidad de instalación y el centrado perfecto del cristal, lo cual son puntos muy positivos. Paso 2: A pesar de mencionar un pequeño levantamiento de 1mm en los laterales, se califica como 'casi inapreciable', minimizando su impacto negativo. Paso 3: Se compara favorablemente con el producto original de Apple, lo que refuerza la percepción positiva. Paso 4: La mención sobre la durabilidad es una observación futura y no un problema actual. Por lo tanto, el sentimiento general de la reseña es claramente positivo."
## }
## ]Procesando comentario 2 de 1259
## Gemini is generating a structured response...
## [
## {
## "clasif": "Negativa",
## "expl": "El usuario indica claramente que 'no he notado ningún cambio' después de usar la mitad del producto. Esto sugiere que el producto no está cumpliendo con sus expectativas o no está produciendo los efectos deseados, lo cual es una experiencia negativa para el consumidor."
## }
## ]Procesando comentario 3 de 1259
## Gemini is generating a structured response...
## [{"clasif":"Negativa","expl":"Paso 1: El usuario expresa que el producto 'dio problemas serios' al primer día de uso. Paso 2: Esta falla inicial fue considerada una 'muy mala señal' para un producto nuevo, lo que llevó a la decisión de devolverlo. Paso 3: Aunque se destaca positivamente la gestión del vendedor al no presentar inconvenientes con el reembolso, el foco principal de la reseña sobre el *producto* es su mal funcionamiento y la necesidad de devolución. Por lo tanto, la experiencia global con el producto es negativa."}]Procesando comentario 4 de 1259
## Gemini is generating a structured response...
## [{"clasif": "Negativa", "expl": "Paso 1: La frase \"Se rompen con mirarlos\" es una expresión idiomática que denota que el producto es sumamente frágil y de baja calidad. Paso 2: Esta observación implica una experiencia muy insatisfactoria con la durabilidad del artículo. Paso 3: Por lo tanto, la reseña se clasifica como negativa."}]Procesando comentario 5 de 1259
## Gemini is generating a structured response...
## [
## {
## "clasif": "Positiva",
## "expl": "El texto presenta múltiples frases con connotación positiva: 'Bonitos los colores' valora la estética, 'Relación calidad precio adecuada' indica satisfacción con el valor, 'muy rápido en envío' elogia el servicio de entrega y 'se adapta a lo que necesitaba' resalta la utilidad del producto. La reseña culmina con la declaración 'Contenta con la compra', confirmando una experiencia globalmente positiva. Todos los comentarios son favorables, sin incluir aspectos negativos."
## }
## ]
Cada iteración:
review_id.Finalmente, unificamos todo en una tibble
test <- rtas %>%
map_dfr(as_tibble, .id = "name")
test
## # A tibble: 5 × 3
## name clasif expl
## <chr> <chr> <chr>
## 1 es_0179810 Positiva "Paso 1: La reseña destaca la facilidad de instalación y …
## 2 es_0506233 Negativa "El usuario indica claramente que 'no he notado ningún ca…
## 3 es_0627963 Negativa "Paso 1: El usuario expresa que el producto 'dio problema…
## 4 es_0629229 Negativa "Paso 1: La frase \"Se rompen con mirarlos\" es una expre…
## 5 es_0776446 Positiva "El texto presenta múltiples frases con connotación posit…
El resultado es un tibble con columnas:
name: identificador (review_id)clasif: etiqueta asignadaexpl: explicación textualVemamos ahora, entonces, cómo funcionó nuestro modelo. Vamos a calcular las métricas habituales.
library(tidymodels)
## ── Attaching packages ────────────────────────────────────── tidymodels 1.3.0 ──
## ✔ broom 1.0.8 ✔ rsample 1.3.0
## ✔ dials 1.4.0 ✔ tune 1.3.0
## ✔ infer 1.0.8 ✔ workflows 1.2.0
## ✔ modeldata 1.4.0 ✔ workflowsets 1.1.0
## ✔ parsnip 1.3.1 ✔ yardstick 1.3.2
## ✔ recipes 1.3.0
## ── Conflicts ───────────────────────────────────────── tidymodels_conflicts() ──
## ✖ scales::discard() masks purrr::discard()
## ✖ dplyr::filter() masks stats::filter()
## ✖ recipes::fixed() masks stringr::fixed()
## ✖ jsonlite::flatten() masks purrr::flatten()
## ✖ dplyr::lag() masks stats::lag()
## ✖ yardstick::spec() masks readr::spec()
## ✖ recipes::step() masks stats::step()
accuracy(rtas_df, stars, llm_stars) %>%
bind_rows(precision(rtas_df, stars, llm_stars)) %>%
bind_rows(recall(rtas_df, stars, llm_stars)) %>%
bind_rows(f_meas(rtas_df, stars, llm_stars))
## # A tibble: 4 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 accuracy binary 0.900
## 2 precision binary 0.830
## 3 recall binary 0.993
## 4 f_meas binary 0.904
conf_mat(rtas_df, stars, llm_stars)
## Truth
## Prediction Negativa Positiva
## Negativa 589 121
## Positiva 4 532
Un punto importante del trabajo con LLMs (y con cualquier modelo de clasificación de texto o no) es tratar de entender de alguna forma los errores que cometen.
Examinar las instancias mal clasificadas permite identificar patrones de error y comprender las limitaciones del modelo. Esto puede revelar sesgos en los datos de entrenamiento, características mal interpretadas o la necesidad de ajustes en el preprocesamiento del texto. Un análisis de errores riguroso proporciona información valiosa para refinar el modelo, mejorar su precisión y adaptarlo a casos de uso específicos.
Para ello vamos a realizar un análisis de casos individuales. Vamos a examinar manualmente los ejemplos mal clasificados para comprender por qué el modelo se equivocó. Puede revelar patrones de error específicos o problemas con el preprocesamiento del texto. Es útil para identificar sesgos en los datos de entrenamiento.
Veamos primero una muestra de 5 “falsos negativos”:
rtas_df %>%
filter(stars == "Positiva" & llm_stars == "Negativa") %>%
sample_n(5) %>%
select(review_body, llm_expl)
## # A tibble: 5 × 2
## review_body llm_expl
## <chr> <chr>
## 1 pará los huesos y dolores "La fra…
## 2 Interesante sin embargo a veces tedioso para leer "La res…
## 3 Es muy bonito, como todos los del autor, pero me parece que no es ad… "La res…
## 4 La sensación en la mano es de endebles. Diría que no van a durar dem… "La res…
## 5 Cumple su función pero la calidad de enlace con la radio genera ruid… "La res…
Ahora, los “falsos positivos”, según la matriz de confusión solamente tenemos 4, así que podemos analizarlos todos
rtas_df %>%
filter(stars == "Negativa" & llm_stars == "Positiva") %>%
select(review_body, llm_expl)
## # A tibble: 4 × 2
## review_body llm_expl
## <chr> <chr>
## 1 Con estas cubiteras no tengo este problema, ya que por su diseño, no… "El usu…
## 2 Fue un regalo para navidades desconozco las opciones pero visto cali… "La res…
## 3 Ha llegado bien , aun no lo he visto con atencio. Pues es un regalo … "El usu…
## 4 Se calienta muy rápido. "El tex…
¿Qué pueden decir de los errores que comete el modelo?