Coding/Study

[AI]RAG 기본 이론&실습(2)

후__아 2024. 8. 1. 17:31

https://hoooa.tistory.com/65 에서 정리했던 RAG의 기본 파이프라인(Data Load, Text Split, Indexing, Retrieval, Generation)을 한층 자세하게 들어가보자~

 

[AI]RAG 기본 이론&실습(1)

RAG에 대해선 이전에 아주 짧게 다뤄봤어서 공부가 필요한 상황,,,경진대회 문제라도 제대로 풀려면 해야된다 아자아자!!! (이제는 더 이상 물러설 곳이 없다)  https://hoooa.tistory.com/58에 이어서 Lang

hoooa.tistory.com

 

1. Data Load

불러오고자 하는 데이터의 형태에 따라 다양한 Document Loader를 활용할 수 있음!

웹 문서 WebBaseLoader

특정 웹 페이지에서 문서를 가져오기

import bs4
from langchain_community.document_loaders import WebBaseLoader

url1 = "https://blog.langchain.dev/week-of-1-22-24-langchain-release-notes/"
url2 = "https://blog.langchain.dev/week-of-2-5-24-langchain-release-notes/"

loader = WebBaseLoader(
    web_paths=(url1, url2),	# 로드할 웹페이지 url - 단일 문자열 or 시퀀스 배열
    bs_kwargs=dict(
        parse_only = bs4.SoupStrainer( # 특정 클래스 이름의 HTML 요소만 추출
            class_ = ("article-header", "article-content")
        )
    ),
)
docs = loader.load()
print(len(docs))
docs[0]

 

텍스트 문서 TextLoader
# 텍스트 문서
path = '/content/drive/MyDrive/재정정보경진대회/'
from langchain_community.document_loaders import TextLoader

loader = TextLoader(path + 'test.txt')
data = loader.load()

print(type(data))
data[0].page_content
<class 'list'>
안녕하세요~ 테스트 load 테스트 하고 있습니다~

 

폴더 DirectoryLoader

 

csv 파일 CSVLoader

 

PDF

- PyPDFLoader(PDF문서 페이지별) / UnstructuredPDFLoader(형식없는 PDF) / PyMuPDFLoader(상세한 MetaData) / OnlinePDFLoader(온라인에 업로드된 PDF) / PyPDFDirectoryLoader(특정 폴더의 모든 PDF)

!pip install -q pypdf
#!pip install unstructured unstructured-inference
!pip install unstructured[all-docs]
!pip install pymupdf

from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import UnstructuredPDFLoader
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_community.document_loaders import OnlinePDFLoade
from langchain_community.document_loaders import PyPDFDirectoryLoader

pdf = 'test.pdf'

# PyPDFLoader
loader = PyPDFLoader(pdf)
pages = loader.load()

print(len(pages))
print(pages[20])

# UnstructuredPDFLoader
loader = UnstructuredPDFLoader(pdf, mode='elements')	# elements: 텍스트 청크가 분리된 채로 유지 - 원본 레이아웃과 유사함
pages = loader.load()

# PyMuPDFLoader
loader = PyMuPDFLoader(pdf)
pages = loader.load()

# OnlinePDFLoader
loader = OnlinePDFLoader("https://arxiv.org/pdf/1706.03762.pdf")    # Transformer 논문
pages = loader.load()
pages[0].page_content[:1000]

# PyPDFDirectoryLoader
loader = PyPDFDirectoryLoader('./')
data = loader.load()

+UnstructuredPDFLoader 오류 해결~  https://hoooa.tistory.com/68

 

 

2. Text Split

LLM의 입력 토큰 한도에 맞추기 위해 긴 문서 → Chunk로 분리

 

- 각 청크가 독립적 의미를 갖도록 나눠야함

- LLM 모델의 입력 크기/비용을 고려하여 적합한 최적 크기를 조정할 수 있음

CharacterTextSplitter

개별 문자(Separator)를 기준으로 청크 분리 

from langchain_community.document_loaders import TextLoader

loader = TextLoader(path+'test.txt')
data = loader.load()

from langchain_text_splitters import CharacterTextSplitter
ts = CharacterTextSplitter(
    separator = '',       # 청크 나누는 기준
    chunk_size = 500,     # 청크 최대 길이
    chunk_overlap = 100,  # 인접 청크 사이 중복으로 포함될 문자 수
    length_function = len,  # 청크 길이 계산 함수
)

texts = ts.split_text(data[0].page_content)
print(len(texts))
print(len(texts[0]))


text_splitter = CharacterTextSplitter(
    separator = '\n',   # 줄바꿈 문자 기준으로 청크 나누기
    chunk_size = 500,
    chunk_overlap  = 100,
    length_function = len,
)

texts = text_splitter.split_text(data[0].page_content)
texts[0]
한국의 역사는 수천 년에 걸쳐 이어져 온 긴 여정 속에서 다양한 문화와 전통이 형성되고 발전해 왔습니다. 고조선에서 시작해 삼국 시대의 경쟁, 그리고 통일 신라와 고려를 거쳐 조선까지, 한반도는 많은 변화를 겪었습니다.\n고조선은 기원전 2333년 단군왕검에 의해 세워졌다고 전해집니다. 이는 한국 역사상 최초의 국가로, 한민족의 시원이라 할 수 있습니다. 이후 기원전 1세기경에는 한반도와 만주 일대에서 여러 소국이 성장하며 삼한 시대로 접어듭니다.\n...<중략>...\n해방 후 한반도는 남북으로 분단되어 각각 다른 정부가 수립되었고, 1950년에는 한국전쟁이 발발하여 큰 피해를 입었습니다. 전쟁 후 남한은 빠른 경제 발전을 이루며 오늘날에 이르렀습니다.

 

RecursiveCharacterTextSplitter

재귀적으로 텍스트 분할, 의미적으로 관련있는 청크 조각들이 모이도록 함

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 500,
    chunk_overlap  = 100,
    length_function = len,
)

texts = text_splitter.split_text(data[0].page_content)
print(len(texts[0]), len(texts[1]))
texts[0]

 

Tokenizer

토큰 수 기준으로 분할

text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=600,
    chunk_overlap=200,
    encoding_name='cl100k_base'
)

docs = text_splitter.split_documents(data)

 

3. Embedding

텍스트 → 숫자 벡터

 

- 텍스트 데이터를 벡터 공간 내에서 다룸: 텍스트 간 유사성 계산, 머신러닝/자연어처리 가능

- 활용

ㄴ의미 검색: 의미적 유사 텍스트 검색, 관련도 높은 문서/정보

ㄴ문서 분류: 특정 카테고리/주제에 분류

ㄴ텍스트 유사도 계싼

 

임베딩 모델 1. OpenAIEmbeddings

embed_documents(문서), embed_query(단일 쿼리)

from langchain_openai import OpenAIEmbeddings

embeddings_model = OpenAIEmbeddings()
embeddings = embeddings_model.embed_documents(
    [
        '안녕하세요!',
        '어! 오랜만이에요',
        '이름이 어떻게 되세요?',
        '날씨가 추워요',
        'Hello LLM!'
    ]
)

# 텍스트 리스트 개수(임베딩 과정을 거친 총 문서 수), 첫번째 문서의 벡터 차원
print(len(embeddings), len(embeddings[0]))
embeddings[0][:10]
(5, 1536)
[-0.010432514362037182,
 -0.013580637983977795,
 -0.0064862752333283424,
 -0.018673377111554146,
 -0.018267985433340073,
 0.01667175441980362,
 -0.009222672320902348,
 0.003898732829838991,
 -0.00743641285225749,
 0.010071462020277977]
# embed_query: 단일 쿼리 문자열 - 임베딩
embedded_query = embeddings_model.embed_query('첫인사를 하고 이름을 물어봤나요?')
embedded_query[:10]
[0.003605559002608061,
 -0.024263586848974228,
 0.010929940268397331,
 -0.04110211506485939,
 -0.004533691331744194,
 0.021859880536794662,
 -0.004130976274609566,
 0.020613981410861015,
 -0.006814695429056883,
 0.007387306075543165]

 

해당 문서와 쿼리 간의 유사도를 측정해보면,

# 코사인 유사도(-1 ~ 1)
# 상위 문서와 쿼리 간 유사도 측정
import numpy as np
from numpy import dot
from numpy.linalg import norm

def cos_sim(a, b):
  return dot(a,b) / (norm(a) * norm(b))

for embedding in embeddings:
  print(cos_sim(embedding, embedded_query))
0.8348635137337618
0.8153783857089105
0.8844739248939817
0.7899103053431074
0.7468845030598241

세번째 문서('이름이 어떻게 되세요?')와 쿼리('첫인사를 하고 이름을 물어봤나요?')의 유사도가 가장 높게 나옴

2. HuggingFaceEmbeddings

sentence-transformers 라이브러리를 통해 사전훈련된 임베딩 모델 활

### HuggingFaceEmbeddings
from langchain_community.embeddings import HuggingFaceEmbeddings

embeddings_model = HuggingFaceEmbeddings(
    model_name = 'jhgan/ko-sroberta-nli',   # 사용할 모델: 자연어 추론NLI에 적합한 ko-sroberta
    model_kwargs = {'device': 'cpu'},       # 'cuda'는 GPU
    # 임베딩 정규화하여 모든 벡터가 같은 범위 값을 같도록 -> 유사도 계산 시 일관성 높임
    encode_kwargs = {'normalize_embeddings': True},
)

embeddings_model
HuggingFaceEmbeddings(client=SentenceTransformer(
  (0): Transformer({'max_seq_length': 128, 'do_lower_case': False}) with Transformer model: RobertaModel 
  (1): Pooling({'word_embedding_dimension': 768, 'pooling_mode_cls_token': False, 'pooling_mode_mean_tokens': True, 'pooling_mode_max_tokens': False, 'pooling_mode_mean_sqrt_len_tokens': False, 'pooling_mode_weightedmean_tokens': False, 'pooling_mode_lasttoken': False, 'include_prompt': True})
), model_name='jhgan/ko-sroberta-nli', cache_folder=None, model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True}, multi_process=False, show_progress=False)
embeddings = embeddings_model.embed_documents(
    [
        '안녕하세요!',
        '어! 오랜만이에요',
        '이름이 어떻게 되세요?',
        '날씨가 추워요',
        'Hello LLM!'
    ]
)
embedded_query = embeddings_model.embed_query('첫인사를 하고 이름을 물어봤나요?')

for embedding in embeddings:
    print(cos_sim(embedding, embedded_query))
0.5899016189601531
0.4182631225980652
0.7240604521610333
0.05702662997392148
0.4316418328113528

 

 

 

cf)

https://wikidocs.net/231364