feat: prepare summary (No.7) section in certificate format
요약 항목도 같은 서식으로 준비한다. 정답셋이 없어 수치는 채울 수 없으므로
서식과 시현 절차만 확정하고, 결과 자리는 자리표시자로 둔다.
eval_rouge.py: 성적서 '4. 결과' 형식 JSON 출력(--out 으로 저장). 보고 대상이
ROUGE-1 recall 임을 reported_metric 으로 명시한다. 계획서 수식 분모가 참조
n-gram 수인데 전 차수 코드는 f1 을 반환해 정의가 어긋나 있었다.
show_summary_samples.py: 전 차수 '1. 데이터 준비' 의 {index, original} 형식
출력. 정답셋이 없으면 그 사실과 참조 문서를 알리고 종료한다.
시험방법 수정 두 곳 — 1,000건에서 300건으로(사람 작성 정답이 필요해 1,000건은
구축 비용 과다), 지표를 ROUGE-1 recall 로 명시.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
99f0ac9b88
commit
6308c6b452
@ -3,7 +3,7 @@
|
||||
> 작성: 에이아이오투오 / 2026-09-16
|
||||
> 서식: 구미전자정보기술원 `성능평가확인서_출판과제_1029` 동일 구성
|
||||
> 선행 시험: `GERIR.CE.2511-02.009` (2025-11-19, 3개 항목 PASS)
|
||||
> **범위: 요약 성능(성능지표 #7) 제외 — 2개 항목**
|
||||
> **범위: 3개 항목.** 요약 성능(#7)은 정답셋 미구축으로 서식과 절차만 확정한 상태다.
|
||||
>
|
||||
> 성적서 서식 그대로 옮겨 적을 수 있도록 구성했다. 각 절의 번호·표 구성은
|
||||
> 원본 서식과 일치하며, 수치와 데이터만 2-1년차 측정값으로 교체하였다.
|
||||
@ -18,7 +18,7 @@
|
||||
| 주소 | (13453) 경기도 성남시 수정구 금토로 32 ㈜KT 판교빌딩 East 504호 ~ 505호 |
|
||||
| 시험품 | 출판콘텐츠 분석 및 공유 기술용 AI 모델 |
|
||||
| 성적서 용도 | 출판환경 변화 대응을 위한 생성형 AI 기반 출판 콘텐츠 분석 및 공유 플랫폼 기술 개발과제 성능 평가 |
|
||||
| 시험 항목 | **2개 항목** |
|
||||
| 시험 항목 | **3개 항목** |
|
||||
| 시험 장소 | 에이아이오투오 판교 연구소 |
|
||||
| 시험 결과 | 붙임(시험결과) 참조 |
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
### 2.1 시험 규격
|
||||
|
||||
- **2.1.1 시험규격** : 시험절차서 기준
|
||||
- **2.1.2 시험항목** : 시험절차서 **2개 항목**
|
||||
- **2.1.2 시험항목** : 시험절차서 **3개 항목**
|
||||
|
||||
### 2.2 시험 장비 정보
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
| 구분 | 시스템 사양 | 운영 SW 및 시험도구 | 구현 모듈 |
|
||||
|---|---|---|---|
|
||||
| 시험 PC | CPU : Apple M3 pro<br>RAM : 18GB | SSH 터미널 | 없음 |
|
||||
| GPU서버 | GPU : RTX 3090 24G × 2<br>CPU : Intel i9-12900KS (24core)<br>RAM : 125GB | Ubuntu 24.0.2<br>Pytorch 2.8<br>Python 3.9<br>Cuda 12.2 | 메타 데이터 추출 모듈,<br>표절 여부 판별 모듈 |
|
||||
| GPU서버 | GPU : RTX 3090 24G × 2<br>CPU : Intel i9-12900KS (24core)<br>RAM : 125GB | Ubuntu 24.0.2<br>Pytorch 2.8<br>Python 3.9<br>Cuda 12.2 | 메타 데이터 추출 모듈,<br>표절 여부 판별 모듈,<br>요약 모듈 |
|
||||
|
||||
#### 2.2.3 평가방법
|
||||
|
||||
@ -78,6 +78,7 @@
|
||||
|---|---|---|---|
|
||||
| 1 | 메타 데이터 추출<br>(F1 – score) | KLUE 데이터셋의 NER 데이터셋을 활용하여, 21,008개의 학습 데이터로 개발된 모델을 학습시키고, 5,000개의 테스트 데이터를 활용하여 개발된 모델에 대한 상대 평가 진행 | 과제 성능지표 3번에 해당 |
|
||||
| 2 | 표절 여부 판별 정밀도<br>(precision) | 자체 제작된 실제 표절 글과, 표절이 아닌 글을 Classification하여, precision 계산<br>(시험 데이터 1,000건 = 표절 500건 + 비표절 500건. 모델이 학습하지 않은 작성자의 자료로 구성) | 과제 성능지표 4번에 해당 |
|
||||
| 3 | 요약 성능<br>(N-gram ROUGE score) | 자체 제작된 요약 데이터셋 300건을, 요약 모델에 테스트하여 N-gram ROUGE score(ROUGE-1 recall) 계산 | 과제 성능지표 7번에 해당 |
|
||||
|
||||
---
|
||||
|
||||
@ -89,6 +90,7 @@
|
||||
|---|---|---|---|
|
||||
| 메타 데이터 추출(F1-score) | 83% | **83.77** | 83.77% 달성 |
|
||||
| 표절 여부 판별 정밀도(Precision) | 97% | **98.40** | 98.40% 달성 |
|
||||
| 요약 성능 (N-gram ROUGE score) | 65% | (미측정) | 정답셋 구축 후 측정 |
|
||||
|
||||
### 3.2 항목별 시험결과
|
||||
|
||||
@ -590,6 +592,174 @@ f1 = 2 * precision * recall / (precision + recall) if (precision + recall
|
||||
실제 운영 조건을 반영하고 있음을 확인하였다. 완전복제·문장재배열·축약은
|
||||
미탐지 0건이며, 어휘치환 150건 중 6건만 놓쳤다.
|
||||
|
||||
---
|
||||
|
||||
## 3.2.3 요약 성능
|
||||
|
||||
> **미측정 항목** — 요약 정답셋이 구축되지 않아 결과를 채울 수 없다.
|
||||
> 아래는 서식과 시현 절차를 확정해 둔 것이며, 정답셋 수령 후 같은 명령으로
|
||||
> 수치가 채워진다. 접수 전 결정이 필요한 사항은 3.2.3.4 항에 정리했다.
|
||||
|
||||
### 3.2.3.1 시험방법
|
||||
|
||||
```
|
||||
① 자체 제작된 요약 데이터셋 준비
|
||||
② 요약 모델에 학습되지 않은 데이터 300건에 대해 모델 요약 진행
|
||||
③ 생성된 결과물에 대한 N-gram ROUGE score(ROUGE-1 recall) 측정
|
||||
```
|
||||
|
||||
> **전 차수 대비 수정 두 곳**
|
||||
> - ② **1,000건 → 300건**. 사람이 작성한 요약 정답이 필요한 항목이라 1,000건은
|
||||
> 구축 비용이 과다하다. 통계적 신뢰구간을 확보하면서 현실적인 규모로 300건을 잡는다.
|
||||
> - ③ **지표 정의 명시**. 계획서 p.24 수식의 분모가 참조 n-gram 수이므로
|
||||
> 해당 지표는 **recall** 이다. 전 차수 코드는 f1 을 반환했다. 어느 값을
|
||||
> 보고하는지 성적서에 남긴다.
|
||||
|
||||
### 3.2.3.2 시험결과
|
||||
|
||||
| 시험항목 | 목표치 | 결과 | 비고 |
|
||||
|---|---|---|---|
|
||||
| 요약 성능 (N-gram ROUGE score) | 65% | (미측정) | 정답셋 구축 후 측정 |
|
||||
|
||||
### 3.2.3.3 성능 평가 과정
|
||||
|
||||
#### 1. 데이터 준비
|
||||
|
||||
```bash
|
||||
docker run --rm -v $PWD:/app -w /app o2o-plagia-eval:py39 \
|
||||
python scripts/show_summary_samples.py data/eval/summary.jsonl \
|
||||
--head 2 --tail 2 --max-chars 300 --with-reference
|
||||
```
|
||||
|
||||
출력 형식:
|
||||
|
||||
```json
|
||||
1. 데이터 준비
|
||||
{
|
||||
"index": 1,
|
||||
"original": "(원문 …)",
|
||||
"reference": "(사람 작성 요약 정답 …)",
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"original": "(원문 …)",
|
||||
"reference": "(사람 작성 요약 정답 …)",
|
||||
},
|
||||
...
|
||||
{
|
||||
"index": 300,
|
||||
"original": "(원문 …)",
|
||||
"reference": "(사람 작성 요약 정답 …)",
|
||||
},
|
||||
|
||||
총 데이터 300건
|
||||
```
|
||||
|
||||
#### 2. 요약 생성
|
||||
|
||||
```python
|
||||
# scripts/eval_rouge.py — system 요약이 없으면 자체 Summarizer 로 생성한다.
|
||||
summarizer = get_summarizer()
|
||||
|
||||
pairs = []
|
||||
for row in rows:
|
||||
# references(복수) 우선, 없으면 reference(단수)
|
||||
raw = row.get("references") or row.get("reference", "")
|
||||
reference = [raw] if isinstance(raw, str) else [r for r in raw if r and r.strip()]
|
||||
if not reference:
|
||||
continue
|
||||
if "system" in row and row["system"]:
|
||||
system = row["system"]
|
||||
else:
|
||||
system = summarizer.summarize(row.get("text", ""), ratio=args.ratio).final
|
||||
pairs.append((system, reference))
|
||||
```
|
||||
|
||||
#### 3. n-gram ROUGE 스코어 측정
|
||||
|
||||
```python
|
||||
# app/engine/rouge.py
|
||||
def _prf(match: int, sys_total: int, ref_total: int) -> RougeScore:
|
||||
precision = match / sys_total if sys_total else 0.0
|
||||
recall = match / ref_total if ref_total else 0.0
|
||||
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
|
||||
return RougeScore(precision, recall, f1)
|
||||
|
||||
|
||||
def evaluate_pairs(pairs, mode: str = "lemma") -> dict:
|
||||
"""(system, reference) 페어 → 코퍼스 평균 ROUGE-1/2/L.
|
||||
|
||||
**목표와 대조하는 값은 recall** 이다(계획서 수식의 분모가 참조 n-gram 수).
|
||||
"""
|
||||
acc = {"rouge1": [], "rouge2": [], "rougeL": []}
|
||||
for system, reference in pairs:
|
||||
acc["rouge1"].append(rouge_n(system, reference, 1, mode))
|
||||
acc["rouge2"].append(rouge_n(system, reference, 2, mode))
|
||||
acc["rougeL"].append(rouge_l(system, reference, mode))
|
||||
...
|
||||
```
|
||||
|
||||
실행:
|
||||
|
||||
```bash
|
||||
docker run --rm -v $PWD:/app -w /app o2o-plagia-eval:py39 \
|
||||
python scripts/eval_rouge.py data/eval/summary.jsonl \
|
||||
--out data/eval/summary_scorecard.json
|
||||
```
|
||||
|
||||
#### 4. 결과
|
||||
|
||||
```json
|
||||
{
|
||||
"summary_performance": {
|
||||
"model": "O2O Summarizer",
|
||||
"test_dataset": {
|
||||
"total_samples": 300,
|
||||
"multi_reference_samples": 0,
|
||||
"source": "data/eval/summary.jsonl"
|
||||
},
|
||||
"rouge_scores": {
|
||||
"rouge1": { "precision": 0.0, "recall": 0.0, "f1": 0.0 },
|
||||
"rouge2": { "precision": 0.0, "recall": 0.0, "f1": 0.0 },
|
||||
"rougeL": { "precision": 0.0, "recall": 0.0, "f1": 0.0 }
|
||||
},
|
||||
"reported_metric": "rouge1_recall",
|
||||
"performance_metrics": {
|
||||
"n_gram_rouge_score": 0.0,
|
||||
"target": 0.65,
|
||||
"achieved": false
|
||||
},
|
||||
"settings": { "tokenization": "lemma", "summary_ratio": 0.3 },
|
||||
"interpretation": {
|
||||
"n_gram_rouge_score": "참조 요약의 1-gram 중 0.0% 를 시스템 요약이 담아냄",
|
||||
"note": "계획서 수식 분모가 참조 n-gram 수이므로 recall 을 보고한다."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
최종 결과 : 평균 수치 __%
|
||||
```
|
||||
|
||||
> 위 JSON 은 **형식 예시**이며 수치는 전부 자리표시자다. 정답셋 투입 후
|
||||
> 실제 값으로 대체한다.
|
||||
|
||||
### 3.2.3.4 접수 전 결정 필요 사항
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| **정답셋 미구축** | 파일럿 20건·본 구축 300건 모두 미완. 이 항목의 유일한 선행 조건이다 |
|
||||
| **사람 상한 확인** | 본 구축 전 파일럿 20건으로 사람 간 일치도(IAA)를 측정해 상한을 먼저 본다(`eval_rouge.py --iaa`). 상한이 65 미만이면 300건 구축 전에 규격 조정을 협의해야 한다 |
|
||||
| **건수 변경** | 계획서·전 차수는 1,000건. 300건으로 줄이려면 시험기관 협의 필요 |
|
||||
| **지표 정의** | 계획서는 recall 수식, 전 차수 코드는 f1 반환. 정의를 확정해야 한다 |
|
||||
| **전 차수 방식 승계 불가** | 전 차수 로그에 `optimal_60_modification` / `success_rate 0.15` 기록이 있다. 목표치에 맞춰 요약문을 수정한 것으로 보여 그대로 승계하기 어렵다 |
|
||||
| **시험 환경** | 계획서가 No.7 에 A100 을 명시. 본 서버(RTX 3090)로 가능한지 별도 협의 필요 |
|
||||
|
||||
> **내부 A/B 벤치는 성적서용이 아니다** — `reports/summary_bench.json`,
|
||||
> `summary_bench_v2.json` 은 참조가 gpt-4o 산출물(은 기준)이고 30건 규모다.
|
||||
> 파일 자체에 "성적서용이 아님" 이 기록돼 있다. 사람 작성 정답셋과는 다르다.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
|
||||
@ -115,11 +115,53 @@ def _run_iaa(rows: list[dict], mode: str) -> None:
|
||||
print("여유가 충분합니다. 이 규격으로 본 구축을 진행해도 됩니다.")
|
||||
|
||||
|
||||
def build_scorecard(scores: dict, count: int, mode: str, ratio: float,
|
||||
multi_ref_rows: int, dataset: str | None) -> dict:
|
||||
"""성적서 "4. 결과" 게재 형식으로 정리한다.
|
||||
|
||||
보고 대상은 **ROUGE-1 recall** 이다. 계획서 p.24 수식의 분모가 참조 n-gram
|
||||
수이므로 recall 이 지표에 해당한다. f1 은 참고로만 남긴다. 전 차수 코드가
|
||||
f1 을 반환한 탓에 정의가 어긋나 있어, 어느 값을 보고하는지 명시한다.
|
||||
"""
|
||||
r1 = scores["rouge1"]
|
||||
target = 0.65
|
||||
return {
|
||||
"summary_performance": {
|
||||
"model": "O2O Summarizer",
|
||||
"test_dataset": {
|
||||
"total_samples": count,
|
||||
"multi_reference_samples": multi_ref_rows,
|
||||
"source": dataset or "(내장 dry-run 샘플)",
|
||||
},
|
||||
"rouge_scores": {
|
||||
"rouge1": scores["rouge1"],
|
||||
"rouge2": scores["rouge2"],
|
||||
"rougeL": scores["rougeL"],
|
||||
},
|
||||
"reported_metric": "rouge1_recall",
|
||||
"performance_metrics": {
|
||||
"n_gram_rouge_score": r1["recall"],
|
||||
"target": target,
|
||||
"achieved": r1["recall"] >= target,
|
||||
},
|
||||
"settings": {"tokenization": mode, "summary_ratio": ratio},
|
||||
"interpretation": {
|
||||
"n_gram_rouge_score":
|
||||
"참조 요약의 1-gram 중 %.1f%% 를 시스템 요약이 담아냄"
|
||||
% (r1["recall"] * 100),
|
||||
"note": "계획서 수식 분모가 참조 n-gram 수이므로 recall 을 보고한다. "
|
||||
"ROUGE-1 F1 %.4f 는 계획서 지표가 아니다." % r1["f1"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="요약 ROUGE 평가 (성능지표 No.7)")
|
||||
ap.add_argument("dataset", nargs="?", default=None, help="JSONL 정답셋 (없으면 dry-run)")
|
||||
ap.add_argument("--mode", choices=["lemma", "char"], default="lemma", help="토큰화 방식")
|
||||
ap.add_argument("--ratio", type=float, default=0.3, help="자체 요약 길이 비율")
|
||||
ap.add_argument("--out", help="scorecard JSON 저장 경로")
|
||||
ap.add_argument("--iaa", action="store_true",
|
||||
help="사람 상한 측정 — 참조 2개 이상인 행에서 작성자끼리 채점")
|
||||
args = ap.parse_args()
|
||||
@ -169,8 +211,20 @@ def main() -> None:
|
||||
print(f" (참고: ROUGE-1 F1 = {scores['rouge1']['f1']:.4f} — 계획서 지표 아님)")
|
||||
if multi_ref_rows:
|
||||
print(f" 다중 참조 사용: {multi_ref_rows}/{len(pairs)}건")
|
||||
scorecard = build_scorecard(scores, len(pairs), args.mode, args.ratio,
|
||||
multi_ref_rows, args.dataset)
|
||||
rendered = json.dumps(scorecard, ensure_ascii=False, indent=2)
|
||||
print("\n" + "=" * 62)
|
||||
print("4. 결과")
|
||||
print()
|
||||
print(rendered)
|
||||
print("\n최종 결과 : 평균 수치 %.2f%%" % (r1 * 100))
|
||||
if args.out:
|
||||
Path(args.out).write_text(rendered + "\n", encoding="utf-8")
|
||||
print("\n%s 에 결과 기록" % args.out)
|
||||
|
||||
if is_dry:
|
||||
print("\n※ 이는 파이프라인 검증용 dry-run 수치입니다. 컴북스 요약 정답셋 수령 후 본 평가로 정식 측정.")
|
||||
print("\n※ 이는 파이프라인 검증용 dry-run 수치입니다. 요약 정답셋 수령 후 본 평가로 정식 측정.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
95
scripts/show_summary_samples.py
Normal file
95
scripts/show_summary_samples.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""성능지표 #7 요약 정답셋 예시를 성적서 형식으로 출력한다.
|
||||
|
||||
전 차수 성적서(GERI `GERIR.CE.2511-02.009`) 3.2.3.3 "1. 데이터 준비" 는
|
||||
원문을 `{index, original}` 형태로 앞뒤 몇 건만 싣고 중간을 `...` 로 생략한 뒤
|
||||
총 건수를 적는다. 그 형식으로 낸다.
|
||||
|
||||
입력은 `scripts/eval_rouge.py` 와 같은 JSONL 이다.
|
||||
{"text": "원문 ...", "reference": "사람 작성 요약 ..."}
|
||||
{"text": "원문 ...", "references": ["정답 1 ...", "정답 2 ..."]}
|
||||
|
||||
`--with-reference` 를 주면 정답 요약도 함께 싣는다.
|
||||
|
||||
사용:
|
||||
python scripts/show_summary_samples.py data/eval/summary.jsonl
|
||||
python scripts/show_summary_samples.py data/eval/summary.jsonl --head 2 --tail 2
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def record(index: int, row: dict, max_chars: int | None,
|
||||
with_reference: bool) -> list[str]:
|
||||
def clean(value: str) -> str:
|
||||
body = " ".join(value.split())
|
||||
if max_chars and len(body) > max_chars:
|
||||
body = body[:max_chars] + " …"
|
||||
return body
|
||||
|
||||
lines = [" {", ' "index": %d,' % index,
|
||||
' "original": "%s",' % clean(row.get("text", ""))]
|
||||
if with_reference:
|
||||
raw = row.get("references") or row.get("reference", "")
|
||||
references = [raw] if isinstance(raw, str) else list(raw)
|
||||
if len(references) == 1:
|
||||
lines.append(' "reference": "%s",' % clean(references[0]))
|
||||
else:
|
||||
lines.append(' "references": [')
|
||||
for n, reference in enumerate(references):
|
||||
comma = "," if n < len(references) - 1 else ""
|
||||
lines.append(' "%s"%s' % (clean(reference), comma))
|
||||
lines.append(" ],")
|
||||
lines.append(" },")
|
||||
return lines
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("dataset", type=Path, help="JSONL 정답셋")
|
||||
parser.add_argument("--head", type=int, default=2, help="앞에서 N건")
|
||||
parser.add_argument("--tail", type=int, default=2, help="뒤에서 N건")
|
||||
parser.add_argument("--max-chars", type=int, default=None,
|
||||
help="본문 길이 제한 (미지정 시 전문)")
|
||||
parser.add_argument("--with-reference", action="store_true",
|
||||
help="정답 요약도 함께 출력")
|
||||
parser.add_argument("--out", help="파일로 저장")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.dataset.exists():
|
||||
raise SystemExit(
|
||||
"정답셋이 없습니다: %s\n"
|
||||
"성능지표 #7 은 요약 정답셋 구축 후 측정 가능하다. "
|
||||
"docs/TEST_PLAN_2026_PHASE2.md 부록 E 참조." % args.dataset)
|
||||
|
||||
rows = [json.loads(line) for line
|
||||
in args.dataset.read_text(encoding="utf-8").splitlines() if line.strip()]
|
||||
total = len(rows)
|
||||
|
||||
indices = list(range(min(args.head, total)))
|
||||
tail_start = max(len(indices), total - args.tail)
|
||||
indices += list(range(tail_start, total))
|
||||
|
||||
lines = ["1. 데이터 준비"]
|
||||
previous = -1
|
||||
for i in indices:
|
||||
if previous >= 0 and i != previous + 1:
|
||||
lines.append("...")
|
||||
lines.extend(record(i + 1, rows[i], args.max_chars, args.with_reference))
|
||||
previous = i
|
||||
lines.append("")
|
||||
lines.append("총 데이터 %d건" % total)
|
||||
|
||||
text = "\n".join(lines)
|
||||
if args.out:
|
||||
Path(args.out).write_text(text + "\n", encoding="utf-8")
|
||||
print("%s 에 기록" % args.out)
|
||||
else:
|
||||
print(text)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in New Issue
Block a user