주제

1.  워드 임베딩
2. 벡터 DB
3. LangChain과 RAG
4. LangChain 기반 QA 챗봇 만들기

 

1. 워드 임베딩

  • 임베딩의 필요성 : 텍스트 처리를 위해서는 텍스트를 숫자(벡터 혹은 행렬)로 변환해야 함
  • 두가지 방법 : Bag of Words, Word Embedding

1.1 Bag of Words

  • 단어별로 인덱스를 지정하고 문장의 경우 문장에서 나타난 단어들의 인덱스를 저장(보통 빈도수도 같이 저장)
  • One-Hot Encoding을 사용
    • 굉장히 크지만 sparse한 벡터가 됨
    • 단어들의 관계가 전혀 표현되지 못함
    • 단어의 순서도 표현되지 않음(문맥 존재 ×)
  • 보통 3개의 특수 단어가 존재
    • SOS : Start of Sentence
    • EOS : End of Sentence
    • UNKNOWN : 모르는 단어

1.2 Word Embedding

  • N개 단어들간의 관계를 학습해서 훨씬 더 작은 공간 상에서 표현해주는 딥러닝 모델
  • 단어의 차원수를 의미있게 줄일 수 있음
  • 모든 NLP 처리 딥러닝의 첫 번째 레이어로 존재함
  • 학습 시 두 가지를 사용 : 
    • CBOW(Continuous Bag of Words), Skip-gram
    • Encoder Decoder 아키텍처
  • 많이 쓰이는 Pre-trained Word Embedding 모델 :
    1. Word2Vec
    2. GloVe
    3. FastText
    4. OpenAI Embedding

1.2.1 AutoEncoder : Encoder Decoder

  • 대표적인 비지도학습을 위한 딥러닝 모델
  • 입력을 낮은 차원으로 압축 후, 이를 원본 데이터와 유사하게 복원하는 모델
    • 데이터의 숨겨진 구조를 발견하면서 노드의 수를 줄이는 것이 목표
    • 입력 데이터에서 불필요한 특징들을 제거하는 걸 학습
    • 오토인코더의 출력은 입력을 재구축한 것
      • 최대한 비슷하게 나오도록 학습
      • 입력 데이터와 예상 출력 데이터가 동일 (입력 == 레이블)

1.2.2 Word Embedding 적용

  • 단어 입력 → Word Embedding Layer → 단어 벡터
    • 입력은 이차원 Tensor(문장의 수, 문장의 길이)
  • 각 문장의 길이는 모든 문장에 대해 동일해야함
  • 문장의 수는 보통 배치 크기에 의해 결정
  • 출력은 삼차원 Tensor(문장의 수, 문장의 길이, 임베딩 차원)
    • 임베딩 차원은 유니크한 단어의 수보다 훨씬 더 적어야함

 

2. 벡터 DB

  • 기존 검색 방식의 문제점을 보완하기 위해 나온 솔루션
  • Embedding을 사용해서 데이터의 크기 축소
  • 의미적인 검색 가능
  • 다양한 타입을 동시에 지원(텍스트, 이미지, 오디오)

2.1 벡터 데이터베이스의 유스 케이스

  • 시맨틱 검색 & 유사 검색
  • LLM을 보조하기 위한 롱텀 메모리 (RAG)
  • 추천 시스템
  • 머신 러닝 : 클러스터링과 분류 시스템
  • 이상 징후 감지 (Anomaly Detection)

2.2 벡터 DB의 장점

  • 시맨틱 검색 
    • 단순 키워드 매칭이 아닌 사용자 의도 파악이 쉬움
    • 검색 결과가 향상됨(동의어와 관련 단어를 훨씬 쉽게 파악할 수 있음)
    • 다른 형태의 임베딩을 통해 커스터마이즈나 개인화 가능
    • 과정 :
      1. 모든 문서를 벡터 DB로 변환
      2. 검색어를 벡터로 변환
      3. 벡터 DB 내에서 검색 벡터와 근사한 다른 이웃들을 검색(KNN)
      4. 3의 결과가 검색 결과
  • 다양한 데이터 타입의 인덱싱과 검색이 가능

2.3 실습

  • Pinecone 사용
    • 벡터 임베딩을 활용하여 유사한 콘텐츠 검색 구현을 가능하게 해주는 클라우드 서비스
  • Medium 포스팅을 벡터 DB로 바꿔보기
  • 데이터셋은 Medium 포스팅의 제목과 부제가 담겨있음
  • Medium은 포스트에 clap을 누르는 것이 좋아요의 기능을 함
  • 데이터의 생김새 확인

데이터 확인

  • 데이터 전처리 후 pinecone 사용 준비
  • Pinecone에 로그인 후 발급받은 API KEY를 복사하여 사용
import os

# serverless를 사용하지 않을 예정
API_KEY = ''
use_serverless = False
from pinecone import Pinecone

pc = Pinecone(api_key=API_KEY)
from pinecone import PodSpec

# Starter Plan에 사용가능한 spec :)
spec = PodSpec(
    environment="gcp-starter",
    pod_type="s1.x1",
    pods=1
)
index_name = 'semantic-search-fast'

existing_indexes = [
    index_info["name"] for index_info in pc.list_indexes()
]
  • 임베딩의 차원 수는 내가 사용할 임베딩 모델과 맞춰야함
  • metric의 dotproduct는 cosine similarity와 비슷함.
  • 인덱싱할 때 1초의 시간을 두고 진행
import time

pc.create_index(
    index_name,
    dimension=384,  # dimensionality of minilm
    metric='dotproduct',
    spec=spec
)
# wait for index to be initialized
while not pc.describe_index(index_name).status['ready']:
    time.sleep(1)
  • 모델 가져오기
from sentence_transformers import SentenceTransformer
import torch

model = SentenceTransformer('all-MiniLM-L6-v2', device='cpu') # cuda or cpu
  • metadata의 title 키값들을 임베딩을 하여 임베딩값을 데이터에 추가
df['values'] = df['metadata'].map(
    lambda x: (model.encode(x["title"])).tolist()) # python list, 6k rows 1 min

마지막에 인코딩된 값이 추가됨

  • 필요한 값들만 슬라이싱하여 가져오기(id, values, metadata)
    • upsert = update + insert를 뜻하는 쿼리 용어
df['id'] = df.reset_index(drop = 'index').index

df_upsert = df[['id', 'values', 'metadata']]

# pinecone은 숫자로 된 필드를 받아들이지 않기 때문에 형변환
df_upsert['id'] = df_upsert['id'].map(lambda x: str(x))

 

  • 벡터 DB에 파이썬 판다스 데이터프레임을 바로 업데이트하여 삽입
index = pc.Index(index_name)
index.upsert_from_dataframe(df_upsert) # 6k takes 1 min
  • 쿼리
    • What is ethics in AI라는 지문을 주고, 이와 유사한 포스트의 데이터를 가져오게 함
    • id, 유사도(score), metadata 순으로 출력
xc = index.query(vector=(model.encode("what is ethics in AI")).tolist(), # python list
           top_k=10,
           include_metadata=True,
           include_values=True)
for result in xc['matches']:
    print(result['id'], result['score'], result['metadata'])

결과

 

3. LangChain과 RAG

3.1 RAG이 나오기 전

  • LLM의 지식을 그대로 사용함 (아직까지는 현재 정보를 갖고 있지 않음)
  • 굉장히 범용적인 데이터로 훈련
  • 헛소리도 자주함
  • 파인 튜닝이 쉽지 않음

3.2 RAG(Retrieval Augmented Generation) 아키텍처

  • 자체 데이터를 가장 손쉽게 사용하기 위해 사용되는 방법으로 LLM 모델을 쿼리하는 프롬프트의 일부로 데이터를 제공하는 방식
  • 학습 데이터에서 파생된 지식에만 의존하지 않고 관련 정보를 가져와서 정적 LLM을 실시간 데이터 검색과 연결
    1. 데이터 준비 : 문서 데이터는 메타데이터와 함께 수집되고 전처리를 거침
    2. 관련 데이터 인덱싱 : 문서 임베딩을 생성하고 인덱싱되어 벡터 DB에 저장
    3. 관련 데이터 검색 : 데이터에서 사용자의 쿼리와 관련된 부분을 검색(벡터 DB에서 수치적으로 거리가 가장 가까운 문서들을 검색). 텍스트 데이터가 LLM에 사용되는 프롬프트의 일부로 제공(LLM에게 답변 요청)
    4. LLM 애플리케이션 빌드 : 프롬프트 증강의 구성 요소를 래핑하고 LLM을 엔드포인트로 쿼리함.
  • 이미지 출처(IBM)

RAG architecture

 

3.3 LangChain

  • 간단한 것부터 복잡한 것까지 RAG 애플리케이션 빌딩 블록 제공
  • 즉, RAG 애플리케이션을 만들어주는 라이브러리
  • 비슷한 서비스로는 LlamaIndex가 존재

 

4. LangChain 기반 QA 챗봇 만들기

4.1 RAG 실습 프로그램 개요

  • HuggingFace databricks-dolly-15k 데이터셋을 도메인 정보로 사용
  • 베이스 LLM은 HuggingFaceHub에 있는 google/flan-t5-xxl을 사용
  • flan-t5-xxl 모델의 대화 능력에 databricks-dolly-15k에 있는 도메인 정보를 벡터 DB 형태로 프롬프트에 사용

4.2 LangChain 실습 프로그램 개요

  • 데이터셋 로드 : HuggingFaceDatasetLoader 사용
  • 데이터셋 chunk 생성 : 크기는 1000글자, 오버랩은 150 글자로 설정
    • LangChain의 RecursiveCharacterTextSplitter 사용
  • 텍스트 임베딩 : 'sentence-transformers/all-MiniLM-I6-v2' 사용
  • chunk → 임베딩 → FAISS 인덱싱 수행
    • from_documents : 인덱싱
    • similarity_search : 유사도 검색

4.2.1 QA Chain 생성

  • LLM 모델 준비 - LLM 파이프라인 생성
  • Retrievers 생성 : Vector DB에서 검색을 위한 용도
  • 최종적으로 LLM 파이프라인과 Retriever를 묶기 위한 파이프라인 생성
  • 쿼리 생성 : invoke 함수 사용

4.3 실습

  • 문서 로딩
from langchain.document_loaders import HuggingFaceDatasetLoader

# Specify the dataset name and the column containing the content
# - https://huggingface.co/datasets
dataset_name = "databricks/databricks-dolly-15k"
page_content_column = "context"  # or any other column you're interested in

# Create a loader instance
loader = HuggingFaceDatasetLoader(
    dataset_name,
    page_content_column
)

# Load the data
data = loader.load()

# Display the first entry
data[0]
  • 긴 문서를 작은 chunk로 분할하는 작업
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)

docs = text_splitter.split_documents(data[:1000])
  • 텍스트 임베딩
# Define the path to the pre-trained model you want to use
modelPath = "sentence-transformers/all-MiniLM-l6-v2"

# Create a dictionary with model configuration options,
# specifying to use the cuda for computations
# Runtime -> "Change runtime type"
model_kwargs = {'device':'cpu'}

# Create a dictionary with encoding options, specifically setting 'normalize_embeddings' to False
encode_kwargs = {'normalize_embeddings': False}

# Initialize an instance of HuggingFaceEmbeddings with the specified parameters
embeddings = HuggingFaceEmbeddings(
    model_name=modelPath,     # Provide the pre-trained model's path
    model_kwargs=model_kwargs, # Pass the model configuration options
    encode_kwargs=encode_kwargs # Pass the encoding options
)
  • FAISS 벡터 스토어를 사용하여 벡터 저장소 지정
# gpu(cuda)로 설정했을 때 24초 걸림
db = FAISS.from_documents(docs, embeddings)
  • 질문을 지정하여 벡터 저장소에서 유사한 것을 검색
question = "Who is Thomas Jefferson?"
searchDocs = db.similarity_search(question)
print(searchDocs[0].page_content)
  • 결과 :
"Thomas Jefferson (April 13, 1743 \u2013 July 4, 1826) was an American statesman, diplomat, lawyer, architect, philosopher, and Founding Father who served as the third president of the United States from 1801 to 1809. Among the Committee of Five charged by the Second Continental Congress with authoring the Declaration of Independence, Jefferson was the Declaration's primary author. Following the American Revolutionary War and prior to becoming the nation's third president in 1801, Jefferson was the first United States secretary of state under George Washington and then the nation's second vice president under John Adams."
 
  • Retriever는 쿼리에서 문서를 반환하는 인터페이스로 기본적으로 벡터 저장소는 retriver의 중추이다
retriever=db.as_retriever(search_kwargs={"k": 3})
docs = retriever.get_relevant_documents("What is Cheesemaking?")
print(docs[0].page_content)

결과

  • 위와 같은 질문에 대한 답도 같다
docs = retriever.get_relevant_documents("Who is Thomas Jefferson?")
  • LLM 모델과 LangChain 준비하기
  • 텍스트 전처리를 위한 토큰화 모델과 입력된 텍스트와 질문을 기반으로 답변을 제공하는 질문 답변 모델로 시작
from langchain import HuggingFaceHub
from langchain.chains.question_answering import load_qa_chain

llm=HuggingFaceHub(
    repo_id="google/flan-t5-xxl",
    model_kwargs={"temperature":0.1, "max_length":512}
)
chain = load_qa_chain(llm, chain_type="stuff")
  • 질문을 지정하고 답변 확인
question = "Who is Thomas Jefferson?"
# run the query directly against google/flan-t5-xxl
llm.invoke(question)

답변

  • 벡터 저장소에서 쿼리를 실행한 결과
# run the query via Vector DB
docs = db.similarity_search(question)
chain.run(input_documents=docs, question=question)

답변

  • 다른 질문으로 실습
llm.invoke("Who is the founder of Apple?")

답변

  • QA 파이프라인 생성
from langchain.chains import RetrievalQA

qa = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    verbose=True,
    chain_type_kwargs={
        "verbose": True,
    }
)
qa.run("Who is Thomas Jefferson")

QA 실행 모습

 

 


느낀점

LLM을 점점 써볼수록 더 자세하게 공부해보고 싶은 생각이 들었다. 기초부터 차근차근 공부해보면서 파운데이션 모델을 가지고 파인튜닝을 하는 과정을 겪어보면 좋은 경험이 될 것 같다. 머신러닝/딥러닝 기초부터 잘 다져야겠다는 생각을 했다.

+ Recent posts