[파이썬 프로그래밍 17] 곡선으로 예측(regression)하는 머신러닝과 오버피팅(overfitting: 과적합)

Home / 파이썬 프로그래밍 / [파이썬 프로그래밍 17] 곡선으로 예측(regression)하는 머신러닝과 오버피팅(overfitting: 과적합)

곡선으로 예측(regression)하는 머신러닝과 오버피팅(overfitting: 과적합)

아래의 표와 같은 가상의 데이터가 있습니다.

$$\begin{array}{cc}x&y\\\hline8.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\end{array} $$

총 10개의 $x,y$쌍이 있습니다. 이 데이터를 표의 형식과 비슷하게 2차원 목록(목록의 목록)으로 만들려면 아래와 같이 됩니다.

$$\left[\begin{array}{c}
[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]
\end{array} \right] $$

좀더 정확하게 파이썬의 2차원 목록 형식으로 다시 쓰면 아래와 같습니다.

$$ [[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]] $$

파이썬의 scikit learn을 이용해 머신러닝을 할때 많이 사용할 수 있는 데이터의 형식입니다. 이를 파이썬 코드로 옮겨보겠습니다.

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]]

이 데이터가 어떤 데이이터인지 그래프로 그려 보겠습니다. 그래프를 그릴려면 Data에 저장된 2차원 목록에서 $x$값과 $y$값을 따로 분리해 1차원 목록을 만들어야합니다. 아래의 코드가 그걸 합니다.

x = [Data[k][0] for k in range(len(Data))]
y = [Data[k][1] for k in range(len(Data))]

Data는 목록의 목록(2차원 목록)입니다. 목록안에 총 10개의 목록이 들어있음을 알고 있습니다. 만약에 모른다면 len이라는 함수를 쓰면 됩니다. 만약의 숫자의 목록이라면 목록속에 숫자가 몇개 들어있는지를 알려주고, 2차원목록이라면 목록속에 몇개의 목록이 들어있는지를 알려줍니다.

위의 코드에서는 10개의 목록이 들어있으니 10이라는 결과를 내보냅니다. 이를 range안에 넣었으니 결국 range(len(Data))는 range(10)과 같습니다. range(10)이 for를 쓸때 만드는 목록은 $[0,1,2,3,4,5,6,7,8,9]$ 입니다. 따라서 위의 코드로 만드는 목록은 다음과 같습니다.

x = [Data[0][0], Data[1][0], Data[2][0], Data[3][0], Data[4][0], Data[5][0], Data[6][0], Data[7][0], Data[8][0], Data[9][0]]
y = [Data[0][1], Data[1][1], Data[2][1], Data[3][1], Data[4][1], Data[5][1], Data[6][1], Data[7][1], Data[8][1], Data[9][1]]

그래프를 그리기위한 필요한 목록을 준비했으니, 이제 그래프를 그려보겠습니다.

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.xlim(0,10)은 그래프를 그릴때 $x$축의 범위를 0에서 10사이로 만들고, plt.ylim(0,30)은 $y$축의 범위를 0에서 30사이로 만듭니다.

이전 글에서 한 방법과 같이 scikit learn을 이용해 직선으로 위의 데이터를 표현해보겠습니다. 직선을 나타내는 수식 $y=a_0+a_1x$을 가지고 보면, 데이터를 가장 장 표현하는 $a_0$과 $a_1$의 값을 작업을 합니다. 머신러닝에서는 “학습한다”(learn)고 합니다.

이 작업을 하려면 지난 글에 이미 설명했듯이, 입력 데이터를 2차원 목록으로 만들어야합니다. 이번에도 이렇게 만든 2차원 목록을 변수 X(대문자)에 저장하겠습니다. 파이썬에서는 같은 알파벳이라고 하더라도 변수를 소문자로 쓴 것과 대문자를 쓴 것은 다른 변수를 의미한다는 점을 기억할 필요가 있습니다. 결과 데이터는 1차원 목록을 쓸 수 있으므로, 변수 y를 그대로 쓰면 되겠습니다.

X = [[x[m]] for m in range(len(Data))]

from sklearn import linear_model
reg = linear_model.LinearRegression()
reg.fit(X,y)
print(reg.intercept_)
print(reg.coef_)

xp = [0.1*k for k in range(101)]
Xp = [[xp[m]] for m in range(101)]
yp = reg.predict(Xp)

plt.xlim(0,10)
plt.ylim(0,30)
plt.plot(x, y, "x")
plt.plot(xp, yp)

reg.fit(X,y) 함수를 실행하면 데이터를 가장 잘 표현하는 직선을 찾습니다. 그 결과로 reg.intercept_는 직선이 그래프의 세로축과 만나는 지점의 세로축 값을 나타내고, reg.coef_의 값은 직선의 기울기를 나타냅니다. 직선을 나타내는 수식의 $a_0$과 $a_1$에 해당하는 값입니다. 이들 값이 각각 10.272와 0.013이므로 이 직선을 나타내는 수식은

$$ y = 10.272+0.013x $$
가 됩니다.

그래프를 보면 금방 알 수 있듯이, 직선으로 데이터를 표현하기에 상당히 많이 부족합니다. 데이터가 양쪽은 위로 올라가고 가운데는 아래로 내려가는 곡선 모양을 하고 있어 직선으로 이를 표현하기에는 무리입니다.

그러면 어떤 수식으로 표현할 수 있을까요? 아래와 같이 $x$의 제곱이 들어가는 2차함수를 쓰는 방법이 있습니다.

$$y=a_0+a_1x+a_2x^2$$

소위 포물선 곡선을 쓰는 겁니다. 이 경우도 scikit learn의 LinearRegression 클래스를 쓸 수 있습니다. 그럴려면 기존 입력 데이터 뿐만 아니라 이를 제곱한 값도 따로 준비해야 합니다.

$$
X2 = \left[
\begin{array}{c} [x[0], x[0]^2], \\ [x[1], x[1]^2], \\ \vdots \\ [x[9], x[9]^2] \end{array} \right ]
$$

그런 다음 LinearRegression클래스의 fit함수를 써서 학습을 하면 됩니다.

X2 = [[x[k], x[k]**2] for k in range(len(Data))]

# from sklearn import linear_model # already imported
reg2 = linear_model.LinearRegression()
reg2.fit(X2,y)
print(reg2.intercept_)
print(reg2.coef_)

xp2 = [0.1*k for k in range(101)]
Xp2 = [[xp2[k], xp2[k]**2] for k in range(101)]
yp2 = reg2.predict(Xp2)

plt.xlim(0,10)
plt.ylim(0,30)
plt.plot(x, y, "x")
plt.plot(xp2, yp2)

그래프를 보면 이번에 찾은 곡선은 얼추 데이터을 찍은 점들과 그 모양이 비슷합니다. 위의 코드에서 #로 시작하는 줄이 있습니다. # 다음에오는 텍스트는 줄이 끝날때까지 코멘트로 처리합니다. 코멘트는 실제 돌아가는 코드가 아닙니다. 주로 코드에 대한 설명을 쓸때 코멘트를 사용합니다.

reg2.coef_에 이제 두개의 숫자가 있습니다. 첫번째 값은 입력 데이터에 관련된 값(수식에서 $a_1$)이고 두번째 값은 입력 데이터의 제곱에 관련된 값(수식에서 $a_2$)입니다. 곡선에 세로축과 만나는 세로축 값인 reg2.intercept_의 값과 함께 곡선을 수식을 나타내면

$$ y = 30.324-10.557x+1.055x^2 $$

가 됩니다.

입력 데이터의 세제곱을 추가하면 데이터를 더 잘 표현할까요? 한번 해 보겠습니다.

X3 = [[x[k], x[k]**2, x[k]**3] for k in range(len(Data))]

# from sklearn import linear_model # already imported
reg3 = linear_model.LinearRegression()
reg3.fit(X3,y)
print(reg3.intercept_)
print(reg3.coef_)

xp3 = [0.1*k for k in range(101)]
Xp3 = [[xp[k], xp[k]**2, xp[k]**3] for k in range(101)]
yp3 = reg3.predict(Xp3)

plt.xlim(0,10)
plt.ylim(0,30)
plt.plot(x, y, "x")
plt.plot(xp3, yp3)

제곱까지만 사용한 경우보다 세제곱까지 사용한 경우가 데이터를 좀 더 잘 표현 하는 듯한 느낌이 듭니다. 느낌은 느낌이고, 얼마나 잘 표현하는지를 숫자로 알아보겠습니다. LinearRegression의 score함수를 사용하면 됩니다.

print(reg.score(X,y))
print(reg2.score(X2,y))
print(reg3.score(X3,y))
3.5574365369828165e-05
0.9683471855369512
0.9730757530262825

첫번째 값은 직선으로 한 경우, 두번째 값은 2차함수 곡선으로, 세번째 값은 3차함수 곡선으로 했을때의 결과입니다. 1에 가까울수록 곡선이 데이터에 더 가깝게, 다시 말해 데이터를 더 잘 표현하는 것을 감안하면, 미미하게나마 3차함수 곡선이 데이터를 좀 더 정확하게 표현하고 있음을 알 수 있습니다. 원래 score함수는 학습에 사용하지 않은 데이터로 결과를 예측할때 얼마나 잘 예측하는지 알아보는 함수인데, 학습에 사용한 데이터에 적용해 본 것입니다.

그러면 네제곱, 다섯제곱, … 이렇게 더 많이 포함한 수식을 이용하면 데이터를 더 잘 표현할 수 있을까요? 데이터가 총 10개이니 총 10개의 숫자($a_0, a_1, \dotsb, a_9$)로 만들어진 곡선 수식을 사용해 보겠습니다. intercept_ ($=a_0$)값이 있으니, 아래와 같이 입력 데이터의 아홉제곱까지 수식에 포함합니다.

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

코드는 다음과 같습니다.

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

# from sklearn import linear_model # already imported
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)

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

곡선이 데이터를 얼머나 잘 표현하는지를 알려주는 reg9.score(X9,y)의 결과는 사실상 1입니다. 데이터가 곡선위에 정확히 있다는 얘기이고, 곡선이 데이터를 완벽하게 표현한다는 얘기이기도 합니다.

그런데 문제는 학습에 사용한 데이터에서는 볼 수 없는 출렁임이 곡선에 있다는 것입니다. 예를 들어 입력값(가로축)이 1에서 2사이일때의 곡선을 보면 결과값(세로축)이 10에서 20 사이에서 출렁입니다. 7과 9사이에서도 곡선이 심하게 출령입니다. 만약에 학습에 사용하지 않은 데이터의 입력값이 이부분에 있다면 결과를 제대로 예측할지는 의문입니다.

이렇게 출렁임이 나오는 이유는 데이터에 들어간 잡음까지 너무 과하게 곡선을 맞추기 때문입니다. 이로 인해 학습에 사용하지 않은 데이터를 가지고 결과를 예측해 보면, 예측한 결과가 실제 결과와는 많이 차이가 나는 상황이 발생합니다. 이를 영어로 오버피팅(overfitting)이라고 부릅니다. 한국말로 과적합이라고도 합니다.

이번에는 아얘 입력 데이터의 15제곱까지 포함해 위의 과정을 반복 해보겠습니다.

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

# from sklearn import linear_model # already imported
reg15 = linear_model.LinearRegression()
reg15.fit(X15,y)
print(reg15.intercept_)
print(reg15.coef_)
print(reg15.score(X15,y))

xp15 = [0.1*k for k in range(101)]
Xp15 = [[xp15[k]**n for n in range(1,16)] for k in range(101)]
yp15 = reg15.predict(Xp15)

plt.xlim(0,10)
plt.ylim(0,30)
plt.plot(x, y, "x")
plt.plot(xp15, yp15)

이번에도 score함수값은 사실상 1로 학습에 사용한 데이터를 완변하게 표현합니다. x로 표현한 데이터가 완벽하게 곡선 위에 있습니다. 하지만 입력값(가로축)이 6과 9사이일때 그래프의 범위를 벗어날 정도로 많이 출렁입니다.

세로축의 범위를 넓혀서 다시 그래프를 그려보겠습니다.

plt.xlim(0,10)
plt.ylim(-100,100)
plt.plot(x, y, "x")
plt.plot(xp15, yp15)

세로방향의 출렁임의 크기가 100이 넘습니다. 학습에 사용한 데이터에서는 전혀 볼 수 없는 매우 큰 출렁임입니다. 오버피팅이 매우 심한 경우가 되겠습니다. 학습하지 않은 데이터의 입력 값이 6과 9 사이일때는 예측한 결과에 심한 오류가 발생할 가능성이 큽니다. 그 만큼 예측 정확도는 심하게 안 좋습니다. 학습에 사용한 데이터에서 볼 수 없는 출렁임이 곡선에 나오면 일단 오버피팅을 의심하고 봐야 합니다.