
📓Google Colab에서 실습하기
이 레슨은 PyTorch/GPU가 필요합니다. 노트북을 다운로드 후 Google Colab에서 열어주세요.
학습 내용
Positional Encoding - 순서의 비밀
학습 목표
이 레슨을 완료하면:
- •Transformer에 위치 정보가 필요한 이유를 설명할 수 있다
- •사인/코사인 기반 Positional Encoding의 원리를 이해한다
- •왜 사인/코사인 함수가 위치 표현에 적합한지 설명할 수 있다
- •numpy와 matplotlib로 Positional Encoding을 직접 생성하고 시각화할 수 있다
- •학습 가능한 위치 임베딩과의 차이를 이해한다
핵심 메시지
"Transformer는 칠판에 적힌 글을 한눈에 보지만, 줄 번호가 없으면 순서를 모른다." Positional Encoding은 각 단어에 고유한 "줄 번호 도장"을 찍어주는 것입니다.
왜 위치 정보가 필요한가?
비유: 줄 번호 없는 칠판
비유: Transformer는 칠판에 적힌 모든 단어를 한꺼번에 봅니다. 하지만 줄 번호가 없으면 어떤 단어가 먼저 나왔는지 알 수 없습니다.
줄 번호가 없는 칠판: "물었다" "개를" "사람이"
이것은 아래 두 문장 중 어느 것일까요? A: "사람이 개를 물었다" B: "개를 사람이 물었다"
단어 집합은 동일하지만, 순서에 따라 의미가 완전히 달라집니다!
RNN vs Transformer의 위치 인식
| 모델 | 처리 방식 | 위치 정보 |
|---|---|---|
| RNN | 단어를 순서대로 하나씩 처리 | 자연스럽게 "이것은 3번째 단어"라는 정보가 포함됨<br/>별도의 위치 정보가 필요 없음 |
| Transformer | 모든 단어를 동시에 처리 | Self-Attention은 순서를 전혀 고려하지 않음<br/>"나는 너를 좋아한다"와 "너를 나는 좋아한다"가 동일하게 처리됨<br/>반드시 위치 정보를 별도로 추가해야 함! |
위치 정보 없이는 어떤 문제가 생기나?
위치 정보가 없으면 Self-Attention은 "Bag of Words"와 같아집니다:
"고양이가 쥐를 쫓았다" = "쥐를 고양이가 쫓았다" = "쫓았다 고양이가 쥐를"
모두 같은 단어 집합이므로 같은 결과가 나옵니다. 하지만 자연어에서 순서는 의미에 결정적인 영향을 줍니다!
간단한 해결책은 왜 안 될까?
시도 1: 정수 인덱스 사용
위치 = [0, 1, 2, 3, 4, ...]
문제점:
- •문장이 길어지면 값이 무한히 커짐 (0, 1, 2, ..., 10000)
- •단어 임베딩 벡터의 값은 보통 -1 ~ 1 범위
- •위치 값이 10000이면 임베딩 정보를 압도해 버림
- •학습이 매우 불안정해짐
시도 2: 정규화 (0~1로 맞추기)
5단어 문장: [0.0, 0.25, 0.5, 0.75, 1.0] 10단어 문장: [0.0, 0.11, 0.22, 0.33, ...]
문제점:
- •같은 "2번째 위치"가 문장 길이에 따라 다른 값을 가짐
- •5단어 문장의 2번째: 0.25
- •10단어 문장의 2번째: 0.11
- •모델이 일관된 위치 패턴을 학습하기 어려움
좋은 위치 인코딩의 조건
| 조건 | 설명 |
|---|---|
| 1️⃣ 제한된 범위 | 값의 범위가 제한적이어야 함 (임베딩과 비슷한 스케일) |
| 2️⃣ 고유성 | 각 위치마다 고유한 값을 가져야 함 |
| 3️⃣ 일관성 | 문장 길이에 상관없이 같은 위치는 같은 값이어야 함 |
| 4️⃣ 상대 거리 | 상대적 거리도 표현할 수 있으면 좋음 |
| 5️⃣ 일반화 | 학습하지 않은 긴 문장에도 적용 가능해야 함 |
Sinusoidal Positional Encoding
핵심 아이디어
사인(sin)과 코사인(cos) 함수를 서로 다른 주파수로 사용하여, 각 위치를 고유한 벡터로 표현합니다.
수학적 공식
변수 설명:
- •: 단어의 위치 (0, 1, 2, 3, ...)
- •: 차원 인덱스 (0, 1, 2, ..., )
- •: 임베딩 차원 (예: 512)
규칙:
- •짝수 차원(0, 2, 4, ...): sin 사용
- •홀수 차원(1, 3, 5, ...): cos 사용
직관적 이해: 이진수와의 유사성
비유: 이진수를 떠올려 보세요. 각 자릿수(비트)는 서로 다른 주기로 0과 1을 반복합니다.
이진수 카운팅:
위치 bit3 bit2 bit1 bit0
0 0 0 0 0
1 0 0 0 1 ← bit0: 매번 변함
2 0 0 1 0 ← bit1: 2번마다 변함
3 0 0 1 1
4 0 1 0 0 ← bit2: 4번마다 변함
5 0 1 0 1
6 0 1 1 0
7 0 1 1 1
Positional Encoding도 비슷합니다:
- •낮은 차원: 빠르게 변하는 sin/cos (bit0처럼)
- •높은 차원: 느리게 변하는 sin/cos (bit3처럼)
- •이 조합으로 각 위치가 고유한 패턴을 가짐!
주파수별 변화
| 차원 | 함수 | 변화 속도 |
|---|---|---|
| 차원 0-1 | sin(pos/1), cos(pos/1) | 매우 빠른 변화 ⚡ |
| 차원 2-3 | sin(pos/10), cos(pos/10) | 중간 변화 ⚙️ |
| 차원 4-5 | sin(pos/100), cos(pos/100) | 느린 변화 🐌 |
| 차원 6-7 | sin(pos/1000), cos(pos/1000) | 매우 느린 변화 🐢 |
| ... | ... | ... |
각 위치는 이 모든 주파수의 값을 조합한 고유한 벡터를 가집니다. 마치 각 위치에 고유한 "지문"을 부여하는 것과 같습니다!
실행해보기: Positional Encoding 생성과 시각화
pythonimport numpy as np import matplotlib.pyplot as plt def positional_encoding(max_len, d_model): """Sinusoidal Positional Encoding 생성""" PE = np.zeros((max_len, d_model)) for pos in range(max_len): for i in range(0, d_model, 2): freq = 1.0 / (10000 ** (i / d_model)) PE[pos, i] = np.sin(pos * freq) if i + 1 < d_model: PE[pos, i+1] = np.cos(pos * freq) return PE max_len = 50 d_model = 64 PE = positional_encoding(max_len, d_model) print(f"Positional Encoding 크기: {PE.shape}") print(f" - {max_len}개 위치 x {d_model}차원") print(f" - 값 범위: [{PE.min():.3f}, {PE.max():.3f}]") print(f" - sin/cos이므로 항상 [-1, 1] 범위!") print() for pos in range(3): print(f"위치 {pos}의 처음 8차원: {np.round(PE[pos, :8], 3)}") print() fig, axes = plt.subplots(2, 2, figsize=(14, 10)) ax1 = axes[0, 0] im = ax1.imshow(PE, aspect="auto", cmap="RdBu_r") ax1.set_xlabel("Dimension (i)") ax1.set_ylabel("Position (pos)") ax1.set_title("Positional Encoding Heatmap") plt.colorbar(im, ax=ax1) ax2 = axes[0, 1] positions = np.arange(max_len) for d in [0, 4, 8, 16, 32]: if d < d_model: ax2.plot(positions, PE[:, d], label=f"dim {d}") ax2.set_xlabel("Position") ax2.set_ylabel("Value") ax2.set_title("Dimension-wise Patterns (different frequencies)") ax2.legend() ax2.grid(True, alpha=0.3) ax3 = axes[1, 0] similarity = PE @ PE.T im3 = ax3.imshow(similarity, cmap="viridis") ax3.set_xlabel("Position") ax3.set_ylabel("Position") ax3.set_title("Position Similarity (dot product)") plt.colorbar(im3, ax=ax3) ax4 = axes[1, 1] for p in [0, 1, 2, 10, 25, 49]: ax4.plot(PE[p, :32], label=f"pos {p}", alpha=0.7) ax4.set_xlabel("Dimension") ax4.set_ylabel("Value") ax4.set_title("PE vectors at different positions (first 32 dims)") ax4.legend() ax4.grid(True, alpha=0.3) plt.tight_layout() plt.savefig("positional_encoding.png", dpi=100, bbox_inches="tight") plt.show() print("Positional Encoding 시각화 완료!") print() print("===== 위치 간 유사도 분석 =====") print() ref_pos = 0 print(f"위치 {ref_pos}과 다른 위치들의 유사도 (내적):") for p in [1, 2, 5, 10, 20, 49]: sim = np.dot(PE[ref_pos], PE[p]) print(f" 위치 {ref_pos} vs 위치 {p:>2}: {sim:>8.3f} (거리: {p - ref_pos})") print() print("가까운 위치일수록 유사도가 높고,") print("먼 위치일수록 유사도가 낮아지는 경향을 보입니다.") print("이것이 모델이 상대적 거리를 학습할 수 있게 해줍니다!")
Sinusoidal Encoding의 장점
1. 값이 항상 -1 ~ 1 사이
sin과 cos의 출력 범위: [-1, 1] 단어 임베딩의 값 범위: 보통 [-1, 1] 근처
→ 위치 정보와 단어 의미 정보의 스케일이 비슷 → 단순히 더해도(+) 한쪽이 다른 쪽을 압도하지 않음
final_input = word_embedding + positional_encoding (의미 정보) + (위치 정보)
2. 상대적 위치 표현 가능
수학적 특성: PE(pos + k)는 PE(pos)의 선형 변환으로 표현 가능
이것이 의미하는 바: "3칸 뒤의 단어"라는 상대적 관계를 Attention이 학습할 수 있음
언어에서 상대 위치가 중요한 예:
- •형용사는 보통 명사 바로 앞에 옴 (상대 거리 = 1)
- •주어와 동사 사이 거리는 다양하지만 관계가 있음
3. 길이 일반화 가능
학습 시: 최대 512 토큰까지만 봄 추론 시: 513번째 위치의 PE(513, i) = sin(513 / 10000^(2i/d_model)) → 수학적으로 자연스럽게 계산 가능!
학습하지 않은 긴 문장에도 적용할 수 있음 (다만 성능은 학습 범위를 크게 벗어나면 저하될 수 있음)
Positional Encoding 적용 방법
입력 처리 전체 과정
1. 토큰화:
"나는 사과를 먹었다" → [토큰1, 토큰2, 토큰3]
2. 단어 임베딩:
토큰1 → [0.2, -0.5, 0.8, ...] (d_model 차원 벡터)
토큰2 → [0.1, 0.3, -0.2, ...]
토큰3 → [-0.4, 0.7, 0.1, ...]
3. Positional Encoding 더하기:
위치0의 PE → [0.0, 1.0, 0.0, ...]
위치1의 PE → [0.84, 0.54, 0.01, ...]
위치2의 PE → [0.91, -0.42, 0.02, ...]
4. 최종 입력 = 임베딩 + PE:
토큰1 → [0.2+0.0, -0.5+1.0, 0.8+0.0, ...]
토큰2 → [0.1+0.84, 0.3+0.54, -0.2+0.01, ...]
토큰3 → [-0.4+0.91, 0.7-0.42, 0.1+0.02, ...]
→ 이제 각 토큰이 "의미 + 위치" 정보를 모두 담고 있습니다!
Sinusoidal vs 학습 가능한 위치 임베딩
두 가지 방식 비교
| 특성 | Sinusoidal (원본 Transformer) | Learned (BERT, GPT) |
|---|---|---|
| 파라미터 | 없음 (수학 공식으로 고정) | max_len x d_model 개 |
| 학습 | 학습 불필요 | 학습으로 최적화 |
| 길이 일반화 | 학습 범위 밖도 가능 | 학습 범위 내로 제한 |
| 성능 | 좋음 | 비슷 (약간 더 나을 수도) |
| 상대 위치 | 수학적으로 보장 | 학습으로 습득 |
학습 가능한 위치 임베딩 (Learned Positional Embedding):
- •위치별 벡터를 처음에 랜덤 초기화
- •학습 과정에서 최적의 위치 표현을 자동으로 배움
- •BERT: 최대 512 위치, GPT-2: 최대 1024 위치
최신 기법 (RoPE - Rotary Position Embedding):
- •LLaMA, ChatGPT 등 최신 모델에서 사용
- •회전 행렬을 이용한 상대적 위치 인코딩
- •길이 일반화 성능이 매우 좋음
PyTorch로 보는 Positional Encoding (참고용)
import torch
import math
class PositionalEncoding(torch.nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(
torch.arange(0, d_model, 2).float() *
(-math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer("pe", pe)
def forward(self, x):
return x + self.pe[:, :x.size(1), :]
핵심 요약
| 개념 | 설명 | 비유 |
|---|---|---|
| 위치 정보 필요성 | Transformer는 순서를 모름 | 줄 번호 없는 칠판 |
| Sinusoidal PE | sin/cos로 위치를 벡터로 변환 | 각 위치의 고유 지문 |
| 다양한 주파수 | 낮은 차원은 빠르게, 높은 차원은 느리게 변화 | 이진수의 비트와 유사 |
| 값 범위 | 항상 [-1, 1] | 임베딩과 같은 스케일 |
| 상대 위치 | 선형 변환으로 표현 가능 | 거리 관계 학습 가능 |
| 적용 방법 | 임베딩에 단순히 더함 | 의미 + 위치 결합 |
핵심 공식
학습 체크리스트
- • Transformer에 위치 정보가 필요한 이유를 설명할 수 있다
- • 정수 인덱스나 정규화가 왜 좋은 해결책이 아닌지 이해했다
- • sin/cos 공식에서 pos, i, d_model 각각의 의미를 안다
- • 다양한 주파수가 고유한 위치 패턴을 만드는 원리를 이해했다
- • matplotlib 시각화에서 위치별 패턴 차이를 확인했다
- • Sinusoidal과 Learned 방식의 장단점을 비교할 수 있다
- • Positional Encoding이 임베딩에 더해지는 과정을 설명할 수 있다
다음 레슨: Transformer 전체 구조를 조립합니다. Encoder-Decoder 아키텍처, Layer Normalization, Residual Connection을 배웁니다!
레슨 정보
- 레벨
- Level 7: Transformer & LLM 원리
- 예상 소요 시간
- 40분
- 참고 영상
- YouTube 링크
💡실습 환경 안내
이 레벨은 PyTorch/GPU가 필요하여 Google Colab 사용을 권장합니다.
Colab은 무료 GPU를 제공하여 PyTorch, CNN, Transformer 등을 실행할 수 있습니다.