EnJinnier
[NLP] 바이트 페어 인코딩(Byte Pair Encoding, BPE) 본문
0. Background
만약, 기계에게 학습시키지 못했던 단어가 등장하면 어떻게 될까?
이렇게 기계가 모르는 단어를 OOV(Out-Of-Vocabulary), 또는 UNK(Unknown Token)이라고 표현한다.
이러한 모르는 단어가 등장하면 기계가 주어진 문제를 푸는 것이 까다로워지며, 이러한 상황을 OOV문제라고 한다.
그렇다면, 기계는 모르는 단어를 가진 상태로 어떻게 문제를 풀어야할까?
하나의 단어는 더 작은 단위의 의미있는 여러 서브워드들(Ex) birthplace = birth + place)의 조합으로 구성된 경우가 많기 때문에, 하나의 단어를 여러 서브워드로 분리해서 단어를 인코딩 및 임베딩하겠다는 의도를 가진 전처리 작업인 서브워드 분리(Subword segmenation) 작업을 처리한다. 이런 작업을 하는 토큰을 서브워드 토크나이저라고 한다.
서브워드 토크나이저의 주요 알고리즘이 바로 바이트 페어 인코딩이다.
1. BPE(Byte Pair Encoding)
BPE는 OOV문제를 완화하는 대표적인 서브워드 분리(subword segmentation) 알고리즘이다.BPE를 요약하자면, 데이터에 있는 단어들을 글자(character) 단위로 단어 집합(vocabulary)을 만들고, 가장 많이 등장하는 유니그램을 하나의 유니그램으로 통합한다.
간단한 예시를 통해 살펴보자. 어떠한 훈련 데이터에 low, lower, newest, widest 라는 단어가 있고, 이 기계에 lowest라는 학습한적 없는 단어가 등장했다. (OOV 문제 발생)
1. 우선, 딕셔너리의 모든 단어들을 글자(character)단위로 분리한다. ( 딕셔너리는 자신 또한 업데이트되며 앞으로 단어 집합을 업데이트하기 위해 지속적으로 참고되는 참고 자료의 역할을 한다.)
# dictionary
l o w : 5, l o w e r : 2, n e w e s t : 6, w i d e s t : 3
2. 사용자가 지정한 반복(iteration) 횟수를 통해 알고리즘의 동작을 반복하며 가장 빈도수가 높은 유니그램의 쌍을 하나의 유니그램으로 통합한다. 1회 - 딕셔너리를 참고로 하였을 때 빈도수가 9로 가장 높은 (e, s)의 쌍을 es로 통합한다.
# dictionary update!
l o w : 5,
l o w e r : 2,
n e w es t : 6,
w i d es t : 3
# vocabulary update!
l, o, w, e, r, n, s, t, i, d, es
2회 - 빈도수가 9로 가장 높은 (es, t)의 쌍을 est로 통합한다.
# dictionary update!
l o w : 5,
l o w e r : 2,
n e w est : 6,
w i d est : 3
# vocabulary update!
l, o, w, e, r, n, s, t, i, d, es, est
3회 - 빈도수가 7로 가장 높은 (l, o)의 쌍을 lo로 통합한다.
# dictionary update!
lo w : 5,
lo w e r : 2,
n e w est : 6,
w i d est : 3
# vocabulary update!
l, o, w, e, r, n, s, t, i, d, es, est, lo
이와 같은 방식으로 10회까지 반복하면 다음과 같은 딕셔너리와 단어 집합을 얻을 수 있다.
# dictionary update!
low : 5,
low e r : 2,
newest : 6,
widest : 3
# vocabulary update!
l, o, w, e, r, n, s, t, i, d, es, est, lo, low, ne, new, newest, wi, wid, widest
3. 이후, 테스트 과정에서 lowest 란 단어가 등장하면, 기계는 'lowest'를 전부 글자 단위( l o w e s t )로 분할한다. 그리고 위의 단어 집합을 참고하여 low와 est를 찾아내 두 단어로 인코딩한다. 이 두 단어는 둘 다 단어 집합에 있는 단어이므로 더이상 OOV가 아니다.
2. 코드 실습
#필요한 도구 임포트
import re, collections
from IPython.display import display, Markdown, Latex
#BPE 수행 횟수 지정(예: 10회)
num_merges = 10
#훈련 데이터 딕셔너리 설정( </w>는 단어 맨 끝에 붙이는 특수문자, 각 단어는 글자(character)단위로 분리됨)
dictionary = {'l o w </w>' : 5,
'l o w e r </w>' : 2,
'n e w e s t </w>':6,
'w i d e s t </w>':3
}
#BPE 코드 : 가장 빈도수가 높은 유니그램의 쌍을 하나의 유니그램으로 통합하는 과정
#주어진 딕셔너리에 유니그램의 빈도수를 계산하여 반환하는 함수
def get_stats(dictionary):
# 유니그램의 pair들의 빈도수를 카운트
pairs = collections.defaultdict(int) #새로운 딕셔너리 pairs를 생성(유니그램의 빈도수를 저장하기 위해 사용)
for word, freq in dictionary.items(): #입력된 딕셔너리의 각 항목에 대해 반복
symbols = word.split() #단어를 공백을 기준으로 분할하여 symbols 리스트에 저장
for i in range(len(symbols)-1):
pairs[symbols[i],symbols[i+1]] += freq #현재 토큰과 다음 토큰을 튜플로 묶은 것이 딕셔너리 pairs의 키가 되며, 해당 키에 대한 값을 freq만큼 증가시킴
print('현재 pair들의 빈도수 :', dict(pairs))
return pairs
#두 가지 딕셔너리를 병합하는 함수
#정규 표현식을 사용하기 위해 're'모듈 import
def merge_dictionary(pair, v_in): #pair: 병합할 pair, v_in: 입력으로 받는 딕셔너리
v_out = {} #최종적으로 병합된 결과를 저장하는 딕셔너리
bigram = re.escape(' '.join(pair)) #입력된 pair를 공백을 기준으로 분할하고, 정규 표현식에 사용될 수 있도록 이스케이프 처리한 후 bigram 변수에 할당
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)') #정규 표현식 패턴 p를 컴파일(이 패턴은 입력된 pair가 단어 경계로 감싸여 있는 경우를 찾음)
for word in v_in:
w_out = p.sub(''.join(pair), word) #패턴 p를 사용하여 현재 단어 word에서 pair를 찾고, 해당 pair를 입력된 pair로 교체하여 w_out 변수에 할당
v_out[w_out] = v_in[word] #새로운 pair로 교체된 단어 w_out을 키로 사용하여 v_out 딕셔너리에 있는 원래 단어 word의 값을 복사
return v_out
#BPE알고리즘 실행
bpe_codes = {}
bpe_codes_reverse = {}
#아래의 단계를 반복하면서 BPE 알고리즘이 수행되고, 딕셔너리가 점진적으로 업데이트됨
for i in range(num_merges):
display(Markdown("### Iteration {}".format(i + 1)))
pairs = get_stats(dictionary) #현재 딕셔너리에서 유니그램(pair들)의 빈도수 계산
best = max(pairs, key=pairs.get) #가장 빈도가 높은 pair를 선택
dictionary = merge_dictionary(best, dictionary) #선택된 pair를 기반으로 딕셔너리를 업데이트
#새로운 merge에 대한 정보를 기록
bpe_codes[best] = i
bpe_codes_reverse[best[0] + best[1]] = best
#반복에서 수행된 merge 및 업데이트된 딕셔너리를 출력
print("new merge: {}".format(best))
print("dictionary: {}".format(dictionary))
참고한 글