일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- langchain
- splitter
- ai language
- csvloader
- GPT
- azure ai
- azure services
- 챗봇 만들기
- 자연어 처리
- runnableparallel
- Azure
- 자연어처리
- RecursiveCharacterTextSplitter
- rag
- teddynote
- webbaseloader
- 문서/번역 서비스 사용
- Parallel
- azure open ai service
- runnablelambda
- Runnable
- pdfloader
- chain생성
- azureai
- Multimodal
- azure open ai
- lcel
- runnablepassthrough
- langsmith
- OpenAI
- Today
- Total
Nathaniel
10. Langchain-Embeddings 본문
Embed는 문서인 Document → Load하고 문서 내부의 문단, 문장, 단어로 Splitter 분할기를 사용하여 자른다.
그 자른 값 즉 chunk들을 Embedding/ 벡터화 된 숫자값으로 변환해야한다.
Embedding을 왜 벡터 값으로 변환하냐면 chunk별로 별도의 숫자를 부여해서 이 숫자를 가진 값과 유저가 입력 값으로 넣은 질문이 비슷한 값을 가지고 있는지 유사도 검색(retriever)하고 유사한 문장을 가져와 유저에게 답변을 주는 데에 중요한 역할을 하기 때문에 Embedding을 하는 것.
문서 내용을 Split으로 첫 단추를 잘 끼우는 것 좋지만, Embedding 모델을 잘 선택해야 하는 것도 문제이다. 우리는 한글로 번역된 모델을 사용해야 하기 때문에 영어보다는 한글에 특화된 임베딩 모델을 써야 그만큼 임베딩시켜서 유사도 검색으로 할루시네이션을 어느정도 상쇄시킬 수 있기 때문이다.
벡터값인 숫자표현으로 어떠한 알고리즘을 써야 할 것인가를 신경써서 적용하면 된다.
임베딩 모델은 각 벡터값을 가진 차원이 존재한다. 흔히 쓰고 있는 ChatGPT의 Embedding FAISS는 1536차원을 쓰고 있지만, 다른 모델들은 767차원, 1580차원 등등을 쓸 수 있을것이다.
차원이 높아짐에 따라서 정교한 표현이 가능하다는 것을 이해할 수 있는데 그게 곧 성능이 좋아진다는 뜻은 아니다.
어떤 임베딩을 써야 RAG해서 잘 사용할 수 있을지는 오픈소스 모델인 huggingFace Embeddings, Ollama Embeddings를 사용해서 공부해보려고 한다.
※ 여기서 벡터 값인 차원의 개수들은 1536차원이라면 → 1536개 숫자 값들을 가지고 있다는 뜻이다.
Split, Embedding
1. Split하고 난 후의 벡터 값을 가진 3개 단락
- 1번 단락: [0.1, 0.9, 0.5, ... , 0.3]
- 2번 단락: [0.2, 0.5, 0.4, ... , 0.2]
- 3번 단락: [0.5, 0.1, 0.2, ... , 0.8]
2. 유저 질문: "한국에서 가장 유명한 문화재는 어디인가요?"
- [0.2, 0.5, 0.4, ... , 0.1]
3. 유사도 계산 시
- 1번 단락: 20%
- 2번 단락: 89% → 유사도가 제일 높으므로 유저 질문에 맞는 단락으로 선택
- 3번 단락: 33%
그런데 이 유사도 검색은 알고리즘에 따라서 다르다. 어떠한 임베딩 알고리즘을 쓰는지에 따라서
유사도 검색 결과는 달라지기 때문에 잘 선택해야 한다는 뜻이다.
OpenAI Embedding을 쓰는 이유는???
1. 다국어 지원, 2. 임베딩을 하는 과정(하드웨어 리소스 GPU 등)이 있는데.
위 두 개를 만족하는 OpenAI의 Embedding을 쓰게 되면 서버 GPU 리소스 자원을 쓰기 때문에 비용이 든다...
나의 대용량 문서를 사용해서 답변 출력을 받으려면 당연히 리소스 비용이 엄청날 것이다
그래서 오픈소스인 HuggingFace, Ollama를 써서 내가 직접 RAG 또는 파인튜닝을 하는 방법이 있다.
하지만 그만큼 인력소모가 필요하다.
※ HuggingFace 한국어모델 좋은 성능 : BGE-M3, e5
아래는 임베딩 벤치마크 순위 링크이다. 참고!
https://github.com/Atipico1/Kor-IR?tab=readme-ov-file
GitHub - Atipico1/Kor-IR: Kor-IR: Korean Information Retrieval Benchmark
Kor-IR: Korean Information Retrieval Benchmark. Contribute to Atipico1/Kor-IR development by creating an account on GitHub.
github.com
벡터값을 Embedding을 하게 되면 VectorDB에 저장을 해줘야 벡터값들이 저장되어서 유사도 검색까지가 진행이된다.
그리고 OpenAIEmbedding 알고리즘의 1536차원을 Vector DB에 저장해야 할 때, Vector DB가 1024차원만 지원이되어서
서로 호환이 안된다면 별도로 `dimesions=1024`를 줘서 차원을 낮춰 임베딩 값을 DB와 맞출 수 있다.
# OpenAI의 "text-embedding-3-small" 모델을 사용하여 1024차원의 임베딩을 생성하는 객체를 초기화합니다.
embeddings_1024 = OpenAIEmbeddings(model="text-embedding-3-small", dimensions=1024)
# 주어진 텍스트를 임베딩하고 첫 번째 임베딩 벡터의 길이를 반환합니다.
len(embeddings_1024.embed_documents([text])[0])
# 출력
1024
유사도 검색
그다음은 유사도 계산을 해야하는데, 문장 5개를 Cosine similarity 유사도를 쓸 것이다.
아래의 sentence 1, 2, 3은 같으면서 비슷한 문장들이 있어서 유사도 값은 1에 조금 가까운 값들이 나올 것이다.
영어로 된 sentence4는 1,2,3 과 비슷하지만 Cross Languqge라고 한글과 영어와의 유사도를 구하게 된다면
유사도에 손실이 생겨서 유사도 값이 '1'과 조금 많이 떨어질 수 있다. 이거는 또 알고리즘마다 차이가 있다.
한글, 영어와의 유사도를 잘 잡아주는 embeder를 잘 선택해서 사용해야한다.
sentence1 = "안녕하세요? 반갑습니다."
sentence2 = "안녕하세요? 반갑습니다!"
sentence3 = "안녕하세요? 만나서 반가워요."
sentence4 = "Hi, nice to meet you."
sentence5 = "I like to eat apples."
from sklearn.metrics.pairwise import cosine_similarity
sentences = [sentence1, sentence2, sentence3, sentence4, sentence5]
embedded_sentences = embeddings_1024.embed_documents(sentences)
def similarity(a, b):
return cosine_similarity([a], [b])[0][0]
# sentence1 = "안녕하세요? 반갑습니다."
# sentence2 = "안녕하세요? 만나서 반가워요."
# sentence3 = "Hi, nice to meet you."
# sentence4 = "I like to eat apples."
for i, sentence in enumerate(embedded_sentences):
for j, other_sentence in enumerate(embedded_sentences):
if i < j:
print(
f"[유사도 {similarity(sentence, other_sentence):.4f}] {sentences[i]} \t <=====> \t {sentences[j]}"
)
# 출력
[유사도 0.9644] 안녕하세요? 반갑습니다. <=====> 안녕하세요? 반갑습니다!
[유사도 0.8375] 안녕하세요? 반갑습니다. <=====> 안녕하세요? 만나서 반가워요.
[유사도 0.5043] 안녕하세요? 반갑습니다. <=====> Hi, nice to meet you.
[유사도 0.1362] 안녕하세요? 반갑습니다. <=====> I like to eat apples.
[유사도 0.8142] 안녕하세요? 반갑습니다! <=====> 안녕하세요? 만나서 반가워요.
[유사도 0.4792] 안녕하세요? 반갑습니다! <=====> Hi, nice to meet you.
[유사도 0.1318] 안녕하세요? 반갑습니다! <=====> I like to eat apples.
[유사도 0.5128] 안녕하세요? 만나서 반가워요. <=====> Hi, nice to meet you.
[유사도 0.1409] 안녕하세요? 만나서 반가워요. <=====> I like to eat apples.
[유사도 0.2250] Hi, nice to meet you. <=====> I like to eat apples.
위 출력 값들을 보면 sentence1인 "안녕하세요? 반갑습니다." ↔ sentence2인 "안녕하세요? 반갑습니다!"는
유사도가 0.9644가 나온 것을 볼 수 있는데 둘의 차이는 '!'와 '.'의 차이밖에없다. 문장으로서는 동일하게 볼 수 있기 때문에
96.44%의 유사도가 나온 것을 알 수 있다.
하지만 반면에 sentence1 ↔ sentence2, 3과 비교할 때 문장이 비슷하지만
안녕하세요? 다음으로오는 문장들이 바뀌면서 유사도가 떨어지는 것을 볼 수 있다. 이처럼 한글과의 임베딩 유사도는 잘 측정하는 것을 볼 수 있는데 한글과 영어가 뜻은 같아도 sentence1 ↔ sentence4, 5와 비교 시 유사도가 많이 떨어진다.
위 방법은 오픈소스가 아니라, OpenAI의 Embedding 모델인 text-embedding-3-small을 써서 임베딩 했기 때문에
돈이 들었지만 이부분을 또 CacheBackedEmbeddings로 이미 Embedding 했던 문서를 또 다시 안 해도 되게끔
데이터 캐싱을 하는 방법이 있다. 이렇게 쓰면 리소스 비용을 소모하지 않을 수 있다.
CacheBackedEmbeddings
아래 코드는 로컬 파일 저장소 설정이다.
from langchain.storage import LocalFileStore
from langchain_openai import OpenAIEmbeddings
from langchain.embeddings import CacheBackedEmbeddings
from langchain_community.vectorstores.faiss import FAISS
# OpenAI 임베딩을 사용하여 기본 임베딩 설정
embedding = OpenAIEmbeddings()
# 로컬 파일 저장소 설정
store = LocalFileStore("./cache/")
# 캐시를 지원하는 임베딩 생성
cached_embedder = CacheBackedEmbeddings.from_bytes_store(
underlying_embeddings=embedding,
document_embedding_cache=store,
namespace=embedding.model, # 기본 임베딩과 저장소를 사용하여 캐시 지원 임베딩을 생성
)
# store에서 키들을 순차적으로 가져옵니다.
list(store.yield_keys())
from langchain.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter
# 문서 로드
raw_documents = TextLoader("./data/appendix-keywords.txt", encoding="UTF-8").load()
# 문자 단위로 텍스트 분할 설정
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
# 문서 분할
documents = text_splitter.split_documents(raw_documents)
※ 아래 코드는 캐싱 임베딩을 사용해서 FAISS 데이터 베이스 생성하는 코드
# 코드 실행 시간을 측정합니다.
%time db = FAISS.from_documents(documents, cached_embedder) # 문서로부터 FAISS 데이터베이스 생성
# 출력
CPU times: total: 0 ns
Wall time: 10.8 ms
캐싱 임베딩 파일들은 cache파일 생성한 코드로 인해서 벡터 값들이 저장되었고
아래 사진이 내가 지정한 임베딩 알고리즘으로 벡터화 시킨 데이터이다. chunk(문단, 문장, 단어)별로 만들어졌따!
이번엔 아래의 코드는 로컬 파일에 별도로 저장시키는 캐싱 임베딩 데이터가 아니라,
사용할 때만 잠시 메모리에 비영구적으로 캐싱 시키는 InMemory 코드이다. 사용에 따라 다르겠지만,
아래의 코드는 굳이...사용을 할까? 라는 생각을한다.
from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import InMemoryByteStore
store = InMemoryByteStore() # 메모리 내 바이트 저장소 생성
# 캐시 지원 임베딩 생성
cached_embedder = CacheBackedEmbeddings.from_bytes_store(
embedding, store, namespace=embedding.model
)
HugginFaceEmbedding
허깅페이스 임베딩은 앞에 언급한 것처럼 오픈소스로 굉장히 유명한 것인데, 임베딩 모델을 다운 받아야 되지만 용량이 크다보니 별도의 파일에 모델을 다운로드해서 디스크용량 관리를 위해 사용해야 한다. 어딘지 모르는 곳에 다운로드 받는 순간 찾을 수 없을 수도 있다.... 조심해서 사용해야 하니깐 cache 폴더에 별도로 지정해서 사용하자.
import os
import warnings
# 경고 무시
warnings.filterwarnings("ignore")
# ./cache/ 경로에 다운로드 받도록 설정
os.environ["HF_HOME"] = "./cache/"
샘플 데이터
# 샘플 데이터
texts = [
"안녕, 만나서 반가워.",
"LangChain simplifies the process of building applications with large language models",
"랭체인 한국어 튜토리얼은 LangChain의 공식 문서, cookbook 및 다양한 실용 예제를 바탕으로 하여 사용자가 LangChain을 더 쉽고 효과적으로 활용할 수 있도록 구성되어 있습니다. ",
"LangChain은 초거대 언어모델로 애플리케이션을 구축하는 과정을 단순화합니다.",
"Retrieval-Augmented Generation (RAG) is an effective technique for improving AI responses.",
]
허깅페이스 임베딩 사용 코드
from langchain_huggingface.embeddings import HuggingFaceEndpointEmbeddings
model_name = "intfloat/multilingual-e5-large-instruct"
hf_embeddings = HuggingFaceEndpointEmbeddings(
model=model_name,
task="feature-extraction",
huggingfacehub_api_token=os.environ["HUGGINGFACEHUB_API_TOKEN"],
)
print("[HuggingFace Endpoint Embedding]")
print(f"Model: \t\t{model_name}")
print(f"Dimension: \t{len(embedded_documents[0])}")
# 출력
[HuggingFace Endpoint Embedding]
Model: intfloat/multilingual-e5-large-instruct
Dimension: 1024
# Document Embedding 수행
embedded_query = hf_embeddings.embed_query("LangChain 에 대해서 알려주세요.")
임베딩된 벡터 값은 유사도 계산을 해서 query(질문) * documents(문서)Τ 를 해야한다.
유사도 계산은 아래 사진에 나와 있는 데이터를 보면 이해하기 쉽다!
이제 아래는 유사도 계산하는 코드이다
행렬 데이터를 볼 때 자주 사용하는 Numpy 모듈을 가져와서 사용한다.
샘플 데이터 text에 넣은 5개 문장을 아래와 같이 문서와 유사도 검사를 한 출력 값이 나온다.
texts :
- "안녕, 만나서 반가워.",
- "LangChain simplifies the process of building applications with large language models"
- "랭체인 한국어 튜토리얼은 LangChain의 공식 문서, cookbook 및 다양한 실용 예제를 바탕으로 하여 사용자가 LangChain을 더 쉽고 효과적으로 활용할 수 있도록 구성되어 있습니다. ",
- "LangChain은 초거대 언어모델로 애플리케이션을 구축하는 과정을 단순화합니다."
- "Retrieval-Augmented Generation (RAG) is an effective technique for improving AI responses.",
사용자 질문:
- "Langchain에 대해서 알려주세요"
사용자 질문(query) * texts(documents)Τ를 했을 때
array([0.84186316, 0.8650232 , 0.86470298, 0.89564878, 0.76847333])가 나온다.
import numpy as np
# 질문(embedded_query): LangChain 에 대해서 알려주세요.
np.array(embedded_query) @ np.array(embedded_documents).T
# 출력
array([0.84186316, 0.8650232 , 0.86470298, 0.89564878, 0.76847333])
그리고 argsort로 내림차순 분류를 해서 가장 유사도 순위가 높은 순으로 출력을 받아본 것인데,
인덱스 순서상 0.89564878이 3번 째이고 가장 유사도가 높아서 첫 번째 순서로 나온 것을 볼 수 있다.
sorted_idx = (np.array(embedded_query) @ np.array(embedded_documents).T).argsort()[::-1]
sorted_idx
# 출력
array([3, 1, 2, 0, 4], dtype=int64)
다시 유사도 순위 별로 인덱스 값들을 뽑아내자면 아래의 코드를 쓰면된다.
print("[Query] LangChain 에 대해서 알려주세요.\n====================================")
for i, idx in enumerate(sorted_idx):
print(f"[{i}] {texts[idx]}")
print()
# 출력
[Query] LangChain 에 대해서 알려주세요.
====================================
[0] LangChain은 초거대 언어모델로 애플리케이션을 구축하는 과정을 단순화합니다.
[1] LangChain simplifies the process of building applications with large language models
[2] 랭체인 한국어 튜토리얼은 LangChain의 공식 문서, cookbook 및 다양한 실용 예제를 바탕으로 하여 사용자가 LangChain을 더 쉽고 효과적으로 활용할 수 있도록 구성되어 있습니다.
[3] 안녕, 만나서 반가워.
[4] Retrieval-Augmented Generation (RAG) is an effective technique for improving AI responses.
'AI' 카테고리의 다른 글
9. Langchain-RecursiveCharacterTextSplitter (0) | 2025.03.22 |
---|---|
8. Langchain Document_Loader, Parser (0) | 2025.03.19 |
7. Langchain 캐싱(API 호출 비용 감소) (0) | 2025.03.17 |
6. Langchain Parser?? (0) | 2025.03.11 |
5. Lagnchain-prompt-template 생성 (0) | 2025.03.07 |