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+에서TrainerAPI 표준화. - 검색 평가 지표 — 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+ 신규 코드는
SentenceTransformerTrainerAPI를 사용하고,BatchSamplers.NO_DUPLICATES로 배치 내 중복 방지.
연습문제
- 개념. 배치 내 네거티브 샘플링이 전통적인 명시적 네거티브 준비 방식보다 유리한 이유를 2가지 들어라.
- 비교. 하드 네거티브와 일반 네거티브의 차이를 "당뇨병 vs 저혈당" 예시를 응용해 의료 외 다른 도메인(법률, 금융 등)에서 하나씩 만들어보라.
- 코드.
InputExample(texts=[앵커, 포지티브, 하드네거티브])를 사용할 때 MNRL이 어떤 조합을 포지티브·네거티브로 처리하는지 설명하라. - 적용. 사내 기술 문서(500페이지 PDF) 기반 RAG 시스템을 위해 임베딩 파인튜닝 데이터를 구성하는 절차를 설계하라 (합성 데이터 생성 포함).
- 설계. 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()은 하위호환으로 유지되지만, 신규 코드는 HuggingFaceTrainingArguments기반 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. 연습문제 풀이
-
(배치 크기에 비례해 자동 네거티브가 늘어나고, 별도 네거티브 데이터 구성이 불필요하다) 첫째, 배치 내 네거티브 샘플링은 한 배치 안의 다른 포지티브 쌍이 자동으로 네거티브 역할을 하므로 별도의 네거티브 데이터를 준비하지 않아도 된다. 둘째, 배치 크기를 키울수록 앵커당 자동 네거티브 수가 선형으로 증가해(배치 32 → 31개 자동 네거티브) 추가 레이블링 없이도 학습 신호가 풍부해진다. 반면 명시적 네거티브 방식은 데이터 레이블링 비용이 높고, 배치 크기를 늘려도 네거티브 수가 자동으로 늘지 않는다.
-
(법률·금융 도메인에서의 하드 네거티브 예시) 하드 네거티브는 주제가 유사해 모델이 구분하기 어려운 반례다. 일반 네거티브는 주제가 완전히 달라 모델이 쉽게 구분한다.
-
법률 도메인 — 앵커: "손해배상 청구 소송의 소멸시효는?" / 포지티브: "불법행위로 인한 손해배상 청구권은 손해와 가해자를 안 날로부터 3년입니다." / 하드 네거티브: "형사 고소의 공소시효는 범죄 종류에 따라 다릅니다." (법적 시효를 다루지만 민사 청구권이 아님) / 일반 네거티브: "오늘 주식 시장이 상승했습니다."
-
금융 도메인 — 앵커: "ETF와 인덱스 펀드의 차이는?" / 포지티브: "ETF는 장중 실시간 매매가 가능하고 인덱스 펀드는 종가 기준으로만 거래됩니다." / 하드 네거티브: "뮤추얼 펀드는 전문 운용사가 적극적으로 종목을 선정해 운용합니다." (투자 상품 비교이지만 패시브 인덱스 추종이 아님) / 일반 네거티브: "오늘 날씨가 맑습니다."
-
(MNRL은 명시적 하드 네거티브 + 배치 내 다른 포지티브 문서를 모두 네거티브로 처리한다)
InputExample(texts=[앵커, 포지티브, 하드네거티브])를 사용하면 MNRL은 (앵커, 포지티브) 쌍의 유사도를 올리면서, 하드 네거티브와 같은 배치 안의 다른 포지티브 문서들(자동 네거티브) 모두에 대해 유사도를 낮추는 방향으로 학습한다. 즉, 배치 내 네거티브 샘플링으로 생성된 자동 네거티브에 명시적 하드 네거티브가 추가되어 전체 네거티브 풀이 더 풍부해진다. 코드상으로는texts리스트의 세 번째 원소가 하드 네거티브로 인식된다.
python
# MNRL의 처리 방식 요약
# - (앵커, 포지티브): 유사도 ↑
# - (앵커, 하드네거티브): 유사도 ↓ (명시적)
# - (앵커, 배치 내 다른 포지티브 문서들): 유사도 ↓ (자동)
InputExample(texts=["딥러닝이란?",
"신경망을 여러 층 쌓아 데이터로부터 학습하는 기계학습 방법입니다.",
"컴퓨터가 스스로 생각하는 방법입니다."]) # 하드 네거티브
-
(PDF 청크 추출 → LLM 합성 질문 생성 → InputExample 변환 → 학습 → 평가의 순서로 설계한다) 500페이지 사내 기술 문서를 기반으로 한 파인튜닝 절차는 다음과 같다.
-
청크 분할:
PyPDF2(또는 LangChainPyPDFLoader)로 페이지 단위 또는 의미 단위로 청크를 추출한다. 너무 짧은 청크(10자 미만)는 필터링한다. - 합성 질문 생성:
generate_queries()패턴으로 각 청크마다 GPT-4o 등 LLM에 "실제 사용자가 입력할 법한 질문 2~3개"를 생성시킨다. 프롬프트에 "본문을 볼 수 없다고 가정"을 명시해 독립적인 질문이 되도록 유도한다. - 하드 네거티브 선정(선택):
mine_hard_negatives()로 임베딩 유사도가 중간 범위(0.5~0.9)인 문서를 자동 선정해 트리플렛에 추가한다. InputExample변환:(질문, 원본 청크)포지티브 쌍 또는(질문, 원본 청크, 하드 네거티브)트리플렛으로 변환한다.- 학습:
SentenceTransformerTrainerAPI(v3+ 권장)로BAAI/bge-m3등 베이스 모델을 파인튜닝한다. 배치 크기는 GPU 메모리 최대치로 설정한다. -
평가: 문서 일부를 검증 셋으로 분리해
InformationRetrievalEvaluator로 파인튜닝 전후 Recall@k·MRR·NDCG를 비교한다. -
(검색 성능과 생성 품질은 별개 게이트이며, 청크 크기나 LLM 생성 품질이 병목일 수 있다) Recall@1이 크게 향상됐음에도 RAG 응답 품질이 낮은 경우 가능한 원인과 진단 방법은 다음과 같다.
-
원인 1 — 검색된 문서의 내용 자체가 RAG 생성에 충분하지 않음: 청크가 너무 짧거나 문맥이 잘려 있어 LLM이 정답을 생성하기에 부족한 경우다. 진단: 상위 검색 결과 청크를 직접 열어 "이 청크만으로 질문에 답할 수 있는가"를 확인한다. 청크 크기를 늘리거나 sliding window 방식을 도입해 문맥을 보존한다.
- 원인 2 — 평가 지표와 실제 사용 시나리오의 분포 불일치: 파인튜닝 평가는 합성 질문(LLM 생성) 기준이었으나 실제 사용자 질문은 패턴이 다를 수 있다. 진단: 실제 사용자 질문 로그를 수집해 평가 셋에 포함시키고, 합성 질문 프롬프트를 실제 질문 패턴에 맞게 조정한다.
클릭하거나 Space를 눌러 뒤집기