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는 전혀 구축되지 않은 상태이다.
print (bert_tokenizer.get_vocab_size())
-
Normalization
tokenizer 성능을 높이기 위한 title 정규화를 진행합니다
from tokenizers import normalizers
normalizer = normalizers.BertNormalizer()
bert_tokenizer.normalizer = normalizer
# 정규화 예시
normalizer.normalize_str("Héllò hôw \n are ü? " )
-
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
규칙
문장 가장 앞에 [CLS] 토큰이 있어야 하고 두 문장을 입력받았을 때 문장을 구별하기 위한 [SEP] 토큰으로 감싸져 있어야 한다.
single
과 pair
는 문장이 한 개씩 들어오는지 혹은 두 개씩 들어오는지를 나타낸다.
하나만 주어졌을 때는 ‘[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
가 필요하다.
config
는 embedding 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(
NVIDIA A100 기준 15분 소요
학습 완료!