llm 프로그램을 작성하기 위해 크게 두 개의 오픈소스 라이브러리가 있다. 바로 langchain과 llamaindex이다.
한 블로그에서 이 두 라이브러리 중 하나를 택하는 기준을 다음과 같이 제시한 적이 있다. (참고: https://datasciencedojo.com/blog/llamaindex-vs-langchain/ )
- langchain : llm query 자주 바꾸고 계속해서 바뀌는 실험을 해볼 때 추천. performance tuning 할 때.
- llamaindex : 검색이 중요한 application을 만들 고 싶을 때 추천. 큰 규모의 데이터셋을 처리하고 throughput이 중요할 때.
내 식대로 번역하자면 프롬프트 엔지니어링 이것저것 해보고 그냥 이렇게 했다 저렇게 했다 왔다갔다 할 것 같을 땐 langchain을,
활용해야 할 (RAG에 넣을) 데이터셋이 정해져 있고 데이터의 규모가 클 때는 llamaindex를 시작점으로 잡는 게 좋은 듯하다.
따라서 오늘은 langchain을 통해 RAG를 하는 튜토리얼을 가볍게 작성해 보고자 한다.
1. 왜 RAG를 쓰는가
먼저 이 단락을 읽어보신 후에, 본인이 굳이 RAG 없이도 llm 을 사용할 수 있을 것 같다고 한다면 아래 retrieval 쪽은 생략하셔도 될 듯하다.
RAG를 쓰는 이유는 간단하다. llm에게 생각하고 말하게 하고 싶은 추가적인 데이터가 존재하는 경우다.
아래의 경우들은 RAG가 필요 없다.
1. llm이 특정 포맷으로 output을 뱉었으면 좋겠어.
이 부분은 그냥 llm 프롬프트를 작성할 때, 어떤 output을 뱉기를 원하는지 잘 설명해서 작성하면 된다.
또는 langchain의 경우 OutputParser를 추가할 수 있다. 이부분은 아래에서 다시 다룰 것이다.
2. 내 데이터를 활용해서 llm이 질답을 잘 했으면 좋겠어. (여기 까지라면 RAG가 필요해 보인다.) 하지만 데이터가 프롬프트에 바로 다 넣어도 될 정도로 작아.
openai api를 사용하는 경우, prompt에 넣을 수 있는 토큰 수는 무려 128,000 개다! 한국어로만 이루어진 데이터라 할 경우 약 6000자는 바로 input으로 줄 수 있단 것이다.
만약 내 데이터가 6000자 언저리라면 고민해볼 만하다. 프롬프트에 데이터를 정제해서 넣고자 한다면 RAG를 통해 필요한 부분의 데이터만 retreival(검색)해서 input query로 추가해 줄 수 있기 때문이다.
(참고: https://platform.openai.com/docs/models/gpt-4o )
위 2가지 경우에 해당한다면, 활용하고자 하는 데이터를 vector db에 넣어 retrieval하는 수고를 하지 않아도 된다.
2. Langchain chain 만들기
(참고: 랭체인 github: https://github.com/langchain-ai/langchain )
(참고: 랭체인 공식 페이지: https://www.langchain.com/)
랭체인을 활용해 llm을 활용하는 방법은 간단하다.
1) input으로 들어갈 질문, 2) llm이 참고할 내 데이터, 3) 원하는 output 구조를 넣어 llm에 넣으면 된다.
차근차근 코드 블럭과 함께 설명해 보겠다.
llm 로드하기
먼저 openai API를 활용해 llm을 로딩한다. gpt4o가 비용적으로도, 성능적으로도 현시점(24년 8월에 글을 작성 중이다.)에서 가장 최선이기 때문에 이걸 사용한다.
#installation
### langchain을 먼저 install한다.
### openai api도 사용할 것이기 때문에 langchain-openai 도 설치한다.
! pip install langchain langchain-openai
import os
os.environ['OPENAI_API_KEY']="" #가지고 있는 openai api key를 입력한다.
#gpt4o mini를 사용해보자.
llm = ChatOpenAI(model="gpt-4o-mini", temperature = 0)
prompt template , output parser
랭체인에선 prompt template를 지원해준다. llm에서의 프롬프트는 크게 두 개로 나뉜다. 1) system(instruction) prompt와 2) user prompt다.
llm에게 어떤 걸 내가 원하는지, 어떤 식으로 답변하기 원하는지를 적어주는 부분이 system prompt라고 볼 수 있다. 예를 들어 '너는 초등학교 선생님을 도와주는 llm이야.' 라든가, '너는 항상 한국어, 존댓말로만 대답하는 llm이야'가 여기에 해당한다.
user prompt는 우리가 원하는 질문을 넣어주는 부분이다. PromptTemplate를 통해 system prompt를 고정하고 질문을 갈아낄 수 있다. 아래 코드 예시에서는 {query}가 여기에 해당한다.
아이들의 친절한 수학 선생님 llm을 예시로 만들어보겠다.
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate(
template="You are a wonderful math teacher for kids. Given the question below, Answer the question politely. Question: {query}",
input_variables=["query"],
)
만약 추가적으로 llm에게 줄 데이터가 있다면 그 부분도 템플렛에 변수로 지정해주자. 템플렛의 어디에 데이터를 넣을지 수정하고, input_variable에 내 데이터 변수를 추가해 주면 된다. 코드 예시에서는 context라는 변수명을 사용하고 있다.
llm에게 6학년 학생들의 수학 시험 예시들을 데이터로 추가해준다고 가정했다.
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate(
template="You are a wonderful math teacher for kids. Here is the 6th grade kids exam examples: {context}\n Given the question below, Answer the question politely. Question: {query}",
input_variables=["query", "context"],
)
llm 질문에 대해서 항상 정해진 포맷으로 대답을 했으면 좋겠다면 outputparser를 커스텀할 수 있다.
나는 llm이 학생이 질문한 수학문제의 답변과, 질문이 쉬운 문제인지 어려운 문젠지를 판단해서 output으로 주길 바랐다. 그래서 ExamParser를 구현했다.
Parser는 Field를 반드시 포함해야 한다.
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
class ExamParser(BaseModel):
answer: str = Field(description="the answer of the question")
question_rank: str = Field(description="학생이 질문한 question의 난이도. 1점부터 5점까지로 표현하며, 5점이 최고로 어려운 질문인 것.")
parser = JsonOutputParser(pydantic_object=ExamParser)
이제 llm chain을 만들 수 있다! 위에서 만든 prompt, output parser를 다같이 활용해서 llm에게 질문하는 코드를 완성했다.
#전체 chain 코드.
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
class ExamParser(BaseModel):
answer: str = Field(description="the answer of the question")
question_rank: str = Field(description="학생이 질문한 question의 난이도. 1점부터 5점까지로 표현하며, 5점이 최고로 어려운 질문인 것.")
#output parser를 정의해 주었다.
parser = JsonOutputParser(pydantic_object=ExamParser)
prompt = PromptTemplate(
template="You are a wonderful math teacher for kids. Here is the 6th grade kids exam examples: {context}\n Given the question below, Answer the question politely.\n {format_instructions}\n Question: {query}",
input_variables=["query", "context"],
partial_variables={"format_instructions": parser.get_format_instructions()},
#prompt의 Partial_variables를 통해 output format에 대한 정보를 넣어준다.
)
question = '3 더하기 6이 뭐지?'
mydata = '초등학교 6학년들은 선형대수, 공학수학, 미적분을 배운다.'
# 프롬프트, llm 모델, output parser를 엮어서 chain 을 완성했다.
chain = prompt | llm | parser
llm_answer = chain.invoke({"query": question, "context": mydata })
#llm_answer는 {'answer': '', 'question_rank': ''} 의 형태로 return될 것이다.
print(llm_answer)
3. langchain RAG tutorial
위에서 만든 llm chain은 수학문제를 물어보는 간단한 llm이었다. 만약 아마존의 올해 8월 1일에 나온 최신 IR 자료를 분석하는 주식분석가 llm을 만들고 싶다면? gpt4o가 아무리 빠르게 학습했어도 열흘 전 데이터가 들어가 있을 것 같진 않으니, RAG에 넣을 데이터로 선정해 보겠다.
나를 슬프게 만든(...) amazon의 기업분석을 시키기 위해 위에서 다운받은 pdf를 vector db에 넣고 retrieval 해서 대답하는 llm을 예제로 만들어보겠다.
data loading
langchain에서 바로 지원하는 db중 chroma를 사용해 보겠다.
db에 저장할 때 벡터, 즉 임베딩 형태로 데이터를 저장하기 때문에 임베딩용 모델을 load 해준다. 만약 sbert 같은 다른 모델을 사용하고 싶다면 그래도 전혀 상관없다.
from langchain_openai import OpenAIEmbeddings
# vector db와 호환되는 임베딩 모델 사용할 수 있음.
embeddings = OpenAIEmbeddings(model='text-embedding-3-small')
이제 데이터를 잘라볼 차례다. IR 자료가 pdf 형태기 때문에 PDF loader를 이용했다. 이 외에도 랭체인에선 엑셀을 위한 UnstructedExcelLoader, json 데이터용 JsonLoader, csv 용 등등을 지원한다.
! pip install pypdf
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader('/content/AMZN-Q2-2024-Earnings-Release.pdf')
docs = loader.load()
print(len(docs))#12 -> pdf파일에 총 12장이 있었다.
만약 아마존 말고 다른 기업 IR자료들도 넣고 싶다면, docs에 append 해주면 된다.
#만약 아마존뿐 아닌 원하는 여러 기업들의 데이터를 사용한다면?
other_irs = PyPDFLoader('어떤다른기업.pdf')
other_docs = other_irs.load()
docs.extend(other_docs)
text splitter, chroma vector db
이제 docs에 저장된 ir 자료를 잘라볼 것이다.
데이터를 잘라서 texts라는 변수에 저장할 건데, 이 texts가 하나의 vector가 되어 vector db에 저장될 것이다.
docs의 데이터를 어떻게 자를지에 대한 text_splitter도 langchain에서 지원해 준다.
이번 예시에서는 RecursiveCharacterTextSplitter를 사용하고, 글자수가 1000개에 가깝게 하나의 덩어리로 잘리게 해 보겠다. 저장되는 text가 이상하게 잘리면 문맥이 이상해질 수 있으니, 20자씩 겹치게 잘라본다.
! pip install langchain-text-splitters
from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=20)
texts = text_splitter.split_documents(docs)
정성껏 자른 texts를 vector db에 넣어주자. chroma를 사용하기로 했으니, chroma에 넣어준다.
! pip install langchain_chroma
from langchain_chroma import Chroma
db = Chroma.from_documents(texts, embeddings)
vector db에 데이터를 넣었다!
retrieval
retrieval이란 내 llm의 질문에 맞게 vector db에 검색을 해서, 가장 유사한 text(아까 textsplitter로 잘라서 만든 text.)를 return 하는 작업이다.
벡터 db에 데이터를 넣었다면 retrieval은 금방이다. 아래 예시 코드를 보자.
# db 검색을 위한 retreiver.
retriever = db.as_retriever()
my_question = '아마존 당기순이익이랑 주당순이익이 어때?'
docs = retriever.invoke(my_question)
print(len(docs)) #총 4개의 데이터 덩어리를 반환했다. db retriever가 반환하는 default 문서수가 4이기 때문.
검색된 데이터가 총 4건이다. default 문서수가 4개이기 때문이다. 4개 중 하나만 까서 어떻게 생겼는지 확인해 보자.
print(docs[0])
#Document(metadata={'page': 4, 'source': '/content/AMZN-Q2-2024-Earnings-Release.pdf'},
# page_content='Total non-operating income (expense) (118) 573 (773) (1,751) \nIncome before income taxes 7,563 15,245 11,682 28,228 \nProvision for income taxes (804) (1,767) (1,752) (4,234) \nEquity-method investment activity, net of tax (9) 7 (8) (78) \nNet income $ 6,750 $ 13,485 $ 9,922 $ 23,916 \nBasic earnings per share $ 0.66 $ 1.29 $ 0.97 $ 2.30 \nDiluted earnings per share $ 0.65 $ 1.26 $ 0.95 $ 2.24 \nWeighted-average shares used in computation of earnings per \nshare:\nBasic 10,285 10,447 10,268 10,420 \nDiluted 10,449 10,708 10,398 10,689'),
아하, page_content에 내용이 들어가 있음을 확인할 수 있다.
이제 retreival 한 데이터도 llm에 넣어 활용해 보자.
llm rag tutorial 전체 코드
이번엔 question에 이번 분기 아마존의 주당순이익을 물어보았다. 굳이 output parser는 필요 없을 듯해서 뺐다.
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
prompt = PromptTemplate(
#은퇴한 월가의 영웅이라고 LLM을 가스라이팅 해주자..!
template="You are entired legendary IR analysist. You were the best fund manager. Answer the Question accurately. Use korean please. Here is the current IR reports, you must use this report only!: {context}. \n Question : {query}",
input_variables=["query", "context"],
)
question = '아마존의 이번 주당순이익은?'
#rag로 retrieval한 데이터들을 사용하는 부분.
mydata = '\n'.join(doc.page_content for doc in docs)
# 프롬프트, llm 모델 2개를 엮어서 chain 을 완성했다.
chain = prompt | llm
llm_answer = chain.invoke({"query": question, "context": mydata })
print(llm_answer)
#content='아마존의 이번 주당순이익(기본 주당순이익)은 $0.66입니다.'
(llm_answer가 정답인지 아닌지는 아직 찾아보지 않았다.)
오늘은 langchain으로 llm chain을 만들어보는 과정을 전체적으로 훑어보았다. 원하는 llm application을 재밌게 만들어보신다면 좋겠다:)
'머신러닝 > 약간 덜매운맛' 카테고리의 다른 글
Gaussian, Bernoulli로 이해하는 머신러닝 (0) | 2024.08.24 |
---|---|
LLM 학습 개요 - pretrain vs finetuning (0) | 2024.05.31 |
구현체를 통해 PEFT lora 를 알아보자 (0) | 2024.03.17 |
sklearn SVM(Support Vector Machine) 가이드 (0) | 2023.07.23 |
transformer 구현, pytorch 공식 코드로 알아보기 (0) | 2023.07.15 |