앞에서 했던 k-최근접 이웃회귀는 치명적인 오류가 있다. 길이가 50이던 100이던 아무리 길이가 길어도 무게를 1000 정도로 밖에 예측하지 못한다. k-최근접 이웃 회귀는 가장 가까운 샘플을 찾아 타깃을 평균하므로 새로운 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값을 예측하게 된다. 이를 해결하기 위해서 선형 회귀에 대해서 학습했다.
선형 회귀
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_input, train_target)
print(lr.predict([[50]]))
[1241.83860323]
LinearRegression은 train_input, train_target처럼 일차원 배열의 데이터로 학습 시킬 때, y=ax+b 처럼 일차함수 형태로 이해하면 된다.
print(lr.coef_, lr.intercept_)
[39.01714496] -709.0186449535477
coef_ 속성은 기울기(계수), intercept_ 속성은 상수항이다.
plt.scatter(train_input, train_target)
plt.plot([15,50], [15*lr.coef_+lr.intercept_, 50*lr.coef_+lr.intercept_])
plt.scatter(50,1241, marker='^')

이렇게 하면 훈련 세트를 벗어나는 데이터에 대해서도 결과를 예측할 수 있다. 다만 문제가 있다.
print(lr.score(train_input,train_target))
print(lr.score(test_input, test_target))
0.939846333997604 0.8247503123313558
훈련세트와 테스트 세트의 점수를 출력해보면 과소 적합인 것을 알 수 있다. 또한 무게가 0 이하로 내려가는 경우가 발생하는데, 이를 해결하는 것이 다항 회귀이다.
다항 회귀
일차함수 즉, 직선이 아닌 2차함수의 형태로 훈련을 시킨다면, 이 곡선은 0 보다 무조건 큰 값을 예측하도록 만들 수 있다.
train_poly= np.column_stack((train_input**2, train_input))
test_poly=np.column_stack((test_input**2, test_input))
column_stack 함수를 사용해서 인풋 데이터들을 제곱해서 왼쪽 열에 붙여준다.
lr.fit(train_poly,train_target)
print(lr.predict([[50**2,50]]))
[1573.98423528]
앞에서 선형회귀로 구한 예측값보다 훨씬 더 높은 값을 예측해준다.
point=np.arange(15,50)
plt.scatter(train_input,train_target)
plt.plot(point, 1.014*point**2-21.558*point+116.05)
plt.scatter(50,1573.98,marker='^')

그래프를 그려보면 아주 이쁜 그래프가 그려지는 것을 확인할 수 있다.
print(lr.score(train_poly,train_target))
print(lr.score(test_poly, test_target))
0.9706807451768623
0.9775935108325122
점수가 높게 나오지만, 여전히 과소적합 문제가 발생한다.
다중 회귀
다중회귀는 여러 개의 특성을 사용한 선형 회귀이다. 무게를 알기 위해서 길이 데이터만 활용하는게 아니라 높이, 두께와 같은 데이터들 같이 한꺼번에 사용하는 것이다. 기존의 특성을 사용해서 새로운 특성을 뽑아내는 작업을 특성 공학이라고 부른다.
import pandas as 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)
다양한 자료가 있으니까 csv 파일을 읽어올 수 있도록 pandas를 활용한다.
head()메서드를 활용하면 맨 앞 5개의 행을 보여준다.
데이터들을 train_test_split으로 훈련데이터와 테스트 데이터로 나눠준다.
변환기
특성들을 전처리하기 위해서 변환기를 사용해보겠다.
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(include_bias=False)
poly.fit(train_input)
train_poly= poly.transform(train_input)
test_poly=poly.transform(test_input)
fit() 메서드는 새롭게 만들 특성 조합을 찾아주고 transform() 메서드는 실제로 데이터를 변환해준다.
선언할 때 include_bias를 False로 해주는 이유는 자동으로 절편을 추가하지 말라고 하는것이다.
poly로 새로운 조합들을 찾아주고(fit) , 훈련데이터와 테스트 데이터를 그 조합들의 데이터로 변환시켜준다(transform).
조합된 특성들의 이름을 보고싶으면 아래의 메서드를 사용하면 된다.
poly.get_feature_names_out()
array(['length', ' height', ' width', 'length^2', 'length height',
'length width', ' height^2', ' height width', ' width^2'],
dtype=object)
이제 변환된 데이터들을 이용해서 다중 회귀 모델을 훈련시켜보겠다.
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
와우. 매우 높은 점수가 나온다. 과소적합 현상도 해결되었다. 그렇다면 특성들을 더 많이 추가해보면 어떻게 될까? 2제곱 뿐만 아니라 3제곱, 4제곱을 넣으면 더 정확해 질까?
poly= PolynomialFeatures(degree=5, include_bias=False)
poly.fit(train_input)
train_poly= poly.transform(train_input)
test_poly= poly.transform(test_input)
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))
0.9999999999996433
-144.40579436844948
degree를 5로 하면 만들어지는 특성의 개수는 55개나 된다. 그런데 점수 출력이 이상하다. 훈련데이터 점수는 진짜 거의 퍼펙트 한데, 테스트 데이터가 무슨 말도 안되는 이런쌉쌉 스러운 값이 나온다. 특성의 개수를 늘리면 선형모델은 훈련 세트에 대해서 거의 완벽하게 학습을 하지만 훈련 세트에 너무 과대적합 되므로 테스트 세트에서는 형편 없는 점수가 나온다. 이를 해결하기 위해서는
규제가 필요하다.
규제
규제는 모델이 훈련 세트에 과대적합되지 않도록 하는데 선형 회귀 모델의 경우 특성에 곱해지는 계수(기울기)의 크기를 작게 만든다. 규제에 적용하기 전, 앞 게시물에서도 알아봤듯이 스케일이 제각각이면 제어가 공정하게 이루어지지 않는다. 따라서 규제를 적용하기 전에 정규화를 진행해야한다(평균과 표준편차를 구해 표준점수 변환). 앞으로는 사이킷런에서 제공하는 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를 활용해서 정규화를 마친 train_scaled와 test_scaled 를 얻게 되었다.
릿지 회귀
from sklearn.linear_model import Ridge
ridge= Ridge()
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))
0.9896101671037343
0.9790693977615387
릿지 회귀를 이용하니 점수가 선형회귀를 사용했을때보다 확실히 안정적으로 변한 것이 확인된다.
릿지 모델에서는 alpha 매개변수로 규제의 강도를 조절한다. alpha 값이 크면 규제 강도가 세지므로 과소적합 되도록 유도되고 alpha 값을 줄이면 계수를 줄이는 역할이 줄어들어서 선형 회귀 모델과 유사해지므로 과대적합될 가능성이 크다.
R^2 그래프를 그려보면 훈련세트와 테스트 세트의 점수가 가장 가까운 지점이 최적의 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(alpha_list, train_score)
plt.plot(alpha_list, test_score)
plt.xscale('log')
plt.show()

파란색이 훈련세트, 주황색이 테스트 세트이다. alpha값이 0.1일때 두 그래프가 가장 가까운 것을 확인할 수 있다. 이제 alpha 값을 0.1로 설정하고선 릿지모델을 훈련 시킨 후, 점수를 확인해보겠다.
ridge=Ridge(alpha=0.1)
ridge.fit(train_scaled,train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))
0.9903815817570367
0.9827976465386928
와우!!! 아주 적절한 값을 보여준다. 그렇다면 이제 라쏘모델을 살펴보겠다.
라쏘 회귀
라쏘 모델을 훈련시키는 것은 릿지와 매우 비슷하다.
from sklearn.linear_model import Lasso
lasso=Lasso()
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))
0.989789897208096
0.9800593698421883
라쏘 모델도 과대적합을 잘 억제한 결과를 보여준다.
라쏘 모델 또한 최적의 alpha 값을 찾아보겠다.
train_score=[]
test_score=[]
alpha_score=[0.001,0.01,0.1,1,10,100]
for alpha in alpha_score:
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(alpha_score, train_score)
plt.plot(alpha_score, test_score)
plt.xscale('log')

이번에는 alpha 값이 10일때 두 그래프가 가장 가까운 것을 볼 수 있다. 가장 오른쪽은 점수가 크게 떨어지는데, 과소적합 되는 모델일 것이다.
alpha값을 10으로 놓고 라쏘모델을 최종 훈련시키겠다.
lasso=Lasso(alpha=10)
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))
0.9888067471131867
0.9824470598706695
릿지와 마찬가지로 라쏘도 과대적합을 잘 억제하고 테스트 세트의 성능을 크게 높였다.
릿지는 규제가 있는 선형 회귀 모델 중 하나이면 선형 모델의 계수를 작게 만들어 과대적합을 완화시킨다.
라쏜는 또 다른 규제가 있는 선형 회귀 모델인데, 릿지와 달리 계수 값을 아예 0으로 만들 수 있다.
'ML' 카테고리의 다른 글
| ML/ 로지스틱 회귀, 이진 분류 vs 다중 분류 (0) | 2026.01.15 |
|---|---|
| ML/ k-최근접 이웃 회귀로 무게 예측하기 (0) | 2026.01.14 |
| ML/ 데이터 전처리 방법 (0) | 2026.01.13 |
| ML/ 훈련 세트와 테스트 세트 분리 (0) | 2026.01.13 |
| ML/k-최근접 이웃 알고리즘 (0) | 2026.01.13 |