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:
parent
8ae52b0a82
commit
8aac18c255
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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의 산식과 평가 경로를 변경하지 않는다.
|
||||
@ -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 서비스의 코드/이미지는 배포하지 않았다.
|
||||
@ -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` 일치 ③ 사건번호 순이며, 생성형 인용은 어떤 경로로도 발생하지 않는다.
|
||||
@ -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`, 시험셋 변형도 전부 규칙 기반 |
|
||||
|
||||
> 성적서 게재용 데이터 예시는 자서전 본문에 실명이 남아 있으므로
|
||||
> **익명화가 끝난 생활수기에서 발췌**하여 제출한다. 접수 시 시험기관에 함께 요청할 것.
|
||||
@ -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가 부여한 것으로, 법률 전문가의 확정 판단이 아니다.
|
||||
@ -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자로 되돌리고 유사도 임계값만 조정 |
|
||||
@ -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 통합
|
||||
@ -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) 불일치 미해결 |
|
||||
@ -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 = [
|
||||
|
||||
@ -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()]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user