들어가며
지난 글에서는 Redis를 이용해 기존에 문의했던 질문들을 재차 AI에 요청하여 답변받는 프로세스 소요 시간을 줄이기 위해 캐시 기법을 적용해 보았습니다. 유사한 질문에 대한 답변 속도는 굉장히 빨라진 반면 질문의 유사성을 제대로 파악하지 못해 잘못된 답변을 주는 문제가 발생했습니다.
이를 해결하기 위해 이번 글에서는 유사도 계산 자체를 수정하여 질문들의 유사성을 좀 더 사람과 가깝게 개선해 보려고 합니다.
Dense 임베딩을 사용했을 때
Part 1에서 데이터셋을 저장할 때 자연어를 임베딩하였는데, OpenAI의 text-embedding-ada-002 모델로 임베딩할때는 dense 임베딩이라는 텍스트 데이터를 벡터로 표현하는 방식을 사용하게 됩니다.
앞에 사용했던 예제 문장인 “title hamilton men's h77705145 khaki navy 42mm automatic watch product type wristwatch. seller watchgooroo price $ 519.99. sold 10.0 many times.”라는 문장을 dense 임베딩 했을때 벡터값은 다음과 같습니다.
Sparse 임베딩을 사용했을 때
Dense 임베딩 방식 외에도 단어나 긴 문장을 임베딩하는 방식으로 Sparse 임베딩을 사용할 수도 있습니다. 일반적으로 단어나 문장의 출현 빈도를 기반으로 생성되며, 벡터의 각 차원이 해당 단어의 출현 여부와 빈도를 나타냅니다. 따라서 Dense 임베딩 방식과 달리 대부분의 차원값이 0이며, 같은 단어가 출현할 때마다 그 출현 횟수를 카운트해서 문장의 의미 해석에 중점을 두게 됩니다.
Sparse 임베딩 시 특정 단어들의 출현 횟수에 집중하는 만큼 질문이나 검색했을 때 키워드를 잡고 관련 답변을 찾기가 수월해질 수 있습니다.
Dense 임베딩과 Sparse 임베딩을 결합한 하이브리드 서치 모델 구현하기
앞선 Part 2에서는 질문의 문맥은 잘 이해하지만, 특정 브랜드와 같은 문장 내 중요한 단어가 다름에도 기존과 유사한 문장으로 인식했습니다. 자연어 처리에서 단어의 의미와 맥락을 고려해 좀 더 정확한 파악을 위해서 하이브리드 서치 방법을 사용하는데, 이는 Sparse 임베딩과 Dense 임베딩을 결합하여 단어의 의미와 맥락을 모두 고려하는 방법입니다.
Dense 임베딩과 Sparse 임베딩을 결합한 하이브리드 서치 모델을 구현하기 위해선 먼저 기존의 문장을 Dense 임베딩뿐만 아니라 동일한 문장을 추가로 Sparse 임베딩하여 Pinecone DB에 저장해야 합니다.
코드에서는 sparse_embedding 함수를 만들어 Sparese 임베딩 처리를 한 후 문장 안에 해당 단어가 포함된 횟수 혹은 빈번도를 저장해 두었습니다.
from transformers import BertTokenizerFast
from collections import Counter
tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')
def sparse_embedding(text):
inputs = tokenizer(
text,
padding=True,
truncation=True,
max_length=512,
return_tensors='pt' # Return PyTorch tensors
)
input_ids = inputs['input_ids']
# Convert PyTorch tensor to a list
input_ids_list = input_ids.squeeze().tolist()
# Create a dictionary using Counter
sparse_vec_dict = dict(Counter(input_ids_list))
return sparse_vec_dict
그리고 임베딩 된 벡터를 pinecone에 upsert 할 때 dense 임베딩과 더불어 sparse embedding을 metadata로 같이 적재시켜 줍니다:
meta_data = [
{
"index": str(row['Index']),
"itemNumber": str(row['itemNumber']),
"price": str(row['priceWithCurrency']),
"seller": str(row['seller']),
"title": str(row["title"]),
"type": str(row["type"]),
"sold": str(row["sold"]),
"sparse_vector": str(sparse_embedding(str(row["filtered_text"])))
}
for _, row in batch.iterrows()
Upsert가 완료되어 Pinecone에서 데이터를 확인해 보면, Dense 임베딩된 값과 Sparse 임베딩된 값이 같이 적재된 것을 볼 수 있습니다.
유사도 판단하기
그렇다면, 이제 두 개의 임베딩 모델을 조합하여 새로운 방식으로 유사도를 판단해야 하겠죠. 하이브리드로 유사도를 계산하는 함수는 코드로 다음과 같이 작성하였는데요. alpha 값은 0과 1사이의 소수로 1에 근사할수록 Dense 임베딩에, 0에 근사할수록 Sparse 임베딩에 좀 더 무게를 둘 수 있는 기준치라고 볼 수 있습니다.
def hybrid_scale(dense, sparse, alpha: float):
indices = list(map(int, sparse.keys()))
values = [v * (1 - alpha) for v in sparse.values()]
hsparse = {'indices': indices, 'values': values}
hdense = [v * alpha for v in dense]
return hdense, hsparse
기존의 dense 임베딩만을 기준으로 유사한 데이터를 찾은 것과 달리, 질문이 들어와 Pinecone DB에서 근사 데이터를 찾을 때 dense와 sparse를 적절히 비율을 섞어서 근사한 데이터를 찾을 수 있는 프로세스를 함수로 만들어 보겠습니다.
def hybrid_query(question):
sparse_vec = sparse_embedding(question)
dense_vec = get_embedding(question)
dense_vec, sparse_vec = hybrid_scale(dense_vec, sparse_vec, 0.7)
result = index.query(
vector=dense_vec,
sparse_vector=sparse_vec,
top_k=3,
include_metadata=True,
)
metadata_values = [match['metadata'] for match in result['matches']]
return metadata_values
챗봇과 대화하기
이제 다른 시계를 추천해달라고 했을 때 이전과 유사한 문장으로 인식하여 캐시 답변을 주는지, AI가 새로 DB 정보를 기반으로 답변을 주는지 확인해 볼까요?
이렇게 하이브리드 서치 방식을 사용해 본 결과, 정확한 답변을 줘야 하는 상황에서는 유사한 답변을 캐싱해 오는 것이 아닌 시간이 소요되더라도 새로운 답변을 생성해 주는 것을 확인할 수 있었습니다.
마치며
비즈니스 용도로 LLM만 단독으로 사용했을 때는 실제 서비스를 제공하기까지 어려움이 있습니다. 특히 벡터 데이터베이스와 캐시 스토리지를 구축하고 또 본 블로그 글에서는 아직 다루지 않았지만, 대화 내용들을 비정형 데이터로 실시간으로 전송하고 저장하기까지 셀프로 구축하기에 시간과 비용이 많이 소모되는 부분들을 영역마다 제공되는 SaaS를 활용해 쉽게 구현해 보았습니다.
확실히 개발 및 운영 부담을 크게 줄일 수 있다는 것을 체감하면서 이를 통해 보다 효율적이고 사용자 친화적인 챗봇 서비스를 쉽게 제공할 수 있게 될 것 같습니다. 이 글을 읽으신 분들께서도 제 체험기가 실무에 조금이나마 도움이 되셨길 바라면서 다음번에는 챗봇 대화 내용을 SaaS 제품을 활용해 손쉽게 저장하고 활용할 수 있는 방법을 다뤄보려고 합니다.
관련하여 궁금한 사항이나 커피챗을 원하시면 링크드인 DM 보내주세요 : )