# 성능 평가 확인서 — 시험결과 초안 (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명 → 시험 질의로만 사용 (인덱스에 미포함)
```
**비표절 데이터 예시 :**
```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건만 놓쳤다.
---
---
# ※ 부록 — 성적서 전사 대상 아님 (내부 참고용)
## 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) 불일치 미해결 |