(이 글은 rat'sgo 님의 블로그 (https://ratsgo.github.io/nlpbook/)와 네이버 커넥트재단 부스트캠프 AI Tech 4기의 강의 자료를 바탕으로 작성하였습니다.)
1. Intro
BPE(Byte Pair Encoding)는 토큰화를 할 때 단어를 단어보다 더 작은 단위인 subword로 쪼개어 표현하는 기법입니다. 자연어처리 분야에서 문장을 잘 처리하기 위해서는 문장을 더 작게 나눠주어야 합니다. 이때 나누어지는 한 단위를 '토큰(token)'이라고 하고, 이렇게 문장을 토큰 단위로 쪼개는 과정을 '토큰화(Tokenization)'라고 합니다. (토큰화 방법에는 여러 가지 방법이 제안되었습니다. 특히, 교착어인 한국어를 토큰화하는 것은 꽤나 어려운 작업입니다. 이에 대해서는 https://bongseok.tistory.com/11 를 참고해주시면 감사하겠습니다.)
단어를 subword 단위로 자르게 되면, 최악의 경우에도 문자 단위로 토큰화가 진행됩니다. 이 덕분에, 자연어처리 모델의 어휘집(vocabulary)에 없는 토큰일지라도 UNK(unknown) 토큰으로 처리하지 않아도 됩니다. 즉, BPE 기반 subword 토큰화 기법은 OOV(Out-of Vocabulary) 문제에 강한 방법이라고 할 수 있습니다.
2. BPE란?
BPE란 데이터에서 가장 많이 등장하는 문자열을 병합하는 기법으로, 데이터를 압축하는 기법으로써 먼저 제안된 방법입니다. 이러한 BPE로 토큰화를 하게 되면, 우리는 별도의 학습 없이도 토큰화를 위한 vocabulary를 구축할 수 있습니다. 절차는 다음과 같습니다.
첫째, vocabulary(이하 vocab)를 구축합니다. 먼저 코퍼스에 있는 모든 단어들을 사전에 정의한 최소 단위(보통 character)로 쪼갠 것을 초기 vocab으로 삼습니다. 그 뒤, 서로 자주 출현하는 문자열을 병합하고 vocab에 추가하는 과정을 사전에 정의한 vocab의 최대 크기(vocab_size)가 다 찰 때까지 반복합니다.
둘째, 구축된 vocab을 바탕으로 토큰화를 수행합니다. 주어진 문장의 각 어절에서, vocab에 수록되어 있는 subword를 발견한다면, 해당 subword를 따로 분리해냅니다.
3. BPE 기반 Vocab 만들기
BPE를 기반으로 vocab을 만드는 과정을 좀 더 자세하게 살펴보겠습니다. 먼저, 코퍼스 내의 모든 문장을 공백을 기준으로 나눠줍니다. 즉, 어절 단위로 나눠줍니다. 그리고 문자열 단위로 초기의 vocab을 만들어줍니다. 예를 들어 코퍼스가 "철수는 치킨을 먹고 볶음밥을 먹고 있다"라는 한 문장으로만 구성되어 있다면, {철, 수, 는, 치, 킨, 을, 먹, 고, 볶, 음, 밥, 있, 다}
가 초기의 vocab이 됩니다.
BPE에서는 문자열들을 subword를 만들기 위한 기준으로 빈도를 활용합니다. 문자열 쌍이 한 어절 내에서 얼마나 서로 많이 출현했는지를 알아보기 위해 빈도표를 만들어줍니다. 이때 동일 어절 내 출현 빈도를 바이그램(bigram) 방식으로 나타냅니다.
철, 수 | 10 |
---|---|
수, 는 | 10 |
치, 킨 | 9 |
킨, 을 | 9 |
먹, 고 | 11 |
볶, 음, 밥, 을 | 5 |
볶, 음 | 5 |
음, 밥 | 5 |
밥, 을 | 5 |
있, 다 | 2 |
위의 표는 "철수는"이라는 어절이 10번, "치킨을"이 9번, "먹고"가 11번, "볶음밥을"이 5번, "있다"가 2번 출현했다고 가정했을 때의 동일 어절 내 바이그램 출현 빈도표입니다. 여기에서 가장 많이 출현한 바이그램 쌍(pair)은 "먹고"입니다. 이제 이 "먹고"를 vocab에 새로운 어휘로써 추가해줍니다. 그러면 vocab은 {철, 수, 는, 치, 킨, 을, 먹, 고, 볶, 음, 밥, 있, 다, 먹고}
이 됩니다.
새롭게 추가된 어휘를 기반으로 다시 바이그램 빈도표를 만들어봅시다. 이제 "먹고"라는 어절은 "먹, 고"라고 나타내는 것이 아니라 "먹고"라는 하나의 어휘(토큰)으로 나타냅니다.
철, 수 | 10 |
---|---|
수, 는 | 10 |
치, 킨 | 9 |
킨, 을 | 9 |
먹고 | 11 |
볶, 음, 밥, 을 | 5 |
볶, 음 | 5 |
음, 밥 | 5 |
밥, 을 | 5 |
있, 다 | 2 |
이번에는 "철, 수", "수, 는"이 가장 많이 출현한 바이그램 쌍입니다. 이 쌍들을 각각 이어붙인 "철수"와 "수는"을 vocab에 추가해줍니다. 그러면 vocab은 {철, 수, 는, 치, 킨, 을, 먹, 고, 볶, 음, 밥, 있, 다, 먹고, 철수, 수는}
이 됩니다.
이러한 과정을 vocab_size가 다 찰 때까지 반복하면 BPE 기반 토큰화를 위한 vocab을 완성할 수 있게 됩니다. 그런데 과연 vocab의 size를 얼마나 해야 적당할까요? vocab size가 크면 클수록, 코퍼스에 등장하는 모든 시퀀스를 vocab에 등재할 수 있으므로 좋을 것이라는 생각이 듭니다. 하지만 vocab size를 너무 키우면 PLM에 들어가는 행렬의 사이즈도 커져서 학습이 지나치게 오래 걸리게 됩니다. Park et al. (2020)에서는 한국어 BERT 모델 실험에서 vocab size를 키울수록 성능이 더 좋아진다고 보고한 바 있습니다. 이 연구에서는 vocab size를 최대 64,000까지 키웠다고 합니다.
4. BPE 토큰화
이제 vocab이 완성되었으니, 이것을 바탕으로 주어진 문장을 토큰 단위로 쪼개줄 수 있습니다. 문장을 토큰 단위로 쪼개는 토큰화는 다음과 같은 과정으로 이루어집니다.
먼저 문장을 어절 단위로 나눈 뒤, 각 어절을 살펴봅니다. 이때 어절 내에, vocab에 들어있는 subword가 들어있다면 subword를 분리해줍니다. 이때 가장 긴 subword를 우선적으로 복원해줍니다. 예를 들어, 어절에 "먹고"라는 subword가 발견되었고, vocab에 "먹고", "먹", "고"가 모두 들어있다면, 세 토큰 중에서 길이가 가장 긴 "먹고"라고 복원해주는 것입니다.
일반적으로, 개별 문자 전체가 초기 vocab에 들어가기 때문에 UNK(Unknown token)이 발생하는 경우는 거의 없다고 합니다. 따라서 "설명충", '프로출근러"와 같은 신조어가 있더라도, 서로 자주 등장하는 쌍인 "설명", "충", "프로", "출근", "러"와 같은 방식으로 쪼개질 수 있습니다. 이처럼 BPE 기반 토큰화는 별도의 학습 과정 없이도 어절을 유의미한 단위로 분석해낼 수 있다는 장점이 있습니다. 또한, UNK 문제를 경감할 수 있다는 점에서도 큰 장점을 지닙니다.
5. 코드 실습
마지막으로, BPE 기반 토큰화를 위한 vocab을 구축하는 과정을 코드로 살펴보겠습니다. 초기 어휘 사전을 구축한 뒤, Counter를 활용하여 어절별 빈도표를 만들고, 가장 자주 등장하는 문자열 쌍을 하나의 토큰으로 취급하여 vocab에 넣어줍니다. 이 과정을 vocab size가 다 찰 때까지 반복합니다. 마지막으로, vocab을 길이를 기준으로 오름차순으로 정렬하여 리턴합니다.
from collections import Counter
from itertools import chain
WORD_END = '_'
def build_bpe(corpus, vocab_size):
# 초기 어휘 사전을 구축합니다.
# corpus에서 중복 어휘들을 제거한 뒤, 어절 끝을 표시하는 '_'도 함께 추가해줍니다.
vocab = list(set(chain.from_iterable(corpus)) | {WORD_END})
# 코퍼스를 만들어줍니다. Couunter를 활용하여 어절별 빈도를 나타내는 dictionary를 만듭니다.
corpus = {' '.join(word + WORD_END): count for word, count in Counter(corpus).item()}
# vocab_size를 다 채울 때까지 진행합니다.
while len(vocab) < vocab_size:
counter = Counter()
for word, word_count in corpus.items():
word = word.split()
for pair, count in Counter(zip(word, word[1:])).items():
counter.update({pair:count})
if not counter:
break
# 가장 흔하게 나오는 pair는 하나의 어휘로 vocab에 추가해줍니다.
most_common_pair = counter.most_common(1)[0][0]
vocab.append(''.join(pair))
corpus = {word.replace(' '.join(pair), ''.join(pair)):count for word, count in corpus.items()}
# 길이를 기준으로 오름차순 정렬한 vocab을 리턴합니다.
return sorted(vocab, key=len, reverse=True)
Reference
- Park, K., Lee, J., Jang, S., & Jung, D. (2020). An empirical study of tokenization strategies for various Korean NLP tasks. arXiv preprint arXiv:2010.02534.
- https://ratsgo.github.io/nlpbook/docs/preprocess/bpe/#:~:text=%EC%9B%8C%EB%93%9C%ED%94%BC%EC%8A%A4-,BPE%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C%3F,%EB%A5%BC%20%EC%95%95%EC%B6%95%ED%95%98%EB%8A%94%20%EA%B8%B0%EB%B2%95%EC%9E%85%EB%8B%88%EB%8B%A4.
'NLP' 카테고리의 다른 글
Llama2 초간단 요약 (0) | 2023.07.23 |
---|---|
NLP 트렌드의 흐름 간단 요약 (0) | 2023.06.23 |
[NLP] Attention의 개념 간단 정리 (0) | 2023.04.17 |
Boostcamp AI Tech 4기 최종 프로젝트 후기 (일기 감성 분석 및 코멘트 생성) (1) | 2023.02.19 |
ODQA (Open Domain Question Answering) 경진대회 후기 (Naver BoostCamp AI tech 4기) (0) | 2023.01.23 |