[Basic] Regression - kNN, Linear, 다항회귀, 다중회귀, 과적합과 규제, Ridge, Lasso

고보·2024년 3월 7일
post-thumbnail

train set으로 나온 예측이 일반적으로 test set으로 한 예측보다 좋은 값이 나온다. 이 차이가 적을수록 좋은 모델.

1 kNN Regression

#데이터
import numpy as np
perch_length = np.array([25.4, 26.3, 26.5, 29.0, 29.0, 29.7, 29.7, 30.0, 30.0, 30.7, 31.0, 31.0,
                31.5, 32.0, 32.0, 32.0, 33.0, 33.0, 33.5, 33.5, 34.0, 34.0, 34.5, 35.0,
                35.0, 35.0, 35.0, 36.0, 36.0, 37.0, 38.5, 38.5, 39.5, 41.0, 41.0, 9.8,
                10.5, 10.6, 11.0, 11.2, 11.3, 11.8, 11.8, 12.0, 12.2, 12.4, 13.0, 14.3, 15.0])
perch_weight = np.array([242.0, 290.0, 340.0, 363.0, 430.0, 450.0, 500.0, 390.0, 450.0, 500.0, 475.0, 500.0,
                500.0, 340.0, 600.0, 600.0, 700.0, 700.0, 610.0, 650.0, 575.0, 685.0, 620.0, 680.0,
                700.0, 725.0, 720.0, 714.0, 850.0, 1000.0, 920.0, 955.0, 925.0, 975.0, 950.0, 6.7,
                7.5, 7.0, 9.7, 9.8, 8.7, 10.0, 9.9, 9.8, 12.2, 13.4, 12.2, 19.7, 19.9])

#데이터 시각화해서 파악
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_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)
  • 이건 reshape 하기 전의 결과로, 1차원 배열이다.
  • 이건 reshape 한 후의 결과로, 2차원 배열이다.

  • 이유를 설명. 일단 train_test_split에 넣은 데이터가 각각 perch_length(X), perch_weight(y)로 길이로 무게를 예측할 건데, 각각 1차원 numpy 배열이다. => 따라서 split한 결과도 1차원 배열로 나온다.
    • 매개변수들: 처음은 X, y
    • test_size=0.2: 테스트셋의 사이즈를 지정. 남은 게 train_set.
    • shuffle=True: 데이터를 분할하기 전에 무작위로 섞어서, 순서에 의존하지 않도록 한다.
    • stratify=label: 분할된 데이터셋이 원본 데이터셋의 클래스 비율을 유지하도록 한다. 클래스간 불균형이 클 때 중요.
  • .rehspae(-1, 1): reshape는 넘파이 배열의 엘리먼트를 보고, 원하는 모습으로 shape를 바꾼다. -1은 자동으로 맞춘다는 뜻. 즉, 열은 1열, 행은 그거에 맞춰서, 2차원 모양으로 만든다.
from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor()
knr.fit(train_input, train_target)
knr.score(test_input, test_target)

from sklearn.metrics import mean_absolute_error

test_prediction = knr.predict(test_input)
# 테스트 세트에 대한 평균 절댓값 오차를 계산
mae = mean_absolute_error(test_target, test_prediction)
  • 회귀에서 knr.score()로 나오는 값은 R2R^2. 아래 수식에서 y는 실제값, y_hat은 예측값, y_bar는 관측값 평균. 즉, 1은 모델이 완벽하게 예측, 0은 평균값만큼 예측, 0보다 작으면 그냥 평균값 넣은 것보다 나쁜 예측.
  • mean_absolute_error(y_tue, y_pred): 예측값과 실제값 사이의 절대 오차 평균을 계산해서, 회귀 모델의 예측 성능을 평가.
  • 평균 절대 오차(MAE).
  • 과대 적합 vs 과소 적합 이슈.
    kNN으로 치면 k를 너무 줄이면 과대 적합, 너무 높이면 과소 적합이 된다.
    knr.score(train_input, train_target)알 때 0.977 => 이걸 knr.n_neighbors=3으로 바꾸면 -.988 즉, 과소적합이 좀 잡혔다
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=1, 5, 10으로 바꿔가면서 예측 결과 그래프를 그린 것

2 Linear Regression

2-1 kNN Regression이 사용되지 않는 이유

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

from sklearn.neighbors import KNeighborsRegressor
knr = KNeighborsRegressor(n_neighbors = 3)
knr.fit(train_input, train_target)

knr.predict([[50]])

distances, indexes = knr.kneighbors([[50]])
plt.scatter(train_input, train_target)
plt.scatter(train_input[indexes], train_target[indexes], 
            marker = 'D')
#50cm 농어의 예측치(knr.predict([[50]])이 1033
plt.scatter(50, 1033, marker = '^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
  • 위와 같이 예측을 했을 때 결과는 아래와 같다.
  • 하지만 값들을 자세히 보면 좀 이상하다.
#train_target 근처 값들의 평균이 1033
print(np.mean(train_target[indexes]))

print(knr.predict([[100]]))
#이상하다. 50도 1033, 100도 1033

#100으로 해보면, 그래도 똑같다. 즉, 잘못 됐다 모델이
distances, indexes = knr.kneighbors([[100]])
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.ylabel('weight')
plt.show()
  • 50에서 가장 가까운 값들 3개의 np.mean을 해보면 1033. 그리고 이걸 100으로 바꿔도 1033. 그래서 그림을 그려보면 아래와 같다.
  • 즉, 가장 가까운 값 3개의 평균을 내기 때문에, 항상 1033으로 되는 것. => Linear Regression이 필요.

2-2 Linear Regression

  • 선형 회귀: 데이터 관계 모델링하기 위해 직선을 사용하는 회귀 분석. 주어진 독립 변수와 종속 변수 사이의 선형 관계를 찾아서 1차(선형) 방정식으로 표현.
from sklearn.linear_model import LinearRegression

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

print(lr.predict([[50]]))
#기울기(계수), 절편
print(lr.coef_, lr.intercept_)

plt.scatter(train_input, train_target)
#15에서 50까지 1차 방정식(y=ax+b) 그래프를 그립니다
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))
print(lr.score(test_input, test_target))
  • LinearRegression(): LinearRegression 객체 생성
  • predict[[50]]을 하니까 1241이 나온다(위의 kNN보다 훨씬 합리적인 숫자)
  • lr.coef_, lr.intercept_: 각각 이 모델의 계수(기울기)와 절편.
  • score를 해보면 별로 잘 맞지 않다. trainset은 0.93, testset은 0.82. score는 kNN과 마찬가지로 R2R^2.

2-3 다항 회귀(polynomial regression)

  • 비선형 관계 모델링을 위해 독립 변수의 고차항(제곱, 세제곱)을 사용한 선형 회귀의 한 형태.
    비선형이지만, 새로운 특성 공간(x2x^2 등)에서는 여전히 선형이다. 즉, 모델이 예측하는 값은 새로운 특성들의 선형 조합으로 표현된다.
#[제곱값, 원래값]의 배열로
train_poly = np.column_stack((train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 2, test_input))

lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.predict([[50**2, 50]]))

print(lr.coef_, lr.intercept_)

#15에서 49까지 정수 배열
point = np.arange(15, 50)
plt.scatter(train_input, train_target)
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()

  • [제곱값, 원래값]으로 배열을 만들어서 그걸 LinearRegression 입력값에 넣는다. => 그리고 lr.coef, lr.intercept를 보면 2차, 1차, 절편값으로 뜬다.
  • 그걸 plot에 식으로 반영해서 입력.
  • score값을 보면 훨씬 잘 나왔다

2-4 다중 회귀(multiple, multimodal regression)

  • 2개 이상의 독립 변수를 사용해서 종속 변수와의 관계를 모델링.
#온라인에 올린 데이터 불러오기
df = pd.read_csv('https://bit.ly/perch_csv_data')
perch_full = df.to_numpy()
#length, weight, width

train_input, test_input, train_target, test_target = train_test_split(
    perch_full, perch_weight, random_state=42)
  • 데이터 온라인에서 긁어오고, 그걸 numpy로 만들고, train_test_split을 한다.
#preprocessing은 전처리기
from sklearn.preprocessing import PolynomialFeatures

#실험용
poly = PolynomialFeatures()
poly.fit([[2, 3, 4.5]])
print(poly.transform([[2, 3, 4.5]]))

#실험용
poly = PolynomialFeatures()
poly.fit([[2, 3]])
print(poly.transform([[2, 3]])

#3개의 특성 => 9개가 된다
poly = PolynomialFeatures(include_bias = False)
poly.fit(train_input)
train_poly = poly.transform(train_input)

poly.get_feature_names_out()
  • PolynomialFeatures(): 원본 데이터의 특성을 사용해, 지정된 차수까지의 모든 다항식 조합을 생성하는 인스턴스. 여기서 별도의 차수(degree) 지정하지 않으면 기본값인 2차 다항식 특성 생성. 3차로 하면 3차 다항식 특성 생성.
    • include_bias=False: 아래에 있는 다항식의 절편 1을 생략.
  • poly.fit([[2, 3]]): 입력 데이터에 대한 변환 준비. [2, 3]이라는 2차원 배열을 입력으로 사용.
  • poly.transform([[2, 3]]): fit 메서드에서 학습된 변환을 실제 데이터에 적용하여 다항 특성을 생성. => poly.fit과 poly.transform에 들어간 데이터가 다를 경우, transform의 다항식 특성 조합을 생성한다.
  • 이 결과물은 아래와 같다
    • 1: 모든 다항식 특성의 생성에는 절편(intercept)가 항상 포함되며, 항상 1의 값을 가진다
    • 원본값: 원본값 [2, 3]이 그대로 포함
    • 원본값의 거듭제곱: 2의 제곱인 4, 3의 제곱인 9
    • 원본값끼리의 상호작용: 6
      => 결과물은 [1, 2, 3, 4, 6, 9]
    • 예를 들면, [2, 3, 4.5]로 하면 => [1, 2, 3, 4.5, 4, 9, 20.25, 6, 9, 13.5]
  • poly.transform(train_input): 여기서 train_input은 (42, 3)으로 총 3개의 특성을 갖고 있다. => 이게 9개로 1을 제외하고 9개로 늘어난다. 원본값, 원본값의 제곱, 원본값의 조합으로.
  • poly.get_feature_names_out(): PolynomialFeatures 변환기를 사용해 생성된 다항식 특성 이름을 반환.
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))
  • 이렇게 특성을 9개로 늘린 후 넣으니 성능이 더 좋아짐. 이유는 1 다항식의 특성을 생성하면 원본 특성의 제곱, 세제곱, 상호작용을 추가해 비선형 관계를 더 잘 포착하게 된다. / 그리고 모델의 유연성이 증가한다. 즉, 데이터 포인트를 더 정밀하게 학습하고, 복잡한 패턴을 더 잘 반영한다. 훈련 데이터 내의 복잡한 구조를 모델링해야 하는 경우 성능 개선으로 이어진다. / 실제 세계에선 데이터에서 특성 간 상호작용이 종종 중요한 역할을 하는데, 그런 것들을 포함시켜 원본 데이터에서 안드러나는 패턴이 드러나기도 한다.
  • 그렇지만 특성의 수를 과도하게 늘리면 과적합이 된다. => 그게 아래.
  • 따라서 다항식 차수 선택, 교차 검증(cross-validation) 등으로 모델의 복잡성과 일반화 성능 사이의 균형을 찾는 것이 중요.
    • 교차 검증(cross-validation) 모델 성능 평가하고, 일반화 능력 검증하는 통계적 방법. 데이터 훈련 세트/테스트 세트로 1번 나누는 대신, 여러 번 분할해서 여러 번 학습. => k-Fold Cross-validation(k-겹 교차 검증)이 가장 일반적. k개의 동일한 크기를 가진 부분 집합으로 나누어서, k번 반복. 각 반복에서 다른 부분 집합 하나가 테스트 세트로, 나머지 k-1개가 훈련 세트로 사용.
    • 데이터 사용과 효율성 높이고, 안전성과 신뢰성 높인다. 과적합 위험 줄인다. but 계산 비용이 높다.

3 과적합, 규제, Ridge, Lasso

3-1 과적합(Overfitting)

#degree=5로 해서 변수를 55개로 늘렸다
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))
  • 위와 같이 degree=5로 해서 변수를 55개까지 늘리면, 훈련한 train set은 100%로 맞추지만, 테스트 세트는 -144로 완전히 못맞춘다.
    과적합 발생

3-2 규제(Regularization)

  • 모델이 과적합되었을 때, 일반화하는 능력이 떨어진다. 이를 완화하기 위해 규제 기법이 사용된다.
  • 규제는 모델의 복잡도를 제한해 과적합의 위험을 줄이는 방법이다.
  • 규제 전의 표준화 과정이 필요하고, 그게 아래다.
from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
ss.fit(train_poly)

train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)
  • StandScaler: 데이터 표준화(Standardization)을 위한 전처리 기능. 각 특성(변수)의 평균을 0으로, 표준편차를 1로 조정해서 데이터의 스케일을 통일하는 인스턴스 생성.
  • (원래값 - 평균)/표준편차
  • 필요한 이유?
    • 다양한 스케일 가진 여러 특성을 동일한 스케일로 조정. 0~1과 1000~10000을.
    • 규제(regularization)을 적용할 떄, 각 특성이 모델에 미치는 영향을 동일하게 취급 가능하므로, 규제 적용 용이.
    • 경사 하강법(Gradient Descent) 같은 최적화 알고리즘의 수렴 속도를 높일 수 있다. 특성간 스케일이 비슷하면, 모델 파라미터가 최적값에 더 빨리 도달.
  • ss.fit(): 데이터의 평균과 표준편차를 계산.
  • ss.transform(): 표준화.

3-3 Ridge규제(L2 규제)

  • 모델의 가중치(계수)의 제곱합에 대해 페널티를 부과해서 가중치의 크기를 줄이는 역할을 한다. 이를 통해 모델 복잡도가 감소하고, 데이터에 대한 과도한 학습 방지.
  • overfitting된 그래프의 경우 선형 회귀의 계수 값이 매우 크게 나타난다(y=341522 - 24613x + 451262x^2처럼). 이렇게 variance가 큰 상황을 막기 위해, 계수 자체가 크면 페널티를 주는 수식을 추가한다.
  • 릿지 규제를 적용한 선형 회귀의 비용함수
  • J(w)는 비용 함수. MSE는 모델의 가중치에 대한 평균제곱오차. lambda는 규제 강도 결정하는 하이퍼파라미터로, lambda가 크면 클수록 규제가 더 커진다. w^2의 시그마는, 모델 가중치의 제곱합으로, 모든 가중치의 제곱합을 더한 것.
    즉, MSE만 최소화하는 게 아니라, J(w) 전체도 최소화를 하는 방향으로 적용되어야 한다.
    여기서 가중치가 크면 모델 출력은 입력 데이터의 작은 변화에도 크게 반응해서 과적합으로 이어지고, 가중치가 작으면 모델의
    - lambda=0이면, 릿지 규제 적용되지 않고 일반 선형 회귀와 동일. lambda가 커질수록, 모델의 가중치에 대한 제약이 강해지고 복잡도 감소해서 과적함 방지하는 데 도움된다.
    - 적절한 lambda 찾는 게 중요하고, 보통 교차 검증으로 최적화.
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))
  • Ridge()는 Ridge Regression 하는 객체. fit하고, score 매기고, 똑같다.
    매개변수 alpha=를 넣을 수 있는데, 이게 위의 하이퍼파라미터값. 1이 기본.
  • 이렇게 해보면 과적합이 잡힌 것을 확인할 수 있다.
#alpha 값을 10으 ㅣ배수로. 이걸로 최적의 하이퍼 파라미터 찾기
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, label='Train Score')
plt.plot(np.log10(alpha_list), test_score, label='Test Score')
plt.xlabel('log10(alpha)')
plt.ylabel('R^2')
plt.legend()
plt.show()
  • 적당한 alpha 값을 찾기. for문으로 순환하면서, 각각의 train_score를 list에 append하고, 그걸 시각화.
  • log10이 -1이 최선, 즉, 0.1.

3-4 Lasso Regression(L1 규제)

  • Least Absolute Shrinkage and Selection Operator. 모델의 가중치의 절대값의 합에 페널티 부과해서 일부 가중치를 0으로 만든다. => 불필요한 특성의 가중치를 제거.
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))

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, label='Train Score')
plt.plot(np.log10(alpha_list), test_score, label='Test Score')
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.legend()
plt.show()
  • 위의 Ridge와 같은 흐름. Lasso()라는 객체를 만들어서 사용.
  • 여기서 차이점은 alpha=의 디폴트 값이 1000.
  • max_iter=10000
  • np.sum(lasso.coef_==0))이 40. 위의 55개까지 속성값을 늘린 걸 사용했다. 그런데 40개 밖에 사용되지 않았다. 이유는?
profile
일본에서 일하는 게임 기획자. 시시해서 죽어버리지 않게, 재밌고 의미 있는 컨텐츠에 관심 있습니다. 그 도구로 데이터, AI도 찝적댑니다.

0개의 댓글