No se han encontrado widgets en la barra lateral

En el artículo anterior explicaba cómo usar una base de datos vectorial, como ChromaDB, para almacenar información y usarla para crear un prompt enriquecido para interrogar Grandes Modelos de Lenguaje de Hugging Face.

En este vamos a ver como usar LangChain para realizar la misma acción, pero sin tener que crear el prompt enriquecido. Será LangChain el responsable de buscar entre nuestra información almacenada en ChromaDB, para pasársela al Modelo de lenguaje utilizado.

De esta forma conseguimos usar nuestros datos con Modelos de Lenguaje sin tener la necesidad de realizar un fine tunning del modelo. Como usaremos modelos de Hugging Face, que se pueden descargar y ofrecer desde servidores propios, o espacios de cloud privados, la información no tiene que pasar por empresas como OpenAI.

Vemos los pasos que seguiremos en este artículo:

  • Instalar las librerías necesarias, como chromadb o langchain.
  • Cargar el dataset y crear un documento en LangChain usando uno de sus document_loaders.
  • Crear los embeddings para guardarlos en la base de datos.
  • Crear un índice con la información.
  • Crear un retriever con el índice que será usado por LangChain para obtener la información.
  • Cargar el modelo de Hugging Face.
  • Crear un pipeline de LangChain usando el modelo de lenguaje y el retriever.
  • Usar el pipeline para realizar las preguntas.

¿Qué tecnologías vamos a usar?

La base de datos de Vectores es ChromaDB. Posiblemente, la opción más conocida entres las bases de datos de vectores de Código abierto.

Para los modelos se ha escogido el universo de Hugging Face. He usado dos modelos, dolly-v2-3b y flan-t5-large. A destacar que no tan solo són dos modelos diferentes, sino que han sido entrenados para funciones diferentes.

T5 es un modelo text2text-generation, estos modelos están pensados para generar texto, pero basados en los Datasets con los que han sido entrenados. Pueden usarse para generación de texto, pero sus respuestas no son muy creativas.

En cambio Dolly es un modelo puro de text-generation, estos modelos suelen generar respuestas más creativas y extensas.

La librería estrella es LangChain, una plataforma de código abierto que permite crear aplicaciones de lenguaje natural aprovechando los grandes modelos de lenguaje. Nos permite encadenar entradas y salidas entre estos modelos y otras tecnologías como bases de datos o diferentes pluguins.

Empezamos nuestro proyecto con LangChain.

El código está disponible en un notebook en Kaggle. Este artículo y notebook forman parte de un curso de creación de aplicaciones con grandes modelos de lenguaje, disponible en mi perfil de github. Si no os queréis perder ninguna lección o modificación de las existentes, lo mejor es seguir el repositorio, ya que en el público las diferentes lecciones a medida que las voy finalizando.

Notebook en Kaggle: https://www.kaggle.com/code/peremartramanonellas/ask-your-documents-with-langchain-vectordb-hf

Repositorio Github con el curso de Grandes Modelos de Lenguajes: https://github.com/peremartra/Large-Language-Model-Notebooks-Course

Instalar y cargar las librerias.

Si estás trabajando en tu entorno particular y ya has estado haciendo pruebas con estas tecnologías, posiblemente no necesites instalar nada. Pero si lo hace en Kaggle o Colab vas a necesitar instalar las siguientes librerías:

  • langchain: La revolucionaria librería que permite crear aplicaciones con los grandes modelos de lenguaje.
  • sentence_transformers: vamos a tener que generar embeddings del texto que queremos guardar en la base de datos de vectores, para ello necesitamos esta librería.
  • chromadb: la base de datos de vectores a usar. A destacar la sencillez de uso de ChromaDB.
!pip install chromadb
!pip install langchain
!pip install sentence_transformers

Aparte de estas librerías, vamos a importar las dos librerías de python más usadas en ciencia de datos, pandas y numpy.

import numpy as np 
import pandas as pd

Cargar Los Datasets.

Tal como he dicho anteriormente el notebook lo he preparado para que funcione con dos datasets diferentes. Son dos de los datasets ya usados en el ejemplo anterior de RAG (Retrieval Augmented Generation), es decir, usar los datos propios con grandes modelos de lenguaje, lo que se ha venido llamando: Interrogar a tus documentos.

Los dos son datasets tabulares, que contienen información de noticias:

https://www.kaggle.com/datasets/kotartemiy/topic-labeled-news-dataset

https://www.kaggle.com/datasets/deepanshudalal09/mit-ai-news-published-till-2023

El contenido de ambos Datasets es similar, pero los nombres de las columnas son diferentes, y la información almacenada también. Personalmente, creo que las cosas quedan más claras cuando usas más de un Dataset. La opción buena sería que al finalizar vuestro notebook busquéis un Dataset diferente y repliquéis el funcionamiento con él.

Como estamos trabajando en Kaggle los recursos son limitados, por lo que no vamos a trabajar con el Dataset entero, ya que podríamos exceder el límite de memoria que nos ofrece Kaggle.

topiclinkdomainpublished_datetitle
0SCIENCEhttps://www.eurekalert.org/pub_releases/2020-0…eurekalert.org2020-08-06 13:59:45A closer look at water-splitting’s solar fuel …en
1SCIENCEhttps://www.pulse.ng/news/world/an-irresistibl…pulse.ng2020-08-12 15:14:19An irresistible scent makes locusts swarm, stu…en
Veamos los dos primeros registros del dataset: topic-labeled-news-dataset.

En el primer Dataset la columna que vamos a usar como document es title. No tiene textos de una longitud muy grande, pero como ejemplo nos sirve perfectamente. Podríamos usarlo para buscar entre la base de datos de nuestros artículos, para encontrar los que hablen de un tema en concreto.

news = pd.read_csv('/kaggle/input/topic-labeled-news-dataset/labelled_newscatcher_dataset.csv', sep=';')
MAX_NEWS = 1000
DOCUMENT="title"
TOPIC="topic"

#news = pd.read_csv('/kaggle/input/bbc-news/bbc_news.csv')
#MAX_NEWS = 500
#DOCUMENT="description"
#TOPIC="title"

#Because it is just a course we select a small portion of News.
subset_news = news.head(MAX_NEWS)

Hemos creado el DataFrame subset_news que contiene una parte de las noticias del Dataset. Para usar un Dataset u otro es tan sencillo como quitar los comentarios de las líneas del dataset que queremos usar. En cada uno he ajustado el nombre de la columna a usar como datos y el número de registros que contendrá el subconjunto de registros a usar.

Generar el Documento desde el Dataframe.

Para crear el documento que usaremos con LangChain vamos a usar uno de sus loaders. Para nuestro ejemplo usaremos el DataFrameLoader, pero tenemos disponibles loaders para una gran variedad de fuentes, como podrían ser: CSV’s, ficheros de texto, html, json, pdf, incluso loaders de productos como Confluence.

from langchain.document_loaders import DataFrameLoader
from langchain.vectorstores import Chroma

Una vez ya tenemos la librería cargada tenemos que crear el loader. Para ello se le debe indicar el Data Frame y el nombre de la columna que queremos usar como contenido del documento. Esta información es la que vamos a pasar a la base de datos de vectores, ChromaDB, para que la almacene y pueda ser utilizada por el modelo de lenguaje al generar sus respuestas.

df_loader = DataFrameLoader(subset_news, page_content_column=DOCUMENT)

Para crear el documento tan solo tenemos que llamar a la función load del Loader.

df_document = df_loader.load()
display(df_document)

Veamos el contenido del documento:

[Document(page_content="A closer look at water-splitting's solar fuel potential", metadata={'topic': 'SCIENCE', 'link': 'https://www.eurekalert.org/pub_releases/2020-08/dbnl-acl080620.php', 'domain': 'eurekalert.org', 'published_date': '2020-08-06 13:59:45', 'lang': 'en'}),
 Document(page_content='An irresistible scent makes locusts swarm, study finds', metadata={'topic': 'SCIENCE', 'link': 'https://www.pulse.ng/news/world/an-irresistible-scent-makes-locusts-swarm-study-finds/jy784jw', 'domain': 'pulse.ng', 'published_date': '2020-08-12 15:14:19', 'lang': 'en'}),

Como vemos nos ha creado un documento donde cada página es el contenido de un registro de la columna que le hemos indicado. Pero también encontramos los otros datos en el campo metadata, con el nombre de la columna como etiqueta.

Os animo a probar con el otro Dataset y mirad cómo quedan los datos.

Crear los embeddings.

Primero, por si hace falta, veamos que es un Embedding. No es nada más que una representación numérica de cualquier dato. En nuestro caso en particular será la representación numérica del texto a almacenar.

Esta representación numérica toma la forma de vectores. Un vector no es nada más que la representación de un punto en un espacio multidimensional. Es decir, no tenemos por qué pintar el punto en un plano de dos dimensiones, o de tres dimensiones, que es a lo que estamos acostumbrados. El vector puede representar el punto en cualquier número de dimensiones.

Para nosotros es un concepto quizás complicado, o más bien difícil de imaginar, pero matemáticamente no hay una gran diferencia entre calcular la distancia entre dos puntos, ya estén dos dimensiones, tres o las que sean.

Estos vectores nos permiten calcular las diferencias o similitudes entre ellos, por lo que podemos buscar información parecida de una forma muy eficiente.

El truco está en saber qué vectores asignamos a cada palabra, porque lo que queremos es que las palabras con un significado parecido estén a menos distancia que aquellas que tienen un significado más diferente. De esto se encargan librerías de Hugging Face, por lo que no tenemos que preocuparnos demasiado, tan solo de usar siempre la misma conversión para todos los datos a almacenar, y las consultas a realizar.

Vamos a importar un par de librerias:

  • CharacterTextSplitter. Que la usaremos para agrupar la información en bloques.
  • HuggingFaceEmbeddings o SentenceTransformerEmbedding. En el notebook he usado las dos, y realmente no he encontrado ninguna diferencia. Estas librerías son las que se encargaran de recuperar el modelo que ejecutará el embedding de los datos.
from langchain.text_splitter import <a></a><a>CharacterTextSplitter
#from langchain.embeddings import HuggingFaceEmbeddings

No hay una forma 100 % correcta de dividir los documentos en bloques. Lo que tenemos que tener en cuenta es que cuanto mayor sea el bloque utilizado, más contexto recibirá el modelo. Como contrapartida, el tamaño de nuestro Vector Store aumenta, lo que no es bueno para la memoria.

Yo he decidido usar un tamaño de bloque de 250 caracteres con un overlap de 10. Es decir, que los 10 últimos caracteres de un bloque será los diez primeros del siguiente. Es un tamaño muy pequeño, pero para la tipología de nuestra información más que suficiente.

text_splitter = CharacterTextSplitter(chunk_size=250, chunk_overlap=10)
texts = text_splitter.split_documents(df_document)

Ahora ya podemos crear los embeddings con el texto.

from langchain.embeddings.sentence_transformer import SentenceTransformerEmbeddings
embedding_function = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")

#embedding_function = HuggingFaceEmbeddings(
#    model_name="sentence-transformers/all-MiniLM-L6-v2"
#)  

Como veis he estado usando SentenceTransformerEmbeddings en lugar de HuggingFaceembeddings, podéis cambiarlo fácilmente tan solo modificando la línea que está comentada.

Con las dos librerías se puede llamar al mismo modelo preentrenado de creación de embeddings, y estoy usando all-MiniLM-L6-v2 para las dos. Por lo que aunque posiblemente existan diferencias entre los embeddings generados por una librería y otra, estas diferencias serán mínimas y no afectarán al rendimiento.

SentenceTransformerEmbeddings en un principio está especializada en la transformación de sentencias, mientras que HuggingFaceEmbeddings es de carácter más general, pudiendo sacar embeddings de parrafos o documentos completos.

En todo caso, por la tipología de nuestros documentos, es normal que no exista casi diferencia al utilizar una librería u otra.

Con los embeddings generados podemos crear el índice.

chromadb_index = Chroma.from_documents(
    texts, embedding_function, persist_directory='./input'
)

¡Este índice es el que usaremos para realizar las preguntas, y está especialmente diseñado para ser altamente eficiente! Solo faltaría que después de toda esta historia lo hubieran diseñado para ser lento e impreciso.

Empezamos a usar LangChain.

Ahora toca la parte divertida, encadenar las acciones con LangChain para crear nuestra primera pequeña aplicación usando un modelo de lenguaje grande.

La aplicación es muy sencilla, va a constar tan solo de dos pasos y dos piezas. La primera va a ser un retriever. Es un componente que se usa para recuperar la información de documentos, o del texto que le hayamos facilitado nosotros como documento. En nuestro caso, va a realizar una búsqueda por similitud basada en embeddings para recuperar la información relevante para la consulta del usuario, entre la que hemos almacenado en ChromaDB.

El segundo y último paso va a ser nuestro modelo de lenguaje. Que recibirá la información devuelta por el retriever.

Por lo tanto, necesitamos importar las librerías para crear el retriever y el pipeline.

from langchain.chains import RetrievalQA
from langchain.llms import HuggingFacePipeline

Ahora ya podemos crear el retriever, usando el índice de embeddings que hemos creado anteriormente.

retriever = chromadb_index.as_retriever()

Ya tenemos el primer paso de nuestra cadena, o pipeline. Ahora toca el modelo. Para el notebook he usado dos modelos diferentes disponibles en Hugging Face.

El primero es dolly-v2-3b, el más pequeño de la familia Dolly. Es un modelo que a mí especialmente me gusta mucho. No está en la cresta de la ola, pero las respuestas obtenidas son mucho mejores que con GPT2 llegando a un nivel muy parecido al que se podría obtener con GPT-3.5 de OpenAI. Con 3b de parámetros está al límite de lo que podemos cargar en Kaggle sin que tengamos un problema de memoria. Este modelo está entrenado para text generation, con lo que produce unas respuestas elaboradas.

El segundo modelo es uno de la familia t5. Atención porque este modelo está preparado para text-2-text generation, por lo que sus respuestas son mucho más escuetas.

Aseguraos de, como mínimo, probar ambos.

model_id = "databricks/dolly-v2-3b" #my favourite textgeneration model for testing
task="text-generation"

#model_id = "google/flan-t5-large" #Nice text2text model
#task="text2text-generation"

¡Perfecto, ya lo tenemos todo para poder crear el pipeline! ¡Vamos a ello!

hf_llm = HuggingFacePipeline.from_model_id(
    model_id=model_id,
    task=task,
    model_kwargs={
        "temperature": 0,
        "max_length": 256
    },
)

Veamos que significa cada uno de los parámetros informados:

  • model_id: El identificador del modelo en Hugging Face. No hay más secreto, se obtiene desde Hugging Face y suele ser el nombre del modelo más versión.
  • task: Aquí especificamos la tarea para la que queremos usar el modelo. Hay modelos que están entrenados para más de una tarea. En la ficha del modelo de hugging_face podemos encontrar que tareas soporta el modelo.
  • model_kwargs. Nos permite indicar argumentos adicionales específicos del modelo. En este caso informo temperatura (cuán imaginativo queremos que sea el modelo) y la longitud máxima de la respuesta.

Ahora toca configurar la cadena usando modelo y retriever.

document_qa = RetrievalQA.from_chain_type(
    llm=hf_llm, chain_type="stuff", retriever=retriever
)

En la variable chain_type le indicamos como debe funcionar la cadena, tenemos cuatro opciones:

  • stuff. El más simple, tan solo coge los documentos que crre convenientes y los usa en el prompt a pasar al modelo.
  • refine. Va realizando diferentes llamadas al modelo con diferentes documentos, intentando obtener cada vez una respuesta más refinada. Puede ejecutar un número de llamadas elevado al modelo, por lo que se debe usar con cuidado.
  • map reduce. Trata de reducir todos los documentos en uno solo, puede hacer varias iteraciones. Puede comprimir, colapsar e intenta que quepan en el prompt a enviar al modelo.
  • map re-rank. Realiza la llamada al modelo para cada documento y los rankea, para acabar devolviendo la mejor. Como refine, es peligroso usarla dependiendo del número de llamadas que se prevean.

Ya está, ya podríamos usar la cadena recién creada para pasar nuestras preguntas, que se solucionarán teniendo en cuenta los datos de nuestro DataFrame, que ahora forman parte de una base de datos de Vectores.

#Sample question for newscatcher dataset. 
response = document_qa.run("Can I buy a Toshiba laptop?")

#Sample question for BBC Dataset. 
#response = document_qa.run("Who is going to meet boris johnson?")
display(response)

La respuesta obtenida dependerá, como esta claro, del Dataset usado, y tambíen del Modelo. Para la pregunta de si podemos comprarnos un Laptop de Toshiba, obtenemos dos respuestas muy diferentes dependiendo del modelo:

  • Dolly: No, Toshiba officially shuts down their laptops in 2023. The Legendary Toshiba is Officially Done With Making Laptops. Toshiba shuts the lid on laptops after 35 years. Toshiba officially shut down their laptops in 2023.
  • T5: No.

¡Como podéis ver, cada modelo le da su personalidad a la respuesta!

¡Conclusiones y continuar aprendiendo!

La verdad es que ha sido mucho más sencillo de lo que se podría esperar. Mucho más sencillo de lo que era antes de la explosión de los grandes modelos de lenguaje y de la aparición de herramientas, como LangChain, a su alrededor.

Hemos usado una Base de datos vectorial para guardar los datos que previamente hemos cargado en un DataFrame. Aunque podríamos haber usado cualquier otra fuente de datos. Los hemos usado como entrada de un par de modelos de lengua¡je que están disponibles en Hugging Face y hemos visto cómo los modelos nos devolvían una respuesta teniendo en cuenta la información del DataFrame.

Pero no paréis aquí, realizad vuestras propias modificaciones al notebook y solucionad los problemas que pueden ir saliendo. Algunas ideas son:

  • Usad los dos Datasets, y preferiblemente buscad un tercer Dataset. O incluso mejor, ¿no creéis que se puede adaptar para leer vuestro curriculum? Yo estoy seguro de que se puede conseguir con unas miniadaptaciones.
  • Usad un tercer modelo de Hugging Face.
  • Cambiad la fuente de los datos. Puede ser un fichero texto, un excel, o incluso alguna herramienta documental como Confluence.

Espero que os haya gustado. Este artículo forma parte de un curso de Como usar Grandes Modelos de Lenguaje que está disponible en mi perfil de GitHub. Mirad los otros artículos y notebooks y si os gusta dadle una estrella y seguidlo, así recibiréis un aviso cuando vaya publicando nuevas lecciones.

No hace falta mencionar que podéis abrir “issues” en GitHub si encontráis algo que no os gusta o no funciona, o realizar un fork… es un curso gratuito y open source que espero que vaya creciendo.

Como crear sentencias SQL usando lenguaje natural con la API de OpenAI.

En mi trabajo tengo relación con varios equipos de desarrollo, uno de ellos dedica una buena parte de su tiempo Read more

Fine-Tuning eficiente con LoRA. Entrena de forma óptima los Grandes Modelos de Lenguaje.

LoRA es una de las técnicas más eficientes y efectivas de Fine-Tuning aplicable a Grandes Modelos de Lenguaje que existe Read more

Preparando la certificación TensorFlow Developer by Google. 1-Image Clasification.

Recientemente he finalizado la especialización de Deeplearning.ai: TensorFlow Developer Professional Certificate. Lo primero es decir que recomiendo a cualquier persona Read more

Tutorial GAN-3. Cómo utilizar TPUs de Google para entrenar GANs en un abrir y cerrar de ojos.

Aprende a usar las TPU de Google Colab para acelerar el proceso de entreno de tus Notebooks.

Por Martra

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *