03-1장에서 배운 k-최근접 이웃 회귀를 사용해서 농어의 무게를 예측했을 때 발생하는 큰 문제는 훈련 세트 범위 밖의 샘플을 예측할 수 없다는 점이다.
앞서 발생한 문제를 재현해보겠다.
이전에 사용했던 데이터를 준비해준다.
import numpy as np
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) # -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]]))
>>> [1033.33333333]
k-최근접 이웃 회귀 모델을 사용하여 농어의 무게를 예측한 결과 1,033g 정도로 예측했다. 다만, 실제 무게는 1.5kg라고 한다.
문제를 확인하기 위해 산점도를 표시하여 확인해보자.
사이킷런의 k-최근접 이웃 모델의 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 농어 데이터이며 🔶는 50cm의 이웃을 나타낸다.
이웃 샘플의 타깃의 평균을 토대로 50cm 농어의 무게를 결정짓기에 1.033kg로 예측한다.
k-최근접 이웃 회귀는 가장 가까운 샘플을 찾아 타깃을 평균한다. 따라서 새로운 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값을 예측할 수도 있다.
만약 농어의 길이가 100cm라면?
print(knr.predict([[100]]))
>>> [1033.33333333]
여전히 1.033kg로 예측한다.
# 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)는 널리 사용되는 대표적인 회귀 알고리즘이다. 선형 회귀는 훈련 세트에 잘 맞는 직선의 방정식을 찾는 것이다.
사이킷런은 선형 회귀 모델을 제공한다. 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-최근접 이웃 회귀를 사용했을 때 보다 선형 회귀는 무게를 높게 예측하였다.
계수 또는 가중치에 저장된다. 머신러닝에서 종종 가중치는 방정식의 기울기와 절편을 모두 의미하는 경우가 많다.fit_intercept 매개변수를 False로 지정하면 절편을 학습하지 않는다. 기본 값은 True 이다.coef_ 속성은 특성에 대한 계수를 포함한 배열이다. 즉 이 배열의 크기는 특성의 개수와 같다. intercept_ 속성에는 절편이 저장되어 있다.일반적으로 선형 회귀는 형식의 직선의 방적식을 따른다. 여기서 는 농어의 길이 는 농어의 무게로 바꾸면 이해가 쉽다.

여기서 는 기울기, 절편을 말한다. 가장 잘 맞는 직선의 방정식을 찾는다는 것은 최적의 기울기와 절편을 구한다는 의미이다. LinearRegression 클래스에서 찾은 는 coef_, intercept_의 속성으로 확인할 수 있다.
print(lr.coef_, lr.intercept_)
>>> [39.01714496] -709.0186449535477
coef_ 이름에서도 알 수 있듯이 머신러닝에서 기울기를 종종 계수(coefficient) 또는 가중치(weight)라고 부른다.
여기서 lr.coef_ 값이 리스트 형식으로 출력되는데 이는 계수가 늘어나면 늘어날 수록 추가가 되기 때문이다. 지금은 1차 방정식이므로 의 계수만 포함된다.
변수이름_의 형태를 가지고 있으면 머신러닝에서 학습한 파라미터임을 인지하자.
coef_, intercept_를 머신러닝 알고리즘이 찾은 값이라는 의미로 모델 파라미터라고 부른다. 많은 머신러닝 알고리즘의 훈련 과정은 최적의 모델 파라미터를 찾는 것과 같다. 이를 모델 기반 학습이라고 부른다. 다만, k-최근접 이웃에는 모델 파라미터가 없다. 훈련 세트를 저장하는 것이 훈련의 전부였기에 이를 사례 기반 학습이라고 한다.
# 훈련 세트의 산점도를 그린다.
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()

훈련 세트와 테스트 세트의 점수를 확인해보자.
print(lr.score(train_input, train_target)) # 훈련 세트
print(lr.score(test_input, test_target)) # 테스트 세트
>>>
0.939846333997604
0.8247503123313558
그렇지만, 훈련 세트와 테스트 세트의 점수가 조금 차이가 나는 것을 확인하였다. 이 훈련 모델은 과대 적합 되었다고 말할 수 있을까?
여기서 훈련 세트의 점수가 k-최근접 이웃 회귀의 점수보다 작게 나온 것을 확인되었기에 과소 적합되었다고 볼 수 있다.
또한, 그래프를 보면 큰 문제가 있는데 선형 회귀가 만든 직선이 왼쪽 아래로 쭉 뻗어 있다. 이 직선대로 예측하면 농어의 길이가 작아질 수록 농어의 무게가 0g 이하로 내려갈 수도 있다. 이 점이 문제점이다.
이를 해결하기 위해 다항 회귀를 사용하였다.
우선, 간단하게 2차 방정식의 그래프를 그리기 위해 길이를 제곱한 항이 훈련 세트에 추가되어야 한다.

Numpy 패키지 내에 있는 column_stack() 함수를 사용하면 간단하다. train_input을 제곱한 것과 train_input 두 배열을 나란히 붙인다.
train_poly = np.column_stack((train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 2, test_input))
train_input ** 2 식에도 넘파이 브로드캐스팅이 적용되어 모든 원소를 제곱하였다.
해당 데이터를 사용하여 선형 회귀 모델을 다시 훈련한다. 여기서 주목할 점은 2차 방정식 그래프를 찾기 위해 훈련 세트에 제곱 항을 추가하였지만, 타깃값은 그대로 사용한다는 점이다.
목표하는 값은 어떤 그래프를 훈련하든지 바꿀 필요가 없다.
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.predict([[50**2, 50]]))
>>> [1573.98423528]
선형 회귀보다는 더 높은 값을 예측했다.
훈련한 계수와 절편을 확인해보자.
print(lr.coef_, lr.intercept_)
>>> [ 1.01433211 -21.55792498] 116.0502107827827
해당 계수와 절편을 토대로 아래의 식으로 학습 된 것을 알 수 있다.
길이² = 왕길이로 치환하면 무게 = 1.01 × 왕길이 - 21.6 × 길이 + 116.05와 같이 쓸 수 있다. 즉, 무게는 왕길이와 길이의 선형 관계로 표현할 수 있다.이런 방정식을 다항식이라고 부르며 다항식을 사용한 선형회귀를 다항 회귀(polynomial regression)이라고 한다.
이전과 동일하게 훈련 세트의 산점도를 그려보자.
# 구간별 직선을 그리기 위해 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()

이 그래프를 보면 무게가 음수로 나올리가 없을 것 같다. 모델의 점수를 평가해보자.
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
>>>
0.9706807451768623
0.9775935108325122
훈련 세트와 테스트 세트의 점수가 크게 높아졌다. 하지만 여전히 테스트 세트의 점수가 높은 것으로 보아 과소 적합이 아직 남아 있는 것으로 판단된다.
JeongeunBae의 Github에서 전체 코드를 확인하실 수 있습니다. 👻
https://github.com/JeongEunBae/TIL/blob/main/Basic_ML_DL%20(%ED%98%BC%EA%B3%B5%EB%A8%B8%EC%8B%A0)/3_2_%EC%84%A0%ED%98%95_%ED%9A%8C%EA%B7%80.ipynb
많은 도움이 되었습니다, 감사합니다.