포스트

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. 자주 나오는 실수 / 주의사항
  15. 측정값과 해석의 범위를 맞추기

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 호출 없이 즉시 반환

같은 문서를 반복 처리하는 구간에서 API 재호출을 줄일 수 있으며, 전체 효과는 캐시 적중률과 저장소 조건에 따라 확인한다.


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

2차원 그림은 원래 벡터 공간을 압축한 결과다. 군집이 보기 좋게 나뉘었다는 이유만으로 검색 성능이 좋아졌다고 결론 내리지 않고, 원래 차원에서의 이웃과 실제 검색 결과를 함께 확인한다.

항목PCAt-SNE
변환 방식선형비선형
속도빠름느림
군집 시각화보통뛰어남 (더 명확한 군집)
글로벌 구조 보존잘 보존국소 구조 중심
비교 조건입력과 solver 설정을 고정입력과 random_state 등 설정을 고정
데이터 크기대용량도 OK소규모 적합
사용 상황탐색적 분석 시작최종 시각화

언제 무엇을 쓰나?

빠른 확인이 필요할 때 → PCA 먼저

군집이 명확하게 보이는 시각화가 필요할 때 → t-SNE


10. FAISS 벡터스토어 기초

FAISS란?

  • Facebook AI Similarity Search
  • 대규모 벡터의 효율적인 유사도 검색 라이브러리
  • 인덱스 종류와 데이터 규모에 맞춰 검색 성능을 비교할 수 있음

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 호출 감소, 캐시 적중 조건에서 효과 확인
배치 처리대량 문서 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

15. 측정값과 해석의 범위를 맞추기

캐시 비교와 부정 표현 실험은 서로 다른 질문에 답한다. 전자는 중복 요청의 처리 비용을, 후자는 현재 모델이 문장의 어떤 유사성을 반영하는지 보여 준다. 각각의 결과를 해당 실험 범위 안에서 해석한다.

검색기로 확장할 때는 캐시가 없는 첫 실행과 재실행을 나누어 시간을 측정하고, 부정·동의·주제 일치 문장을 구분한 질문 묶음으로 결과를 확인한다. 한 번의 속도 측정이나 그림만으로 모델 선택을 끝내지 않는다.

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

인기 태그