[혼공학습단 14기] 혼자 공부하는 머신러닝 + 딥러닝 2주차

Lucel·2025년 7월 13일
0
post-thumbnail

저번 포스팅으로 1주차 우수혼공족으로 선정되었습니당
혼공족장님 맛있는 할메가 커피 감사합니다 !!!

이어서 바로 2주차 공부에 들어가보겠습니다

1. k-최근접 이웃 회귀

지도 학습 알고리즘은 크게 분류와 회귀로 나뉜다
회귀라는 것은 임의의 어떤 숫자를 예측하는 것을 회귀라고 부른다

예를 들면 내년도 경제 성장률 예측이나 배달이 도착할 시간을 예측하는 것이 회귀다

두 변수 사이의 상관관계를 분석하는 방법을 회귀라고 하였다

우리가 전에 k-최근접 이웃 분류 알고리즘을 공부했었는데
이번에는 이와 유사한 k-최근접 이웃 회귀를 사용해볼 것이다
원리는 기존의 k-최근접 이웃 분류와 거의 유사하다

원리는 똑같이 샘플에 가장 가까운 샘플 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])

다음과 같이 훈련 데이터를 준비했다 농어의 길이가 특성이고 무게 타깃이다
과거에는 파이썬 리스트에서 넘파이 배열로 변환했지만 이번에는 넘파이 배열에서 만들어보자

확실히 좀 더 이해하기 위해서 산점도로 표현을 해보자

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)

사이킷런의 train_test_split() 함수를 사용해서 훈련 데이터와 테스트 데이터로 나눴다

하지만 사이킷런에 사용할 수 있는 훈련 데이터는 2차원 배열이어야 한다
perch_length가 1차원 배열이기 때문에
이를 나눈 train_input과 test_input도 1차원 배열이다

이런 1차원 배열을 1개의 열이 있는 2차원 배열로 바꿔야 한다

파이썬 1차원 배열의 크기는 원소가 1개인 튜플로 나타난다
예를 들어 [1,2,3]의 크기는 (3,)이다

이를 2차원 배열로 만들기 위해 억지로 하나의 열을 추가하면 (3,1)이 된다
배열을 나타내는 방식만 달라졌을 뿐 배열에 있는 원소의 개수는 동일하게 3개이다

넘파이에서는 쉽게 배열의 크기를 바꿀 수 있게 reshape() 메서드를 제공한다
예를 좀 더 들오보자 (4,) 배열을 (2,2) 크기로 바꿔보자

test_array = np.array([1,2,3,4])
print(test_array.shape) # (4,)

test_array = test_array.reshape(2,2)
print(test_array.shape) # (2,2)

이렇게 reshape() 메서드는 바꾸려는 배열의 크기를 지정할 수 있다
이제 다시 돌아와서 메서드를 통해 train_input과 test_input을 2차원 배열로 바꾸자

train_input의 크기는 (42,)이다 이를 2차원 배열인 (42,1)로 바꾸려면
train_input.reshape(42,1)와 같이 코드를 입력한다

넘파이는 배열의 크기를 자동으로 지정하는 기능도 제공하는데
크기에 -1을 지정하면 나머지 원소 개수로 모두 채우라는 의미이다

예를 들어 첫 번째 크기를 나머지 원소로 채우고, 두 번째 크기를 1로 하려면
train_input.reshape(-1,1) 처럼 사용한다
-> 열은 1개로 만들고 행은 원소 개수에 맞게 자동으로 정해줘

train_input = train_input.reshape(-1,1)
test_input = test_input.reshape(-1,1)
print(train_input.shape, test_input.shape) # (42, 1) (14, 1)

이렇게 reshape(-1,1)과 같이 사용하면
배열의 전체 원소 개수를 매번 외우지 않아도 되므로 편리하다
그러니 잘 알고 있도록 하자

2) 결정계수

사이킷런에서 k-최근접 이웃 회귀 알고리즘을 구현한 클래스는 KNeighborsRegressor 이다
이 클래스의 사용법은 KNeighborsClassifier와 매우 비슷하다

from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor()

# k-최근접 이웃 회귀 모델 훈련
knr.fit(train_input, train_target)

print(knr.score(test_input, test_target)) # 0.992809406101064

매우 좋은 점수가 나왔다
그런데 이 점수는 뭘까?

분류의 경우에는 테스트 데이터에 있는 샘플을 정확하게 분류한 개수의 비율이다
그래서 정확도라고 불렸다
간단히 말하면 정답을 맞힌 개수의 비율이다

회귀에서는 정확한 숫자를 맞힌다는 것은 거의 불가능하다
왜냐하면 예측하는 값이나 타깃 모두 임의의 수치이기 때문이다

그래서 회귀의 경우 조금 다른 값으로 평가하는데 이 점수를 결정계수라고 한다
또는 간단히 말해서 R^2이라고 부른다

결정계수 = 1 - ((타깃-예측)^2의 합 / (타깃-평균)^2의 합)

각 샘플의 타켓과 예측한 값의 차이를 제곱해서 더한다
그 다음 타킷과 타깃 평균의 차이를 제곱하여 더한 값으로 나눈다

만약에 타깃의 평균 정도를 예측하는 수준이라면
(즉 분자와 분모가 비슷해져) R의 제곱은 0에 가까워지고,
예측이 타깃에 아주 가까워지면 (분자가 0에 가까워지기 때문에)
1에 가까운 값이 된다

결론을 내면 사이킷런의 score() 메서드가 출력하는 값은 높으면 높을수록 좋다

0.99면 매우 좋은 값인데 정확도처럼 결정계수가 얼마나 좋은지 이해하기가 어렵다
그래서 타깃값과 예측값 사이의 차이를 구해보자

사이킷런은 sklearn.metrics 패키지 아래 여러가지 측정도구를 제공한다
그 중에서 mean_abosolute_error는 타깃과 예측의 절댓값 오차를 평균하여 반환한다

from sklearn.metrics import mean_absolute_error

# 테스트 세트에 대한 예측
test_prediction = knr.predict(test_input)

# 테스트 세트에 대한 평균 절댓값 오차를 계산
mae = mean_absolute_error(test_target, test_prediction)
print(mae) # 19.157142857142862

결과를 보면 예측이 평균적으로 19g 정도 타깃값과 다르다는 것을 알 수 있다
이렇게 우리는 보통적으로 훈련 데이터로 학습을 시키고, 테스트 데이터 평가를 했다
그런데 훈련 데이터를 사용해서 평가를 해보면 어떨까?

여기서도 우리가 배울 수 있는 인사이트가 있다
바로 해보자!!

3) 과대적합 vs 과소적합

print(knr.score(train_input,train_target)) # 0.9698823289099254

앞에서 테스트 데이터를 사용했을 때와 비교해보면 이상한 점이 있다
보통 모델을 훈련 데이터에 훈련시키면 훈련 데이터에 잘 맞는 모델이 만들어진다
그렇다면 이 모델을 훈련 데이터와 테스트 데이터에서 평가하면 두 값 중에 누가 더 높을까>

보통은 훈련 데이터의 점수가 조금 더 높게 나온다
왜냐하면 훈련 데이터에서 모델을 훈련시켰으므로 훈련 데이터에서 더 잘 나온다

만약 훈련 데이터에서 점수가 매우 좋았는데 테스트 데이터로 점수가 나쁘면
우리는 모델이 훈련 데이터에 과대적합(overfitting) 되었다고 말한다

즉, 훈련 데이터에만 잘 맞는 모델이라 테스트 데이터나 나중에 실전에 투입하여
새로운 샘플에 대한 예측을 만들 때 잘 동작하지 않을 것이다

반대로 훈련 데이터보다 테스트 데이터의 점수가 높거나 두 점수가 너무 낮은 경우는
모델이 훈련 데이터에 과소적합(underfitting) 되었다고 말한다

즉, 모델이 너무 단순하여 훈련 데이터에 적절히 훈련되지 않은 경우이다
훈련 데이터가 전체 데이터를 대표한다고 가정하기 때문에
훈련 데이터를 잘 학습시키는 것이 중요하다

왜 과소적합이 일어날까??
이런 현상의 또 다른 원인으로는 훈련 데이터와 테스트 데이터의 크기가 매우 작기 때문이다
데이터가 작으면 테스트 데이터가 훈련 데이터의 특징을 따르지 못할 수 있다

그렇다면 앞에서 학습시킨 K-최근접 이웃 회귀 모델을 생각해보자
훈련 데이터보다 테스트 데이터가 점수가 더 높으니 이는 과소적합이다

그렇다면 우리는 이 문제를 어떻게 해결할 수 있을까?

바로 모델을 조금 더 복잡하게 만들면 된다
즉, 훈련 데이터에 더 잘 맞게 만들면 테스트 데이터의 점수는 조금 더 낮아질 것이다

k-최근접 이웃 알고리즘으로 모델을 더 복잡하게 만드는 방법은 이웃의 개수 k를 줄이는 것이다
이웃의 개수를 줄이면 훈련 데이터에 있는 국지적인 패턴에 민감해지고
이웃의 개수를 늘리면 데이터 전반에 있는 일반적인 패턴을 따를 것이다

그렇기에 우리는 기본값인 5에서 3으로 바꿔볼 것이다

knr.n_neighbors =3  # 이웃의 개수를 3으로 설정

# 모델을 다시 훈련
knr.fit(train_input, train_target)
print(knr.score(train_input, train_target)) # 0.9804899950518966
print(knr.score(test_input,test_target)) # 0.9746459963987609

k값 줄였더니 훈련 데이터의 결정계수값이 올라갔다
반대로 테스트 데이터의 점수는 훈련 데이터보다 낮아졌으므로 과소적합 문제는 해결했다
또한 두 점수 차이가 크지 않으므로 과대적합도 되지 않은 것 같다

우리는 이 모델이 테스트 데이터와 앞으로 추가될 데이터에도 일반화를 잘하리라 예상할 수 있다

4) 정리

회귀는 임의의 수치를 예측하는 문제이다
우리는 이번 시간에 k-최근접 이웃 회귀 모델을 만들었다

k-최근접 이웃 회귀 모델은 분류와 비슷하게 가장 먼저 가까운 k개의 이웃을 찾고
그리고 이웃들의 타깃값을 평균하여 예측했다

그리고 사이킷런에서 회귀 모델의 점수로 R^2,
즉 결정계수를 반환하는데 이 값은 1에 가까울수록 더 좋다

만약 정량적인 평가를 해보고 싶다면 대표적으로 절댓값 오차가 있다

모델을 훈련하고 나서 훈련 데이터와 테스트 데이터에 대해 평가점수를 구할 수 있다
이 때 테스트 데이터와 훈련 데이터의 점수 차이가 크면 좋지 않다
일반적으로는 훈련 데이터가 테스트 데이터보다 조금 더 높다

하지만 만약 테스트 데이터가 너무 낮으면 우리는 그걸 보고 과대적합 이라고 하며
모델이 훈련 데이터에 과도하게 맞춰졌다고 한다

반대로 테스트 데이터 점수가 너무 높거나 두 점수가 너무 낮으면 과소적합이라고 한다

과대적합일 경우 모델을 덜 복잡하게 만들어야 해서
k-최근접 이웃 회귀 모델일 경우에는 k값을 늘리고

과소적합일 경우 모델을 더 복잡하게 만들어야 해서
k-최근접 이웃 회귀 모델일 경우에는 k값을 줄인다

2. 선형회귀

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

from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor(n_neighbors=3)

# k-최근접 이웃 회귀 모델 훈련
knr.fit(train_input, train_target)

일단 위에서 했던 것처럼 최근접 이웃 개수를 3으로 잡고 모델을 학습시켰다
그리고 이 모델을 기반으로 길이가 50cm인 농어의 무게를 예측했다

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

모델은 50cm의 농어의 무게를 1,033g으로 예측을 했다
그런데 실제 이 농어의 무게는 훨씬 많이 나간다고 한다
그렇다면 어디서 문제가 생긴걸까??

일단 산점도로 훈련 데이터와 50cm이 농어, 최근접 이웃을 그려보자

import matplotlib.pyplot as plt

# 50cm 농어의 이웃 
distance, 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 농어에서 가장 가까운 것은 45cm 근방이고
k-최근접 이웃 알고리즘은 이 샘플들의 무게를 평균한다

따라서 새로운 샘플이 훈련 데이터의 범위를 넘어가면 엉뚱한 값을 예측할 수 있다
예를 들면 길이 100cm인 농어도 아마 1,033g로 예측할 것이다

이를 시각화해서 표현해보면 다음과 같다

k-최근접 이웃을 사용해서 이 문제를 해결하려면
가장 큰 농어가 포함되도록 다시 훈련 데이터를 만들어야 한다

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 농어의 무게를 아주 높게 예측 했다

LinearRegression 클래스는 y=ax+b 하나의 직선을 그리게 되는데
여기에서 x를 농어의 길이, y를 농어의 무게로 들어간다
그리고 이 데이터에 가장 잘 맞는 a와 b를 찾게 되는데

LinearRegression 클래스가 찾은 a와 b는
lr객체의 coef_intercept_ 속성에 저장되어 있다

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

coef_intercept_를 머신러닝이 찾은 값이라는 의미로 모델 파라미터라고 부른다
많은 머신러닝의 훈련과정은 최적의 모델 파라미터를 찾는 것과 같다
그래서 우리는 이를 모델 기반 학습이라고 부른다

앞서 사용한 k-최근접 이웃에는 모델 파라미터가 없었다
훈련 세트를 저장하는 것이 훈련의 전부였기 때문이다
우리는 이러한 것을 사례 기반 학습이라고 부른다

그러면 농어의 길이 15cm에서 50cm까지 직선으로 그려보자
이 직선을 그리려면 앞에서 구한 기울기와 절편을 사용해서 두 점을 이으면 된다
(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

바로 이 직선이 선형 회귀 알고리즘이 이 데이터셋에서 찾은 최적의 직선이다

print(lr.score(train_input,train_target)) # 훈련 데이터 0.939846333997604
print(lr.score(test_input,test_target)) # 테스트 데이터 0.824750312331355

각각 훈련 데이터와 테스트 데이터에 대한 결정계수를 구해보자
훈련 데이터와 테스트 데이터의 점수가 조금 나고 있다

이 모델이 훈련 데이터에 과대적합되었다고 말할 수 있을까?

그런데 훈련 데이터의 점수도 그렇게 높지는 않다
오히려 전체적으로 과소적합 되었다고 볼 수도 있다

2) 다항 회귀

그런데 방금 그래프에서 이상한 점이 있다
바로 선형 회귀가 만든 직선이 왼쪽 아래로 쭉 뻗어 있다는 것이다
이 직선대로 예측하면 농어의 무게 0g 이하로 내려갈 텐데 현실에는 있을 수 없는 일이다

농어의 길이와 무게에 대한 산점도를 보면 일직선이라고 보기보다는
왼쪽 위로 조금 구부러진 곡선에 가깝다 그렇다면 최적의 직선보다는 곡선을 찾아보면 어떨까?

그래서 이러한 2차 방정식을 그리려면 길이를 제곱한 항이 훈련 데이터에 추가되어야 한다
이는 넘파이를 이용하면 쉽게 만들 수 있다
column_stack() 함수를 사용하면 간단하다

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) # (42, 2) (14, 2)

train_input을 제곱한 것과 train_input 두 배열을 나란히 붙이면 된다
test_input도 마찬가지이다

train_input ** 2 식에도 넘파이 브로드캐스팅이 적용된다
즉, train_input에 있는 모든 원소를 제곱한다

원래 특성인 길이를 제곱하여 왼쪽 열에 추가했기 때문에
훈련 데이터와 테스트 데이터 모두 열이 2개로 늘어놨다

좀 더 쉽게 설명을 해보면 원래는 ax+b 인데 지금은 ax^2+bx+c가 되었다
그러니 기존에 데이터에서 x^2이 필요해진 것이다
그래서 원시데이터가 다음과 같다면

# 원시데이터 : train_input
train_input = [[2],
               [4],
               [6]]

# 제곱한 값 : train_input ** 2
train_input ** 2 = [[4],
                    [16],
                    [36]]

# 합침 : np.column_stack
train_poly = [[4, 2],
              [16, 4],
              [36, 6]]

이제 train_poly를 이용해서 선형회귀 모델을 다시 학습시켜보자
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

이런 방정식을 다항식(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.xlabel('length')
plt.ylabel('weight')
plt.show()

앞선 단순 선형 회귀 모델보다 훨씬 나은 그래프가 되었다
무게가 음수로 나오지도 않고 훈련 데이터의 경향을 잘 따르고 있다

print(lr.score(train_poly,train_target)) # 0.9706807451768623
print(lr.score(test_poly, test_target)) # 0.9775935108325122

하지만 여전히 점수를 보면 테스트 데이터가 높아서 이번에는 과소적합이 있는 것 같다

3. 특성 공학과 규제

1) 다중회귀

우리가 이전에는 하나의 특성을 사용하여 선형 회귀 모델을 훈련했다
하지만 이제는 여러 개의 특성을 사용하는 선형 회귀를 사용할 것인데
우리는 이를 다중회귀(multiple regression)라고 부른다

생각을 해보자!!!
1개의 특성을 사용했을 때 선형 회귀 모델이 학습하는 것은 직선이였다
그렇다면 2개의 특성을 사용하면 뭘까?
특성이 2개면 선형 회귀는 평면을 학습한다

특성이 2개면 타깃값과 함께 3차원 공간을 형성하고 선형 회귀 방정식
선형 회귀 방정식 타깃 = a x 특성1 + b x 특성2 + 절편은 평면이 된다

특성이 3개인 경우에는 우리는 3차원 공간 이상을 그리거나 상상할 수 없다
하지만 분명한 것은 선형 회귀를 단순한 직선이나 평면으로 생각하여
성능이 무조건 낮다고 오해해서는 안된다

특성이 많은 고차원에서는 선형 회귀가 매우 복잡한 모델을 표현할 수도 있다

또한 더 나아가 특성들을 제곱하여 추가하거나
각 특성들을 서로 곱해서 또 다른 특성을 만들수도 있다
이렇게 기존의 특성을 사용해 새로운 특성을 뽑아내는 작업을
특성공학(feature engineering)이라고 한다

우리가 직접 특성을 제곱하고 특성끼리 곱해서 새로운 특성을 추가할 수도 있지만
사이킷런에서는 여러 편리한 도구를 제공한다

2) 데이터 준비

데이터가 많이 늘어났기 때문에 데이터를 복사해 붙여넣는 것도 번거롭다
그렇다면 인터넷에서 바로 다운로드하여 사용할 수는 없을까?
넘파이는 이런 작업을 지원하지 않지만 판다스를 사용하면 손쉽게 할 수 있다

판다스(pandas)는 유명한 데이터 분석 라이브러리로
데이터프레임(dataframe)은 판다스의 핵심 구조이다
넘파이와 비슷하게 다차원 배열을 다룰 수 있지만 훨씬 더 많은 기능을 제공한다
또 데이터프레임은 넘파이 배열로 쉽게 바꿀 수도 있다

그래서 우리는 판다스를 통해서 데이터를 인터넷에서 내려받아 데이터프레임에 저장해볼 것이다
판다스 데이터 프레임을 만들기 위해 많이 사용하는 파일은 CSV 파일이다
우리는 이 파일을 판다스의 read_csv() 함수를 통해 데이터 프레임을 만든 다음
to_numpy() 를 통해 넘파이 배열로 바꿀 것이다

import pandas as pd # pd는 관례적으로 사용하는 판다스의 별칭!!
df = pd.read_csv("https://bit.ly/perch_csv_data")
perch_full = df.to_numpy()
print(perch_full)

이번 시간에는 농어의 길이, 높이, 넓이가 주어졌다

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

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)

그리고 이에 따른 농어의 무게 데이터를 준비했고
준비된 perch_full와 perch_weight를 훈련 데이터와 테스트 데이터로 나눠줬다

3) 사이킷런의 변환기

사이킷런은 특성을 만들거나 전처리하기 위한 다양한 클래스를 제공한다
사이컷런에서 이런 클래스를 변환기(transformer)라고 부른다
사이킷런의 모델 클래스에 일관된 fit(), score(), predict()가 있던것처럼
변환기 클래스는 모두 fit()transform()을 제공한다

우리가 먼저 살펴볼 변환기는 PolynomialFeatures 클래스이다
이 친구는 sklearn.preprocessing 패키지에 포함되어 있다

from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures()
poly.fit([[2,3]])
print(poly.transform([[2,3]])) # [[1. 2. 3. 4. 6. 9.]]

들어가기 전에 먼저 2개의 특성 2와 3으로 이루어진 샘플 하나를 가지고 실험해봤다
fit() 메서드는 새롭게 만들 특성 조합을 찾고
transform() 메서드는 실제로 데이터를 변환한다

변환기는 입력 데이터를 변환하는데 타깃 데이터가 필요하지 않다
따라서 모델 클래스와 다르게 fit() 메서드에 입력 데이터만 전달했고
여기에서는 2개의 특성(원소)을 가진 샘플이 6개의 특성을 가진
샘플 [1. 2. 3. 4. 6. 9.]로 바뀌었다

PolynomialFeatures 클래스는 기본적으로
각 특성을 제곱한 항을 추가하고 특성끼리 서로 곱한 항을 추가한다
그래서 2와 3을 제곱한 4,9가 추가되었고 서로를 곱한 6이 추가되었다
그렇다면 1은 왜 추가되었을까?

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

사실 선형 방정식의 절편을 항상 값이 1인 특성과 곱해지는 계수라고 볼 수 있다
이렇게 보면 특성은 (길이, 높이, 두께, 1)이 된다
하지만 사이킷런의 선형 모델은 자동으로 절편을 추가하므로 이렇게 특성을 만들 필요는 없다

poly = PolynomialFeatures(include_bias = False) # 절편을 위한 항 제거 
poly.fit([[2,3]])
print(poly.transform([[2,3]])) # [[2. 3. 4. 6. 9.]]

이제 충분히 실험을 해봤으니 train_input에 적용을 해보자

poly = PolynomialFeatures(include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
print(train_poly.shape) # (42, 9)

PolynomialFeature 클래스는 9개의 특성이 어떻게 만들어졌는지 확인하는 좋은 방법을 제공한다
get_feature_names_out() 메서드를 호출하면 9개의 특성이
각각 어떤 입력의 조합으로 만들어졌는지 알려준다

poly.get_feature_names_out() 
# array(['x0', 'x1', 'x2', 'x0^2', 'x0 x1', 'x0 x2', 'x1^2', 'x1 x2','x2^2'], dtype=object)

'x0'은 첫 번째 특성을 의미하고, 'x0^2'는 첫 번째 특성의 제곱,
'x0 x1'은 첫 번째 특성과 두 번째 특성의 곱을 나타내는 식이다

test_poly = poly.transform(test_input)

테스트 데이터도 변환을 해준다

4) 다중 회귀 모델 훈련하기

다중 모델 회귀 훈련을 하는 것은 선형 회귀 모델을 훈련하는 것과 같다
다만 여러 개의 특성을 사용하여 선형 회귀를 수행하는 것뿐

from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target)) # 0.9903183436982125
print(lr.score(test_poly, test_target)) # 0.971455991159411

그래서 사이킷런의 LinearRegressions 클래스를 임포트하고
앞에서 만든 train_poly를 사용해 모델을 훈련시켰다
그 결과 성능이 매우 우수한 모델을 만들 수 있었다

우리는 이를 통해 특성이 늘어나면 선형 회귀의 능력이 매우 강해질 수 있다는 사실을 알 수 있다

그러면 여기서 특성을 더 많이 추가하면 어떨까? 예를 들어 3제곱, 4제곱 항을 넣는 것이다

PolynomialFeatures 클래스의 degree 매개변수를 사용하면
필요한 고차항의 최대 차수를 지정할 수 있다

poly = PolynomialFeatures(degree=5, include_bias = False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)
print(train_poly.shape) # (42, 55)

lr.fit(train_poly,train_target)
print(lr.score(train_poly, train_target)) # 0.9999999999996433
print(lr.score(test_poly,test_target)) # -144.40579436844948

결과를 보면 훈련 데이터에는 매우 높은 점수를 보여주고 있지만
테스트 데이터에서는 음수를 보여주고 있다

특성의 개수를 늘리면 선형 모델을 성능이 좋아진다
훈련 데이터에 대해 거의 완벽하게 학습을 할 수 있다
하지만 이런 모델은 훈련 데이터에 너무 과대적합되므로 테스트 데이터에서는 형편 없어진다

5) 규제

규제(regularization)는 머신러닝 모델이 훈련 데이터를 너무 과도하게 학습하지 못하게
설정하는 것을 의미한다 즉, 모델이 훈련 데이터에 과대적합 되지 않도록 만드는 것이다

선형 회귀 모델에서는 각 특성에 곱해지는 계수나 기울기가 있는데
이 숫자들이 너무 크면 모델이 복잡해서 과적합을 일으킨다
그래서 우리는 보통 계수를 작게 만드는 규제라는 기법을 사용한다

여기서 알아야하는 점은
각 특성의 값들이 제각각 크기가 다르면, 계수도 균형 있게 작아지지 않는다

예를 어떤 특성은 0.01부터 1까지 작게 움직이고,
어떤 특성은 1000부터 5000까지 크게 움직인다면
두 특성에 곱해지는 계수 크기는 당연히 다르게 나올 수 밖에 없다

그래서 선형 회귀 모델에서 규제를 하기 전에는 정규화를 꼭 먼저 해줘야 한다
이번에는 사이킷런에서 제공하는 StandardScaler 클래스를 사용해볼 것이다

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)

먼저 StandardScaler 클래스의 객체 ss를 초기화 후
PolynomialFeatures 클래스로 만든 train_poly를 사용해 이 객체를 훈련시킨다
여기서 꼭 기억해야 하는 것은 꼭 훈련 데이터 학습한 변환기를
사용해서 테스트 데이터까지 변환해야 한다

이렇게 선형 회귀 모델에 규제를 추가한 모델 릿지(ridge)라쏘(lasso)라고 부른다
두 모델은 규제를 가하는 방법이 다르다

릿지는 계수를 제곱한 기준으로 규제를 적용하고,
라쏘는 계수의 절댓값을 기준으로 규제를 적용합니다

두 규제 방법 모두 계수의 크기를 줄이지만 라쏘는 아예 0으로 만들수도 있다
그래서 일반적으로 릿지를 조금 더 사람들이 선호한다
물론 사이킷런은 두 알고리즘을 모두 제공한다

6) 릿지 회귀

릿지와 라쏘 모두 sklearn.linear_model 패키지 안에 있다
그렇기에 모델 객체를 만들고 fit() 메서드에서 훈련한 다음 score()로 평가한다

from sklearn.linear_model import Ridge
ridge = Ridge()
ridge.fit(train_scaled, train_target)

print(ridge.score(train_scaled,train_target)) # 0.9896101671037343
print(ridge.score(test_scaled, test_target)) # 0.9790693977615387

릿지로 훈련 데이터와 테스트 데이터를 가지고 모두 평가를 해봤을 때
좋은 결과가 나오는 것을 확인할 수 있다

이러한 릿지와 라쏘 모델을 사용할 때 규제의 양을 임의로 조절할 수 있다
모델 객체를 만들 때 alpha 라는 매개변수로 규제의 강도를 조절할 수 있는데
alpha 값이 크면 규제강도가 강해져 계수값을 더 줄이고 조금 더 과소적합되도록 유도한다

반대로 alpha 값이 작으면 계수를 줄이는 역할이 줄어들고
선형회귀 모델과 유사해지므로 과대적합될 가능성이 커진다

alpha 값은 릿지 모델이 학습하는 값이 아니라 사전에 우리가 지정해줘야 하는 값이다
이렇게 머신러닝 모델이 학습할 수 없고 사람이 알려줘야 하는 파라미터를 하이퍼파라미터라고 부른다

사이킷런과 같은 머신러닝 라이브러리에서 하이퍼파라미터는 클래스와 메서드의 매개변수로 표현된다

그러면 적절한 alpha 값을 찾는 방법은 뭘까?
alpha에 대한 R^2 값의 그래프를 그려보는 것이다
훈련 데이터와 테스트 데이터의 점수가 가장 가까운 지점이 최적의 alpha가 된다

이를 하기 위해서는 먼저 맷플롯립을 임포트하고,
alpha 값을 바꿀 때마다 score() 메서드의 결과를 저장할 리스트를 만들어보자

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

alpha 값을 0.001에서 100까지 10배씩 늘려가며 릿지 회귀 모델을 훈련한 다음
훈련 데이터와 테스트 데이터의 점수를 리스트에 저장했다
이제 그래프를 본격적으로 그려보자

그런데 값을 0.001부터 10배씩 늘렸기 때문에 그대로 그래프를 그리게 되면
그래프의 왼쪽이 너무 촘촘해진다 그렇기에 alpha_list에 있는 6개의 값을
동일한 간격으로 나타내기 위해 로그 함수로 바꾸어 지수로 표현했다

즉 0.001은 -3, 0.01은 -2가 되는 식이다

plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show()

위는 훈련 데이터 그래프, 아래는 테스트 데이터 그래프다

이 왼쪽을 보면 훈련 데이터와 테스트 데이터의 점수 차이가 매우 크다
훈련 데이터에는 잘맞고 테스트 데이터에는 형편없는 전형적인 과대적합의 경우이다
반대로 오른쪽은 훈련 데이터와 테스트 데이터 모두 떨어지는 과소적합의 경우를 보여주고 있다

적절한 alpha의 값은
두 그래프가 가장 가깝고 테스트 데이터 점수가 가장 높은 -1 즉, 0.1이다
그렇기에 우리는 최종적으로 alpha에 0.1을 두고 훈련을 진행한다

ridge = Ridge(alpha=0.1)
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target)) # 0.9903815817570367
print(ridge.score(test_scaled, test_target)) # 0.9827976465386928

7) 라쏘 회귀

라쏘 모델을 훈련하는 것은 릿지와 매우 비슷하다
Ridge 클래스를 Lasso 클래스로 바꾸기만 하면 된다

from sklearn.linear_model import Lasso
lasso = Lasso()
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled,train_target)) # 0.989789897208096
print(lasso.score(test_scaled,test_target)) # 0.9800593698421883

라쏘도 과대적합을 잘 억제한 결과를 보여준다
라쏘 역시 alpha 매개변수로로 규제의 강도를 정할 수도 있다

train_score = []
test_score = []
alpha_list = [0.001, 0.01, 0.1,1,10,100]
for alpha in alpha_list:
  # 라쏘 모델
  lasso = Lasso(alpha=alpha, max_iter=10000)
  # 라쏘 모델 훈련
  lasso.fit(train_scaled, train_target)
  # 훈련 점수와 테스트 점수 저장
  train_score.append(lasso.score(train_scaled,train_target))
  test_score.append(lasso.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^2')
plt.show()

이 그래프도 왼쪽은 과대적합을 보여주고 있고 오른쪽 과소적합을 보여주고 있다
라쏘 모델에서 최적의 값은 1, 즉 10이다 이 값으로 다시 모델을 훈련시키자

lasso = Lasso(alpha=10)
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target)) # 0.9888067471131867
print(lasso.score(test_scaled, test_target)) # 0.9824470598706695

앞에서 라쏘 모델은 계수 값을 아예 0으로 만들 수 있다고 했다
라쏘 모델의 계수는 coef_ 속성에 저장되어 있는데 한 번 이 중에 0인 것을 헤아려보자

print(np.sum(lasso.coef_ == 0)) # 40

np.sum() 함수는 배열을 모두 더한 값을 반환한다 넘파이 배열에 비교 연산자를 사용했을 때
각 원소는 True 아니면 False가 된다 np.sum() 함수는 True를 1로, False를 0으로
인식하여 덧셈을 할 수 있기 때문에 마치 비교 연산자에 맞는 원소 개수를 헤아리는 효과를 낸다

55개의 특성을 모델에 주입했지만 라쏘 모델이 사용한 것은 15개에 불과하다
이런 특징 때문에 라쏘 모델을 유용한 특성을 골라내는 용도로 사용할 수 있다

8) 정리

다중 회귀는 여러 개의 특성을 사용하는 회귀 모델이다
특성이 많으면 선형 모델은 강력한 성능을 발휘할 수 있다

특성 공학은 주어진 특성을 조합하여 새로운 특성을 만드는 일련의 작업 과정이다

릿지는 규제가 있는 선형회귀모델 중 하나이며 선형 모델의 계수를 작게 만들어
관대적합을 완화시킨다 릿지는 비교적 효과가 좋아 널리 사용되는 기법이다

라쏘는 또 다른 규제가 있는 선형 회귀 모델이다
릿지와 달리 계수 값을 아예 0으로 만들 수 있다

하이퍼 파라미터는 머신러닝 알고리즘이 학습하지 않는 파라미터다
이런 파라미터는 사람이 사전에 지정을 해야한다
대표적으로 릿지와 라쏘의 규제강도 alpha 파라미터이다

4. 숙제

그래요 숙제를 해봅시다

1) 기본숙제 (필수)

과대적합과 과소적합에 대한 이해를 돕기 위한 복잡한 모델과 단순한 모델을 만들고 있다
앞에서 만든 k-최근접 이웃 회귀 모델의 k값을 1, 5, 10으로 바꿔가며 훈련하고 있다

# k-최근접 이웃 회귀 객체 생성
knr = KNeighborsRegressor()

# 5에서 45까지 x 좌표 만들기
x = np.arange(5,45).reshape(-1,1)

# n =1, 5, 10일 때 예측 결과를 그래프로
for n in [1, 5, 10]:
  # 모델 훈련 
  knr.n_neighbors = n
  knr.fit(train_input, train_target)
  
  # 지정한 범위 x에 대한 예측
  prediction = knr.predict(x)
  
  # 훈련 데이터와 예측 결과를 그래프로
  plt.scatter(train_input,train_target)
  plt.plot(x, prediction)
  plt.title('n_neighbors={}'.format(n))
  plt.xlabel('length')
  plt.ylabel('weight')

파란색 선은 n=1이고 주황색, 초록색 선이 각각 n=5, n=10 이다

n=1은 거의 데이터의 분포를 따라가기 때문에 과적합이 현재 의심되고 있고
주황색 선은 무난무난하지만 초록색 선이 가장 부드럽고
노이즈에 덜 영향을 받고 있어서 더 좋은 것 같아보인다

2) 추가 숙제 (선택)

모델 파라미터를 설명하기

모델 파라미터는 우리가 데이터를 잘 표현하기 위한 그래프를 그리는데
그 그래프에 계수나 절편을 의미한다

다시 정리를 해보면 모델 파라미터란 데이터를 가장 잘 설명할 수 있도록
모델이 학습 과정에서 조정하는 값들을 말한다

profile
열쩡열정열정!! 🔥🔥🔥

0개의 댓글