혼자 공부하는 머신러닝 + 딥러닝 03-2 선형회귀

손지호·2023년 7월 14일
0

k-최근접 이웃의 한계

# 1절에서 사용한 데이터와 모델 준비
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)
test_input = test_input.reshape(-1, 1)
from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor(n_neighbors=3)
# k-최근접 이웃 회귀 모델을 훈련합니다
knr.fit(train_input, train_target)

여기까지는 1절 내용 그대로!

# 길이가 50cm인 농어의 무게 예측
print(knr.predict([[50]]))
>>> [1033.33333333]
# 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이고 무게가 1,033g인 농어는 세모(marker='^')로 표시, 주변 샘플은 마름모(marker='D')
산점도를 보니 길이가 커질수록 노어 무게 증가하는 경향 있음. 하지만 50cm 농어에서 가장 가까운 것은 45cm 근방이기 때문에 k-최근접 이웃 알고리즘은 이 샘플들의 무게를 평균함!

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

모델이 예측한 값과 일치. k-최근접 이웃 회귀는 가장 가까운 샘플 찾아 타깃을 평균. 따라서 새로운 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값 예측할 수 있다.
예를들어, 100cm인 농어도 여전히 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) : 널리 사용되는 대표적인 회귀 알고리즘. 비교적 간단하고 성능이 뛰어나기 땜누에 맨 처음 배우는 머신러닝 알고리즘 중 하나.
선형이란, 특성이 하나인 경우 어떤 직선을 학습하는 알고리즘

그래프 ①은 모든 농어의 무게를 하나로 예측. 이때 R^2은 0에 가까운 값이 된다.
그래프 ②는 완전히 반대로 예측. 이때 R^2은 음수가 될 수 있다.
그래프 ③이 가장 그럴싸한 직선! 이걸 머신러닝으로 찾을 수 있음!!

사이킷런은 sklearn.linear_model 패키지 아래 LinearRegression 클래스로 선형 회귀 알고리즘 구현해 놓음.
LinearRegression 클래스에도 fit(), score(), predict() 메서드 있음!

from sklearn.linear_model import LinearRegression

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

# 50cm 농어에 대한 예측
print(lr.predict([[50]]))
>>> [1241.83860323]

k-최근접 이웃 회귀를 사용했을 때와 달리 선형 회귀는 50cm 농어의 무게를 아주 높게 예측. 이 선형 회귀가 학습한 직선을 그려보고 어떻게 이런 값이 나왔는지 알아보자!!


여기서 a와 b는 LineearRegression 클래스 속 lr 객체의 coef와 intercept 속성에 저장되어 있다!

print(lr.coef_, lr.intercept_)
>>> [39.01714496] -709.0186449535477

+ coef와 intercept를 머신러닝 알고리즘이 찾은 값이란는 의미의 모델 파라미터(model parametet)라고 부른다. 책에서 사용하는 많은 머신러닝 알고리즘의 훈련 과정은 최적의 모델 파라미터를 찾는 것과 같고 이를 모델 기반 학습이라고 부른다. 앞서 사용한 k-최근접 이웃에는 모델 파라미터가 없다. 훈련 세트를 저장하는 것이 훈련의 전부였던 것. 이를 사례 기반 학습이라 부른다.

농어의 길이를 15에서 50까지 직선으로 그려보자. 앞서 구한 기울기와 절편을 사용하여 (15, 15x39-709)와 (50, 50x39-709) 두 점 이으면 된다.

# 훈련 세트의 산점도를 그립니다
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))
print(lr.score(test_input, test_target))
>>> 0.939846333997604
	0.8247503123313558

무언가 이상. 과대적합 되었다고 하고 싶지만 훈련 세트의 점수도 높지 않아서 애매! 오히려 전체적으로 과소적합되었다고 볼 수 있다. 하지만 과소적합 말고도 다른 문제 발견. (그래프 왼쪽 아래 이상함!)

다항 회귀

선형 회귀가 만든 직선이 왼쪽 아래로 쭉 뻗어 있다. 이 직선대로 예측하면 농어의 무게가 0g 이하로 내려갈 텐데 현실에서는 있을 수 없는 일!
농어의 길이와 무게에 대한 산점도 자세히 보면 일직선보단 곡선에 가까움. 2차 방정식의 그래프 그리려면 길이제곱한 항이 훈련 세트에 추가되어야 한다.

2장에서 사용했던 column_stack() 함수 사용하면 아주 간단!

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

train_input ** 2 식에도 넘파이 브로드캐스팅 적용됨. 데이터셋의 크기 확인해보기.

print(train_poly.shape, test_poly.shape)
>>> (42, 2) (14, 2)

원래 특성인 길이를 제곱하여 왼쪽 열에 추가했기 때문에 훈련 세트와 테스트 세트 모두 열이 2개로 늘어남!
이제 train_poly를 사용해 선형 회귀 모델을 다시 훈련. 이 모델이 2차 방정식의 a, b, c를 잘 찾을 것으로 기대. 여기서 2차 방정식 그래프 찾기 위해 훈련 세트에 제곱항 추가했지만, 타깃값은 그대로 사용함. 목표하는 값은 어떤 그래프를 훈련하든지 바꿀 필요 없음!!

# 테스트할 때는 이 모델에 농어 길이의 제곱과 원래 길이 함께 넣어주어야 한다.
lr = LinearRegression()
lr.fit(train_poly, train_target)

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

1절에서 훈련한 모델보다 저 높은 값 예측함

# 이 모델이 훈련한 계수와 절편 출력해보기
print(lr.coef_, lr.intercept_)
>>> [  1.01433211 -21.55792498] 116.0502107827827

최종적으로 이 모델은 무게 = 1.01 x 길이^2 -21.6 x 길이 + 116.05
그래프를 학습함.
이렇 방정식을 다항식(polynomial)이라 부르며 다항식을 사용한 선형 회귀를 다항 회귀(polynomial regression) 라고 부른다.

2차 방정식의 계수와 절편 a, b, c를 알았으니 훈련 세트의 산점도에 그래프로 그려보자. 여기서는 1씩 짧게 끊어 그려본다.

# 구간별 직선을 그리기 위해 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()

앞서 단순 선형 회귀 모델보다 훨씬 나은 그래프 그려짐. 훈련 세트의 경향을 잘 따르고 있고 무게가 음수로 나오는 일도 없음!!

# 훈련 세트와 테스트 세트의 R^2 점수 평가
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
>>> 0.9706807451768623
	0.9775935108325122

훈련 세트와 테스트 세트에 대한 점수가 크게 높아졌다! 하지만 여전히 테스트 세트의 점수가 조금 더 높기 때문에 과소적합 남아 있음.


선형 회귀로 훈련 세트 범위 밖의 샘플 예측

k-최근접 이웃 회귀를 사용해서 농어의 무게 예측했을 때 발생하는 큰 문제는 훈련 세트 범위 밖의 샘플을 예측할 수 없다는 것. k-최근접 이웃 회귀는 아무리 떨어져 있더라도 무조건 가장 가까운 샘플의 타깃을 평균하여 예측한다.
해결을 위해 선형 회귀를 사용한다. 사이킷런의 LinearRegression 클래스 사용하면 k-최근접 이웃 알고리즘을 사용했을 떄와 동일한 방식으로 모델 훈련하고 예측에 사용할 수 있다.
가장 잘 맞는 직선의 방정식 찾는다는 것은 최적의 기울기와 절편 구한다는 의미. 이 값들은 선형 회귀 모델의 coef와 intercept 속성에 저장되어 있다. 선형 회귀 모델은 k-최근접 이웃 회귀와 다르게 훈련 세트 벗어난 범위의 데이터도 잘 예측함. 하지만 모델이 단순하여 무게가 음수가 나왔다!!
해결을 위해 다항 회귀를 사용했다. 제곱한 값을 훈련 세트에 추가하여 선형 회귀 모델을 다시 훈련. 이 모델은 2차 방정식의 그래프 형태를 학습했고 훈련 세트가 분포된 형태를 잘 표현했다. 또 훈련 세트와 테스트 세트의 성능이 단순한 선형 회귀보다 훨씬 높아짐! 하지만 아직까지 훈련 세트 성능보다 테스트 세트 성능이 조금 높아 과소적합된 경향이 남아 있다.
다음 절에서 이 문제 해결!


전체 코드 (출처 : https://bit.ly/hg-03-2)

# 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)
test_input = test_input.reshape(-1, 1)

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor(n_neighbors=3)
# k-최근접 이웃 회귀 모델을 훈련합니다
knr.fit(train_input, train_target)

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


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

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

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

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


# 선형 회귀
from sklearn.linear_model import LinearRegression

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

# 50cm 농어에 대한 예측
print(lr.predict([[50]]))
print(lr.coef_, lr.intercept_)

# 훈련 세트의 산점도를 그립니다
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))


# 다항 회귀
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)

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

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

# 구간별 직선을 그리기 위해 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))

정리

  • 선형 회귀는 특성과 타깃 사이의 관계를 가장 잘 나타내는 선형 방정식을 찾는다. 특성이 하나면 직선 방정식이 된다.
  • 선형 회귀가 찾은 특성과 타깃 사이의 관계는 선형 방정식의 계수 또는 가중치에 저장된다. (머신러닝에서 종종 가중치는 방정식의 기울기와 절편을 모두 의미하는 경우가 많다.)
  • 모델 파라미터는 선형 회귀가 찾은 가중치처럼 머ㅣㄴ러닝 모델이 특성에서 락습한 파라미터를 말한다.
  • 다항 회귀는 다항식을 사용하여 특성과 타깃 사이의 관계를 나타낸다. 이 함수는 비선형일 수 있지만 여전히 선형 회귀로 표현할 수 있다.

핵심 패키지와 함수

scikit-learn

  • LinearRegression : 사이킷런의 선형 회귀 클래스.
    fitintercept 매개변수를 False로 지정하면 절편 학습 X. 이 매개변수의 기본 값은 True.
    학습된 모델의 coef
    속성은 특성에 대한 계수를 포함한 배열. 즉 이 배열의 크기는 특성의 개수와 같다. intercept_ 속성에는 절편이 저장되어 있다.
profile
초보 중의 초보. 열심히 하고자 하는 햄스터!

0개의 댓글