서론
『분석가를 위한 수리 최적화 입문』(이토 모토하루・하라다 코헤이・마츠오카 유키, 소시무, 2026년)의 제4장을 5개의 글로 나누어 작성하고 있습니다. 이 글은 다섯 번째 글로, PyTorch 등에서 제공하는 AdaGrad, RMSProp, Adam을 다룹니다.
확률적 경사 하강법(Stochastic Gradient Descent, SGD)은 모든 변수에 동일한 스텝 사이즈를 적용하여 변수별 크기의 차이를 상쇄시키는 방법입니다.
본 기사에서는 설명 변수를 표준화하지 않은 회귀 문제에 대해 네 가지 방법을 직접 구현하고 비교합니다. 글을 마치면 다음 질문들에 답할 수 있기를 목표로 합니다.
- 왜 '경사의 크기로 나누는 것'만으로도 표준화되지 않은 데이터에서도 진행할 수 있는가?
- AdaGrad, RMSProp, Adam은 각각 이전 방법의 무엇을 개선했는가?
- Adam에서 $\alpha$는 무엇을 나타내는가? 편향 보정(Bias Correction)은 어떤 용도로 사용되는가?
수식은 고등학교 수준의 수열과 대학교 1학년 정도의 편미분 및 벡터를 사용합니다.
이번에 만들 것들
웹 서버의 가상 요청 로그 10만 건으로부터 응답 시간을 예측하는 다중 선형 회귀 문제를, 설명 변수를 표준화하지 않고 해결합니다.
| 열 | 의미 | 생성 방법 |
|---|
| 응답 크기(Response Size) | 반환된 데이터의 크기 (KB) | 평균 50, 표준편차 20 |
| ... | | |
예측식은 다음과 같습니다.
| 파일 | 역할 |
|---|
request_log.py | 데이터를 생성하고 미니 배치로 1 에포크를 실행하는 함수를 준비합니다. |
optimizers.py | SGD, AdaGrad, RMSProp, Adam 구현체입니다. |
compare.py | 네 가지 방법을 비교합니다. |
bias_correction.py | Adam의 편향 보정을 제거했을 때의 움직임을 확인합니다. |
신경망(Neural Network) 학습은 다루지 않습니다. 답이 알려진 선형 회귀에 초점을 맞추어 각 방법들이 무엇을 하는지 확인하는 데 집중합니다.
전제 조건
환경 준비
1. 하나의 스텝 사이즈로는 모든 변수에 적합하지 않다
SGD는 미니 배치로 계산한 경사(Gradient)를 이용하여, 이번 데이터에서는 가장 민감한 행 수의 계수가 너무 지나치지 않도록 작은 스텝 사이즈가 필요합니다. 대략적인 기준은 다음과 같습니다.
설명 변수를 표준화하면 경사의 크기가 균일해지므로 이 문제는 발생하기 어렵습니다. AdaGrad, RMSProp, Adam은 이를 성분별(component-wise) 스텝 사이즈로 자동 처리하는 방법입니다.
2. AdaGrad: 지금까지의 경사 크기로 나누기
AdaGrad (Duchi 외, 2011)는 성분별로 지금까지의 경사 제곱을 계속 누적한 값을 사용합니다.
경사의 각 성분이 매번 같은 값으로 움직이는 양이 결정됩니다. 경사가 65든 13652든 움직이는 양은 동일합니다. 이것이 바로 성분별 크기 차이를 상쇄하는 메커니즘입니다.
반면에,
3. RMSProp: 최근의 경사 크기로 나누기
RMSProp (Tieleman과 Hinton의 강의 슬라이드, 2012)은 경사의 제곱을 계속 누적하는 대신 이동 평균(Moving Average)을 사용합니다.
4. Adam: RMSProp에 모멘텀법의 방향을 결합하기
Adam (Kingma와 Ba, 2015)은 다음 세 가지를 조합한 것입니다.
| 부분 | 역할 |
|---|
| $\beta_1$로 계산된 이동 평균(Momentum) | 모멘텀법과 마찬가지로 과거 경사를 섞어 방향을 결정합니다. 다만 합산이 아닌 평균을 사용하므로 크기는 경사와 비슷하게 유지되며, 방향이 분산되는 성분은 서로 상쇄되어 작아집니다. |
| $\beta_2$로 계산된 성분별 경사 크기 | RMSProp과 마찬가지로 성분별 경사의 크기로 나눕니다. |
| 편향 보정 (Bias Correction) | 초기 단계의 편향을 수정합니다. |
원 논문의 기본값은 torch.optim.Adam의 기본값과 동일합니다.
4-1. $\alpha$는 '한 번에 움직이는 양'의 기준
경사가 매번 같은 방향이라면, 이는 Adam의 방향을 의미합니다.
즉, Adam의
4-2. 편향 보정은 '첫 번째 치우침'을 고치는 나눗셈
보정을 하지 않으면, 첫 번째 업데이트는 다음과 같이 됩니다.
5. Python으로 네 가지 방법 구현하기
5-1. 데이터와 미니 배치
"""웹 서버의 가상 요청 로그 10만 건(설명 변수는 표준화하지 않음)과, 미니 배치로 1 에포크를 실행하는 함수"""
import numpy as np
def make_problem(n=100_000, seed=0):
...
X는 1열, 사이즈, 행 수를 표준화하지 않고 나열한 10만 행 3열의 행렬입니다. answer는 연립 방정식을 푸는 lstsq 결과값입니다.
구한 답입니다. run_epoch은 100개 단위의 미니배치로 기울기(gradient)를 계산하고, 업데이트는 optimizer.step에 맡깁니다.
5-2. 네 가지 방법
"""SGD, AdaGrad, RMSProp, Adam. 모두 step(x, g)으로, 기울기 g를 받아 다음 x를 반환합니다"""
import numpy as np
class SGD:
...
네 개의 클래스 모두 step(x, g)을 통해 기울기를 받고 다음 지점을 반환합니다. 수식은 2~4장에서와 같이, AdaGrad와 RMSProp의 step_size는 성분별 스텝 사이즈 g**2를 사용하며, 나눗셈 연산은 NumPy 배열에서 성분별로 계산되므로, 수식 그대로 작성만 하면 '성분별 스텝 사이즈'가 됩니다.
6. 표준화하지 않은 데이터로 비교하기
네 가지 방법으로 각각 20 에포크씩 풀고, 절편(intercept)을 확인합니다.
"""표준화하지 않은 데이터에서, SGD・AdaGrad・RMSProp・Adam 을 비교합니다"""
from pathlib import Path
import matplotlib
...
답: [20.029 0.499 0.1 ], L = 48655
x = 0 에서의 기울기: [ -65. -3449. -13652.]
SGD α=1/L : 절편 0.037(1)→ 0.609(20 에포크)
...
절편의 변화를 그래프로 그리면 다음과 같습니다. 점선이 답입니다.
切片θ0の推移
SGD는 절편 움직임이 거의 없었습니다: 20 에포크(2만 번 업데이트)가 지나도 0.609에 머물러, 정답인 20과는 거리가 있습니다. 1장에서와 같이 행 수의 계수에 맞춘 작은 스텝 사이즈로는 절편을 거의 진전시킬 수 없습니다.
기울기의 크기로 나누는 세 가지 방법은 답에 도달했습니다: AdaGrad()、RMSProp(α = 1 )、Adam(α = 0.01 )은 10 에포크가 지나자마자 절편이 약 20에 가까워졌습니다. 표준화하지 않아도 기울기가 작은 절편을 진전시킬 수 있었습니다.
`
\alpha = 0.01 -
AdaGrad의 스텝 사이즈는 작아지고, RMSProp은 작아지지 않았습니다: 절편의 스텝 사이즈는 AdaGrad에서는 0.0045에서 0.0034로 줄어들고, [수식 생략]에서는 0.0009에서 0.0004로 반감되어, 절편의 증가세가 둔화되어 20 에포크에서도 11.908을 기록했습니다. RMSProp은 0.0058에서 0.0072로 줄어들지 않았습니다 (답에 가까워져 기울기가 작아질 경우, 이동 평균도 작아지기 때문입니다).
\alpha = 0.1 - **Adam의**: 그래프의 빨간 점선은 거의 일정한 기울기로 상승하고 있습니다. 한 번 움직이는 양이 약 [수식 생략]에 머물렀는데, 이는 절편이 10.966까지만 진전되었기 때문입니다. 4-1과 같이,
\alpha`는 '한 번 움직이는 양'의 기준이 됩니다.
참고로, 데이터를 표준화할 수 있다면, 표준화한 후 SGD를 사용하는 것도 확실한 방법입니다.
7. Adam의 바이어스 보정(Bias Correction)을 제거하면 초기 스텝 크기가 커진다
4-2의 '보정이 없을 경우, 첫 번째 움직임이'와 비교합니다.
"""Adam의 바이어스 보정을 제거했을 때, 처음 몇 번의 스텝 크기가 어떻게 변하는지 확인합니다"""
import numpy as np
from optimizers import Adam
...
uv run python bias_correction.py
보정 있음: 움직인 양/α = 1.00, 0.95, 0.85(1~3회차), f - f* = 828 → 181 → 115 → 66(1, 2, 3, 10회차)
보정 없음: 움직인 양/α = 3.16, 2.26, 1.21(1~3회차), f - f* = 260 → 2656 → 974 → 585(1, 2, 3, 10회차)
보정 있음에서는 첫 번째에 정확히 [수식 생략]이 나타납니다.
원 논문에 따르면, 보정이 없다는 것의 영향은 [
막혔을 때 확인 항목
| 오류 및 증상 | 원인 | 대처 |
|---|
| 표준화되지 않은 데이터에서, SGD 계수의 일부가 거의 움직이지 않음 | 기울기 크기가 성분별로 크게 달라 가장 큰 성분에 맞춰 스텝 사이즈를 작게 설정함 | 설명 변수를 표준화한다. 또는 6장과 같이 AdaGrad, RMSProp, Adam을 사용한다. |
| AdaGrad가 정답에 도달하기 전에 멈춘 것처럼 보임 | 기울기의 제곱을 계속 더하기 때문에 스텝 사이즈가 계속 작아짐. | |
| (빈칸) | (빈칸) |
| Adam이 한 번 움직이는 양은, 대략 4-1과 같이 파라미터의 크기에 맞춰서 지정한다. |
| PyTorch의 RMSprop에서 alpha를 변경해도 보폭이 변하지 않음 | PyTorch의 alpha는 이동 평균 상수인 lr이다. |
| 스텝 사이즈는 lr로 지정한다. |
| Git Bash에서 출력을 파이프나 리다이렉트로 전달하면 일본어가 깨짐 (문자화질) | 출력 목적지가 콘솔이 아니면 Python이 Windows 문자 인코딩(cp932)으로 출력함.<br>PYTHONUTF8=1 uv run python compare.py와 같이 UTF-8 모드로 실행한다. |
요약
이 글에서는 성분별로 스텝 사이즈를 변경하는 세 가지 방법을 직접 작성하여, 표준화되지 않은 데이터에서 SGD와 비교했습니다.
- SGD는 기울기 크기가 성분별로 약 200배 차이가 나는 데이터에서, 20 에포크 후에도 절편이 0.609(정답은 약 20)에 머물렀습니다.
- AdaGrad는 지금까지의 기울기 크기로 나누어 주기 때문에, 기울기 크기에 관계없이 동일하게 움직였습니다. 다만, 스텝 사이즈는 계속 작아졌습니다.
- RMSProp은 합계 대신 이동 평균으로 나누어 줌으로써, 스텝 사이즈가 계속 작아지는 것을 방지했습니다.
- Adam은 모멘텀 방식의 방향을 RMSProp과 같은 방식으로 나누어 주었으며, 한 번 움직이는 양은 대략 m 이하가 되었습니다. 바이어스 보정(Bias correction)을 없애면, 첫 걸음이 $\alpha$의 3배 이상이 되고 목적 함수가 일시적으로 튀어 올랐습니다.
세 가지 방법 모두 '기울기 크기로 나누면 성분별 크기의 차이가 사라진다'는 하나의 아이디어를 조금씩 개선한 것이었습니다. 사용할 때는,
참고 자료
- 伊藤元治・原田耕平・松岡勇気『分析者のための数理最適化入門 アルゴリズムの仕組みから定式化の実践まで、網羅的に理解する』ソシム, 2026年, 第4章
- Duchi, J., Hazan, E., Singer, Y. Adaptive Subgradient Methods for Online Learning and Stochastic Optimization. Journal of Machine Learning Research, 12, 2121–2159, 2011
- Tieleman, T., Hinton, G. Lecture 6e: rmsprop: Divide the gradient by a running average of its recent magnitude(강의 슬라이드)
- Kingma, D. P., Ba, J. Adam: A Method for Stochastic Optimization. ICLR 2015(arXiv:1412.6980)
- RMSprop - PyTorch documentation
- Adam - PyTorch documentation
토론 (Discussion)