생각보다 시간이 빠르게 흘러가 어느덧 벌써 2주차!
마음은 예습을 해야지 생각하고 있지만 현실은 따라가기 바쁨!
아무 것도 모른 상태로 시작은 했지만, 생각보다 책의 흐름이 잘되어있어 나쁘지 않은 것 같다.
Ch. 03 회귀 알고리즘과 모델 규제
농어의 무게를 예측하라!
챕터 02에서는 도미와 빙어를 두고, k-최근접 이웃 분류 알고리즘을 사용하여,
선택한 대상의 주변을 참고하여 대상을 예측하는 알고리즘을 사용했었다.
k-최근접 이웃 회귀
Ch.03에서는 k-최근접 이웃 회귀 알고리즘을 이용하여 대상의 값을 예측해 본다.
k-최근접 이웃 분류 vs k-최근접 이웃 회귀
두 알고리즘은 입력 데이터와 가까운 k개의 이웃을 기준으로 예측한다는 공통점이 존재하지만,
분류는 범주를 예측하고 (예를 들어 고양이와 강아지 중 어디에 속할까?)
회귀는 연속적인 수치를 예측(내일의 기온은 몇 도일까?)한다.
아래 <그림 1>을 참조하면 더 쉽게 이해가 가능하다.

넘파이 배열은 크기를 바꿀 수 있는 reshape 제공
- 배열 변경 방법
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)
회귀 모델 훈련 방법
from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor()
# k-최근접 이웃 회귀 모델을 훈련합니다
knr.fit(train_input, train_target)
knr.score(test_input, test_target)
0.992809406101064
정확도(맞춘 비율-분류)와 결정계수[R²](예측의 설명력-회귀) 그리고 결정계수 구하는 법

과대 적합과 과소 적합
과대 적합 : 훈련 세트 점수가 굉장히 좋지만, 테스트 세트에서는 굉장히 점수가 나쁜 경우
-> 해결 방법 : 모델을 덜 복잡하게 만들어야 하기 때문에 이웃의 수를 늘린다.
과소 적합 : 훈련 세트보다 테스트 세트의 점수가 높거나 두 점수가 모두 너무 낮은 경우
-> 해결 방법 : 모델을 더 복잡하게 만들어야 하기 때문에 이웃의 수를 줄인다.
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)
길이가 50cm인 농어의 무게 측정
print(knr.predict([[50]]))
[1033.33333333]
결과 값을 확인하면 1.033g으로 무게를 예측했다.
하지만 실제 이 농어의 무게를 측정해보니 무게는 훨씬 많이 나간다.
농어의 다이어트인가?
왜 회귀 모델을 이용하여 예측한 무게가 다를까?
참고한 모델을 확인해보기 위해 참고한 모델을 선점도에 표시해보자.
import matplotlib.pyplot as plt
# 50cm 농어의 이웃을 구합니다.
distances, indexs = knr.kneighbors([[50]])
# 훈련 세트의 산점도를 그립니다.
plt.scatter(train_input, train_target)
# 훈련 세트 중에서 이웃 샘플만 마커를 다이아몬드('D')로 표시
plt.scatter(train_input[indexs], train_target[indexs], marker='D')
# 50cm 농어 데이터
plt.scatter(50, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

출력 된 산점도를 보면 길이가 커질 수록 무게도 증가한고 있다는 것이 보인다.
그럼 내가 선택한 50cm 농어(△)를 보면 좌측의 농어들을 무게를 참조한 것을 확인 수 있다.
그렇다면!! k-최근접 이웃 알고리즘은 이웃들의 수치를 평균하여 예측을 하기 때문에 참조한 농어들의 무게 평균을 구해보자.
print(np.mean(train_target[indexs]))
1033.3333333333333
출력 된 결과물을 보면 50cm 농어의 무게와 같은 것을 알 수 있다.
그럼 여기서 길이가 100cm인 농어의 무게를 예측한다면? 어떤 값이 나올까?
print(knr.predict([[100]]))
1033.3333333333333
잉? 50cm 농어의 무게와 동일하다..
# 100cm 농어의 이웃을 구합니다.
distances, indexs = knr.kneighbors([[100]])
# 훈련 세트의 산점도를 그립니다.
plt.scatter(train_input, train_target)
# 훈련 세트 중에서 이웃 샘플만 다시 그립니다.
plt.scatter(train_input[indexs], train_target[indexs], marker='D')
# 100cm 농어 데이터
plt.scatter(100, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

산점도를 보게되면 이전 50cm 농어의 무게를 예측할 때와 똑같은 농어 모델들을 참조하면서 발생한 일인 것을 알 수 있다.
이처럼 k-최근접 이웃 알고리즘은 주변 이웃들을 참조하여 결과 값을 예측하기에 한계가 있다.
과제 (4번 문제)
# 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')
plt.show()

선형 회귀
선형 회귀는 간단하고 성능이 뛰어나 청므 배우는 머신러닝 알고리즘 중 하나로 이름에서도 알 수 있듯이 특성이 하나인 경우 어떤 직선을 학습하는 알고리즘이다.
쉽게 말하자면 "두 변수 사이의 관계를 직선으로 표현하는 방법"이다.
그렇기 때문에 k-최근접 이웃 알고리즘과 달리 물고기의 길이가 길수록 무게도 늘어난다고 할 때, 그 관계를 일직선으로 그려서 처음 보는 길이의 물고기도 무게를 예측할 수 있다.

위에서 보면 실제 데이터가 우상향으로 가고 있어 , 선형 회귀 직선도 우상향으로 학습한다.
그럼 위에서 예측했었던 50cm 농어의 무게를 선형 회귀로 다시 예측 해보자.
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
# 선형 회귀 모델을 훈련
lr.fit(train_input, train_target)
# 50cm 농어 대해 예측
print(lr.predict([[50]]))
[1241.83860323]
출력 된 결과 값을 보면 k-최근접 이웃 알고리즘과는 다르게 더 무거운 무게의 값을 출력 한 것을 확인 할 수 있다.
# 훈련 세트의 산점도를 그립니다.
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()

다항 회귀
선형 회귀가 데이터를 일직선으로 표현했다면, 다항 회귀는 조금 더 복잡한 데이터를 설명할 수 있도록 설계됐다.
예를 들어 예를 들어 물고기의 무게는 길이가 커질수록 더 빠르게 증가할 수도 있는데,
이 때 직선이 아닌 곡선으로 데이터를 설명하는 것이 더 정확하다.
# 구간별 직선을 그리기 위해 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()

특성 공학과 규제
다중 회귀
선형 회귀가 한 가지의 특성으로 예측을 했다면,
다중 회귀는 여러가지 특성을 함께 고려해서 결과를 예측하는 방법이다.
예를 들어 선형 회귀에서는 길이만을 가지고 예측을 했다면,
다중 회귀는 길이, 두께, 높이 등의 특성을 종합해서 예측한다.

특성이 늘어난 만큼 데이터도 복잡해지는 것을 볼 수 있다.
이때 우리는 판다스를 사용할 수 있다. 판다스는 넘파이와 비슷하게 2차원 배열을 다루지만, 훨씬 더 많은 기능을 제공한다.
또한 사이킷런에 데이터프레임을 입력하면 넘파이 배열로 바꾸어 모델을 훈련하기 때문에, 데이터프레임과 넘파이 배열 중 어떤 것을 사용하더라고 성능에 차이가 없다!
import pandas as pd # pd는 관례적으로 사용하는 판다스의 별칭입니다.
perch_full = pd.read_csv('https://bit.ly/perch_csv_data')
perch_full.head()
사이킷런의 변환기
사이킷런은 특성을 만들거나, 전처리하기 위한 다양한 클래스들을 제공하는데,
사이킷런에서는 이런 클래스를 변환기라고 부른다.
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)
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures()
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))
[[1. 2. 3. 4. 6. 9.]]
poly = PolynomialFeatures(include_bias=False)
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))
[[2. 3. 4. 6. 9.]]
include_bias=False는 절편을 제거하는 옵션이지만,
False로 지정하지 않아도 사이킷런 모델은 자동으로 특성에 추가된 절편 항을 무시한다!
다중 회귀 모델 훈련하기
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
0.9903183436982125
0.9714559911594111
훈련 세트는 높은 점수가 나왔으며, 테스트 세트도 점수가 변하지는 않았지만 높은 점수가 나와 농어의 길이만 사용했을 때와는 다르게 과소 적합 문제는 발견되지 않았다.
그럼 특성을 더 많이 추가하게되면 어떤 증상이 발생할까?
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)
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
(42, 55)
0.9999999999996433
-144.40579436844948
훈련 세트에서는 1에 가까운 높은 점수가 나왔다.
하지만 테스트 세트에서는 아주 큰 음수로 나왔다.
왜 그럴까?
바로 샘플의 수는 42개였지만 위 코드에선 샘플의 데이터보다 많은 55개의 특성으로 훈련을 시켰다.
그렇기 때문에 패턴을 학습하기 보다는 그냥 외우기만 해서 훈련 세트에서는 강하지만, 새로운 문제인 테스트 세트에서는 약한 모습을 보인다.
그럼 이와 같은 문제를 해결하려면?
다시 특성을 줄이자! 특성은 줄이는 방법은 바로 규제를 하면 된다.
규제
선형 회귀 모델에 규제를 추가한 모델을 릿지와 라쏘라고 부른다.
릿지 회귀
계수를 제곱한 값을 기준으로 규제를 적용하는 모델
alpha 값이 크면 규제 강도가 세지므로 계수 값을 더 줄이고 조금 더 과소적합 되도록 유도하고,
alpha 값이 작으면 계수를 줄이는 역할이 줄어들어 선형회귀 모델과 유사해지므로 과대적합 될 가능성이 높다.
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))
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()

라쏘 회귀
계수의 절댓값을 기준으로 규제를 적용하는 모델
from sklearn.linear_model import Lasso
lasso = Lasso()
lasso.fit(train_scaled, train_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)
# 라쏘 모델을 훈련합니다.
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()

모델 파라미터란?
간단하게 말하자면 모델이 결과를 계산하기 위해 스스로 배운 숫자!
예를들어 햄버거 가게가 있고 고객이 아래와 같이 주문을 하였다.
"패티 2개, 치즈 1장, 양상추는 3장 넣어주세요"
가격 = 패티 × 3000원 + 치즈 × 1000원 + 양상추 × 200원
여기서 패티, 치즈, 양상추는 입력 값이 되고,
3000원, 1000원, 200원은 모델이 스스로 배운 숫자인 모델 파라미터이다.
'Newb > 머신러닝' 카테고리의 다른 글
| [혼공머신] 6주차 [6/6] (8) | 2025.08.15 |
|---|---|
| [혼공머신] 5주차 [5/6] (13) | 2025.08.09 |
| [혼공머신] 4주차 [4/6] (3) | 2025.07.27 |
| [혼공머신] 3주차[3/6] (1) | 2025.07.20 |
| [혼공머신] 1주차 (2) | 2025.07.06 |
