계획서 p.24 수식의 분모가 참조 n-gram 수이므로 지표는 recall 인데
eval_rouge.py 는 ROUGE-1 F1 을 목표 0.65 와 대조하고 있었다. 통상 시스템
요약이 참조보다 길면 recall > F1 이므로 우리에게 불리한 자체 기준으로
채점해 온 셈이다(내장 샘플에서 recall 0.5778 vs F1 0.5539).
같은 수식이 Σ_S∈{Reference Summaries} 로 다중 참조를 전제하는데 rouge_n /
rouge_l 은 참조를 문자열 하나만 받았다. 이제 문자열도 리스트도 받는다.
참조가 1개면 기존과 완전히 같은 값이 나오며 이를 테스트로 고정했다.
--iaa 모드를 추가했다. 사람 둘이 같은 글을 요약해도 ROUGE 는 100 이 안
나오고, 그 상한이 65 보다 낮으면 어떤 시스템도 목표를 달성할 수 없다.
정답셋 300건을 만들기 전에 파일럿 20건으로 상한을 먼저 재기 위한 것이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""ROUGE 평가 모듈 단위테스트 (성능지표 No.7)."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.engine.rouge import rouge_n, rouge_l, evaluate_pairs, tokenize
|
|
|
|
|
|
def test_identical_is_perfect():
|
|
s = rouge_n("홍길동은 활빈당을 만들었다", "홍길동은 활빈당을 만들었다", n=1)
|
|
assert s.f1 == 1.0
|
|
assert s.recall == 1.0
|
|
|
|
|
|
def test_no_overlap_is_zero():
|
|
s = rouge_n("우주선이 행성을 탐사한다", "사랑은 아름다운 감정이다", n=1, mode="char")
|
|
assert s.f1 == 0.0
|
|
|
|
|
|
def test_partial_overlap_between_zero_and_one():
|
|
s = rouge_n("홍길동은 활빈당을 만들어 재물을 빼앗았다",
|
|
"홍길동은 활빈당을 조직했다", n=1)
|
|
assert 0.0 < s.f1 < 1.0
|
|
|
|
|
|
def test_rouge2_stricter_than_rouge1():
|
|
sys = "홍길동은 활빈당을 만들어 재물을 빼앗았다"
|
|
ref = "홍길동은 재물을 활빈당으로 만들어 빼앗았다" # 단어 순서 섞임
|
|
r1 = rouge_n(sys, ref, n=1).f1
|
|
r2 = rouge_n(sys, ref, n=2).f1
|
|
assert r2 <= r1 # bigram 은 순서에 민감 → 더 낮거나 같음
|
|
|
|
|
|
def test_rouge_l_rewards_sequence():
|
|
s = rouge_l("홍길동은 활빈당을 만들어 재물을 빼앗았다",
|
|
"홍길동은 활빈당을 만들어 재물을 빼앗았다")
|
|
assert s.f1 == 1.0
|
|
|
|
|
|
def test_evaluate_pairs_aggregates():
|
|
pairs = [
|
|
("홍길동은 활빈당을 만들었다", "홍길동은 활빈당을 만들었다"),
|
|
("어린왕자가 여우를 만났다", "어린왕자가 여우를 만났다"),
|
|
]
|
|
out = evaluate_pairs(pairs)
|
|
assert set(out) == {"rouge1", "rouge2", "rougeL"}
|
|
assert out["rouge1"]["f1"] == 1.0
|
|
|
|
|
|
def test_tokenize_char_fallback():
|
|
toks = tokenize("Hello 월드 123", mode="char")
|
|
assert "hello" in toks and "월드" in toks and "123" in toks
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 다중 참조 — 계획서 p.24 수식이 Σ_S∈{Reference Summaries} 로 전제하는 형태
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMultiReference:
|
|
SYS = "홍길동은 활빈당을 만들어 백성을 도왔다"
|
|
REF_A = "홍길동은 활빈당을 세워 백성을 구했다"
|
|
REF_B = "홍길동이 백성을 도왔다"
|
|
|
|
def test_single_string_and_single_item_list_are_identical(self):
|
|
"""하위 호환 — 기존 호출부의 값이 절대 바뀌면 안 된다."""
|
|
for n in (1, 2):
|
|
assert (rouge_n(self.SYS, self.REF_A, n, "char").as_dict()
|
|
== rouge_n(self.SYS, [self.REF_A], n, "char").as_dict())
|
|
assert (rouge_l(self.SYS, self.REF_A, "char").as_dict()
|
|
== rouge_l(self.SYS, [self.REF_A], "char").as_dict())
|
|
|
|
def test_recall_follows_plan_formula(self):
|
|
"""분자·분모를 참조별로 합산한 값과 일치해야 한다."""
|
|
from app.engine.rouge import _ngrams, tokenize
|
|
|
|
sys_g = _ngrams(tokenize(self.SYS, "char"), 1)
|
|
match = ref_total = 0
|
|
for ref in (self.REF_A, self.REF_B):
|
|
ref_g = _ngrams(tokenize(ref, "char"), 1)
|
|
match += sum((sys_g & ref_g).values())
|
|
ref_total += sum(ref_g.values())
|
|
|
|
got = rouge_n(self.SYS, [self.REF_A, self.REF_B], 1, "char")
|
|
assert got.recall == pytest.approx(match / ref_total)
|
|
|
|
def test_empty_reference_list_is_zero(self):
|
|
assert rouge_n(self.SYS, [], 1, "char").recall == 0.0
|
|
assert rouge_l(self.SYS, [], "char").recall == 0.0
|
|
|
|
def test_blank_references_are_dropped(self):
|
|
assert (rouge_n(self.SYS, [self.REF_A, "", " "], 1, "char").as_dict()
|
|
== rouge_n(self.SYS, [self.REF_A], 1, "char").as_dict())
|
|
|
|
def test_evaluate_pairs_accepts_mixed_forms(self):
|
|
scores = evaluate_pairs(
|
|
[(self.SYS, self.REF_A), (self.SYS, [self.REF_A, self.REF_B])], mode="char"
|
|
)
|
|
assert 0.0 < scores["rouge1"]["recall"] <= 1.0
|