diff --git a/app/engine/legal_risk.py b/app/engine/legal_risk.py index 0e130f1..29f5e19 100644 --- a/app/engine/legal_risk.py +++ b/app/engine/legal_risk.py @@ -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 diff --git a/docs/CASE_MATCHING_KPI.md b/docs/CASE_MATCHING_KPI.md deleted file mode 100644 index 16e6a0d..0000000 --- a/docs/CASE_MATCHING_KPI.md +++ /dev/null @@ -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의 산식과 평가 경로를 변경하지 않는다. diff --git a/docs/COMBOOKS_CASE_MATCHING_REPLY_20260918.md b/docs/COMBOOKS_CASE_MATCHING_REPLY_20260918.md deleted file mode 100644 index 7473300..0000000 --- a/docs/COMBOOKS_CASE_MATCHING_REPLY_20260918.md +++ /dev/null @@ -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 서비스의 코드/이미지는 배포하지 않았다. diff --git a/docs/IMPLEMENTATION_RUNBOOK.md b/docs/IMPLEMENTATION_RUNBOOK.md deleted file mode 100644 index 95c0183..0000000 --- a/docs/IMPLEMENTATION_RUNBOOK.md +++ /dev/null @@ -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= -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` 일치 ③ 사건번호 순이며, 생성형 인용은 어떤 경로로도 발생하지 않는다. diff --git a/docs/PERF_EVIDENCE_CAPTURE_2026.md b/docs/PERF_EVIDENCE_CAPTURE_2026.md deleted file mode 100644 index bf712b0..0000000 --- a/docs/PERF_EVIDENCE_CAPTURE_2026.md +++ /dev/null @@ -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 \ - --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`, 시험셋 변형도 전부 규칙 기반 | - -> 성적서 게재용 데이터 예시는 자서전 본문에 실명이 남아 있으므로 -> **익명화가 끝난 생활수기에서 발췌**하여 제출한다. 접수 시 시험기관에 함께 요청할 것. diff --git a/docs/PRECEDENT_LISTUP.md b/docs/PRECEDENT_LISTUP.md deleted file mode 100644 index 389890b..0000000 --- a/docs/PRECEDENT_LISTUP.md +++ /dev/null @@ -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가 부여한 것으로, 법률 전문가의 확정 판단이 아니다. diff --git a/docs/PRECISION_TEST_PROCEDURE.md b/docs/PRECISION_TEST_PROCEDURE.md deleted file mode 100644 index d69bfb8..0000000 --- a/docs/PRECISION_TEST_PROCEDURE.md +++ /dev/null @@ -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 \ - --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자로 되돌리고 유사도 임계값만 조정 | diff --git a/docs/SCOPE_AND_METRICS.md b/docs/SCOPE_AND_METRICS.md deleted file mode 100644 index 5c96f4d..0000000 --- a/docs/SCOPE_AND_METRICS.md +++ /dev/null @@ -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 통합 diff --git a/docs/TEST_PLAN_2026_PHASE2.md b/docs/TEST_PLAN_2026_PHASE2.md deleted file mode 100644 index 04a3ed7..0000000 --- a/docs/TEST_PLAN_2026_PHASE2.md +++ /dev/null @@ -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
RAM : 18GB | SSH 터미널 | 없음 | -| GPU서버 | GPU : RTX 3090 24G × 2
CPU : Intel i9-12900KS (24core)
RAM : 125GB | Ubuntu 24.0.2
Pytorch 2.8
Python 3.9
Cuda 12.2 | 메타 데이터 추출 모듈,
표절 여부 판별 모듈,
요약 모듈 | - -#### 2.2.3 평가방법 - -| 순번 | 평가지표(성능지표) | 평가방법 | 비고 | -|---|---|---|---| -| 1 | 메타 데이터 추출
(F1 – score) | KLUE 데이터셋의 NER 데이터셋을 활용하여, 21,008개의 학습 데이터로 개발된 모델을 학습시키고, 5,000개의 테스트 데이터를 활용하여 개발된 모델에 대한 상대 평가 진행 | 과제 성능지표 3번에 해당 | -| 2 | 표절 여부 판별 정밀도
(precision) | 자체 제작된 실제 표절 글과, 표절이 아닌 글을 Classification하여, precision 계산
(시험 데이터 1,000건 = 표절 500건 + 비표절 500건. 모델이 학습하지 않은 작성자의 자료로 구성) | 과제 성능지표 4번에 해당 | -| 3 | 요약 성능
(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) 불일치 미해결 | diff --git a/scripts/build_precedent_listup_xlsx.py b/scripts/build_precedent_listup_xlsx.py index bc9a898..932679f 100644 --- a/scripts/build_precedent_listup_xlsx.py +++ b/scripts/build_precedent_listup_xlsx.py @@ -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 = [ diff --git a/scripts/show_summary_samples.py b/scripts/show_summary_samples.py index c6f1b0b..a8ca53e 100644 --- a/scripts/show_summary_samples.py +++ b/scripts/show_summary_samples.py @@ -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()]