KT AIVLE School/[TIL] AIVLE School 당일 복습

[TIL] [KT AIVLE School] 에이블스쿨 DX 트랙 10주차 3일. 딥러닝 심화(2). RAG

guoyee94 2024. 11. 6. 18:14

 

지난 줄거리.

NLP 분야의 독보적 혁신을 몰고 온 Transformer 아키텍처.
그러나 이를 바탕으로 등장한 LLM
모델이 학습하지 않은 내용을 전혀 예측하지 못한다는 문제가 발견되는데...

 

오늘을 LLM의 한계를 극복하기 위한 방법들.

그중에서도 RAG에 대해 알아본다.

 

 

 


 

 

1. LangChain

LangChainLLM 기반 개발을 지원하는 라이브러리로,

강사님의 표현을 빌리자면 지금 가장 핫한 녀석이다.

 

기본적으로 LangChain에서는 세 가지 메시지를 사용한다.

SystemMessage 시스템의 롤과 환경을 정의함
HumanMessage 사람이 던지는 질문
AIMessage 인공지능의 대답

 

이렇게 역할을 분리하고 대화 자체를 구조화함으로써 관리가 용이해진다.

 

이녀석은 sklearn처럼 다양한 모듈들을 지원하며,

대표적인 모델들은 아래와 같다.

 

Family Module 설명
Model I/O Prompt Templates 입력 프롬프트를 템플릿으로 관리
  Language Models (LLMs) 다양한 언어 모델과의 통합
  Embeddings 문서 및 텍스트를 벡터 형태로 표현
Data Connection Data Loaders 외부 데이터 소스에서 데이터 로드
  Text Splitters 긴 문서를 여러 조각으로 분할
Chains Simple Chains 하나의 작업을 순차적으로 처리
  Sequential Chains 여러 작업을 순차적으로 연결
Agents Tool Selection Agents 상황에 맞는 도구를 선택해 실행
Memory Short-term Memory 대화 중 최근 문맥 유지
Callbacks Logging 이벤트 기록 및 디버깅 지원

 

sklearn처럼 모듈들을 불러와 간단하게 LLM 모델을 구현할 수 있다.

 


 

LangChain의 대표적인 사용법은 다음과 같다.

 

(1). 적절한 모델 import하여 선언

# langeuage_models
from langchain.chat_models import ChatOpenAI
chat = ChatOpenAI(model = "gpt-3.5-turbo")

 

 

(2). HumanMessage 프롬프트에 content를 넣어서 전달

result = chat([HumanMessage(content="안녕하세요!")])
print(result.content)

안녕하세요! 무엇을 도와드릴까요? 😊

 

 

(3). SysyemMessage로 role 부여

sys_role = '당신은 애국심을 가지고 있는 건전한 대한민국 국민입니다.'
question = "독도는 어느나라 땅이야?"

result = chat([HumanMessage(content = question), SystemMessage(content = sys_role)])
print(result.content)

독도는 대한민국의 땅입니다.

 

 

(4). 프롬프트 템플릿을 생성하여 반복 질문

from langchain import PromptTemplate

prompt = PromptTemplate(template = "{nation}의 인구수는?", input_variables = ["nation"])
result = chat([HumanMessage(content=prompt.format(nation = "한국"))])
print(result.content)

2021년 9월 현재 한국의 인구수는 약 51,780만 명입니다.

순식간의 한국의 인구수가 5억 명을 돌파했다. 배양이라도 했나.

 

그런데 중요한 건, 우리가 이 LangChain을 왜 쓰느냐이다.

 

오늘 우리의 목적은,

LLM의 약점을 RAG로 극복하는 것.

 

RAG를 구현하는데에 LangChain이 어떤 역할을 하는가?

 

이유는 다음과 같다.

1. 데이터 로딩, 임베딩, 벡터 DB 검색정보 준비를 지원한다.
2. 체인을 활용해 검색과 생성 단계를 연결하여 RAG를 효율적으로 구성한다.
3. 프롬프트 템플릿과 메모리 기능으로 일관성 있는 응답과 문맥 유지를 돕는다.

 

이것은 RAG를 이해해야 이해가능한 영역이니,

우선은 RAG 구현을 전반적으로 편하게 만들어 준다는 것만 생각하고 넘어가자.

물론 나도 이 부분을 마지막에 작성했다.

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

2. RAG

내게 적합한 LLM을 만드는 방법은 크게 두가지를 들 수 있다.

 

  • 나의 데이터로 직접 학습시킨다.
  • 기존 LLM을 조정하여 사용한다.

 

전자의 방법이 모델링, 후자의 방법이 Fine-TunningRAG이다.

 

모델링을 수행하기에는 너무 많은 장애 요인이 있는 고로,

현실적으로 우리는 후자의 방법을 사용한다.

※ 우리가 직접 LLM 모델링을 하기에는, LLM의 본질적 문제들로 인해 사실상 불가능하다.

1
. 요구되는 컴퓨팅 자원 인프라가 매우 고비용이다.
2. 데이터 수집 전처리 과정의 규모가 지나치게 크다.
3. 유해하거나 잘못된 결과를 학습/생성할 가능성이 있다.
4. 훈련시간이 지나치게 오래 걸린다.
5. 과적합으로 인한 편향 가능성이 있다.
6. 큰 요구전력과 발열로 인해 환경 문제를 야기한다.

 

 

후자의 방법, 기존 LLM을 조정하여 사용하는 방법은 이렇다.

Fine-Tunning 나의 데이터로 기존 모델을 추가 학습시킨다.
RAG 나의 데이터를 바탕으로 한 답변을 시킨다.

 

위 방법 중 RAG에 대해 알아보자.

 

RAG 작동 원리. 'Knoledge Sources'라고 된 부분이 지식DB에 해당한다.

 

RAG의 작동을 단계적으로 살펴보자.

1. 사용자 프롬프트로부터 질문을 받는다. 이때 질문에는 Key, Querry, Value 정보가 있다.
2. 질문의 Querry를 Tokenize + Embedding하여 질문 벡터를 형성한다.
3. 질문 벡터를 바탕으로 정보 DB에서 답변에 필요한 문서를 검색한다.
  - 이 과정에서 질문 벡터 문서 벡터  코사인 유사도를 계산한다.
  - 이를 통해 가장 유사도가 높은 문서 n개를 찾는다.
4. 이 문서를 초기에 받은 프롬프트에 포함하여 문맥을 강화한다.
5. 강화된 문맥을 바탕으로 답변을 생성한다.
※ 코사인 유사도
분모의 벡터의 길이 곱.

두 벡터 간의 방향 유사성을 측정하는 방법 중 가장 널리 쓰이는 방식.
특히 텍스트나 문서 간 유사도 계산은 거의 이렇게 쓰인다.
두 벡터 사이의 각도에 비례하므로, 값이 작을수록 유사도가 높다는 의미.

 

자 그럼 이제부터 RAG를 구현하는 방법을 하나씩 볼 건데,

그전에 전체적인 RAG 구현 흐름을 먼저 보자.

 

 

지식 DB를 생성한다.

DB를 탐색할 retrieval와 사용할 LLM을 선언한다.

대화 메모리를 선언한다.

DB, LLM, 메모리를 묶어 Chain을 생성한다.


보아하니, DB, retrieval, 메모리가 중요한 듯 하다.

 

이걸 미리 생각하면서, 하나씩 살펴보자.

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

3. Vector DB

 

RAG(Retrieval Augmented Generation) 나의 지식 체계를 LLM이 참조하도록 만드는 방법이다.

 

따라서 RAG의 핵심은 나의 지식 체계를 정리해 놓은 DB이다.

<Vector DB>

RAG의 수행을 위해 DB를 참조하는 구체적인 방법은,
입력받은 질문과 문서를 비교하는 것이다.

그런데 여기서 '입력받은 질문'이란,
이미 Embedding을 거친 벡터의 형태(질문 벡터)다.

따라서 이와 비교하기 위해서 DB도 벡터의 형태로 구축(문서 벡터)해 둘 필요가 있다.
이를 Vector DB라고 한다.

이를 구축하기 위한 다양한 방법이 있는데,
우리는 ChromaDB를 통해 구축한다.

ChromaDB는 VectorDB 구축을 도와주는 파이썬 라이브러리이자,
그렇게 구축된 VectorDB를 가리키는 말이기도 하다.

종종 VectorDB와 같은 의미로 쓰이기도 한다.
용어란게 다 그렇지 뭐


<Chroma DB를 통한 Vector DB 형성 과정>

1. 텍스트 추출 : Document Loader
2. 텍스트 분할 : Text Splitter
    - 이때 분할된 텍스트 덩어리를 chunk라고 한다.
    - 분할된 텍스트는 Document 객체가 된다.
3. 텍스트 벡터화 : Embedding
4. 저장 : Vector Store

 

그럼 코드를 보며 Chroma DB flow를 따라가 보자.

 


 

 

(1). Chroma DB 객체 생성

embeddings = OpenAIEmbeddings(model="text-embedding-ada-002") # OpenAI

database = Chroma(persist_directory = path + "db",  # 경로 지정(구글 드라이브에서 db 폴더 생성)
                    embedding_function = embeddings)      # 임베딩 벡터로 만들 모델 지정

 

Chroma 클래스의 객체 database를 선언하여 사용한다.

Chroma 클래스는 langchain.vetorstores에서 import할 수 있다.

 

기본적으로 Chroma 객체를 만들 때 두 가지를 지정해줘야 하는데,

DB 경로 : 해당 경로에 이미 DB가 있으면 연결시키고, 없으면 새로 DB 파일을 생성한다.
임베딩 함수 : 내가 쓸 모델의 임베딩 함수를 써야 한다. 여기선 GPT를 쓰니 그거에 맞게 지정.

 

 

 

(2). INSERT

 

add_texts()로 텍스트만 DB에 저장할 수도 있고,

add_documents()로 메타 데이터와 함께 저장할 수도 있다.

input_list = ['test 데이터 입력1', 'test 데이터 입력2']

# 입력시 인덱스 저장(조회시 사용)
ind = database.add_texts(input_list)
ind
input_list2 = ['오늘 날씨는 매우 맑음. 낮 기온은 30도 입니다.', '어제 주가는 큰 폭으로 상승했습니다.']
metadata = [{'category':'test'}, {'category':'test'}]

doc2 = [Document(page_content = input_list2[i], metadata = metadata[i]) for i in range(2)]
 
ind2 = database.add_documents(doc2)

 

그런데... 실제로는 DB를 구성할 정보가 있는 상태여야겠지?

 

물론 일일이 입력하는 경우도 있다고 한다. ??? : 전문용어로 인형 눈깔 붙이기라고 하죠.

 

하지만 아래처럼 데이터프레임의 내용을 list로 만들어

반복문을 통해 DB에 Document 형태로 등록하기도 한다.

# 데이터프레임의 텍스트 열(시리즈)을 리스트로 변환
text_list = data['내용'].tolist()

# 리스트 내용을 각각 document로 변환
documents = [Document(page_content=text) for text in text_list]
database.add_documents(documents)

 

 


 

 

여하튼, 이런 방식으로 DB를 생성하고 나면,

위의 단계대로 RAG를 수행할 수 있다.

 

그럼 모델이 받은 질문의 Querry를 바탕으로

DB에서 가장 유사한 문서를 가져 온다.(검색 단계)

아래가 그 예시.

 

(3). 유사도가 비슷한 것 조회(similarity_search())

# 문서 조회
query = "오늘 낮 기온은?"   # 질문할 문장
k = 3                      # 유사도 상위 k 개 문서 가져오기.

result = database.similarity_search(query, k = k) #← 데이터베이스에서 유사도가 높은 문서를 가져옴
print(result)
print('-'*50)
for doc in result:
    print(f"문서 내용: {doc.page_content}")  # 문서 내용 표시

 

(2)까지는 정보 DB 형성 단계의 내용이고,

 

(3)은 위에서 말한 '질문을 받아 비슷한 내용을 DB에서 탐색'까지의 내용이다.

 

그런데 여기서, 대화 전체의 맥락을 고려하여 사용자에게 답을 보여주기 위해

모델이 들여다보는 장소가 있다. 그게 바로 메모리다.

 

 

 

 

 

 


 

 

 

 

 

 

4. 메모리


LLM이 세션 내에서 이루어진 대화를 기억해야, 맥락적 응답이 가능해진다.


이를 허용해주는 것이 ConversationBufferMemory() 클래스이다.

from langchain.memory import ConversationBufferMemory
 
# 메모리 선언하기(초기화)
memory = ConversationBufferMemory(return_messages=True)

 

ConversationBufferMemory()는 모델의 메모리 공간으로,

대화 내용을 저장하는 버퍼 역할을 한다.

 

일반적으로 아래의 매개변수 세 개를 통해 대화 내용을 효율적으로 저장한다.

 

# 대화 메모리 생성
memory = ConversationBufferMemory(memory_key="chat_history", input_key="question", output_key="answer",
                                  return_messages=True)

 

memory_key버퍼에 저장된 대화 기록을 나타내는 변수 이름을 지정한다.

여기서는 해당 변수를 'chat_history'로 지정한 것.

 

input_key사용자가 입력한 질문을 저장한다.

여기서는 'question' 변수에 질문 내용을 담았다.

 

output_key모델이 생성하는 응답을 저장한다.

여기서는 'answer' 변수에 담았다.

 

이후 모델은 해당 변수들을 계속 참조하며 문맥에 맞는 답을 생성한다.

 

 

 

 

 

 


 

 

 

 

 

 

 

 

5. Chain을 통한 LLM 생성

 

위에서 본 RAG flow를 다시 살펴보자.

지식 DB를 생성한다.

DB를 탐색할 retrieval와 사용할 LLM을 선언한다.

대화 메모리를 선언한다.

DB, LLM, 메모리를 묶어 Chain을 생성한다.

 

 

마지막 부분이랑 retrieval빼고 다 했다.

 

리트리버 이름이 귀엽다DB를 탐색하여 모델에게 전달하는 과정을 의미한다.

(retriever은 이 과정을 수행하는 객체를 뜻한다. 발음이 비슷하니 주의)

 

당연히 DB 선언하고 나서 선언한다.

 

그럼 지금 한 부분까지 코드로 살펴보자.

 

 


 

 

1. DB 생성

embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
database = Chroma(persist_directory = path + "db", embedding_function = embeddings)

 

 

2. retriever 및 모델 생성

chat = ChatOpenAI(model="gpt-3.5-turbo")
retriever = database.as_retriever()
qa = RetrievalQA.from_llm(llm=chat,  retriever=retriever,  return_source_documents=True )

 

DB에 as_retriever() 메서드를 적용하여 간단하게 retriever 객체를 생성한다.

 

RetrievalQA()는 RAG용 QA chat 함수이다.

 

매개변수 llm은 모델을, retriever는 retrieval 과정을 수행하는 모듈을 의미한다.

 

 

3. 메모리 생성

memory = ConversationBufferMemory(memory_key="chat_history", input_key="question", output_key="answer",
                                  return_messages=True)

 

자. 이까지 하면 이제 LLM을 RAG로 강화할 준비가 끝난 것이다.

 

이제 이 요소들을 한데 묶어 사용하는데, LangChain의 가장 중요한 역할이 이거다.

 

 

4. Chain으로 각 모듈 연결

# ConversationalRetrievalQA 체인 생성
qa = ConversationalRetrievalChain.from_llm(llm=chat, retriever=retriever, memory=memory,
                                           return_source_documents=True,  output_key="answer")

 

이건 뭐... 그냥 묶기만 하면 된다.

 

메모리에서 지정한 answer을 output_key로 지정해 줘야 하는 것은 주의.