[NLP] Tokenizer 제작하기

잉송·2022년 2월 14일
2

NLP

목록 보기
2/9
post-thumbnail

개요

현재 대부분의 NLP task는 PLM (Pre-trained Language Model)을 통한 전이 학습이 대세로 자리잡았다. 하지만 긴 Pretraining을 수행 전 vocab을 만드는 건 정말 중요하다. 좋은 vocab을 만드는 것이 곧 토크나이징 퀄리티와 직결되고, 이는 모델이 맥락 지식을 잘 학습하여 downstream task에서 좋은 성능을 내는 데에까지 영향을 미치기 때문이다. 모델 학습에 들어가기에 앞서 vocab을 잘 살펴볼 필요가 있다.

PLM
PLM (Pre-trained Language Model)은 사전에 학습시킨 언어 모델을 의미한다. PLM은 많은 양의 text data를 이용하여, 일반적인 수준의 언어 이해가 가능하도록 모델을 사전 훈련한다. 훈련한 PLM은 텍스트 분류, 생성, 번역 등과 같은 다양한 NLP task에서 좋은 성능을 내는것이 목적이다. 이렇게 pre-train한 모델을 가져온 뒤 NLP task를 수행함으로써 파라미터들을 업데이트하는 과정을 fine-tuning이라고 한다. end-to-end로 모델을 학습하는 task-specific 방식에 비해 여러 자연어처리 task의 성능을 비약적으로 끌어올렸다.

End-to-End (종단간 기계학습)
신경망은 한쪽 끝에서 입력을 받아들이고 다른 쪽 끝에서 출력을 생성하는데, 입력 및 출력을 직접 고려하여 네트워크 가중치를 최적화 하는 학습을 종단 간 학습(End-to-end Learning) 이라고 한다.

우선 vocab을 만들기 위해서는 우리가 가진 말뭉치, 문장 데이터를 토큰으로 나누어 주어야한다. 이때 말뭉치나 입력으로 들어온 문장을 토큰로 나누어 주는것을 토큰화한다고 하며, Tokenizer가 문장을 토큰으로 나누어 준다. 이 때 주의할 점은 토큰은 단어 일 수도 있으며, 단어가 아닐 수도 있다.

Vocab

한 마디로 사전이다. 텍스트 파일에 토큰화 된 단어들이 나열 되어 있는 파일로 보면 된다. 아래는 vocab.txt의 예이다.

[PAD]
[UNK]
[CLS]
[SEP]
[MASK]
##여름
##르모
##전자
##블린
##노잼
##인격
...

Tokenizer

입력으로 들어온 문장들에 대해 토큰으로 나누어 주는 역할을 한다. tokenizer은 크게 word tokenizer와 subword tokenizer 두 가지로 나눌수 있다.

1. Word Tokenizer

단어를 기준으로 토큰화를 한 토크나이저이다.

경찰차, 경찰복, 경찰관

2. Subword Tokenizer

단어를 나누어 단어 안에 단어들로 토큰화를 하는것을 말한다. 단어의 분절을 하는 토크나이저이다.

경찰, #차, #복, #관

subword tokenizer은 vocab에 없는 단어들에 대해서도 좋은 성능을 보인다는 장점을 가진다. 이를 통해 OOV나 희귀 단어, 신조어와 같은 문제를 완화시킬 수 있다. 하지만 그렇다고 BPE가 완벽하게 OOV를 해결하진 못한다. 코퍼스에서 몇번 등장하지 않는 단어들은 굳이 vocab에 포함하지 않는 경우도 있다. ETRI의 KoBERT는 5번 미만 등장한 단어는 포함시키지 않았다. 즉 이 단어들은 OOV를 야기할 수도 있다. 실제로 한국어는 서브워드 분리를 시도했을 때 어느정도 의미있는 단위로 나누는 것이 가능하다.

Subword 분절 방식엔 BPE, SentencePiece, WordPiece 등 작은 변형들이 존재한다. 한국어 자연어처리에 BERT, Transformer를 사용하기 위해서는 개인 한글 데이터의 subword 분절을 직접 구축해야 한다.

BPE
BPE(Byte Pair Encoding)란 연속적으로 가장 많이 등장한 글자의 쌍을 찾아서 하나의 글자로 병합하는 방식이다. 장점은 말뭉치가 있다면 비교적 간단하게 만들 수 있다. OOV를 최소화 할 수 있다. 단점은 Subword의 분할이 의미 기준이 아닐수 있다. 예를 들어 '포항에'라는 문장을 분할할 때 ['#포항', '에']가 아닌 ['#포', '항에']로 분할되기도 한다. 또는 ‘대한민국을’, ‘대한민국은’, ‘대한민국으로’ 등의 빈도수가 단어들은 [‘대한민국’, ‘을’, ‘은’, ‘으로’] 형태로 분류되길 원하지만 [‘대한민국을’, ‘대한민국은’, ‘대한민국으로’] 그대로 분류되기도 합니다.


Huggingface Tokenizer

자연어 처리 스타트업 허깅페이스가 개발한 패키지 tokenizers는 자주 등장하는 서브워드들을 하나의 토큰으로 취급하는 다양한 서브워드 토크나이저를 제공합니다. Huggingface tokenizer는 아래 4가지 Tokenizer를 제공한다. 일반 BPE, Byte level BPE, SentencePiece, WordPiece이다.

  • CharBPETokenizer: The original BPE
  • ByteLevelBPETokenizer: The byte level version of the BPE
  • SentencePieceBPETokenizer: A BPE implementation compatible with the one used by SentencePiece
  • BertWordPieceTokenizer: The famous Bert tokenizer, using WordPiece

GPT, Roberta: ByteLevelBPETokenizer
BERT: BertWordPieceTokenizer

하지만 BERT의 저자는 WordPiece의 구현체를 공개하지 않아 SentencePiece를 사용하길 추천하기도 한다. SentencePieceBPETokenizer는 구글의 SentencePiece 구현체 이다. 이번 실습에서는 이 중에서 WordPiece Tokenizer를 실습해보겠습니다.

WordPiece Tokenizer
BPE의 변형 알고리즘입니다. Wordpiece의 경우 likelihood를 기반으로 BPE를 수행한 알고리즘이다.


1. huggingface tokenizer 설치

pip install tokenizers

2. WordPiece Vocab 생성

import os
from tokenizers import BertWordPieceTokenizer

tokenizer = BertWordPieceTokenizer(strip_accents=False, lowercase=False)

corpus_file   = [path]  # data path
vocab_size    = 32000   #vocab의 크기. 보통 32,000이 좋다고 알려짐.
limit_alphabet= 6000    #merge 수행 전 initial tokens이 유지되는 숫자 제한
output_path   = 'hugging_%d'%(vocab_size)
min_frequency = 5  # 단어의 최소 발생 빈도

tokenizer.train(files=corpus_file,
               vocab_size=vocab_size,
               min_frequency=min_frequency,
               limit_alphabet=limit_alphabet, 
               show_progress=True)

tokenizer.save_model(hf_model_path)

text data는 2021 모두의 말뭉치 감성분석 + NSMC(네이버 영화 리뷰 감성분석) 데이터를 사용하였다.

3. tokenizer 테스트

from transformers import BertTokenizerFast

tokenizer = BertTokenizerFast.from_pretrained(hf_model_path,
                                                       strip_accents=False,
                                                       lowercase=False) 
                                                       
tokenized_input_for_pytorch = tokenizer("비급영화 병맛컨셉 시청 잘 했구여", return_tensors="pt")

print("Tokens (str)      : {}".format([tokenizer.convert_ids_to_tokens(s) for s in tokenized_input_for_pytorch['input_ids'].tolist()[0]]))
print("Tokens (int)      : {}".format(tokenized_input_for_pytorch['input_ids'].tolist()[0]))
print("Tokens (attn_mask): {}\n".format(tokenized_input_for_pytorch['attention_mask'].tolist()[0]))

'''
Tokens (str)      : ['[CLS]', '비급', '##영화', '병맛', '##컨', '##셉', '시청', '잘', '했', '##구', '##여', '[SEP]']
Tokens (int)      : [2, 10612, 4944, 5401, 4013, 3765, 4837, 1900, 2630, 2911, 2901, 3]
Tokens (attn_mask): [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
'''

token과 그 index, 그리고 attention_mask도 자동으로 생성해준다.

SentencePiece

Sentencepiece은 BPE의 변형 알고리즘이다. Sentencepiece의 경우 빈도수를 기반으로 BPE를 수행한다.

1. SentencePiece 설치

pip install sentencepiece

2. SentencePiece Vocab 생성

  • input_file : text data 경로
  • vocab_size : BPE의 단어수를 얼마로 할 것인가 이다. 너무 적으면 한 글자 단위로 쪼개지는 경향이 있고, 너무 많으면 쓸데없는 단어들이 만들어진다. 주로 32,000이 가장 좋다고 알려져 있다.
  • model_name : 저장할 model 이름. <model_name>.model, <model_name>.vocab 2개의 파일이 만들어진다.
  • model_type : bpe, unigram 등이 있는데 두가지를 모두 사용해 보고 성능이 좋은것 선택
  • character_coverage : 모든 단어를 커버할것인가, 너무 희귀한 단어는 뺄 것인가 이다. 학습 코퍼스가 대용량이라면 보통 default=0.9995로 사용하면 된다. 그런데 코퍼스가 작다면 1.0으로 지정하자. 그럼 [UNK]가 없다.
  • user_defined_symbols : BPE로 생성된 단어 외 알고리즘에서 사용할 특수문자들을 지정한다.
import sentencepiece as spm

# spm_train --input=data/train_tokenizer.txt  --model_prefix=sentencepiece/sp --vocab_size=32000 character_coverage=1.0 --model_type="unigram"

input_file = data_path
vocab_size = 32000

sp_model_root='sentencepiece'
if not os.path.isdir(sp_model_root):
    os.mkdir(sp_model_root)
sp_model_name = 'tokenizer_%d' % (vocab_size)
sp_model_path = os.path.join(sp_model_root, sp_model_name)
model_type = 'unigram'  # 학습할 모델 선택, unigram이 더 성능이 좋음'bpe'
character_coverage  = 1.0  # 전체를 cover 하기 위해, default=0.9995
user_defined_symbols = '[PAD],[UNK],[CLS],[SEP],[MASK],[BOS],[EOS],[UNK0],[UNK1],[UNK2],[UNK3],[UNK4],[UNK5],[UNK6],[UNK7],[UNK8],[UNK9],[unused0],[unused1],[unused2],[unused3],[unused4],[unused5],[unused6],[unused7],[unused8],[unused9],[unused10],[unused11],[unused12],[unused13],[unused14],[unused15],[unused16],[unused17],[unused18],[unused19],[unused20],[unused21],[unused22],[unused23],[unused24],[unused25],[unused26],[unused27],[unused28],[unused29],[unused30],[unused31],[unused32],[unused33],[unused34],[unused35],[unused36],[unused37],[unused38],[unused39],[unused40],[unused41],[unused42],[unused43],[unused44],[unused45],[unused46],[unused47],[unused48],[unused49],[unused50],[unused51],[unused52],[unused53],[unused54],[unused55],[unused56],[unused57],[unused58],[unused59],[unused60],[unused61],[unused62],[unused63],[unused64],[unused65],[unused66],[unused67],[unused68],[unused69],[unused70],[unused71],[unused72],[unused73],[unused74],[unused75],[unused76],[unused77],[unused78],[unused79],[unused80],[unused81],[unused82],[unused83],[unused84],[unused85],[unused86],[unused87],[unused88],[unused89],[unused90],[unused91],[unused92],[unused93],[unused94],[unused95],[unused96],[unused97],[unused98],[unused99]'

input_argument = '--input=%s --model_prefix=%s --vocab_size=%s --user_defined_symbols=%s --model_type=%s --character_coverage=%s'
cmd = input_argument%(input_file, sp_model_path, vocab_size,user_defined_symbols, model_type, character_coverage)

spm.SentencePieceTrainer.Train(cmd)

3. tokenizer 테스트

import sentencepiece as spm
sp = spm.SentencePieceProcessor()
sp.Load('{}.model'.format(sp_model_path))

tokens = sp.encode_as_pieces(text)
ids = sp.encode_as_ids(text)

print("Tokens (str)      : {}".format(tokens))
print("Tokens (int)      : {}".format(ids))

'''
Tokens (str)      : ['▁비급영화', '▁병맛', '컨셉', '▁시청', '▁잘', '▁했', '구여']
Tokens (int)      : [26135, 1629, 17080, 3210, 153, 3733, 19414]
'''

비교

- koelectra tokenizer 결과
['[CLS]', '비', '##급', '##영화', '병', '##맛', '##컨', '##셉', '시청', '잘', '했', '##구', '##여', '[SEP]']

- Huggingface : WordPiece tokenizer 결과
['[CLS]', '비급', '##영화', '병맛', '##컨', '##셉', '시청', '잘', '했', '##구', '##여', '[SEP]']

- SentencePiece 결과
['▁비급영화', '▁병맛', '컨셉', '▁시청', '▁잘', '▁했', '구여']

koelectra보다 확실히 WordPiece tokenizer와 SentencePiece가 영화 감성 위주의 단어가 잘 반영되었다. 비급, 병맛 같은 감성에 영향을 줄 수 있는 단어를 잘 파악했다. 특히 sentencepiece는 비급영화를 하나의 단어로 보고 있다.

향후

각각의 Subword 분절 방식을 비교해보고 어떤 분절 방식이 중요 감성 영역 추출 task에 적절한지 파악해보도록 하겠습니다. 아직 다 실험해본것은 아니지만 현재 koelectra tokenizer와 BertWordPieceTokenizer를 비교하면 koelectra tokenizer이 더 잘나오는 편이다... 과연 tokenizer 생성이 도움이 될 것인가... 자세한 비교는 추후에 가져오도록 하겠다...





참조문헌

profile
NLP 공부하는 사람

0개의 댓글