9장. 임베딩 모델 파인튜닝

출처: 『RAG 마스터: 랭체인으로 완성하는 LLM 서비스』(브라이스 유·조경아·박수진·김재웅, 프리렉 2025) | 공식: https://www.langchain.com/

RAG 시스템의 검색 품질은 결국 임베딩 모델이 "의미를 얼마나 잘 표현하느냐"에 달렸다. 일반 임베딩 모델은 범용 텍스트로 학습됐기 때문에, 전문 도메인 문서에서는 검색 정확도가 떨어질 수 있다. 이 장은 대조 학습(Contrastive Learning)의 원리부터 하드 네거티브 선정, 합성 데이터 생성, 실전 파인튜닝, 검색 성능 평가까지 임베딩 모델을 도메인에 맞게 최적화하는 전 과정을 다룬다.

실습 — 책 공식 노트북: Ch09. Embedding Fine-tuning 폴더. 코랩 바로 열기: 임베딩 파인튜닝 종합 — SentenceTransformerTrainer + MNRL + 하드 네거티브 마이닝 종합.

학습 목표

이 장을 끝내면 다음을 할 수 있다.

  • 대조 학습의 원리와 앵커·포지티브·네거티브 샘플의 역할을 설명한다.
  • 배치 내 네거티브 샘플링과 하드 네거티브의 차이를 구분하고, 각각의 학습 효과를 비교한다.
  • MultipleNegativesRankingLoss의 수식과 동작 방식을 설명하고, 배치 크기·scale 파라미터를 조정해 성능을 개선한다.
  • LLM으로 합성 데이터를 생성하고 sentence-transformers Trainer API로 임베딩 모델을 파인튜닝한다.
  • InformationRetrievalEvaluator로 파인튜닝 전후 검색 성능(Recall@k·MRR·NDCG)을 정량 평가하고 결과를 해석한다.

전체 흐름도

임베딩 모델 파인튜닝은 데이터 구성 → 대조 학습 → 모델 저장 → 평가 → RAG 적용의 순서로 진행된다.

[ 도메인 문서 수집 ]
   │  PDF, 청크 단위 분할
   ▼
[ 학습 데이터 구성 ]
   ├─ 포지티브 쌍: LLM 합성 질문 생성 (앵커, 포지티브)
   └─ 하드 네거티브: 유사도 기반 자동 선정 또는 직접 선별
   ▼
[ 대조 학습 (Contrastive Learning) ]
   │  MultipleNegativesRankingLoss + 배치 내 네거티브 샘플링
   │  SentenceTransformerTrainer (v3+ 권장) 또는 model.fit()
   ▼
[ 파인튜닝 모델 저장 ]
   ▼
[ 검색 성능 평가 ]
   │  InformationRetrievalEvaluator
   │  Recall@k · MRR · NDCG · Precision@k
   ▼
[ RAG 파이프라인 적용 ]
   │  파인튜닝 모델을 벡터 스토어 임베딩 함수로 교체
   └─ 검색 품질 재측정 후 배포

0. 사전 필수 용어

  • 임베딩(Embedding) — 텍스트를 고차원 실수 벡터로 변환한 것. 의미가 비슷한 텍스트는 벡터 공간에서 가깝게(코사인 유사도 ↑), 다른 텍스트는 멀게 위치한다.
  • 앵커(Anchor) — 대조 학습의 기준 문서(또는 검색어). 포지티브·네거티브 샘플 모두 이 앵커를 기준으로 정의된다.
  • 포지티브 샘플(Positive Sample) — 앵커와 의미적으로 관련 있는 문서 쌍. "(검색어, 정답 문서)" 형태.
  • 네거티브 샘플(Negative Sample) — 앵커와 관련 없거나 관련성이 낮은 문서 쌍. 모델이 포지티브와 구분하도록 학습시키는 반례.
  • 하드 네거티브(Hard Negative) — 주제가 유사해 모델이 헷갈리기 쉬운 네거티브 샘플. 일반 네거티브("파이썬은 언어다")보다 미묘하며("저혈당 증상" vs "당뇨병 증상") 학습 효과가 크다.
  • 배치 내 네거티브 샘플링(In-Batch Negative Sampling) — 한 배치 안의 다른 포지티브 쌍을 자동으로 네거티브로 활용하는 방식. 별도 네거티브 데이터 구성 불필요.
  • MultipleNegativesRankingLoss(MNRL) — sentence-transformers의 핵심 손실 함수. 포지티브 쌍 유사도↑, 배치 내 나머지 쌍 유사도↓ 방향으로 학습. 배치 내 네거티브 샘플링과 찰떡 궁합.
  • sentence-transformers — 문장 임베딩 특화 파이썬 라이브러리. SentenceTransformer, InputExample, losses, SentenceTransformerTrainer 등을 제공. v3+에서 Trainer API 표준화.
  • 검색 평가 지표 — Recall@k(정답 포함률), MRR(첫 정답 순위 역수 평균), NDCG(순위 가중 정확도), Precision@k(상위 k 중 정답 비율), MAP(평균 정밀도). InformationRetrievalEvaluator로 한 번에 계산.

1. 임베딩 모델의 학습 원리

1.1 대조 학습

임베딩 학습의 핵심 목표는 의미적으로 유사한 텍스트는 벡터 공간에서 가깝게, 다른 텍스트는 멀게 만드는 것이다.

비유로 이해하면 — 도서관 사서가 책을 주제별로 정리하는 것과 같다. 같은 선반(가까운 벡터)에는 관련 책이, 다른 구역(먼 벡터)에는 무관한 책이 놓인다. 대조 학습은 "이 두 책은 같은 선반에 있어야 해(포지티브)", "이 두 책은 다른 구역에 있어야 해(네거티브)"를 반복 학습하며 사서의 판단력을 키운다.

대조 학습 3단계: 1. 포지티브·네거티브 샘플 구성 — 앵커 기준으로 관련/비관련 쌍 준비. 2. 대조 학습 실행 — 포지티브 거리↓, 네거티브 거리↑. 3. 손실 함수 최적화 — 코사인 유사도 기반으로 학습 방향 결정.

1.2 데이터셋 구성

트리플렛 구성(전통 방식) — (앵커, 포지티브, 네거티브) 3개를 명시적으로 준비한다.

# 전통적인 트리플렛 구성
triplets = [
    # (앵커, 포지티브, 네거티브)
    ("강아지를 기르는 방법", "반려견 양육 가이드", "고양이 사료 추천"),
    ("파이썬 코딩 튜토리얼", "파이썬 프로그래밍 기초", "자바스크립트 입문 강의"),
    # ... 수천, 수만 개 필요
]

다중 네거티브 구성 — 앵커 하나에 여러 네거티브를 붙인다. 학습 효과는 크지만 데이터 준비 난이도가 높다.

training_data = [
    {
        "anchor": "머신러닝이란?",
        "positive": "기계학습은 데이터로부터 패턴을 찾는 AI 기술입니다.",
        "negatives": [
            "오늘 날씨가 좋네요.",
            "내일 회의는 2시에 시작합니다.",
            # ... 여러 개의 네거티브
        ]
    }
]

핵심 난점 — 네거티브 샘플은 앵커와 관련 없어야 하되, 너무 쉬우면 학습 효과가 없고 너무 어려우면 오히려 학습을 방해한다. 적정 난이도 선정이 전체 과정에서 가장 까다로운 작업이다.

1.3 배치 내 네거티브 샘플링

별도 네거티브 데이터를 준비하는 부담을 없애는 핵심 방법이다.

원리 — 한 배치 안의 다른 포지티브 쌍이 자동으로 네거티브 역할을 한다. 배치 크기가 4라면 앵커마다 3개의 자동 네거티브가 생긴다.

배치 = [
    (앵커1, 문서1),  # 포지티브 쌍 1
    (앵커2, 문서2),  # 포지티브 쌍 2
    (앵커3, 문서3),  # 포지티브 쌍 3
    (앵커4, 문서4),  # 포지티브 쌍 4
]
# 앵커1의 학습: 문서1=포지티브, 문서2/3/4=자동 네거티브
# 앵커2의 학습: 문서2=포지티브, 문서1/3/4=자동 네거티브

배치 크기 ↑ → 자동 네거티브 ↑ → 성능 ↑ (GPU 메모리가 허용하는 한).

1.4 MultipleNegativesRankingLoss

손실 함수는 모델이 얼마나 틀렸는지 계산해 학습 방향을 잡는 기준이다.

MNRL의 수식:

L = -log( exp(sim(q, p⁺)) / (exp(sim(q, p⁺)) + Σ exp(sim(q, p⁻))) )
  • q: 앵커 임베딩(검색어)
  • p⁺: 포지티브 문서 임베딩
  • p⁻: 네거티브 문서 임베딩들
  • sim(): 코사인 유사도

직관 — "포지티브 유사도를 분자에, 전체(포지티브+네거티브)를 분모에 놓고 포지티브가 1위가 되도록 밀어붙인다." 분모 대비 분자가 커질수록 손실이 0에 가까워진다.

scale 파라미터는 temperature의 역수 — 값이 클수록 학습 강도가 세진다(기본 20.0).

1.5 학습 코드의 이해 (model.fit API)

from sentence_transformers import SentenceTransformer, losses, InputExample
from torch.utils.data import DataLoader

# 1. 기본 모델 로드 (한국어 강점)
model = SentenceTransformer('BAAI/bge-m3')

# 2. 학습 데이터 준비 — InputExample(texts=[앵커, 포지티브])
train_examples = [
    InputExample(texts=["AI란 무엇인가?", "AI는 인간의 지능을 모방한 기술입니다."]),
    InputExample(texts=["딥러닝이란?", "신경망을 여러 층 쌓아 데이터로부터 학습하는 기계학습 방법입니다."]),
    InputExample(texts=["Python은 어디에 쓰이나요?", "Python은 데이터 분석, 웹 개발, AI 등에 사용됩니다."]),
    InputExample(texts=["자연어 처리란?", "컴퓨터가 인간의 언어를 이해하고 처리하는 AI 분야입니다."]),
]

# 3. DataLoader — 배치 단위 처리 (배치 클수록 자동 네거티브 ↑)
batch_size = 32
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=batch_size)

# 4. 손실 함수 설정 (scale은 temperature 역수)
train_loss = losses.MultipleNegativesRankingLoss(model, scale=20.0)

# 5. 워밍업 — 초기 학습률을 점진적으로 올려 안정적 학습
warmup_steps = int(len(train_dataloader) * 0.1)

# 6. 학습 (model.fit — v2 호환 API)
model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    epochs=3,
    warmup_steps=warmup_steps,
    optimizer_params={'lr': 2e-5},
    output_path='./korean-embedding-finetuned'
)

학습률 2e-5 — 임베딩 파인튜닝의 일반적인 시작점. 너무 크면 사전 학습 지식이 무너지고, 너무 작으면 학습이 안 된다.

2. 학습 시 성능을 높이는 방법

2.1 배치 크기 키우기

배치 내 네거티브 샘플링 방식에서 배치 크기 = 자동 네거티브 수 - 1.

배치 크기 앵커당 자동 네거티브
4 3개
32 31개
128 127개

무한정 키울 수 없는 이유 — GPU 메모리 한계. 구글 코랩 무료 T4는 현실적으로 배치 3~4 수준. 배치 크기는 nvidia-smi 로 메모리 사용량을 보면서 조절.

2.2 하드 네거티브 선정

하드 네거티브는 배치 내 자동 네거티브(이지 네거티브)와 달리, 사용자가 의도적으로 선별한 고난도 네거티브다.

구분 선택 방식 특징 학습 효과
일반 네거티브 배치 내 자동 주제가 완전히 다름 보통
하드 네거티브 사용자 직접 선별 주제 유사, 미묘한 차이 높음

예시: - 앵커: "당뇨병의 증상은 무엇인가요?" - 포지티브: "당뇨병의 주요 증상으로는 갈증 증가, 빈뇨, 체중 감소 등이 있습니다." - 하드 네거티브: "저혈당의 증상으로는 현기증, 발한, 불안감 등이 있습니다." (의료 관련이지만 당뇨병 아님) - 일반 네거티브: "파이썬은 객체지향 프로그래밍 언어입니다." (완전히 다른 주제)

코드 구현InputExample에 3번째 인수로 하드 네거티브를 추가하면 MNRL이 자동으로 처리한다.

# 하드 네거티브 포함 트리플렛 구성
train_examples_hard = [
    # InputExample(texts=[앵커, 포지티브, 하드네거티브])
    InputExample(texts=[
        "AI란 무엇인가?",
        "AI는 인간의 지능을 모방한 기술입니다.",
        "AI는 로봇과 같은 물리적 형태를 가진 기계입니다."  # 하드 네거티브
    ]),
    InputExample(texts=[
        "딥러닝이란?",
        "신경망을 여러 층 쌓아 데이터로부터 학습하는 기계학습 방법입니다.",
        "컴퓨터가 스스로 생각하는 방법입니다."  # 하드 네거티브 (모호한 설명)
    ]),
]
# MNRL은 (앵커, 포지티브)에 대해 배치 내 다른 포지티브들 + 모든 하드 네거티브를 네거티브로 사용

2.3 그 외 성능 향상 팁

  • 학습 데이터와 실전의 괴리 최소화 — 앵커(질문)는 실제 RAG 사용자가 입력할 법한 문장으로. 학습-실전 분포가 다를수록 효과가 줄어든다.
  • 데이터 증강 — 하드 네거티브를 충분히 확보하면 모델이 더 섬세한 의미 차이를 학습한다.
  • 학습률 탐색 — 2e-5를 기준으로 상하로 바꿔가며 평가 지표를 비교한다.
  • 온도(temperature) 파라미터 조정 — MNRL의 scale 값을 바꿔 손실 강도를 세밀하게 조정한다.

3. 실전 파인튜닝

이번 실습은 GPU 환경을 전제로 한다. 구글 코랩이라면 [런타임 유형 변경 → T4 GPU] 선택. 실습 파일: Ch09.Embedding Fine-tuning/ch09_EMBEDDING_FINE-TUNING.ipynb

3.1 데이터 로드

# 라이브러리 설치
# !pip install PyPDF2 datasets sentence-transformers==3.4.1

import os, requests, json
import pandas as pd
import numpy as np
from tqdm.notebook import tqdm
from openai import OpenAI
from torch.utils.data import DataLoader
from sentence_transformers import SentenceTransformer, losses, InputExample
from sentence_transformers.evaluation import InformationRetrievalEvaluator
import torch
from sklearn.metrics.pairwise import cosine_similarity
import PyPDF2

# PDF 텍스트 추출 함수
def extract_text_from_pdf(pdf_path):
    """PDF 파일에서 페이지 단위 텍스트 청크를 추출하는 함수"""
    text_chunks = []
    with open(pdf_path, 'rb') as file:
        pdf_reader = PyPDF2.PdfReader(file)
        for page_num in range(len(pdf_reader.pages)):
            text = pdf_reader.pages[page_num].extract_text()
            if text and text.strip() and len(text.strip()) > 10:
                text_chunks.append(text.strip())
    return text_chunks

# 미국 ICT 동향(학습) / 일본 ICT 동향(검증) PDF 다운로드 후 로드
train_corpus = extract_text_from_pdf('ict_usa_2024.pdf')   # 26개 문서
val_corpus   = extract_text_from_pdf('ict_japan_2024.pdf') # 27개 문서

설계 포인트 — 학습 도메인(미국 ICT)과 평가 도메인(일본 ICT)을 분리해 모델의 도메인 일반화 능력을 측정한다. 실무에서는 실제 RAG에 사용할 문서로 학습하면 효과가 더 크다.

3.2 하드 네거티브 선정 방법론

이 실습에서는 명시적 하드 네거티브 없이 배치 내 네거티브 샘플링으로 진행한다. 필요 시 임베딩 유사도 기반으로 하드 네거티브를 자동 선정하는 방식도 사용 가능하다.

# (고급) 임베딩 유사도 기반 하드 네거티브 자동 선정 예시
def mine_hard_negatives(model, corpus, top_k=5, min_threshold=0.5, max_threshold=0.9):
    """
    각 문서에 대해 유사도가 중간 범위(min~max)인 문서를 하드 네거티브로 선정.
    너무 유사하면(>max) 실제 포지티브일 가능성, 너무 다르면(<min) 이지 네거티브.
    """
    embeddings = model.encode(corpus, convert_to_tensor=True)
    hard_negatives = {}
    for i, emb in enumerate(embeddings):
        sims = cosine_similarity(emb.unsqueeze(0).cpu().numpy(),
                                  embeddings.cpu().numpy())[0]
        # 자기 자신 제외, 유사도 범위 필터
        candidates = [(j, sims[j]) for j in range(len(corpus))
                      if j != i and min_threshold < sims[j] < max_threshold]
        candidates.sort(key=lambda x: x[1], reverse=True)
        hard_negatives[i] = [corpus[j] for j, _ in candidates[:top_k]]
    return hard_negatives

3.3 합성 데이터 생성 (LLM 활용)

도메인 문서에서 사람이 직접 질문을 만들기 어려울 때, LLM으로 포지티브 샘플을 자동 합성한다.

client = OpenAI()

def generate_queries(corpus, num_questions_per_chunk=2):
    """
    각 문서(청크)로부터 LLM(GPT-4o)을 사용해 관련 질문을 생성한다.
    반환: (질문 리스트, 해당 문서 리스트) — 인덱스로 매칭된 포지티브 쌍.
    """
    all_queries, all_positive_docs = [], []
    prompt_template = """\
다음은 참고할 내용입니다.
{context_str}

위 내용을 바탕으로 낼 수 있는 질문을 {num_questions_per_chunk}개 만들어 주세요.
질문만 작성하고 실제 정답이나 보기 등은 작성하지 않습니다.
해당 질문은 본문을 볼 수 없다고 가정합니다.
따라서 '위 본문을 바탕으로~' 라는 식의 질문은 할 수 없습니다.
질문은 아래와 같은 형식으로 번호를 나열하여 생성하십시오.
1. (질문)
2. (질문)"""

    for text in tqdm(corpus):
        messages = [
            {"role": "system", "content": "You are a helpful assistant that generates questions based on provided content."},
            {"role": "user", "content": prompt_template.format(
                context_str=text,
                num_questions_per_chunk=num_questions_per_chunk
            )}
        ]
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            temperature=0.7,
        )
        result = response.choices[0].message.content.strip().split("\n")
        questions = []
        for line in result:
            if line.strip():
                parts = line.strip().split(".", 1)
                questions.append(parts[1].strip() if len(parts) > 1 else parts[0])
        questions = [q for q in questions if len(q) > 0]
        for question in questions:
            all_queries.append(question)
            all_positive_docs.append(text)
    return all_queries, all_positive_docs

# 실행 — 문서당 2개 질문 생성
train_queries, train_positive_docs = generate_queries(train_corpus)  # 26문서 → 52 질문
val_queries, val_positive_docs     = generate_queries(val_corpus)    # 27문서 → 54 질문

# InputExample 변환 (앵커=질문, 포지티브=원본 문서)
train_examples = [
    InputExample(texts=[query, doc])
    for query, doc in zip(train_queries, train_positive_docs)
]

합성 데이터 품질 팁 — 프롬프트를 "실제 RAG 사용자가 입력할 법한 질문"으로 유도하면 학습-실전 분포가 일치해 파인튜닝 효과가 극대화된다.

3.4 모델 로드

BATCH_SIZE = 4  # GPU 메모리에 맞게 조절 (클수록 성능 ↑)
loader = DataLoader(train_examples, batch_size=BATCH_SIZE, shuffle=True)

# 한국어·영어·중국어 강점의 다국어 임베딩 모델
model_id = "BAAI/bge-m3"
model = SentenceTransformer(model_id)

# 손실 함수
loss_fn = losses.MultipleNegativesRankingLoss(model)

3.5 평가 데이터 전처리

InformationRetrievalEvaluator는 3가지 딕셔너리를 요구한다.

# InformationRetrievalEvaluator 입력 형식
# queries      : {"q0": "질문 텍스트", "q1": ...}
# corpus       : {"d0": "문서 텍스트", "d1": ...}
# relevant_docs: {"q0": {"d0"}, "q1": {"d2"}, ...}  # 질문→정답 문서 ID 집합

def build_eval_dataset(queries, positive_docs):
    """val_queries, val_positive_docs를 InformationRetrievalEvaluator 형식으로 변환"""
    dataset = {"queries": {}, "corpus": {}, "relevant_docs": {}}
    # corpus 구성 (중복 문서는 동일 ID 공유)
    doc_ids = {}
    for i, doc in enumerate(positive_docs):
        if doc not in doc_ids:
            doc_id = f"d{len(doc_ids)}"
            dataset["corpus"][doc_id] = doc
            doc_ids[doc] = doc_id
    # queries & relevant_docs 구성
    for i, (query, doc) in enumerate(zip(queries, positive_docs)):
        query_id = f"q{i}"
        dataset["queries"][query_id] = query
        doc_id = doc_ids[doc]
        dataset["relevant_docs"].setdefault(query_id, set()).add(doc_id)
    return dataset

val_dataset = build_eval_dataset(val_queries, val_positive_docs)

evaluator = InformationRetrievalEvaluator(
    val_dataset["queries"],
    val_dataset["corpus"],
    val_dataset["relevant_docs"]
)

3.6 모델 학습

import os

EPOCHS = 2
os.environ["WANDB_DISABLED"] = "true"  # W&B 로깅 비활성화

warmup_steps = int(len(loader) * EPOCHS * 0.1)  # 전체 스텝의 10% 워밍업

model.fit(
    train_objectives=[(loader, loss_fn)],
    epochs=EPOCHS,
    warmup_steps=warmup_steps,
    output_path='exp_finetune',    # 파인튜닝 모델 저장 경로
    show_progress_bar=True,
)

v3+ SentenceTransformerTrainer API — sentence-transformers v3 이상에서는 SentenceTransformerTrainer를 사용하면 TrainingArguments로 학습률·에포크·평가 주기를 더 세밀하게 제어할 수 있다. model.fit()은 여전히 동작하지만, 신규 코드는 Trainer API를 권장한다.

# v3+ Trainer API (권장)
from sentence_transformers import SentenceTransformerTrainer, SentenceTransformerTrainingArguments
from sentence_transformers.training_args import BatchSamplers
from datasets import Dataset as HFDataset

# 학습 데이터를 HuggingFace Dataset 형태로 변환
train_dataset = HFDataset.from_dict({
    "anchor": train_queries,
    "positive": train_positive_docs,
})

training_args = SentenceTransformerTrainingArguments(
    output_dir="exp_finetune_v3",
    num_train_epochs=EPOCHS,
    per_device_train_batch_size=BATCH_SIZE,
    warmup_ratio=0.1,
    learning_rate=2e-5,
    batch_sampler=BatchSamplers.NO_DUPLICATES,  # MNRL 필수: 배치 내 중복 앵커 방지
)

trainer = SentenceTransformerTrainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    loss=loss_fn,
    evaluator=evaluator,
)
trainer.train()

3.7 검색 성능 평가 지표

InformationRetrievalEvaluator는 아래 6개 지표를 자동으로 계산한다.

지표 의미 직관적 질문
Accuracy@k 상위 k개 중 정답 포함 여부 (포함만 하면 성공) "정답이 상위 k개 안에 있나?"
Precision@k 상위 k개 중 정답 비율 "상위 k개가 얼마나 정확한가?"
Recall@k 전체 정답 중 상위 k에 포함된 비율 "정답을 빠뜨리지 않고 찾나?"
NDCG@k 정답 순위를 가중해 평가 (1위 > 5위) "정답이 높은 순위에 있나?"
MRR@k 정답이 처음 등장한 순위의 역수 평균 "사용자가 정답을 얼마나 빨리 찾나?"
MAP@k 정답을 찾을 때마다의 Precision 평균 "전반적인 검색 정확도·일관성은?"

간단한 예시 (질문 3개, 상위 5개 검색): - Accuracy@5 — 3개 중 2개에서 정답 포함 → 0.667 - MRR@10 — 1위, 2위, 3위에 정답 → (1/1 + 1/2 + 1/3)/3 = 0.611 - NDCG@3 — 정답이 1위, 2위, 3위 순으로 배치될수록 높은 점수

3.8 파인튜닝 모델 평가

def evaluate_st(model_id, name, evaluator):
    """SentenceTransformer 모델의 검색 성능을 평가하고 결과를 CSV로 저장"""
    os.makedirs('results', exist_ok=True)
    model = SentenceTransformer(model_id)
    result = evaluator(model)
    result_df = pd.DataFrame([result]) if isinstance(result, dict) else result
    result_df.to_csv(f'results/IR_evaluation_{name}.csv', index=False)
    return result

# 원본 모델 vs 파인튜닝 모델 비교
evaluate_st("BAAI/bge-m3",  "original",  evaluator)
evaluate_st("exp_finetune", "finetuned", evaluator)

실험 결과 — 파인튜닝 전 모델이 이미 1.0(만점)인 지표를 제외한 모든 평가 지표에서 파인튜닝 후 모델이 더 높은 성능을 보였다. 예시:

모델 Recall@1 NDCG@10 MRR@10
BAAI/bge-m3 (원본) 0.314 0.344 0.314
Fine-tuned 0.870 0.981 0.870

실습 데이터가 소량(52개 학습, 54개 평가)임에도 성능이 크게 향상된 것은 도메인(ICT)에 맞춘 합성 데이터의 효과다.

핵심 개념 정리

개념 한 줄 요약
대조 학습 포지티브 거리↓ / 네거티브 거리↑ 로 임베딩 공간을 도메인에 맞게 재편
앵커·포지티브·네거티브 학습 데이터의 3요소. 앵커=기준, 포지티브=정답, 네거티브=반례
배치 내 네거티브 샘플링 배치 안 다른 쌍을 자동 네거티브로 활용. 별도 데이터 준비 불필요
하드 네거티브 주제 유사·미묘한 차이의 고난도 네거티브. 학습 효과 극대화
MNRL 포지티브 유사도↑ / 배치 내 나머지↓ 로 손실 최소화
InformationRetrievalEvaluator queries/corpus/relevant_docs 3종 딕셔너리로 검색 성능 다각 평가

한 문장 요약 — 임베딩 모델 파인튜닝은 도메인 문서로부터 합성 데이터(LLM)를 만들고, MNRL + 배치 내 네거티브 샘플링으로 학습해, InformationRetrievalEvaluator로 검색 성능을 정량 검증하는 전 과정이다.

실무 체크리스트

  • [ ] 학습 앵커(질문)를 실제 RAG 사용자의 질문 패턴과 일치시킨다 (학습-실전 분포 일치).
  • [ ] 배치 크기는 GPU 메모리를 최대한 활용해 설정한다 (배치↑ → 자동 네거티브↑ → 성능↑).
  • [ ] 하드 네거티브를 포함한 트리플렛 데이터를 추가하면 성능이 더 향상된다.
  • [ ] 학습 데이터가 부족하면 LLM(GPT-4o 등)으로 합성 포지티브 쌍을 생성한다.
  • [ ] InformationRetrievalEvaluator로 파인튜닝 전후 Recall@k·MRR·NDCG를 비교해 개선 여부를 수치로 확인한다.
  • [ ] v3+ 신규 코드는 SentenceTransformerTrainer API를 사용하고, BatchSamplers.NO_DUPLICATES로 배치 내 중복 방지.

연습문제

  1. 개념. 배치 내 네거티브 샘플링이 전통적인 명시적 네거티브 준비 방식보다 유리한 이유를 2가지 들어라.
  2. 비교. 하드 네거티브와 일반 네거티브의 차이를 "당뇨병 vs 저혈당" 예시를 응용해 의료 외 다른 도메인(법률, 금융 등)에서 하나씩 만들어보라.
  3. 코드. InputExample(texts=[앵커, 포지티브, 하드네거티브])를 사용할 때 MNRL이 어떤 조합을 포지티브·네거티브로 처리하는지 설명하라.
  4. 적용. 사내 기술 문서(500페이지 PDF) 기반 RAG 시스템을 위해 임베딩 파인튜닝 데이터를 구성하는 절차를 설계하라 (합성 데이터 생성 포함).
  5. 설계. Recall@1이 0.3에서 0.87로 향상됐음에도 실제 RAG 응답 품질이 크게 개선되지 않았다면 가능한 원인 2가지와 진단 방법을 논하라.

최신 동향 (2026-05 기준)

최신 동향 (검증 2026-05-21) — 책은 sentence-transformers 3.4.1 기준이므로 아래 변화만 보완.

  • SentenceTransformerTrainer 표준화. sentence-transformers v3+에서 SentenceTransformerTrainer가 표준 학습 인터페이스로 정립됐다. model.fit()은 하위호환으로 유지되지만, 신규 코드는 HuggingFace TrainingArguments 기반 Trainer API 사용이 권장된다. (출처: https://sbert.net/docs/sentence_transformer/training_overview.html)
  • MNRL + 하드 네거티브 마이닝 조합이 실무 표준. 검색 임베딩 파인튜닝에서 MultipleNegativesRankingLoss에 자동 하드 네거티브 마이닝(mine_hard_negatives 유틸리티)을 결합하는 패턴이 강력한 성능을 보여 산업 표준으로 자리 잡고 있다. (출처: https://sbert.net/docs/sentence_transformer/dataset_overview.html)
  • 모델·가격 수시 변동. 책의 gpt-4o, BAAI/bge-m3 표기는 2025년 기준 예시다. 실제 사용 시 공식 모델 목록을 확인: OpenAI(https://platform.openai.com/docs/models)·HuggingFace MTEB 리더보드.

부록 A. 용어 사전

한글 영문 의미
임베딩 Embedding 텍스트를 고차원 실수 벡터로 변환한 표현. 의미가 비슷한 텍스트는 벡터 공간에서 가깝게 위치한다.
앵커 Anchor 대조 학습의 기준 샘플. 포지티브·네거티브 모두 이 앵커를 기준으로 정의된다.
포지티브 샘플 Positive Sample 앵커와 의미적으로 관련 있는 문서 쌍. "(검색어, 정답 문서)" 형태.
네거티브 샘플 Negative Sample 앵커와 관련 없거나 관련성이 낮은 반례 샘플. 모델이 포지티브와 구분하도록 학습된다.
하드 네거티브 Hard Negative 주제가 유사해 모델이 구분하기 어려운 고난도 네거티브 샘플. 학습 효과가 일반 네거티브보다 높다.
배치 내 네거티브 샘플링 In-Batch Negative Sampling 한 배치 안의 다른 포지티브 쌍을 자동으로 네거티브로 활용하는 방식.
다중 네거티브 순위 손실 MultipleNegativesRankingLoss (MNRL) 포지티브 유사도를 올리고 배치 내 나머지 쌍 유사도를 낮추는 방향으로 학습하는 손실 함수.
대조 학습 Contrastive Learning 포지티브 쌍은 가깝게, 네거티브 쌍은 멀게 밀어 임베딩 공간을 재편하는 학습 방식.
정보 검색 평가기 InformationRetrievalEvaluator queries·corpus·relevant_docs 3종 딕셔너리로 검색 성능을 다각 평가하는 sentence-transformers 유틸리티.
합성 데이터 Synthetic Data LLM을 사용해 도메인 문서로부터 자동 생성한 학습용 질문-문서 쌍.
온도 역수(스케일) Scale (temperature inverse) MNRL의 손실 강도를 조절하는 파라미터. 값이 클수록 학습 강도가 세진다(기본값 20.0).
워밍업 스텝 Warmup Steps 학습 초기 일정 스텝 동안 학습률을 점진적으로 올려 안정적 학습을 유도하는 기법.

부록 B. 핵심 비교표

사전학습 임베딩 vs 파인튜닝 임베딩

구분 사전학습 임베딩 파인튜닝 임베딩
학습 데이터 범용 대규모 텍스트 도메인 특화 문서 + 합성 질문
도메인 적합성 범용적, 전문 도메인에서 정확도 저하 가능 대상 도메인에 최적화
준비 비용 없음 (그대로 사용) 데이터 구성·학습 시간 필요
검색 정확도 (Recall@1 예) 0.314 (실습 기준) 0.870 (실습 기준)
적합한 상황 범용 문서, 빠른 프로토타이핑 전문 도메인, 높은 검색 품질 요구

손실 함수 비교 (MNRL vs 트리플렛 손실)

구분 MultipleNegativesRankingLoss (MNRL) TripletLoss
데이터 형식 (앵커, 포지티브) 쌍 또는 트리플렛 (앵커, 포지티브, 네거티브) 트리플렛 필수
네거티브 생성 배치 내 자동 생성 + 하드 네거티브 선택 명시적 네거티브 필수
데이터 준비 난이도 낮음 높음
학습 효율 배치 크기에 비례해 효율 증가 고정적
권장 상황 네거티브 구성이 어려운 경우, 대규모 학습 고품질 트리플렛 확보 가능한 경우

부록 C. 추천 참고 자료

검증된 외부 자료 (Tier 1 공식, 생존 확인 2026-05-21)

자료 링크
sentence-transformers 공식 문서 sbert.net
학습 개요 (v3+ Trainer API) training_overview
손실 함수 레퍼런스 (MNRL 등) losses reference
하드 네거티브 마이닝·데이터셋 개요 dataset_overview
HuggingFace 모델 허브 huggingface.co/models
실습 코드 (깃허브) langchain-tutorial Ch09
자료 설명
책 8장 LLM 자체를 파인튜닝하는 RAFT+LoRA 방식. 임베딩 파인튜닝과 상보적으로 활용한다.
책 1~3장 RAG 파이프라인 전체 구조. 파인튜닝 임베딩 모델을 어디에 끼워 넣는지 맥락 파악에 필요.

부록 D. 연습문제 풀이

  1. (배치 크기에 비례해 자동 네거티브가 늘어나고, 별도 네거티브 데이터 구성이 불필요하다) 첫째, 배치 내 네거티브 샘플링은 한 배치 안의 다른 포지티브 쌍이 자동으로 네거티브 역할을 하므로 별도의 네거티브 데이터를 준비하지 않아도 된다. 둘째, 배치 크기를 키울수록 앵커당 자동 네거티브 수가 선형으로 증가해(배치 32 → 31개 자동 네거티브) 추가 레이블링 없이도 학습 신호가 풍부해진다. 반면 명시적 네거티브 방식은 데이터 레이블링 비용이 높고, 배치 크기를 늘려도 네거티브 수가 자동으로 늘지 않는다.

  2. (법률·금융 도메인에서의 하드 네거티브 예시) 하드 네거티브는 주제가 유사해 모델이 구분하기 어려운 반례다. 일반 네거티브는 주제가 완전히 달라 모델이 쉽게 구분한다.

  3. 법률 도메인 — 앵커: "손해배상 청구 소송의 소멸시효는?" / 포지티브: "불법행위로 인한 손해배상 청구권은 손해와 가해자를 안 날로부터 3년입니다." / 하드 네거티브: "형사 고소의 공소시효는 범죄 종류에 따라 다릅니다." (법적 시효를 다루지만 민사 청구권이 아님) / 일반 네거티브: "오늘 주식 시장이 상승했습니다."

  4. 금융 도메인 — 앵커: "ETF와 인덱스 펀드의 차이는?" / 포지티브: "ETF는 장중 실시간 매매가 가능하고 인덱스 펀드는 종가 기준으로만 거래됩니다." / 하드 네거티브: "뮤추얼 펀드는 전문 운용사가 적극적으로 종목을 선정해 운용합니다." (투자 상품 비교이지만 패시브 인덱스 추종이 아님) / 일반 네거티브: "오늘 날씨가 맑습니다."

  5. (MNRL은 명시적 하드 네거티브 + 배치 내 다른 포지티브 문서를 모두 네거티브로 처리한다) InputExample(texts=[앵커, 포지티브, 하드네거티브])를 사용하면 MNRL은 (앵커, 포지티브) 쌍의 유사도를 올리면서, 하드 네거티브와 같은 배치 안의 다른 포지티브 문서들(자동 네거티브) 모두에 대해 유사도를 낮추는 방향으로 학습한다. 즉, 배치 내 네거티브 샘플링으로 생성된 자동 네거티브에 명시적 하드 네거티브가 추가되어 전체 네거티브 풀이 더 풍부해진다. 코드상으로는 texts 리스트의 세 번째 원소가 하드 네거티브로 인식된다.

python # MNRL의 처리 방식 요약 # - (앵커, 포지티브): 유사도 ↑ # - (앵커, 하드네거티브): 유사도 ↓ (명시적) # - (앵커, 배치 내 다른 포지티브 문서들): 유사도 ↓ (자동) InputExample(texts=["딥러닝이란?", "신경망을 여러 층 쌓아 데이터로부터 학습하는 기계학습 방법입니다.", "컴퓨터가 스스로 생각하는 방법입니다."]) # 하드 네거티브

  1. (PDF 청크 추출 → LLM 합성 질문 생성 → InputExample 변환 → 학습 → 평가의 순서로 설계한다) 500페이지 사내 기술 문서를 기반으로 한 파인튜닝 절차는 다음과 같다.

  2. 청크 분할: PyPDF2(또는 LangChain PyPDFLoader)로 페이지 단위 또는 의미 단위로 청크를 추출한다. 너무 짧은 청크(10자 미만)는 필터링한다.

  3. 합성 질문 생성: generate_queries() 패턴으로 각 청크마다 GPT-4o 등 LLM에 "실제 사용자가 입력할 법한 질문 2~3개"를 생성시킨다. 프롬프트에 "본문을 볼 수 없다고 가정"을 명시해 독립적인 질문이 되도록 유도한다.
  4. 하드 네거티브 선정(선택): mine_hard_negatives()로 임베딩 유사도가 중간 범위(0.5~0.9)인 문서를 자동 선정해 트리플렛에 추가한다.
  5. InputExample 변환: (질문, 원본 청크) 포지티브 쌍 또는 (질문, 원본 청크, 하드 네거티브) 트리플렛으로 변환한다.
  6. 학습: SentenceTransformerTrainer API(v3+ 권장)로 BAAI/bge-m3 등 베이스 모델을 파인튜닝한다. 배치 크기는 GPU 메모리 최대치로 설정한다.
  7. 평가: 문서 일부를 검증 셋으로 분리해 InformationRetrievalEvaluator로 파인튜닝 전후 Recall@k·MRR·NDCG를 비교한다.

  8. (검색 성능과 생성 품질은 별개 게이트이며, 청크 크기나 LLM 생성 품질이 병목일 수 있다) Recall@1이 크게 향상됐음에도 RAG 응답 품질이 낮은 경우 가능한 원인과 진단 방법은 다음과 같다.

  9. 원인 1 — 검색된 문서의 내용 자체가 RAG 생성에 충분하지 않음: 청크가 너무 짧거나 문맥이 잘려 있어 LLM이 정답을 생성하기에 부족한 경우다. 진단: 상위 검색 결과 청크를 직접 열어 "이 청크만으로 질문에 답할 수 있는가"를 확인한다. 청크 크기를 늘리거나 sliding window 방식을 도입해 문맥을 보존한다.

  10. 원인 2 — 평가 지표와 실제 사용 시나리오의 분포 불일치: 파인튜닝 평가는 합성 질문(LLM 생성) 기준이었으나 실제 사용자 질문은 패턴이 다를 수 있다. 진단: 실제 사용자 질문 로그를 수집해 평가 셋에 포함시키고, 합성 질문 프롬프트를 실제 질문 패턴에 맞게 조정한다.
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플