SentencePiece를 활용한 효과적인 한국어 토크나이저 만들기

소개

자연어 문장을 컴퓨터가 쉽게 이해하게 만들기 위해서는 다양한 전처리 과정을 거쳐야합니다.
그 중 하나로 문장을 토큰 단위로 쪼개서 처리하는 토크나이징 기법이 있습니다.
오늘은 SentencePiece를 활용하여 한국어 텍스트를 효과적으로 토크나이징 하는 방법을 소개합니다.

SentencePiece는 Google에서 2018년도에 공개한 오픈소스 라이브러리로, 다양한 자연어처리 태스크에서 널리 사용되고 있습니다.
최근에는 Huggingface에서 공개한 Tokenizers도 자주 사용되고 있지만 오늘은 Sentencepiece에 대한 내용을 주로 다루도록 하겠습니다.

텍스트 전처리 과정 예시

데이터 전처리 예시
텍스트 데이터를 모델이 이해하는 벡터로 바꾸려면 다양한 전처리 과정을 거치게 됩니다. 오늘 다루는 주제는 이중에서도 가장 앞단에 있는 텍스트를 토큰 단위로 쪼개는(Split) 전처리 과정을 다룹니다. 오른쪽 그림은 "히어로 무비 중 가장 어둡지만 가장 참신했다." 라는 문장을 벡터로 변환하는 과정입니다. 텍스트 문장은 word, character, 형태소, subword (char, byte)등 다양한 방법으로 쪼개서 처리될 수 있습니다. image

설치방법

  • pyenv를 통해 sentencepiece를 설치할 환경을 구성합니다
  • 작성시점 기준 비교적 최신인 3.11.x 버전을 설치해줍니다
  • 설치는 pyenv 기준으로 진행하겠습니다 (MacOS 환경입니다)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ pyenv install -list
Available versions:
....
3.11.1
3.12.0a4
3.12-dev
activepython-3.6.0
anaconda-1.4.0

$ pyenv install 3.11.1
python-build: use openssl@1.1 from homebrew
python-build: use readline from homebrew
Downloading Python-3.11.1.tar.xz...
-> https://www.python.org/ftp/python/3.11.1/Python-3.11.1.tar.xz
Installing Python-3.11.1...
python-build: use readline from homebrew
Installed Python-3.11.1 to /Users/사용자계정명/.pyenv/versions/3.11.1
  • pyenv로 파이썬 가상환경을 구성 후 필요한 패키지를 설치합니다
1
2
3
4
5
$ pyenv virtualenv 3.11.1 nlp
$ pyenv activate nlp
$(nlp) pip install sentencepiece
$(nlp) pip install datasets
$(nlp) pip install transformers

학습방법

  • SentencePiece는 subword 토크나이저로 단어를 subword 단위로 구성하여 학습합니다
  • 주어진 corpus를 subword 단위로 구성하여 subword의 빈도수를 계산해서 높은 빈도수를 가진 subword를 병합하여 모델을 완성합니다
  • subword의 패턴을 파악하기 위해서는 corpus가 필요합니다
  • 학습할 corpus는 주로 wiki 데이터 또는 모델을 활용하고자 하는 도메인에서 일부를 샘플링해서 구축하지만 실습의 편의를 고려하여 nsmc (naver sentiment movie corpus)를 사용하겠습니다
  • nsmc 데이터는 huggingface dataset hub에서도 다운받을 수 있습니다
1
2
3
4
5
6
7
8
9
10
11
from datasets import load_dataset
import os

data_dir = './data'
dataset = load_dataset('nsmc')
os.makedirs(data_dir, exist_ok=True)
for split_key in dataset.keys():
doc_path = f"{data_dir}/{split_key}.txt"
with open(doc_path, 'w') as f:
for doc in dataset[split_key]['document']:
f.write(doc+'\n')
  • 데이터를 다운받으면 아래와 같이 학습에 사용할 데이터가 구축됩니다
1
2
3
4
5
.
├── README.md
├── data
├── test.txt
└── train.txt
1
2
3
4
5
6
$ head -n 5 train.txt 
아 더빙.. 진짜 짜증나네요 목소리
흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나
너무재밓었다그래서보는것을추천한다
교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정
사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 던스트가 너무나도 이뻐보였다
  • 데이터가 구축되었으면 sentencepiece 라이브러리를 활용해서 토크나이저를 학습해봅니다
    • 본 예제에서는 T5 모델을 위한 CBPE(char level BPE) 토크나이저를 학습하겠습니다
      • BPE는 Byte-Pair Encoding의 약자로 1994년에 제안된 데이터 압축 알고리즘입니다.
        • 자연어처리에서는 char-level(CBPE) or byte-level(BBPE)에서 시작해서 점차적으로 vocab을 만들어내는 Bottom-up 방식으로 학습하게 됩니다
        • 자연어 처리에서는 기계번역 태스크에서 적용되기 시작했습니다
    • vocab size는 special_tokens (pad, bos, eos, unk, ….) 7개 + additional_special_tokens (T5를 위한 <extra_id_XX> 토큰) 100개 + subwords 토큰 31,900개로 구성되어 총 32,000 의 크기를 갖게 셋팅했습니다.
    • vocab size는 하이퍼파라미터로 정해진 값은 없습니다 (보통은 10,000~52,000 사이지만 모델의 크기에 따라 다름)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sentencepiece as spm
from pathlib import Path

data_dir = './data'
paths = [str(x) for x in Path(data_dir).glob("*.txt")]
corpus = ",".join(paths)
prefix = "t5-sp-bpe-nsmc"
vocab_size = 31900-7
spm.SentencePieceTrainer.train(
f"--input={corpus} --model_prefix={prefix} --vocab_size={vocab_size + 7}" +
" --model_type=bpe" +
" --max_sentence_length=999999" + # 문장 최대 길이 (너무 길면 에러발생)
" --pad_id=0 --pad_piece=<pad>" + # pad (0)
" --unk_id=1 --unk_piece=<unk>" + # unknown (1)
" --bos_id=2 --bos_piece=<s>" + # begin of sequence (2)
" --eos_id=3 --eos_piece=</s>" + # end of sequence (3)
" --user_defined_symbols=<sep>,<cls>,<mask>") # 사용자 정의 토큰
  • sentencepiece 학습로그
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29600 all=641749 active=32848 piece=틈없이
bpe_model_trainer.cc(167) LOG(INFO) Updating active symbols. max_freq=11 min_freq=5
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29620 all=641915 active=32245 piece=...0
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29640 all=641935 active=32265 piece=▁out
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29660 all=641945 active=32275 piece=▁같더라
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29680 all=641952 active=32282 piece=▁공연을
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29700 all=641961 active=32291 piece=▁기억할
bpe_model_trainer.cc(167) LOG(INFO) Updating active symbols. max_freq=11 min_freq=5
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29720 all=641995 active=32128 piece=▁나이든
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29740 all=642015 active=32148 piece=▁누나가
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29760 all=642043 active=32176 piece=▁돌려도
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29780 all=642071 active=32204 piece=▁로코물
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29800 all=642089 active=32222 piece=▁머랄까
bpe_model_trainer.cc(167) LOG(INFO) Updating active symbols. max_freq=11 min_freq=5
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29820 all=642124 active=32140 piece=▁미친짓
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29840 all=642148 active=32164 piece=▁범죄가
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29860 all=642164 active=32180 piece=▁봣지만
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29880 all=642172 active=32188 piece=▁사고의
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29900 all=642198 active=32214 piece=▁서로가
bpe_model_trainer.cc(167) LOG(INFO) Updating active symbols. max_freq=11 min_freq=5
trainer_interface.cc(685) LOG(INFO) Saving model: t5-sp-bpe-nsmc.model
trainer_interface.cc(697) LOG(INFO) Saving vocabs: t5-sp-bpe-nsmc.vocab
  • sentencepiece의 학습이 끝나면 아래와 같이 학습된 파일이 만들어집니다
1
2
3
4
5
6
7
├── README.md
├── data
│ ├── test.txt
│ └── train.txt
├── t5-sp-bpe-nsmc.model
├── t5-sp-bpe-nsmc.vocab
└── train.py

학습된 토크나이저 사용하기

  • 학습된 토크나이저 모델파일을 sentencepiece 라이브러리를 통해 다시 로딩해서 쓸 수 있지만, 사용성을 위해 huggingface의 T5Tokenizer로 랩핑해서 사용하겠습니다
  • save_pretrained()함수를 사용하면 최근 많이 사용되는 huggingface의 토크나이저의 포멧으로 저장되어 재사용 할 수 있습니다
  • huggingface 클래스를 사용할때 실제 필요한 파일은 t5-sp-bpe-nsmc.model 하나만 있으면 됩니다
1
2
3
from transformers import T5Tokenizer
tokenizer = T5Tokenizer(vocab_file="t5-sp-bpe-nsmc.model")
tokenizer.save_pretrained("t5-tokenizer-bpe-nsmc")
  • huggingface에서 사용하는 토크나이저로 저장된 모습
1
2
3
4
5
6
7
8
9
10
11
├── README.md
├── data
│ ├── test.txt
│ └── train.txt
├── t5-sp-bpe-nsmc.model
├── t5-sp-bpe-nsmc.vocab
├── t5-tokenizer-bpe-nsmc
│ ├── special_tokens_map.json
│ ├── spiece.model
│ └── tokenizer_config.json
└── train.py
  • 이제 학습한 토크나이저를 테스트해보겠습니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from transformers import T5Tokenizer
tokenizer = T5Tokenizer(vocab_file="t5-sp-bpe.model")
tokenizer.save_pretrained("t5-tokenizer-bpe-nsmc")
lines = [
"`DEVOCEAN`은 SK그룹의 대표 개발자 커뮤니티이자🧑",
"내/외부 개발자 간 소통과 성장을 위한 플랫폼을 상징합니다.👋",
"`Developers`' Ocean 개발자들을 위한 영감의 바다🙏",
"`Devotion` 헌신,몰두,전념💯",
"`Technology for Everyone` 모두를 위한 기술👍",
]
for line in lines:
tokens = tokenizer.tokenize(line)
inputs = tokenizer(line)
decoded_sequence = tokenizer.decode(inputs['input_ids'])
print(line) # 입력 데이터
print(tokens) # subword로 토큰화된 데이터
print(decoded_sequence) # subword토큰화된 데이터 -> token id -> 복원된데이터
print()
  • 실행 결과 입력 데이터들이 대부분 subword로 잘 분절되어 쪼개지고 다시 decoding 했을때 복원된걸 확인할 수 있습니다
  • 하지만 위 결과에서 한가지 문제가 있습니다
  • 학습 corpus에 없었던 🧑👋🙏💯👍와 같은 토큰들은 decoding시에 unk(Unknown Token) 토큰으로 복원되게 됩니다
  • 🧑👋🙏💯👍와 같은 특수문자 또는 외국어등은 학습 corpus에 등장하는 비율이 낮아 토크나이저에서 decoding시에 unk토큰으로 복원될 가능성이 있습니다
  • 이러한 문제를 해결하기 위해 등장한것이 BBPE (Byte-level BPE)입니다 (GPT 계열의 모델이 사용함)
    • subword 학습을 char level이 아닌 byte level 단위로 수행하는 것입니다
    • 이러한 방법을 사용했을때는 사람이 육안으로 토큰들을 확인하는건 불편하지만, OOV(Out-Of-Vocabulary)를 줄일 수 있다는 장점이 있습니다
    • sentencepice에서는 공식적으로 BBPE를 지원하지 않지만, 비슷한 효과를 내는 " --byte_fallback=true" 옵션이 있습니다
    • " --byte_fallback=true" 옵션은 unk 토큰을 만났을때 해당 토큰을 utf-8 byte level로 쪼개서 처리해줍니다 (참고)
  • " --byte_fallback=true" 옵션을 적용해서 재학습후 평가해보겠습니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sentencepiece as spm
from pathlib import Path

paths = [str(x) for x in Path(data_dir).glob("*.txt")]
corpus = ",".join(paths)
prefix = "t5-sp-bpe-nsmc-byte-fallback"
vocab_size = 31900-7
spm.SentencePieceTrainer.train(
f"--input={corpus} --model_prefix={prefix} --vocab_size={vocab_size + 7}" +
" --model_type=bpe" +
" --max_sentence_length=999999" + # 문장 최대 길이 -> 이게 너무 길면 에러발생함
" --pad_id=0 --pad_piece=<pad>" + # pad (0)
" --unk_id=1 --unk_piece=<unk>" + # unknown (1)
" --bos_id=2 --bos_piece=<s>" + # begin of sequence (2)
" --eos_id=3 --eos_piece=</s>" + # end of sequence (3)
" --byte_fallback=true" + # add byte_fallback for unk tokens
" --user_defined_symbols=<sep>,<cls>,<mask>") # 사용자 정의 토큰
  • 실행 로그
1
2
3
4
5
6
7
8
9
bpe_model_trainer.cc(167) LOG(INFO) Updating active symbols. max_freq=11 min_freq=5
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29820 all=642124 active=32140 piece=▁미친짓
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29840 all=642148 active=32164 piece=▁범죄가
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29860 all=642164 active=32180 piece=▁봣지만
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29880 all=642172 active=32188 piece=▁사고의
bpe_model_trainer.cc(258) LOG(INFO) Added: freq=11 size=29900 all=642198 active=32214 piece=▁서로가
bpe_model_trainer.cc(167) LOG(INFO) Updating active symbols. max_freq=11 min_freq=5
trainer_interface.cc(685) LOG(INFO) Saving model: t5-sp-bpe-nsmc-byte-fallback.model
trainer_interface.cc(697) LOG(INFO) Saving vocabs: t5-sp-bpe-nsmc-byte-fallback.vocab
  • 학습한 모델 로딩 후 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from transformers import T5Tokenizer
tokenizer = T5Tokenizer(vocab_file="t5-sp-bpe-nsmc-byte-fallback.model")
tokenizer.save_pretrained("t5-tokenizer-bpe-nsmc-byte-fallback")
lines = [
"`DEVOCEAN`은 SK그룹의 대표 개발자 커뮤니티이자🧑",
"내/외부 개발자 간 소통과 성장을 위한 플랫폼을 상징합니다.👋",
"`Developers`' Ocean 개발자들을 위한 영감의 바다🙏",
"`Devotion` 헌신,몰두,전념💯",
"`Technology for Everyone` 모두를 위한 기술👍",
# "🧜‍♂️🧚‍♀️🧚🧚‍♂️👼🤰🧟‍♂️🧞‍♀️🧞🧞‍♂️🧜‍♀️🧜🧝‍♂️🧛‍♀️🧛ꎐదꁯᛠ፨ꏍ "

]
for line in lines:
tokens = tokenizer.tokenize(line)
inputs = tokenizer(line)
decoded_sequence = tokenizer.decode(inputs['input_ids'])
print(line)
print(tokens)
print(decoded_sequence)
print()

결과

image.png

  • 기존과 달리 🧑👋🙏💯👍와 같은 토큰들이 제대로 복원된 것을 확인할 수 있습니다

맺으며

오늘은 자연어처리에서 가장 많이 쓰이는 라이브러리중 하나인 SentencePiece와 Huggingface의 transformers를 통해 토크나이저를 학습하고 OOV 문제를 해결하는 방법을 다뤘습니다. 가독성 있는 토큰화된 결과와 OOV 이슈를 해소하기 원한다면 SentencePiece CBPE + byte_fallback=true 옵션을 고려해보실 수 있을 것 같습니다.

참고자료

SentencePiece를 활용한 효과적인 한국어 토크나이저 만들기

https://eagle705.github.io/SentencePiece를 활용한 효과적인 한국어 토크나이저 만들기/

Author

Joosung Yoon

Posted on

2023-02-20

Updated on

2023-03-04

Licensed under

댓글