포스트

2026-03-18 강의 정리

RAG의 기본 개념 / 문서 전처리 과정의 이해

2026-03-18 강의 정리

목차

  1. 오늘 강의 개요
  2. CacheBackedEmbeddings 심화 - 성능 비교
  3. 배치(Batch) 처리로 대량 문서 임베딩
  4. 코사인 유사도 심화 - 부정 표현의 한계
  5. 유사 문서 Top-K 검색 함수
  6. 임베딩 시각화 준비 - 4개 토픽 데이터
  7. PCA - 주성분 분석으로 2차원 시각화
  8. t-SNE - 비선형 차원 축소
  9. PCA vs t-SNE 비교
  10. FAISS 벡터스토어 기초
  11. Document + 메타데이터 벡터스토어
  12. 다국어 임베딩 (Cross-lingual)
  13. 핵심 개념 총정리
  14. 자주 나오는 실수 / 주의사항

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 비교

항목PCAt-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

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그