Positional Encoding & Layer Normalization
Transformer가 "순서"를 인식하는 수학적 트릭, 그리고 깊은 신경망을 안정시키는 정규화의 비밀. 사인·코사인 파동에서 평균·분산 계산까지 — 공식을 코드와 시각화로 완전히 분해한다.
왜 위치 정보가
필요한가
Self-Attention은 모든 토큰을 동시에(병렬로) 처리한다. 덕분에 빠르지만, 치명적인 문제가 생긴다 — 입력의 순서를 전혀 모른다는 것이다.
"나는 AI를 공부한다"와 "AI를 나는 공부한다"는 Attention 연산에서 동일하게 처리된다. 단어 집합(bag of words)과 다를 바 없다. 언어는 순서가 의미이므로, 이는 치명적이다.
사인·코사인이 만드는
위치의 지문
원논문 "Attention Is All You Need"가 제안한 PE 공식은 우아하게 단순하다.
PE(pos, 2i+1) = cos( pos / 100002i/d_model )
- pos
- 시퀀스에서 토큰의 위치 (0, 1, 2, ... seq_len-1)
- i
- 임베딩 차원의 인덱스 (0, 1, 2, ... d_model/2-1)
- d_model
- 임베딩 벡터의 총 차원 수 (GPT-3: 12288, BERT-base: 768)
- 10000
- 파장 스케일 상수 — 이 값으로 저주파~고주파 다양한 파동 생성
이 공식은 각 위치(pos)에 대해 d_model 차원의 벡터를 생성한다. 짝수 차원(0, 2, 4...)에는 sin, 홀수 차원(1, 3, 5...)에는 cos을 배치한다. 핵심은 차원마다 서로 다른 주파수를 사용한다는 것이다 — i가 커질수록 파장이 길어진다(주파수가 낮아진다).
PE 행렬을
히트맵으로 보다
실제 PE 행렬 [seq_len × d_model]을 2D 히트맵으로 시각화하면 그 구조가 아름답게 드러난다. 가로축(x)은 임베딩 차원, 세로축(y)은 시퀀스 위치다.
sin·cos를 선택한
4가지 이유
유계성 (Boundedness) — 값이 항상 [-1, 1] 범위
sin과 cos은 항상 -1에서 1 사이값을 가진다. 이는 임베딩 벡터와 더할 때 값의 폭발 없이 안정적인 덧셈이 가능하다. 랜덤 초기화나 지수 함수를 쓰면 값이 폭발하거나 소멸할 수 있다.
결정론적 고유성 — 모든 위치가 유일한 패턴
어떤 두 위치 pos₁ ≠ pos₂에 대해서도, 생성되는 PE 벡터는 다르다. 수백 개의 차원(sin·cos 쌍)이 복합적으로 조합되므로 충돌(collision) 없이 각 위치를 구분한다. 동시에 파라미터 없이 공식으로만 생성되므로 추가 학습 비용이 없다.
상대 위치 표현 — 선형 변환으로 오프셋 표현 가능
sin·cos의 삼각함수 덧셈 공식: sin(α+β) = sin(α)cos(β) + cos(α)sin(β). 이 성질 덕분에 PE(pos+k)를 PE(pos)의 선형 변환으로 표현할 수 있다. Attention이 상대적 위치 관계를 학습하는 데 유리하다.
길이 외삽 (Extrapolation) — 훈련보다 긴 시퀀스도 처리
공식 기반이므로 훈련 중 보지 못한 위치(예: 훈련 max=512인데 추론 시 pos=600)도 수식으로 계산 가능하다. 물론 성능은 저하될 수 있지만, 학습형 PE는 아예 처리 불가다. 이것이 RoPE(Rotary PE)가 등장한 배경이기도 하다.
고정 PE vs
학습형 PE 비교
| 구분 | 고정형 (Sinusoidal PE) | 학습형 (Learned PE) | RoPE / ALiBi |
|---|---|---|---|
| 원리 | sin/cos 공식으로 결정론적 생성 | 위치 임베딩 파라미터를 데이터에서 학습 | 회전 행렬 / 어텐션 편향으로 상대 위치 인코딩 |
| 파라미터 | 0개 추가 없음 | max_len × d_model개 추가 | 최소 또는 0개 |
| 외삽성 | 이론적 가능 (성능 저하) | 불가 훈련 길이 초과 시 오류 | 우수 특히 ALiBi |
| 채택 모델 | 원본 Transformer, BERT(초기) | BERT, GPT-2, GPT-3 | LLaMA, Mistral, Falcon (RoPE) |
| 성능 | 실용적으로 학습형과 유사 | 특정 태스크에서 약간 우수 | 긴 컨텍스트에서 최우수 |
왜 정규화가
필요한가
깊은 신경망(예: 96층 GPT-3)을 학습할 때 나타나는 두 가지 고질적 문제가 있다. 기울기 폭발(Gradient Explosion)과 기울기 소실(Gradient Vanishing)이다.
Layer Normalization의
수학적 원리
② 분산: σ² = (1/H) · Σᵢ (xᵢ - μ)²
③ 정규화: x̂ᵢ = (xᵢ - μ) / √(σ² + ε)
④ 스케일: LN(x)ᵢ = γ · x̂ᵢ + β
- H
- 레이어 내 뉴런(피처) 수 — d_model (예: 768, 4096)
- μ
- 해당 레이어 입력 벡터의 평균 — 스칼라값
- σ²
- 해당 레이어 입력 벡터의 분산 — 스칼라값
- ε
- 수치 안정성을 위한 작은 상수 (보통 1e-5 또는 1e-6)
- γ, β
- 학습 가능한 스케일·이동 파라미터 (각 H개) — 모델이 최적 분포 스스로 결정
Layer Norm의 핵심은 같은 레이어 내 모든 피처에 걸쳐 정규화한다는 것이다. 평균을 빼고(centering), 분산으로 나눠(scaling) 분포를 표준화한다. 이후 학습 파라미터 γ(감마)와 β(베타)로 모델이 원하는 스케일과 이동을 다시 자유롭게 결정하게 한다.
Layer Norm을
직접 체험하다
입력 벡터의 분포를 직접 조절하고, Layer Normalization이 어떻게 표준화하는지 실시간으로 확인하세요.
Batch Norm vs
Layer Norm — 차원의 차이
Layer Norm 이전에는 Batch Normalization(BN)이 표준이었다. 두 방식의 핵심 차이는 "무엇에 걸쳐 정규화하느냐"다.
| 구분 | Batch Normalization | Layer Normalization |
|---|---|---|
| 정규화 축 | 배치 차원 (N) + 공간 차원 | 피처 차원 (H) — 레이어 내부 |
| 통계량 계산 | 같은 피처의 배치 전체 평균/분산 | 같은 샘플의 모든 피처 평균/분산 |
| 배치 의존성 | 강함 배치 크기가 작으면 불안정 | 없음 샘플 1개도 처리 가능 |
| 추론 시 동작 | 이동평균 통계량 사용 (학습-추론 불일치) | 동일한 방식 (학습=추론) |
| RNN/Transformer 적합성 | 나쁨 시퀀스 길이 변화에 취약 | 최적 토큰별 독립 정규화 |
| 주요 사용처 | CNN 이미지 처리 (ResNet, VGG) | Transformer, RNN, BERT, GPT |
코드로 완성하는
PE & LN 구현
import torch
import torch.nn as nn
import math
# ════════════════════════════════════════════════════
# Part A: Positional Encoding (고정형 Sinusoidal)
# PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
# PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
# ════════════════════════════════════════════════════
class SinusoidalPositionalEncoding(nn.Module):
def __init__(self, d_model: int, max_len: int = 5000, dropout: float = 0.1):
super().__init__()
self.dropout = nn.Dropout(dropout)
# PE 행렬 사전 계산: [max_len, d_model]
pe = torch.zeros(max_len, d_model)
# pos: [max_len, 1] — 위치 벡터
pos = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# 분모 계산: 10000^(2i/d_model) → 로그 공간에서 안정적 계산
div_term = torch.exp(
torch.arange(0, d_model, 2, dtype=torch.float) *
-(math.log(10000.0) / d_model)
)
# 짝수 차원 = sin, 홀수 차원 = cos
pe[:, 0::2] = torch.sin(pos * div_term) # [max_len, d_model/2]
pe[:, 1::2] = torch.cos(pos * div_term) # [max_len, d_model/2]
# buffer로 등록 (학습 파라미터 X, 저장은 O)
self.register_buffer('pe', pe.unsqueeze(0)) # [1, max_len, d_model]
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: [batch, seq_len, d_model]
x = x + self.pe[:, :x.size(1), :] # 임베딩에 PE 더하기
return self.dropout(x)
# ════════════════════════════════════════════════════
# Part B: Layer Normalization (수식 직접 구현)
# LN(x) = γ · (x - μ) / √(σ² + ε) + β
# ════════════════════════════════════════════════════
class LayerNorm(nn.Module):
def __init__(self, d_model: int, eps: float = 1e-6):
super().__init__()
self.eps = eps
# γ(감마): 스케일 파라미터 — 초기값 1
self.gamma = nn.Parameter(torch.ones(d_model))
# β(베타): 이동 파라미터 — 초기값 0
self.beta = nn.Parameter(torch.zeros(d_model))
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: [batch, seq_len, d_model]
# 마지막 차원(d_model)에 대해 평균·분산 계산 — Layer Norm의 핵심!
mean = x.mean(dim=-1, keepdim=True) # μ: [B, S, 1]
var = x.var(dim=-1, keepdim=True, unbiased=False) # σ²: [B, S, 1]
# 정규화: x̂ = (x - μ) / √(σ² + ε)
x_hat = (x - mean) / torch.sqrt(var + self.eps)
# 스케일·이동: γ·x̂ + β
return self.gamma * x_hat + self.beta
# ════════════════════════════════════════════════════
# 실전 Transformer Block에서의 사용 패턴
# ════════════════════════════════════════════════════
class TransformerBlock(nn.Module):
"""Pre-Norm 방식 (최신 LLM 표준: LLaMA, GPT-4)"""
def __init__(self, d_model=512, n_heads=8, d_ffn=2048, dropout=0.1):
super().__init__()
self.norm1 = LayerNorm(d_model) # Attention 전 정규화
self.norm2 = LayerNorm(d_model) # FFN 전 정규화
self.attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout, batch_first=True)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ffn),
nn.GELU(), # 최신 모델은 ReLU 대신 GELU/SwiGLU
nn.Linear(d_ffn, d_model),
nn.Dropout(dropout)
)
def forward(self, x):
# Pre-Norm: LayerNorm → SubLayer → 잔차 연결
# (원본 Post-Norm과 다름: 원본은 SubLayer → Add → LayerNorm)
attn_out, _ = self.attn(self.norm1(x), self.norm1(x), self.norm1(x))
x = x + attn_out # 잔차 연결
x = x + self.ffn(self.norm2(x)) # 잔차 연결
return x
# ════ 전체 파이프라인 사용 예시 ════
d_model, n_heads, seq_len, batch = 512, 8, 64, 4
# 1. 토큰 임베딩
embedding = nn.Embedding(50000, d_model)
token_ids = torch.randint(0, 50000, (batch, seq_len))
x = embedding(token_ids) # [4, 64, 512]
# 2. Positional Encoding 추가
pe = SinusoidalPositionalEncoding(d_model)
x = pe(x) # [4, 64, 512] — 순서 정보 주입
# 3. Transformer 블록 통과 (LayerNorm 내장)
block = TransformerBlock(d_model, n_heads)
out = block(x) # [4, 64, 512]
print(f"PE 포함 입력: {x.shape}") # torch.Size([4, 64, 512])
print(f"블록 출력: {out.shape}") # torch.Size([4, 64, 512])
print(f"LN 파라미터: γ={block.norm1.gamma.shape}, β={block.norm1.beta.shape}")
# LN 파라미터: γ=torch.Size([512]), β=torch.Size([512])
# torch.nn.LayerNorm으로도 동일하게 사용 가능:
ln = nn.LayerNorm(d_model, eps=1e-6) # 내부적으로 동일한 수식
수학이 만드는
AI의 질서
Positional Encoding은 병렬 처리의 단점(순서 무지)을 sin·cos의 수학적 파동으로 극복한다. 주파수가 다른 수백 개의 파동이 조합되어 각 위치마다 세상에 하나뿐인 "지문"을 만든다.
Layer Normalization은 수십~수백 층을 쌓아도 기울기가 소멸하거나 폭발하지 않도록 각 레이어의 출력을 안정화한다. 평균을 0으로, 분산을 1로 정규화하되 γ·β로 모델이 원하는 분포를 다시 찾을 자유를 준다.
이 두 기술이 없었다면 GPT-3의 96층, LLaMA의 80층은 존재할 수 없었다. 화려한 어텐션 뒤에서 조용히 질서를 지키는 수학의 힘이다.
가치 보강: 2026년 5월 23일 기준
이 글은 독자가 바로 적용할 수 있는 기준을 더하기 위해 2026년 5월 23일 기준으로 보강했습니다. 단순 정보 나열보다 실제 예시, 확인 순서, 관련 글 연결을 함께 보는 것이 블로그 글의 가치를 높입니다.
실전 적용 예시
| 상황 | 어떻게 보면 좋은가 |
|---|---|
| 처음 읽을 때 | 글의 결론과 적용 대상을 먼저 확인합니다. |
| 실제로 쓸 때 | 내 상황에 맞는 예시만 골라 적용하고, 숫자나 정책은 원문을 확인합니다. |
| 다음 행동 | 관련 글을 이어 읽어 주제 전체 흐름을 잡습니다. |
읽고 바로 확인할 것
- 내 상황에 적용 가능한 글인지 확인했는가?
- 날짜, 정책, 요금, 게임 정보처럼 바뀌는 내용은 다시 확인했는가?
- 관련 글을 함께 읽어 맥락을 보완했는가?
- 글의 예시를 그대로 복사하지 않고 내 상황에 맞게 바꿨는가?
같이 보면 좋은 글
'AI란 무엇인가 > AI 기본' 카테고리의 다른 글
| AI 저작권 기본 정리: 블로그 글·이미지·상업적 사용 주의점 (0) | 2026.05.22 |
|---|---|
| 생성형 AI가 틀린 답을 하는 이유: 할루시네이션 쉽게 이해하기 (0) | 2026.05.22 |
| Transformer 어텐션 메커니즘 완전 해부 (0) | 2026.04.01 |
| AI Transformer~~ (0) | 2026.03.31 |
| AI 파라미터란 무엇인가 — 종류·역할·차이점 완전 정복 (3) | 2026.03.30 |