
비용 함수의 바닥까지 걸어 내려가는 경사하강법
- 1머신러닝은 데이터에서 규칙을 찾는다, 지도·비지도·강화학습
- 2문제 정의부터 배포까지, 머신러닝 프로젝트의 전 과정
- 3데이터에 가장 잘 맞는 직선을 찾는 선형 회귀
- 4모델이 얼마나 틀렸는지 재는 비용 함수
- 5비용 함수의 바닥까지 걸어 내려가는 경사하강법읽는 중
비용 함수는 모델이 얼마나 틀렸는지를 숫자 하나로 알려준다. 남은 질문은 하나다. 그 숫자를 어떻게 줄이는가?
비용 함수 의 그래프는 그릇 모양이고 바닥이 최적의 파라미터다. 문제는 우리가 그릇의 어디에 서 있는지 모른다는 점이다. 바닥이 왼쪽인지 오른쪽인지도 모른다.
안개가 짙게 낀 산에 서 있다고 해보자. 사방이 안 보여서 계곡이 어디인지 알 수 없다. 할 수 있는 건 발밑의 경사를 느끼고 내리막 쪽으로 한 발 내딛는 것뿐이다. 그걸 반복하면 결국 바닥에 닿는다. 경사하강법(Gradient Descent) 이 정확히 이 방법이다. “발밑의 경사”가 미분값이고 “한 발”이 학습률이다.
기울기의 반대 방향으로 한 걸음
갱신 규칙은 두 줄이 전부다.
는 학습률(learning rate)로 한 번에 얼마나 움직일지를 정하고, 는 현재 위치에서 비용 곡면의 기울기다. :=는 등호가 아니라 “오른쪽을 계산해 왼쪽에 대입한다”는 뜻이고, 코드의 w = w - alpha * dw와 같다.
왜 기울기를 빼는가? 기울기가 양수라는 건 를 키우면 가 올라간다는 뜻이다. 를 줄이려면 반대로 가야 하니 뺀다. 기울기가 음수면 빼기가 곧 더하기가 되어 자연히 오른쪽으로 움직인다. 곡면의 어느 쪽에 서 있든 부호가 알아서 방향을 잡아준다.
그림에서 눈여겨볼 건 걸음 폭이다. 학습률은 처음부터 끝까지 고정인데도 바닥에 가까워질수록 보폭이 저절로 줄어든다. 경사가 완만해지면서 기울기 값 자체가 작아지기 때문이다. 별도의 감속 장치 없이도 바닥 근처에서 조심스러워지는 셈이다.
MSE를 파라미터로 미분하면
선형 회귀의 비용 함수는 잔차를 제곱해 평균 낸 값이다. 예측값 를 대입해 풀어 쓰면 이렇게 된다.
이걸 와 에 대해 각각 편미분한다.
합성함수의 미분(chain rule)을 적용한 결과다. 제곱의 지수 2가 앞으로 나오고, 괄호 안을 로 미분한 가 곱해진다. 로 미분할 때는 괄호 안의 가 1이 되므로 가 붙지 않는다. 두 식의 유일한 차이가 이 이고, 그래서 가 큰 데이터 포인트에서 생긴 오차일수록 를 더 세게 흔든다. 이 비대칭이 곧 실제 문제로 나타난다.
처음부터 구현하기
import numpy as np
X = np.array([60, 75, 85, 95, 110, 120, 140, 155], dtype=float)
y = np.array([2.1, 2.8, 3.2, 3.6, 4.1, 4.5, 5.2, 5.8])
w, b = 0.0, 0.0
lr, n = 1e-5, len(y)
for epoch in range(5000):
y_pred = w * X + b
cost = np.mean((y_pred - y) ** 2)
dw = (2 / n) * np.sum((y_pred - y) * X)
db = (2 / n) * np.sum(y_pred - y)
w -= lr * dw
b -= lr * db
if epoch % 1000 == 0:
print(f"{epoch:5d} cost={cost:.6f} w={w:.6f} b={b:.6f}") 0 cost=16.648750 w=0.008920 b=0.000078
1000 cost=0.003062 w=0.037320 b=0.000201
2000 cost=0.003061 w=0.037322 b=0.000074
3000 cost=0.003059 w=0.037323 b=-0.000053
4000 cost=0.003057 w=0.037324 b=-0.000179첫 epoch에서 16.6이던 비용이 1000번 만에 0.003까지 떨어진다. 그런데 파라미터를 보면 이상하다. 는 0.0373으로 정답인 0.0380에 거의 닿았는데, 는 -0.0002라서 정답인 -0.0818 근처에도 못 갔다.
원인은 입력의 스케일이다. 면적 값이 60에서 155 사이라 에는 이 큰 숫자가 곱해지지만 에는 곱해지지 않는다. 두 기울기의 크기가 수십 배 차이 나는데 학습률은 하나뿐이다. 가 발산하지 않도록 를 작게 잡으면 는 거의 제자리걸음을 하게 된다.
해결책은 입력을 미리 비슷한 범위로 맞춰주는 것이다. 면적을 평균 0, 표준편차 1로 표준화하면 두 기울기의 크기가 맞아떨어지고, 학습률 0.01로 1000번만 돌려도 정규 방정식이 주는 해에 그대로 도달한다. 이 전처리를 Feature Scaling이라 부르고, 경사하강법을 쓰는 한 사실상 필수다.
학습률이 결정하는 것
학습률 는 경사하강법에서 가장 중요한 하이퍼파라미터다. 너무 작으면 바닥까지 수만 번을 걸어야 하고, 너무 크면 바닥을 지나쳐 반대편 벽으로 튕겨 나간다. 튕긴 자리는 경사가 더 가파르니 다음 걸음은 더 크게 튕기고, 몇 번만 반복되면 비용이 무한대로 폭주한다.
같은 데이터에 학습률만 바꿔 3000번씩 돌려보면 차이가 그대로 드러난다.
for lr in [2e-7, 1e-5, 1e-4]:
w, b = 0.0, 0.0
for _ in range(3000):
y_pred = w * X + b
w -= lr * (2 / n) * np.sum((y_pred - y) * X)
b -= lr * (2 / n) * np.sum(y_pred - y)
cost = np.mean((w * X + b - y) ** 2)
print(f"lr={lr:.0e} w={w:<8.4f} b={b:<8.4f} cost={cost:.6f}")lr=2e-07 w=0.0373 b=0.0003 cost=0.003064
lr=1e-05 w=0.0373 b=-0.0001 cost=0.003059
lr=1e-04 w=nan b=nan cost=nan앞의 두 학습률이 3000번에서 사실상 같은 자리에 있는 것은 둘 다 앞 절의 스케일 문제에 걸려 가 제자리이기 때문이다. 이 데이터에서 셋을 갈라놓는 것은 마지막 값이다. 0.0001에서는 첫 갱신부터 비용이 16.6에서 32.2로 뛰고, 이후 매 epoch마다 대략 두 배씩 커지다가 부동소수점 범위를 넘어 nan이 된다. 실무에서는 0.1, 0.01, 0.001처럼 10배씩 낮춰가며 비용 곡선이 매끄럽게 떨어지는 가장 큰 값을 고른다. 비용이 진동하거나 오히려 커지면 그 학습률은 너무 크다.
한 걸음에 데이터를 얼마나 쓸 것인가
지금까지 본 방식은 매 갱신마다 전체 데이터로 기울기를 계산했다. 이걸 배치 경사하강법(Batch Gradient Descent) 이라 부른다. 데이터가 수백만 건이면 한 걸음을 떼는 데 수백만 개를 훑어야 해서 현실적이지 않다.
| 방식 | 한 번 갱신에 쓰는 데이터 | 성격 |
|---|---|---|
| 배치(Batch) | 전체 | 방향이 정확하지만 느림 |
| 확률적(SGD) | 1건 | 매우 빠르지만 방향이 흔들림 |
| 미니배치(Mini-batch) | 32, 64, 128건 | 속도와 안정성의 절충 |
실무 표준은 미니배치다. 데이터를 섞어 작은 묶음으로 자르고 묶음 하나마다 파라미터를 한 번씩 갱신한다. 기울기 방향은 전체를 훑을 때보다 부정확하지만 같은 시간에 갱신 횟수가 훨씬 많아져 결과적으로 더 빨리 내려가고, 흔들리는 방향이 얕은 국소 최솟값을 빠져나오는 데 도움이 되기도 한다. SGD라는 이름은 엄밀히 1건짜리 방식을 가리키지만 실무에서는 미니배치까지 묶어 통칭으로 쓴다. torch.optim.SGD도 배치 크기가 얼마인지는 모르고, 넘겨받은 기울기를 갱신식에 그대로 넣을 뿐이다.
언제 멈출 것인가
- 최대 반복 횟수: 정한 epoch만큼 돌고 끝낸다. 가장 단순하지만 이미 수렴한 뒤에도 계속 도는 낭비가 생긴다.
- 비용 변화량: 직전 epoch과 비용 차이가 임계값보다 작으면 멈춘다. 예를 들어 이다.
- 기울기 크기: 기울기의 크기가 거의 0이면 바닥에 도달한 것으로 보고 멈춘다.
보통 최대 반복 횟수를 안전장치로 걸어두고 비용 변화량으로 조기 종료를 건다. 기울기 크기 기준은 임계값을 데이터 스케일에 맞춰야 해서 손이 더 간다.
마치며
경사하강법은 한 문장으로 줄어든다. 기울기의 반대 방향으로 조금씩 이동한다.
이 단순한 규칙이 살아남은 이유는 비용 함수의 전체 모양을 몰라도 된다는 데 있다. 해를 한 번에 구하는 방법은 문제 구조가 정확히 알려져 있어야 하고 특성 수가 늘면 계산량을 감당하지 못한다. 경사하강법은 현재 위치의 기울기 하나만 알면 다음 걸음을 뗄 수 있고, 그래서 파라미터가 수십억 개인 언어 모델에서도 그대로 쓰인다.
대신 사람이 정해줘야 하는 것이 남는다. 얼마나 큰 보폭으로 갈지, 언제 멈출지, 그리고 입력의 스케일을 어떻게 맞출지다. 특히 마지막 항목은 선택이 아니다. 스케일이 제각각이면 보폭 하나로 모든 파라미터를 감당할 수 없고, 이 글의 예제에서 가 5000번을 돌고도 제자리였던 이유가 정확히 그것이다.
함께 보면 좋은 글
- 비용 함수 : 경사하강법이 내려가는 그 지형의 정의
- 선형 회귀 : 정규 방정식으로 같은 해를 한 번에 구하는 방법
- 특성 스케일링 : 입력 범위를 맞춰 수렴을 안정시키는 전처리
- 옵티마이저 : 학습률을 파라미터마다 자동으로 조절하는 개선안