Chat de Towards Data Science: Creación de un chatbot con Zilliz Cloud
Este artículo es la primera parte de la serie de blogs Chat Towards Data Science.
En la era de la IA generativa, los desarrolladores aprovechan los modelos de lenguaje grandes (LLMs) para crear aplicaciones más inteligentes. Sin embargo, los LLMs son propensos a alucinar debido a su conocimiento limitado. Retrieval Augmented Generation (RAG) aborda eficazmente este problema al complementar los LLMs con conocimiento externo. En mi serie de blogs Chat Towards Data Science, te guiaré en la creación de un chatbot basado en RAG utilizando tu conjunto de datos como columna vertebral del conocimiento.
En la primera parte de mi serie de blogs, te guiaremos en la creación de un chatbot para el sitio web Towards Data Science, utilizando web scraping para crear una base de conocimiento almacenada en Zilliz Cloud, un servicio de base de datos vectorial totalmente gestionado construido sobre Milvus.
Extracción de datos web con BeautifulSoup4
El primer paso en cualquier proyecto de aprendizaje automático (ML) es recopilar los datos necesarios. Para este proyecto, empleamos técnicas de web scraping para recopilar datos para nuestra base de conocimiento. Utilizamos la biblioteca requests para obtener páginas web y luego usamos BeautifulSoup4 (una biblioteca que extrae información de páginas web) para analizar la información HTML y extraer los párrafos.
Preparación de BeautifulSoup4 y Requests para web scraping
Para empezar, instala BeautifulSoup ejecutando pip install beautifulsoup4 sentence-transformers. Solo necesitamos dos importaciones para esta sección: requests y BeautifulSoup. A continuación, creamos un diccionario de URLs que queremos extraer. En este ejemplo, solo extraemos contenido de Towards Data Science, pero también puedes extraer contenido de otros sitios web. Recuperamos datos de cada página de archivo utilizando el formato que se muestra en el código a continuación.
import requests
from bs4 import BeautifulSoup
urls = {
'Towards Data Science': 'https://towardsdatascience.com/archive/{0}/{1:02d}/{2:02d}',
}
También necesitamos dos funciones auxiliares para nuestro web scraping. La primera función convierte el número de días del año a un formato de mes y día. La segunda recupera el número de aplausos (los aplausos muestran apoyo a un artículo de Medium) de un artículo.
La función de conversión de días es relativamente sencilla. Codificamos de forma fija el número de días de cada mes y usamos esa lista para realizar la conversión. Dado que este proyecto de web scraping apunta explícitamente al año 2023, no necesitamos tener en cuenta los años bisiestos. Si lo prefieres, puedes modificarla para diferentes años.
La función de conteo de aplausos representa los aplausos de un artículo de Medium. Algunos artículos pueden recibir miles de aplausos, que Medium denota con la letra "K." Por lo tanto, necesitamos considerar esta representación en nuestra función.
# takes the number day of the year and returns month, day
def convert_day(day):
month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
m = 0
d = 0
while day > 0:
d = day
day -= month_days[m]
m += 1
return (m, d)
# converts the claps string
def get_claps(claps_str):
if (claps_str is None) or (claps_str == '') or (claps_str.split is None):
return 0
split = claps_str.split('K')
claps = float(split[0])
claps = int(claps*1000) if len(split) == 2 else int(claps)
return claps
Análisis de la respuesta de web scraping de BeautifulSoup4
Ahora que hemos configurado los componentes necesarios, podemos proceder con el web scraping. En esta sección, importamos una biblioteca adicional llamada "time.” Añadí esta biblioteca porque encontré errores 429 (demasiadas solicitudes) durante el proceso. Usando la biblioteca time, podemos introducir un retraso entre el envío de solicitudes. Además, utilizamos la biblioteca sentence transformers para obtener nuestro modelo de embeddings, específicamente un modelo MiniLM, de Hugging Face.
Como se mencionó anteriormente, solo estamos extrayendo datos de 2023, por lo que establecemos el año en 2023. Además, solo necesitamos datos desde el día 1 (1 de enero) hasta el día 243 (30 de agosto). En el momento de la extracción, era principios de septiembre. En retrospectiva, habría sido mejor extraer hasta el día 244, pero por ahora procederemos con lo que ya he hecho. En la primera parte de nuestra función, hasta la primera llamada a time.sleep(), configuramos los componentes necesarios. Convertimos el iterador en un mes y un día, convertimos eso en una cadena de fecha y luego recuperamos la respuesta HTML desde la URL del archivo.
Después de obtener el HTML, lo analizamos usando BeautifulSoup y buscamos elementos div con un nombre de clase específico (indicado en el código), lo que indica que se trata de un artículo. A partir de ahí, analizamos el título, el subtítulo, la URL del artículo, el número de aplausos, el tiempo de lectura y el número de respuestas. Posteriormente, usamos requests nuevamente para recuperar el artículo desde la URL del artículo.
Llamamos a time.sleep() por segunda vez para concluir la segunda sección de este bucle for. En este punto, hemos obtenido la mayor parte de los metadatos requeridos para cada artículo. Extraemos cada párrafo y su embedding correspondiente usando nuestro modelo de Hugging Face. Luego creamos un diccionario que contiene todos los metadatos para ese párrafo y artículo específicos.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L12-v2")
import time
# data_batch = []
year = 2023
for i in range(1, 243):
month, day = convert_day(i)
date = '{0}-{1:02d}-{2:02d}'.format(year, month, day)
for publication, url in urls.items():
response = requests.get(url.format(year, month, day), allow_redirects=True)
if not response.url.startswith(url.format(year, month, day)):
continue
time.sleep(8)
page = response.content
soup = BeautifulSoup(page, 'html.parser')
articles = soup.find_all(
"div",
class_="postArticle postArticle--short js-postArticle js-trackPostPresentation js-trackPostScrolls")
for article in articles:
title = article.find("h3", class_="graf--title")
# print(title.contents)
if title is None:
continue
title = str(title.contents[0]).replace(u'\xA0', u' ').replace(u'\u200a', u' ')
# title = title.contents[0]
subtitle = article.find("h4", class_="graf--subtitle")
subtitle = str(subtitle.contents[0]).replace(u'\xA0', u' ').replace(u'\u200a', u' ') if subtitle is not None else ''
article_url = article.find_all("a")[3]['href'].split('?')[0]
try:
claps = get_claps(article.find_all("button")[1].contents[0])
except:
claps = 0
reading_time = article.find("span", class_="readingTime")
reading_time = 0 if reading_time is None else int(reading_time['title'].split(' ')[0])
responses = article.find_all("a")
if len(responses) == 7:
responses = responses[6].contents[0].split(' ')
if len(responses) == 0:
responses = 0
else:
responses = responses[0]
else:
responses = 0
article_res = requests.get(article_url)
time.sleep(8)
soup = bs4.BeautifulSoup(article_res.text)
paragraphs = soup.select('[class*="pw-post-body-paragraph"]')
for i, paragraph in enumerate(paragraphs):
embedding = model.encode(paragraph.text)
data_batch.append({
"_id": f"{article_url}+{i}",
"article_url": article_url,
"title": title,
"subtitle": subtitle,
"claps": claps,
"responses": responses,
"reading_time": reading_time,
"publication": publication,
"date": date,
"paragraph": paragraph.text,
"embedding": embedding
})
El último paso es serializar nuestro archivo con pickle.
import pickle
filename="TDS_8_30_2023"
with open(f'{filename}.pkl', 'wb') as f:
pickle.dump(data_batch, f)
¿Cómo se ven nuestros datos?
Siempre es útil visualizar los datos. A continuación se muestra cómo se ven los datos en Zilliz Cloud. Presta atención a los embeddings, que representan el vector. Generamos estos embeddings vectoriales a partir del texto en la siguiente sección.
Ingerir datos de TDS en tu base de datos vectorial
Una vez que tenemos los datos, el siguiente paso es canalizarlos a una base de datos vectorial. En este proyecto, utilicé un cuaderno separado para ingerir los datos en Zilliz Cloud, un servicio de base de datos vectorial totalmente gestionado construido sobre Milvus, en lugar de extraerlos de Towards Data Science.
Para insertar nuestros datos en Zilliz Cloud, seguimos estos pasos:
Conectarse a Zilliz Cloud.
Definir los parámetros de nuestra colección.
Insertar los datos en Zilliz Cloud.
Configuración del Jupyter Notebook
Ejecuta el comando pip install pymilvus python-dotenv para configurar nuestro Jupyter Notebook e iniciar el proceso de ingestión de datos. Uso la biblioteca dotenv para gestionar mis variables de entorno como de costumbre. Para el paquete pymilvus, necesitamos importar los siguientes módulos:
utility- para comprobar el estado de tus coleccionesconnections- para conectarte a tu instancia de MilvusFieldSchema- para definir el esquema de un campoCollectionSchema- para definir el esquema de una colecciónDataType- tipos de datos almacenados en un campoCollection- la forma en que accedemos a una colección
Luego, abrimos los datos que previamente serializamos con pickle, recuperamos nuestras variables de entorno y nos conectamos a Zilliz Cloud.
import pickle
import os
from dotenv import load_dotenv
from pymilvus import utility, connections, FieldSchema, CollectionSchema, DataType, Collection
filename="TDS_8_30_2023"
with open(f'{filename}.pkl', 'rb') as f:
data_batch = pickle.load(f)
load_dotenv()
zilliz_uri = os.getenv("ZILLIZ_URI")
zilliz_token = os.getenv("ZILLIZ_TOKEN")
connections.connect(
uri= zilliz_uri,
token= zilliz_token
)
Configuración de tu base de datos vectorial Zilliz e ingestión de datos
Ahora, necesitamos configurar Zilliz Cloud. Debemos crear una colección para almacenar y organizar los datos que extraemos del sitio web de TDS. Se requieren dos constantes: dimensión y nombre de la colección. La dimensión se refiere al número de dimensiones que tiene nuestro vector. En este caso, usamos el modelo MiniLM con 384 dimensiones. El nombre de la colección se explica por sí solo.
Con la nueva función de esquema dinámico de Milvus, podemos definir simplemente los campos id y embedding para una colección sin preocuparnos por el número de otros campos almacenados ni por su tipo de dato requerido. Sin embargo, es vital recordar los nombres específicos de los campos que conservamos para recuperarlos correctamente.
Para el ejemplo de este proyecto, usamos la función de esquema dinámico para definir solo los campos ID y embedding. Luego asignamos estos campos a un esquema y el esquema a una colección. A continuación, especificamos los parámetros del índice. En este caso, usamos un índice de archivo invertido (IVF) sin cuantización (FLAT), una distancia euclidiana (L2) y 128 centroides para el índice.
IVF sin cuantización es el método de indexación más intuitivo. Esencialmente, estamos agrupando los vectores en 128 secciones para consultar. 128 es un número arbitrario en este caso. Es un hiperparámetro que podemos ajustar dependiendo de cómo se vean nuestros resultados. Heurísticamente, es un buen punto de partida porque puede proporcionar una buena diversidad de clústeres, así como reducir la complejidad inicial de la consulta. Una vez que hemos definido estos parámetros, creamos el índice en el campo embedding y cargamos la colección en memoria.
DIMENSION=384
COLLECTION_NAME="tds_articles"
fields = [
FieldSchema(name='id', dtype=DataType.VARCHAR, max_length=200, is_primary=True),
FieldSchema(name='embedding', dtype=DataType.FLOAT_VECTOR, dim=DIMENSION)
]
schema = CollectionSchema(fields=fields, enable_dynamic_field=True)
collection = Collection(name=COLLECTION_NAME, schema=schema)
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "L2",
"params": {"nlist": 128},
}
collection.create_index(field_name="embedding", index_params=index_params)
collection.load()
Una vez que hemos definido la colección y la hemos cargado en memoria, tenemos dos opciones para insertar los datos:
Recorrer el lote de datos e insertar cada elemento individualmente
Insertar los datos por lotes
Después de insertar todos los datos, es importante vaciar la colección para indexarla y garantizar la consistencia. Ingerir grandes cantidades de datos puede llevar algo de tiempo.
for data in data_batch:
collection.insert([data])
collection.flush()
Consultar segmentos de artículos de TDS
Ahora, todo está configurado y listo para realizar consultas. En esta publicación, mantendremos esta parte sencilla vectorizando nuestra consulta y buscando la coincidencia más cercana en Zilliz Cloud. En mis próximas publicaciones, también usaremos LlamaIndex y LangChain.
Obtener tu modelo de HuggingFace y configurarlo para consultar Zilliz
Debes obtener el modelo de embeddings y configurar la base de datos vectorial para consultar nuestra base de conocimiento de Towards Data Science. Este paso requiere ejecutar un notebook independiente. Usaré la biblioteca dotenv para gestionar las variables de entorno. Además, necesitamos usar el modelo MiniLM de Sentence Transformers. Para este paso, puedes reutilizar el código proporcionado en la sección Web Scraping.
import os
from dotenv import load_dotenv
from pymilvus import connections, Collection
load_dotenv()
zilliz_uri = os.getenv("ZILLIZ_URI")
zilliz_token = os.getenv("ZILLIZ_TOKEN")
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L12-v2")
Ejecutar una consulta de búsqueda vectorial
Ahora, necesitas conectarte a tu base de datos vectorial y realizar una búsqueda. En este proyecto, nos conectaremos a una instancia de Zilliz Cloud y recuperaremos la colección que creamos anteriormente, tds_articles. Para obtener nuestra consulta, le pedimos al usuario que introduzca su pregunta sobre TDS hasta septiembre de 2023.
A continuación, usamos el modelo de embeddings de Hugging Face para codificar la consulta. Este proceso convierte la pregunta del usuario en una representación vectorial de 384 dimensiones. Luego usamos esta consulta codificada para buscar en la base de datos vectorial. Durante la búsqueda, necesitamos especificar el campo de vecinos más cercanos aproximados (anns_field), los parámetros del índice, el límite deseado para el número de resultados de búsqueda y los campos de salida que queremos.
Anteriormente, utilizamos la función de esquema dinámico de Milvus para optimizar la definición del esquema de campos. Ahora, al buscar en la base de datos vectorial, es esencial incluir los campos dinámicos deseados en los resultados de búsqueda. Este escenario específico implica solicitar el campo paragraph, que contiene el texto de cada párrafo en los artículos.
connections.connect(uri=zilliz_uri, token=zilliz_token)
collection = Collection(name="tds_articles")
query = input("What would you like to ask Towards Data Science's 2023 publications up to September? ")
embedding = model.encode(query)
closest = collection.search([embedding],
anns_field='embedding',
param={"metric_type": "L2",
"params": {"nprobe": 16}},
limit=2,
output_fields=["paragraph"])
print(closest[0][0])
print(closest[0][1])
Le pedí a la aplicación que proporcionara información sobre los modelos de lenguaje grandes, y devolvió las dos respuestas siguientes. Aunque estas respuestas mencionan "modelos de lenguaje" e incluyen alguna información relacionada, no proporcionan una explicación detallada de los modelos de lenguaje grandes. La segunda respuesta es semánticamente similar, pero debe estar lo suficientemente cerca de lo que solicitamos.
Añadir a una base de conocimiento de base de datos vectorial
Hasta ahora, hemos creado una base de conocimiento sobre artículos de TDS usando Zilliz como nuestra base de datos vectorial. Aunque hemos podido recuperar fácilmente resultados de búsqueda semánticamente similares, solo a veces son lo que necesitamos. El siguiente paso es mejorar nuestros resultados incorporando nuevos marcos y tecnologías.
En la próxima sección, exploraremos la incorporación de LlamaIndex para enrutar nuestros resultados, de forma similar al enrutamiento que realizamos en mi blog anterior sobre Consultar varios documentos usando LlamaIndex, LangChain y Milvus.
Resumen
Este tutorial cubre la creación de un chatbot para la publicación Towards Data Science. Ilustramos el proceso de web scraping para crear y almacenar una base de conocimiento en una base de datos vectorial (específicamente, Zilliz Cloud). Luego demostramos cómo solicitar al usuario una consulta, vectorizar la consulta y consultar la base de datos vectorial.
Sin embargo, descubrimos que, aunque los resultados son semánticamente similares, no son exactamente lo que deseamos. En la siguiente parte de esta serie de blogs, exploraremos el uso de LlamaIndex para enrutar consultas y ver si podemos lograr mejores resultados. Además de los pasos discutidos aquí, también puedes experimentar con Zilliz Cloud reemplazando el modelo, combinando textos o usando un conjunto de datos diferente.
Para leer nuevamente toda la serie Chat Towards Data Science, aquí están los enlaces:
Sigue leyendo

8 Latest RAG Advancements Every Developer Should Know
Explore eight advanced RAG variants that can solve real problems you might be facing: slow retrieval, poor context understanding, multimodal data handling, and resource optimization.

Zilliz Cloud Introduces Advanced BYOC-I Solution for Ultimate Enterprise Data Sovereignty
Explore Zilliz Cloud BYOC-I, the solution that balances AI innovation with data control, enabling secure deployments in finance, healthcare, and education sectors.

Proactive Monitoring for Vector Database: Zilliz Cloud Integrates with Datadog
we're excited to announce Zilliz Cloud's integration with Datadog, enabling comprehensive monitoring and observability for your vectorDB deployments.



