From 37f61c1bea18490f40541e05782525dba0a8a9c7 Mon Sep 17 00:00:00 2001 From: hbyang Date: Wed, 16 Sep 2026 13:40:04 +0900 Subject: [PATCH] fix: derive KLUE NER labels from dataset, not hardcoded order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scripts/train_klue_ner.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/scripts/train_klue_ner.py b/scripts/train_klue_ner.py index 8e18e67..0fc66f7 100644 --- a/scripts/train_klue_ner.py +++ b/scripts/train_klue_ner.py @@ -26,11 +26,22 @@ from pathlib import Path import numpy as np -# KLUE NER 태그 6종. BIO 로 12개 + O = 13 labels (전 차수와 동일). +# KLUE NER 태그 6종. 성적서 게재 순서이며 출력 정렬에만 쓴다. 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): @@ -129,10 +140,15 @@ def main() -> int: print("학습 %d건 / 평가 %d건" % (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 tokenizer = AutoTokenizer.from_pretrained(str(source)) 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( lambda batch: tokenize_and_align(batch, tokenizer, args.max_length), @@ -156,7 +172,7 @@ def main() -> int: train_dataset=encoded["train"], eval_dataset=encoded["validation"], data_collator=DataCollatorForTokenClassification(tokenizer), - compute_metrics=build_metrics(ID2LABEL), + compute_metrics=build_metrics(id2label), ) if not args.eval_only: