SGD는 어떻게 동작하는가
— 전부 보지 않고 내려가는 법
경사 하강법(GD)은 한 발짝을 떼기 위해 학습 데이터 전부를 봅니다. 데이터가 100만 장이면 100만 번 계산해서 딱 한 번 가중치를 고칩니다. 확률적 경사 하강법(SGD)은 그중 몇 개만 보고 그냥 갑니다. 방향이 조금 틀려도 상관없다는 것인데 — 왜 그래도 되는지, 틀린 정도가 얼마나 되는지, 그리고 그 "틀림"이 오히려 도움이 되는 경우까지 브라우저에서 직접 계산해 봅니다.
§1 손실은 평균이다 — GD의 한 발짝은 왜 비싼가
거의 모든 지도학습의 손실 함수는 데이터 하나하나의 손실을 평균 낸 것입니다.
미분은 선형이므로 기울기도 그대로 평균입니다.
여기서 문제가 시작됩니다. 경사 하강법은 θ ← θ − η∇L(θ) 한 줄이지만,
그 ∇L 하나를 얻으려면 N개 전부에 대해 순전파와 역전파를 돌려야 합니다.
ImageNet처럼 N = 128만이면, 가중치를 한 번 고치려고 128만 장을 통과시키는 셈입니다.
100번 고치려면 1억 2,800만 번. 학습이 끝나지 않습니다.
y = 2x + 1 + 잡음으로 만들고,
직선 ŷ = w·x + b를 맞춥니다. 파라미터가 (w, b) 둘뿐이라
손실 지형을 평면에 그대로 그릴 수 있고, 진짜 기울기도 정확히 계산됩니다.
샘플 하나의 손실은 Li = ½(w·xi + b − yi)²,
따라서 ∇Li = [(ŷi−yi)·xi, (ŷi−yi)]입니다.
아래 데모의 숫자는 전부 이 식을 페이지 안에서 실제로 돌린 결과입니다.
§2 표본으로 기울기를 추정한다 — 평균 내면 제자리로 온다
SGD의 착상은 한 줄입니다. 평균을 구하려고 전수 조사를 할 필요는 없다. 여론조사가 5천만 명이 아니라 1천 명에게 묻듯, 기울기도 무작위로 뽑은 B개의 평균으로 대신합니다.
증명은 한 줄입니다. 각 샘플이 뽑힐 확률이 모두 같으니 E[∇Li] = (1/N)Σ∇Lj = ∇L,
그 B개의 평균도 여전히 ∇L입니다. 한 스텝 한 스텝은 틀리지만, 틀린 방향이 한쪽으로 쏠려 있지 않습니다.
아래에서 직접 확인해 보세요 — 미니배치 기울기(회색)는 제각각이지만, 그것들을 평균 내면(초록) 진짜 기울기(파랑)로 수렴합니다.
개별 화살표는 제각각인데(오른쪽 끝 수치) 평균의 오차는 계속 줄어듭니다. B를 키우면 개별 화살표부터 이미 촘촘해집니다 — 그 "촘촘해지는 속도"가 §4의 주제입니다.
§3 GD · 미니배치 · SGD — 정확히 무엇이 다른가
세 방법은 서로 다른 알고리즘이 아닙니다. 갱신 규칙은 전부 똑같은 한 줄이고, 기울기를 몇 개의 샘플로 계산하느냐(B)만 다릅니다.
| 배치 경사 하강법 (GD) | 미니배치 SGD | SGD (B=1) | |
|---|---|---|---|
| 한 스텝에 쓰는 샘플 | N (전부) | B (보통 32~512) | 1 |
| 1 에폭당 갱신 횟수 | 1 | N/B | N |
| 기울기 방향 | 정확 | 기댓값은 정확, 흔들림 있음 | 기댓값은 정확, 매우 흔들림 |
| 궤적 | 매끄럽게 단조 감소 | 지그재그로 감소 | 요동치며 감소 |
| GPU 활용 | 메모리 초과로 애초에 불가 | 행렬 곱으로 병렬 — 최적 | 병렬성 없음, 비효율 |
| 수렴 지점 | 최솟값에 정확히 정착 | 최솟값 주변을 맴돔 (노이즈 볼 · §7) | |
| 지역 최솟값·안장점 | 갇히면 못 나옴 | 노이즈가 밀어내 빠져나올 수 있음 (§6) | |
| 데이터가 계속 들어올 때 | 불가 (전부를 알아야 함) | 가능 (온라인 학습) | |
아래 실험실에서 B를 움직여 보세요. B = 40(전체)으로 두면 궤적이 GD와 정확히 겹칩니다 — 같은 알고리즘이라는 게 눈으로 보입니다. B를 1로 내리면 같은 목적지를 향해 가면서도 길이 들쭉날쭉해집니다.
같은 스텝 수에서는 GD가 유리해 보입니다. 당연합니다 — GD는 한 스텝마다 40개를 보고 SGD는 B개만 봅니다.
공정한 비교는 "스텝 수"가 아니라 "본 샘플 수"로 해야 합니다. §5에서 그렇게 다시 겨룹니다.
η를 키워 보면 차이가 하나 더 보입니다. GD는 여전히 ★에 정확히 멈추지만,
SGD는 ★ 주변을 도는 구름이 눈에 띄게 커집니다 — 이 구름의 정체가 §7입니다.
§4 노이즈의 크기는 1/√B — 배치를 4배 키워야 노이즈가 절반
미니배치 기울기는 평균적으로는 옳지만(§2), 한 번 한 번은 틀립니다. 얼마나 틀릴까요? 독립적인 값 B개를 평균 내면 분산이 B로 나뉘는, 표본평균의 기본 성질 그대로입니다.
이 식이 SGD의 손익 계산서 전부입니다. 배치를 2배 키우면 계산은 2배 드는데 노이즈는 √2배(≈1.41배)밖에 안 줄어듭니다. 계산량당 노이즈 감소로 보면 큰 배치는 손해입니다. 그런데도 실무에서 B를 키우는 이유는 GPU가 32개든 256개든 거의 같은 시간에 처리하기 때문이지, 통계적으로 이득이어서가 아닙니다.
보정 항을 끄면(복원 추출 가정) B가 N에 가까울 때 이론이 실측보다 큽니다 — 실제로는 배치가 데이터 전체에 가까워질수록 "뽑을 게 남지 않아" 오차가 0으로 떨어지기 때문입니다. B = N에서 오차가 정확히 0인 것이 곧 GD입니다.
§5 같은 계산 예산이면 누가 이기는가
지금까지의 비교는 전부 불공정했습니다. 진짜 질문은 이것입니다 — 샘플 기울기를 딱 1만 번 계산할 수 있다면, 어떻게 나눠 쓰는 게 이득인가? GD는 40번 계산해 1스텝, B=1은 1번 계산해 1스텝입니다. 같은 예산으로 SGD는 40배 많이 움직입니다.
초반이 승부처입니다. 기본값(η = 0.12)에서 1에폭 시점의 남은 거리는 B=1이 0.023, GD가 6.44 — 280배 차이입니다. GD가 첫 한 발짝을 떼는 동안 B=1은 이미 40발짝을 갔고, 그 한 발짝 한 발짝이 틀린 방향이더라도 평균적으로는 옳은 방향이기(§2) 때문입니다. 대신 B=1은 금세 더 내려가지 못하고 0.0006~0.04 사이를 오르내립니다 — 노이즈 바닥(§7)에 닿은 것입니다. 12에폭까지 가면 중간 크기인 B=4·16이 가장 깊이(≈0.0009) 내려가고, GD는 그때까지도 0.08에 머뭅니다. "가장 작은 배치가 항상 이긴다"가 아니라, "전체를 다 보는 것은 거의 항상 손해다"가 결론입니다.
기본값은 ImageNet(N = 1,281,167) · B = 256 · 90에폭입니다. 계산량은 완전히 같은데 가중치를 고치는 횟수가 90번이냐 45만 번이냐로 갈립니다. GD로 딥러닝이 불가능한 이유가 이 한 줄입니다.
§6 노이즈는 버그가 아니다 — 지역 최솟값 탈출
여기까지는 노이즈를 "참고 견디는 비용"으로 다뤘습니다. 그런데 노이즈에는 GD가 절대 할 수 없는 일이 하나 있습니다.
GD는 기울기가 0인 곳에 도착하면 영원히 멈춥니다. 그곳이 전체 최솟값이든, 얕은 웅덩이든, 안장점이든 똑같습니다.
∇L = 0 → θ ← θ − η·0 → 제자리. 반면 SGD의 기울기는 그 지점에서도 0이 아닙니다 —
평균이 0일 뿐이라 계속 흔들리고, 운이 좋으면 언덕을 넘어갑니다.
σ = 0에서 탈출률은 정확히 0%입니다 — GD는 얕은 웅덩이에서 절대 못 나옵니다. σ를 올리면 탈출률이 오르지만, 너무 올리면 깊은 웅덩이에서도 튕겨 나와 "어느 쪽에도 못 앉음"이 늘어납니다. 이 맞교환이 §7의 학습률 감쇠로 이어집니다.
§7 그래서 학습률을 줄인다 — 노이즈 볼과 Robbins–Monro 조건
학습률 η를 상수로 두면 SGD는 최솟값에 정착하지 못합니다.
최솟값에 도착해도 gB가 0이 아니라서 계속 걷어차이기 때문입니다.
결국 최솟값을 중심으로 한 일정 반경의 구름 안을 맴도는데, 이걸 노이즈 볼이라고 부릅니다.
그 반경은 대략 √(η·S²)에 비례합니다 — η를 1/4로 줄이면 반경이 절반이 됩니다.
상수 학습률 두 줄은 각자 다른 높이에서 평평해집니다. η를 1/4로 줄였을 때 바닥이 약 2배 낮아지는지 확인해 보세요 — √η 법칙 그대로입니다. "학습률을 10배 줄였더니 손실이 뚝 떨어지는" 계단식 스케줄이 실제로 하는 일이 이것입니다. 반면 감쇠 학습률(초록)은 바닥 없이 계속 내려갑니다.
DataLoader 방식이 되는데,
이때 바닥 비율은 3 이상으로 커집니다 — 같은 η에서 바닥이 더 낮다는 뜻입니다.
한 에폭 안에서 모든 샘플이 정확히 한 번씩 쓰이므로 기울기 오차가 에폭 안에서 서로 상쇄되기 때문입니다
(random reshuffling이 i.i.d. SGD보다 빠르다는, 이론적으로도 알려진 현상 —
Gürbüzbalaban et al., 2015).
셔플이 그저 구현 편의가 아니라는 근거입니다.
언제까지 줄여야 할까요? 1951년 Robbins와 Monro가 이미 답을 내놨습니다. 충분히 많이 걸을 수 있을 만큼 크되, 노이즈가 결국 사그라들 만큼 작아야 한다는 두 조건입니다.
ηt = η₀/t는 둘 다 만족하고(조화급수는 발산, 제곱합은 수렴), ηt = η₀/√t는 두 번째를 어기며,
상수 η는 첫 번째만 만족합니다 — 그래서 상수 η는 수렴하지 않고 노이즈 볼 안을 맴돕니다. 위 실험이 보여준 그대로입니다.
1/t를 쓰지는 않습니다. 너무 빨리 줄어들어 학습이 조기에 멈추기 때문입니다.
현실에서는 계단식 감쇠(30에폭마다 ÷10), 코사인 감쇠, 초반에 오히려 올리는 워밍업 등을 씁니다.
이론적 수렴 조건은 "감쇠가 왜 필요한가"를 설명해 줄 뿐, 스케줄 자체를 정해 주지는 않습니다.
§8 실전 메모 — 셔플·에폭·선형 스케일링, 그리고 이름의 혼선
1. 실제 구현은 "무작위 추출"이 아니라 "섞고 자르기"
이론은 매 스텝 독립적으로 B개를 뽑는다고 가정하지만, 실제 DataLoader는
에폭 시작 때 전체를 한 번 섞고 앞에서부터 B개씩 끊어 씁니다.
한 에폭 안에서 모든 샘플이 정확히 한 번씩 쓰이고(비복원), 구현도 빠르며, 경험적으로 더 잘 됩니다.
shuffle=True를 빼면 데이터가 클래스 순으로 정렬돼 있을 때 배치 하나가 통째로 한 클래스가 되어 학습이 망가집니다.
위 실험 2·4도 이 방식(에폭마다 셔플 후 분할)으로 돌아갑니다.
2. 배치를 키우면 학습률도 키운다 — 선형 스케일링 법칙
B를 k배 키우면 스텝 수가 1/k로 줄어듭니다. 같은 거리를 가려면 한 걸음을 k배로 키워야 하므로 η ← k·η.
이것이 Goyal et al. (2017)이
ImageNet을 B = 8,192로 1시간에 학습시킬 때 쓴 규칙입니다(초반 워밍업을 함께 씀).
다만 노이즈는 1/√k로만 줄어드는데(§4) 걸음은 k배로 키우는 것이라 무한정 통하지는 않습니다 — 어느 배치 크기부터는 깨집니다.
GPU를 여러 장 쓰는 것이 곧 배치를 키우는 것인 이유는 §9에서 확인합니다.
3. 이름의 혼선 — 프레임워크의 SGD는 B=1이 아니다
torch.optim.SGD를 쓴다고 해서 샘플 하나씩 학습하는 게 아닙니다.
배치 크기는 DataLoader가 정하고, 옵티마이저는 "들어온 기울기로 θ ← θ − η·g를 한다"는 규칙일 뿐입니다.
게다가 momentum=0.9를 주면 그건 이미 순수 SGD가 아니라 모멘텀 SGD입니다.
오늘날 "SGD"는 거의 항상 "미니배치 확률적 경사 하강법"을 뜻하고, B=1은 실무에서 쓰지 않습니다(GPU를 놀리기 때문).
| 말 | 엄밀한 뜻 | 실무에서 통용되는 뜻 |
|---|---|---|
| SGD | B = 1, 샘플 하나로 갱신 | 미니배치 갱신 전반 (B = 32~512 등) |
| batch size | — | 미니배치 크기 B (전체 N이 아님) |
| batch gradient descent | B = N, 전체 배치 | 거의 안 씀 (혼동을 피하려 "full-batch"라 부름) |
| iteration / step | 갱신 1회 | 미니배치 1개 처리 = 갱신 1회 |
| epoch | 데이터 전체를 한 번 훑음 | = ⌈N/B⌉ iteration |
4. Adam·모멘텀은 SGD를 대체하는 게 아니라 위에 얹는 것
모멘텀·RMSProp·Adam은 전부 "gB를 받아서 어떻게 쓸 것인가"에 대한 개선입니다. 기울기를 미니배치로 추정한다는 전제는 똑같이 깔고 갑니다. 즉 §2~§4의 노이즈 이야기는 Adam을 쓰더라도 그대로 유효하며, 모멘텀은 그 노이즈를 시간축으로 평균 내 흔들림을 줄이는 장치로도 읽을 수 있습니다. 이 옵티마이저들의 비교는 How Gradient Descent Works에서 다룹니다.
§9 여러 대로 나눠 돌리기 — 동기 SGD와 비동기 SGD
한 대로는 부족할 때 워커 K대에 데이터를 나눕니다(데이터 병렬). 각 워커는 자기 몫의 미니배치 B개로 기울기를 계산하는데, 그 다음이 문제입니다 — 다른 워커를 기다릴 것인가, 말 것인가. 이 한 가지 선택이 동기 SGD와 비동기 SGD를 가릅니다.
동기 SGD — 전원이 같은 θ에서 출발한다
모든 워커가 같은 θt로 기울기를 계산하고, 다 모아 평균 낸 뒤 한 번 갱신합니다 (이 "다 모아 평균 내기"가 all-reduce입니다).
gradient accumulation도 똑같은 이야기입니다.
대가는 동기화 장벽입니다. 매 스텝 전원을 기다리므로 가장 느린 워커가 전체 속도를 정합니다. 한 대가 2배 느리면 8대 전체가 2배 느려집니다. 이 느린 워커를 스트래글러(straggler)라고 부릅니다.
비동기 SGD — 기다리지 않는다, 대신 낡은 θ를 쓴다
파라미터 서버를 두고, 워커는 θ를 받아 가 계산이 끝나는 대로 기울기를 밀어 넣습니다. 아무도 기다리지 않으니 노는 시간이 없습니다. 문제는 받아 간 θ와 밀어 넣을 때의 θ가 다르다는 것입니다 — 그 사이 다른 워커들이 이미 서버를 여러 번 갱신했으니까요. 이 시차를 지연(staleness) τ라고 하고, 워커 K대가 비슷한 속도라면 평균 τ ≈ K − 1입니다.
E[gB(θt−τ)] = ∇L(θt−τ) ≠ ∇L(θt) —
평균이 0이 아닌 편향입니다. 많이 뽑는다고 상쇄되지 않습니다.
비동기 SGD를 "노이즈가 조금 더 낀 SGD"로 생각하면 안 되는 이유가 이것입니다.
그래서 누가 이기는가 — 벽시계 시간으로 재기
동기는 스텝당 품질이 좋고, 비동기는 시간당 스텝 수가 많습니다. 둘을 한 축에서 비교하려면 "스텝"이 아니라 벽시계 시간으로 재야 합니다(§5와 같은 논리입니다). 아래는 워커마다 처리 시간이 다른 상황을 이산 사건으로 모의한 것입니다.
스트래글러가 1.0×면 동기가 이깁니다 — 같은 시간에 처리하는 샘플 수가 비슷한데 동기 쪽 기울기가 더 정확하니까요. 스트래글러를 4×로 올려 보세요. 동기는 매 라운드 그 한 대를 기다리느라 라운드 수가 3분의 1 아래로(101 → 30) 줄지만, 비동기는 956 → 867로 거의 그대로입니다. 비동기가 존재하는 이유는 "더 좋은 기울기"가 아니라 "놀지 않는 워커"입니다. 그리고 같은 조건에서 η를 0.12로 올려 보세요 — 이번엔 지연이 발목을 잡아 순서가 다시 뒤집힙니다(동기 0.0003 vs 비동기 0.0825). K를 16으로 올리면 비동기는 아예 발산합니다.
비동기의 진짜 비용 — 학습률 상한이 내려간다
지연이 있으면 "지금 있지도 않은 위치의 기울기"로 걷는 셈이라, 걸음이 크면 목표를 지나쳐 진동합니다. 아래 격자는 지연 τ와 학습률 η를 바꿔 가며 같은 400번의 갱신 뒤 남은 거리를 잰 것입니다.
τ = 0 줄은 η를 0.30까지 올려도 멀쩡합니다. τ = 16 줄은 η = 0.10에서 이미 터집니다. 워커를 늘릴수록(τ가 커질수록) 쓸 수 있는 학습률이 작아진다 — 비동기의 확장성이 여기서 막힙니다. 동기 SGD는 정반대로, 워커를 늘리면 배치가 커져서 학습률을 올릴 수 있습니다(§8 선형 스케일링).
Deep Speech 2가 동기를 고른 이유
2015년의 주류는 파라미터 서버 + 비동기 SGD였습니다. DS2는 정반대로 갔고, 근거가 정확도가 아니었습니다.
We find synchronous SGD useful because it is reproducible and deterministic. We have found that the appearance of non-determinism in our system often signals a serious bug. — Amodei et al., 4.1절
비동기 SGD는 같은 코드를 두 번 돌려도 결과가 다릅니다 — 어느 워커의 기울기가 몇 번째로 도착했는지가 매번 달라지니까요. 그러면 "어제보다 나빠졌는데 이게 내 코드 탓인지 운 탓인지"를 가릴 수가 없습니다. DS2는 디버깅 가능성을 근거로 동기 SGD를 고르고, 대신 통신을 직접 최적화했습니다(링 all-reduce 직접 구현, GPU 8장 이하에서 OpenMPI 대비 최대 20배). 자세한 내용은 How Deep Speech 2 Works §11에 있습니다.
DistributedDataParallel이 하는 일이 바로 이것입니다).
비동기가 여전히 쓰이는 곳은 워커 성능이 제각각이거나 중간에 사라질 수 있는 환경(선점형 인스턴스, 연합 학습),
또는 갱신이 희소해서 충돌이 거의 없는 경우입니다 —
잠금 없이 공유 메모리를 덮어쓰는 Hogwild!(2011)가 후자의 대표입니다.
§10 정리 — 한 문장으로
헷갈리기 쉬운 것 5가지
- SGD는 GD의 "빠른 근사"가 아니다. 방향의 기댓값은 완전히 같습니다. 다른 것은 분산뿐입니다.
- 배치를 키운다고 그만큼 좋아지지 않는다. 계산은 B에 비례해 늘지만 노이즈는 √B로만 줄어듭니다.
- "1 에폭"은 진척도의 단위가 아니다. 같은 1 에폭이 GD에겐 1스텝, B=256·N=128만이면 5,005스텝입니다.
- 상수 학습률로는 수렴하지 않는다. 손실 곡선이 평평해진 것은 "다 배웠다"가 아니라 "노이즈 볼에 닿았다"일 수 있습니다 — 학습률을 낮춰 보면 구분됩니다.
- 노이즈가 크다고 좋은 게 아니다. 탈출에는 유리하지만 정착에는 불리합니다. 그래서 초반엔 크게, 후반엔 작게 씁니다.
- GPU를 늘리는 것은 새로운 알고리즘이 아니다. 동기 SGD는 배치를 키우는 것과 정확히 같은 일이고, 비동기 SGD만이 다른 물건입니다 — 지연된 기울기는 편향이라 §2의 "평균 내면 맞는다"가 통하지 않습니다.
§11 이해도 퀴즈
부록 · 1951년, 신경망도 없던 시절에 이미 증명된 것
"확률적 경사 하강법"이라는 이름의 뿌리는 딥러닝보다 60년 이상 앞섭니다. 1951년 Herbert Robbins와 Sutton Monro는 A Stochastic Approximation Method에서 전혀 다른 문제를 풀고 있었습니다 — 약물의 반응 곡선처럼 측정할 때마다 값이 튀는 함수에서 M(x) = α가 되는 x를 어떻게 찾는가. 측정이 노이즈 투성이라 함수를 정확히 알 수 없는 상황입니다.
그들의 답은 xn+1 = xn − an(yn − α)였고,
yn은 노이즈 섞인 한 번의 측정값입니다. 오늘날의 SGD와 같은 모양입니다.
그리고 Σan = ∞, Σan² < ∞이면 노이즈가 있어도 수렴한다는 것을 증명했습니다.
§7에서 본 그 조건입니다. 신경망도, GPU도, 미니배치도 없던 시절에 나온 결과가
지금 우리가 학습률을 왜 감쇠시키는지를 설명하고 있는 셈입니다.
여기서 "stochastic"은 알고리즘이 무작위라는 뜻이 아니라, 우리가 손에 넣는 관측이 무작위라는 뜻입니다. 딥러닝에서 그 "노이즈 섞인 관측"의 정체가 바로 무작위로 뽑은 미니배치의 기울기고요. 이 연결을 신경망 학습에 본격적으로 가져온 것은 1980~90년대의 온라인 학습 연구, 그리고 대규모 학습에서의 이론적 이점을 정리한 Bottou (2010) 등입니다.