본문 바로가기
로봇/인공지능, AI

[핸즈온 머신러닝 3판] 4.모델 훈련

by 33곰탱 2024. 9. 28.

1부 4장

오늘 정리할 부분은 4장 모델 훈련이다. 선형회귀와 경사 하강법, 다항 회귀, 과적합 감지 및 규제 기법과 로지스틱 회귀, 소프트맥스 회귀를 살펴본다고 한다. 회귀회귀회귀회귀

4장 모델 훈련

  • 4.1 선형 회귀
  • 4.2 경사 하강법
  • 4.3 다항 회귀
  • 4.4 학습 곡선
  • 4.5 규제가 있는 선형 모델
  • 4.6 로지스틱 회귀

4.1 선형 회귀

1장에서 삶의 만족도에 대한 선형 회귀 모델을 만들었었다. 기억 하나..? (나는 잘 기억이..)

$삶의 만족도 = \theta_0 + \theta_1 \times 1인당\_GDP$

 여기서 $\theta_0$ 과 $\theta_1$은 모델 파라미터이고 독립변수(입력 특성)로 1인당 GDP를 사용한다.
 일반적으로 선형 모델은 아래와 같이 입력 특성의 가중치 합과 bias를 더해서 예측을 만든다.
 
$\hat{y} = \theta_0 + \theta_1 x_1 + \theta_2 x_2 + \dots + \theta_n x_n$
 
  • $\hat{y}$ 은 예측값
  • $n$은 특성의 수
  • $x_i$는은 $i$번째 특성값
  • $\theta_j$는 $j$번째 모델 파라미터를 뜻한다.

이 식은 벡터 형태로 더 간단하게 쓸 수 있다.

$\hat y=h_\theta( x)=\theta\cdot x$

 

  • $h_\theta$는 모델 파라미터 $\theta$를 사용한 가설 함수
  • $\theta$는 bias $\theta_0$와 $\theta_1$에서 $\theta_n$까지의 특성 가중치를 담은 모델의 파라미터 벡터
  • $x$는 $x_0$에서 $x_n$까지 담은 샘플의 특성 벡터, $x_0$은 항상 1임
  • $\theta^T x$는 벡터 $\theta$와 $x$의 스칼라곱임, 이는 $\theta_0 x_0 + \theta_1 x_1 + \theta_2 x_2 + \dots + \theta_n x_n$과 같음.

 

이제 이 선형 회귀 모델을 훈련시켜보자.

모델을 훈련시킨다는 것은 훈련 데이터 세트에 가장 적합한 모델의 파라미터를 찾는 과정을 의미한다.

이를 위해서는 성능 측정 지표를 설정하고, 모델이 데이터에 얼마나 잘 맞는지 평가해야 한다.

 

2장에서 언급한 것처럼, 선형 회귀 모델을 훈련하는 과정은 **RMSE(평균 제곱근 오차)**를 최소화하는 최적의 파라미터 $\theta$를 찾는 과정이다.

 

하지만 여기서는 RMSE 대신 MSE최소화하는 것이 동일한 결과를 낼 수 있으면서도 계산이 더 간단하다고 설명한다.

왜 MSE가 더 간단하지?

왜냐하면 RMSEMSE에 제곱근을 취한 값인데. 하지만, 모델을 훈련할 때는 손실 함수를 최소화하는 것이 목적이기 때문에, 제곱근을 취하든 취하지 않든 최적의 파라미터 $\theta$ 를 찾는 데에는 차이가 없다고 한다.

 

여기 MSE의 식이 있다.

$\text{MSE}(X, h_\theta) = \frac{1}{m} \sum_{i=1}^{m} \left( \theta^T x^{(i)} - y^{(i)} \right)^2$

앞으로는 간단하게 표시하기 위해 $\text{MSE}(X, h_\theta)$ 말고 $\text{MSE}(\theta)$로 표기하도록 하겠다.

4.1.1 정규 방정식

비용 함수를  최소화하는 $\theta$ 값을 얻을 수 있는 수학 공식이 있다. 이를 정규 방정식이라고 하는데 아래와 같다.

$\hat{\theta} = (X^T X)^{-1} X^T y$

 

이 공식이 맞는지 확인해 보자 $y = 4 + 3 \times X + \text{잡음}$의 데이터 100개를 넣어보자.

import numpy as np

np.random.seed(42)  # 코드 예제를 재현 가능하게 만들기 위해
m = 100  # 샘플 개수
X = 2 * np.random.rand(m, 1)  # 열 벡터
y = 4 + 3 * X + np.random.randn(m, 1)  # 열 벡터

# 추가 코드 - 그림 4-1 생성 및 저장

import matplotlib.pyplot as plt

plt.figure(figsize=(6, 4))
plt.plot(X, y, "b.")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([0, 2, 0, 15])
plt.grid()
save_fig("generated_data_plot")
plt.show()

일단 위의 코드를 통해서 선형적인 모습을 띄는 데이터를 만들었다.

 

계산을 위해서 넘파이 선형대수 모듈에 있는 inv() 함수를 사용해 역행렬을 계산하고 dot() 으로 행렬 곱셈을 해주자.

 

from sklearn.preprocessing import add_dummy_feature

X_b = add_dummy_feature(X)  # 각 샘플에 x0 = 1을 추가합니다.
theta_best = np.linalg.inv(X_b.T @ X_b) @ X_b.T @ y

theta_best

@는 처음 봤는데 행렬 곱셈을 수행하는 연산자라고 한다. (파이썬의 리스트는 사용 불가능하고 넘파이 배열에 사용 가능한듯..?)

 

$y = 4 + 3 * X + \text{잡음}$의 함수를 사용했는데 정규 방정식으로 계산한 결과 $y = 4.2150.. + 2.770 * X$가 나왔다. 아무래도 4와 3과는 살짝 거리가 있다. 이는 왜냐하면 데이터셋이 작고 잡음이 많을수록 정확하기 어렵다고 한다.

 

이제 이 함수로 모델의 예측을 나타내 보자

X_new = np.array([[0], [2]])
X_new_b = add_dummy_feature(X_new)  # 각 샘플에 x0 = 1을 추가합니다.
y_predict = X_new_b @ theta_best
y_predict

X=0일 때는 y=4.21509616, X=2일때는 y= 9.75532293이다 이 두 점을 이어주자

import matplotlib.pyplot as plt

plt.figure(figsize=(6, 4))  # 추가 코드
plt.plot(X_new, y_predict, "r-", label="Predictions")
plt.plot(X, y, "b.")

# 추가 코드 - 그림 4-2를 꾸미고 저장합니다.
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([0, 2, 0, 15])
plt.grid()
plt.legend(loc="upper left")
save_fig("linear_model_predictions_plot")

plt.show()

 

이러한 과정을 역시 사이킷런을 사용해서 수행할 수 있는데 아래의 코드를 확인해 보자

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(X, y)
lin_reg.intercept_, lin_reg.coef_

사이킷런은 특성의 가중치bias를 분리하여 저장한다.

lin_reg.predict(X_new)

위에서 했던 정규 방정식의 값과 똑같은 것을 확인할 수 있다!!

 

또한 유사역행렬..? 로도 계산을 할 수있다고 하는데 무슨 말인지 몰라서 일단 코드만 적어두겠다..

theta_best_svd, residuals, rank, s = np.linalg.lstsq(X_b, y, rcond=1e-6)
np.linalg.pinv(X_b) @ y # 두 코드 둘다 같은 값을 출력함

4.1.2 계산 복잡도

1. 정규 방정식의 계산 복잡도:

  • 정규 방정식에서 (n+1) * (n+1) 크기의 $X^TX$역행렬을 계산하는데, 그 복잡도는 일반적으로 O(n^2.4)에서 O(n^3) 사이라고 한다. (특성 수)

2. SVD 방법의 계산 복잡도:

  • 사이킷런의 LinearRegression 클래스는 SVD(특잇값 분해) 방법을 사용하며, 계산 복잡도는 O(n^2)이다. 이 방법을 사용하면 특성이 두 배로 늘어나도 계산 시간이 4배 증가한다고 한다 ㄷㄷㄷ..

3. 주의사항:

  • 정규 방정식이나 SVD 방법 모두 특성 수가 많으면(예: 100,000개 이상) 매우 느려지게 된다고 한다. (주의: 훈련 세트의 수가 아닌 특성의 수이다..!
  • 훈련 샘플의 수가 많을 때는 선형적으로 증가하는 특성을 보여, 큰 훈련 세트에도 적합할 수 있다고 한다.

4 2 경사 하강법

경사 하강법(Gradient Descent)

경사 하강법(Gradient descent)은 여러 종류의 문제에서 최적의 해법을 찾을 수 있는 일반적인 최적화 알고리즘이다.

 

비용 함수를 최소화하기 위해 반복적으로 파라미터를 조정하는 방법인데, 마치 산을 내려가는 것처럼, 현재 위치에서 가장 가파른 경사를 따라 내려가는 방식으로 최솟값을 찾는 방법이다! 

 

이 과정에서 파라미터 벡터 $\theta$에 대해 비용 함수의 현재 기울기(Gradient)를 계산하고 기울기가 감소하는 방향으로 진행한다. 그리고 기울기가 0이 되면 최솟값에 도달한 것으로 판단할 수 있다!!

 

경사 하강법임의의 값으로 초기화(random initalization)된 파라미터에서 시작하며, 비용 함수가 감소되는 방향으로 진행하여 알고리즘이 최솟값에 수렴할 때까지 점진적으로 향상시킨다. 이해를 위해서 아래의 그림을 봐보자.

 

 

위의 사진을 분석해보자.

  • 경사 하강법에서 중요한 파라미터는 스텝의 크기이다. 이 때 스텝의 크기는 학습률 하이퍼파라미터로 결정이 된다.
  • 위에서 볼 수 있듯이 학습률이 너무 작으면 최적의 지점에 도달하는 시간이 매우 오래 걸릴 수 있고, 학습률이 너무 크면 최적의 지점을 지나칠 수 있게된다.
    • 학습률이 너무 작을 때: 비용 함수가 매우 천천히 줄어들어서, 시간을 낭비할 수 있음.
    • 학습률이 너무 클 때: 이전보다 더 높은 곳으로 올라갈 수도 있다.. 그렇게 되면 최솟값으로 수렴하지 못할 수도 있음.
  • 경사 하강법은 지역 최솟값에 빠질 수 있다는 한계가 있다... 이때 전역 최솟값에 도달하지 못하고 지역 최솟값에서 멈출 수 있게 된다.
    • 하지만..! MSE 비용 함수불록 함수인데, 불록 함수는 그래프가 곡선으로 매끄럽게 이어져 있고, 한 개의 최저점(전역 최솟값)을 가진다고 한다.
    • 따라서 경사 하강법을 사용하면, 여러 지점에서 시작하더라도 결국 전역 최솟값에 도달할 수 있게된다!!! 지역 최솟값에 빠질 위험이 없는 구조를 갖고 있다는 뜻이다.

특성 스케일링

  • 또한, 특성의 스케일이 다르면 경사 하강법의 진행 속도가 달라진다고 한다. 따라서 경사 하강법을 사용할 때는 모든 특성의 스케일을 맞춰 주는 것이 중요하다고 한다. 
    • 집의 가격을 예측하는 모델을 예로 들어보고 두 가지 특성을 사용한다고 가정해보자!
      • 면적: 100~300 (m²)
      • 방 개수: 1~5 (개)
    • 이때, 경사 하강법은 면적이라는 큰 값에만 집중하게 되고, 방 개수는 상대적으로 작은 값이므로 거의 영향을 미치지 않게 된다. 이 때문에 학습이 편향되고, 느리게 수렴하거나 최적값을 찾지 못할 수 있다.

 

4.2.1 배치 경사 하강법

  • 경사 하강법을 구현하려면 각 모델 파라미터 $\theta_j$​에 대해 비용 함수의 기울기(그래디언트)를 계산해야 한다. 이때 비용 함수가 약간 변할 때 기울기가 얼마나 변하는지를 계산하는 것을 편도함수라고 부른다.
  • 편도함수는 파라미터마다 계산된다. 여러 파라미터에 대한 편도함수를 묶어, 비용 함수의 기울기를 나타내는 벡터를 그래디언트 벡터라고 한다.

아래가 비용 함수의 편도함수이다.

 

$\frac{\partial}{\partial \theta_j} MSE(\theta) = \frac{2}{m} \sum_{i=1}^{m} \left( \theta^T x^{(i)} - y^{(i)} \right) x_j^{(i)}$

 

그래디언트 벡터 $\nabla_\theta MSE(\theta) $는 비용 함수의 편도함수를 모두 담고 있다고 한다. 아래와 같다.

 

$\nabla_\theta MSE(\theta) = 
\begin{bmatrix}
    \frac{\partial}{\partial \theta_0} MSE(\theta) \\
    \frac{\partial}{\partial \theta_1} MSE(\theta) \\
    \vdots \\
    \frac{\partial}{\partial \theta_n} MSE(\theta)
\end{bmatrix}
= \frac{2}{m} X^T (X \theta - y)$

 

파라미터 $\theta$는 아래 수식에 따라 업데이트된다.

 

$\theta_{\text{next step}} = \theta - \eta \nabla_\theta MSE(\theta)$

 

( 위로 향하는 그레이디언트 벡터가 구해지면 반대 방향인 아래로 가야 하기 때문에 $\theta$에서 $\nabla_\theta MSE(\theta)$를 빼줘야 함)

  • 그레이디언트 벡터는 비용 함수가 증가하는 방향(오르막길)을 가리키게 된다. (왜냐하면 기울기는 어떤 함수의 특정 지점에서 가장 가파르게 증가하는 방향을 나타내는 벡터이기 때문에)
  • 하지만, 우리가 찾고자 하는 것은 최솟값(내리막길의 끝)이기 때문에 그레이디언트 벡터가 가리키는 오르막 방향의 반대쪽(내리막 방향)으로 이동해야 함.
  • 따라서 파라미터 $\theta$는 그레이디언트 벡터가 가리키는 방향의 반대 방향으로 업데이트 해주기!

 

 

여기서 $\eta$는 학습률(learning rate)을 나타내며, 기울기를 따라 내려가는 스텝 크기를 결정해준다.

 

이 알고리즘을 코드로 구현해 보면

eta = 0.1  # 학습률
n_epochs = 1000
m = len(X_b)  # 샘플 개수

np.random.seed(42)
theta = np.random.randn(2, 1)  # 모델 파라미터를 랜덤하게 초기화합니다

for epoch in range(n_epochs):
    gradients = 2 / m * X_b.T @ (X_b @ theta - y)
    theta = theta - eta * gradients

theta

정규 방정식으로 찾은 값과 같은 모습을 확인할 수 있다!

 

여기서 학습률을 바꿔주면 어떻게 되는지 확인해보자

 

# 추가 코드 - 그림 4-8을 생성하고 저장합니다.

import matplotlib as mpl

def plot_gradient_descent(theta, eta):
    m = len(X_b)
    plt.plot(X, y, "b.")
    n_epochs = 1000
    n_shown = 20
    theta_path = []
    for epoch in range(n_epochs):
        if epoch < n_shown:
            y_predict = X_new_b @ theta
            color = mpl.colors.rgb2hex(plt.cm.OrRd(epoch / n_shown + 0.15))
            plt.plot(X_new, y_predict, linestyle="solid", color=color)
        gradients = 2 / m * X_b.T @ (X_b @ theta - y)
        theta = theta - eta * gradients
        theta_path.append(theta)
    plt.xlabel("$x_1$")
    plt.axis([0, 2, 0, 15])
    plt.grid()
    plt.title(fr"$\eta = {eta}$")
    return theta_path

np.random.seed(42)
theta = np.random.randn(2, 1)  # 랜덤 초기화

plt.figure(figsize=(10, 4))
plt.subplot(131)
plot_gradient_descent(theta, eta=0.02)
plt.ylabel("$y$", rotation=0)
plt.subplot(132)
theta_path_bgd = plot_gradient_descent(theta, eta=0.1)
plt.gca().axes.yaxis.set_ticklabels([])
plt.subplot(133)
plt.gca().axes.yaxis.set_ticklabels([])
plot_gradient_descent(theta, eta=0.5)
save_fig("gradient_descent_plot")
plt.show()

아까전에 알아 봤듯이 학습률이 너무 낮으면 시간이 매우 오래걸리고 학습률이 너무 크면 점점 더 멀어지면서 발산하는 모습을 확인할 수 있다.

 

가운데 처럼 적절한 학습률을 찾기 위해서는 어떻게 해야할까..?

2장에서 알아본 것 처럼 그리드 서치를 사용해서 찾으면 된다! 하지만 그리드 서치에서 너무 오래걸리는 모델은 제외할 수 있도록 반복 횟수를 제한해야 한다.

 

반복 횟수를 아주 크게 지정하고 그레이디언트 벡터가 아주 작아질 때 거의 경사하강법이 최솟값에 도달한 것이므로 알고리즘을 중지하면 된다고 말한다.

4.2.2 확률적 경사 하강법

매번 훈련 세트 전체를 사용하는 배치 경사 하강법과 달리, 확률적 경사 하강법은 랜덤하게 하나의 샘플을 선택해 경사(gradient)를 계산하는 방법이다. 이렇게 하면 하나의 샘플을 처리하면 매 반복에서 다뤄야 할 데이터가 매우 적기 때문에 알고리즘이 확실이 더 빠르고, 훈련 시간을 단축할 수 있다.

 

반면 확률적이라서 배치 경사 하강법보다 불안정하다는 단점이있다. 확률적이라서 부드럽게 감소하지 않고 위아래로 요동치면서 평균적으로 감소하기 때문에 최솟값에 매우 근접은 하지만 안착하지는 못하게 된다. 

 

하지만 또 좋은 점으로 비용 함수가 매우 불규칙해서 지역 최솟값을 건너뛰도록 도와줘서 전역 최솟값을 경사 하강법보다 전역 최솟값을 찾을 가능성이 높다고 한다.. 이런 무작위성은 이처럼 좋을 때도 있고 안 좋을때도 있다. 그렇다면 이러한 딜레마를 어떻게 해결해야 할까..?

학습률을 점진적으로 감소시키자

바로 시작할 때는 학습률을 크게 하고 점차 작게 줄여서 알고리즘이 전역 최솟값에 도달하게 하자는 것이 핵심이다. 이 과정은 금속공학 분야에서 어닐링 과정에서 영감을 얻은 담금질 기법 알고리즘과 유사하다고 한다. 매 반복에서 학습률을 결정하는 함수를 학습 스케줄이라고 부른다.

 

학습률이 너무 빨리 줄어들거나 천천히 줄어들면 역시 문제가 생기기에 적절한 학습률 조절이 필요하다.

 

아래 확률적 경사 하강법을 구현한 코드를 봐보자

아까전에 알아 봤듯이 학습률이 너무 낮으면 시간이 매우 오래걸리고 학습률이 너무 크면 점점 더 멀어지면서 발산하는 모습을 확인할 수 있다.

 

가운데 처럼 적절한 학습률을 찾기 위해서는 어떻게 해야할까..?

 

2장에서 알아본 것 처럼 그리드 서치를 사용해서 찾으면 된다! 하지만 그리드 서치에서 너무 오래걸리는 모델은 제외할 수 있도록 반복 횟수를 제한해야 한다.

 

반복 횟수를 아주 크게 지정하고 그레이디언트 벡터가 아주 작아질 때 거의 경사하강법이 최솟값에 도달한 것이므로 알고리즘을 중지하면 된다고 말한다.

 

n_epochs = 50
t0, t1 = 5, 50  # 학습 스케줄 하이퍼파라미터

def learning_schedule(t):
    return t0 / (t + t1)

np.random.seed(42)
theta = np.random.randn(2, 1)  # 랜덤 초기화

n_shown = 20  # 추가 코드 - 아래 그림을 생성하는 데만 필요합니다.
plt.figure(figsize=(6, 4))  # 추가 코드

for epoch in range(n_epochs):
    for iteration in range(m):

        # 추가 코드 - 이 네 라인은 그림을 생성하는 데 사용됩니다.
        if epoch == 0 and iteration < n_shown:
            y_predict = X_new_b @ theta
            color = mpl.colors.rgb2hex(plt.cm.OrRd(iteration / n_shown + 0.15))
            plt.plot(X_new, y_predict, color=color)

        random_index = np.random.randint(m)
        xi = X_b[random_index : random_index + 1]
        yi = y[random_index : random_index + 1]
        gradients = 2 * xi.T @ (xi @ theta - yi)  # SGD의 경우 m으로 나누지 않습니다
        eta = learning_schedule(epoch * m + iteration)
        theta = theta - eta * gradients
        theta_path_sgd.append(theta)  # 추가 코드 - 그림을 생성하기 위해

theta

# 추가 코드 - 이 섹션은 그림 4-10을 꾸미고 저장합니다.
plt.plot(X, y, "b.")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([0, 2, 0, 15])
plt.grid()
save_fig("sgd_plot")
plt.show()

여기서 일반적으로 한 반복에서 m번 되풀이되는데 이때 각 반복을 에포크라고 한다!

 

위의 사진을 보면 그래프는 SGD 알고리즘이 초기에는 불안정하지만 반복 학습을 통해 점차적으로 더 나은 추정치로 수렴하는 과정을 보여준다. 초기에는 여러 추정 값들이 데이터 포인트와 잘 맞지 않지만, 시간이 지남에 따라 경사가 점차 조정되어 데이터에 맞는 최종 모델에 수렴하는 모습을 확인할 수 있다!!

 

사이킷런에서 확률적 경사 하강법을 사용하려면 제곱 오차 비용 함수를 최적화하는 SGDRegressor 클래스를 사용하면 된다. 아래의 코드를 확인해 보자. 최대 1000번 동안 실행되고 100번 동안 손실이 10^-5 보다 작아지면 실행을 멈춘다. 또한 학습률은 0.01로 설정이 되어있는데 

from sklearn.linear_model import SGDRegressor

sgd_reg = SGDRegressor(max_iter=1000, tol=1e-5, penalty=None, eta0=0.01,
                       n_iter_no_change=100, random_state=42)
sgd_reg.fit(X, y.ravel())  # fit()이 1D 타깃을 기대하기 때문에 y.ravel()로 씁니다

sgd_reg.intercept_, sgd_reg.coef_

정규 방정식으로 구한 값과 비슷함을 알 수 있다.

4.2.3 미니배치 경사 하강법

미니배치 경사 하강법은 확률적 경사 하강법과 배치 경사 하강법의 중간 방식이다.

 

이는 배치 경사 하강법처럼 전체 데이터를 사용하지 않지만, 확률적 경사 하강법처럼 하나의 샘플만 사용하는 것이 아니라 여러 샘플을 한 번에 사용하는 방식으로 작은 샘플 집합(미니배치)을 사용해 각 반복마다 그레이디언트를 계산하는 방식이다!

 

장점으로는

 

  • 파라미터 공간에서 최적화 경로가 SGD보다 더 부드럽게 움직이기 때문에 더 안정적으로 최소값에 도달할 수 있게 됨.
  • 과도한 시간 소비를 방지하면서 배치 경사 하강법에 비해 더 효율적임.

단점으로는 SGD 보다는 지역 최솟값에서 빠져나오기는 더 힘들다는 점이 있고, 적절한 학습 스케줄이 필요하다는 것이다..!

 

코드가 너무 길어서 아래 주소에서 확인하세용^^

https://colab.research.google.com/github/rickiepark/handson-ml3/blob/main/04_training_linear_models.ipynb#scrollTo=z1CaEtJUYjKk

 

04_training_linear_models.ipynb

Run, share, and edit Python notebooks

colab.research.google.com

 

 

4.3 다항 회귀

다항 회귀

단순한 선형 회귀로는 복잡한 데이터 패턴을 설명할 수 없을 때는 어떻게 해야할까?? 이럴때 변수의 차수를 높여 모델링하는 방식이 다항 회귀이다.

 

$y = 0.5x^2 + x + 2 + \text{잡음}$ 인 2차방정식에 약간의 잡음을 더한 비선형 데이터를 생성하고 확인해보자

np.random.seed(42)
m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 0.5 * X ** 2 + X + 2 + np.random.randn(m, 1)

# 추가 코드 - 이 셀은 그림 4-12를 생성하고 저장합니다.
plt.figure(figsize=(6, 4))
plt.plot(X, y, "b.")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([-3, 3, 0, 10])
plt.grid()
save_fig("quadratic_data_plot")
plt.show()

그러면 위와 같은 데이터를 얻을 수 있다.

from sklearn.preprocessing import PolynomialFeatures

poly_features = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly_features.fit_transform(X)
X[0]

X_poly[0]

lin_reg = LinearRegression()
lin_reg.fit(X_poly, y)
lin_reg.intercept_, lin_reg.coef_


# 추가 코드 - 이 셀은 그림 4-13을 생성하고 저장합니다.

X_new = np.linspace(-3, 3, 100).reshape(100, 1)
X_new_poly = poly_features.transform(X_new)
y_new = lin_reg.predict(X_new_poly)

plt.figure(figsize=(6, 4))
plt.plot(X, y, "b.")
plt.plot(X_new, y_new, "r-", linewidth=2, label="Predictions")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.legend(loc="upper left")
plt.axis([-3, 3, 0, 10])
plt.grid()
save_fig("quadratic_predictions_plot")
plt.show()

$y = 0.564..x^2 + 0.9336..x + 1.78134581$의 2차 방정식을 얻었다 그래프에 그려보면

어느정도 비슷함을 알 수 있다!!

 

PolynomialFeatures를 사용할 때는 다항식의 차수가 높아질수록 특성의 개수가 기하급수적으로 늘어날 수 있다는 점을 주의해야 한다. 차수가 높아지면 많은 교차항이 포함될 수 있기 때문에 계산 비용이 증가할 수 있게 된다는 것이다.

4.4 학습 곡선

아래의 사진을 확인해보자 300차 다항 회귀 모델은 하나하나의 데이터에 맞추기 위해서 이리 갔다 저리 갔다 한다.. ㅋㅋ

 

이러한 고차 다항 회귀는 훈련 데이터에 과적합(overfitting)될 수 있다. 아니 거의 과적합일 것이다.. 반면 선형 회귀 모델은 과소적합 되어있다. 반면, 2차 다항 회귀는 훈련 데이터와 적절히 맞아떨어지며 과적합과 과소적합의 균형을 유지하는 모습을 볼 수 있다.

 

이렇게 모델이 과적합 또는 과소적합 됐는지 확인하기 위해서  2장에서는 교차검증을 사용했었다. 이번 장에서는 새로운 것으로 학습 곡선에 대해 배운다. 학습 곡선은 모델의 성능을 평가할 때 훈련 오차와 검증 오차를 보여주는 도구인데, 이를 통해 모델이 얼마나 훈련 데이터에 적합한지, 그리고 검증 데이터에 얼마나 잘 일반화되는지를 확인할 수 있다고 한다.

 

사이킷런에는 이를 위해 교차 검증을 사용하여 모델을 훈련하고 평가하는 learning_curve() 함수가 있다..! 말로만 듣지 말고 아래의 코드를 통해 직접 확인해보자

 

from sklearn.model_selection import learning_curve

train_sizes, train_scores, valid_scores = learning_curve(
    LinearRegression(), X, y, train_sizes=np.linspace(0.01, 1.0, 40), cv=5,
    scoring="neg_root_mean_squared_error")
train_errors = -train_scores.mean(axis=1)
valid_errors = -valid_scores.mean(axis=1)

plt.figure(figsize=(6, 4))  # 추가 코드
plt.plot(train_sizes, train_errors, "r-+", linewidth=2, label="train")
plt.plot(train_sizes, valid_errors, "b-", linewidth=3, label="valid")

# 추가 코드 - 그림 4-15를 꾸미고 저장합니다.
plt.xlabel("Training set size")
plt.ylabel("RMSE")
plt.grid()
plt.legend(loc="upper right")
plt.axis([0, 80, 0, 2.5])
save_fig("underfitting_learning_curves_plot")

plt.show()

과소적합의 주요 내용:

  1. 초기 상태에서 모델의 작동:
    • 그래프에서 시작할 때 훈련 세트가 매우 적으면 모델이 단순한 형태로 작동하는데, 예를 들어, 훈련 데이터가 적은 상황에서는 복잡한 패턴을 학습하지 못하고, 간단한 직선형 모델을 사용하려고 하게 된다.
    • 이런 경우, 모델은 데이터를 충분히 설명하지 못하고 과소적합됨.
  2. 샘플 추가 후 변화:
    • 훈련 샘플이 추가되면 모델이 점점 더 학습할 수 있게 되어, 초기에 비해 오차가 점차 감소하는데, 특히 검증 데이터에서 초기에 큰 오차를 보이지만, 샘플이 많아지면 학습이 진행되면서 검증 오차가 서서히 줄어들게 된다.
  3. 평형점:
    • 훈련 세트에 샘플이 많이 추가되면, 어느 시점에서 훈련 오차와 검증 오차가 더 이상 줄어들지 않고 안정화됨. 이 시점에서는 모델이 충분히 학습되었지만, 모델의 복잡도가 부족해 여전히 데이터를 충분히 설명하지 못할 수 있다.
  4. 과소적합 해결:
    • 과소적합 상태를 해결하려면 모델의 복잡도를 높이거나 더 나은 특성을 선택해야 한다. 예를 들어, 고차 다항식 모델을 사용하거나, 모델에 더 많은 특성을 추가하는 것등이 있다.

 

from sklearn.pipeline import make_pipeline

polynomial_regression = make_pipeline(
    PolynomialFeatures(degree=10, include_bias=False),
    LinearRegression())

train_sizes, train_scores, valid_scores = learning_curve(
    polynomial_regression, X, y, train_sizes=np.linspace(0.01, 1.0, 40), cv=5,
    scoring="neg_root_mean_squared_error")
    
    # 추가 코드 - 그림 4-16을 생성하고 저장합니다.

train_errors = -train_scores.mean(axis=1)
valid_errors = -valid_scores.mean(axis=1)

plt.figure(figsize=(6, 4))
plt.plot(train_sizes, train_errors, "r-+", linewidth=2, label="train")
plt.plot(train_sizes, valid_errors, "b-", linewidth=3, label="valid")
plt.legend(loc="upper right")
plt.xlabel("Training set size")
plt.ylabel("RMSE")
plt.grid()
plt.axis([0, 80, 0, 2.5])
save_fig("learning_curves_plot")
plt.show()

학습 곡선은 이전과 비슷해 이지만 가지 매우 중요한 차이점이 있는데.

  • 훈련 데이터의 오차가 이전보다 훨씬 낮다는 점!
  • 두 곡선 사이에 공간이 있는데 이 말은 검증 데이터에서보다 훈련 데이터에서 모델이 훨씬 더 나은 성능을 보인다는 뜻으로 이는 과대적합 모델의 특징이라고 한다. 만약 더 큰 훈련 세트를 사용하면 두 곡선이 점점 가까워진다고 한다

4.5 규제가 있는 선형 모델

1,2장에서 보았듯과적합을 줄이는 좋은 방법은 모델을 규제하는 것이었다. 유도를 줄이면 데이터에 과적합되기 더 어려워지는데 다항 회귀 모델에서 규제하는 간단한 방법은 다항식의 차수를 줄이는 것이다. 이러한 몇가지 방법들을 알아보자

4.5.1 릿지 회귀

릿지 회귀는 규제항 $\frac{\alpha}{m} \sum_{i=1}^{n} \theta_i^2$이 $MSE(\theta)$ 에 추가된 것을 말한다. 이 규제항은 학습 알고리즘을 데이터에 맞추는 것뿐만 아니라 모델의 가중치가 가능한 작게 유지되도록 하는 역할을 한다. 이 때 규제항은 훈련하는 동안에만 비용함수에 추가되고 모델 훈련이 끝나면 다시 MSE로 평가한다고 한다.

 

여기서 $\alpha$는 모델을 얼마나 규제할지 조절한다.  $\alpha=0$이면 선형 회귀와 같아지고  $\alpha$가 커질 수록 가중치가 작아져서 평균을 지나는 수평선이 된다고 한다. 여기서 bias $\theta_0$는 규제되지 않고 $\theta_1$부터 규제된다. 아래는 릿지 회귀의 비용 함수이다.

\[J(\theta) = MSE(\theta) + \frac{\alpha}{m} \sum_{i=1}^{n} \theta_i^2\]

 

$\alpha$의 값에 따라 작을 수록 선형 클 수록 평행에 가까워지는 모습을 확인할 수 있다. 왼쪽은 선형 회귀이고 오른쪽이 다항 회귀이다.

 

릿지 회귀를 계산하기 위해서 정규 방정식경사 하강법을 사용할 수 있다.

 

아래는 정규 방정식을 사용한 릿지 회귀 코드이다.

from sklearn.linear_model import Ridge

ridge_reg = Ridge(alpha=0.1, solver="cholesky")
ridge_reg.fit(X, y)
ridge_reg.predict([[1.5]])

아래는 확률적 경사 하강법을 사용했을 때의 코드이다.

sgd_reg = SGDRegressor(penalty="l2", alpha=0.1 / m, tol=None,
                       max_iter=1000, eta0=0.01, random_state=42)
sgd_reg.fit(X, y.ravel())  # fit()은 1D 타겟을 기대하므로 y.ravel()을 사용합니다.
sgd_reg.predict([[1.5]])

penalty는 사용할 규제를 의미한다 "l2"로 지정하면 가중치 벡터의 크기를 줄이는 방식으로 과적합을 방지하는 데 사용한다. 하지만 여기서는 $\alpha \sum_{i=1}^{n} \theta_i^2$ 이기 때문에 alpha에 1 / m 을 붙여서 $\frac{\alpha}{m} \sum_{i=1}^{n} \theta_i^2$을 만들어주자. l1은 밑에서 말하겠지만 라쏘 회귀를 의미한다.

4.5.2 라쏘 회귀

라쏘 회귀는 또 다른 규제 방법이다. 라쏘 회귀의 비용 함수는 아래와 같다.

\[J(\theta) = MSE(\theta) + 2 \alpha \sum_{i=1}^{n} \left| \theta_i \right|\]

 

릿지 회귀와 비슷하지만 라쏘 회귀는 일부 가중치 값을 0으로 만든다. 이는 자동으로 특성 선택을 수행하는 결과를 가져오며, 불필요한 특성들을 제거할 수 있게 된다!

 

아래의 코드를 확인해보자.

from sklearn.linear_model import Lasso

lasso_reg = Lasso(alpha=0.1)
lasso_reg.fit(X, y)
lasso_reg.predict([[1.5]])

# 추가 코드 - 이 셀은 그림 4-18을 생성하고 저장합니다.
plt.figure(figsize=(9, 3.5))
plt.subplot(121)
plot_model(Lasso, polynomial=False, alphas=(0, 0.1, 1), random_state=42)
plt.ylabel("$y$  ", rotation=0)
plt.subplot(122)
plot_model(Lasso, polynomial=True, alphas=(0, 1e-2, 1), random_state=42)
plt.gca().axes.yaxis.set_ticklabels([])
save_fig("lasso_regression_plot")
plt.show()

α=0.01일 때, 일부 가중치가 0이 되며, 복잡한 다항식이 단순화되는 모습을 확인할 수 있다. 이렇게 라쏘 회귀는 자동을 특성 선택을 수행하고 희소 모델을 만든다!

 

아래의 그림을 봐보자.

 

이 부분 뭔 소린지 모르겠어서 일단 GPT 말 첨부..

그림 해석

  • 왼쪽 그래프들 (L2 페널티, 릿지 회귀): 두 개의 가중치(𝜃₀, 𝜃₁)가 함께 줄어드는 경로를 나타냅니다. 최종적으로 두 가중치가 작은 값으로 수렴하지만, 0으로는 수렴하지 않습니다.
  • 오른쪽 그래프들 (L1 페널티, 라쏘 회귀): 두 개의 가중치 중 하나가 0으로 수렴하는 경로를 보여줍니다. 특성 선택 효과가 나타나서, 특정 가중치(특성)가 완전히 제거되고 다른 가중치만 남게 됩니다.

결론

  • 라쏘 회귀는 특정 가중치를 0으로 만들어 특성 선택을 수행하며, 릿지 회귀는 모든 가중치를 줄이지만, 특성을 제거하지는 않습니다.
  • 이 그래프는 두 가지 규제가 가중치에 어떻게 영향을 미치는지, 그리고 그 최적화 과정에서 라쏘는 일부 특성을 제거하고 릿지는 모든 특성을 유지하는 차이를 보여줍니다.

 

라쏘 회귀에서 사용하는 L1 규제는 가중치의 절댓값 합을 최소화하는 방식인데, 이 절댓값 함수는 0에서 미분 불가능하다. 이를 해결하기 위해 서브그래디언트를 사용하는데 서브그래디언트는 다음과 같다.

 

\[g(\theta, J) = \nabla_\theta MSE(\theta) + \alpha \cdot \begin{pmatrix}\text{sign}(\theta_1) \\\text{sign}(\theta_2) \\
\vdots \\\text{sign}(\theta_n)\end{pmatrix}\]

\[\text{여기서} \quad \text{sign}(\theta_i) =\begin{cases}-1, & \theta_i < 0 \\0, & \theta_i = 0 \\+1, & \theta_i > 0
\end{cases}\]

 

 

아래는 Lasso 클래스를 이용한 사이킷런 예제이다.

from sklearn.linear_model import Lasso

lasso_reg = Lasso(alpha=0.1)
lasso_reg.fit(X, y)
lasso_reg.predict([[1.5]])

4.5.3 엘라스틱넷

엘라스틱넷 회귀는 릿지 회귀와 라쏘 회귀를 결합한 회귀 방법이다. 릿지 회귀의 L2 규제와 라쏘 회귀의 L1 규제를 결합해, 두 규제의 장점을 동시에 취할 수 있는 방법으로, 혼한 비율 r을 사용해 조절한다. r=0이면 릿지 회귀와 같고 r=1이면 라쏘 회귀와 같다고 한다. 

 

아래는 엘라스틱넷의 비용 함수 수식이다.

 

\[
J(\theta) = MSE(\theta) + r \left( 2\alpha \sum_{i=1}^{n} |\theta_i| \right) + (1 - r) \left( \frac{\alpha}{m} \sum_{i=1}^{n} \theta_i^2 \right)
\]

 

보통 선형 회귀 모델에서는 규제가 약간 있는 것이 대부분 좋아서 평범한 선형 회귀보다는 릿지를 기본으로 하되 몇 가지 특성만 유용하다고 생각하면 라쏘 또는 엘라스틱넷을 사용하면 된다고 한다. 만약 특성 수가 훈련 샘플 수보다 많거나 특성 몇 개가 강하게 연관되어 있을때 라쏘가 보통 문제를 일으켜서 엘라스틱넷을 사용하는 것이 좋다고 책에서 말한다.

 

아래는 사이킷런 엘라스틱넷 코드이다!

from sklearn.linear_model import ElasticNet

elastic_net = ElasticNet(alpha=0.1, l1_ratio=0.5)
elastic_net.fit(X, y)
elastic_net.predict([[1.5]])

4.5.4 조기 종료

위의 세 방법과 달리 색다른 방식이 있다. 검증 오차가 최솟값에 도달하면 훈련을 중지시키는 방법인데 이것을 조기 종료라고 부른다. 

 

방법은 간단하다. 검증 오차가 최소에 도달하는 즉시 훈련을 멈추면 된다!

 

from copy import deepcopy
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler

# 추가 코드 - 이전과 동일한 2차방정식 데이터셋을 생성하고 분할합니다.
np.random.seed(42)
m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 0.5 * X ** 2 + X + 2 + np.random.randn(m, 1)
X_train, y_train = X[: m // 2], y[: m // 2, 0]
X_valid, y_valid = X[m // 2 :], y[m // 2 :, 0]

preprocessing = make_pipeline(PolynomialFeatures(degree=90, include_bias=False),
                              StandardScaler())
X_train_prep = preprocessing.fit_transform(X_train)
X_valid_prep = preprocessing.transform(X_valid)
sgd_reg = SGDRegressor(penalty=None, eta0=0.002, random_state=42)
n_epochs = 500
best_valid_rmse = float('inf')
train_errors, val_errors = [], []  # 추가 코드 - 아래 그림을 위한 것입니다.

for epoch in range(n_epochs):
    sgd_reg.partial_fit(X_train_prep, y_train)
    y_valid_predict = sgd_reg.predict(X_valid_prep)
    val_error = mean_squared_error(y_valid, y_valid_predict, squared=False)
    if val_error < best_valid_rmse:
        best_valid_rmse = val_error
        best_model = deepcopy(sgd_reg)

 

이 코드는 partial_fit을 사용하여 점진적으로 모델을 학습시키고, 각 에포크마다 검증 세트의 성능을 평가한 후 검증 오차가 개선되면 그 모델을 best_model로 저장하고, 그렇지 않으면 학습을 중지하는 코드이다.

 

4.6 로지스틱 회귀

일부 회귀 알고리즘분류에서도 사용할 수가 있다 그 중 로지스틱 회귀가 있는데 샘플이 특정 클래스에 속할 확률을 추정할 때 널리 사용된다. 추정 확률이 주어진 임곗값을 넘으면 해당 클래스에 속한다고 예측 또는 클래스에 속하지 않는다고 예측하는 등 이를 이진 분류기라고 부를 수 있다. (전에 배웠다..) 알아보도록 하자!

4.6.1 확률 추정

로지스틱 회귀는 어떻게 작동할까..? 일반 선형 회귀 모델과 같이 입력 특성의 가중치 합을 계산한다. 대신 결과를 바로 출력하는 것이 아닌 로지스틱을 출력한다.

 

# 로지스틱 회귀 모델  '$\hat{p} = h_\theta(x) = \sigma(\theta^T x)$'

 

로지스틱은 0과 1사이의 값을 출력하는 시그모이드 함수  '$\sigma(t) = \frac{1}{1 + e^{-t}}$' 인데 밑을 봐보자.

# 추가 코드 - 그림 4-21을 생성하고 저장합니다.

lim = 6
t = np.linspace(-lim, lim, 100)
sig = 1 / (1 + np.exp(-t))

plt.figure(figsize=(8, 3))
plt.plot([-lim, lim], [0, 0], "k-")
plt.plot([-lim, lim], [0.5, 0.5], "k:")
plt.plot([-lim, lim], [1, 1], "k:")
plt.plot([0, 0], [-1.1, 1.1], "k-")
plt.plot(t, sig, "b-", linewidth=2, label=r"$\sigma(t) = \dfrac{1}{1 + e^{-t}}$")
plt.xlabel("t")
plt.legend(loc="upper left")
plt.axis([-lim, lim, -0.1, 1.1])
plt.gca().set_yticks([0, 0.25, 0.5, 0.75, 1])
plt.grid()
save_fig("logistic_function_plot")
plt.show()

로지스틱 함수는 다음과 같이 생겼다. 

 

여기서 로지스틱 회귀 모델이 샘플 x가 양성 클래스에 속할 확률 '$\hat{p} = h_\theta(x)$'을 추정하면 예측을 쉽게 구할 수 있게 된다.

 

# 로지스틱 회귀의 예측 r'$ y = \begin{cases} 0 & \text{if } \hat{p} < 0.5 \\ 1 & \text{if } \hat{p} \geq 0.5 \end{cases} $'

 

양수일 때 1이라고 예측하고 음수일 때 0이라고 예측을 하게된다.

4.6.2 훈련과 비용 함수

그렇다면 로지스틱 회귀 모델을 어떻게 훈련시킬 수 있을까..? 훈련의 목적은 양성 샘플은 높은 확률을 추정하고 음성 샘플에 대해서는 낮은 확률을 추정하는 모델의 파라미터를 찾는 것이다. 

 

이를 위해 비용 함수가 정의되는데,

양성 샘플 \( (y = 1) \)에 대한 비용 함수는: \[ c(\theta) = -\log(\hat{p}), \quad y = 1일 때 \] 음성 샘플 \( (y = 0) \)에 대한 비용 함수는: \[ c(\theta) = -\log(1 - \hat{p}), \quad y = 0일 때 \]

로 정의가 된다.

 

비용 함수가 0에 가까우면 잘 예측한 것이고, 비용이 커지면 예측이 잘못되었다는 의미를 갖는다.

 

여러 샘플에 대한 비용 함수들을 평균하여 로그 손실을 계산하는데 이를 로그 손실 함수라 부르며 아래의 식과 같다.

\[ J(\theta) = -\frac{1}{m} \sum_{i=1}^{m} \left[ y^{(i)} \log(\hat{p}^{(i)}) + (1 - y^{(i)}) \log(1 - \hat{p}^{(i)}) \right] \]

 

아쉽지만 이 비용 함수의 최솟값을 계산하는 해는 아직 알려지지 않았다고 한다. (선형 회귀는 정규 방정식이 있었지만...)

그래도 이 비용 함수는 볼록 함수여서 전역 최솟값을 찾는 것을 보장한다고 한다. (볼록 함수는 그래프가 아래로 볼록한 모양을 가진 함수를 의미한다.)

 

만약 위의 로그 손실 함수를 j번째 파라미터에 대해 편미분을 하면 아래의 식이 된다.

\[ \frac{\partial}{\partial \theta_j} J(\theta) = \frac{1}{m} \sum_{i=1}^{m} \left( \sigma(\theta^\top x^{(i)}) - y^{(i)} \right) x_j^{(i)} \]

 

이 식을 편도함수라고 하는데 모델의 파라미터를 업데이트하는 데 사용하며 위의 식과 같이 각 샘플에 대해 예측 오차와 특징값을 곱하여 모델의 가중치(파라미터)를 경사 하강법 등을 통해 업데이트할 수 있다고 한다.

 

4.6.3 결정 경계

이 책은 로지스틱 회귀를 설명하기 위해 붓꽃 데이터셋을 사용해서 설명한다. (이 데이터셋은 세 개의 품종 Iris-Setosa, Iris-Versicolor, lris-Virginica에 속하는 붓꽃 150개의 꽃잎과 꽃받침의 너비와 길이를 담고 있는 데이터셋이다.)

 

꽃잎의 너비를 기반으로 Iris-Versicolor 종을 감지히는 분류기를 만들어보자. 아래의 코드는 데이터를 로드하는 과정과 살펴보는 과정이다.

from sklearn.datasets import load_iris

iris = load_iris(as_frame=True)
list(iris)



iris.data.head(3)

 

iris.target_names

아래의 코드는 데이터를 분할하고 훈련시킨 후 꽃잎의 너비가 0~3cm인 꽃에 대해 모델의 추정 확률을 계산해보는 코드이다.

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

X = iris.data[["petal width (cm)"]].values
y = iris.target_names[iris.target] == 'virginica'
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

log_reg = LogisticRegression(random_state=42)
log_reg.fit(X_train, y_train)

X_new = np.linspace(0, 3, 1000).reshape(-1, 1)  # 크기를 바꾸어 열 벡터를 얻습니다.
y_proba = log_reg.predict_proba(X_new)
decision_boundary = X_new[y_proba[:, 1] >= 0.5][0, 0]

plt.figure(figsize=(8, 3))  # 추가 코드
plt.plot(X_new, y_proba[:, 0], "b--", linewidth=2,
         label="Not Iris virginica proba")
plt.plot(X_new, y_proba[:, 1], "g-", linewidth=2, label="Iris virginica proba")
plt.plot([decision_boundary, decision_boundary], [0, 1], "k:", linewidth=2,
         label="Decision boundary")

# 추가 코드 - 이 섹션에서는 그림 4-23을 꾸미고 저장합니다.
plt.arrow(x=decision_boundary, y=0.08, dx=-0.3, dy=0,
          head_width=0.05, head_length=0.1, fc="b", ec="b")
plt.arrow(x=decision_boundary, y=0.92, dx=0.3, dy=0,
          head_width=0.05, head_length=0.1, fc="g", ec="g")
plt.plot(X_train[y_train == 0], y_train[y_train == 0], "bs")
plt.plot(X_train[y_train == 1], y_train[y_train == 1], "g^")
plt.xlabel("Petal width (cm)")
plt.ylabel("Probability")
plt.legend(loc="center left")
plt.axis([0, 3, -0.02, 1.02])
plt.grid()
save_fig("logistic_regression_plot")

plt.show()

 

삼각형으로 표시 된 꽃(Iris-Virginica)의 꽃잎 너비는 1.4~2.5cm에 분포하며 사각형으로 표시된 꽃잎의 경우는 0.1~1.8cm에 분포해서 1.4~1.8cm의 경우 중첩이 된다. 여기서 1.6cm 근방에서 양쪽의 학률이 똑같아 지는데, 꽃잎 너비가 1.6cm 보다 크면 분류기는 Iris-Virginica로 분류하고 그보다 작으면 아니라고 예측할 것이다..!!

 

아래의 그림을 봐보자.

꽃잎 너비와 꽃잎 길이라는 두 개의 특성이 있다. 훈련이 끝나면 로지스틱 회귀 분류기기- 두 특성을 기반으로 새로운 꽃이 Iris-Virginica인지 확률을 추정할 수 있는데 여기서 점선은 50% 확률을 추정하는 지점으로 경계가 선형이라는 것에 주목을 하라고 한다. (경계는 \( \theta_0 + \theta_1 \cdot x_1 + \theta_2 \cdot x_2 = 0 \)을 만족한다고 한다)

4.6.4 소프트맥스 회귀

소프트맥스 회귀는 다중 클래스 분류 문제를 해결하기 위한 확장된 형태의 로지스틱 회귀로, 로지스틱 회귀는 이진 분류에 사용되지만, 소프트맥스 회귀는 세 개 이상의 클래스에 대해 적용할 수 있다.!  

 

먼저, 각 클래스 k에 대해 소프트맥스 회귀에서 계산되는 점수는 다음과 같이 정의된다.

\[ s_k(x) = \theta_k^T x \]

 

이 점수를 바탕으로 소프트맥스 함수는 각 클래스에 대한 확률을 계산하게된다.

소프트맥스 함수는 다음과 같이 정의된다.

\[ \hat{p}_k = \frac{\exp(s_k(x))}{\sum_{j=1}^{K} \exp(s_j(x))} \]

 

그 후에 가장 높은 확률을 가진 클래스를 최종 예측값으로 선택하는 방식으로 이루어진다.

\[ \hat{y} = \arg\max_k \hat{p}_k = \arg\max_k s_k(x) \]

 

이제 훈련 방법에 대해 살펴보자. 우리는 모델이 타깃 클래스에 대해서 높은 확률을 추정하도록 만드는 것이 목적이다. 소프트맥스 회귀에서 사용하는 비용 함수는 크로스 엔트로피 비용 함수이다. 크로스 엔트로피는 추정된 클래스의 확률이 타깃 클래스에 얼마나 잘 맞는지 측정하는 용도로 사용되곤 한다고 한다. 

 

아래의 식이 크로스 엔트로피 비용 함수이다.

\[ J(\theta) = -\frac{1}{m} \sum_{i=1}^{m} \sum_{k=1}^{K} y_k^{(i)} \log(\hat{p}_k^{(i)}) \]

 

 

  • 은 전체 샘플 수
  • $K$는 클래스 수
  • $y_k^{(i)}$는 샘플 \(i\)가 클래스 \(k\)에 속하는지 여부(0 또는 1)를 나타냄.  
  • $\hat{p}_k^{(i)}$는 샘플 \(i\)가 클래스 \(k\)에 속할 확률을 나타냄.

이 비용 함수는 모델이 예측한 확률과 실제 레이블 사이의 차이를 측정하여, 이 차이를 줄이는 방향으로 학습이 진행된다.

 

모델을 학습시키기 위해 경사 하강법이 사용되는데 크로스 엔트로피 비용 함수에 대한 그래디언트 벡터는 다음과 같다.

\[ \nabla_{\theta} J(\theta) = \frac{1}{m} \sum_{i=1}^{m} \left( \hat{p}_k^{(i)} - y_k^{(i)} \right) x^{(i)} \]

 

이제 소프트맥스 회귀를 사용해 붓꽃을 세 개의 클래스로 분류해보도록 하겠다.

 

X = iris.data[["petal length (cm)", "petal width (cm)"]].values
y = iris["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

softmax_reg = LogisticRegression(C=30, random_state=42)
softmax_reg.fit(X_train, y_train)

softmax_reg.predict([[5, 2]])

 

꽃잎의 길이가 5cm 너비가 2cm인 붓꽃을 발견했다고 가정하고 붓꽃의 품종이 무엇인지 물어보면 클래스 2라고 대답을 한다.

이것의 확률을 알아보면

softmax_reg.predict_proba([[5, 2]]).round(2)

클래스 0은 0% 1은 4% 2는 96%를 갖는 것을 확인할 수 있다.

 

 

이 그림은 만들어진 결정 경계를 배경색으로 구분하여 나타내고 있는데, 클래스에 대한 학률은 곡선으로 나타난다고 한다.

 

(이게 뭔소린가 싶어서 gpt에 물어봤다)

 

그래프에서 "곡선"으로 보이는 부분은 결정 경계가 아니라, 클래스에 속할 확률이 같은 지점들을 나타낸 것입니다. 즉, 특정 클래스에 속할 확률이 30%, 75%, 90% 같은 확률 수준을 시각화한 곡선들입니다. 이 곡선은 특정 클래스에 속할 확률이 동일한 점들을 연결한 선입니다.

  • 예를 들어, "0.30 곡선"은 입력 값이 Iris-Versicolor에 속할 확률이 30%인 점들을 연결한 곡선입니다.
  • "0.75 곡선"은 확률이 75%인 점들을 나타냅니다.

이 확률 곡선은 각 클래스에 대한 확률 분포를 나타내며, 비선형적으로 보이는 이유는 소프트맥스 함수가 비선형 함수이기 때문입니다. 확률 계산은 지수 함수와 같이 비선형 변환을 거치기 때문에, 확률이 특정 값으로 동일해지는 지점들이 곡선 형태로 나타날 수 있습니다.

요약:

  • 결정 경계선형이 맞습니다. 이는 두 클래스 간에 예측 확률이 동일해지는 직선입니다.
  • 그러나 클래스에 대한 확률을 나타낸 곡선은, 각 클래스에 속할 확률이 같은 지점들을 연결한 선으로, 이는 비선형 함수(소프트맥스 함수)에 의해 만들어진 곡선입니다.

그래서 결정 경계는 선형이지만, 특정 클래스에 속할 확률 분포는 곡선 형태로 나타날 수 있는 것입니다.

 

 

 

아아아 끝났다... 이해가 가는 것도 있고 안가는 것도 있는데 너무 급하게 하지말고 머리 식히면 다시 차근차근 해보자..