목차
- 오늘 강의 개요
- CacheBackedEmbeddings 심화 - 성능 비교
- 배치(Batch) 처리로 대량 문서 임베딩
- 코사인 유사도 심화 - 부정 표현의 한계
- 유사 문서 Top-K 검색 함수
- 임베딩 시각화 준비 - 4개 토픽 데이터
- PCA - 주성분 분석으로 2차원 시각화
- t-SNE - 비선형 차원 축소
- PCA vs t-SNE 비교
- FAISS 벡터스토어 기초
- Document + 메타데이터 벡터스토어
- 다국어 임베딩 (Cross-lingual)
- 핵심 개념 총정리
- 자주 나오는 실수 / 주의사항
1. 오늘 강의 개요
오늘은 임베딩을 실제로 활용하는 심화 내용을 다룬다. 캐시로 비용 절감, 배치로 효율화, 시각화로 임베딩 이해도 높이기, FAISS 입문.
17일 복습 → 오늘 심화
- 17일: 임베딩 개념, tiktoken, chunking, 코사인 유사도 기초
- 18일: 캐시 성능 비교, 배치 처리, 부정표현 한계, 시각화(PCA/t-SNE), FAISS
오늘 핵심 질문
- 임베딩 캐시가 실제로 얼마나 빠른가?
- “좋아한다” vs “싫어한다” - 임베딩이 반대 의미를 인식할까?
- 1536차원 벡터를 어떻게 눈으로 볼 수 있을까?
- FAISS가 cosine_similarity와 어떻게 다른가?
2. CacheBackedEmbeddings 심화 - 성능 비교
캐시 설정
1
2
3
4
5
6
7
8
9
| from langchain_classic.embeddings import CacheBackedEmbeddings
from langchain_core.stores import InMemoryByteStore
import time
embedding_model = OpenAIEmbeddings(model='text-embedding-3-small')
store = InMemoryByteStore()
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
embedding_model, store, namespace="embedding-cache"
)
|
성능 비교 실험
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| texts = ["오늘 점심에는 라면을 먹었습니다", "내일 저녁에는 햄버거를 먹을겁니다"]
# 1차 요청 - API 호출 발생
start = time.time()
vecs1 = cached_embeddings.embed_documents(texts)
t1 = time.time() - start
print(f"1차 call: {t1:.4f}초") # 약 0.4248초
# 2차 요청 - 캐시에서 반환
start = time.time()
vecs2 = cached_embeddings.embed_documents(texts)
t2 = time.time() - start
print(f"2차 call: {t2:.4f}초") # 약 0.0015초
print(f"속도 향상: {t1/t2:.0f}배") # 약 280배 빠름!
|
실제 출력 결과
1
2
3
| 1차 call: 0.42476630210876465초 ← API 네트워크 왕복
2차 call: 0.0014560222625732422초 ← 메모리에서 즉시 반환
# 속도 향상: 약 280배
|
같은 텍스트를 다시 임베딩하면 API 호출 없이 즉시 반환
RAG 시스템에서 문서를 반복 처리할 때 엄청난 비용/시간 절감
3. 배치(Batch) 처리로 대량 문서 임베딩
왜 배치 처리가 필요한가?
- 한 번에 수백 개의 문서를 임베딩할 때 API 제한(rate limit)에 걸릴 수 있음
- batch_size로 나눠서 처리하면 안정적
배치 처리 함수
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
| import tiktoken
def count_tokens(text, model='gpt-4o-mini'):
enc = tiktoken.encoding_for_model(model)
return len(enc.encode(text))
def process_documents(docs, embedding_model, batch_size=5):
all_vectors = []
total_tokens = 0
start = time.time()
for i in range(0, len(docs), batch_size):
batch = docs[i:i+batch_size]
batch_tokens = sum(count_tokens(d) for d in batch)
total_tokens += batch_tokens
vectors = embedding_model.embed_documents(batch)
all_vectors.extend(vectors)
progress = min(i + batch_size, len(docs))
print(f" [{progress:3}/{len(docs)}] batch {i//batch_size+1} ({batch_tokens} tokens)")
elapsed = time.time() - start
print(f"\n완료: {len(docs)}개 문서, {total_tokens} tokens, {elapsed:.2f}초")
return all_vectors
|
실행 결과 (20개 문서, batch_size=5)
1
2
3
4
5
6
7
8
9
10
11
| docs = [f"doc {i}: 이것은 테스트 문서입니다. 인공지능에 관련한 문서입니다" for i in range(20)]
vectors = process_documents(docs, cached_embeddings, batch_size=5)
# [ 5/20] batch 1 process (100 tokens)
# [10/20] batch 2 process (100 tokens)
# [15/20] batch 3 process (100 tokens)
# [20/20] batch 4 process (100 tokens)
# 완료: 20개 문서, 400 tokens, 3.58초
print(len(vectors)) # 20 (벡터 20개)
print(len(vectors[0])) # 1536 (각 벡터 차원)
|
range(0, len(docs), batch_size) : 0, 5, 10, 15 씩 이동
docs[i:i+batch_size] : 5개씩 슬라이싱
4. 코사인 유사도 심화 - 부정 표현의 한계
중요한 실험 결과
1
2
3
4
5
6
7
8
9
10
11
12
13
| from sklearn.metrics.pairwise import cosine_similarity
pairs = [
("나는 축구를 많이 좋아한다", "나는 축구를 싫어한다"),
("나는 축구를 많이 좋아한다", "나는 축구를 좋아한다"),
("나는 축구를 많이 싫어한다", "나는 축구를 싫어한다"),
]
for s1, s2 in pairs:
v1 = embedding_model.embed_query(s1)
v2 = embedding_model.embed_query(s2)
sim = cosine_similarity([v1], [v2])[0][0]
print(f"{sim:.4f} | '{s1[:15]}' ↔ '{s2[:15]}'")
|
실험 결과
| 유사도 | 문장 쌍 |
|---|
| 0.8318 | “좋아한다” ↔ “싫어한다” ← 의미 반대인데 유사도 높음! |
| 0.9600 | “많이 좋아한다” ↔ “좋아한다” ← 비슷한 의미, 매우 높음 |
| 0.9490 | “많이 싫어한다” ↔ “싫어한다” ← 비슷한 의미, 매우 높음 |
핵심 포인트
“좋아한다”와 “싫어한다”는 의미가 반대지만 유사도 0.83으로 매우 높음!
임베딩은 단어의 “극성(긍정/부정)” 보다 “주제/맥락” 유사성을 더 반영
“축구”라는 공통 주제 때문에 비슷한 벡터 위치에 있는 것
실무 함의:
- “재택근무 좋아요” vs “재택근무 싫어요” → 둘 다 “재택근무 관련 문서”로 검색됨
- 감정 분석, 의견 분류 등에서 임베딩만으로 극성 구분은 어려울 수 있음
5. 유사 문서 Top-K 검색 함수
구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| def find_most_similar(query, corpus, top_k=3):
"""쿼리와 가장 유사한 corpus 문서 top_k개 반환"""
all_texts = [query] + corpus
all_embs = embedding_model.embed_documents(all_texts)
query_emb = all_embs[0]
corpus_emb = all_embs[1:]
# 코사인 유사도 계산
sims = cosine_similarity([query_emb], corpus_emb)[0]
# 유사도 내림차순 정렬
ranked = sorted(enumerate(sims), key=lambda x: x[1], reverse=True)
print(f"\n쿼리: '{query}'")
print("-" * 50)
for rank, (idx, sim) in enumerate(ranked[:top_k]):
print(f" {rank+1}위 sim={sim:.4f} | {corpus[idx]}")
return ranked[:top_k]
|
사용 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
| corpus = [
"LangChain으로 챗봇 만들기",
"LangChain은 LLM 앱 개발 프레임워크입니다",
"프롬프트 엔지니어링은 AI에게 좋은 지시를 작성하는 기술입니다",
"파이썬은 데이터 분석에 많이 사용됩니다",
"오늘 저녁 치킨 먹고 싶다"
]
find_most_similar('AI 챗봇 개발하고 싶어요', corpus)
# 쿼리: 'AI 챗봇 개발하고 싶어요'
# 1위 sim=0.5934 | LangChain으로 챗봇 만들기
# 2위 sim=0.3132 | LangChain은 LLM 앱 개발 프레임워크입니다
# 3위 sim=0.2639 | 프롬프트 엔지니어링은 AI에게 좋은 지시를 작성하는 기술입니다
|
6. 임베딩 시각화 준비 - 4개 토픽 데이터
1536차원을 눈으로 보는 방법
1
2
3
| 1536차원 벡터는 시각화 불가능
→ 차원 축소로 2차원으로 압축 후 시각화
→ PCA 또는 t-SNE 사용
|
데이터 준비
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
| topic_sentences = {
"tech" : ["인공지능이 산업을 혁신하고 있다",
"머신러닝 모델이 의료 진단을 개선하고 있다",
"클라우드 컴퓨팅이 비즈니스를 변화시키고 있다",
"자율주행 기술이 교통의 미래를 바꾸고 있다"],
"sports" : ["축구 경기에서 역전골이 터졌다",
"마라톤 선수가 세계 신기록을 세웠다",
"농구팀이 챔피언십 결승에 진출했다",
"수영 선수가 올림픽 금메달을 획득했다"],
"food" : ["이 레스토랑의 파스타가 맛있었다",
"한국 전통 음식인 비빔밥을 만들었다",
"새로운 카페에서 특별한 커피를 마셨다",
"요즘 유행하는 마라탕을 처음 먹어봤다"],
"finance": ["주식 시장이 크게 하락했다",
"금리 인상으로 대출 부담이 증가했다",
"비트코인 가격이 급등하고 있다",
"기업 실적 발표로 주가가 상승했다"],
}
# 임베딩
all_texts = [s for sentences in topic_sentences.values() for s in sentences]
all_labels = [topic for topic, sentences in topic_sentences.items() for _ in sentences]
all_embs = embedding_model.embed_documents(all_texts)
emb_matrix = np.array(all_embs) # shape: (16, 1536)
|
7. PCA - 주성분 분석으로 2차원 시각화
PCA란?
- Principal Component Analysis (주성분 분석)
- 1536차원 → 2차원으로 선형 변환
- 분산이 가장 큰 방향(주성분)을 2개 선택해서 압축
코드
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
| from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
pca = PCA(n_components=2)
coords_2d = pca.fit_transform(emb_matrix) # (16, 1536) → (16, 2)
fig, ax = plt.subplots(figsize=(12, 8))
colors = {'tech': 'blue', 'sports': 'red', 'food': 'green', 'finance': 'orange'}
for topic in topic_sentences:
mask = [l == topic for l in all_labels]
indices = [i for i, m in enumerate(mask) if m]
x_coords = [coords_2d[i, 0] for i in indices]
y_coords = [coords_2d[i, 1] for i in indices]
ax.scatter(x_coords, y_coords, label=topic, s=100, c=colors[topic])
# 텍스트 레이블
for i, idx in enumerate(indices):
ax.annotate(f"{topic}{i+1}", (coords_2d[idx, 0], coords_2d[idx, 1]))
ax.legend(fontsize=12)
ax.set_title('PCA - 임베딩 2차원 시각화')
plt.show()
|
fit_transform(emb_matrix) : 1536차원 → 2차원으로 변환
결과: 같은 토픽끼리 군집(cluster)을 이루는지 확인
PCA의 특징
- 선형 변환 → 빠름
- 분산이 큰 방향 순서로 차원 압축
- 글로벌 구조(전체 분포) 잘 보존
- 비선형 구조 포착에 한계
8. t-SNE - 비선형 차원 축소
t-SNE란?
- t-Distributed Stochastic Neighbor Embedding
- 비선형 방식으로 고차원 → 2차원 압축
- 가까운 점은 가깝게, 먼 점은 멀게 유지하는 방식
- 군집 패턴을 더 명확하게 시각화
코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| from sklearn.manifold import TSNE
tsne = TSNE(
n_components = 2,
random_state = 42,
perplexity = 5 # 주변 이웃 고려 범위 (데이터 수가 적으면 작게!)
)
coords_tsne = tsne.fit_transform(emb_matrix) # (16, 1536) → (16, 2)
# 시각화 (PCA와 동일한 방식)
fig, ax = plt.subplots(figsize=(12, 8))
# ... (PCA와 동일)
ax.set_title('t-SNE - 임베딩 2차원 시각화')
plt.show()
|
perplexity : 주변 이웃 고려 개수 설정
- 데이터가 적으면 작게 (데이터 수의 1/4 정도)
- 데이터가 많으면 크게 (30~50)
- perplexity >= 데이터 수 이면 에러
random_state=42 : 재현 가능한 결과를 위해 고정
9. PCA vs t-SNE 비교
| 항목 | PCA | t-SNE |
|---|
| 변환 방식 | 선형 | 비선형 |
| 속도 | 빠름 | 느림 |
| 군집 시각화 | 보통 | 뛰어남 (더 명확한 군집) |
| 글로벌 구조 보존 | 잘 보존 | 국소 구조 중심 |
| 결정적 | 항상 같은 결과 | random_state 필요 |
| 데이터 크기 | 대용량도 OK | 소규모 적합 |
| 사용 상황 | 탐색적 분석 시작 | 최종 시각화 |
언제 무엇을 쓰나?
빠른 확인이 필요할 때 → PCA 먼저
군집이 명확하게 보이는 시각화가 필요할 때 → t-SNE
10. FAISS 벡터스토어 기초
FAISS란?
- Facebook AI Similarity Search
- 대규모 벡터의 효율적인 유사도 검색 라이브러리
- sklearn cosine_similarity보다 훨씬 빠름 (대규모 데이터에서)
cosine_similarity vs FAISS 차이
cosine_similarity:
- 점수 높을수록 유사 (1에 가까울수록)
- 직접 코드로 구현
FAISS:
- L2 거리(유클리드 거리) 기본 사용
- 점수 낮을수록 유사 (0에 가까울수록)
- ← 방향이 반대!
기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
embedding_model = OpenAIEmbeddings(model='text-embedding-3-small')
# 문자열 리스트로 벡터스토어 생성
faq_docs = [
"RAG는 외부 문서를 검색하여 LLM 답변 정확도를 높이는 기술입니다.",
"토큰은 LLM이 텍스트를 처리하는 기본 단위입니다.",
"LangChain은 LLM 애플리케이션 개발을 위한 프레임워크입니다.",
"임베딩은 텍스트를 숫자 벡터로 변환하는 기술입니다.",
]
vectorstore = FAISS.from_texts(faq_docs, embedding_model)
|
유사도 검색 (점수 포함)
1
2
3
4
5
6
7
8
9
| query = "RAG가 뭐에요?"
results = vectorstore.similarity_search_with_score(query, k=3)
for doc, score in results:
print(f"[score={score:.4f}] {doc.page_content}")
# [score=0.9826] RAG는 외부 문서를 검색하여 LLM 답변 정확도를 높이는 기술입니다.
# [score=1.4886] 토큰은 LLM이 텍스트를 처리하는 기본 단위입니다.
# [score=1.5458] LangChain은 LLM 애플리케이션 개발을 위한 프레임워크입니다.
|
점수 0.98 → 가장 유사 (거리가 가까움)
점수 1.54 → 덜 유사 (거리가 멈)
FAISS 점수는 낮을수록 유사! (cosine_similarity와 반대)
11. Document + 메타데이터 벡터스토어
메타데이터 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| from langchain_core.documents import Document
docs_with_meta = [
Document(
page_content = 'LangChain은 LLM앱 개발 프레임워크입니다',
metadata = {'category': 'framework', 'level': 'beginner'}
),
Document(
page_content = 'RAG는 검색증강생성 기술입니다',
metadata = {'category': 'technique', 'level': 'intermediate'}
),
Document(
page_content = '에이전트는 LLM이 도구를 자율적으로 사용하는 시스템입니다',
metadata = {'category': 'technique', 'level': 'advanced'}
),
]
vs_meta = FAISS.from_documents(docs_with_meta, embedding_model)
# 검색 결과에서 메타데이터 활용
results = vs_meta.similarity_search('AI 개발 도구', k=3)
for doc in results:
print(f"[{doc.metadata['category']}] [{doc.metadata['level']}] {doc.page_content}")
|
검색된 문서의 카테고리, 레벨, 출처 등 부가 정보 활용 가능
RAG 답변 시 “출처: [파일명]” 표시에 활용
12. 다국어 임베딩 (Cross-lingual)
text-embedding-3-small은 다국어 지원!
- 한국어 ↔ 영어 간 의미 유사도 계산 가능
- 한국어로 질문해도 영어 문서 검색 가능
한영 유사도 실험
1
2
3
4
5
6
7
8
9
10
| multilingual = [
('인공지능이 세상을 바꾸고 있다', '한국어'),
('Artificial intelligence is changing the world', '영어'),
('나는 학교에 갑니다', '한국어'),
('I go home', '영어'),
]
texts = [t for t, _ in multilingual]
embs = embedding_model.embed_documents(texts)
sim = cosine_similarity(embs)
|
결과
| 문장 쌍 | 유사도 |
|---|
| “인공지능이 세상을 바꾸고 있다” ↔ “AI is changing…” | 0.6016 ← 높음! |
| “나는 학교에 갑니다” ↔ “I go home” | 0.2973 ← 낮음 |
한국어 쿼리 → 영어 문서 검색
1
2
3
4
5
6
7
8
9
10
11
12
13
| english_docs = [
"RAG improves LLM accuracy by retrieving external documents.",
"Vector databases store and search embedding vectors efficiently.",
"Fine-tuning adapts pre-trained models to specific domains.",
]
vs_en = FAISS.from_texts(english_docs, embedding_model)
# 한국어로 질문!
results = vs_en.similarity_search_with_score("RAG가 뭐에요?", k=3)
for doc, score in results:
print(f"[{score:.4f}] {doc.page_content}")
# [1.0924] RAG improves LLM accuracy by retrieving external documents. ← 관련 문서 찾음!
|
한국어로 질문해도 영어 문서에서 관련 내용 검색 가능
다국어 서비스, 한영 혼합 문서 시스템에서 매우 유용
13. 핵심 개념 총정리
| 개념 | 핵심 내용 |
|---|
| CacheBackedEmbeddings | 중복 임베딩 API 호출 방지, 100배+ 속도 향상 |
| 배치 처리 | 대량 문서 batch_size 단위로 나눠서 처리 |
| 부정 표현 한계 | 임베딩은 극성(긍/부정)보다 주제 유사성 반영 |
| Top-K 검색 | 코사인 유사도 내림차순 정렬로 상위 k개 반환 |
| PCA | 선형 차원 축소 (빠름, 글로벌 구조 보존) |
| t-SNE | 비선형 차원 축소 (느림, 군집 시각화 우수) |
| FAISS | 대규모 벡터 유사도 검색 (L2 거리, 낮을수록 유사) |
| from_texts() | 문자열 리스트로 벡터스토어 생성 |
| from_documents() | Document 객체 리스트로 벡터스토어 생성 |
| similarity_search_with_score | 유사도 점수 포함 검색 |
| 다국어 임베딩 | 한국어 쿼리 → 영어 문서 검색 가능 |
FAISS 점수 vs 코사인 유사도 방향 정리
| 방법 | 높을수록 | 낮을수록 |
|---|
| cosine_similarity | 유사함 | 다름 |
| FAISS L2 distance | 다름 | 유사함 ← 반대! |
14. 자주 나오는 실수 / 주의사항
실수 1: FAISS 점수 방향 혼동 (★ 매우 흔한 실수)
1
2
3
4
5
6
7
8
9
10
| # FAISS similarity_search_with_score 결과
for doc, score in results:
print(f"score: {score:.4f}")
# 낮은 score = 유사함 (코사인과 반대!)
# threshold 적용 시 방향 주의
if score < 1.0: # 낮은 score가 관련 있음
print("관련 있는 문서")
if score > 1.5: # 높은 score는 관련 없음
print("관련 없는 문서")
|
실수 2: t-SNE perplexity가 데이터 수보다 크거나 같은 경우
1
2
3
| # 데이터 16개일 때
tsne = TSNE(n_components=2, perplexity=16) # ValueError! perplexity < n_samples 필요
tsne = TSNE(n_components=2, perplexity=5) # 올바른 코드 (16의 1/4 정도)
|
실수 3: FAISS.from_texts vs from_documents 혼용
1
2
3
4
5
6
7
| # from_texts: 문자열 리스트
vectorstore = FAISS.from_texts(["텍스트1", "텍스트2"], embedding_model)
# from_documents: Document 객체 리스트
vectorstore = FAISS.from_documents([Document(page_content="텍스트1"), ...], embedding_model)
# 두 방법 혼용하면 타입 에러!
|
실수 4: CacheBackedEmbeddings import 경로
1
2
3
4
5
| # 잘못된 경로 (에러 발생)
from langchain.embeddings import CacheBackedEmbeddings
# 올바른 경로
from langchain_classic.embeddings import CacheBackedEmbeddings
|
끝