반응형


 

자체 데이터셋으로 BertTokenizer 학습하기 

 

이번 게시글에서는 Pretrained Weight를 이용하지 않고, 특정 Domain에 맞도록 직접 Custom Dataset을 통해 BertTokenizer를 학습시키는 방법을 다룬다.

 

현재 진행하고 있는 프로젝트에서, 특정 분야에 대해서 처음부터 다시 학습된 (Train from Scratch) BERT 모델을 구현할 필요가 있어서 많은 시행착오를 가지며, 포스팅을 진행하였다.

 

라이브러리로는 가장 편리성이 좋은 huggingface를 사용한다.huggingface는 transformer, tokenizers, datasets 와 같이 다양한 라이브러리의 집합체로, 서로 호환성이 좋기 때문에 쉽게 모델을 학습시킬 수 있다는 장점이 있다. 다만, 공식 Documentation에 최신 버전에 맞지 않는 예전 내용이 담겨있는 경우가 있거나, 비슷한 기능을 하는 수 많은 function(save, save_model, save_pretrained 등등)들이 서로 뭐가 다른지 직관적이지 않은 이름을 가지고 있어서 매우 혼란스러운탓에 혼자서 개발하기에 어려움이 있었다. 부디 이 글이 NLP를 시도해보고자하는 학생분들과 개발자분들에게 도움이 되었으면 한다.

 

BertTokenizer 학습부터 시작해서, TPU 기반의 학습 방법까지 모든 내용을 향후 포스팅을 통해 다루고자 한다.

 

Requirements


 

pip install pathlib
pip install tokenizers

 

먼저 huggingface의 tokenizers를 pip를 통해 다운받는다.

자세한 설명은 아래 링크의 Readme를 통해 참고할 수 있다.

https://github.com/huggingface/tokenizers

 

GitHub - huggingface/tokenizers: 💥 Fast State-of-the-Art Tokenizers optimized for Research and Production

💥 Fast State-of-the-Art Tokenizers optimized for Research and Production - GitHub - huggingface/tokenizers: 💥 Fast State-of-the-Art Tokenizers optimized for Research and Production

github.com

 

 

Parameters


 

먼저 tokenizer config을 custom dataset에 맞춰서 진행해주어야 한다.

parameter를 적절하게 고르는것이 어려운데, 이럴 땐 naive하게 기존에 유명한 모델들의 값을 참고하면 좋다.

어차피 vocab_size등은 데이터셋에 따라서 엄청 크게 요동치지는 않는 것으로 보이며, 대부분의 모델들이 30000~35000사이로 많이 사용하고 있다. 

 

vocab_size를 아무리 크게 잡아도, 학습 데이터 수가 충분하지 않으면, vocab_size보다 작은 vocab이 최종적으로 도출될 수 있으므로, 무조건 크게 하는 것이 좋지는 않다는 것을 명심하자.

 

my_vocab_size = 32000
# vocab의 크기를 의미한다. 적을 수록 "단어" 단위로, 클 수록 "음절" 단위로 나뉘어진다.
my_limit_alphabet = 6000
# 모든 알파벳을 커버할 수 있도록 하여, [UNK] 빈도를 줄이기 위해 6000을 선택했다.
my_special_tokens = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]
# tokenizer 에서 사용될 special_tokens이다. 필수 토큰은 위와 같다.
user_defined_symbols = ['[BOS]','[EOS]']
# 이제부터는 부가적인 토큰이다. 문장의 시작과 끝을 알리는 토큰을 추가했다.
unused_token_num = 200
unused_list = ['[unused{}]'.format(n) for n in range(unused_token_num)]
# KoELECTRA Github를 참고하여, unused 토큰을 약 200개 추가했다. 범용성을 높일 수 있다.
user_defined_symbols = user_defined_symbols + unused_list
my_special_tokens = my_special_tokens + user_defined_symbols

 

 

Training


위에서 설정한 내용을 바탕으로 학습을 진행한다.

Corpus들은 알맞게 전처리를 하여 txt형태로 넣으면 되는데,

일반적으로 한 줄에 한 문장씩만 배치하는 방식으로 깔끔하게 쪼개 넣으면 된다.

nltk를 사용하여 Sentence 분리하는 방법도 곧 포스팅 할 예정이다.

 

paths = [str(x) for x in Path("./data/train").glob("*.txt")]
# 학습에 사용될 Corpus들을 넣으면 된다. 

tokenizer = BertWordPieceTokenizer(
    clean_text=True,
    handle_chinese_chars=True,
    strip_accents=True, 
    # 만약 cased model이라면 반드시 False로 해야한다, 또한 한글의 경우 cased model로 하면 글자가 자소분리된다.
    lowercase=True,
    # 대소문자 구분 여부를 의미한다. 한글의 경우 무의미하므로 신경쓰지 않아도 된다.
    wordpieces_prefix="##"
)

tokenizer.train(
    files=paths,
    limit_alphabet=my_limit_alphabet,
    vocab_size=my_vocab_size,
    min_frequency=5,
    # pair가 5회이상 등장할시에만 학습
    show_progress=True,
    # 진행과정 출력 여부
    special_tokens=my_special_tokens
)

 

Saving


 

Saving 방법이 매우 다양해서 혼란스러웠는데, 최종적으로 아래 2가지 방법 중 편리한 것을 선택하면 된다.

 

1. vocab만 추출하는 방식

 

tokenizer.save("./tokenizer/tok_added-ch-{}-wpm-{}".format(my_limit_alphabet, my_vocab_size),True)
# config, model등 저장하지 않고 vocab 정보만 json 형태로 저장



import json
vocab_path = "your tokenizer path"
# save의 결과로 추출된 파일 경로
vocab_file = './tokenizer/vocab.txt'
# vocab.txt 형태로 저장할 경로
f = open(vocab_file,'w',encoding='utf-8')
with open(vocab_path) as json_file:
    json_data = json.load(json_file)
    for item in json_data["model"]["vocab"].keys():
        f.write(item+'\n')

    f.close()

 

위와 같은 방식으로 저장했을 시, 아래와 같이 vocab 목록만 저장된 txt파일이 최종적으로 저장되며, 이를 통해 추후 tokenizer를 불러올 수 있다.

 

 

2. pretrained model 형식으로 저장하기

 

huggingface는 자체적으로 pretrained model을 불러올 수 있도록 규격화된 모델 디렉터리 형식이 존재한다.

아래 코드를 통해 tokenizer를 pretrained_model 형태로 저장한다. 1번 방식과 달리 vocab.txt, tokenizer_config.json, special_tokens_map.json 3개의 파일이 저장된다.

 

Tokenizer를 활용하여 추후 자체 NLP 모델까지 학습을 진행하고자 한다면, pretrained model형식으로 저장해두는것이 나중에 학습용 스크립트에서 AutoTokenizer를 통해 불러올 때 용이하므로, 이 방식을 추천한다.

 

문제는 BertWordPieceTokenizer 클래스는 save_pretrained 파라미터가 없다는 것이다.

아마 tokenizers와 transformers 라이브러리의 차이가 아닐까 싶다.

 

따라서, 위에서 학습된 vocab.txt파일을 먼저 BertTokenizer 형태로 불러온 후,

save_pretrained를 진행할 수 있다.

애초부터 BertTokenizer로 학습이 가능한지는 시도를 못해봤다. (혹시 아시는 분 있으시면 댓글 부탁드립니다!)

 

from transformers import BertTokenizer
vocab_path = "<vocab.txt 경로>"
tokenizer = BertTokenizer(vocab_file=vocab_path, do_lower_case=True) 
tokenizer.save_pretrained('./<저장하고 싶은 경로>/')

 

 

Loading


저장한 방법 두가지에 따라서 불러오는 방법도 다르다.

 

1. Vocab 파일을 통해 불러오는 방식

 

from transformers import BertTokenizer
vocab_path = "<vocab.txt 경로>"
tokenizer = BertTokenizer(vocab_file=vocab_path, do_lower_case=True) 
# 학습할 때 썻던 config값을 바탕으로 do_lower_case 여부를 선택해야함

test_str = 'This invention relates to improved metal-containing spinel compositions, particularly for use in a manner to effect a reduction in the emission of sulfur oxides and/or nitrogen oxides to the atmosphere.'
print('테스트 문장: ',test_str)
encoded_str = tokenizer.encode(test_str,add_special_tokens=True)
print('문장 인코딩: ',encoded_str)
decoded_str = tokenizer.decode(encoded_str)
print('문장 디코딩: ',decoded_str)

## 실행 결과 ##
# 테스트 문장:  This invention relates to improved metal-containing spinel compositions, particularly for use in a manner to effect a reduction in the emission of sulfur oxides and/or nitrogen oxides to the atmosphere.
# 문장 인코딩:  [2, 831, 730, 1265, 639, 2081, 1350, 17, 1601, 22544, 2344, 16, 1447, 659, 1038, 625, 43, 2047, 639, 1645, 43, 2809, 625, 618, 3448, 630, 4334, 5878, 641, 19, 661, 3377, 5878, 639, 618, 5376, 18, 3]
# 문장 디코딩:  [CLS] this invention relates to improved metal - containing spinel compositions, particularly for use in a manner to effect a reduction in the emission of sulfur oxides and / or nitrogen oxides to the atmosphere. [SEP]

 

2. AutoTokenizer를 통해 pretrained model을 불러오는 방식

 

tokenizer = AutoTokenizer.from_pretrained('저장된 경로')

 

반응형
블로그 이미지

Hyunsoo Luke HA

석사를 마치고 현재는 Upstage에서 전문연구요원으로 활동중인 AI 개발자의 삽질 일지입니다! 이해한 내용을 정리하는 용도로 만들었으니, 틀린 내용이 있으면 자유롭게 의견 남겨주세요!

,