머신러닝 교과서_파이토치편

[머신러닝 교과서] 2. 분류를 위한 머신러닝 기법들 - 1 (Logistic Regression, SVM)

cheorish 2024. 12. 26. 00:08

 

 

 

 

시작하면서

우선 교본 안에 서술된 여러 머신러닝 기법들 중에는 아달린 알고리즘(경사하강법을 이용한)이 존재하나, 이미 이전에 서술했던 내용이기에 생략하고 로지스틱 회귀분석 - 결정 트리까지 정리하겠다.

로지스틱 회귀분석

  • 로지스틱 회귀분석을 통한 클래스 확률 모델링

기존 퍼셉트론을 통한 훈련은 분류 알고리즘에 적합하지만 동시에 선형적으로 구분되지 않을 때, 학습 수렴을 할 수 없는 단점이 있음(XOR 게이트), 그렇기에 간단한 이진분류를 더욱 더 강력하게 학습이 가능한 로지스틱 회귀에 대해서 정리하겠다.

1. 정의

  • 이진분류 문제를 해결하기 위한 통계적 모델 중 하나
    • 두 가지 범주(\(Yes\) \(Or\) \(No\)) 중 하나로 분류
    • 예) : 스팸 분류, 환자 질병 예측 등에 사용

2. 로지스틱 회귀의 핵심

2-1. 선형모델

  • 선형회귀와 비슷하게, 입력 특성(\(X\))과 가중치(\(w\)), 편향(\(b\))의 선형 조합을 수행함.
    • 수식
      • \(z\) = \(w_1 x_1\) + \(w_2 x_2 \)+ \(\cdots + w_n x_n + b\)
        • \(x_i\): 입력 변수 • \(b\) : 편향(Bias)
        • \(w_i\): 각 입력 변수에 대한 가중치

2-2. 로짓(Logit) 함수

  • 로짓 함수는 선형 모델의 출력을 오즈비(Odds Ratio)로 변환함
    • 수식
      • \(\log\left(\frac{P(y=1)}{1 - P(y=1)}\right)\) = \(w_1 x_1 + w_2 x_2\) + \(\cdots + w_n x_n + b\)

2-3. 시그모이드 함수

  • 로짓 값을 확률 값으로 변환(0과 1사이의 실수 값으로 수렴한다는 것)
    • 수식
    • \(P(y=1|x)\) = \(\sigma(z)\) = \(\frac{1}{1 + e^{-z}}\)

$$
\hat{y} =
\begin{cases}
1, & \text{if } P(y=1|x) = \sigma(z) \geq 0.5 \\
0, & \text{그 외}
\end{cases}
$$

  • 각 값 설명
    1. \(\hat{y}\): 예측된 클래스 레이블 (0 또는 1)
    2. \(P(y=1|x)\): 입력 데이터 \(x\)가 클래스 1에 속할 확률
    3. \(\sigma(z)\): 시그모이드 함수의 출력값 \((0 \leq \sigma(z) \leq 1)\)
    4. 0.5: 임계값(Threshold)
      • 확률이 0.5 이상이면 1로 분류
      • 확률이 0.5 미만이면 0으로 분류

 


 

여기서 궁금증

🚀 오즈비란?

3. 오즈(Odds)

  • 사건이 발생할 확률과 발생하지 않을 확률의 비율(베르누이 분포와 같이 0과 1로 수렴하는 결과를 찾는 것이라고 보면 된다 홀짝..
    • 수식
    • \(\text{Odds}\) = \(\frac{P(y=1)}{P(y=0)}\) = \(\frac{P(y=1)}{1 - P(y=1)}\)
  • 예) 환자가 질병에 걸릴 확률이 0.8%라면
    • 수식
    • \(\text{Odds}\) = \(\frac{0.8}{0.2} = 4\)
    • 식 그대로 걸릴 확률이 걸리지 않을 확률보다 4배가 높다는 이야기로 설명이 가능(특정 조건을 걸어야 더 이해가 쉽습니다.. 담배를 피는 사람들을 실험 조건으로)

3-1. 오즈비(Odds Ratio)

  • 두 확률의 비율을 나타내며, 계수 w에 따라 해석됨:

\(OR = e^{w}\)

  • \(OR > 1\): 해당 특성이 사건 발생 가능성을 높임
  • \(OR < 1\): 해당 특성이 사건 발생 가능성을 낮춤

출처 : https://thebook.io/080311/

 

아달린과 로지스틱 회귀를 비교하였을 때 큰 흐름 자체는 비슷하나, 아달린 알고리즘은 0~1 사이의 실수 값, 로지스틱 회귀 자체는 확률 값으로 나뉜다는 것을 알 수 있다.

🎯 차이점을 요약하자면 ...

구분 ADALINE 로지스틱 회귀
활성화 함수 선형 활성화 함수 시그모이드 활성화 함수
출력 값 실수 값 (-∞ ~ +∞) 확률 값 (0 ~ 1)
임계값 0을 기준으로 이진 분류 0.5를 기준으로 이진 분류
해석 수치적 해석만 가능 확률적 해석 가능
손실 함수 평균 제곱 오차 (MSE) 교차 엔트로피 손실 (CE Loss)
결과 예시 \( y = 1.5 \) → 1 \( P(y=1) = 0.8 \) → 1

💻 예제 코드

import numpy as np

class LogisticRegression:
    """
    로지스틱 회귀 분류기 (Logistic Regression Classifier)

    Parameters:
    ----------
    eta : float
        학습률 (Learning Rate) - 각 반복에서 가중치를 얼마나 조정할지 결정합니다.
    n_iter : int
        반복 횟수 (Number of Iterations) - 경사 하강법을 몇 번 수행할지 설정합니다.
    random_state : int
        난수 시드 (Random State) - 가중치 초기화의 재현성을 보장합니다.

    Attributes:
    ----------
    w_ : 1d-array
        학습된 가중치 벡터 (Weight Vector)
    b_ : float
        학습된 절편 (Bias)
    losses_ : list
        각 반복(epoch)마다의 손실 함수 값 (Loss Function Value)
    """

    def __init__(self, eta=0.01, n_iter=1000, random_state=1):
        self.eta = eta  # 학습률 (Learning Rate)
        self.n_iter = n_iter  # 반복 횟수 (Epochs)
        self.random_state = random_state  # 난수 시드 (Random State)
        self.w_ = None  # 가중치 초기화
        self.b_ = None  # 절편 초기화
        self.losses_ = []  # 손실값 기록

    def sigmoid(self, z):
        """
        시그모이드 함수 (Sigmoid Function)
        z: 선형 결합 값 (Linear Combination)
        """
        return 1 / (1 + np.exp(-z))

    def loss(self, y_true, y_pred):
        """
        교차 엔트로피 손실 함수 (Cross-Entropy Loss Function)
        """
        return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))

    def fit(self, X, y):
        """
        로지스틱 회귀 모델 학습

        Parameters:
        ----------
        X : ndarray
            입력 데이터 (Feature Matrix)
        y : ndarray
            실제 레이블 (Target Labels)
        """
        np.random.seed(self.random_state)
        n_samples, n_features = X.shape

        # 가중치와 절편 초기화
        self.w_ = np.random.randn(n_features)
        self.b_ = 0.0

        # 경사 하강법(Gradient Descent) 학습
        for _ in range(self.n_iter):
            linear_output = np.dot(X, self.w_) + self.b_
            y_pred = self.sigmoid(linear_output)

            # 손실 함수 계산
            loss = self.loss(y, y_pred)
            self.losses_.append(loss)

            # 기울기 계산 (Gradient)
            dw = (1 / n_samples) * np.dot(X.T, (y_pred - y))
            db = (1 / n_samples) * np.sum(y_pred - y)

            # 가중치와 절편 업데이트
            self.w_ -= self.eta * dw
            self.b_ -= self.eta * db

    def predict_proba(self, X):
        """
        예측 확률 반환

        Parameters:
        ----------
        X : ndarray
            입력 데이터 (Feature Matrix)

        Returns:
        --------
        ndarray
            클래스 1에 대한 확률
        """
        linear_output = np.dot(X, self.w_) + self.b_
        return self.sigmoid(linear_output)

    def predict(self, X):
        """
        클래스 레이블 예측 (0 또는 1)

        Parameters:
        ----------
        X : ndarray
            입력 데이터 (Feature Matrix)

        Returns:
        --------
        ndarray
            예측된 클래스 레이블 (0 또는 1)
        """
        probabilities = self.predict_proba(X)
        return np.where(probabilities >= 0.5, 1, 0)

다음시간에는 규제와 관련된 옵티마이제이션에 대해서 정리할 예정 ... (교본에는 너무 내용이 방대해서 일단은 분류기까지만)

서포트벡터 머신

What is SVM(Support Vector Machine)?

  • 서포트벡터 머신이란?
    • 분류와 회귀 두 가지 문제를 모두 해결할 수 있는 지도학습 알고리즘
    • 보통 이진분류 문제에서 주로 사용하며, 클래스별 결정경계를 통해 데이터 포인트를 분리하는 것이 특징
  • 결정경계?? 그게 뭔가요?
    • 예를 들어서 질병의 양/음성 유무를 판단하기 위한 분류기를 만든다고 가정할 때
    •  

출처 : https://thebook.io/080311/

  • 클래스 A(음성): ○
  • ○ ○ ○ | × × × (결정경계: 직선)
  • 클래스 B(양성): ×
    • 여기서 알아둬야 할 표를 보며 이해할 내용들
      • 마진 : 결정경계와 초평면 사이의 간격
      • 초평면 : 클래스를 구분하는 핵심 요소 중 하나(결정 경계를 통해 나누어진 공간을 보고 클래스별~ 초평면이라고 한다

그렇게 SVM을 통해 마진을 최대화시키는 것이 목적(초평면에 가까운 훈련 샘플을 사이의 거리로 정의 → 이러한 샘플을 보고 서포트 벡터라고 한다.)


🚀 슬랙 변수를 통한 비선형 분류 문제 해결

  • 실제 데이터는 노이즈 및 선형으로 분리되지 않는 경우가 발생한다(XOR게이트와 같은... 곧 XOR 게이트와 비선형에 대한 내용도 정리 예정)
  • 슬랙 변수를 통해 마진을 최대화 하려함

 

 

    • 슬랙 변수(ξ, Xi): 초평면으로부터 벗어난 데이터 포인트의 오차를 측정하는 값
    • 수식:
      • \(y_i (w^T x_i + b) \geq 1\) - \(\xi_i, \quad \xi_i \geq 0\)
        • \(y_i\): 실제 레이블 (+1 또는 -1)
        • \(w^T x_i + b\): 초평면과의 거리
        • \(\xi_i\): 슬랙 변수 (데이터 포인트가 결정 경계를 얼마나 위반했는지 나타냄)

슬랙 변수의 역할

  1. 오분류 허용: 완벽한 선형 분리가 불가능한 상황에서 일부 오분류를 허용
  2. 하이퍼파라미터 C: 슬랙 변수를 제어하기 위해 비용(패널티) 파라미터 C가 사용
    • C가 크면: 오분류를 거의 허용하지 않고 마진이 좁아짐
    • C가 작으면: 오분류를 더 많이 허용하며 마진이 넓어짐

슬랙 변수를 포함한 SVM의 최적화 문제:

\[
\min_{w, b, \xi} \frac{1}{2} ||w||^2 + C \sum_{i=1}^n \xi_i
\]

 

  • 제약 조건:
  • \(y_i (w^T x_i + b) \geq 1\) - \(\xi_i, \quad \xi_i \geq 0\)

설명:

  • 1차항: 마진을 최대화
  • 2차항: 슬랙 변수의 합 (\xi_i)을 최소화하여 오분류를 줄임
  • C: 두 항목 간의 균형을 조정

슬랙변수를 통해 나누어지는 마진을 구분하는 방법

  • 하드 마진(Hard Margin): 완벽하게 선형으로 분리 가능한 경우
  • 소프트 마진(Soft Margin): 선형으로 완벽하게 분리할 수 없어서 약간의 오분류를 허용하는 경우

출처 : https://eehoeskrap.tistory.com/656

 

그렇지만, 실무 데이터를 슬랙 변수를 통해 구분하는 것은 비용이 많이 발생할 우려가 있어 대책안이 필요했다

 

🎯 커널 (Kernel)로 비선형 문제 해결

  • 슬랙 변수를 통해 해결하기 어려운(비용이 많이 발생하는 부분) 상황을 고려하여 고차원으로 매핑시킨 후 비선형 문제를 해결하기 위해 등장
  • 두 개념은 서로 독립적이지 않고 함께 사용이 가능하다.

출처 : https://thebook.io/080311/

 

보기와 같이 비선형 구조는 아예 차원을 바꿔서 분류시킨다(고차원 공간에 투영)

 


주요 커널 함수

      • 선형 커널 (Linear Kernel): \(K(x_i, x_j) = x_i^T x_j\)
      • 다항식 커널 (Polynomial Kernel): \(K(x_i, x_j) = (x_i^T x_j + 1)^d\)  
      • 가우시안 RBF 커널 (RBF Kernel): \(K(x_i, x_j) = \exp(-\gamma ||x_i - x_j||^2)\)

각 함수별 기능 및 장단점

    • 선형 커널 (Linear Kernel): \(K(x_i, x_j) = x_i^T x_j\)
      • 데이터를 고차원으로 매핑하지 않고 그대로 사용
      • 선형적으로 분리 가능한 문제에 적합
      • 계산 비용이 적고, 빠르게 학습됨
      • 데이터가 선형적으로 잘 분리될 때, 특성의 수가 샘플 수보다 많을 때 사용, 보통 이진 분류 문제에 사용 
    • 다항식 커널 (Polynomial Kernel): \(K(x_i, x_j) = (x_i^T x_j + 1)^d\)  
      • 비선형 경계를 다룰 수 있음
      • 차수(d)가 높을수록 복잡한 패턴을 학습할 수 있지만, 과적합 가능성이 있음
      • 저차원 데이터에서 효과적, 보통은 간단한 곡선 경계 분류 문제 
    • 가우시안 RBF 커널 (RBF Kernel): \(K(x_i, x_j) = \exp(-\gamma ||x_i - x_j||^2)\)
      • 가장 많이 사용되는 커널
      • 다양한 형태의 비선형 경계를 학습할 수 있음
      • \(\gamma\) 값이 클수록 결정 경계가 복잡해지고, 작을수록 단순해짐
      • 데이터를 무한 차원 공간으로 매핑

 

💻 예제 코드

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 데이터셋 생성 (2차원 이진 분류 문제)
X, y = datasets.make_classification(n_samples=200, 
                                    n_features=2, 
                                    n_redundant=0, 
                                    n_informative=2, 
                                    random_state=42, 
                                    n_clusters_per_class=1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# 시각화를 위한 함수
def plot_decision_boundary(X, y, model, title):
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
                         np.linspace(y_min, y_max, 100))
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    plt.contourf(xx, yy, Z, alpha=0.3)
    plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k')
    plt.title(title)
    plt.show()

# 1️⃣ C 값 비교 (선형 커널 사용)
for C_value in [0.1, 1, 10]:
    model = SVC(kernel='linear', C=C_value)
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    acc = accuracy_score(y_test, y_pred)
    print(f"Linear Kernel with C={C_value}, Accuracy: {acc:.2f}")
    plot_decision_boundary(X, y, model, f"Linear Kernel (C={C_value})")

# 2️⃣ 커널 비교 (C 값 고정)
for kernel in ['linear', 'poly', 'rbf', 'sigmoid']:
    model = SVC(kernel=kernel, C=1.0)
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    acc = accuracy_score(y_test, y_pred)
    print(f"{kernel.capitalize()} Kernel, Accuracy: {acc:.2f}")
    plot_decision_boundary(X, y, model, f"{kernel.capitalize()} Kernel (C=1.0)")