[Day19] Transformer

 

중요

BPE(Byte Pair Encoding)

일반적으로 하나의 단어에 대해 하나의 embedding을 생성할 경우 out-of-vocabulary(OOV)라는 치명적인 문제를 갖게 된다. 학습 데이터에서 등장하지 않은 단어가 나오는 경우 Unknown token으로 처리해주어 모델의 입력으로 넣게 되면서 전체적으로 모델의 성능이 저하될 수 있다. 반면 모든 단어의 embedding을 만들기에는 필요한 embedding parameter의 수가 지나치게 많아지게 된다. 서브워드 분리(Subword segmenation)작업은 하나의 단어는 더 작은 단위의 의미있는 여러 서브워드들

ex) birthplace = birth + place의 조합으로 구성된 경우가 많기 때문에, 하나의 단어를 여러 서브 워드로 분리해서 단어를 인코딩 및 임베딩하겠다는의도를 가진 전처리 작업이다. 이를 통해 OOV 나 희귀 단어, 신조어와 같은 문제를 완화시킬 수 있다. 대표저인 서브워드 분리 알고리즘인 BPE(Byte Pair Encoding)알고리즘을 알아보자.

 

접근 방식

글자(charcter)단위에서 점차적으로 단어 집합(vocabulary)을 만들어 내는 Bottom up 방식의 접근을 사용한다. 우선 가장 먼저 모든 단어를 한글자씩 분리한다. 그런 다음 각 단어들에 대해서 가장 많이 등장하는 2-gram(바이그램)을 하나의 단어로 통합한다. 위 링크의 논문 4페이지에 BPE의 코드를 공개했기 때문에 따라하기 쉽다.

 

예시

가장 먼저 단어의 개수를 Count한 딕셔너리를 만든다.

# dictionary
# 훈련 데이터에 있는 단어와 등장 빈도수
low : 5, lower : 2, newest : 6, widest : 3

 

 

이후에 모든 단어들을 글자(characters) 단위로 분리한다.

# 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

이때 초기 딕셔너리를 참고한 단어장은 아래와 같다.

# vocabulary
l, o, w, e, r, n, w, s, t, i, d

 

 

BPE의 특징은 알고리즘의 동작을 몇 회 반복(iteration)할 것인지를 사용자가 정한다는 점입니다.

논문을 기준 10회 수행하므로 여기도 10회로 예시를 들겠다. 가장 빈도수가 높은 2-gram(바이그램)을 하나의 유니그램으로 통합하는 과정을 10회 하는 것이다.

 

1번째 loop

# 1회 딕셔너리를 참고로 빈도수계산
('e', 's'): 9, ('s', 't'): 9, ('w', 'e'): 8, ('l', 'o'): 7, ...

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, w, s, t, i, d, es

 

2번째 loop

# 2회 딕셔너리를 참고로 빈도수계산
('es', 't'): 9, ('l', 'o'): 7, ('o', 'w'): 7, ('n', 'e'): 6, ...

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, w, s, t, i, d, es, est

...

이와 같은 방식으로 총 10회반복하면 아래와 같은 딕셔너리와 단어장이 만들어 진다.

# dictionary update!
low : 5,
low e r : 2,
newest : 6,
widest : 3
# vocabulary update!
l, o, w, e, r, n, w, s, t, i, d, es, est, lo, low, ne, new, newest, wi, wid, widest

 

 

논문의 예시 코드

import re, collections

def get_stats(vocab):
	pairs = collections.defaultdict(int)
	for word, freq in vocab.items():
		symbols = word.split()
	for i in range(len(symbols)-1):
		pairs[symbols[i],symbols[i+1]] += freq
	return pairs

def merge_vocab(pair, v_in):
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
        for word in v_in:
            w_out = p.sub(''.join(pair), word)
            v_out[w_out] = v_in[word]
    return v_out

vocab = {'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}
num_merges = 10
for i in range(num_merges):
    pairs = get_stats(vocab)
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    print(best)

논문 코드에서는 따로 단어장을 만드는 코드는 추가 되어 있지 않지만 위 코드를 보고 잘 따라하면 단어장을 만드는 건 쉽게 할 수 있다.

 

참고 자료

 

피어세션

  • 백준 9252번: LCS 2 문제 풀고 토론함

    # 9252번: LCS 2
    A,B=input(),input()
    def lcs(A,B):
        dp=['']*len(B)
        for i in range(len(A)):
            max_dp=''
            for j in range(len(B)):
                if len(max_dp) < len(dp[j]):
                    max_dp=dp[j]
                elif A[i] == B[j]:
                    dp[j]=max_dp+B[j]
    
        # 가장 큰 값 찾기
        ans=''
        for s in dp:
            if len(ans) < len(s):
                ans=s
        print(len(ans))
        if len(ans):
            print(ans)
    
    lcs(A,B)
    

     

     

    Git 원본 repo 가져 오기

    현재 원본 repo에서 자신의 repo로 fork를 해서 받았다.

    fork한 저장소에서 자신의 로컬로 clone한 상태에서 원본 repo에 데이터가 새로 update되었다.

    로컬 위치에 update된 repo를 받고 싶다

    1. 먼저 리모트 저장소 이름을 설정한다.

      여기서 말하는 리모트 저장소는 원본 repo가 있는 위치를 의미한다.

      git remote add AI

      여기서 저장소의 이름은 "AI"이다. 이제 그 밑에 저장소의 이름은 "AI"로 통일

    2. git pull <리모트 저장소> BRANCH_NAME

      해당 예시에서는 git pull AI main을 사용하면 원본 repo에 데이터가 로컬로 merge가 된다.

     

+ Recent posts