Modelado de temas en producción

Modelado de tendencias en producción

Aprovechando LangChain para pasar de cuadernos de Jupyter ad-hoc a servicios modulares de producción

Imagen por DALL-E 3

En el artículo anterior, discutimos cómo realizar el modelado de temas utilizando ChatGPT y obtuvimos excelentes resultados. La tarea consistía en analizar las reseñas de los clientes de cadenas hoteleras y definir los principales temas mencionados en las reseñas.

En la iteración anterior, utilizamos la API de completados de ChatGPT estándar(API de Interfaz de Programación de Aplicaciones) y enviamos las indicaciones en bruto nosotros mismos. Este enfoque funciona bien cuando estamos realizando investigaciones analíticas ad-hoc.

Sin embargo, si tu equipo está utilizando y monitoreando activamente las reseñas de los clientes, vale la pena considerar cierta automatización. Una buena automatización no solo te ayudará a construir un pipeline autónomo, sino que también será más conveniente (incluso los miembros del equipo no familiarizados con los LLM y la programación podrán acceder a estos datos) y más rentable (enviarás todos los textos al LLM y solo pagarás una vez).

Supongamos que estamos construyendo un servicio sostenible y listo para producción. En ese caso, vale la pena aprovechar los frameworks existentes para reducir la cantidad de código de pegamento y tener una solución más modular (para poder cambiar fácilmente, por ejemplo, de un LLM a otro).

En este artículo, me gustaría hablarte sobre uno de los frameworks más populares para aplicaciones de LLM, LangChain. Además, entenderemos en detalle cómo evaluar el rendimiento de tu modelo, ya que es un paso crucial para las aplicaciones empresariales.

Matices del proceso de producción

Revisando el enfoque inicial

Primero, revisemos nuestro enfoque anterior para el Modelado de Temas ad-hoc con ChatGPT.

Paso 1: Obtener una muestra representativa.

Queremos determinar la lista de temas que utilizaremos para nuestro marcado. La forma más sencilla es enviar todas las reseñas y pedirle al LLM que defina la lista de 20-30 temas mencionados en nuestras reseñas. Desafortunadamente, no podremos hacerlo, ya que no cabrá en el tamaño del contexto. Podríamos usar un enfoque de map-reduce, pero podría ser costoso. Por eso nos gustaría definir una muestra representativa.

Para esto, construimos un modelo de temas BERTopic y obtuvimos las reseñas más representativas para cada tema.

Paso 2: Determinar la lista de temas que utilizaremos para el marcado.

El siguiente paso es pasar todos los textos seleccionados a ChatGPT y pedirle que defina una lista de temas mencionados en estas reseñas. Luego, podemos utilizar estos temas para el marcado posterior.

Paso 3: Realizar el marcado de los temas en lotes.

El último paso es el más sencillo: podemos enviar las reseñas de los clientes en lotes que se ajusten al tamaño del contexto y pedirle al LLM que devuelva los temas para cada reseña de cliente.

Finalmente, con estos tres pasos, podríamos determinar la lista de temas relevantes para nuestros textos y clasificarlos todos.

Funciona perfectamente para investigaciones únicas. Sin embargo, nos faltan algunos detalles para una solución excelente lista para producción.

De ad-hoc a producción

Analizaremos qué mejoras podríamos realizar en nuestro enfoque inicial ad-hoc.

  • En el enfoque anterior, tenemos una lista estática de temas. Pero en ejemplos de la vida real, podrían surgir nuevos temas con el tiempo, por ejemplo, si lanzas una nueva función. Por lo tanto, necesitamos un bucle de retroalimentación para actualizar la lista de temas que estamos utilizando. La forma más fácil de hacerlo es capturar la lista de reseñas sin ningún tema asignado y ejecutar regularmente el modelado de temas en ellas.
  • Si estamos realizando una investigación única, podemos validar los resultados de las asignaciones de temas manualmente. Pero para el proceso que se ejecuta en producción, debemos pensar en una evaluación continua.
  • Si estamos construyendo un pipeline para el análisis de las reseñas de los clientes, debemos considerar más casos de uso potenciales y almacenar otra información relacionada que pueda ser necesaria. Por ejemplo, es útil almacenar versiones traducidas de las reseñas de los clientes para que nuestros compañeros no tengan que usar Google Traductor todo el tiempo. Además, el sentimiento y otras características (por ejemplo, los productos mencionados en la reseña del cliente) pueden ser valiosos para el análisis y filtrado.
  • La industria de los LLM está progresando rápidamente en este momento y todo cambia constantemente. Vale la pena considerar un enfoque modular donde podemos iterar rápidamente y probar nuevos enfoques con el tiempo sin tener que reescribir todo el servicio desde cero.
Esquema del servicio por el autor

Tenemos muchas ideas sobre qué hacer con nuestro servicio de modelado de temas. Pero vamos a enfocarnos en las partes principales: enfoque modular en lugar de llamadas API y evaluación. El marco de trabajo LangChain nos ayudará con ambos temas, así que vamos a aprender más sobre él.

Conceptos básicos de LangChain

LangChain es un marco de trabajo para construir aplicaciones basadas en Modelos de Lenguaje. Aquí están los principales componentes de LangChain:

  • Schema son las clases más básicas como Documentos, Mensajes de Chat y Textos.
  • Modelos. LangChain proporciona acceso a LLMs, Modelos de Chat y Modelos de Incrustación de Texto que se pueden utilizar fácilmente en sus aplicaciones y cambiar entre ellos si es necesario. No hace falta decir que admite modelos populares como ChatGPT, Anthropic y Llama.
  • Prompts es una funcionalidad para ayudar a trabajar con indicaciones, incluyendo plantillas de indicaciones, analizadores de salida y selectores de ejemplos para indicaciones de muestra.
  • Cadenas son el núcleo de LangChain (como podrás adivinar por el nombre). Las cadenas te ayudan a construir una secuencia de bloques que se ejecutarán. Puedes apreciar realmente esta funcionalidad si estás construyendo una aplicación compleja.
  • Índices: cargadores de documentos, separadores de texto, almacenes de vectores y recuperadores. Este módulo proporciona herramientas que ayudan a los LLMs a interactuar con sus documentos. Esta funcionalidad sería valiosa si estás construyendo un caso de uso de preguntas y respuestas. No utilizaremos mucho esta funcionalidad en nuestro ejemplo de hoy.
  • LangChain proporciona un conjunto completo de métodos para gestionar y limitar la memoria. Esta funcionalidad es principalmente necesaria para escenarios de ChatBot.
  • Una de las últimas y más poderosas características es agentes. Si eres un usuario frecuente de ChatGPT, debes haber oído hablar de los plugins. Es la misma idea de que puedes potenciar LLMs con un conjunto de herramientas personalizadas o predefinidas (como Búsqueda de Google o Wikipedia), y luego el agente puede utilizarlas al responder tus preguntas. En esta configuración, LLM actúa como un agente de razonamiento y decide lo que necesita hacer para lograr el resultado y cuándo obtiene la respuesta final que puede compartir. Es una funcionalidad emocionante, así que definitivamente merece una discusión separada.

Entonces, LangChain puede ayudarnos a construir aplicaciones modulares y ser capaz de cambiar entre diferentes componentes (por ejemplo, de ChatGPT a Anthropic o de CSV como entrada de datos a Snowflake DB). LangChain tiene más de 190 integraciones, por lo que puede ahorrarte bastante tiempo.

También podríamos reutilizar cadenas prehechas para algunos casos de uso en lugar de comenzar desde cero.

Cuando llamamos a la API de ChatGPT manualmente, tenemos que gestionar bastante código de enlace en Python para que funcione. No es un problema cuando estás trabajando en una tarea pequeña y sencilla, pero puede volverse inmanejable cuando necesitas construir algo más complejo y enrevesado. En esos casos, LangChain puede ayudarte a eliminar este código de enlace y crear un código modular más fácil de mantener.

Sin embargo, LangChain tiene sus propias limitaciones:

  • Está principalmente enfocado en modelos de OpenAI, por lo que es posible que no funcione tan bien con modelos de código abierto locales.
  • El lado negativo de la conveniencia es que no es fácil entender qué está sucediendo internamente y cuándo y cómo se ejecuta la API de ChatGPT por la que estás pagando. Puedes usar el modo de depuración, pero debes especificarlo y revisar los registros completos para tener una vista más clara.
  • A pesar de tener una documentación bastante buena, a veces tengo dificultades para encontrar respuestas a mis preguntas. No hay tantos tutoriales y recursos en Internet aparte de la documentación oficial, muchas veces solo se ven páginas oficiales en Google.
  • La biblioteca LangChain está progresando mucho y el equipo constantemente ofrece nuevas características. Por lo tanto, la biblioteca no está madura y es posible que tengas que cambiar la funcionalidad que estás utilizando. Por ejemplo, la clase SequentialChain se considera obsoleta ahora y podría ser descontinuada en el futuro, ya que han introducido LCEL – hablaremos de eso con más detalle más adelante.

Hemos obtenido una visión general de la funcionalidad de LangChain, pero la práctica hace al maestro. Comencemos a usar LangChain.

Mejorando la asignación de temas

Refactoricemos la asignación de temas ya que será la operación más común en nuestro proceso regular, y nos ayudará a comprender cómo usar LangChain en la práctica.

En primer lugar, necesitamos instalar el paquete.

!pip install --upgrade langchain

Carga de documentos

Para trabajar con las opiniones de los clientes, primero debemos cargarlos. Para ello, podemos usar Cargadores de Documentos. En nuestro caso, las opiniones de los clientes se almacenan como un conjunto de archivos .txt en un directorio, pero también puedes cargar fácilmente documentos desde herramientas de terceros. Por ejemplo, hay una integración con Snowflake.

Utilizaremos el DirectoryLoader para cargar todos los archivos del directorio ya que tenemos archivos separados para hoteles. Para cada archivo, especificaremos TextLoader como cargador (por defecto, se utiliza un cargador para documentos no estructurados). Nuestros archivos están codificados en ISO-8859–1, por lo que la llamada por defecto devuelve un error. Sin embargo, LangChain puede detectar automáticamente la codificación utilizada. Con esta configuración, funciona bien.

from langchain.document_loaders import TextLoader, DirectoryLoadertext_loader_kwargs={'autodetect_encoding': True}loader = DirectoryLoader('./hotels/london', show_progress=True,     loader_cls=TextLoader, loader_kwargs=text_loader_kwargs)docs = loader.load()len(docs)82

Dividiendo documentos

Ahora, nos gustaría dividir nuestros documentos. Sabemos que cada archivo consiste en un conjunto de comentarios de clientes delimitados por \n. Dado que nuestro caso es muy sencillo, utilizaremos el CharacterTextSplitter más básico que divide los documentos por caracteres. Cuando se trabaja con documentos reales (textos largos completos en lugar de comentarios cortos e independientes), es mejor usar la división recursiva por caracteres ya que te permite dividir los documentos en fragmentos de forma más inteligente.

Sin embargo, LangChain es más adecuado para la división de texto difuso. Así que tuve que hacer un pequeño truco para que funcionara como quería.

Cómo funciona:

  • Especificas chunk_size y chunk_overlap, e intenta hacer el menor número de divisiones para que cada fragmento sea más pequeño que chunk_size. Si no puede crear un fragmento lo suficientemente pequeño, imprimirá un mensaje en la salida de la libreta Jupyter.
  • Si especificas un chunk_size demasiado grande, no se separarán todos los comentarios.
  • Si especificas un chunk_size demasiado pequeño, tendrás declaraciones de impresión para cada comentario en tu salida, lo que provocará la recarga de la libreta. Desafortunadamente, no pude encontrar ningún parámetro para desactivarlo.

Para solucionar este problema, especificé length_function como una constante igual a chunk_size. Entonces obtuve una división estándar por caracteres. LangChain proporciona la suficiente flexibilidad para hacer lo que deseas, pero solo de una manera algo chapucera.

from langchain.text_splitter import CharacterTextSplittertext_splitter = CharacterTextSplitter(    separator = "\n",    chunk_size = 1,    chunk_overlap  = 0,    length_function = lambda x: 1, # usualmente se usa len     is_separator_regex = False)split_docs = text_splitter.split_documents(docs)len(split_docs) 12890

También agreguemos el ID del documento a los metadatos, lo usaremos más adelante.

for i in range(len(split_docs)):    split_docs[i].metadata['id'] = i

La ventaja de usar Documentos es que ahora tenemos fuentes de datos automáticas y podemos filtrar datos por ellas. Por ejemplo, podemos filtrar solo los comentarios relacionados con el hotel Travelodge.

list(filter(    lambda x: 'travelodge' in x.metadata['source'],    split_docs))

A continuación, necesitamos un modelo. Como discutimos anteriormente en LangChain, hay LLMs y modelos de chat. La diferencia principal es que LLMs toman texto y devuelven texto, mientras que los modelos de chat son más adecuados para casos de uso de conversación y pueden obtener un conjunto de mensajes como entrada. En nuestro caso, utilizaremos el ChatModel de OpenAI ya que nos gustaría pasar también mensajes del sistema.

from langchain.chat_models import ChatOpenAIchat = ChatOpenAI (temperatura=0.0, modelo="gpt-3.5-turbo", openai_api_key = "tu_clave")

Prompts

Avancemos a la parte más importante: nuestra indicación. En LangChain, hay un concepto de Plantillas de Indicaciones. Ayudan a reutilizar indicaciones parametrizadas por variables. Es útil ya que, en aplicaciones reales, las indicaciones pueden ser muy detalladas y sofisticadas. Entonces, las plantillas de indicaciones pueden ser una abstracción útil de alto nivel que te ayudaría a gestionar tu código de manera efectiva.

Como vamos a utilizar el modelo de chat, necesitaremos ChatPromptTemplate.

Pero antes de entrar en las indicaciones, hablemos brevemente de una función útil: un analizador de resultados. Sorprendentemente, pueden ayudarnos a crear una indicación efectiva. Podemos definir la salida deseada, generar un analizador de resultados y luego usar el analizador para crear instrucciones para la indicación.

Definamos lo que nos gustaría ver en la salida. Primero, nos gustaría poder pasar una lista de reseñas de clientes a la indicación para procesarlas por lotes, para obtener una lista con los siguientes parámetros en el resultado:

  • id para identificar los documentos,
  • lista de temas de la lista predefinida (utilizaremos la lista de nuestra iteración anterior),
  • sentimiento (negativo, neutral o positivo).

Especifiquemos nuestro analizador de resultados. Como necesitamos una estructura JSON bastante compleja, utilizaremos Pydantic Output Parser en lugar del analizador de resultados estructurados más comúnmente utilizado Structured Output Parser.

Para eso, necesitamos crear una clase heredada de BaseModel y especificar todos los campos que necesitamos con nombres y descripciones (para que LLM pueda entender qué esperamos en la respuesta).

from langchain.output_parsers import PydanticOutputParserfrom langchain.pydantic_v1 import BaseModel, Fieldfrom typing import Listclass CustomerCommentData(BaseModel):    doc_id: int = Field(description="doc_id de los parámetros de entrada")    topics: List[str] = Field(description="Lista de los temas relevantes \        para la reseña del cliente. Por favor, incluye solo temas de \        la lista proporcionada.")    sentiment: str = Field(description="sentimiento del comentario (positivo, neutral o negativo)")output_parser = PydanticOutputParser(pydantic_object=CustomerCommentData)

Ahora, podríamos utilizar este analizador para generar instrucciones de formato para nuestra indicación. Es un caso fantástico en el que podrías utilizar las mejores prácticas de indicación y ahorrar tiempo en la creación de la indicación.

format_instructions = output_parser.get_format_instructions()print(format_instructions)

Luego, es hora de pasar a nuestra indicación. Tomamos un lote de comentarios y los formateamos en el formato esperado. Luego, creamos un mensaje de indicación con un montón de variables: topics_descr_list, format_instructions y input_data. Después de eso, creamos mensajes de indicación de chat que consisten en un mensaje del sistema constante y un mensaje de indicación. El último paso es formatear los mensajes de indicación de chat con valores reales.

from langchain.prompts import ChatPromptTemplatedocs_batch_data = []for rec in docs_batch:    docs_batch_data.append(        {            'id': rec.metadata['id'],            'review': rec.page_content        }    )topic_assignment_msg = '''A continuación se muestra una lista de reseñas de clientes en formato JSON con las siguientes claves:1. doc_id - identificador de la reseña2. review - texto de reseña de clientesPor favor, analiza las reseñas proporcionadas e identifica los temas principales y el sentimiento. Incluye solo temas de la lista proporcionada.Lista de temas con descripciones (delimitadas con ":"):{topics_descr_list}Formato de salida:{format_instructions}Reseñas de clientes:```{input_data}```'''topic_assignment_template = ChatPromptTemplate.from_messages([    ("sistema", "Eres un asistente útil. Tu tarea es analizar las reseñas del hotel."),    ("humano", topic_assignment_msg)])topics_list = '\n'.join(    map(lambda x: '%s: %s' % (x['nombre_tema'], x['descripcion_tema']),       temas))messages = topic_assignment_template.format_messages(    topics_descr_list = topics_list,    format_instructions = format_instructions,    input_data = json.dumps(docs_batch_data))

Ahora podemos pasar estos mensajes formateados a LLM y ver una respuesta.

response = chat(messages)type(response.content)strprint(response.content)

Obtuvimos la respuesta como un objeto string, pero podríamos aprovechar nuestro analizador y obtener la lista de objetos de la clase CustomerCommentData como resultado.

response_dict = list(map(lambda x: output_parser.parse(x),   response.content.split('\n')))response_dict

Así que hemos aprovechado LangChain y algunas de sus características y ya hemos construido una solución un poco más inteligente que podría asignar temas a los comentarios en lotes (lo que nos ahorraría algunos costos) y comenzó a definir no solo temas sino también sentimientos.

Agregando más lógica

Hasta ahora, hemos construido solo llamadas de LLM individuales sin relaciones ni secuencias. Sin embargo, en la vida real, a menudo queremos dividir nuestras tareas en múltiples pasos. Para eso, podemos usar Cadenas. La Cadena es el bloque de construcción fundamental de LangChain.

LLMChain

El tipo más básico de cadena es un LLMChain. Es una combinación de LLM y una plantilla.

Entonces, podemos reescribir nuestra lógica en una cadena. Este código nos dará absolutamente el mismo resultado que antes, pero es bastante conveniente tener un solo método que lo define todo.

from langchain.chains import LLMChaintopic_assignment_chain = LLMChain(llm=chat, prompt=topic_assignment_template)response = topic_assignment_chain.run(    topics_descr_list = topics_list,    format_instructions = format_instructions,    input_data = json.dumps(docs_batch_data))

Cadenas Secuenciales

La cadena LLM es muy básica. El poder de las cadenas radica en la construcción de lógica más compleja. Intentemos crear algo más avanzado.

La idea de las cadenas secuenciales es utilizar la salida de una cadena como entrada para otra.

Para definir las cadenas, utilizaremos LCEL (Lenguaje de Expresión de LangChain). Este nuevo lenguaje se introdujo hace apenas un par de meses y ahora todos los enfoques antiguos con SimpleSequentialChain o SequentialChain se consideran obsoletos. Por lo tanto, vale la pena dedicar algún tiempo a comprender el concepto de LCEL.

Reescribamos la cadena anterior en LCEL.

chain = topic_assignment_template | chatresponse = chain.invoke(    {        'topics_descr_list': topics_list,        'format_instructions': format_instructions,        'input_data': json.dumps(docs_batch_data)    })

Si quieres aprenderlo de primera mano, te sugiero que veas este video sobre LCEL del equipo de LangChain.

Usando cadenas secuenciales

En algunos casos, puede ser útil tener varias llamadas secuenciales para que la salida de una cadena se utilice en las otras.

En nuestro caso, primero podemos traducir las reseñas al inglés y luego realizar el modelado de temas y el análisis de sentimientos.

from langchain.schema import StrOutputParserfrom operator import itemgetter# translationtranslate_msg = '''A continuación se muestra una lista de reseñas de clientes en formato JSON con las siguientes claves:1. doc_id - identificador de la reseña2. review - texto de la reseña del clientePor favor, traduzca la reseña al inglés y devuelva el mismo JSON. Por favor, devuelva en la salida SOLO JSON válido sin ninguna otra información.Reseñas de los clientes:```{input_data}```'''translate_template = ChatPromptTemplate.from_messages([    ("system", "Eres una API, por lo que solo devuelves JSON válido sin comentarios."),    ("human", translate_msg)])# asignación de temas y análisis de sentimentostopic_assignment_msg = '''A continuación se muestra una lista de reseñas de clientes en formato JSON con las siguientes claves:1. doc_id - identificador de la reseña2. review - texto de la reseña del clientePor favor, analice las reseñas proporcionadas e identifique los temas principales y el sentimiento. Incluya solo los temas de la lista proporcionada a continuación.Lista de temas con descripciones (delimitados con ":"): {topics_descr_list}Formato de salida: {format_instructions}Reseñas de los clientes:```{translated_data}```'''topic_assignment_template = ChatPromptTemplate.from_messages([    ("system", "Eres un asistente útil. Tu tarea es analizar las reseñas de hoteles."),    ("human", topic_assignment_msg)])# definiendo las cadenastranslate_chain = translate_template | chat | StrOutputParser()topic_assignment_chain = {'topics_descr_list': itemgetter('topics_descr_list'),                           'translated_data': translate_chain,                           'format_instructions': itemgetter('format_instructions')}                         | topic_assignment_template | chat # ejecuciónresponse = topic_assignment_chain.invoke(    {        'topics_descr_list': topics_list,        'format_instructions': format_instructions,        'input_data': json.dumps(docs_batch_data)    })

De manera similar, definimos plantillas de indicaciones para la traducción y asignación de temas. Luego, determinamos la cadena de traducción. Lo único nuevo aquí es el uso de StrOutputParser(), que convierte los objetos de respuesta en cadenas (nada complicado).

A continuación, definimos la cadena completa, especificando los parámetros de entrada, la plantilla de indicaciones y LLM. Para los parámetros de entrada, tomamos translated_data de la salida de translate_chain mientras que los otros parámetros provienen de la entrada de invocación utilizando la función itemgetter.

Sin embargo, en nuestro caso, ese enfoque con una cadena combinada puede no ser tan conveniente, ya que nos gustaría guardar también la salida de la primera cadena para tener los valores traducidos.

Con las cadenas, todo se vuelve un poco más complicado, por lo que podríamos necesitar ciertas capacidades de depuración. Hay dos opciones para la depuración. La primera es que puedes activar la depuración localmente.

import langchainlangchain.debug = True

La otra opción es utilizar la plataforma LangChain – LangSmith. Sin embargo, todavía está en modo de prueba beta, por lo que es posible que debas esperar para obtener acceso.

Enrutamiento

Uno de los casos más complejos de las cadenas es el enrutamiento cuando se utilizan diferentes indicaciones para diferentes casos de uso. Por ejemplo, podríamos guardar diferentes parámetros de reseñas de clientes según el sentimiento:

  • Si el comentario es negativo, guardaremos la lista de problemas mencionados por el cliente.
  • De lo contrario, obtendremos la lista de puntos buenos de la reseña.

Para usar una cadena de enrutamiento, deberemos pasar los comentarios uno por uno en lugar de agruparlos como lo hicimos antes.

Entonces, nuestra cadena a alto nivel se verá así.

Primero, necesitamos definir la cadena principal que determine el sentimiento. Esta cadena consta de una indicación, LLM y la ya familiar StrOutputParser().

sentiment_msg = '''Dado el comentario del cliente a continuación, clasifica si es negativo. Si es negativo, devuelve "negativo", de lo contrario, devuelve "positivo". No respondas con más de una palabra.Comentario del cliente:```{input_data}```'''sentiment_template = ChatPromptTemplate.from_messages([    ("sistema", "Eres un asistente. Tu tarea es etiquetar el sentimiento de las reseñas de hoteles."),    ("humano", sentiment_msg)])sentiment_chain = sentiment_template | chat | StrOutputParser()

Para las reseñas positivas, pediremos al modelo que extraiga los puntos buenos, mientras que para las negativas pediremos los problemas. Por lo tanto, necesitaremos dos cadenas diferentes. Utilizaremos los mismos analizadores de salida Pydantic que antes para especificar el formato de salida deseado y generar instrucciones.

Utilizamos partial_variables junto con el mensaje general de asignación de temas para especificar distintas instrucciones de formato para casos positivos y negativos.

from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate# definiendo la estructura para casos positivos y negativos class PositiveCustomerCommentData(BaseModel):    topics: List[str] = Field(description="Lista de los temas relevantes para la reseña del cliente. Incluye solo temas de la lista proporcionada.")        advantages: List[str] = Field(description = "Enumera los puntos buenos mencionados por el cliente.")    sentiment: str = Field(description="Sentimiento del comentario (positivo, neutral o negativo).")class NegativeCustomerCommentData(BaseModel):    topics: List[str] = Field(description="Lista de los temas relevantes para la reseña del cliente. Incluye solo temas de la lista proporcionada.")        problems: List[str] = Field(description = "Enumera los problemas mencionados por el cliente.")    sentiment: str = Field(description="Sentimiento del comentario (positivo, neutral o negativo).")# definiendo analizadores de salida e instrucciones de formatopositive_output_parser = PydanticOutputParser(pydantic_object=PositiveCustomerCommentData)positive_format_instructions = positive_output_parser.get_format_instructions()negative_output_parser = PydanticOutputParser(pydantic_object=NegativeCustomerCommentData)negative_format_instructions = negative_output_parser.get_format_instructions()general_topic_assignment_msg = '''A continuación hay una reseña de un cliente delimitada por ```.Analiza la reseña proporcionada e identifica los temas principales y el sentimiento. Incluye solo temas de la lista proporcionada.Lista de temas con descripciones (delimitadas por ":"):{topics_descr_list}Formato de salida:{format_instructions}Reseñas de cliente:```{input_data}```'''# definiendo las plantillas de indicacionespositive_topic_assignment_template = ChatPromptTemplate(     messages=[         SystemMessagePromptTemplate.from_template("Eres un asistente útil. Tu tarea es analizar las reseñas de hoteles."),        HumanMessagePromptTemplate.from_template(general_topic_assignment_msg)     ],     input_variables=["topics_descr_list", "input_data"],     partial_variables={"format_instructions": positive_format_instructions} )negative_topic_assignment_template = ChatPromptTemplate(     messages=[         SystemMessagePromptTemplate.from_template("Eres un asistente útil. Tu tarea es analizar las reseñas de hoteles."),        HumanMessagePromptTemplate.from_template(general_topic_assignment_msg)     ],     input_variables=["topics_descr_list", "input_data"],     partial_variables={"format_instructions": negative_format_instructions} )

Entonces, ahora solo necesitamos construir la cadena completa. La lógica principal se define utilizando RunnableBranch y una condición basada en el sentimiento, una salida de sentiment_chain.

from langchain.schema.runnable import RunnableBranchbranch = RunnableBranch(  (lambda x: "negativo" in x["sentiment"].lower(), negative_chain),  positive_chain)full_route_chain = {    "sentiment": sentiment_chain,    "input_data": lambda x: x["input_data"],    "topics_descr_list": lambda x: x["topics_descr_list"} | branchfull_route_chain.invoke({'input_data': review,   'topics_descr_list': topics_list})

Aquí hay un par de ejemplos. Funciona muy bien y devuelve objetos diferentes según el sentimiento.

Hemos analizado en detalle el enfoque modular para hacer Topic Modelling utilizando LangChain e introducir lógica más compleja. Ahora, es hora de pasar a la segunda parte y discutir cómo podríamos evaluar el rendimiento del modelo.

Evaluación

La parte crucial de cualquier sistema en producción es evaluación. Cuando tenemos un modelo LLM en producción, queremos asegurarnos de la calidad y supervisarlo con el tiempo.

En muchos casos, no solo se puede utilizar a las personas para verificar los resultados del modelo durante un pequeño período de tiempo y controlar el rendimiento, sino que también se puede aprovechar LLM para esta tarea. Podría ser una buena idea utilizar un modelo más complejo para realizar controles en tiempo de ejecución. Por ejemplo, usamos ChatGPT 3.5 para nuestras asignaciones de temas, pero podríamos usar GPT 4 para evaluación (similar al concepto de supervisión en la vida real cuando se pide a colegas más senior una revisión de código).

Langchain también puede ayudarnos en esta tarea, ya que proporciona algunas herramientas para evaluar los resultados:

  • Evaluadores de Cadena ayudan a evaluar los resultados de su modelo. Existe un conjunto bastante amplio de herramientas, desde validar el formato hasta evaluar la corrección basada en el contexto o la referencia proporcionada. Hablaremos de estos métodos con más detalle a continuación.
  • La otra clase de evaluadores son los Evaluadores de Comparación. Serán útiles si desea evaluar el rendimiento de 2 modelos LLM diferentes (caso de prueba A/B). No entraremos en detalles hoy.

Coincidencia exacta

El enfoque más sencillo es comparar la salida del modelo con la respuesta correcta (es decir, la de los expertos o un conjunto de entrenamiento) utilizando una coincidencia exacta. Para eso, podríamos usar ExactMatchStringEvaluator, por ejemplo, para evaluar el rendimiento de nuestro análisis de sentimiento. En este caso, no necesitamos LLMs.

from langchain.evaluation import ExactMatchStringEvaluatorevaluator = ExactMatchStringEvaluator(    ignore_case=True,    ignore_numbers=True,    ignore_punctuation=True,)evaluator.evaluate_strings(    prediction="positivo.",    reference="Positivo")# {'score': 1}evaluator.evaluate_strings(    prediction="negativo",    reference="Positivo")# {'score': 0}

Puede crear su propio Evaluador de Cadena personalizado o hacer coincidir la salida con una expresión regular.

También hay herramientas útiles para validar la salida estructurada, ya sea si la salida es un JSON válido, tiene la estructura esperada y está cerca de la referencia por distancia. Puede encontrar más detalles al respecto en la documentación.

Evaluación de distancia de embeddings

Otro enfoque útil es analizar la distancia entre embeddings. Obtendrá una puntuación en el resultado: cuanto menor sea la puntuación, mejor, ya que las respuestas están más cerca entre sí. Por ejemplo, podemos comparar los buenos puntos encontrados por distancia euclidiana.

from langchain.evaluation import load_evaluatorfrom langchain.evaluation import EmbeddingDistanceevaluator = load_evaluator(    "embedding_distance", distance_metric=EmbeddingDistance.EUCLIDEAN)evaluator.evaluate_strings(  prediction="habitaciones bien diseñadas, limpias, excelente ubicación",   reference="habitaciones bien diseñadas, limpias, excelente ubicación, buena atmósfera"){'score': 0.20732719121627757}

Obtuvimos una distancia de 0.2. Sin embargo, los resultados de esta evaluación pueden ser más difíciles de interpretar, ya que deberás examinar las distribuciones de tus datos y definir umbrales. Pasemos a enfoques basados en LLMs, ya que podremos interpretar sus resultados sin esfuerzo.

Evaluación de criterios

Puedes utilizar LangChain para validar la respuesta de LLM frente a un conjunto de rúbricas o criterios. Existe una lista de criterios predefinidos, o puedes crear uno personalizado.

from langchain.evaluation import Criterialist(Criteria)[<Criteria.CONCISENESS: 'conciseness'>, <Criteria.RELEVANCE: 'relevance'>, <Criteria.CORRECTNESS: 'correctness'>, <Criteria.COHERENCE: 'coherence'>, <Criteria.HARMFULNESS: 'harmfulness'>, <Criteria.MALICIOUSNESS: 'maliciousness'>, <Criteria.HELPFULNESS: 'helpfulness'>, <Criteria.CONTROVERSIALITY: 'controversiality'>, <Criteria.MISOGYNY: 'misogyny'>, <Criteria.CRIMINALITY: 'criminality'>, <Criteria.INSENSITIVITY: 'insensitivity'>, <Criteria.DEPTH: 'depth'>, <Criteria.CREATIVITY: 'creativity'>, <Criteria.DETAIL: 'detail'>]

Algunos de ellos no requieren referencia (por ejemplo, harmfulness o conciseness). Pero para correctness, necesitas saber cuál es la respuesta correcta. Intentemos utilizarlo para nuestros datos.

evaluator = load_evaluator("criteria", criteria="conciseness")eval_result = evaluator.evaluate_strings(    prediction="habitaciones bien diseñadas, limpias, excelente ubicación",    input="Enumera los puntos buenos que mencionó el cliente",)

Como resultado, obtenemos la respuesta (si los resultados cumplen el criterio especificado) y el razonamiento de la cadena de pensamiento para que podamos entender la lógica detrás del resultado y ajustar la sugerencia si es necesario.

Si estás interesado en cómo funciona, puedes activar langchain.debug = True y ver la sugerencia enviada a LLM.

Veamos el criterio de correctness. Para evaluarlo, necesitamos proporcionar una referencia (la respuesta correcta).

evaluator = load_evaluator("labeled_criteria", criteria="correctness")eval_result = evaluator.evaluate_strings(    prediction="habitaciones bien diseñadas, limpias, excelente ubicación",    input="Enumera los puntos buenos que mencionó el cliente",    reference="habitaciones bien diseñadas, limpias, excelente ubicación, buena atmósfera",)

Incluso puedes crear tus propios criterios personalizados, por ejemplo, para determinar si se mencionan varios puntos en la respuesta.

custom_criterion = {"multiple": "¿La respuesta contiene varios puntos?"}evaluator = load_evaluator("criteria", criteria=custom_criterion)eval_result = evaluator.evaluate_strings(    prediction="habitaciones bien diseñadas, limpias, excelente ubicación",    input="Enumera los puntos buenos que mencionó el cliente",)

Evaluación de puntuación

Con la evaluación de criterios, solo obtenemos una respuesta de Sí o No, pero en muchos casos, no es suficiente. Por ejemplo, en nuestro ejemplo, la predicción tiene 3 de los 4 puntos mencionados, lo cual es un buen resultado, pero obtuvimos N al evaluarlo en términos de corrección. Entonces, utilizando este enfoque, las respuestas “habitaciones bien diseñadas, limpias, excelente ubicación” y “internet rápido” serán iguales en términos de nuestras métricas, lo cual no nos dará suficiente información para entender el rendimiento del modelo.

Hay otra técnica bastante cercana de puntuación cuando se le pide a LLM que proporcione la puntuación en la salida, lo que podría ayudar a obtener resultados más detallados. Vamos a intentarlo.

from langchain.chat_models import ChatOpenAIaccuracy_criteria = {    "accuracy": """Puntuación 1: La respuesta no menciona ningún punto relevante.Puntuación 3: La respuesta menciona solo algunos puntos relevantes pero tiene imprecisiones importantes o incluye varias opciones no relevantes.Puntuación 5: La respuesta tiene una cantidad moderada de opciones relevantes pero puede tener imprecisiones o puntos incorrectos.Puntuación 7: La respuesta se alinea con la referencia y muestra la mayoría de los puntos relevantes y no menciona opciones completamente incorrectas.Puntuación 10: La respuesta es completamente precisa y se alinea perfectamente con la referencia."""}evaluator = load_evaluator(    "labeled_score_string",     criteria=accuracy_criteria,     llm=ChatOpenAI(model="gpt-4"),)eval_result = evaluator.evaluate_strings(    prediction="habitaciones bien diseñadas, limpias, excelente ubicación",    input="""A continuación se muestra una reseña del cliente delimitada por ```. Proporcione la lista de los puntos buenos que menciona el cliente en la reseña del cliente.    Reseña del cliente:    ```    Habitaciones pequeñas pero bien diseñadas, limpias, excelente ubicación, buena atmósfera. Me quedaría allí nuevamente. El desayuno continental es débil pero está bien.    ```    """,    reference="habitaciones bien diseñadas, limpias, excelente ubicación, buena atmósfera")

Obtuvimos un puntaje de siete, que parece bastante válido. Veamos la indicación real utilizada.

Sin embargo, consideraría con precaución las puntuaciones de LLMs. Recuerda, no es una función de regresión y las puntuaciones pueden ser bastante subjetivas.

Hemos estado utilizando el modelo de puntuación con la referencia. Pero en muchos casos, es posible que no tengamos las respuestas correctas o podría resultar costoso obtenerlas. Puedes utilizar el evaluador de puntuación incluso sin puntuaciones de referencia pidiendo al modelo que evalúe la respuesta. Vale la pena utilizar GPT-4 para tener más confianza en los resultados.

accuracy_criteria = {    "recall": "La respuesta del asistente debe incluir todo lo mencionado en la pregunta. Si falta información, la respuesta se califica más baja.",    "precision": "La respuesta del asistente no debe tener puntos que no estén presentes en la pregunta."}evaluator = load_evaluator("score_string", criteria=accuracy_criteria,   llm=ChatOpenAI(model="gpt-4"))eval_result = evaluator.evaluate_strings(    prediction="habitaciones bien diseñadas, limpias, excelente ubicación",    input="""A continuación se muestra una reseña del cliente delimitada por ```. Proporcione la lista de los puntos buenos que menciona el cliente en la reseña del cliente.    Reseña del cliente:    ```    Habitaciones pequeñas pero bien diseñadas, limpias, excelente ubicación, buena atmósfera. Me quedaría allí nuevamente. El desayuno continental es débil pero está bien.    ```    """)

Obtuvimos un puntaje bastante cercano al anterior.

Hemos examinado muchas formas posibles de validar la salida, así que espero que estés listo ahora para probar los resultados de tus modelos.

Resumen

En este artículo, hemos discutido algunas sutilezas que debemos tener en cuenta si queremos utilizar LLMs para procesos de producción.

  • Hemos analizado el uso del marco de trabajo LangChain para hacer nuestra solución más modular, de manera que pudiéramos iterar fácilmente y utilizar nuevos enfoques (por ejemplo, cambiar de un LLM a otro). Además, los marcos de trabajo suelen ayudar a que nuestro código sea más fácil de mantener.
  • Otro tema importante que hemos discutido son las diferentes herramientas que tenemos para evaluar el rendimiento del modelo. Si estamos utilizando LLMs en producción, necesitamos tener un monitoreo constante en su lugar para asegurar la calidad de nuestro servicio, y vale la pena dedicar tiempo para crear un proceso de evaluación basado en LLMs o en una combinación de personas y tecnología.

Gracias por leer este artículo. Espero que haya sido informativo para ti. Si tienes alguna pregunta o comentario adicional, por favor déjalos en la sección de comentarios.

Conjunto de datos

Ganesan, Kavita y Zhai, ChengXiang. (2011). Conjunto de datos OpinRank Review. Repositorio de Aprendizaje Automático de UCI. https://doi.org/10.24432/C5QW4W

Referencia

Este artículo se basa en información del curso “LangChain para el desarrollo de aplicaciones de LLM” de DeepLearning.AI y LangChain.

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Todo lo que necesitas saber sobre la serie de modelos de lenguaje (LLMs) Qwen Large

Los modelos de lenguaje grandes (LLMs) han remodelado significativamente el panorama de la Inteligencia Artificial (I...

Inteligencia Artificial

Este documento de IA muestra cómo la toxicidad de ChatGPT puede aumentar hasta seis veces cuando se le asigna una personalidad

Con los avances tecnológicos recientes, los modelos de lenguaje grandes (LLMs) como GPT-3 y PaLM han mostrado habilid...

Aprendizaje Automático

SiMa.ai traerá el chip de inteligencia artificial más poderoso del mundo a la India.

En un emocionante avance, SiMa.ai, una startup estadounidense de chips de inteligencia artificial, ha anunciado la pr...

Ciencias de la Computación

Sitios web basura llenos de texto generado por inteligencia artificial están generando dinero a través de anuncios programáticos.

Más de 140 marcas están anunciando en sitios web de granjas de contenido de baja calidad, y el problema está creciend...

Inteligencia Artificial

Descifrando los misterios de los modelos de lenguaje grandes un análisis detallado de las funciones de influencia y su escalabilidad

Los modelos de lenguaje grandes (LLMs) han acelerado el desarrollo en varios campos del mundo real y han demostrado h...