개인과제 하면서 배운것들
참고로 일단 만들어 본 코드
import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
from langchain_openai import ChatOpenAI
from langchain.document_loaders import PyPDFLoader
client = ChatOpenAI(model = 'gpt-4o')
loader = PyPDFLoader('C:\Users\kevinkim\OneDrive\바탕 화면\Sparta_Work\초거대 언어모델 연구 동향.pdf')
docs = loader.load()
from langchain.text_splitter import RecursiveCharacterTextSplitter
recursive_text_splitter = RecursiveCharacterTextSplitter(
#chunk_size: 각 청크의 최대 길이
chunk_size=100,
#chunk_overlap: 청크 간 겹침의 길이
chunk_overlap=10,
#lenth_function: 텍스트 청크의 길이를 계산하는 데 사용하는 함수(기본적으로 len 함수 사용)
length_function=len,
#is_separator_regex: 지정한 구분자가 정규식인지 여부를 지정하는 플래그
is_separator_regex=False,
)
splitted_docs = recursive_text_splitter.split_documents(docs)
for i, splitted in enumerate(splitted_docs[:50]):
print(f'Chunk {i+1}:')
print(splitted)
print('=' * 50)
from langchain_openai import OpenAIEmbeddings
from uuid import uuid4
import faiss
from langchain_community.vectorstores import FAISS
embeddings = OpenAIEmbeddings(model = 'text-embedding-ada-002')
vector_store = FAISS.from_documents(documents = splitted_docs, embedding = embeddings)
uuids = [str(uuid4()) for _ in range(len(splitted_docs))]
vector_store.add_documents(documents = splitted_docs, ids = uuids)
#코드에서 FAISS를 retriever로 변환하는 것은, 문서 검색을 위한 기능을 제공하는 객체를 만드는 과정
retriever = vector_store.as_retriever(search_type = 'similarity', search_kwargs = {'k':1})
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
contextual_prompt = ChatPromptTemplate.from_messages([
('system', 'Give the exact answer of the following questions as well as the explanation'),
('user', 'Context: {context}\\n\\nQuestion: {question}')
])
class DebugPassThrough(RunnablePassthrough):
def invoke(self, *args, **kwargs):
output = super().invoke(*args, **kwargs)
print('Debug Output:', output)
return output
class ContexttoText(RunnablePassthrough):
def invoke(self, inputs, config = None, **kwargs):
context_text = '\n'.join([doc.page_content for doc in inputs['context']])
return {'context' : context_text, 'question' : inputs['question']}
rag_chain_debug = {
'context': retriever,
'question' : DebugPassThrough
} | DebugPassThrough() | ContexttoText()| contextual_prompt | client
while True:
print('='*50)
query = input('질문을 입력하세요:')
response = rag_chain_debug.invoke(query)
print('Final Response:')
print(response.content)
PyPDFLoader로 PDF 파일 로드하기
대략적인 코드
from langchain.document_loaders import PyPDFLoader # PDF 파일 경로 설정 pdf_path = "sample.pdf" # 여기에 PDF 파일 경로를 넣습니다. # PyPDFLoader로 PDF 파일 로드 loader = PyPDFLoader(pdf_path) documents = loader.load() # 출력: 문서 객체 리스트 (각 페이지가 하나의 문서 객체로 분리됨) for i, doc in enumerate(documents): print(f"Page {i + 1} content:") print(doc.page_content[:500]) # 처음 500자만 출력 print("\n" + "=" * 50 + "\n")
그런데 그냥 간단하게
loader = PyPDFLoader('C:\\Users\\kevinkim\\OneDrive\\바탕 화면\\Sparta_Work\\초거대 언어모델 연구 동향.pdf') docs = loader.load()
이렇게 썼다.
문서 청크로 나누기 (CharacterTextSplitter, RecursiveCharacterTextSplitter)
1. CharacterTextSplitter
CharacterTextSplitter는 텍스트를 일정한 길이나 규칙에 따라 분할하는 단순한 방법을 제공합니다. 이 방법은 일반적으로 텍스트를 특정 문자 수로 나누는 방식입니다.
주요 파라미터:
chunk_size: 텍스트를 나누기 위한 기준이 되는 문자 수입니다. 이 값은 텍스트가 분할되는 최대 크기입니다.
chunk_overlap: 텍스트 조각들 사이에 겹칠 부분을 설정하는 파라미터입니다. 겹침을 설정하면 분할된 텍스트 조각들 사이에 중복된 부분이 생기게 되어, 텍스트가 문맥을 유지할 수 있도록 합니다.
separator: 텍스트를 분할할 때 사용하는 구분자입니다. 기본값은 공백(' '), 하지만 다른 구분자를 사용할 수도 있습니다.
is_separator_regex: 텍스트 분할 시 구분자(separator)로 사용할 문자열이 정규 표현식(Regular Expression, Regex) 인지 여부를 결정하는 파라미터입니다.from langchain.text_splitter import CharacterTextSplitter text = "This is a long text that needs to be split into smaller chunks for easier processing." splitter = CharacterTextSplitter(chunk_size=10, chunk_overlap=2) chunks = splitter.split_text(text) print(chunks)
2. RecursiveCharacterTextSplitter
RecursiveCharacterTextSplitter는 CharacterTextSplitter와 유사하지만, 더 복잡한 분할 방식을 제공합니다. 이 클래스는 텍스트를 여러 단계로 분할합니다. 첫 번째 단계에서는 기본적으로 CharacterTextSplitter와 비슷한 방식으로 텍스트를 나누고, 나누어진 조각이 여전히 너무 길면 다시 재귀적으로 분할을 시도합니다. 이 방식은 텍스트가 지나치게 길어지지 않도록 하면서도 중요한 구분점이나 문단을 유지하려고 합니다.
주요 파라미터:
chunk_size: 각 텍스트 조각의 최대 길이입니다.
chunk_overlap: 텍스트 조각들 사이에 겹칠 문자의 수입니다.
length_function: 분할 기준이 되는 함수입니다. 기본적으로는 len()을 사용하지만, 특정 조건에 맞는 사용자 정의 함수를 설정할 수도 있습니다.
separator: 텍스트 분할 시 사용할 구분자입니다. 기본값은 공백입니다.
add_start_index: 각 텍스트 덩어리의 시작 인덱스를 추가할지를 결정합니다.
is_separator_regex: 텍스트 분할 시 구분자(separator)로 사용할 문자열이 정규 표현식(Regular Expression, Regex) 인지 여부를 결정하는 파라미터입니다.from langchain.text_splitter import RecursiveCharacterTextSplitter text = "This is a long text that will be split into smaller parts that may contain sections." splitter = RecursiveCharacterTextSplitter(chunk_size=10, chunk_overlap=2) chunks = splitter.split_text(text) print(chunks)
CharacterTextSplitter는 단순히 텍스트를 일정 크기(또는 조건)에 맞춰 나누고, 겹침 부분을 설정할 수 있는 간단한 분할기입니다.
RecursiveCharacterTextSplitter는 더 고급 기능을 제공하며, 텍스트를 여러 번 재귀적으로 나누어, 보다 효율적으로 텍스트를 다룰 수 있도록 설계되었습니다. 텍스트의 논리적인 구분을 고려하여 분할합니다.
여기서 "재귀적으로 나눈다"는 텍스트나 데이터를 여러 단계로 반복적으로 분할하는 과정을 의미합니다. 이 방식은 하나의 분할 기준을 적용한 후, 그 결과로 나온 각 부분이 여전히 기준을 초과하거나 적절하지 않으면, 다시 같은 기준을 적용하여 추가적으로 분할하는 방식입니다.
예를 들어:
첫 번째 단계에서 텍스트를 기본 분할 기준에 따라 나누고, 그 분할된 부분들이 여전히 너무 크거나 길다면, 그 부분들에 대해 다시 분할을 적용합니다.
이를 계속해서 반복하는 방식이 "재귀적인" 분할입니다. 즉, 한 번의 분할로 끝나는 것이 아니라, 결과물에 대해 다시 한 번 또는 여러 번 분할을 진행하는 것입니다.
예시
가장 간단한 예로, 긴 문서를 일정 크기(예: 1000자)로 나누는 경우를 생각해봅시다. 처음에 텍스트를 1000자씩 나누고, 만약 그 나뉜 텍스트가 여전히 너무 길면, 그 부분을 또 다시 나누는 방식입니다.
1단계 (첫 번째 분할):
긴 문서: "This is a long text that will be split into chunks."
기준: 한 덩어리의 최대 크기를 10자로 설정
첫 번째 분할 후: ["This is a", "long text", "that will", "be split", "into chunk", "s."]
2단계 (재귀적 분할):
첫 번째 단계에서 나눈 결과 중, "long text" 같은 부분은 여전히 10자보다 길어서 추가적으로 분할이 필요합니다.
다시 한번 기준을 적용하여 각 부분을 나눕니다.
두 번째 분할 후: ["This is a", "long", "text", "that will", "be split", "into chunk", "s."]
위의 예시처럼, 재귀적 분할은 텍스트나 데이터를 여러 차례에 걸쳐 나누는 방식입니다. 이를 통해 더 작은, 관리하기 쉬운 조각으로 데이터를 나눠서 처리할 수 있게 됩니다.
여기서 chunk와 token의 차이를 말하겠습니다.
Chunk와 Token은 둘 다 데이터나 텍스트를 분할하는 단위이지만, 그 의미와 사용 방식에서 차이가 있습니다. 주로 자연어 처리(NLP)에서 사용되는 개념들이며, 이 둘은 데이터 처리의 목적에 따라 다르게 사용됩니다.
1. Token
"Token"은 텍스트를 가장 작은 의미 단위로 나눈 것입니다. 단어, 구두점, 숫자 등 텍스트의 기본 구성 요소를 의미합니다. 자연어 처리에서 토큰화(tokenization)는 텍스트를 토큰으로 나누는 과정입니다.
예시:
텍스트: "I have a cat."
Token으로 나누면: ["I", "have", "a", "cat", "."]
여기서 "I", "have", "a", "cat", "." 각각은 토큰입니다. 각 토큰은 의미가 있을 수도, 없을 수도 있지만 텍스트를 처리하는 데 있어서 분리된 기본 단위로 간주됩니다.
토큰의 특징:
기본적인 텍스트의 단위: 단어, 구두점, 숫자 등
자연어 처리에서 중요: 텍스트 분석 및 모델 학습에서 기본 입력 단위로 사용됩니다.
문법적 분석: 텍스트를 문법적 단위로 분해하기 위해 사용됩니다.
2. Chunk
"Chunk"는 텍스트나 데이터에서 의미 있는 단위로 나누어진 부분을 의미합니다. 구문적 단위, 즉 문법적인 역할을 갖는 텍스트 조각으로, 주로 명사구(Noun Phrase), 동사구(Verb Phrase) 등과 같이 더 큰 의미 단위로 나누는 경우에 사용됩니다.
예시:
텍스트: "I have a big cat."
Token으로 나누면: ["I", "have", "a", "big", "cat", "."]
Chunk로 나누면: ["I", "have a big cat."] (명사구를 하나의 덩어리로 처리)
여기서 "have a big cat"이라는 명사구는 하나의 chunk로 묶여 있습니다. 이와 같이, chunk는 더 큰 의미 단위로 텍스트를 나누며, 주로 문법적 의미나 구조에 기반하여 나눕니다.
청크의 특징:
문법적인 단위: 명사구, 동사구 등으로 나눔
의미 기반: 더 큰 의미를 가진 조각으로 텍스트를 나눕니다.
문장 내 관계: 문장의 의미적 관계를 고려한 분할
토큰화(tokenization)는 텍스트를 기본적인 구성 요소(단어, 구두점 등)로 나누는 과정이고,
청크화(chunking)는 텍스트를 더 큰 의미를 갖는 구문적 단위로 나누는 과정입니다.
백터 임베딩 생성
from langchain_openai import OpenAIEmbeddings # OpenAI 임베딩 모델 초기화 embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
이건 그냥 간단히 백터 모델 객체 생성.
벡터 스토어 생성
import faiss from langchain_community.vectorstores import FAISS vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
어....?
그런데 이 부분 어딘가 닮았습니다.....맞습니다vector_store = FAISS( embedding_function=embeddings, index=index, docstore=InMemoryDocstore(), index_to_docstore_id={} )
그런데 왜 여기서는 저렇게 안 쓸까요?
현재 코드에서 FAISS.from_documents() 메서드를 사용하고 있기 때문에 index와 docstore를 명시적으로 정의하는 부분은 필요하지 않습니다. FAISS.from_documents()는 내부적으로 index와 docstore를 자동으로 생성하고 설정하기 때문입니다.
하지만, 만약 FAISS 인덱스를 수동으로 설정하거나, 커스터마이즈하려는 목적이라면, index와 docstore를 명시적으로 정의할 필요가 있습니다.
여기서 그때 썼던 index = faiss.IndexFlatL2(...) 부분은 벡터를 저장할 인덱스를 생성하는 코드입니다. FAISS는 벡터 검색을 효율적으로 하기 위해 index를 사용하며, 이 인덱스는 벡터를 저장하고 유사도를 계산하는 데 사용됩니다. 만약 FAISS.from_documents() 메서드를 사용한다면, 이 부분은 내부적으로 처리되기 때문에 따로 정의할 필요는 없습니다.
그렇다면, 아래 두 가지 코드:
vector_store = FAISS(...)
index = faiss.IndexFlatL2(...)
이 부분은 필요하지 않습니다, 왜냐하면 FAISS.from_documents()에서 이미 index와 docstore를 자동으로 설정하기 때문입니다.
결론적으로, 현재 코드에서는 FAISS.from_documents()만 사용하면 되고, index와 docstore를 명시적으로 정의하는 부분은 생략해도 됩니다.
둘 다 FAISS를 사용하여 벡터 저장소를 생성하고, 이를 검색에 활용하는 역할을 하지만, 각 방식의 목적과 사용 방식은 다릅니다.
FAISS(...)는 더 수동적이고 유연한 방식으로, 필요한 세부 설정을 직접 할 수 있습니다.
FAISS.from_documents()는 자동화된 방식으로, 문서 임베딩을 FAISS 벡터 저장소에 쉽게 추가할 수 있습니다.
그리고 from_documents()의 주요 매게변수들은
documents: 문서 객체 리스트 또는 원시 텍스트 리스트
embedding: 임베딩 함수 (보통 OpenAIEmbeddings 클래스)
index_type: FAISS 인덱스 유형 (선택적)
metric: 유사도 계산 방식 (선택적)
faiss_index: 기존 FAISS 인덱스 (선택적)
embedding_function: 사용자 정의 임베딩 함수 (선택적)
docstore: 문서 저장소 (선택적)
index_to_docstore_id: 문서 ID 매핑 (선택적)
texts: 원시 텍스트 (선택적)
이렇게 있습니다.
FAISS를 Retriever로 변환
자! 주목! 여기서부터는 RAG의 개념을 확실히! 아주 견고하게 잡고 가야합니다! 걱정마세요 RAG의 개념도 준비했습니다.
RAG
개념을 배워도 쓰기 힘든 녀석.....넌 뭐니?
RAG (Retrieval-Augmented Generation) 모델은 자연어 처리(NLP)에서 생성과 검색을 결합한 방식으로, 텍스트 생성 모델이 외부 지식 기반에서 정보를 검색하여 더 정확하고 풍부한 답변을 생성하는 방법입니다. 이를 통해 모델이 훈련된 데이터 외의 정보를 활용해 더 효과적인 결과를 낼 수 있습니다.
1. RAG의 기본 개념
RAG는 크게 검색(retrieval)과 생성(generation) 두 가지 부분으로 나누어집니다.
검색(Retrieval): 주어진 질문에 대해 모델이 외부의 문서나 데이터베이스에서 관련된 정보를 찾아냅니다.
생성(Generation): 검색된 정보를 바탕으로, 모델이 새로운 텍스트를 생성합니다.
이 모델은 텍스트 생성 모델(GPT 계열)과 검색 시스템(BERT 계열)을 결합하여, 더 넓고 다양한 정보를 기반으로 고차원적인 답변을 생성하는데 사용됩니다. 즉, RAG는 두 가지 모듈(검색 + 생성)을 함께 활용하여 더 뛰어난 성능을 발휘합니다.
여기서 검색과 생성에 대해 조금 더 자세히 알아보겠습니다.
1. 검색 (Retrieval)
RAG에서 검색 단계는 모델이 질문이나 입력 문장에 대해 관련 정보를 외부 데이터베이스나 문서에서 찾는 과정입니다. 검색의 목표는 모델이 적절하고 유용한 정보를 외부에서 효율적으로 찾아내는 것입니다.
검색의 주요 단계
질의 처리 (Query Processing):
모델이 사용자의 질문이나 입력 문장을 분석합니다. 예를 들어, "2024년 미국 대통령 선거 결과는?"이라는 질문을 받으면, 모델은 이 질문에서 중요한 정보를 추출합니다(예: "2024년", "미국", "대통령 선거", "결과").
검색:
모델은 이 추출된 키워드를 사용하여 외부 문서 저장소(예: 위키피디아, 뉴스 사이트, 특화된 데이터베이스 등)에서 관련된 정보를 검색합니다.
검색 방법은 두 가지로 나뉩니다:
전통적인 검색 방식 (Traditional Search): TF-IDF, BM25 같은 알고리즘을 사용하여 문서에서 키워드의 빈도나 중요도를 기준으로 문서를 정렬합니다.
딥러닝 기반 검색 방식 (Deep Learning-based Search): BERT나 Sentence-BERT와 같은 모델을 사용해 질의와 문서를 벡터화하여, 유사도를 기반으로 문서를 검색합니다. 이 방식은 더 정교하고 의미론적 유사성을 파악할 수 있어 효과적입니다.
검색 결과:
검색된 문서나 텍스트는 주어진 질문에 대한 유용한 정보를 담고 있습니다. 예를 들어, "2024년 미국 대통령 선거"에 관한 최신 뉴스나 관련된 데이터가 될 수 있습니다.
2. 생성 (Generation)
생성 단계는 모델이 검색된 정보를 바탕으로 새로운 텍스트를 만들어내는 과정입니다. 여기서 중요한 점은 모델이 검색된 정보에 기초하여 새로운 답변을 생성한다는 것입니다. 생성된 텍스트는 질문에 대한 자연스럽고 구체적인 답변이 될 수 있습니다.
생성의 주요 단계
정보 통합 (Information Integration):
모델은 검색된 문서들을 받아들여, 그 정보를 통합하여 가장 중요한 내용을 뽑아냅니다.
이때 모델은 단순히 문장을 그대로 복사하거나 나열하는 것이 아니라, 검색된 정보들을 조합하여 새로운 문맥에 맞는 답변을 생성합니다.
답변 생성 (Answer Generation):
모델은 언어 모델(예: GPT, T5 등)을 활용하여 문법적이고 의미론적인 자연어로 답변을 생성합니다.
이때, 생성 모델은 조건부 언어 모델로 작동하여 검색된 정보를 기반으로 답변을 만듭니다. 예를 들어, "2024년 미국 대통령 선거 결과는?"이라는 질문에 대해, 모델은 관련된 정보를 바탕으로 "2024년 미국 대통령 선거는 조 바이든이 재선에 성공했습니다"와 같은 자연스러운 텍스트를 생성합니다.
생성 결과:
생성된 텍스트는 사전 학습된 지식과 검색된 정보를 바탕으로 하여, 질문에 대한 적절하고 정보가 풍부한 답변을 제공합니다. 이 답변은 질문의 구체적인 요구에 맞추어 새로운 사실을 추가하거나 이전의 지식을 조합하여 제공합니다.
검색과 생성의 결합
RAG의 가장 큰 특징은 검색과 생성을 결합하여 성능을 극대화한다는 점입니다. 이 모델은 검색을 통해 외부 지식을 추가하고, 그 지식을 바탕으로 더 정확하고 풍부한 답변을 생성할 수 있습니다.
기존의 언어 모델은 주로 사전 학습된 데이터만을 바탕으로 답변을 생성합니다. 그러나 RAG 모델은 실시간으로 외부에서 정보를 가져와 그 정보를 반영하여 더 최신의, 정확한 답변을 제공할 수 있습니다.
예를 들어, 사전 학습된 모델만으로는 최신 뉴스나 업데이트된 정보를 반영하기 어렵지만, RAG는 외부에서 실시간으로 정보를 검색하여 이를 답변에 반영할 수 있습니다. 이를 통해 보다 정확하고 신뢰성 있는 답변을 제공할 수 있습니다.
그런데 전적으로 RAG는
RagRetriever.from_pretrained()과 as_retriever()이 자주 쓰입니다. 차이점이 뭘까요?
먼저 각각 예시를 하나씩 보겠습니다.
1. RagRetriever.from_pretrained() 사용 예시
from langchain_openai import ChatOpenAI from langchain.chat_models import ChatOpenAI from langchain.prompts import PromptTemplate from langchain.chains import LLMChain from langchain.chat_models import ChatOpenAI from langchain_community.retrievers import RagRetriever from langchain.vectorstores import FAISS from langchain.document_loaders import PyPDFLoader # OpenAI API 키 설정 import os import openai openai.api_key = os.getenv("OPENAI_API_KEY") # 문서 로드 (예시로 PDF 파일 사용) loader = PyPDFLoader('path_to_your_pdf_file.pdf') docs = loader.load() # FAISS 벡터 스토어 생성 from langchain.embeddings.openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings() vector_store = FAISS.from_documents(docs, embeddings) # RAG Retriever 설정 rag_retriever = RagRetriever.from_pretrained('facebook/rag-token-nq', vector_store=vector_store) # 질문에 대해 RAG 모델을 사용하여 답변 생성 query = "What are the key trends in large language models?" response = rag_retriever.run(query) print(response)
코드 설명:
OpenAI API 키 설정:
openai.api_key = os.getenv("OPENAI_API_KEY")는 OpenAI API 키를 환경 변수에서 가져옵니다. 이 키는 OpenAI의 모델을 호출하는 데 사용됩니다.
문서 로드:
PyPDFLoader('path_to_your_pdf_file.pdf')는 PDF 파일을 로드하는 데 사용됩니다. 이 예시에서는 PDF 파일을 로드하여 docs 변수에 저장합니다.
FAISS 벡터 스토어 생성:
OpenAIEmbeddings()는 OpenAI의 텍스트 임베딩 모델을 사용하여 문서를 벡터화합니다.
FAISS.from_documents(docs, embeddings)는 로드된 문서들을 벡터화하여 FAISS 벡터 스토어를 만듭니다. FAISS는 벡터 검색을 위한 라이브러리입니다.
RAG Retriever 설정:
RagRetriever.from_pretrained('facebook/rag-token-nq', vector_store=vector_store)는 사전 학습된 RAG 모델을 로드합니다. 여기서 'facebook/rag-token-nq'는 Hugging Face에서 제공하는 RAG 모델을 불러오는 부분입니다.
vector_store=vector_store는 FAISS 벡터 스토어를 사용하여 검색을 수행하게 설정합니다.
질문에 대해 답변 생성:
rag_retriever.run(query)는 사용자가 입력한 query(예: "What are the key trends in large language models?")에 대해 RAG 모델을 사용하여 검색된 문서들을 기반으로 답변을 생성합니다.
결과는 response로 출력되며, 최종적으로 print(response)로 답변을 출력합니다.
RagRetriever.from_pretrained()의 특징
자동화된 검색과 생성: RAG 모델은 검색과 답변 생성을 자동으로 수행합니다. 이 방법은 RAG 모델을 직접 설정하고, 내부적으로 검색을 진행하며, 검색된 정보를 바탕으로 답변을 생성하는 전체 파이프라인을 제공합니다.
간편한 설정: RagRetriever.from_pretrained()를 사용하면, 복잡한 설정 없이 RAG 모델을 빠르게 사용할 수 있습니다. Hugging Face에서 제공하는 모델을 사용하기 때문에, 별도의 벡터화나 검색 알고리즘 설정이 필요 없습니다.
2. as_retriever() 사용 예시from langchain_openai import ChatOpenAI from langchain.document_loaders import PyPDFLoader from langchain.embeddings.openai import OpenAIEmbeddings from langchain.vectorstores import FAISS from langchain.prompts import ChatPromptTemplate from langchain.chains import LLMChain # OpenAI API 키 설정 import os import openai openai.api_key = os.getenv("OPENAI_API_KEY") # 문서 로드 (예시로 PDF 파일 사용) loader = PyPDFLoader('path_to_your_pdf_file.pdf') docs = loader.load() # FAISS 벡터 스토어 생성 embeddings = OpenAIEmbeddings() vector_store = FAISS.from_documents(docs, embeddings) # 검색 기능 설정 (as_retriever) retriever = vector_store.as_retriever(search_type='similarity', search_kwargs={'k': 1}) # GPT 모델 설정 (생성) client = ChatOpenAI(model="gpt-4") # 질문 응답을 위한 프롬프트 설정 contextual_prompt = ChatPromptTemplate.from_messages([ ('system', 'Provide an accurate answer to the following question based on the context'), ('user', 'Context: {context}\\n\\nQuestion: {question}') ]) # 질문을 받아서 답변 생성 query = "What are the key trends in large language models?" context = retriever.retrieve(query) # 검색된 문서 # 프롬프트에 문맥과 질문을 넣고 답변 생성 llm_chain = LLMChain(prompt=contextual_prompt, llm=client) response = llm_chain.run({"context": context, "question": query}) print(response)
코드 설명:
OpenAI API 키 설정:
openai.api_key = os.getenv("OPENAI_API_KEY")는 OpenAI API 키를 환경 변수에서 가져와 설정합니다.
문서 로드:
PyPDFLoader('path_to_your_pdf_file.pdf')는 PDF 파일을 로드하는 데 사용됩니다. 로드된 문서는 docs에 저장됩니다.
FAISS 벡터 스토어 생성:
OpenAIEmbeddings()는 OpenAI의 임베딩 모델을 사용하여 문서들을 벡터화합니다.
FAISS.from_documents(docs, embeddings)는 로드된 문서를 벡터화하여 FAISS 벡터 스토어를 만듭니다.
검색 기능 설정 (as_retriever()):
vector_store.as_retriever(search_type='similarity', search_kwargs={'k': 1})는 FAISS 벡터 스토어에서 가장 유사한 문서를 검색하는 retriever를 설정합니다. k=1로 설정하여 가장 유사한 문서 1개를 검색합니다.
GPT 모델 설정:
client = ChatOpenAI(model="gpt-4")는 GPT 모델을 설정합니다. 이 모델은 생성 모델로 사용되어 질문에 대한 답변을 생성합니다.
프롬프트 설정:
ChatPromptTemplate.from_messages()는 GPT 모델에 전달할 프롬프트 템플릿을 설정합니다. 사용자 입력을 Context와 Question으로 나누어 전달합니다.
검색 및 생성 파이프라인:
retriever.retrieve(query)는 사용자의 질문에 대해 FAISS 벡터 스토어에서 검색을 수행하고, 해당 문서를 반환합니다.
.retrieve()는 검색 작업을 담당하는 함수입니다. 이 함수는 사용자가 제공한 query(질문 또는 요청)를 바탕으로, 관련 있는 문서나 정보를 벡터 스토어나 데이터베이스에서 찾아 반환하는 역할을 합니다.
retrieve 함수는 질문에 가장 관련성이 높은 문서나 텍스트를 벡터 스토어(예: FAISS, Chroma 등)에서 검색하여, 그 결과를 context로 반환합니다. 이 context는 나중에 모델이 답변을 생성할 때 사용할 수 있는 배경 정보가 됩니다.
retrieve가 수행하는 과정은 대체로 다음과 같습니다:
벡터화: 주어진 질문(query)을 먼저 벡터로 변환합니다. 이를 통해 질문을 수치화된 표현으로 바꿔 벡터 스토어와 비교할 수 있게 됩니다.
유사도 검색: 벡터 스토어에서 저장된 문서들의 벡터들과 사용자의 질문 벡터를 비교하여, 가장 유사한 문서를 찾습니다. 이때 유사도 측정 방식으로는 보통 코사인 유사도나 유클리드 거리 등을 사용합니다.
상위 문서 반환: retrieve는 유사도가 가장 높은 문서들을 k개 반환합니다. 이 문서들이 바로 context가 됩니다. 사용자는 이 문서들을 바탕으로 답변을 생성하게 됩니다.
llm_chain.run()은 context(검색된 문서들)와 question(사용자의 질문)을 GPT 모델에 전달하여 최종적인 답변을 생성합니다.
파이프라인
파이프라인은 데이터 처리의 흐름을 설정하고, 이를 통해 복잡한 작업을 효율적으로 관리할 수 있도록 합니다.
1. 파이프라인의 기본 개념
파이프라인은 여러 작업을 순차적으로 연결한 흐름으로, 각 작업은 독립적인 처리를 담당하면서 이전 작업의 결과를 받아서 이어지는 작업을 수행합니다. 이를 통해 복잡한 프로세스를 체계적으로 관리할 수 있습니다.
예를 들어, 문서 검색 및 질문 응답 시스템에서의 파이프라인은 다음과 같은 단계로 구성될 수 있습니다:
문서 로딩: 다양한 형식의 문서(예: PDF 파일)를 읽어들이는 작업.
문서 임베딩: 텍스트 데이터를 벡터로 변환하여 계산을 용이하게 하는 작업.
문서 검색: 사용자의 질문에 가장 관련 있는 문서를 검색하는 작업.
질문 처리 및 답변 생성: 검색된 문서를 바탕으로 사용자의 질문에 대한 답변을 생성하는 작업.
이 과정이 하나의 파이프라인을 이루게 되는 것입니다. 각 단계는 독립적으로 존재하지만, 전체 파이프라인을 통해 순차적으로 처리되므로 각 단계의 출력을 다음 단계의 입력으로 전달합니다.
2. 파이프라인을 사용한 예시: RAG 모델
RAG 모델에서 파이프라인을 사용한다면, 기본적으로 검색과 생성을 순차적으로 처리하는 구조입니다. 이를 통해 질문에 대한 답변을 찾고, 생성하는 전반적인 과정을 자동화할 수 있습니다.
RAG 모델 파이프라인 예시:
문서 로딩 및 임베딩:
먼저, PDF나 텍스트 문서를 로드하고 이를 임베딩(벡터화)합니다. 이 벡터화된 문서들은 FAISS와 같은 벡터 스토어에 저장됩니다.
문서 검색:
사용자가 질문을 입력하면, 벡터 스토어에서 해당 질문과 가장 유사한 문서를 검색합니다. 이때, 유사도 검색을 통해 질문과 관련 있는 문서를 찾아냅니다.
답변 생성:
검색된 문서들을 바탕으로, 질문에 대한 답변을 생성하는 모델(예: GPT-4)을 호출하여 최종 답변을 만듭니다.
이 각 단계는 순차적으로 연결되어 실행되며, 이 전체 과정이 하나의 파이프라인으로 볼 수 있습니다.
3. 파이프라인을 어떻게 구성하냐에 따른 차이점
단계별 분리 (As_retriever 방식):
as_retriever() 방식에서는 검색과 생성을 명확히 분리할 수 있습니다. 즉, 검색을 담당하는 retriever와 답변 생성을 담당하는 생성 모델을 별도로 관리할 수 있습니다.
이 방식은 각 단계(검색, 생성)를 독립적으로 제어하고 최적화할 수 있습니다.
자동화된 결합 (RagRetriever.from_pretrained() 방식):
RagRetriever.from_pretrained()에서는 검색과 생성이 하나의 자동화된 파이프라인으로 결합되어 있습니다. 즉, 검색과 생성이 내부적으로 처리되므로, 사용자가 개별 단계에서의 세부적인 제어 없이 하나의 모델만으로 두 가지 작업을 동시에 처리할 수 있습니다.
4. 파이프라인이 중요한 이유
파이프라인을 사용하는 이유는 작업의 흐름을 체계적으로 관리하고, 각 단계를 독립적으로 개선하거나 최적화할 수 있기 때문입니다. 예를 들어:
검색 단계에서 사용되는 임베딩 모델을 변경할 수 있습니다.
생성 모델을 바꿔서 더 나은 답변을 생성할 수 있습니다.
각 단계에서 문서의 선택적 처리를 추가하거나, 전처리를 통해 성능을 개선할 수 있습니다.
이처럼 파이프라인을 통해 각 단계에서 모듈화된 작업을 분리하고, 이를 조합하여 복잡한 시스템을 효율적으로 구축할 수 있습니다.
5. 파이프라인 예시: RAG 모델
RAG 모델을 예로 들면, 파이프라인은 다음과 같은 흐름으로 구성될 수 있습니다:
문서 로드 → 문서 임베딩 → 벡터 스토어에 저장 → 검색
검색된 문서 → 질문 → 생성 모델 호출 (예: GPT-4)
생성된 답변 → 출력
# 검색 + 생성 파이프라인 예시 # 1. 문서 로드 및 임베딩 documents = load_documents("your_file.pdf") embeddings = generate_embeddings(documents) vector_store = store_embeddings(embeddings) # 2. 문서 검색 query = "What are the trends in AI?" search_results = search_documents(vector_store, query) # 3. 생성 모델을 통한 답변 생성 response = generate_answer(search_results, query) # 최종 응답 출력 print(response)
여기서 각 함수(load_documents, generate_embeddings, search_documents, generate_answer)는 각각의 파이프라인 단계에 해당합니다. 각 단계는 순차적으로 실행되며, 중간 결과는 다음 단계에 입력으로 전달됩니다.
프롬프트(Prompt)란 무엇인가?
프롬프트는 모델에게 특정 작업을 수행하도록 지시하는 텍스트 또는 메시지를 의미합니다. 예를 들어, 언어 모델인 GPT-4에게 주는 명령이나 질문이 바로 프롬프트입니다. 모델은 이 프롬프트를 기반으로 답변을 생성하거나 특정 작업을 수행합니다.
프롬프트의 역할:
모델에게 무엇을 해야 하는지를 알려줍니다.
모델이 어떤 형식으로 출력을 생성해야 할지를 정의합니다.
주어진 문맥에 따라 적절한 답변을 생성하도록 도와줍니다.
프롬프트의 예시:
단순한 프롬프트: "서울의 날씨는 어때?" (이 프롬프트는 모델에게 서울의 날씨에 대한 정보를 묻습니다.)
명확한 지시가 있는 프롬프트: "서울의 날씨를 바탕으로 내일의 우산 필요 여부를 알려줘." (이 프롬프트는 모델에게 날씨 정보뿐만 아니라 추가적인 판단을 요구합니다.)
2. 프롬프트에 문맥과 질문을 넣는 방식
프롬프트에 문맥과 질문을 넣는 방식은 모델이 보다 정확하고 일관된 답변을 생성할 수 있도록, 주어진 문맥(context)과 질문(question)을 하나의 프롬프트에 잘 결합하는 방식입니다.
왜 문맥과 질문을 하나의 프롬프트에 넣는가?
문맥은 모델에게 제공할 배경 정보를 제공합니다. 예를 들어, 사용자가 질문하는 주제에 대한 관련 정보를 제공하여 모델이 보다 구체적이고 관련성 높은 답변을 하도록 합니다.
질문은 모델에게 답변을 생성할 특정 요청을 전달합니다. 사용자가 어떤 정보나 결과를 원하는지 명확히 전달합니다.
두 가지를 결합하여 프롬프트를 설계함으로써, 모델이 더 정확하고 관련성 높은 답변을 생성할 수 있습니다.
from langchain.prompts import ChatPromptTemplate context = "The recent advancements in artificial intelligence include major breakthroughs in large language models, including models like GPT-4." question = "What are the implications of GPT-4 for natural language processing?" # 프롬프트 생성 contextual_prompt = ChatPromptTemplate.from_messages([ ('system', 'Give the exact answer based on the following context.'), ('user', 'Context: {context}\\n\\nQuestion: {question}') ]) # 모델 실행 (예: GPT-4) response = client.invoke({"context": context, "question": question}) print(response)
이 프롬프트의 흐름:
문맥(context): 모델에게 주어진 배경 정보입니다. 여기서는 AI와 GPT-4의 발전에 대한 간단한 설명을 제공합니다.
질문(question): 사용자가 원하는 정보나 답변을 요청합니다. 이 예시에서는 "GPT-4가 자연어 처리에 미치는 영향"에 대한 질문을 포함합니다.
프롬프트 구성:
시스템 메시지: "Give the exact answer based on the following context" (주어진 문맥을 기반으로 정확한 답변을 제공하라는 지시)
사용자 메시지: "Context: {context}\n\nQuestion: {question}" (문맥과 질문을 포함한 메시지)
이 프롬프트는 모델에게 문맥을 먼저 제공하고, 그 문맥을 바탕으로 사용자의 질문에 대한 답변을 생성하도록 요청합니다.
예시:context = """ Artificial Intelligence (AI) has seen rapid advancements in the past decade. Some of the key trends include improvements in reinforcement learning, transformer architectures, and large pre-trained models like GPT-3 and GPT-4. These models have significantly improved the accuracy and fluency of generated text, enabling applications in various fields such as healthcare, customer service, and entertainment. """ question = "How has GPT-4 contributed to the advancement of AI?" # 프롬프트 생성 contextual_prompt = ChatPromptTemplate.from_messages([ ('system', 'Use the following context to answer the user\'s question.'), ('user', 'Context: {context}\\n\\nQuestion: {question}') ]) # 모델 실행 (예: GPT-4) response = client.invoke({"context": context, "question": question}) print(response)
문맥(context): AI의 발전에 관한 더 구체적이고 깊이 있는 설명을 제공합니다. 이 문맥은 GPT-4와 같은 모델의 발전을 포함하여 AI의 다양한 진전 사항을 다룹니다.
질문(question): "GPT-4가 AI 발전에 어떻게 기여했는가?"라는 구체적인 질문을 포함시켜, 모델이 GPT-4의 기여에 대한 답변을 정확히 할 수 있도록 합니다.
프롬프트 구성:
시스템 메시지: "Use the following context to answer the user's question" (사용자의 질문에 답변하기 위해 주어진 문맥을 사용하라는 지시)
사용자 메시지: "Context: {context}\n\nQuestion: {question}" (문맥과 질문을 하나의 메시지에 통합)
이 방식은 문맥과 질문을 명확히 구분하여 전달하며, 모델이 문맥을 기반으로 정확한 답변을 생성할 수 있도록 유도합니다.
프롬프트에 문맥과 질문을 넣는 이유
정확한 답변 생성: 문맥을 제공함으로써 모델은 질문에 대한 답변을 구체적이고 정확하게 할 수 있습니다. 예를 들어, "AI 발전의 최근 동향은?"이라는 질문에 단순히 GPT-4에 대해 알려주지 않고, AI 발전의 큰 그림을 먼저 설명한 후 그 안에서 GPT-4의 기여를 명확히 파악할 수 있게 됩니다.
정보의 일관성 유지: 문맥을 추가하면 모델이 질문에 대해 일관성 있고 신뢰할 수 있는 답변을 생성할 수 있습니다. 질문이 문맥에 부합하는지 확인하고, 그 문맥에 맞게 답변을 조정할 수 있기 때문입니다.
질문을 더 잘 이해하게 함: 질문만 제공하는 것보다는, 질문에 대한 배경 정보가 포함되면 모델이 질문의 의도를 더 잘 이해하고, 보다 관련성 높은 답변을 생성할 수 있습니다.
프롬프트의 활용 예시
정보 검색 시스템: 사용자가 문서나 웹 페이지에서 특정 정보를 찾도록 요청할 때 문맥과 질문을 결합하여 모델이 정확한 부분을 찾아서 답변하도록 합니다.
대화형 시스템: 챗봇과 같이 대화형 시스템에서 사용자가 계속해서 질문을 할 때, 문맥을 바탕으로 이전 대화를 기억하고 이어서 답변을 생성하도록 할 수 있습니다.
텍스트 생성: 예를 들어, 모델에게 특정 주제에 대한 글을 작성하도록 할 때, 주어진 문맥을 바탕으로 생성된 텍스트가 관련성 있고 흐름이 자연스럽도록 합니다.
나머지는 내일로..