[파이썬 프로그래밍 18] 머신러닝에서 오버피팅을 피하는 방법

Home / 파이썬 프로그래밍 / [파이썬 프로그래밍 18] 머신러닝에서 오버피팅을 피하는 방법

머신러닝에서 오버피팅을 피하는 방법

머신러닝에서 어떻게 어떻게 오버피팅을 피할 수 있을까요? 학습하는 데이터가 충분히 많으면 피할 수 있습니다.
이전 글에서는 학습에 사용한 데이터 갯수가 10개에 불과했습니다. 그런 데이터를 복잡한 곡선으로 표현하다보니 데이터에 있는 잡음까지 정확하게 곡선으로 표현하려하다보닌 생긴 현상입니다.

이번에는 이보다 훨씬 많은 100개의 데이터 가지고 학습해보겠습니다. 곡선은 10개의 데이터로 학습을 했을때 오버피팅이 생겼던, 입력데이터의 9제곱까지 포함하는 수식으로 하겠습니다.

$$
y=a_0+a_1x+a_2x^2+\dotsb+a_9x^9
$$

100개의 데이터를 가지고 하는 학습을 통해, 데이터를 가장 잘 표현하는 곡선 수식의 $a_0, a_1, \dotsb, a_9$ 값을 찾는 겁니다.
데이터의 갯수(좀 더 정확하게는 입력 데이터의 갯수)가 곡선을 나타내는 $a_k$의 갯수보다 10배 많은 경우입니다.

코드는 지난번 코드와 거의 같습니다. 처음에 100개의 가상 데이터를 만드는 과정만 추가했습니다.

import random
datalen = 100
random.seed(1)
x = [random.random()*10.0 for k in range(datalen)]
y = [x[k]**2-10.0*x[k]+30+random.gauss(0.0, 1.0) for k in range(datalen)]
Data = [[x[k],y[k]] for k in range(datalen)]

X9 = [[x[k]**n for n in range(1,10)] for k in range(len(Data))]

from sklearn import linear_model
reg9 = linear_model.LinearRegression()
reg9.fit(X9,y)
print(reg9.intercept_)
print(reg9.coef_)
print(reg9.score(X9,y))

xp9 = [0.1*k for k in range(101)]
Xp9 = [[xp9[k]**n for n in range(1,10)] for k in range(101)]
yp9 = reg9.predict(Xp9)

import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.dpi'] = 150
plt.xlim(0,10)
plt.ylim(0,30)
plt.plot(x, y, "x")
plt.plot(xp9, yp9)

가상의 데이터는 전체적으로 포물선 모양($y=x^2-10x+30$)으로 나오게 만들었고, random.gauss(0.0, 1.0) 함수를 사용해 가우스 잡음(Gaussian noise)을 추가했습니다.

오버피팅에서 보였던 지나친 출렁임이 이번 곡선에서는 사라졌습니다. 비록 데이터 자체는 가우스 잡음때문에 말끔한 곡선 모양이 아닌 곡선 모양에서 좀 퍼져있는 모양입니다. 그럼에도 불구하고 LinearRegression을 이용해 찾은 곡선은 데이터가 전체적으로 어떻게 변하는지를 비교적 잘 표현합니다. 학습 데이터에서 볼 수 없는 출렁임 다시 말해 오버피팅도 없습니다. 이런 경우, 학습을 하지 않은 데이터를 사용해 결과값을 예측한 다음 실제 결과와 비교해보면 오차가 작게 나옵니다. 비교적 빋을만한 예측을 한다는 얘기입니다.

이렇게 학습할 데이터가 많으면 많을수록 오버피팅이 생길 가능성을 줄어듭니다. 하지만, 데이터의 갯수가 너무 적으면 오버피팅이 생길 가능성이 있으므로 이를 피하는 방법을 생각해야 합니다.

학습 데이터의 크기가 작은 경우
학습 데이터의 갯수가 적을 경우에 우선 할 수 있는 일은 좀 더 단순한 곡선을 사용해 데이터를 표현하는 것입니다. 지난번 글에 이미 했듯이 입력 데이터의 제곱까지만 포함하는 포물선 곡선($y=a_0+a_1x+a_2x^2$)으로 표현한다던가, 세제곱까지 포함하는 곡선($y=a_0+a_1x+a_2x^2+a_3x^3$)으로 표현한다던가 하는 식으로 말입니다.

한편 $y = a_0+a_1x+a_2x^2+\dotsb+a_9x^9$와 같이 복잡해질 수 있는 곡선을 적욯하면서도 오버피팅을 줄이는 방법이 있습니다. 그 전에 먼저 지난번 글에서 다뤴던 학습 데이터를 다시 준비해 보겠습니다.

Data = [[8.4, 17.1], [7.6, 11.4], [1.2, 19.5], [2.7, 12.6], [5.1, 4.7], 
[4.0, 5.7], [7.8, 14.0], [3.0, 8.2], [4.8, 5.6], [5.8, 4.6]]
x = [Data[k][0] for k in range(len(Data))]
y = [Data[k][1] for k in range(len(Data))]

이 데이터에 LinearRegression를 변형한 클래스인 Ridge라는 클래스를 적용하겠습니다. Ridge 클래스를 사용하는 방법은 LinearRegression 클래스를 사용하는 방법과 거의 같습니다. 처음에 클래스 객체를 만드는 방법마 바꿔주면 됩니다.
LinearRegression클래스에서는 reg = linear_model.LinearRegression()을 써서 클래스 객체를 만들었다면, Ridge 클래스에서는 rreg = linear_model.Ridge(alpha = 1.0) 을 써서 클래스 객체를 만들면 됩니다. 나머지는 똑같습니다. 클래스 객체를 만들때 넣어주는 값인 alpha값이 매우 중요한 역할을 합니다. 클래스 객체를 만들때 alpha값을 안 넣어줘도 됩니다. 이때는 alpha에는 기본값인 1을 넣습니다.

데이터로부터 9제곱까지 포함하는 입력데이터를 만들고 실행해 보겠습니다. alpha값이 1.0이면 결과가 정확하지 않을 수도 있다는 경고 메시지가 뜹니다. alpha값으로 10을 넣으면 이 경고 베시지는 사라집니다.

X9 = [[x[k]**n for n in range(1,10)] for k in range(len(Data))]

# from sklearn import linear_model # already imported
rreg = linear_model.Ridge(alpha=10.0)
rreg.fit(X9,y)
print(rreg.intercept_)
print(rreg.coef_)
print(rreg.score(X9,y))

xp9 = [0.1*k for k in range(101)]
Xp9 = [[xp9[k]**n for n in range(1,10)] for k in range(101)]
yp9 = rreg.predict(Xp9)

plt.xlim(0,10)
plt.ylim(0,30)
plt.plot(x, y, "x")
plt.plot(xp9, yp9)

오버피팅이 많이 줄었음을 확인할 수 있습니다. 유명한 앤드류 응 교수의 비디오 강의를 보면 오버피팅을 다루는 강의에서 손실함수에 람다(lambda: $\lambda$)값이 등장합니다. 우리가 다루는 데이터를 기준으로 다시 고쳐쓰면

$$
J(\boldsymbol a) = \frac{1}{10}\left [\sum_{i=0}^{9} \left (y_i-f(x_i) \right)^2 +\lambda \sum_{j=0}^{9}a_j^2 \right ] \\
f(x_i) = a_0+a_1x_i+a_2x_i^2+\dotsb+a_9x_i^9
$$

$J(\boldsymbol a)$ 를 손실함수(loss function)라고 합니다. 학습하는 과정에서 손실함수를 작게 만들어 데이터를 잘 표현하는 곡선을 만드는 $a_0, a_2, \dotsb, a_9$ 를 찾게 됩니다. 이때 손실함수에 포함된 $\lambda \sum \limits _{j=0}^{9}a_j^2 $ 은 $a_j$ 값을 작게 만들어 곡선을 부드럽게 만드는 역할을 합니다. 이 과정에서 오버피팅도 줄어듭니다. 이런 과정을 regularization이라고 합니다.

scikit learn의 Ridge에서 사용하는 alpha값이 위에서 설명한 람다 ($\lambda$)값에 해당합니다. Ridge를 사용할때 너무 큰 alpha값을 사용하면 오버피팅은 없어지지만, 학습 데이터를 잘 표현하지 못하는 문제가 발생합니다. alpha = 10000일때의 경우를 보죠.

rreg2 = linear_model.Ridge(alpha=10000.0)
rreg2.fit(X9,y)
print(rreg2.intercept_)
print(rreg2.coef_)
print(rreg2.score(X9,y))

xp9 = [0.1*k for k in range(101)]
Xp9 = [[xp9[k]**n for n in range(1,10)] for k in range(101)]
yp9 = rreg2.predict(Xp9)

plt.xlim(0,10)
plt.ylim(0,30)
plt.plot(x, y, "x")
plt.plot(xp9, yp9)

rreg2.score()의 결과가 0.842로 alpha = 10.0일때의 0.985보다 1에서 더 멀어졌음을 확인할 수 있습니다. 그 만큼 학습 데이터를 잘 표현하지 못한다는 얘기입니다. 학습데이터도 잘 표현하면서 오버피팅도 줄이는 적당한 alpha값을 찾는 것이 관건입니다.