03. 회귀 알고리즘과 모델 규제

 

03-1 k-최근접 이웃 회귀

  • 시작하기 전에

농어의 무게를 예측하는 모델을 만들어보려고 한다.

<k- 최근접 이웃 회귀>

지도 학습 알고리즘은 분류와 회귀(regression)로 나뉜다. 분류는 샘플을 클래스 중 하나로 분류하는 문제이고, 회귀는 분류가 아니라 임의의 어떤 숫자를 예측하는 문제이다. 즉, 정해진 클래스, 타깃 값(정답)이 없는 것을 예측하여 임의의 수치를 출력하는 것이다. 회귀는 19세기 통계학자인 프랜시스 골턴이 처음 사용한 단어로, 두 변수 사이의 상관관계를 분석하는 방법회귀라고 불렀다.

k-최근접 이웃 분류 알고리즘은 샘플에 가장 가까운 샘플 k개를 선택한 다음 그들의 클래스를 확인하여 예측하는 것이다.

X를 추측할때 네모가 더 많기 때문에 동그라미보단 네모로 예측하는 것이다.

X의 타깃값을 예측할 때는 이웃 샘플의 타깃값의 평균으로 구할 수 있다. 고로, 80으로 예측하는 것이다.

 

<데이터 준비>

훈련데이터를 깃허브에서 복사한 다음, 데이터의 산점도를 그려볼 수 있다.

import matplotlib.pyplot as plt
plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

하나의 특성을 사용하기 때문에 특성 데이터를 x축에 두고, 타깃 데이터를 y축에 놓고 산점도를 그린 결과다. 결과에 따르면, 농어의 길이가 길어짐에 따라 무게도 늘어나는 것을 알 수 있다.

이제 이 데이터를 훈련 세트와 테스트 세트로 나눌 것이다.

여기서 사용하는 random_state의 시드 값을 설정해줄 때 보통 0이나 1이 아닌 42를 관용적으로 설정해주길래 이유를 찾아봤더니 은하수를 여행하는 히치하이커를 위한 안내서 책에서 "삶과 우주, 그리고 모든 것에 대한 답은 42입니다." 라는 문장이 있어서 그 이후로 밈처럼 사용되었다고 한다...

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(
perch_length, perch_weight, random_state=42)

train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

print(train_input.shape, test_input.shape)

여기서 perch_length의 특성이 1차원 배열이고 train_input, test_input도 1차원 배열인데 이를 2차원 배열로 바꾼다면 아래와 같이 진행된다.  reshape() 메서드는 크기를 바꿀 수 있는 메서드로, 넘파이에서 제공하는 메서드다. 즉, 바꾸려는 배열의 크기를 지정할 수 있다. 첫 번째 크기에 나머지 원소 개수로 모두 채우려면 -1, 두 번째 크기를  1로 하려면 train_input.shape(-1, 1)처럼 하면 된다.

 

<결정계수(R²)>

사이킷런에서 k-최근접 이웃 회귀 알고리즘을 구현한 클래스는 kNeighborsRegressor로 kNeighborsClassifier와 매우 유사하다. fit() 메서드를 통해 회귀 모델을 훈련할 수 있다. 

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor()

# k-최근접 이웃 회귀 모델 훈련
knr.fit(train_input, train_target)

print(knr.score(test_input, test_target))

여기서 나온 0.992~의 숫자는 결정계수(Coefficient of determination)라고 부른다. R²(알스퀘어)라고도 부르는데, 자세히 설명해 둔 블로그가 있어서 참고하였다. 계산식은 아래와 같다.

즉, 타깃의 평균 정도를 예측하는 수준이라면 R²는 0에 가까워지고, 예측이 타깃에 가까워지면 1에 가까운 값이 된다. 사이킷런의 score()메서드의 값은 높을수록 좋다. 분류 모델에서의 정확도나, 회귀 모델에서의 결정계수 모두 동일하다.

하지만 직관적으로 숫자만 보고 어느정도의 결정계수인지 가늠하기 어렵기 때문에 다른 값으로 치환해서 살펴볼 수도 있다.

from sklearn.metrics import mean_absolute_error

# 테스트 세트에 대한 예측
test_prediction = knr.predict(test_input)

# 테스트 세트에 대한 평균 절댓값 오차 계산
mae = mean_absolute_error(test_target, test_prediction)
print(mae)

mean_absolute_error() 메서드는 타깃과 예측이 절댓값 오차를 평균하여 반환한다. 즉, 이 결과값은 결과에서 예측이 평균 19g정도 타깃값과 다르다는 것이다.

 

<과대적합 vs 과소적합>

위에서는 훈련 세트로 훈련을 하고 테스트 세트로 평가했는데, 만약 훈련 세트로 평가를 한다면 어떤 결과가 나올까?

print(knr.score(train_input, train_target))

보통 테스트 세트를 활용한 모델의 점수가 더 높고 훈련 세트를 활용한 모델의 점수가 더 낮게 나오지만, 이 경우엔 반대로 된 것을 확인할 수 있다. 

훈련 세트에서 점수가 좋았는데 테스트 세트의 점수가 굉장히 나쁘다면 모델이 훈련세트에 과대적합(overfitting) 되었다고 한다. 즉, 훈련 세트에만 잘 맞는 모델이라 테스트 세트 혹은 실전에서 사용할 때 잘 동작하지 않을 수 있다. 이는 복잡도가 높다고 할 수도 있다.

반대로 훈련 세트보다 테스트 세트의 점수가 높거나 두 점수 모두 낮은 경우는 과소적합(underfitting) 되었다고 말한다. 즉, 모델이 너무 단순하여 적절히 훈련되지 않은 경우이다. 이는 복잡도가 낮다고 할 수 있다. 이럴 때는 훈련 세트의 크기가 너무 작거나, 데이터가 중요한 특징을 담고 있지 않은 경우일 수 있다.

위의 경우도 과소적합이다. 이를 해결하기 위해서 모델을 조금 더 복잡하게, 즉 훈련 세트에 더 잘 맞게 만들면 테스트 세트의 점수가 낮아질 것이다. 이웃의 개수인 k를 줄인다면 훈련 세트의 패턴에 더욱 민감해지고, k의 개수를 늘린다면 데이터 전반적인 특징을 따를 것이다. 기본 k값인 5를 3으로 줄여서 실행해보려 한다.

# 이웃의 개수 3개로 설정
knr.n_neighbors = 3

# 모델 재훈련
knr.fit(train_input, train_target)


print(knr.score(train_input, train_target))  # 훈련 세트의 결정계수
print(knr.score(test_input, test_target))  # 테스트 세트의 결정계수

테스트 세트의 점수가 훈련 세틑의 점수보다 낮아졌고, 둘의 차이도 크지 않아 과소적합이 해결된 것을 알 수 있다.

이처럼 최적의 k값을 찾기 위한 과정이 필요하고, 스스로 정의하는 이러한 매개변수를 보통 '하이퍼파라미터'라고 부른다.

파라미터(Parameter)란 모델의 구성요소이자 데이터로부터 학습되는 것이다. 즉, 머신러닝 훈련 모델에 의해 요구되는 변수이다. 머신러닝 훈련 모델의 성능은 파라미터에 의해 결정된다.

그 중에서도 하이퍼파라미터(Hyperparameter)란 최적의 훈련 모델을 구현하기 위해 설정하는 변수이다. 이는 모델 학습 과정에 반영되며, 학습을 시작하기 전에 미리 수동으로 값을 결정하는 것이다. 하이퍼 파라미터는 절대적인 최적값이 존재하지 않기 때문에 사용자가 직접 모델에 대한 이해를 통해 값을 설정하고 결정해야 한다. 좋은 모델을 만들기 위해서는 하이퍼파라미터를 잘 튜닝(컨트롤)해야 한다. 그러기 위해서는 학습 알고리즘, 모델의 구조 등 총체적인 이해가 필요하다.

 

<확인문제>  # 기본미션 

# k-최근접 이웃 회귀 객체 만들기
knr = KNeighborsRegressor()

# 5 ~ 45까지 x 좌표 만들기
x = np.arange(5, 45).reshape(-1, 1)

# n = 1, 5, 10일 때 예측 결과 그래프로 그리기
for n in [1, 5, 10]:

# 모델 훈련하기
  knr.n_neighbors = n
  knr.fit(train_input, train_target)

# 지정한 범위 x에 대한 예측 구하기
  prediction = knr.predict(x)

# 훈련 세트와 예측 결과 그래프로 그리기
  plt.scatter(train_input, train_target)
  plt.plot(x, prediction)
  plt.title('n_neighbors = {}'.format(n))
  plt.xlabel('length')
  plt.ylabel('weight')
  plt.show()

 


03-2 선형회귀

  • 시작하기 전에

print(knr.predict([[50]]))

길이가 50cm인 농어의 무게를 예측했을 때, 예측 결과값은 1,033g이지만 실제로는 1.5kg에 달하는 농어였다. 이렇게 예측값과 실제값의 차이가 많이 나는 이유는 무엇일까?

<k-최근접 이웃의 한계>

훈련데이터를 깃허브에서 복사한다.

perch_length = np.array([8.4, 13.7, 15.0, 16.2, 17.4, 18.0, 18.7, 19.0, 19.6, 20.0, 21.0,
       21.0, 21.0, 21.3, 22.0, 22.0, 22.0, 22.0, 22.0, 22.5, 22.5, 22.7,
       23.0, 23.5, 24.0, 24.0, 24.6, 25.0, 25.6, 26.5, 27.3, 27.5, 27.5,
       27.5, 28.0, 28.7, 30.0, 32.8, 34.5, 35.0, 36.5, 36.0, 37.0, 37.0,
       39.0, 39.0, 39.0, 40.0, 40.0, 40.0, 40.0, 42.0, 43.0, 43.0, 43.5,
       44.0])
perch_weight = np.array([5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
       115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
       150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
       218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
       556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
       850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
       1000.0])

 

훈련 세트와 테스트 세트로 데이터를 나눈다.

from sklearn.model_selection import train_test_split

# 훈련 세트와 테스트 세트로 나누기
train_input, test_input, train_target, test_target = train_test_split(
perch_length, perch_weight, random_state=42)

# 훈련 세트와 테스트 세트를 2차원 배열로 바꾸기
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

 

최근접 이웃 개수를 3으로 하는 모델을 훈련한다.

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor(n_neighbors=3)

# k-최근접 이웃 회귀 모델 훈련
knr.fit(train_input, train_target)

print(knr.predict([[50]]))

이 모델은 무게를 1,033g 정도로 예측했다. 문제점을 찾기 위해 산점도를 표시하는데, kneighbors() 메서드를 사용할 것이다.

import matplotlib.pyplot as plt

# 50cm 농어의 이웃 구하기
distances, indexes = knr.kneighbors([[50]])

# 훈련 세트의 산점도 그리기
plt.scatter(train_input, train_target)

# 훈련 세트 중 이웃 샘플만 그리기
plt.scatter(train_input[indexes], train_target[indexes], marker='D')

# 50cm 농어 데이터
plt.scatter(50, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

길이가 50cm이고 무게가 1,033g인 농어는 삼각형으로 표시되고 그 주변의 샘플은 다이아몬드로 표시된다. 산점도에 따르면, 길이가 길어질수록 무게가 증가하는 경향이 있다. 하지만 50cm의 농어에서 가장 가까운 샘플은 45cm의 샘플이기 때문에, k-최근접 이웃 알고리즘에 따르면 이들의 무게의 평균을 예측 무게값으로 출력할 수 밖에 없다. 

print(np.mean(train_target[indexes]))

새로운 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값을 예측할 수 있다. 예를 들어 길이가 50cm가 아니라 100cm, 200cm 여도 농어의 무게는 여전히 1,033g으로 예측될 것이다.

다시 한 번 그래프를 통해 확인해보려고 한다.

# 100cm 농어의 이웃 구하기
distances, indexes = knr.kneighbors([[100]])

# 훈련 세트의 산점도 그리기
plt.scatter(train_input, train_target)

# 훈련 세트 중 이웃 샘플만 그리기
plt.scatter(train_input[indexes], train_target[indexes], marker='D')

# 100cm 농어 데이터
plt.scatter(100, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

이런 식이면 농어가 아무리 커도 무게는 늘어나지 않을 것이다. k-최근접 이웃이 아닌 다른 알고리즘으로 이 문제를 해결할 수 있을까?

 

<선형 회귀>

선형 회귀(Linear Regression)는 대표적인 회귀 알고리즘 중 하나로, 특성이 하나인 경우 어떤 직선을 학습하는 알고리즘이다. 즉, 그 특성을 가장 잘 나타낼 수 있는 직선을 학습하는 것이다.

사이킷런의 LinearRegression 클래스를 사용하여 선형회귀 알고리즘을 구현할 수 있다. 이 클래스에도 fit(), score(), predict() 메서드가 있다.

from sklearn.linear_model import LinearRegression
lr = LinearRegression()

# 선형 회귀 모델 훈련
lr.fit(train_input, train_target)

# 50cm 농어 예측
print(lr.predict([[50]]))

k-최근접 이웃 회귀와 달리 선형 회귀는 50cm 농어의 무게를 1,241g으로 예측했다. 

직선을 그리려면 기울기와 절편이 있어야 한다. 이 선형회귀 클래스가 이 데이터에 가장 잘 맞는 a와 b를 찾은 것이다. 클래스가 찾은 a와 b는 lr 객체의 coef_와 intercept_ 속성에 저장되어 있다.

 

#선택미션

Q. 모델 파라미터에 대해 설명하기

A. coef_intercept_를 머신러닝 알고리즘이 찾은 값이라는 의미로 모델 파라미터(model parameter)라 부른다. coef_는 기울기로, 계수(coefficient) 또는 가중치(weight)라고 부른다. 모델 기반 학습에서 모델이 찾은 정보는 모델 파라미터에 저장되고, 선형 회귀에서는 방정식의 계수가 여기에 해당된다.

많은 머신러닝 알고리즘의 훈련 과정은 최적의 모델 파라미터를 찾는 것과 같다. 이를 모델 기반 학습이라고 부른다. 앞서 사용한 k-최근접 이웃 알고리즘처럼 모델 파라미터가 없는 경우의 훈련은 사례 기반 학습이라고 부른다.

 

농어의 길이 15~50까지를 직선으로 그릴 때, 앞서 구하나 기울기와 절편을 사용하여 두 점을 잇고, 산점도로 그려볼 수 있다.

# 훈련 세트 산점도 그리기
plt.scatter(train_input, train_target)

# 15~50 1차방정식 그래프 그리기
plt.plot([15, 50], [15 * lr.coef_ + lr.intercept_, 50 * lr.coef_ + lr.intercept_])

# 50cm 농어 데이터
plt.scatter(50, 1241.8, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

이 직선이 선형 회귀 알고리즘이 데이터셋에서 찾은 최적의 직선이다. 길이가 50cm인 농어의 예측값 역시 이 직선의 연장선 상에 있음을 알 수 있다. 훈련 세트와 테스트 세트에 대한 R² 점수도 확인해보겠다.

둘의 결정계수 점수의 차이가 나는 것을 발견할 수 있다. 그럼 이게 훈련 세트에 과적합된걸까? 어디가 문제이니 살펴보니 그래프 왼쪽 아래가 좀 이상해보이기도 한다.

<다항 회귀>

선형 회귀가 만든 직선은 왼쪽 아래로 쭉 뻗어있다. 선형 회귀에 따르면 길이가 짧다면 무게가 0g 이하로 내려갈 것이다. 그래서 최적의 직선을 찾기보다 최적의 곡선을 찾는 것이 더욱 바람직할 것이다.

이런 2차방정식 그래프르르 그리려면 길이를 제곱한 항이 훈련 세트에 추가되어야 한다. 이를 위해서는 넘파이를 사용하면 된다.

train_poly = np.column_stack((train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 2, test_input))

print(train_poly.shape, test_poly.shape)

column_stack() 함수는 1차원 배열을 2차원 배열에 열로 쌓는 함수이다. 새로 만든 데이터의 크기가 위와같이 나온다.

train_poly를 사용해 선형 회귀 모델을 다시 훈련한 후, 2차 방정식의 a,b,c값을 추론할 수 있다. 2차 방정식 그래프를 위해 훈련 세트에 제곱 항을 추가했지만 타깃 값은 그대로 사용한다.

lr = LinearRegression()
lr.fit(train_poly, train_target)

print(lr.predict([[50 ** 2, 50]]))

선형회귀에서 예측한 것보다 훨씬 높은 무게가 예측된 것을 확인할 수 있다. 여기서 다시 계수와 절편을 출력할 수 있다.

이 모델은 위와 같은 그래프를 학습한 것이다. 이런 방정식을 다항식(polynomial)이라 부르며 다항식을 사용한 선형 회귀를 다항 회귀(polynomial regression)라 부른다.

여기서 발견한 절편 a,b,c를 통해 산점도를 그려볼 수 있다.

# 구간별 직선을 그리기 위해 15~49 정수배열 만들기
point = np.arange(15, 50)

# 훈련 세트 산점도 그리기
plt.scatter(train_input, train_target)

# 15~49 2차 방정식 그래프 그리기
plt.plot(point, 1.01 * point ** 2 - 21.6 * point + 116.05)

# 50cm 농어 데이터
plt.scatter(50, 1574, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

무게가 음수로 나올 일도 없고 앞선 모델보다 훨씬 안정적으로 보인다. 결정계수를 확인해보겠다.

아까보단 차이가 많이 줄었지만 여전히 테스트 세트의 점수가 높은 것으로 미루어 보아 과소적합이 있는 것 같다.


03-3 특성 공학과 규제

  • 시작하기 전에

다항 회귀에서 해결하지 못한 과소적합으로 추측되는 문제는, 더 고차항을 넣어서 해결해야 하는 걸까? 아니면 길이와 무게 데이터가 아닌 높이와 두께 데이터를 사용해 볼 수 있을까?

<다중 회귀>

앞서 사용한 선형 회귀는 하나의 특성을 사용했지만, 사실 선형 회귀는 특성이 많을 수록 엄청난 효과를 낸다. 여러 개의 특성을 사용한 선형 회귀를 다중 회귀(multiple regression)라 부른다.

좌측의 그림처럼 1개의 특성을 사용하면 직선을 학습하고, 우측의 그림처럼 2개의 특성을 학습하면 평면을 학습한다. 즉, 타깃값과 함께 3차원 공간을 형성하고 곧 평면이 된다. 특성이 3개인 경우는 어떻게 될까?

우리는 3차원 이상의 공간은 그리거나 상상할 수는 없지만 특성이 적다고 무조건 성능이 안 좋은 것은 아니다. 또, 주어진 특성 외에도 특성들을 연산하여 새로운 특성으로 만들 수도 있다. 이렇게 기존의 특성을 사용하여 새로운 특성을 뽑아내는 작업을 특성 공학(feature engineering)이라 부른다.

<데이터 준비>

농어의 특성이 두께, 길이, 높이 3개로 늘어났고, 복사해서 붙여넣는 방식은 너무 피곤하다. 이럴 경우 판다스를 사용하여 데이터를 내려받아 데이터프레임에 저장할 수 있다. 그 다음 넘파이 배열로 변환하여 사용할 수 있고, 판다스에서는 보통 csv 형태의 파일을 많이 사용한다.

import pandas as pd
df = pd.read_csv('https://bit.ly/perch_csv_data')
perch_full = df.to_numpy()
print(perch_full)

 

이렇게 긴 데이터를 판다스를 통해 쉽게 불러올 수 있다. 타깃데이터는 기존처럼 깃허브에서 복사한다. 

perch_weight = np.array([5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
       115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
       150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
       218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
       556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
       850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
       1000.0])

 

그 다음 훈련 세트와 테스트 세트로 나누고 특성 공학을 사용하여 새로운 특성을 만든다.

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(
perch_full, perch_weight, random_state=42)

 

<사이킷런의 변환기>

사이킷런에서 제공하는 전처리를 위한 클래스들을 변환기(transformer)라고 부르는데, 이 트랜스포머들은 모두 fit(), transform() 메서드를 제공한다.

이번에는 PolynomialFeatures 사용할 것이다. transform(변환)메서드는 fit(훈련)을 한 이후에 사용할 수 있다. 혹은 두 개를 붙인 fit_transform() 메서드를 사용해도 된다.

from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures()
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))

fit() 메서드는 새롭게 만들 특성 조합을 찾고 transform() 메서드는 실제로 데이터를 변환한다. 변환기는 타깃 데이터 없이 입력 데이터를 변환하기 때문에 fit() 메서드에 입력 데이터만 전달한다.

poly = PolynomialFeatures(include_bias=False)
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))

사실 선형 방정식의 절편을 항상 값이 1인 특성과 곱해지는 계수라 할 수 있기 떄문에, 절편을 위한 항을 제거하고 특성의 제곱과 특성끼리 곱한 항만 사용할 것이다.

 

이제 다시 훈련 데이터를 활용하여 학습에 적용할 수 있다.

poly = PolynomialFeatures(include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
print(train_poly.shape)

 

get_feature_names() 메서드는 9개의 특성의 내용을 알려준다. 하지만, 에러가 떠서 찾아보니 사이킷런 블로그에 의하면 get_feature_names_out() 메서드를 대신 사용해달라고 한다.

poly.get_feature_names_out()

'x0'는 첫번째 특성, 'x0^2'는 첫번째 특성의 제곱, 'x0 x1'은 첫번째 특성과 두번째 특성의 곱을 나타낸다고 한다.

 

이제 테스트 세트를 변환하여 다중회귀 모델을 훈련할 것이다.

test_poly = poly.transform(test_input)

 

<다중 회귀 모델 훈련하기>

다중 회귀 모델을 훈련하는 것은 선형 회귀 모델을 훈련하는 것과 같지만 특성의 개수가 여러개일 뿐이다. 

from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))

특성이 증가함으로 인해서 테스트 세트와 훈련 세트 모두의 점수가 높게 나온 것을 확인할 수 있다. 특성을 더 많이 추가해보려고 한다. degree 매개변수를 사용하여 5제곱까지의 특성을 만들어 출력해보려 한다.

poly = PolynomialFeatures(degree=5, include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)
print(train_poly.shape)

특성이 55개나 만들어진 것이다. 이를 통해 다시 훈련을 해보겠다.

점수가 음수가 나와버렸다... 특성의 개수를 크게 늘리면 선형 모델은 거의 완벽하게 학습할 수 있을 정도로 강력해진다. 하지만 훈련 세트에 너무 과대적합되기 때문에 테스트 세트에서는 형편없는 점수가 나올 수 있다.

 

<규제>

규제(regularization)는 모델이 훈련 세트를 너무 과도하게 학습하지 못하도록(과대적합하지 않도록) 하는 것이다. 즉, 선형 회귀모델의 경우 특성에 곱해지는 계수(기울기)의 크기를 작게 만드는 것이다.

좌측은 훈련 세트를 과도하게 학습했고, 우측은 보다 보편적으로 학습한 것을 알 수 있다.

특성의 스케일이 정규화되지 않으면 곱해지는 계수 값도 차이가 난다. 그렇기 때문에 규제를 적ㅇ굥하기 전에 정규화를 먼저해야 한다. 사이킷런의 StandardScaler 클래스를 이용할 수 있다.

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)

 

선형 회귀 모델에 규제를 추가한 모델을 릿지(ridge)라쏘(lasso)라고 부르는데, 규제 방법이 다르다. 릿지는 계수를 제곱한 값을 기준으로 규제하고, 라쏘는 계수의 절댓값을 기준으로 규제한다. 보통 릿지를 더 선호하는 편이고, 두 알고리즘 모두 계수의 크기를 줄인다. 하지만 라쏘는 아예 0으로 만들 수도 있다.

 

<릿지 회귀>

사이킷런에서 제공하는 모델이라 편하게 릿지 회귀 알고리즘을 사용할 수 있다.

from sklearn.linear_model import Ridge
ridge = Ridge()
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))

 

점수가 정상인 것을 알 수 있다. 릿지와 라쏘 모델을 사용할 때 alpha라는 매개변수를 통해 규제의 강도를 조절할 수 있다. 값이 커질수록 규제의 강도도 세진다. 적절한 alpha 값을 찾는 법은 alpha 값에 대한 결정계수 값의 그래프를 그려보는 것이다. 

train_score = []
test_score = []

alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
    # 릿지모델 만들기
    ridge = Ridge(alpha=alpha)
    # 릿지모델 훈련하기
    ridge.fit(train_scaled, train_target)
    # 훈련 점수와 테스트 점수 저장
    train_score.append(ridge.score(train_scaled, train_target))
    test_score.append(ridge.score(test_scaled, test_target))

 

이제 그래프를 그려볼 것이다. alpha 값을 10배씩 늘렸기 때문에 이대로 그래프를 그리면 왼쪽이 너무 촘촘해진다. 따라서, 동일한 간격으로 나타내기 위해 log함수로 바꿔서 표현할 것이다. np.log() 혹은 np.log10() 함수를 사용할 수 있다.

# 그래프 그리기 (로그)
plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show()

위가 훈련세트, 아래가 테스트세트의 그래프이다. 

 

좌측을 보면 두 데이터의 점수차가 아주 큰 것을 통해, 훈련 세트에만 잘 맞는 과대적합의 모습을 볼 수 있다. 반대로 우측을 보면 둘의 점수가 모두 낮아지는 과소적합의 모습을 볼 수 있다. 따라서 적절한 alpha 값은 둘의 거리가 가까우면서도 테스트 세트의 점수가 가장 높은 -1(0.1)임을 알 수 있다.

# 10의 -1제곱인 0.1을 alpha값으로 지정함
ridge = Ridge(alpha=0.1)
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))

결과값을 통해 균형잡힌 모델임을 알 수 있다.

 

<라쏘 회귀>

라쏘 회귀도 릿지와 구현 방법은 비슷하다.

from sklearn.linear_model import Lasso
lasso = Lasso()
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))

결과가 나쁘지 않지만, 릿지에서 했던 것처럼 최적의 alpha 값을 찾아보려 한다.

train_score = []
test_score = []

alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
    # 라쏘모델 만들기
    lasso = Lasso(alpha=alpha, max_iter=10000)
    # 라쏘모델 훈련하기
    lasso.fit(train_scaled, train_target)
    # 훈련 점수와 테스트 점수 저장
    train_score.append(lasso.score(train_scaled, train_target))
    test_score.append(lasso.score(test_scaled, test_target))

경고문구가 뜨지만, 정상작동되는 것이다. 반복 횟수가 부족할 때 경고가 발생할 수 있다.

다시 그래프를 그린다.

# 그래프 그리기 (로그)
plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show()

좌측은 과대적합, 우측은 과소적합되는 걸로 미루어보아 최적의 alpha 값은 1, 즉 10임을 알 수 있다. 이로 다시 훈련을 해볼 수 있다.

# 10의 1제곱인 10을 alpha값으로 지정함
lasso = Lasso(alpha=10)
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))

릿지와 마찬가지로 과대적합을 많이 줄이며 테스트 세트의 성능을 높인 것을 알 수 있다. 라쏘 모델은 계수 값을 0으로 만들 수 있기 때문에 얼마나 0이 되었는지 찾아볼 수 있다.

55개의 특성 중 40개가 0으로 변하고, 15개의 특성만 사용한 것을 알 수 있다. 라쏘 모델은 유용한 특성을 골라내는 데에 사용하기도 좋을 듯 하다.

 


● 마무리

다양한 선형 회귀 알고리즘을 통해 과대적합 문제와 과소적합 문제를 해결할 수 있다. 또한, 특성을 추가할수록 좋은 성능을 내는 선형 회귀 모델에 발맞춰 특성을 만들어보기도 하였다. 랏쏘와 릿지 규제를 사용하여 모델에 제약을 주기도 할 수 있다. 또한, 규제의 양을 조절하는 매개변수인 alpha 값도 직접 찾아볼 수 있다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

 

Chapter03_Regression_algorithms_and_model_regularization.ipynb
0.17MB

+ Recent posts