Field Log · Entry
실전: 재현 가능한 Embedding 연구 Pipeline 만들기 (14/14)
이번 글의 결과물
corpus·query·qrel·split을 immutable snapshot으로 만들고 contamination을 검사합니다.- 공개 model zero-shot, BM25, hybrid를 고정 baseline으로 둔 뒤 pair/triplet contrastive fine-tuning을 실행합니다.
- Hard negative는 mine한 뒤 teacher·human audit으로 false negative를 제거하고, round마다 새 version으로 저장합니다.
- Full-precision exact 평가에서 model을 비교한 뒤 dimension·quantization·ANN 손실을 순서대로 더합니다.
- Paired 통계, 한국어·길이·부정·multi-hop slice, downstream RAG utility, latency·security gate를 모두 통과한 index만 canary로 배포합니다.
앞 글까지 embedding 연구의 기하, 학습, 평가, 최신 흐름을 분해했습니다. 마지막 편에서는 이를 하나의 재현 가능한 연구 pipeline으로 다시 조립합니다.
frozen data
→ zero-shot baseline
→ train data audit
→ contrastive training
→ exact evaluation
→ dimension / quantization
→ ANN + filter load test
→ fixed RAG reader
→ shadow index / canary / rollback
이 글의 code는 구조를 보여 주는 최소 예제입니다. 실제 실행에서는 명시한 package version, model license, hardware에 맞춰 lock file과 test를 유지합니다.
1. 성공 조건을 한 문장으로 쓴다
나쁜 목표:
"우리 domain에서 좋은 embedding을 만든다."
검증 가능한 목표:
한국어 정책 질의에서 candidate Recall@50을 baseline보다 2%p 이상 높이고,
negation·최신 규정 slice regression은 1%p 이내로 제한하며,
ANN recall@50 ≥ 0.96, query p95 ≤ 35ms,
index RAM ≤ 80GB, fixed reader answer correctness를 낮추지 않는다.
Quality, regression, latency, memory, downstream 조건이 함께 있어야 “가장 높은 score”가 아니라 “배포 가능한 개선”을 찾습니다.
2. 실험 Directory
embedding-lab/
├── configs/
│ ├── data-v5.yaml
│ ├── train-e5-domain-v3.yaml
│ └── eval-v8.yaml
├── data/
│ ├── manifests/
│ └── samples/ # 민감하지 않은 작은 fixture만
├── src/
│ ├── build_snapshot.py
│ ├── audit_pairs.py
│ ├── mine_negatives.py
│ ├── train.py
│ ├── encode.py
│ ├── evaluate_exact.py
│ ├── evaluate_ann.py
│ └── evaluate_rag.py
├── tests/
│ ├── test_ids.py
│ ├── test_metrics.py
│ └── test_model_contract.py
├── reports/
│ └── run-2026-07-20-001/
├── requirements.lock
└── README.md
대용량 corpus·vector는 Git에 넣지 않습니다. Manifest와 checksum, 접근 가능한 object-store URI를 versioning합니다.
3. Data Contract
Corpus
{
"doc_id": "policy:refund:2026-03#section-4#chunk-02",
"parent_id": "policy:refund:2026-03#section-4",
"title": "환불 정책",
"heading": "처리 기한",
"text": "...",
"language": "ko",
"valid_from": "2026-03-01",
"valid_to": null,
"acl_scope": "employee",
"source_revision": "sha256:...",
"chunker_revision": "heading-v3"
}
Query
{
"query_id": "q-008421",
"text": "승인된 환불은 언제 입금돼?",
"language": "ko",
"timestamp": "2026-06-14T08:21:00Z",
"source": "anonymized-human",
"group_id": "session-31f...",
"slices": ["policy", "temporal", "colloquial"]
}
Qrel
{
"query_id": "q-008421",
"doc_id": "policy:refund:2026-03#section-4#chunk-02",
"relevance": 3,
"evidence_set_id": "ev-008421-a",
"annotator_count": 2,
"adjudicated": true
}
ID는 row number가 아니라 source와 version에 연결합니다. Chunk text가 달라지면 같은 ID를 재사용하지 않습니다.
4. Split은 Query Random Split보다 먼저 설계한다
같은 source document에서 생성한 유사 query가 train과 test에 나뉘면 성능이 부풀 수 있습니다.
group key candidates:
source_document
source_thread / user session
answer entity
template family
temporal window
권장 split:
split_policy:
train: "documents valid before 2026-01-01"
dev: "2026-01-01 through 2026-03-31"
test: "2026-04-01 through 2026-06-30"
group_by: [source_family, normalized_answer]
hidden_test: true
과거 문서에만 답이 있는 질의와 최신 문서가 정답인 질의를 별도 slice로 둡니다.
5. Contamination Audit
Exact
normalize Unicode / whitespace / boilerplate
→ hash query and document spans
→ train↔dev/test duplicate check
Near Duplicate
- character/token MinHash
- BM25 top neighbor
- 독립 embedding model의 cosine neighbor
- title·entity·answer overlap
Provenance
- 같은 source dump·revision 여부
- translated/paraphrased variant
- synthetic prompt template family
- public benchmark 포함 여부
- teacher model이 평가 data를 학습했을 가능성 disclosure
제거 기준과 남긴 예외를 report에 기록합니다. Audit model이 test를 학습 model에 직접 노출하는 방식도 피합니다.
6. Label Quality를 측정한다
Qrel은 정답 파일이 아니라 관찰입니다.
sample:
100 easy + 100 hard + 100 disagreement + 100 no-answer
measure:
annotator agreement
adjudication change rate
positive completeness
stale-label rate
특히 retrieval에서는 unlabeled relevant document가 negative로 취급되기 쉽습니다. Pooling 방식으로 BM25, 여러 dense model, reranker의 상위 후보를 합쳐 annotation pool을 넓힙니다.
7. Baseline을 네 개 고정한다
B0 BM25
B1 public multilingual embedding + fp32 exact
B2 BM25 + dense reciprocal rank fusion
B3 dense top-N + fixed cross-encoder reranker
Fine-tuned model이 B1만 이기고 B2/B3를 이기지 못할 수 있습니다. 그 경우 model 자체는 개선됐지만 system 투자 우선순위는 다를 수 있습니다.
Baseline contract:
model_contract:
id: org/model
revision: exact-commit
tokenizer_revision: exact-commit
query_template: "query: {text}"
document_template: "passage: {title}\n{heading}\n{text}"
max_query_tokens: 256
max_document_tokens: 1024
truncation_side: right
pooling: official
normalize: true
dimension: 768
similarity: inner_product
이 contract는 학습, corpus encoding, online query encoding에서 하나의 source를 사용합니다.
8. Pair의 의미를 먼저 정의한다
Contrastive loss보다 중요한 것은 row의 의미입니다.
anchor: 실제 query
positive: query를 충분히 해결하는 최신·권한 내 chunk
negative: topic은 비슷하지만 해당 조건을 충족하지 않는 chunk
Negative taxonomy:
| 종류 | 예시 | 학습 목적 |
|---|---|---|
| Random | 전혀 다른 domain | 초기 separation |
| Lexical hard | 단어는 같지만 답이 다름 | keyword shortcut 억제 |
| Dense hard | 의미가 가까운 오답 | local boundary |
| Temporal | 폐기된 과거 규정 | freshness 구분 |
| Negation | 허용/금지 반대 | polarity |
| Entity | 비슷한 상품·법령 번호 | identity precision |
| Near-positive | 일부만 답함 | graded relevance |
Near-positive를 무조건 binary negative로 넣지 않습니다. Multi-positive나 soft/graded supervision을 검토합니다.
9. 학습 Recipe를 단계화한다
Stage 0 zero-shot checkpoint
Stage 1 broad clean pairs, in-batch negatives
Stage 2 mined + relabeled domain negatives
Stage 3 optional teacher margin/listwise distillation
Stage 4 optional MRL multi-dimension objective
처음부터 모든 technique를 섞지 않습니다. Stage마다 exact dev metric과 critical regression을 저장해 gain 출처를 확인합니다.
10. 최소 Contrastive Training 예제
아래 예시는 2026년 7월의 Sentence Transformers 5.x Trainer 계열 API를 기준으로 한 구조입니다. 재현 실행에서는 sentence-transformers==5.5.0처럼 exact version과 transitive dependency를 lock합니다.
from datasets import Dataset
from sentence_transformers import (
SentenceTransformer,
SentenceTransformerTrainer,
SentenceTransformerTrainingArguments,
losses,
)
from sentence_transformers.training_args import BatchSamplers
model = SentenceTransformer(
"intfloat/multilingual-e5-base",
revision="PIN_A_REAL_MODEL_COMMIT",
)
# Trainer가 text column을 model input으로 해석하므로
# ID·metadata는 별도 table에 두고 input column 순서를 test한다.
train_dataset = Dataset.from_dict(
{
"anchor": [
"query: 승인된 환불은 언제 입금돼?",
"query: 해외 배송비는 환불되나요?",
],
"positive": [
"passage: 환불 승인 후 영업일 기준 3~5일 이내 지급됩니다.",
"passage: 상품 하자 시 해외 배송비를 포함해 환불합니다.",
],
"negative": [
"passage: 환불 신청은 구매 후 14일 이내 가능합니다.",
"passage: 단순 변심의 국제 반송비는 구매자가 부담합니다.",
],
}
)
loss = losses.MultipleNegativesRankingLoss(model)
args = SentenceTransformerTrainingArguments(
output_dir="runs/e5-domain-v3",
num_train_epochs=1,
per_device_train_batch_size=64,
gradient_accumulation_steps=4,
learning_rate=2e-5,
warmup_ratio=0.1,
bf16=True,
batch_sampler=BatchSamplers.NO_DUPLICATES,
save_strategy="steps",
save_steps=500,
logging_steps=20,
seed=17,
)
trainer = SentenceTransformerTrainer(
model=model,
args=args,
train_dataset=train_dataset,
loss=loss,
)
trainer.train()
실제 학습 전 smoke test:
- 32개 row overfit 가능 여부
- positive score가 negative보다 증가하는지
- input prefix가 중복되지 않는지
- long input truncation rate
- batch 안 duplicate positive 비율
- gradient NaN과 effective batch size
NO_DUPLICATES sampler는 in-batch negative와 같은 text가 겹치는 문제를 줄이지만 semantic false negative를 자동 제거하지는 않습니다.
11. Loss 선택
| Data | 첫 loss | 주의 |
|---|---|---|
(q, positive) pairs | Multiple Negatives Ranking / InfoNCE | in-batch false negative |
(q, pos, neg) | InfoNCE with explicit negatives | hardness·label audit |
| Cross-encoder scores | MarginMSE / KL distillation | teacher calibration |
| Graded relevance | listwise/pairwise rank objective | group construction |
| Multi-dimension | Matryoshka wrapper/objective | dimension별 weight |
| Parallel language pairs | contrastive + distillation | semantic drift |
Loss 이름으로 결과를 예측하지 않습니다. 대조 학습 편의 temperature·batch·normalization을 함께 기록합니다.
12. Effective Batch와 GradCache
InfoNCE는 비교하는 negative 수가 중요합니다. Gradient accumulation은 optimizer batch를 늘리지만, 각 forward에서 보는 in-batch negative 수를 자동으로 늘리지 않습니다.
micro batch 64 × accumulation 4
optimizer effective batch = 256
contrastive candidate pool = usually 64 per forward
Memory가 제한되면 cached multiple-negatives/GradCache 계열 loss를 검토합니다. Report에는 optimizer effective batch와 contrastive negative pool을 따로 씁니다.
Multi-GPU에서 cross-device negative를 gather하는지, positive pair가 다른 rank에서 false negative가 되지 않는지도 확인합니다.
13. Hard-negative Mining Round
checkpoint r0
→ encode train queries and allowed corpus
→ retrieve top-200
→ remove known positives / same evidence set
→ teacher or human relabel top candidates
→ construct easy+hard mixture
→ train checkpoint r1
Mining record:
{
"query_id": "q-008421",
"doc_id": "policy:refund:2024#section-4",
"miner_model": "run-r0",
"miner_rank": 3,
"miner_score": 0.873,
"teacher_model": "pinned-reranker",
"teacher_score": 0.42,
"label": "temporal_negative",
"audit": "human-confirmed"
}
Miner checkpoint와 corpus snapshot이 없으면 negative dataset을 재현할 수 없습니다.
14. False-negative Guard
다음 조건이면 자동 negative에서 제외하거나 review queue로 보냅니다.
- 같은
evidence_set_id - 같은 parent의 겹치는 chunk
- known answer string 포함
- 여러 teacher가 relevant로 판단
- top lexical과 dense가 동시에 매우 높음
- qrel에 없지만 최신 문서가 query를 직접 답함
- annotation disagreement가 큼
Teacher threshold는 dev metric이 아니라 human-audited precision/recall sample로 정합니다.
negative retention rate
false-negative rate by miner rank
human overturn rate
downstream gain per retained example
이 네 값을 함께 보고합니다.
15. Exact Encoding Artifact
Corpus vector에는 순서가 아니라 ID mapping을 저장합니다.
vectors.f16.npy
doc_ids.parquet
embedding_manifest.yaml
assert vectors.shape[0] == len(doc_ids)
assert unique(doc_ids) == len(doc_ids)
Sample determinism test:
import numpy as np
def assert_normalized(vectors, atol=1e-3):
norms = np.linalg.norm(vectors.astype("float32"), axis=1)
np.testing.assert_allclose(norms, 1.0, atol=atol)
def assert_same_sample(old, new, atol=1e-5):
np.testing.assert_allclose(old, new, atol=atol, rtol=0)
GPU kernel·dtype 차이로 bitwise identity가 어려울 수 있으므로 허용 오차와 ranking stability를 함께 사용합니다.
16. Exact Retrieval부터 계산한다
import numpy as np
def exact_topk(query_vectors, document_vectors, k=100, block=50_000):
"""L2-normalized vector의 inner product top-k; production용 최적화 코드는 아님."""
all_scores = np.full((len(query_vectors), k), -np.inf, dtype=np.float32)
all_ids = np.full((len(query_vectors), k), -1, dtype=np.int64)
for start in range(0, len(document_vectors), block):
docs = document_vectors[start : start + block].astype(np.float32)
scores = query_vectors.astype(np.float32) @ docs.T
local_k = min(k, scores.shape[1])
local = np.argpartition(scores, -local_k, axis=1)[:, -local_k:]
local_scores = np.take_along_axis(scores, local, axis=1)
local_ids = local + start
merged_scores = np.concatenate([all_scores, local_scores], axis=1)
merged_ids = np.concatenate([all_ids, local_ids], axis=1)
keep = np.argpartition(merged_scores, -k, axis=1)[:, -k:]
all_scores = np.take_along_axis(merged_scores, keep, axis=1)
all_ids = np.take_along_axis(merged_ids, keep, axis=1)
order = np.argsort(-all_scores, axis=1)
return (
np.take_along_axis(all_ids, order, axis=1),
np.take_along_axis(all_scores, order, axis=1),
)
이 구현은 작은 연구 corpus에서 검산하기 위한 것입니다. Tie-breaking을 deterministic하게 만들고, toy fixture에서 exhaustive Python sort와 결과가 같은지 test합니다.
17. Primary Metric을 Candidate 역할에 맞춘다
Reranker 앞 candidate generator라면:
primary: Recall@50 or Recall@100
secondary: nDCG@10, MRR@10
evidence: complete-evidence@N for multi-hop
사용자에게 dense result를 바로 노출한다면 nDCG·precision의 비중이 커집니다.
모든 metric 규약:
- positive 없는 query 처리
- graded relevance gain
- unjudged document 처리
- child→parent deduplication
- tie-breaking
- query macro average
를 code test와 report에 고정합니다.
18. Paired Statistics와 Regression Budget
각 query에서 challenger와 baseline metric delta를 만듭니다.
Δ_i = metric_i(challenger) - metric_i(baseline)
Session이나 template family가 상관돼 있으면 query가 아니라 group 단위 paired bootstrap을 사용합니다.
Release rule 예:
statistical_gate:
primary_mean_delta_min: 0.02
primary_ci_lower_min: 0.005
critical_slice_delta_min: -0.01
worst_query_review_count: 100
bootstrap_groups: 10000
seed: 17
P-value 하나보다 effect size, interval, win/tie/loss, worst regression을 같이 봅니다.
19. Slice Dashboard
language: Korean / English / mixed
query style: keyword / natural / conversational / reasoning
match: lexical / paraphrase / entity / number / negation
document: short / long / truncated / context-dependent
evidence: single / multi-hop / no-answer
temporal: current / stale-conflict
risk: normal / policy / security
source: human / synthetic
각 slice에 sample 수와 confidence interval을 표시합니다. 작은 slice의 점 추정치만 보고 model을 기각하거나 승인하지 않습니다.
20. Error Taxonomy
Top regression을 다음으로 분류합니다.
| 실패 | 다음 실험 |
|---|---|
| Gold가 corpus에 없음 | ingestion/qrel 수정 |
| Chunk에 답이 잘림 | chunker/parent retrieval |
| Exact lexical entity 누락 | BM25/hybrid·entity negatives |
| 의미는 비슷한 오답 | hard-negative 정제 |
| 과거 규정이 상위 | temporal metadata·negative |
| 부정 반전 | negation slice/data |
| 긴 chunk 후반 누락 | truncation·late chunking |
| 한국어 colloquial 실패 | native query data |
| Exact 성공, ANN 실패 | index tuning |
| Retrieval 성공, 답 실패 | context packing/reader |
모든 실패를 fine-tuning 문제로 만들지 않습니다.
21. Ablation은 한 축씩
E0 zero-shot
E1 + clean domain pairs
E2 + mined/relabelled negatives
E3 + teacher distillation
E4 + MRL
E5 + contextual chunk encoding
각 run은 이전 run에서 한 축만 바꿉니다. Computing budget이 제한되면 작은 frozen dev corpus에서 screening한 뒤 finalist만 full corpus로 encode합니다.
Run table:
| Run | Change | Exact R@50 | Korean hard | p95 encode | Decision |
|---|---|---|---|---|---|
| E0 | baseline | keep | |||
| E1 | data | ||||
| E2 | negatives | ||||
| E3 | distill | ||||
| E4 | MRL |
22. Dimension·Quantization 단계
Exact full-precision winner에서만 compression sweep을 시작합니다.
full 768d fp32 exact
→ 512 / 256d fp32 exact
→ selected dimension fp16 / int8 exact
→ selected representation ANN
각 단계의 delta를 저장합니다.
compression_report:
full_fp32_exact: 0.842
reduced_fp32_exact: 0.838
reduced_int8_exhaustive: 0.835
reduced_int8_ann: 0.831
이 숫자는 예시 형식이며 실제 결과가 아닙니다.
23. ANN Parameter Sweep
hnsw_grid:
M: [16, 32]
efConstruction: [128, 256]
efSearch: [32, 64, 128, 256]
Offline:
- ANN recall@10/50/100
- qrel Recall/nDCG
- memory·index build time
Load test:
- concurrency 1/16/64
- p50/p95/p99
- QPS at target ANN recall
- filter selectivity 100/10/1/0.1%
- cold/warm cache
Target recall 아래 parameter는 latency가 빨라도 제거합니다.
24. Hybrid와 Reranker를 다시 붙인다
Embedding의 독립 gain과 system gain을 둘 다 봅니다.
S0 BM25
S1 dense baseline
S2 dense challenger
S3 BM25 + dense baseline
S4 BM25 + dense challenger
S5 S3 + fixed reranker
S6 S4 + same fixed reranker
S2 > S1인데 S6 ≈ S5라면 reranker가 차이를 흡수했거나 hybrid 후보가 이미 포화됐을 수 있습니다. Candidate recall·reranker input overlap을 분석합니다.
25. Fixed-reader RAG Gate
Generator, prompt, context budget, reranker, sampling seed를 고정합니다.
retrieval baseline ─┐
├─ same context selector → same reader → answers
retrieval challenger┘
평가:
- answer correctness
- claim support
- citation precision·recall
- complete evidence coverage
- no-answer correctness
- context token 수
- total p95·cost
LLM judge만 쓰지 말고 answerable subset의 human audit와 deterministic evidence metric을 함께 둡니다.
26. Security·Privacy Gate
ACL Isolation
다른 tenant의 relevant vector가 top-k에 섞여도 metadata 단계에서 절대 반환되지 않는지 test합니다. Post-filter로 k가 부족할 때 다른 tenant 결과로 채우지 않습니다.
Deletion
삭제 요청 뒤 online result, cache, replica, backup retention 상태를 추적합니다.
Inversion Probe
허가된 red-team 환경에서 vector만으로 민감 attribute나 text를 추정할 수 있는지 model version별로 점검합니다.
Poisoning
Keyword stuffing, duplicate flood, Unicode 변형, instruction injection 문서를 corpus에 넣어 critical query의 rank 변화를 측정합니다.
27. Run Manifest
run:
id: emb-2026-07-20-001
objective: korean-policy-retrieval
code_commit: abc123
environment:
python: "3.12.x"
lockfile_sha256: "..."
cuda: "..."
gpu: "..."
seeds:
train: 17
mining: 23
evaluation: 47
data:
corpus_manifest: corpus-v12
train_pairs: pairs-v7
dev_queries: dev-v5
test_queries: hidden-test-v3
qrels: qrels-v8
model:
base_revision: "..."
output_checkpoint: "..."
prompt_revision: prompt-v4
pooling: mean
training:
loss: multiple-negatives-ranking
temperature_or_scale: 20
optimizer_batch: 256
contrastive_pool: 64
epochs: 1
evaluation:
metric_code_commit: def456
exact_dtype: float32
bootstrap_groups: 10000
Manifest는 결과 표의 각 row로 역추적할 수 있어야 합니다.
28. Model Card에 추가할 내용
- Intended use와 out-of-scope use
- Training data source·기간·license
- Synthetic generator와 teacher provenance
- Negative mining·relabel policy
- 언어·domain·길이 coverage
- Input prompt·pooling·normalization
- 지원 dimension·dtype
- Benchmark version과 contamination disclosure
- Exact/ANN·hardware result
- Known failure: negation, number, context, modality
- Privacy·security 고려
- Re-index migration requirement
Model weight만 공개하고 사용 contract를 숨기면 결과를 재현할 수 없습니다.
29. Shadow Index와 Canary
offline pass
→ build index v12
→ checksum + ID coverage
→ replay logged queries, no user-visible effect
→ compare overlap, score, latency, ACL
→ 1% canary
→ 10% / 50% / 100%
Canary metric:
- zero-result rate
- top-k overlap과 rank flip
- click/answer/citation proxy
- p95/p99·timeout
- tenant leakage 0
- query/index version mismatch 0
이전 index와 query encoder를 rollback window 동안 함께 보존합니다.
30. Release Decision Template
decision:
candidate: emb-domain-v3-index-v12
status: ship | hold | reject
evidence:
exact_primary_delta: "+..."
paired_ci: ["...", "..."]
critical_worst_delta: "..."
ann_recall_at_50: "..."
p95_ms: "..."
ram_gb: "..."
rag_answer_delta: "..."
security_gate: pass
known_risks:
- "..."
rollback:
index: v11
query_encoder: emb-domain-v2
approvers:
- retrieval-owner
- data-owner
- security-owner
hold는 실패가 아니라 필요한 추가 증거가 명확한 상태입니다.
31. 실패를 빠르게 찾는 Decision Tree
Exact metric가 낮다?
├─ yes → corpus/qrel 문제인가?
│ ├─ yes → data/chunk 수정
│ └─ no → prompt/pooling/data/loss 분석
└─ no → compressed exact가 낮다?
├─ yes → dimension/quantizer 완화
└─ no → ANN이 낮다?
├─ yes → index/search parameter
└─ no → RAG가 낮다?
├─ yes → packing/reranker/reader
└─ no → latency·cost·security gate
이 분해가 embedding 연구를 “checkpoint를 계속 바꿔 보는 일”에서 system science로 바꿉니다.
32. 4주 학습 Project
1주차 · 재현
- 공개 model 두 개의 공식 prompt를 재현
- 500 query·10만 document exact 평가
- BM25/dense/hybrid baseline
- metric unit test
2주차 · 학습
- human/domain pair 정제
- InfoNCE baseline
- hard-negative mining과 100개 audit
- temperature·batch ablation
3주차 · 평가
- paired bootstrap
- 한국어·부정·숫자·긴 문서 slice
- public benchmark 한 suite
- error taxonomy 100건
4주차 · 서빙
- MRL dimension sweep
- int8/PQ 한 방식
- HNSW target-recall sweep
- fixed-reader RAG와 shadow index report
각 주의 결과는 score가 아니라 manifest, report, 실패 분석, 다음 hypothesis입니다.
33. 최종 Checklist
문제·데이터
- 성공 조건에 quality·cost·regression이 있다.
- Corpus·query·qrel·split을 snapshot과 checksum으로 고정했다.
- Exact·near-duplicate·source·process contamination을 검사했다.
- Qrel completeness와 annotator disagreement를 샘플링했다.
학습
- Prompt·pooling·normalization contract가 일관된다.
- Pair/triplet의 relevance 의미가 명확하다.
- Hard negative를 relabel하고 false-negative rate를 냈다.
- Optimizer batch와 contrastive pool을 구분했다.
- Stage별 ablation으로 gain 출처를 확인했다.
평가·서빙
- Full-precision exact에서 먼저 비교했다.
- Paired interval과 critical slice를 보고했다.
- Dimension·quantization·ANN loss를 분해했다.
- Target ANN recall에서 latency·memory를 비교했다.
- Hybrid·reranker·fixed reader에서 system gain을 확인했다.
운영
- Model/index identity와 compatibility check가 있다.
- ACL·삭제·inversion·poisoning test를 통과했다.
- Shadow·canary·rollback 절차가 있다.
- Model card와 run manifest로 결과를 재현할 수 있다.
스스로 확인하기
- Query random split보다 source·time group split이 더 안전한 이유는 무엇인가?
- Gradient accumulation이 contrastive negative pool을 자동으로 늘리지 않는 이유는 무엇인가?
- Hard negative teacher threshold를 dev metric만으로 정하면 어떤 문제가 생기는가?
- Fine-tuned embedding이 exact에서 좋아졌지만 RAG 답변은 같을 때 어디를 분석해야 하는가?
- Full-precision exact, compressed exact, ANN을 순서대로 평가하면 어떤 원인을 분리할 수 있는가?
- Model update 때 shadow index와 이전 query encoder를 함께 보존해야 하는 이유는 무엇인가?
이것으로 14편의 Embedding 연구·엔지니어링 시리즈를 마칩니다. 다시 볼 때는 목적에 따라 다음 경로를 권합니다.
- 학습 중심: 3편 대조 학습 → 4편 데이터·negative → 7편 합성·증류
- 평가 중심: 10편 평가 protocol → 11편 benchmark → 이 글
- 검색 운영 중심: 9편 긴 문서 → 12편 압축·ANN → 이 글
- 연구 탐색: 1편 전체 지도 → 13편 최근 동향 → 관심 논문의 원문과 재현 실험
Reranker까지 함께 연구하려면 Reranker 엔지니어링 첫 글과 production reranking 실습을 이어서 읽으면 됩니다.
참고자료
- Sentence Transformers Training Overview
- Sentence Transformers Losses
- Sentence Transformers Hard-negative Mining Utilities
- MTEB Evaluation Documentation
- Dense Passage Retrieval for Open-Domain Question Answering
- Approximate Nearest Neighbor Negative Contrastive Learning for Dense Text Retrieval
- RocketQAv2: A Joint Training Method for Dense Passage Retrieval and Passage Re-ranking
- Matryoshka Representation Learning
- BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models
- Hard Negatives, Hard Lessons
- Text Embeddings Reveal (Almost) As Much As Text