- NLP 관련 다양한 패키지를 제공하고 있으며, 특히 언어 모델 (language models) 을 학습하기 위하여 세 가지 패키지가 유용
package | note |
---|---|
transformers | Transformer 기반 (masked) language models 알고리즘, 기학습된 모델을 제공 |
tokenizers | transformers 에서 사용할 수 있는 토크나이저들을 학습/사용할 수 있는 기능 제공. transformers 와 분리된 패키지로 제공 |
nlp | 데이터셋 및 평가 척도 (evaluation metrics) 을 제공 |
토크나이저 | 토큰 단위 | 미등록단어 가정 | vocab size |
---|---|---|---|
사전 기반 | 알려진 단어/형태소 | 사전에 등록된 단어/형태소의 결합이라 가정, 필요시 형태소 분석 (형태 변형 가능) | unlimited |
subword | 알려진 subwords (substring) | subwords 의 조합이라 가정, str split 만을 이용 | limited |
(코로나 뉴스 70,963 문장 + BertTokenizer)
sent = '신종 코로나바이러스 감염증(코로나19) 사태가 심각합니다'
bert_tokenizer.tokenize(sent)
# '신종 코로나바이러스 감염증 ( 코로나19 ) 사태 ##가 심 ##각 ##합니다'
komoran.morphs(sent)
# 신종 코로나바이러스 감염증 ( 코로나 19 ) 사태 가 심각 하 ㅂ니다
input eojeol | Mecab | Komoran | Bert + Covid news |
---|---|---|---|
코로나가 | 코 로 나가 | 코로나 가 | 코로나 ##가 |
코로나는 | 코로나 는 | 코로나 는 | 코로나 ##는 |
코로나를 | 코로나 를 | 코로나 를 | 코로나 ##를 |
코로나의 | 코로 나의 | 코로나 의 | 코로나 ##의 |
- 사전 기반 토크나이저는 미등록단어를 맥락에 따라 잘못 분해할 가능성이 존재
- subword 토크나이저는 학습데이터에 자주 등장하는 substring 이라면 단어로 보존될 가능성이 높음
input eojeol | Mecab | Komoran | Bert + Covid news |
---|---|---|---|
심각합니다 | 심각 합니다 | 심각 하 ᄇ니다 | 심 ##각 ##합니다 |
심각했다 | 심각 했 다 | 심각 하 었 다 | 심 ##각 ##했다 |
심각하다고 | 심각 하 다고 | 심각 하 다고 | 심 ##각 ##하다 ##고 |
심각하며 | 심각 하 며 | 심각 하 며 | 심 ##각 ##하며 |
- 반대로 학습데이터에 자주 등장하지 않은 단어
심각
은 잘 인식되지 않음- 반드시 단어로 보존하고 싶은 단어가 있다면 따로 등록이 필요
- 활용된 용언
하다
가 다양한 형태로 표현합니다
,했다
,하다 + 고
,하며
- 반드시 원형으로 처리해야 하는 것은 아니나, 이를 원할 경우 선택할 수 없음
0.9.0.dev0
버그가 있어서0.8.1
기준으로 진- BaseTokenizer 를 상속하는 네 가지 토크나이저를 제공
- Rust 로 구현된 코드를 Python 에서 이용할 수 있도록 도와주는 기능을 제공
- Python 에서 기능을 추가하기 어려움
- base 방식에 따라 WordpieceTrainer, BpeTrainer 를 이용
tokenizer (class name) | unit | base | normalizer | boundary symbol | output |
---|---|---|---|---|---|
Byte-level BPE (ByteLevelBPETokenizer) | byte | BPE | [Unicode, Lowercase] | 어절 앞 Ġ 부착 |
vocab.json , merges.txt |
Character-level BPE (CharBPETokenizer)^1 | char | BPE | [Unicode, BertNormalizer, Lowercase] | 어절 뒤 </w> 부착 |
vocab.json , merges.txt |
Sentencepiece BPE (SentencePieceBPETokenizer)^2 | char | BPE | 어절 앞 ▁ 부착 |
vocab.json , merges.txt |
|
Bert wordpiece (BertWordPieceTokenizer) | char | WordPiece | BertNormalizer | 어절 중간 subword 앞에 ## 부착 |
vocab.txt |
- ^1: split_on_whitespace_only 기능 제공. 이를 이용하면 pre-tokenized 된 텍스트를 이용하여 학습하기에 용이
- ^2: add_prefix_space 기능 제공
- 네 가지 토크나이저가 모두 상속, 대표적인 기능
- get_vocab()
- add_tokens()
- add_special_tokens()
- normalize()
- encode() / encode_batch()
- decode() / decode_batch()
- save() vs save_model()
- tokenize() 기능이 없고, encode() 의 결과를 이용해야 함
- 네 가지 토크나이저들 모두 위의 기능을 상속하니, 각 토크나이저의 특징을 살펴보며 위의 기능들을 알아보자
(very_small_corpus.txt
)
ABCDE ABC AC ABD
DE AB ABC AF
(training)
bert_wordpiece_tokenizer = BertWordPieceTokenizer()
bert_wordpiece_tokenizer.train(
files = [small_corpus],
vocab_size = 10,
min_frequency = 1,
limit_alphabet = 1000,
initial_alphabet = [],
special_tokens = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"],
show_progress = True,
wordpieces_prefix = "##",
)
(vocab check)
vocab = bert_wordpiece_tokenizer.get_vocab()
sorted(vocab, key=lambda x: vocab[x])
['[PAD]', '[UNK]', '[CLS]', '[SEP]', '[MASK]', 'a', 'b', 'c', 'd', 'e',
'f', '##b', '##c', '##d', '##e', '##f']
(tokenization)
encoding = bert_wordpiece_tokenizer.encode('ABCDE')
print(encoding.tokens) # ['a', '##b', '##c', '##d', '##e']
print(encoding.ids) # [5, 13, 12, 14, 11]
(vocab size 를 늘리고 initial alphabet 을 추가하면)
bert_wordpiece_tokenizer.train(
files = [small_corpus],
vocab_size = 20,
min_frequency = 1,
initial_alphabet = ['g'],
)
vocab = bert_wordpiece_tokenizer.get_vocab()
sorted(vocab, key=lambda x: vocab[x])
# ['[PAD]', '[UNK]', '[CLS]', '[SEP]', '[MASK]', 'a', 'b', 'c', 'd', 'e',
# 'f', 'g', '##b', '##c', '##e', '##d', '##f', 'ab', 'abc', 'ac']
(encode_batch 함수)
encodings = bert_wordpiece_tokenizer.encode_batch(['ABCDE', 'abcd'])
encodings[0].tokens # ['abc', '##d', '##e']
(normalize 함수)
# 0.9.0dev 에서 사라
bert_wordpiece_tokenizer.normalize('ABCDE') # 'abcde'
(save / load)
bert_wordpiece_tokenizer.save_model(
directory = './',
name = 'very_small_bertwordpiece'
)
# './very_small_bertwordpiece-vocab.txt' 에 vocab 생성
bert_wordpiece_tokenizer = BertWordPieceTokenizer(
vocab_file = './very_small_bertwordpiece-vocab.txt'
)
bert_wordpiece_tokenizer.encode('ABCDE').tokens
# ['[CLS]', 'abc', '##d', '##e', '[SEP]']
(without special tokens)
bert_wordpiece_tokenizer.encode(
'ABCDE',
add_special_tokens=False
).tokens
# ['abc', '##d', '##e']
(two sentences pair)
bert_wordpiece_tokenizer.encode(
sequence = 'abcde',
pair = 'abcd'
).tokens
# ['[CLS]', 'abc', '##d', '##e', '[SEP]', 'abc', '##d', '[SEP]']
(add_tokens 함수)
bert_wordpiece_tokenizer.add_tokens(['lovit'])
vocab = bert_wordpiece_tokenizer.get_vocab()
sorted(vocab, key=lambda x: vocab[x])
# ['[PAD]', '[UNK]', '[CLS]', '[SEP]', '[MASK]', 'a', 'b', 'c', 'd', 'e',
# 'f', 'g', '##b', '##c', '##e', '##d', '##f', 'ab', 'abc', 'ac',
# 'lovit']
# 그러나 추가한 단어가 함께 저장되지는 않음. `vocab.txt` 에 직접 추가해도
bert_wordpiece_tokenizer.save('./', './very_small_bertwordpiece')
(train)
sentencepiece_tokenizer = SentencePieceBPETokenizer(
add_prefix_space = True,
replacement = '▁'
)
sentencepiece_tokenizer.train(
files = [small_corpus],
vocab_size = 20,
min_frequency = 1,
special_tokens = ['<unk>'],
)
vocab = sentencepiece_tokenizer.get_vocab()
sorted(vocab, key=lambda x: vocab[x])
# ['<unk>', 'A', 'B', 'C', 'D', 'E', 'F', '▁', '▁A', '▁AB',
# '▁ABC', 'DE', '▁DE', '▁AC', '▁AF', '▁ABD', '▁ABCDE']
0.8.1
->0.9.0dev
에서 '\n' 이 글자로 들어옴
(without add_prefix_space)
sentencepiece_tokenizer = SentencePieceBPETokenizer(
add_prefix_space = False
)
sentencepiece_tokenizer.train(
files = [small_corpus],
vocab_size = 20,
min_frequency = 1,
special_tokens = ['<unk>'],
)
vocab = sentencepiece_tokenizer.get_vocab()
sorted(vocab, key=lambda x: vocab[x])
# ['<unk>', 'A', 'B', 'C', 'D', 'E', 'F', '▁', '▁A', '▁AB',
# 'DE', '▁ABC', 'AB', 'CDE', '▁AC', '▁AF', '▁ABD', 'ABCDE']
add_prefix_space=True
는 문장의 첫글자가 공백이 아닐 경우 공백 추- 위에서는
▁ABCDE
가, 아래에서는ABCDE
가 unit 으로 학습됨.
(save / load)
sentencepiece_tokenizer.save_model('./', 'very_small_sentencepiece')
# ['./very_small_sentencepiece-vocab.json',
# './very_small_sentencepiece-merges.txt']
sentencepiece_tokenizer = SentencePieceBPETokenizer(
vocab_file = './very_small_sentencepiece-vocab.json',
merges_file = './very_small_sentencepiece-merges.txt'
)
sentencepiece_tokenizer.encode('ABCDE').tokens
# ['▁ABC', 'DE']
- BPE 계열은
merges.txt
,vocab.json
두 개의 파일 0.9.0dev
에merges.txt
파일 저장 과정 중 버그가 있음
(use BertPreTokenizer)
charbpe_tokenizer = CharBPETokenizer(suffix='</w>')
charbpe_tokenizer.train(
files = [small_corpus],
vocab_size = 15,
min_frequency = 1
)
charbpe_tokenizer.encode('ABCDE.ABC').tokens
# ['AB', 'C', 'DE</w>', 'ABC</w>']
- BertPreTokenizer 는 space, punctuation 에서 분리
(use WhitespacePreTokenizer)
charbpe_tokenizer = CharBPETokenizer(
suffix='</w>',
split_on_whitespace_only = True
)
charbpe_tokenizer.encode('ABCDE.ABC').tokens
# ['AB', 'C', 'D', 'E', 'ABC</w>']
- 공백 기준으로만 어절을 분리
(미등록단어 제거하여 return)
charbpe_tokenizer.encode('ABCDEFGH').tokens
# ['AB', 'C', 'D', 'E', 'F']
# OpenAI GPT2 tokenizer
bytebpe_tokenizer = ByteLevelBPETokenizer(
add_prefix_space = False,
lowercase = False,
)
bytebpe_tokenizer.train(
files = [small_corpus],
vocab_size = 1000,
min_frequency = 1
)
vocab = bytebpe_tokenizer.get_vocab()
sorted(vocab, key=lambda x: vocab[x])
['!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4',
'5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',
']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '¡', '¢', '£', '¤', '¥', '¦',
'§', '¨', '©', 'ª', '«', '¬', '®', '¯', '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»',
'¼', '½', '¾', '¿', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï',
'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã',
'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷',
'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', 'Ā', 'ā', 'Ă', 'ă', 'Ą', 'ą', 'Ć', 'ć', 'Ĉ', 'ĉ', 'Ċ', 'ċ',
'Č', 'č', 'Ď', 'ď', 'Đ', 'đ', 'Ē', 'ē', 'Ĕ', 'ĕ', 'Ė', 'ė', 'Ę', 'ę', 'Ě', 'ě', 'Ĝ', 'ĝ', 'Ğ', 'ğ',
'Ġ', 'ġ', 'Ģ', 'ģ', 'Ĥ', 'ĥ', 'Ħ', 'ħ', 'Ĩ', 'ĩ', 'Ī', 'ī', 'Ĭ', 'ĭ', 'Į', 'į', 'İ', 'ı', 'IJ', 'ij',
'Ĵ', 'ĵ', 'Ķ', 'ķ', 'ĸ', 'Ĺ', 'ĺ', 'Ļ', 'ļ', 'Ľ', 'ľ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'Ń',
'ĠA', 'ĠAB', 'DE', 'ĠABC', 'AB', 'CDE', 'ĠAC', 'ĠAF', 'ĠABD', 'ABCDE']
- 33 - 323 까지 글자를 기본 알파벳으로 가지고 시작
bytebpe_tokenizer.encode('ABCDE ABC').tokens
# ['ABCDE', 'ĠABC']
bytebpe_tokenizer.encode(' ABCDE ABC').tokens
# ['ĠABC', 'DE', 'ĠABC']
BertTokenizer 를 확인해보자.
from transformers import BertTokenizer, GPT2Tokenizer
transformers_bert_tokenizer = BertTokenizer(
vocab_file = './tokenizers/BertWordPieceTokenizer/covid-vocab.txt'
)
sent_ko = '신종 코로나바이러스 감염증(코로나19) 사태가 심각합니다'
print(f'tokenizers : {bert_wordpiece_tokenizer.encode(sent_ko).tokens}')
print(f'transformers: {transformers_bert_tokenizer.tokenize(sent_ko)}')
tokenizers : ['신종', '코로나바이러스', '감염증', '(', '코로나19', ')', '사태', '##가', '심', '##각', '##합니다']
transformers: ['신종', '코로나바이러스', '감염증', '(', '코로나19', ')', '사태', '##가', '심', '##각', '##합니다']
- 토크나이징의 결과가 동일하지만, 초/중/종성이 분해되어 있다.
- 이는 unicode normalization 때문에 발생한 현상.
from unicodedata import normalize
print(normalize('NFKD', '가감')) # 가감 ; 출력 시 글자를 재조합해서 보여줌
print(len(normalize('NFKD', '가감'))) # 5
print(normalize('NFKC', normalize('NFKD', '가감'))) # 가감
print(len(normalize('NFKC', normalize('NFKD', '가감')))) # 2
def compose(tokens):
return [normalize('NFKC', token) for token in tokens]
print(f'tokenizers : {compose(bert_wordpiece_tokenizer.encode(sent_ko).tokens)}')
print(f'transformers: {compose(transformers_bert_tokenizer.tokenize(sent_ko))}')
tokenizers : ['신종', '코로나바이러스', '감염증', '(', '코로나19', ')', '사태', '##가', '심', '##각', '##합니다']
transformers: ['신종', '코로나바이러스', '감염증', '(', '코로나19', ')', '사태', '##가', '심', '##각', '##합니다']
GPT2Tokenizer 를 확인해보자.
transformers_gpt2_tokenizer = GPT2Tokenizer(
vocab_file = './tokenizers/ByteLevelBPETokenizer/covid-vocab.json',
merges_file = './tokenizers/ByteLevelBPETokenizer/covid-merges.txt'
)
print(f'tokenizers : {byte_level_bpe_tokenizer.encode(sent_ko).tokens}')
print(f'transformers: {transformers_gpt2_tokenizer.tokenize(sent_ko)}')
tokenizers : ['ìĭłì¢ħ', 'Ġì½Ķë¡ľëĤĺë°ĶìĿ´ë٬ìĬ¤', 'Ġê°IJìĹ¼ì¦Ŀ', '(', 'ì½Ķë¡ľëĤĺ', '19', ')', 'ĠìĤ¬íĥľ', 'ê°Ģ', 'Ġìĭ¬', 'ê°ģ', 'íķ©ëĭĪëĭ¤']
transformers: ['ìĭłì¢ħ', 'Ġì½Ķë¡ľëĤĺë°ĶìĿ´ë٬ìĬ¤', 'Ġê°IJìĹ¼ì¦Ŀ', '(', 'ì½Ķë¡ľëĤĺ', '19', ')', 'ĠìĤ¬íĥľ', 'ê°Ģ', 'Ġìĭ¬', 'ê°ģ', 'íķ©ëĭĪëĭ¤']
compose(transformers_bert_tokenizer.tokenize('lovit 이란 이름은 인식을 안합니다'))
# ['l', '##o', '##v', '##i', '##t', '이라', '##ᆫ', '이', '##름', '##은', '인', '##식을', '안', '##합니다']
- 데이터에 충분히 등장하지 않았더라도 특정 단어를 보존하고 싶다.
- 기형태소 분석기와 혼합하여 BertTokenizer 를 이용하고 싶다.
- Normalizer 와 PreTokenizer 는 Rust 코드를 Python 에서 이용할 수 있도록 도와주는 클래스로 상속이 되지 않음
from tokenizers.normalizers import Normalizer
class KomoranNormalizer(Normalizer):
def __init__(self):
print('success')
"type 'tokenizers.normalizers.Normalizer' is not an acceptable base type"
from tokenizers.pre_tokenizers import PreTokenizer
class KomoranPreTokenizer(PreTokenizer):
def __init__(self):
print('success')
"type 'tokenizers.pre_tokenizers.PreTokenizer' is not an acceptable base type"
from konlpy.tag import Komoran, Mecab, Okt
class KoNLPyPreTokenizer:
def __init__(self, base_tokenizer):
self.base_tokenizer = base_tokenizer
def __call__(self, sentence):
return self.pre_tokenize(sentence)
def pre_tokenize(self, sentence):
return ' '.join(self.base_tokenizer.morphs(sentence))
konlpy_pretok = KoNLPyPreTokenizer(Komoran())
print(konlpy_pretok(sent_ko))
# 신종 코로나바이러스 감염증 ( 코로나 19 ) 사태 가 심각 하 ㅂ니다
def prepare_pretokenized_corpus(raw_path, pretokenized_path, pretok):
with open(raw_path, encoding='utf-8') as f:
lines = [line.strip() for line in f]
with open(pretokenized_path, 'w', encoding='utf-8') as f:
for line in lines:
f.write(f'{pretok(line)}\n')
prepare_pretokenized_corpus(
'../data/2020-07-29_covid_news_sents.txt',
'../data/2020-07-29_covid_news_sents.komoran.txt',
KoNLPyPreTokenizer(Komoran()))
bert_wordpiece_tokenizer = BertWordPieceTokenizer()
bert_wordpiece_tokenizer.train(
files = ['../data/2020-07-29_covid_news_sents.komoran.txt'],
vocab_size = 3000)
bert_wordpiece_tokenizer.save_model(
directory='./tokenizers/KomoranBertWordPieceTokenizer/',
name='covid')
for vocab, idx in bert_wordpiece_tokenizer.get_vocab().items():
if normalize('NFKD', '니다') in vocab:
print(vocab, idx)
ㅂ니다 1106
습니다 977
##니다 909
- 올해 4월 examples 에 custom_pre_tokenizer 가 업로드 됨. 링크
- 하지만 정상적으로 작동하지 않음을 확인.
- 계속 버전업 중이니 이 기능을 제공해줄 것으로 기대 중
from tokenizers import pre_tokenizers
class GoodCustom:
def pre_tokenize(self, sentence):
return sentence.split(" ")
def decode(self, tokens):
return ", ".join(tokens)
good_custom = GoodCustom()
good_pretok = pre_tokenizers.PreTokenizer.custom(good_custom)
- 학습이 끝났다면 tokenizers.BertTokenizer 의 encode, encode_batch 함수만 수정하면 된다.
class KoNLPyBertWordPieceTokenizer(BertWordPieceTokenizer):
def __init__(
self,
konlpy_pretok,
vocab_file: Optional[str] = None,
...):
super().__init__(...)
self.konlpy_pretok = konlpy_pretok
def encode(self, sequence, pair=None, ...):
if sequence is None:
raise ValueError("encode: `sequence` can't be `None`")
sequence = self.konlpy_pretok(sequence) # <-- 추가
return self._tokenizer.encode(sequence, pair, is_pretokenized, add_special_tokens)
def encode_batch(self, inputs, ...):
if inputs is None:
raise ValueError("encode_batch: `inputs` can't be `None`")
# <-- 추가
input_iterator = tqdm(inputs, desc='konlpy pretok', total=len(inputs))
konlpy_pretok_inputs = [self.konlpy_pretok(sequence) for sequence in input_iterator]
# -->
return self._tokenizer.encode_batch(konlpy_pretok_inputs, is_pretokenized, add_special_tokens)
konlpy_bert_wordpiece_tokenizer = KoNLPyBertWordPieceTokenizer(
konlpy_pretok,
vocab_file = './tokenizers/KomoranBertWordPieceTokenizer/covid-vocab.txt')
class KoNLPyBertTokenizer(BertTokenizer):
def __init__(
self,
konlpy_pretok,
vocab_file,
...):
super().__init__(...)
self.konlpy_pretok = konlpy_pretok
def _tokenize(self, text):
text = self.konlpy_pretok(text) # <-- 추가
split_tokens = []
if self.do_basic_tokenize:
for token in self.basic_tokenizer.tokenize(text, never_split=self.all_special_tokens):
# If the token is part of the never_split set
if token in self.basic_tokenizer.never_split:
split_tokens.append(token)
else:
split_tokens += self.wordpiece_tokenizer.tokenize(token)
else:
split_tokens = self.wordpiece_tokenizer.tokenize(text)
return split_tokens
konlpy_bert_tokenizer = KoNLPyBertTokenizer(
konlpy_pretok, './tokenizers/KomoranBertWordPieceTokenizer/covid-vocab.txt')
compose(bert_wordpiece_tokenizer.encode(sent_ko).tokens)
# ['신종', '코로나바이러스', '감염증', '(', '코로나', '##1', '##9', ')', '사태', '##가', '심각', '##합', '##니다']
compose(konlpy_bert_wordpiece_tokenizer.encode(sent_ko, add_special_tokens=False).tokens)
# ['신종', '코로나바이러스', '감염증', '(', '코로나', '19', ')', '사태', '가', '심각', '하', 'ᄇ니다']
compose(konlpy_bert_tokenizer.tokenize(sent_ko))
# ['신종', '코로나바이러스', '감염증', '(', '코로나', '19', ')', '사태', '가', '심각', '하', 'ᄇ니다']
질문 있습니다. BertWordPieceTokenizer.encode()의 반환되는 값의 attributes 중에 overflowing 이라는 것이 있던데., 이것은 무엇을 의미하는 건가요? 보통 값은 비워있는 리스트가 반환되는데, 용도가 무엇인지 궁금합니다.