최근 LLM 관련 교육을 듣고 있는데요. 이 과정에서 흥미가 가는 부분을 학습했고, 이를 정리하기 위해 글을 작성하게 되었습니다.
1. Tokenization: 텍스트를 조각으로 분리
Tokenization은 텍스트를 처리 가능한 단위(Token)로 쪼개는 과정입니다. 사람은 문장을 단어 단위로 인식하지만, 컴퓨터에게 “단어”라는 개념은 자명하지 않습니다. 어디서 끊을지를 명시적으로 정해줘야 합니다.
토큰화 방식은 단어 단위, 글자 단위, 서브워드(BPE), WordPiece, Unigram 순으로 발전했는데요. 각 과정이 어떻게 발전 했는지, 어떤 문제가 있었는지, 어떻게 해결했는지에 대해 살펴보겠습니다.
- 단어 단위 (Word-level)
- 글자 단위 (Character-level)
- BPE (Byte Pair Encoding, 2015)
- WordPiece
- Unigram (2018)
1-1. 단어 단위 토큰화
가장 직관적인 방법은 공백이나 구두점을 기준으로 텍스트를 단어 단위로 나누는 것입니다. 사람이 텍스트를 읽을 때 자연스럽게 하는 것과 같은 방식입니다. 사전에 사용할 단어를 전부 등록해두고, 텍스트를 공백으로 잘라서 사전에서 찾습니다. 사전에 있으면 해당 토큰으로 인식하고, 없으면 [UNK](Unknown) 토큰으로 처리합니다. 초기 NLP 시스템에서는 직관적이기 때문에 구현도 간단했기 때문에 이 방식을 사용했습니다.
1
2
3
4
5
"I love machine learning"
→ ["I", "love", "machine", "learning"]
"고양이가 소파에서 잔다"
→ ["고양이가", "소파에서", "잔다"]
하지만 실제로 쓰면 문제가 드러납니다. 세상에 존재하는 모든 단어를 사전에 등록해야 합니다. 영어만 해도 수십만 단어가 있고, 신조어나 고유명사는 계속 생겨납니다. 사전에 없는 단어는 전부 [UNK]가 되어 의미 정보가 완전히 사라집니다. 어형 변화(“run”, “running”, “runs”)도 각각 별개의 단어로 취급되어 같은 의미를 공유하지 못합니다. 한국어처럼 교착어는 더 심합니다. “먹었다”, “먹겠다”, “ 먹었었다”를 전부 다른 단어로 등록해야 합니다.
사전 크기 폭발: 영어만 해도 수십만 단어, 한국어는 조합이 거의 무한OOV 문제: “ChatGPT” 같은 신조어 → [UNK]으로 처리 → 의미 손실어형 변화: “play”, “plays”, “played”, “playing” → 4개의 별개 토큰
1-2. 글자 단위 토큰화
단어 단위의 문제는 사전 크기 폭발과 OOV 였는데, 이를 극복하기 위해 가장 작은 단위인 글자로 쪼개는 시도가 나왔습니다. 사전 크기는 영어 기준 알파벳 26개 + 숫자 + 특수 문자 로, 수십만 개였던 사전이 100개 이하로 줄어듭니다. OOV 문제도 완전히 사라집니다. 어떤 단어든 글자의 조합이니까요. 글자 단위로 분해하면 사전에 없는 토큰이 나올 수가 없겠죠.
1
2
3
"unhappiness"
→ ["u", "n", "h", "a", "p", "p", "i", "n", "e", "s", "s"]
← 11개 토큰
하지만 또 다른 문제가 생기는데, 시퀀스가 너무 길어진다는 점입니다. “unhappiness”가 단어 단위면 1개 토큰인데, 글자 단위면 11개 토큰이 됩니다. 문장 하나가 수백 개의 토큰이 되고, 문서 전체는 수만 개로 폭증합니다. Transformer의 Attention은 토큰 수의 제곱에 비례하는 연산량이 필요하므로, 토큰이 많아지면 학습과 추론이 급격히 느려집니다.
1
2
3
4
5
6
7
토큰 수(n) 연산량(n²)
──────────────────────────
10 100
50 2,500
100 10,000 ■
500 250,000 ■■■■■■■■■■
1000 1,000,000 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
1
2
3
4
같은 문장을 단어 단위 vs 글자 단위로 토큰화하면:
단어 단위: "unhappiness is real" → 3 토큰 → 연산량 9
글자 단위: "u n h a p p i n e s s i s r e a l"
→ 19 토큰 → 연산량 361 (약 40배)
더 근본적인 문제는 의미 정보의 손실 입니다. “u”, “n”, “h”, “a”… 각각의 글자는 의미를 거의 담고 있지 않습니다. 단어 단위에서는 “unhappiness” 하나만 봐도 의미를 알 수 있지만, 글자 단위에서는 모델이 11개의 글자 조합에서 “un-“이 부정 접두사라는 것, “happiness”가 하나의 의미 단위라는 것을 스스로 학습해야 합니다. 배울 수는 있지만, 그만큼 더 많은 학습 데이터와 모델 용량이 필요합니다.
시퀀스 폭발: 같은 문장이 5~10배 길어짐의미 손실: 개별 글자는 의미를 거의 안 담음장거리 의존성: 문장이 길어지면 앞뒤 관계 학습이 어려움
1-3. BPE
이를 극복하기 위해 2015년, BPE(Byte Pair Encoding)가 등장했습니다. 원래 데이터 압축 알고리즘이었는데, 자연어 처리에 적용되면서 토큰화의 표준이 되었습니다.
In computing, byte-pair encoding (BPE), or diagram coding, is an algorithm, first described in 1994 by Philip Gage, for encoding strings of text into smaller strings by creating and using a translation table.
핵심 아이디어는 글자에서 시작해서, 자주 붙어 나오는 쌍을 반복적으로 병합하는 것입니다. 글자 단위의 작은 사전에서 출발해서 점점 큰 단위를 만들어가는 상향식(bottom-up) 접근입니다. 학습 데이터를 통계적으로 분석해서 “어떤 글자 쌍이 가장 자주 연속으로 등장하는가”를 찾고, 그 쌍을 새로운 토큰으로 사전에 추가합니다. 이 과정을 원하는 사전 크기가 될 때까지 반복합니다.
1
2
3
4
5
6
7
8
9
10
11
12
초기 사전: [a, b, c, d, ..., z] ← 글자 단위에서 시작
Step 1: 학습 데이터에서 "t"+"h"가 가장 자주 붙어 나옴
→ 사전에 "th" 추가: [a, b, ..., z, th]
Step 2: "th"+"e"가 가장 자주 붙어 나옴
→ 사전에 "the" 추가: [a, b, ..., z, th, the]
Step 3: "i"+"n"+"g"가 자주 나옴
→ 사전에 "ing" 추가
... 원하는 사전 크기가 될 때까지 반복
이렇게 만들어진 사전으로 토큰화하면, 자주 쓰이는 단어는 통째로 하나의 토큰이 되고, 드문 단어는 더 작은 서브워드 조각의 조합으로 표현됩니다. “the”는 충분히 자주 나와서 하나의 토큰이지만, “unhappiness”는 “un” + “happiness”로 분리됩니다. 신조어도 기존 조각의 조합으로 표현할 수 있어서 OOV 문제가 사라집니다.
1
2
3
4
5
6
7
8
9
10
BPE로 토큰화한 결과
"unhappiness"
→ ["un", "happiness"] # 자주 쓰이는 접두사 + 단어
"tokenization"
→ ["token", "ization"] # 어근 + 접미사
"ChatGPT"
→ ["Chat", "G", "PT"] # 신조어도 기존 조각의 조합으로 표현
BPE의 병합 기준은 순수하게 빈도 입니다. 가장 많이 등장하는 쌍을 무조건 병합인데 이 단순함이 장점이자 한계입니다. “ab”가 100번 나오면 병합하지만, 그 병합이 모델의 학습에 실제로 도움이 되는지는 고려하지 않습니다. 빈도가 높다고 해서 반드시 의미적으로 좋은 분리인 것은 아닙니다.
1-4. WordPiece
WordPiece는 Google이 BERT에 사용한 방식입니다. BPE와 구조적으로 비슷합니다. 글자 단위에서 시작해서 점점 큰 단위로 병합해나가는 상향식(bottom-up) 접근이고, 사전 크기도 비슷합니다. 차이는 병합 기준 에 있습니다.
The tokenizer of BERT is WordPiece, which is a sub-word strategy like byte-pair encoding. Its vocabulary size is 30,000, and any token not appearing in its vocabulary is replaced by [UNK] (“unknown”).
BPE가 “가장 자주 등장하는 쌍”을 병합한다면, WordPiece는 “병합했을 때 학습 데이터의 우도(likelihood)를 가장 많이 높이는 쌍”을 병합합니다. 우도란 “현재 사전으로 학습 데이터를 얼마나 잘 설명할 수 있는가”를 수치화한 것입니다. 단순 빈도가 아니라, 그 병합이 전체 모델에 얼마나 이득인지를 따지는 것입니다.
1
2
3
4
5
6
7
8
9
10
BPE vs WordPiece 병합 기준
BPE:
"ab"가 100번, "cd"가 80번 등장
→ "ab"를 먼저 병합 (빈도가 높으니까)
WordPiece:
"ab"를 병합하면 전체 우도가 +5 증가
"cd"를 병합하면 전체 우도가 +8 증가
→ "cd"를 먼저 병합 (모델에 더 이득이니까)
실용적으로 결과가 크게 다르지는 않지만, WordPiece가 더 “모델 친화적인” 토큰 분리를 만드는 경향이 있습니다. WordPiece의 특징 중 하나는 단어 중간에서 분리된 토큰 앞에 ## 을 붙여서, 이 조각이 단어의 시작인지 중간인지를 구분합니다.
1
2
3
4
5
6
7
8
9
10
WordPiece 토큰화 예시 (BERT)
"unhappiness"
→ ["un", "##happi", "##ness"]
↑ 단어 시작 ↑ 단어 중간 (## 접두사)
"playing"
→ ["play", "##ing"]
# ##이 있으면 "이 토큰은 앞 토큰에 이어지는 조각이다"라는 의미
1-5. Unigram
Unigram은 2018년에 제안된 방식으로, 정반대 방향으로 접근합니다. 충분히 큰 사전에서 시작해서, 불필요한 토큰을 하나씩 제거해가는 하향식입니다.
BPE와 WordPiece는 둘 다 “작은 사전에서 시작해서 토큰을 추가”하는 상향식이었습니다.
1
2
3
4
5
BPE/WordPiece (상향식):
작은 사전 → 병합 → 병합 → ... → 목표 크기
Unigram (하향식):
큰 사전 → 제거 → 제거 → ... → 목표 크기
Unigram의 핵심 차이는 확률 기반 토큰화 입니다. 하나의 단어를 토큰으로 분리하는 방법이 여러 가지 있을 때, BPE는 학습된 병합 순서에 따라 결정론적으로 하나만 선택합니다. Unigram은 각 분리 방법에 확률을 부여하고, 가장 확률이 높은 방법을 선택합니다.
1
2
3
4
5
6
7
# Unigram은 각 토큰의 출현 확률을 학습해서 전체 확률이 가장 높은 분리를 선택
"unhappiness"를 분리하는 여러 가지 방법
분리 1: ["un", "happiness"] → 확률 0.45 ← 선택
분리 2: ["un", "happi", "ness"] → 확률 0.30
분리 3: ["unhappi", "ness"] → 확률 0.15
분리 4: ["u", "n", "h", "a", ...] → 확률 0.001
사전 구축 과정에서는 먼저 충분히 큰 후보 사전을 만든 뒤, 각 토큰을 제거했을 때 전체 학습 데이터의 우도 감소량을 계산합니다. 우도를 가장 적게 감소시키는 토큰, 즉 “있어도 없어도 별 차이 없는” 토큰부터 제거합니다. Google의 SentencePiece 라이브러리가 Unigram을 구현한 대표적인 도구이고, T5, LLaMA, ALBERT 등이 이 방식을 사용합니다. SentencePiece는 공백도 특수 문자(▁)로 처리해서 언어에 무관하게 동작 합니다.
1
2
3
4
5
6
7
8
SentencePiece (Unigram) 토큰화 예시
"I love cats"
→ ["▁I", "▁love", "▁cat", "s"]
↑ ▁는 공백을 의미 (단어 시작 표시)
# 공백을 별도 처리하지 않으므로 한국어, 일본어 등
# 공백이 없거나 규칙이 다른 언어에도 바로 적용 가능
2. Embedding: 토큰을 의미 있는 숫자로 바꾸기
토큰화가 끝나면 각 토큰을 숫자로 변환해야 합니다. 이 과정은 두 단계를 거칩니다. 먼저 각 토큰에 고유한 정수 ID를 부여하고(Token ID), 그 ID를 고차원 벡터로 변환합니다(Embedding).
2-1. Token ID: 사전에서의 위치
첫 번째 단계는 단순합니다. 사전에 있는 모든 토큰에 0번부터 순서대로 번호를 매기고, 입력 토큰을 사전에서 찾아 해당 번호를 반환합니다. 이는 배열 인덱스와 같습니다.
1
2
3
"the cat sat on the mat"
→ Tokenization → ["the", "cat", "sat", "on", "the", "mat"]
→ Token ID → [1, 42, 87, 15, 1, 63]
하지만 이 정수 ID는 의미 정보를 전혀 담고 있지 않습니다. 42(cat)와 43(car)은 숫자상 가깝지만 의미는 다르고, 42(cat)와 8721(kitten)은 숫자상 멀지만 의미는 가깝습니다. 정수 ID는 사전에서의 위치일 뿐, 토큰 간의 관계를 표현할 수 없습니다. 컴퓨터가 “cat과 kitten은 비슷하다”는 것을 알려면, 단순한 번호가 아니라 의미를 담은 숫자 표현이 필요합니다. 이것이 Embedding입니다.
1
2
3
4
5
6
7
Token ID는 의미를 담지 않는다
ID 토큰 숫자 거리 의미 거리
───────────────────────────────────────────
42 cat │ │
43 car 1 (가까움) 멀다 (동물 vs 탈것)
8721 kitten 8679 (멀음) 가깝다 (둘 다 고양이)
2-2. Embedding: ID를 벡터로 변환
Embedding은 Token ID를 고차원 벡터 로 변환하는 과정입니다. 정수 하나를 수백 ~ 수천 개의 실수로 이루어진 배열로 바꾸는 것입니다.
구조는 단순합니다. 거대한 행렬(matrix) 하나가 있습니다. 사전 크기가 50,000이고 벡터 차원이 768이면, 50,000 x 768 크기의 행렬입니다. Token ID가 입력되면 해당 행을 꺼냅니다. ID 42가 들어오면 42번째 행을 반환합니다. 그게 “cat”의 벡터입니다. 연산이라기보다는 테이블 lookup에 가깝습니다.
1
2
3
4
5
6
7
Embedding Matrix (50,000 × 768)
dim_0 dim_1 dim_2 ... dim_767
ID 0 [ 0.01, 0.23, -0.45, ..., 0.67 ]
ID 1 [ 0.21, -0.05, 0.87, ..., 0.12 ] ← "the"
...
ID 42 [ 0.21, -0.05, 0.87, ..., 0.12 ] ← "cat"
처음에는 랜덤 으로 초기화됩니다. 모든 토큰의 벡터가 무작위 숫자입니다. 이 상태에서 모델이 대량의 텍스트를 학습하면서, 벡터 값이 조금씩 조정됩니다. 학습의 원리는 이렇습니다. 모델이 “the cat sat on the ___“에서 다음 단어를 예측하는 과제를 반복하면, “cat”과 “dog”은 비슷한 문맥에서 등장하므로 비슷한 방향으로 벡터가 업데이트됩니다. 수십억 개의 문장을 학습하면, 의미적으로 비슷한 토큰이 비슷한 벡터를 갖게 됩니다. 이 과정이 끝나면 벡터 간의 거리가 의미적 유사도를 반영합니다.
1
2
3
학습 전: 랜덤 벡터 → 관계 없음
cat → [0.52, -0.91, 0.03, ...]
kitten → [0.17, 0.44, 0.78, ...]
1
2
3
4
학습 후: 의미가 벡터에 인코딩됨
cat → [0.21, -0.05, 0.87, ...]
kitten → [0.19, -0.03, 0.85, ...] ← 가까움
car → [-0.45, 0.72, 0.11, ...] ← 멀음
2-3. 왜 고차원인가
차원이 많을수록, 단어가 가진 여러 의미 속성을 동시에 독립적으로 표현할 수 있기 때문입니다. GPT-3는 12,288차원, BERT는 768차원의 벡터를 사용합니다. 단어의 의미는 하나의 측면이 아니라 여러 측면을 동시에 가지고 있습니다. “고양이”는 동물이면서, 포유류이면서, 작은 크기이면서, 가축이면서, 야행성입니다. 이 속성들을 모두 표현하려면 각 속성을 담당하는 축이 필요하고, 축이 곧 차원입니다.
1
2
3
4
5
6
7
8
9
10
# 각 관계가 서로 다른 축에서 독립적으로 표현됨
768차원: 축이 768개 → 관계마다 다른 축 사용
[반려동물 축] 고양이 ● ── ● 강아지 (가까움)
──────────── ● 호랑이 (멀음)
[고양이과 축] 고양이 ● ── ● 호랑이 (가까움)
──────────── ● 강아지 (멀음)
[크기 축] 고양이 ● ── ● 강아지 (비슷)
──────────── ● 호랑이 (멀음)
2차원 평면에서 “고양이”와 “강아지”를 가깝게 놓으면서, 동시에 “고양이”와 “호랑이”도 가깝게 놓아야 합니다. x축 하나, y축 하나로는 이 관계를 동시에 만족시킬 수 없습니다. 768차원이면 768개의 독립적인 축이 있어서, 한 축은 “반려동물 여부”, 다른 축은 “고양이과 여부”, 또 다른 축은 “크기”를 담당할 수 있습니다. 각 단어는 이 축들 위의 좌표로 표현되고, 어떤 축에서 가깝고 어떤 축에서 먼지에 따라 관계가 정의됩니다.
1
2
3
4
5
6
2차원: 축이 2개뿐 → 여러 관계를 동시에 표현 불가
y
│ 고양이 ● ● 강아지
│ ● 호랑이 ← "고양이과"로 가깝게 놓으면
│ "반려동물"로 멀어짐. 둘 다 만족 불가
└────────────── x
다만 각 차원이 사람이 해석할 수 있는 명확한 의미를 갖는 것은 아닙니다. 이는 모델이 학습 과정에서 스스로 발견한 추상적인 축이고, 여러 차원이 조합되어 하나의 개념을 표현하기 때문 입니다. 중요한 것은 차원이 충분히 많으면, 수만 개의 토큰이 각자 고유한 위치를 갖고 의미적 관계를 유지할 수 있는 공간이 만들어진다는 것입니다.
3. Cosine Similarity: 벡터 간 유사도 측정
코사인 유사도(Cosine Similarity)는 두 벡터가 얼마나 같은 방향을 가리키는지를 측정하는 방법입니다. 벡터의 크기(길이)는 무시하고 방향만 비교 합니다. Embedding 벡터 간의 의미적 유사도를 측정할 때 가장 널리 쓰이는 방식입니다.
3-1. 왜 코사인인가
두 점 사이의 거리를 재는 방법으로는 유클리드 거리(직선 거리)가 가장 직관적입니다. 일상에서 “두 점이 얼마나 가까운가”를 말할 때 쓰는 그 거리입니다. 하지만 Embedding 벡터에서는 유클리드 거리가 의미 유사도를 제대로 반영하지 못하는 경우가 있습니다.
같은 의미의 텍스트라도 문서 길이나 빈도에 따라 벡터의 크기가 달라질 수 있습니다. 유클리드 거리는 이 크기 차이에 민감합니다. 의미는 같은데 크기만 다른 두 벡터를 “멀다”고 판단해버립니다. 코사인 유사도는 크기를 무시하고 방향만 보기 때문에, 같은 방향을 가리키면 크기와 관계없이 유사도가 1입니다.
1
2
3
4
5
A = [1, 2, 3]
B = [2, 4, 6] # A와 같은 방향, 크기만 2배
유클리드 거리: √((2-1)² + (4-2)² + (6-3)²) = √14 ≈ 3.74 → "멀다"
코사인 유사도: 1.0 → "같다"
3-2. 어떻게 계산하는가
수학적으로는 두 벡터의 내적을 각 벡터의 크기(노름)로 나눈 값입니다. 기하학적으로는 두 벡터가 이루는 각도(θ)의 코사인 값입니다. 두 벡터가 완전히 같은 방향이면 θ = 0°이고 cos(0°) = 1, 직교하면 θ = 90°이고 cos(90°) = 0, 정반대면 θ = 180°이고 cos(180°) = -1입니다.
1
2
3
4
5
6
7
코사인 유사도 = cos(θ) = (A · B) / (|A| × |B|)
│ 결과 범위: -1 ~ 1
│
├─ 1: 같은 방향 (θ = 0°) → 의미가 같음
├─ 0: 직교 (θ = 90°) → 관계 없음
└─ -1: 반대 방향 (θ = 180°) → 의미가 반대
3-3. 벡터 연산: Embedding이 관계를 인코딩하는 증거
Embedding이 의미를 잘 학습하면, 벡터 간의 연산으로 의미적 관계를 표현할 수 있습니다. 2013년 Word2Vec에서 처음 발견된 현상입니다. “king”의 벡터에서 “man”의 벡터를 빼고 “woman”의 벡터를 더하면 “queen” 근처의 벡터가 됩니다.
이것이 가능한 이유는 “king → queen”의 벡터 차이와 “man → woman”의 벡터 차이가 거의 같기 때문입니다. “성별”이라는 의미 축이 벡터 공간에 하나의 방향으로 인코딩되어 있고, 그 방향을 더하거나 빼면 성별만 바뀌는 것입니다. 이런 벡터 연산이 성립한다는 것은 Embedding이 단순히 단어를 구분하는 것이 아니라, 단어 간의 관계까지 구조적으로 인코딩하고 있다는 증거입니다.
1
2
3
4
5
king - man + woman ≈ queen
Paris - France + Japan ≈ Tokyo
→ "성별", "수도"라는 관계가 벡터 공간의 방향으로 인코딩됨
→ 그 방향을 더하고 빼면 관계를 이동할 수 있음
3-4. 시맨틱 검색에서의 활용
코사인 유사도가 실제로 활용되는 대표적인 예가 시맨틱 검색입니다. 기존 키워드 검색은 질문과 문서에 같은 단어가 있어야 매칭됩니다. 시맨틱 검색은 질문과 문서를 각각 벡터로 변환한 뒤 코사인 유사도를 계산해서, 의미적으로 가까운 문서를 찾습니다.
“고양이 밥 주는 시간”을 검색하면, “고양이 사료 급여 권장 시간”이라는 문서는 겹치는 단어가 “고양이”, “시간” 정도뿐이지만 벡터 공간에서 가깝기 때문에 상위에 노출됩니다. “반려묘 영양 급여 스케줄”이라는 문서는 겹치는 단어가 하나도 없지만, 의미적으로 유사하기 때문에 역시 검색됩니다. 이것이 키워드 매칭으로는 불가능하고, 벡터 검색으로만 가능한 것입니다.
1
2
3
4
5
6
7
8
9
10
검색어: "고양이 밥 주는 시간" → 벡터 변환 → [0.42, -0.11, 0.78, ...]
키워드 매칭 (TF-IDF/BM25):
"고양이 사료 급여 권장 시간" → "고양이", "시간" 겹침 → ✅
"반려묘 영양 급여 스케줄" → 겹치는 단어 없음 → ❌
벡터 검색 (Cosine Similarity):
"고양이 사료 급여 권장 시간" → cos = 0.91 → ✅
"반려묘 영양 급여 스케줄" → cos = 0.87 → ✅ (의미적으로 유사)
"자동차 정비 일정" → cos = 0.08 → ❌
4. Transformer
Transformer는 입력된 벡터 시퀀스를 받아서, 토큰 간의 관계를 계산하고, 문맥이 반영된 새로운 벡터 시퀀스를 출력하는 모델 구조입니다.
2017년 Google이 “Attention Is All You Need” 논문에서 제안했고, GPT, BERT, T5, LLaMA 등 현대 LLM은 전부 이 구조를 기반으로 합니다. 앞에서 텍스트를 토큰으로 쪼개고, 토큰을 정수로 매핑하고, 정수를 벡터로 변환하는 과정을 살펴봤는데, 이 벡터들이 최종적으로 전달되는 곳이 Transformer입니다.
Transformer 이전에는 RNN(Recurrent Neural Network)이 텍스트 처리의 표준이었습니다. RNN은 토큰을 하나씩 순서대로 처리해야 했기 때문에 병렬화가 안 되고, 문장이 길어지면 앞쪽 정보를 잊어버리는 문제가 있었습니다. Transformer는 이 두 문제를 Attention 메커니즘으로 해결했습니다.
4-1. Attention: 토큰 간의 관계를 계산
Attention의 핵심 아이디어는 “모든 토큰이 다른 모든 토큰을 직접 참조할 수 있게 하자”입니다. RNN처럼 순서대로 하나씩 넘기는 게 아니라, 문장 전체를 한 번에 보고 각 토큰이 다른 토큰과 얼마나 관련 있는지를 계산합니다.
“나는 고양이를 좋아한다”에서 “좋아한다”가 무엇을 좋아하는지를 알려면, “고양이를”에 높은 가중치를 줘야 합니다. Attention은 이 가중치를 학습으로 결정합니다.
1
2
3
4
5
6
7
8
"나는 고양이를 좋아한다"
"좋아한다"가 다른 토큰에 주는 Attention 가중치:
나는 → 0.10 (주어이긴 하지만 직접 관련은 낮음)
고양이를 → 0.75 (좋아하는 대상 → 높은 가중치)
를 → 0.05 (조사 → 낮음)
좋아 → 0.08
한다 → 0.02
Embedding 단계에서 받은 벡터는 문맥을 모르는 정적 벡터였습니다. “배가 고프다”의 “배”와 “배를 타다”의 “배”가 같은 벡터입니다. Attention을 거치면, 주변 토큰의 정보를 반영한 새로운 벡터로 변환됩니다. “고프다”와 함께 등장한 “배”는 신체 부위 방향으로, “타다”와 함께 등장한 “배”는 탈것 방향으로 벡터가 바뀝니다.
1
2
3
4
5
6
7
Embedding (정적, 문맥 무관):
"배가 고프다"의 "배" → [0.5, 0.3, ...]
"배를 타다"의 "배" → [0.5, 0.3, ...] ← 같은 벡터
Attention 통과 후 (동적, 문맥 반영):
"배가 고프다"의 "배" → [0.8, -0.2, ...] ← 신체 부위 방향
"배를 타다"의 "배" → [-0.1, 0.7, ...] ← 탈것 방향
4-2. Self-Attention의 동작 방식
Self-Attention은 각 토큰에서 세 가지 벡터를 만듭니다. Query(질문), Key(키), Value(값)입니다. 이 세 벡터는 Embedding 벡터에 학습된 가중치 행렬을 곱해서 만들어집니다.
Query는 “나는 어떤 정보가 필요한가”, Key는 “나는 어떤 정보를 가지고 있는가”, Value는 “내가 실제로 전달할 정보”입니다. Query와 Key의 내적으로 관련도를 계산하고, 그 관련도를 가중치로 사용해서 Value를 가중 합산합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Self-Attention 계산 과정
1. 각 토큰의 Embedding 벡터에서 Q, K, V 생성
"좋아한다" → Q_좋아 = W_q × embedding_좋아
K_좋아 = W_k × embedding_좋아
V_좋아 = W_v × embedding_좋아
2. Q와 모든 K의 내적 → 관련도 점수
score("좋아한다", "나는") = Q_좋아 · K_나는 = 2.1
score("좋아한다", "고양이를") = Q_좋아 · K_고양이를 = 8.5 ← 높음
score("좋아한다", "를") = Q_좋아 · K_를 = 0.3
3. Softmax로 정규화 → 가중치
[2.1, 8.5, 0.3, ...] → [0.10, 0.75, 0.05, ...]
4. 가중치 × V를 합산 → 새로운 벡터
output = 0.10 × V_나는 + 0.75 × V_고양이를 + 0.05 × V_를 + ...
이 과정이 모든 토큰에 대해 동시에(병렬로) 수행됩니다. RNN처럼 하나씩 순서대로 처리하지 않기 때문에 GPU에서 효율적으로 병렬 연산이 가능하고, 문장이 아무리 길어도 모든 토큰이 직접 참조할 수 있어서 장거리 의존성 문제가 사라집니다.
4-3. Multi-Head Attention
실제 Transformer에서는 Attention을 한 번만 하지 않고, 여러 번 병렬로 수행합니다. 이것이 Multi-Head Attention입니다.
하나의 Attention Head가 하나의 관계 유형을 학습한다고 생각하면 됩니다. Head 1은 “주어-동사” 관계에 집중하고, Head 2는 “동사-목적어” 관계에 집중하고, Head 3은 “수식어-피수식어” 관계에 집중하는 식입니다. 여러 Head의 결과를 합쳐서 다양한 관계를 동시에 반영한 벡터를 만듭니다.
1
2
3
4
5
6
7
8
9
10
11
12
Multi-Head Attention (8개 Head 예시)
입력: "나는 고양이를 좋아한다"의 Embedding 벡터들
Head 1: "좋아한다" → "고양이를"에 집중 (동사-목적어)
Head 2: "좋아한다" → "나는"에 집중 (동사-주어)
Head 3: "고양이를" → "좋아한다"에 집중 (무엇을 당하는가)
...
Head 8: 다른 관계 패턴
→ 8개 Head의 결과를 이어붙이고(concatenate) 선형 변환
→ 최종 벡터: 여러 관계가 동시에 반영됨
BERT-Base는 12개 Head, GPT-3는 96개 Head를 사용합니다. Head가 많을수록 더 다양한 관계 유형을 동시에 포착할 수 있습니다.
4-4. Transformer 블록의 구조
Attention만으로 Transformer가 완성되는 것은 아닙니다. 하나의 Transformer 블록은 Multi-Head Attention과 Feed-Forward Network(FFN)로 구성되고, 각각에 Residual Connection과 Layer Normalization이 적용됩니다. 이 블록을 여러 층 쌓아서 모델을 만듭니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Transformer 블록 1개의 구조
입력 벡터
│
├──→ Multi-Head Attention ──→ Add & LayerNorm ──┐
│ (토큰 간 관계) (잔차 연결) │
└───────────────────────────────────────────────┘
│
├──→ Feed-Forward Network ──→ Add & LayerNorm ──┐
│ (각 토큰 개별 변환) (잔차 연결) │
└───────────────────────────────────────────────┘
│
출력 벡터
이 블록을 N개 쌓음
BERT-Base: 12층
GPT-3: 96층
Feed-Forward Network는 Attention과 달리 각 토큰을 개별적으로 변환합니다. Attention이 “토큰 간의 관계”를 다뤘다면, FFN은 “각 토큰의 표현을 비선형 변환으로 풍부하게” 만드는 역할입니다. 세계 지식(factual knowledge)의 상당 부분이 FFN의 가중치에 저장되어 있다는 연구 결과도 있습니다.
Residual Connection(잔차 연결)은 입력을 출력에 더하는 것입니다. 층이 깊어지면 학습이 어려워지는 문제를 해결합니다. 입력이 그대로 전달되는 경로가 있으므로, 모델이 “변화량”만 학습하면 되기 때문입니다.
5. 참조
- Vaswani et al. - Attention Is All You Need (2017)
- Devlin et al. - BERT: Pre-training of Deep Bidirectional Transformers (2018)
- Brown et al. - Language Models are Few-Shot Learners (GPT-3, 2020)
- Sennrich et al. - Neural Machine Translation of Rare Words with Subword Units (BPE, 2015)
- Kudo - Subword Regularization (Unigram, 2018)
- Wikipedia - Byte pair encoding
- Wikipedia - WordPiece
- Wikipedia - Transformer (deep learning architecture)
- Jay Alammar - The Illustrated Transformer
- Jay Alammar - The Illustrated Word2vec
- OpenAI - Tokenizer
- Hugging Face - Tokenizers