아래 내용은 "혼자 공부하는 머신러닝+딥러닝" 을 공부하며 간략하게 정리한 내용입니다. 쉬운 사례로 어려운 수식 없이 설명하므로 입문자에게 강추하는 책입니다!
1. 시작하기 전에
이번에는 김팀장이 담당자에게 새로운 문제를 머신러닝으로 해결해달라고 요청합니다. 올 여름 농어 철로 기존과 다르게 무게 단위로 가격을 책정하기로 했는데 공급처에서 생선 무게를 잘못 측정해서 보내왔습니다. 농어의 길이, 높이, 두께를 측정한 데이터로 무게를 측정할 수 있을까요...?
2. 데이터 준비
담당자는 어떻게 해결할까 고민하다 분류에서 사용했던 k-최근접 이웃 알고리즘이 회귀에서도 사용할 수 있다는 것을 알게 되었습니다. 농어 데이터를 준비하고 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])
농어 데이터가 어떤 형태를 띠고 있는지 산점도로 그려보겠습니다. 길이 특성만 사용하기 때문에 x축에 두고 타겟인 무게는 y축에 놓겠습니다. 아래 그림을 보면 농어의 길이가 커질수록 무게도 늘어나는 것을 알 수 있습니다.
plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
그럼 2장에서 모델링을 한 과정대로 모델에 사용하기 전 훈련 셋과 테스트 셋으로 나누겠습니다.
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)
print(train_input.shape)
→(42, )
데이터셋을 나눈 후 배열의 형태를 확인해보니 튜플 형태 (42, )로 42개의 데이터가 1차원 리스트 형태입니다. 사이킷런에서는 훈련 셋의 형태가 2차원이어야 하기 때문에 reshape() 메서드를 사용하여 임의로 바꿔주도록 하겠습니다. 넘파이는 배열의 크기를 자동으로 지정하는 기능도 제공하는데요 크기에 -1을 지정하면 나머지 원소 개수로 모두 채우라는 의미입니다. reshape(-1, 1)을 사용하면 전체 원소 개수를 매번 외우지 않아도 되므로 편리합니다.
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)
print(train_input.shape, test_input.shape)
→ (42,1) (14, 1)
2. 결정계수($R^2$)
사이킷런에서 k-최근접 이웃 회귀 알고리즘을 구현한 클래스는 KNeighborsRegressor입니다.
학습시켜보도록 하겠습니다.
from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor()
knr.fit(train_input, train_target)
print(knr.score(test_input, test_target))
→ 0.992809406101064
0.99로 엄청 좋은 점수가 나왔습니다. 이 점수는 무엇을 의미할까요?
분류의 경우 테스트 세트에 있는 샘플을 정확하게 분류한 개수의 비율이었습니다. 다른말로 정확도라고 불렀죠. 하지만 회귀에서는 정확한 숫자를 맞힌다는 것은 거의 불가능하기 때문에 다른 값으로 평가하는데 이 점수를 결정계수(coefficient of determination)라고 부릅니다.
$$R^2 = 1-\frac{(타깃-예측)^2의 합}{(타깃-평균)^2의합}$$
만약 모델이 타겟의 평균 정도를 예측하는 수준이라면 분자와 분모가 비슷해 R^2은 0에 가까워지게되고, 예측이 타겟에 아주 가까워지면 분자가 0에 가까워지기 때문에 1에 가까운 값이 됩니다.
그렇다면 0.99의 값은 아주 좋은값인데요 이는 직감적으로 얼마나 좋은지 이해하기가 어렵습니다. 타겟과 예측한 값 사이의 차이를 구해서 어느 정도 예측이 벗어났는지 가늠해보도록 하겠습니다.
from sklearn.metrics import mean_absolute_error
# 테스트 셋에 대한 예측
test_prediction = knr.predict(test_input)
# 테스트 셋에 대한 평균 절대값 오차
mae = mean_absolute_error(test_target, test_prediction)
mae
→ 19.157142857142862
사이킷런은 sklearn.metrics 패키지 아래 여러 가지 측정 도구를 제공합니다. 이 중에서 mean_absolute_error는 타겟과 예측의 절댓값 오차를 평균하여 반환합니다. 결과에서 평균적으로 예측이 19g 정도 타겟값과 다르다는 것을 알 수 있습니다.
3. 과대적합 vs 과소적합
보통 모델을 훈련셋으로 학습하면 훈련셋에 잘 맞는 모델이 만들어집니다. 만약 훈련셋에서 평가 점수가 좋았는데 테스트셋에서 점수가 굉장히 나쁘다면 훈련 세트에 과대적합(overfitting) 되었다고 말합니다. 즉, 훈련셋에만 잘 맞는 모델이랑 테스트셋과 나중에 실전에 투입할 경우 잘 동작하지 않을 것 입니다.
반대로 훈련셋보다 테스트셋의 점수가 더 높거나 두 점수가 모두 너무 낮은 경우 모델이 훈련셋에 과소적합(underfitting)되었다고 말합니다. 즉, 모델이 너무 단순해서 훈련셋에 적절히 훈련되지 않은 경우입니다.
print(knr.score(train_input, train_target))
→ 0.9698823289099254
앞선 모델은 훈련셋(0.97)보다 테스트셋(0.99)의 점수가 높으니 과소적합 모델입니다. 이런 경우 어떻게 해결해야 할까요? 모델을 조금 더 복잡하게 만들면 됩니다. k-최근접 이웃 알고리즘으로 모델을 더 복잡하게 만드는 방법은 이웃의 개수 k를줄이는 것입니다.
# 이웃의 개수를 3으로 설정
knr.n_neighbors = 3
# 모델을 다시 학습
knr.fit(train_input, train_target)
print(knr.score(train_input, train_target))
→ 0.9804899950518966
# 테스트 세트 점수 확인
print(knr.score(test_input, test_target))
→ 0.9746459963987609
테스트셋의 점수가 훈련셋의 점수보다 낮아졌으므로 과소적합 문제를 해결한 것 같습니다. 또한 두 점수의 차이가 크지 않으므로 모델이 과대적합 된 것 같지도 않습니다.
4. 선형회귀
하지만 k-최근접 이웃 알고리즘의 한계가 있습니다. 이 알고리즘은 가까운 k개의 샘플들의 평균값으로 예측합니다. 따라서 새로운 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값을 예측할 수 있습니다. 널리 사용되는 대표적인 선형 회귀 알고리즘을 사용해보도록 하겠습니다.
사이킷런은 sklearn.linear_model 패키지에 LinearRegression 클래스로 선형 회귀 알고리즘을 구현해 놓았습니다.
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
# 선형 회귀 모델을 훈련
lr.fit(train_input, train_target)
# 50cm 농어에 대해 예측
print(lr.predict([[50]]))
→ [1241.83860323]
k-최근접 이웃 회귀를 사용했을 때와는 달리 선형 회귀는 50cm 농어의 무게를 아주 높게 예측했습니다. 선형 회귀의 모형은 $y = ax+b$로 LinearRegression 클래스는 데이터와 가장 잘 맞는 a 와 b를 찾게됩니다.
a는 계수(coefficient) 또는 가중치(weight)라고 부르며, a 또는 b와 같이 머신러닝 알고리즘이 찾은 값을 모델 파라미터(model parameter)라고 부릅니다.
print(lr.coef_, lr.intercept_)
→ [39.01714496] -709.0186449535477
훈련셋 샘플 산점도와 모델의 파라미터로 계산된 선형 직선을 그려보도록 하겠습니다.
# 훈련 셋의 산점도
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()
바로 이 직선이 선형 회귀 알고리즘이 찾은 최적의 직선입니다. 훈련셋과 테스트셋에 대한 $R^2$ 점수를 확인해보겠습니다.
print(lr.score(train_input, train_target))
→ 0.939846333997604
print(lr.score(test_input, test_target))
→ 0.8247503123313558
꽤 높은 점수를 보이고 있는데 이상한 점이 있습니다. 선형 회귀 선은 쭉 뻗은 직선으로 농어 무게가 0g 이하로 예측할 가능성이 있습니다. 그림2 산점도를 자세히 보면 왼쪽 위로 조금 구부러진 곡선에 가까운 모습입니다. 만약 최적의 곡선을 찾는다면 길이를 제곱한 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)
→ (42, 2) (14, 2)
원래 특성인 길이를 제곱하여 왼쪽 열에 추가했기 때문에 훈련셋과 테스트셋 모두 열이 2개로 늘어났습니다. 이 데이터로 모델에 학습을 시켜보겠습니다. 앞선 모델보다 더 높은 값을 예측했습니다!!
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.predict([[50**2, 50]]))
→ [1573.98423528]
2차 방정식은 선형이 아닌 비선형이라고 볼 수 있지만 $길이^2$를 다른 변수로 치환하게 되면 선형 관계로 표현할 수 있게 되는데 이런 방정식을 다항식(polynomial)이라 부르며 다항회귀(polynomial regression)이라 부릅니다. 각 세트별로 평가 $R^2$ 점수를 평가하면 훈련셋과 테스트셋에 대한 점수가 크게 높아졌지만 테스트셋의 평가 점수가 조금 더 높은 것으로 보아 과소적합이 조금 남아있는 것 같습니다.
print(lr.score(train_poly, train_target))
→ 0.9706807451768623
print(lr.score(test_poly, test_target))
→ 0.9775935108325121
5. 특성 공학과 규제
지금까지 농어의 길이 특성만 사용하여 선형 회귀 모델을 훈련시켰습니다.
이번엔 여러 개의 특성을 사용한 선형 회귀인 다중 회귀(Multiple regression)를 사용해보도록 하겠습니다. 공급처에서 농어의 길이 뿐만 아니라 높이, 두께 특성도 같이 줬기 때문에 각 특성을 제곱한 값도 추가해보고 길이X높이 등과 같이 새로운 특성도 만들어서 사용해보겠습니다. 이렇게 기존의 특성을 사용해 새로운 특성을 뽑아내는 작업을 특성공학(feature engineering) 이라고 부릅니다. 3개의 특성 값과 무게값을 가져와보겠습니다.
# 특성 공학과 규제
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. ]
...
[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])
특성과 무게 데이터를 훈련셋과 테스트셋으로 나누겠습니다. 그리고 사이킷런에서 제공하는 변환기(transformer)를 사용하여 feature engineering을 해보겠습니다.
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)
# feature engineering
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(include_bias=False) # 회귀식의 절편값 제거
poly.fit(train_input)
train_poly = poly.transform(train_input)
print(train_poly.shape)
→ (42, 9)
기존에 3개였던 특성이 9개로 늘어났습니다. 각 특성이 어떻게 만들어졌는지 get_feature_names() 메서드를 호출해보고 테스트 셋에도 적용해보겠습니다.
poly.get_feature_names()
→ ['x0', 'x1', 'x2', 'x0^2', 'x0 x1', 'x0 x2', 'x1^2', 'x1 x2', 'x2^2']
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))
→ 0.9903183436982125
# 테스트셋 평가
print(lr.score(test_poly, test_target))
→ 0.9714559911594155
학습셋 평가 결과 0.99로 아주 높은 점수가 나왔습니다. 농어의 길이뿐만 아니라 높이와 두께를 모두 사용했고 각 특성을 제곱하거나 서로 곱해서 다항 특성을 더 추가했더니 선형 회귀의 능력이 매우 강하는 것을 알 수 있었습니다. 테스트셋의 점수는 높아지지 않았지만 농어의 길이만 사용했을 때 있던 과소적합 문제는 더 이상 나타나지 않았습니다.
그럼 특성을 더 많이 추가하면 어떻게 될까요? PolynomialFeatures클래스의 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)
→ (42, 55)
특성 개수가 무려 55개나 생겼습니다. 다시 학습해보겠습니다.
lr = LinearRegression()
lr.fit(train_poly, train_target)
# 학습셋 평가
print(lr.score(train_poly, train_target))
→ 0.9999999999938143
# 테스트셋 평가
print(lr.score(test_poly, test_target))
→ -144.40744532797535
학습셋에는 0.99로 동일하게 높은 점수를 보이나 테스트셋은 -144로 아주 낮은 수치를 보입니다. 왜그럴까요?
특성의 개수가 많아지면 선형 모델은 학습셋에 거의 완벽하게 학습을 하는 과대적합이 되어 테스트셋에는 맞지 않는 모델이 됩니다. 이런 현상을 줄이기 위해 규제(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으로 만들수도 있습니다. 릿지와 라쏘를 이용하여 모델을 학습해보겠습니다.
1) 릿지(ridge) 모델
# 릿지 회귀 학습
from sklearn. linear_model import 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.9790693977615386
학습셋의 평가 점수는 조금 낮아졌지만 테스트셋의 평가 점수는 0.97로 정상으로 돌아왔습니다. 많은 특성을 사용했음에도 불구하고 과대적합되지 않아 테스트세트에서도 좋은 성능을 내고 있습니다. 릿지와 라쏘 모델을 사용할 때 규제의 양을 alpha 매개변수로 규제의 강도를 조절할 수 있습니다. alpha 값이 크면 규제 강도가 세지므로 계수 값을 더 줄이고 더 과소적합이 되도록 유도합니다. alpha값이 작으면 계수를 줄이는 역할이 줄어들고 선형 회귀 모델과 유사해지므로 과대적할될 가능성이 커집니다.
alpha값은 학습에 의해 정해지는 값이 아니라 사람이 지정해야하는 값으로 이런 파라미터를 하이퍼파라미터(hyperparameter)라고 부릅니다. 적절한 alpha값을 찾는 한 가지 방법은 $R^2$값의 그래프를 그려봐서 훈련셋과 테스트셋의 점수가 가장 가까운 지점을 찾는 것입니다.
import matplotlib.pyplot as plt
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()
alpha값을 0.001에서 100까지 10배씩 늘려가면서 릿지 회귀 모델을 훈련한 다음 훈련셋과 테스트셋의 점수를 리스트에 저장하여 그래프로 그렸습니다. np.log10() 함수를 사용한 이유는 0.001값이 너무 작기 때문에 왼쪽으로 너무 촘촘해져서 6개의 값을 동일한 간격으로 나타내기 위해 지수로 표현하였습니다.
파란색은 훈련셋, 노란색은 테스트셋으로 왼쪽은 점수 차이가 아주 많이 크고, 과대적합의 전형적인 모습을보익 있습니다. 오른쪽은 훈련셋과 테스트셋이 모두 낮아지는 과소적합으로 가는 모습을 보이고 있습니다.
적절한 alpha값으 두 그래프가 가장 가깝고 테스트셋의 점수가 가장 높은 -1(즉 $10^(-1) = 0.1$)입니다. aplha값을 0.1로 하여 최종 모델을 훈련해보겠습니다.
ridge = Ridge(alpha = 0.1)
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
→ 0.9903815817570368
print(ridge.score(test_scaled, test_target))
→ 0.9827976465386896
훈련셋과 테스트셋의 점수가 비슷하게 모두 높고 과대적합과 과소적합 사이에서 균형을 맞추고 있습니다!!
그럼 이번에 라쏘 모델을 훈련해보겠습니다.
2) 라쏘(lasso) 모델
위와 동일하게 라쏘 모델이 alpha값에 따라 어떻게 변하는지 그래프로 확인해보겠습니다. 릿지 모델과 다르게 라쏘 모델에 max_iter 매개변수가 추가 되었는데 이는 반복횟수가 부족할때 경고메세지가 나와서 반복횟수를 충분히 늘려주기 위해 사용한 것입니다.
# 라쏘 회귀
from sklearn.linear_model import Lasso
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^1$ = 10)입니다. 라쏘는 의미 없는 계수값을 0으로 만드는 특징이 있다고 하였는데 얼마나 많은 feature들을 0으로 만들었는지 확인해보겠습니다.
print(np.sum(lasso.coef_==0))
→ 35
55개의 특성 중 35개를 0으로 만들어서 20개 특성만 사용하였습니다. 이런 특징 때문에 라쏘 모델은 유용한 특성을 골라내는 용도로 사용할 수도 있습니다.
담당자는 규제 모델을 사용해 농어의 무게를 아주 잘 예측할 수 있도록 되었습니다!! (다음장에서 계속....)
감사합니다 :)
참고자료
혼자 공부하는 머신러닝+딥러닝
'머신러닝&딥러닝 > 책요약및리뷰' 카테고리의 다른 글
[혼자공부하는머신러닝+딥러닝] 5. 트리 알고리즘 (0) | 2022.06.22 |
---|---|
[혼자공부하는머신러닝+딥러닝] 4. 다양한 분류 알고리즘 (0) | 2022.06.17 |
[혼자공부하는머신러닝+딥러닝] 2. 데이터 다루기 (0) | 2022.06.13 |
[혼자공부하는머신러닝+딥러닝] 1. 마켓과 머신러닝 (0) | 2022.06.10 |
[XAI 설명가능한 인공지능] 3. 모델 튜닝하기-Xgboost (0) | 2022.05.17 |