1장 AI는 데이터를 어떻게 이해할까: 특징량화의 원리
서브워드 분할 기법 중에서 자주 사용되는 바이트쌍 부호화(Byte Pair Encoding, BPE)를 다음 2개의 원 논문을 읽어가며 이해해 봅시다.
첫번째 논문은 서브워드의 장점을 설명할 때 다뤘던 논문으로 머신러닝에서 텍스트를 서브워드 단위로 자를 때 BPE를 쓸 경우 자주 인용됩니다. 다만 BPE(Byte Pair Encoding)라는 이름 자체는 두번째 논문에서 압축 알고리즘으로 처음 제안되었으며, 첫번째 논문에서도 이 후자 논문을 인용하고 있습니다.
이번에는 두번째 논문을 통해 원래의 BPE가 어떤 압축 기법이었는지를 파악한 뒤, 첫번째 논문을 통해 머신러닝에서 텍스트 분할용 BPE가 어떻게 쓰이는지를 이해합니다.
Neural Machine Translation of Rare Words with Subword Units 2015/08/31
A new algorithm for data compression 1994/02/01
Gage 원논문에서는 당시 널리 사용되던 사전식 압축 알고리즘인 LZW 알고리즘에 대해,
마찬가지로 사전(dictionary)을 사용하는 방식이지만
압축과정에서 만든 사전을 복원시 그대로 사용할 수 있어서, 복원속도가 빠른 BPE 알고리즘을 제안합니다.
BPE 알고리즘은 매우 단순합니다.
주어진 데이터에서 자주 등장하는 바이트쌍(byye pair)을 아직 사용되지 않은 다른 바이트로 치환하고 '더 이상 빈도가 높은 쌍이 없거나, 더 쓸 수 있는 바이트가 없을 때까지'이 과정을 반복해 압축하는 방식이다.
압축할 때는,
어떤 바이트쌍을
어떤 새 바이트로 치환했는지
를 기록한 페어 테이블(pair table)을 만들어 두고 복원시에는 이 테이블을 참조해 압축된 바이트열을 원래 바이트열로 되돌립니다.
원 논문에서 상정한는 데이터는 ASCII 코드로 작성된 텍스트이며, 각 1바이트가 하나의 ASCII 문자에 해당하는 상황입니다. 위 압축 알고리즘은 텍스트 관점에서 보면,
자주 같이 등장하는 문자 2개를 "원문에서 사용되지 않은 문자"로 치환하는 것
으로 이해할 수 있습니다.
예를 들면,
이것이 압축용 알고리즘입니다.
이미 한번 치환된 쌍에 대해서도 다시 치환을 적용할 수 있기 때문에, 데이터에 따라서는 지수적으로 압축이 가능합니다. 또한,
AB → X
XC → Y
라는 정보만 알고 있으면 압축 후, 문자열을 원래 문자열로 돌리는 것도 간단합니다.
원 논문에서는 C언어로 구현 코드도 제공하고 있으며, 빈번한 바이트 쌍을 치환해 압축하는 부분의 구현은 아래 코드와 같습니다. 여기서는 일부만 발췌했기 때문에 정의되지 않은 변수들이 좀 있지만,
buffer에 대상 데이터를 읽어두고
가장 자주 등장하는 바이트쌍의 왼쪽문자 leftch와 오른쪽 문자 rightch가 주어졌을 때
buffer를 순회하며 해당 쌍이 나타나는 위치에서 빈도 카운트를 업데이트하고 그 위치의 데이터를 치환 문자 code로 바꾸는
코드라고 이해하면 됩니다.
치환 대상 쌍들에 대한 사전정보와 압축된 데이터를 함께 저장해 두면, 복원 시에는 그 사전 정보를 참조해 압축된 데이터를 다시 펼칠 수 있습니다. 압축된 데이터 1바이트를 복원하는 구현은 아래 코드와 같으며, 이를 전체 데이터에 대해 수행하면 복원이 가능해집니다.
이 외에도 여러 최적화 요소가 있지만, 대부분 구현 효율과 연관된 부분이라 여기서는 생략합니다. 원 논문에서는 "바이트쌍을 다른 바이트로 치환"하기 때문에 Byte Pair Encoding이라는 이름이 딱 들어맞는 방식입니다.
이제 Neural Machine Translation of Rare Words with Subword Units 논문으로 넘어가서 자연어 처리 관점에서 BPE가 어떻게 쓰이는지 살펴봅시다. 앞서 살펴본 "자주 등장하는 바이트쌍을 다른 바이트로 치환한다"는 아이디어를 이 논문에서는
자주 등장하는 문자열을 하나의 토큰으로 합친다
라는 방식으로 확장합니다. 구체적인 예시는 아래 Python코드로 제시되어 있습니다.
전제는 다음과 같습니다.
vocab은 key: 공백으로 분리된 '문자시퀀스'로 표현된 단어, value: 그 단어의 등장횟수
예시 vocab에는 4개의 단어가 있으며, </w>는 단어의 끝(end-of-word)을 나타내는 특수 기호입니다.
처음에는 모든 단어가 문자단위로 공백 구분되어 있고, 각 병합(merge)을 할 때마다 해당 쌍을 붙여 하나의 심보롤 만듭니다. 이 논문에서 제안하는 방법은, 영어처럼 공백 기준으로 단어를 분할할 수 있는 언어를 염두에 둔 알고리즘입니다. 따라서 한국어나 일본어, 중국어처럼 공백으로 단어를 명확히 나누기 어렵거나 형태소 분석이 필요한 언어는 처음부터 고려되어 있지 않습니다.
논문은 보통 저자의 관심사 안에서만 논의를 전개하는 경우가 많고, 언어적으로 비주류인 한국어나 일본어 입장에서는 조금 아쉬울 수 있습니다. 하지만,
"그렇다면 우리가 필요한 방향으로 이 방법을 확장해 볼 수 있지 않을까?
라는 여지를 남긴 것으로도 볼 수 있습니다. 완전 언어 비의존적인 BPE에 대해서는 따로 다룹니다.
연습문제7: 위 Python코드를 실제로 실행했을 때, vocab이 어떤 형태로 변해 가는지 확인해 보고,
논문이 이 코드를 통해 독자가 무엇을 이해하길 기대하는지
추측해 봅시다.
솔직히 말해서, 이 코드는 독자의 이해를 돕는데 그리 친철한 예시는 아닙니다.
BPE를 실행하는 일부만 보여주고 있고,
정말 핵심만 설명하고 싶다면 더 단순하게 만들 수 있으며,
반대로 전체 알고리즘을 모두 보여주는 것도 아니기 때문에
v_out이 최종 출력이라고 오해하는 등 오히려 혼란을 줄 여지도 있습니다.
논문에 실린 예제가 항상 독자의 이해에 최적화되어 있지는 않다는 점은 원 논문을 읽다 보면 자주 마주칩니다. 알고리즘이 널리 퍼지고 여러 사람이 쓰기 시작하면, 그 뒤에 나온 글 및 블로그등에서 더 세련된 설명이 등장하는 경우도 많습니다.
여기서는 " 원 논문을 직접 읽고 이해할 수 있게 되는 것"을 목표로 하고 있으므로, 조금 더 깊숙이 들어가 원논문의 내용을 파헤쳐 봅시다. 구체적으로는
1. BPE학습결과로 어떤 파일이 생성되는지,
2. 그 파일을 사용해 입력 텍스트에 어떻게 BPE 분할을 적용하는지
이 두 가지를 명확히 할 것입니다. 이 두 내용은 원 논문만으로는 부족하고, 공식구현코드를 함께 읽어야 비로소 이해할 수 있습니다.
먼저 ① BPE 학습 생성되는 파일부터 봅시다.
해당 부분은 아래 코드처럼 learn_bpe 함수 안에서 파일에 출력되는 부분입니다. 여기서도 일부만 발췌했기 때문에 정의되지 않은 변수들이 있지만 핵심은 다음과 같습니다.
stats: key - 문자쌍(tuple), value - 해당 쌍의 등장빈도
most_frequent: 예를 들어 ('a', 'm')처럼 가장 자주 등장하는 문자쌍을 저장
이렇게 자주 등장하는 문자 쌍들을, 빈도가 높은 순서대로 파일에 기록합니다.
또한, BPE는 "토큰 쌍의 등장 빈도"가 핵심인 알고리즘이라, 출현 빈도에 관한 유명한 법칙인 Zipf의 법칙을 참고해 threshold(임계값)를 설정하는 부분도 보입니다. 이런 내용은 원 논문만 읽어서는 잘 드러나지 않지만, "빈도 기반 알고리즘을 설계하면서 저자들이 어떤 점들을 고민했는지"를 엿볼 수 있습니다. 이런 식으로 원 논문과 저자 구현을 함께 읽으면서 저자의 사고과정을 느겨보는 것도 하나의 재미입니다.
연습문제8: 위 코드와 관련된 공식 구현 전체를 읽어보고 주석에 적힌
we probably missed the best pair because of pruning
이 문장이 의미하는 바를 자신의 말로 설명해 봅시다.
이제 ② 입력 텍스트에 BPE를 적용해 서브워드 분할을 수행하는 부분을 봅시다.
이 작은 encode라는 함수 안에 단어하나에 대한 BPE적용 절차로 구현되어 있고, 그 일부가 아래 코드에 나와 있습니다. 여기에도 정의되지 않은 변수들이 있지만, 대략 다음과 같이 이해할 수 있습니다.
word: 문자 리스트로 표현된 단어
알고리즘은 단어 단위로 BPE를 적용하는 방식
bpe_codes: 예로 ('a', 'm'): 1처럼 각 문자 쌍에 '당장순위(랭크)'가 붙어 있는 딕셔너리
이 함수는 다음을 반복합니다.
1. word 안에서 인접한 문자쌍들을 훑는다.
2. 그 중 bpe_codes에 포함된 쌍만 뽑고,
3. 그 중 "가장 먼저 등장하는(= 가장 빈도가 높았던) 쌍을 골라
4. 해당 쌍을 하나의 심볼로 병합한다.
5. 단어 길이가 1이 되거나, 더 이상 병합할 쌍이 없을 때까지 반복한다.
BPE는 개념적으로는 단순하지만,
처리 속도
문자열 조작의 자잘한 예외 케이스
를 제대로 고려해 구현하려면 코드가 꽤 복잡해 집니다.
실제로 쓰기 좋은 품질의 알고리즘을 만들려면 이런 구현 디테일이 필수이고, 이런 류의 알고리즘 논문은 언젠가 한번쯤 구현까지 따라 읽어 보는 것이 깊이 있는 이해에 정말 도움이 됩니다.
연습문제9: 위 코드에 대해, 직접 간단한 예시(예: "l o w e r")를 준비해 실제로 코드가 어떤 순서로 word를 병합해 가는지 손으로 또는 코드 실행으로 확인해 봅시다.
이제 원 논문에 등장하는 텍스트 분할 기법으로의 BPE를 구현 일부까지 포함해 대략 이해한 셈입니다.
"자주 등장하는 문자열을 하나의 토큰으로 묶어 쓰자"
라는 아이디어 자체는 굉장히 단순합니다. 하지만,
단어를 더 작은 요소인 서브워드로 나눌 수 있다는 점,
그 덕분에 희귀 단어(낮은 빈도 단어)도 이미 알고 있는 서브워드들의 조합으로 처리할 수 있다는 점
이 결합되면서, BPE는 희귀단어 처리에 매유 효과적인 방법이 됩니다.
실험결과는 여기서는 생략하지만, 원 논문에서는 기계번역에서 좋은 성능을 보였고, 다양한 자연어 처리 태스크에서도 성능이 잘 나와 넓게 쓰이게 되었습니다.
개념 자체는 언어 언어에도 적용가능하지만, 원 논문에서 중요한 전제는 다음과 같습니다.
"기본적으로 단어 분할이 쉬운 언어(띄어쓰기 기반)에서 단어 빈도를 세기 좋은 환경을 가정한다.
즉, 한국어나 중국어처럼 단어 경계를 자동으로 잡기 어려운 언어에는 그대로 적용하기 어렵고, 모든 언어를 커버하는 범용 생성형 AI모델에 쓰려면, 추가적인 개선이 필요합니다.
©2024-2025 MDRULES.dev, Hand-crafted & made with Jaewoo Kim.
AI 에이전트 개발, 컨텍스트 엔지니어링 교육 컨설팅, 바이브코딩 강의 문의: https://bit.ly/4kjk5OB