chore: remove remaining project docs

docs 에 남아 있던 8개를 삭제한다. 내용은 git 이력(bbdda66)에 그대로 있으므로
필요하면 `git show bbdda66:docs/<파일>` 로 꺼낸다.

삭제: TEST_PLAN_2026_PHASE2, PERF_EVIDENCE_CAPTURE_2026, PRECISION_TEST_
PROCEDURE, SCOPE_AND_METRICS, CASE_MATCHING_KPI, IMPLEMENTATION_RUNBOOK,
PRECEDENT_LISTUP, COMBOOKS_CASE_MATCHING_REPLY_20260918.

삭제본을 출처로 가리키던 주석 4곳을 함께 정리했다(legal_risk.py,
build_precedent_listup_xlsx.py 2곳, show_summary_samples.py). 동작에는 영향이
없고 판례 표의 실제 원본은 data/precedents/precedent_listup.csv 와 해당
스크립트의 상수다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-09-28 10:04:10 +09:00
parent 8ae52b0a82
commit 8aac18c255
11 changed files with 4 additions and 2507 deletions

View File

@ -30,7 +30,7 @@ class Precedent:
outcome: str | None = None
#: 사람이 판시 본문을 확인하고 매긴 등급. A=직접 적용, B=조건부, C=참고.
#: None 은 "아직 검토되지 않음"이며 C(검토 후 부적합)와 다르다.
#: 출처: docs/PRECEDENT_LISTUP.md / data/precedents/precedent_listup.csv
#: 출처: data/precedents/precedent_listup.csv (서술본은 git 이력의 docs/PRECEDENT_LISTUP.md)
grade: str | None = None

View File

@ -1,48 +0,0 @@
# 케이스 매칭 정확도 — 합의안 (2026-09-18)
상태: 컴북스·바이칼 합의 전. 평가 코드와 목표치는 아직 확정하지 않는다.
이는 표절 검출 정밀도(성능지표 4)와 별도 지표다.
## 평가 단위와 기록
한 요청의 한 매칭 원천에 대한 관리자 확정을 1건으로 한다. 요청 전체에 여러
매칭이 있으면 원천별로 평가한다. 로그에는 request_id, doc_id, 원천 문서·세그먼트 ID,
engine_version, taxonomy_version, 분석 시각, 당시 자동 대표 코드, 순서 있는 전체 후보,
후보별 출간 판정, 관리자 최종 케이스 집합·출간 판정, 검토자·확정 시각·수정 사유를 남긴다.
재검사 요청과 재확정을 중복 분모로 세지 않도록 평가 대상 요청/매칭 ID를 고정하고,
집계 마감 시점의 마지막 확정만 사용한다. 원래 자동 예측은 덮어쓰지 않는다.
5년 보존·접근 제어·삭제 정책 구현은 바이칼 DB 담당이다.
## 제안 산식
- 평가 대상 N: 동일 taxonomy 버전에서 유효한 최종 케이스 집합 G가 확정된 매칭 수.
미확정·보류·버전 변환 불가 로그는 제외하고 제외 사유별 건수를 별도 보고한다.
- top-1 일치율 = 자동 대표 코드가 G에 포함된 건수 / N.
현 case_id는 태그 동점군의 첫 항목이지 확률 순위 1위가 아니다. 동점 여부와
후보 수별 성적을 함께 보고하고, 이를 모델의 순위 정확도로 해석하지 않는다.
- 후보군 포함률(hit@k) = 상위 k개 후보 집합 Ck와 G의 교집합이 비어 있지 않은 건수 / N.
단일 정답에서는 recall@k와 같다. 복수 정답에서는 별도로
macro recall@k = sum(|Ck ∩ G| / |G|) / N을 보고한다.
- 전체 후보 포함률도 별도 보고한다. 후보 동률을 임의 절단한 k 결과에는 절단 사실을 표시한다.
- 후보 누락/기권은 G가 존재하는 한 분모에 포함하고 실패로 센다.
관리자가 '해당 케이스 없음'으로 확정한 로그는 별도 음성 집단으로 두고
무후보 정확률 및 잘못 부착한 비율을 보고한다(양성 분모와 섞지 않음).
- 출간 판정 일치율은 코드표·보수적 우선순위 합의 후 별도 측정한다.
미확보 null을 출간 허용이나 정답으로 취급하지 않는다.
- N=0은 null/측정 불가. 모든 지표에 분자·분모, 평가 기간, 데이터/엔진/분류체계
버전, 미확정 수, 후보 수 분포를 함께 기재한다.
확정할 사항: 단위(원천별/요청별), 복수 정답 허용, 음성 정의, 동점 처리, k 값,
버전 간 코드 대응, 관리자 간 불일치 조정, 대표 판정 우선순위. 97% 목표를 전용하지 않는다.
## 기존 정밀도와 구분
운영 코퍼스의 71.7%는 회의 자료상 76/(76+30)=76/106이다. 7,786은 검사 모집단이며
정밀도의 분모가 아니다. 현 코퍼스 검출 수만으로 TP/FP를 만들 수 없다.
전체 새 검출 결과의 동일 기준 사람 검토와 문서/쌍 단위 합의가 필요하다.
별도 평가셋 성능지표 4는 기존 scripts/run_precision_eval.py의
precision=TP/(TP+FP), recall=TP/(TP+FN), F1을 그대로 사용한다.
시험셋은 scripts/build_plagiarism_testset.py로 만든 작성자 분리·규칙 변형 평가셋이다.
운영 표본의 사람 판정 수치와 공인인증 목표를 같은 표본의 성적으로 비교하지 않는다.
기존 docs/PERF_EVIDENCE_CAPTURE_2026.md의 산식과 평가 경로를 변경하지 않는다.

View File

@ -1,99 +0,0 @@
# 컴북스 월례회의 후속 회신 초안 — 2026-09-18
외부 발송 전 초안. 브리핑 CASE_MATCHING_BRIEF.md 전체와 저장소를 대조했다.
52a0fdc의 동점 후보 반환·대표판례 적재·태그 확장은 재작업하지 않았다.
## 1. 우선 재검사
King 52a0fdc 운영 엔진의 연속 일치 필수 게이트(true), 최소 35자 설정을 확인했다.
현재 corpus.sqlite3의 검색 세그먼트는 8,025개지만 기존 배치의 검사 대상은
6,343건이다. 회의 자료 7,786편과 동일한 모집단이 아니므로 직접 전후 비교 불가다.
현재 스냅샷으로 별도 재검사를 시작했으나 모집단 불일치를 확인하고 중단했다.
부분 실행은 성적으로 집계하지 않는다. 6,343건은 자서전 6,331건 + 생활수기 12건이다.
기존 106건의 정답 라벨 및 새 검출분의 검토 없이 오탐 수/정밀도를 추정하지 않는다.
## 2. 요청 자료 — 수령 전 데이터 변경 보류
| 요청 | 목적/확인 조건 |
|---|---|
| 아카이빙 DB v2.3 엑셀 9시트 및 매핑표/구축보고서 v2.2 | A그룹 28번째 정의의 코드·제목·태그·처리·대표판례 확인. A28이라고 가정하지 않음 |
| 출간 판정 9종 코드표, 라벨확정본 v6, 40건별 대응표 | 코드/뜻/출처, 동점 후보의 보수적 우선순위 및 동률·미확정 처리 합의 |
| 확보 판례 132건의 원문/출처 URL, 중복 31건과 무관 4건의 제외 명세 | 545+132−31−4=642 대조, 신규 97건 식별. URL 없는 건 적재 제외 |
| 원래 검사한 7,786편의 ID 목록·스냅샷·자기매칭 제외 규칙 | 현재 6,343건과 차이 확인, 동일 조건 재검사 |
| 기존 106건의 76/30 사람 판정과 정정본 v1.1, 라벨 v6의 행 ID | 새 검사 결과와 연결해 TP/FP 실측. '0자 22건'과 코드 주석 '29건' 차이 확인 |
| 관리자 최종 확정 로그 스키마 및 케이스/출간 판정 합의 | CASE_MATCHING_KPI.md 산식 확정 |
현재 39건(A 27건)과 판례 545건을 유지한다. 39/27 고정 테스트를 느슨하게 바꾸지 않는다.
대표판례로 지정됐으나 운영 적재본 545건에 없어 출처를 제시할 수 없는 14건은 아래와 같다.
신규 97건에 포함된다고 단정하지 않는다. 요청 내용은 판결문 원문 또는 검증 가능한 공식 출처 URL이다.
| 사건번호 | 영향받는 침해 케이스 |
|---|---|
| 2006가합8583 | A21 |
| 2006나16757 | A6, A7, B2 |
| 2007가합16095 | A17, A18, A19 |
| 2007가합43936 | A16 |
| 2007다354 | A19, A3 |
| 2008다53812 | B3 |
| 2010도4468 | A8, A9 |
| 2012도13718 | X2 |
| 2013나2004096 | A23, E2 |
| 2016고정432 | A14 |
| 2017도19025 | B3 |
| 2019가단31377 | A26 |
| 2019가단5207564 | A17 |
| 2021가합588060 | A1 |
## 3. 전용 방지 장치 공백 7건에 대한 기술 회신
| 케이스 | 현 엔진 근거와 범위 | 요청/사람 확인 |
|---|---|---|
| A6 유명인 자서전 베끼기 | ●, 태그 후보 도달 가능 | 2006나16757 출처 필요; 원저작물 종류 확인 |
| A15 교과서·교재 수록 | ●, 태그 후보 도달 가능 | 교재 여부·권리/이용허락 확인 |
| A16 외국 도서 번역 수록 | ●, 태그 후보 도달 가능 | 2007가합43936 출처 필요; 번역·원본 관계 확인 |
| A13 강연·설교 녹음 | ○, 현 태그 경로 미도달 | 녹음 대상/동의/이용 사실, 텍스트화 및 사실 입력 필요 |
| A22 단톡방 캡처 | ○, 현 태그 경로 미도달 | 대화 공개 범위·동의·권리자·사용 사실 입력 필요 |
| B4 AI 학습 무단 수집 | ○, 현 태그 경로 미도달 | 실제 수집/학습·권리 관계 증빙 필요 |
| E2 사후 미공표 일기 | ○, 현 태그 경로 미도달 | 사망·미공표·유족/권리 관계 확인 필요 |
A6·A15·A16은 '기술 후보 검출 공백' 목록에서 제외 요청한다. 후보 도달은 전용 탐지기의
정확도나 법적 침해 확정을 증명하지 않는다. 나머지 네 건은 사람 확인이 전제다.
현재 LegalContext는 접근·표현 검토·권리 확인만 지원하므로 위 상세 사실을 넣으면
자동 해결된다고 약속할 수 없다. 별도 사실 필드·규칙·근거 라벨 합의 후 구현한다.
## 4. API 변경과 남은 연동
options.audience는 admin(기본, 기존 응답 유지)/author다. author 직렬화는 케이스 코드·후보·
태그·판례/법률판단을 제외하고 일치 위치·점수·검출 근거를 제공한다.
자유형 ccl_basis는 코드 없는 '확인이 필요한 부분' 문장으로 대체한다.
detect 응답 및 배치 항목마다 request_id, engine_version, taxonomy_version, analyzed_at을 제공한다.
경량 review 경로도 author 옵션에서는 판례 ID와 자유형 법률 요약을 제거한다.
이 옵션은 표시용이며 인증/관리자 권한 확인을 대신하지 않는다. 바이칼 서버에서 사용자 역할에
따라 옵션을 설정하고 관리자용 원본 응답을 저자에게 전달하지 않아야 한다.
39개 케이스와 후보별 publication_verdict 필드를 추가했으나 원본 미확보로 모두 null이다.
대표 publication_verdict도 null, 상태는 source_pending이다. 추후 일부 후보에 코드가
들어와도 우선순위 합의 전에는 대표를 임의 선정하지 않고 review_required로 표시한다.
9종 코드 enum·보수적 대표값 자동 선정은 코드표 수령 후 구현할 미완료 항목이다.
분류체계 버전은 사실대로 cases_1.3을 유지한다. 5년 감사 로그 저장은 구현하지 않았다.
## 5. 별도 평가셋 재측정 완료
기존 testset_v2(표절 500 + 비표절 500건)를 별도 작업 경로로 복사하고
기존 run_precision_eval.py로 새 인덱스를 구축해 전체 재측정했다.
TP 493 / FP 8 / TN 492 / FN 7, precision **98.4032%**, recall **98.60%**다.
이 결과는 기존 testset_v2 결과와 동일하다. 새로운 독립 시험셋이나 공인인증 성적은 아니다.
76/106 운영 정밀도를 대체하지 않으며 7,786편 재검증은 여전히 자료 대기다.
- 성적서: [CASE_MATCHING_PRECISION_SCORECARD_20260918.json](../reports/CASE_MATCHING_PRECISION_SCORECARD_20260918.json)
- 입력 해시·환경·혼동행렬·검사 상태: [CASE_MATCHING_EVAL_20260918.json](../reports/CASE_MATCHING_EVAL_20260918.json)
- 연동 계약: [API_SPEC_BAIKAL.md 7절](API_SPEC_BAIKAL.md)
- 합의할 KPI: [CASE_MATCHING_KPI.md](CASE_MATCHING_KPI.md)
평가 프로세스 시작 시 코드 기본 버전 문자열은 2.0.0-pdf-v1.2였으므로 성적서도 해당
값을 그대로 보존했다. 이번 응답 계약 변경의 새 코드 기본값은 2.2.1-cases-v1.3이며
검출 게이트·점수·케이스 매칭 알고리즘에는 변경이 없다.
코드 기본값은 King 운영값(.env `ENGINE_VERSION=o2o-plagiarism-2.2.0-persistent-cpu`)보다
낮은 번호였다. 성적서·로그 대조 시 운영 버전과 뒤집혀 보이지 않도록 2.2.1 로 맞췄다.
관련 회귀 테스트 57개 통과. 변경 파일의 diff 공백 검사 통과.
별도 요약 작업 파일은 수정하지 않았으며, King 서비스의 코드/이미지는 배포하지 않았다.

View File

@ -1,214 +0,0 @@
# O2O 표절·AI 의심도·판례 위험도 운영 런북
## 안전한 제품 경계
세 결과는 서로 다른 증거를 사용하며 합쳐서 하나의 `침해 확정` 값으로 만들지 않는다.
1. **유사구간 검색**: 현재 등록 코퍼스 안에서 발견된 후보와 원문 위치
2. **AI 생성 의심도**: 원문 문체 특징에 대한 검토 우선순위(확정 판정 금지)
3. **법적 위험도**: 등록 판례와 판단 요소에 대한 검토 보조(법률 자문 아님)
후방 호환을 위해 `is_infringement` 필드는 유지하지만 실제 의미는 임계값을 넘은
`has_similarity_match`와 같다. 신규 연동은 `has_similarity_match`,
`corpus_scope_note`, `legal_risk`를 사용한다.
## 수령 데이터 실사 결과
- XLSX 본 데이터 34,105행, 원천 도서 79권
- 고유 에피소드 31,560개, 같은 책 내부 중복 추가 행 2,545개
- 고유 본문 약 2,946만 자
- 페이지/문단 원문 좌표 없음: XLSX 적재 결과는 `coordinate_scope=episode`
## King 서버 데이터 적재
원고 데이터와 학습 산출물은 Git에 커밋하지 않는다. `/app/data` Docker 볼륨 아래의
`runtime`, `models`, `input`을 사용한다.
```bash
python -m scripts.ingest_o2o_xlsx \
data/input/o2o_episodes.xlsx \
--database data/runtime/corpus.sqlite3
python -m scripts.build_persistent_index \
--database data/runtime/corpus.sqlite3 \
--index-dir data/runtime/index
```
같은 DB에 신규 세그먼트만 추가한 뒤 `build_persistent_index`를 다시 실행하면 결과의
`mode`가 `append`이며 기존 행을 다시 벡터화하지 않는다. 삭제 또는 본문 변경이
감지된 경우에만 `rebuild`한다.
`.env`에서 다음을 설정하고 재기동한다.
```dotenv
USE_PERSISTENT_INDEX=true
CORPUS_DB_PATH=/app/data/runtime/corpus.sqlite3
PERSISTENT_INDEX_DIR=/app/data/runtime/index
PERSISTENT_SIMILARITY_THRESHOLD=0.65
# 바이칼과 키 전달을 합의한 뒤 활성화
API_KEY=<secret-manager-or-protected-env-value>
REQUIRE_API_KEY=true
```
`0.65`는 영속 문자 n-gram 후보 검색의 보수적인 시작값일 뿐 운영 확정값이 아니다.
삽입 복제 테스트와 79권 상호비교 오탐 분포를 측정한 뒤 버전별로 보정한다.
## PDF/DOCX 원문 위치 재구축
```bash
python -m scripts.extract_source_documents \
/mnt/data2/demo/ai_publish/data/combooks \
--database data/runtime/source_corpus.sqlite3 \
--chunk-size 1000 --stride 500
```
- PDF: 페이지 번호와 해당 페이지 추출 텍스트의 글자 offset 저장
- DOCX: 문단 번호와 문단 내부 글자 offset 저장
- 텍스트가 없는 PDF 페이지는 OCR 필요 경고
- 구형 `.doc`은 먼저 DOCX 또는 PDF로 변환 필요
96개 raw와 79개 처리 도서의 대응표를 검수한 후, 위치 정보가 더 정확한
`source_corpus.sqlite3`를 운영 코퍼스로 승격한다.
## AI 탐지 학습
XLSX의 `에피소드` 열은 원문이 사람 작성임이 계약·생성 이력으로 확인된 경우에만
human 라벨로 사용한다. AI 데이터는 모델·프롬프트·편집 유형별 provenance와
`source_group`을 가져야 한다. 학습/검증은 행이 아니라 book/source group으로 나눈다.
AI 모델 파일이 없거나 단일 클래스 데이터뿐이면 API는 실제 점수를 가장하지 않고
미학습 상태를 표시해야 한다. 완전 AI, 사람 편집 AI, AI 윤문, 혼합 문서를 각각
미학습 모델·미학습 도서로 평가한다.
## 판례 적재
`data/precedents/precedents.jsonl`에는 공식 HTTPS 출처와 사건번호가 확인된 레코드만
넣는다.
```bash
python -m scripts.validate_precedents data/precedents/precedents.jsonl
```
2026-08-18 기준 공식 상세 출처와 엔진 매칭 라벨이 확인된 국내 사건 545건이
등록되어 있다. 한국저작권위원회 전체 2,085건을 모두 적재한 것은 아니다. 자동
선별·라벨 결과이므로 재배포 허용 범위 확인과 저작권 전문가의 다음 항목 검수가 필요하다.
- 보호되는 표현 / 아이디어·사실·상투적 표현
- 의거관계 판단 근거
- 실질적 유사성 인정·부정 이유
- 저작물 유형과 결론
엔진은 등록된 사건번호만 반환하며 판례를 자유 생성하지 않는다. 적재본 545건 전체를
검색 후보로 사용하고, 저작물 유형·법적 태그·입력 및 증거 텍스트와 판시 내용의 어휘 관련성으로
재정렬한다. `precedent_listup.csv`의 A/B/C 등급은 사람 검토 수준을 나타내는 작은 보조
가중치이며 미등급 판례도 검색에서 제외하지 않는다. 등급을 적재본에 다시 반영할 때는
다음 명령을 실행한다.
```bash
python scripts/apply_precedent_grades.py --write
```
## 검증 게이트
- 데이터: 79권/31,560 고유 XLSX 세그먼트 적재 수 일치
- 증분성: 신규 문서 추가 후 index sync `mode=append`
- 검색: 100/200/300/500자 복사·삽입 세트의 Recall@20 기록
- 근거 위치: 반환한 query/source offset으로 원문 substring이 정확히 복원됨
- 오탐: 책 단위 분리 및 자서전 공통표현 hard-negative 검수
- AI: unseen book/model의 AUROC뿐 아니라 FPR, AUPRC, 혼합·편집 유형별 결과 기록
- 법적 위험도: 미등록 사건번호 0건, 빠진 법적 사실을 항상 명시
- API: CPU 작업 중 `/v1/health` event loop가 응답 가능
## 배포/롤백
1. 테스트 통과 및 Git SHA 기록
2. King에서 `git pull --ff-only`
3. 데이터 적재/인덱싱/AI 학습은 호스트 또는 일회성 Compose 컨테이너에서 실행
4. `docker compose up -d --build`
5. `/v1/health`, `/v1/plagiarism/detect`, 코퍼스 수, 모델 준비 상태 확인
6. 문제 시 이전 Git SHA의 이미지를 다시 빌드하되 `data/runtime`은 보존
Ubuntu 18.04는 지원 종료 상태이므로 OS 업그레이드 전까지 외부 공개 범위를 최소화하고,
API 인증·방화벽·키 회전을 별도 운영 작업으로 완료해야 한다.
## 선택적 KoSimCSE
기본 CPU 이미지는 새 영속 문자 인덱스를 사용하며 `torch`와 `sentence-transformers`를
포함하지 않는다. 일반 PyPI의 최신 torch가 CUDA 런타임 수 GB를 함께 설치할 수 있기
때문이다. 레거시 KoSimCSE가 반드시 필요한 별도 이미지에서만 해당 Python 버전에 맞는
공식 CPU 전용 torch wheel을 먼저 설치한 뒤 sentence-transformers를 추가한다.
## 인증 fail-closed (#1)
| 설정 | 기본 | 의미 |
|---|---|---|
| `API_KEY` | 빈 값 | 비어 있으면 **인증이 걸리지 않는다**. 기동 시 critical 로그가 남는다. |
| `REQUIRE_API_KEY` | `false` | `true` 인데 `API_KEY` 가 비면 **앱이 기동에 실패**한다(`AuthConfigurationError`). |
| `PUBLIC_HEALTH` | `true` | `/v1/health` 를 무인증 공개할지. 모니터링이 키를 못 넣으면 `true` 유지. |
| `PUBLIC_DOCS` | `true` | `/docs`, `/openapi.json`, `/redoc` 공개 여부. |
- **King 실제 활성화는 바이칼 측 키 전달 후로 보류**한다. 그때까지 `REQUIRE_API_KEY=false`
로 두되, 서버를 외부에 노출하지 않는다. 키를 받으면 `API_KEY` 설정과 동시에
`REQUIRE_API_KEY=true` 로 올려 "키를 깜빡한 채 무인증으로 떠 있는" 상태를 원천 차단한다.
- **운영 키는 반드시 ASCII 로 발급**한다. HTTP 헤더는 비ASCII 를 전송할 수 없어
한글 키는 인증 자체가 불가능하다(서버는 500 대신 401 을 반환한다).
- `PUBLIC_DOCS=false` 이면 `/docs`, `/openapi.json`, `/redoc`에도 API 키가
필요하다. 외부 노출 환경에서는 리버스 프록시 차단도 함께 적용하는 편이 안전하다.
## coverage 정의와 CPU 상한 (#3/#5)
두 가지 coverage 를 구분한다. 혼동하면 긴 원고에서 위험도가 항상 낮게 나온다.
- `matches[].matched_coverage` — **세그먼트 1건**의 일치 구간 / 질의 전체 길이.
분모가 원고 전체라 30만 자 원고에서는 한 세그먼트가 최대 수천분의 1에 그친다.
개별 후보의 기여도를 볼 때만 쓴다.
- `score_semantics.union_coverage` — **정밀 비교한 후보 전체**의 일치 구간을 질의
좌표에서 **합집합**으로 묶은 비율(중복 구간 1회만 계산). "이 원고의 몇 %가 등록
코퍼스와 겹치는가"에 답하는 값이며, `PERSISTENT_MIN_COVERAGE` 게이트와 판례
위험도(`legal_risk`)는 **이 값만** 사용한다.
CPU 상한은 `PERSISTENT_RERANK_TOP_K`(기본 20) 하나로 통제한다. 후보 검색은 전량
행렬곱으로 하되, `SequenceMatcher` 정밀 비교는 상위 N건에만 돌린다. 나머지 후보는
`reranked=false` 로 반환되며 `evidence`/`coverage`/`longest_span` 이 0 이고 score 만
의미가 있다. 잘림이 발생하면 `score_semantics.evidence_truncated=true` 로 노출된다.
이 값을 올리면 요청당 지연이 선형으로 증가한다.
참조 lemma/요소는 `scripts/build_persistent_index.py` 가 인덱싱 때 DB 에 사전계산해
둔다(`--skip-precompute` 로 생략 가능). 캐시가 없으면 질의 시 계산 후 자동 백필된다.
500 세그먼트×937자 기준 사전계산 시 요청 지연 0.499s → 0.415s (약 17%).
## 점수 의미와 잠정 임계값 (#9)
`confidence` / `matches[].similarity` 는 hashing 어휘 점수와 lemma 겹침을 설정
가중치로 섞은 **검색 랭킹 점수**이며 침해 확률이 아니다. 응답의 `score_semantics`
가 이를 명시한다.
- `threshold_source` — `server_default` / `request_override`
- `threshold_calibrated` — `SIMILARITY_THRESHOLD_CALIBRATED` 설정값. 79권 상호비교
FP 분포를 측정하기 전까지 `false` 로 두고, `provisional=true` 로 노출된다.
- `matches[].match_reasons` — 후보가 채택된 이유. `score_threshold`(결합점수 초과),
`exact_span`(연속 일치 ≥ `PERSISTENT_MIN_EXACT_SPAN`), `coverage`(union coverage
≥ `PERSISTENT_MIN_COVERAGE`). 이때 후보 채택용 coverage는 해당 출처 문서의
세그먼트끼리만 합산한다. 서로 다른 출처의 일치를 합쳐 개별 후보를 통과시키지
않는다. 임계값이 아니라 연속 일치 때문에 올라온 후보도 구분할 수 있다.
임계값 수치는 캘리브레이션 전까지 **임의로 바꾸지 않는다.** 기존 값을 유지하고
provisional 플래그로만 알린다.
## 법적 맥락 입력 (#10)
`POST /v1/plagiarism/detect` 의 `legal_context` 는 선택 필드다. 엔진은 이 사실들을
**추론하지 않으며**, 미제공 시 `legal_risk.missing_factors` 에 그대로 남는다.
GPT 판례 재판단을 활성화하는 방법과 응답 필드는
`USE_LLM_LEGAL_JUDGE` 환경변수로 제어한다. 기본값은 비활성이며,
원고 증거 구간의 외부 API 전송이 승인된 환경에서만 활성화한다.
```json
{"doc_id":"x","text":"...","legal_context":{
"work_type":"literary","access_evidence":true,
"protected_expression_reviewed":true,"rights_verified":true}}
```
판례는 등록된 것만 반환한다(`precedent_ids`). 랭킹은 ① 태그 교집합 수 ②
`work_type` 일치 ③ 사건번호 순이며, 생성형 인용은 어떤 경로로도 발생하지 않는다.

View File

@ -1,750 +0,0 @@
# 성능평가확인서 시험결과 캡처용 소스코드
> 대상 문서: `성능평가확인서_출판과제_1029.hwp`
> 성적서번호 `GERIR.CE.2211-02.013` / 구미전자정보기술원
> 범위: **3.2.1 메타 데이터 추출**, **3.2.2 표절 여부 판별 정확도**
> (3.2.3 요약 성능은 이 문서 범위에서 제외)
HWP 본문에서 빈칸으로 남아 있는 "성능 평가 과정" 슬롯에 그대로 붙여 넣을
소스코드와 실행 화면을 항목 순서대로 정리한다. 각 절의 **캡처 대상**이
스크린샷 1장 단위다.
## 시험 환경 (2.2.2 대응)
| 구분 | 값 |
|---|---|
| 시험 PC | Apple M3 Pro / RAM 18GB / SSH 터미널 |
| GPU 서버 | RTX 3090 24G × 2 / i9-12900KS / RAM 125GB |
| OS·런타임 | Ubuntu 24.0.2 / Python 3.9 / PyTorch 2.8 / CUDA 12.2 |
| 구현 모듈 | 메타 데이터 추출 모듈, 표절 여부 판별 모듈 |
---
# 3.2.1 메타 데이터 추출 (F1-score)
시험방법 ① KLUE NER 데이터셋 약 11,000건으로 모델 학습
시험방법 ② 학습되지 않은 2,000여건 테스트 데이터로 상대 평가
주 스크립트: `scripts/train_klue_ner.py`
보조 스크립트: `scripts/eval_metadata_f1.py`
## 3.2.1.3-① 데이터 준비 — 학습 데이터 예시
**캡처 대상**: 아래 명령의 터미널 출력 (총 건수 + 샘플 레코드)
```bash
python - <<'PY'
from datasets import load_dataset
ds = load_dataset("klue", "ner")
print("학습 %d건 / 평가 %d건" % (len(ds["train"]), len(ds["validation"])))
ex = ds["train"][0]
names = ds["train"].features["ner_tags"].feature.names
print("tokens:", ex["tokens"][:40])
print("ner_tags:", [names[t] for t in ex["ner_tags"]][:40])
PY
```
HWP 원문의 `<학습 데이터 예시(총 11479 건)>` 자리에 위 출력을 넣는다.
## 3.2.1.3-② 데이터 로드 코드
**캡처 대상**: `scripts/train_klue_ner.py:30-64`
라벨 스킴 정의 (KLUE NER 태그 6종 → BIO 12개 + O = 13 labels):
```python
# scripts/train_klue_ner.py:30-34
TAGS = ["DT", "LC", "OG", "PS", "QT", "TI"]
LABELS = ["O"] + [f"{p}-{t}" for t in TAGS for p in ("B", "I")]
LABEL2ID = {label: i for i, label in enumerate(LABELS)}
ID2LABEL = {i: label for label, i in LABEL2ID.items()}
```
음절 단위 라벨을 서브워드 토큰에 정렬하는 로드 로직:
```python
# scripts/train_klue_ner.py:36-64
def tokenize_and_align(examples, tokenizer, max_length: int):
"""KLUE NER 은 음절 단위 라벨이라 서브워드 토큰에 맞춰 정렬한다.
첫 서브워드에만 라벨을 주고 나머지는 -100 으로 두어 손실에서 제외한다.
"""
encoded = tokenizer(
examples["tokens"],
is_split_into_words=True,
truncation=True,
max_length=max_length,
padding=False,
)
aligned = []
for i, tags in enumerate(examples["ner_tags"]):
word_ids = encoded.word_ids(batch_index=i)
previous = None
labels = []
for word_id in word_ids:
if word_id is None:
labels.append(-100)
elif word_id != previous:
labels.append(tags[word_id])
else:
labels.append(-100)
previous = word_id
aligned.append(labels)
encoded["labels"] = aligned
return encoded
```
## 3.2.1.3-③ 모델 실행
**캡처 대상**: `scripts/train_klue_ner.py:100-147` + 학습 진행 터미널 로그
```python
# scripts/train_klue_ner.py:105-137
dataset = load_dataset("klue", "ner")
print("학습 %d건 / 평가 %d건"
% (len(dataset["train"]), len(dataset["validation"])))
source = args.out_dir if args.eval_only else args.model
tokenizer = AutoTokenizer.from_pretrained(str(source))
model = AutoModelForTokenClassification.from_pretrained(
str(source), num_labels=len(LABELS), id2label=ID2LABEL, label2id=LABEL2ID)
encoded = dataset.map(
lambda batch: tokenize_and_align(batch, tokenizer, args.max_length),
batched=True, remove_columns=dataset["train"].column_names)
training_args = TrainingArguments(
output_dir=str(args.out_dir / "checkpoints"),
learning_rate=args.lr, # 2e-5
per_device_train_batch_size=args.batch_size, # 16
per_device_eval_batch_size=args.batch_size * 2,
num_train_epochs=args.epochs, # 3.0
weight_decay=0.01,
logging_steps=100,
save_strategy="no",
report_to=[],
fp16=torch.cuda.is_available(),
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=encoded["train"],
eval_dataset=encoded["validation"],
data_collator=DataCollatorForTokenClassification(tokenizer),
compute_metrics=build_metrics(ID2LABEL),
)
```
실행 명령 (시험관 입회 시연용):
```bash
python scripts/train_klue_ner.py --out-dir data/models/klue_ner
```
> 전 차수(`GERIR.CE.2511-02.009`) 재현이 필요하면 `--model klue/bert-base`.
> 기본값은 `klue/roberta-large` 이며 2-1년차 목표 83% 달성을 위한 설정이다.
## 3.2.1.3-④ 테스트 데이터 예시
**캡처 대상**: 아래 명령 출력
```bash
python - <<'PY'
from datasets import load_dataset
ds = load_dataset("klue", "ner", split="validation")
print("테스트 데이터 총 %d건" % len(ds))
names = ds.features["ner_tags"].feature.names
for i in (0, 1, 2):
ex = ds[i]
print("[%d] text: %s" % (i, "".join(ex["tokens"])[:60]))
print(" tags: %s" % [names[t] for t in ex["ner_tags"]][:30])
PY
```
## 3.2.1.3-⑤ 예측 결과 예시
**캡처 대상**: 예측 태그 배열 출력
HWP 원문에 이미 아래 형태의 예시가 들어가 있다. 동일 형식으로 재생성한다.
```
"original_tags": ["I-QT", "I-QT", "I-QT", "I-QT", "I-QT", "I-QT", "I-QT", "I-OG",
"I-QT", "I-QT", ... , "I-DT", "B-TI", ... , "I-QT"]
```
```bash
python - <<'PY'
import json, torch
from transformers import AutoTokenizer, AutoModelForTokenClassification
from datasets import load_dataset
path = "data/models/klue_ner"
tok = AutoTokenizer.from_pretrained(path)
model = AutoModelForTokenClassification.from_pretrained(path).eval()
ds = load_dataset("klue", "ner", split="validation")
names = ds.features["ner_tags"].feature.names
ex = ds[0]
enc = tok(ex["tokens"], is_split_into_words=True, return_tensors="pt", truncation=True)
with torch.no_grad():
pred = model(**enc).logits.argmax(-1)[0].tolist()
wid = enc.word_ids(0)
tags, seen = [], set()
for p, w in zip(pred, wid):
if w is None or w in seen:
continue
seen.add(w)
tags.append(model.config.id2label[p])
print(json.dumps({"original_tags": [names[t] for t in ex["ner_tags"]],
"predicted_tags": tags}, ensure_ascii=False))
PY
```
## 3.2.1.3-⑥ 결과 측정
> 전 차수 성적서 9페이지 "4. 결과 측정" 은 **JSON 한 덩어리**를 캡처해 실었다.
> 최상위에 micro 평균이 `precision` / `recall` / `f1_score` 로 오고, 태그별 상세가
> `detailed_report` 아래에 DT→LC→OG→PS→QT→TI→micro/macro/weighted 순으로 붙는다.
> `scripts/train_klue_ner.py` 가 **이 형식 그대로** 출력하도록 맞춰져 있다.
**캡처 대상 (1)**: 평가 함수 `scripts/train_klue_ner.py:66-84`
```python
# scripts/train_klue_ner.py:66-84
def build_metrics(id2label: dict):
from seqeval.metrics import classification_report, f1_score
def compute(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
true_labels, true_preds = [], []
for prediction, label in zip(predictions, labels):
pairs = [(p, l) for p, l in zip(prediction, label) if l != -100]
true_preds.append([id2label[int(p)] for p, _ in pairs])
true_labels.append([id2label[int(l)] for _, l in pairs])
report = classification_report(
true_labels, true_preds, output_dict=True, zero_division=0)
return {
"f1": f1_score(true_labels, true_preds, zero_division=0),
"report": report,
}
return compute
```
**캡처 대상 (2)**: 성적서 형식 변환 `scripts/train_klue_ner.py:87-107`
```python
# scripts/train_klue_ner.py:87-107
def build_scorecard(report: dict) -> dict:
"""seqeval 리포트를 전 차수 성적서와 같은 키 구성으로 재배열한다.
성적서는 최상위에 micro 평균을 precision/recall/f1_score 로 두고,
태그별 상세를 detailed_report 아래에 DT→LC→OG→PS→QT→TI 순으로 싣는다.
seqeval 의 dict 순서는 태그 등장 순서라 성적서 순서와 달라지므로 여기서 고정한다.
"""
micro = report["micro avg"]
detailed: dict[str, dict] = {}
for tag in TAGS:
if tag in report:
detailed[tag] = report[tag]
for name in ("micro avg", "macro avg", "weighted avg"):
if name in report:
detailed[name] = report[name]
return {
"precision": micro["precision"],
"recall": micro["recall"],
"f1_score": micro["f1-score"],
"detailed_report": detailed,
}
```
**캡처 대상 (3)**: 출력·기록부 `scripts/train_klue_ner.py:188-208`
```python
# scripts/train_klue_ner.py:188-208
scorecard = build_scorecard(report)
rendered = json.dumps(scorecard, ensure_ascii=False, indent=4)
print("\n" + "=" * 62)
print("4. 결과 측정")
print()
print(rendered)
print("\n결과 : 평균 f1 -score %d%%" % round(micro["f1-score"] * 100))
args.out_dir.mkdir(parents=True, exist_ok=True)
(args.out_dir / "result.json").write_text(rendered + "\n", encoding="utf-8")
```
**캡처 대상 (4)**: 실행 + 최종 JSON 출력 — **이것이 HWP 본문에 들어갈 화면**
```bash
python scripts/train_klue_ner.py --eval-only --out-dir data/models/klue_ner
# 또는 기록된 결과를 그대로 열람
cat data/models/klue_ner/result.json
```
출력 형식 (아래는 **전 차수 실측값**. 2-1년차 측정 후 이 자리를 교체한다):
```json
{
"precision": 0.8160559736973584,
"recall": 0.8067849745060547,
"f1_score": 0.8113939923241485,
"detailed_report": {
"DT": {
"precision": 0.8626870237635195,
"recall": 0.828588722428993,
"f1-score": 0.8452941402199001,
"support": 12397
},
"LC": {
"precision": 0.7572073055161723,
"recall": 0.7553675850368563,
"f1-score": 0.7562863264653191,
"support": 6997
},
"OG": {
"precision": 0.8023425167572489,
"recall": 0.7706453793914474,
"f1-score": 0.7861745849948081,
"support": 11492
},
"PS": {
"precision": 0.853834243620867,
"recall": 0.8543908500378298,
"f1-score": 0.8541124561472994,
"support": 7670
},
"QT": {
"precision": 0.7617685880582717,
"recall": 0.7730987193907924,
"f1-score": 0.7673918351662147,
"support": 16674
},
"TI": {
"precision": 0.9005210695901854,
"recall": 0.8999231140326887,
"f1-score": 0.9002219925162598,
"support": 7530
},
"micro avg": {
"precision": 0.8160559736973584,
"recall": 0.8067849745060547,
"f1-score": 0.8113939923241485,
"support": 62760
},
"macro avg": {
"precision": 0.8230601245510442,
"recall": 0.8136690617197679,
"f1-score": 0.8182468892516335,
"support": 62760
},
"weighted avg": {
"precision": 0.8165231436063907,
"recall": 0.8067849745060548,
"f1-score": 0.811516424716427,
"support": 62760
}
}
}
```
```
결과 : 평균 f1 -score 81%
```
**결과 기록 위치**
| 파일 | 내용 |
|---|---|
| `data/models/klue_ner/result.json` | 성적서 게재용 JSON (위 형식 그대로) |
| `data/models/klue_ner/run_meta.json` | 모델명·epochs·batch_size·lr·max_length (재현용) |
> `support 62760` 은 평가 세트의 엔티티 총수다. 문서 2,000여건 기준이며
> 시험방법 ②의 "학습되지 않은 2,000여건" 과 대응한다.
---
# 3.2.2 표절 여부 판별 정확도 (Precision)
시험방법 ① 모델이 학습하지 않은, 자체 제작 표절 글 데이터 준비
시험방법 ② 표절 판별 알고리즘에 대한 전처리 진행
시험방법 ③ 데이터 표절 여부의 precision 점수 계산
주 스크립트: `scripts/run_precision_eval.py`
시험셋 생성: `scripts/build_plagiarism_testset.py`
시험 절차서: `docs/PRECISION_TEST_PROCEDURE.md`
## 3.2.2.3-1 데이터 준비
**캡처 대상 (1)**: 시험셋 구성 명세 `data/eval/testset_v2/manifest.json`
```bash
wc -l data/eval/testset_v2/pairs.jsonl data/eval/testset_v2/index.jsonl
cat data/eval/testset_v2/manifest.json
```
```json
{
"seed": 20260908,
"index_author_ratio": 0.7,
"authors": { "total": 290, "index": 203, "query": 87 },
"counts": { "plagiarism": 500, "legitimate": 500, "index_segments": 4033 },
"plagiarism_mix": {
"verbatim": 100, "partial": 100, "sentence_shuffle": 100,
"lexical_swap": 150, "compress": 50
},
"hard_negatives": 150,
"lexical_swap_rate": 0.35
}
```
> **"모델이 학습하지 않은 데이터" 요건 충족 근거** — 전체 작성자 290명을
> 코퍼스 적재 203명 / 시험 질의 전용 87명으로 분리했다. 비표절 500건은
> 인덱스에 존재하지 않는 작성자의 원문이다.
**캡처 대상 (2)**: 비표절 / 표절 데이터 예시
```bash
# 비표절 데이터 예시 (총 500건)
python - <<'PY'
import json
rows = [json.loads(l) for l in open("data/eval/testset_v2/pairs.jsonl", encoding="utf-8")]
neg = [r for r in rows if not r["is_plagiarism"]]
pos = [r for r in rows if r["is_plagiarism"]]
print("비표절 데이터 총 %d건" % len(neg))
for r in neg[:2]:
print(" -", r["pair_id"], "|", r["derived_text"][:70], "...")
print("\n표절 데이터 총 %d건" % len(pos))
for r in pos[:2]:
print(" -", r["pair_id"], "(%s)" % r["transformation"])
print(" 원문 :", r["original_excerpt"][:60], "...")
print(" 변형 :", r["derived_text"][:60], "...")
print("\n전체 데이터 총 %d건" % len(rows))
PY
```
**캡처 대상 (3)**: 표절 데이터 생성 알고리즘 `scripts/build_plagiarism_testset.py:60-131`
```python
# scripts/build_plagiarism_testset.py:60-129
def t_verbatim(text: str, rng: random.Random) -> str:
return text
def t_partial(text: str, rng: random.Random) -> str:
parts = sentences(text)
if len(parts) < 4:
return text
keep = max(2, int(len(parts) * rng.uniform(0.4, 0.7)))
start = rng.randint(0, len(parts) - keep)
return " ".join(parts[start:start + keep])
def t_sentence_shuffle(text: str, rng: random.Random) -> str:
parts = sentences(text)
if len(parts) < 3:
return text
rng.shuffle(parts)
return " ".join(parts)
def t_lexical_swap(text: str, rng: random.Random, rate: float = 0.35) -> str:
"""어절의 rate 비율을 실제로 바꾼다.
사전 치환만 하면 사전에 없는 어절이 그대로 남아 원문과 98% 가 같아진다.
그러면 '어휘 치환' 이라는 이름만 붙은 복제가 되어 시험 난이도가 무너진다.
사전에 걸리지 않는 어절은 어간 일부를 잘라 다른 표현으로 바꾼다.
"""
swap_map = dict(SWAPS)
words = text.split()
targets = rng.sample(range(len(words)), k=max(1, int(len(words) * rate)))
for i in targets:
word = words[i]
replaced = None
for before, after in swap_map.items():
if before in word:
replaced = word.replace(before, after)
break
if replaced is None:
# 사전 미등록 어절: 어순을 흔들어 연속 일치를 끊는다.
replaced = _reorder_syllables(word, rng)
words[i] = replaced
return " ".join(words)
def t_compress(text: str, rng: random.Random) -> str:
parts = sentences(text)
if len(parts) < 4:
return text
return " ".join(parts[::2])
TRANSFORMS = {
"verbatim": t_verbatim,
"partial": t_partial,
"sentence_shuffle": t_sentence_shuffle,
"lexical_swap": t_lexical_swap,
"compress": t_compress,
}
```
시험셋 재생성 시연 (재현성 증빙):
```bash
python scripts/build_plagiarism_testset.py \
--excerpts-jsonl reports/excerpts_20260902.jsonl \
--hash-map <hash_map.json> \
--out-dir /tmp/testset_reproduce
diff <(cut -c1-80 /tmp/testset_reproduce/pairs.jsonl) \
<(cut -c1-80 data/eval/testset_v2/pairs.jsonl) && echo "동일"
```
## 3.2.2.3-2 전처리 알고리즘
HWP 원문의 `2. 전처리 알고리즘 / 글 구성요소에 대한 전처리 과정` 슬롯.
전처리는 **(가) 자서전 특화 정규화**와 **(나) 형태소 lemma 추출** 두 단계다.
### (가) 공통 표현 제거 + 개체명 마스킹
**캡처 대상**: `app/engine/autobiography_filter.py:46-105`
```python
# app/engine/autobiography_filter.py:46-52
def remove_common_patterns(text: str, patterns_path: str) -> str:
"""공통 표현 제거. 매칭된 자리를 공백으로 치환."""
result = text
for p in _common_patterns(patterns_path):
result = result.replace(p, " ")
return re.sub(r"\s+", " ", result).strip()
# app/engine/autobiography_filter.py:55-96
_TOKEN_PERSON = "[PERSON]"
_TOKEN_PLACE = "[PLACE]"
_TOKEN_DATE = "[DATE]"
_TOKEN_NUM = "[NUM]"
_DATE_PATTERN = re.compile(r"\b(19|20)\d{2}\s*년|\d{1,2}\s*월\s*\d{1,2}\s*일|\d{4}-\d{2}-\d{2}")
_NUM_PATTERN = re.compile(r"\b\d{2,}\b")
def mask_entities(text: str) -> str:
"""인명/지명/날짜/숫자 마스킹."""
# 날짜·숫자 먼저 (kiwi 토큰화 전에)
masked = _DATE_PATTERN.sub(_TOKEN_DATE, text)
masked = _NUM_PATTERN.sub(_TOKEN_NUM, masked)
# 형태소 분석 → NNP(고유명사) 마스킹
tokens = _kiwi().tokenize(masked)
parts: list[tuple[int, int, str]] = []
for t in tokens:
if t.tag == "NNP":
parts.append((t.start, t.start + t.len, _TOKEN_PERSON))
if not parts:
return masked
# 뒤에서부터 치환 (인덱스 보존)
parts.sort(key=lambda p: p[0], reverse=True)
chars = list(masked)
for start, end, repl in parts:
chars[start:end] = list(repl)
return "".join(chars)
# app/engine/autobiography_filter.py:99-105
def preprocess_for_autobiography(text: str, patterns_path: str,
enable_mask: bool = True) -> str:
"""자서전 모드 전처리 = 공통 표현 제거 + 엔티티 마스킹."""
if not text:
return text
cleaned = remove_common_patterns(text, patterns_path)
if enable_mask:
cleaned = mask_entities(cleaned)
return cleaned
```
### (나) 내용어 lemma 추출 — 구조 유사도용
**캡처 대상**: `app/engine/structural.py:25-58`
```python
# app/engine/structural.py:22-23
# 내용어 태그 - 명사/동사/형용사/부사. (조사 JK*, 어미 EF/EC/EP 등 기능어는 제외)
_CONTENT_TAGS = ("NNG", "NNP", "VV", "VA", "MAG", "VV-R", "VV-I", "VA-R", "VA-I")
# app/engine/structural.py:31-45
def extract_lemmas(text: str, min_length: int = 1) -> list[str]:
"""텍스트에서 내용어 lemma 리스트 반환 (등장 순서, 중복 포함)."""
if not text.strip():
return []
tokens = _get_kiwi().tokenize(text)
lemmas: list[str] = []
for t in tokens:
if not any(t.tag.startswith(prefix)
for prefix in ("NNG", "NNP", "VV", "VA", "MAG")):
continue
lemma = getattr(t, "lemma", None) or t.form
if len(lemma) >= min_length:
lemmas.append(lemma)
return lemmas
# app/engine/structural.py:48-58
def lemma_overlap_ratio(query_lemmas: list[str], ref_lemmas: list[str]) -> float:
"""query 기준 다중집합 교집합 비율 = |Q ∩ R (다중집합)| / |Q|."""
if not query_lemmas:
return 0.0
qc = Counter(query_lemmas)
rc = Counter(ref_lemmas)
intersection = sum((qc & rc).values())
total = sum(qc.values())
return intersection / total if total else 0.0
```
### 전처리 예시
**캡처 대상**: HWP 의 `예시` 슬롯 — 전처리 전후 대조 출력
```bash
python - <<'PY'
from app.engine.autobiography_filter import preprocess_for_autobiography
from app.engine.structural import extract_lemmas
src = ("1978년 3월, 나는 춘천초등학교에 입학했다. "
"어머니께서 지어주신 새 옷을 입고 교문을 들어섰다.")
out = preprocess_for_autobiography(src, "data/autobiography/common_patterns.txt")
print("[전처리 전]", src)
print("[전처리 후]", out)
print("[lemma ]", extract_lemmas(out)[:20])
PY
```
## 3.2.2.3-3 표절 여부 판별
**캡처 대상 (1)**: 판정 기준 — `app/engine/detector.py:296-313`
세 조건 중 **①결합유사도와 ②최장 연속 일치를 함께** 요구한다.
| 조건 | 기준값 | 설정 키 |
|---|---|---|
| ① 결합 유사도 | ≥ 0.65 | `persistent_similarity_threshold` |
| ② 최장 연속 일치 | ≥ 35자 (9어절) | `persistent_min_exact_span` |
| ③ 문서 커버리지 | ≥ 0.30 | `persistent_min_coverage` |
```python
# app/engine/detector.py:296-313
reasons: list[str] = []
if h.score >= threshold:
reasons.append("score_threshold")
if provenance_hit:
if provenance_hit.longest_span >= self.settings.persistent_min_exact_span:
reasons.append("exact_span")
# 커버리지 조건은 문서 단위 union 으로 판단한다. 세그먼트 단독
# 비율은 긴 원고에서 구조적으로 작아 조건이 성립하지 않는다.
if document_coverage.get(provenance_hit.document_id, 0.0) >= self.settings.persistent_min_coverage:
reasons.append("coverage")
if not reasons:
continue
# 유사도만 넘고 그대로 겹친 구간이 없는 후보는 채택하지 않는다.
# 같은 주제를 다룬 글은 임베딩 유사도가 함께 오르므로, 유사도 단독
# 판정은 오탐을 만든다. 실데이터 106건 중 29건이 이 경우였고 검토에서
# 전부 오탐으로 확인됐다. 반대로 이 조건을 걸어도 새로 놓치는 건은
# 없었다(미탐지 7,680건 재판정 결과 0건).
if self.settings.require_exact_span_evidence and "exact_span" not in reasons:
continue
```
기준값 확인 시연:
```bash
python -c "from app.core.config import Settings; s=Settings(); \
print(s.persistent_similarity_threshold, s.persistent_min_exact_span, \
s.persistent_min_coverage, s.require_exact_span_evidence)"
```
**캡처 대상 (2)**: 판정 루프 — `scripts/run_precision_eval.py:118-140`
```python
# scripts/run_precision_eval.py:118-140
detector = PlagiarismDetector(settings)
print("판정 기준: 유사도>=%.2f | 연속일치>=%d자 | 커버리지>=%.2f | 연속일치 필수=%s"
% (settings.persistent_similarity_threshold,
settings.persistent_min_exact_span,
settings.persistent_min_coverage,
settings.require_exact_span_evidence))
tp = fp = tn = fn = 0
buckets: dict[str, dict[str, int]] = defaultdict(lambda: {"tp": 0, "fp": 0, "tn": 0, "fn": 0})
for i, row in enumerate(pairs, start=1):
result = detector.detect(doc_id=row["pair_id"], text=row["derived_text"])
predicted = bool(result.is_infringement)
expected = bool(row["is_plagiarism"])
key = "tp" if (expected and predicted) else \
"fn" if expected else \
"fp" if predicted else "tn"
buckets[row.get("transformation", "unknown")][key] += 1
```
**캡처 대상 (3)**: precision 계산부 — `scripts/run_precision_eval.py:142-145`
```python
# scripts/run_precision_eval.py:142-145
precision = tp / (tp + fp) if (tp + fp) else 0.0
recall = tp / (tp + fn) if (tp + fn) else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
```
## 3.2.2.3-4 모델 결과
**캡처 대상**: 평가 실행 명령 1줄 + 터미널 최종 출력
```bash
docker build -f Dockerfile.eval -t o2o-plagia-eval:py39 .
docker run --rm -v $PWD:/app -w /app o2o-plagia-eval:py39 \
python scripts/run_precision_eval.py --testset data/eval/testset_v2
```
> 평가환경은 계획서가 Ubuntu 22.04 / Python 3.9 / GPU 불필요로 고정하고 있고,
> 시험 서버 시스템 파이썬은 3.6.9 이므로 **반드시 컨테이너로 실행한다.**
> 소요 약 10분(1,000건), 진행률이 100건 단위로 출력된다.
사전 측정 결과 (`data/eval/testset_v2/result.json`):
```
==============================================================
표절 여부 판별 정밀도 (precision) : 0.9840 [목표 0.97]
재현율 (recall) : 0.9860
F1 : 0.9850
TP=493 FP=8 TN=492 FN=7
[변형 유형별]
compress n= 50 P=1.000 R=1.000 TP=50 FP=0 TN=0 FN=0
hard_negative n= 150 P=0.000 R=0.000 TP=0 FP=7 TN=143 FN=0
legitimate n= 350 P=0.000 R=0.000 TP=0 FP=1 TN=349 FN=0
lexical_swap n= 150 P=1.000 R=0.960 TP=144 FP=0 TN=0 FN=6
partial n= 100 P=1.000 R=0.990 TP=99 FP=0 TN=0 FN=1
sentence_shuffle n= 100 P=1.000 R=1.000 TP=100 FP=0 TN=0 FN=0
verbatim n= 100 P=1.000 R=1.000 TP=100 FP=0 TN=0 FN=0
```
**최종 결과: precision 98.4% (목표 97% 달성)**
오탐 8건은 전부 비표절 측에서 발생했고 그중 7건이 하드 네거티브 150건에서
나왔다. 정밀도 = TP / (TP + FP) 이므로 예측 양성 501건 기준 허용 오탐은
15건이며, 실측 8건으로 기준 내에 있다.
---
# 부록 — 시험관 예상 질의 대응
| 질문 | 근거 |
|---|---|
| 시험셋을 직접 만들었으면 유리하게 만든 것 아닌가 | 구성 비율·시드(20260908)가 `manifest.json` 에 사전 기록. 재생성 시 동일 결과. 하드 네거티브 150건을 의도적으로 투입해 난이도 상향 |
| 연속 일치를 필수로 하면 재현율이 떨어지지 않나 | 실데이터 7,680건 재판정 결과 새로 놓친 건 0건. 계획서도 재현율을 희생해 오탐을 줄이는 방향으로 규정 (`docs/SCOPE_AND_METRICS.md`) |
| 35자 기준의 근거는 | 어절 3~9 스윕 + 문서쌍 148,240쌍 전수 대조 실측. 4어절 이하는 임의 조합의 99% 이상이 겹쳐 신호가 되지 못함 |
| 원문이 외부로 나가는가 | 나가지 않음. `USE_LLM_LEGAL_JUDGE=false`, 시험셋 변형도 전부 규칙 기반 |
> 성적서 게재용 데이터 예시는 자서전 본문에 실명이 남아 있으므로
> **익명화가 끝난 생활수기에서 발췌**하여 제출한다. 접수 시 시험기관에 함께 요청할 것.

View File

@ -1,243 +0,0 @@
# 판례 리스트업 — 자서전·출판물 표절 판단 근거
작성: dev o2o / 대상: 엄진섭 님 / 근거: 2026-08-11 실무진 회의 액션아이템
데이터 기준일: 2026-08-19, 저장소 `o2o-plagiarism-api`
---
## 1. 요약
- 저장소 운영 적재본 545건을 전수 검토해 본 과제에 쓸 수 있는 **57건**을 골랐다.
직접 적용 가능한 **A등급 24건**, 출판 실무 주변 쟁점인 **B등급 23건**,
법리만 참고할 **C등급 10건**이다.
- **자서전·회고록을 직접 다룬 국내 판례는 확인되지 않았다.** 545건 전체에서 `자서전`,
`회고록`, `대필`, `평전`, `구술`, `유고` 키워드는 **모두 0건**이다. 회의에서 보고한
"즉시 적용 가능한 판례가 많지 않다"는 진단은 데이터로 확인된다.
- 다만 **인접 유형의 판례는 충분히 두껍다.** 실화·역사 소재 서적과 소설의 비교(석굴암 3심급,
선덕여왕 2심급), 실용서·수험서의 보호 범위, 인용 비율과 정당한 인용, 대필·공저 표기의
형사책임까지 자서전 사안에 그대로 대응시킬 수 있는 법리가 모여 있다.
- 본 과제에 가장 직접적인 판례 3건을 꼽으면 다음과 같다.
1. **2013도8793**(대법원) — 저서 각 장의 11~40%를 인용하고 각주·참고문헌을 달았어도
정당한 인용이 아니라고 본 사례. 출처를 표시했다는 이유만으로 면책되지 않는다는
정량 기준을 제시한다.
2. **2014다14375**(대법원, 석굴암) — 역사적 사실과 그에 대한 새로운 해석은 아이디어
영역이라는 판시. 실재 인물·사건을 공유하는 자서전 간 오탐을 걸러내는 근거다.
3. **2018도144**(대법원, 표지갈이) — 저작자 아닌 자를 저작자로 표시해 공표하면
**당사자 전원이 동의했더라도** 형사처벌 대상이다. 대필 자서전 관행에 직접 걸린다.
- 전문가 상담 전에 **원문 확보가 필요한 항목이 남아 있다**(6장). 무리해서 목록을 채우기보다
이 공백을 상담 질의로 돌리는 편이 유용하다고 판단했다.
---
## 2. 검토 대상과 방법
| 단계 | 건수 | 비고 |
|---|---:|---|
| 한국저작권위원회 공식 판례 목록 | 2,085 | 209페이지 전수 |
| 제목 기준 후보 | 702 | 자서전·출판물 본문/구조/인용 및 부속 자산 관련 |
| 사건번호 확인 후 병합 | 679 | 프로젝트 제공 엑셀 사건번호 142개 역검색 포함 |
| 엔진 라벨 부족으로 제외 | 134 | 저작물 유형·판단 기준·법적 태그가 모두 없는 건 |
| **운영 적재본** | **545** | `data/precedents/precedents.jsonl` |
| 이 중 어문저작물 + 판단 기준 라벨 보유 | 77 | 1차 선별 모집단 |
| **본 리스트업 최종 선정** | **57** | 판시 본문을 개별 확인해 등급 부여 |
선정 기준은 "자서전·출판물의 **본문 표절 판단에 실제로 기준을 주는가**" 하나다.
저작권 사건이라는 이유만으로는 넣지 않았다.
**등급 정의**
- **A — 직접 적용.** 어문저작물의 표절·유사성·인용 판단 기준을 직접 제시한다.
판정 로직과 결과 리포트의 근거로 바로 인용할 수 있다.
- **B — 조건부 적용.** 자서전 출판 실무의 주변 쟁점(성명 표시, 대필·공저, 동일성유지,
출판계약, 번역·개정판)을 다룬다. 표절 탐지 자체의 기준은 아니지만 서비스가 다뤄야 할
위험 영역이다.
- **C — 참고.** 법리는 유용하나 사안이 어문·출판이 아니거나(음악·미술·게임·영상),
가처분·구법 판단이어서 그대로 인용하면 무리가 있다.
---
## 2-1. 적재본 545건의 출처 — 표절 탐지에 실제로 쓰이는 판례
엔진은 적재본 **545건 전체를 런타임 검색 후보로 사용한다**
(`PRECEDENTS_PATH=./data/precedents/precedents.jsonl`). 판정 1건마다 저작물 유형으로
명백히 다른 사건을 제외하고, 법적 태그와 입력·증거 텍스트 대비 판시 내용의 어휘 관련성을
합산해 상위 5건을 인용한다(`app/engine/legal_risk.py`). A/B/C 등급은 사람 검토 수준을
나타내는 작은 보조 가중치이며 절대 순서나 검색 제외 조건이 아니다. 등급이 없는 488건도
내용 관련성이 높으면 인용하되 화면에 미검토로 표시한다.
| 출처 | 건수 | 비고 |
|---|---:|---|
| 한국저작권위원회 공식 목록 수집분 | 514 | 공식 판례 목록 2,085건에서 제목·라벨 기준으로 선별 |
| **컴북스 제공 엑셀 유래** | **31** | 레코드의 `excel_duplicate` 필드가 `true` |
| **합계 (엔진 적재본)** | **545** | |
### 컴북스 제공분의 반영 내역
| 단계 | 건수 | 설명 |
|---|---:|---|
| 제공 엑셀 고유 사건번호 | 142 | 중복 제거 기준 |
| 공식 상세 페이지 미확인 → 제외 | −103 | 사건번호는 있으나 공식 상세 페이지를 찾지 못한 건. 출처를 임의 생성하지 않기 위해 제외했다 |
| 엔진 라벨 부족 → 제외 | −8 | 저작물 유형·판단 기준·법적 태그가 모두 없는 건 |
| **엔진 적재본에 포함** | **31** | 런타임에 실제로 로드된다 |
| 이 중 본 리스트업에 선정 | 10 | A 3 · B 6 · C 1 |
즉 컴북스가 제공한 142건 중 실제로 엔진에 들어간 것은 **31건(21.8%)** 이다.
**되돌려야 할 항목** — 제외된 103건은 사건번호만으로는 엔진이 인용할 출처가 되지
않아 적재하지 못했다. **판결문 원문이나 확인 경로를 받으면 그대로 적재할 수 있다.**
전체 사건번호 목록은 `selection_audit.json` 의
`excel_without_official_detail_case_ids` 와 배포용 Excel 의 「컴북스 제공분」 시트에 있다.
> ⚠️ 컴북스 제공 목록의 주제 분류와 실제 판시가 어긋나는 건이 리스트업에도 흘러들어갔다.
> 컴북스 유래 A등급 3건 중 `2012다73493`(깃털 공예품)은 어문 사건이 아니며,
> `2017다212095`(게임)도 마찬가지다. 6-3의 2번 항목과 같은 문제이므로 인용 전
> 원문 대조가 필요하다.
---
## 3. A등급 — 직접 적용 (24건)
| 사건번호 | 법원 | 선고일 | 주제 | 쟁점 요약 | 적용 판단 근거·한계 |
|---|---|---|---|---|---|
| [2012다73493](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=16504) | 대법원 | 2014. 1. 29. | 실질적 유사성·의거관계 총론 | 실질적 유사성은 창작적 표현형식만을 대비해 판단한다. 의거관계는 접근 가능성과 유사성 등 간접사실로 사실상 추정되며, 의거 판단에서는 보호받지 못하는 표현의 유사성도 참작할 수 있다. | 본 과제 판정 로직의 최상위 기준. 다만 사안 자체는 깃털 공예품이라 어문 적용 시 법리만 인용해야 한다. |
| [2010가합1884](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=10809) | 미확인 | 미확인 | 어문 유사성 2분법(선덕여왕 1심) | 어문저작물의 유사성을 부분적·문언적 유사성과 포괄적·비문언적 유사성으로 나누어 판단한다. 역사적 사실을 소재로 한 저작물은 전개가 유사해질 수밖에 없으므로 이 특성을 충분히 고려해야 한다. | 자서전은 실재 사건을 소재로 하므로 '전형적 표현·역사적 사실은 보호 밖'이라는 이 판시가 오탐 억제의 직접 근거가 된다. |
| [2012나17150](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11754) | 서울고등법원 | 2012. 12. 20. | 선덕여왕 항소심 | 1심과 달리 접근 가능성을 인정해 대본 표절을 인정. 접근 가능성 인정 여부가 결론을 뒤집을 수 있음을 보여준다. | 동일 사건의 심급 간 결론 역전 사례. 기계 판정 결과를 확정 결론으로 제시하면 안 된다는 근거. 다만 이 사건의 상고심 판단은 적재본에 없으므로 항소심 결론을 확정된 법리로 인용해서는 안 된다. |
| [2014다14375](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=16615) | 대법원 | 미확인 | 역사·학술 서적 대 소설(석굴암, 대법원) | 역사적 사실과 설화에 대한 '새로운 해석'은 아이디어 영역이다. 사실 나열·기능 설명·통상적 묘사는 창작성이 없고, 장르와 주제가 다르면 공통 소재에서 오는 표현 중복은 부득이하다. | 실화·인물·시대 배경을 공유하는 자서전 간 유사도 오탐을 걸러내는 핵심 근거. 1심 2012가합80045, 항소심 2013나33609와 함께 3심급 세트로 확보됨. |
| [2019가합541129](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=48403) | 서울중앙지방법원 | 2021. 2. 5. | 역사 소재 저작물 간 실질적 유사성 | 세종·신미대사 훈민정음 창제라는 동일 소재를 다루었으나 아이디어·인물 설정만 유사하고 창작적 표현형식은 유사하지 않아 실질적 유사성을 부정. | 위 석굴암 법리의 최신 재확인. 인물·사건이 겹치는 자서전 비교에 그대로 대응된다. |
| [2020도13556](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=50296) | 대법원 | 2021. 8. 26. | 편집저작물(교재) 창작성·특정 | 소재의 선택·배열·구성에 창작성이 있으면 편집저작물로 보호된다. 실질적 유사성은 창작적 표현형식만 대비하며, 법원은 침해가 다투어지는 부분을 명확히 특정해야 한다. | 자서전의 구성·목차·에피소드 배열 유사도를 다룰 근거이자, 리포트가 '어느 구간이 문제인지'를 특정해야 한다는 요구사항의 근거. |
| [2013가합5771](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=16726) | 서울남부지방법원 | 2014. 6. 12. | 원저작물 기반 파생 출판물 | 교과서 목차·단원 제목·배열 순서·핵심 내용을 그대로 따른 문제집은 2차적저작물작성권과 저작인격권을 침해한다. | 원저작물의 구조를 따라간 후속 출판물의 침해 인정례. 구조 유사도 신호의 법적 의미를 뒷받침. |
| [2013도8793](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=17000) | 대법원 | 미확인 | 인용 비율과 정당한 인용(대법원) | 저서 각 장의 11~40%를 타인 저서에서 인용하고 참고문헌·일부 각주를 달았더라도, 인용량·내용·구성에 비추어 인용 부분을 구별하기 어렵고 스스로 창작한 것처럼 보이는 부분이 상당하며 시장에서 경쟁 관계라면 정당한 범위·공정한 관행에 합치한 인용이 아니다. | 본 과제에서 가장 직접적인 정량 기준. 출처 표시가 있어도 비율과 식별 가능성에 따라 침해가 된다는 점을 명시. |
| [2013노232](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11938) | 서울중앙지방법원 | 2013. 5. 24. | 전부 복제형 인용 부정 | 저작물의 일부가 아닌 전체를 변형·부가 없이 그대로 복제해 그 자체로 이용한 것은 저작권법 제28조의 인용이 아니다. 교육 목적이 일부 있어도 근본 목적이 영리이고 원저작물의 완전한 대체물이면 인용에 해당하지 않는다. | 인용 항변을 배척하는 기준 4요소(일부/변형/부가/대체성)를 명확히 제시. |
| [2011도3599](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11472) | 대법원 | 2013. 8. 22. | 요약물의 실질적 유사성(대법원) | 요약물이 원저작물과 실질적 유사성이 있는지 판단하는 기준을 제시. | AI 요약·재서술형 표절 판정의 직접 근거. 단 적재본에는 사건 표시만 있고 판시 본문이 없어 원문 확보 필요. |
| [2024노803](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=54578) | 대구지방법원 | 미확인 | 요약·발췌 강의교안 | 교재 내용을 요약·발췌한 강의교안은 학문 일반의 개념·표현에 불과해 창작적 표현이 아니므로 침해가 성립하지 않는다. | 위 2011도3599의 반대 방향 사례. 요약형 표절의 성립·불성립 경계를 함께 제시할 수 있다. |
| [2012나4904](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11781) | 서울남부지방법원 | 2013. 1. 17. | 소설 간 표절 인정·손해 부정 | 의거관계와 실질적 유사성이 모두 인정되어 저작권 침해는 성립하나, 장르·독자층·유통 방식이 달라 재산상 손해는 부정하고 저작인격권 침해에 따른 위자료만 인정. | '침해 인정 ≠ 손해 인정'을 보여주는 사례. 리포트가 위험도와 손해를 분리해 제시해야 하는 근거. |
| [2013나18193](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11419) | 수원지방법원 | 2013. 8. 16. | 시(詩)의 실질적 유사성 | 사용된 단어가 일부 일치해도 단어·문장 배치, 행·연 구분 등 전체 구성을 모방한 정도에 이르지 않으면 실질적 유사성을 쉽게 인정할 수 없다. | 어휘 단위 일치율만으로 표절을 단정하면 안 된다는 직접 근거. 본 엔진의 n-gram 신호 해석에 대응. |
| [2019가합588425](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=52126) | 서울중앙지방법원 | 2023. 4. 21. | 웹소설 간 실질적 유사성 부정 | 원고·피고 웹소설 사이에 실질적 유사성을 인정하기 어려워 침해금지 청구 기각. | 장편 서사물 간 비교의 최근 기각례. 적재본에 요지만 있어 원문 확인 필요. |
| [2025가단81306](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=55676) | 서울중앙지방법원 | 미확인 | 어문저작물 창작성·의거관계 부정 | 원고 어문저작물에 보호 대상인 창작적 표현방식이 포함되어 있다고 보기 어렵고 실질적 유사성·의거관계도 인정되지 않아 청구 기각. | 가장 최근의 어문 표절 기각례. 요지만 적재되어 있어 원문 확인 필요. |
| [2021가합570441](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=51912) | 서울중앙지방법원 | 2022. 9. 23. | 어문저작물 창작성 인정·침해 | 원고 어문저작물의 창작성을 인정하고, 비슷한 표현을 사용한 피고에게 저작재산권 침해를 인정. | 어문 침해 인정례. 요지만 적재되어 있어 원문 확인 필요. |
| [2021가합572409](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=51913) | 서울중앙지방법원 | 2023. 1. 13. | 실용서(교본) 간 실질적 유사성 인정 | 동일 주제 실용서(플루트 연주법·기초 음악이론) 사이의 실질적 유사성을 인정해 침해금지 청구 인용. | 실용적 저작물이라도 유사성이 인정될 수 있음을 보여, 아래 2019가합537427과 균형을 이룬다. |
| [2019가합537427](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=46501) | 서울중앙지방법원 | 2020. 7. 10. | 실용적 저작물의 보호 범위 제한 | 수험서가 전체로는 저작물이라도 창작성이 인정되지 않는 개별 부분에는 저작권 효력이 미치지 않는다. 서술방식·체계의 차이, 표현 유사 정도와 비중을 종합해 침해를 부정. | 정보 전달형 서술(연보·경력·사실 기술)이 많은 자서전의 오탐 억제 근거. |
| [2010다70520](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=12278) | 대법원 | 2012. 8. 30. | 수험서의 저작물성(대법원) | 수험서 등 실용적 저작물의 저작물성과 침해 판단 기준. | v1.3에서 오탐 억제 근거로 쓰는 판례. 적재본에는 사건 표시만 있어 원문 확보 필요. |
| [2012가합80045](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11934) | 서울중앙지방법원 | 2013. 5. 10. | 석굴암 1심 | 사실·정보 전달을 주목적으로 하는 사실적·기능적 저작물은 표현 방법이 제한되므로 보호 범위를 좁게 해석해야 한다. | '사실적 저작물 보호범위 축소' 법리의 원문. 항소심 2013나33609, 상고심 2014다14375와 연결. |
| [2013나33609](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=16460) | 서울고등법원 | 2014. 1. 23. | 석굴암 항소심 | 1심의 실질적 유사성 부정 판단 유지. | 3심급 세트 완성용. |
| [2022노434](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=52111) | 서울서부지방법원 | 2023. 3. 28. | 논문 발췌의 창작성 | 타인 논문에서 발췌해 사용한 부분에 창작성이 있다고 보기 어려워 저작권 침해를 인정하지 않음. | 학술적 서술의 창작성 하한선. 요지만 적재되어 있어 원문 확인 필요. |
| [2021가합558106](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=53528) | 서울중앙지방법원 | 미확인 | 논문 표절 침해 인정 | 원고 논문을 표절한 논문을 게재한 행위에 대해 저작권 침해금지와 손해배상을 인정. | 표절이라는 표현이 직접 쓰인 민사 인용례. 요지만 적재되어 있어 원문 확인 필요. |
| [2024도19189](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=54566) | 대법원 | 미확인 | 정당한 인용 부정 확정(대법원) | 설교 영상 무단 게시가 저작권법 제28조의 정당한 인용에 해당하지 않는다며 유죄 확정. | 인용 항변 배척의 최신 대법원 확정례. 매체는 영상이나 인용 법리는 공통. 원심은 2024노28. |
---
## 4. B등급 — 조건부 적용 (23건)
| 사건번호 | 법원 | 선고일 | 주제 | 쟁점 요약 | 적용 판단 근거·한계 |
|---|---|---|---|---|---|
| [2018도144](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=49295) | 대법원 | 2021. 7. 15. | 표지갈이 — 저작자 아닌 자 표시(대법원) | 저작자 아닌 자를 저작자로 표시해 공표하면 저작권법 제137조 제1항 제1호 위반죄가 성립하고, 저작자 아닌 자와 실제 저작자의 동의가 있었더라도 원칙적으로 달라지지 않는다. 실제 저작자가 가담하면 공범으로 처벌된다. | 자서전 대필·공저 표기 관행에 직접 적용되는 형사 법리. 당사자 합의만으로는 면책되지 않는다는 점이 실무상 가장 중요. 다만 표절 탐지가 아니라 저작자 표시 문제이므로 B로 분류. |
| [2020고정2156](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=48628) | 서울중앙지방법원 | 2021. 2. 4. | 공저 집필자 미표시 | 공저자 중 1인이 집필한 부분이 포함된 문제집을 그 저작권자를 표시하지 않고 출판해 영리 목적 저작재산권 침해와 비저작자 표시 공표가 모두 인정(선고유예). | 대필·공저 자서전에서 기여자 누락 시의 형사 리스크 사례. |
| [2024나13167](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=55561) | 대구고등법원 | 미확인 | 공동저작물 초고와 성명표시 | 공동저작물인 초고를 활용해 논문을 게재하면서 성명을 표시하지 않은 피고에게 저작인격권 침해에 따른 위자료 책임 인정. 표절 인식이 없던 다른 피고에 대한 청구와 저작재산권 침해 주장은 기각. | 초고 제공자(구술자·대필자) 관계에 대응. 가해자별 고의 유무에 따라 책임이 갈린다는 점도 유용. |
| [2021가합25193](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=53543) | 서울북부지방법원 | 2024. 7. 11. | 2차적저작물과 원저작자 표시 | 타인 대본의 2차적저작물에 해당하는 대본을 작성해 자신을 '작/연출'로 표시한 경우 성명표시권 침해로 위자료 책임이 있고, 원저작자를 표시하지 않는 한 공연할 수 없다. | 각색·윤문된 자서전에서 원저작자 표시 의무의 근거. |
| [2022가합516598](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=54474) | 서울중앙지방법원 | 미확인 | 교재 무단복제·성명표시권 | 영어교재 무단복제와 성명표시권 침해에 따른 손해배상 인정. | 출판물 무단복제 + 인격권 병합 청구의 최근 사례. 요지만 적재. |
| [2019가합106853](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=53665) | 대전지방법원 | 미확인 | 무단 게시와 출처 미표시 | 저작권 있는 서적을 무단으로 블로그에 게시하며 출처를 표시하지 않아 성명표시권 침해 인정. | 출처 미표시가 인격권 침해로 구성되는 사례. 요지만 적재. |
| [2010다79923](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11905) | 대법원 | 2013. 4. 26. | 출판계약과 동일성유지권(대법원) | 저작자가 명시적·묵시적으로 동의한 범위 내의 변경은 동일성유지권 침해가 아니며, 동의 여부와 범위는 출판계약의 성질·경위·내용, 당사자 지위, 출판 목적, 저작물 성격 등을 종합해 개별적으로 판단한다. | 자서전 편집·윤문의 허용 범위를 가르는 기준. |
| [2020가합589318](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=51871) | 서울중앙지방법원 | 2022. 9. 16. | 무단 변경 게시 | 허락 없이 저작물 내용 일부를 변경해 블로그에 올린 사안에서 복제권·공중송신권과 동일성유지권 침해를 인정. | 부분 변경도 동일성유지권 침해가 된다는 사례. |
| [2013가합85566](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=17143) | 서울중앙지방법원 | 2015. 1. 16. | 줄거리 변경과 동일성유지권 | 작가 교체 후 극중 사망 인물을 살아나도록 줄거리를 변경한 것은 저작물의 본질을 해하는 중대한 변경으로 동일성유지권 침해. | 대필 교체·후속 편집으로 서사가 바뀌는 상황에 대응. |
| [2024나2061521](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=54686) | 서울고등법원 | 미확인 | 번역저작물 무단 전재·수정 | 번역저작물을 무단 전재·수정해 저작재산권과 저작인격권을 침해했으나 손해배상 범위는 제한. | 번역·재편집물 취급 사례. 요지만 적재. |
| [2024가합96852](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=55685) | 서울중앙지방법원 | 미확인 | 번역서의 창작성과 상속 | 성경 번역서의 창작성을 인정하고 번역자 상속인들의 저작권 승계를 인정, 무단 복제·배포에 대해 출판·판매 금지, 폐기, 손해배상 인용. | 고인 저작물의 상속 저작권 문제. 유고 자서전 취급에 관련. 요지만 적재. |
| [2019가단5264420](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=52008) | 서울중앙지방법원 | 2023. 4. 13. | 번역자료의 창작성 부정 | 미국 변호사 윤리규정·이민법 등의 번역자료에 창작성을 부정해 침해를 인정하지 않음. | 번역·인용 자료의 창작성 하한. 2024가합96852와 대비하면 경계가 드러난다. |
| [2020도6425](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=47378) | 대법원 | 2020. 12. 10. | 회복저작물의 2차적저작물(대망, 대법원) | 1975년판 번역서를 일부 수정·증감해 2005년판을 발행한 행위가 회복저작물 저작자의 권리를 침해하는지 판단. | 기존 판본을 수정·증보해 재출간하는 자서전 개정판 상황에 대응. |
| [2021가합20761](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=52002) | 서울북부지방법원 | 2023. 4. 20. | 출판계약과 외전 집필 | 소설 작가가 기존 소설의 외전을 집필해 게재한 것이 출판계약 위반이 아니라고 판단. | 자서전 후속·개정판 집필의 계약상 허용 범위. 요지만 적재. |
| [2022나40712](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=53047) | 서울중앙지방법원 | 2023. 8. 11. | 출판계약 종료 후 판매 | 출판계약 이후에도 출판권을 지속 판매해 저작재산권을 침해한 사건. | 계약 종료 후 재고 유통 문제. 요지만 적재. |
| [87도2604](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=9956) | 대법원 | 1989. 1. 17. | 교과서 무단 수록과 저작인격권 | 타인의 동시·산문을 제목과 지은이를 고치고 문구를 수정해 교과서에 수록한 사안에서 저작인격권 침해를 다룸. | 성명표시·동일성유지 침해의 고전 사례. 판결 원문이 구자체 혼용이라 인용 시 정리 필요. |
| [64다515](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=9960) | 대법원 | 1964. 9. 22. | 저작인격권 침해 최초 사례 | 출판물 발매배포금지 사건에서 성명표시권·동일성유지권·개작권을 다룬 초기 대법원 판결. | 연혁 정리용. 판결문이 한자 원문이라 실무 인용 가치는 낮다. |
| [2012다204587](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=38632) | 대법원 | 2015. 8. 27. | 저작물 폐기와 인격적 법익(대법원) | 저작물 폐기 행위는 동일성유지권 침해 성립 여부와 별개로 저작자의 인격권을 침해하는 위법행위가 될 수 있다(도라산역 벽화). | 저작인격권 밖의 일반 인격권 구성. 자서전 원고 폐기·회수 상황에 유추 가능. |
| [2011가합60365](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=12156) | 미확인 | 2012. 5. 25. | 무단 수록과 명예훼손(미네르바) | 타인이 게시한 글을 승낙 없이 복제해 카페에 게시하고 이를 모아 책으로 출판한 행위 및 허위 비방 게시글에 대한 책임을 판단. | 제3자 글의 무단 서적화 + 명예훼손 병합. 자서전에 타인 서술·사생활이 등장하는 경우에 대응. |
| [2024나306378](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=54579) | 대구지방법원 | 미확인 | 표지 일러스트 무단 수정 | 웹소설 표지 일러스트를 무단 수정하고 제3자를 성명 표시한 것은 동일성유지권·성명표시권 침해. | 표지·삽화 등 부속 시각 자산 취급. 요지만 적재. |
| [2018고정220](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=47470) | 홍성지원 | 2020. 2. 12. | 저작자 허위 표시 형사 처벌 | 노래의 저작자를 저작자 아닌 자로 표시해 공표한 행위로 형사 처벌. | 2018도144의 하급심형 사례. 매체는 음악. |
| [2024누94](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=54602) | 광주지방법원 | 미확인 | 논문 일부 표절과 행정 제재 | 논문 일부 표절만으로 전체 표절을 전제한 성과연봉 환수처분은 위법. | 표절 범위 인정의 비례성. 저작권 침해가 아닌 행정 사건. |
| [2025구합31](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=55552) | 전주지방법원 | 미확인 | 출처 미표시 인용과 징계 | 타인 연구보고서를 출처 표시 없이 무단 인용한 표절 행위에 대한 견책처분은 적법. | 연구윤리형 표절의 제재 기준. 저작권 침해와 구별해 인용해야 한다. |
---
## 5. C등급 — 참고 (10건)
| 사건번호 | 법원 | 선고일 | 주제 | 쟁점 요약 | 적용 판단 근거·한계 |
|---|---|---|---|---|---|
| [2017다212095](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=47452) | 대법원 | 2019. 6. 27. | 게임 저작물의 실질적 유사성(대법원) | 게임 저작물의 창작적 개성과 실질적 유사성 판단. | v1.3이 어문 유사성 근거로 인용하지만 사안은 게임이다. 어문 사건에 그대로 대응시키면 무리. |
| [2011도5835](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11810) | 대법원 | 미확인 | 공정이용 법리(대법원) | 공정이용 법리는 요건이 명확히 규정되어 있어야 적용되며, 구 저작권법 하에서 논문 무단 복제물 제출의 침해 여부를 판단. | 현행 제35조의5 도입 전 판시라 최신 공정이용 기준으로 쓰기 어렵다. |
| [2013카합1287](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=16550) | 서울중앙지방법원 | 2014. 2. 10. | 교재 유사 발행과 공정이용 | 교육 목적으로 자격시험 표준교재와 유사한 교재를 발행·판매해도 공정이용에 해당하지 않는다. | 가처분 결정이라 본안 판단과 구별 필요. |
| [2013나36100](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=16555) | 서울중앙지방법원 | 2014. 2. 11. | 내부 이용과 공정이용 | 영리 회사가 타인 저작물을 내부적으로만 이용해도 공정이용이나 사적 이용을 위한 복제에 해당하지 않는다. | 사내 이용 국면. 출판 표절과는 거리. |
| [2012다41410](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11932) | 대법원 | 미확인 | 제호·표지 디자인(대법원) | 요리책의 표지와 제호 디자인이 응용미술저작물이 아니라고 판단. | 제호 보호 부정 계열. 자서전 제목 유사 문의에 대한 답변 근거. |
| [76가합25](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=10013) | 영등포지원 | 1976. 6. 9. | 제호의 저작물성(또복이 1심) | 만화 제명의 저작물성 부정. | 제호 불보호 원칙의 고전. 연혁 참고. |
| [77다90](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=10012) | 대법원 | 미확인 | 제호의 저작물성(또복이 상고심) | 만화 제명 '또복이'의 저작물성 부정. | 제호 불보호 원칙의 대법원 확인. |
| [2012가합88872](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11942) | 서울중앙지방법원 | 2013. 5. 31. | 외국 소설 출판 분쟁 | 일본 소설 '불모지대' 작가가 국내 출판사를 상대로 제기한 소. | 출판권 분쟁이나 표절 판단 기준은 제공하지 않음. |
| [2012가합541175](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=17008) | 서울중앙지방법원 | 2015. 2. 12. | 교재 기반 동영상 강의 | 교재를 이용한 동영상 강의의 저작권 침해를 판단. | 매체 전환 이용. 본문 표절과는 국면이 다르다. |
| [2012나33296](https://www.copyright.or.kr/information-materials/trend/precedents/view.do?brdctsno=11816) | 서울중앙지방법원 | 2013. 4. 5. | 아이리스 표절 사건 | 의거관계를 부정해 저작권 침해를 부정. | 영상 저작물 사안. 의거관계 판단 예시로만 참고. |
---
## 6. 제외한 판례와 사유
### 6-1. 데이터 수집 단계에서 제외
| 사유 | 건수 | 설명 |
|---|---:|---|
| 공식 상세 페이지 미확인 | 103 | 프로젝트 제공 엑셀의 사건번호 142개 중 한국저작권위원회 공식 사이트에서 상세 페이지를 찾지 못한 건. 출처를 임의 생성하지 않기 위해 적재에서 제외했다. 전체 목록은 `selection_audit.json`. |
| 엔진 라벨 부족 | 134 | 저작물 유형·판단 기준·법적 태그가 모두 없어 무차별 매칭을 유발하는 건 |
| 제목 후보 탈락 | 약 1,380 | 공식 목록 2,085건 중 자서전·출판물 범위와 무관한 제목 |
### 6-2. 적재본에는 있으나 본 리스트업에서 뺀 유형
| 유형 | 대표 사건번호 | 제외 사유 |
|---|---|---|
| 노래방·음원 이용 사건 | 2001도4100, 2011다101148, 2012다109798 | 공연권·복제권 사안으로 어문 표절 기준을 주지 않는다 |
| 사진·이미지 무단 사용 | 2019고정1107, 2020고정1154 등 다수 | 동일 이미지 복제 여부가 쟁점이라 유사도 판단 기준이 없다 |
| 캐릭터·위조상품 밀수 | 2019고단9348, 2019고정667 | 상품 유통 사안 |
| 웹툰·영상 불법 업로드 | 2019가합570806, 2019고단2659 | 전송권 침해이며 본문 대비 판단이 없다 |
| 폰트·프로그램 | 95가합11403, 98나23616, 2020나31370 | 저작물 유형이 다르다 |
| 조세·행정 사건 | 2011구합31796, 2016구합54602 | 저작권 수입의 과세 문제이며 침해 판단이 아니다 |
| 저작물 표시만 있고 판시 본문 없음 | 다수 | 제목·선고일만으로 법리를 추론하지 않는다는 원칙에 따름 |
### 6-3. 확인된 데이터 공백 — 상담 시 짚어야 할 항목
1. **v1.3 대표판례 29건 중 17건이 운영 적재본에 없다.**
`2006나16757, 2007가합43936, 2010도4468, 2019가단31377, 96다273, 2023카합21631, 2021가합588060, 2007가합16095, 2019가단5207564, 2013나2004096, 2012도13718, 2007다354, 2017도19025, 2008카합968, 2008다53812, 2016고정432, 2006가합8583`
대부분 6-1의 "공식 상세 페이지 미확인 103건"에 걸린 사건이다. 분류체계의 근거로는
쓰이지만 엔진이 인용할 수 있는 출처가 확보되지 않은 상태다.
2. **v1.3 문서의 주제 분류와 실제 판시 내용이 어긋나는 건이 있다.**
예: `2011다101148`, `2012다109798`은 v1.3에서 "동일성유지·출판계약"으로 묶여 있으나
적재본의 판시 내용은 노래방 음악저작물 사건이다. 인용 전 원문 대조가 필요하다.
`2012다73493`(깃털 공예품), `2017다212095`(게임)도 어문 사건이 아니다.
3. **선고일이 확인되지 않은 건이 18건 있다.** 적재본에 선고일 필드가 없어
판시 본문에서 추출했고, 본문이 요지 한 줄뿐인 건은 채우지 못했다.
`2010가합1884, 2014다14375, 2013도8793, 2024노803, 2025가단81306, 2021가합558106, 2024도19189, 2024나13167, 2022가합516598, 2019가합106853, 2024나2061521, 2024가합96852, 2024나306378, 2024누94, 2025구합31, 2011도5835, 2012다41410, 77다90`
4. **자서전·회고록·대필을 직접 다룬 판례가 없다.** 인접 유형(교재·수험서·논문·소설·드라마)
법리를 유추 적용해야 하며, 그 유추가 타당한지가 전문가 상담의 핵심 질의가 되어야 한다.
---
## 7. 전문가 상담 시 질의 제안
1. 자서전처럼 **실재 사건·인물·연보를 공유하는 저작물** 사이에서, 석굴암·선덕여왕 판례의
"역사적 사실과 그 해석은 아이디어" 법리는 어디까지 확장되는가.
2. **2013도8793의 11~40% 인용 비율**을 서비스의 경고 임계값으로 참조하는 것이 타당한가.
비율 외에 "인용 부분을 구별할 수 있는가"라는 질적 기준을 시스템이 어떻게 반영해야 하는가.
3. **대필·공저 자서전**에서 2018도144(표지갈이)의 형사 법리가 실제로 적용될 위험은
어느 정도인가. 구술자를 저자로 표기하는 관행은 어떻게 평가되는가.
4. 자서전의 **연보·경력·수상 이력 등 사실 나열 구간**은 2019가합537427(수험서)의
"창작성 없는 개별 부분에는 저작권 효력이 미치지 않는다"는 판시로 처리해도 되는가.
5. 위 3장 A등급 목록에서 **빠진 쟁점이 있는가.** 특히 자서전 특유의 문제(사자의 명예,
유족 동의, 제3자 사생활 서술)에 관해 실무상 참조되는 판례를 추가로 알고 있는가.
---
## 8. 첨부·원본 위치
- **배포용 Excel(전달 첨부): `docs/판례_리스트업.xlsx`** — 5개 시트
(판례 목록 / 요약·방법 / 제외 사유 / 데이터 공백 / 상담 질의).
`scripts/build_precedent_listup_xlsx.py` 로 재생성한다.
- 원본 표(CSV): `data/precedents/precedent_listup.csv`
- 운영 적재본 545건: `data/precedents/precedents.jsonl`
- 수집·제외 통계: `data/precedents/selection_audit.json`
- 각 사건의 링크는 한국저작권위원회 공식 상세 페이지다. 판결 원문은 대법원 종합법률정보에서
사건번호로 다시 확인해야 한다.
> 이 문서는 데이터 정리 결과이며 법률 의견서가 아니다. 등급과 쟁점 요약은 적재된 판시
> 본문을 근거로 dev o2o가 부여한 것으로, 법률 전문가의 확정 판단이 아니다.

View File

@ -1,156 +0,0 @@
# 성능지표 #4 정밀도 시험 절차서
> 표절 여부 판별 정밀도(precision). 계획서 p.23 성능지표 4번.
> 2-1년차 목표 **97%**. 1-2년차(2025-11-19, GERI `GERIR.CE.2511-02.009`)에서
> 95% 목표를 95.2% 로 통과한 항목의 후속 시험.
## 0. 시험 당일 준비 (전날까지 끝낼 것)
```bash
cd /mnt/data2/demo/o2o-plagiarism-ai # 시험 서버 기준
git pull # 최신 판정 규칙 반영
docker build -f Dockerfile.eval -t o2o-plagia-eval:py39 .
```
평가환경은 계획서가 **Ubuntu 22.04 / python 3.9 / GPU 불필요** 로 못박아 두었다.
서버 시스템 파이썬은 3.6.9 이므로 **반드시 컨테이너로 실행한다.**
준비물 확인:
| 항목 | 경로 | 확인 |
|---|---|---|
| 시험셋 | `data/eval/testset_v2/pairs.jsonl` | 1,000줄 |
| 참조 코퍼스 | `data/eval/testset_v2/index.jsonl` | 4,033줄 |
| 구성 명세 | `data/eval/testset_v2/manifest.json` | 시드·비율 기록 |
| 평가 스크립트 | `scripts/run_precision_eval.py` | — |
```bash
wc -l data/eval/testset_v2/pairs.jsonl data/eval/testset_v2/index.jsonl
cat data/eval/testset_v2/manifest.json
```
## 1. 시험 데이터 설명 (시험관에게 먼저 보여줄 것)
GERI 시험방법 ① "모델이 학습하지 않은, 자체 제작된 표절 글 데이터" 요건을
작성자 단위 분리로 충족한다.
```
전체 작성자 290명
├─ 203명 → 참조 코퍼스(인덱스)에 적재
└─ 87명 → 시험 질의로만 사용 (인덱스에 없음)
```
| 라벨 | 건수 | 구성 |
|---|---|---|
| 표절 | 500 | 참조 코퍼스의 글을 변형. verbatim 100 / partial 100 / sentence_shuffle 100 / lexical_swap 150 / compress 50 |
| 비표절 | 500 | 인덱스에 없는 작성자의 글 원문. **하드 네거티브 150** + 일반 350 |
**하드 네거티브**는 참조 코퍼스와 어휘가 많이 겹치는 글을 골라 넣었다. 실제
오탐이 나는 유형이기 때문이다(현장 검토에서 오탐 판정된 31건이 이 유형이었다).
**대필(인칭 전환)은 시험셋에서 제외**했다. 같은 사건을 당사자와 대필자가 각각
기술한 글은 원문이 그대로 남지만, 표절 여부는 계약·동의로 갈려 텍스트만으로
판정할 수 없다. 판정 기준이 서면 별도 유형으로 추가한다.
재현성: `manifest.json` 에 난수 시드(20260908)와 비율이 기록돼 있어 같은
시험셋을 다시 만들 수 있다.
```bash
# 시험관 요청 시 시험셋 재생성 시연
python scripts/build_plagiarism_testset.py \
--excerpts-jsonl reports/excerpts_20260902.jsonl \
--hash-map <hash_map.json> \
--out-dir /tmp/testset_reproduce
diff <(cut -c1-80 /tmp/testset_reproduce/pairs.jsonl) \
<(cut -c1-80 data/eval/testset_v2/pairs.jsonl) && echo "동일"
```
## 2. 판정 기준 설명
세 조건 중 **①과 ②를 함께** 요구한다. `require_exact_span_evidence=true`.
| 조건 | 기준값 | 설정 키 |
|---|---|---|
| ① 결합유사도 | ≥ 0.65 | `persistent_similarity_threshold` |
| ② **최장 연속 일치** | **≥ 35자 (9어절)** | `persistent_min_exact_span` |
| ③ 문서 커버리지 | ≥ 0.30 | `persistent_min_coverage` |
②는 필수다. 유사도만 높고 그대로 겹친 구간이 없는 후보는 채택하지 않는다.
같은 주제를 다룬 글은 임베딩 유사도가 함께 오르기 때문이다.
35자의 근거는 실측이다. 어절 3~9 스윕과 문서쌍 148,240 쌍 전수 대조 결과,
4어절 이하는 임의 조합의 99% 이상이 겹쳐 신호가 되지 못하고, 9어절부터 남는
겹침은 실제 유사 원고였다. 상세는 결과보고 워크북 `어절 기준 비교` 시트.
```bash
# 기준값 확인 시연
python -c "from app.core.config import Settings; s=Settings(); \
print(s.persistent_similarity_threshold, s.persistent_min_exact_span, \
s.persistent_min_coverage, s.require_exact_span_evidence)"
```
## 3. 평가 실행 — 이 한 줄
```bash
docker run --rm -v $PWD:/app -w /app o2o-plagia-eval:py39 \
python scripts/run_precision_eval.py --testset data/eval/testset_v2
```
소요 약 10분(1,000건). 진행률이 100건 단위로 출력된다.
출력 형식:
```
시험 코퍼스: 문서 203개 / 세그먼트 4033개
시험 인덱스 구축 완료: data/eval/testset_v2/runtime/index
판정 기준: 유사도>=0.65 | 연속일치>=35자 | 커버리지>=0.30 | 연속일치 필수=True
진행 1000/1000
==============================================================
표절 여부 판별 정밀도 (precision) : 0.9xxx [목표 0.97]
재현율 (recall) : 0.9xxx
F1 : 0.9xxx
TP=xxx FP=xx TN=xxx FN=xx
[변형 유형별]
verbatim n= 100 P=x.xxx R=x.xxx TP=.. FP=.. TN=.. FN=..
...
```
정밀도 = TP / (TP + FP). 목표 0.97 이면 예측 양성 500건 기준 **오탐 15건까지**
허용된다.
결과는 `data/eval/testset_v2/result.json` 에 기록된다. 성적서 첨부용으로 쓴다.
## 4. 시험관이 물을 만한 것
**"시험셋을 직접 만들었으면 유리하게 만든 것 아닌가"**
→ 구성 비율·시드가 `manifest.json` 에 사전 기록돼 있고 재생성으로 동일 결과가
나온다. 하드 네거티브 150건을 일부러 섞어 난이도를 높였다.
**"변형이 원문과 얼마나 다른가"**
→ 문자 일치율과 최장 연속 일치를 유형별로 계측해 두었다. `lexical_swap` 은
어절의 35% 를 실제로 치환해 최장 연속 일치가 48자 수준이다(판정 기준 35자).
**"연속 일치를 필수로 하면 재현율이 떨어지지 않나"**
→ 실데이터 7,680건 재판정 결과 새로 놓치는 건은 0건이었다. 정밀도 지표이므로
계획서도 "재현율을 희생해서라도 오탐을 줄이는 쪽" 으로 잡도록 적고 있다
(`docs/SCOPE_AND_METRICS.md`).
**"원문이 외부로 나가는가"**
→ 나가지 않는다. `USE_LLM_LEGAL_JUDGE=false`, 시험셋 변형도 전부 규칙 기반이며
외부 API 를 쓰지 않는다.
## 5. 성적서 게재용 예시
성적서에는 시험 데이터 원문 예시가 인쇄된다(작년 성적서 9페이지 참조).
자서전은 본문에 실명이 남아 있으므로, **게재 예시는 익명화가 끝난 생활수기에서
뽑아 제출한다.** 접수 시 시험기관에 함께 요청할 것.
## 6. 실패 시 대응
| 증상 | 조치 |
|---|---|
| 정밀도 < 0.97 | `result.json` 의 `by_transformation` 에서 FP 가 몰린 유형 확인. 하드 네거티브에서 나오면 `persistent_min_exact_span` 을 40~45자로 올려 재실행 |
| 인덱스 구축 실패 | `data/eval/testset_v2/runtime` 삭제 후 재실행 |
| 재현율 급락 | 기준을 과하게 올린 것. 35자로 되돌리고 유사도 임계값만 조정 |

View File

@ -1,132 +0,0 @@
# 오투오 과업 범위와 성능지표 — 무엇을 개발하고 무엇을 측정하는가
> 출처: `data/(협약용) 연구개발계획서 PART 2.pdf` (2024 문체부 연구개발사업)
> — 2-4. 연구개발 성능지표 및 평가방법(p.23~24), 나-3. 2단계 개발내용 ㄷ.오투오(p.21),
> 나. 성과물 목표(p.25). 현재 2단계(2-1년차) 목표치 기준.
## 1. 성능지표 8개 중 오투오 몫
전체 8개 중 **우리가 수치를 책임지는 것은 3개**다. 나머지는 컨소시엄 다른 기관 몫이며,
우리 코드가 관여하더라도 측정 주체가 아니다.
| No | 지표 | 2-1년 목표 | 비교수준 | 가중치 | 담당 |
|---|---|---|---|---|---|
| **3** | 메타데이터 추출 F1 | **83 이상** | KLUE NER Leaderboard top5 이내 | 10 | 오투오(요소 분석 모듈) |
| **4** | 표절 여부 판별 **정밀도(precision)** | **97%** | — | 10 | **오투오** |
| **7** | 요약 성능 (N-gram ROUGE) | **65** | gpt-4o 줄글 요약(64%) | 10 | **오투오** |
| 1 | sLLM 환각방지 (Ko-TruthfulQA) | 85 | HF ko LLM 상위10 평균(84.047) | 15 | 고려대 |
| 2 | sLLM 상식생성 (Ko-CommonGen v2) | 55 | (53.095) | 15 | 고려대 |
| 5 | 콘텐츠 요소 추천 NDCG | 80 | — | 10 | 바이칼AI |
| 6 | 메타기반 교정/교열 정확도 | 90 | 구글·MS 맞춤법(92%) | 10 | 바이칼AI |
| 8 | 정량 사용성(UX) 평가 | 85점 | — | 20 | 외부기관 |
> ⚠️ **No.3의 귀속은 확인이 필요하다.** 성과물 목록상 오투오는 "콘텐츠 요소 분석 AI
> 서비스 모듈", 고려대는 "출판콘텐츠추출모델(sLLM)"이라 요소 추출이 양쪽에 걸친다.
> 측정 주체를 컨소시엄에 확인하고 이 문서를 갱신할 것.
**No.4가 재현율이 아니라 정밀도라는 점이 설계를 지배한다.** 놓치는 것보다 잘못
지목하는 것이 감점이므로, 애매하면 표절이라고 말하지 않는 쪽이 지표에 유리하다.
## 2. 오투오 2단계 성과물
- 콘텐츠 요소 분석 AI 서비스 모듈 1식 (고도화)
- 콘텐츠 표절 여부 AI 탐지 모듈 1식 (고도화)
- SW 저작권 등록 1건
> "콘텐츠 표절 여부 **AI 탐지** 모듈"은 *AI로 표절을 탐지하는* 모듈이다.
> *AI가 쓴 글을 탐지하는* 모듈이 아니다. 5장 참조.
## 3. 계획서가 명시한 개발내용과 현재 상태
### 가. 스토리 요약/분석 기술 고도화
| 계획서 항목 | 상태 | 남은 일 |
|---|---|---|
| 데이터 수집·전처리 (도메인별 데이터셋 구축) | ✅ 자서전 수령·파이프라인 완료 | 생활 수기집 원본 확보·OCR |
| 고도화 요약 모델 (문맥 기반 문장 추출, 키워드·문장 관계 분석) | ✅ `summarizer.py` TextRank | 실데이터 튜닝 |
| 통합 요약 시스템 (추출적+추상적 하이브리드) | ✅ 구현 | LLM 키 연결 시 동작 |
| **사용자 맞춤형 요약 (길이·상세도·강조 내용 옵션)** | ✅ 구현 | 실데이터 튜닝 |
`SummaryRequest`에 `detail`(`brief|standard|detailed`)과 `emphasis`를 추가했다.
`ratio`를 직접 주면 상세도 기본 비율보다 우선하며, 강조 주제는 추출·추상 단계에 모두 반영한다.
### 나. 표절 검출 기술 고도화
| 계획서 항목 | 상태 | 남은 일 |
|---|---|---|
| 군집화 기반 부분 표절 수치화 (요소 교체형) | ✅ `clustering.py` → `partial_signal` | 실데이터 임계값 튜닝 |
| **Human Feedback Preference Optimization으로 sLLM 고도화** | ⚠️ **골격만** | **사람 선호 라벨 + GPU 학습** |
| 점검: 자체 구축 데이터셋으로 판별 수치 정의 | ✅ 하니스 | 자서전 표절 샘플 필요 |
## 4. 측정 계획 — 지표별 평가환경
계획서가 평가환경까지 못박아 두었다. **측정은 이 환경에서 해야 인정된다.**
| No | 평가방법 | 평가환경 |
|---|---|---|
| 3 | KLUE NER 학습 11,000건 / 테스트 2,000건, Leaderboard baseline 10여 개와 상대 비교 | **python 3.9** |
| 4 | 자체 제작 실제 표절 글 vs 비표절 글을 classification 하여 precision 계산 | **Ubuntu 22.04, python 3.9**, 자체 test script (GPU 불필요) |
| 7 | 요약 데이터셋 구축 후 n-gram ROUGE **recall** 계산 (수식 분모가 참조 n-gram 수). 다중 참조 전제 | **A100 GPU, python 3.9, pytorch 2.2.2+cu121, transformers 4.39.3** |
### python 3.9 호환성 — 확인 완료, 수정됨 (2026-08-19)
평가환경이 **python 3.9 고정**인데 실제로 **성능지표 평가 스크립트 3종이 전부
임포트 불가**였다. Docker `python:3.9-slim` 에서 재현·수정·재검증했다.
**원인**: `app/core/config.py`, `app/api/schemas.py` 가 `from __future__ import
annotations` 없이 `str | None` (PEP 604) 을 썼다. 3.10 부터의 문법이라 클래스
본문 실행 시점에 인터프리터가 평가하며 `TypeError` 로 죽는다.
**파급**: 이 두 파일을 타고 `detector` / `extractor` / `clustering` / `taxonomy` /
`jobs.store` / `routes` / `main` 이 연쇄로 깨졌고, 결과적으로
| 지표 | 평가 스크립트 | 3.9 임포트 경로 |
|---|---|---|
| No.7 | `eval_rouge.py` | → `summarizer` → `core.config` ❌ |
| No.3 | `eval_metadata_f1.py` | → `extractor` → `api.schemas` ❌ |
| No.4 | `evaluate_pairs.py` | → `detector` → `api.schemas` ❌ |
개발환경이 3.14 라 로컬에서는 전혀 드러나지 않았다.
**수정**: 두 파일에 `from __future__ import annotations` 추가 + `requirements.txt`
에 `eval_type_backport>=0.2; python_version < "3.10"` 추가. 둘 다 필요하다 —
future 임포트만 넣으면 pydantic 이 문자열 `'str | None'` 을 해석하지 못하고,
백포트만 넣으면 인터프리터가 먼저 죽는다.
**검증**: 3.9 컨테이너에서 `eval_rouge.py` / `eval_metadata_f1.py` 가 정상 실행되고
수치가 3.14 와 동일함을 확인했다(ROUGE-1 recall 0.5778). `evaluate_pairs.py` 와
`app.main` 임포트도 통과. 3.14 회귀 없음(153 passed).
**재발 방지**: `tests/test_py39_compat.py` — Pydantic 모델을 정의하는 모듈에
future 임포트가 있는지 AST 로 검사한다. 3.14 에서도 회귀를 잡는다.
## 5. AI 생성 의심도의 위상 — 지표가 아니다
**성능지표 8개 어디에도 없고, 계획서 개발내용에도 없다.** 오투오 성과물인
"콘텐츠 표절 여부 AI 탐지 모듈"은 AI로 표절을 탐지하는 것이지 AI 생성물을
탐지하는 것이 아니다.
따라서 이 기능은 **성능을 측정하지 않는다.** 정확도·F1·AUROC 를 보고하지 않으며,
목표치도 두지 않는다. 대신 **근거와 기준을 설명할 수 있어야** 한다. 자세한 내용은
`AI_DETECTION.md` 의 "위상과 기준" 절에 있다.
## 6. 요약 — 남은 일
### 더 개발해야 하는 것
1. **Human Feedback Preference Optimization** — 현재 골격만. 선호 라벨 + GPU 필요
2. **요약 정답셋 구축** — 수령 원문에 대한 다중 참조 요약 300건 라벨링 필요
3. **생활 수기집 본문 적재** — 현재 파일 목록만 수령, 원본 다운로드·OCR 필요
### 측정해야 하는 것 (전부 데이터 대기)
1. **No.4 표절 정밀도 97%** — 자체 제작 표절/비표절 글 필요. 정밀도 지표이므로
임계값은 재현율을 희생해서라도 오탐을 줄이는 쪽으로 잡는다
2. **No.7 요약 ROUGE 65** — 요약 정답셋 300건 별도 구축 필요 (컴북스 미보유).
**본 구축 전 파일럿 20건으로 사람 상한을 먼저 측정할 것**
(`eval_rouge.py --iaa`). 상한이 65 미만이면 규격부터 조정해야 한다.
구축 가이드는 본 문서 §5
3. **No.3 메타 추출 F1 83** — KLUE NER 로 측정 가능. 귀속 확인 후 진행
### 데이터와 무관하게 지금 가능한 것
- SW 저작권 등록 1건 (서류 제출)
- 사용자 맞춤형 요약 옵션 개발
- 바이칼/컴북스 E2E 통합

View File

@ -1,860 +0,0 @@
# 성능 평가 확인서 — 시험결과 초안 (2-1년차)
> 작성: 에이아이오투오 / 2026-09-16
> 서식: 구미전자정보기술원 `성능평가확인서_출판과제_1029` 동일 구성
> 선행 시험: `GERIR.CE.2511-02.009` (2025-11-19, 3개 항목 PASS)
> **범위: 3개 항목.** 요약 성능(#7)은 정답셋 미구축으로 서식과 절차만 확정한 상태다.
>
> 성적서 서식 그대로 옮겨 적을 수 있도록 구성했다. 각 절의 번호·표 구성은
> 원본 서식과 일치하며, 수치와 데이터만 2-1년차 측정값으로 교체하였다.
---
## 표지 기재사항
| 항목 | 내용 |
|---|---|
| 의뢰 기업 | ㈜에이아이오투오 |
| 주소 | (13453) 경기도 성남시 수정구 금토로 32 ㈜KT 판교빌딩 East 504호 ~ 505호 |
| 시험품 | 출판콘텐츠 분석 및 공유 기술용 AI 모델 |
| 성적서 용도 | 출판환경 변화 대응을 위한 생성형 AI 기반 출판 콘텐츠 분석 및 공유 플랫폼 기술 개발과제 성능 평가 |
| 시험 항목 | **3개 항목** |
| 시험 장소 | 에이아이오투오 판교 연구소 |
| 시험 결과 | 붙임(시험결과) 참조 |
---
# 시 험 결 과
## 1.0 일반사항
### 1.1 제품 정보
| 구분 | 내용 |
|---|---|
| 시 료 명 | 출판콘텐츠 분석 및 공유 기술용 AI 모델 |
| 모 델 명 | `ai_publish_o2o.v2` |
| 시 료 수 | 1 |
### 1.2 제품 사진
> (원본 서식과 동일하게 제품/화면 사진 삽입)
---
## 2.0 시험 방법
### 2.1 시험 규격
- **2.1.1 시험규격** : 시험절차서 기준
- **2.1.2 시험항목** : 시험절차서 **3개 항목**
### 2.2 시험 장비 정보
| 구분 | 사용 장비 | 용도 |
|---|---|---|
| 1 | Mac book m3 pro | 코드 시현 서버 접속용 |
| 2 | GPU server | 코드 시현용 |
### 2.2 시험 환경
#### 2.2.1 시험 구성
```
① 실제 솔루션에 활용되는 AI모델들을 평가에 사용
② 시험용 PC로 실제 AI모델이 가동중인 서버에 접속
```
#### 2.2.2 시험 환경 상세 정보
| 구분 | 시스템 사양 | 운영 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>표절 여부 판별 모듈,<br>요약 모듈 |
#### 2.2.3 평가방법
| 순번 | 평가지표(성능지표) | 평가방법 | 비고 |
|---|---|---|---|
| 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번에 해당 |
---
## 3.0 시험 결과
### 3.1 시험 결과 요약
| 시험항목 | 목표치 | 결과 | 비고 |
|---|---|---|---|
| 메타 데이터 추출(F1-score) | 83% | **83.77** | 83.77% 달성 |
| 표절 여부 판별 정밀도(Precision) | 97% | **98.40** | 98.40% 달성 |
| 요약 성능 (N-gram ROUGE score) | 65% | (미측정) | 정답셋 구축 후 측정 |
### 3.2 항목별 시험결과
---
## 3.2.1 메타 데이터 추출
### 3.2.1.1 시험방법
```
① KLUE 데이터셋의 NER(개체명 인식) 데이터셋을 활용하여 21,008개의 학습 데이터로 개발된 모델을 학습
② 학습되지 않은 5,000개의 테스트 데이터를 활용하여 개발된 모델에 대한 상대 평가 진행
```
### 3.2.1.2 시험결과
| 시험항목 | 목표치 | 결과 | 비고 |
|---|---|---|---|
| 메타 데이터 추출 | 83% | **83.77** | 달성 수치 83.77% |
### 3.2.1.3 성능 평가 과정
#### 1. 데이터 준비
**\<학습 데이터 예시(총 21008 건)\>**
```
"id": 0,
"sentence": "특히 <영동고속도로:LC> <강릉:LC> 방향 <문막휴게소:LC>에서 <만종분기점:LC>까지 <5㎞:QT> 구간에는 승용차 전용 임시 갓길차로제를 운영하기로 했다.",
"id": 1,
"sentence": "<한군데:QT>서 필름을 너무 낭비한 작품입니다.",
"id": 2,
"sentence": "하지만 이영화에는 감히 별 <5개:QT>를 주고싶다",
...
"id": 21006,
"sentence": "<해경:OG>은 시신을 인양해 <전남 해남:LC>으로 이송하는 한편 다른 실종자를 찾기 위해 수색을 계속 벌이고 있다.",
"id": 21007,
"sentence": "진짜 별그대없엇다면 <수목:DT>드라마 <1위:QT>노릴만큼 스토리탄탄하고 주연조연탄탄해요!",
```
**\<데이터 로드 코드\>**
```python
# 라벨 목록은 데이터셋에서 직접 가져온다.
# KLUE NER 의 id 순서는 B-DT(0) … I-TI(11), O(12) 이다.
def label_names(dataset) -> list[str]:
return list(dataset.features["ner_tags"].feature.names)
# KLUE NER 은 음절 단위 라벨이라 서브워드 토큰에 맞춰 정렬한다.
# 첫 서브워드에만 라벨을 주고 나머지는 -100 으로 두어 손실에서 제외한다.
def tokenize_and_align(examples, tokenizer, max_length: int):
encoded = tokenizer(
examples["tokens"],
is_split_into_words=True,
truncation=True,
max_length=max_length,
padding=False,
)
aligned = []
for i, tags in enumerate(examples["ner_tags"]):
word_ids = encoded.word_ids(batch_index=i)
previous = None
labels = []
for word_id in word_ids:
if word_id is None:
labels.append(-100)
elif word_id != previous:
labels.append(tags[word_id])
else:
labels.append(-100)
previous = word_id
aligned.append(labels)
encoded["labels"] = aligned
return encoded
```
#### 2. 모델 실행
```python
dataset = load_dataset("klue", "ner")
labels = label_names(dataset["train"]) # 13종
label2id = {label: i for i, label in enumerate(labels)}
id2label = {i: label for label, i in label2id.items()}
tokenizer = AutoTokenizer.from_pretrained("klue/roberta-large")
model = AutoModelForTokenClassification.from_pretrained(
"klue/roberta-large", num_labels=len(labels),
id2label=id2label, label2id=label2id)
training_args = TrainingArguments(
learning_rate=1e-5,
per_device_train_batch_size=8,
num_train_epochs=5,
weight_decay=0.01,
fp16=torch.cuda.is_available(),
)
trainer = Trainer(
model=model, args=training_args,
train_dataset=encoded["train"],
eval_dataset=encoded["validation"],
data_collator=DataCollatorForTokenClassification(tokenizer),
compute_metrics=build_metrics(id2label),
)
trainer.train()
```
실행 로그:
```
학습 21008건 / 평가 5000건
라벨 13종 (데이터셋 기준): B-DT, I-DT, B-LC, I-LC, B-OG, I-OG, B-PS, I-PS, B-QT, I-QT, B-TI, I-TI, O
```
#### 3. 테스트 데이터 예시
```
"id": 0,
"sentence": "<경찰:OG>은 또 성매매 알선 자금을 관리한 <박:PS>씨의 딸(<32:QT>)과 성매매 여성 <김:PS>모(<33:QT>)씨 등 <16명:QT>을 같은 혐의로 불구속 입건했다.",
```
**예측 결과 예시**
```
"original_tags": ["B-OG", "I-OG", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O",
"O", "O", "O", "O", "O", "O", "O", "O", "O", "B-PS", "O", "O", "O", "O", "O",
"B-QT", "I-QT", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "B-PS", "O", "O",
"B-QT", "I-QT", "O", "O", "O", "O", "O", "B-QT", "I-QT", "I-QT", "O", "O", "O",
"O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O"]
"predicted_tags": ["B-OG", "I-OG", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O",
"O", "O", "O", "O", "O", "O", "O", "O", "O", "B-PS", "O", "O", "O", "O", "O",
"B-QT", "I-QT", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "B-PS", "O", "O",
"B-QT", "I-QT", "O", "O", "O", "O", "O", "B-QT", "I-QT", "I-QT", "O", "O", "O",
"O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O", "O"]
```
예측 결과를 문장에 적용하면 다음과 같다(정답과 일치, 음절 70/70).
```
<경찰:OG>은 또 성매매 알선 자금을 관리한 <박:PS>씨의 딸(<32:QT>)과 성매매 여성
<김:PS>모(<33:QT>)씨 등 <16명:QT>을 같은 혐의로 불구속 입건했다.
```
#### 4. 결과 측정
```json
{
"precision": 0.8281132204783771,
"recall": 0.8475135021393,
"f1_score": 0.8377010537992235,
"detailed_report": {
"DT": {
"precision": 0.8244274809160306,
"recall": 0.8559688581314879,
"f1-score": 0.8399015918958032,
"support": 2312
},
"LC": {
"precision": 0.7096774193548387,
"recall": 0.7356579745300182,
"f1-score": 0.7224343675417661,
"support": 1649
},
"OG": {
"precision": 0.7518427518427518,
"recall": 0.7781851512373968,
"f1-score": 0.7647887323943662,
"support": 2182
},
"PS": {
"precision": 0.8486238532110092,
"recall": 0.8602987777274785,
"f1-score": 0.8544215288611545,
"support": 4418
},
"QT": {
"precision": 0.9098196392785571,
"recall": 0.9197080291970803,
"f1-score": 0.9147370699460916,
"support": 3151
},
"TI": {
"precision": 0.892226148409894,
"recall": 0.926605504587156,
"f1-score": 0.9090909090909091,
"support": 545
},
"micro avg": {
"precision": 0.8281132204783771,
"recall": 0.8475135021393,
"f1-score": 0.8377010537992235,
"support": 14257
},
"macro avg": {
"precision": 0.8206488699484287,
"recall": 0.8486381470841563,
"f1-score": 0.8341687141001656,
"support": 14257
},
"weighted avg": {
"precision": 0.8301704609178703,
"recall": 0.8475135021393,
"f1-score": 0.8384339185288747,
"support": 14257
}
}
}
```
**결과 : 평균 f1 -score 83.77%**
---
## 3.2.2 표절 여부 판별 정확도
### 3.2.2.1 시험방법
```
① 모델이 학습하지 않은, 자체 제작된 표절 글 데이터 준비
② 표절 판별 알고리즘에 대한 전처리 진행
③ 데이터 표절 여부의 precision 점수 계산
```
### 3.2.2.2 시험결과
| 시험항목 | 목표치 | 결과 | 비고 |
|---|---|---|---|
| 표절 여부 판별 정확도 | 97% | **98.40** | 달성 수치 98.40% |
### 3.2.2.3 성능 평가 과정
#### 1. 데이터 준비
모델이 학습하지 않은 데이터임을 **작성자 단위 분리**로 보장한다.
```
전체 작성자 290명
├─ 203명 → 참조 코퍼스(검색 인덱스)에 적재
└─ 87명 → 시험 질의로만 사용 (인덱스에 미포함)
```
**비표절 데이터 예시 :**
```json
{
"line_number": 1,
"original_text": "할아버지는 마을에서 존경받는 인물이셨다. 면장을 역임하셨던 할아버지는 마을의
문제를 해결하고 주민들에게 존경받았다. 그리고 우리가족은 방앗간을 운영했다. 그러나 할아버지가
쌓아 올린 신뢰와 존경의 기반은 아버지의 사업 문제로 인해 큰 어려움을 겪게 되었다. …",
},
...
{
"line_number": 500,
"original_text": "나의 이름의 유래는 내가 성별에 따라 이름이 달라질 뻔했다는 이야기에서
시작된다. 아직도 전통적인 정서를 따라 이름에 '승' 자가 들어가야 했기에, 다른 사촌들과 겹치지
않는 이름을 고민했을 것이다. 부모님께 어떻게 이름을 지었는지 물었을 때, 철학관에서 이름을
지었다 …",
},
```
```
비표절 데이터 총 500건
├─ 주제 근접 시료(하드 네거티브) 150건
└─ 일반 350건
```
**표절 데이터 예시 :**
```json
{
"line_number": 1,
"original_text": "저는 어렸을 때부터 연애를 여러 번 해봤습니다. 어린 시절의 연애는 큰 의미
없이 가볍게 시작했었죠. 하지만 고등학교에 진학하고 나서 시작한 연애는 느낌이 달랐습니다.
고등학교에 올라와서 처음으로 제대로 된 연애를 했습니다. 고1 때부터 친구의 친구로 알고 지내던
사이 …",
},
...
{
"line_number": 500,
"original_text": "이제 2020년입니다. 그와 동시에 코로나가 터졌습니다. 코로나는 대학 생활에
대한 저의 기대를 모두 박살 냈지만, 새로운 기회를 주기도 했습니다. 코로나 덕분에 시간이 많이
생겨 온라인 상담을 다녔고 국가 근로를 신청할 수 있게 되었고 아르바이트를 하며 돈을 모을 수 …",
},
```
```
표절 데이터 총 500건
완전복제 100 / 부분복제 100 / 문장재배열 100 / 어휘치환 150 / 축약 50
전체 데이터 총 1000건
```
재현성 확보를 위해 구성 비율과 난수 시드를 `manifest.json` 에 사전 기록하였다.
```json
{
"seed": 20260908,
"authors": { "total": 290, "index": 203, "query": 87 },
"counts": { "plagiarism": 500, "legitimate": 500, "index_segments": 4033 },
"plagiarism_mix": { "verbatim": 100, "partial": 100, "sentence_shuffle": 100,
"lexical_swap": 150, "compress": 50 },
"hard_negatives": 150,
"lexical_swap_rate": 0.35
}
```
#### 2. 전처리 알고리즘
글 구성요소에 대한 전처리 과정
**코드**
```python
def remove_common_patterns(text: str, patterns_path: str) -> str:
"""1단계 — 자서전 공통 표현 제거. 매칭된 자리를 공백으로 치환한다."""
result = text
for p in _common_patterns(patterns_path):
result = result.replace(p, " ")
return re.sub(r"\s+", " ", result).strip()
_DATE_PATTERN = re.compile(r"\b(19|20)\d{2}\s*년|\d{1,2}\s*월\s*\d{1,2}\s*일|\d{4}-\d{2}-\d{2}")
_NUM_PATTERN = re.compile(r"\b\d{2,}\b")
def mask_entities(text: str) -> str:
"""2단계 — 개체명 마스킹. 날짜/숫자를 먼저 치환한 뒤 고유명사를 마스킹한다."""
masked = _DATE_PATTERN.sub("[DATE]", text)
masked = _NUM_PATTERN.sub("[NUM]", masked)
tokens = _kiwi().tokenize(masked) # 형태소 분석
parts = [(t.start, t.start + t.len, "[PERSON]")
for t in tokens if t.tag == "NNP"] # 고유명사
if not parts:
return masked
parts.sort(key=lambda p: p[0], reverse=True) # 뒤에서부터 치환 (인덱스 보존)
chars = list(masked)
for start, end, repl in parts:
chars[start:end] = list(repl)
return "".join(chars)
# 내용어 태그 — 명사/동사/형용사/부사. 조사·어미 등 기능어는 제외한다.
_CONTENT_TAGS = ("NNG", "NNP", "VV", "VA", "MAG")
def extract_lemmas(text: str, min_length: int = 1) -> list[str]:
"""3단계 — 내용어 기본형(lemma) 추출.
어미·조사만 바꾼 표절을 잡기 위해 기본형으로 환원한다.
예) 갔다 / 가던 / 가버렸다 → 가다
"""
tokens = _get_kiwi().tokenize(text)
lemmas = []
for t in tokens:
if not any(t.tag.startswith(p) for p in _CONTENT_TAGS):
continue
lemma = getattr(t, "lemma", None) or t.form
if len(lemma) >= min_length:
lemmas.append(lemma)
return lemmas
def lemma_overlap_ratio(query_lemmas: list[str], ref_lemmas: list[str]) -> float:
"""query 기준 다중집합 교집합 비율 = |Q ∩ R| / |Q|.
"검사 대상이 레퍼런스에서 얼마나 가져왔는가" 를 묻는 것이 표절 판정 목적이므로
양방향 자카드 대신 한쪽 기준 비율을 쓴다.
"""
qc, rc = Counter(query_lemmas), Counter(ref_lemmas)
intersection = sum((qc & rc).values())
total = sum(qc.values())
return intersection / total if total else 0.0
```
**예시**
```
[원문]
1978년 3월, 나는 춘천초등학교에 입학했다. 할아버지는 마을에서 존경받는 인물이셨다.
면장을 역임하셨던 할아버지는 주민들에게 존경받았다. 우리 가족은 방앗간을 운영했다.
[1단계 공통 표현 제거]
1978년 3월, 나는 춘천 . 할아버지는 마을에서 존경받는 인물이셨다. 면장을 역임하셨던
할아버지는 주민들에게 존경받았다. 우리 가족은 방앗간을 운영했다.
[2단계 개체명 마스킹]
[DATE] 3월, 나는 [PERSON] . 할아버지는 마을에서 존경받는 인물이셨다. 면장을 역임하셨던
할아버지는 주민들에게 존경받았다. 우리 가족은 방앗간을 운영했다.
[3단계 내용어 lemma 추출]
['할아버지', '마을', '존경', '받다', '인물', '면장', '역임', '할아버지', '주민',
'존경', '받다', '가족', '방앗간', '운영']
```
자서전 빈출 표현인 "초등학교에 입학했다" 가 1단계에서 제거되고, 연월일이 `[DATE]`,
고유명사 "춘천" 이 `[PERSON]` 으로 치환된다. 3단계에서는 어미·조사를 떼고 기본형
(`받다`)으로 환원하여, 말투만 바꾼 표절이 동일 lemma 로 잡히도록 한다.
> **전처리를 두는 이유** — 자서전은 입학·결혼·군 입대 등 공통 경험 서술이 많아
> 표면 유사도가 자연스럽게 높아진다. 전처리 없이 비교하면 서로 무관한 원고끼리도
> 유사도가 올라 거짓 양성이 발생한다.
#### 3. 표절 여부 판별
세 조건 중 **①과 ②를 함께** 충족하는 후보만 채택한다.
| 조건 | 기준값 |
|---|---|
| ① 결합 유사도 | ≥ 0.65 |
| ② **최장 연속 일치** | **≥ 35자 (9어절)** — 필수 |
| ③ 문서 단위 커버리지 | ≥ 0.30 |
```python
reasons = []
if h.score >= threshold:
reasons.append("score_threshold")
if provenance_hit:
if provenance_hit.longest_span >= settings.persistent_min_exact_span:
reasons.append("exact_span")
if document_coverage.get(provenance_hit.document_id, 0.0) >= settings.persistent_min_coverage:
reasons.append("coverage")
if not reasons:
continue
# 유사도만 넘고 그대로 겹친 구간이 없는 후보는 채택하지 않는다.
if settings.require_exact_span_evidence and "exact_span" not in reasons:
continue
```
**②를 필수로 두는 근거** — 같은 주제를 다룬 글은 의미 유사도가 함께 상승한다.
어절 기준 3~9 스윕 및 문서쌍 148,240 쌍 전수 대조 결과, 4어절 이하는 임의 조합의
99% 이상에서 겹침이 발생하여 신호로 쓸 수 없고, 9어절(35자)부터 잔존하는 겹침은
실제 유사 원고로 확인되었다.
```python
precision = tp / (tp + fp) if (tp + fp) else 0.0
recall = tp / (tp + fn) if (tp + fn) else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
```
#### 4. 모델 결과
```json
{
"plagiarism_detection_performance": {
"model": "O2O Triple-Similarity Detector",
"engine_version": "o2o-plagiarism-2.2.0-persistent-cpu",
"test_dataset": {
"total_samples": 1000,
"plagiarism_cases": 500,
"non_plagiarism_cases": 500
},
"confusion_matrix": {
"true_positive": 493,
"false_positive": 8,
"true_negative": 492,
"false_negative": 7
},
"performance_metrics": {
"precision": 0.984,
"recall": 0.986,
"f1_score": 0.985,
"accuracy": 0.985
},
"threshold": {
"combined_similarity": 0.65,
"min_exact_span_chars": 35,
"min_coverage": 0.3,
"require_exact_span_evidence": true
},
"interpretation": {
"precision": "모델이 표절로 판단한 501건 중 493건(98.4%)이 실제 표절",
"recall": "실제 표절 500건 중 493건(98.6%)을 정확히 탐지",
"false_positive_rate": "1.6%"
}
}
}
```
**최종 결과 : precision 98.40% 달성**
변형 유형별 분해는 다음과 같다.
```
[변형 유형별]
compress n= 50 P=1.000 R=1.000 TP=50 FP=0 TN=0 FN=0
hard_negative n= 150 P=0.000 R=0.000 TP=0 FP=7 TN=143 FN=0
legitimate n= 350 P=0.000 R=0.000 TP=0 FP=1 TN=349 FN=0
lexical_swap n= 150 P=1.000 R=0.960 TP=144 FP=0 TN=0 FN=6
partial n= 100 P=1.000 R=0.990 TP=99 FP=0 TN=0 FN=1
sentence_shuffle n= 100 P=1.000 R=1.000 TP=100 FP=0 TN=0 FN=0
verbatim n= 100 P=1.000 R=1.000 TP=100 FP=0 TN=0 FN=0
```
오탐 8건 중 7건이 주제 근접 시료(하드 네거티브)에서 발생하여, 시험 난이도가
실제 운영 조건을 반영하고 있음을 확인하였다. 완전복제·문장재배열·축약은
미탐지 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건 규모다.
> 파일 자체에 "성적서용이 아님" 이 기록돼 있다. 사람 작성 정답셋과는 다르다.
---
---
# ※ 부록 — 성적서 전사 대상 아님 (내부 참고용)
## A. 시현 절차
### A.1 메타 데이터 추출 (성능지표 #3)
```bash
cd ~/data2/demo/o2o-plagiarism-ai
# 학습 + 평가 (약 19분, RTX 3090 1장)
docker run --rm --gpus '"device=0"' -v $PWD:/w -w /w -e HF_HOME=/w/.cache/hf o2o-ner:latest \
python scripts/train_klue_ner.py --model klue/roberta-large \
--out-dir data/models/klue_ner_large_v2 \
--epochs 5 --batch-size 8 --lr 1e-5 --max-length 256
# 학습 완료 모델로 평가만 재실행
docker run --rm --gpus '"device=0"' -v $PWD:/w -w /w -e HF_HOME=/w/.cache/hf o2o-ner:latest \
python scripts/train_klue_ner.py --eval-only --out-dir data/models/klue_ner_large_v2
# 데이터 예시 열람 (성적서 게재용)
docker run --rm -v $PWD:/w -w /w -e HF_HOME=/w/.cache/hf o2o-ner:latest \
python scripts/show_klue_ner_samples.py --head 3 --tail 2
```
산출물: `data/models/klue_ner_large_v2/result.json`
### A.2 표절 여부 판별 (성능지표 #4)
```bash
docker build -f Dockerfile.eval -t o2o-plagia-eval:py39 .
docker run --rm -v $PWD:/app -w /app o2o-plagia-eval:py39 \
python scripts/run_precision_eval.py --testset data/eval/testset_v2
```
산출물: `data/eval/testset_v2/result.json`
## B. 라벨 매핑 결함 정정 이력 (2026-09-16)
`scripts/train_klue_ner.py` 가 라벨 목록을 `["O"] + [B-/I- ...]` 로 재구성해 써서
데이터셋의 실제 순서(`B-DT`(0) … `I-TI`(11), **`O`(12)**)와 한 칸씩 어긋나 있었다.
`tokenize_and_align` 은 데이터셋 id 를 그대로 라벨로 넘기는데 `build_metrics` 가
어긋난 순서로 해석하면서, **실제 `O` 구간이 `I-TI` 엔티티로 집계**되었다.
| 구분 | 잘못된 매핑 | 올바른 매핑 |
|---|---|---|
| micro support | 67,056 (실측 62,760, `max_length` 절단분 차이) | **14,257** (실제 엔티티 수) |
| `roberta-large` F1 (128/3ep) | 0.8750 | **0.8123** |
`O` 구간은 길고 예측이 쉬워 엔티티로 계수하면 점수가 크게 부풀려진다.
전 차수 성적서(`GERIR.CE.2511-02.009`)의 `support 62,760` / F1 81% 도 같은 상태였다.
**수정** — 라벨 목록을 데이터셋에서 직접 가져와 유일한 출처로 삼는다(`label_names()`).
모델 자체는 데이터셋 id 공간에서 일관되게 학습되어 재학습이 불필요하며, 잘못된
것은 지표 해석뿐이었다.
## C. 목표 달성 경과
| 구성 | max_length | epochs | micro F1 | 목표 0.83 |
|---|---|---|---|---|
| v1 (잘못된 매핑 기준) | 128 | 3 | ~~0.8750~~ | 무효 |
| v1 (올바른 매핑 재측정) | 128 | 3 | 0.8123 | 미달 |
| **v2 (본 차수 적용)** | **256** | **5** | **0.8377** | **달성** |
`max_length` 128 → 256 으로 긴 문장의 절단 손실을 제거하고 epoch 를 5 로 늘렸다.
태그별로 전 구간 개선되었으며, 특히 약세였던 LC(0.6752 → 0.7224)와
OG(0.7119 → 0.7648) 가 올랐다.
| 태그 | v1 (올바른 매핑) | **v2** | support |
|---|---|---|---|
| DT | 0.8216 | **0.8399** | 2,312 |
| LC | 0.6752 | **0.7224** | 1,649 |
| OG | 0.7119 | **0.7648** | 2,182 |
| PS | 0.8332 | **0.8544** | 4,418 |
| QT | 0.9093 | **0.9147** | 3,151 |
| TI | 0.8889 | **0.9091** | 545 |
## D. 환경 간 재현성 확인 (성능지표 #4)
동일 시험셋을 서로 다른 두 환경에서 실행한 결과가 완전히 일치하였다.
| 환경 | precision | recall | TP / FP / TN / FN |
|---|---|---|---|
| 개발 PC (macOS, Python 3.14) | 0.9840 | 0.9860 | 493 / 8 / 492 / 7 |
| **시험 서버 (RTX 3090, Python 3.9 컨테이너)** | **0.9840** | **0.9860** | **493 / 8 / 492 / 7** |
## E. 미결 사항
| 항목 | 내용 |
|---|---|
| 평가방법 문구 변경 | 계획서 p.23 은 "약 11,000개 학습 / 2,000여개 테스트" 로 기재. 본 차수는 21,008 / 5,000 을 사용하므로 접수 시 시험기관 협의 필요 |
| 시험 데이터 게재 | 표절 항목 시험 데이터는 개인 자서전 기반. **게재용 예시는 익명화 완료 생활수기로 대체** 요청 |
| No.3 측정 주체 | 성과물 목록상 요소 추출이 오투오와 고려대 양쪽에 걸쳐 있어 컨소시엄 확인 필요 |
| 전 차수 성적서 | `GERIR.CE.2511-02.009` 의 81% 도 B 항 결함 기준. 컨소시엄·시험기관 상의 필요 |
| 요약 성능(No.7) | 본 문서 범위 외. 정답셋 미구축, 지표 정의(recall/f1) 불일치 미해결 |

View File

@ -9,8 +9,7 @@
원본(source of truth):
- 표 데이터: `data/precedents/precedent_listup.csv`
- 서술 내용: `docs/PRECEDENT_LISTUP.md` — 아래 상수는 그 문서에서 옮긴 것이며,
문서가 바뀌면 여기도 함께 고쳐야 한다. 수치를 이 파일에서 새로 만들지 말 것.
- 서술 내용: 아래 상수가 원본이다. 수치를 이 파일에서 새로 만들지 말 것.
사용:
python scripts/build_precedent_listup_xlsx.py
@ -76,7 +75,7 @@ COLUMNS = [
]
# ---------------------------------------------------------------------------
# 서술 시트 — 원본은 docs/PRECEDENT_LISTUP.md
# 서술 시트 — 원본은 아래 상수
# ---------------------------------------------------------------------------
FUNNEL = [

View File

@ -62,7 +62,7 @@ def main() -> int:
raise SystemExit(
"정답셋이 없습니다: %s\n"
"성능지표 #7 은 요약 정답셋 구축 후 측정 가능하다. "
"docs/TEST_PLAN_2026_PHASE2.md 부록 E 참조." % args.dataset)
"시험 계획 부록 E 참조." % args.dataset)
rows = [json.loads(line) for line
in args.dataset.read_text(encoding="utf-8").splitlines() if line.strip()]