본문 바로가기
필기 노트/Huggingface Transformers

[Huggingface] Huggingface Tokenizer

by misconstructed 2021. 7. 9.
728x90

 

Huggingface의 tokenizer 동작 방식에 대한 간단한 정리. 

컴퓨터는 (당연히) 텍스트를 이해하지 못하고, 각 텍스트에 대응하는 숫자들을 이해한다. 그러므로, 우리는 어떤 텍스트를 어떤식으로 분리해서, 분리된 텍스트를 특정한 숫자(id)에 대응시키고, 해당 id를 모델의 입력으로 넣어주는 과정이 필요하다. 우선, 입력으로 들어온 텍스트를 조금 더 작은 단위로 분리하는 과정이 있는데, 이 과정에서 크게 3가지 방식이 사용된다 : word-based, character-based, subword-based.


1. word-based : 단순하게 단어 단위로 분리하고, 각 단어별로 고유의 id 값을 부여한다. 상당히 단순한 방식이고 구현이 간단하지만 몇 가지 문제점이 있다. 첫 번째로, 각 id에 포함되어있는 정보가 상대적으로 많다. 또한, 비슷한 단어임에도 완전히 다른 id를 부여해서 완전히 다른 embedding을 학습하게 된다. 예를 들어, dog라는 단어와 dogs라는 단어가 있을 때, 우리가 봤을 땐 dogs라는 단어는 dog라는 단어의 복수형태라는걸 쉽게 알 수 있지만, word-based toknizing을 하면 두 단어가 서로 다른 id를 부여받게 된다. 마지막으로, 단어 자체가 너무 많아질 수 있기 때문에 전체 vocab set의 크기가 매우 커지고, embedding등 입력을 처리할 때 발생하는 비용이 커진다. 이를 해결하기 위해서 자주 사용하는 단어 몇 개만 뽑아서 사용하는 방식이 있는데 (N-most frequent words), 여기서 제외된 단어들은 unknown token으로 처리하게 된다. 자주 사용하는 단어의 수를 적게 가져가면 unknown token의 수가 많아지게 되는데, 이러면 정보의 손실이 발생할 수 있다.

 

2. character-based : word-based와는 다르게, 모든 텍스트를 character 단위로 자르고, 각 character마다 고유의 id를 부여하게 된다. word-based보다 vocab set의 크기도 많이 줄어들고, 처음 보는 단어들도 character의 조합으로 처리할 수 있기 때문에 unknown word에 대한 처리도 용이하다. 하지만, character 자체가 갖는 의미는 상대적으로 적다는 문제가 있고, 모든 텍스트를 character 단위로 늘린다면, 입력의 길이가 매우 길어져서 모델이 처리할 수 있는 context의 길이가 줄어든다는 단점이 있다.

 

3. subword-based : word-based와 character-based 사이의 방식이다. 자주 사용되는 단어들은 subword로 분리하지 않고, 자주 등장하지 않는 단어들은 의미있는 subword로 분리하는 방법이다. 예를 들어, dogs라는 단어는 자주 사용하는 dog와 ##s 로 분리할 수 있다. 영어에서 가장 좋은 성능을 보이고 있느느 tokenizer 이다.

##prefix : BERT가 사용하는 subword tokenizer인 wordpiece tokenizer에서 사용하는 방식인데, 특정 token 앞에 ##가 붙는다면 해당 token은 독립적으로 사용할 수 있는 token이 아닌, 다른 단어를 구성하는 일부라는 의미이다.

Tokenizer Pipeline

AutoTokenizer에서 from_pretrained() 함수를 통해 우리가 사용하고자 하는 PLM의 tokenizer를 그대로 불러와서 사용할 수 있다. tokenizer 내부에서 어떤 연산이 수행되는지 간단하게 설명하면 다음과 같다.

1. raw text가 입력으로 주어졌을 때, tokenize() 함수를 통해 텍스트를 token 단위로 분리한다. Token을 구성하는 방식은 위에서 설명한 word-based, characted-based, subword-based를 기반으로한 다양한 방식들이 사용될 수 있다.

2. token들을 convert_tokens_to_ids() 함수를 통해서 각 token들에 대응되는 id로 변환된다. 

3. prepare_for_model() 함수를 통해서 모델의 입력으로 넣기 위해 필요한 special token들을 추가한다. BERT의 경우 [CLS], [SEP] 등의 token id를 문장의 앞/뒤에 추가하고, RoBERTa의 경우 <s>, </s> 등의 token id를 추가한다. 이렇게 생성된 입력은 바로 모델의 입력으로 넣어준다.

token id를 다시 텍스트로 변환하기 위해서는 decode() 함수를 사용하면 된다. 

tokenizer를 통해서 input_ids도 생성되지만, 동시에 token_type_ids, attention_mask 도 함께 생성된다.


여기서 생각해볼 수 있는 문제 : 두 문장의 길이가 서로 다른 경우, 한 batch로 입력을 넣어주려면 어떻게 해야할까? (두 문장의 길이가 다르면 input_ids의 길이도 달라지게 되는데!)

 

우선, 모델이 처리할 수 있는 입력의 길이보다 긴 경우는 모델이 처리할 수 있는 길이만큼 잘라서 사용한다. 이런 경우가 아니라면, 짧은 문장을 긴 문장에 맞게 길이를 늘리게 되는데, 이 때 padding token을 사용한다. padding token은 tokenizer.pad_token_id 를 통해서 id 값을 확인할 수 있다. 이렇게 해서 짧은 문장을 긴 문장의 길이와 같게 만들어서 batch를 구축한다.

하지만 여기서 또 주의해야하는 점! Transformer 계열의 모델들은 self-attention을 수행하기 때문에 위와 같이 만든 입력을 그냥 넣어주면 실제 입력과 padding token 사이의 self-attention 연산을 수행하기 때문에 padding token들이 결과에 영향을 미치게 된다. 이런 문제를 방지하기 위해서 attention_mask 를 사용한다. attention_mask는 1과 0으로 구성되어있는데, 1인 경우는 정상적으로 연산을 수행하고, 0인 경우는 모델이 무시하게 된다. (지금과 같은 경우, padding token이 위치한 index에 attention_mask 값을 모두 0으로 지정하면 padding token이 결과에 영향을 미치지 않게 된다.) 이 과정을 한 번에 처리하기 위해서는 tokenizer(sentence, padding=True) 를 통해서 수행할 수 있다. 


Huggingface Transformers 모델에서 from_pretrained() 함수를 호출할 때 이루어지는 연산
1. from_pretrained() 함수의 파라미터로 넘겨준 checkpoint 또는 local folder에서 데이터를 읽어와서 config file과 model weight를 로드한다.
2. config file 을 통해서 해당 모델에 적합한 config class를 생성한다. (instantiate)
3. config class를 기반으로 해당 모델의 model class를 생성한다. (random initialized)
4. 1번에서 로드한 model weight를 가져와서 model weight를 초기화한다.
728x90

댓글