ratsgo / embedding

한국어 임베딩 (Sentence Embeddings Using Korean Corpora)

Home Page:https://ratsgo.github.io/embedding

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

sentencepiece vs FullTokenizer

hccho2 opened this issue · comments

다음 질문은 꼭 책에 국한된 질문은 아닐 수 있습니다.
마땅히 질문할 곳이 없어, 이곳에 남깁니다. 어떤 의견이든 감사히 받겠습니다.

==============================================================
[질문]

책에서는 sentencepiece의 SentencePieceTrainer.Train()을 통해 sentpiece.model, sentpiece.vocab 2개의 파일을 생성 후,

sentpiece.vocab 파일로 부터 '_'를 '##'으로 변환하여 vocab.txt를 만들고 있습니다.

tokenizing을 2가지 방식으로 할 수 있을 것 같습니다.

  1. sentnecepiece의 EncodeAsPieces() <------ 앞에서 만든 sentpiece.model 이용

  2. FullTokenizer <---- 앞에서 만든 vocab.txt를 이용

===============
제가 주목한 token이 `자동차' 입니다.
vocab.txt, 에는 '자동차', '##자동차' 가 모두 포함되어 있습니다.

sentpiece.vocab 에도 '자동차', `▁자동차' 가 모두 포함되어 있습니다.

=================
이제 s = '학교 앞을 지나는 자동차` 를 tokenizing해보면 다음과 같습니다.

sentnecepiece 결과: ['▁학교', '▁앞', '을', '▁지나는', '▁자동차']

FullTokenizer 결과: ['학교', '앞', '##을', '지나', '##는', '자동차']

'자동차' 라는 token을 각각 '▁자동차', '자동차' 로 처리했습니다.

2가지 방법이 일관성이 있기 위해서는
모두 `자동차'가 되든지, 아니면
'▁자동차', '##자동차'가 되어야 할 것 같은데 다른 결과가 나왔습니다.

===================
책 page 105에 보면
'학교에서 밥을 먹었다' ---> ▁학교, 에서, ▁밥, 을, ▁먹었, 다
로 설명하고 있습니다. '학교'가 아닌 '▁학교'로.

책 page 106에 보면
'집에 좀 가자' ---> '집에' '##좀', '가자'
로 설명하고 있습니다. 105페이지 내용과 일관성이 있으려면 '집에'가 아니고, '##집에'가 되어야 하지 않나요?

===================
정리하면,
sentencepiece의 tokenizing은 subword로 우선 처리하고,
FullTokenizer는 단독 word를 우선 처리하고 있습니다.

2가지의 처리 방식이 분명히 다른 것 같은데, 특별한 이유가 있는 것인가요?
처리 방식 또한 모델의 일부라 보고, 다르게 했다고 하면, 할말은 없겠지만요.

제 생각에는 make_bert_vocab()가 수정되어야 할 것 같습니다.

원본:

def make_bert_vocab(input_fname, output_fname):
    train = '--input=' + input_fname + ' --model_prefix=sentpiece --vocab_size=32000 --model_type=bpe --character_coverage=0.9995'
    print('cmd: ', train)
    spm.SentencePieceTrainer.Train(train) 
    with open('sentpiece.vocab', 'r', encoding='utf-8') as f1, \
            open(output_fname, 'w', encoding='utf-8') as f2:
        f2.writelines("[PAD]\n[UNK]\n[CLS]\n[SEP]\n[MASK]\n")
        for line in f1:
            word = line.replace('\n', '').split('\t')[0].replace('▁', '##')   #  '▁' != '_'(underscore)
            if not word or word in ["##", "<unk>", "<s>", "</s>"]: continue
            f2.writelines(word + "\n")

수정:

def make_bert_vocab(input_fname, output_fname):
    train = '--input=' + input_fname + ' --model_prefix=sentpiece --vocab_size=32000 --model_type=bpe --character_coverage=0.9995'
    print('cmd: ', train)
    spm.SentencePieceTrainer.Train(train) 
    with open('sentpiece.vocab', 'r', encoding='utf-8') as f1, \
            open(output_fname, 'w', encoding='utf-8') as f2:
        f2.writelines("[PAD]\n[UNK]\n[CLS]\n[SEP]\n[MASK]\n")
        for line in f1:
            word = line.replace('\n', '').split('\t')[0]
            
            if not word or word in ["▁", "<unk>", "<s>", "</s>"]:
                continue            
            if word[0] == '▁':
                word = word.replace('▁', '')
            else:
                word = '##' + word
            f2.writelines(word + "\n")

안녕하세요, @hccho2 님! 매번 날카로운 질문 해주셔서 감사드립니다.

sentencepiece의 와 full tokenizer의 ##은 둘 모두 어절(string을 공백으로 구분했을 경우 하나의 단위) 경계를 나누기 위해 도입된 구분자이지만, 실제로는 정반대 뜻으로 사용되고 있습니다. 다시 말해 sentencepiece 토크나이저에서 가 나타났을 경우 해당 토큰이 어절의 시작을, 가 나타나지 않았을 경우 해당 토큰이 어절의 시작이 아님을 의미하는 것으로 알고요. 반면 FullTokenizer에서는 ##가 나타났을 경우 해당 토큰이 어절의 시작이 아님을, ##가 나타나지 않았을 경우 해당 토큰이 어절의 시작임을 나타내고 있습니다(이와 관련해 BERT 공식 리포의 FullTokenizer 분석 예시 참조 : 링크).

@hccho2 님께서 들어주신 예시를 제 나름대로 분석하면 다음과 같습니다. sentencepiece와 FullTokenizer가 일관된 분석을 하고 있음을 확인할 수 있습니다. 토큰화를 할 때 이 같이 어절 경계 구분자를 남겨놓으면 간단한 룰로 토큰 시퀀스를 원래 문장으로 복원 가능합니다(mecab 등 특정 언어에 의존적인 분석기는 토큰 시퀀스/List[str]를 원래 문장/str으로 복원하기가 상대적으로 까다롭습니다).

  • 학교 앞을 지나는 자동차 > ▁학교, ▁앞, 을, ▁지나는, ▁자동차 : 가 나타난 ▁학교학교라는 어절의 시작이라는 의미, 가 나타난 ▁자동차자동차라는 어절의 시작이라는 의미
  • 학교 앞을 지나는 자동차 > 학교, 앞, ##을, 지나, ##는, 자동차 : ##가 나타나지 않은 학교학교라는 어절의 시작이라는 의미, ##가 나타나지 않은 자동차자동차라는 어절의 시작이라는 의미

아울러 책의 예시를 좀 더 풀어 설명하면 다음과 같습니다.

  • 학교에서 밥을 벅었다 > ▁학교, 에서, ▁밥, 을, ▁먹었, 다 : 가 나타난 ▁학교학교에서라는 어절의 시작이라는 의미
  • 집에좀 가자 > 집에, ##좀, 가자 : ##가 나타나지 않은 집에집에좀이라는 어절의 시작이라는 의미, ##이 나타난 ##좀집에좀이라는 어절의 시작이 아니라는 의미

@hccho2 님께서 의문점을 가지신 근본적인 원인은 make_bert_vocab 함수 때문인 것 같습니다. 이것은 전적으로 제 착오로 코드 수정과 관련한 @hccho2 님 의견이 맞다는 판단입니다. 다시 말해 sentencepiece의 와 full tokenizer의 ##이 정반대 뜻으로 활용되고 있으므로 sentencepiece 도움을 받아 BPE 보캡을 만든 후 이를 BERT 저자들이 작성한 FullTokenizer에 적용하려면 sentencepiece의 ##로 대치(replace)하는 것은 적절하지 않습니다. @hccho2 님께서 작성해주신 코드처럼 가 나타는 sentencepiece 보캡의 단어에서 를 없애고, 가 나타나지 않는 단어 앞에 ##를 붙여주는 것이 타당하다고 생각합니다.

이 이슈에 제시됐던 내용은 @hccho2 님과 조금 더 토론을 진행한 후에 정오표와 전자책, 코드, 그리고 4쇄 이후 종이책에 반영하도록 하겠습니다. 늘 신세만 지는 것 같습니다. 날카로운 의견 진심으로 감사드립니다.

검토해 주셔서 감사합니다. 저도 혼란스러움이 해소되었습니다.