Tokenizer training

Author

차상진

Published

May 2, 2025

Tokenizers 학습

목표: Transformers 라이브러리에서 사용되는 토크나이저를 만들어보자

- Load data

from datasets import load_dataset
dataset = load_dataset('klue','ynat')
dataset['train'][0]
/root/anaconda3/envs/nlp/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
{'guid': 'ynat-v1_train_00000',
 'title': '유튜브 내달 2일까지 크리에이터 지원 공간 운영',
 'label': 3,
 'url': 'https://news.naver.com/main/read.nhn?mode=LS2D&mid=shm&sid1=105&sid2=227&oid=001&aid=0008508947',
 'date': '2016.06.30. 오전 10:36'}

- 제목만 따로 저장

tokenizer 학습을 위해서 제목만 사용할 것이다. 굳이 메모리를 낭비할 필요가 없기 때문에 제목만 따로 저장하여 파일로 저장 후 그 파일을 사용하자.

target_key = 'title'
for key in dataset.column_names.keys():
    with open(f'./tokenizer_data_{key}.txt', 'w') as f:
        f.write('\n'.join(dataset[key][target_key]))

- 특수 토큰 정의

user_defined_symbols = [
    '[PAD]', # 문장길이 맞추는 토큰
    '[UNK]', # 토크나이저가 인식할 수 없는 토큰
    '[CLS]', # BERT계열 모델에서 문장 전체 정보를 저장하는 토큰
    '[SEP]', # BERT계열 모델에서 문장 구분을 위해 사용하는 토큰
    '[MASK]' # Masked LM에서 토큰 마스킹을 위해 사용하는 토큰
]

unused_token_num = 100
unused_list = [f'[UNUSED{i}' for i in range(100)] # 사전학습시 어휘에 없는 토큰을 추가하기 위한 빈 공간
whole_user_defined_symbols = user_defined_symbols + unused_list
print(whole_user_defined_symbols[:10])
['[PAD]', '[UNK]', '[CLS]', '[SEP]', '[MASK]', '[UNUSED0', '[UNUSED1', '[UNUSED2', '[UNUSED3', '[UNUSED4']

- 베이스 토크나이저 불러오기

현재 선언된 토크나이저는 아직 학습되지 않은 WordPiece라는 규칙만 지정되고 빈 상태이다.

  • BERT 토크나이저는 WordPiece를 기반으로 하는 모델이다.
from tokenizers import Tokenizer
from tokenizers.models import WordPiece

bert_tokenizer = Tokenizer(WordPiece(unk_token = '[UNK]')) # 토크나이저 객체 생성(WordPiece토크나이저 사용(사전에 없는 단어는 UNK로 처리하도록 설정))

WordPiece 규칙만 지정되어있고 vocab는 전혀 구축되지 않은 상태이다.

  • vocab size가 0이다
print(bert_tokenizer.get_vocab_size()) 
0

- Normalization

tokenizer 성능을 높이기 위한 title 정규화를 진행합니다

from tokenizers import normalizers

normalizer = normalizers.BertNormalizer()
bert_tokenizer.normalizer = normalizer

# 정규화 예시
normalizer.normalize_str("Héllò hôw\nare ü? ")
'hello how are u? '

- Whitespace

Whitespace 클래스를 이용해서 줄 바꿈, 공백을 처리한다.

from tokenizers.pre_tokenizers import Whitespace

pre_tokenizer = Whitespace()
bert_tokenizer.pre_tokenizer = pre_tokenizer

# Whitespace 예시
pre_tokenizer.pre_tokenize_str("안녕하세요. 제대로 인코딩이 되는지 확인 중입니다.")
[('안녕하세요', (0, 5)),
 ('.', (5, 6)),
 ('제대로', (7, 10)),
 ('인코딩이', (11, 15)),
 ('되는지', (16, 19)),
 ('확인', (20, 22)),
 ('중입니다', (23, 27)),
 ('.', (27, 28))]

- 문장이 인코딩되었을 때 기본적으로 어떤 형태를 취할지 양식 작성

토큰화된 결과에 특별한 규칙을 적용하는 후처리(post-processing) 단계를 정의

from tokenizers.processors import TemplateProcessing

post_processor = TemplateProcessing(
    single="[CLS] $A [SEP]",
    pair="[CLS] $A [SEP] $B:1 [SEP]:1",
    special_tokens=[(t, i) for i, t in enumerate(user_defined_symbols)]
)

bert_tokenizer.post_processor = post_processor

규칙

  1. 문장 가장 앞에 [CLS] 토큰이 있어야 하고 두 문장을 입력받았을 때 문장을 구별하기 위한 [SEP] 토큰으로 감싸져 있어야 한다.

  2. singlepair는 문장이 한 개씩 들어오는지 혹은 두 개씩 들어오는지를 나타낸다.

  3. 하나만 주어졌을 때는 ‘[CLS] 문장 [SEP]’ 형태를 나타내고 두 개가 들어왔을 때는 ‘[CLS] 문장1 [SEP] 문장2 [SEP]’ 형태로 출력되어야 한다

- Vocab 구축을 위한 학습

Vocab을 구축하려면 학습을 해야하므로 Trainer를 지정한다.

# vocab_size 지정, trainer 생성
from tokenizers.trainers import WordPieceTrainer

vocab_size = 24000
trainer = WordPieceTrainer(
    vocab_size = vocab_size,
    special_tokens = whole_user_defined_symbols)

위에서 저장한 txt 파일을 통해 학습

from glob import glob

bert_tokenizer.train(glob(f'/*.txt'),trainer)


- 디코딩으로 학습이 잘 되었는지 판단

인코딩은 잘 되었지만 디코딩은 제대로 되지 않은 것을 알 수 있다.

  • 사실은 정상적인 작동이다..! 이상해보이지만 ##이 띄어쓰기 없이 붙히는 의미라는 것을 안다면 정상적인 결과라는 것을 알 수 있다.
  • 아직 디코딩 방법이 적용되지 않았기 때문에 그런 것이다.
output = bert_tokenizer.encode('인코딩 및 디코딩이 제대로 이루어지는지 확인 중입니다.')
print(output.ids)

bert_tokenizer.decode(output.ids)
[2, 675, 906, 2220, 4518, 1240, 906, 2220, 569, 6727, 12916, 10780, 586, 1881, 16618, 10188, 106, 3]
'인 ##코 ##딩 및 디 ##코 ##딩 ##이 제대로 이루 ##어지는 ##지 확인 중이 ##ᆸ니다 .'

- WordPiece 디코더를 할당

WordPiece 토크나이저에 맞는 디코더를 추가하니 이번에는 제대로 디코딩이 된 것을 확인할 수 있다.

from tokenizers import decoders

bert_tokenizer.decoder = decoders.WordPiece()
bert_tokenizer.decode(output.ids)
'인코딩 및 디코딩이 제대로 이루어지는지 확인 중입니다.'

- 우리가 만든 토크나이저를 transformers 토크나이저로 대체해서 사용

잘 작동하는 것을 볼 수 있다.

from transformers import BertTokenizerFast

fast_tokenizer = BertTokenizerFast(tokenizer_object=bert_tokenizer)
encoded = fast_tokenizer.encode("인코딩 및 디코딩이 제대로 이루어지는지 확인 중입니다.")
decoded = fast_tokenizer.decode(encoded)
print(encoded)
print(decoded)
[2, 675, 906, 2220, 4518, 1240, 906, 2220, 569, 6727, 12916, 10780, 586, 1881, 16618, 10188, 106, 3]
[CLS] 인코딩 및 디코딩이 제대로 이루어지는지 확인 중입니다. [SEP]

- tokenizer 저장

잘 작동하므로 tokenizer 저장한다.

# 기본 작업환경이 /root/NLP이기에 output_dir는 저렇게 작성한다.
output_dir = './MyTokenizer'
fast_tokenizer.save_pretrained(output_dir)
('./MyTokenizer/tokenizer_config.json',
 './MyTokenizer/special_tokens_map.json',
 './MyTokenizer/vocab.txt',
 './MyTokenizer/added_tokens.json',
 './MyTokenizer/tokenizer.json')

- 저장한 tokenizer를 가져와서 테스트

new_tokenizer = BertTokenizerFast.from_pretrained(output_dir)

encoded = new_tokenizer(["인코딩 잘 되는지 확인", "안되면 다시 학습하자"])

for k, v in encoded.items():
  print(k, v)

print(new_tokenizer.decode(encoded["input_ids"][0]))
print(new_tokenizer.decode(encoded["input_ids"][1]))
input_ids [[2, 675, 906, 2220, 1675, 6464, 586, 1881, 3], [2, 18633, 1594, 6985, 3782, 3]]
token_type_ids [[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]
attention_mask [[1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]]
[CLS] 인코딩 잘 되는지 확인 [SEP]
[CLS] 안되면 다시 학습하자 [SEP]

tokenizer 완성!

BERT 초기화 후 학습

사전학습 모델은 그에 해당하는 토크나이저가 있다. 이것을 반대로 말하면 토크나이저를 사전학습해도 사전학습한 토크나이저를 쓰는 모델이 없다면 아무 의미가 없다는 의미이다.

그래서 우리가 만든 토크나이저(WordPiece)에 대응하는 BERT모델을 초기화하여 처음부터 학습시키자.

- Load data

from datasets import load_dataset
from transformers import BertTokenizerFast

dataset = load_dataset('klue','ynat')
model_name = "./MyTokenizer"
tokenizer = BertTokenizerFast.from_pretrained(model_name)

- config 설정

모델의 최초 선언을 위해서는 해당 모델 config가 필요하다.

configembedding size, hidden size, num layers등 모델의 전반적인 구조 정보를 저장하고 있다.

모델 vocab size는 토크나이저의 vocab size를 따라가야 하므로 vocab size만 따로 설정

from transformers import (
    BertForMaskedLM,
    BertConfig,
    DataCollatorForLanguageModeling,
    Trainer,
    TrainingArguments,
    BertTokenizerFast
)

config = BertConfig(
    vocab_size=tokenizer.vocab_size,
    hidden_size=256,            
    num_hidden_layers=4,        
    num_attention_heads=4,      
    intermediate_size=1024,     
)

- Model 선언

config를 준비했으니 모델은 간단하게 config 인자를 넣으면 선언 가능하다.

model = BertForMaskedLM(config)
model
BertForMaskedLM(
  (bert): BertModel(
    (embeddings): BertEmbeddings(
      (word_embeddings): Embedding(24000, 256, padding_idx=0)
      (position_embeddings): Embedding(512, 256)
      (token_type_embeddings): Embedding(2, 256)
      (LayerNorm): LayerNorm((256,), eps=1e-12, elementwise_affine=True)
      (dropout): Dropout(p=0.1, inplace=False)
    )
    (encoder): BertEncoder(
      (layer): ModuleList(
        (0-3): 4 x BertLayer(
          (attention): BertAttention(
            (self): BertSdpaSelfAttention(
              (query): Linear(in_features=256, out_features=256, bias=True)
              (key): Linear(in_features=256, out_features=256, bias=True)
              (value): Linear(in_features=256, out_features=256, bias=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
            (output): BertSelfOutput(
              (dense): Linear(in_features=256, out_features=256, bias=True)
              (LayerNorm): LayerNorm((256,), eps=1e-12, elementwise_affine=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
          )
          (intermediate): BertIntermediate(
            (dense): Linear(in_features=256, out_features=1024, bias=True)
            (intermediate_act_fn): GELUActivation()
          )
          (output): BertOutput(
            (dense): Linear(in_features=1024, out_features=256, bias=True)
            (LayerNorm): LayerNorm((256,), eps=1e-12, elementwise_affine=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
  )
  (cls): BertOnlyMLMHead(
    (predictions): BertLMPredictionHead(
      (transform): BertPredictionHeadTransform(
        (dense): Linear(in_features=256, out_features=256, bias=True)
        (transform_act_fn): GELUActivation()
        (LayerNorm): LayerNorm((256,), eps=1e-12, elementwise_affine=True)
      )
      (decoder): Linear(in_features=256, out_features=24000, bias=True)
    )
  )
)

- 데이터셋 토크나이징

def tokenize_function(example):
    return tokenizer(example["title"], truncation=True, padding="max_length", max_length=128)

tokenized_dataset = dataset["train"].map(tokenize_function, batched=True, remove_columns=dataset["train"].column_names)
Map: 100%|██████████| 45678/45678 [00:03<00:00, 12817.99 examples/s]

- DataCollator

data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=True,
    mlm_probability=0.15
)

- 학습

training_args = TrainingArguments(
    output_dir="./bert-mlm-klue-ynat",
    overwrite_output_dir=True,
    num_train_epochs=3,
    per_device_train_batch_size=16,
    save_steps=500,
    save_total_limit=2,
    logging_steps=100,
    logging_dir="./logs",
    report_to="none"
)
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    tokenizer=tokenizer,
    data_collator=data_collator
)
/tmp/ipykernel_16388/507548412.py:1: FutureWarning: `tokenizer` is deprecated and will be removed in version 5.0.0 for `Trainer.__init__`. Use `processing_class` instead.
  trainer = Trainer(
# trainer.train()

NVIDIA A100 기준 15분 소요

학습 완료!