머신 러닝 교과서/ 사이킷런을 타고 떠나는 머신 러닝 분류 모델 투어

분류 알고리즘 선택

  • 모든 경우에 뛰어난 성능을 낼 수 있는 분류 모델은 없다. 실제로 최소한 몇 개의 학습 알고리즘 성능을 비교하고 해당 문제에 최선인 모델을 선택하는 것이 항상 권장된다.
  • 머신 러닝 알고리즘을 훈련하기 위한 다섯 가지 주요 단계는 다음과 같다.
    • 특성을 선택하고 훈련 샘플을 모은다.
    • 성능 지표를 선택한다.
    • 분류 모델과 최적화 알고리즘을 선택한다.
    • 모델의 성능을 평가한다.
    • 알고리즘을 튜닝한다.

사이킷런 첫걸음: 퍼셉트론 훈련

  • 2장에서 했던 것을 쉬운 인터페이스로 분류 알고리즘을 최적화하여 구현한 사이킷런 API를 사용해 보겠다.
import numpy as np
from sklearn import datasets

iris = datasets.load_iris()
X = iris.data[:, [2, 3]]
y = iris.target

print('클래스 레이블:', np.unique(y))

--
클래스 레이블: [0 1 2]
  • np.unique(y) 함수는 iris.target에 저장된 세 개의 고유한 클래스 레이블을 반환한다.
    • 결과는 0 1 2가 나오는데 이는 붓꽃 클래스 이름인 Iris-setosa, Iris-versicolor, Iris-virginica의 정수 형태
    • 사이킷런의 많은 함수와 클래스 메서드는 문자열 형태의 클래스 레이블을 다룰 수 있지만, 정수 레이블을 권장한다.
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)

print('y의 레이블 카운트:', np.bincount(y))
print('y_train의 레이블 카운트:', np.bincount(y_train))
print('y_test의 레이블 카운트:', np.bincount(y_test))

---
y의 레이블 카운트: [50 50 50]
y_train의 레이블 카운트: [35 35 35]
y_test의 레이블 카운트: [15 15 15]
  • 훈련된 모델 성능을 평가하기 위해 데이터셋을 훈련 세트와 테스트 세트로 분할 한다. 사이킷런 model_selection 모듈의 train_test_split 함수를 이용하면 X와 y 배열을 나눌 수 있다.
    • 30%는 테스트 데이터, 70%는 훈련 데이터가 된다.
  • 데이터를 분할 하기 전에 데이터셋을 섞는 것이 좋으므로 유사 난수 생성기에 random_state 매개변수로 고정된 랜덤시드(random_state=1)을 전달한다.
  • 마지막으로 stratify=y를 통해 계층화(stratification) 기능을 사용하는데, 여기서 계층화는 train_test_split 함수가 훈련 세트와 테스트 세트의 클래스 레이블 비율을 입력 데이터셋과 동일하게 만든다는 의미이다.
from sklearn.preprocessing import StandardScaler

sc = StandardScaler()
sc.fit(X_train)

X_train_std = sc.transform(X_train)
X_test_std = sc.transform(X_test)
  • 앞장의 경사 하강법 예제에서 보았던 것처럼 머신러닝 알고리즘과 최적화 알고리즘은 성능을 위해 특성 스케일 조정이 필요하다. 여기서는 사이킷런의 preprocessing 모듈의 StandardScaler 클래스를 사용해서 특성을 표준화 했다.
  • 위 코드에서 StandardScaler의 fit 메서드는 훈련 세트의 각 특성 차원마다 \mu (샘플 평균)와 \sigma (표준 편차)를 계산한다.
    • transform 메서드를 실행하면 계산된 \mu \sigma 를 사용하여 훈련세트를 표준화 한다.
    • 훈련 세트와 테스트 샘플이 서로 같은 비율로 이동되도록 동 일한 \mu \sigma 를 사용하여 테스트 세트를 표준화 한다.
from sklearn.linear_model import Perceptron

ppn = Perceptron(max_iter=40, eta0=0.1, tol=1e-3, random_state=1)
ppn.fit(X_train_std, y_train)

y_pred = ppn.predict(X_test_std)

print('잘못 분류된 샘플 개수: %d' % (y_test != y_pred).sum())

---
잘못 분류된 샘플 개수: 14
  • 훈련 데이터를 표준화한 후 퍼셉트론 모델을 훈련한다. 사이킷런의 알고리즘은 대부분 기본적으로 OvR(One-versus-Rest) 방식을 사용하여 다중 분류(multiclass classification)를 지원한다.
  • 사이킷런의 인터페이스는 2장에서 구현한 퍼셉트론과 미슷하다. linear_model 모듈에서 Perceptron 클래스를 로드하여 새로운 객체를 생성한 후 fit 메서드를 사용하여 모델을 훈련한다.
    • eta0은 학슙릴이고, max_iter는 훈련 세트를 반복할 에포크 횟수를 말한다.
  • 2장에서 했던 것처럼 적절한 학습률을 찾으려면 어느 정도 실험이 필요하다.
    • 학습률이 너무 크면 알고리즘은 전역 최솟값을 지나치고, 너무 작으면 학습 속도가 느리기 때문에 대규모 데이터셋에서 수렴하기까지 많은 에포크가 필요하다.
  • 사이킷런 라이브러리는 metrics 모듈 아래에 다양한 성능 지표를 구현해 놓았는데, 예는 아래와 같다.
from sklearn.metrics import accuracy_score

print('정확도: %.2f' % accuracy_score(y_test, y_pred))
print('정확도: %.2f' % ppn.score(X_test_std, y_test))

---
정확도: 0.69
정확도: 0.69
  • 마지막으로 2장에서 사용했던 plot_decision_regions 함수를 아래와 같이 수정한다.
import numpy as np
from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt

def plot_decision_regions(X, y, classifier, test_idx=None, resolution=0.02):
   # 마커와 컬러맵을 설정
   markers = ('s', 'x', 'o', '^', 'v')
   colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    cmap = ListedColormap(colors[:len(np.unique(y))])

    # 결정 경계를 그린다.
   x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
   x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution), np.arange(x2_min, x2_max, resolution))
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
   Z = Z.reshape(xx1.shape)
   plt.contourf(xx1, xx2, Z, alpha=0.3, cmap=cmap)
    plt.xlim(xx1.min(), xx1.max())
    plt.xlim(xx2.min(), xx2.max())

   for idx, cl in enumerate(np.unique(y)):
       plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1], alpha=0.8, c=colors[idx], marker=markers[idx], label=cl, edgecolor='black')   

   if test_idx:
       X_test, y_test = X[test_idx, :], y[test_idx]
       plt.scatter(X_test[:, 0], X_test[:, 1], c='', edgecolor='black', alpha=1.0, linewidth=1, marker='o', s=100, label='test set')
  • 수정된 plot_decision_regions 함수를 이용하여 그래프를 그리면 아래 그림과 같다.
plot_decision_regions(X=X_combined_std, y=y_combined, classifier=ppn, test_idx=range(105, 150))

plt.xlabel('petal length [standardized]')
plt.ylabel('petal width [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

로지스틱 회귀를 사용한 클래스 확률 모델링

  • 퍼셉트론 규칙은 간단하고 좋은 모델이지만 가장 큰 단점은 클래스가 선형적으로 구분되지 않을 때 수렴할 수 없다는 점이다.
  • 선형 이진 분류 문제에는 로지스틱 회귀(logistic regression)이 더 강력한 알고리즘이다.
    • 이름이 회귀지만 로지스틱 회귀는 분류 모델이다.

로지스틱 회귀의 이해와 조건부 확률

  • 로지스틱 회귀는 구현하기 쉽고 선형적으로 구분되는 클래스에 뛰어난 성능을 내는 분류 모델로 산업계에서 가장 널리 사용되는 알고리즘 중 하나이다.
    • 로지스틱 회귀모델도 퍼셉트론이나 아달리과 마찬가지로 이진 분류를 위한 선형 모델이지만 다중 분류로 확장할 수 있다. 예컨대 OvR 방식을 사용한다.
  • 확률 모델로서 로지스틱 회귀 모델 이면에 있는 아이디어를 설명하기 위해 먼저 오즈비(odds ratio)를 설명한다.
    • 오즈는 특정 이벤트가 발생할 확률로 오즈비는 {P \over 1 - P } 처럼 쓸 수 있다.
    • 여기서 P 는 양성 샘플일 확률로서 양성 샘플은 예측하려는 대상을 말한다.
  • 오즈비에 로그 함수(로그 오즈)를 취해 로짓(logit) 함수를 정의한다.

logit(P) = \log {P \over 1 - P}

  • 여기서 \log 자연로그이다. logit 함수는 0과 1사이의 입력 값을 받아 실수 범위 값으로 변환한다.
    • 특성의 가중치 합과 로그 오즈 사이의 선형 관계를 다음과 같이 쓸 수 있다.

logit(P(y=1|x)) = w_{0} x_{0} + w_{1} x_{1} + ... + w_{m} x_{m} \\ = \sum_{i=0}^{m} w_{i} x_{i} = w^{T} x

  • 여기서 P(y=1|x) 는 특성 x 가 주어졌을 때 이 샘플이 클래스 1에 속할 조건부 확률이다.
  • 어떤 샘플이 특정 클래스에 속할 확률을 예측하는 것이 관심 대상이므로 logit 함수를 거꾸로 뒤집는데, 이 함수를 로지스틱 시그모이드 함수(logistic sigmoid function)라고 한다.
    • 함수 모양이 S자 형태를 띠기 때문에 간단하게 시그모이드 함수(sigmoid function) 라고도 한다.

\phi(z) = {1 \over 1 + e^{-z}}

  • 여기서 z 는 가중치와 샘플 특성의 선형 조합으로 이루어진 최종 입력이다. 즉 z = w^{T} x 이다.
  • 시그모이드 함수를 그리면 아래와 같다.

  • z 가 무한대로 가면 (z \to \infty ) e^{-z} 가 매우 작아지기 때문에 \phi(z) 는 1에 가까워진다.
    • 비슷하게 z \to -\infty 이면 분모가 점점 커지기 때문에 \phi(z) 는 0에 수렴한다.
    • 이 시그모이드 함수는 실수 입력 값을 [0, 1] 사이의 값으로 변환한다. 중간은 \phi(0) = 0.5 이다.
  • 아달린에서 활성화 함수로 항등 함수 \phi(z) = z 를 사용했는데, 로지스틱 회귀에서는 시그모이드 함수가 활성화 함수가 된다.
    • 아달린과 로지스틱 회귀의 차이는 아래 그림과 같다.

  • 가중치 w 와 곱해지는 특성 x 에 대한 시그모이드 함수의 출력을 특정 샘플이 클래스 1에 속할 확률 \phi(z) = P(y=1|x; w) 로 해석한다.
    • 예컨대 어떤 붓꽃 샘플이 \phi(z) = 0.8 이라면 이 샘플은 Iris-versicolor일 확률이 80%라는 뜻이 된다.
    • 이 예측 확률은 임계 함수를 사용하여 간단하게 이진 출력으로 바꿀 수 있다.

\hat{y} = \begin{cases} 1 & \phi(z) \geq 0.5 \\ 0 & else \end{cases}

  • 앞의 시그모이드 함수 그래프를 보면 다음과 동일하다는 것을 알 수 있다.

\hat{y} = \begin{cases} 1 & z \geq 0.0 \\ 0 & else \end{cases}

  • 실제로 클래스 레이블을 예측하는 것 외에 클래스에 속할 확률(임계 함수를 적용하기 전 시그모이드 함수 출력)을 추정하는 거싱 유용한 애플리케이션도 많다.
    • 예컨대 비가 오는지 예측하는 것 뿐만 아니라 비 올 확률을 예측해야 하는 경우에 로지스틱 회귀를 사용할 수 있다.
    • 비슷하게 어떤 증상이 있는 환자가 특정 질병을 가질 확률을 예측하는데 로지스틱 회귀를 사용할 수 있다.

로지스틱 비용 함수의 가중치 학습

  • 이전 장에서 다음과 같은 제곱 오차합 비용 함수를 정의했었다.

J(w) = \sum_{i} {1 \over 2} (\phi(z^{(i)}) - y^{(i)})^{2}

  • 아달린 분류 모델에서 이 함수를 최소화하는 가중치 w 를 학습한다. 로지스틱 회귀의 비용 함수를 유도하는 방법을 설명하기 위해 먼저 로지스틱 회귀 모델을 만들 때 최대화 하려는 가능도(likehood) L 을 정의하겠다.
    • 데이터셋에 있는 각 샘플이 서로 독립적이라고 가정한다. 공식은 다음과 같다.

L(w) = P(y|x;w) = \Pi_{i = 1}^{n} P(y^{(i)} | x^{(i)}; w) \\ = \Pi_{i=1}^{n}(\phi(z^{(i)}))^{y^{(i)}} (1 - \phi(z^{(i)}))^{1 - y^{(i)}}

  • 실전에서는 이 공식의 (자연)로그를 최대화하는 것이 더 쉽다. 이 함수를 고르 가능도 함수라고 한다.

l(w) = \log L(w) \\= \sum_{i=1}^{n} [y^{(i)} \log(\phi(z^{(i)})) + (1 - y^{(i)}) \log (1 - \phi(z^{(i)}))]

  • 첫째, 로그 함수를 적용하면 가능도가 매우 작을 때 일어나는 수치상의 언더플로(underflow)를 미연에 방지한다.
  • 둘째, 계수의 곱을 계수의 합으로 바꿀 수 있다. 이렇게 하면 도함수를 구하기 쉽다.
  • 경사 상승법 같은 최적화 알고리즘을 사용하여 이 로그 가능도 함수를 최대화 할 수 있다.
    • 또는 로그 가능도 함수를 다시 비용 함수 J로 표현하여 경사 하강법을 사용하여 최소화 할 수 있다.

J(w) = \sum_{i=1}^{n} [-y^{(i)} \log(\phi(z^{(i)})) - (1 - y^{(i)}) \log (1 - \phi(z^{(i)}))]

  • 이 비용 함수를 더 잘 이해하기 위해 샘플이 하나일 때 비용을 계산해 보자

J(\phi(z), y; w) = -y \log(\phi(z)) - (1 - y) \log (1 - \phi(z))]

  • 이 식을 보면 y = 0 일 때 첫 번째 항이 0 이 되고  y = 1 일 때 두 번째 항이 0 이 된다.

J(\phi(z), y; w) = \begin{cases} - \log(\phi(z)) & y = 1 \\ - \log(1 - \phi(z)) & y = 0 \end{cases}

  • 클래스 1에 속한 샘플을 정확히 예측하면 비용이 0에 가까워지는 것을 볼 수 있다(실선)
    • 비슷하게 클래스 0에 속한 샘플을 y = 0 으로 정확히 예측하면 y축의 비용이 0에 가까워진다(점선)
    • 예측이 잘못되면 비용이 무한대가 된다.

아달린 구현을 로지스틱 회귀 알고리즘으로 변경

  • 로지스틱 회귀를 구현하려면 2장의 아달린 구현에서 비용 함수 J 를 새로운 비용함수로 바꾸기만 하면 된다.

J(w) = \sum_{i=1}^{n} [y^{(i)} \log(\phi(z^{(i)})) + (1 - y^{(i)}) \log (1 - \phi(z^{(i)}))]

  • 이 함수로 에포크마다 모든 훈련 샘플을 분류하는 비용을 계산한다.
    • 선형 활성화 함수를 시그모이드 활성화로 바꾸고 임계 함수가 클래스 레이블 -1과 1이 아니고 0과 1을 반환하도록 변경한다.
    • 아달린 코드에 이 세가지를 반영하면 로지스틱 회귀 모델을 얻을 수 있다.
import numpy as np

class LogisticRegressionGD(object):
   """ 경사 하강법을 사용한 로지스틱 회귀 분류기
    매개변수
   ----------
   eta : float
       학습률 (0.0과 1.0사이)
    n_iter : int
       훈련 데이터셋 반복 횟수
    random_state : int
        가중치 무작위 초기화를 위한 난수 생성기 시드

    속성
   ---------
   w_ : 1d-array
       학습된 가중치
    errors_ : list
       에포크마다 누적된 분류 오류
    """

    def __init__(self, eta = 0.05, n_iter = 100, random_state = 1):
       self.eta = eta
       self.n_iter = n_iter
        self.random_state= random_state

    def fit(self, X, y):
       """ 훈련 데이터 학습
        매개변수
       ----------
       X : {array-like}, shape = [n_samples, n_features]
            n_samples개의 샘플과 n_features개의 특성으로 이루어진 훈련 데이터
       y : array-like, shape = [n_samples]
           타겟 값       

        반환값
       ---------
        self : object

        """
       rgen = np.random.RandomState(self.random_state)
        self.w_ = rgen.normal(loc = 0.0, scale = 0.01, size = 1 + X.shape[1])
        self.cost_ = []

       for i in range(self.n_iter):
           net_input = self.net_input(X)
           output = self.activation(net_input)
            errors = (y - output)
           self.w_[1:] += self.eta * X.T.dot(errors)
           self.w_[0] += self.eta * errors.sum()       

        # 오차 제곱합 대신 로지스틱 비용을 계산한다.
        cost = (-y.dot(np.log(output)) - ((1-y).dot(np.log(1-output))))
       self.cost_.append(cost)

        return self

   def net_input(self, X):
       """ 최종 입력 계산 """
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def activation(self, z):
       """ 로지스틱 시그모이드 활성화 계산 """       
        return 1. / (1. + np.exp(-np.clip(z, -250, 250)))

    def predict(self, X):
       """ 단위 계단 함수를 사용하여 클래스 레이블을 반환합니다 """
        return np.where(self.net_input(X) >= 0.0, 1, 0)
  • 다음 코드로 로지스틱 모델을 실행해 보면 아래 그림과 같은 결과를 얻을 수 있다.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)

X_train_01_subset = X_train[(y_train == 0) | (y_train == 1)]
y_train_01_subset = y_train[(y_train == 0) | (y_train == 1)]

lrgd = LogisticRegressionGD(eta=0.05, n_iter=1000, random_state=1)
lrgd.fit(X_train_01_subset, y_train_01_subset)

plot_decision_regions(X=X_train_01_subset, y=y_train_01_subset, classifier=lrgd)

plt.xlabel('petal length [standardized]')
plt.ylabel('petal width [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

사이킷런을 사용하여 로지스틱 회귀 모델 훈련

  • 사이킷런에서 로지스틱 회귀를 사용하는 법을 배워보자. 이 구현은 매우 최적화 되어 있고 다중 분류도 지원한다. (OvR이 기본값)
    • 아래 코드에 대한 결과는 아래와 같다.
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(solver='liblinear', multi_class='auto', C=100.0, random_state=1)lr.fit(X_train_std, y_train)

plot_decision_regions(X=X_combined_std, y=y_combined, classifier=lr, test_idx=range(105, 150))

plt.xlabel('petal length [standardized]')
plt.ylabel('petal width [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

  • 훈련 샘플이 어떤 클래스에 속할 확률은 predict_proba 메서드를 사용하여 계산한다. 예컨대 테스트 세트에 있는 처음 세 개의 샘플 확률은 다음과 같이 예측할 수 있다.
lr.predict_proba(X_test_std[:3, :])

---
arry([[3.20136878e-08, 1.46953648e-01, 8.53046320e-01],
[8.34428069e-01, 1.65571931e-01, 4.57896429e-12],
[8.49182775e-08, 1.50817225e-01, 4.65678779e-13]])
  • 첫 번째 행은 첫 번째 붓꽃의 클래스 소속 확률이고,두 번째 행은 두 번째 꽃의 클래스 소속 확률이다.
    • 당연히 열을 모두 더하면 1이 된다.
  • 첫 번째 행에서 가장 큰 값은 대략 8.53인데, 이는 첫 번째 샘플이 클래스 3(Iris-virginica)에 속할 확률이 85.7%라는 뜻이다.
  • 넘파이의 argmax 함수를 이용하면 결과를 다음과 같이 얻을 수 있다.
lr.predict_proba(X_test_std[:3, :]).argmax(axis=1)

---
arry([2, 0, 0])
  • 조건부 활률로부터 얻은 클래스 레이블은 수동적인 방법이므로 직접 predict 메서드를 호출하여 결과를 얻을 수도 있다.
lr.predict(X_test_std[:3, :])

---
arry([2, 0, 0])
  • 샘플 하나의 클래스 레이블을 예측할 때 주의할 점이 있다. 사이킷런은 입력 데이터로 2차원 배열을 기대한다. 고로 하나의 행을 2차원 포맷으로 먼저 변환해야 한다.
    • 하나의 행을 2차원 배열로 변환하는 한 가지 방법은 넘파이 reshape 메서드를 사용하여 새로운 차원을 추가하는 것이다.
lr.predict(X_test_std[0, :]).reshape(1, -1)

---
arry([2])

규제를 사용하여 과대적합 피하기

  • 과대적합(overfitting)은 머신러닝에서 자주 발생하는 문제이다.
    • 모델이 훈련 데이터로는 잘 동작하지만 본 적 없는 데이터(테스트 데이터)로는 잘 일반화되지 않는 현상이다. 모델이 과대적합일 때 분산이 크다고 말한다.
    • 모델 파라미터가 너무 많아 주어진 데이터에서 너무 복잡한 모델을 만들기 때문이다.
  • 비슷하게 모델이 과소적합(underfitting)일 때도 있다(편향이 크다) 이는 훈련 데이터에 있는 패턴을 감지할 정도로 충분히 모델이 복잡하지 않다는 것을 의미하는 것으로 이 때문에 새로운 데이터에서도 성능이 낮다.

  • 좋은 편향-분산 트레이드오프를 찾는 한 가지 방법은 규제를 사용하여 모델의 복잡도를 조정하는 것이다.
    • 규제(regularization)은 공선성(collinearity)(특성 간의 높은 상관관계)을 다루거나 데이터에서 잡음을 제거하여 과대적합을 방지할 수 있는 매우 유용한 방법이다.
    • 규제는 과도한 파라미터(가중치) 값을 제한하기 위해 추가적인 정보(편향)을 주입하는 개념이다.
    • 가장 널리 사용하는 규제 형태는 다음과 같은 L2 규제이다 (이따금 L2 축소 또는 가중치 감쇠라고 부른다)

{\lambda \over 2} \|w\|^{2} = {\lambda \over 2} \sum_{j=1}^{m} w_{j}^{2}

  • 이 식에서 \lambda 는 규제 하이퍼파라미터이다.
  • 로지스틱 회귀의 비용 함수는 규제 항을 추가해서 규제를 적용한다. 규제 항은 모델 훈련 과정에서 가중치를 주이는 역할을 한다.

J(w) = \sum_{i=1}^{n} [-y^{(i)} \log(\phi(z^{(i)})) - (1 - y^{(i)}) \log (1 - \phi(z^{(i)}))] + {\lambda \over 2} \|w\|^{2}

  • 규제 하이퍼파라미터 \lambda 를 사용하여 가중치를 작게 유지하면서 훈련 데이터에 얼마나 잘 맞출지를 조정할 수 있다.
    • \lambda 값을 증가하면 규제 강도가 높아진다.
  • 사이킷런의 LogisticRegression 클래스의 매개변수 C는 서포트 벡터 머신 형식에서 따왔다. 매개변수 C는 규제 하이퍼파라미터 \lambda 의 역수이다.
    • 결과적으로 역 규제 파라미터 C의 값을 감소시키면 규제 강도가 증가한다.
weights, params = [], []

for c in np.arange(-5, 5):
   lr = LogisticRegression(solver='liblinear', multi_class='auto', C=10.**c, random_state=1)
   lr.fit(X_train_std, y_train)
    weights.append(lr.coef_[1])
    params.append(10.**c)

weights = np.array(weights)

plt.plot(params, weights[:, 0], label='petal length')
plt.plot(params, weights[:, 1], linestyle='--', label='petal width')
plt.xlabel('C')
plt.ylabel('weight coefficient')
plt.legend(loc='upper left')
plt.xscale('log')
plt.show()
  • 위 코드를 실행하면 역 규제 매개변수 C의 값을 바꾸면서 열 개의 로지스틱 회귀 모델을 훈련한다.
    • 시연을 위해 모든 분류기에서 클래스 1의 가중치 값만 사용한다.
    • 다중 분류에는 OvR 기법을 사용한다.
  • 아래의 결과 그래프에서 볼 수 있듯이 매개변수 C가 감소하면 가중치 절댓값이 줄어든다. 즉, 규제 강도가 증가한다.

서포트 벡터 머신을 사용한 최대 마진 분류

  • 서포트 벡터 머신(Support Vector Machine, SVM)은 퍼셉트론의 확장으로 생각할 수 있다. 앞선 퍼셉트론 알고리즘을 사용하여 분류 오차를 최소화 했는데, SVM의 최적화 대상은 마진을 최대화 하는 것이다.
    • 마진은 클래스를 구분하는 초평면(결정 경계)과 이 초평면에 가장 가까운 훈련 샘플 사이의 거리로 정의한다.
    • 이런 샘플을 서포트 벡터(support vector)라고 한다.

최대 마진

  • 큰 마진의 결정 경계를 원하는 이유는 일반화 오차가 낮아지는 경향이 있기 때문이다. 반면 작은 마진의 모델을 과대적합(over fitting)이 되기 쉽다.
  • 마진 최대화를 이해하기 위해 결정 경계와 나란히 놓인 양성 샘플 쪽의 초평면과 음성 샘플 쪽의 초평면을 살펴보자.
    • 두 초평면은 다음과 같이 쓸 수 있다.
  1. w_{0} + w^{T} x_{pos} = 1
  2. w_{0} + w^{T} x_{neg} = -1
  • 위의 두 선형식 1과 2를 빼면 다음 결과를 얻을 수 있다.

w^{T} (x_{pos} - x_{neg}) = 2

  • 이 식을 다음과 같은 벡터 w의 길이로 정규화 할 수 있다.

\|w\| = \sqrt{\sum_{j=1}^{m} w_{j}^{2}}

  • 결과 식은 다음과 같다.

{w^{T} (x_{pos} - x_{neg}) \over \|w\|} = {2 \over \|w\|}

  • 이 식의 좌변은 양성 쪽 초평면과 음성 쪽 초평면 사이의 거리로 해석할 수 있다. 이것이 최대화하려고 하는 마진(margin)이다.
  • SVM의 목적 함수는 샘플을 정확하게 분류된다는 제약 조건하에서 {2 \over \|w\|} 를 최대화함으로써 마진을 최대화하는 것이다.
    • 이 제약은 다음과 같이 쓸 수 있다.

\begin{cases} w_{0} + w^{T} x^{(i)} \geq 1 & y^{(i)} = 1 \\ w_{0} + w^{T} x^{(i)} \leq -1 & y^{(i)} = -1  \end{cases} (i = 1, 2, ...  N)

  • 여기서 N 은 데이터셋에 있는 샘플 개수이다.
  • 이 두 식이 말하는 것은 다음과 같다. 모든 음성 샘플은 음성 쪽 초평면 너머에 있어야 하고, 양성 샘플은 양성 쪽 초평면 너머에 있어야 한다. 이를 다음과 같이 간단히 쓸 수 있다.

\forall i, y^{(i)} (w_{0} + w^{T} x^{(i)}) \geq 1

  • 실제로는 동일한 효과를 내면서 콰드라틱 프로그래밍(quadratic programming) 방법으로 풀 수 있는 {1 \over 2} \|w\|^{2} 을 최소화하는 것이 더 쉽다.
    • 콰드라틱 프로그래밍에 대한 설명은 이 책의 범위를 넘어서는 것이므로 생략한다

슬랙 변수를 사용하여 비선형 분류 문제 다루기

  • 최대 마진 분류 이면에 있는 수학 개념에 너무 깊이 들어가지는 않을 것이다. 1995년 블라드미르 바프닉(Vladimir Vapnik)이 소개한 슬랙 변수 \zeta (zeta) 만 간략히 소개하겠다. 이를 소프트 마진 분류(soft maring classification)라고 한다.
    • 슬랙 변수는 선형적으로 구분되지 않는 데이터에서 선형 제약 조건을 완화할 필요가 있기 때문에 도입되었다.
    • 이를 통해 적절히 비용을 손해 보면서 분류 오차가 있는 상황에서 최적화 알고리즘이 수렴한다.
    • 양수 값은 슬랙 변수를 선형 제약 조건에 더하면 된다.

\begin{cases} w_{0} + w^{T} x^{(i)} \geq 1 & y^{(i)} = 1 - \zeta^{(i)} \\ w_{0} + w^{T} x^{(i)} \leq -1 & y^{(i)} = -1 + \zeta^{(i)} \end{cases} (i = 1, 2, ...  N)

  • 여기서 N 은 데이터셋에 있는 샘플 개수이다.
  • 최소화할 새로운 목적 함수는 다음과 같다.

{1 \over 2} \|w\|^{2} + C(\sum_{i} \zeta^{(i)})

  • 변수 C 를 통해 분류 오차에 대한 비용을 조정할 수 있다.
    • C 값이 크면 오차에 대한 비용이 커진다. C   값이 작으면 분류 오차에 덜 엄격해 진다.
    • 매개변수 C 를 사용하여 마진 폭을 제어할 수 있고 아래 그림과 같이 편향-분산의 트레이드오프를 조정한다.

  • 이 개념은 규제와 관련이 있다. 이전 절에서 언급한 것처럼 규제가 있는 로지스틱 회귀 모델은 C 값을 줄이면 편향이 늘고 모델 분산이 줄어든다.
from sklearn.svm import SVC

svm = SVC(kernel='linear', C=1.0, random_state=1)
svm.fit(X_train_std, y_train)

plot_decision_regions(X=X_combined_std, y=y_combined, classifier=svm, test_idx=range(105, 150))
plt.xlabel('petal length [standardized]')
plt.ylabel('petal width [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

사이킷런의 다른 구현

  • 이전 절에서 보았던 사이킷런 라이브러리의 LogisticRegression 클래스는 LIBLINEAR 라이브러리를 사용한다. 국립 타이완 대학교에서 개발한 것으로 최적화가 매우 잘된 C/C++ 라이브러리이다.
    • SVM을 훈련하는 SVC 클래스는 LIBSVM 라이브러리를 사용한다. 이 라이브러리는 SVM에 특화된 C/C++라이브러리이다.
    • 순수한 파이썬 구현에 비해 LIBLINEAR와 LIBSVM은 많은 선형 분류기를 아주 빠르게 훈련할 수 있는 장점이 있다.
  • 이따금 데이터셋이 너무 커서 컴퓨터 메모리 용량에 맞지 않는 경우에 사이킷런은 대안으로 SGDClassifier 클래스를 제공한다.
    • 이클래스는 partial_fit 메서드를 사용하여 온라인 학습을 지원한다.
    • SGDClasssfier 클래스 이면에 있는 개념은 2장에서 아달린을 위해 구현한 확률적 경사 하강법과 비슷하다.
    • 기본 매개변수를 사용한 퍼셉트론, 로지스틱 회귀, 서포트 벡터 머신의 확률적 경사 하강법 버전은 다음과 같다.
from sklearn.linear_model import SGDClassifier

ppn = SGDClassifier(loss='perceptron')
lr = SGDClassifier(loss='log')
svm = SGDClassifier(loss='hinge')

커널 SVM을 사용하여 비선형 문제 풀기

  • 머신러닝 기술자 사이에서 SVM이 인기가 높은 또 다른 이유는 비선형 분류 문제를 풀기 위해 커널 방법을 사용할 수 있기 때문이다.

선형적으로 구분되지 않는 데이터를 위한 커널 방법

  • 비선형 분류 문제가 어떤 모습인지 보기 위해 샘플 데이터셋을 만들어 보자.
    • 다음 코드에서 넘파이 logical_or 함수를 사용하여 XOR 형태의 간단한 데이터셋을 만든다.
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
X_xor = np.random.randn(200,2)
y_xor = np.logical_xor(X_xor[:,0] > 0, X_xor[:, 1] > 0)
plt.scatter(X_xor[y_xor == 1, 0], X_xor[y_xor == 1, 1], c='b', marker='x', label='1')
plt.scatter(X_xor[y_xor == -1, 0], X_xor[y_xor == -1, 1], c='r', marker='s', label='-1')
plt.xlim([-3, 3])
plt.ylim([-3, 3])
plt.legend(loc='best')
plt.tight_layout()
plt.show()
  • 위 코드를 실행하면 아래 그림과 같이 랜덤한 잡음이 섞인 XOR 데이터셋이 만들어진다.

  • 이런 데이터셋은 양성 클래스와 음성 클래스를 선형 초평면으로 구분할 수 없다.
    • 이렇게 선형적으로 구분되지 않는 데이터를 다루는 커널 방법(kernel method)의 기본 아이디어는 매핑 함수 \phi 를 사용하여 원본 특성의 비선형 조합을 선형적으로 구분되는 고차원 공간에 투영하는 것이다.
    • 아래 그림에서 볼 수 있듯이 2차원 데이터셋을 다음과 같은 투영을 통해 새로운 3차원 특성 공간으로 변환하면 클래스를 구분할 수 있다.

\phi(x_{1}, x_{2}) = (z_{1}, z_{2}, z_{3}) = (x_{1}, x_{2}, x_{1}^{2} + x_{2}^{2})

  • 고차원 공간에서 두 클래스를 구분하는 선형 초평면은 원본 특성 공간으로 되돌리면 비선형 결정 경계가 된다.

커널 기법을 사용하여 고차원 공간에서 분할 초평면 찾기

  • SVM으로 비선형 문제를 풀기 위해 매핑 함수 \phi 를 사용하여 훈련 데이터를 고차원 특성 공간으로 변환한다.
    • 그 다음 이 새로운 특성 공간에서 데이터를 분류하는 선형 SVM 모델을 훈련한다.
    • 동일한 매핑함수 \phi 를 사용하여 새로운 본 적 없는 데이터를 변환하고 선형 SVM 모델을 사용하여 분류할 수 있다.
  • 이런 매핑 방식의 한 가지 문제점은 새로운 특성을 만드는 계산 비용이 매우 비싸다는 것이다. 특히 고차원 데이터일 때 더욱 그렇다.
    • 여기에 소위 커널 기법이 등장하게 된다. SVM 을 훈련하기 위해 콰드라틱 프로그래밍 문제를 어떻게 푸는지 상세히 다루지는 않겠지만 실전에서 필요한 것은 점곱 x^{(i)T} x^{(j)} \phi (x^{(i)})^{T} \phi(x^{(j)}) 로 바꾸는 것이다.
    • 두 포인트 사이 점곱을 계산하는 데 드는 높은 비용을 절감하기 위해 커널 함수(kernel function) \mathcal{K} (x^{(i)}, x^{(j)}) = \phi(x^{(i)})^{T} \phi(x^{(j)}) 를 정의한다.
  • 가장 널리 사용되는 커널 중 하나는 방사 기저 함수(Radial Basis Function, RBF)이다. 가우시안 커널(Gaussian Kernel)이라고도 한다.

\mathcal{K} (x^{(i)}, x^{(j)}) = \exp({\|x^{(i)} - x^{(j)}\|^{2} \over 2 \sigma^{2}})

  • 간단하게 다음과 같이 쓰기도 한다.

\mathcal{K} (x^{(i)}, x^{(j)}) = \exp(-\gamma \|x^{(i)} - x^{(j)}\|^{2})

  • 여기서 \gamma = {1 \over 2 \sigma^{2}} 은 최적화 대상 파라미터가 아니다.
  • 대략적으로 말하면 커널(kernel)이란 용어를 샘플 간의 유사도 함수(similarity function)로 해석할 수 있다.
    • 음수 부호가 거리 측정을 유사도 점수로 바꾸는 역할을 한다.
    • 지수 함수로 얻게 되는 유사도 점수는 1(매우 유사)과 0(매우 다름) 사이의 범위를 가진다.
  • 이제 커널 SVM을 훈련하여 XOR 데이터를 구분하는 비선형 결정 경계를 그려보겠다.
svm = SVC(kernel='rbf', random_state=1, gamma=0.10, C=10.0)
svm.fit(X_xor, y_xor)

plot_decision_regions(X=X_xor, y=y_xor, classifier=svm)

plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

  • gamma=0.1로 지정한 매개변수 \gamma 를 가우시안 구(Gaussian Sphere)의 크기를 제한하는 매개변수로 이해할 수 있다.
    • \gamma 값을 크게 하면 서포트 벡터의 영향이나 범위가 줄어든다. 결정 경계는 더욱 샘플에 가까워지고 구불구불해진다.
    • \gamma 를 잘 이해하기 위해 붓꽃 데이터셋에서 RBF 커널 SVM을 적용해 보자
svm = SVC(kernel='rbf', random_state=1, gamma=0.2, C=1.0)
svm.fit(X_train_std, y_train)

plot_decision_regions(X=X_combined_std, y=y_combined, classifier=svm, test_idx=range(105, 150))

plt.xlabel('petal length [standardized]')
plt.ylabel('petal width [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()
  • 비교적 \gamma 값을 작게 했기 때문에 RBF 커널 SVM 모델이 만든 결정 경계는 아래 그림과 같이 부드럽다.

  • \gamma 값을 크게 하면 (위 코드에서 gamma를 100.0으로 설정)아래 그림과 같이 클래스 0과 클래스 1 주위로 결정 경계가 매우 가깝게 나타난다.

  • 이런 분류기는 훈련 데이터에서는 잘 맞지만 본 적 없는 데이터에서는 일반화 오차가 높을 것이다. 여기서 \gamma 매개변수가 과대적합을 조절하는 중요한 역할도 한다는 것을 알 수 있다.

결정트리 학습

  • 결정 트리(decision tree) 분류기는 설명이 중요할 때 아주 유용한 모델이다. 결정 트리라는 이름처럼 일련의 질문에 대한 결정을 통해 데이터를 분해하는 모델로 생각할 수 있다.

  • 결정 트리는 훈련 데이터에 있는 특성을 기반으로 샘플의 클래스 레이블을 추정할 수 있는 일련의 질문을 학습한다.
    • 위 그림은 범주형 변수를 사용한 결정 트리를 설명하고 있지만 동일한 개념이 붓꽃 데이터셋 같은 실수형 특성에도 적용된다.
    • 예컨대 꽃받침 너비 특성 축에 기준 값을 정하고 ‘꽃받침 너비가 2.8cm보다 큰가’라는 질문을 할 수 있다.
  • 결정 알고리즘을 사용하면 트리의 루트(root)에서 시작해서 정보 이득(Information Gain, IG)이 최대가 되는 특성으로 데이터를 나눈다.
    • 반복 과정을 통해 리프 노드(leaf node)가 순수해질 때까지 모든 자식 노드에서 이 분할 작업을 반복한다. 즉 이 노드의 모든 샘플은 동일한 클래스에 속한다.
    • 실제로 이렇게 하면 노드가 많은 깊은 트리가 만들어지고 과대적합될 가능성이 높다. 일반적으로 트리의 최대 깊이를 제한하여 트리를 가지치기(pruning) 한다.

정보 이득 최대화: 자원을 최대로 활용

  • 가장 정보가 풍부한 특성으로 노드를 나누기 위해 트리 알고리즘으로 최적화할 목적 함수를 정의한다. 이 목적 함수는 각 분할에서 정보 이득을 최대화한다.
    • 정보 이득은 다음과 같이 정의한다.

IG(D_{p}, f) = I(D_{P}) - \sum_{j=1}^{m} {N_{j} \over N_{p}} I(D_{j})

  • 여기서 f 는 분할에 사용할 특성이다.
    • D_{P}, D_{j} 는 부모와 j 번째 자식 노드의 데이터셋이다.
    • I 는 불순도(impurity) 지표이다.
    • N_{p} 는 부모 노드에 있는 전체 샘플 개수이다.
    • N_{j} j 번째 자식 노드에 있는 샘플 개수이다.
  • 여기서 볼 수 있듯이 정보 이득은 단순히 부모 노드의 불순도와 자식 노드의 불순도 합의 차이이다.
    • 자식 노드의 불순도가 낮을수록 정보이득이 커진다.
  • 구현을 간단하게 하고 탐색 공간을 줄이기 위해 (사이킷런을 포함해서) 대부분의 라이브러리는 이진 결정 트리를 사용한다.
    • 즉 부모 노드는 두 개의 자식 노드 D_{left}, D_{right} 로 나누어진다.

IG(D_{p}, f) = I(D_{P}) - {N_{left} \over N_{p}} I(D_{left}) - {N_{right} \over N_{p}} I(D_{right})

  • 이진 결정 트리에 널리 사용되는 세 개의 불순도 지표 또는 분할 조건은 지니 불순도(Gini impurity, I_{G} ), 엔트로피(entropy, I_{H} ), 분류 오차(Classification error, I_{g} )이다.
    • 샘플이 있는 모든 클래스 (p(i|t) \neq 0) 에 대한 엔트로피 정의는 다음과 같다.

I_{H}(t) = -\sum_{i=1}^{c} p(i|t) \log_{2} p(i|t)

  • 여기서 p(i|t) 는 특정 노드 t 에서 클래스 i 에 속한 샘플 비율이다.
    • 한 노드의 모든 샘플이 같은 클래스이면 엔트로피는 0이 된다. 클래스 분포가 균등하면 엔트로피는 최대가 된다.
    • 예컨대 이진 클래스일 경우 p(i=1|t) = 1 또는 p(i=0|t) = 0 이면 엔트로피는 0이다.
    • 클래스가 p(i=1|t) = 0.5 p(i=0|t) = 0.5 처럼 균등하게 분포되어 있으면 엔트로피는 1이 된다.
    • 엔트로피 조건을 트리의 상호 의존 정보를 최대화하는 것으로 이해할 수 있다.
  • 자연스럽게 지니 불순도는 잘못 분류될 확률을 최소화하기 위한 기준으로 이해할 수 있다.

I_{G}(t) = \sum_{i=1}^{c} p(i|t)(1 - p(i|t)) = 1 - \sum_{i=1}^{c} p(i|t)^{2}

  • 엔트로피와 비슷하게 지니 불순도는 클래스가 완벽하게 섞여 있을 때 최대가 된다. 예컨대 이진 클래스 환경 (c = 2 )에서는 다음과 같다.

I_{G}(t) = 1 - \sum_{i=1}^{c} 0.5^{2} = 0.5

  • 실제로는 지니 불순도와 엔트로피 모두 매우 비슷한 결과가 나온다. 보통 불순도 조건을 바꾸어 트리를 평가하는 것보다 가지치기 수준을 바꾸면서 튜닝하는 것이 훨씬 낫다.
  • 또 다른 불순도 지표는 분류 오차이다.

I_{E} = 1 - max \{p(i|t)\}

  • 가지치기에는 좋은 기준이지만 결정 트리를 구성하는데는 권장되지 않는다. 노드의 클래스 확률 변화에 덜 민감하기 때문이다. 아래 그림의 두 개의 분할 시나리오를 보면서 이를 확인해 보겠다.

  • 부모 노드에서 데이터셋 D_{P} 로 시작한다. 이 데이터셋은 클래스 1이 40개 샘플, 클래스 2가 40개의 샘플로 이루어져 있다.
    • 이를 두 개의 데이터셋 D_{left}, D_{right} 으로 나눈다. 분류 오차를 분할 기준으로 사용했을 때 정보 이득은 시나리오 A, B가 동일하다. (IG_{E} = 0.25 )

I_{E}(D_{P}) = 1 - 0.5 = 0.5

A:I_{E}(D_{left}) = 1 - {3 \over 4} = 0.25

A:I_{E}(D_{right}) = 1 - {3 \over 4} = 0.25

A:IG_{E} = 0.5 - {4 \over 8} 0.25 - {4 \over 8} 0.25 = 0.25

B:I_{E}(D_{left}) = 1 - {4 \over 6} = 0.33

A:I_{E}(D_{right}) = 1 - 1 = 0

A:IG_{E} = 0.5 - {6 \over 8} \times {1 \over 3} - 0 = 0.25

  • 지니 불순도는 시나리오 A(IG_{G} = 0.125 ) 보다 B(IG_{G} = 0.1\bar{\bar{6}} )가 더 순수하기 때문에 값이 더 높다.

I_{G}(D_{P}) = 1 - (0.5^{2} + 0.5^{2}) = 0.5

A:I_{G}(D_{left}) = 1 - (({3 \over 4}^{2} + {1 \over 4}^{2})) = 0.375

A:I_{G}(D_{right}) = 1 - (({1 \over 4}^{2} + {3 \over 4}^{2})) = 0.375

A:IG_{G}(D_{left}) = 0.5 - {4 \over 8}0.375 - {4 \over 8}0.375 = 0.125

B:I_{G}(D_{left}) = 1 - (({2 \over 6}^{2} + {4 \over 6}^{2})) = 0.\bar{4}

B:I_{G}(D_{right}) = 1 - (1^{2} + 0^{2}) = 0

B:IG_{G} = 0.5 - {6 \over 8}0.\bar{4} - 0 = 0.1\bar{\bar{6}}

  • 비슷하게 엔트로피 기준도 시나리오 A(IG_{H} = 0.19 )보다 시나리오 B(IG_{H} = 0.31 )를 선호한다.

I_{H}(D_{P}) = - (0.5 \log_{2} (0.5) + 0.5 \log_{2} (0.5)) = 1

A:I_{H}(D_{left}) = -(({3 \over 4} \log_{2}({3 \over 4}) + {1 \over 4} \log_{2} ({1 \over 4}))) = 0.81

A:I_{H}(D_{right}) = -(({1 \over 4} \log_{2}({1 \over 4}) + {3 \over 4} \log_{2} ({3 \over 4}))) = 0.81

A:IG_{H} = 1 - {4 \over 8}0.81 - {4 \over 8}0.81 = 0.19

B:I_{H}(D_{left}) = - (({2 \over 6} \log_{2}({2 \over 6}) + {4 \over 6} \log_{2} ({4 \over 6}))) = 0.92

B:I_{H}(D_{right}) = 0

B:IG_{H} = 1 - {6 \over 8}0.92 - 0 = 0.31

  • 3개의 불순도를 시각화하면 아래와 같다.

결정 트리 만들기

  • 결정 트리는 특성 공간을 사각 격자로 나누기 때문에 복잡한 결정 경계를 만들 수 있다. 결정 트리가 깊어질 수록 결정 경계가 복잡해지고 과대적합되기 쉽기 때문에 주의해야 한다.
    • 사이킷런을 사용하여 지니 불순도 조건으로 최대 깊이가 4인 결정 트리를 훈련해 본 결과는 다음과 같다.
from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(criterion='gini', max_depth=4, random_state=1)
tree.fit(X_train, y_train)

X_combined = np.vstack((X_train, X_test))
y_combined = np.hstack((y_train, y_test))

plot_decision_regions(X=X_combined, y=y_combined, classifier=tree, test_idx=range(105, 150))

plt.xlabel('petal length [cm]')
plt.ylabel('petal width [cm]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

  • 사이킷런은 훈련한 후 결정 트리를 .dot 파일로 추출할 수 있는 기능을 갖고 있는데, GraphViz 프로그램을 이용하면 이 파일을 시각화 할 수 있다.
    • (이하 프로그램 설치하는 내용 생략)

랜덤 포레스트로 여러 개의 결정 트리 연결

  • 랜덤 포레스트(random forest)는 뛰어난 분류 성능, 확장성, 쉬운 사용법 때문에 지난 10년간 머신러닝 애플리케이션에서 큰 인기를 누렸다.
    • 랜덤 포레스트는 결정 트리의 앙상블(ensemble)로 생각할 수 있다.
    • 랜덤 포레스트 이면의 아이디어는 여러 개의 (깊은) 결정 트리를 평균 내는 것이다.
    • 개개의 트리는 분산이 높은 문제가 있지만 앙상블은 견고한 모델을 만들어 일반화 성능을 높이고 과대적합의 위험을 줄여준다.
  • 랜덤 포레스트의 알고리즘은 다음 4단계로 요약할 수 있다.
    1. n개의 랜덤한 부트스트랩(bootstrap) 샘플을 뽑는다(훈련 세트에서 중복을 허용하면서 랜덤하게 n개의 샘플을 선택한다)
    2. 부트스트랩 샘플에서 결정 트리를 학습한다. 각 노드에서 다음과 같이 한다.
      1. 중복을 허용하지 않고 랜덤하게 d개의 특성을 선택한다.
      2. 정보 이득과 같은 목적 함수를 기준으로 최선의 분할을 만드는 특성을 사용해서 노드를 분할한다.
    3. 단계 1, 2를 k번 반복한다.
    4. 각 트리의 예측을 모아 다수결 투표(majority voting)로 클래스 레이블을 할당한다.
  • 단계 2에서 각각의 결정 트리를 훈련할 때 조금 다른 점이 있다. 각 노드에서 최선의 분할을 찾기 위해 모든 특성을 평가하는 것이 아니라 랜덤하게 선택된 일부 특성만 사용한다.
  • 랜덤 포레스트는 결정 트리만큼 해석이 쉽지 않지만 하이퍼 파라미터 튜닝에 많은 노력을 기울이지 않아도 되는 것이 큰 장점이다.
    • 일반적으로 랜덤 포레스트는 가지치기할 필요가 없다. 앙상블 모델이 개별 결정 트리가 만드는 잡음으로부터 매우 안정되어 있기 때문이다.
    • 실전에서 신경 써야 할 파라미터는 랜덤 포레스트가 만들 트리 개수(단계 3) 하나이다.
    • 일반적으로 트리 개수가 많을수록 계산 비용이 증가하는 만큼 랜덤 포레스트 분류기의 성능이 좋아진다.
  • 실전에서 자주 사용되지는 않지만 랜덤 포레스트 분류기에서 최적화할만한 다른 하이퍼 파라미터는 부트스트랩 샘플의 크기 n(단계 1)과 각 분할에서 무작위로 선택할 특성 개수 d(단계 2-a)이다.
    • 부트스트랩 샘플의 크기 n을 사용하면 랜덤 포레스트의 편향-분산 트레이드 오프를 조절할 수 있다.
  • 부트스트랩 샘플 크기가 작아지면 개별 트리의 다양성이 증가한다. 특정 훈련 샘플이 부트스트랩 샘플에 포함될 확률이 낮기 때문이다.
    • 결국 부트스트랩 샘플 크기가 감소하면 랜덤 포레스트의 무작위성이 증가하고 과대적합의 영향이 줄어든다.
    • 일반적으로 부트스트랩 샘플이 작을수록 랜덤 포레스트의 전체적인 성능이 줄어든다. 훈련 성능과 테스트 성능 사이에 격차가 작아지지만 전체적인 테스트 성능이 감소하기 때문이다.
    • 반대로 부트스트랩 샘플 크기가 증가하면 과대적합 가능성이 늘어난다. 부트스트랩 샘플과 개별 결정 트리가 서로 비슷해지기 때문에 원본 훈련 데이터셋에 더 가깝게 학습된다.
  • 사이킷런의 RandomForestClassifier를 포함하여 대부분의 라이브러리에서는 부트스트랩 샘플 크기를 원본 훈련 세트의 샘플 개수와 동일하게 해야 한다.
    • 보통 이렇게 하면 균형 잡힌 편향-분산 트레이드오프를 얻는다.
    • 분할에 사용할 특성 개수 d는 훈련 세트에 있는 전체 특성 개수보다 작게 지정하는 편이다.
    • 사이킷런과 다른 라이브러리에서 사용하는 적당한 기본값은 d = \sqrt{m} 이다. 여기서 m 은 훈련 세트에 있는 특성 개수이다.
from sklearn.ensemble import RandomForestClassifier

forest = RandomForestClassifier(criterion='gini', n_estimators=25, random_state=1, n_jobs=2)
forest.fit(X_train, y_train)

X_combined = np.vstack((X_train, X_test))
y_combined = np.hstack((y_train, y_test))

plot_decision_regions(X=X_combined, y=y_combined, classifier=forest, test_idx=range(105, 150))

plt.xlabel('petal length')
plt.ylabel('petal width')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

k-최근접 이웃: 게으른 학습 알고리즘

  • k-최근접 이웃(K-Nearest Neighbor, KNN)은 앞선 학습 알고리즘과 근본적으로 다른데, KNN은 전형적인 게으른 학습기(lazy learner)이다.
    • 훈련 데이터에서 판별 함수(discriminative function)를 학습하는 대신 훈련 데이터셋을 메모리에 저장하기 때문이다.
  • (참고)
    • 머신러닝 알고리즘은 모수 모델(parametric model)과 비모수 모델(nonparametric model)로 묶을 수 있다.
    • 모수 모델은 새로운 데이터 포인트를 분류할 수 있는 함수를 학습하기 위해 훈련 데이터셋에서 모델 파라미터를 추정한다. 훈련이 끝나면 원본 훈련 데이터셋이 더는 필요하지 않다. 전형적인 모수 모델은 퍼셉트론, 로지스틱 회귀, 선형 SVM이다.
    • 비모수 모델은 고정된 개수의 파라미터로 설명될 수 없다. 훈련 데이터가 늘어남에 따라 파라미터 개수도 늘어난다. 비모수 모델 예는 결정 트리/랜덤 포레스트와 커널 SVM이다.
    • KNN은 비모수 모델에 속하며 인스턴스 기반 모델이라 한다. 인스턴스 기반 모델은 훈련 데이터셋을 메모리에 저장하는 것이 특징이다.
    • 게으른 학습은 인스턴스 기반 학습의 특별한 경우이며 학습 과정에 비용이 전혀 들지 않는다.
  • KNN 알고리즘은 매우 간단해서 다음 단계로 요약할 수 있다.
    1. 숫자 k와 거리 측정 기준을 선택한다.
    2. 분류하려는 샘플에서 k개의 최근접 이웃을 찾는다.
    3. 다수결 투표를 통해 클래스 레이블을 할당한다.

  • 위 그림은 새로운 데이터 포인트(물음표로 표시된 포인트)가 어떻게 이웃 다섯 개의 다수결 투표를 기반으로 삼각형 클래스 레이블에 할당 되는지를 보여준다.
  • 선택한 거리 측정 기준에 따라 KNN 알고리즘이 훈련 데이터셋에서 분류하려는 포인트와 가장 가까운 샘플 k개를 찾는다.
    • 새로운 데이터 포인트의 클래스 레이블은 이 k개의 최근접 이웃에서 다수결 투표를 하여 결정된다.
  • 이런 메모리 기반 방식의 분류기는 수집된 훈련 데이터에 즉시 적응할 수 있는 것이 주요 장점이다. 새로운 샘블을 분류하는 계산 복잡도는 단점이다.
    • 데이터셋의 차원(특성)이 적고 알고리즘이 KD-트리 같은 효율적인 데이터 구조로 구현되어 있지 않다면 최악의 경우 훈련 데이터셋의 샘플 개수에 선형적으로 증가한다.
    • 또 훈련 데이터가 없기 때문에 훈련 샘플을 버릴 수 없다. 대규모 데이터셋에서 작업한다면 저장 공간에 문제가 생긴다.
from sklearn.neighbors import KNeighborsClassifier

knn = KNeighborsClassifier(n_neighbors=5, p=2, metric='minkowski')
knn.fit(X_train_std, y_train)

plot_decision_regions(X=X_combined_std, y=y_combined, classifier=knn, test_idx=range(105, 150))

plt.xlabel('petal length [standardized]')
plt.ylabel('petal width [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

  • 적절한 k를 선택하는 것은 과대적합과 과소적합 사이에서 올바른 균형을 잡기 위해 중요하다. 데이터셋의 특성에 알맞은 거리 측정 지표를 선택해야 한다.
    • 붓꽃 데이터셋의 센티미터 단위를 가진 특성처럼 시룻 값을 가진 특성에는 보통 간단한 유클리디안 거리를 사용한다.
    • 유클리디안 거리를 사용하려면 각 특성이 동일하게 취급되도록 표준화를 하는 것이 중요하다.
    • 위 코드에서 사용한 minkowski 거리를 유클리디안 거리와 맨해튼 거리를 일반화한 것으로 다음과 같이 쓸 수 있다.

d(x^{(i)}, x^{(j)}) = \sqrt[p]{\sum_{k} |x_{k}^{(i)} - x_{k}^{(j)}|^{p}}

  • 매개변수 p=2 로 지정하면 유클리디안 거리가 되고 p=1 로 지정하면 맨해튼 거리가 된다.
    • 사이킷런에는 다른 거리 측정 기준이 많으며 metric 매개변수로 지정할 수 있다.
[ssba]

The author

지성을 추구하는 사람/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.