Byte Pair Encoding

Updated:

Reference

BPE 알고리즘에 대한 설명은 링크한 곳에 잘 설명되어 있습니다. 여기서는 참고한 곳의 내용을 바탕으로 직접 구현했습니다.

https://ratsgo.github.io/nlpbook/docs/preprocess/bpe/

Get Vocabulary

토크나이징을 위해 문서내에 등장한 단어의 등장횟수가 기록된 dictionary를 사용하여 단어 집합인 vocabulary를 만들어야 합니다.

dictionary = {
    'hug':10,
    'pug':5,
    'pun':12,
    'bun':4,
    'hugs':5
}

vocabulary = list(set(sum([list(word) for word in dictionary.keys()],[])))
# ['p', 'b', 'n', 'g', 'h', 'u', 's']

위 코드는 dictionary의 단어들을 구성하는 글자들만을 추출하여 vocabulary에 저장한 코드입니다. 알고리즘 내에서 vocabulary를 사용하고 다시 업데이트를 반복할 것입니다.

모든 단어에 대한 bigram 쌍을 pairs라는 딕셔너리에 저장하면서 횟수를 더해줍니다.

import collections

pairs = collections.defaultdict(int)
for word, cnt in dictionary.items():
    i, word_splited = 0,[] # word_splited는 vocabulary에 있는 단어들로 word를 분리하고 저장합니다
    while i < len(word):
        if i+1<len(word) and word[i]+word[i+1] in vocabulary:
            word_splited.append(word[i]+word[i+1])
            i+=2
        else:
            word_splited.append(word[i])
            i+=1
for word1,word2 in zip(word_splited[:-1],word_splited[1:]):
  pairs[word1+word2]+=cnt
 
print(pairs.items())

# dict_items([('hu', 15), ('ug', 20), ('pu', 17), ('un', 16), ('gs', 5), ('bu', 4)])

pairs에서 가장 많이 등장한 순대로 정렬 후 가장 많은 단어를 vocabulary에 저장합니다.

subword = sorted(list(pairs.items()),key=lambda x : -x[1])[0][0]
vocabulary.append(subword)
print(vocabulary)
# ['p', 'b', 'n', 'g', 'h', 'u', 's', 'ug']

여기까지의 과정을 반복합니다.

for _ in range(3):
    pairs = collections.defaultdict(int)
    for word, cnt in dictionary.items():
        i, word_splited = 0,[]
        while i < len(word):
            if i+1<len(word) and word[i]+word[i+1] in vocabulary:
                word_splited.append(word[i]+word[i+1])
                i+=2
            else:
                word_splited.append(word[i])
                i+=1
        for word1,word2 in zip(word_splited[:-1],word_splited[1:]):
            pairs[word1+word2]+=cnt
    subword = sorted(list(pairs.items()),key=lambda x : -x[1])[0][0]
    vocabulary.append(subword)

# iter = 0
# [('ug', 20), ('pu', 17), ('un', 16), ('hu', 15), ('gs', 5), ('bu', 4)]
# ['p', 'b', 'n', 'g', 'h', 'u', 's', 'ug']

# iter = 1
# [('un', 16), ('hug', 15), ('pu', 12), ('pug', 5), ('ugs', 5), ('bu', 4)]
# ['p', 'b', 'n', 'g', 'h', 'u', 's', 'ug', 'un']

# iter = 2
# [('hug', 15), ('pun', 12), ('pug', 5), ('ugs', 5), ('bun', 4)]
# ['p', 'b', 'n', 'g', 'h', 'u', 's', 'ug', 'un', 'hug']

이렇게 vocabulary['p', 'b', 'n', 'g', 'h', 'u', 's', 'ug', 'un', 'hug']를 얻을 수 있습니다. corpus에서 가장 많이 등장한 bigram 쌍이 순서대로 추가되었습니다.

Huggingface의 토크나이저의 경우 이 vocabularyvocab.json으로 출력되어 단어와 단어 인덱스 값이 key-value 형태로 저장되어 있습니다.

Tokenizing

토크나이징을 하려면 위에서 구한 vocabulary가 필요합니다. vocabulary에 따라 단어들을 병합합니다.

UNK = '<unk>'
word = list('hugs')

i, r = 0, []
while i < len(word):
    if i + 1 < len(word) and word[i] + word[i+1] in vocabulary:
        r.append(word[i] + word[i+1])
        i += 1
    elif word[i] in vocabulary:
        r.append(word[i])
    else:
        r.append(UNK)
    i += 1

print(r)

# ['h', 'ug', 's']

<unk> 토큰은 토크나이저가 vocabulary에 없는 단어를 대체할 때 사용합니다. 이런 문제를 OOV(Out of Vocabulary)라 하는데 여기서는 corpus의 크기가 크지 않아 발생합니다.

다시 돌아와서 위의 코드는 hugs를 h,u,g,s로 나누고 h,ug,s로 병합하는 과정이었습니다. 이 과정을 더 이상 병합하지 않을 때까지 반복하여 토크나이징을 수행합니다.

word = list('hugs')

while True:
    i, r = 0, []
    while i < len(word):
        if i + 1 < len(word) and word[i] + word[i+1] in vocabulary:
            r.append(word[i] + word[i+1])
            i += 1
        elif word[i] in vocabulary:
            r.append(word[i])
        else:
            r.append(UNK)
        i += 1
      if word == r:
          break
    word = r
    print(word)

# ['h', 'ug', 's']
# ['hug', 's']

이 코드를 함수로 정의하고 문장에 대해 토크나아징할 수 있습니다.

# BPE Tokenizer
sentence = 'pug bug mug hug hugs'
UNK = '<unk>'

def word_tokenizing(word, vocabulary):
    while True:
        i, r = 0, []
        while i < len(word):
            if i + 1 < len(word) and word[i] + word[i+1] in vocabulary:
                r.append(word[i] + word[i+1])
                i += 1
            elif word[i] in vocabulary:
                r.append(word[i])
            else:
                r.append(UNK)
            i += 1
          if word == r:
              # 더이상 병합되지 않는다면 종료
              break
          word = r
    return word

r = []
for word in sentence.split():
    r += word_tokenizing(list(word), vocabulary)

print(r)

# ['p', 'ug', 'b', 'ug', '<unk>', 'ug', 'hug', 'hug', 's']

이렇게 BPE 알고리즘을 통해 subword를 분리할 수 있어 합성어를 사전에 포함하지 않아도 되는 장점이 있습니다. 또한, 사전에 단어가 포함되지 않아 발생하는 OOV(Out of vocabulary) 문제를 해결하는 방법이기도 합니다.

여기까지 BPE 토크나이징 기법을 python으로 구현해봤습니다.

Tags:

Categories:

Updated:

Comments