Field Log · Entry

Learning to Rank: Pointwise·Pairwise·Listwise와 LambdaMART (2/14)

Query별 후보 목록을 pointwise 점수, pairwise 선호, listwise 순열과 nDCG 가중치로 학습하는 Learning to Rank 지도

이번 글의 결론

  • Ranking dataset의 기본 단위는 독립적인 row가 아니라 같은 query에 묶인 후보 목록입니다.
  • Pointwise는 relevance 값을, pairwise는 두 문서의 순서를, listwise는 목록 전체의 분포나 순열을 직접 학습합니다.
  • Model architecture와 loss formulation은 별개입니다. Cross-Encoder도 pointwise·pairwise·listwise 중 어느 목표로든 학습할 수 있습니다.
  • LambdaRank는 nDCG 자체를 미분하는 대신 문서 쌍을 바꿨을 때의 ΔnDCG로 gradient의 중요도를 정합니다. LambdaMART는 이 아이디어를 boosted tree에 결합합니다.
  • Loss 이름만 고르지 말고 label 형태, candidate distribution, top-k metric, batch grouping을 하나의 contract로 맞춰야 합니다.

앞 글에서 reranker를 후보 집합의 순서를 다시 만드는 단계로 정의했습니다. 이번에는 “좋은 순서”를 model에 어떻게 가르치는지 다룹니다.

Neural reranker가 등장하기 전부터 검색과 추천 시스템은 Learning to Rank, LTR라는 문제를 풀었습니다. Cross-Encoder와 LLM reranker도 이 수학을 벗어나지 않습니다. 달라진 것은 feature를 만드는 model과 목록을 읽는 방식입니다.

input
  query q
  candidates d1 ... dn
  relevance labels y1 ... yn

learn
  scoring function fθ(q, d)
  or permutation model gθ(q, D)

evaluate
  top-heavy ranking metric: MRR, MAP, nDCG@k

1. Ranking Dataset은 Query Group을 보존한다

다음 네 row는 서로 독립적인 classification example이 아닙니다.

query_iddocument_idrelevance
q-17d-approved-s23
q-17d-draft-s21
q-17d-approved-s10
q-17d-s2-pressure0

의미는 같은 query 안의 순서입니다.

d-approved-s2
  > d-draft-s2
  > d-approved-s1 ≈ d-s2-pressure

다른 query q-42의 score scale과 직접 비교할 필요는 없습니다. 검색 결과는 query별로 정렬되기 때문입니다.

따라서 split도 query 단위로 해야 합니다.

# 잘못된 방식: 같은 query의 문서가 train과 test에 흩어진다.
train_rows, test_rows = random_split(all_query_document_rows)

# 올바른 출발점: query_id를 먼저 나눈다.
train_query_ids, test_query_ids = split_query_ids(all_query_ids)

Document template나 source가 거의 같은 duplicate라면 query split만으로도 leakage가 남을 수 있습니다. 시간, 고객, 제품군, document family 단위 holdout이 필요한지 확인합니다.

2. Relevance는 Binary보다 풍부할 수 있다

Reranker label은 document의 고정 속성이 아니라 (query, document) 관계입니다.

3 = 질문의 답을 직접 지지하는 authoritative evidence
2 = 답의 일부를 지지하거나 중요한 supporting evidence
1 = 주제는 관련 있지만 답하기에는 불충분
0 = 무관, 잘못된 entity·version·status, contradiction

같은 과거 정책 문서는 “현재 환불 정책”에는 0 또는 1이고, “2023년과 현재 정책 비교”에는 3일 수 있습니다.

Graded relevance는 다음을 표현합니다.

  • direct answer와 background의 차이
  • primary source와 commentary의 차이
  • complete evidence와 partial evidence의 차이
  • approved와 draft의 차이
  • 최신 문서와 obsolete 문서의 차이

다만 annotator가 grade 경계를 일관되게 구분하지 못하면 세밀한 숫자가 오히려 noise가 됩니다. 먼저 rubric과 disagreement를 측정합니다.

3. Pointwise: 각 Pair의 Relevance를 맞힌다

Pointwise model은 하나의 query-document pair를 입력받아 label을 예측합니다.

s_i = fθ(q, d_i)
L_point = Σ_i ℓ(s_i, y_i)

Binary Cross-Entropy

Binary relevance라면 logit s_i에 BCE를 사용할 수 있습니다.

p_i = sigmoid(s_i)

L_BCE = -y_i log p_i - (1-y_i) log(1-p_i)

장점은 단순한 batching과 명확한 class probability 해석입니다. 하지만 실제 serving score를 확률 threshold로 쓸 생각이라면 별도 calibration을 확인해야 합니다. BCE로 학습했다는 사실만으로 production probability가 보정되지는 않습니다.

Regression

Grade 0...3을 실수로 회귀할 수도 있습니다.

L_MSE = (s_i - y_i)²

하지만 grade 0과 1의 거리, 2와 3의 거리가 같은 의미인지 확인해야 합니다. Ordinal classification처럼 순서 구조를 명시하는 방법도 있습니다.

Pointwise의 장점

  • Pair 단위로 shuffle·batching하기 쉽습니다.
  • 독립 score가 있어 threshold·filtering과 결합하기 쉽습니다.
  • Query마다 candidate 수가 달라도 구현이 단순합니다.
  • Human binary label과 바로 연결됩니다.

Pointwise의 한계

  • 같은 query의 후보 간 상대 순서를 loss가 직접 보지 않습니다.
  • Easy negative를 많이 넣으면 class imbalance를 푸는 데 capacity를 씁니다.
  • 모든 오차가 top rank에 미치는 영향이 같지 않은데 동일하게 취급할 수 있습니다.
  • Query별 score scale이 다를 수 있습니다.

4. Pairwise: Positive가 Negative보다 높게 만든다

같은 query에서 d_id_j보다 관련 있다면:

y_i > y_j
s_i = fθ(q, d_i)
s_j = fθ(q, d_j)

RankNet-style pairwise logistic loss는 다음과 같이 쓸 수 있습니다.

P(d_i > d_j) = sigmoid(s_i - s_j)
L_pair = -log sigmoid(s_i - s_j)

Margin ranking loss도 가능합니다.

L_margin = max(0, m - s_i + s_j)

s_is_j의 절대값보다 차이를 학습한다는 점이 핵심입니다.

모든 Pair를 만들면 안 되는 이유

후보가 n개면 가능한 unordered pair는:

n(n - 1) / 2

n=100이면 query 하나에 4,950쌍입니다. 대부분이 쉬운 3-vs-0 pair일 수 있습니다.

의미 있는 pair를 의도적으로 sampling합니다.

pair_types:
  - grade3_vs_grade2
  - approved_vs_draft
  - correct_entity_vs_same_name_wrong_entity
  - direct_answer_vs_topic_only
  - supported_vs_keyword_stuffed
  - latest_vs_obsolete

가까운 grade는 informative하지만 annotator disagreement도 큽니다. Hard pair의 label audit 비율을 높이는 이유입니다.

Order Swap으로 Bias를 찾는다

Pairwise LLM prompt는 A를 먼저 보여 줬기 때문에 A를 고를 수 있습니다.

run 1: compare(A, B)
run 2: compare(B, A)

두 결과가 논리적으로 일치하지 않으면 tie·abstain·추가 비교 정책이 필요합니다. 이것은 pairwise loss 자체보다 inference protocol의 문제이며 6편에서 다시 다룹니다.

5. Listwise: Query의 후보 목록을 하나의 학습 단위로 본다

Listwise 방법은 score vector 전체와 label vector 전체를 비교하거나 정답 순열의 likelihood를 학습합니다.

s = [s_1, ..., s_n]
y = [y_1, ..., y_n]

ListNet-style Softmax Cross-Entropy

Label과 model score를 query 내부 분포로 바꿉니다.

P_label(i) = softmax(y_i / τ_label)
P_model(i) = softmax(s_i / τ_model)

L_list = -Σ_i P_label(i) log P_model(i)

Binary label에서 positive가 하나라면 positive index의 softmax probability를 키우는 형태가 됩니다.

import torch
import torch.nn.functional as F

def listwise_softmax_loss(
    scores: torch.Tensor,
    grades: torch.Tensor,
    mask: torch.Tensor,
) -> torch.Tensor:
    masked_scores = scores.masked_fill(~mask, float("-inf"))
    masked_grades = grades.masked_fill(~mask, float("-inf"))

    target = torch.softmax(masked_grades, dim=-1)
    log_probs = torch.log_softmax(masked_scores, dim=-1)
    per_query = -(target * log_probs).masked_fill(~mask, 0.0).sum(dim=-1)
    return per_query.mean()

Batch 안에서 query마다 candidate 수가 다르면 mask가 필요합니다. Padding document가 loss 분모에 들어가면 학습 목표가 바뀝니다.

ListMLE-style Permutation Likelihood

정답 순열을 π*라고 하면 Plackett–Luce 형태로 위에서부터 선택할 likelihood를 최적화할 수 있습니다.

P(π*) = ∏_{k=1}^{n}
        exp(s_{π*(k)}) /
        Σ_{j=k}^{n} exp(s_{π*(j)})

Grade tie가 많을 때 정답 순열 하나를 임의로 고정하면 불필요한 순서를 강요합니다. Tie-aware sampling이나 group objective가 필요합니다.

Listwise의 장점과 비용

  • Query 내부 competition을 직접 학습합니다.
  • Graded relevance 분포를 활용하기 쉽습니다.
  • Top-k 목록 전체의 모양과 더 가깝습니다.
  • 반면 query별 group batching, variable length, label tie 처리가 복잡합니다.
  • Training list와 serving candidate distribution이 다르면 효과가 약해질 수 있습니다.

6. Metric은 순위가 바뀔 때 불연속적이다

검색에서는 보통 top rank를 더 중요하게 봅니다. DCG@k의 한 convention은 다음과 같습니다.

DCG@k = Σ_{r=1}^{k} (2^rel_r - 1) / log2(r + 1)
nDCG@k = DCG@k / IDCG@k

문서 score가 0.701 → 0.702로 조금 바뀌어도 순서가 그대로면 nDCG는 변하지 않습니다. 반대로 두 score가 교차해 rank가 바뀌면 nDCG가 갑자기 변합니다.

이런 metric은 일반적인 gradient descent로 직접 미분하기 어렵습니다. 그래서 surrogate loss를 쓰거나 metric 변화량으로 gradient의 중요도를 조정합니다.

7. LambdaRank: 중요한 Swap에 더 큰 Gradient를 준다

같은 query의 문서 i, j를 바꿨을 때 metric 변화량을 계산합니다.

ΔnDCG_ij = |nDCG(before) - nDCG(after swapping i, j)|

Pairwise gradient의 크기를 ΔnDCG_ij로 가중합니다.

λ_ij ∝ ΔnDCG_ij × sigmoid(-(s_i - s_j))

직관은 간단합니다.

  • Rank 1과 2를 잘못 바꾼 오류는 큽니다.
  • 둘 다 evaluation cutoff 밖인 rank 80과 81의 swap은 작거나 0입니다.
  • Relevance grade 차이가 큰 pair가 더 중요합니다.
  • 이미 올바르고 score margin도 큰 pair에는 gradient가 작습니다.

LambdaRank는 명시적인 scalar loss를 먼저 설계하기보다 원하는 metric 방향의 lambda gradient를 구성한 아이디어입니다.

8. LambdaMART: Neural 이전의 유물이 아니다

MART는 Multiple Additive Regression Trees, 즉 gradient-boosted regression tree 계열입니다. LambdaMART는 tree ensemble을 LambdaRank식 gradient로 학습합니다.

입력은 query-document text 자체가 아니라 feature vector일 수 있습니다.

x(q, d) = [
  bm25_title,
  bm25_body,
  dense_score,
  cross_encoder_score,
  freshness_days,
  source_authority,
  click_prior,
  exact_id_match,
  document_length,
]

여전히 유용한 이유

  • 서로 단위가 다른 검색·business feature를 결합하기 쉽습니다.
  • Feature attribution과 debugging이 neural text model보다 단순할 수 있습니다.
  • CPU serving이 빠르고 stable합니다.
  • Freshness, stock, policy, authority처럼 text만으로 알 수 없는 값을 넣을 수 있습니다.
  • Cross-Encoder score를 feature 하나로 받아 마지막 business rerank를 할 수 있습니다.
BM25 + dense candidates
→ Cross-Encoder semantic score
→ LambdaMART semantic + freshness + authority + behavior
→ policy filter / diversification

“Neural reranker냐 LambdaMART냐”는 양자택일이 아닙니다. 서로 다른 stage에서 결합할 수 있습니다.

9. Pointwise Model과 Pointwise Loss를 혼동하지 않는다

다음 네 문장은 구분해야 합니다.

  1. 입력 formulation: 한 번에 후보 하나를 읽는다.
  2. output formulation: 후보마다 scalar score를 낸다.
  3. training loss: 같은 query의 score를 listwise softmax로 학습한다.
  4. inference: 모든 scalar score를 정렬한다.

예를 들어 RankT5는 query-document를 하나씩 encode하고 scalar를 낼 수 있지만, 같은 query의 여러 score를 listwise loss로 학습할 수 있습니다. 입력은 pointwise여도 loss는 listwise입니다.

반대로 decoder-only LLM이 후보 20개를 함께 읽고 ID 순열을 생성하면 입력과 output이 모두 listwise에 가깝습니다.

10. Implicit Feedback에는 Position Bias가 들어 있다

Click을 relevance label로 바로 쓰면 순환이 생깁니다.

높은 rank라서 많이 노출됨
→ 많이 클릭됨
→ relevant label로 학습됨
→ 더 높은 rank가 됨

관측 click c는 relevance r뿐 아니라 examination e에 영향을 받습니다.

P(click = 1) ≈ P(examined = 1) × P(relevant = 1)

Randomized interleaving, propensity estimation, inverse propensity weighting 같은 debiasing이 필요한 이유입니다. 구매·dwell time도 availability, UI, price, brand bias와 섞여 있습니다.

Human qrel, behavior, teacher label을 한 숫자로 합치기 전에 provenance를 보존합니다.

{
  "grade": 2,
  "label_source": "human",
  "annotator_count": 3,
  "agreement": 0.67,
  "policy_version": "relevance-rubric-v4"
}

11. 어떤 Objective를 선택할까

상황강한 출발점이유
Binary pair label이 많음pointwise BCE단순하고 강한 baseline
명확한 preference pair가 있음pairwise logistic상대 순서를 직접 반영
Graded qrels와 query group이 있음listwise softmax목록 내 grade 분포 활용
nDCG@k가 핵심이고 feature가 다양함LambdaMARTtop-heavy metric과 tabular feature 결합
Score threshold가 운영 contractpointwise + calibration절대 score 해석 필요
Top-k 순서만 필요pair/listwise + ranking metriccalibration보다 order 우선

실전에서는 하나만 고정하지 않습니다.

L_total
  = α L_point
  + β L_pair
  + γ L_list
  + regularization

하지만 loss를 많이 섞는다고 자동으로 좋아지지 않습니다. 각 항의 scale, sampling frequency, metric contribution을 ablation해야 합니다.

12. 작은 실험 설계

같은 architecture와 training data를 고정하고 objective만 바꿉니다.

experiment:
  model: cross-encoder-base-v1
  train_candidates: hybrid-v3-top50
  train_queries: query-split-v5
  max_length: 512
  negatives_per_query: 7
  variants:
    - pointwise_bce
    - pairwise_logistic
    - listwise_softmax
  evaluation:
    candidate_file: frozen-candidates-v8.jsonl
    metrics: [mrr_at_10, ndcg_at_5, ndcg_at_10, ece]

Model size나 candidate가 바뀌면 objective 효과를 분리할 수 없습니다.

13. 흔한 실패

Row 단위 Random Split

같은 query와 duplicate document가 train·test에 섞입니다.

처방: query와 document family를 고려해 split합니다.

Random Negative만 사용

Model이 topic classification만 배우고 production의 near-miss를 구분하지 못합니다.

처방: 실제 retriever top-k에서 hard negative를 만듭니다.

모든 Pair를 동일하게 Sampling

Easy pair가 학습을 지배합니다.

처방: grade gap, rank position, confusion type으로 sampling합니다.

Loss가 낮으면 Ranking이 좋다고 판단

Surrogate loss와 nDCG는 같은 값이 아닙니다.

처방: query-level ranking metric과 downstream metric을 함께 봅니다.

Cross-query Score를 직접 비교

Query마다 score 분포가 달라 threshold가 깨집니다.

처방: threshold가 필요하면 calibration dataset과 slice별 reliability를 평가합니다.

스스로 확인하기

  1. Ranking dataset을 row가 아니라 query group으로 split해야 하는 이유는 무엇인가?
  2. Pointwise BCE와 pairwise logistic은 각각 어떤 관계를 직접 학습하는가?
  3. Listwise softmax에서 padding mask가 잘못되면 어떤 문제가 생기는가?
  4. LambdaRank가 rank 1·2의 swap을 rank 80·81보다 크게 취급하는 이유는 무엇인가?
  5. LambdaMART와 Cross-Encoder를 한 pipeline에서 함께 쓸 수 있는 방법은 무엇인가?

다음 글에서는 query와 document token을 모든 layer에서 함께 읽는 Cross-Encoder를 구조·score·truncation·실습 코드까지 내려가 봅니다.

참고자료