fix: derive KLUE NER labels from dataset, not hardcoded order

KLUE NER 의 라벨 id 순서는 B-DT(0) … I-TI(11), O(12) 로 'O' 가 맨 뒤다.
스크립트는 ["O"] + [B-/I- ...] 로 재구성해 써서 전체가 한 칸씩 밀렸다.
tokenize_and_align 은 데이터셋 id 를 그대로 넘기는데 build_metrics 가
어긋난 순서로 해석하면서, 실제 O 구간이 I-TI 엔티티로 집계됐다.

영향 — micro support 14,257(실제 엔티티 수) → 67,056 으로 부풀고
F1 도 함께 올랐다. 학습된 klue_ner_large 를 올바른 매핑으로 재평가하면
0.8123 이며 보고돼 온 0.8750 이 아니다. 전 차수 성적서의 support 62,760
(max_length 절단분 차이) 도 같은 상태였다.

모델 자체는 데이터셋 id 공간에서 일관되게 학습돼 재학습이 필요없다.
잘못된 것은 지표 해석뿐이다.

라벨 목록을 데이터셋에서 직접 가져와 유일한 출처로 삼는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-09-16 13:40:04 +09:00
parent be25b6c053
commit 37f61c1bea

View File

@ -26,11 +26,22 @@ from pathlib import Path
import numpy as np import numpy as np
# KLUE NER 태그 6종. BIO 로 12개 + O = 13 labels (전 차수와 동일). # KLUE NER 태그 6종. 성적서 게재 순서이며 출력 정렬에만 쓴다.
TAGS = ["DT", "LC", "OG", "PS", "QT", "TI"] 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()} def label_names(dataset) -> list[str]:
"""라벨 목록은 데이터셋에서 직접 가져온다. 코드에서 다시 만들지 않는다.
KLUE NER 의 id 순서는 B-DT(0) … I-TI(11), **O(12)** 로 'O' 가 맨 뒤다.
이를 `["O"] + [B-/I- ...]` 로 재구성하면 전체가 한 칸씩 밀려, 실제 O 구간이
I-TI 엔티티로 집계된다. 그러면 micro support 가 14,257(실제 엔티티 수) 에서
67,056 으로 부풀고 F1 도 함께 올라간다(0.8123 → 0.8750). 전 차수 성적서의
support 62,760 이 이 상태였다.
데이터셋을 유일한 출처로 삼아 그 경로를 막는다.
"""
return list(dataset.features["ner_tags"].feature.names)
def tokenize_and_align(examples, tokenizer, max_length: int): def tokenize_and_align(examples, tokenizer, max_length: int):
@ -129,10 +140,15 @@ def main() -> int:
print("학습 %d건 / 평가 %d건" print("학습 %d건 / 평가 %d건"
% (len(dataset["train"]), len(dataset["validation"]))) % (len(dataset["train"]), len(dataset["validation"])))
labels = label_names(dataset["train"])
label2id = {label: i for i, label in enumerate(labels)}
id2label = {i: label for label, i in label2id.items()}
print("라벨 %d종 (데이터셋 기준): %s" % (len(labels), ", ".join(labels)))
source = args.out_dir if args.eval_only else args.model source = args.out_dir if args.eval_only else args.model
tokenizer = AutoTokenizer.from_pretrained(str(source)) tokenizer = AutoTokenizer.from_pretrained(str(source))
model = AutoModelForTokenClassification.from_pretrained( model = AutoModelForTokenClassification.from_pretrained(
str(source), num_labels=len(LABELS), id2label=ID2LABEL, label2id=LABEL2ID) str(source), num_labels=len(labels), id2label=id2label, label2id=label2id)
encoded = dataset.map( encoded = dataset.map(
lambda batch: tokenize_and_align(batch, tokenizer, args.max_length), lambda batch: tokenize_and_align(batch, tokenizer, args.max_length),
@ -156,7 +172,7 @@ def main() -> int:
train_dataset=encoded["train"], train_dataset=encoded["train"],
eval_dataset=encoded["validation"], eval_dataset=encoded["validation"],
data_collator=DataCollatorForTokenClassification(tokenizer), data_collator=DataCollatorForTokenClassification(tokenizer),
compute_metrics=build_metrics(ID2LABEL), compute_metrics=build_metrics(id2label),
) )
if not args.eval_only: if not args.eval_only: