====== 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)