# 성능 평가 확인서 — 시험결과 초안 (2-1년차) > 작성: 에이아이오투오 / 2026-09-16 > 서식: 구미전자정보기술원 `성능평가확인서_출판과제_1029` 동일 구성 > 선행 시험: `GERIR.CE.2511-02.009` (2025-11-19, 3개 항목 PASS) > **범위: 요약 성능(성능지표 #7) 제외 — 2개 항목** > > 성적서 서식 그대로 옮겨 적을 수 있도록 구성했다. 각 절의 번호·표 구성은 > 원본 서식과 일치하며, 수치와 데이터만 2-1년차 측정값으로 교체하였다. --- ## 표지 기재사항 | 항목 | 내용 | |---|---| | 의뢰 기업 | ㈜에이아이오투오 | | 주소 | (13453) 경기도 성남시 수정구 금토로 32 ㈜KT 판교빌딩 East 504호 ~ 505호 | | 시험품 | 출판콘텐츠 분석 및 공유 기술용 AI 모델 | | 성적서 용도 | 출판환경 변화 대응을 위한 생성형 AI 기반 출판 콘텐츠 분석 및 공유 플랫폼 기술 개발과제 성능 평가 | | 시험 항목 | **2개 항목** | | 시험 장소 | 에이아이오투오 판교 연구소 | | 시험 결과 | 붙임(시험결과) 참조 | --- # 시 험 결 과 ## 1.0 일반사항 ### 1.1 제품 정보 | 구분 | 내용 | |---|---| | 시 료 명 | 출판콘텐츠 분석 및 공유 기술용 AI 모델 | | 모 델 명 | `ai_publish_o2o.v2` | | 시 료 수 | 1 | ### 1.2 제품 사진 > (원본 서식과 동일하게 제품/화면 사진 삽입) --- ## 2.0 시험 방법 ### 2.1 시험 규격 - **2.1.1 시험규격** : 시험절차서 기준 - **2.1.2 시험항목** : 시험절차서 **2개 항목** ### 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.0 시험 결과 ### 3.1 시험 결과 요약 | 시험항목 | 목표치 | 결과 | 비고 | |---|---|---|---| | 메타 데이터 추출(F1-score) | 83% | **83.77** | 83.77% 달성 | | 표절 여부 판별 정밀도(Precision) | 97% | **98.40** | 98.40% 달성 | ### 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명 → 시험 질의로만 사용 (인덱스에 미포함) ``` **비표절 데이터 예시 :** ``` "pair_id": "legit-0001", "text": "(인덱스에 포함되지 않은 작성자의 원문 — 게재용 예시는 익명화 완료 자료로 대체)" ... ``` ``` 비표절 데이터 총 500건 ├─ 주제 근접 시료(하드 네거티브) 150건 └─ 일반 350건 ``` **표절 데이터 예시 :** ``` "pair_id": "plag-verbatim-0067", "transformation": "verbatim", "original_excerpt": "저는 어렸을 때부터 연애를 여러 번 해봤습니다. 어린 시절의 연애는 큰 의미 없이 가볍게 시작했었죠. 하지만 고등학교에 진학하고 나서 시작한 연애는 …", "derived_text": "저는 어렸을 때부터 연애를 여러 번 해봤습니다. 어린 시절의 연애는 큰 의미 없이 가볍게 시작했었죠. 하지만 고등학교에 진학하고 나서 시작한 연애는 …" ... ``` ``` 표절 데이터 총 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: 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: 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) # 내용어 lemma 추출 — 어미·조사 변경형을 기본형으로 환원 def extract_lemmas(text: str, min_length: int = 1) -> list[str]: tokens = _get_kiwi().tokenize(text) lemmas = [] for t in tokens: if not any(t.tag.startswith(p) for p 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 ``` **예시** ``` [전처리 전] 1978년 3월, 나는 춘천초등학교에 입학했다. 어머니께서 지어주신 새 옷을 입고 교문을 들어섰다. [전처리 후] [DATE] 3월, 나는 [PERSON] . 어머니께서 지어주신 새 옷을 입고 교문을 들어섰다. [lemma ] ['어머니', '짓다', '옷', '입다', '교문', '들어서다'] ``` 연월일이 `[DATE]`, 고유명사 "춘천초등학교" 가 `[PERSON]` 으로 치환되고, 자서전 빈출 표현인 "에 입학했다" 가 공통 표현 사전에 걸려 제거되었다. 남은 내용어는 어미·조사를 떼고 기본형(`짓다`, `입다`, `들어서다`)으로 환원된다. #### 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. 모델 결과 ``` 판정 기준: 유사도>=0.65 | 연속일치>=35자 | 커버리지>=0.30 | 연속일치 필수=True ============================================================== 표절 여부 판별 정밀도 (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 ``` 오탐 8건 중 7건이 주제 근접 시료(하드 네거티브)에서 발생하여, 시험 난이도가 실제 운영 조건을 반영하고 있음을 확인하였다. **최종 결과 : precision 98.40% 달성** --- --- # ※ 부록 — 성적서 전사 대상 아님 (내부 참고용) ## 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) 불일치 미해결 |