LangChain 의 대표적인 Components

  • Prompts:
    • Prompt Templates
    • Output Parsers: 5+ implementations
      • Retry/Fixing Logic
    • Example Selectors: 5 + implementations
  • Models:
    • LLM: 20 + Integrations
    • Chat Models
    • Text Embedding Model: 10 + Integrations
  • Indexes:
    • Document Loaders: 50 + Implementations
    • Text Splitters: 10 + implementations
    • Vector Stores: 10 + Integrations
    • Retrievers: 5 + Integrations / Implementations
  • Chains:
    • Can be used as build blocks for other chains
    • More application specific chains: 20 + different types
  • Agents:
    • Agent Types: 5 + types
      • Algorithms for getting LLMs to use tools
    • Agent Tookits: 10 + implementations
      • Agent armed with specific tools for a specific application

 

 

Q) LangChain 에서 Document 를 Vector Store 에 저장하기 위해서는 의미있는 단위인 Chunk 들로 분리해야해?

 

맞다.

 

LangChain에서 Document를 Vector Store에 저장할 때, 큰 문서를 의미 있는 단위로 분리하는 것이 일반적이다.

 

이러한 단위는 "Chunk"라고 부르며, 이를 사용하여 검색이나 임베딩과 같은 작업을 효율적으로 수행할 수 있다.

 

Chunk 로 분리하는 이유는 다음과 같다:

  • 효율적인 검색: 큰 문서 전체를 벡터화하는 것보다 작은 단위로 분리하면 검색이 더 효과적이니까 토큰의 수를 생각해봐라. Chunk를 통해 특정 주제나 키워드에 대한 검색 결과를 더 정확하게 얻을 수 있다.
  • 메모리 관리: 대형 문서 전체를 벡터화하면 메모리 요구량이 증가할 수 있으므로 . Chunk로 분리하면 메모리 사용을 줄일 수 있습니다.
  • 정보의 집중화: 각 Chunk는 특정 주제나 내용을 집중적으로 다룰 수 있으므로, 검색 결과가 더 정확하게 일치할 수 있다.

 

Chunk 로 분리하는 단위는 작업의 유형에 따라 다를 것.

 

 

Q) LLM 을 이용한 Semantic Search 란 뭐야?

 

LLM 을 이용한 검색 엔진이다.

 

Semantic Search는 대규모 언어 모델(LLM, Large Language Model)의 자연어 이해 능력을 활용하여 텍스트 데이터에서 의미 기반으로 정보를 검색하는 것을 말한다.

 

일반적인 키워드 검색과 달리, 의미론적 검색(Semantic Search)은 단어의 의미와 문맥을 고려하여 유사성을 찾는 데 초점을 둔다.

 

Semantic Search는 검색 엔진뿐만 아니라 챗봇, 추천 시스템, 지식 그래프, 질의응답 시스템 등 다양한 분야에 적용될 수 있다.

 

 

Q) 내가 가진 Document 와 LLM 을 가지고 Chatbot 을 만들 때 주의할 사항은 뭐가 있을까?

 

데이터 프라이버시와 보안:

  • 문서에 개인 정보나 민감한 데이터가 포함되어 있지 않은지 확인해야함.
  • LLM이 훈련 데이터나 문서를 기반으로 작동할 때, 데이터 유출을 방지하기 위한 보안 조치를 취해야할 수 있다.

 

모델의 한계 이해:

  • LLM은 때때로 잘못된 정보를 생성할 수 있으므로, 챗봇이 정확한 정보를 제공하도록 검증하는 프로세스가 필요하다.
  • LLM은 훈련 데이터에서 편향성을 가지고 있을 수 있다. 챗봇의 응답이 편향적이거나 부적절한 내용이 되지 않도록 주의해야함.

 

챗봇 디자인:

  • 챗봇의 인터페이스와 상호작용 방식을 사용자 친화적으로 설계해야함. 사용자의 기대에 맞는 응답을 제공하고, 불필요한 복잡성을 줄여야한다.
  • 챗봇이 대화의 컨텍스트를 잘 이해하고 유지할 수 있도록 설계해야한다. 대화의 흐름을 적절히 이어갈 수 있도록 해야함.

 

문서 처리:

  • 문서를 적절한 크기의 단위(예: Chunk)로 나누어 LLM이 처리하기 쉽게 해야함. 이는 챗봇이 문서의 정보를 효율적으로 검색하고 사용할 수 있도록 도와줌.
  • 챗봇이 사용하는 문서와 데이터 소스가 신뢰할 수 있는지 확인해야함. 잘못된 데이터나 오래된 정보를 사용하면 챗봇의 신뢰도가 떨어질 수 있으니.

 

지속적인 개선:

  • 사용자로부터 피드백을 수집하여 챗봇의 성능을 지속적으로 개선해야한다. 사용자 경험을 개선하기 위한 주기적인 업데이트와 개선 작업이 필요하다.
  • 챗봇의 작동 상태와 성능을 모니터링해야한다.

 

 

Document Loading

이 Short Course 의 주제인 LangChain 을 내가 가진 데이터를 이용해서 답변을 하도록 만들려면 데이터가 있어야하니까, 데이터를 수집하는 방법에 대해서 다룸.

 

Document Loading 에서 중요한 건 다양한 데이터 소스에서 가져온 데이터들을 일관된 포맷으로 잘 변경하는 것. (e.g Youtube, Notion, News, Twitter 등)

  • 그리고 가지고 온 Text 를 Post Processing 하는 것도 중요하다. 가지고 온 문서가 텅텅 비어있는 부분이 많을 수 있으니까.

 

LangChain 에서는 여러가지 도큐먼트 유형이 있음.

 

PDF 문서를 데이터로 가지고 오는 Document Loader 예시:

from langchain.document_loaders import PyPDFLoader
loader = PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf")
pages = loader.load()

print(page.page_content[0:500])

 

 

출력:

MachineLearning-Lecture01  
Instructor (Andrew Ng):  Okay. Good morning. Welcome to CS229, the machine 
learning class. So what I wanna do today is ju st spend a little time going over the logistics 
of the class, and then we'll start to  talk a bit about machine learning.  
By way of introduction, my name's  Andrew Ng and I'll be instru ctor for this class. And so 
I personally work in machine learning, and I' ve worked on it for about 15 years now, and 
I actually think that machine learning i

 

 

Youtube 동영상 오디오를 텍스트로 변환해서 가지고 오는 Document Loader 예시:

from langchain.document_loaders.generic import GenericLoader
from langchain.document_loaders.parsers import OpenAIWhisperParser
from langchain.document_loaders.blob_loaders.youtube_audio import YoutubeAudioLoader

url="https://www.youtube.com/watch?v=1EWgpbcZ2jQ"
save_dir="docs/youtube/"
loader = GenericLoader(
    YoutubeAudioLoader([url],save_dir),
    OpenAIWhisperParser()
)
docs = loader.load()

docs[0].page_content[0:500]

 

 

URL 정보를 바탕으로 웹 문서를 가지고오는 Document Loader 예시:

from langchain.document_loaders import WebBaseLoader

loader = WebBaseLoader("https://github.com/basecamp/handbook/blob/master/37signals-is-you.md")

docs = loader.load()

print(docs[0].page_content[:500])

 

 

Notion 정보를 가지고 Document 를 가지고 오는 예시:

from langchain.document_loaders import NotionDirectoryLoader
loader = NotionDirectoryLoader("docs/Notion_DB")
docs = loader.load()

print(docs[0].page_content[0:200])

 

 

Document Spliting

가져온 Document 들을 의미있는 단위로 쪼개는 작업에 대한 내용임. 이렇게 해야 LLM 이 검색하고 이 검색된 문서의 내용에 대해 답변을 더 잘할 수 있게 될테니까.

 

LangChain 에서는 다양한 Document Spliter 를 제공해준다 하나씩 살펴보자.

  • CharacterTextSplitter: 주어진 문자를 기준으로 문서를 분할한다. 기본적으로 정해진 길이의 문자열로 나누며, 일반적으로 텍스트 데이터를 일정한 크기로 유지할 때 사용한다.
  • MarkdownHeaderTextSplitter: Markdown 문서의 헤더(header) 기반으로 분할한다. 헤더 레벨에 따라 문서를 논리적인 구분으로 나눈다.
  • TokenTextSplitter: 토큰의 수를 기준으로 문서를 분할한다. 토큰은 일반적으로 단어, 구두점, 기호 등의 단위를 의미하며, 토큰의 개수를 기준으로 문서를 나눈다. 언어 모델의 토큰 한계에 맞추거나, 텍스트를 처리하기에 적합한 크기로 분할할 때 활용된다.
  • SentenceTransformersTokenTextSplitter: SentenceTransformers와 같은 문장 기반 토큰화 기술을 사용하여 문서를 분할한다.
  • RecursiveTextSplitter: 여러 분할 전략을 재귀적으로 적용하여 문서를 분할한다. 예를 들어, 큰 문서를 먼저 섹션으로 분할한 다음, 각 섹션을 다시 작은 단위로 분할할 수 있다.
  • Language: 컴퓨터 언어를 분할하는데 사용한다.
  • NLTKTextSplitter: NLTK(Natural Language Toolkit) 라이브러리를 사용하여 문서를 분할한다.
  • SpacyTextSplitter: Spacy 라이브러리를 활용하여 문서를 분할한다.

 

유튜브 같은 영상을 텍스트로 만들 때는 LLM 이용해서 유의미한 단락으로 텍스트를 분리해놓고 RecursiveTextSplitter 를 사용하면 되지 않을까.

 

 

Q) 글을 전체적으로 읽고나서 유의미한 단락으로 구분해주는 Spliter 는 없나?

 

없는듯. 사용자 정의 Spliter 를 만들어야 할 듯하다.

 

Recursive splitting details 예제:

  • Recursive spliter 를 사용하면 문장 단위로 잘린다. 다만 유의미한 단락을 기준으로 자르지는 않고 구분자를 이용해서 텍스트를 자른다. 이 점이 좀 아쉽긴 하지만 텍스트 자체가 잘 나눠져있다면 괜찮을듯.
  • RecursiveCharacterTextSplitter 만들 떄 사용한 변수는 chunk_size, chunk_overlap, separators 가 있음:
    • 청크 크기 (chunk_size): 각 청크의 최대 문자 수를 나타냅니다. 이 분할기는 텍스트를 이 크기에 맞게 자른다.
    • 청크 겹침 (chunk_overlap): 각 청크 사이의 겹침을 의미한다. 0을 넣으면 분할된 청크는 서로 겹치지 않는다.
    • 구분자 (separators): ["\n\n", "\n", " ", ""]는 텍스트를 분할할 때 사용하는 구분자이다. 이들은 우선 순위대로 정렬되어 있으며, 분할은 가장 높은 우선순위의 구분자로 시도한다. 예를 들어, 텍스트를 분할할 때 먼저 두 번의 줄바꿈을 찾고, 다음으로 단일 줄바꿈, 공백, 마지막으로는 아무 구분자 없이 분할을 시도한다.
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitter

chunk_size =26
chunk_overlap = 4

some_text = """When writing documents, writers will use document structure to group content. \
This can convey to the reader, which idea's are related. For example, closely related ideas \
are in sentances. Similar ideas are in paragraphs. Paragraphs form a document. \n\n  \
Paragraphs are often delimited with a carriage return or two carriage returns. \
Carriage returns are the "backslash n" you see embedded in this string. \
Sentences have a period at the end, but also, have a space.\
and words are separated by space."""

r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=450,
    chunk_overlap=0, 
    separators=["\n\n", "\n", " ", ""]
)

r_splitter.split_text(some_text)

 

 

출력:

["When writing documents, writers will use document structure to group content. This can convey to the reader, which idea's are related. For example, closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document.",
 'Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the "backslash n" you see embedded in this string. Sentences have a period at the end, but also, have a space.and words are separated by space.']

 

 

Markdown Splitter 예제:

  • markdown 은 텍스트를 유의미한 단락으로 분리시켜놓는 경우가 많으니까 텍스트 분할에 유용함.
from langchain.document_loaders import NotionDirectoryLoader
from langchain.text_splitter import MarkdownHeaderTextSplitter

markdown_document = """# Title\n\n \
## Chapter 1\n\n \
Hi this is Jim\n\n Hi this is Joe\n\n \
### Section \n\n \
Hi this is Lance \n\n 
## Chapter 2\n\n \
Hi this is Molly"""

headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on
)
md_header_splits = markdown_splitter.split_text(markdown_document)

md_header_splits[0]
md_header_splits[1]

 

 

출력:

Document(page_content='Hi this is Jim  \nHi this is Joe', metadata={'Header 1': 'Title', 'Header 2': 'Chapter 1'})

Document(page_content='Hi this is Lance', metadata={'Header 1': 'Title', 'Header 2': 'Chapter 1', 'Header 3': 'Section'})

 

 

Vector Store Embedding

이제는 나눠진 Chunk 로 Vector Store 에 넣어서 인덱싱을 하는 단계임.

 

RAG 방식의 검색은 Vector Store 에서 질문과 유사한 내용이 담겨있는 내용을 N 개 뽑아서 이걸 LLM 에게 던져줘서 답변을 작성하는 방식이다.

  • 유사도 랭킹에서 밀려서 안뽑히는 경우도 발생할 수 있음. 이건 뭐 실험을 거쳐서 발견해야하는 부분인듯. 하이퍼파라미터와 같은.
  • LLM 이 답변하는 방식은 여러가지가 있었다. (e.g Map Reduce, Refine, Map ReRank 등)

 

Vector Store 를 활용할 때 주로 만나는 실패 유형:

  • 중복된 Document 가 임베딩 되어 있을 때 LLM 이 중복 답변을 생산하는 것
  • 특정 부분만 가져와서 답변을 작성해야하는데, Semantic Search 다 보니 연관되기만 하면 가져와서 답변을 작성하기도 함.

 

 

Q) Vector Store 는 클라우드 서비스에서 제공해주는 것도 있어?

 

있다.

 

클라우드 기반 벡터 스토어를 제공하는 몇 가지 주요 서비스는 다음과 같다:

  • Pinecone: 완전 관리형 벡터 스토어로, 대규모 벡터 데이터에 대해 빠르고 확장 가능한 검색을 제공한다.
  • Weaviate: 오픈소스 벡터 검색 엔진이지만, 클라우드에서도 제공되어 관리된 서비스를 통해 벡터 데이터베이스를 운영할 수 있다.

 

 

Q) Vector Store 의 Edge Case 인 중복된 Document 가 임베딩 되어 있을 때 LLM 이 중복 답변을 생산하는 문제는 어떻게 해결하지?

  • 벡터 스토어에 문서를 추가하기 전에 중복을 식별하고 제거하는 것.
  • 벡터 스토어에 데이터를 삽입하기 전에 데이터 클렌징 작업을 수행하는 것.
  • 각 문서에 고유한 메타데이터를 추가하여 중복된 문서를 구별하는 것
  • 검색 결과를 얻은 후 중복된 답변을 제거하는 로직을 추가하는 것
  • 검색 결과에 중복된 답변이 포함될 때, 이를 하나의 응답으로 합치는 것.

 

 

Q) Vector Store 의 Edge Case 인 특정 부분만 가져와서 답변을 작성해야하는데, Semantic Search 다 보니 연관되기만 하면 가져와서 답변을 작성하는 경우는 어떻게 해결하지?

  • 검색 결과를 더 정교하게 필터링하기 위해 질의(Query) 작성에 집중하는 것 특정 문서의 청크만 가져오려면 검색어에 더 구체적인 조건을 포함하거나 특정 문서의 ID 또는 범위를 기준으로 검색을 제한할 수 있습니다.
  • 검색된 결과에서 불필요한 청크나 문서를 필터링하는 로직을 추가해야한다.
  • 벡터 스토어에서 검색할 때 특정 필터링 조건을 적용할 수 있다.
  • 분할 전략을 조정하여 문서의 특정 부분만 포함되도록 할 수 있다.
  • 특정 문서의 청크만 가져오려면 문서 ID 또는 특정 키워드를 기준으로 결과를 필터링할 수 있다.

 

 

Vector Store 활용 예시:

from langchain.document_loaders import PyPDFLoader

# Load PDF
loaders = [
    # Duplicate documents on purpose - messy data
    PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf"),
    PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf"),
    PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture02.pdf"),
    PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture03.pdf")
]
docs = []
for loader in loaders:
    docs.extend(loader.load())

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 1500,
    chunk_overlap = 150
)

splits = text_splitter.split_documents(docs)

from langchain.embeddings.openai import OpenAIEmbeddings
embedding = OpenAIEmbeddings()

from langchain.vectorstores import Chroma

persist_directory = 'docs/chroma/'

vectordb = Chroma.from_documents(
    documents=splits,
    embedding=embedding,
    persist_directory=persist_directory
)

question = "is there an email i can ask for help"

docs = vectordb.similarity_search(question,k=3)

vectordb.persist()

 

 

Retrieval

이전에 본 Semantic Search 의 Edge Case 들을 극복하는 여러 테크닉들을 소개함.

 

MMR (Maximum Marginal Relevance):

  • 문서나 청크의 관련성과 다양성을 동시에 고려하여 검색 결과를 최적화하는 기법임. 이 방법은 정보 검색에서 결과의 중복성을 최소화하고, 주어진 쿼리에 대한 최적의 답변 세트를 제공하는 데 특히 유용하다.
    • 관련성(Relevance): MMR은 쿼리와의 관련성이 높은 문서를 우선적으로 선택한다. 이는 사용자가 제공한 쿼리와 문서 사이의 유사성을 측정하여 결정된다.
    • 다양성(Diversity): 단순히 관련성만 고려하는 대신, MMR은 이미 선택된 문서와의 유사성을 최소화하여 새로운 문서를 선택한다. 이로 인해 결과 세트의 다양성이 증가하고, 중복된 정보의 제공을 피할 수 있다.
  • MMR 알고리즘은 각 단계에서 관련성과 다양성 사이의 균형을 맞추기 위해 이 두 요소를 상호 교차하여 계산한다. 사용자는 이 균형을 조절하기 위해 하이퍼파라미터(λ)를 설정할 수 있음. λ 값이 높으면 관련성을, 낮으면 다양성을 더 중요시하게 됨.
  • MMR (Maximal Marginal Relevance) 메커니즘은 두 단계 접근 방식을 사용한다. 먼저 벡터 스토어에서 쿼리와 관련성이 높은 K개의 문서를 선택하고, 그 다음 단계에서 다양성을 고려하여 M개의 문서를 추려낸다.

 

Self Query 방법:

  • 유사성 검색에다가 참조해야 될 문서의 필터링 조건까지 적용해서 검색하는 것.
  • “What are some movies about aliens made in 1980? 이라고 질문하면 Aliens 와 연관되어 있는 문서를 검색하고, 여기서 연도가 1980년 인 문서만 참조하도록 필터링을 거는 걸 말한다.
  • LLM 을 이용해서 질문을 보면 필터링을 생성해서 Vector Store 에서 관련 문서만 검색하도록 한다. Vector Store 에서는 Metadata 기반의 필터링을 제공해주는데 이를 이용함.

 

Compression 방법:

  • 질문을 던졌을 때 Vector Store 에서 관련 도큐먼트를 뽑아오고 해당 내용들 중 필요한 정보들만을 추출해서 압축해서 최종 답변을 생성해내는 방식임.
  • 압축하는 LLM 과, 최종 답변을 하는 LLM 이렇게 사용된다. 대신에 더 많은 LLM 호출을 사용하니까 비용적인 부담도 듬.
  • 압축하는 LLM 은 요약 알고리즘을 사용하여 문서에서 핵심 요점이나 키워드를 식별하고, 불필요한 정보를 제거한다. 이를 통해 최종 답변이 간결하고 중복이 없게 되는 것.
  • MMR 방법과 같이 적용하는 것도 가능함.

 

이외의 NLP 를 사용하는 기법:

  • SVM
  • TF-IDF

 

MMR 을 사용하는 예제:

from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
persist_directory = 'docs/chroma/'

embedding = OpenAIEmbeddings()
vectordb = Chroma(
    persist_directory=persist_directory,
    embedding_function=embedding
)

texts = [
    """The Amanita phalloides has a large and imposing epigeous (aboveground) fruiting body (basidiocarp).""",
    """A mushroom with a large fruiting body is the Amanita phalloides. Some varieties are all-white.""",
    """A. phalloides, a.k.a Death Cap, is one of the most poisonous of all known mushrooms.""",
]

smalldb = Chroma.from_texts(texts, embedding=embedding)

question = "Tell me about all-white mushrooms with large fruiting bodies"

smalldb.max_marginal_relevance_search(question,k=2, fetch_k=3)

 

 

Self Query 를 사용하는 예제:

  • SelfQueryRetriever 를 만들 떄 관련 파라미터:
    • Verbose Mode: True/False 값으로, 쿼리 처리 중 자세한 로그 정보를 볼 수 있다.
    • Relevance Threshold: 쿼리 결과의 관련성 임계값을 설정하여 결과를 필터링할 수 있다.
    • Metadata Field: 벡터 스토어에 저장된 문서의 메타데이터 필드 정보를 제공한다. LLM 이 자연어 쿼리를 처리할 때 사용할 수 있는 필드와 그 설명을 포함한다.
    • Document Content Description: 벡터 스토어에 저장된 문서의 일반적인 내용이나 주제를 설명한다. LLM이 자연어 쿼리를 메타데이터 필드와 연결하는 데 도움이 된다.
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo

metadata_field_info = [
    AttributeInfo(
        name="source",
        description="The lecture the chunk is from, should be one of `docs/cs229_lectures/MachineLearning-Lecture01.pdf`, `docs/cs229_lectures/MachineLearning-Lecture02.pdf`, or `docs/cs229_lectures/MachineLearning-Lecture03.pdf`",
        type="string",
    ),
    AttributeInfo(
        name="page",
        description="The page from the lecture",
        type="integer",
    ),
]

document_content_description = "Lecture notes"
llm = OpenAI(model='gpt-3.5-turbo-instruct', temperature=0)
retriever = SelfQueryRetriever.from_llm(
    llm,
    vectordb,
    document_content_description,
    metadata_field_info,
    verbose=True
)

question = "what did they say about regression in the third lecture?"
docs = retriever.get_relevant_documents(question)

 

 

Compression 예제:

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

def pretty_print_docs(docs):
    print(f"\n{'-' * 100}\n".join([f"Document {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)]))

# Wrap our vectorstore
llm = OpenAI(temperature=0, model="gpt-3.5-turbo-instruct")
compressor = LLMChainExtractor.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=vectordb.as_retriever()
)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=vectordb.as_retriever(search_type = "mmr")
)

question = "what did they say about matlab?"
compressed_docs = compression_retriever.get_relevant_documents(question)
pretty_print_docs(compressed_docs)

 

 

Other types of retrieval 예제:

from langchain.retrievers import SVMRetriever
from langchain.retrievers import TFIDFRetriever
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Load PDF
loader = PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf")
pages = loader.load()
all_page_text=[p.page_content for p in pages]
joined_page_text=" ".join(all_page_text)

# Split
text_splitter = RecursiveCharacterTextSplitter(chunk_size = 1500,chunk_overlap = 150)
splits = text_splitter.split_text(joined_page_text)

# Retrieve
svm_retriever = SVMRetriever.from_texts(splits,embedding)
tfidf_retriever = TFIDFRetriever.from_texts(splits)

question = "What are major topics for this class?"
docs_svm=svm_retriever.get_relevant_documents(question)
docs_svm[0]

question = "what did they say about matlab?"
docs_tfidf=tfidf_retriever.get_relevant_documents(question)
docs_tfidf[0]

 

 

Question Answering

여기서는 Document 를 가지고 왔으니까, 이제 질문을 가지고 답변을 작성하는 작업을 보자.

 

LLM 이 답변을 작성하는 작업은 다음과 같다:

    1. Question 에 해당하는 관련 문서를 Vector Store 에서 뽑아온다.
    1. 관련 문서를 Compression 해서 LLM Context 에 맞게 Fit 하게 맞춤.
    1. LLM 은 Context 정보와 질문을 가지고 답변을 작성한다.

 

이 방법외에도 Map Reduce, Refine Map ReRank 방법들이 있기도 함.

  • 이 방법들이 있는 이유는 Vector Store 에서 조회한 Document 들이 하나만 있는게 아니라서 Context 를 많이 차지할 위험이 있기 때문임.

 

여러가지 방법들을 사용하면서 잘 대답하는지 봐야한다. LangChain 에서는 UI 환경을 제공해줘서 내부적으로 어떤 일들이 일어나는지, 어떤 문서를 가지고 오고 어떻게 답변해서 최종 답변을 만드는지 등을 볼 수 있음.

 

 

Q) Map Reduce 방식의 단점은 느린것과 명료한 답변이 아닐 수도 있다는건가?

  • 속도: MapReduce 방식은 각 조각에 대한 독립적인 작업을 수행한 후 그 결과를 병합하기 때문에 속도가 느릴 수 있다.. 특히 대규모 데이터 세트나 복잡한 작업의 경우 이러한 지연이 더 두드러질 수 있음.
  • 답변의 명확성: MapReduce 방식에서는 각 조각의 독립적인 처리로 인해 전체적인 맥락이 누락되거나 답변이 충분히 명확하지 않을 수 있다. 특히 여러 정보 출처에서 데이터를 수집하여 병합하는 경우, 중복되거나 모순되는 내용이 포함될 가능성이 있다.

 

 

Q) Refine 방식과 Map ReRank 방식의 단점은 뭐야?

 

Refine 방식의 단점:

  • Refine 방식은 검색한 결과를 더욱 정교하게 다듬기 위해 추가적인 처리 단계를 거치기 때문에 지연이 있음.
  • Refine 과정은 종종 더 복잡한 결과를 도출할 수도 있음.

 

Map ReRank 방식의 단점:

  • Map ReRank 방식은 초기 검색 결과에 대해 재정렬 과정을 수행함. 이 과정에서 추가적인 시간이 소요될 수 있으며, 대량의 데이터를 처리할 때는 특히 느려질 수 있음.
  • 재정렬 하는 과정에서 좋은 품질의 답변이 누락될 수 있음.
  • Map ReRank는 강력한 모델에 의존하여 초기 검색 결과를 재정렬함. 이 모델의 성능이 최종 결과의 품질을 결정짓기 때문에, 모델이 잘못 훈련되었거나 적절하지 않은 경우 결과의 품질이 저하될 수 있음.

 

 

Q) 그러면 정리해서 Stuff, Map Reduce, Refine, Map ReRank 방식은 각각 언제 사용하면 좋을까?

 

Stuff 방식:

  • 모든 검색 결과를 한 번에 수집하고 이를 기반으로 답변을 생성하는 방식임.
  • 데이터의 양이 적거나, 문서의 크기가 작아 한 번에 모든 데이터를 다룰 수 있는 경우 적합하다. 빠른 응답 시간이 필요한 경우 Stuff 방식을 선택할 수 있다.
  • 데이터가 많을 경우 메모리 소모가 커질 수 있으므로, 대규모 데이터 세트에는 적합하지 않을 수 있다.

 

Map Reduce 방식:

  • Map Reduce 방식은 데이터를 여러 부분으로 나누고 각 부분을 개별적으로 처리한 다음, 결과를 병합한다.
  • 대규모 데이터 세트를 처리해야 하거나, 분산 처리가 필요한 경우 적합합니다. 병렬 처리로 속도를 향상시킬 수 있다.
  • 병합 과정에서 시간이 소요될 수 있으며, 답변이 명확하지 않을 수 있다.

 

Refine 방식:

  • Refine 방식은 초기 결과에 대한 추가 분석과 정교화를 통해 더 정확한 답변을 생성한다.
  • 초기 결과를 세밀하게 다듬어야 하거나, 답변의 정확성을 높이고자 할 때 적합하다. 복잡한 질문에 대한 정교한 답변이 필요한 경우에도 유용하다.
  • 처리 시간이 길어질 수 있으며, 자원 소모가 클 수 있다. 또한 과적합의 위험이 존재할 수 있따.

 

Map ReRank 방식:

  • 검색된 결과를 재정렬하여 최상의 결과를 선택한다.
  • 초기 검색 결과의 품질이 낮거나, 최상의 결과를 선택하고자 할 때 적합하다.
  • 다양한 출처에서 정보를 수집한 후 최적의 답변을 도출하는 데 유용하다.
  • 재정렬 과정에서 시간이 소요될 수 있으며, 모델의 성능에 의존하므로 모델 선택에 주의해야한다.

 

 

Q) 애초에 Vector Store 에 데이터를 중복 없이, 필요한 데이터만 잘 넣어두냐에 따라서 RAG 방식의 성능이 급격하게 차이가 날 수 있곘네?

 

맞다.

 

벡터 스토어에 데이터를 저장하기 전에 적절한 전처리가 필요할 수도 있고, 질의응답에 적합하고 관련성이 높은 데이터가 중복없이 있어야하고, 관련성이 낮은 데이터로 답변이 나가지 않게 해야한다.

 

 

Stuff 방식을 이용해서 답변을 얻는 예제:

  • 다만 현재 방식들은 Memory 를 사용해서 이전 답변과 질문을 기억하지 않는 방식임.
from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
persist_directory = 'docs/chroma/'
embedding = OpenAIEmbeddings()
vectordb = Chroma(persist_directory=persist_directory, embedding_function=embedding)

from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name=llm_name, temperature=0)

from langchain.chains import RetrievalQA

from langchain.prompts import PromptTemplate

# Build prompt
template = """Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. Use three sentences maximum. Keep the answer as concise as possible. Always say "thanks for asking!" at the end of the answer. 
{context}
Question: {question}
Helpful Answer:"""
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)

# Run chain
qa_chain = RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever(),
    return_source_documents=True,
    chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)

question = "Is probability a class topic?"

result = qa_chain({"query": question})

result["result"]
result["source_documents"][0]

 

 

Map Reduce 를 이용해서 답변을 얻는 예제:

qa_chain_mr = RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever(),
    chain_type="map_reduce"
)

result = qa_chain_mr({"query": question})

result["result"]

 

 

LangChain Plus Platform 을 이용해서 LangChain UI 를 통해 디버깅 하는 방법:

  • Go to langchain plus platform and sign up
  • Create an API key from your account's settings
  • Use this API key in the code below
#import os
#os.environ["LANGCHAIN_TRACING_V2"] = "true"
#os.environ["LANGCHAIN_ENDPOINT"] = "https://api.langchain.plus"
#os.environ["LANGCHAIN_API_KEY"] = "..." # replace dots with your api key

qa_chain_mr = RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever(),
    chain_type="map_reduce"
)
result = qa_chain_mr({"query": question})
result["result"]

qa_chain_mr = RetrievalQA.from_chain_type(
    llm,
    retriever=vectordb.as_retriever(),
    chain_type="refine"
)
result = qa_chain_mr({"query": question})
result["result"]

 

 

Chat

여기서는 답변을 주고 받을 때 이전 답변과 질문을 기억하도록 하는 작업을 다뤄보자.

 

ConservationRetrivealChain 을 사용해서 Chatbot 을 만들 수 있다:

  • ConversationRetrievalChain 는 질의응답(Q&A)을 구현하기 위한 핵심적인 컴포넌트임.
  • 체인은 이전 대화 내용을 기억하고 이를 사용하여 현재 질문에 대한 답변을 생성할 수 있다.

 

ConservationRetrivealChain 의 사용 가이드:


from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
persist_directory = 'docs/chroma/'
embedding = OpenAIEmbeddings()
vectordb = Chroma(persist_directory=persist_directory, embedding_function=embedding)

from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name=llm_name, temperature=0)

from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True
)

from langchain.chains import ConversationalRetrievalChain
retriever=vectordb.as_retriever()
qa = ConversationalRetrievalChain.from_llm(
    llm,
    retriever=retriever,
    memory=memory
)

question = "Is probability a class topic?"
result = qa({"question": question})

result['answer']

question = "why are those prerequesites needed?"
result = qa({"question": question})

result['answer']

 

 

Chatbot 생성 가이드:

  • ConversationalRetrievalChain 을 생성하는 코드를 보면 return_source_documents, return_generated_question 파라미터가 있는데 이 설명은 다음과 같다:
    • return_source_documents:
      • 파라미터를 True로 설정하면, 체인은 생성된 답변과 함께 답변의 출처가 된 소스 문서를 반환한다. 이를 통해 답변의 근거를 확인하거나 추가적인 맥락을 얻을 수 있다.
      • 이 파라미터를 사용하는 이유는 디버깅을 위해서, 문서의 출처를 사용자에게 제공하기 위해서이다.
    • return_generated_question:
      • 이 파라미터를 True로 설정하면, 체인은 생성된 답변과 함께 내부적으로 생성된 질문도 반환한다. 이는 사용자가 입력한 원래의 질문을 더 잘 이해하고, 체인이 어떻게 답변을 도출했는지 파악하는 데 도움이 된다.
      • 입력된 질문이 복잡하거나 애매한 경우, 체인이 내부적으로 생성한 재구성된 질문을 확인하여 어떻게 질문을 이해했는지 알 수 있다. 이 또한 디버깅을 위해서 사용하기도 한다.
def load_db(file, chain_type, k):
    # load documents
    loader = PyPDFLoader(file)
    documents = loader.load()
    # split documents
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
    docs = text_splitter.split_documents(documents)
    # define embedding
    embeddings = OpenAIEmbeddings()
    # create vector database from data
    db = DocArrayInMemorySearch.from_documents(docs, embeddings)
    # define retriever
    retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": k})
    # create a chatbot chain. Memory is managed externally.
    qa = ConversationalRetrievalChain.from_llm(
        llm=ChatOpenAI(model_name=llm_name, temperature=0), 
        chain_type=chain_type, 
        retriever=retriever, 
        return_source_documents=True,
        return_generated_question=True,
    )
    return qa
    
import panel as pn
import param

class cbfs(param.Parameterized):
    chat_history = param.List([])
    answer = param.String("")
    db_query  = param.String("")
    db_response = param.List([])
    
    def __init__(self,  **params):
        super(cbfs, self).__init__( **params)
        self.panels = []
        self.loaded_file = "docs/cs229_lectures/MachineLearning-Lecture01.pdf"
        self.qa = load_db(self.loaded_file,"stuff", 4)
    
    def call_load_db(self, count):
        if count == 0 or file_input.value is None:  # init or no file specified :
            return pn.pane.Markdown(f"Loaded File: {self.loaded_file}")
        else:
            file_input.save("temp.pdf")  # local copy
            self.loaded_file = file_input.filename
            button_load.button_style="outline"
            self.qa = load_db("temp.pdf", "stuff", 4)
            button_load.button_style="solid"
        self.clr_history()
        return pn.pane.Markdown(f"Loaded File: {self.loaded_file}")

    def convchain(self, query):
        if not query:
            return pn.WidgetBox(pn.Row('User:', pn.pane.Markdown("", width=600)), scroll=True)
        result = self.qa({"question": query, "chat_history": self.chat_history})
        self.chat_history.extend([(query, result["answer"])])
        self.db_query = result["generated_question"]
        self.db_response = result["source_documents"]
        self.answer = result['answer'] 
        self.panels.extend([
            pn.Row('User:', pn.pane.Markdown(query, width=600)),
            pn.Row('ChatBot:', pn.pane.Markdown(self.answer, width=600, style={'background-color': '#F6F6F6'}))
        ])
        inp.value = ''  #clears loading indicator when cleared
        return pn.WidgetBox(*self.panels,scroll=True)

    @param.depends('db_query ', )
    def get_lquest(self):
        if not self.db_query :
            return pn.Column(
                pn.Row(pn.pane.Markdown(f"Last question to DB:", styles={'background-color': '#F6F6F6'})),
                pn.Row(pn.pane.Str("no DB accesses so far"))
            )
        return pn.Column(
            pn.Row(pn.pane.Markdown(f"DB query:", styles={'background-color': '#F6F6F6'})),
            pn.pane.Str(self.db_query )
        )

    @param.depends('db_response', )
    def get_sources(self):
        if not self.db_response:
            return 
        rlist=[pn.Row(pn.pane.Markdown(f"Result of DB lookup:", styles={'background-color': '#F6F6F6'}))]
        for doc in self.db_response:
            rlist.append(pn.Row(pn.pane.Str(doc)))
        return pn.WidgetBox(*rlist, width=600, scroll=True)

    @param.depends('convchain', 'clr_history') 
    def get_chats(self):
        if not self.chat_history:
            return pn.WidgetBox(pn.Row(pn.pane.Str("No History Yet")), width=600, scroll=True)
        rlist=[pn.Row(pn.pane.Markdown(f"Current Chat History variable", styles={'background-color': '#F6F6F6'}))]
        for exchange in self.chat_history:
            rlist.append(pn.Row(pn.pane.Str(exchange)))
        return pn.WidgetBox(*rlist, width=600, scroll=True)

    def clr_history(self,count=0):
        self.chat_history = []
        return

+ Recent posts