한국어 형태소 분석기 Kiwi를 활용한 딥러닝 언어 모델들을 실험적으로 키우는 공간입니다.
BERT, GPT, BART와 같은 딥러닝 언어모델에서는 크기가 고정된 닫힌 어휘 집합을 사용합니다. 따라서 딥러닝 언어모델에 텍스트를 입력하려면 임의의 텍스트를 고정된 어휘 집합으로 분할하여 변환해주는 토크나이저(Tokenizer)가 필수적입니다. 한국어의 경우 오랫동안 개발되어온 형태소 분석기가 있으나, 기존의 형태소 분석기들은 분석결과를 고정된 개수의 어휘로 출력하는 기능이 없었으므로 형태소 분석기를 토크나이저로 사용할 수 없었습니다. 그래서 한국어의 특징을 고려하지 못함에도 Byte Pair Encoding이나 SentencePiece 등을 토크나이저로 사용하고 있는 상황입니다.
Kiwi
는 0.15버전에서부터 형태소 분석과 Subword 분절 기능을 통합한 Unigram 토크나이저를 제공합니다.
이 저장소에서는 Kiwi를 기반으로한 토크나이저의 성능을 실험하고, 실제로 이 토크나이저를 기반으로 학습한 딥러닝 모델의 특징을 분석해보고자 합니다.
이 저장소에서 공개된 Kiwi 기반의 딥러닝 언어 모델을 사용하려면 kiwipiepy>=0.15.1
과 transformers>=4.12
가 필요합니다. 요구사항이 모두 준비된 상황이라면 아래와 같이 간단하게 kiwi-farm의 모델을 가져와서 사용할 수 있습니다.
from transformers import (
AutoTokenizer,
AutoModelForMaskedLM,
)
import kiwipiepy.transformers_addon
# kiwipiepy.transformers_addon를 import해야
# KiwiTokenizer가 AutoTokenizer에 등록된다.
# KiwiTokenizer는 PreTrainedTokenizer와 대부분의 기능이 호환되므로
# 기존의 transformers 코드를 그대로 사용할 수 있다.
tokenizer = AutoTokenizer.from_pretrained('kiwi-farm/roberta-base-32k')
model = AutoModelForMaskedLM.from_pretrained('kiwi-farm/roberta-base-32k')
KiwiTokenizer
를 학습하는 데에는 다음과 같은 말뭉치를 사용했습니다.
- 모두의 말뭉치: 문어 말뭉치
- 모두의 말뭉치: 구어 말뭉치
- 모두의 말뭉치: 비출판물 말뭉치
- 모두의 말뭉치: 신문 말뭉치
아래는 KiwiTokenizer
와 다른 토크나이저를 비교한 결과입니다.
토크나이저 | Vocab Size | Special Tokens | Hangul Tokens | Alnum Tokens | Hanja Tokens |
---|---|---|---|---|---|
KiwiTokenizer 16k | 16000 | 7 | 12777 (11130 / 1647) | 804 (295 / 509) | 1598 (1598 / 0) |
KiwiTokenizer 32k | 32000 | 7 | 26285 (21917 / 4368) | 2446 (965 / 1481) | 2334 (2334 / 0) |
KiwiTokenizer 48k | 48000 | 7 | 39670 (32345 / 7325) | 4478 (1960 / 2518) | 2799 (2799 / 0) |
KiwiTokenizer 64k | 64000 | 7 | 52877 (42693 / 10184) | 6806 (3150 / 3656) | 3173 (3173 / 0) |
klue/roberta-base | 32000 | 5 | 28388 (24638 / 3750) | 2389 (1461 / 928) | 335 (335 / 0) |
beomi/kcbert-base | 30000 | 5 | 28137 (18874 / 9263) | 630 (425 / 205) | 0 (0 / 0) |
HanBert-54kN-torch | 54000 | 5 | 45702 (32462 / 13240) | 3533 (2096 / 1437) | 1821 (914 / 907) |
- 괄호 안의 숫자는 차례로
(Word의 개수 / Subword의 개수)
입니다. - 여기서
Word
는 공백으로 시작하는 토큰을 가리키며,Subword
는 공백 없이 이어지는 토큰을 가리킵니다.
토크나이저 | 제임스웹우주망원경이 발사됐다. |
---|---|
KiwiTokenizer 32k | ['제임스', '##', '웹', '##우주', '##', '망원경', '이/J', '발사', '되/V', '었/E', '다/E', '.'] |
klue/roberta-base | ['제임스', '##웹', '##우주', '##망', '##원', '##경', '##이', '발사', '##됐', '##다', '.'] |
beomi/kcbert-base | ['제', '##임', '##스', '##웹', '##우', '##주', '##망', '##원', '##경이', '발사', '##됐다', '.'] |
HanBert-54kN-torch | ['제임스', '##웹', '##우', '##주', '##망', '##원경', '~~이', '발사', '##됐다', '.'] |
KiwiTokenizer
는 Glue 토큰(##
)을 사용합니다. 특정 문자열을 Subword로 분절하는 것보다 Glue + Word를 사용하는게 낫다고 판단되면 후자를 선택합니다. 그 결과 다른 토크나이저에서는망원경
이 망/원/경, 혹은 망/원경 등으로 분절되지만,KiwiTokenizer
에서는망원경
원형 전체가 보존됩니다.
토크나이저 | 힘들어도 끝까지 버텼다. |
---|---|
KiwiTokenizer 32k | ['힘들/V', '어도/E', '끝', '까지/J', '버티/V', '었/E', '다/E', '.'] |
klue/roberta-base | ['힘들', '##어도', '끝', '##까', '##지', '[UNK]', '.'] |
beomi/kcbert-base | ['힘들어도', '끝까지', '버', '##텼', '##다', '.'] |
HanBert-54kN-torch | ['힘들', '##어', '##도', '끝', '~~까지', '버텼', '##다', '.'] |
- 다른 토크나이저에서는 비교적 출현 빈도가 적은 음절인
텼
이 어휘집합에 포함되어 있지 않아UNK
(알 수 없는 토큰)로 처리되곤 합니다.KiwiTokenizer
에서는 동사/형용사에 대해서는 형태소 분석을 수행하므로 어간이 어미와 결합해 희귀한 형태가 되더라도 절대UNK
로 처리되지 않습니다. - 추가적으로 한글의 경우 초/중성 + 종성을 분리해 표현하는 대체 기능이, 그 외의 문자에 대해서는 UTF8 byte 단위로 분리해 표현하는 대체 기능이 포함되어 있어서, 어떤 문자에 대해서도
UNK
가 나오지 않습니다.
토크나이저 | 달려가는 날쌘돌이 |
---|---|
KiwiTokenizer 32k | ['달려가/V', '는/E', '날쌔/V', 'ᆫ/E', '##돌', '이/J'] |
klue/roberta-base | ['달려가', '##는', '[UNK]'] |
beomi/kcbert-base | ['달려', '##가는', '날', '##쌘', '##돌이'] |
HanBert-54kN-torch | ['달려가', '~~는', '[UNK]', '~~이'] |
- 위와 유사하게
쌘
이라는 음절 때문에UNK
가 생성되는 토크나이저가 있습니다.
토크나이저 | 주거니 받거니 줬거니 받았거니 |
---|---|
KiwiTokenizer 32k | ['주/V', '거니/E', '받/V', '거니/E', '주/V', '었/E', '거니/E', '받/V', '었/E', '거니/E'] |
klue/roberta-base | ['주거', '##니', '받', '##거', '##니', '줬', '##거', '##니', '받', '##았', '##거', '##니'] |
beomi/kcbert-base | ['주거', '##니', '받', '##거니', '줬', '##거니', '받았', '##거니'] |
HanBert-54kN-torch | ['주거', '##니', '받', '##거니', '줬', '##거니', '받', '##았', '##거니'] |
- Kiwi는 동/형용사의 경우 형태소 분석을 사용하기 때문에 동일한 단어가 활용형이 달라져서 다른 토큰으로 처리되는 경우가 적습니다.
토크나이저 | 띄 어 쓰 기 엉 망 진 창 으 로 하 기 |
---|---|
KiwiTokenizer 32k | ['띄/V', '어/E', '쓰/V', '기/E', '엉망', '지/V', 'ᆫ/E', '창', '으로/J', '하/V', '기/E'] |
klue/roberta-base | ['띄', '어', '쓰', '기', '엉', '망', '진', '창', '으', '로', '하', '기'] |
beomi/kcbert-base | ['띄', '어', '쓰', '기', '엉', '망', '진', '창', '으', '로', '하', '기'] |
HanBert-54kN-torch | ['띄', '어', '쓰', '기', '엉', '망', '진', '창', '으', '로', '하', '기'] |
토크나이저 | 띄어쓰기엉망진창으로하기 |
---|---|
KiwiTokenizer 32k | ['띄', '##어', '##쓰기', '##', '엉망', '##진', '##창', '으로/J', '하/V', '기/E'] |
klue/roberta-base | ['띄', '##어', '##쓰기', '##엉', '##망', '##진', '##창', '##으로', '##하기'] |
beomi/kcbert-base | ['띄', '##어', '##쓰기', '##엉', '##망', '##진창', '##으로', '##하기'] |
HanBert-54kN-torch | ['띄', '##어', '##쓰기', '##엉', '##망', '##진', '##창', '##으로', '##하기'] |
- Kiwi가 가지고 있는 띄어쓰기 오류 보정 모델 덕분에 띄어쓰기가 엉망인 텍스트에 대해서도 잘 작동합니다.
토크나이저 | 나랏〮말〯ᄊᆞ미〮 듀ᇰ귁〮에〮달아〮 문ᄍᆞᆼ〮와〮로〮서르ᄉᆞᄆᆞᆺ디〮아니〮ᄒᆞᆯᄊᆡ〮 |
---|---|
KiwiTokenizer 32k | ['나', '##랏', '말', 'ᄊ', '##ᆞ', '##미', 'ᄃ', '##ᅲ', '##ᇰ', '##귀', '##ᆨ', '에/J', '달/V', '어/E', '문', '##ᄍ', '##ᆞ', '##ᆼ', '<0xE3>', '<0x80>', '<0xAE>', '##와', '<0xE3>', '<0x80>', '<0xAE>', '##로', '<0xE3>', '<0x80>', '<0xAE>', '##서', '##르', '##ᄉ', '##ᆞ', '##ᄆ', '##ᆞ', '##ᆺ', '##디', '아니', 'ᄒ', '##ᆞ', '##ᆯ', '##ᄊ', '##ᆡ'] |
klue/roberta-base | ['[UNK]', '[UNK]', '[UNK]'] |
beomi/kcbert-base | ['[UNK]', '[UNK]', '[UNK]'] |
HanBert-54kN-torch | ['나', '##랏', '[UNK]', '말', '[UNK]', '[UNK]', '[UNK]', '[UNK]', '[UNK]', '에', '[UNK]', '달아', '[UNK]', '[UNK]', '[UNK]', '와', '[UNK]', '로', '[UNK]', '[UNK]', '[UNK]', '아니', '[UNK]', '[UNK]', '[UNK]'] |
- Kiwi는 첫가끝 코드를 지원하여 옛한글에 대해서도
UNK
를 생성하지 않습니다. 다만 일부 방점은 어휘집합에 포함되지 않아서 UTF8 byte로 분절됩니다.
KiwiTokenizer
가 딥러닝 언어 모델에서 얼마나 유용한지 확인하기 위해 실제로 RoBERTa 모델을 사전학습해 보았습니다. 사전학습은 바닥부터 진행된 것은 아니며 이미 강력한 것으로 확인된 klue/roberta-base 모델을 재활용하여 어휘 집합만 갈아끼운 뒤 추가 학습을 진행하는 방식으로 수행되었습니다. 사전 학습은 klue/roberta-base와 동일한 어휘 집합 크기를 가진 KiwiTokenizer 32k와 klue보다 어휘 집합이 2배 큰 KiwiTokenizer 64k를 바탕으로 진행되었습니다. 사전 학습 절차에 대해서는 train_bert.py
코드를 참조해주세요. 사전학습이 완료된 모델은 kiwi-farm/roberta-base-32k(huggingface 모델 저장소) 및 kiwi-farm/roberta-base-64k(huggingface 모델 저장소)에서 다운로드 받을 수 있습니다.
사전 학습이 완료된 모델의 성능을 평가하기 위해 다양한 데이터셋으로 미세조정을 실시하였습니다. 노이즈가 많은 환경을 고려하여 미세조정을 평가할 때 평가데이터셋을 크게 3종류로 변형하였습니다.
- 기본: 변형 적용 안 함
- NoSpace: 평가 텍스트에서 공백을 모두 제거
- AllSpace: 평가 텍스트의 각 글자 사이에 공백 모두 삽입
- Random: 20%의 확률로 공백을 삽입하거나 제거함
모델 | NSMC | KLUE YNAT |
---|---|---|
Kiwi RoBERTa Base (32k) | 0.8992 | 0.8501 |
Kiwi RoBERTa Base (64k) | 0.9030 | 0.8510 |
Klue RoBERTa Base | 0.8282 | 0.7088 |
Beomi KcBert Base | 0.8353 | 0.6456 |
HanBert 54kN Base | 0.8363 | 0.7345 |
- 기본, NoSpace, AllSpace, Random 테스트 결과를 평균낸 것
- 변형이 적용 안 된 평가셋에 대해서는 Klue 모델이 제일 좋은 성능을 내었으나, 공백 오류가 들어갈 수록 모델의 성능이 급하락
- Kiwi 모델의 경우 공백 오류에 대해 대체적으로 강건한 성능을 보임
따라해보기
python src/finetuning/sequence_classification.py --model_name_or_path kiwi-farm/roberta-base-32k --output_dir results --dataset nsmc --key document --num_train_epochs 2
python src/finetuning/sequence_classification.py --model_name_or_path kiwi-farm/roberta-base-64k --output_dir results --dataset nsmc --key document --num_train_epochs 2
python src/finetuning/sequence_classification.py --model_name_or_path klue/roberta-base --output_dir results --dataset nsmc --key document --num_train_epochs 2
python src/finetuning/sequence_classification.py --model_name_or_path beomi/kcbert-base --output_dir results --dataset nsmc --key document --num_train_epochs 2
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.90852 | 0.90304 | 0.89204 | 0.8933 |
Train NoSpace | 0.90894 | 0.90692 | 0.89142 | 0.897 |
Train AllSpace | 0.9055 | 0.89748 | 0.90544 | 0.897 |
Train Random | 0.9063 | 0.9054 | 0.9006 | 0.90262 |
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.91278 | 0.90742 | 0.89512 | 0.89676 |
Train NoSpace | 0.91172 | 0.90962 | 0.89124 | 0.89918 |
Train AllSpace | 0.90712 | 0.8979 | 0.90668 | 0.89734 |
Train Random | 0.91164 | 0.90942 | 0.904 | 0.9071 |
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.91336 | 0.88068 | 0.7013 | 0.81746 |
Train NoSpace | 0.91014 | 0.8928 | 0.73992 | 0.84966 |
Train AllSpace | 0.88248 | 0.84712 | 0.89244 | 0.85532 |
Train Random | 0.9039 | 0.88418 | 0.8723 | 0.88838 |
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.90508 | 0.88036 | 0.73222 | 0.82366 |
Train NoSpace | 0.89216 | 0.88262 | 0.76896 | 0.83242 |
Train AllSpace | 0.85986 | 0.84332 | 0.88926 | 0.8565 |
Train Random | 0.89212 | 0.87988 | 0.86962 | 0.88248 |
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.90594 | 0.8733 | 0.74226 | 0.82358 |
Train NoSpace | 0.89868 | 0.8911 | 0.8171 | 0.8501 |
Train AllSpace | 0.87606 | 0.85156 | 0.88936 | 0.85506 |
Train Random | 0.89408 | 0.88142 | 0.87072 | 0.88 |
따라해보기
python src/finetuning/sequence_classification.py --model_name_or_path kiwi-farm/roberta-base-32k --output_dir results --dataset klue --subset ynat --key title --num_train_epochs 3
python src/finetuning/sequence_classification.py --model_name_or_path kiwi-farm/roberta-base-64k --output_dir results --dataset klue --subset ynat --key title --num_train_epochs 3
python src/finetuning/sequence_classification.py --model_name_or_path klue/roberta-base --output_dir results --dataset klue --subset ynat --key title --num_train_epochs 3
python src/finetuning/sequence_classification.py --model_name_or_path beomi/kcbert-base --output_dir results --dataset klue --subset ynat --key title --num_train_epochs 3
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.86570 | 0.85275 | 0.84396 | 0.83814 |
Train NoSpace | 0.86274 | 0.85560 | 0.84396 | 0.84671 |
Train AllSpace | 0.86449 | 0.84396 | 0.85582 | 0.83902 |
Train Random | 0.86603 | 0.85736 | 0.84385 | 0.84737 |
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.86581 | 0.85780 | 0.84034 | 0.84034 |
Train NoSpace | 0.86691 | 0.86449 | 0.84100 | 0.84769 |
Train AllSpace | 0.86724 | 0.85560 | 0.85944 | 0.84802 |
Train Random | 0.86548 | 0.86329 | 0.84616 | 0.85198 |
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.86845 | 0.82431 | 0.43043 | 0.71186 |
Train NoSpace | 0.87152 | 0.85703 | 0.53167 | 0.76128 |
Train AllSpace | 0.80125 | 0.77610 | 0.78401 | 0.75974 |
Train Random | 0.86054 | 0.84495 | 0.68957 | 0.81058 |
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.83770 | 0.79620 | 0.29559 | 0.65279 |
Train NoSpace | 0.82980 | 0.81618 | 0.31997 | 0.67179 |
Train AllSpace | 0.74371 | 0.71845 | 0.77303 | 0.72823 |
Train Random | 0.81805 | 0.80432 | 0.59591 | 0.77709 |
기본 | NoSpace | AllSpace | Random | |
---|---|---|---|---|
Train 기본 | 0.86680 | 0.82573 | 0.51564 | 0.72976 |
Train NoSpace | 0.85297 | 0.82595 | 0.50444 | 0.73448 |
Train AllSpace | 0.77105 | 0.74536 | 0.76798 | 0.73679 |
Train Random | 0.84901 | 0.81969 | 0.65872 | 0.78851 |
- 키위 농장의 huggingface 페이지: https://huggingface.co/kiwi-farm
- KLUE 모델 및 데이터셋: https://github.com/KLUE-benchmark/KLUE
- NSMC 데이터셋: https://github.com/e9t/nsmc
- Beomi KcBert: https://github.com/Beomi/KcBERT
- HanBert: https://github.com/monologg/HanBert-Transformers