본문 바로가기

KHUDA 활동 아카이브/Machine Learning 기초

Ch 3. 선형회귀

도미 - 빙어 : 두 생선 중 어떤 생선인지 판별하는 문제

 

도미와 빙어 각각을 클래스라고 부름

 

우리가 앞서 다룬 문제는 즉 클래스 분류이다.

 

그러면 클래스 분류말고 또 있어?

 

있다.

 

우리는 지금 머신러닝에 대해 다루고 있다.

 

머신러닝은 크게 지도학습과 비지도학습으로 나뉜다.

 

우리는 지도학습 중에서도 클래스분류에 대해 살펴본것이다.

 

자, 그럼 지도학습에 ‘분류’말고 또 뭐가 있나?

 

맞다. 회귀다.

 

우리는 이제 회귀에 대한 예시를 살펴볼것이다.

 

—-

 

우리 생선가게는 이제 도미, 빙어 뿐만 아니라 농어도 팔기 시작헸다.

 

보통 생선들도 그렇듯이, 우리 가게도 농어를 1kg에 얼마 형식으로 팔려고 한다.



그런데 지금 좀 큰일났다.

 

농어를 공급해주는 공급처에서 농어의 무게를 잘못쟀다고 연락이 온 것이다.

 

농어의 길이에 대한 정보는 이상이 없다고 하지만, 우리는 무게가 더 중요한데??

 

불행 중 다행으로, 예전에도 농어를 받을 상황이 한번 있었다. 그때 한 56마리 정도의 농어 무게와 길이에 대한 올바른 값을 저장해뒀었다.

 

즉 저 농어 56마리의 길이와 무게를 이용해서 규칙을 찾아낸 후, 새로운 농어의  길이를 이용해 무게를 예측해야하는 상황이다. 

 

이것도 머신러닝으로 할 수 있을까?



—---------------------------------------

 

위같은 문제를 해결할 수 있는 방법이 ‘회귀’ 이다.

 

그래서 회귀가 뭔데? 첨듣는데? 

아니면

이거 통계학에서 한번 들어본거 같은데? 이게 대체 뭔데?



간단하게 말하면, 회귀는 두 변수 사이의 관계를 분석하는 방법이다.

 

여기서는 농어의 길이와 농어의 무게에 대한 관계를 분석해야하기에 회귀를 사용하는 것이다.



아 ㅇㅋ. 그래서 농어의 길이를 가지고 무게를 어케 분석함? 

뭐 대충 농어의 길이가 길면 무게도 크긴 하겠지. 근데 그게 정확한 관계가 있는건 아니잖아?

 

맞습니다. 그래서 사실 회귀를 사용한다고해서, 길이가 주어지면 정확히 무게를 구할 수 있는건 아니다. 하지만 그래도 규칙을 찾아내서 최대한 오차가 적게 무게를 구하려는게 우리 목적이고, 회귀의 목적이다.

 

  • 오차를 적게… ㅇㅋ. 그러면 회귀 어떻게 하는건데?

 

와 근데 우리가 앞에서 봤던 k-최근접 이웃 알고리즘이 회귀에서도 작동한다네?

 

자 여기서 k-최근접 이웃 분류 알고리즘을 까먹었을 당신에게 다시 상기시켜주겠다.

 

한 생선이 도미인지, 빙어인지 구분하는 문제에서 k- 최근접 이웃 알고리즘을 사용했다.

 

그 원리는 그 생선과 가장 비슷한 무게와 길이를 갖는 생선 k개를 찾은 후, 그 k개 생선 중 도미가 많으면 도미로, 빙어가 많으면 빙어로 판단하는 형식이었다. 예를들어 k가 3이면, 아래와 같이 길이와 무게가 비슷한 3개의 생선을 찾아서 판단했다.

 




k-최근접 이웃 회귀도 비슷하다. 

 

똑같이 내가 예측하려는 샘플과 가장 가까운 k개의 샘플을 선택한다.

 

k-최근접 이웃 분류 알고리즘에서는 그 k개 중 개수가 많은 샘플의 클래스를 선택했다면,

여기서는 그 k개의 값의 평균을 택하는 것이다.

 

  • 오… 뭔말이지?? 이해가 안되는데 예시좀

 

간단한 예시를 들어보겠다. 아래 그림을 보자.

 

 

만약 k가 3이라면, 주변 3개의 샘플들의 값을 찾은 후, 그 값들의 평균이 우리의 예측값이라고 가정하는 형식이다.

 

이 방법을 통해, 농어의 길이를 이용해서 무게를 예측해보도록 하자.



일단 예전에 저장해두었다던 올바른 농어의 길이와 몸무게 정보는 다음과 같다.



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



예를들어, 첫 번째 농어의 길이는 8.4cm이고 무게는 5.9g 이다. 새끼 농어인가보다.

 

이렇게만 보면 뭔가 직관적이지가 않다. x축을 길이로, y축을 무게로 두고 점을 한번 찍어볼까? 앞에서도 봤듯이, 파이썬에서 좌표에 점을 찍기 위한 코드는 다음과 같다.

 

import matplotlib.pyplot as plt

plt.scatter(perch_length, perch_weight)

plt.xlabel('length')

plt.ylabel('weight')

plt.show()

 

 

이제야 좀 직관적이다.

 

그럼 이제 이 데이터들을 훈련모델로 이용해서 어떤 관계인지 찾아야한다. 

 

역시 앞에서 이용한 것처럼 train_test_split 함수를 이용해 훈련 데이터와 검증데이터로 나눠보자.

 

리마인드 : 훈련데이터는 train_input과 train_target으로 보통 지칭한다. 위 예시에서는 train_input에는 농어의 길이에 대한 데이터, 즉 perch_length 리스트의 일부분일 것이다.

train_target은 그 농어의 길이에 매치되는 농어의 무게, 즉 perch_weight 리스트의 일부분이다. 

이 훈련데이터를 이용해 머신러닝 모델(여기서는 k-최근접 이웃 회귀 알고리즘)이 학습을 한다. 얘가 얼마나 잘 학습했는지를 확인을 하기 위해 검증데이터도 필요하다. 이 검증 데이터도 test_input과 test_target으로 나뉜다. train_test_split 함수는 기본적으로 훈련데이터와 검증데이터는 3:1 비율로 나눈다.

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 : ', train_input)

print('')

print('test_input : ', test_input)

print('')

print('train_target : ', train_target)

print('')

print('test_target : ', test_target)



헷갈려하시는 분들을 위해 각각을 출력까지 해봤다. 

 

 

보니깐 잘 나뉘었다.

 

  • 자, 그럼 이제 이 값들을 이용해 k-최근접 이웃 회귀 인가 하는 알고리즘을 이용하면 되는건가?

 

안타깝게도, 바로 저 알고리즘을 사용할 순 없다. 

 

기억을 거슬러 올라가보자. k-최근접 이웃 알고리즘은 사이킷런에 포함되있던거 기억하는가? 그럼 사이킷런은 뭐였지? 거의 대부분의 머신러닝 알고리즘을 포함하는 라이브러리였다.



 근데 이 사이킷런에서 사용할 훈련세트는 반드시 2차원이어야한다.

 

  • 2차원? 갑자기 왠 차원?

 

차원에 대해 간단히 말하고 넘어가겠다.

 

1차원 리스트 : 일반적으로 우리가 아는 리스트 형태는 1차원 리스트이다.

 

ex) [1,2,3,4,5]

 

2차원 리스트 : 리스트 안 각각의 원소(아이템)들이 리스트인 형태를 2차원 리스트라고 부른다.

 

ex) [[1,2,3,4,5], [6,7,8,9,10]] : 행이 2, 열이 5인 2차원 리스트



3차원 리스트는 각각의 원소가 2차원 리스트인 그런 형태일것이다.

ex) [ [ [1,2,3], [4,5,6] ] ,[ [ 7,8,9], [10,11,12] ] ]




위에서 train_input을 보자. 19.6으로 시작하는 리스트가 맞다. 

 

몇 차원인가? 맞다. 1차원이다. 그런데 저걸 강제로 2차원으로 바꿔야한다.

 

  • 어떻게 강제로 2차원으로 바꾸는데?

 

단순하다. 그냥 각각의 원소에 ‘[ ]’ 이걸 씌워주면 끝난다.

 

그러면 train_input은 [ [19.6], [22] , [18.7] …..]    이런식으로 될거다. 그러면 각각의 원소는 [19.6], [22] 로 원소의 갯수가 1개인 리스트, 즉 1차원 리스트가 되므로 train_input은 2차원 리스트가 되는 것이다. 



  • 그럼 저 train_input, test_input을 다 저렇게 일일히 바꿔줘야한다고?

 

그렇다. 여간 귀찮은 일이 아니다. 하지만 다행히도 numpy에서는 이를 간단하게 해주는 함수가 존재한다. 

 



다음과 같이 test_array가 존재한다. 여기서 np.array([1,2,3,4])는 numpy에서 사용하는 리스트인데, 지금은 우리가 흔히 아는 [1,2,3,4]랑 차이가 없다고 봐도 무방하다.

 

여기서 test_array.shape를 출력하라고 했다. shape는 말그대로 모양이다. 어떤 모양인지 알려주는 것이다. 이를 실행하면 뭐가 나올까?







이렇게 나온다. 크기가 4인거는 직관적으로 이해가는데, 뒤에 콤마(,)는 뭐야? 그냥 4라고 표현하면 되지 왜 굳이 저렇게 표현하는거야?



만약 이걸 실행시키면 어떻게 될까?

 

test_array = np.array([[1,2,3],[4,5,6]])

print(test_array.shape)

 

  • 쟤는 뭐…6인가?

 

아니다. 사실 shape는 차원을 나타내주는 도구이다. 얘는 ,(콤마)를 이용해 차원을 나타낸다.

 

위 test_array  2차원인건 알겠다. 그러면 콤마를 이용해서 어떻게 나타내야할까?

 

쉽게 말하면 (행, 열) 형식으로 표현된다.

 

[[1,2,3],[4,5,6]] 은 크게 보면 원소 각각이 [1,2,3] 과 [4,5,6] 으로 두개이고, 각 원소 안에는 3개의 원소가 들어있다. 이를 2행 3열이라고 표현한다. 그래서 이  test_array 의 크기는 (2,3)으로 나타낸다.  그럼 한번 위 코드를 출력해볼까?



 

진짜 그렇게 나왔다.

 

  • 번외 : 1차원 리스트의 shape를 출력했더니 위에서 (4,)로 나왔다. 뒤에 콤마는 왜 붙이는걸까?

그냥 (4)라고 적으면 튜플이랑 헷갈릴 수 있기 때문이다. 저 괄호사이에 있는게 리스트의 shape라는 것을 의미하기 위해 뒤에 콤마를 붙이는 것이다. 

쉽게 말해 아무 의미 없다!



본론으로 돌아오자. 우리는 1차원 리스트를 2차원 리스트로 바꿔야한다.

 

예를 들어 우리는 [1,2,3,4,5,6] 이라는, 크기가 (6,)인 리스트를 (2,3)인 리스트로 바꾸고 싶다.

[[1,2,3],[4,5,6]] 꼴로 말이다. 

 

다행히 이를 쉽게 가능케 해주는 함수가 있다. 바로 reshape이다.

 

그 방법도 엄청 쉽다. 그냥 이렇게 하면 된다!

 

test_array = test_array.reshape(2,3)

 

reshape() 의 괄호 안에 어떤 모양으로 바꾸고 싶은지 입력하면 끝이다.

 

실제로 출력해보면?

 

 

우리가 원하는 모양으로 나온다.

 

(물론, 저 크기가 6인 리스트를 (3,4)로 reshape하고 싶다고 하면 에러가 뜬다. 크기가 (3,4)이려면 총 숫자의 갯수가 12개여야하는데 6밖에 안되기 때문이다.)




너무 많은걸 얘기하는 바람에 까먹었을 당신을 위해 우리가 뭘 구하고 있는지 간단하게 정리하고 넘어가자.

 



자, train_input, test_input은 1차원 리스트이다. 각각의 원소가 몇 개인지 세어보니 각각 42개, 14개다. 그러면 shape는 (42,)와 (14,)가 나올거다.  우리는 이를 2차원 리스트로 바꾸는게 목적이다. 사이킷런에서 그렇게 하라고 하니깐.

이걸 어떻게 할까?



맞다. reshape를 이용하면 쉽다. 

위의 train_input이  [ [19.6], [22] , [18.7] …..] 이런 모양으로 되야하니깐, 42행 1열로 모양을 바꿔버리면 된다.

 

train_input = train_input.reshape(42,1)

test_input = test_input.reshape(14,1)

 

print(train_input.shape)

print(test_input.shape)



이렇게만 하면 된다!! 그러면 저걸 print해보면 역시

이렇게 예쁘게 잘 나온다.





  • 근데 우리가 여기서 원소의 개수가 42개, 14개라는걸 알아서 다행이지, 몰랐으면 하나하나 세보고 (42,1)이라고 할뻔했네? 좀 귀찮은데?

 

인정. 그래서 좀 쉬운 방법이 있다. 우리가 원소의 개수를 모른다고 해보자. 그러면 우리가 reshape(42,1)이라고는 못적을 것이다. 행이 42개가 있는지 모르니깐. 하지만 어쨋든 열은 1인거를 알고있다. 그렇게 되야만 하니깐. 그런데 사실 행과 열을 곱한 수가 원래 1차원 배열의 원소 개수이기 때문에, 뒤에 열만 알면 행의 숫자는 자동으로 정해진다. 

 

그래서 우리는 행과 열 중 하나의 숫자만 적고, 나머지 숫자는 그냥 -1로 적어버리면 컴퓨터가 알아서 해준다!!



즉, 우리가 위 예시에서 원소의 개수가 42개라는걸 몰랐어도, 우리는 이렇게 적으면 되는 것이다!!

 

train_input = train_input.reshape(-1,1)

test_input = test_input.reshape(-1,1)

 

print(train_input.shape)

print(test_input.shape)

 

이거 한번 출력해보면?

 

 

역시 이렇게 나온다!!





아무래도 파이썬의 기초지식만 있는 독자분들을 대상으로 설명하려다보니 서론이 길어졌다.

 

이제 한번 k-최근접 이웃 회귀 알고리즘을 직접 써보자!!

 

k-최근접 이웃 회귀 알고리즘의 사용방법은 앞서 사용한 k-최근접 이웃 분류 알고리즘과 거의 똑같다. 이름이 너무 길어서 짧게 k회귀, k분류 알고리즘이라고 부르겠다.



k분류 알고리즘에서 사용한 클래스는 KNeighborsClassifier 였다. 

k회귀 알고리즘에서 사용할 클래스는 KNeighborsRegressor이다. 코드로 살펴보면 다음과 같다.



 

내친김에 fit까지 이용해서 학습도 시켜보자. fit() 함수 안에 train_input, train_target 을 넣으면 얘가 k회귀 알고리즘을 사용해 학습을 시작한다.

 

얘가 잘 학습했는지 확인하기 위해 검증 데이터(test_input, test_target)을 넣어서 테스트해보자. 테스트하는 함수는 score이다. 점수를 보여주는 것이다.




 

오, 점수가 정말 높게 나왔다. 점수는 0점부터 1점 사이의 값으로 나오니 0.99이면 99점인 것이다. 

 

 저 점수는 어떻게 나오는 건데? 0.9928… 같은 점수는 어떻게 도출되는건데?




이 질문을 했다면 당신은 이 분야에 잘 맞는 사람일지도 모르겠다.



이전 글에서 k분류 알고리즘을 소개할 때도 score함수를 사용했다. 이때 이런 말을 했다.

 

즉 k분류 알고리즘에서는 정답을 맞힌 개수의 비율이 점수였다.



그러면 k회귀 알고리즘이 점수를 내는 원리는 뭘까? 얘는 값을 정확하게는 못맞추고 조금이라도 오차가 작게 맞추는게 목적인데?

 

얘는 결정계수(R^2) 라는 것으로 점수를 판단한다.

 

머리가 지끈거려 오는거 이해한다. 수학 맞다. 그래서 깊이는 안다루려 한다. 그래도 궁금한 사람이 있을 수 있으니 링크를 남긴다. 사실 그렇게 안복잡하고 글도 짧으니 한번 읽어보는 것도 도움될 것이다.

 

https://ko.khanacademy.org/math/statistics-probability/describing-relationships-quantitative-data/assessing-the-fit-in-least-squares-regression/a/r-squared-intuition



대충 결정계수가 점수이니, 높을수록 좋다. 하지만 얘는 너무 직관적이지 않다. 어렵기 때문이다.

 

그래서 우리는 좀 간단하게 접근해보자. 

 

우리가 k회귀 알고리즘으로 만든 저 knr이라는 놈을 이용해 test_input 을 예측해보고, 이를 실제 정답인 test_target이랑 비교해보는 것이다. 너무 간단하다.

 

비교 방법은 그냥 각각의 오차를 더해서 평균내보는거다. 

 

이것조차 귀찮아하는 우리를 위해, 사이킷런은 이것도 해주는 함수가 있다. 보통 sklearn.metrics라는 패키지 안에 이런것들이 존재한다. 코드로 보면 이렇다.

 

from sklearn.metrics import mean_absolute_error

 

# 예측시킬때는 predict라는 함수를 사용한다.

test_prediction = knr.predict(test_input)

 

 

mae = mean_absolute_error(test_target, test_prediction)

print(mae)



이를 실행했더니 다음과 같은 결과가 출력됐다.

 

 

즉, 이 knr을 이용해서 값을 예측했더니 평균적으로 19.15그램 정도 오차가 발생했다는 의미이다.

  • 과대적합 vs 과소적합



우리는 앞에서 knr의 점수를 구할 때 knr.score() 안에 test_input과  test_target을 넣었다. 당연히 테스트 용도의 데이터를 넣어야 정상적으로 점수가 구해질 것이기 때문이다.

 

근데 만약에 저  score안에 train_input과 train_target을 넣으면 어떻게 될까?

 

  • 당연히 100점 나오겠지;

 

한번 넣어볼까?

 

print(knr.score(train_input, train_target))

 

이걸 실행하면 될거다. 실행하면?

 

 

  • ??? test_input이랑 test_target을 넣은 점수가 99점이었는데?? 어떻게 테스트용 데이터보다 점수가 더 낮지?

 

맞다. 이게 말이 되나? 어떻게 우리가 훈련시킨 데이터를 다시 줬는데도 테스트 데이터를 줬을때보다 점수가 낮을 수 있을까?



뭔가 이상하다.



여기서 과대적합과 과소적합이라는 개념이 나온다.

 

만약 훈련세트에서 나온 점수가 테스트 세트에서 나온 점수보다 높다고 가정해보자. 사실 이게 상식적으로 맞을거라고 생각할 것이다. 근데 훈련 세트의 점수에 비해 테스트 세트의 점수가 터무니없이 낮다면, 이를 훈련한 모델은 너무 훈련 세트의 데이터에만 적합한 모델이고, 다른 일반적인 경우를 고려하지 않은 모델일 것이다. 즉 훈련 세트의 데이터에 과도하게 적합한 것이다. 이를 과대적합(overfitting)이라고 부른다.



반대로, 지금 우리 상황같은 경우를 보자. 훈련 세트의 점수보다 테스트 세트의 점수가 더 높은 경우이다. 이는 모델이 너무 단순해서 훈련 세트에서 잘 훈련을 못한 것이다. 이를 과소적합(underfitting)이라고 부른다.



그렇다. 우리의 모델은 지금 과소적합인 상태이다. 

 

그 이유는? 데이터양이 작은 것도 있겠지만, 일단 모델이 너무 단순하기 때문이다.

 

  • 모델이 단순한건 또 뭔데??

우리 모델은 k회귀 모델이다. 얘가 단순하단다. 얘가 왜 단순한지는 조금 뒤에 살펴보자.

 

아무튼 단순한건 그렇다 치자. 그러면 더 복잡한 다른 모델을 사용해야하나?  그럴 필요 없다. 얘를 더 복잡한 모델로 만드는 방법이 있다. 바로 k값을 감소시키면 되는거다. 즉, 지금 모델이 참고하고 있는  주변 이웃들을 더 줄이는 것이다. 

 

  • 왜 주변 이웃들을 줄이면 더 복잡해진다는 거지??



주변 이웃들을 줄였다고 생각해보자. 그러면 이 모델은 주변 소수의 데이터에 의해 예측이 결정된다. 그러면 원래 데이터보다 조금 엇나간 데이터들에게도 조금 더 민감하게 반응 할 수 있다. 즉, 더 다양한 패턴을 학습할 수 있는 것이다. 

 

그 말은 즉슨, k값이 크다면, 즉 주변 이웃을 더 많이 살펴본다면 이러한 다양한 패턴들을 학습하지 못한다는 말이다. 그래서 모델이 단순하다는 말을 사용한거다.

 

  • ㅇㅋ 그럼 k값을 낮춰보고 다시 테스트해보면 되겠네.



k회귀와 k분류 알고리즘 모두 기본적인 k값은 5이다. 주변 5개의 이웃을 참고한다는 말이다.

 

이를 3 정도로 낮춰보면 좀 해결이 될까? 모델의 k값은 그 모델의 n_neighbors 변수로 저장된다. 이를 3으로 바꿔보자. 

 

knr.n_neighbors = 3

 

이제 train_input과 train_target으로 다시 훈련시켜볼까?

 

knr.fit(train_input, train_target)

print(knr.score(train_input, train_target))

 

자, 이러면 점수가 몇점이 나올까?

 

 

  • 뭐야, 그래도 99점보다 낮은데??

 

아니지. 이웃의 수를 바꿨으니깐 당연히 test_input과 test_target을 이용한 점수도 바뀌었을 거다. 이것도 한번 출력해볼까?



print(knr.score(test_input, test_target))

 

이제야 테스트 데이터에 대한 점수가 훈련 데이터 점수보다 낮아졌다. 

과소적합을 해결한 것이다. 

 

  • ㅇㅋ 그러면 k는 낮을수록 좋은거네

 

절대 아니다. 우리는 과소적합을 해결하기 위해 k값을 낮춘거다. k를 무조건 낮게 설정한다면 당연히 그 반대인 과대적합 문제가 발생할 것이다. 너무 훈련데이터에만 과도하게 맞는 모델이 탄생하는 것이다. 그러면 당연히 테스트 데이터 점수는 처참할거다. 

 

그래서 우리가 k-최근접 이웃 알고리즘을 사용할 때, 과대적합과 과소적합이 발생하지 않도록 적절히 저울질을 해야하는 것이다. 

 

  • 그러면 모델이 과대적합인지 과소적합인지 판단하는 기준도 있나? 최적의 k값을 찾는 방법이 뭔데?

 

아쉽게도 이는 지금 다루지 못한다. 필자도 지금 배워가고 있는 과정인데, 이는 Ch 5장에 답이 나온다고 한다. 그때가서 다시 언급해보도록 하겠다.




아무튼 우리는 성공적으로 회귀모델을 만드는데 성공했다. 

 

k회귀 알고리즘을 사용해 점수를 계산하고, 이게 과소적합임을 깨달아서 k값을 높임으로써 말이다.





3-2. 선형회귀

 

이전 글의 상황을 요약하면 다음과 같다.

 

‘우리 생선가게에 농어를 공급해주는 공급처에서 농어의 길이는 정상적으로 보내줬지만, 농어의 무게는 정상적인 데이터가 아니라고 했다. 나는 이를 k-최근접 이웃 회귀 알고리즘을 이용해, 길이를 알면 무게를 예측할 수 있도록 했다. ‘

 

충분히 합리적이라고 생각하나?

 

이 상황을 보면 생각이 달라질 것이다.

 

“ 내가 만든 모델이 정상적인 모델인지 테스트하기 위해, 사장님이 무려 50cm 짜리 대물 농어를 들고왔다. 비정상적으로 큰 농어의 무게도 잘 맞추는지 테스트한다면서 말이다. 뭔가 불안하다. 내가 학습시킨 데이터에 50cm 짜리 농어는 없었다. 아니나 다를까, 실제 무게보다 훨씬 작게 나온다고 엄청 뭐라했다. 왜 그런걸까?? “

 

진짜 왜그런걸까?



복습하는겸 앞에서 다뤄봤던 모델을 다시 만들어보자. 주어진 학습 데이터는 다음과 같았다.

 

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



이제 train_test_split을 이용해 훈련 데이터와 테스트 데이터로 학습 데이터를 분리했었다.

 

사이킷런에서 input 값들은 모두 2차원이어야 한다고 했으니, reshape(-1,1)을 이용해 억지로 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)

 

train_input = train_input.reshape(-1,1)

test_input = test_input.reshape(-1,1)




그 다음 k-최근접 이웃 회귀 알고리즘을 이용한 모델을 정의했는데, 과소적합을 해결하기 위해 근처 이웃의 값 k를 3으로 줄였었다. 이웃 값을 나타내는 변수는 n_neighbors였다.

 

from sklearn.neighbors import KNeighborsRegressor

 

knr = KNeighborsRegressor(n_neighbors = 3)

 

그리고 이 knr을 훈련시키기 위해 fit 함수를 사용하고, 파라미터로 훈련 데이터인 train_input과 trian_target을 집어넣었다.



knr.fit(train_input, train_target)



이렇게 모델을 완성했다. 이제 한번 50cm 농어를 넣으면 우리 모델은 무게를 얼마로 예측하는지 볼까?

 

input은 무조건 2차원이어야 한다고 했으므로, [[50]] 을 집어넣어야겠다. predict함수를 이용하면 모델이 어떻게 예측하는지 확인할 수 있다.

 

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

 

 

1033그램 정도라고 예측한다. 근데 실제로 사장님이 들고온 50cm 농어의 무게는 1.5kg, 즉 1500g이라고 한다. 오차가 무슨 50퍼다. 왜 이런 문제가 발생한걸까?




이 50cm 농어가 학습 데이터들에 비해 어디 정도에 위치하는지 직관적으로 알고 싶다. 그러면? 좌표를 찍어보면 된다. 

 

앞에서 계속 봤듯이, 좌표를 찍으려면 matplotlib를 import해서 scatter() 함수로 찍으면 됐었다. 

 

근데 우리는 학습데이터 뿐만 아니라, 50cm 농어의 위치도 찍어보고 싶다. 농어의 무게는 우리가 예측한 값인 1033그램으로 말이다. 그리고 이 1033그램이라는 무게가 나오게끔한 주변 3개의 점들도 확인하고 싶다.



머신러닝 라이브러리 1황 사이킷런은 주변 점들의 정보를 제공해주는 함수도 존재한다. knr.kneighbors([[50]])을 입력하면 주변 3개의 이웃들의 거리와 그 이웃들의 정보도 제공한다. 

 

distances, indexes = knr.kneighbors([[50]])

print(distances, indexes)

 

 

이 의미는, 50cm 농어의 점으로부터 6, 7, 7만큼 떨어져있는 점들이 가장 가까운 3개의 점들이고, 걔네들은 train_input의 34, 8, 14 인덱스라는 것이다. 즉, train_input[34], train_input[8], train_input[14]가 그 3개의 점들이다.

 

이 3개의 점들은 다른 훈련 데이터와는 구분되게 표기하고 싶다. 동그란 점이 아닌 마름모로 표기해볼까? 그러면 이렇게 나타내면 된다.

 

plt.scatter(train_input[indexes], train_target[indexes], marker='D')

 

indexes에 점들의 인덱스 정보가 있었으므로, 이렇게 하면 그 3개의 점들이 다시 찍히는데. 특히 marker=’D’ 옵션으로 인해 마름모로 점이 다시 찍힌다.

 

또한 50cm 농어도 좀 특별한 경우이므로, 얘는 삼각형으로 찍어보자.

 

plt.scatter(50, 1033, marker='^')

plt.show()

 

이러면 아래와 같이 그래프가 나타난다.

 



확실히 저 세모는 다른 점들과 경향이 좀 다른거 같다. 혼자 뚝 떨어져있는 느낌이 있지 않나?

진짜 예리하신 분들이라면 뭐가 잘못된건지 파악했을 것이다. 

 

만약 100cm 농어의 무게를 예측한다고 생각해보자. 그런데 농어의 길이가 몇 센티이든, 어차피 그 농어 근처의 세 점은 저 다이아몬드 점 3개로 항상 같을 것이다. 즉, 100cm이든 1000cm이든 k-최근접 알고리즘을 활용한 모델로 예측하면 항상 1033.3333그램으로 무게를 예측할 것이란 말이다.



진짜 그런지 확인해볼까?



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

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

 

 

역시나 그렇다. 여러분이 짐작한대로, 이는 명백히 k-최근접 알고리즘의 한계이다. 



어쩔 수 없다. 이제는 더 나은 알고리즘을 찾아야한다. 생선가게 직원도 그렇게 생각한 모양이다.



“ k-최근접 알고리즘은 쓰레기다. 다른 알고리즘이 필요해. 그래도 회귀는 사용해보고 싶은데…  가장 유명한 회귀 알고리즘 중 하나인 선형회귀를 한번 사용해볼까?”



선형회귀(linear regression)는 가장 유명하면서도 쉬운 회귀 알고리즘이다. 

 

말 그대로, ‘선’ 형태의 어떤 것을 활용해 예측한다는 말이다.

 

무슨 말이냐,

 

 

이렇게 점들을 잘 나타내는 빨간색 직선을 찾아서, 이 직선을 이용해 예측해보겠다, 이 말이다.

 

역시 사이킷런에는 선형회귀도 포함되어있다. 

 

코드로 나타내보면 다음과 같다.

 

from sklearn.linear_model import LinearRegression

lr = LinearRegression()

 

사실 사이킷런에 포함된 여러 알고리즘의 모델 클래스는 사용법이 거의 다 동일하다.

 

위와 같이 LinearRegression 클래스를 생성하고, fit(), score(), predict() 함수를 이용해 모델을 훈련시키고, 검증하고, 예측하는 형식으로 말이다. 그러니 이 선형회귀를 코드로 나타내는 방법도 이해하기 쉬울거다.



fit을 이용해 모델을 훈련시키자.

lr.fit(train_input, train_target)



이제 50cm 농어의 값을 대입해 무게를 예측해볼까?

 

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

 

역시나 k어쩌고 알고리즘보다 더 정확하게 예측했다. 점들의 성향을 이용해 예측한 것이니 직관적으로도 그럴만 하다. 

 

  • 수치만 보면 그렇네. 근데 정확히 저 직선을 이용해 어떻게 예측을 하는건데?



위에서 선형회귀는 점들을 가장 잘 나타내는 직선을 찾는게 목표라고 했다. 즉, 점들을 대표하는 일차함수를 찾는 것이다. 여러분이 아무리 수학을 싫어해도, 일차함수 정도는 알고 있으리라 믿겠다. 

 

일차함수는 y=ax+b꼴로 나타내진다. 위 예시에서 x는 농어의 길이, y는 농어의 무게일 것이다. 

 

  • 오 그러면 사이킷런의 선형회귀 알고리즘을 활용한 모델은 저 a,b값을 찾았다는 말인가?

 

정확하다. 어떤 방식으로 a,b 값을 결정했을까? 

 

아쉽게도 그 방식은 여기에서 다루지 않을거다. 중3이 이거 이해하려면 머리터질거 같기도 하고, 수학적인 내용이 너무 많기 때문이다. 우리는 입문자이니, 대충 어떤 원리로 돌아가는지만을 습득하는 것을 목표로 하자. 




아무튼 모델이 어떤 방법을 통해 a,b값을 찾았다고 한다. 그 a,b값은 다음과 같이 확인할 수 있다.

 

print(lr.coef_, lr.intercept_)

 

 

여기서 lr.coef_는 lr 모델의 coefficient(가중치)라는 의미로, 여기서는 기울기인 a값을 의미한다. 

또한 lr.intercept_는 lr 모델의 intercept(절편)라는 의미로, 여기서는 y절편인 b값을 의미한다.

 

즉, lr 모델은 y = 39.01714496x - 709.0186449535477 이라는 일차함수를 세웠다. 그리고 50cm 농어의 무게를 구하기 위해 x값에 50을 대입했고, 그 y값으로 1241.8360323이라는 무게가 나온 것이다.



일단은 구하는 방법은 잠시 제쳐두고, 정말 이 직선이 점들과 유사한 경향을 가지는지 눈으로 확인해보자.

 

그래프에 점을 찍는 방법은 matplotlib의 scatter()를 이용한다고 계속 말했었다. 직선을 그리는 방법은 plot()을 이용하는 것이다. 즉, 다음 코드를 실행하면 될 것이다.



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

 

여기서 50cm 농어를 나타내는 점을 잘 나타내기 위해 삼각형 점으로 표현했다.





  • 오, 확실히 점들을 잘 나타내는 직선이 그려진거 같네…. 그런데 뭔가 좀 어색한데? 뭔가 직선이 아니라 곡선 형태가 보이는건 나만 그런가??



나도 그렇다. 확실히 저 직선이 점들의 경향을 잘 표현한 것 같긴 하지만, 뭔가 최선은 아닌거 같다. 한번 테스트를 거쳐볼까?

 

테스트는 socre()함수를 이용한다고 앞에서도 언급한 바가 있다. 혹시나 직선이 과대적합이거나 과소적합일 수도 있기에, 테스트 데이터 뿐만 아니라 훈련 데이터로도 테스트해보자. 

 

print(lr.score(train_input, train_target))

print(lr.score(test_input, test_target))

 

 

  • 일단 훈련 데이터가 테스트 데이터보다 점수가 높으니깐 과소적합은 아니네.

 

우리는 그렇게 배웠다. 앞에서도 그렇게 언급했었고. 하지만 이 경우는 조금 다르다. 앞서 살펴보았던 score 값은 거의다 95점 이상이었고, 99점도 있었다. 하지만 이 경우 훈련 데이터를 넣었음에도 불구하고 점수가 93점밖에 안나왔다. 100점이 나와 과대적합이 아닐지 걱정해도 모자랄 망정, 93점이 나온 것이다. 아무리 훈련 데이터가 테스트 데이터보다 점수가 더 나왔다고 해도, 93점은 너무 낮은 점수이기에 이는 오히려 과소적합이라고 판단할 수 있다.

 

  • 흠… 뭔가 찜찜하지만 오케이. 그러면 어떻게 해결해야 하는데?



예측한 대로다. 바로 직선이 아닌 곡선을 이용하면 된다. 우리가 알고 있는 곡선 중 가장 간단한 함수가 무엇인가? 바로 이차함수이다. 즉, y = ax+b꼴이 아닌, y = ax^2 + bx + c꼴을 이용해서 a,b,c를 구하게 하면 어떨까?

 

 

이런식으로 말이다. 

 

일차함수를 이용할 때에는 ‘무게 = a*길이 + b’ 꼴이었으므로, ‘길이’가 train_input이었고 ‘무게’가 train_target 이었다. 그런데 이차함수에서는 ‘길이^2’ , ‘길이’가 train_input이 돼야하므로 열이 2개여야 한다. 즉, train_input이 [ [19.6], [22], ….] 꼴이 아닌

[ [19.6^2, 19.6], [22^2, 22], ….] 꼴이 되어야하는 것이다. for문을 이용하면 뭐 만들수는 있겠지만 역시 귀찮다. 그래서 이를 쉽게 해주는 numpy 함수가 존재한다. 

 

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

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

 

바로 column_stack 함수이다. 위와 같이 np.colum_stack((train_input**2, train_input)) 을 실행하면 다음과 같은 일이 발생한다.

 

  1. train_input = [[19.6], [22], ….] 형태로 그대로 존재한다.
  2. train_input**2, train_input 순서이므로, 먼저 첫 항 19.6에 대해, 19.6을 제곱한 값을 먼저 적고 그 다음 19.6을 적는다.
  3. 이렇게 한 세트가 train_input의 19.6자리에 들어간다고 보면 된다. 즉, [ [19.6**2, 19.6], ….] 이런 형식으로 말이다.

 

  1. 그 뒤로도 마찬가지로 [22] 대신 [22**2, 22]가 들어온다.
  2. 따라서 train_poly는  [ [19.6^2, 19.6], [22^2, 22], ….] 꼴이 되는 것이다.



위에 적은 순서는 이해를 돕기위한 순서이지, 실제로 이렇게 동작한다는 것은 아닌 것을 참고하기 바란다.

 

아무튼 이렇게 바뀐 trian_poly와 test_ploy의 shape는 어떻게 될까?

 

  • 내 기억으로는 train_input과 test_input은 원래 (42,1)와 (14,1)였지. 이제 각 원소들을 제곱한 값이 하나의 열로 추가되었으니깐 (42,2)와 (14,2)이 아닐까?



확인해보자.

 

print(train_poly.shape, test_poly.shape)

 

 

예상한대로 잘 나왔다. 굳이 train_poly를 표를 이용해 그려보면 다음과 같을 것이다.



 

자, 이제 코드를 이용해 모델에게 직접 학습시켜보자.

 

LinearRegression 클래스를 이용해 선형회귀 모델을 다시 정의하고, 훈련데이터로는 train_input이 아닌 train_poly를 대입하고, train_target은 그대로 냅두면 된다.



lr = LinearRegression()

lr.fit(train_poly, train_target)

 

이제 50cm 농어를 예측시켜보자. train_poly는 길이의 제곱과 길이가 한 쌍이었으니 [[50]]이 아닌 [[50**2, 50]]을 넣어야할 것이다.

 

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

 

50cm 농어의 실제 무게가 1500그램이라 했으므로, 정말 근접한 값을 내놓은걸 볼 수 있다.

 

그러면 실제 이 lr모델은 a,b,c값을 어떻게 설정했을까? 마찬가지로 coef_와 intercept_를 출력해보면 된다.



print(lr.coef_, lr.intercept_)

 

다음과 같이 값이 나오는데, 여기서 coef_ 안의 두 개의 값 1.01433211과 -21.55792498이 각각 a,b이고 116.0502107827827이 c값이다. 즉, 이차함수 식은 다음과 같다.



  • ㅇㅋ 이해완료. 근데 이건 이차함수잖아. 일차 함수일때는 선 모양이었으니깐 선형회귀라고 부를 수 있었는데, 이것도 선형회귀라고 부를 수 있나?



좋은 지적이다. ‘길이의 제곱’ 때문에 이는 선형이 아니라고 생각할 수 있다. 하지만 여기서 ‘길이의 제곱’ 을 가령 ‘왕길이’로 치환한다고 생각해보자. 그러면 이 식은 ‘무게 = 1.01 x 왕길이 - 21.6 x 길이 + 116.05 ‘ 와 같이 쓸 수 있다. 즉, 무게는 왕길이와 길이의 선형 관계로 표현할 수 있기 때문에 선형회귀라고 부르는 것이다.



이렇게 2차식 이상의 다항식을 이용한 선형회귀를 다항 회귀라고 부른다.

 

이 이차함수를 직접 좌표에 나타내보는건 여러분들이 직접 해보시길 바란다. 결과는 이렇다.

 



너무 만족스러운 결과이다. 내친김에 이것도 한번 훈련 세트와 테스트 세트를 계산해보자.

 

다시 기억해보자. 테스트는 score 함수를 이용했다.

 

print(lr.score(train_poly, train_target))

print(lr.score(test_poly, test_target))


확실히 점수가 높아지긴 했다. 그런데 뭔가 이상한 점이 보인다.

 

  • 뭐야, 왜 훈련 데이터보다 테스트 데이터의 점수가 높지?? 설마 과소적합???



맞다… 과소적합이 발생했다. 이렇게나 열심히 모델을 갈고 닦았는데도 과소적합이 발생하다니, 절망스럽기 짝이 없다. 하지만 희망스러운 점은, 아직 더 정확한 모델을 만들 수 있다는 것이다. 과소적합이 발생했으므로, 조금 더 복잡한 모델이 필요할거 같다. 

 

더 복잡한 모델은 다음 장에서 소개한다.










다중 회귀

 

앞의 생선가게 상황을 요약하면 다음과 같다.



“나는 생선가게에서 일하고 있다. 그런데 어느 날 농어를 공급해주는 업체에서 농어의 무게 정보가 이상하다고 우리한테 말했다. 무게를 잘못 측정했다고 한다. 농어는 kg에 얼마로 팔아야하기 때문에 각 생선의 무게를 꼭 알아야한다. 대신 농어의 길이에 대한 정보는 정상적이라고 한다. 다행히 예전에 농어를 공급받았을 때에는 길이, 무게 모두 정상적이었고, 그 정보가 나한테 남아있었다. 그래서 나는 그 정보를 머신러닝 모델에 학습시키려고 한다. 그리고 이번에 들어온 그 농어들의 길이를 이용해 무게를 예측하려는 셈이다. k-최근접 알고리즘인가 하는 모델을 이용했었다. 이 모델이 성공적으로 무게를 예측해주는줄 알았는데,  50cm짜리 거대 농어의 무게는 형편없게 예측했다.그래서 k어쩌고 알고리즘 대신 선형회귀를 사용해봤다. 특히 직선 형태보다는 곡선 형태(이차 함수 형태)가 더 높은 score 점수를 받았다. 하지만 그럼에도 테스트 데이터가 훈련 데이터보다 점수가 높았다. 아래는 각각 훈련 데이터와 테스트 데이터였다.

즉, 과소적합이 발생한 것이다. 과소적합은 모델이 단순할 때 발생한다고 배웠던거 같다. 그러면 우리는 선형회귀를 보다 복잡한 모델로 만들어야 한다. 어떻게 해야할까?”





다행히 나의 지인인 홍 선배가 이 분야의 전문이었기에, 이 선배한테 조언을 구해봤다.

“근데 보통 생선을 받을 때에는 그 생선의 무게랑 길이 뿐만 아니라 높이랑 두께에 대한 정보도 줄텐데… 확인해보면 있을거야. 그래서 길이만 이용해 무게를 예측하는게 아니라, 길이, 높이, 두께를 이용해서 무게를 예측하는 선형회귀를 구현해봐. 선형회귀는 그 특성이 많을수록 엄청난 효과를 내거든”



맞다. 내가 길이에 꽂혀서 다른 정보들이 있는걸 간과했다. 얼른 공급체에서 준 정보들을 확인해보니, 역시나 농어의 높이와 두께에 대한 데이터도 각각 존재했다.





이전 게시글을 본 사람은 알겠지만, 선형 회귀에서 이차함수를 이용할 때 이를 다항회귀라고 불렀다. 하지만 그때에도 이용한 특성은 생선의 길이 하나였다. 정확히는 길이의 제곱과 길이를 활용했지만 어쨋든.

 

이제 길이 뿐만 아니라 높이, 두께라는 특성도 학습 데이터로 이용하고자 한다. 즉 여러 개의 특성을 사용해서 회귀를 해보겠다는 말이다. 이를 다중 회귀라고 한다.



앞에서 본 다항회귀(다중회귀가 아니다. 주의)에서 그 식은 다음과 같았다.

이제 길이뿐만 아니라 높이, 두께도 추가되었으므로, 대충 이런식으로 식을 적으면 될거 같기도 하다.

 

무게 = a x 길이 + b x 높이 + c x 두께 + d

 

이렇게만 해도 충분할거 같긴 하다. 그런데 앞에서도 봤듯이 ‘길이’ 만으로 선형회귀 했을때보다 ‘길이’의 제곱을 이용했더니 훨씬 더 정확한 결과가 나왔음을 확인해봤다.

그래서 우리는 ‘길이’, ‘높이’, ‘두께’ 이 세 가지 특성을 각각 제곱한 값도 식에 추가해버리고 싶다. 그리고 좀 더 찾아보니, 각각을 제곱하는 것 뿐만 아니라 각 특성끼리의 곱도 새로운 특성으로 추가해보면 모델이 더 복잡해진다고 한다. 

 

즉 우리는 길이, 높이, 두께 이 3가지 특성에 더해 

 

‘길이^2’, 높이^2’, ‘두께^2’, ‘길이x높이’, ‘길이x두께’, ‘높이x두께’ 

 

이 특성까지도 추가하고 싶은거다. 이렇게 기존의 특성을 이용해 새 특성을 뽑아내는 것을 특성 공학이라고 부른다. 그냥 그런가보다 하고 넘겨도 된다.



자, 그러면 이를 코드로 나타내기 위해서는 일단 농어의 길이, 높이, 두께에 대한 데이터가 있어야 한다. 이전까지는 직접 그 리스트를 긁어왔었지만, 매번 그러는 것도 좀 귀찮긴 하다. 

그냥 인터넷에 있는 데이터를 긁어오면 안되나? 물론 이거는 생선가게 직원의 입장이 아니라 우리의 입장이긴 하다.\

 

아무튼, 이를 지원해주는 라이브러리가 당연히 있다.

 

  • 설마 또 넘파이???

 

아쉽게도 아니다. 넘파이는 이런거 지원 안한단다. 대신 판다스라는 애를 사용해보자.

 

판다스는 데이터 분석에 너무나도 중요한 라이브러리이다. 하지만 여기서는 그냥 인터넷 주소창의 정보를 가져오는 역할을 한다는 것 정도만 알아두자.

 

농어의 길이, 높이, 두께에 대한 정보는 다음 주소에 저장돼있다고 한다.

 

https://bit.ly/perch_csv_data



접속해보니 다음과 같더라.

 

 

이렇게 데이터가 쭉 이어져있다. 이를 전문용어로 csv 파일이라고 하는데, 그냥 그렇구나하고 넘기면 된다. 판다스는 이러한 csv 파일을 읽어온 후 넘파이의 배열(우리가 아는 그 리스트)로 바꿔주는 기능이 있다. 

 







코드로 구현해보면 다음과 같다.



import pandas as pd

df = pd.read_csv("https://bit.ly/perch_csv_data")

perch_full = df.to_numpy()

print(perch_full)



 

실행시켜보면 이렇게 데이터가 쭉 이어져있다. 보니깐 저 perch_full은 2차원 리스트이고, 행이 몇개인지는 모르겠지만 열은 3개로 보인다. 각각은 순서대로 길이, 높이, 두께일 것이다.

 

그리고 정답 데이터인 타깃 데이터는 이전과 그대로이다. 이거는 직접 긁어오도록 하겠다. 

 

import numpy as np

 

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








데이터 준비가 대충 끝난거 같다. 이제 늘 하던대로 train_test_split을 이용해 위 데이터들을 훈련 데이터와 테스트 데이터로 나눠보자.



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

)




이제 모든 준비가 끝난것 처럼 보이지만, 아직이다. 앞에서 우리는 각 특성의 제곱과 그 곱들까지도 새 특성으로 이용해보고자 했다. 모델을 더 복잡하게 만들기 위함이었다. 

 

당연히 앞의 다항회귀에서 그랬던것 처럼 일일히 만들어 줄 수도 있다. 하지만 역시 사이킷런은 이런것 조차 자동으로 만들어주는 클래스가 존재한다.



두 번째 글에서 두 데이터의 스케일이 안맞아서 표준점수를 이용해 데이터를 변환시켰던 기억이 나려나 모르겠다. 아무튼 이러한 절차를 우리는 ‘데이터 전처리’라고 불렀다. 모델한테 학습시키기 ‘전’에 ‘처리’를 좀 해보겠다 뭐 이런 의미이다. 

 

머신러닝 모델한테 학습시키기 전에 이렇게 특성을 더 만드는 것도 전처리의 일종이다. 

사이킷런에는 다양한 클래스가 존재하는데, 우리가 많이 사용했던 k어쩌고랑 선형회귀는 ‘모델 클래스’ 였다. 그리고 특성을 만들거나 전처리하기 위한 클래스는 ‘변환기 클래스’라고 부른다. 



"현재 계속 글을 추가하고 수정중입니다. 빠른 시일 안에 완성하겠습니다!!"


—-----------------------

 

  1. 왜 사이킷 런에 사용할 훈련 세트는 2차원 배열이어야 하는걸까? 1차원이면 안되나? 1차원을 넣어도 2차원로 자동으로 변형해주면 더 편할텐데 왜 이런 기능은 지원안해주고 우리가 직접 2차원 배열로 바꿔야하는걸까?

2. 결정계수는 왜 타깃과 평균의 값의 제곱을 분모로 하는걸까? 또한 왜 제곱을 씌워야 하는걸까? 제곱을 씌우지 않으면 어떤 문제가 발생할까?

 

3. 다항회귀 예시를 볼 때, 2차 함수가 1차 함수보다 더 데이터를 잘 나타낸다고 파악했다. 하지만 2차 함수보다 3차 함수가 더 데이터를 잘 나타낼 수도 있고, 지수함수가 더 잘 나타낼 수도 있는거 아닌가? 결국 과대적합의 문제로 귀결될거 같은데, 이는 어떻게 판단할까?

4. 다항회귀에 대한 설명을 시작할 때, 직선을 이용한 선형회귀의 훈련세트의 score가  0.93밖에 되지 않아 과소적합이라 했다. 개인적으로 0.93 정도도 높은 수치라고 생각했었는데, score 값이 과소적합인지 과대적합인지 판단하는 기준은 무엇일까?

 

5.  다중회귀에서 왜 특성끼리 곱해서 새로운 특성을 만드는걸까? 어차피 특성에 대한 학습이 이뤄질 것이므로, 그 특성끼리 곱한 값도 결국 모델이 이미 학습한 것의 일부분일거라 생각되는데, 그렇지 않은건가?