[ML] 혼공머닝 2편 k-Nearest Neighbors Regressor(k-최근접 이웃 회귀)
카테고리: ML
💡 ‘혼자 공부하는 머신러닝 딥러닝(박해선 저)’ 책을 읽고 공부한 내용을 요약한 페이지입니다.
책에 나오는 코드를 그대로 쓰지 않고, 코드와 파라미터를 변형하고 조정해가며 다른 결과를 출력하며 공부했습니다.
03 회귀 알고리즘과 모델 규제
03-1 k-최근접 이웃 회귀
- k-최근접 이웃 회귀: 예측하려는 샘플에 가장 가까운 샘플 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)
#현재 1차원 배열인 데이터들을 2차원 배열로 만드는 작업
train_input = train_input.reshape(-1,1) #reshape()메서드는 배열 크기를 바꿔줌.
test_input = test_input.reshape(-1,1) #-1을 지정하면 나머지 원소 개수로 모두 채우라는 의미
from sklearn.neighbors import KNeighborsRegressor #k-최근접 이웃 회귀 알고리즘을 구현한 클래스
knr = KNeighborsRegressor()
knr.fit(train_input, train_target)
print(knr.score(test_input, test_target)) #이 때 출력되는 점수가 바로 결정계수(R^2)
#결정계수가 아닌 타깃과 예측의 절댓값 오차를 평균하는 방식
from sklearn.metrics import mean_absolute_error
test_prediction = knr.predict(test_input)
mae = mean_absolute_error(test_target, test_prediction)
print(mae)
- 과대적합(overfitting): 훈련세트에서 점수가 아주 좋았는데 테스트 세트에서는 점수가 굉장히 나쁜 경우
- 과소적합(underfitting): 훈련세트보다 테스트세트의 점수가 높거나 둘 다 너무 낮은 경우
knr.n_neighbors = 3 #기본값 5보다 이웃의 개수를 줄이면 국지적인 패턴에 민감해짐
#즉 overfitting의 여지가 커짐
knr.fit(train_input, train_target)
print(knr.score(train_input, train_target)) #0.9804899950518966
print(knr.score(test_input, test_target)) #0.974645996398761
#테스트세트의 점수가 더 낮고 둘 간의 차이도 크지 않으니 과소적합도 과대적합도 아님!
n_neighbors 매개변수를 늘릴수록 데이터 전반의 경향을 따라가고, 줄일수록 훈련세트 샘플에 철저하게 먖춰지는 모습을 볼 수 있다.
03-1 핵심 키워드
- k-최근접 이웃 회귀: 예측하려는 샘플에 가장 가까운 샘플 k개를 선택하고 이 수치들의 평균값을 예측 타깃값으로 추정
- 결정계수: 대표적인 회귀 문제의 성능 측정 도구. 1에 가까울수록 좋다.
- 과대적합: 훈련세트에서 점수가 아주 좋았는데 테스트 세트에서는 점수가 굉장히 나쁜 경우
- 과소적합: 훈련세트보다 테스트세트의 점수가 높거나 둘 다 너무 낮은 경우
03-1 핵심 패키지와 함수
- scikit-learn
KNeighborsRegressor
: k-최근접 이웃 회귀를 구현하는 사이킷런 클래스. n_neighbors 매개변수로 이웃의 개수를 지정한다. 기본값은 5mean_absolute_error()
: 회귀모델의 평균 절댓값 오차를 계산한다.
- numpy
reshape()
: 배열의 크기를 바꾸는 메서드.
03-2 선형 회귀
k-최근접 이웃 알고리즘의 한계
print(knr.predict([[50]]))
#실제로 저울에 재보니 1500g인데, 최근접회귀모델에선 1033.33g으로 예측함
import matplotlib.pyplot as plt
distnces, indexes = knr.kneighbors([[50]])
plt.scatter(train_input, train_target)
plt.scatter(train_input[indexes], train_target[indexes], marker='D')
plt.scatter(50,1033,marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
#길이가 길어질수록 무게가 늘어나는 것은 당연한데, 길이가 더 길어져도 최근접하는 것들의 무게는 일정하기에, 예측값이 커지지 않는다.
#예를 들어 길이가 100cm라도 무게는 여전히 1033g일 것이라는 말이다.
산점도로 보면 확연한 k-최근접 이웃 회귀의 한계점. 이런 식이면 농어가 아무리 커도 무게가 더 늘어나지 않는다.
- 선형회귀(linear regression): 특성이 하나인 경우 그 특성을 가장 잘 나타내는 직선을 학습하는 알고리즘
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_input, train_target)
print(lr.predict([[50]])) #1241.83 출력
print(lr.coef_,lr.intercept_) #coef에는 기울기 계수가, intercept에는 절편이 저장되어 있다.
plt.scatter(train_input, train_target)
plt.plot([15,50],[15*lr.coef_ + lr.intercept_, 50*lr.coef_ + lr.intercept_])
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))
#두 결정계수(R_squared) 모두 높은 편이 아니라 과소적합 된 듯하다.
#또한, 그래프 좌측하단을 보면 weight가 0아래로 내려가는 경우가 있는데, 무게가 음수일 수는 없으니 말이 안된다.
상기한 두 가지 문제점을 해소하기 위해 선형 회귀가 아니라 다항회귀(polynomial regression) 모델을 적용해야 한다. 다시말해, 최적의 직선을 찾는 것이 아니라 최적의 곡선을 찾는 것이다.
#2차 방정식 그래프를 만드려면 길이를 제곱한 항이 train_input에 추가되어야 한다!
train_poly = np.column_stack((train_input**2, train_input))
test_poly = np.column_stack((test_input**2, test_input))
lr.fit(train_poly,train_target)
print(lr.predict([[50**2,50]]))
print(lr.coef_, lr.intercept_) #[ 1.01433211 -21.55792498] 116.05021078278276
# 즉, 무게 = 1.01*길이^2 - 21.6*길이 +116.05라는 회귀식을 만든 것.
point = np.arange(15,50) #구간별 직선을 그리기 위해 15에서 49까지 정수 배열을 만듦.
plt.scatter(train_input, train_target)
plt.plot(point, point**2*1.01 - point*21.6 + 116.05) #구간별로 직선 여러개를 합쳐 곡선처럼 만든 것.
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))
선형회귀보다 더 적합한 그래프가 그려졌다.
03-2 핵심 키워드
- 선형 회귀: 특성과 타깃 사이 관계를 가장 잘 나타내는 선형 방정식을 찾는 모델
- 모델 파라미터: 선형 회귀가 찾은 가중치처럼 모델이 특성에서 학습한 파라미터를 의미
- 다항 회귀: 다항식을 사용하여 특성과 타깃 사이의 관계를 나타냄
03-2 핵심 패키지와 함수
- scikit-learn
LinearRegression
은 사이킷런의 선형 회귀 클래스이다.coef_
속성은 계수의 배열이고,intercept_
속성은 절편이다.
03-3 특성 공학
지금까지는 하나의 특성을 사용하여 선형회귀모델을 훈련시켰다. 이번에는 여러 개의 특성을 사용한 다중회귀(multiple regression)를 실행해본다. 특성이 2개면 선형 회귀는 평면을 학습한다.
-
특성 공학(feature engineering): 기존의 특성을 가공하여 새로운 특성을 뽑아내는 작업을 의미한다. 예를 들어 ‘농어길이 * 농어높이’로 새로운 특성을 만드는 방식
-
판다스(pandas): 인터넷에서 데이터를 바로 다운로드하여 사용할 수 있는 데이터 분석 라이브러리이다. 데이터프레임은 판다스의 핵심 데이터 구조로, 넘파이 배열과 비슷하게 다차원 배열을 다룰 수 있고 훨씬 많은 기능을 제공한다.
import pandas as pd
df = pd.read_csv('https://bit.ly/perch_csv_data')
perch_full = df.to_numpy()
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) 라고 한다.
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(include_bias = False) #절편은 제외한다는 의미
poly.fit(train_input) #훈련세트의 샘플들을 서로 곱한 값과 제곱한 값으로 특성들을 훈련
train_poly = poly.transform(train_input) #훈련한 값을 토대로 변환
poly.get_feature_names() # 각각의 특성들이 어떤 조합으로 만들어졌는지 이름을 표시
test_poly = poly.transform(test_input)
poly.get_feature_names()
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target)) #0.990318. 매우 높은 점수
print(lr.score(test_poly, test_target)) #0.971455
#5제곱까지 늘려보면 어떨까?
poly = PolynomialFeatures(degree=5, include_bias = False)
#degree매개변수로 고차항의 최대 차수를 지정
poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target)) #0.999999
print(lr.score(test_poly, test_target)) #-144.405
#즉, 특성의 개수를 늘릴수록 형편없는 overfitting이 된다.
03-3 핵심 키워드
- 다중 회귀(mutiple regression): 여러 개의 특성을 사용하는 회귀 모델
- 특성 공학: 주어진 특성을 조합하여 새로운 특성을 만드는 일련의 작업
03-3 핵심 패키지와 함수
- pandas
read_csv()
는 csv파일을 판다스 데이터프레임으로 변환하는 함수이다.
- scikit-learn
PolynomialFeatures
는 주어진 특성을 조합하여 새로운 특성을 만드는 변환기 클래스이다.include_bias
가 False이면 절편을 위한 특성을 추가하지 않는다.degree
는 최고 차수를 지정하고 기본값은 2이다.
03-4 규제
- 규제(Regularization): 모델이 과대적합되지 않도록 만드는 작업이다. 선형 회귀 모델의 경우 특성에 곱해지는 계수의 크기를 작게 만드는 과정이다.
특성의 스케일이 정규화되지 않으면 곱해지는 계수 값에도 차이가 난다. 만약 선형 회귀 모델에 규제를 적용할 때 계수 값의 크기가 서로 많이 다르면 공정하게 규제되지 않을 테니, 먼저 정규화를 해줘야 한다.
from sklearn.preprocessing import StandardScaler #표준점수로 바꿔주는 변환기 클래스
ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)
- 릿지(ridge): 계수를 제곱한 값을 기준으로 규제하는 것
from sklearn.linear_model import Ridge
ridge = Ridge()
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target)) #0.9896. 완벽에 가까운 점수에서 살짝 하락
print(ridge.score(test_scaled, test_target)) #0.9790. 다시 정상으로 돌아옴. 규제가 잘 됐다는 의미.
규제를 할 때에 규제의 양을 조절하는 alpha 매개변수가 있다. 모델이 학습하는 파라미터는 모델 파라미터였다면, 이렇게 사람이 직접 지정하는 파라미터는 하이퍼 파라미터라고 한다.
#적절한 alpha 값을 찾기 위한 비교 그래프 그리기
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_squared')
plt.show()
파란색이 훈련세트, 주황색이 테스트 세트이다.
적절한 alpha값은 두 그래프가 제일 가깝고 테스트 세트의 점수가 가장 높은 -1, 즉 10^-1=0.1로 보인다.
ridge=Ridge(alpha=0.1)
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target)) #0.9903
print(ridge.score(test_scaled, test_target)) #0.9827
#alpha를 0.1로 설정하니 과대적합과 과소적합 사이의 균형을 잘 찾음
- 라쏘(lasso): 계수의 절댓값을 기준으로 규제하는 것
#Ridge클래스를 Lasso클래스로 바꾸기만 하면 됨.
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))
03-4 핵심 키워드
- 릿지: 계수를 제곱한 값을 기준으로 규제하는 것. 효과가 좋아 널리 사용됨
- 라쏘: 계수의 절댓값을 기준으로 규제하는 것. 릿지와 달리 계수를 아예 0으로 만들 수도 있음
- 하이퍼파라미터: 알고리즘이 학습하는 파라미터가 아니라 사람이 사전에 지정하는 파라미터
03-4 핵심 패키지와 함수
- scikit-learn
Ridge
는 릿지 회귀모델을 훈련하는 알고리즘. alpha매개변수로 규제 강도를 조정하고, 기본값은 1이다.Lasso
는 라쏘 회귀모델을 훈련하는 알고리즘. 이 클래스는 최적의 모델을 찾기 위해 좌표축을 따라 최적을 수행해가는 좌표 하강법(coordinate descent)을 사용한다.