티스토리 뷰

 직전 포스팅에서 선형 회귀를 다루며, 농어의 길이에 따른 무게를 예측하는 모델을 만들었었다. 

 

길이라는 1개의 특성으로 이차 방정식을 도출하여 값을 예측했는데, 길이 뿐만 아니라 다른 특성이 추가가 된다면 방정식에 고차항이 추가가 되며 그 계수 또한 맞춰서 늘어난다.

 

이를 다중 회귀(multiple regression)이라고 한다. 

 

다중 회귀는 가지고 있는 특성 데이터를 각각 제곱하여 추가하며, 각 특성을 서로 곱하여 또 다른 특성을 만든다. 

 

이렇게 기존의 특성을 사용해 새로운 특성을 뽑아내는 작업을 특성 공학이라고 부른다.

 

바로 예제로 한번 확인해보자.


이번 예제는 농어의 길이 뿐만 아니라, 높이, 두께 데이터도 함께 사용할 것이다. 

 

아래와 같이 pandas를 사용하여 csv 데이터를 가져와서, 이중 배열 형태로 변환한다.

 

또한 농어의 무게는 직전 포스팅에서 사용했던 데이터를 그대로 사용할 것이다.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

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

print(perch_full)

# [[ 8.4   2.11  1.41]
#  [13.7   3.53  2.  ]
#  [15.    3.82  2.43]
#  [16.2   4.59  2.63]
#  [17.4   4.59  2.94]
#  [18.    5.22  3.32]
#          ...
#  [43.   12.51  7.42]
#  [43.5  12.6   8.14]
#  [44.   12.49  7.6 ]]

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

그 다음 perch_full과 perch_weight 데이터를 사용하여 테스트 데이터와 타깃 데이터를 나누자. 

train_input, test_input, train_target, test_target = train_test_split(
	perch_full, perch_weight, random_state=42)

 

이제, 각 특성을 이용하여 다차항 방정식을 만들기 위해 데이터 전처리를 해야한다. PolynomialFeatures 클래스를 이용하여 각 특성을 제곱한 항을 추가하고 특성끼리 서로 곱한 항도 추가한다. 

 

아래 코드는 PolynomialFeatures를 사용하여 다차원으로 데이터를 변환하여 확인한 코드이다.

# 각 특성을 제곱한 항을 추가하고 특성끼리 서로 곱한 항을 추가한다. (다항 처리)
# include_bias = 자동으로 특성에 추가된 절편을 무시하는지 여부
poly = PolynomialFeatures(include_bias=False)

# 훈련을 먼저 시켜야 변환이 가능하다.
poly.fit(train_input)

# 변환
train_poly = poly.transform(train_input)

# 만들어진 특성의 개수
print(train_poly.shape)
#(42, 9)

# 특성에 따라 만들어진 항들
print(poly.get_feature_names_out())
# ['x0' 'x1' 'x2' 'x0^2' 'x0 x1' 'x0 x2' 'x1^2' 'x1 x2' 'x2^2']

# 훈련 데이터를 fit한 poly로 테스트 데이터를 변환해야 한다.
test_poly = poly.transform(test_input)

이제 선형 회귀로 해당 다차원 데이터를 학습시킨 뒤 Score를 확인해보면 아래와 같다.

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

# 훈련 데이터의 Score
print(lr.score(train_poly, train_target))
# 0.9903183436982126

# 테스트 데이터의 Score
print(lr.score(test_poly, test_target))
# 0.9714559911594125

 

물론 PolynomialFeatures에서 특성을 임의로 더 많이 추가하여 고차항의  최대 차수를 지정할 수 있다.

# 특성을 더 많이 추가하여 고차항의 최대 차수를 지정할 수 있음
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)
# (42, 55)

lr.fit(train_poly, train_target)

# 훈련 데이터 셋에 대한 선형 회귀 모델 점수
print(lr.score(train_poly, train_target))
# 0.9999999999997232  >> 매우 정확

# 테스트 데이터 셋에 대한 선형 회귀 모델 점수
print(lr.score(test_poly, test_target))
# -144.40564483377855 >> ????

하지만, 이렇게 임의로 특성을 추가하여 선형 회귀 학습을 시킬 경우, 훈련 데이터에 과도하게 적합하여 테스트 데이터 셋으로 Score를 구했을 때 완전히 형편 없는 정확도를 보여준다.

 

이 문제를 해결하기 위해서는 다시 특성을 줄여 과대적합을 줄여 나가야 하는데, 이 과정을 머신러닝에서는 Regularization(규제)라고 한다.

 

- 규제

 규제는 선형회귀 모델의 경우, 곱해지는 계수(기울기)를 줄이는 것을 의미한다. 

 

우선, 규제를 하기 전에 특성의 스케일이 정규화되지 않으면 여기에 곱해지는 계수의 값도 차이가 나기 때문에 정규화를 먼저 해줘야 한다.

ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)

 

선형회귀 모델의 경우, 규제를 추가한 모델을 크게 Ridge(릿지) 와 Lasso(라쏘)라고 부른다.

 

1) Ridge: 계수를 제곱한 값을 기준으로 규제를 적용한다.

Ridge는 아래와 같이 Ridge 클래스를 사용하여 모델에 규제를 추가할 수 있다.

# Ridge의 경우
ridge = Ridge()
ridge.fit(train_scaled, train_target)

print(ridge.score(train_scaled, train_target))
# 0.9896101671037343

print(ridge.score(test_scaled, test_target))
# 0.9790693977615387

또한 생성자에 alpha 매개 변수를 주어 규제의 정도를 임의로 지정할 수 있는데, 아래와 같이 지수함수 형태로 직접 최적의 규제 강도를 찾아 적용시키는 것이 가장 좋다.

# Ridge와 Lasso 모델을 사용할 때, 규제의 양을 임의로 결정할 수 있다.
# 아래 코드는 Ridge의 적절한 규제의 양을 찾기 위한 참조 코드이다.
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))

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()

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

훈련 데이터와 테스트 데이터 간의 간격이 가장 적은 0.1 정도의 알파 값이 가장 성능이 좋은 것을 확인할 수 있다.

 

2) Lasso: 계수의 절대 값을 기준으로 규제를 적용한다.

 마찬가지로 Lasso 클래스로 규제를 추가할 수 있다.

lasso = Lasso()
lasso.fit(train_scaled, train_target)

print(lasso.score(train_scaled, train_target))
# 0.989789897208096

print(lasso.score(test_scaled, test_target))
# 0.9800593698421883

동일하게 alpha 매개변수를 넣어 최적의 규제 값을 찾을 수도 있으며, Lasso의 경우는 최적의 계수를 찾기 위해 반복적인 계산을 수행하는데, 이 반복 계산의 횟수를 직접 지정해줄 수 있다.

train_score = []
test_score = []
alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
       # Lasso 모델은 최적의 계수를 찾기 위해 반복적인 계산을 수행하는데, 이 반복 계산의 횟수를 지정해줘야 한다.
       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()

lasso = Lasso(alpha=10)
lasso.fit(train_scaled, train_target)

print(lasso.score(train_scaled, train_target))
# 0.9888067471131867

print(lasso.score(test_scaled, test_target))
# 0.9824470598706695

Lasso의 경우 100 이상으로 규제가 걸리면 성능이 급격하게 하락하는 것을 볼 수 있으며, 10일 때 가장 차이가 없는 것을 알 수 있다.

 

또한 두 규제 모두 기울기가 0이 된 계수를 찾으면, 제거된 특성이 몇개가 되는 지 확인할 수 있다.

print(np.sum(lasso.coef_ == 0))
#40

전체 코드는 아래와 같다.

 

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.linear_model import Lasso

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

# [[ 8.4   2.11  1.41]
#  [13.7   3.53  2.  ]
#  [15.    3.82  2.43]
#  [16.2   4.59  2.63]
#  [17.4   4.59  2.94]
#  [18.    5.22  3.32]
#          ...
#  [43.   12.51  7.42]
#  [43.5  12.6   8.14]
#  [44.   12.49  7.6 ]]

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

train_input, test_input, train_target, test_target = train_test_split(perch_full, perch_weight, random_state=42)

# 각 특성을 제곱한 항을 추가하고 특성끼리 서로 곱한 항을 추가한다. (다항 처리)
# include_bias = 자동으로 특성에 추가된 절편을 무시하는지 여부
poly = PolynomialFeatures(include_bias=False)

# 훈련을 먼저 시켜야 변환이 가능하다.
poly.fit(train_input)

# 변환
train_poly = poly.transform(train_input)

# 만들어진 특성의 개수
print(train_poly.shape)
#(42, 9)

# 특성에 따라 만들어진 항들
print(poly.get_feature_names_out())
# ['x0' 'x1' 'x2' 'x0^2' 'x0 x1' 'x0 x2' 'x1^2' 'x1 x2' 'x2^2']

# 훈련 데이터를 fit한 poly로 테스트 데이터를 변환해야 한다.
test_poly = poly.transform(test_input)

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

# 훈련 데이터의 Score
print(lr.score(train_poly, train_target))
# 0.9903183436982126

# 테스트 데이터의 Score
print(lr.score(test_poly, test_target))
# 0.9714559911594125

# 특성을 더 많이 추가하여 고차항의 최대 차수를 지정할 수 있음
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)
# (42, 55)

lr.fit(train_poly, train_target)

# 훈련 데이터 셋에 대한 선형 회귀 모델 점수
print(lr.score(train_poly, train_target))
# 0.9999999999997232  >> 매우 정확

# 테스트 데이터 셋에 대한 선형 회귀 모델 점수
print(lr.score(test_poly, test_target))
# -144.40564483377855 >> 훈련 데이터에 과도하게 적합되어 테스트 데이터에서는 형편 없는 점수를 보여준다.
# 이 문제를 해결하기 위해서 다시 특성을 줄여야 한다.
# 특성을 줄이는 것보다 과대 적합을 줄이는 또 다른 방법을 사용할 것이다.


# 과대적합을 줄이기 위해서는 regularization(규제)를 해야하는데, 선형회귀 모델의 경우 곱해지는 계수(기울기)를 줄이는 것을 의미한다.
# 우선 규제를 하기 전, 특성의 스케일이 정규화되지 않으면 여기에 곱해지는 계수의 값도 차이가 나기 때문에 정규화를 먼저 해주자.
ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)

# 선형 회귀 모델에 규제를 추가한 모델을 Ridge(릿지)와 Lasso(라쏘)라고 부른다.
# Ridge: 계수를 제곱한 값을 기준으로 규제를 적용한다.
# Lasso: 계수의 절대값을 기준으로 규제를 적용한다.

# # Ridge의 경우
# ridge = Ridge()
# ridge.fit(train_scaled, train_target)
#
# print(ridge.score(train_scaled, train_target))
# # 0.9896101671037343
#
# print(ridge.score(test_scaled, test_target))
# # 0.9790693977615387
#
# # Ridge와 Lasso 모델을 사용할 때, 규제의 양을 임의로 결정할 수 있다.
# # 아래 코드는 Ridge의 적절한 규제의 양을 찾기 위한 참조 코드이다.
# 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))
#
# 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()

# 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))


lasso = Lasso()
lasso.fit(train_scaled, train_target)

print(lasso.score(train_scaled, train_target))
# 0.989789897208096

print(lasso.score(test_scaled, test_target))
# 0.9800593698421883

train_score = []
test_score = []
alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
       # Lasso 모델은 최적의 계수를 찾기 위해 반복적인 계산을 수행하는데, 이 반복 계산의 횟수를 지정해줘야 한다.
       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()

lasso = Lasso(alpha=10)
lasso.fit(train_scaled, train_target)

print(lasso.score(train_scaled, train_target))
# 0.9888067471131867

print(lasso.score(test_scaled, test_target))
# 0.9824470598706695

print(np.sum(lasso.coef_ == 0))
#40

 

 

즉, 다중 회귀를 사용하면 특성이 늘어난 만큼 고려해야할 부분도 생기지만, 특성 하나로만 선형 회귀를 돌렸을 때 보다 높은 정확도로 예측치를 도출해낼 수 있다.

 

끝!

Comments