목차
- A/B 테스트 개념 (t-test, Cohen’s d)
- A/B 테스트 구현 (ab_test)
- 리랭킹 효과 측정 실험
- BLEU / ROUGE 개요
- simple_rouge_f1 구현
- LLM-as-judge 개요
- basic_judge (비구조화 출력)
- basic_judge2 (JSON 구조화 출력)
- Pointwise Judge with RUBRIC
- Pairwise Judge + Position Bias 처리
- Reference-based Judge
- 자주 나오는 실수 / 주의사항
- [보충] 평가 기법 비교
1. A/B 테스트 개념 (t-test, Cohen’s d)
왜 필요한가?
리랭킹 기법을 바꿨을 때 “실제로 성능이 좋아진 건가?” 를 수치로 검증합니다.
1
2
3
4
5
6
7
8
| A (기존 방식): 벡터 검색만 사용
B (변경 방식): 벡터 검색 + BM25 리랭킹
예시:
A: AP = [0.87, 1.0, 0.58] → mAP ≈ 0.82
B: AP = [0.75, 1.0, 0.33] → mAP ≈ 0.69
→ "A가 더 좋아 보이지만, 이 차이가 통계적으로 의미 있는가?"
|
t-test와 p-value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| [검정 구조]
귀무가설(H₀): A와 B의 성능 차이가 없다
대립가설(H₁): A와 B의 성능 차이가 있다
t-test → p-value 계산
p-value: H₀가 참일 때 지금의 결과(또는 더 극단적인 결과)가 나올 확률
p-value = 0.01 → "차이가 없는데도 이런 결과가 나올 확률이 1%" → H₀ 기각 가능
p-value < alpha(유의수준, 보통 0.05) → 통계적으로 유의미한 차이
[t-test 종류]
stats.ttest_rel(a, b) : 대응표본 (같은 쿼리셋에 A/B 적용) ← 주로 사용
welch's t-test : 두 집단의 분산이 다를 때
모집단이 같은 경우 : 같은 반의 수학점수 vs 영어점수
모집단이 다른 경우 : 우리 반 수학점수 vs 옆 반 수학점수
|
Cohen’s d — 효과 크기
1
2
3
4
5
6
7
8
9
10
| d = (mean_A - mean_B) / pooled_std
pooled_std: 두 집단을 합친 표준편차 (얼마나 퍼져있는지)
해석:
d < 0.2 : 효과 작음
d ≈ 0.5 : 효과 중간
d > 0.8 : 효과 큼
p-value가 유의해도 Cohen's d가 작으면 → 통계적으로는 차이 있지만 실용적으로는 미미
|
2. A/B 테스트 구현 (ab_test)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from scipy import stats
def ab_test(scores_a, scores_b, alpha=0.05):
t_stat, p_val = stats.ttest_rel(scores_a, scores_b) # 대응표본 t-test
mean_a = np.mean(scores_a)
mean_b = np.mean(scores_b)
pooled_std = np.sqrt((np.std(scores_a)**2 + np.std(scores_b)**2) / 2)
cohens_d = (mean_a - mean_b) / pooled_std if pooled_std > 0 else 0.0
return {
'mean_A': mean_a,
'mean_B': mean_b,
'p_val': p_val,
'cohens_d': cohens_d,
'significant': p_val < alpha # True면 통계적으로 유의한 차이
}
|
3. 리랭킹 효과 측정 실험
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| test_queries = {
'트랜스포머 아키텍처': {'d1', 'd2', 'd3'},
'벡터 검색 방법': {'d5'},
'LLM 학습 방법': {'d6', 'd7'}
}
aps_before = [] # 리랭킹 전 AP (벡터 검색만)
aps_after = [] # 리랭킹 후 AP (BM25 리랭킹 적용)
bm25_reranker = BM25Reranker()
for q, relevant in test_queries.items():
orig = vector_search(q, vectorstore)
reranked = bm25_reranker.rerank(q, orig)
orig_ids = [doc.metadata['id'] for doc, _ in orig]
reranked_ids = [doc.metadata['id'] for doc, _ in reranked]
aps_before.append(average_precision(orig_ids, relevant))
aps_after.append(average_precision(reranked_ids, relevant))
result = ab_test(aps_before, aps_after, alpha=0.05)
|
1
2
3
4
5
6
| 결과 해석 예시:
mean_A = 0.82, mean_B = 0.69
p_val = 0.32 → 유의수준 0.05보다 크다 → 통계적으로 유의하지 않음
significant = False
→ 샘플이 3개뿐이라 통계 검정력 낮음 → 더 많은 쿼리로 실험 필요
|
4. BLEU / ROUGE 개요
텍스트 생성 품질을 n-gram 겹침으로 측정하는 전통적 지표입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| [BLEU]
- 기계 번역 평가에서 출발
- n-gram precision 기반: 생성문이 정답에 얼마나 정확한지
- 1-gram(unigram), 2-gram(bigram) 등 사용
[ROUGE]
- 요약 평가에 주로 사용
- n-gram recall 기반: 정답의 내용을 얼마나 커버하는지
- ROUGE-1 (unigram), ROUGE-2 (bigram), ROUGE-L (최장 공통 부분수열)
[F1 = ROUGE-F1]
Precision: 생성문 단어 중 정답에 있는 비율
Recall: 정답 단어 중 생성문에 있는 비율
F1 = 2 × P × R / (P + R)
|
한계
1
2
3
4
5
6
| BLEU/ROUGE의 한계:
- 단어 일치만 봄 → 의미적 유사성 측정 불가
- "딥러닝은 신경망 기반 AI 기술" vs "딥러닝은 심층 신경망을 사용하는 머신러닝"
→ 의미는 같지만 단어가 다르면 낮은 점수
- 언어, 전문용어, 패러프레이즈에 취약
→ 보완책으로 LLM-as-judge 사용
|
5. simple_rouge_f1 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
| from collections import Counter
def simple_rouge_f1(reference, candidate):
ref_words = Counter(reference.split())
cand_words = Counter(candidate.split())
# 겹치는 단어 수 (최솟값 기준)
overlap = sum(min(ref_words[w], cand_words.get(w, 0)) for w in ref_words)
r = overlap / sum(ref_words.values()) if ref_words else 0 # Recall
p = overlap / sum(cand_words.values()) if cand_words else 0 # Precision
return 2*p*r / (p+r) if (p+r) > 0 else 0 # F1
|
1
2
3
4
5
6
7
| 예시:
reference : "딥러닝은 다층 신경망을 사용하여 데이터에서 복잡한 패턴을 학습하는 머신러닝 방법입니다"
candidate1: "딥러닝은 다층 신경망을 사용하여..." (정답 복사) → ROUGE ≈ 1.0
candidate2: "인공 신경망의 층을 깊게 쌓아..." (좋은 패러프레이즈) → ROUGE ↓ (단어 다름)
candidate3: "딥러닝 딥러닝 신경망 데이터 학습..." (키워드 나열) → ROUGE 중간 (단어 겹침)
→ ROUGE만으로는 candidate2 같은 좋은 패러프레이즈를 제대로 평가 못함
|
6. LLM-as-judge 개요
LLM을 평가자(Judge)로 활용하여 텍스트 품질을 평가합니다.
1
2
3
4
5
6
7
8
9
10
11
| [LLM-as-judge 유형]
1. Pointwise : 질문 + 답변 1개 → 점수(1~5)
용도: 단일 답변의 절대적 품질 평가
2. Pairwise : 질문 + 답변 A + 답변 B → A / B / Tie 중 선택
용도: 두 시스템의 상대적 비교
복잡도: N*(N-1)/2 쌍 (N=5 → 10쌍, N=500 → 125,000쌍)
3. Reference-based : 질문 + 정답 + 학생 답변 → 점수
용도: 정답이 있는 경우의 채점
|
LLM-as-judge 장단점
1
2
3
4
5
6
7
8
9
10
| [장점]
- 의미적 이해: 단어가 달라도 의미가 같으면 높은 점수
- 유연함: 다양한 평가 기준 적용 가능
- 복잡한 품질 평가: 창의성, 논리성 등 ROUGE로 못 하는 평가 가능
[단점]
- 비용: 평가마다 LLM API 호출
- 기준 모호: 같은 답변도 실행마다 다른 점수가 나올 수 있음
- 재현성: 동일 입력에 대해 결과가 달라질 수 있음
- Position bias: pairwise에서 A를 먼저 보여주면 A에게 유리한 경향
|
7. basic_judge (비구조화 출력)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| def basic_judge(question, answer, scale='1~5'):
"""기본 LLM 평가: 텍스트 형태로 점수 반환"""
prompt = f"""다음 답변을 {scale}스케일로 평가하세요.
질문 : {question}
답변 : {answer}
평가 기준
- 정확성 : 사실적으로 올바른가?
- 완전성 : 질문에 충분히 답했는가?
- 명확성 : 이해하기 쉽게 설명했는가?
반드시 아래 형식으로 답하세요:
점수 : [숫자]
이유 : [한 줄 설명]"""
return llm.invoke(prompt).content
|
1
2
3
4
5
6
7
8
9
10
11
| 사용 예시:
question = "파이썬의 장점은 무엇인가요?"
답변A: "파이썬은 간결한 문법으로 초보자도 쉽게 배울 수 있으며..."
→ 점수: 5 / 이유: 핵심을 명확하게 잘 설명했습니다.
답변B: "파이썬 좋아요."
→ 점수: 1 / 이유: 구체적인 내용이 전혀 없습니다.
답변C: "자바스크립트는 웹 개발에 주로 사용됩니다."
→ 점수: 1 / 이유: 질문과 무관한 답변입니다.
|
8. basic_judge2 (JSON 구조화 출력)
파싱 가능한 JSON 형태로 점수를 받습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| def basic_judge2(question, answer, scale='1~5'):
prompt = f"""다음 답변을 {scale}스케일로 평가하세요. 답변은 반드시 JSON으로 반환하세요
질문 : {question}
답변 : {answer}
평가 기준
- 정확성 : 사실적으로 올바른가?
- 완전성 : 질문에 충분히 답했는가?
- 명확성 : 이해하기 쉽게 설명했는가?
반드시 아래 형식으로 답하세요:
{{"score": 1~5점수, "confidence": "high/mid/low"}}"""
result = llm.invoke(prompt).content
cleaned = result.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("```")[1]
if cleaned.startswith("json"):
cleaned = cleaned[4:]
return json.loads(cleaned)
|
1
2
3
4
5
| 비구조화 vs 구조화 비교:
basic_judge → 텍스트 파싱 필요, 형식 불안정
basic_judge2 → JSON 파싱으로 직접 .get('score') 접근 가능
→ 프로그램에서 점수를 활용할 때는 반드시 JSON 구조화 사용
|
9. Pointwise Judge with RUBRIC
루브릭(채점 기준표)을 제공하여 더 일관된 점수를 얻습니다.
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
| RUBRIC = {
5: "정확하고 완전하며, 예시와 설명이 풍부하다",
4: "정확하고 핵심을 다루지만, 일부 세부사항이 부족하다",
3: "대체로 정확하지만, 중요한 내용 일부가 누락되었다",
2: "부분적으로만 정확하거나, 핵심을 빗나갔다",
1: "부정확하거나 질문과 무관하다",
}
def pointwise_judge(question, answer, rubric=RUBRIC):
rubric_text = '\n'.join(
f' {k} 점: {v}' for k, v in sorted(rubric.items(), reverse=True)
)
prompt = f"""다음 답변을 아래 루브릭에 따라 평가해주세요.
질문 : {question}
답변 : {answer}
루브릭 :
{rubric_text}
반드시 JSON으로 답하세요:
{{"score": 1-5, "reasoning": "이유"}}"""
result = llm.invoke(prompt).content
result = result.replace('```json', '').replace('```', '')
return json.loads(result)
|
1
2
3
4
5
6
7
8
9
10
| 루브릭 사용 효과:
루브릭 없음: LLM이 기준을 자의적으로 설정 → 일관성 낮음
루브릭 있음: 명확한 기준 제공 → 일관성 ↑, 재현성 ↑
사용 예시:
question = "REST API와 GraphQL의 차이점을 설명해주세요"
답변A (상세): REST는 리소스 단위 URL에 HTTP 메서드를 사용하고... → 점수: 5
답변B (짧음): REST는 URL을 사용하고 GraphQL은 쿼리를 사용합니다. → 점수: 3
답변C (부적절): 둘 다 API입니다. → 점수: 2
|
10. Pairwise Judge + Position Bias 처리
기본 Pairwise Judge
1
2
3
4
5
6
7
8
9
10
11
12
| def pairwise_judge(question, answer_a, answer_b):
prompt = f"""두 답변을 비교하여 더 나은 쪽을 선택하세요
질문 : {question}
답변 A : {answer_a}
답변 B : {answer_b}
반드시 JSON으로 답하세요:
{{"winner": "A 또는 B 또는 Tie", "reasoning": "선택 이유"}}"""
result = llm.invoke(prompt).content
return json.loads(result.replace('```json', '').replace('```', ''))
|
Position Bias 문제와 해결
1
2
3
4
5
6
7
| [Position Bias (위치 편향)]
LLM은 "Lost-in-the-middle" 현상이 있어 먼저 보여준 답변에 유리한 경향이 있음
(A, B) 순서: A 승리
(B, A) 순서: B 승리 ← 실제로는 같은데 순서가 바뀌어서 결과가 달라짐
→ 순서를 바꿔서 두 번 비교하고 결과가 일치하는 경우만 신뢰
|
1
2
3
4
5
6
7
8
9
10
11
12
| def pairwise_judge_with_swap(question, answer_a, answer_b):
"""A→B, B→A 순서로 두 번 비교하여 일관된 결과만 채택"""
result1 = pairwise_judge(question, answer_a, answer_b) # (A, B) 순서
result2 = pairwise_judge(question, answer_b, answer_a) # (B, A) 순서
# result2에서 A가 이겼다면 실제 winner는 B (순서가 뒤집혔으므로)
swapped_winner = {'A': 'B', 'B': 'A', 'Tie': 'Tie'}.get(result2['winner'])
if result1['winner'] == swapped_winner:
return {'final': result1['winner'], 'status': '일치'}
else:
return {'final': 'Tie', 'status': '불일치'} # 결과가 다르면 Tie 처리
|
Position Bias 측정
1
2
3
4
5
6
7
8
9
10
11
12
13
| def detect_position_bias(question, answer_a, answer_b, n_trials=3):
"""n번 반복 비교해서 불일치율(bias_rate) 계산"""
consistent = 0
inconsistent = 0
for _ in range(n_trials):
result = pairwise_judge_with_swap(question, answer_a, answer_b)
if result['status'] == '일치':
consistent += 1
else:
inconsistent += 1
return inconsistent / (consistent + inconsistent) # bias_rate
|
1
2
3
4
| bias_rate 해석:
0.0 → 항상 일관된 결과 (Position Bias 없음)
0.5 → 절반은 일치, 절반은 불일치
1.0 → 항상 순서에 따라 결과가 달라짐 (심각한 Bias)
|
11. Reference-based Judge
정답(reference)이 있을 때 학생 답변을 채점합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| def reference_based_judge(question, reference, answer):
prompt = f"""당신은 AI 교육 전문가입니다.
정답을 기준으로 학생의 답변을 평가하세요.
질문 : {question}
정답 : {reference}
학생 답변 : {answer}
평가 기준:
- 정답의 핵심 포인트를 얼마나 포함하는가
- 사실적으로 틀린 내용이 있는가
- 정답에 없는 올바른 추가 정보가 있는가
JSON으로 답하세요:
{{
"score": 1-5,
"covered_points": ["포함된 핵심 포인트들"],
"missing_points": ["누락된 포인트들"],
"errors": ["틀린 내용"]
}}"""
result = llm.invoke(prompt).content
cleaned = result.replace('```json', '').replace('```', '')
return json.loads(cleaned)
|
1
2
3
4
5
6
7
8
9
10
| 사용 예시:
question = "TCP와 UDP의 차이점은?"
reference = "TCP는 연결지향적이고 신뢰성 있는 전송을 보장하며, UDP는 비연결지향적이고 빠르지만 신뢰성을 보장하지 않습니다."
answer = "TCP는 연결을 먼저 수립하고 데이터 전송의 순서와 무결성을 보장합니다. 반면 UDP는 연결 없이 바로 전송하여 빠르지만 패킷 손실이 가능합니다."
결과:
score: 5
covered_points: ["연결지향 vs 비연결지향", "신뢰성", "속도"]
missing_points: []
errors: []
|
12. 자주 나오는 실수 / 주의사항
- 대응표본 t-test 조건:
ttest_rel은 두 배열의 길이가 같아야 함. 쿼리 수가 다르면 오류 - p-value 오해: p-value < 0.05 → “통계적으로 유의한 차이” 이지 “실용적으로 중요한 차이”가 아님 → Cohen’s d로 효과 크기 함께 확인
- 샘플 수 부족: 쿼리 3개로는 검정력(statistical power)이 너무 낮아 significant=False가 자주 나옴 → 최소 30개 이상 쿼리 권장
- Pairwise O(N²) 비용: 답변 5개 → 10쌍, 답변 500개 → 125,000쌍 → 실용적으로 비용 폭발. 토너먼트 방식 또는 샘플링 필요
- JSON 파싱 실패: LLM이
'''json 블록에 싱글쿼터를 써서 json.loads 실패 가능 → 파싱 전에 클리닝 로직 작성 필수 - Rubric 일관성: 루브릭이 있어도 경계 케이스(3점 vs 4점)에서 LLM이 흔들릴 수 있음 → 여러 번 실행하고 평균 사용 권장
[보충] 평가 기법 비교
| 기법 | 정답 필요 | 속도 | 의미 이해 | 특징 |
|---|
| ROUGE-F1 | 필요 | 빠름 | 없음 | n-gram 겹침, 요약 평가에 적합 |
| BLEU | 필요 | 빠름 | 없음 | n-gram precision, 번역 평가에 적합 |
| LLM Pointwise | 불필요 | 보통 | 있음 | 단일 답변 절대 평가 |
| LLM Pointwise + Rubric | 불필요 | 보통 | 있음 | 일관성 향상 |
| LLM Pairwise | 불필요 | 느림 | 있음 | 상대 비교, O(N²) 비용 |
| Reference-based | 필요 | 보통 | 있음 | 정답 있을 때 가장 정확 |
전체 평가 파이프라인
1
2
3
4
5
6
7
8
9
10
11
12
13
| 생성 시스템 평가
↓
[기초 평가]
ROUGE / BLEU → 빠른 참고용 (의미 한계 인지)
↓
[LLM 평가]
├─ 정답 있음 → Reference-based Judge
├─ 비교 필요 → Pairwise Judge (+ Swap으로 Bias 제거)
└─ 단일 평가 → Pointwise Judge with Rubric
↓
[통계 검증]
A/B test (t-test) + Cohen's d
→ p-value < 0.05 AND d > 0.5 → 신뢰할 수 있는 성능 향상
|
끝