머신러닝 기초 (3) - 경사하강법: 손실을 줄이는 법
학습이란 손실을 최소로 만드는 파라미터를 찾는 문제라는 관점에서 출발해, 경사하강법의 갱신식과 학습률의 역할, 볼록, 비볼록 문제, 배치, 미니배치, SGD의 차이를 numpy와 SGDRegressor로 확인하는 3편입니다.
머신러닝 기초 시리즈의 3편입니다. 2편의 “손실 함수: 모델이 최소화하는 것”에 이어집니다.
학습은 손실을 최소화하는 문제다
2편에서 손실 함수를 정의했습니다. 손실은 모델의 예측이 정답에서 얼마나 벗어났는지를 하나의 숫자로 요약한 값이고, 회귀의 기본은 MSE, 즉 (정답 - 예측)을 제곱해 평균한 값이었습니다.
그런데 손실이 큰지 작은지는 모델의 파라미터에 달려 있습니다. 1편에서 본 1변수 선형회귀 pred = w * x + b를 생각해 보면, 예측은 w와 b로 결정되고 손실도 결국 w, b의 함수입니다. 데이터 x, y는 고정이고 우리가 움직일 수 있는 것은 w, b뿐입니다.
그러면 “학습한다”는 말의 의미가 분명해집니다.
학습이란 손실을 가장 작게 만드는 파라미터를 찾는 문제다. 모델이 데이터에서 배운다는 말은, 손실이라는 값을 최소로 낮추는
w,b를 찾아간다는 뜻이다.
파라미터가 두 개면 손실은 (w, b) 평면 위의 지형이 되고, 우리는 그 지형에서 가장 낮은 지점을 찾으려 합니다. 문제는 이 지형을 통째로 볼 수 없다는 것입니다. 파라미터가 수백만 개인 모델에서 모든 조합의 손실을 계산해 최솟값을 고르는 것은 불가능합니다. 그래서 현재 위치에서 얻을 수 있는 국소 정보만으로 조금씩 내려가는 방법이 필요합니다. 그것이 경사하강법입니다.
기울기 반대 방향으로 내려간다
파라미터가 하나뿐이라고 해봅시다. 손실 L(w)는 w에 대한 곡선이고, 그 곡선의 기울기(미분값) dL/dw는 “w를 조금 키우면 손실이 어느 방향으로 변하는가”를 알려줍니다.
- 기울기가 양수면 오른쪽으로 갈수록 손실이 커진다는 뜻이므로,
w를 줄여야 손실이 낮아집니다. - 기울기가 음수면 오른쪽으로 갈수록 손실이 작아지므로,
w를 키워야 합니다.
두 경우 모두 “기울기의 반대 방향으로 움직인다”로 요약됩니다. 이를 그대로 식으로 쓰면 다음과 같습니다.
1
w = w - lr * grad_w # grad_w = dL/dw, 손실을 w로 미분한 값
lr은 학습률(learning rate)로, 한 번에 얼마나 크게 움직일지를 정하는 값입니다. 기울기가 양수면 w가 줄고, 음수면 - lr * grad_w가 양수가 되어 w가 늘어납니다. 부호를 따로 신경 쓸 필요 없이 이 한 줄이 두 경우를 모두 처리합니다.
파라미터가 여럿이어도 원리는 같습니다. 각 파라미터에 대해 손실의 기울기(편미분)를 구하고, 각자 자기 기울기의 반대 방향으로 움직입니다. 여러 파라미터의 기울기를 모은 것을 그래디언트(gradient)라 부르며, 그래디언트는 손실이 가장 가파르게 증가하는 방향을 가리킵니다. 그 반대로 가는 것이 손실을 가장 빠르게 줄이는 국소적 방향입니다.
MSE와 선형 모델의 경우 기울기는 직접 미분해서 얻습니다. pred = w * x + b, error = pred - y일 때 데이터 n개에 대한 기울기는 이렇게 됩니다.
1
2
grad_w = (2 / n) * (error * x).sum() # w에 대한 MSE 기울기
grad_b = (2 / n) * error.sum() # b에 대한 MSE 기울기
여기서 계산이 낯설어도 괜찮습니다. 핵심은 공식 유도가 아니라 “예측과 정답의 오차(error)로부터 각 파라미터를 어느 쪽으로 얼마나 움직일지가 결정된다”는 구조입니다.
학습률이 결정하는 것
갱신식에서 방향은 기울기가 정하고, 보폭은 학습률 lr이 정합니다. 학습률은 경사하강법에서 가장 먼저 조정하는 값이고, 잘못 잡으면 학습 자체가 실패합니다.
- 너무 크면 발산한다. 보폭이 지나치게 크면 최저점을 지나쳐 반대편 더 높은 곳으로 튀고, 다음 걸음은 더 크게 튑니다. 손실이 줄기는커녕 매 걸음 커지다 무한대로 발산합니다.
- 너무 작으면 느리다. 보폭이 작으면 방향은 옳아도 한 걸음에 조금씩만 내려가, 정해진 반복 횟수 안에 최저점 근처에 도달하지 못합니다.
적절한 학습률은 이 둘 사이에 있습니다. 값 자체는 문제마다 다르므로, 보통 몇 개의 후보(예: 0.001, 0.01, 0.1)를 시도해 손실이 안정적으로 줄어드는 값을 고릅니다.
numpy로 몇 줄로 확인하기
말로 설명한 것을 코드로 확인합니다. scikit-learn의 load_diabetes에서 변수 하나(BMI)만 뽑아 1변수 선형회귀를 경사하강으로 풀어 봅니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import numpy as np
from sklearn.datasets import load_diabetes
data = load_diabetes()
x = data.data[:, 2] # BMI 한 변수만 사용
y = data.target
# 경사하강은 변수 스케일에 민감하다. 평균 0, 표준편차 1로 맞춘다
x = (x - x.mean()) / x.std()
y = (y - y.mean()) / y.std()
def gradient_descent(lr, n_iter=50):
w, b = 0.0, 0.0
n = len(x)
for _ in range(n_iter):
pred = w * x + b
error = pred - y # 예측 - 정답
grad_w = (2 / n) * (error * x).sum()
grad_b = (2 / n) * error.sum()
w = w - lr * grad_w # 기울기 반대 방향으로 한 걸음
b = b - lr * grad_b
return w, b
gradient_descent(lr=0.1) # (약 0.586, 약 0.0), 최적값으로 수렴
w가 약 0.586으로 수렴합니다. 표준화한 1변수 회귀에서 이 값은 두 변수의 상관계수와 같고, 실제로 np.corrcoef(x, y)[0, 1]도 0.586입니다. 경사하강이 우리가 아는 정답을 스스로 찾아온 것입니다.
이제 학습률만 바꿔 세 경우를 비교합니다.
1
2
3
gradient_descent(lr=0.001) # (약 0.056, ~0), 너무 작아 50번으로는 한참 못 감
gradient_descent(lr=0.1) # (약 0.586, ~0), 적절한 보폭, 정답에 도달
gradient_descent(lr=1.5) # (약 -6.6e14, ...), 보폭이 커서 발산
lr=0.001은 방향은 맞지만 50번으로는 0.586의 10분의 1도 못 갑니다(반복을 수천 번 늘리면 결국 도달합니다). lr=1.5는 걸음마다 오차가 커집니다. 반복 중 손실을 찍어보면 차이가 분명합니다. lr=0.1은 손실이 1.0에서 0.656으로 줄어 멈추는 반면, lr=1.5는 1.0 → 약 353 → 약 36만 → 1e29로 폭발합니다.
위에서 변수를 표준화한 것은 장식이 아니다. 변수마다 스케일이 크게 다르면 손실 지형이 한 방향으로 길쭉해져, 어떤 파라미터에는 큰 보폭이, 다른 파라미터에는 작은 보폭이 필요해진다. 하나의 학습률로는 둘을 동시에 만족시키기 어렵다. 그래서 경사하강 기반 모델에는 스케일링이 사실상 필수다. 전처리는 12편에서 다룬다.
배치, 미니배치, SGD
위 코드는 매 걸음에서 전체 데이터 n개로 기울기를 계산했습니다. 이렇게 전체 데이터를 한 번에 쓰는 방식을 배치 경사하강법(batch gradient descent)이라 합니다. 방향은 가장 정확하지만, 데이터가 수백만 건이면 한 걸음마다 전부를 훑어야 해서 느립니다. 반대편 끝에는 확률적 경사하강법(SGD, stochastic gradient descent)이 있습니다. 데이터 한 건으로 기울기를 어림잡아 곧바로 한 걸음 내딛는 방식으로, 방향은 매번 흔들리지만 걸음이 훨씬 잦고 대용량에서 빠릅니다. 실무의 표준은 그 중간인 미니배치(mini-batch) 방식입니다. 32, 64, 128건 같은 작은 묶음으로 기울기를 계산해, 계산 효율과 방향 안정성을 절충합니다. 딥러닝에서 말하는 “배치 크기”가 바로 이 묶음의 크기입니다.
scikit-learn의 SGDRegressor가 이 확률적 방식을 구현합니다. 이름의 S가 stochastic입니다. 위와 같은 데이터에 적용하면 결과가 사실상 일치합니다.
1
2
3
4
5
6
from sklearn.linear_model import SGDRegressor
model = SGDRegressor(loss="squared_error", penalty=None,
eta0=0.01, max_iter=1000, random_state=42)
model.fit(x.reshape(-1, 1), y)
model.coef_, model.intercept_ # (약 0.587, 약 -0.003), 직접 짠 경사하강과 같은 답
eta0은 초기 학습률이고, penalty=None은 규제를 끈 설정입니다(규제는 6편에서 다룹니다). random_state=42는 데이터를 섞는 순서를 고정해 결과를 재현 가능하게 합니다. 직접 짠 경사하강과 같은 답에 도달한다는 점만 확인하면 충분합니다.
볼록 문제와 비볼록 문제
경사하강법은 항상 전역 최솟값을 찾아줄까요. 그렇지 않습니다. 답은 손실 지형의 모양에 달려 있습니다.
손실 함수가 볼록(convex)이면 지형에 웅덩이가 하나뿐이고, 그 바닥이 곧 전역 최솟값입니다. 이 경우 어디서 출발하든 경사하강은 같은 지점에 도달합니다. 위에서 다룬 MSE 선형회귀가 여기에 해당하고, 로지스틱 회귀의 손실(로그 손실)도 파라미터에 대해 볼록입니다. 9편에서 다룰 선형 모델들이 안정적으로 학습되는 근본 이유입니다.
반면 신경망의 손실은 비볼록(non-convex)입니다. 웅덩이가 여러 개고, 지역 최솟값과 안장점이 곳곳에 있습니다. 경사하강은 국소 정보만 보고 내려가므로 출발점(초기값)과 학습률에 따라 도착하는 지점이 달라질 수 있습니다. 딥러닝에서 초기화 방법, 학습률 스케줄, 옵티마이저 같은 최적화 기법이 큰 비중을 차지하는 이유가 여기에 있습니다.
선형회귀에는 미분해서 곧바로 최적해를 구하는 닫힌 형태(정규방정식)가 존재한다. 그런데도 경사하강을 쓰는 이유는, 대부분의 모델에는 그런 닫힌 해가 없고, 있더라도 파라미터가 아주 많거나 데이터가 클 때는 경사하강이 더 실용적이기 때문이다. 경사하강은 손실만 미분할 수 있으면 모델 종류를 가리지 않고 쓸 수 있는 일반적인 방법이다.
정리
| 개념 | 한 줄 요약 |
|---|---|
| 학습 | 손실을 최소로 만드는 파라미터 w, b를 찾는 문제 |
| 경사하강법 | 기울기 반대 방향으로 조금씩 이동: w = w - lr * grad_w |
| 그래디언트 | 손실이 가장 가파르게 커지는 방향. 그 반대로 내려간다 |
| 학습률 | 보폭. 너무 크면 발산, 너무 작으면 느림 |
| 변수 스케일 | 스케일이 다르면 하나의 학습률로 학습이 어렵다 → 스케일링 필요 |
| 배치/미니배치/SGD | 한 걸음에 쓰는 데이터 양의 차이. 실무 표준은 미니배치 |
| 볼록 vs 비볼록 | 볼록(선형과 로지스틱)은 어디서 출발해도 전역 최솟값. 비볼록(신경망)은 출발점에 따라 달라짐 |
다음 편에서는 손실을 잘 줄인 모델이 왜 새 데이터에서는 틀리는지, 즉 과대적합과 일반화를 다룹니다. train/valid/test 분리와 편향-분산 관점을 정리합니다.