자연어 문장을 컴퓨터가 쉽게 이해하게 만들기 위해서는 다양한 전처리 과정을 거쳐야합니다. 그 중 하나로 문장을 토큰 단위로 쪼개서 처리하는 토크나이징 기법이 있습니다. 오늘은 SentencePiece를 활용하여 한국어 텍스트를 효과적으로 토크나이징 하는 방법을 소개합니다.
텍스트 데이터를 모델이 이해하는 벡터로 바꾸려면 다양한 전처리 과정을 거치게 됩니다. 오늘 다루는 주제는 이중에서도 가장 앞단에 있는 텍스트를 토큰 단위로 쪼개는(Split) 전처리 과정을 다룹니다. 오른쪽 그림은 "히어로 무비 중 가장 어둡지만 가장 참신했다." 라는 문장을 벡터로 변환하는 과정입니다. 텍스트 문장은 word, character, 형태소, subword (char, byte)등 다양한 방법으로 쪼개서 처리될 수 있습니다.
$ 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
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" withopen(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>") # 사용자 정의 토큰
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" 옵션이 있습니다
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()
결과
기존과 달리 🧑👋🙏💯👍와 같은 토큰들이 제대로 복원된 것을 확인할 수 있습니다
맺으며
오늘은 자연어처리에서 가장 많이 쓰이는 라이브러리중 하나인 SentencePiece와 Huggingface의 transformers를 통해 토크나이저를 학습하고 OOV 문제를 해결하는 방법을 다뤘습니다. 가독성 있는 토큰화된 결과와 OOV 이슈를 해소하기 원한다면 SentencePiece CBPE + byte_fallback=true 옵션을 고려해보실 수 있을 것 같습니다.