본문 바로가기

AI/기계학습(Machine Learning)

[기계학습/ML]6. 회귀 알고리즘(1) - K-최근접 이웃 회귀, 선형회귀

728x90
반응형

박팀장은 도미와 빙어를 성공적으로 분류한 Pupbani에게 다음과 같은 머신러닝 프로그램을 만들라고 했다.

  • "농어의 길이, 높이, 두께 데이터로 농어의 무게를 예측할 수 있는 프로그램을 만들어 주세요."
  • "농어의 무게를 정확하게 측정한 샘플 56개, 농어의 길이, 높이, 두께를 측정한 데이터를 보내줄게요."
  • "이렇게 예측하는 문제를 회귀 문제라고 하더 군요. 부탁합니다."

회귀라는 단어에 힌트를 얻어서 머신러닝 프로그램을 작성해보자.

회귀(Regression)

  • 지도학습은 크게 분류회귀로 나뉜다.
  • 분류는 이전에 했던 방식으로 말 그대로 샘플을 몇 개의 클래스 중 하나로 분류하는 것이다.
  • 회귀는 임의의 어떤 숫자를 예측하는 문제이다.
    • 예를 들어 배달이 도착할 시간을 예측하는 것
    • 두 변수 사이의 상관관계를 분석하는 방법
    • 정해진 클래스가 없고 임의의 수치를 출력한다.
  • K-최근접 이웃에도 회귀가 있다는 것을 알게된 Pupbani는 이 방법을 써보기로 했다.

K-최근접 이웃 회귀

  • K-최근접 이웃 회귀도 분류와 똑같이 예측하려는 샘플에 가장 가까운 샘플 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]
     )
  • 다음과 같이 데이터를 준비하고 이를 시각화 해보자.
import matplotlib.pyplot as plt
plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

  • 데이터를 살펴보니 농어의 길이가 커짐에 따라 무게도 늘어나는 것이 보인다.
  • 이제 이 데이터를 훈련 세트와 테스트 세트로 나눠 보겠다.
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, test_input.shape)

  • 그런데 만들어진 데이터 세트가 둘다 1차원 배열이다. 
  • 사이킷런에서 데이터를 사용하려면 2차원 배열이여야 하기 때문에 두 데이터 세트를 2차원 배열로 변형 시켜 준다.
    • Numpy의 reshape() 메서드를 사용하여 배열의 크기를 바꿀 수 있다.
reshape(세로(row), 가로(col))
row나 col의 자리에 -1을 지정하면 나머지 원소로 모두 채우라는 뜻이다.
test_arr = np.array([1,2,3,4,5,6,7,8,9])
print(test_arr)

test_arr = test_arr.reshape(-1,3)
print(test_arr)

  • reshape()를 사용하여 데이터 세트를 2차원 배열로 바꿔 보자.
train_input = train_input.reshape(-1, 1) // 세로로 한줄 세우기
test_input = test_input.reshape(-1, 1)  // 세로로 한줄 세우기
print(train_input.shape, test_input.shape)

 

모델 평가

  • 데이터를 가지고 회귀모델의 학습을 진행하였다.
from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor()
knr.fit(train_input, train_target)
  • 분류 모델에서는 "테스트 세트에 있는 샘플을 정확하게 분류한 개수의 비율"을 평가했고 정확도라고 불렀다.
  • 회귀 모델에서는 "정답을 맞춘 개수의 비율"을 평가한다.
  • 이렇게 평가한 점수를 결정계수(coefficient of determination)이라고 부른다.
    • 간단하게 R^2(R의 제곱)이라고 도 부른다.

print(f"결정계수 : {knr.score(test_input, test_target).round(2) * 100}%")

  • 99% 이면 좋은 값인것 같다. 하지만 결정계수가 얼마나 좋은지는 이해할 수 없다.
  • 다른 값을 계산 해보자.
  • 타깃과 예측한 값 사이의 차이를 구하는 "절댓값 오차 평균"을 구해보자.
    • 어느 정도 예측이 벗어 났는지 가늠하기 좋다.
    • mean_absolute_error()로 구할 수 있다.
mean_absolute_error(타겟 데이터, 예측 데이터(입력 데이터로 예측한))
from sklearn.metrics import mean_absolute_error
# 테스트 세트에 대한 예측
test_prediction = knr.predict(test_input)
# 테스트 세트에 대한 평균 절댓값 오차를 계산
mae = mean_absolute_error(test_target, test_prediction)
print(mae.round(2),end="g\n")

  • 결과를 보니 약 19g 정도 타깃값과 다르다는 것을 알 수 있다.

 

과대적합 vs 과소적합

  • 훈련 세트와 테스트 세트의 결정계수를 확인해 봤는데 이상한 점을 찾았다.
print(f"훈련 세트 결정계수 : {knr.score(train_input, train_target).round(2)*100}%")
print(f"테스트 세트 결정계수 : {knr.score(test_input, test_target).round(2)*100}%")

  • 테스트 세트의 결정계수가 훈련 세트의 결정계수보다 높았다.
    • 왜 이런 현상이 벌어 질까?
  • 일반적으로 모델을 훈련하면 훈련 세트에 잘 맞는 모델이 완성 된다.
    • 훈련 세트의 점수가 높게 나온다.
  • 과대적합(Overfitting) 
    • 훈련 세트는 점수가 높다.
    • 테스트 세트는 점수가 많이 낮다.
    • 모델 복잡도를 낮춰야 한다.(입력 데이터를 줄인다.)
  • 과소적합(Underfitting)
    • 훈련 세트보다 테스트 세트의 점수가 높다.
    • 두 개의 세트의 점수가 모두 너무 낮다.
    • 모델이 단순하거나 데이터 세트의 크기가 작은 경우에 일어난다.
    • 모델을 복잡하게 하거나 데이터 세트의 양을 늘리면 된다.
  • K-최근접 이웃 회귀에서 모델 복잡도를 늘리는 방법은 이웃의 개수를 줄이는 것이다.
    • n_neighbors 속성값을 바꾸면 된다.
# 이웃의 갯수를 3으로 설정
knr.n_neighbors = 3
# 모델을 다시 훈련
knr.fit(train_input, train_target)
print(f"훈련 세트 결정계수 : {knr.score(train_input, train_target).round(2)*100}%")
print(f"테스트 세트 결정계수 : {knr.score(test_input, test_target).round(2)*100}%")

  • 바뀐점
    • 훈련 세트의 결정계수가 높아졌다.
    • 테스트 세트의 결정계수가 훈련 세트의 결정계수 보다 작아 졌다. - 과소적합 해결
    • 두 세트의 결정계수의 차이가 크지 않게 변했다. - 과대적합 아님\

Pupbani는 회귀 모델을 완성하고  박 팀장에게 보고 했다.

  • 이제 실전 테스트를 해보기로 했다.
  • 농어 담당 직원이 길이가 50cm인 농어를 가져와서 테스트를 해봤다.
  • 그런데 실제 무게와 예측 무게의 차이가 너무 많이나 버렸다. 
  • Pupbani는 문제점을 찾아보기로 했다.

 

K-최근접 이웃의 한계

  • Pupbani가 맞닥뜨린 문제를 재현한다.
  • 모델을 학습한다.
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)

train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor(n_neighbors=3)
knr.fit(train_input, train_target)
  • 이제 길이가 50cm인 농어의 무게를 예측해 보겠다.
print(f"예측한 농어의 무게 : {knr.predict([[50]])[0].round(2)}g")

  • 다른 값들과의 비교를 위해서 시각화를 해보겠다.
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 농어의 무게는 그 보다 작은 농어들 보다 작게 예측되었다.
  • K-최근접 이웃 알고리즘에서 무게를 예측할 때 주변의 이웃의 평균 값을 구하기 때문에 50cm의 이웃들인 45cm 근방의 농어들의 무게 값들을 평균한 값으로 예측하게 된다.
  • 이렇듯 K-최근접 이웃 알고리즘은 새로운 샘플이 훈련 세트의 범위를 넘어가면 엉뚱한 값을 예측할 수 있다.

Pupbani는 다른 회귀 알고리즘을 찾아보기로 했다.

 

선형회귀(Linear Regression)

  • 널리 사용되는 대표적인 회귀 알고리즘이다.
  • 특성이 하나인 경우 어떤 직선을 학습하는 알고리즘이다.

  1. 모든 농어의 무게를 동일하게 예측
  2. 완전 반대로 예측
  3. 길이가 길어질 수록 무게도 무겁게 예측
  • 사이킷런에는 3번처럼 예측하는 선형 회귀 알고리즘이 구현되어 있다.
    • sklearn 패키지의 linear_model 아래 LinearRegression 클래스
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_input, train_target)
# 50cm 농어에 대한 예측
print(f"예측한 농어의 무게 : {lr.predict([[50]])[0].round(2)}g")

  • K-최근접 이웃 모델과 다르게 농어의 무게를 높게 측정했다.
  • 어떻게 이런 값이 나왔을까?
  • 선형회귀는 선형 방정식을 만들어서 예측을 한다.

  • LinearRegression 모델에서 기울기와 절편 값(모델 파라미터(Model Parameter))을 얻을 수 있다.
    • coef_ 속성 : a 기울기에 해당하는 값(weight, 가중치)
    • intercept_ 속성 : b 절편에 해당하는 값(bias)
print(f"기울기(weight,가중치) : {lr.coef_}")
print(f"절편(bias) : {lr.intercept_}")

  • 15cm ~ 50cm 까지의 농어 데이터를 직선으로 그려본다.
    • 앞에서 구한 기울기와 절편으로 1차 방정식을 이용해서 무게 값을 구할 수 있다.
      • 무게 = 기울기 x 길이 + 절편
plt.scatter(train_input, train_target)
plt.plot([15, 50], [lr.coef_*15+lr.intercept_, lr.coef_*50+lr.intercept_])
plt.scatter(50, 1241.8, marker='^')
plt.xlabel("length")
plt.ylabel("weight")
plt.show()

  • 이 모델의 결정계수를 구해봤다.
print(f"훈련 세트의 결정계수 : {lr.score(train_input, train_target).round(2) * 100}%")
print(f"테스트 세트의 결정계수 : {lr.score(test_input, test_target).round(2) * 100}%")

  • 훈련세트가 테스트 세트에 비해 많이 높지만 테스트 세트와 훈련 세트의 점수가 많이 낮으므로 과소적합되었다고 말할 수 있다.

다항회귀(Polynomial Regression)

  • 이전의 선형 회귀 그래프를 보면 직선이 왼쪽 아래로 쭉 뻗어 있다.
  • 이 그래프 대로라면 농어의 무게가 0g 이하로 내려가는 경우가 생기는데 현실에서는 있을 수 없는 일이다.
  • 산점도를 잘 살펴보면 일직선이라기 보단 왼쪽 위로 구부러진 곡선에 가까운 그래프이다.

  • 이러한 그래프를 그리려면 길이를 제곱한 항이 훈련 세트에 추가되어야 한다.

  • column_stack()을 이용해 제곱항을 추가한 데이터 세트를 만든다.
train_poly = np.column_stack((train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 2, test_input))
print(f"훈련 세트 : {train_poly.shape}")
print(f"테스트 세트 : {test_poly.shape}")

  • 이 데이터 세트로 선형 회귀 모델을 학습해 보겠다.
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(f"50cm 농어 예측 무게 : {lr.predict([[50**2, 50]])[0].round(2)}g")

  • 이 모델이 훈련한 계수와 절편으로 예측값을 구해 비교해보자
a = lr.coef_
b = lr.intercept_
print(f"기울기 : {a}")
print(f"절편 : {b}")
weight = (a[0] * (50**2)) + (a[1] * 50) + b
print(f"50cm 농어 예측 무게 : {weight.round(2)}g")

  • 동일하게 나온다. 
  • 이 모델은 다음과 같은 그래프를 학습하였다.

  • 이러한 그래프의 방정식을 다항식(Polynomial)이라고 부른다.
  • 이러한 다항식을 사용한 선형 회귀를 다항 회귀(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.show()

  • 2차 방정식인 곡선의 형태로 나오는 것을 확인 할 수 있다.
  • 모델의 결정 계수를 구해서 모델이 잘 학습 되었는지 확인해 보자.
print(f"훈련 세트의 결정계수 : {lr.score(train_poly, train_target).round(3) * 100}%")
print(f"테스트 세트의 결정계수 : {lr.score(test_poly, test_target).round(3) * 100}%")



  • 훈련 세트와 테스트 세트의 점수가 크게 올랐다. 
  • 하지만 아직 테스트 세트의 점수가 더 크므로 과소적합이 남아 있는 것 같다.

Pupbani는 문제 해결을 위해 선형회귀다항회귀를 통해 문제를 해결하려고 했다.

하지만 아직 모델에 과소적합이 남아있다.

이를 해결하기 위해 조금 더 복잡한 모델을 만들어야 할 것 같다.

또 복잡한 모델을 만들다가 너무 복잡해진 경우 모델의 복잡도를 억제하는 방법도 알아봐야 할 것 같다.

728x90
반응형