Data Science를 향한 채팅: Zilliz Cloud로 챗봇 구축하기
이 글은 Chat Towards Data Science 블로그 시리즈의 첫 번째 파트입니다.
생성형 AI 시대에 개발자들은 더 지능적인 애플리케이션을 위해 대규모 언어 모델(LLM)을 활용합니다. 하지만 LLM은 제한된 지식으로 인해 환각이 발생하기 쉽습니다. 검색 증강 생성(RAG)은 LLM에 외부 지식을 보완함으로써 이 문제를 효과적으로 해결합니다. 제 Chat Towards Data Science 블로그 시리즈에서는 여러분의 데이터셋을 지식 기반으로 사용해 RAG 기반 챗봇을 구축하는 과정을 안내하겠습니다.
제 블로그 시리즈의 첫 번째 파트에서는 웹 스크래핑을 활용해 웹사이트 Towards Data Science용 챗봇을 만드는 방법을 안내합니다. 이를 통해 Milvus 기반의 완전 관리형 벡터 데이터베이스 서비스인 Zilliz Cloud에 저장되는 지식 베이스를 생성합니다.
BeautifulSoup4로 웹 데이터 스크래핑하기
모든 머신러닝(ML) 프로젝트의 첫 번째 단계는 필요한 데이터를 수집하는 것입니다. 이 프로젝트에서는 지식 베이스를 위한 데이터를 수집하기 위해 웹 스크래핑 기법을 사용합니다. requests 라이브러리를 사용해 웹 페이지를 가져온 다음, BeautifulSoup4(웹 페이지에서 정보를 스크래핑하는 라이브러리)를 사용해 HTML 정보를 파싱하고 단락을 추출합니다.
웹 스크래핑을 위한 BeautifulSoup4 및 Requests 준비하기
시작하려면 pip install beautifulsoup4 sentence-transformers를 실행해 BeautifulSoup을 설치합니다. 이 섹션에서는 requests와 BeautifulSoup 두 가지 import만 필요합니다. 다음으로 스크래핑하려는 URL의 딕셔너리를 만듭니다. 이 예제에서는 Towards Data Science의 콘텐츠만 스크래핑하지만, 다른 웹사이트도 스크래핑할 수 있습니다. 아래 코드에 표시된 형식을 사용해 각 아카이브 페이지에서 데이터를 가져옵니다.
import requests
from bs4 import BeautifulSoup
urls = {
'Towards Data Science': 'https://towardsdatascience.com/archive/{0}/{1:02d}/{2:02d}',
}
웹 스크래핑을 위해 두 개의 헬퍼 함수도 필요합니다. 첫 번째 함수는 해당 연도의 일수를 월과 일 형식으로 변환합니다. 두 번째 함수는 글에서 박수 수(박수는 Medium 글에 대한 지지를 나타냅니다)를 가져옵니다.
일수 변환 함수는 비교적 간단합니다. 각 달의 일수를 하드코딩하고 해당 목록을 사용해 변환을 수행합니다. 이 웹 스크래핑 프로젝트는 명시적으로 2023년을 대상으로 하므로 윤년을 고려할 필요가 없습니다. 원한다면 다른 연도에 맞게 수정해도 됩니다.
박수 수 계산 함수는 Medium 글의 박수 수를 나타냅니다. 일부 글은 수천 개의 박수를 받을 수 있으며, Medium은 이를 문자 "K"로 표시합니다. 따라서 함수에서 이 표현 방식을 고려해야 합니다.
# 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
BeautifulSoup4 웹 스크래핑 응답 파싱하기
이제 필요한 구성 요소를 설정했으므로 웹 스크래핑을 진행할 수 있습니다. 이 섹션에서는 "time"이라는 추가 라이브러리를 import합니다. 이 라이브러리를 추가한 이유는 과정 중에 429 오류(요청이 너무 많음)가 발생했기 때문입니다. time 라이브러리를 사용하면 요청을 보내는 사이에 지연 시간을 도입할 수 있습니다. 또한 sentence transformers 라이브러리를 활용해 Hugging Face에서 임베딩 모델, 구체적으로 MiniLM 모델을 가져옵니다.
앞서 언급했듯이, 우리는 2023년 데이터만 스크래핑하고 있으므로 연도를 2023으로 설정합니다. 또한 1일차(1월 1일)부터 243일차(8월 30일)까지의 데이터만 필요합니다. 스크래핑 당시에는 9월 초였습니다. 돌이켜보면 244일차까지 스크래핑하는 것이 더 좋았겠지만, 지금은 이미 제가 수행한 내용으로 진행하겠습니다. 함수의 첫 번째 부분에서는 첫 번째 time.sleep() 호출까지 필요한 구성 요소를 설정합니다. 반복자를 월과 일로 변환하고, 이를 날짜 문자열로 변환한 다음, 아카이브 URL에서 HTML 응답을 가져옵니다.
HTML을 얻은 후에는 BeautifulSoup을 사용해 이를 파싱하고, 특정 클래스 이름(코드에 표시됨)을 가진 div 요소를 검색하는데, 이는 해당 요소가 기사임을 나타냅니다. 그다음 제목, 부제목, 기사 URL, 박수 수, 읽기 시간, 응답 수를 파싱합니다. 이후 requests를 다시 사용하여 기사 URL에서 기사를 가져옵니다.
이 for 루프의 두 번째 섹션을 마무리하기 위해 time.sleep()을 두 번째로 호출합니다. 이 시점에서 각 기사에 필요한 대부분의 메타데이터를 확보했습니다. 그런 다음 Hugging Face 모델을 사용하여 각 문단과 그에 해당하는 임베딩을 추출합니다. 이후 해당 특정 문단과 기사에 대한 모든 메타데이터를 포함하는 딕셔너리를 생성합니다.
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
})
마지막 단계는 파일을 pickle로 저장하는 것입니다.
import pickle
filename="TDS_8_30_2023"
with open(f'{filename}.pkl', 'wb') as f:
pickle.dump(data_batch, f)
우리 데이터는 어떻게 생겼나요?
데이터를 시각화하는 것은 언제나 도움이 됩니다. 아래는 Zilliz Cloud에서 데이터가 어떻게 보이는지 보여줍니다. 벡터를 나타내는 임베딩에 주목하세요. 다음 섹션에서는 텍스트로부터 이러한 벡터 임베딩을 생성합니다.
TDS에서 벡터 데이터베이스로 데이터 수집하기
데이터가 준비되면, 다음 단계는 이를 벡터 데이터베이스로 전달하는 것입니다. 이 프로젝트에서는 Towards Data Science에서 스크래핑하는 대신, Milvus 기반으로 구축된 완전 관리형 벡터 데이터베이스 서비스인 Zilliz Cloud에 데이터를 수집하기 위해 별도의 노트북을 사용했습니다.
Zilliz Cloud에 데이터를 삽입하기 위해 다음 단계를 따릅니다:
Zilliz Cloud에 연결합니다.
컬렉션의 매개변수를 정의합니다.
Zilliz Cloud에 데이터를 삽입합니다.
Jupyter Notebook 설정하기
pip install pymilvus python-dotenv 명령을 실행하여 Jupyter Notebook을 설정하고 데이터 수집 프로세스를 시작합니다. 저는 평소처럼 환경 변수를 관리하기 위해 dotenv 라이브러리를 사용합니다. pymilvus 패키지의 경우, 다음 모듈을 가져와야 합니다:
utility- 컬렉션의 상태를 확인하기 위해connections- Milvus 인스턴스에 연결하기 위해FieldSchema- 필드의 스키마를 정의하기 위해CollectionSchema- 컬렉션의 스키마를 정의하기 위해DataType- 필드에 저장되는 데이터 유형Collection- 컬렉션에 접근하는 방식
그런 다음, 이전에 pickle로 저장한 데이터를 열고, 환경 변수를 가져온 뒤, 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
)
Zilliz 벡터 데이터베이스와 데이터 수집 설정하기
이제 Zilliz Cloud를 설정해야 합니다. TDS 웹사이트에서 스크래핑한 데이터를 저장하고 정리하기 위한 컬렉션을 생성해야 합니다. 두 가지 상수가 필요합니다: 차원과 컬렉션 이름입니다. 차원은 벡터가 가진 차원의 수를 의미합니다. 이 경우 384차원의 MiniLM 모델을 사용합니다. 컬렉션 이름은 자명합니다.
Milvus의 새로운 동적 스키마 기능을 사용하면, 저장된 다른 필드의 수나 필요한 데이터 유형을 걱정하지 않고 컬렉션의 id와 임베딩 필드만 간단히 정의할 수 있습니다. 하지만 해당 필드들을 올바르게 검색하려면 우리가 보유한 필드의 구체적인 이름을 기억하는 것이 중요합니다.
이 프로젝트 예제에서는 동적 스키마 기능을 사용하여 ID와 임베딩 필드만 정의합니다. 그런 다음 이 필드들을 스키마에 할당하고, 스키마를 컬렉션에 할당합니다. 다음으로 인덱스의 매개변수를 지정합니다. 이 경우 양자화(FLAT)가 없는 역파일 인덱스(IVF), 유클리드 거리(L2), 그리고 인덱스에 대한 128개의 중심점을 사용합니다.
양자화가 없는 IVF는 가장 직관적인 인덱싱 방법입니다. 본질적으로 벡터를 쿼리할 128개의 섹션으로 클러스터링하는 것입니다. 이 경우 128은 임의의 숫자입니다. 이는 결과가 어떻게 보이는지에 따라 조정할 수 있는 하이퍼파라미터입니다. 경험적으로, 클러스터의 좋은 다양성을 제공하면서 쿼리의 초기 복잡도를 줄일 수 있기 때문에 좋은 시작점입니다. 이러한 매개변수를 정의한 후에는 임베딩 필드에 인덱스를 생성하고 컬렉션을 메모리에 로드합니다.
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()
컬렉션을 정의하고 메모리에 로드한 후에는 데이터를 삽입하는 두 가지 옵션이 있습니다.
데이터 배치를 반복하면서 각 조각을 개별적으로 삽입하기
데이터를 배치 단위로 삽입하기
모든 데이터를 삽입한 후에는 컬렉션을 flush하여 인덱싱하고 일관성을 보장하는 것이 중요합니다. 많은 양의 데이터를 수집하는 데는 시간이 걸릴 수 있습니다.
for data in data_batch:
collection.insert([data])
collection.flush()
TDS 기사 세그먼트 쿼리하기
이제 모든 것이 설정되어 쿼리할 준비가 되었습니다. 이 게시물에서는 쿼리를 벡터화하고 Zilliz Cloud에서 가장 가까운 일치 항목을 검색하는 방식으로 이 부분을 간단하게 유지하겠습니다. 다음 게시물들에서는 LlamaIndex와 LangChain도 사용할 것입니다.
HuggingFace 모델 가져오기 및 Zilliz 쿼리를 위한 설정
임베딩 모델을 가져오고 벡터 데이터베이스를 설정하여 Towards Data Science 지식 베이스를 쿼리해야 합니다. 이 단계에서는 별도의 노트북을 실행해야 합니다. 저는 환경 변수를 관리하기 위해 dotenv 라이브러리를 사용할 것입니다. 또한 Sentence Transformers의 MiniLM 모델을 사용해야 합니다. 이 단계에서는 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")
벡터 검색 쿼리 실행하기
이제 벡터 데이터베이스에 연결하고 검색을 수행해야 합니다. 이 프로젝트에서는 Zilliz Cloud 인스턴스에 연결하고 앞서 생성한 컬렉션인 tds_articles를 가져올 것입니다. 쿼리를 얻기 위해 사용자에게 2023년 9월까지의 TDS에 관한 질문을 입력하도록 요청합니다.
다음으로 Hugging Face의 임베딩 모델을 사용하여 쿼리를 인코딩합니다. 이 과정은 사용자의 질문을 384차원 벡터 표현으로 변환합니다. 그런 다음 이 인코딩된 쿼리를 사용하여 벡터 데이터베이스를 검색합니다. 검색 중에는 근사 최근접 이웃 필드(anns_field), 인덱스 매개변수, 원하는 검색 결과 수 제한, 그리고 원하는 출력 필드를 지정해야 합니다.
이전에는 필드 스키마 정의를 간소화하기 위해 Milvus의 동적 스키마 기능을 활용했습니다. 이제 벡터 데이터베이스를 검색할 때 원하는 동적 필드를 검색 결과에 포함하는 것이 필수적입니다. 이 특정 시나리오에서는 기사 각 단락의 텍스트를 포함하는 paragraph 필드를 요청합니다.
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])
저는 앱에 대규모 언어 모델에 대한 정보를 제공해 달라고 요청했고, 앱은 다음 두 가지 응답을 반환했습니다. 이 응답들은 "언어 모델"을 언급하고 일부 관련 정보를 포함하지만, 대규모 언어 모델에 대한 자세한 설명은 제공하지 않습니다. 두 번째 응답은 의미적으로 유사하지만 우리가 요청한 내용에 충분히 가깝지는 않습니다.
벡터 데이터베이스 지식 베이스에 추가하기
지금까지 우리는 Zilliz를 벡터 데이터베이스로 사용하여 TDS 기사에 대한 지식 베이스를 만들었습니다. 의미적으로 유사한 검색 결과를 쉽게 검색할 수 있었지만, 그것들이 항상 우리가 필요로 하는 것은 아닙니다. 다음 단계는 새로운 프레임워크와 기술을 통합하여 결과를 향상시키는 것입니다.
다가오는 섹션에서는 Query Multiple Documents using LlamaIndex, LangChain, and Milvus에 대한 이전 블로그에서 수행했던 라우팅과 유사하게, 결과를 라우팅하기 위해 LlamaIndex를 추가하는 방법을 살펴보겠습니다.
요약
이 튜토리얼에서는 Towards Data Science 출판물을 위한 챗봇 구축을 다룹니다. 벡터 데이터베이스(구체적으로는 Zilliz Cloud)에 지식 베이스를 생성하고 저장하기 위한 웹 스크래핑 과정을 설명합니다. 그런 다음 사용자에게 쿼리를 입력하도록 요청하고, 쿼리를 벡터화하며, 벡터 데이터베이스를 쿼리하는 방법을 보여줍니다.
하지만 결과가 의미적으로는 유사하지만, 우리가 원하는 것과 정확히 일치하지는 않는다는 것을 발견했습니다. 이 블로그 시리즈의 다음 파트에서는 LlamaIndex를 사용하여 쿼리를 라우팅하고 더 나은 결과를 얻을 수 있는지 살펴보겠습니다. 여기서 논의한 단계 외에도, 모델을 교체하거나, 텍스트를 결합하거나, 다른 데이터셋을 사용하여 Zilliz Cloud를 실험해볼 수도 있습니다!
전체 Chat Towards Data Science 시리즈를 다시 읽으려면, 링크는 다음과 같습니다:
계속 읽기

Zilliz Cloud On-Demand Compute: Pay Only for What You Use
The customer case behind Zilliz Cloud On-Demand: how a $10K vector search bill came down to under $500, and the engineering changes that made it possible.

Cosmos World Foundation Model Platform for Physical AI
NVIDIA's Cosmos platform enables safe, digital twin training of GenAI models for physical applications, overcoming data scarcity and safety challenges.

Legal Document Analysis: Harnessing Zilliz Cloud's Semantic Search and RAG for Legal Insights
Enhance legal document analysis with Zilliz Cloud’s Semantic Search and RAG. Improve accuracy, efficiency, and scalability for contracts, case law, and compliance.



