데이터분석(AI학습 25)

이유진·2024년 7월 8일

--07.회귀(regession).ipynb--

회귀 알고리즘

KNN regession 사용

지도학습은 target y 의 형태에 따라

회귀(regression) 와 분류 (classification) 으로 나뉜다.

'분류' 는 몇개의 클래스 중 하나로 분류 하는 문제

'회귀' 는 임의의 어떤 숫자를 예측 하는 문제

- 내년 경제 성장률 예측, 주가 예측

- 주문후 배달 도착시간 예측

- 회귀 문제의 출력은 정해진 클래스가 아니라 '임의의 수치'

K-최근접 이웃 회귀

K-nearest neighbor regression

"""
'k-최근접 이웃 회귀' 도 비슷하다.
'분류' 와 똑같이 예측하려는 샘플에 가장 가까운 샘플 k개를 선택합니다.
하지만 회귀이기 때문에 이웃한 샘플의 타깃은 어떤 클래스가 아니라 '임의의 수치' 가 된다.
이웃 샘플의 수치를 사용해 새로운 샘플 X 의 타킷을 예측하는 방법은 ?
=> 바로 이 수치들의 평균값을 구하면 된다.

아래 그림에서 이웃한 샘플의 타킷값이 각각 100, 80, 60 이고, 이를 평균하면 X의 예측 타킷값은 80이 된다.

결국 k-최근접 이웃 분류 알고리즘과 비숫하되, 타깃값을 결정할 때만 조금 다르다.
"""
None

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

base_path = r'/content/drive/MyDrive/dataset'

file_path = os.path.join(base_path, 'fish.csv')

fish_df = pd.read_csv(file_path)
fish_df

농어의 길이(feature)가 주어졌을 때 무게(target)를 예측해보자

농어 (Perch의 데이터)

길이 (feature)

perch_length = fish_df[fish_df.Species == 'Perch']['Length'].values
print(perch_length)

무게 (target)

perch_weight = fish_df[fish_df.Species == 'Perch']['Weight'].values
print(perch_weight)

plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

train / test 로 나누기

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.shape, test_input.shape

학습할 데이터 세트는 feature들의 벡터 형태 이어야 한다!!!!(2차원)

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)

학습 모델

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor()
knr.fit(train_input, train_target)

knr.score(test_input, test_target)

"""
매우 높은 점수가 나왔다? 이 점수는 무엇일까?
'분류' 문제의 경우 test 세트의 샘플을 정확하게 분류한 개수의 비율이다. (accuracy: 정확도)
'회귀' 문제는 정확한 숫자를 맞힌다는것이 거의 불가능하다. 예측하는 값이나 타킷 모두 임의의 수치이기 때문이다.

'회귀' 의 경우 평가방식이 조금 다르다
이 점수를 '결정계수' (coeffiencit of determination) 이라고 부름.
간단히 R² 라고도 부름

이 값은 다음과 같은 식으로 계산됨

      (target - 예측)² 의 합

R² = 1 - ──────────────────
(target - 평균)² 의 합

"""
None

sklearn.metrics 에는 다양한 평가 측정 도구들이 있다.

mean_absolute_error 는 target과 예측값의 절대값 오차를 평균하여 리턴함

from sklearn.metrics import mean_absolute_error

테스트 세트에 대한 예측값

test_prediction = knr.predict(test_input)

test_prediction

test_target

예측이 평균적으로 19g 정도 target값과 다르다는 것을 알 수 있다.

mae = mean_absolute_error(test_target, test_prediction)
print(mae)

train 세트로 평가해보자

knr.score(train_input, train_target)

현재 train 세트보다 test 점수가 높으니 과소적합(underfit) 이라 볼수 있습니다.

이 문제를 어떻게 해결할수 있는가?

=> 모델을 조금 더 복잡하게 만들어 보자. 모델이 좀더 복잡해질수록 train 세트에 더 잘 맞게 되고

test 세트의 점수는 조금 낮아질 거다.

k-최근접 이웃 알고리즘의 복잡도를 높이는 방법은 '이웃의 개수 k' 를 줄이는 거다.

이웃의 개수를 줄이면 훈련 세트에 있는 '국지적인 패턴' 에 민감해지고,

이웃의 개수를 늘리면 데이터 전반에 있는 '일반적인 패턴' 을 따르게 될것이다.

이웃의 개수(k)를 조정

knr.n_neighbors = 3
knr.fit(train_input, train_target)
knr.score(train_input, train_target)

k 값을 줄였더니 점수가 높아졌다.

test 세트의 점수도 확인해보자

knr.score(test_input, test_target)

"""
↑ 예상대로, test 세트의 점수는 train 세트때보다 낮아졌으므로 과소적합 문제를 해결한것 같습니다.
또한, 두 점수의 차이가 크지 않으므로 이 모델이 과대 적합 된거 같지도 않습니다.

이제 이 학습된 모델이 test 세트와 추가될 데이터에도 '일반화' 를 잘 하리라 기대할수 있다.
"""
None

단순한 모델 vs 복잡한 모델에 대한 시각화

k 값을 1, 5, 10로 바꿔가며 학습(train)|

농어의 길이는 5 ~ 45까지 바꿔가며 예측 한 결과를 시각화

knr = KNeighborsRegressor()
x = np.arange(5,45).reshape(-1,1) # feature vector 형태로 만들어야 predict사용가능.

k값을 1, 5, 10 일때의 각각의 예측결과

for n in [1, 5, 10] :

모델 훈련

knr.n_neighbors = n
knr.fit(train_input, train_target)

지정한 범위 x에 대한 예측값 구하기

prediction = knr.predict(x)

train세트와 예측결과 그래프 그리기

plt.scatter(train_input, train_target)
plt.plot(x, prediction)
plt.title(f'n_neighbors = {n}')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

KNN Regressor의 문제점

length 50cm 농어의 무게를 예측해보기

train_input, test_input, train_targer, 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)

train_input.shape, test_input.shape

KNN 이웃개수를 3으로 하는 모델을 훈련.

knr = KNeighborsRegressor(n_neighbors = 3)
knr.fit(train_input, train_target)

위 학습된 모델을 사용하여 길이 50cm 농어의 무게 예측

knr.predict([[50]])

"""
모델은 1,033g으로 예측함.

그런데, 실제 측정해보니 농어의 무게가 훨~씬 더 많이 나간다고 해보자.
무엇이 문제일까?
"""
None

시각화를 통해 확인해보자.

50cm 농어의 이웃들을 구하기

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

train 그리기

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.xlabel('weight')
plt.show()

이웃 샘플의 타겟 평균

np.mean(train_target[indexes])

100cm 농어의 이웃들을 구하기

distance, indexes = knr.kneighbors([[100]])

train 그리기

plt.scatter(train_input, train_target)

이웃 샘플 그리기

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

50cm 농어

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

plt.xlabel('length')
plt.xlabel('weight')
plt.show()

선형회귀 (Linear Regression)

특정(feature)가 한개인 경우 어떤 '직선'을 학습하는 알고리즘

from sklearn.linear_model import LinearRegression

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

50cm 농어에 대한 예측

lr.predict([[50]])

"""
농어의 무게 = a x length + b

LinearRegression 이 찾은 a, b는 coef와 intercept 속성에 저장됨.
"""
None

lr.coef_

lr.intercept_

"""
coef 와 intercept 를 머신러닝 알고리즘이 찾은 값이 라는 의미로 모델 파라미터 (model parameter) 라고 부릅니다.
대부분의 많은 머신러닝 알고리즘의 훈련과정은 최적의 모델 파라미터를 찾는 것과 같다.
이를 '모델기반 학습(model based learning)' 이라 합니다. 앞서 사용한 KNN 에는 모델파라미터가 없습니다.
훈련세트를 저장하는 것이 훈련의 전부였습니다. 이를 '사례기반 학습(instance based learning)' 이라 합니다.
"""
None

시각화

plt.scatter(train_input, train_target)

15에서 50까지의 직선의 방정식 그래프를 그리기

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

문제점

lr.score(train_input, train_target)

lr.score(test_input, test_target)

"""
train 세트와 test 세트의 점수가 조금 차이 납니다.
이 모델이 train 세트에 overfit (과대 적합) 된걸까요?
그런데 train 세트의 점수도 그닥 높지도 않습니다.
오히려 전체적으로 underfit (과소 적합) 되었다고 볼수 있습니다.
"""
None

"""
좌 하단이 이상하다
농어의 길이가 15 라고 한다면 무게가 음수가 나올 것이다.
"""
None

해결방안

최적의 '직선' 보다는 최적의 '곡선'을 찾아야한다

2차 방정식

무게 = a x 길이^2 + b x 길이 + c

다항회귀 (Polynomial Regression)

"""
이러한 2차 방정식 그래프를 그리려면 길이를 제곱한 항이 훈련세트에 추가되어야 합니다.
numpy 를 사용하면 손쉽게 만들수 있다.

무게 = a x 길이² - b x 길이 + c <= 길이의 제곱과 원래길이 두가지 모두 필요하다.

length² ← length
[[ 384.16 19.6 ][ 484. 22. ]
[ 349.69 18.7 ][ 302.76 17.4 ]
[1296. 36. ]
...
[ 655.36 25.6 ][1764. 42. ]
[1190.25 34.5 ]]
"""
None

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

train_poly.shape, test_poly.shape

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

예측) 50cm 농어의 무게는?

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

훈련한 계수와 절편

lr.coef_ # a, b 값

lr.intercept_ # c 값

무게 = 1.01 x 길이^2 - 21.7 x 길이 + 116.06

"""
이러한 방정식을 다항식 (polynomial) 이라 부르며 다항식을 사용한 선형 회귀를 다항회귀 (polynomizal regression) 이라 한다
"""
None

훈련 세트의 산점도를 그립니다

plt.scatter(train_input, train_target)

15에서 49까지 2차 방정식 그래프를 그립니다.

point = np.arange(15,50)
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()

lr.score(train_poly, train_target)

lr.score(test_poly, test_target)

"""
train 세트와 test 세트에 대한 점수가 크게 높아졌다
그럼에도 여전히 test 세트의 점수가 조금 더 높다
underfit 이 남아 있는 것으로 보입니다. => 조금더 복잡한 모델이 필요할수도 있다고 생각해야한다.
"""
None

fish_df

fish_df.columns

"""
그렇다! 무게 예측에 영향을 주는건 Length 만이 아니라, 다른 특징들도 영향을 주고 있는 것이다.
선형회귀는 영향을 주는 특성이 많을수록 효과가 크다.
"""
None

다중 회귀(Multiple Regression)

여러개의 특성을 사용한 선형회귀

'특성' 이 2개 이면 '평면' 을 학습하게 됩니다

특성이 2개 + 타겟값 1개 => 3차원 공간을 형성하게 됨.

이때 선형 회귀 방정식은

타킷 = a x feature1 + b x feature2 + c (절편) 의 평면이 됩니다.

만약 특성에 3개 이상이면? => 그림으로는 그릴수 없는 초평면..

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

농어의 길이, 높이, 두께

perch_df = fish_df[fish_df.Species == 'Perch']['Length', 'Height', 'Width']]
perch_df

numpy 배열로 변환후 모델 학습

perch_full = perch_df.to_numpy()
perch_full

perch_weight # target 값

train 값 test 값 나누기

train_input, test_input, train_target, test_target = train_test_split(perch_full, perch_weight, random_state=42)

PolynomualFeature 클래스

from sklearn.preprocessing import PolynomialFeatures

동작 확인

poly = PolynomialFeatures()
poly.fit([[2,3]]) # fit() 새롭게 만든 특성들의 조합을 찾음
poly.transform([[2,3]]) # transform() 실제로 데이터를 변환함.

2개의 특성을 가진 샘플[2,3] -> 6개의 특성을 가진 샘플[1., 2., 3., 4., 6., 9.] 로 변환되었다!

"""
↑ fit() 은 새롭게 만들 특성 조합을 찾고
transform() 은 실제로 데이터를 변환함

fit() 을 잘 보시면, target 을 필요로 하지 않음을 알수 있다. (model 의 fit() 과는 다르다!)
입력데이터만 전달되면 됩니다.

여기서는 2개의 특성(원소)을 가진 샘플[2, 3] 이 6개의 특성을 가진 샘플 [1. 2. 3. 4. 6. 9.] 로 바뀌었습니다.

PolynomialFeatures 클래슨 기본적으로
각 특성을 제곱한 항을 추가하고 특성끼리 서로 곱한 항을 추가합니다.
2 와 3을 각기 제곱한 4와 9가 추가되었고, 2 와 3을 곱한 6이 추가되었습니다.

앞의 1은 왜 추가 되었을까요? 다음식을 봅니다.

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

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

그래서 include_bias=False 로 지정하여 다시 특성을 변환하겠습니다.

"""
None

poly = PolynomialFeatures(include_bias = False)
poly.fit([[2,3]])
poly.transform([[2,3]])

train_input 에 적용하기

poly = PolynomialFeatures(include_bias = False)
poly.fit(train_input)
train_poly = poly.transform(train_input)

train_poly.shape

train_input.shape

어떻게 (42,9) 가 만들어 진걸까?

poly.get_feature_names_out()

"""
'x0' 는 첫번째 특성
'x0^2' 는 첫번째 특성의 제곱
'x0 x1' 은 첫번째 특성과 두번째 특성의 곱

이와 같은 것을 나타냅니다.
"""
None

test_poly = poly.transform(test_input)
test_poly.shape

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

lr.score(test_poly, test_target)

특성을 더 많이 추가하면 어떻게 될까?

ex) 3제곱항, 4제곱 항, ...

poly = PolynomialFeatures(degree=5 , include_bias = False) # degree = 5 5제곱까지의 특성을 만듦

poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)

print(train_poly.shape)
print(test_poly.shape)

lr.fit(train_poly, train_target)
lr.score(train_poly, train_target)

lr.score(test_poly, test_target) # overfit 증상중 하나. 학습모델에서 너무 정확도가 높으면 test 에서 망할 수 있다.

규제 (regularization)

규제 (regularization) 은 머신러닝 모델이 train 세트를 너무 과도하게 학습하지 못하도록 훼방하는 것

즉, 모델이 훈련세트에 과대적합 (overfit) 되지 않도록 만드는 거다.

선형 회귀 모델의 경우 특성에 곱해지는 계수 (또는 기울기) 의 크기를 작게 만드는 일입니다.

아래 그림을 살펴봅니다

오른쪽의 그림은 train 세트를 과도하게 학습했고, 가운데는 기울기를 줄여서 보다 보편적인 (general) 패턴을 학습하고 있습니다

from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
ss.fit(train_poly)

train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)

train 세트에서 학습한 평균과 표준편차 정보

ss.mean, ss.scale

"""
선형 회귀 모델에 규제를 추가한 모델을 릿지 (ridge) 와 라쏘(lasso) 라고 부릅니다.
두 모델은 규제를 가하는 방법에 차이가 있습니다.

릿지: 계수를 제곱한 값을 기준으로 규제를 적용. (일반적으로 릿지 선호)
라쏘: 계수의 절댓값을 기준으로 규제를 적용.

두 알고리즘 모두 계수의 크기를 줄이지만, 라쏘는 아예 0으로 만들 수도 있습니다.
사이킷런은 두 알고리즘 모두 제공합.
"""
None

릿지 회귀 : Ridge Regression

from sklearn.linear_model import Ridge

ridge = Ridge()
ridge.fit(train_scaled, train_target)
ridge.score(train_scaled, train_target)

ridge.score(test_scaled, test_target)

alpha 값 : 규제의 강도를 조정한다.

릿지 와 라쏘 모델을 사용할 때 규제의 양을 임의로 조절할 수 있습니다.

모델 객체를 만들때 alpha 매개변수로 규제의 강도를 조절할수 있다

alpha 값이 크면 규제강도가 세지므로 계수 값을 더 줄이고 조금 더 과소 적합 되도록 유도합니다.

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

alpha 값은 hyper parameter : 사전에 직접 세팅해 주어야 하는 값.

적절한 alpha 값을 찾기위해 alpha 값에 대한 score를 그래프로 그려보자

train 세트와 test 세트의 점수가 둘다 높으면서 + 가장 가까운 지점이 최적의 alpha 값이다.

alpha값을 바꿔가며 그 때 마다 score() 결과를 저장할 리스트 준비

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

print(train_score)
print(test_score)

np.log() : 자연상수 e를 '밑'으로 하는 자연로그

np.log10() : 10을 '밑'으로 하는 상용로그

np.log10(alpha_list)

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

"""
↑ 위의 파란색이 train 세트 그래프
아래 주황색이 test 세트 그래프

왼쪽을 보면 train 세트와 test 세트의 점수차이가 아주 큽니다.
=> train 세트에는 잘 맞고 test 세트에는 형편없는 과대적합(overfit)의 전형적인 모습

반대로 오른쪽 편은 train 세트와 test 셑트긔 점수가 모두 낮아지는 경향을 보이는 과소적합 (underfit) 으로 가는 모습

적절한 alpha 값은 두 그래프가 가장 가깝고 테스트 점수가 가장 높은 -1, 즉 0.1 입니다.
alpha 값을 0.1 로 세팅하여 최종 모델을 훈련해봅니다.
"""
None

ridge = Ridge(alpha = 0.1) # 최적의 alpha 값 세팅
ridge.fit(train_scaled, train_target)

ridge.score(train_scaled, train_target)

ridge.score(test_scaled, test_target)

라쏘 회귀 (Lasso Regression)

from sklearn.linear_model import Lasso

lasso = Lasso()
lasso.fit(train_scaled, train_target)

lasso.score(train_scaled, train_target)

lasso.score(test_scaled, test_target)

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) # 모델 생성 # max_iter : 최적의 계수를 찾기 위한 반복 계산 횟수
lasso.fit(train_scaled, train_target) # 모델 훈련

훈련 점수와 테스트 점수 저장.

train_score.append(lasso.score(train_scaled, train_target))
test_score.append(lasso.score(test_scaled, test_target))

""" ConvergenceWarning
↑ warning 이 뜬다. 뭐지?
라쏘 모델을 훈련할때 ConvergenceWarning 경고가 뜰수 있다.
사이킨 것의 라쏘 모델은 최적의 계수를 찾기 위해 반복적인 계산을 수행하는데, 지정한 반복횟수가 부족할 때 이런 경고가 발생합니다
이 반복 횟수를 충분히 늘리기 위해 max_iter 매개변수의 값을 10000 으로 지정했습니다.
필요하면 더 늘릴 수 있지만, 이 문제에서는 큰 영향을 끼치지 않습니다.
"""
None

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

lasso = Lasso(alpha = 10)
lasso.fit(train_scaled, train_target)

lasso.score(train_scaled, train_target)

lasso.score(test_scaled, test_target)

"""
라쏘 모델은 계수 값을 아예 0으로 만들수도 있습니다.
라쏘 모델의 계수는 coef_ 속성에 저장되어 있습니다. 이 중에 0 인것을 헤아려 보겠습니다.
"""
None

np.sum(lasso.coef_ == 0) # 0 인것은 총 40개 따라서 55개가 아닌 15개면 충분했다.

"""
↑ 정말 많은 계수가 0이 되었습니다. 55개의 특성을 모델에 주입했지만 라쏘 모델이 사용한 특성은 15개 밖에 되지 않습니다.
이러한 특징 때문에 라쏘 모델은 유용한 특성을 골라내는 용도로도 사용할수 있답니다.
"""
None

profile
독해지자

0개의 댓글