훈련 오차를 0으로 만드는 법과 그 대가
지금까지 여섯 편은 전부 손실을 어떻게 내리느냐였다. 걸음의 크기, 미분이 흐르는 길, 층을 쌓는 법, 데이터를 쪼개는 법. 이번 편은 방향이 다르다. 손실을 끝까지 내렸을 때 무슨 일이 생기는지 본다.
점 18개, 차수를 올린다
곡선 하나에서 점 18개를 뽑고 노이즈를 얹는다. 노이즈의 표준편차가 0.25 이므로,
새 데이터에서 기대되는 평균제곱오차는 0.0625 아래로 못 내려간다. 그 아래는
노이즈 자체라서 맞힐 대상이 아니다.
훈련 오차에는 이 하한이 걸리지 않는다. 훈련점은 이미 본 값이라 노이즈까지 외울
수 있고, 실제로 아래 표에서 훈련 오차는 5차부터 0.0625 를 통과해 내려간다.
그게 바로 이 글의 주제다.
f = lambda x: np.sin(1.6*x) + 0.35*x
x_tr = np.sort(rng.uniform(-3, 3, 18))
y_tr = f(x_tr) + 0.25 * rng.standard_normal(18)
다항식 차수를 1부터 17까지 올리면서, 훈련에 쓴 18개와 새로 뽑은 500개에서
각각 오차를 잰다. 맞추는 방식은 이렇다. x^17 은 x=3 에서 1.3e8 이라 그대로
쓰면 열끼리 크기가 8자리 차이 나므로, 각 거듭제곱 열을 훈련 데이터 기준으로
중심화하고 표준화한 뒤 맞춘다. 상수항은 벌점에서 뺀다.
def design(x, deg, mu=None, sd=None):
A = np.vander(x, deg+1, increasing=True)[:, 1:] # 상수항 제외
if mu is None: mu, sd = A.mean(0), A.std(0) + 1e-12
return np.hstack([np.ones((len(x), 1)), (A - mu) / sd]), mu, sd
def fit(deg, lam=0.0):
A, mu, sd = design(x_tr, deg)
if lam == 0:
c = np.linalg.lstsq(A, y_tr, rcond=None)[0]
else:
P = np.eye(A.shape[1]); P[0, 0] = 0 # 상수항엔 벌점 없음
c = np.linalg.solve(A.T @ A + lam * P, A.T @ y_tr)
return c
아래 표의 “계수” 는 전부 이 표준화된 기저에서의 계수다. 기저를 바꾸면 값이 달라진다는 점은 뒤에서 다시 짚는다.
차수 훈련 검증 가장 큰 계수
1 0.38697 0.7104 0.68
3 0.08564 0.3343 1.73
6 0.04163 0.0934 4.41
8 0.01869 3.8295 57.08
12 0.01721 587.0880 1144
17 0.00000 38959385960 2.2e+07
두 선은 갈라진다
훈련 오차는 끝까지 내려간다. 당연하다. 차수를 올릴 때마다 곡선이 자유로워지고,
17차에 이르면 파라미터 18개로 점 18개를 지나므로 정확히 통과한다. 훈련
오차 3e-17 은 부동소수점 0이다.
검증 오차는 6차에서 0.0934 로 최저를 찍고 돌아선다. 노이즈 바닥 0.0625 에
꽤 가깝다. 그 뒤로는 8차에서 3.83, 12차에서 587, 17차에서 3.9e10 이다.
훈련에서 마지막 0.04 를 짜내는 동안 검증에서 열두 자리를 잃었다.
무슨 일이 벌어졌는지는 계수를 보면 나온다. 6차까지는 가장 큰 계수가 4.4 인데
17차에서는 2.2e+07 이다. 점 18개를 정확히 지나려면 그 사이를 극단적으로
휘어야 하고, 그 휘어짐이 훈련점 사이에서 폭발한다. 훈련점 위에서는 오차가
0이므로 훈련 손실은 그것을 볼 수 없다.
계수 값은 기저에 딸린 숫자라는 단서를 붙여 둔다. 표준화하지 않은 원래 단항식
기저에서 np.polyfit 으로 재면 최대 계수가 17차에서 455 이고, 6차 1.67 이
8차 1.36 보다 오히려 크다. 같은 곡선을 다른 좌표로 적은 것뿐인데 숫자도
단조성도 달라진다. 폭발을 보여 주는 지표로는 쓸 수 있지만, 그 절대값에 의미를
두면 안 된다.
이게 과적합의 정체다. 모델이 신호를 배운 게 아니라 노이즈의 좌표를 외운 것이다. 다음 번 뽑은 점들은 노이즈가 다르니 외운 값이 맞을 리 없다.
차수를 줄이는 것만이 답은 아니다
여기서 흔한 결론은 “모델을 작게 하라” 다. 절반만 맞다. 17차 그대로 두고
계수에 벌점만 붙여 보자. 손실에 lambda * (계수 제곱합) 을 더하는 릿지이고,
위 fit 의 lam 이 그것이다. 벌점은 표준화된 계수에 걸린다 - 릿지는
스케일 불변이 아니라서, 같은 lambda 를 원래 단항식 기저에 걸면 완전히 다른
결과가 나온다(그 경우 lambda=0.01 의 검증 오차는 1.1e6 이다).
17차 고정 훈련 검증 가장 큰 계수
lambda 0 0.00000 38959385960 2.2e+07
lambda 1e-4 0.02105 3.4008 10.09
lambda 1e-2 0.03354 0.1354 3.13
lambda 0.1 0.06441 0.3578 1.77
lambda 1.0 0.11434 0.7403 1.15
lambda = 0.01 에서 검증 오차가 0.1354 다. 차수를 6으로 낮춘 모델의 0.0934
에 근접한다. 같은 17차 모델이다. 표현력을 줄인 게 아니라, 그 표현력을 쓰는
방식에 값을 매겼을 뿐이다.
계수가 2.2e+07 에서 3.13 으로 내려온 것이 그 값이 한 일이다. 17차 곡선을
쓸 수는 있지만 심하게 휘려면 손실을 지불해야 하므로, 최적화가 알아서 완만한
쪽을 고른다.
벌점이 너무 세면 반대로 간다. lambda = 1.0 에서는 훈련 오차마저 0.114 로
올라가고 검증도 0.74 로 나빠진다. 신호까지 눌러 버린 것이다.
무엇을 보고 멈출 것인가
위 표에서 검증 오차를 알 수 있었던 이유는 정답을 아는 데이터를 500개 따로 갖고 있었기 때문이다. 실제로는 그 500개가 곧 검증 세트다.
여기서 규칙 하나가 나온다. 훈련 손실은 언제 멈출지 알려 주지 못한다. 끝까지 내려가기 때문이다. 멈추는 시점은 훈련에 쓰지 않은 데이터로만 정할 수 있다. 그리고 그 데이터로 하이퍼파라미터를 고르는 순간 그것도 일종의 훈련이 되므로, 최종 성능은 또 다른 데이터에서 재야 한다.
그래서
- 훈련 오차는 파라미터가 늘면 계속 내려간다. 0까지 간다. 그 값은 성능이 아니다
- 검증 오차는 U자를 그린다. 이 실험에서는 6차가 바닥이고, 노이즈 바닥
0.0625에0.0934까지 접근했다 - 과적합은 표현력의 문제라기보다 제약 없는 표현력의 문제다. 17차에 릿지
0.01만 걸면 검증이3.9e10에서0.1354로 돌아온다 - 벌점은 세면 세는 대로 해롭다. 신호까지 누른다
- 언제 멈출지는 훈련에 쓰지 않은 데이터만 답할 수 있다
여기까지가 층을 쌓는 이야기였다. 다음 편부터는 구조가 다른 층 하나를 본다. 가중치를 학습된 상수로 갖는 대신 매번 입력에서 계산하는 층이다.
Comments