LLM : Token

LLM을 처음 접하면 어렵고 헷갈리는 용어와 개념들이 많습니다. 그중에서도 가장 알듯 말듯 아리송한 개념이 바로 Token이 아닐까 합니다.

Token이라는 단어를 처음 접하는 건 LLM을 사용하는 입장인지 LLM을 만드는 입장인지에 따라 다릅니다.

LLM을 사용하는 입장에서는 "이 LLM의 최대 Context Window 크기가 4K tokens이다", "이 LLM은 비용이 1M tokens 당 0.5 달러다"와 같은 설명을 통해 Token이라는 표현을 처음 접하게 됩니다. Token이 입출력 텍스트 데이터의 크기를 나타내는 것 같기는 한데, 글자수도 아니고 단어수도 아니고 Token 수라는 이상한 단위 때문에 고개를 갸웃하게 되고요.

그러고 나서 이어지는 자연스러운 질문은 "그래서 1,000 Token이 몇 글자예요?"일 것입니다. 하지만 돌아오는 질문은 더 헷갈립니다. 보통 답은 "딱 떨어지게 말할 수는 없는데 영어 기준으로 2천 자에서 4천 자 정도 됩니다. 한국어는 또 다를 수 있고요"같은 류입니다.

오늘은 도대체 Token이 뭐길래 이런 선문답같은 대화가 이어질 수밖에 없는지 이야기해보려고 합니다.

모든 것은 숫자

이 글에서 LLM 이라는 주제 아래 Token을 이야기하고 있지만 사실 Token은 자연어처리 분야에서는 LLM이 등장하기 아주 오래전부터 쓰인 개념입니다. 본격적으로 Token을 이야기하기 전에 컴퓨터가 텍스트를 이해하는 방식을 짚어 보겠습니다.

잘 알려진 것처럼 컴퓨터는 모든 데이터를 숫자로 표현합니다. 화려한 영상도, 멋진 사진도, 아름다운 소설도 데이터로 저장할 때는 컴퓨터가 이해할 수 있는 숫자로 저장합니다. 우리가 영상을 보고, 사진을 보고, 소설을 보기 위해서는 숫자로 표현된 데이터를 우리가 이해할 수 있는 형태로 변환하는 것이고요. 예를 들어 지금 읽고 있는 이 글도 서버에 숫자 덩어리로 저장되어 있지만, 그 데이터를 우리가 읽을 수 있는 글씨 형태로 변환해서 모니터에 그려주고 있는 것입니다.

새삼스럽게 왜 이 이야기를 했을까요? 바로 LLM을 포함한 머신러닝 알고리즘들도 숫자만 처리할 수 있기 때문입니다. 우리가 LLM에 입력으로 주는 텍스트는 LLM이 이해할 수 있는 숫자로 변환되고, LLM이 생생한 텍스트는 숫자로 돼있기 때문에 우리가 이해할 수 있는 텍스트로 변환하는 과정이 필요합니다.

그럼 텍스트를 어떻게 숫자로 변환할 수 있을까요? 가장 직관적이고 널리 쓰이는 방법은 글자 하나하나를 숫자로 매핑하는 방법입니다. 아스키코드, 유니코드 같은 것들이 그런 방법입니다.

"나는 어제 사과를 먹었다."라는 텍스트를 LLM에게 입력으로 준다고 생각해보죠. 글자를 숫자로 매핑한다면 아래와 같이 될 겁니다. Space도 한 글자로 취급을 했습니다. 각 글자에는 임의의 숫자를 지정했습니다.

나: 1
는: 2
 : 3
어: 4
제: 5
 : 3
사: 6
과: 7
를: 8
 : 3
먹: 9
었: 10
다: 11
.: 12

입력을 숫자로 표현하면 [1, 2, 3, 4, 5, 3, 6, 7, 8, 3, 9, 10, 11, 12]가 됩니다.

이 경우를 Case 1이라고 불러보겠습니다.

글자가 숫자로 변환됐으니 이제 LLM이 이 입력을 처리할 수 있습니다.

그런데 이 방법에는 한가지 문제가 있습니다. 언어를 이해하는 관점에서 보면 "어", "제" 보다는 "어제"가, "사", "과", "를" 보다는 "사과를"이 더 유의미한 단위라고 볼 수 있습니다. LLM에게 큰 의미 없는 글자를 입력으로 주는 것보다 어느 정도 의미를 가지고 있는 단위를 입력으로 주면 왠지 LLM의 품질이 좋아질 것 같은 느낌적인 느낌이 듭니다.

이제 글자 단위가 아닌 띄어쓰기 단위로 입력을 구분해 보기로 합니다.

나는 : 1
 : 2
어제 : 3
 : 2
사과를 : 4
 : 2
먹었다 : 5
. : 6

이제 "나는 어제 사과를 먹었다."라는 입력은 [1, 2, 3, 2, 4, 2, 5, 6] 이라는 값(Vector)으로 LLM이게 들어갑니다.

이 경우는 Case 2라고 해보겠습니다.

입력이 숫자로 변환됐을 뿐 아니라 왠지 언어학적으로 좀 더 유의미한 값이 된 것 같습니다.

그런데 여기에는 또다른 문제가 있습니다.

"사과를"이라는 부분을 살펴보겠습니다. "를"은 조사이기 때문에 "사과"라는 단어를 이용해서 많은 조합을 만들 수 있습니다. "사과가", "사과는", "사과와", "사과와" 등과 같이요. 한국어에 얼마나 많은 단어들이 있는지를 생각해 보면 이런 조합 또한 엄청나게 많이 만들 수 있습니다. 조합이 많이 나온다는 것은 LLM 입장에서는 신경 써야 할 입력이 많아진다는 뜻입니다. 입력이 많아지면 계산 복잡도가 올라가고 그만큼 좋은 품질을 얻기가 어려워집니다.

이런 면에서는 처음의 글자 단위 입력이 좋습니다. 영어만 다룬다면 대소문자, 기호를 다해도 100가지가 되지 않고요. 한글까지 다룬다고 하더라도 10,000자가 조금 넘습니다. 하지만 이렇게 되면 글자보다 유의미한 단위를 LLM의 입력으로 쓰고 싶다는 소망(?)을 이루기 어렵게 됩니다.

하지만 포기하기는 이릅니다. 글자보다는 길지만 어절(한국어의 띄어쓰기 단위)보다는 작은 크기를 사용하면 어떨까요? 예를 들어 "사과를" 대신에 "사과"와 "를"을 입력으로 사용하는 것입니다.

나 : 1
는 : 2
 : 3
어제 : 4
 : 3
사과 : 5
를 : 6
: 3
먹 : 7
었 : 8
다 : 9
. : 10

숫자로 표현하면 [1, 2, 3, 4, 3, 5, 6, 3, 7, 8, 9, 10] 이 됩니다.

이건 Case 3이라고 해보겠습니다.

입력은 "나는 어제 사과를 먹었다."로 같지만 숫자로 변환하는 단위를 어떻게 하느냐에 따라 LLM에 들어가는 실제 입력을 달라지는 것이죠.

Token

명확히 이야기하지 않았지만 이미 Token에 대해서 설명을 마쳤습니다. Token은 텍스트 입력을 숫자로 변환할 때 기준이 되는 각각의 단위입니다.

  • Case 1에서는 ["나", "는", " ", "어", "제", " ", "사", "과", "를", " ", "먹", "었", "다", "."] 이렇게 14개의 Token을 가집니다.
  • Case 2에서는 ["나는", " ", "어제", " ", "사과를", " ", "먹었다", "."] 이렇게 8개의 Token을 가지고요.
  • Case 3에서는 ["나", "는", " ", "어제", " ", "사과", "를", " ", "먹", "었", "다", "."] 이렇게 12개의 Token을 가집니다.

똑같은 입력이라고 하더라도 어떻게 Token으로 나누냐에 따라 LLM의 입력이 달라집니다.

주어진 텍스트를 Token으로 나누어주는 모듈을 Tokenizer라고 부릅니다.

  • "나는 어제 사과를 먹었다." -> Case 1 Tokenizer -> ["나", "는", " ", "어", "제", " ", "사", "과", "를", " ", "먹", "었", "다", "."]
  • "나는 어제 사과를 먹었다." -> Case 2 Tokenizer -> ["나는", " ", "어제", " ", "사과를", " ", "먹었다", "."]
  • "나는 어제 사과를 먹었다." -> Case 3 Tokenizer -> ["나", "는", " ", "어제", " ", "사과", "를", " ", "먹", "었", "다", "."]

Tokenizer와 Context Window Size

이제 LLM의 입력 크기 제한을 다룰 때 왜 글자 수나 단어 수가 아닌 Token수로 말하는지 이야기할 준비가 됐습니다.

  • LLM은 Text를 그대로 받을 수 없기 때문에 입력 Text를 LLM이 이해할 수 있는 숫자로 변환하는 과정이 필요합니다.
  • Text를 숫자로 변환하기 위해서는 숫자로 변환할 단위로 쪼개야합니다.
  • 이 단위는 글자가 될 수도 있고, 단어가 될 수도 있고, 글자보다는 크고 단어보다는 작은 어떤 단위가 될 수도 있습니다.
  • 이 단위로 쪼개진 것을 Token이라고 부릅니다.
  • Token은 정하기에 따라 1글자가 될 수도("나", "는"), 2글자("어제", "사과")가 될 수도, 그 이상이 될 수도 있습니다.

만약 우리 LLM이 Case 1 Tokenizer를 쓰기로 했다면 "나는 어제 사과를 먹었다."가 14 Token을 차지하고, Case 2 Tokenizer를 쓴다면 같은 입력이 8 Token을 차지합니다.

다르게 말하면 Case 1 Tokenizer는 1 Token이 1 글자(14/14)이고, Case 2 Tokenizer는 1 Token이 평균 1.75 글자 (14/8)입니다. LLM이나 Tokenizer 관련 내용을 볼 때 1 Token이 평균 몇 글자다라는 표현이 나오는 이유가 이 때문입니다.

LLM은 여러 기술적인 이유로 입력으로 받을 수 있는 Token 수(Context Window)에 제약이 있습니다. 그렇기 때문에 1 Token이 많은 글자를 표현할 수 있을수록 LLM에 입력으로 넣을 수 있는 텍스트 길이가 길어집니다.

그럼 Case 2 Tokenizer를 쓰는 것이 좋지 않을까요? LLM이 한 번에 받아들일 수 있는 Token 수는 정해져 있기 때문에 Case 2 Tokenizer를 쓰면 실제로 받아들일 수 있는 입력 텍스트가 Case 1 Tokenizer 보다 길어지니까요.

전체 Token 종류

답은 꼭 그렇지 않다입니다.

"나는 어제 사과를 먹었다."에 그치지 않고 "너는 배를 먹었지.", "너가 어제는 보고 싶었다." 와 같이 입력이 엄청나게 많아진다고 생각해 보겠습니다. LLM이니만큼 훈련시킬 때 쓰는 문장이 매우 많겠죠.

Case 1은 각 글자를 Token으로 하기 때문에 아무리 많은 입력을 보더라도 전체 Token 종류는 전체 한글 글자수인 10,000여 개에서 멈춥니다.

나: 1
는: 2
 : 3
어: 4
제: 5
 : 3
사: 6
과: 7
를: 8
 : 3
먹: 9
었: 10
다: 11
.: 12

너: 13
는: 2
 : 3
배: 14
를: 8
 : 3
먹: 9
었: 10
지: 15
.: 12

너: 13
가: 16
 : 3
어: 4
제: 5
는: 2
 : 3
보: 17
고: 18
 : 3
싶: 19
었: 10
다: 11
.: 12

세 문장의 공백 포함 전체 글자수는 38글자지만 전체 Token 종류는 20개뿐입니다. "는", "었", "다" 같은 Token은 여러 번 나타나기 때문입니다.

Case 2 Tokenizer는 어떨까요? Case 2 Tokenizer는 띄어쓰기 단위이기 때문에 겹치는 부분이 거의 없습니다. Case 1 Tokenizer에서 "나는" -> ["나", "는"], "너는" -> ["너", "는"]가 돼서 "는"은 공통 Token이 되지만, Case 2 Tokenizer에서는 "나는"과 "너는"이 다른 Token이기 때문에 공통 Token이 전혀 없습니다.

이 현상은 데이터가 많아질수록 심각해집니다. Case 1 Tokenizer에서는 점점 공통 Token이 많아지다가 전체 한글 수만큼이 되면 더 이상 Token이 늘어나지 않습니다.

하지만 Case 2 Tokenizer는 모든 조합가능한 한글 어절(나는, 너는, 그는, 사과는, 등등)이 개별적이 Token이 되기 때문에 Token 종류가 무한정 늘어납니다.

Token 종류가 늘어난다는 것은 LLM이 이해하고 또 생성해야 하는 Token 종류가 많아진다는 의미입니다. 그만큼 LLM이 품질을 높이기가 어렵다는 뜻이죠.

사진을 분류하는 AI 모델은 만든다고 생각해 보겠습니다.

모델 A는 동물과 식물을 분류하는 모델입니다.

모델 B는 고양이, 강아지, 소나무, 해바라기를 분류하는 모델입니다.

둘 중 무엇을 만들기 더 어려울까요? 모델 B입니다. 판단하고 예측해야 할 정보가 많기 때문입니다. Token 종류가 많아진다는 것도 같은 문제입니다.

즉, Token 종류가 적을수록 LLM이 처리해야 하기 때문에 품질을 확보하기 쉽습니다. 처리해야 할 데이터가 줄어들기 때문에 속도도 빠를 것이고요.

Tradeoff

Token을 얼마나 잘게 쪼개느냐는 입력 데이터 수와 처리해야할 Token 종류 간에 Tradeoff 관계에 있습니다.

  • Token을 잘게 쪼개면,
    • 같은 입력에 대해 많은 Token이 됩니다. 즉, LLM에 넣을 수 있는 텍스트 양이 줄어듭니다.
    • 하지만 LLM이 처리해야할 Token이 종류는 줄어듭니다. LLM의 복잡도가 낮아집니다.
  • Token을 크게 쪼개면,
    • 같은 입력에 대해 적은 Token이 됩니다. 즉, LLM에 넣을 수 있는 실제 텍스트 양은 많아집니다.
    • 하지만 LLM이 처리해야할 Token이 종류는 많아집니다. LLM의 복잡도가 높아집니다.

이 외에도 고려해야 할 점이 있습니다. Token은 LLM 입력 기본 단위이다 보니 가급적이면 각 Token이 의미를 가지면 더 좋습니다. 아무래도 "어", "제" 보다는 "어제"가 한 Token이면 문장을 이해하는데 도움이 되겠죠.

이런 이유 때문에 LLM(을 포함한 자연어처리 모델)을 만들 때 Tokenizer를 어떻게 설계할 것이냐는 매우 중요한 문제입니다.

모든 Token을 다 학습시킬 수는 없다

Case 1 Tokenizer는 많아봐야 전체 한글 + 영어 알파벳 + 기호 정도를 고려하면 전체 Token 종류가 10,000여개에 그칩니다. 하지만 Case 2, Case 3 Tokenizer는 엄청나게 많은 조합이 나올 수 있기 때문에 Token 종류도 수십만, 수백만이 될 수도 있습니다. 이 모든 Token을 LLM에 훈련에 다 사용할 수는 없습니다. 입력 데이터 종류가 너무 많아지면 큰 모델이 필요하고, 큰 모델은 그만큼 많은 데이터를 필요로 하고 훈련도 어렵기 때문입니다.

이런 이유로 Tokenizer를 만들 때는 보통 전체 Token 종류를 제한합니다. 예를 들어 Case 2 Tokenizer로 만들 수 있는 Token 종류가 실제로는 1백만 개라고 하더라도, 나는 이 모델에서는 빈도수로 잘라서 100,000개까지만 훈련에 쓰겠다고 하는 식입니다. 그럼 이 100,000개에 들어가지 못하는 Token들은 어떻게 할까요? 보통은 특별한 Token(보통 UNK라고 많이 부릅니다)을 하나 두고, 범위 밖의 Token이 나오면 이 특별한 Token으로 치환합니다. 아주 정확하지는 않지만 Engineering tradeoff 인 셈입니다.

생성 관점

지금까지는 LLM 입력 관점에서 Token을 살펴보았는데요. 이번에는 생성(출력)관점에서 살펴보겠습니다.

https://jins-sw.tistory.com/49 에서 다뤘듯이 LLM(정확히는 Decoder 모델)이 하는 일은 입력에 이어질 다음 Token을 생성하는 것입니다. 중요한 문장인데요. LLM이 예측하는 것은 다음 글자나 단어가 아닌 다음 Token이라는 점입니다.

하지만 LLM은 글자는 모르고 숫자만 압니다. 그럼 LLM이 진짜 생성하는 것은 무엇일까요? 바로 Token에 매핑된 숫자입니다.

예를 들어 Case 2 Tokenizer를 쓰고 있는 LLM이 있다고 가정해 보겠습니다. 이 LLM에게 입력으로 "나는 어제 사과를 "을 입력으로 주었습니다. 이 입력은 Tokenizer를 거치고, 숫자 변화를 거쳐 [1, 2, 3, 2, 4, 2]라는 값으로 바뀝니다. LLM이 이 값을 처리해 이다음에 이어질 값인 6을 예측합니다. 숫자 6에 해당하는 Token은 "먹었다"입니다.

앞의 Tradeoff 이야기를 여기에서도 적용해 볼 수 있습니다. "나는 어제 사과를 "이 입력이라고 가정하면,

  • Case 1 Tokenizer : "먹", "었", "다", "." 총 4 Token을 생성해야 합니다. (정확히는 End-of-Sentence 까지 5 Token)
  • Case 2 Tokenizer : "먹었다", "." 총 2 Token을 생성해야합니다.

Tokenizer를 어떻게 구성하느냐가 입력뿐 아니라 출력의 복잡도, 속도에도 영향을 미칩니다.

이제 Tokenizer를 어떻게 만드는지에 대해 간단히 이야기해보려고 합니다. 구현에 대한 이야기보다는 Tokenizer를 만드는 두 가지 큰 접근법에 대해서 다루어 보겠습니다.

언어학적 접근

"사과를"을 Tokenize 한다고 생각해 보겠습니다. 앞에 예시를 다시 가져와보겠습니다.

  • Case 1: "사", "과", "를"
  • Case 2: "사과를"
  • Case 3: "사과", "를"

잘은 모르겠지만 왠지 Case 1 처럼 쪼개면 중요한 정보들이 사라질 것 같습니다. Token만 봐서는 "사과"에 대한 이야기인지 잘 모를 것 같습니다.

Case 2는 Case 1보다는 좀 나아 보이지만 "를"이라는 조사가 불필요하게 붙어있는 것 같습니다. 의미적으로도 숫자만 아는 LLM이 "사과"와 "사과를"을 구분할 수 있을 것 같지 않습니다. 예를 들어 숫자로 변환되고 나면 "사과"는 56, "사과를"은 921이 돼버리면 두 가지가 비슷하다는 것을 알 도리가 없으니까요.
(물론 요즘 자연어처리에서는 Embedding이라는 기법을 쓰기 때문에 이런 문제는 덜 합니다)

Case 3은 각 Token이 의미를 잘 살리고 있는 것 같습니다. 실제로 한국어에서 의미를 유지한 가장 작은 단위를 형태소라고 부릅니다. 이 경우에는 "사과"와 "를"이 형태소에 해당합니다. 형태소라는 언어학적 특징을 활용하기 위해 한국어 자연어처리에서는 형태소 분석기라는 모듈을 사용하는 것을 자주 발 수 있습니다.

이처럼 Tokenizer를 만들 때 언어학적 지식을 최대한 동원해서 가장 효과적인 Token을 정의할 수 있습니다. 그런데 이 방식에는 단점들이 있습니다. 바로 확장성이 부족하다는 점입니다.

한국어는 형태소라는 단위가 최적일 수 있지만 영어, 중국어, 일본어는 아닐 수 있습니다. 영어는 영어의 특성을 고려하고, 중국어는 중국어의 특성을 고려해서 최적 Token을 정의해야 합니다. 그리고 이에 따라 언어별 Tokenizer를 만들어야 합니다.

새로운 언어를 위한 Tokenizer를 만든다고 해보죠. 일단 그 언어의 언어학 전문가를 찾아야 합니다. 그리고 언어학 전문가가 판단한 최적 Token을 위한 Tokenizer를 만들어야 합니다. Tokenizer 자체도 또 다른 머신러닝 모델인 경우가 많기 때문에 훈련 데이터를 만들어 모델을 훈련해야 합니다. 실제로 한국어 형태소 분석기를 만들기 위해서 역사적으로 여러 머신 러닝 기법들이 쓰였습니다.

또 다른 문제도 있습니다. 다국어 지원이 어렵다는 점입니다. ChatGPT를 써보시면 ChatGPT가 여러 언어를 이해하고 생성할 수 있다는 것을 쉽게 알 수 있습니다. ChatGPT가 여러 언어로 된 Token을 이해하고, 여러 언어로 된 Token을 생성할 수 있다는 뜻입니다.

언어별로 최적의 Tokenizer를 만들었다고 가정해 보겠습니다. 그럼 LLM은 어떻게 다국어 입력을 처리할 수 있을까요? 한 가지 방법은 LLM 전에 언어 감지 모듈을 돌리는 것입니다. 입력이 한국어라고 판단되면 한국어 Tokenizer를 돌리고, 영어라고 판단되면 영어 Tokenizer를 돌립니다.

이 방법에는 몇 가지 문제가 있지만 가장 큰 문제는 Mixed Language를 처리하지 못한다는 점입니다. 만약 입력이 "아침에 친구를 만나면 How are you? 라고 해보세요. 기분이 Happy 해집니다." 라면 한국어 Tokenizer를 사용해야 할까요, 영어 Tokenizer를 사용해야 할까요?

이 두 가지 문제를 해결하기 위해 새로운 접근 방법이 필요합니다. 난 언어학은 모르겠고 통계적으로 접근하겠다는 방법입니다.

통계적 접근

Tokenizer를 만들기 위해 언어학이 아닌 통계적인 접근을 사용하는 것은 그다지 놀라운 일이 아닙니다. 따지고 보면 모든 현대의 자연어처리를 통계적인 방법입니다.

통계적인 방법이라는 것은 무슨 뜻일까요? 여러 해석이 가능하겠지만 무엇인가를 결정할 때 확률적으로 결정하겠다는 해석이 대표적입니다. 예를 들어 "사과를"을 Token으로 나눈다면 ["사", "과", "를"] 이나 ["사과를"]보다 ["사과", "를"]으로 나누는 것이 더 좋은 Token일 확률이 높다고 판단하고 "사과를"을 ["사과", "를"]으로 나누는 방식입니다.

그럼 어떻게 ["사과", "를"]이 다른 방식보다 좋은 Token일 확률이 높다는 것을 알 수 있을까요? 바로 수많은 데이터로부터 배울 수 있습니다. 데이터를 봤더니 "사과를"을 ["사", "과", "를"] 이나 ["사과를"]로 나누는 경우보다 ["사과", "를"]으로 나누는 경우가 많다면 ["사과", "를"]이 더 좋은 (더 그럴듯한) Token 나누기라고 가정합니다.

그런데 이 방식에는 한 가지 문제가 있습니다. 누군가는 "사과를"이 어떻게 나뉘는지 정답을 제시해줘야 합니다. 이렇게 정답을 주고 학습시키는 방식을 Supervised Learning이라고 합니다. 언어학 전문가들을 동원해서 Tokenizer를 일일이 만드는 것보다 나을지는 모르지만 여전히 확장성이 좋다고 말하기는 어렵습니다.

하지만 꼭 정답이 있어야만 통계적 Tokenizer를 만들 수 있는 것은 아닙니다. 정답이 없더라도 텍스트만 가지고 Tokenizer를 만들 수 있는 Unsupervised Learning 기법이 있습니다. 가장 유명한 방법은 BPE(Byte Pair Encoding)라는 방식입니다. 요즘 대부분은 약간의 변형은 있지만 기본적으로 BPE 방식의 Tokenzier를 쓴다고 보셔도 됩니다.

BPE가 LLM 개발의 핵심적인 기술인 것은 맞지만 사실 BPE가 처음 만들어진 것은 1994년입니다. LLM이라는 개념이 생기기 훨씬 전의 일이죠.

BPE를 자세하게 다루기에는 글이 너무 길어지기 때문에 간단히 개념만 소개하려고 합니다. BPE는 간단히 설명하면 전체 텍스트에서 가장 빈번하게 나타나는 조합을 Token으로 간주하는 방식입니다. 예를 들어 입력으로 아래와 같은 텍스트가 주어졌다고 가정해 보겠습니다.

나는 항상 사과를 먹었다. 너도 사과를 먹었지. 사과는 항상 맛있어.

BPE는 가장 작은 단위(보통 글자지만 더 작을 수도 있습니다)에서 시작해 여러 단계를 반복하면서 자주 나타나는 조합을 찾아냅니다. 이 경우에는 "사과"와 "항상"이 자주 나타나는 조합이라는 것을 파악하고 "사과"와 "항상"을 Token으로 간주합니다. "를", "는"은 다른 글자와 조합이 자주 나타나지 않기 때문에 아마도 단독 Token으로 남을 겁니다.

BPE의 가장 큰 장점은 Tokenizer를 만들기 위해서 정답이 필요하지 않다는 점입니다. 텍스트 데이터만 잔뜩 있으면 자동으로 Tokenizer를 만들 수 있습니다. 또 다른 장점은 언어를 가리지 않는다는 점입니다. 언어학적 지식 없이 단순히 글자와 Token 조합의 빈도만 세기 때문입니다. 같은 이유로 여러 언어가 섞여 있어도 문제가 없습니다.

하지만 BPE에는 단점도 있습니다. 언어학적인 특성을 무시하고 오로지 통계에만 의존한다는 점입니다. 앞의 예에서는 "먹었"이라는 조합이 자주 나타나기 때문에 BPE Tokenizer에는 "먹었"이라는 Token이 존재할 것입니다. 하지만 언어학적으로는 먹다의 어간인 "먹"과 과거형을 나타내는 "었"으로 분리하는 것이 더 좋습니다. 실제로 BPE Tokenizer보다 언어학적 특성을 살린 Tokenizer를 썼을 때 성능이 더 좋다고 합니다.

하지만 BPE가 주는 장점이 단점을 무시할 만큼 크기 때문에 LLM 계에서 Tokenizer는 BPE 또는 BPE의 변형으로 통일되는 추세입니다.

마무리

오늘은 LLM에서 자주 등장하는 용어인 Token에 대해서 이야기해 보았습니다.

  • Token 수와 글자 수의 관계
  • 왜 Token이라는 개념이 등장했나
  • Token을 정하는 방식
  • Tokenizer를 만드는 방식

등에 대해서 알아보았습니다. Token에 대한 이해를 통해 자연어처리에 조금 더 친숙해졌으면 하는 바람입니다.

'Deep Learning' 카테고리의 다른 글

LLM : On-Device LLM  (0) 2024.08.29
LLM : Context Window  (0) 2024.05.25
LLM : RAG  (3) 2024.04.28
LLM : Fine Tuning & LoRA  (3) 2023.11.12
Cross Entropy 이야기  (0) 2023.05.21