오늘의 핵심
Gradient Descent가 Parameter를 어떻게 업데이트하는지 이해하고, 초기 Weight를 왜 적절하게 설정해야 하는지 학습했다.
함수
를 미분하면
이고 이라면
이다.
여기서 6은 x를 1 증가시키면 y가 정확히 6 증가한다는 의미가 아니다.
💡 현재 근처에서 x가 아주 조금 변했을 때, 함수값은 그 변화량의 약 6배만큼 변한다는 의미다.
즉,
이므로 에서
이다.
예를 들어 가 0.01 변하면
정도 변한다.
Gradient의 절댓값이 크다는 것은
현재 Parameter가 조금만 변해도 Loss가 크게 변할 수 있다는 뜻이다.
즉 현재 위치의 경사가 가파르다는 의미다.
🚨 하지만
Gradient가 크다
≠
최솟값에서 멀리 떨어져 있다
이다.
Gradient는 어디까지나 현재 위치에서의 순간적인 변화율이다.
우리는 Loss를 최소화하고 싶다.
Gradient는 함수가 가장 빠르게 증가하는 방향을 알려주므로, 반대 방향으로 이동하면 된다.
Gradient는
어느 방향으로 이동해야 하는지 + 현재 경사가 얼마나 가파른지
를 알려준다.
Learning Rate는
그 방향으로 실제로 얼마나 이동할지
결정한다.
즉,
이다.
예를 들어 Gradient가 6이고 Learning Rate가 0.1이라면
현재 위치가 이라면
가 된다.
Gradient가 크고 LR까지 크면:
큰 Gradient
×
큰 Learning Rate
↓
Parameter가 크게 이동
↓
Minimum을 지나침
↓
Loss 증가
↓
더 큰 Gradient
↓
발산
이 발생할 수 있다.
선형회귀를 생각하면
MSE는
이고 Weight에 대한 Gradient는
이다.
여기서
즉 Gradient 안에 입력 가 직접 곱해진다.
키 데이터가 155 ~ 190처럼 크고 예측 오차도 크면 Gradient 역시 커질 수 있다.
Standardization:
등을 사용하면 입력값의 Scale을 조절할 수 있다.
입력 Scale 안정화
↓
Gradient Scale 안정화
↓
Parameter Update 안정화
↓
학습이 쉬워짐
🚨 정규화를 하지 않으면 무조건 발산하는 것은 아니다.
작은 Learning Rate를 사용하면 학습할 수도 있지만, Scaling을 하면 최적화가 훨씬 쉬워지는 경우가 많다.
Parameter가 이고 데이터가 철수, 영희, 윤수 3개 있다고 하자.
각 데이터에서:
철수 → dw철수, db철수
영희 → dw영희, db영희
윤수 → dw윤수, db윤수
가 계산된다.
GD에서는 Parameter별로 전체 데이터의 Gradient를 평균낸다.
그리고 한 번 업데이트한다.
💡 Parameter끼리 Gradient를 평균내는 것이 아니다.
각 Parameter가 자신의 Gradient를 가지며, 그 Parameter에 대한 여러 데이터의 Gradient를 평균내는 것이다.
MNIST 학습 데이터가 60,000장이고
model = nn.Linear(784, 10)
이라면 Parameter는
Weight = 784 × 10 = 7,840
Bias = 10
총 7,850개
이다.
Batch GD라면:
MNIST 60,000장
↓
Forward
↓
Loss 계산
↓
60,000장 전체를 기준으로
7,850개 Parameter의 Gradient 계산
↓
Parameter 전체 Update 1회
즉 한 Epoch에 Update가 한 번 일어난다.
SGD는 전체 데이터를 기다리지 않고 데이터 하나를 사용해 Gradient를 계산하고 바로 Parameter를 업데이트한다.
여기서 는 i번째 데이터 하나에 대한 Loss다.
MNIST라면:
1번째 이미지 → Gradient → Update
2번째 이미지 → Gradient → Update
3번째 이미지 → Gradient → Update
...
60,000번째 이미지 → Gradient → Update
따라서 순수 SGD라면:
이다.
Batch GD는 정확한 전체 Gradient를 얻지만, 업데이트 한 번을 위해 모든 데이터를 계산해야 한다.
SGD는 데이터 일부로 전체 Gradient를 확률적으로 추정한다.
그래서 계산 하나당 훨씬 자주 Parameter를 업데이트할 수 있다.
실제 딥러닝에서는 순수 SGD보다 Mini-batch SGD를 훨씬 많이 사용한다.
예를 들어:
DataLoader(
dataset,
batch_size=100,
shuffle=True
)
라면 100개의 데이터에 대해
를 계산하고 Parameter를 한 번 업데이트한다.
MNIST 60,000장이라면:
이므로 한 Epoch에 600번 Update한다.
| 방법 | 한 번에 사용하는 데이터 | 60,000개 기준 Update |
|---|---|---|
| Batch GD | 60,000 | 1 |
| Mini-batch SGD | 100 | 600 |
| Pure SGD | 1 | 60,000 |
Batch Size가 커지면 일반적으로 여러 데이터의 Gradient가 평균되므로
한다.
즉 전체 데이터 Gradient에 더 가까운 안정적인 Gradient를 얻을 수 있다.
하지만 무조건 크게 하는 것도 좋은 것은 아니다.
작은 Batch
→ Gradient가 noisy
→ Update를 자주 함
큰 Batch
→ Gradient가 안정적
→ Update 횟수가 줄어듦
Full Batch
→ 가장 정확한 전체 Gradient
→ Epoch당 Update 1회
🎯 Batch Size는 Gradient의 안정성, Update 빈도, GPU 효율 사이의 Trade-off다.
신경망을 학습하기 전에 Weight의 시작값을 결정해야 한다.
좋은 초기화에서 중요한 것은 크게 두 가지다.
그래야 각 뉴런이 서로 다른 Feature를 학습할 수 있다.
Weight가 너무 크거나 너무 작으면 Layer를 지나면서 Activation과 Gradient의 Scale이 불안정해질 수 있다.
🎯 좋은 초기화 = Symmetry를 깨면서 Activation과 Gradient의 Scale을 안정적으로 유지하는 것
모든 Weight를 0으로 초기화한다.
PyTorch:
nn.init.zeros_(model.weight)
문제는 모든 뉴런이 동일하게 시작한다는 것이다.
같은 Weight
↓
같은 출력
↓
같은 Gradient
↓
같은 Update
↓
계속 같은 Weight
이를 Symmetry Problem이라고 한다.
결국 여러 뉴런을 만들어도 비슷한 Feature만 학습하게 된다.
Bias는 0으로 초기화하는 경우가 흔하다.
nn.init.zeros_(model.bias)
따라서
Deep Network의 Weight 전체를 Zero Initialization하는 것이 문제이지, 모든 Parameter를 절대 0으로 만들면 안 된다는 뜻은 아니다.
모든 Weight를 같은 특정 값으로 초기화한다.
예:
nn.init.constant_(model.weight, 0.5)
Zero가 아니므로 괜찮아 보이지만 여전히 문제가 있다.
Neuron 1 = [0.5, 0.5, 0.5]
Neuron 2 = [0.5, 0.5, 0.5]
두 뉴런이 똑같은 상태로 시작한다.
따라서:
같은 출력
→ 같은 Gradient
→ 같은 Update
가 반복된다.
💡 문제는
0이라는 숫자가 아니라 뉴런들의 Weight가 동일하다는 것이다.
Weight를 Uniform Distribution에서 랜덤하게 뽑는다.
예:
PyTorch:
nn.init.uniform_(
model.weight,
a=-0.1,
b=0.1
)
각 Weight가 서로 다른 값을 가지므로 Symmetry가 깨진다.
서로 다른 Weight
↓
서로 다른 Activation
↓
서로 다른 Gradient
↓
서로 다른 Feature 학습
Uniform Distribution의 평균과 분산은
이다.
Normal Distribution에서 Weight를 랜덤하게 뽑는다.
PyTorch:
nn.init.normal_(
model.weight,
mean=0.0,
std=0.01
)
보통 평균은 0 주변으로 두고 Standard Deviation을 통해 Weight의 Scale을 결정한다.
Uniform과 달리 Normal Distribution은:
값 많음
↓
/\
/ \
-------- --------
0
처럼 평균 근처의 값이 많이 나온다.
| Uniform | Normal |
|---|---|
| 특정 범위 안에서 고르게 추출 | 평균 근처 값이 많이 등장 |
| 범위를 벗어나지 않음 | 큰 값도 낮은 확률로 등장 |
U(a,b) | N(μ,σ²) |
하지만 더 중요한 것은
Uniform과 Normal 중 무엇을 선택했느냐보다 Weight의 Scale과 Variance를 적절하게 설정했느냐이다.
실제로 Xavier와 Kaiming 모두 Uniform / Normal 버전이 존재한다.
뉴런 하나를 생각해보자.
여기서:
그리고:
이다.
여기서:
전체 흐름:
이전 Layer의 출력 x
↓
z = Wx + b
↓
Activation Function
↓
a = φ(z)
↓
다음 Layer
에서 개의 값을 계속 더한다.
입력이 2개인 경우:
w1x1 + w2x2
입력이 1000개인 경우:
w1x1 + w2x2 + ... + w1000x1000
Weight의 Scale을 똑같이 사용한다면 입력 개수가 많을수록 의 Scale이 커질 가능성이 있다.
단순한 독립성 등의 가정 아래에서는 대략:
라고 생각할 수 있다.
따라서
일수록 일반적으로 Weight의 Scale을 어느 정도 줄여줄 필요가 있다.
이때 들어오는 입력 뉴런의 개수를 fan-in이라고 한다.
큰 Weight
↓
큰 Pre-activation
↓
Activation Scale 불안정
↓
다음 Layer
↓
Scale이 계속 커질 가능성
Backward에서도 Weight가 반복해서 관여하기 때문에 Gradient Scale 역시 불안정해질 수 있다.
작은 Weight
↓
작은 Pre-activation
↓
작은 Activation
↓
Layer를 거치며 Signal 감소 가능
Backward에서도 Gradient가 지나치게 작아질 가능성이 있다.
🚨 다만:
Activation이 크다 → Gradient가 반드시 크다
는 것은 아니다.
Activation Function에 따라 다르다.
예를 들어 Sigmoid는 입력 절댓값이 너무 커지면 포화되어 오히려 미분값이 0에 가까워지고 Gradient Vanishing이 발생할 수도 있다.
초기화 방법의 흐름은 다음과 같다.
Zero Initialization
↓
모든 뉴런이 동일
↓
Symmetry 문제
Constant Initialization
↓
0이 아니어도 동일한 값
↓
여전히 Symmetry 문제
Random Initialization
↓
┌─────────────┐
↓ ↓
Uniform Normal
↓ ↓
뉴런마다 서로 다른 Weight
↓
Symmetry Breaking
하지만 여기서 끝이 아니다.
Random하게 만들었다
↓
그런데 Weight를 얼마나 크게 해야 하지?
↓
너무 크면 Scale 불안정
너무 작으면 Signal 감소
↓
입력 개수까지 고려해서
적절한 Weight Variance를 결정하자
↓
Xavier / Kaiming Initialization
좋은 초기화의 핵심은 두 가지다.
1. Weight들이 서로 다른 값을 가져 Symmetry를 깨야 한다.
2. Weight가 적절한 Scale을 가져 Activation과 Gradient가 안정적으로 전달되어야 한다.
그리고 입력 개수 fan-in이 많아질수록 같은 Weight Scale을 그대로 사용하는 것이 적절하지 않을 수 있다.
이 문제를 해결하기 위해 다음으로 배우게 되는 것이:
이다.
🎯 오늘 가장 중요한 연결
Gradient를 이해하면 →GD / SGD를 이해할 수 있고
Activation과 Gradient의 Scale을 이해하면 →Xavier / Kaiming이 왜 필요한지 이해할 수 있다.