Label Alignment in NER
- Label Alignment in Named Entity Recognition (NER)
Label Alignment in Named Entity Recognition (NER)
NER Dataset의
Dataset의 4.0부터 이 전 방식이 안 되는 경우 많음.
revision을refs/convert/parquet로 바꾸는 게 가능한 데이터셋을 사용할 것.
1. Named Entity Recognition (NER)와 BIO Tagging
Named Entity Recognition (NER, 개체명 인식)은
- 입력 문장의 각 token(토크)에 대해
- entity(개체) 범주를 예측하는 Token Classification (토큰 분류) 문제.
1-1. IOB2 Taggking Sceme
NER에서 가장 널리 사용되는 라벨링 체계는 IOB2 scheme (IOB2 태깅 체계, BIO) 이다.
- B-X (Begin): 개체 X의 시작 토큰
- I-X (Inside): 개체 X 내부 토큰
- O (Outside): 개체가 아닌 토큰
1-2. 주의 사항
대부분의 NER 데이터셋에서
- BIO 라벨은 이미 주어진 상태 임.
2. Label Alignment (라벨 정렬)의 필요성
현대의 Transformer 기반 언어 모델은
- Subword Tokenization (서브워드 토크나이제이션)을 사용.
- 대표적인 예로
- WordPiece,
- BPE,
- SentencePiece 방식이 있다.
이는 NER 데이터셋의 라벨과 다른 기준 좌표계(label coordinate system)를 가질 수 있다는 문제를 일으킴:
- word-level (단어 단위) 라벨
- character-level (문자 단위) 라벨
Transformer 모델 입력은 항상 subword-level (서브워드 단위) Token Sequence 임.
- 라벨 좌표계와 모델 입력 좌표계가 일치하지 않는 문제가 발생할 수 있음,
이를 해결하기 위한
전처리 단계가 바로 Label Alignment (라벨 정렬) 이다.
Hugging Face의 (Fast) Tokenizer는 이를 위해 두 가지 핵심 정보를 제공한다.
word_ids(): word-level 라벨 정렬을 위한 메타정보offset_mapping: character-level 라벨 정렬을 위한 문자 오프셋 정보
NER 에선 사실상 Fast Tokenizer 를 사용해야 함. Tokenization 과정에서 alignment metadata를 함께 생성하기 때문임.
3. Test 용 유틸함수
이 문서에서 NER 전처리의 Label Alignment가 잘 되었는지 여부를 다음을 확인한다:
- subword 토큰과 label을 인덱스 별로 나란히 출력
- 학습에서 제외할 토큰은
-100(ignore index)로 표시
def show_alignment(encoded, tokenizer, id2label, n=80):
tokens = tokenizer.convert_ids_to_tokens(encoded["input_ids"])
labels = encoded["labels"]
m = min(len(tokens), len(labels), n)
for i in range(m):
lab = "IGN" if labels[i] == -100 else id2label[labels[i]]
print(f"{i:3d} {tokens[i]:16s} {lab}")
4. Case 1: 영어 · word-level BIO → word_ids() 정렬
4.1 데이터셋: CoNLL-2003
CoNLL-2003은 대표적인 word-level BIO 데이터셋.
tokens[i]: 단어(word)ner_tags[i]: 해당 단어의 BIO 라벨
https://huggingface.co/datasets/tner/conll2003
datasets 4.0.0 부터 parquet 가 없이 script코드만으로 구성된 데이터셋은 다운로드가 안된다.
다음의 코드로 받을 것.
from datasets import load_dataset
ds_en = load_dataset(
"tner/conll2003",
revision="refs/convert/parquet" #Parquet으로 변환된 브랜치.
)
print(ds_en)
print(ds_en['train'].features) # ner_tags 가 없다. int인 tasg만 존재.
print(ds_en['train'].column_names)
print(ds_en['train'][0])
example_en = ds_en['train'][0]
보안의 문제로 인해서임. (쓸데없이 악용하는 인간들 때문에 불편함만 늘어난다.) 스크립트 기반 데이터셋은
refs/convert/parquet브랜치로 우회 필요
위의 tner/conll2003 의 경우, ClassLabel 메타데이터가 깨져서 단순 int32 리스트임. ==;;
다음의 코드로 수동 매핑을 정의한다 (Parquet Conversion에서 생각보다 이런 경우 많음).
# tner/conll2003 레이블 정의
label_names = ['O', 'B-ORG', 'B-MISC', 'B-PER', 'I-PER', 'B-LOC', 'I-LOC', 'I-ORG', 'I-MISC']
id2label_en = {i: label for i, label in enumerate(label_names)}
label2id_en = {label: i for i, label in enumerate(label_names)}
print(id2label_en)
4.2 전처리 (word-level BIO 표준 패턴)
def tokenize_and_align_labels_wordlevel(example, tokenizer):
tokenized = tokenizer(
example["tokens"],
is_split_into_words=True,
truncation=True
)
word_ids = tokenized.word_ids()
labels = []
prev_word_id = None
for word_id in word_ids:
if word_id is None:
labels.append(-100)
elif word_id == prev_word_id:
labels.append(-100)
else:
# labels.append(example['ner_tags'][wid]) # 'conll2003' 사용시
labels.append(example['tags'][wid]) # 'tner/conll2003' 사용시
prev_word_id = word_id
tokenized["labels"] = labels
return tokenized
참고: https://huggingface.co/learn/llm-course/en/chapter7/2
4.3 전처리 결과 확인
다음은 전처리 결과를 보기 위한 코드
tokenizer_en = AutoTokenizer.from_pretrained('bert-base-cased', use_fast=True)
encoded_en = tokenize_and_align_labels_wordlevel(example_en, tokenizer_en)
show_alignment(encoded_en, tokenizer_en, id2label_en)
결과는 다음과 같음:
0 [CLS] IGN
1 EU B-ORG
2 rejects O
3 German B-MISC
4 call O
5 to O
6 boycott O
7 British B-MISC
8 la O
9 ##mb IGN
10 . O
11 [SEP] IGN
5. Case 2: 영어 · character-level BIO → offset_mapping 정렬
5.1 데이터셋: EMBO/sd-character-level-ner
이 데이터셋은 영어 문장에 대해 character-level BIO 라벨을 제공.
text: 원문 문자열labels: 각 문자(character)에 대응하는 BIO 라벨
참고: https://huggingface.co/datasets/EMBO/sd-character-level-ner
이 경우 라벨의 기준 좌표계는 문자 인덱스이므로,
word_ids() 기반 정렬은 논리적으로 적합하지 않다.
ds_char = load_dataset(
'EMBO/sd-character-level-ner',
revision='refs/convert/parquet',
)
example_char = ds_char['train'][0]
label_names_char = ds_char['train'].features['labels'].feature.names
id2label_char = {i: s for i, s in enumerate(label_names_char)}
- 여기엔 메타 데이터가 남아있어서 수동으로 넣을 이유가 없음.
5.2 전처리 (character-level BIO 표준 패턴)
def tokenize_and_align_labels_charlevel(example, tokenizer):
tokenized = tokenizer(
example["text"],
return_offsets_mapping=True,
truncation=True
)
offsets = tokenized["offset_mapping"]
char_labels = example["labels"]
labels = []
for start, end in offsets:
if start == end:
labels.append(-100)
elif start < 0 or start >= len(char_labels):
labels.append(-100)
else:
labels.append(char_labels[start])
tokenized["labels"] = labels
return tokenized
이 방식은 Fast Tokenizer가 제공하는 문자 오프셋 정보를 가장 직접적으로 활용하는 정렬 방식임.
참고: https://huggingface.co/docs/transformers/main/en/main_classes/tokenizer
5.3 전치리 결과 확인
다음은 전처리 결과를 보기 위한 코드
tokenizer_char = AutoTokenizer.from_pretrained('bert-base-cased', use_fast=True)
encoded_char = tokenize_and_align_labels_charlevel(example_char, tokenizer_char)
show_alignment(encoded_char, tokenizer_char, id2label_char)
결과는 대략 다음과 같음.
0 [CLS] IGN
1 ( O
2 E O
3 ) O
4 Q O
5 ##uant O
6 ##ification O
7 of O
8 the O
9 number B-EXP_ASSAY
10 of O
11 cells O
12 without O
13 γ B-GENEPROD
14 - I-GENEPROD
15 Tu I-GENEPROD
16 ##bul I-GENEPROD
17 ##in I-GENEPROD
18 at O
19 cent B-SUBCELLULAR
20 ##ros I-SUBCELLULAR
21 ##ome I-SUBCELLULAR
22 ##s I-SUBCELLULAR
23 ( O
이하 생략.
Entity Type에 대한 참고 표임:
| ENTITY_TYPE | 의미 (영문/한글) | 등장 예시 | BIO 패턴 |
|---|---|---|---|
| EXP_ASSAY | Experimental Assay (실험/분석 지표) | number | B |
| GENEPROD | Gene Product (유전자 산물, 단백질) | γ-Tubulin | B + I* |
| SUBCELLULAR | Subcellular Location (세포내 위치) | centrosomes | B + I* |
6. Case 3: 한글 · character-level BIO → offset_mapping 정렬
6.1 데이터셋: KLUE NER
KLUE (Korean Language Understanding Evaluation) NER는 한국어 NER 벤치마크로,
tokens자체가 character-level (문자 단위 토큰) 로 제공됨.
tokens[i]: 문자ner_tags[i]: 해당 문자의 BIO 라벨
KLUE의 NER 데이테셋에는 공백문자도 token임.
참고: https://huggingface.co/datasets/klue/klue
ds_ko = load_dataset('klue', 'ner')
label_names_ko = ds_ko['train'].features['ner_tags'].feature.names
id2label_ko = {i: s for i, s in enumerate(label_names_ko)}
example_ko = ds_ko['train'][0]
6.2 전처리 (KLUE character-level BIO 표준 패턴)
def tokenize_and_align_labels_klue_charlevel(example, tokenizer):
text = "".join(example["tokens"]).replace("\xa0", " ")
char_labels = example["ner_tags"]
tokenized = tokenizer(
text,
return_offsets_mapping=True,
truncation=True
)
labels = []
for start, end in tokenized["offset_mapping"]:
if start == end:
labels.append(-100)
elif start < 0 or start >= len(char_labels):
labels.append(-100)
else:
labels.append(char_labels[start])
tokenized["labels"] = labels
return tokenized
KLUE 논문에서는 NER 태깅이 문자 단위(character-level)로 정의되어 있음을 명시한다.
참고: https://arxiv.org/abs/2105.09680
6.3 전치리 결과 확인
다음은 전처리 결과를 보기 위한 코드
tokenizer_ko = AutoTokenizer.from_pretrained('klue/bert-base', use_fast=True)
encoded_ko = tokenize_and_align_labels_klue(example_ko, tokenizer_ko)
show_alignment(encoded_ko, tokenizer_ko, id2label_ko)
결과는 다음과 같음:
0 [CLS] IGN
1 특히 O
2 영동고속도로 B-LC
3 강릉 B-LC
4 방향 O
5 문 B-LC
6 ##막 I-LC
7 ##휴 I-LC
8 ##게 I-LC
9 ##소 I-LC
10 ##에서 O
11 만 B-LC
12 ##종 I-LC
13 ##분 I-LC
14 ##기 I-LC
15 ##점 I-LC
16 ##까 O
17 ##지 O
18 5 B-QT
19 ##㎞ I-QT
20 구간 O
21 ##에 O
22 ##는 O
이하 생략.
7. 정리
| 데이터셋 | BIO 기준 좌표계 | Label Alignment 방식 |
|---|---|---|
| CoNLL-2003 | word-level | word_ids() |
| EMBO/sd-character-level-ner | character-level | offset_mapping |
| KLUE NER | character-level | offset_mapping |
Label Alignment의 본질은 BIO 여부가 아니라, BIO 라벨이 정의된 좌표계임.
이를 사용한 Tokenizer 의 token들에 맞춰야 함.
References
- Hugging Face Datasets – CoNLL-2003 https://huggingface.co/datasets/conll2003
- Hugging Face LLM Course – Token Classification & NER https://huggingface.co/learn/llm-course/en/chapter7/2
- Hugging Face Transformers Documentation – Fast Tokenizer https://huggingface.co/docs/transformers/main/en/main_classes/tokenizer
- EMBO/sd-character-level-ner Dataset https://huggingface.co/datasets/EMBO/sd-character-level-ner
- KLUE Benchmark Dataset https://huggingface.co/datasets/klue/klue
- Park et al., “KLUE: Korean Language Understanding Evaluation” https://arxiv.org/abs/2105.09680