====== Evaluación ====== Es importante saber cómo evaluar una aplicación con modelos LLM, para poder medir de algún modo con qué modelo funciona mejor, o con que //retriever//, o cualquier otro cambio. \\ Estas aplicaciones que usan LLM, en realidad son "chains" con concatenaciones de éstos, y es posible usar a su vez modelos LLM para evaluar nuestra aplicación. \\ ===== Evaluación manual ===== Consiste en crear por mi parte unos ejemplos de pregunta - respuesta, y después hago que el modelo cree otros ejemplos de forma automática. \\ La idea es que el modelo responda a estas preguntas y ver si su respuesta es correcta: en las pruebas que he hecho deben ser igual que las mías, y en las generadas de forma automática, deben ser correctas.\\ Partimos del ejempo en el que obtenemos los datos de un CSV: # Ejemplo en el que cargaremos información de un documento CSV y le preguntaremos al modelo # cuestiones acerca de esta información from langchain_ollama import ChatOllama from langchain_ollama import OllamaEmbeddings # Modelo embedding para crear las representaciones numéricas de los datos. from langchain.chains.retrieval_qa.base import RetrievalQA # Sirve para recuperar información de documentos from langchain_community.document_loaders import CSVLoader # Para cargar informaciónd de documentos CSV from langchain_community.vectorstores import DocArrayInMemorySearch # Forma de guardar la información en un "vector store" ó almacén de datos. Muy sencilla y no requiere conexión a base de datos from langchain.indexes import VectorstoreIndexCreator # Para crear un índice que nos ayudará a crear un almacén de datos de forma fácil import pprint # Cargamos el documento CSV file = 'profesores.csv' loader = CSVLoader(file_path=file, encoding="utf-8") data = loader.load() # Especificamos el modelo que generará los "embeddings" embeddings = OllamaEmbeddings (model="llama3.2") # Creamos el índice especificando su clase (tipo de almacén de datos) y la lista de "loaders" de donde extraer la información, que en nuestro caso sólo hay uno index = VectorstoreIndexCreator (vectorstore_cls=DocArrayInMemorySearch, embedding=embeddings).from_loaders([loader]) # Definimos el modelo con el que vamos a trabajar llm = ChatOllama( model = "llama3.2", temperature = 0.3, verbose = True ) # Definimos la interfaz con el vectorstore qa = RetrievalQA.from_chain_type( llm = llm, chain_type="stuff", retriever=index.vectorstore.as_retriever(), verbose=True, chain_type_kwargs={ "document_separator": "<<<<>>>>", } ) A continuación realizamos la comprobación "manual", para lo cual: - Generamos nuestros propios ejemplos. - Usamos el chain **QAGenerateChain** que genera ejemplos de pregunta - respuesta a partir del LLM indicado. - Añadimos a nuestros ejemplos hechos de forma manual, los generados automáticamente. - A partir del retriever (RetrievarQA), ejecutamos las consultas de todos los ejemplos. - Mostramos las respuestas para comprobar si el modelo a resuelto las consultas correctamente, como era esperado. # Evaluación manual: Creo unos ejemplos de pregunta - respuesta y después creo otros de forma automática con LLM. # Hago que el modelo responda a la misma pregunta (mía o de las que ha creado él) para ver directamente si acierta o no. ejemplos = [ { "query": "¿Qué clase impartió Ana María de Santiago Nocito \ el 2/15/2024?", "answer": "RCP básica con DESA y fármacos." }, { "query": "¿Cuántas clases ha impartido en total Luis España Barrio?", "answer": "Luis España Barrio ha impartido 11 clases." } ] from langchain.evaluation.qa import QAGenerateChain import langchain langchain.debug = True chain_generador_ejemplos = QAGenerateChain.from_llm(llm) nuevos_ejemplos = chain_generador_ejemplos.apply_and_parse( [{"doc": t} for t in data[:5]] ) ejemplos += nuevos_ejemplos pprint.pprint(ejemplos[0]) #respuesta=qa.run(ejemplos[0]["qa_pairs"]["query"]) respuesta=qa.run(ejemplos[0]["query"]) langchain.debug = False pprint.pprint(respuesta) ===== Evaluación automática ===== Podemos programar la automatización de la comprobación de las respuestas. \\ Partimos de nuevo de la carga del documento CSV y la creación del vectorstore y el //retrieval//: # Ejemplo en el que cargaremos información de un documento CSV y le preguntaremos al modelo # cuestiones acerca de esta información from langchain_ollama import ChatOllama from langchain_ollama import OllamaEmbeddings # Modelo embedding para crear las representaciones numéricas de los datos. from langchain.chains.retrieval_qa.base import RetrievalQA # Sirve para recuperar información de documentos from langchain_community.document_loaders import CSVLoader # Para cargar informaciónd de documentos CSV from langchain_community.vectorstores import DocArrayInMemorySearch # Forma de guardar la información en un "vector store" ó almacén de datos. Muy sencilla y no requiere conexión a base de datos from langchain.indexes import VectorstoreIndexCreator # Para crear un índice que nos ayudará a crear un almacén de datos de forma fácil import pprint # Cargamos el documento CSV file = 'profesores.csv' loader = CSVLoader(file_path=file, encoding="utf-8") data = loader.load() # Especificamos el modelo que generará los "embeddings" embeddings = OllamaEmbeddings (model="llama3.2") # Creamos el índice especificando su clase (tipo de almacén de datos) y la lista de "loaders" de donde extraer la información, que en nuestro caso sólo hay uno index = VectorstoreIndexCreator (vectorstore_cls=DocArrayInMemorySearch, embedding=embeddings).from_loaders([loader]) # Definimos el modelo con el que vamos a trabajar llm = ChatOllama( model = "llama3.2", temperature = 0.3, verbose = True ) # Definimos la interfaz con el vectorstore qa = RetrievalQA.from_chain_type( llm = llm, chain_type="stuff", retriever=index.vectorstore.as_retriever(), verbose=True, chain_type_kwargs={ "document_separator": "<<<<>>>>", } ) Ahora vamos a realizar la comprobación automática del software: - En primer lugar volveremos a generar nuestros propios ejemplos. - Volvemos a generar de forma automática ejemplos pregunta-respuesta con QAGenerateChain - Ahora deberíamos juntar todos los ejemplos, como necesitamos que tengan un formato concreto, lo ajustamos. - A partir del retriever, usamos la función **apply()**, que ejecuta las consultas ('query') que le pasamos como argumento, de forma que devuelve por cada una un diccionario con 3 elementos: * query: consulta realizada. * answer: respuesta que se generó anteriormente, o que hicimos nosotros de forma manual. * result: respuesta gnerada ahora por el modelo especificado. - Ahora con la función **evaluate()** del chain **QAEvalChain** le metemos los datos obtenidos, el LLM especificado compara los //answer// con los //result//, determinando que la respuesta es correcta //correct// si son los mismos, o incorrecta //incorrect// si son diferentes. # Evaluación automática: Creo unos ejemplos de pregunta - respuesta y después creo otros de forma automática con LLM. # El modelo responda a la mismas preguntas (mías o de las que ha creado él) y deberá clasificar las respuestas en correctas o incorrectas. # Creo mis propios ejemlos de forma manual ejemplos = [ { "query": "¿Qué clase impartió Ana María de Santiago Nocito el 2/15/2024?", "answer": "RCP básica con DESA y fármacos." }, { "query": "¿Cuántas clases ha impartido en total Luis España Barrio?", "answer": "Luis España Barrio ha impartido 11 clases." } ] # Dependencias necesarias para la evaluación from langchain.evaluation.qa import QAGenerateChain # Generación automática de pares pregunta-respuesta from langchain.evaluation.qa import QAEvalChain # Evaluación automática de respuestas #Definimos el chain QAGenerateChain y lo usamos para definir pares pregunta-respuestas de forma automática chain_generador_ejemplos = QAGenerateChain.from_llm(llm) nuevos_ejemplos = chain_generador_ejemplos.apply_and_parse( [{"doc": t} for t in data[:5]] ) # Junto todos los ejemplos, ajustándolos a la forma query:answer, que es el formato necesario para usar después la función "apply" ejemplos = ejemplos + [ { "query": ejemplo['qa_pairs']["query"], "answer": ejemplo['qa_pairs']["answer"] } for ejemplo in nuevos_ejemplos ] # Ejecuto las consultas de los ejemplos y obtnego: la consulta (query), la respuesta del ejemlop, ya sea la mía o la que generó el modelo (answwer) y la que ha generado ahora (result) predicciones = qa.apply(ejemplos) # Evaluamos las respuestas, 'evaluate' compara a través del LLM especificado la respuesta del ejemlo y la obtenida posteriormente y determina si son 'correct' o 'incorrect' eval_chain = QAEvalChain.from_llm(llm) salidas_clasificadas = eval_chain.evaluate(ejemplos, predicciones) # Mostramos las conclusiones de la evaluación automática for i, eg in enumerate (ejemplos): print(f"Ejemplo {i}: ") print("Question: " + predicciones[i]['query']) print("Real Answer: " + predicciones[i]['answer']) print("Predicted Answer: " + predicciones[i]['result']) print("Clasificación de predicción: " + salidas_clasificadas[i]['results']) print() print(salidas_clasificadas[0]) En nuestro caso es siempre incorrecto, por el problema que arrastramos cuando vimos los chains QA para preguntar acerca del contenido de documentos: Como cargamos los datos de un CSV, los registros se pasan como "documentos" o "porciones de información" independientes, lo que hace que los pares preguntas-respuesta generadas sean de registros concretos. El problema es que cuando el modelo es consultado, el retriever no funciona correctamente y no recupera los datos correctos. Puede deverse a múltiples factores relacionados con la forma en que funciona el retriever, pero eso ahora mismo excede el objetivo de este texto formativo. * usaremos el chain **QAEvalChain** ===== Depuración ===== También es importante saber que hay varias maneras de ver qué está pasando "tras bambalinas", a la hora de depurar. \\ Esto se puede hacer de dos Maneras: * **verbose = true**, como parámetro del chain, de modo que se podrá observar qué ocurre "dentro" de ese chain cuando se ejecuta. # Anteriormente se define qa como un QAChain y data como el contenido de un loaderCSV from langchain.evaluation.qa import QAGenerateChain chain_generador_ejemplos = QAGenerateChain.from_llm(llm, verbose=True) # Indicamos que muestre lo que ocurre "dentro" de este chain ejemplos = chain_generador_ejemplos.apply_and_parse( [{"doc": t} for t in data[:5]] ) pprint.pprint(ejemplos[0]) respuesta=qa.run(ejemplos[0]["qa_pairs"]["query"]) pprint.pprint(respuesta) * **Debug Mode**: importamos directamente langchain, para poder activar (y desactivar) el modo depuración, que provocará que se muestre TODO lo que ocurre, cada vez que se llama a una función de langchain. # Anteriormente se define qa como un QAChain y data como el contenido de un loaderCSV from langchain.evaluation.qa import QAGenerateChain import langchain # Dependencia necesaria para entrar en el modo 'debug' langchain.debug = True chain_generador_ejemplos = QAGenerateChain.from_llm(llm) ejemplos = chain_generador_ejemplos.apply_and_parse( [{"doc": t} for t in data[:5]] ) pprint.pprint(ejemplos[0]) respuesta=qa.run(ejemplos[0]["qa_pairs"]["query"]) langchain.debug = False pprint.pprint(respuesta)