Tag Archives: 머신러닝

머신 러닝 교과서/ 모델 평가와 하이퍼파라미터 튜닝의 모범 사례

파이프라인을 사용한 효율적인 워크플로

  • 테스트 세트에 있는 별도의 샘플처럼 새로운 데이터의 스케일을 조정하고 압축하기 위해 훈련 세트에서 학습한 파라미터를 재사용해야 한다.
  • 이 장에서는 이를 위해 아주 유용하게 사용할 수 있는 사이킷런의 Pipeline 클래스를 배우겠다.

위스콘신 유방암 데이터셋

  • 이 장에서는 위스콘신 유방암 데이터셋을 사용하겠다. 여기에는 악성과 양성인 종양 세포 샘플 569개가 포함되어 있다.
  • 이 절에서는 3단계로 나누어 데이터셋을 읽고 훈련 세트와 테스트 세트로 분할 하겠다.
    1. pandas를 사용하여 UCI 서버에서 데이터셋을 Load
    2. 30개의 특성을 넘파이 배열 X에 할당.
      • LabelEncoder 객체를 사용하여 클래스 레이블을 원본 문자열 표현에서 정수로 변환한다.
    3. 첫 번째 모델 파이프라인을 구성하기 전에 데이터셋을 훈련 세트(80%)와 별도의 테스트 세트(20%)로 나눈다.
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split

df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data', header=None)

X = df.loc[:, 2:].values
y = df.loc[:, 1].values

le = LabelEncoder()

y = le.fit_transform(y)

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

파이프라인으로 변환기와 추정기 연결

  • 위스콘신 유방암 데이터셋을 선형 분류기에 주입하기 전에 특성을 표준화 해야 한다.
    • 여기서는 우선 주성분 분석(PCA)를 통해 초기 30차원에서 좀 더 낮은 2차원 부분 공간으로 데이터를 압축한다.
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline

pipe_lr = make_pipeline(StandardScaler(), PCA(n_components=2), LogisticRegression(solver='liblinear', random_state=1))

pipe_lr.fit(X_train, y_train)

y_pred = pipe_lr.predict(X_test)
  • make_pipeline 함수는 여러 개의 사이킷런 변환기(입력에 대해 fit 메서드와 transform 메서드를 지원하는 객체)와 그 뒤에 fit 메서드와 predict 메서드를 구현한 사이킷런 추정기를 연결할 수 있다.
  • 위 코드에서는 StandardScaler와 PCA 두 개의 변환기와 LogisticRegression 추정기를 make_pipeline 의 입력으로 넣었고, 이 함수는 이 객체들을 사용하여 사이킷런의 Pipeline 클래스 객체를 생성하여 반환한다.
  • 사이킷런의 Pipeline 클래스를 메타 추정기(meta-estimator)나 개별 변환기와 추정기를 감싼 래퍼(wrapper)로 생각할 수 있다.
    • Pipeline 객체의 fit 메서드를 호출하면 데이터가 중간 단계에 있는 모든 변환기의 fit 메서드와 transform 메서드를 차례로 거쳐 추정기 객체에 도달한다.
    • 추정기는 변환된 훈련 세트를 사용하여 학습한다.
  • 위 예제에서 pipe_lr 파이프라인의 fit 메서드를 호출할 때 먼저 훈련 세트에 StandardScaler의 fit 메서드와 transform 메서드가 호출된다.
    • 변환된 훈련 데이터는 파이프라인의 다음 요소인 PCA 객체로 전달된다. 이전 단계와 비슷하게 스케일 조정된 입력 데이터에 PCA의 fit 메서드와 transform 메서드가 호출된다.
    • 그 다음 파이프라인의 최종 요소인 추정기에 훈련 데이터가 전달된다.
    • 마침내 LogisticRegression 추정기가 StandardScaler와 PCA로 변환된 훈련 데이터로 학습한다.
    • 파이프라인의 중간 단계 횟수에는 제한이 없지만, 파이프라인의 마지막 요소는 추정기가 되어야 한다.
  • 파이프라인에서 fit 메서드를 호출하는 것과 비슷하게 predict 메서드도 제공한다.
    • Pipeline 인스턴스의 predict 메서드를 호출할 때 주입된 데이터는 중간 단계의 transform 메서드를 통과한다.
    • 마지막 단계에서 추정기 객체가 변환된 데이터에 대한 예측을 반환한다.
  • 사이킷런의 파이프라인은 매우 유용한 래퍼 도구이기 때문이 책의 나머지 부분에서 자주 사용할 것이다.
    • 객체가 동작하는 아래 그림을 참고하라.

k-겹 교차 검증을 사용한 모델 성능 평가

  • 모델이 너무 간단하면 under-fitting 이 발생하고 너무 복잡하면 훈련 데이터에 over-fitting이 될 수 있다.
  • 적절한 편향-분산 트레이드오프를 찾으려면 모델을 주의 깊게 평가해야 한다. 이 절에서는 보편적인 교차 검증 기법인 홀드아웃 교차 검증(holdout cross-validation)과 k-겹 교차 검증(k-fold corss-validation)을 다루겠다.

홀드아웃 방법

  • 홀드아웃 방법은 데이터셋을 훈련 세트와 테스트 세트로 나눈 후, 전자는 모델 훈련에 사용하고 후자는 일반화 성능을 추정하는데 사용한다.
  • 일반적인 머신 러닝 애플리케이션에서는 처음 본 데이터에서 예측 성능을 높이기 위해 하이퍼파라미터를 튜닝하고 비교해야 하는데, 이 과정을 모델 선택이라고 한다.
    • 모델 선택이라는 용어는 주어진 분류 문제에서 튜닝할 파라미터(또는 하이퍼 파라미터)의 최적 값을 선택해야 하는 것을 의미한다.
    • 모델 선택에 같은 테스트 세트를 반복해서 재사용하면 훈련 세트의 일부가 되는 셈이고 결국 모델은 over-fitting이 될 것이다.
    • 아직도 많은 사람이 모델 선택을 위해 테스트 세트를 사용하는데 이는 좋은 머신 러닝 작업 방식이 아니다.
  • 모델 선택에 홀드아웃 방법을 사용하는 가장 좋은 방법은 데이터를 훈련 세트, 검증 세트, 테스트 세트 3개의 부분으로 나누는 것이다.
    • 훈련 세트는 여러 모델을 훈련하는데 사용한다.
    • 검증 세트에 대한 성능은 모델 선택에 사용한다.
    • 테스트 세트를 분리했기 때문에 새로운 데이터에 대한 일반화 능력을 덜 편향되게 추정할 수 있는 장점이 있다.
  • 아래 그림은 홀드아웃 교차 검증의 개념을 보여준다. 검증 세트를 사용하여 반복적으로 다른 파라미터 값에서 모델을 훈련한 후 성능을 평가한다.
    • 만족할 만한 하이퍼 파라미터 값을 얻었다면 테스트 세트에서 모델의 일반화 성능을 추정한다.

  • 홀드아웃 방법은 훈련 데이터를 훈련 세트와 검증 세트로 나누는 방법에 따라 성능 추정이 민감할 수 있다는 것이 단점이다.
    • 검증 세트의 성능 추정이 어떤 샘플을 사용하느냐에 따라 달라질 것이다.

k-겹 교차 검증

  • k-겹 교차 검증에서는 중복을 허락하지 않고 훈련 데이터셋을 k개의 폴드로 랜덤하게 나눈다.
    • k-1 개의 폴드로 모델을 훈련하고 나머지 하나의 폴드로 성능을 평가한다.
    • 이 과정을 k번 반복하여 k개의 모델과 성능 추정을 얻는다.
  • 그 다음 서로 독립적인 폴드에서 얻은 성능 추정을 기반으로 모델의 평균 성능을 계산한다.
    • 홀드아웃 방법에 비해 훈련 세트의 분할에 덜 민감한 성능 추정을 얻을 수 있다.
    • 일반적으로 모델 튜닝에 k-겹 교차 검증을 사용한다. 즉, 만족할 만한 일반화 성능을 내는 최적의 하이퍼파라미터 값을 찾기 위해 사용한다.
  • 만족스러운 하이퍼파라미터 값을 찾은 후에는 전체 훈련 세트를 사용하여 모델을 다시 훈련한다.
    • 그 다음 독립적인 테스트 세트를 사용하여 최종 성능 추정을 한다.
    • k-겹 교차 검증 후에 전체 훈련 세트로 모델을 학습하는 이유는 훈련 샘플이 많을수록 학습 알고리즘이 더 정확하고 안정적인 모델을 만들기 때문이다.
  • k-겹 교차 검증이 중복을 허락하지 않는 리샘플링 기법이기 때문에 모든 샘플 포인트가 훈련하는 동안 (테스트 폴드로) 검증에 딱 한번 사용되는 장점이 있다.
    • 이로 인해 홀드아웃 방법보다 모델 성능의 추정에 분산이 낮다.
  • 아래 그림은 k=10일 때 k-겹 교차 검증의 개념을 요약한 것이다.
    • 훈련 데이터는 10개의 폴드로 나누어지고 열 번의 반복 동안 아홉 개의 폴드는 훈련에, 한 개의 폴드는 모델 평가를 위해 사용된다.
    • 각 폴드의 추정 성능 E_{i} 를 사용하여 모델의 평균 성능 E를 계산한다.

  • 경험적으로 보았을 때 k-겹 교차 검증에서 좋은 기본값은 k=10이다.
    • 예컨대 론 코하비(Ron Kohavi)는 여러 종류의 실제 데이터셋에서 수행한 실험을 통해 10-겹 교차 검증이 가장 뛰어난 편향-분산 트레이드오프를 가진다고 제안했다
  • 비교적 작은 훈련 세트로 작업한다면 폴드 개수를 늘리는 것이 좋다.
    • k값이 증가하면 더 많은 훈련 데이터가 각 반복에 사용되고 모델 성능을 평균하여 일반화 성능을 추정할 때 더 낮은 편향을 만든다.
    • k 값이 아주 크면 교차 검증 알고리즘의 실행 시간이 늘어나고 분산이 높은 추정을 만든다. 이는 훈련 폴드가 서로 많이 비슷해지기 때문이다.
    • 다른 말로 하면 대규모 데이터셋으로 작업할 때는 k=5와 같은 작은 k 값을 선택해도 모델의 평균 성능을 정확하게 추정할 수 있다.
    • 또 폴드마다 모델을 학습하고 평가하는 계산 비용을 줄일 수 있다.
  • 기본 k-겹 교차 검증 방법보다 좀 더 향상된 방법은 계층적 k-겹 교차 검증(stratified k-fold cross-validation)이다.
    • 이는 좀 더 나은 편향과 분산 추정을 만든다. 특히 론 코하비가 보인 것처럼 클래스 비율이 동등하지 않을 때도 그렇다.
    • 계층적 교차 검증은 각 폴드에서 클래스 비율이 전체 훈련 세트에 있는 클래스 비율을 대표하도록 유지한다.
    • 사이킷런의 StratifiedKFold 반복자를 사용하여 구현할 수 있다.
import numpy as np
from sklearn.model_selection import StratifiedKFold

kfold = StratifiedKFold(n_splits=10, random_state=1).split(X_train, y_train)

scores = []

for k, (train, test) in enumerate(kfold):
   pipe_lr.fit(X_train[train], y_train[train])
   score = pipe_lr.score(X_train[test], y_train[test])
    scores.append(score)
  • 먼저 sklearn.model_selection 모듈에 있는 StratifiedKFold 클래스를 훈련 세트의 y_train 클래스 레이블에 전달하여 초기화한다. 그 후 n_splits 매개변수로 폴드 개수를 지정한다.
    • kfold 반복자를 사용하여 k개의 폴드를 반복하여 얻은 train의 인덱스를 로지스틱 회귀 파이프라인을 훈련하는데 사용할 수 있다.
    • pipe_lr 파이프라인을 사용하므로 각 반복에서 샘플의 스케일이 적절하게 조정된다 (예컨대 표준화를 통해)
    • 그 다음 테스트 인덱스를 사용하여 모델의 정확도 점수를 계산한다.
    • 이 점수를 리스트에 모아서 추정한 정확도의 평균과 표준편차를 계산한다.
  • 사이킷런은 k-겹 교차 검증 함수를 제공하기 때문에 좀 더 간단하게 계층별 k-겹 교차 검증을 사용하여 모델을 평가할 수 있다.
 
from sklearn.model_selection import cross_val_score

scores = cross_val_score(estimator=pipe_lr, X=X_train, y=y_train, cv=10, n_jobs=1)
  • cross_val_score 함수의 아주 유용한 기능은 각 폴드의 평가를 컴퓨터에 있는 복수 개의 CPU 코어에 분산할 수 있다는 점이다.
    • n_jobs 매개변수를 1로 설정하면 하나의 CPU 코어만 성능 평가에 사용되며, 2로 설정하면 2개의 CPU 코어에 교차 검증을 10회씩 분산할 수 있다. -1을 설정하면 컴퓨터에 설치된 모든 CPU 코어를 사용하여 병렬처리를 한다.

학습 곡선과 검증 곡선을 사용한 알고리즘 디버깅

학습 곡선으로 편향과 분산 문제 분석

  • 훈련 데이터셋에 비해 모델이 너무 복잡하면, 즉 모델의 자유도나 모델 파라미터가 너무 많으면 모델이 훈련 데이터에 과대적합되고 처음 본 데이터에 일반화 되지 못하는 경향이 있다.
  • 보통 훈련 샘플을 더 모으면 과대적합을 줄이는데 도움이 되지만, 실전에서는 데이터를 더 모으는 것이 매우 비싸거나 불가능한 경우가 많다.
  • 모델의 훈련 정확도와 검증 정확도를 훈련 세트의 크기 함수로 그래프를 그려보면 모델에 높은 분산의 문제가 있는지 높은 편향의 문제가 있느지 쉽게 감지할 수 있다.

  • 왼쪽 위 그래프는 편향이 높은 모델로서 훈련 정확도와 교차 검증 정확도가 모두 낮다. 이는 훈련 데이터에 과소적합되었다는 것을 나타낸다.
    • 이 문제를 해결하는 일반적인 방법은 모델의 파라미터 개수를 늘리는 것이다.
  • 오른쪽 위 그래프는 분산이 높은 모델을 보여준다. 훈련 정확도와 교차 검증 정확도 사이에 큰 차이가 있다는 것을 나타낸다.
    • 과대적합 문제를 해결하려면 더 많은 훈련 데이터를 모으거나 모델 복잡도를 낮추거나 규제를 증가시킬 수 있다.
  • 사이킷런의 학습 곡선 함수를 사용하면 모델을 평가할 수 있다.
from sklearn.model_selection import learning_curve
import numpy as np
import matplotlib.pyplot as plt

pipe_lr = make_pipeline(StandardScaler(), LogisticRegression(solver='liblinear', penalty='l2', random_state=1))

train_sizes, train_scores, test_scores = learning_curve(estimator=pipe_lr, X=X_train, y=y_train, train_sizes=np.linspace(0.1, 1.0, 10), cv=10, n_jobs=1)

train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)

plt.plot(train_sizes, train_mean, color='blue', marker='o', markersize=5, label='training accuracy')
plt.fill_between(train_sizes, train_mean + train_std, train_mean - train_std, alpha=0.15, color='blue')
plt.plot(train_sizes, test_mean, color='green', linestyle='--', marker='s', markersize=5, label='validation accuracy')
plt.fill_between(train_sizes, test_mean + test_std, test_mean - test_std, alpha=0.15, color='green')
plt.grid()
plt.xlabel('Number of training samples')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim([0.8, 1.03])
plt.tight_layout()
plt.show()

  • learning_curve 함수의 train_sizes 매개변수를 통해 학습 곡선을 생성하는데 사용할 훈련 샘플의 개수나 비율을 지정할 수 있다.
    • 기본적으로 learning_curve 함수는 계층별 k-겹 교차 검증을 사용하여 분류기의 교차 검증 정확도를 계산한다.
    • cv 매개변수를 통해 k 값을 10으로 지정했기 때문에 계층별 10-겹 교차 검증을 사용한다.

검증 곡선으로 과대적합과 과소적합 조사

  • 검증 곡선은 과대적합과 과소적합 문제를 해결하여 모델 성능을 높일 수 있는 유용한 도구이다.
    • 사이킷런의 validation_curve를 이용해서 검증곡선을 만들 수 있다.
from sklearn.model_selection import validation_curve
import numpy as np
import matplotlib.pyplot as plt

param_range = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0]

train_scores, test_scores = validation_curve(estimator=pipe_lr, X=X_train, y=y_train, param_name='logisticregression__C', param_range=param_range, cv=10)

train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)

plt.plot(param_range, train_mean, color='blue', marker='o', markersize=5, label='training accuracy')
plt.fill_between(param_range, train_mean + train_std, train_mean - train_std, alpha=0.15, color='blue')
plt.plot(param_range, test_mean, color='green', linestyle='--', marker='s', markersize=5, label='validation accuracy')
plt.fill_between(param_range, test_mean + test_std, test_mean - test_std, alpha=0.15, color='green')
plt.grid()
plt.xscale('log')
plt.legend(loc='lower right')
plt.xlabel('Parameter C')
plt.ylabel('Accuracy')
plt.ylim([0.8, 1.0])
plt.tight_layout()
plt.show()

  • learning_curve와 유사하게 validation_curve도 기본적으로 계층별 k-겹 교차 검증을 사용하여 모델의 성능을 추정한다.
    • validation_curve 함수 안에서 평가하기 원하는 매개변수를 지정한다. 위 경우에는 LogisticRegression 분류기의 규제 매개변수인 C이다.
    • param_range 매개변수에는 값 범위를 지정한다.
    • C 값이 바뀜에 따라 정확도 차이가 미묘하지만 규제 강도를 높이면 (C값을 줄이면) 모델이 데이터에 조금 과소적합 되는 것을 볼 수 있다. 규제 강도가 낮아지는 큰 C 값에서는 모델이 데이터에 조금 과대적합 되는 경향을 보인다.

그리드 서치를 사용한 머신 로닝 모델 세부 튜닝

  • 머신 러닝에는 두 종류의 파라미터가 있는데, 하나는 훈련 데이터에 학습되는 파라미터로 로지스틱 회귀의 가중치가 그 예이다.
    • 다른 하나는 별도로 최적화 되는 학습 알고리즘의 파라미터로 튜닝 파라미터고, 하이퍼파라미터라고도 부른다. 예컨대 로지스틱 회귀의 규제 매개변수나 결정 트리의 깊이 매개변수이다.
  • 이전 절에서는 검증 곡선을 사용하여 하이퍼파라미터를 튜닝하여 모델 성능을 향상시켰다면 이번 절에서는 그리드 서치라는 인기 있는 하이퍼파라미터 최적화 기법을 살펴보겠다.

그리드 서치를 사용한 하이퍼파라미터 튜닝

  • 그리드 서치가 사용하는 방법은 아주 간단한데, 리스트로 지정된 여러 가지 하이퍼파라미터 값 전체를 모두 조사한다.
    • 이 리스트에 있는 값의 모든 조합에 대해 모델 성능을 평가하여 최적의 조합을 찾는다.
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC

pipe_svc = make_pipeline(StandardScaler(), SVC(random_state=1))

param_range = [0.0001, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0]

param_grid = [{'svc__C': param_range, 'svc__kernel':['linear']}, {'svc__C': param_range, 'svc__gamma':param_range, 'svc__kernel':['rbf']}]

gs = GridSearchCV(estimator=pipe_svc, param_grid=param_grid, scoring='accuracy', cv=10, n_jobs=1)
gs = gs.fit(X_train, y_train)

clf = gs.best_estimator_
clf.fit(X_train, y_train)
  • 위 코드는 sklearn.model_selection 모듈에 있는 GridSearchCV 클래스의 객체를 만들고 SVM을 위한 파이프라인을 훈련하고 튜닝한다.
  • 훈련 세트를 사용하여 그리드 서치를 수행한 후 최상의 모델 점수는 best_score_ 속성에서, 이 모델의 매개변수는 betst_params_ 속성에서 확인할 수 있다.
  • 독립적인 테스트 세트를 사용하여 최고 모델의 성능을 추정할 수 있는데 이 모델은 GridSearchCV 객체의 best_estimator_ 속성에서 얻을 수 있다.

중첩 교차 검증을 사용한 알고리즘 선택

  • 그리드 서치와 k-겹 교차 검증을 함께 사용하면 머신 러닝 모델의 성능을 세부 튜닝하기 좋다.
  • 여러 종류의 머신 러닝 알고리즘을 비교하려면 중첩 교차 검증(nested cross-validation) 방법이 권장된다.
    • 오차 예측에 대한 편향을 연구하는 중에 바르마(Varma)와 사이몬(Simon)은 중첩된 교차 검증을 사용했을 때 테스트 세트에 대한 추정 오차는 거의 편향되지 않는다는 결론을 얻었다.
  • 중첩 교차 검증은 바깥쪽 k-겹 교차 검증 루프가 데이터를 훈련 폴드와 테스트 폴드로 나누고 안쪽 루프가 훈련 폴드에서 k-겹 교차 검증을 수행하여 모델을 선택한다.
    • 모델이 선택되면 테스트 폴드를 사용하여 모델 성능을 평가한다.
    • 아래 그림은 바깥 루프에 다섯 개의 폴드를 사용하고 안쪽 루프에 두 개의 폴드를 사용하는 중첩 교차 검증의 개념을 보여준다.
    • 이런 방식은 계산 성능이 중요한 대용량 데이터셋에서 유용하다.
    • 중첩 교차 검증의 폴드 개수를 고려하여 5 x 2 교차 검증이라고도 한다.

  • 사이킷런에서는 다음과 같이 중첩 교차 검증을 수행할 수 있다.
gs = GridSearchCV(estimator=pipe_svc, param_grid=param_grid, scoring='accuracy', cv=2)

scores = cross_val_score(gs, X_train, y_train, scoring='accuracy', cv=5)
  • 반면 평균 교차 검증 점수는 모델의 하이퍼파라미터를 튜닝했을 때 처음 본 데이터에서 기대할 수 있는 추정값이 된다.
    • 예컨대 중첩 교차 검증을 사용하여 SVM 모델과 단일 결정 트리 분류기를 비교할 수 있다.

여러 가지 성능 평가 지표

  • 이전 절에서는 정확도를 사용하여 모델을 평가했다. 이 지표는 일반적으로 분류 모델의 성능을 정량화 하는데 유용하다.
  • 주어진 문제에 모델이 적합한지 측정할 수 있는 다른 성능 지표도 여럿 있는데, 정밀도(precision), 재현율(recall), F1-Score 이다.

오차 행렬

  • 학습 알고리즘의 성능을 행렬로 펼쳐 놓은 오차 행렬(confusion matrix)를 을 살펴보자.
    • 오차 행렬은 아래 그림과 같이 진짜 양성(True Positive, TP), 진짜 음성(True Negative, TN), 거짓 양성(False Positive, FP), 거짓 음성(False Negative, FN)의 개수를 적은 정방 행렬이다.

  • 이 행렬은 타깃 클래스와 예측 클래스의 레이블을 직접 세어 계산할 수 있지만 사이킷런에서 제공하는 편리한 confusion_matrix 함수를 사용할 수 있다.
from sklearn.metrics import confusion_matrix

pipe_svc.fit(X_train, y_train)

y_pred = pipe_svc.predict(X_test)

confmat = confusion_matrix(y_true=y_test, y_pred=y_pred)

fig, ax = plt.subplots(figsize=(2.5,2.5))

ax.matshow(confmat, cmap=plt.cm.Blues, alpha=0.3)

for i in range(confmat.shape[0]):
   for j in range(confmat.shape[1]):
        ax.text(x=j, y=i, s=confmat[i, j], va='center', ha='center')

plt.xlabel('predicted label')
plt.ylabel('true label')
plt.tight_layout()
plt.show()

분류 모델의 정밀도와 재현율 최적화

  • 예측 오차(ERR)와 정확도(AcC) 모두 얼마나 많은 샘플을 잘못 분류했는지 일반적인 정보를 알려준다.
    • 오차는 잘못된 예측의 합을 전체 예측 샘플 개수로 나눈 것이다.
    • 정확도는 옳은 예측의 합을 전체 예측 샘플 개수로 나누어 계산한다.

ERR = {FP + FN \over FP + FN + TP + TN}

  • 예측 정확도는 오차에서 바로 계산할 수 있다.

ACC = {TP + TN \over FP + FN + TP + TN} = 1 - ERR

  • 진짜 양성 비율 (True Positive Rate, TPR)과 거짓 양성 비율 (False Positive Rate, FPR)은 클래스 비율이 다른 경우 유용한 성능 지표이다.

FPR = {FP \over N} = {FP \over FP + TN}

TPR = {TP \over P} = {TP \over FN + TP}

  • 정확도(PRE)와 재현율(REC) 성능 지표는 진짜 양성과 진짜 음성 샘플의 비율과 관련이 있다. 사실 재현율은 TPR의 다른 이름이다.

PRE = {TP \over TP + FP}

REC = TPR = {TP \over P} = {TP \over FN + TP}

  • 실전에서는 PRE와 REC를 조합한 F1-Score를 자주 사용한다.

F1 = 2 \times {PRE \times REC \over PRE + REC}

  • (위 식에서 2배를 해주는 까닭은 2배를 안 하면 점수가 0-0.5 사이의 값이 되기 때문. 보기 좋게 0-1의 값을 만들어주기 위해 2배를 하는 것이다)
  • 이런 성능 지표들은 모두 사이킷런에 구현되어 있다.
from sklearn.metrics import precision_score, recall_score, f1_score

print ('정밀도: %.3f' % precision_score(y_true=y_test, y_pred=y_pred))
print ('재현율: %.3f' % recall_score(y_true=y_test, y_pred=y_pred))
print ('F1: %.3f' % f1_score(y_true=y_test, y_pred=y_pred))
  • GridSearchCV의 scoring 매개변수를 사용하여 정확도 대신 다른 성능 지표를 사용할 수 도 있다.
  • 사이킷런에서 양성 클래스는 레이블이 1인 클래스이다.
    • 양성 레이블을 바꾸고 싶다면 make_scorer 함수를 사용하여 자신만의 함수를 만들 수 있다.
    • (예시 생략)

ROC 곡선 그리기

  • ROC(Receiver Operating Characteristic) 그래프는 분류기의 임계 값을 바꾸어 가며 계산된 FPR과 TPR 점수를 기반으로 분류 모델을 선택하는 유용한 도구이다.
    • ROC 그래프의 대각선은 랜덤 추측으로 해석할 수 있고 대각선 아래에 위치한 분류 모델은 랜덤 추측 보다 나쁜 셈이다.
    • 완벽한 분류기의 그래프는 TPR이 1이고 FPR이 0인 왼쪽 위 구석에 위치한다.
    • ROC 곡선의 아래 면적인 ROC AUC(ROC Area Under the Curve)를 계산하여 분류 모델의 성능을 종합할 수 있다.
  • ROC 곡선과 비슷하게 분류 모델의 확률 임계값을 바꾸어가며 정밀도-재현율 곡선을 그릴 수 있다.
    • 정밀도-재현율 곡선을 그리는 함수도 사이킷런에 구현되어 있다.
from sklearn.metrics import roc_curve, auc
from scipy import interp

pipe_lr = make_pipeline(StandardScaler(), PCA(n_components=2), LogisticRegression(solver='liblinear', penalty='l2', random_state=1, C=100.0))

X_train2 = X_train[:, [4, 14]]

cv = list(StratifiedKFold(n_splits=3, random_state=1).split(X_train, y_train))

fig = plt.figure(figsize=(7,5))

mean_tpr = 0.0
mean_fpr = np.linspace(0, 1, 100)

all_tpr = []

for i, (train, test) in enumerate(cv):
    probas = pipe_lr.fit(X_train2[train], y_train[train]).predict_proba(X_train2[test])
    fpr, tpr, thresholds = roc_curve(y_train[test], probas[:, 1], pos_label=1)
   mean_tpr += interp(mean_fpr, fpr, tpr)
    mean_tpr[0] = 0.0
   roc_auc = auc(fpr, tpr)
    plt.plot(fpr, tpr, label='ROC fold %d (area=%0.2f)' % (i+1, roc_auc))

plt.plot([0,1], [0,1], linestyle='--', color=(0.6,0.6,0.6), label='random guessing')

mean_tpr /= len(cv)
mean_tpr[-1] = 1.0
mean_auc = auc(mean_fpr, mean_tpr)

plt.plot(mean_fpr, mean_tpr, 'k--', label='mean ROC (area = %0.2f)' % mean_auc, lw=2)
plt.plot([0, 0, 1], [0, 1, 1], linestyle=':', color='black', label='perfect performance')
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.legend(loc='lower right')
plt.xlabel('false positive rate')
plt.ylabel('true positive rate')
plt.tight_layout()
plt.show()

  • ROC AUC 점쉥만 관심 있다면 sklearn.metrics 모듈의 roc_auc_score 함수를 사용할 수도 있다.
    • ROC AUC로 분류 모델의 성능을 조사하면 불균형한 데이터셋에서 분류기의 성능에 대해 더 많은 통찰을 얻을 수 있다.
    • 정확도를 ROC 곡선 하나의 구분점으로 해석할 수 있지만 브래들리(A. P. Bradley)는 ROC AUC와 정확도가 대부분 서로 비례한다는 것을 보였다.

다중 분류의 성능 지표

  • 이 절에서 언급한 성능 지표는 이진 분류에 대한 것이다. 사이킷런은 이런 평균 지표에 매크로(Macro)와 마이크로(Micro) 평균 방식을 구현하여 OvA(One-Versus-All) 방식을 사용하는 다중 분류로 확장한다.
    • 마이크로 평균은 클래스별로 TP, TN, FP, FN을 계산한다.
    • 예컨대 k개의 클래스가 있는 경우 정밀도의 마이크로 평균은 다음과 같이 계산한다.

PRE_{micro} = {TP_{1} + TP_{2} + ... + TP_{k} \over TP_{1} + TP_{2} + ... + TP_{k} + FP_{1} + FP_{2} + ... + FP_{k}}

  • 매크로 평균은 단순하게 클래스별 정밀도의 평균이다.

PRE_{macro} = {PRE_{1} + PRE_{2} + ... + PRE_{k} \over k}

  • 마이크로 평균은 각 샘플이나 예측에 동일한 가중치를 부여하고자 할 때 사용하고, 매크로 평균은 모든 클래스에 동일한 가중치를 부여하여 분류기의 전반적인 성능을 평가한다. 이 방식에서는 가장 빈도 높은 클래스 레이블의 성능이 중요하다.
  • 사이킷런에서 이진 성능 지표로 다중 분류 모델을 평가하면 정규화 또는 가중치가 적용된 매크로 평균이 기본으로 적용된다.
    • 가중치가 적용된 매크로 평균은 평균을 계산할 때 각 클래스 레이블의 샘플 개수를 가중하여 계산한다.
    • 가중치 적용된 매크로 평균은 레이블마다 샘플 개수가 다른 불균형한 클래스를 다룰 때 유용하다.
  • 사이킷런에서 가중치가 적용된 매크로 평균이 다중 분류 문제에서 기본값이지만 sklearn.metrics 모듈 아래에 있는 측정함수들은 average 매개변수로 평균 계산 방식을 지정할 수 있다.
    • 예컨대 precision_score나 make_scorer 함수이다.

불균형한 클래스 다루기

  • 클래스 불균형은 실전 데이터를 다룰 때 자주 나타나는 문제로 한개 또는 여러 개의 클래스 샘플이 데이터셋에 너무 많을 때 등장한다.
  • 이 장에서 사용한 유방암 데이터셋이 90%는 건강한 환자라고 가정할 때, 지도 학습 알고리즘을 사용하지 않고 모든 샘플에 대해 다수의 클래스(양성 종양)를 예측하기만 해도 테스트 세트에서 90% 정확도를 달성할 수 있다.
  • 불균형한 데이터셋을 다룰 때 도움이 되는 몇 가지 기법을 알아보겠다.
  • 우선 불균형한 데이터셋을 만들어 보자.
X_imb = np.vstack((X[y == 0], X[y == 1][:40]))
y_imb = np.hstack((y[y == 0], y[y == 1][:40]))

y_pred = np.zeros(y_imb.shape[0])
np.mean(y_pred == y_imb) * 100
  • 이런 데이터셋에 분류 모델을 훈련할 때 모델을 비교하기 위해 정확도를 사용하는 것보다 다른 지표를 사용하는 것이 낫다.
    • 애플리케이션에서 주요 관심 대상이 무엇인지에 따라 정밀도, 재현율, ROC 곡선 등을 사용할 수 있다.
  • 머신 로닝 모델을 평가하는 것과 별개로 클래스 불균형은 모델이 훈련되는 동안 학습 알고리즘 자체에 영향을 미친다.
    • 머신 러닝 알고리즘이 일반적으로 훈련하는 동안 처리한 샘플에서 계산한 보상 또는 비용 함수의 합을 최적화 한다.
    • 결정 규칙은 다수 클래스 쪽으로 편향되기 쉽다.
    • 다른 말로 하면 알고리즘이 훈련 과정에서 비용을 최소화하거나 보상을 최대화하기 위해 데이터셋에서 가장 빈도가 높은 클래스의 예측을 최적화하는 모델을 학습한다.
  • 모델을 훈련하는 동안 불균형한 클래스를 다루는 한 가지 방법은 소수 클래스에서 발생한 예측 오류에 큰 벌칙을 부여하는 것이다.
    • 사이킷런에서 대부분의 분류기에 구현된 class_weight 매개변수를 class_weight=’balanced’로 설정해서 이런 벌칙을 편리하게 조정할 수 있다.
  • 불균형한 클래스를 다루는데 널리 사용되는 다른 전략은 소수 클래스의 샘플을 늘리거나 다수 클래스의 샘플을 줄이거나 인공적으로 훈련 샘플을 생성하는 것이다.
    • 아쉽지만 여러 도메인에 걸쳐 가장 잘 작동하는 보편적인 솔루션이나 기법은 없기 때문에 실전에서는 주어진 문제에 여러 전략을 시도해서 결과를 평가하고 가장 적절한 기법을 선택하는 것이 좋다.
  • 사이킷런 라이브러리에서는 데이터셋에서 중복을 허용한 샘플 추출 방식으로 소수 클래스의 샘플을 늘리는데 사용할 수 있는 resample 함수를 제공한다.
    • 다음 코드는 불균형한 유방암 데이터셋에서 소수 클래스를 선택하여 클래스 레이블이 0인 샘플 개수와 동일할 때까지 새로운 샘플을 반복적으로 추출한다.
from sklearn.utils import resample

X_upsampled, y_upsampled = resample(X_imb[y_imb==1], y_imb[y_imb==1], replace=True, n_samples=X_imb[y_imb==0].shape[0], random_state=123)
  • 샘플을 추출한 후 클래스 0인 원본 샘플과 업샘플링된 클래스 1을 연결하여 균형 잡힌 데이터셋을 얻을 수 있다.
  • 비슷하게 데이터셋에서 다수 클래스의 훈련 샘플을 삭제하여 다운샘플링(downsampling)을 할 수 있다. resample 함수를 사용하여 다운샘플링을 수행하려면 클래스 레이블 1과 0을 서로 바꾸면 된다.

머신 러닝 교과서/ 차원 축소를 사용한 데이터 압축

주성분 분석을 통한 비지도 차원 축소

  • 특성 선택과 마찬가지로 여러 특성 추출 기법을 사용하여 데이터셋의 특성 개수를 줄일 수 있다.
    • 특성 선택과 특성 추출의 차이는 원본 특성을 유지하느냐에 있다.
    • 순차 후진 선택 같은 특성 선택 알고리즘을 사용할 때는 원본 특성을 유지하지만, 특성 추출은 새로운 특성 공간으로 데이터를 변환하거나 투영한다.
    • 차원 축소 관점에서 보면 특성 추출은 대부분의 관련 있는 정보를 유지하면서 데이터를 압축하는 방법으로 이해할 수 있다.
  • 특성 추출이 저장 공간을 절약하거나 학습 알고리즘의 계산 효율성을 향상할 뿐만 아니라 차원의 저주 문제를 감소시켜 예측 성능을 향상하기도 한다. 특히 규제가 없는 모델로 작업할 때 그렇다.

주성분 분석의 주요 단계

  • PCA는 비지도 선형 변환 기법으로 주로 특성 추출과 차원 축소 용도로 많은 분야에서 널리 사용된다.
  • PCA는 특성 사이의 상관관계를 기반으로 하여 데이터에 있는 특성을 잡아낼 수 있다.
    • 요약하자면 PCA는 고차원 데이터에서 분산이 가장 큰 방향을 찾고 좀 더 작거나 같은 수의 차원을 갖는 새로운 부분 공간으로 이를 투영한다.
    • 새로운 부분 공간의 직교 좌표는 주어진 조건 하에서 분산이 최대인 방향으로 해석할 수 있다.
    • 새로운 특성 축은 아래 그림과 같이 서로 직각을 이룬다. 아래 그림에서 x_{1}, x_{2} 는 원본 특성 축이고 PC1, PC2 는 주성분이다.

  • PCA를 사용하여 차원을 축소하기 위해 d \times k 차원의 변환행렬 W 를 만든다.
    • 이 행렬로 샘플 벡터 x 를 새로운 k 차원의 특성 부분 공간으로 매핑한다.
    • 이 부분 공간은 원본 d 차원의 특성 공간보다 작은 차원을 가진다.

x = [x_{1}, x_{2}, ... , x_{d}], x \in \mathbb{R}^{d}

\downarrow xW, W \in \mathbb{R}^{d \times k}

z = [z_{1}, z_{2}, ... , z_{k}], z \in \mathbb{R}^{k}

  • 원본 d 차원 데이터를 새로운 k 차원의 부분 공간(일반적으로 k < d )으로 변환하여 만들어진 첫 번째 주성분이 가장 큰 분산을 가질 것이다.
    • 모든 주성분은 다른 주성분들과 상관관계가 있더라도 만들어진 주성분은 서로 직각을 이룰 것이다.
    • PCA 방향은 데이터 스케일에 매우 민감하다.
    • 특성의 스케일이 다르고 모든 특성의 중요도를 동일하게 취급하려면 PCA를 적용하기 전에 특성을 표준화 전처리해야 한다.
  • 차원 축소 PCA 알고리즘의 단계는 다음과 같다.
    1. d 차원 데이터셋을 표준화 전처리한다.
    2. 공분산 행렬(covariance matrix)을 만든다.
    3. 공분산 행렬을 고유 벡터(eigenvector)와 고윳값(eigenvalue)으로 분해한다.
    4. 고윳값을 내림차순으로 정렬하고 그에 해당하는 고유 벡터의 순위를 매긴다.
    5. 고윳값이 가장 큰 k 개의 고유 벡터를 선택한다. 여기서 k 는 새로운 특성 부분 공간의 차원이다. (k \leq d )
    6. 최상위 k 개의 고유벡터로 투영행렬(projection matrix) W 를 만든다.
    7. 투영 행렬 W 를 사용해서 d 차원 입력 데이터셋 X 를 새로운 k 차원의 특성 부분 공간으로 변환한다.

주성분 추출단계

  • 우선 PCA 처음 4단계를 처리한다.
    1. 데이터를 표준화 전처리 한다.
    2. 공분산 행렬을 구성한다.
    3. 공분산 행렬의 고윳값과 고유벡터를 구한다.
    4. 고윳값을 내림차순으로 정렬하여 고유 벡터의 순위를 매긴다.
  • Wine 데이터셋을 가져와서 70:30 비율로 훈련 세트와 테스트 세트를 나누고 표준화를 적용하여 단위 분산을 갖도록 한다.
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data', header=None)

X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:,0].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y, random_state=0)

sc = StandardScaler()
X_train_std = sc.fit_transform(X_train)
X_test_std = sc.transform(X_test)
  • 그 다음으로 공분산 행렬을 만드는 단계를 진행한다. 공분산 행렬은 d \times d 차원의 대칭 행렬로 특성 상호 간의 공분산을 저장한다. d 는 데이터셋에 있는 차원 개수이다.
    • 예컨대 전체 샘플에 대한 두 특성 x_{j} x_{k} 사이의 공분산은 다음 식으로 계산할 수 있다.

\sigma_{jk} = {1 \over n} \sum_{i = 1}^{n} (x_{j}^{(i)} - \mu_{j})(x_{k}^{(i)} - \mu_{k})

  • 여기서 \mu_{j} \mu_{k} 는 특성 j k 의 샘플 평균이다.
  • 데이터셋을 표준화 전처리했기 때문에 샘플 평균은 0이다.
  • 두 특성 간 양의 공분산은 특성이 함께 증가하거나 감소하는 것을 나타낸다. 반면 음의 공분산은 특성이 반대 방향으로 달라진다는 것을 나타낸다.
    • 예컨대 세 개의 특성으로 이루어진 공분산 행렬은 다음과 같이 쓸 수 있다. (여기서 \Sigma 는 대문자 표기일 뿐 합을 뜻하는 것이 아니므로 주의)

\Sigma = \left[ \begin{array}{rrr} \sigma_{1}^{2} & \sigma_{12} & \sigma_{13} \\ \sigma_{21} & \sigma_{2}^{2} & \sigma_{23} \\ \sigma_{31} & \sigma_{32} & \sigma_{3}^{2} \end{array} \right]

  • 공분산 행렬의 고유 벡터가 주성분(최대 분산의 방향)을 표현한다. 이에 대응되는 고윳값은 주성분의 크기이다.
    • Wine 데이터셋의 경우 13 x 13 차원의 공분산 행렬로부터 13개의 고유벡터와 고윳값을 얻을 수 있다.
  • 다음 단계로 공분산 행렬의 고유벡터와 고윳값의 쌍을 구해 보자.
    • 선형대수학에 따르면 고유벡터 v 는 다음 식을 만족한다.

\Sigma v = \lambda v

  • 여기서 \lambda 는 스케일을 담당하는 고윳값이다. 고윳벡터와 고윳값을 직접 계산하는 것은 복잡한 작업이기 때문에 Numpy의 linalg.eig 함수를 이용하여 계산한다.
import numpy as np

cov_mat = np.cov(X_train_std.T)
eigen_vals, eigen_vecs = np.linalg.eig(cov_mat)
  • Note) numpy.linalg.eig 함수는 대칭과 비대칭 정방 행렬을 모두 다룰 수 있지만 이따금 복소수 고윳값을 반환한다. 이와 비슷하게 에르미트 행렬을 분해하기 위해 구현된 numpy.linalg.eigh 함수는 공분산 행렬과 같은 대칭 행렬을 다룰 때 수치적으로 더 안정된 결과를 만든다. (numpy.linalg.eigh는 항상 실수 고윳값을 반환한다.)
  • Note) 사이킷런의 PCA 클래스는 직접 고윳값과 고유 벡터를 계산하는 대신 특이값 분해 (singular value decomposition) 방식을 이용하여 주성분을 구한다.

총분산과 설명된 분산

  • 데이터셋 차원을 새로운 특성 부분 공간으로 압축하여 줄여야 하기에 가장 많은 정보(분산)를 가진 고유 벡터(주성분) 일부만 선택한다.
    • 고윳값은 고유 벡터의 크기를 결정하므로 고윳값을 내림차순으로 정렬한다.
    • 고윳값 순서에 따라 최상위 k 개의 고유벡터를 선택한다.
  • 가장 정보가 많은 k 개의 고유 벡터를 선택하기 전에 고윳값의 설명된 분산(explained variance) 그래프로 그려보겠다.
    • 고윳값 \lambda_{j} 의 설명된 분산 비율은 전체 고윳값의 합에서 고윳값 \lambda_{j} 의 비율이다.

{\lambda_{j} \over \sum_{d}^{j=1} \lambda_{j}}

  • numpy의 cumsum 함수로 설명된 분산의 누적 합을 계산하고 matplotlib의 step 함수로 그래프를 그리면 다음과 같다.
import matplotlib.pyplot as plt

tot = sum(eigen_vals)

var_exp = [(i / tot) for i in sorted(eigen_vals, reverse=True)]

cum_var_exp = np.cumsum(var_exp)

plt.bar(range(1, 14), var_exp, alpha=0.5, align='center', label='individual explained variance')
plt.step(range(1, 14), cum_var_exp, where='mid', label='cumulative explained variance')
plt.ylabel('Explained variacne ratio')
plt.xlabel('Principal component index')
plt.legend(loc='best')
plt.tight_layout()
plt.show()

  • 랜덤 포레스트는 클래스 소속 정보를 사용하여 노드의 불순도를 계산하는 방면, 분산은 특성 축을 따라 값들이 퍼진 정도를 측정한다.

특성 변환

  • 공분산 행렬을 고유 벡터와 고윳값 쌍으로 성공적으로 분해한 후 Wine 데이터 셋을 새로운 주성분 축으로 변환하는 3개의 단계는 다음과 같다.
    • 고윳값이 가장 큰 k 개의 고유 벡터를 선택한다. 여기서 k 는 새로운 특성 부분 공간의 차원이다 (k \leq d )
    • 최상위 k 개의 고유벡터로 투영 행렬 W 를 만든다.
    • 투영행렬 W 를 사용해서 d 차원 입력 데이터셋 X 를 새로운 k 차원의 특성 부분 공간으로 변환한다.
  • 좀 더 쉽게 설명하면 고윳값의 내림차순으로 고유 벡터를 정렬하고 선택된 고유 벡터로 투영 행렬을 구성한다. 이 투영 행렬을 사용하여 데이터를 저차원 부분 공간으로 변환한다.
eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:, i]) for i in range(len(eigen_vals))]
eigen_pairs.sort(key=lambda k: k[0], reverse=True)
  • 앞의 코드를 이용하여 최상위 두 개의 고유 벡터로부터 13 x  2 차원의 투영행렬 W 를 만든다.
  • 투영 행렬을 사용하면 샘플 x (1 x 13 차원의 행 벡터)를 PCA 부분 공간(두 개의 주성분)을 투영하여 x' 를 얻을 수 있다. 두 개의 특성으로 구성된 2차원 샘플 벡터이다.

x' = xW

w = np.hstack((eigen_pairs[0][1][:, np.newaxis], eigen_pairs[1][1][:, np.newaxis]))
  • 비슷하게 124 x 13 차원의 훈련 데이터셋을 행렬 내적으로 두 개의 주성분으로 변환할 수 있다.

X' = XW

X_train_pca = X_train_std.dot(w)
  • 124 x 2 차원 행렬로 변환된 Wine 훈련 세트를 2차원 산점도로 시각화 하면 아래와 같다.
colors = ['r', 'b', 'g']
markers = ['s', 'x', 'o']

for l, c, m in zip(np.unique(y_train), colors, markers):
    plt.scatter(X_train_pca[y_train==l, 0], X_train_pca[y_train==l, 1], c=c, label=l, marker=m)

plt.ylabel('PC 1')
plt.xlabel('PC 2')
plt.legend(loc='lower left')
plt.tight_layout()
plt.show()

  • 위 이미지에서 볼 수 있듯이 데이터가 y축 보다 x 축을 따라 더 넓게 퍼져 있다. 이는 이전에 만든 설명된 분산의 그래프와 동일한 결과이다.

사이킷런의 주성분 분석

  • 다음의 코드를 실행하면 두 개의 주성분 축으로 줄어든 훈련 데이터로 만든 결정 경계를 볼 수 있다.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, 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.ylim(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.6, c = cmap.colors[idx], edgecolor='black', marker = markers[idx], label = cl)

from sklearn.linear_model import LogisticRegression
from sklearn.decomposition import PCA

pca = PCA(n_components=2)

X_train_pca = pca.fit_transform(X_train_std)
X_test_pca = pca.transform(X_test_std)

lr = LogisticRegression(solver='liblinear', multi_class='auto')
lr.fit(X_train_pca, y_train)

plot_decision_regions(X_train_pca, y_train, classifier=lr)

plt.ylabel('PC 1')
plt.xlabel('PC 2')
plt.legend(loc='lower left')
plt.tight_layout()
plt.show()

  • 테스트 세트에 적용한 결과는 아래와 같다.

선형 판별 분석을 통한 지도 방식의 데이터 압축

  • 선형 판별 분석(Linear Discriminant Analysis, LDA)은 규제가 없는 모델에서 차원의 저주로 인한 과대적합 정도를 줄이고 계산 효율성을 높이기 위한 특성 추추르이 기법으로 사용할 수 있다.
  • LDA 이면에 있는 일반적인 개념은 PCA와 매우 비슷하다. PCA가 데이터셋에 있는 분산이 최대인 직교 성분 축을 찾으려고 하는 반면, LDA 목표는 클래스를 최적으로 구분할 수 있는 특성 부분 공간을 찾는 것이다.

주성분 분석 vs 선형 판별 분석

  • PCA와 LDA 모두 데이터셋의 차원 개수를 줄일 수 있는 선형 변환 기법이다. 전자는 비지도 학습 알고리즘이지만, 후자는 지도 학습 알고리즘이다.
    • 직관적으로 LDA가 PCA 보다 분류 작업에서 더 뛰어난 특성 추출 기법이라고 생각할 수 있다.
    • 마르티네스는 PCA를 통한 전처리가 특정 이미지 인식 작업에 더 뛰어난 분류 결과를 내는 경향이 있다고 보고 했다. 예컨대 각 클래스에 속한 샘플이 몇 개 되지 않았을 때이다.
  • 다음 그림은 이진 분류 문제를 위한 LDA 개념을 요약하여 나타낸다. 클래스1의 샘플은 동그라미고, 클래스 2의 샘플은 덧셈 기호이다.

  • x축(LD 1)으로 투영하는 선형 판별 벡터는 두 개의 정규 분포 클래스를 잘 구분한다. y 축(LD 2)으로 투영하는 선형 판별 벡터는 데이터셋에 있는 분산을 많이 잡아내지만, 클래스 판별 정보가 없기 떄문에 좋은 선형 판별 벡터가 되지 못한다.
  • LDA는 데이터가 정규분포라고 가정한다. 또 클래스가 동일한 공분산 행렬을 가지고 샘플은 서로 통계적으로 독립적이라고 가정한다.
    • 하나 이상의 가정이 조금 위반되더라도 여전히 LDA는 차원 축소를 상당히 잘 수행한다.

선형 판별 분석의 내부 동작 방식

  • LDA 수행에 필요한 단계는 다음과 같다.
    1. d 차원의 데이터셋을 표준화 전처리한다 (d 는 특성 개수)
    2. 각 클래스에 대해 d 차원의 평균 벡터를 계산한다.
    3. 클래스 간의 산포 행렬(scatter matrix) S_{B} 와 클래스 내 산포 행렬 S_{W} 를 구성한다.
    4. S_{W}^{-1} S_{B} 행렬의 고유 벡터와 고윳값을 계산한다.
    5. 고윳값을 내림차순으로 정렬하여 고유 벡터의 순서를 매긴다.
    6. 고윳값이 가장 큰 k 개의 고유 벡터를 선택하여 d \times k 차원의 변환 행렬 W 를 구성한다. 이 행렬의 열이 고유벡터이다.
    7. 변환 행렬 W 를 사용하여 새로운 특성 부분 공간으로 투영한다.
  • 여기서 볼 수 있듯 LDA는 행렬을 고윳값과 고유 벡터로 분해하여 새로운 저차원 특성 공간을 구성한다는 점에서 PCA와 매우 닮았다.

산포 행렬 계산

  • PCA에서 했기 때문에 데이터셋의 특성을 표준화하는 단계는 건너뛰고 바로 평균 벡터 계산을 진행한다.
    • 평균 벡터를 사용하여 클래스 간의 산포 행렬과 클래스 내 산포 행렬을 구성한다.
    • 평균 벡터 m_{i} 는 클래스 i 의 샘플에 대한 특성의 평균값 \mu_{m} 을 저장한다.

m_{i} = {1 \ over n_{i}} \sum_{x \in D_{i}}^{c} x_{m}

  • 3개의 평균 벡터가 만들어진다.

m_{i} = \left[ \begin{array}{rrrr} \mu_{i, alcohol} \\ \mu_{i, malic acid} \\ ... \\ \mu_{i, proline} \end{array} \right] i \in \{1 , 2, 3 \}

np.set_printoptions(precision=4)
mean_vecs = []

for label in range(1, 4):
   mean_vecs.append(np.mean(X_train_std[y_train==label], axis=0))
  • 평균 벡터를 사용하여 클래스 내 산포 팽렬 S_{W} 를 계산할 수 있다.

S_{W} = \sum_{i=1}^{c} S_{i}

  • 이 행렬은 개별 클래스 i 의 산포 행렬 S_{i} 를 더하여 구한다.

S_{i} = \sum_{x \in D_{i}}^{c} (x - m_{i})(x - m_{i})^{T}

d = 13
S_W = np.zeros((d,d))

for label, mv in zip(range(1, 4), mean_vecs):
    class_scatter = np.zeros((d, d))

   for row in X_train_std[y_train == label]:
       row, mv = row.reshape(d, 1), mv.reshape(d, 1)
       class_scatter += (row - mv).dot((row - mv).T)   

    S_W += class_scatter
  • 개별 산포 행렬 S_{i} 를 산포 행렬 S_{W} 로 모두 더하기 전에 스케일 조정을 해야 한다.
    • 산포 행렬을 클래스 샘플 개수 \eta_{i} 로 나누면 사실 산포 행렬을 계산하는 것이 공분산 행렬 \Sigma_{i} 를 계산하는 것과 같아진다.
    • 즉 공분산 행렬은 산포 행렬의 정규화 버전이다.

\Sigma_{i} = {1 \over n_{i}} S_{i} = {1 \over n_{i}} \sum_{x \in D_{i}}^{c} (x - m_{i})(x - m_{i})^{T}

d = 13
S_W = np.zeros((d,d))

for label, mv in zip(range(1, 4), mean_vecs):
    class_scatter = np.cov(X_train_std[y_train==label].T, bias=True)
    S_W += class_scatter
  • 클래스 내 산포 행렬(또는 공분산 행렬)을 계산한 후 다음 단계로 넘어가 클래스 간의 산포 행렬 S_{B} 를 계산한다.

S_{B} = \sum_{i = 1}^{c} n_{i} (m_{i} - m)(m_{i} - m)^{T}

  • 여기서 m 은 모든 클래스의 샘플을 포함하여 계산된 전체 평균이다.
mean_overall = np.mean(X_train_std, axis=0)
mean_overall = mean_overall.reshape(d, 1)
d = 13
S_B = np.zeros((d, d))

for i, mean_vec in enumerate(mean_vecs):
   n = X_train[y_train == i +1, :].shape[0]
    mean_vec = mean_vec.reshape(d, 1)
    S_B += n * (mean_vec - mean_overall).dot((mean_vec - mean_overall).T)

새로운 특성 부분 공간을 위해 선형 판별 벡터 선택

  • LDA의 남은 단계는 PCA와 유사하다. 공분산 행렬에 대한 고윳값 분해를 수행하는 대신 행렬 S_{W}^{-1} S_{B} 의 고윳값을 계산하면 된다.
eigen_vals, eigen_vecs = np.linalg.eig(np.linalg.inv(S_W).dot(S_B))
  • 고유 벡터와 고윳값 쌍을 계산한 후 내림차순으로 고윳값을 정렬한다.
eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:,i]) for i in range(len(eigen_vals))]
eigen_pairs = sorted(eigen_pairs, key=lambda k: k[0], reverse=True)
  • LDA에서 선형 판별 벡터는 최대 c-1 개이다. c 는 클래스 레이블의 개수이다. 클래스 내 산포 행렬 S_{B} 가 랭크(rank) 1 또는 그 이하인 c 개의 행렬을 합한 것이기 때문이다.
    • 0이 아닌 고윳값이 두 개만 있는 것을 볼 수 있다.
  • 선형 판별 벡터(고유 벡터)로 잡은 클래스 판별 정보가 얼마나 많은지 측정하기 위해 선형 판별 벡터를 그려보면 아래 그림과 같다.
tot = sum(eigen_vals.real)
discr = [(i/tot) for i in sorted(eigen_vals.real, reverse=True)]
cum_discr = np.cumsum(discr)

plt.bar(range(1, 14), discr, alpha=0.5, align='center', label='individual discriminability')
plt.step(range(1, 14), cum_discr, where='mid', label='cumulative "discriminability"')
plt.ylabel('"discriminability" ratio')
plt.xlabel('Linear Discriminants')
plt.ylim([-0.1, 1.1])
plt.legend(loc='best')
plt.tight_layout()
plt.show()

  • 두 개의 판별 고유 벡터를 열로 쌓아서 변환 행렬 W 를 만든다.
w = np.hstack((eigen_pairs[0][1][:, np.newaxis].real, eigen_pairs[1][1][:, np.newaxis].real))

새로운 특성 공간으로 샘플 투영

  • 이전 절에서 만든 변환 행렬 W 를 훈련 세트에 곱해서 데이터를 변환할 수 있다.

X' = XW

X_train_lda = X_train_std.dot(w)

colors = ['r', 'b', 'g']
markers = ['s', 'x', 'o']

for l, c, m in zip(np.unique(y_train), colors, markers):
    plt.scatter(X_train_lda[y_train==l, 0], X_train_lda[y_train==l, 1] * -1, c=c, label=l, marker=m)

plt.ylabel('LD 1')
plt.xlabel('LD 2')
plt.legend(loc='lower right')
plt.tight_layout()
plt.show()

사이킷런의 LDA

  • 사이킷런에 구현된 LDA 클래스를 살펴보겠다.
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA

lda = LDA(n_components=2)
X_train_lda = lda.fit_transform(X_train_std, y_train)

lr = LogisticRegression(solver='liblinear', multi_class='auto')
lr = lr.fit(X_train_lda, y_train)

plot_decision_regions(X_train_lda, y_train, classifier=lr)

plt.ylabel('LD 1')
plt.xlabel('LD 2')
plt.legend(loc='lower right')
plt.tight_layout()
plt.show()

  • 테스트 결과는 다음과 같다.

커널 PCA를 사용하여 비선형 매핑

  • 많은 머신 러닝 알고리즘은 입력 데이터가 선형적으로 구분 가능하다는 가정을 한다. 그래서 지금까지 다루었던 알고리즘들은 선형적으로 완벽하게 분리되지 못한 이유를 잡음 때문이라고 가정한다.
  • 그러나 실제 애플리케이션에서는 비선형 문제를 더 자주 맞닥뜨리게 될 것이다.
    • 이런 비선형 문제를 다룰 때 PCA와 LDA 같은 차원 축소를 위한 선형 변환 기법은 최선의 선택이 아니다.
    • 이절에서는 PCA의 커널화 버전 또는 KPCA를 다루겠다.
    • 커널 PCA를 사용하여 선형적으로 구분되지 않는 데이터를 선형 분류기에 적합한 새로운 저차원 부분 공간으로 변환하는 방법을 살펴보겠다.

커널 함수와 커널 트릭

  • 커널 SVM에 관해 배운 것을 떠올려 보면 비선형 문제를 해결하기 위해 클래스가 선형으로 구분되는 새로운 고차원 특성 공간으로 투영할 수 있었다.
    • k 고차원 부분 공간에 있는 샘플 x \in \mathbb{R}^{d} 를 변환하기 위해 비선형 매핑 함수 \phi 를 정의한다.

\phi : \mathbb{R}^{d} \to \mathbb{R}^{k} (k >> d)

  • \phi 함수를 d 차원의 원본 데이터셋에서 더 큰 k 차원의 특성 공간으로 매핑하기 위해 원본 특성의 비선형 조합을 만드는 함수로 생각할 수 있다.
    • 예컨대 2차원 (d=2) 특성 벡터 x \in \mathbb{R}^{d} 가 있으면 (x d 개의 특성으로 구성된 열 벡터) 매핑 가능한 3D 공간은 다음과 같다.

x = [x_{1}, x_{2}]^{T}

\downarrow \phi

z = [x_{1}^{2}, \sqrt{2x_{1}x_{2}}, x_{2}^{2}]^{T}

  • 다른 말로 하면 커널 PCA를 통한 비선형 매핑을 수행하여 데이터를 고차원 공간으로 변환한다.
    • 그다음 고차원 공간에 표준 PCA를 사용하여 샘플이 선형 분류기로 구분될 수 있는 저차원 공간으로 데이터를 투영한다 (샘플이 이 입력 공간에서 잘 구분될 수 있다고 가정).
    • 이 방식의 단점은 계산 비용이 매우 비싸다는 것이다. 여기에 커널 트릭(kernel trick)이 등장한다.
    • 커널 트릭을 사용하면 원본 특성 공간에서 두 고차원 특성 벡터의 유사도를 계산할 수 있다.
  • 커널 트릭에 대해 알아보기 전에 표준 PCA 방식을 다시 살펴보자
    • 두 개의 특성 k, j 사이의 공분산은 다음과 같이 계산한다.

\sigma_{jk} = {1 \over n} \sum_{i = 1}^{n} (x_{j}^{(i)} - \mu_{j})(x_{k}^{(i)} - \mu_{k})

  • \mu_{j} = 0, \mu_{k} = 0 처럼 특성 평균을 0에 맞추었으므로 이 식은 다음과 같이 간단히 쓸 수 있다.

\sigma_{jk} = {1 \over n} \sum_{i = 1}^{n} x_{j}^{(i)} x_{k}^{(i)}

  • 이 식은 두 특성 간의 공분산을 의미한다. 공분산 행렬 \Sigma 를 계산하는 일반식으로 써보자.

\Sigma = {1 \over n} \sum_{i = 1}^{n} x^{(i)} x^{(i)^{T}}

  • 베른하르트 슐코프(Bernhard Scholkopf)는 이 방식을 일반화 하여 \phi 를 통한 비선형 특성 조합으로 원본 특성 공간의 샘플 사이의 내적을 대체했다.

\Sigma = {1 \over n} \sum_{i = 1}^{n} \phi (x^{(i)}) \phi(x^{(i)})^{T}

  • 이 공분산 행렬에서 고유 벡터(주성분)를 얻기 위해서는 다음 식을 풀어야 한다.

\Sigma v = \lambda v

\Rightarrow {1 \over n} \sum_{i = 1}^{n} \phi (x^{(i)}) \phi(x^{(i)})^{T} v = \lambda v

\Rightarrow v = {1 \over n \lambda} \sum_{i = 1}^{n} \phi (x^{(i)}) \phi(x^{(i)})^{T} = \sum_{i=1}^{n} a^{(i)} \phi(x^{(i)})

  • 여기서 \lambda v 는 공분산 행렬 \Sigma 의 고윳값과 고유 벡터이다. a 는 커널(유사도) 행렬 K 의 고유 벡터를 추출함으로써 구할 수 있다.
  • 커널 SVM을 사용하여 비선형 문제 풀기를 떠올리면 커널 트릭을 사용하여 샘플 x 끼리의 \phi 함수 내적을 커널 함수 K 로 바꿈으로써 고유 벡터를 명싲거으로 계산할 필요가 없었다.

\kappa (x^{(i)}, x^{(j)}) = \phi (x^{(i)})^{T} \phi (x^{(j)})

  • 다른 말로 하면 커널 PCA로 얻은 것은 표준 PCA 방식에서처럼 투영 행렬을 구성한 것이 아니고 각각의 성분에 이미 투영된 샘플이다.
    • 기본적으로 커널 함수는 두 벡터 사이의 내적을 계산할 수 있는 함수이다. 즉, 유사도를 측정할 수 있는 함수이다.
  • 가장 널리 사용되는 커널은 다음과 같다.
  • 다항 커널
    • \kappa (x^{(i)}, x^{(j)}) = (x^{(i)T} x^{(j)} + \theta)^{P}
    • 여기서 \theta 는 임계 값이고 P 는 사용자가 지정한 거듭제곱이다.
  • 하이퍼볼릭 탄젠트(hyperbolic tangent) (시그모이드(sigmoid)) 커널
    • \kappa (x^{(i)}, x^{(j)}) = tanh (\eta x^{(i)T} x^{(j)} + \theta)
  • 방사 기저 함수(Radial Basis Function, RBF) 또는 가우시안 커널
    • \kappa (x^{(i)}, x^{(j)}) = \exp(- {\|x^{(i)} - x^{(j)}\|^{2} \over 2 \sigma^{2}})
    • 변수 \gamma = {1 \over 2 \sigma^{2}} 을 도입하여 종종 다음과 같이 쓴다.
    • \kappa (x^{(i)}, x^{(j)}) = \exp(- \gamma \|x^{(i)} - x^{(j)}\|^{2})
  • 지금까지 배운 것을 요약하면 RBF 커널 PCA를 구현하기 위해 다음 3 단계를 정의할 수 있다.
    1. 커널 (유사도) 행렬 K 를 다음 식으로 계산한다.
      • \kappa (x^{(i)}, x^{(j)}) = \exp(- \gamma \|x^{(i)} - x^{(j)}\|^{2})
      • 샘플의 모든 쌍에 대해 구한다.
        • K = \left[ \begin{array}{rrrr} \kappa (x^{(1)}, x^{(1)}) & \kappa (x^{(1)}, x^{(2)}) & ... & \kappa (x^{(1)}, x^{(n)}) \\  \kappa (x^{(2)}, x^{(1)}) & \kappa (x^{(2)}, x^{(2)}) & ... & \kappa (x^{(2)}, x^{(n)}) \\ ... & ... & ... & ... \\ \kappa (x^{(n)}, x^{(1)}) & \kappa (x^{(n)}, x^{(2)}) & ... & \kappa (x^{(n)}, x^{(n)}) \end{array} \right] 
        • 100개의 훈련 샘플이 담긴 데이터셋이라면 각 싸으이 유사도를 담은 대칭 커널 행렬은 100 x 100 차원이 된다.
    2. 다음 식을 사용하여 커널 행렬 K 를 중앙에 맞춘다.
      • K' = K - 1_{n} K - K 1_{n} + 1_{n} K 1_{n}
      • 여기서 1_{n} 은 모든 값이 {1 \over n} n \times n 차원 행렬이다. (커널 행렬과 같은 차원)
    3. 고윳값 크기대로 내림차순으로 정렬하여 중앙에 맞춘 커널 행렬에서 최상위 k 개의 고유 벡터를 고른다. 표준 PCA와 다르게 고유 벡터는 주성분 축이 아니며, 이미 이 축에 투영된 샘플이다.
  • 위 단계의 2번째에서 왜 커널 행렬을 중앙에 맞추었는지 궁금할 수 있다.
    • 우리는 앞서 표준화된 전처리된 데이터를 다룬다고 가정했다. 공분산 행렬을 구성하고 비선형 특성 조합으로 내적을 \phi 를 사용한 비선형 특성 조합으로 내적을 대체할 때 사용한 모든 특성의 평균이 0이다.
    • 반면 새로운 특성 공간을 명시적으로 계산하지 않기 때문에 이 특성 공간이 중앙에 맞추어져 있는지 보장할 수 없다.
    • 이것이 새로운 두 번째 단계에서 커널 행렬의 중앙을 맞추는 것이 필요한 이유이다.

파이썬으로 커널 PCA 구현

  • RBF 커널 PCA 코드
from scipy.spatial.distance import pdist, squareform
from scipy import exp
from scipy.linalg import eigh
import numpy as np

def rbf_kernel_pca(X, gamma, n_components):
    """
   RBF 커널 PCA 구현

    매개변수
   ----------
   X: {넘파이 ndarray}, shape = [n_samples, n_features]

    gamma: float
     RBF 커널 튜닝 매개변수
    n_components: int
     변환한 주성분 개수   

    반환값
   -----------
   X_pc: {넘파이 ndarray}, shape = [n_samples, k_features]
      투영된 데이터셋
    """

   # M x N 차원의 데이터셋에서 샘플 간의 유클리디안 거리의 제곱을 계산
    sq_dists = pdist(X, 'sqeuclidean')

   # 샘플 간의 거리를 정방 대칭 행렬로 반환
    mat_sq_dists = squareform(sq_dists)

    # 커널 행렬을 계산
    K = exp(-gamma * mat_sq_dists)

   # 커널 행렬을 중앙에 맞춘다
    N = K.shape[0]
   one_n = np.ones((N, N)) / N
    K = K - one_n.dot(K) - K.dot(one_n) + one_n.dot(K).dot(one_n)

   # 중앙에 맞춰진 커널 행렬의 고윳값과 고유 벡터를 구한다.
    # scipy.linalg.eigh 함수는 오름차순으로 반환한다.
    eigvals, eigvecs = eigh(K)
    eigvals, eigvecs = eigvals[::-1], eigvecs[:, ::-1]

   # 최상위 k개의 고유 벡터를 선택한다(투영 결과)
    X_pc = np.column_stack([eigvecs[:, i] for i in range(n_components)])

    return X_pc

예제 1

  • 반달 모양을 띤 100개의 샘플로 구성된 2차원 데이터셋을 구성
from sklearn.datasets import make_moons
import matplotlib.pyplot as plt

X, y = make_moons(n_samples=100, random_state=123)

plt.scatter(X[y==0, 0], X[y==0, 1], color='red', marker='^', alpha=0.5)
plt.scatter(X[y==1, 0], X[y==1, 1], color='blue', marker='o', alpha=0.5)
plt.show()

  • PCA의 주성분에 데이터셋을 투영한다.
scikit_pca = PCA(n_components=2)

X_spca = scikit_pca.fit_transform(X)

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(7,3))

ax[0].scatter(X_spca[y==0, 0], X_spca[y==0, 1], color='red', marker='^', alpha=0.5)
ax[0].scatter(X_spca[y==1, 0], X_spca[y==1, 1], color='blue', marker='o', alpha=0.5)
ax[1].scatter(X_spca[y==0, 0], np.zeros((50,1))+0.02, color='red', marker='^', alpha=0.5)
ax[1].scatter(X_spca[y==1, 0], np.zeros((50,1))-0.02, color='blue', marker='^', alpha=0.5)

ax[0].set_xlabel('PC1')
ax[0].set_ylabel('PC2')
ax[1].set_ylim([-1, 1])
ax[1].set_yticks([])
ax[1].set_xlabel('PC1')

plt.show()

  • 이 데이터에 앞서 작성한 rbf_kernel_pca를 적용해 보자
X_kpca = rbf_kernel_pca(X, gamma=15, n_components=2)

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(7,3))

ax[0].scatter(X_kpca[y==0, 0], X_kpca[y==0, 1], color='red', marker='^', alpha=0.5)
ax[0].scatter(X_kpca[y==1, 0], X_kpca[y==1, 1], color='blue', marker='o', alpha=0.5)
ax[1].scatter(X_kpca[y==0, 0], np.zeros((50,1))+0.02, color='red', marker='^', alpha=0.5)
ax[1].scatter(X_kpca[y==1, 0], np.zeros((50,1))-0.02, color='blue', marker='^', alpha=0.5)

ax[0].set_xlabel('PC1')
ax[0].set_ylabel('PC2')
ax[1].set_ylim([-1, 1])
ax[1].set_yticks([])
ax[1].set_xlabel('PC1')

plt.tight_layout()
plt.show()

  • 이제 두 클래스는 선형적으로 구분이 잘 되므로 선형 분류기를 위한 훈련 데이터로 적합하다.
  • 아쉽지만 보편적인 \gamma 파라미터 값은 없다. 주어진 문제에 적합한 \gamma 를 찾으려면 실험이 필요하다.

예제 2

  • 동심원 데이터 셋을 구성
from sklearn.datasets import make_circles

X, y = make_circles(n_samples=1000, random_state=123, noise=0.1, factor=0.2)

plt.scatter(X[y==0, 0], X[y==0, 1], color='red', marker='^', alpha=0.5)
plt.scatter(X[y==1, 0], X[y==1, 1], color='blue', marker='o', alpha=0.5)
plt.tight_layout()
plt.show()

  • 기본 PCA 적용
scikit_pca = PCA(n_components=2)

X_spca = scikit_pca.fit_transform(X)

fit, ax = plt.subplots(nrows=1, ncols=2, figsize=(7,3))

ax[0].scatter(X_spca[y==0, 0], X_spca[y==0, 1], color='red', marker='^', alpha=0.5)
ax[0].scatter(X_spca[y==1, 0], X_spca[y==1, 1], color='blue', marker='o', alpha=0.5)
ax[1].scatter(X_spca[y==0, 0], np.zeros((500,1))+0.02, color='red', marker='^', alpha=0.5)
ax[1].scatter(X_spca[y==1, 0], np.zeros((500,1))-0.02, color='blue', marker='^', alpha=0.5)

ax[0].set_xlabel('PC1')
ax[0].set_ylabel('PC2')
ax[1].set_ylim([-1, 1])
ax[1].set_yticks([])
ax[1].set_xlabel('PC1')

plt.tight_layout()
plt.show()

  • 적절한 gamma를 부여하여 RBF 커널 PCA를 구현
X_kpca = RbfKernelPCA.rbf_kernel_pca(X, gamma=15, n_components=2)

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(7,3))

ax[0].scatter(X_kpca[y==0, 0], X_kpca[y==0, 1], color='red', marker='^', alpha=0.5)
ax[0].scatter(X_kpca[y==1, 0], X_kpca[y==1, 1], color='blue', marker='o', alpha=0.5)
ax[1].scatter(X_kpca[y==0, 0], np.zeros((500,1))+0.02, color='red', marker='^', alpha=0.5)
ax[1].scatter(X_kpca[y==1, 0], np.zeros((500,1))-0.02, color='blue', marker='^', alpha=0.5)

ax[0].set_xlabel('PC1')
ax[0].set_ylabel('PC2')
ax[1].set_ylim([-1, 1])
ax[1].set_yticks([])
ax[1].set_xlabel('PC1')

plt.tight_layout()
plt.show()

새로운 데이터 포인트 투영

  • 앞선 예제는 하나의 데이터셋을 새로운 특성에 투영했는데, 실전에서는 하나 이상의 변환해야 할 데이터셋이 있다.
  • 장의 서두에서 보았던 기본 PCA 방법을 보면 변환 행렬과 입력 샘플 사이의 내적을 계산해서 데이터를 투영했다.
    • 변환 행렬의 열은 공분산 행렬에서 얻은 최상위 k 개의 고유 벡터(v) 이다.
  • 커널 PCA 이면의 아이디어로 돌아가 보면 중심을 맞춘 커널 행렬의 고유 벡터(a) 를 구했다.
    • 즉 샘플은 이미 주성분 축 v 에 투영되어 있다.
    • 새로운 샘플 x' 를 주성분 축에 투영하려면 다음을 계산해야 한다.

\phi (x')^{T} v

  • 다행히 커널 트릭을 사용하여 명시적으로 \phi (x')^{T} v 를 계산할 필요가 없다.
    • 기본 PCA와 다르게 커널 PCA는 메모리 기반 방법이다.
    • 즉, 새로운 샘플을 투영하기 위해 매번 원본 훈련 세트를 재사용해야 한다.
    • 훈련 세트에 있는 i 번째 새로운 샘플과 새로운 샘플 x' 사이 RBF 커널(유사도)을 계산해야 한다.

\phi (x')^{T} v = \sum_{i} a^{(i)} \phi (x')^{T} \phi (x^{(i)}) = \sum_{i} a^{(i)} \kappa (x', x^{(i)})

  • 커널 행렬 K 의 고유 벡터 a 와 고윳값 \lambda 는 다음 식을 만족한다.

Ka = \lambda a

  • 새로운 샘플과 훈련 세트의 샘플 간 유사도를 계산한 후 고윳값으로 고유 벡터 a 를 정규화 해야 한다.
from scipy.spatial.distance import pdist, squareform
from scipy import exp
from scipy.linalg import eigh
import numpy as np

def rbf_kernel_pca(X, gamma, n_components):
    """
   RBF 커널 PCA 구현

    매개변수
   ----------
   X: {넘파이 ndarray}, shape = [n_samples, n_features]

    gamma: float
     RBF 커널 튜닝 매개변수
    n_components: int
     변환한 주성분 개수   

    반환값
   -----------
   X_pc: {넘파이 ndarray}, shape = [n_samples, k_features]
      투영된 데이터셋
    """

   # M x N 차원의 데이터셋에서 샘플 간의 유클리디안 거리의 제곱을 계산
    sq_dists = pdist(X, 'sqeuclidean')

   # 샘플 간의 거리를 정방 대칭 행렬로 반환
    mat_sq_dists = squareform(sq_dists)

    # 커널 행렬을 계산
    K = exp(-gamma * mat_sq_dists)

   # 커널 행렬을 중앙에 맞춘다
    N = K.shape[0]
   one_n = np.ones((N, N)) / N
    K = K - one_n.dot(K) - K.dot(one_n) + one_n.dot(K).dot(one_n)

   # 중앙에 맞춰진 커널 행렬의 고윳값과 고유 벡터를 구한다.
    # scipy.linalg.eigh 함수는 오름차순으로 반환한다.
    eigvals, eigvecs = eigh(K)
   eigvals, eigvecs = eigvals[::-1], eigvecs[:, ::-1]

   # 최상위 k개의 고유 벡터를 선택한다
alphas = np.column_stack([eigvecs[:, i] for i in range(n_components)])

# 고유 벡터에 상응하는 고윳값을 선택한다.
lambdas = [eigvals[i] for i in range(n_components)]

return alphas, lambdas
  • 수정된 커널 PCA를 이용하여 반달 데이터 셋을 테스트 해 보자
X, y = make_moons(n_samples=100, random_state=123)
alphas, lambdas = RbfKernelPCA2.rbf_kernel_pca(X, gamma=15, n_components=1)

x_new = X[25]
x_proj = alphas[25]

def project_x(x_new, X, gamma, alphas, lambdas):
    pair_dist = np.array([np.sum((x_new - row)**2) for row in X])
   k = np.exp(-gamma * pair_dist)
    return k.dot(alphas/lambdas)

x_reproj = project_x(x_new, X, gamma=15, alphas=alphas, lambdas=lambdas)

plt.scatter(alphas[y==0, 0], np.zeros((50)), color='red', marker='^', alpha=0.5)
plt.scatter(alphas[y==1, 0], np.zeros((50)), color='blue', marker='o', alpha=0.5)
plt.scatter(x_proj, 0, color='black', label='original projection of point X[25]', marker='x', s=100)
plt.scatter(x_proj, 0, color='green', label='remapped point X[25]', marker='x', s=500)
plt.legend(scatterpoints=1)
plt.tight_layout()
plt.show()

사이킷런의 커널 PCA

  • 편리하게도 사이킷런은 sklearn.decomposition 모듈 아래 커널 PCA 클래스를 구현해 두었으므로 아래와 같이 사용하면 된다.
from sklearn.datasets import make_moons
from sklearn.decomposition import KernelPCA
import matplotlib.pyplot as plt

X, y = make_moons(n_samples=100, random_state=123)

scikit_kpca = KernelPCA(n_components=2, kernel='rbf', gamma=15)

X_skernpca = scikit_kpca.fit_transform(X)

plt.scatter(X_skernpca[y==0, 0], X_skernpca[y==0, 1], color='red', marker='^', alpha=0.5)
plt.scatter(X_skernpca[y==1, 0], X_skernpca[y==1, 1], color='blue', marker='o', alpha=0.5)
plt.xlabel('PC1')
plt.xlabel('PC2')
plt.tight_layout()
plt.show()

머신 러닝 교과서/ 좋은 훈련 세트 만들기: 데이터 전처리

누락된 데이터 다루기

  • 실제 애플리케이션에서는 여러 이유로 누락된 샘플이 있는 경우가 많다.

테이블 형태 데이터에서 누락된 값 식별

  • (CSV 파일에서 누락된 항목 체크하는 부분 생략. 데이터가 null 이거나 비어 있는 string인지를 체크를 하면 된다.)

누락된 값이 있는 샘플이나 특성 제외

  • (누락된 데이터가 있는 row를 drop 할 수도 있지만, 그런 데이터가 많은 경우 문제가 될 수 있다는 내용)

누락된 값 대체

  • 데이터를 제거하기 어려우면 보간 기법을 사용하여 데이터셋에 있는 다른 훈련 샘플로부터 누락된 값을 추정할 수 있다.
  • 가장 흔한 기법 중 하나는 평균으로 대체하는 것이다.
    • 사이킷런의 Imputer 클래스를 사용하면 간단히 처리할 수 있다.
    • (코드 생략)

사이킷런 추정기 API 익히기

  • Imputer 클래스는 데이터 변환에 사용되는 사이킷런의 변환기(transfomer) 클래스이다. 이런 추정기의 주요 메서드 두 개는 fit과 transform이다.
    • fit 메서드를 사용하여 훈련데이터에서 모델 파라미터를 학습한다.
    • transform 메서드를 사용하여 학습한 파라미터로 데이터를 변환한다.
    • 변환하려는 데이터 배열은 모델 학습에 사용한 데이터의 특성 개수와 같아야 한다.

  • 3장에서 사용한 분류기는 변환기 클래스와 개념상 매우 유사한 API를 가진 사이킷런의 추정기(estimator)이다.
    • 추정기는 predict 메서드가 있지만 transform 메서드도 가질 수 있다.
    • 추정기를 훈련할 때는 fit 메서드를 사용해서 모델의 파라미터를 학습했다.
    • 그 후 predict 메서드를 사용하여 새로운 데이터 샘플에 대한 예측을 만든다.

범주형 데이터 다루기

순서가 있는 특성과 순서가 없는 특성

  • 범주형 데이터에 관해 이야기할 때 순서가 있는 것과 없는 것을 구분해야 한다.
    • 순서가 있는 특성은 정렬하거나 차례대로 놓을 수 있는 범주형 특성으로 생각할 수 있다.
    • 예컨대 티셔츠 사이즈는 XL > L > M 과 같이 순서를 정할 수 있으므로 순서가 있는 특성이다.
    • 반면 티셔츠의 컬러는 순서가 없는 특성이다.

순서 특성 매핑

  • 학습 알고리즘이 순서 특성을 올바르게 인식하려면 문자열을 정수로 바꾸어야 한다.
    • 티셔츠의 size 특성(XL, L, M)은 숫자가 아니라서 이를 바꾸어주는 매핑 함수를 만들어야 한다.
  • (매핑하는 코드 예시 생략)

클래스 레이블 인코딩

  • 많은 머신 러닝 라이브러리는 클래스 레이블이 정수로 인코딩 되었을 것으로 기대한다.
  • (매핑 예시 코드 생략)

순서가 없는 특성에 원-핫 인코딩 적용

  • color와 같이 순서가 없는 특성을 숫자로 바꾸는 경우 학습 알고리즘이 blue가 red 보다 크다는 식의 순서를 부여할 수 있다.
  • 이 문제를 해결하기 위한 통상적인 방법은 원-핫 인코딩(one-hot encoding) 기법이다.
    • 이 방식의 아이디어는 순서 없는 특성에 들어 있는 고유한 값마다 새로운 더미(dummy) 특성을 만드는 것이다.
    • 예컨대 blue에 대한 샘플은 blue=1, red=0, green=0 과 같이 변환하여 사용하는 것이다.
  • 사이킷런의 preprocessing 모듈에 구현된 OneHotEncoder를 사용하면 이런 변환을 수행할 수 있다.
  • 원-핫 인코딩으로 더미 변수를 만드는 더 편리한 방법은 판다스의 get_dummies 메서드를 사용하는 것이다.
  • 원-핫 인코딩된 데이터셋을 사용할 때 다중 공선성(multicollinearity) 문제를 주의해야 한다.
    • 어떤 알고리즘에서는 이슈가 될 수 있다 (예컨대 역행렬을 구해야 하는 경우)
    • 특성 간의 상관관계가 높으면 역행렬을 계산하기 어려워 수치적으로 불안정해진다.
    • 변수 간의 상관관계를 감소하려면 원-핫 인코딩된 배열에서 특성 열 하나를 삭제한다. 이렇게 특성을 삭제해도 잃는 정보는 없는데, 예컨대 blue 열을 삭제해도, green = 0, red=0이면 blue임을 유추할 수 있다.

데이터셋을 훈련 세트와 테스트 세트로 나누기

  • (UCI 머신러닝 저장소에서 Wine 데이터셋을 받는 내용 생략)
  • 사이킷런의 model_selection 모듈에 있는 train_test_split 함수를 사용하면 데이터셋을 훈련 세트와 테스트 세트로 편리하게 나눌 수 있다.

특성 스케일 맞추기

  • 특성 스케일 조정은 전처리 파이프라인에서 아주 중요한 단계이다.
    • 결정 트리와 랜덤 포레스트는 특성 스케일 조정에 대해 걱정할 필요가 없는 몇 안 되는 알고리즘 중 하나이다.
    • 그러나 경사 하강법 같은 대부분의 머신 러닝과 최적화 알고리즘은 특성의 스케일이 같을 때 훨씬 성능이 좋다.
  • 두 개의 특성에서 첫 번째 특성이 1-10 사이의 scale을 갖고 있고 두 번째 특성이 1-100000 사이의 스케일을 갖는다고 가정하면, k-최근접 이웃 같은 알고리즘에서는 샘플 간의 거리가 두 번째 특성 축에 의해 좌우될 것이다.
  • 스케일이 다른 특성을 맞추는 대표적인 방법은 정규화(normalization)와 표준화(standardization)이 있다.
  • 정규화는 특성의 스케일을 [0, 1] 범위에 맞추는 것을 의미한다.
    • 최소-최대 스케일 변환(min-max scaling)의 특별한 경우이다.
    • 데이터를 정규화 하기 위해 다음과 같이 특성의 열마다 최소-최대 변환을 적용하여 샘플 x^{(i)} 에서 새로운 값 x_{norm}^{(i)} 을 계산한다.

x_{norm}^{(i)} = {x^{(i)} - x_{min} \over x_{max} - x_{min}}

  • 최소-최대 스케일 변환을 통한 정규화는 범위가 정해진 값이 필요할 때 유용하게 사용할 수 있는 일반적인 기법이다.
  • 표준화는 많은 머신러닝 알고리즘 특히 경사하강법 같은 최적화 알고리즘에서 널리 사용된다.
    • 표준화를 사용하면 특성의 평균을 0에 맞추고 표준편차를 1로 만들어 정규분포와 같은 특징을 가지도록 만든다. 이는 가중치를 더 쉽게 학습할 수 있도록 만든다.
    • 또 표준화는 이상치 정보가 유지되기 때문에 제한된 범위로 데이터를 조정하는 최소-최대 스케일 변환에 비해 알고리즘이 이상치에 덜 민감하다.

x_{sid}^{(i)} = {x^{(i)} - \mu_{x} \over \sigma_{x}}

  • 여기서 \mu_{x} 는 어떤 특성의 샘플 평균이고 \sigma_{x} 는 그에 해당하는 표준 편차이다.

유용한 특성 선택

  • 모델이 테스트 세트보다 훈련 세트에서 성능이 높다면 과대적합(overfitting)에 대한 강력한 신호이다.
    • 새로운 데이터에는 잘 인반화하지 못하기 때문에 모델 분산이 크다고 말한다.
    • 과대적합의 이유는 주어진 훈련 데이터에 비해 모델이 너무 복잡하기 때문이다.
  • 일반화 오차를 감소시키기 위해 많이 사용하는 방법은 다음과 같다.
    • 더 많은 훈련 데이터를 모은다.
    • 규제를 통해 복잡도를 제한한다.
    • 파라미터 개수가 적은 간단한 모델을 선택한다.
    • 데이터 차원을 줄인다.

모델 복잡도 제한을 위한 L1 규제와 L2 규제

  • 3장에서 L2 규제(L2 gerularization)는 개별 가중치 값을 제한하여 모델 복잡도를 줄이는 한 방법이라고 설명했다.
    • 가중치 벡터 w 의 L2 규제는 다음과 같이 정의한다.

L2:\|w\|_{2}^{2} = \sum_{j=1}^{m} w_{j}^{2}

  • 모델 복잡도를 줄이는 또 다른 방법은 L1 규제(L1 gerularization)이다.

L1:\|w\|_{2} = \sum_{j=1}^{m} |w_{j}|

  • 이는 가중치 제곱을 그냥 가중치 절댓값으로 바꾼 것이다.
    • L2 규제와 대조적으로 L1 규제는 보통 희소한 특성 벡터를 만든다.
    • 대부분의 특성 가중치가 0이 된다.
    • 실제로 관련 없는 특성이 많은 고차원 데이터셋일 경우 이런 희소성이 도움이 될 수 있다.
    • 특히 샘플보다 관련 없는 특성이 더 많은 경우이다.
    • 이런 맥락으로 보면 L1 규제는 특성 선택의 기법이 될 수 있다.

L2 규제의 기하학적 해석

  • L2 규제는 비용 함수에 페널티 항(penalty term)을 추가한다.
    • 규제가 없는 비용 함수로 훈련한 모델에 비해 가중치 값을 아주 작게 만드는 효과를 낸다.
  • L1 규제가 어떻게 희소성을 만드는지 이해하기 위해 규제의 기하학적 해석에 대해 고찰해 보자
    • 두 개의 가중치 값 w_{1} w_{2} 에 대한 볼록한 비용 함수의 등고선을 그려보면 다음과 같다.

  • 2장 아달린에서 사용했던 제곱 오차합(SSE) 비용 함수가 구 모양이어서 로지스틱 회귀의 비용 함수보다 그리기 쉽다.
    • 여기서 얻은 개념은 로지스틱 회귀에도 동일하게 적용 가능하다.
    • 그림 4-4와 같이 우리의 목표는 훈련 데이터에서 비용 함수를 최소화하는 가중치 값의 조합을 찾는 것임을 기억하라 (타원의 중심 포인트)
  • 규제를 더 작은 가중치를 얻기 위해 비용 함수에 추가하는 페널티 항으로 생각할 수 있다. 다른 말로 하면 큰 가중치를 제한한다.
    • 규제 파라미터 \lambda 로 규제의 강도를 크게 하면 가중치가 0에 가까워지고 훈련 데이터에 대한 모델 의존성은 줄어든다.
  • L2 페널티 항에 대한 이 개념을 아래 그림에 표현해 보자.

  • 위 그림에서 이차식인 L2 규제 항은 회색 공으로 표현되어 있다.
    • 가중치 값은 규제 예산을 초과할 수 없다. 즉, 가중치 값의 조합이 회색공 바깥에 놓일 수 없다.
    • 반면 우리는 여전히 비용 함수를 최소화해야 한다.
    • 페널티 제약이 있는 상황에서 최선은 L2 회색 공과 규제가 없는 비용 함수의 등고선이 만나는 지점이다.
    • 규제 파라미터 \lambda 가 커질수록 페널티 비용이 빠르게 증가하여 L2 공을 작게 만든다.
    • 예컨대 규제 파라미터를무한대로 증가하면 가중치 값이 L2 공의 중심인 0이 될 것이다.
  • 이 예시에서 중요한 핵심을 정리하면, 우리의 목표는 규제가 없는 비용과 페널티 항의 합을 최소화 하는 것이다.
    • 이는 모델을 학습할만한 충분한 훈련 데이터가 없을 때 편향을 추가하여 모델을 간단하게 만듦으로써 분산을 줄이는 것으로 해석할 수 있다.

L1 규제를 사용한 희소성

  • L1 규제 이면에 있는 주요 개념은 L2의 것과 유사하다.
    • L1 페널티는 가중치 절댓값의 합이기 때문에 아래 그림과 같이 다이아몬드 모양의 제한범위를 그릴 수 있다. (L2 항은 이차식)

  • 그림 4-6에서 w_{1} = 0 일 때 비용 함수의 등고선이 L1 다이아몬드와 만나는 것을 볼 수 있다.
    • L1 규제의 등고선은 날카롭기 때문에 비용 함수의 포물선과 L1 다이아몬드의 경계가 만나는 최적점은 축에 가까이 위치할 가능성이 높다.
    • 이것이 희소성이 나타나는 이유이다.
  • 규제의 강도를 달리하여 그래프를 그리면 아래와 같다.

순서 특성 선택 알고리즘

  • 모델 복잡도를 줄이고 과대적합을 피하는 다른 방법은 특성 선택을 통한 차원 축소(dimensionality reduction)이다. 이는 규제가 없는 모델에서 특히 유용하다.
  • 차원 축소 기버벵는 두 개의 주요 카테고리인 특성 선택(feature selection)과 특성 추출(feature extraction)이 있다.
    • 특성 선택은 원본 특성에서 일부를 선택하고 특성 추출은 일련의 특성에서 얻은 정보로 새로운 특성을 만든다.
    • 여기서는 특성 선택을 살펴보고 5장에서 특성 추출 기법에 대해 배워보겠다.
  • 순차 특성 선택(sequential feature selection) 알고리즘은 탐욕적 알고리즘(greedy search algorithm)으로 초기 d 차원의 특성 공간을 k < d k s=2 차원의 특성 부분 공간으로 축소한다.
    • 특성 선택 알고리즘은 주어진 문제에 가장 관련이 높은 특성 부분 집합을 자동으로 선택하는 것이 목적으로 관계 없는 특성이나 잡음을 제거하여 계산 효율성을 높이고 모델의 일반화 오차를 줄인다.
    • 규제를 제공하지 않는 알고리즘을 사용할 때 유용하다.
  • 전통적인 순차 특성 알고리즘은 순차 후진 선택 (Sequential Backward Selection, SBS)이다.
    • 계산 효율성을 향상하기 위해 모델 성능을 가능한 적게 희생하면서 초기 특성의 부분 공간으로 차원을 축소한다.
    • 과대적합의 문제를 안고 있는 모델이라면 SBS가 예측 성능을 높일 수도 있다.
  • SBS 알고리즘 이면의 아이디어는 매우 간단하다.
    • SBS는 새로운 특성의 부분 공간이 목표하는 특성 개수가 될 때까지 전체 특성에서 순찾거으로 특성을 제거한다.
    • 각 단계에서 어떤 특성을 제거할지 판단하기 위해 최소화할 기준 함수를 정의한다.
    • 기준 함수에서 계산한 값은 어떤 특성을 제거하기 전후의 모델 성능 차이이다.
    • 각 단계에서 제거할 특성은 기준 값이 가장 큰 특성으로 정의할 수 있다.
    • 이해하기 쉽게 말하면 각 단계에서 제거했을 때 성능 손실이 최대가 되는 특성을 제거한다.
  • SBS 정의에 따라 이 알고리즘을 간단히 네 단계로 정의할 수 있다.
    1. 알고리즘을 k = d 로 초기화한다. d 는 전체 특성 공간 X_{d} 의 차원이다.
    2. 조건 x^{-} = arg max J(X_{k} - x) 를 최대화하는 특성 x^{-} 를 결정한다. 여기서 x \in X_{k} 이다.
    3. 특성 집합에서 특성 x^{-} 를 제거한다. 즉 X_{k-1}:=X_{k} - x^{-}; k:=k-1 이다.
    4. k 가 목표하는 특성 개수가 되면 종료한다. 아니면 2단계로 돌아간다.
  • SBS 알고리즘은 아직 사이킷런에 구현되어 있지 않다. 간단한 알고리즘이므로 다음과 같이 직접 구현할 수 있다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data', header=None)

X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)

sc = StandardScaler()
sc.fit(X_train)
X_train_std = sc.transform(X_train)
X_test_std = sc.transform(X_test)

fig = plt.figure()
ax = plt.subplot(111)

colors = ['blue', 'green', 'red', 'cyan', 'magenta', 'yellow', 'black', 'pink', 'lightgreen', 'lightblue', 'gray', 'indigo', 'orange']

weights, params = [], []

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

weights = np.array(weights)

for column, color in zip(range(weights.shape[1]), colors):
    plt.plot(params, weights[:, column], label=df_wine.columns[column+1], color=color)

plt.axhline(0, color='black', linestyle='--', linewidth=3)
plt.xlim([10**(-5), 10**5])
plt.ylabel('weight coefficient')
plt.xlabel('C')
plt.xscale('log')
plt.legend(loc='upper left')
ax.legend(loc='upper center', bbox_to_anchor=(1.38, 1.03), ncol=1, fancybox=True)
plt.show()
  • 위 SBS 구현을 이용해서 KNN 분류기의 정확도를 그리면 다음과 같다.
    • 아래 그림에서 볼 수 있듯 특성 개수가 줄어들었을 때 검증 세트에서 KNN 분류기의 정확도가 향상되었다. 이는 3장의 KNN 알고리즘에서 설명했던 차원의 저주가 감소하기 때문이다.

랜덤 포레스트의 특성 중요도 사용

  • 데이터셋에 유용한 특성을 선택하는 또 다른 방법은 랜덤 포레스트를 사용하는 것이다.
    • 랜덤 포레스트를 사용하면 앙상블에 참여한 모든 결정 트리에서 계산한 평균적인 불순도 감소로 특성 중요도를 측정할 수 있다.
    • 데이터셋이 선형적으로 구분 가능한지 여부를 가정할 필요가 없다.
  • 편리하게도 사이킷런의 랜덤 포레스트 구현은 특성 중요도 값을 이미 수집하고 있다.
    • RandomForestClassifier 모델을 훈련한 후 feature_importances_ 속성에서 확인할 수 있다.
  • 다음은 Wine 데이터셋에서 500개의 트리를 가진 랜덤 포레스트를 훈련하고 각각의 중요도에 따라 13개의 특성에 순위를 매긴 결과이다.
    • 트리 기반 모델은 표준화나 정규화를 할 필요가 없다.

  • 500개의 결정 트리에서 평균적인 불순도 감소를 기반으로 이 데이터셋에서 가장 판별력이 좋은 특성은 Proline, Flavanoids, Color intensity, OD280/OD315 of diluted wines, Alcohol이다.
  • 랜덤 포레스트에서 두 개 이상의 특성이 매우 상관관계가 높다면 하나의 특성은 매우 높은 순위를 갖지만 다른 특성 정보는 완전히 잡아내지 못할 수 있다.
    • 특성 중요도 값을 해석하는 것보다 모델의 예측 성능에만 관심이 있다면 이 문제를 신경 쓸 필요는 없다.

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

분류 알고리즘 선택

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

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

  • 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 매개변수로 지정할 수 있다.

머신 러닝 교과서/ 간단한 분류 알고리즘 훈련

인공 뉴런: 초기 머신러닝의 간단한 역사

  • 1943년 워런 맥컬록과 월터 피츠는 처음으로 간소화된 뇌의 뉴런 개념을 발표함. 이를 맥컬록-피츠(MCP) 뉴런이라고 한다.
    • 맥컬록과 피츠는 신경 세포를 이진 출력을 내는 간단한 논리 회로로 표현했다. 수상 돌기에 여러 신호가 도착하면 세포체에 합쳐지고 합쳐진 신호가 특정 임계값을 넘으면 출력 신호가 생성되고 축삭 돌기를 이용해서 전달 됨
  • 몇 년 후 프랑크 로젠블라트는 MCP 뉴런 모델을 기반으로 퍼셉트론 학습 개념을 처음 발표함.
    • 퍼셉트론 규칙에서 로젠블라트는 자동으로 최적의 가중치를 학습하는 알고리즘을 제안함.
    • 이 가중치는 뉴런의 출력 신호를 낼지 말지를 결정하기 위해 입력 특성에 곱하는 계수.
    • 지도 학습과 분류 개념으로 말하면 이 알고리즘으로 샘플이 한 클래스에 속하는지 아닌지를 예측할 수 있다.

인공 뉴런의 수학적 정의

  • 인공 뉴런(artifical neuron) 아이디어를 두 개의 클래스가 있는 이진 분류(binary classification) 작업으로 볼 수 있다.
    • 두 클래스는 간단하게 1(양성)과 -1(음성)으로 나타낸다. 그 다음 입력값 x 와 이에 상응하는 가중치 벡터 w 의 선형 조합으로 결정함수(\phi(z) )를 정의한다.
    • 최종 입력(net input)인 z z = w_{1} + x_{1} + ... + w_{m} x_{m} 이다.

w = \left[ \begin{array}{rrr} w_{1} \\ ... \\ w_{m} \end{array} \right], x = \left[ \begin{array}{rrr} x_{1} \\ ... \\ x_{m} \end{array} \right]

  • 이제 특정 샘플 x^{(i)} 의 최종 입력이 사전에 정의된 임계 값 \theta 보다 크면 클래스 1로 예측하고 그렇지 않으면 클래스 -1로 예측한다.
    • 퍼셉트론 알고리즘에서 결정함수 \phi(\cdot) 는 단위 계단 함수(unit step function)를 변형한 것이다.

\phi(z) = \begin{cases} 1 & z \geq \theta \\ -1 & else \end{cases}

  • 식을 간단하게 만들기 위해 임계 값 \theta 를 식의 왼조긍로 옮겨 w_{0} = -\theta x_{0} = 1 인 0번째 가중치를 정의한다. 이렇게 하면 z 를 좀 더 간단하게 쓸 수 있다.

z = w_{0}x_{0} + w_{1}x_{1} + ...  + w_{m}x_{m} = w^{T}x

  • 결정 함수는 다음과 같다.

\phi(z) = \begin{cases} 1 & z \geq \theta \\ -1 & else \end{cases}

  • 머신 러닝 분야에서 음수 임계 값 또는 가중치 w_{0} = -\theta 를 절편이라고 한다.
  • 그림 2-2는 퍼셉트론 결정함수로 최종 입력 z = w^{T}x 가 이진 출력 (-1 또는 1)으로 압축되는 방법(왼쪽)과 이를 사용하여 선형 분리가 가능한 두 개의 클래스 사이를 구별하는 방법(오른쪽)을 나타낸다.

퍼셉트론 학습 규칙

  • MCP 뉴런과 로젠블라트의 임계 퍼셉트론 모델 이면에 있는 전반적인 아이디어는 뇌의 뉴런 하나가 작동하는 방식을 흉내내려는 환원주의(reductionism) 접근 방식을 사용한 것이다.
    • 즉 출력을 내거나 내지 않는 두 가지 경우만 있다. 따라서 로젠블라트의 초기 퍼셉트론 학습 규칙을 요약하면 다음과 같다.
      1. 가중치를 0 또는 랜덤한 작은 값으로 초기화 한다.
      2. 각 훈련 샘플 x^{(i)} 에서 다음 작업을 한다.
        1. 출력값 \hat{y} 를 계산한다.
        2. 가중치를 업데이트 한다.
  • 여기서 출력 값은 앞서 정의한 단위 계단 함수로 예측한 클래스 레이블이다. 가중치 벡터 w 에 있는 개별 가중치 w_{j} 가 동시에 업데이트 되는 것을 다음과 같이 쓸 수 있다.

w_{j} := w_{j} + \Delta w_{j}

  • 가중치 w_{j} 를 업데이트 하는데 사용되는 \Delta w_{j} 값은 퍼셉트론 학습 규칙에 따라 계산된다.

\Delta w_{j} = \eta (y^{(i)} - \hat{y}^{(i)}) x_{j}^{(i)}

  • 여기서 \eta (eta)는 학습률(learning rate)이다. (일반적으로 0.0에서 1.0 사이의 값)
    • y^{(i)} i 번째 훈련 샘플의 진짜 클래스 레이블이다.
    • \hat{y}^{(i)} 는 예측 클래스 레이블이다.
  • 가중치 벡터의 모든 가중치를 동시에 업데이트 한다는 점이 중요하다. 즉, 모든 가중치 \Delta w_{j} 를 업데이트 하기 전에 \hat{y}^{(i)} 를 다시 계산하지 않는다.
  • 구체적으로 2차원 데이터셋에서는 다음과 같이 업데이트 된다.

\Delta w_{0} = \eta (y^{(i)} - output^{(i)})

\Delta w_{1} = \eta (y^{(i)} - output^{(i)}) x_{1}^{i}

\Delta w_{2} = \eta (y^{(i)} - output^{(i)}) x_{2}^{i}

  • 퍼셉트론 규칙이 어떻게 작동하는지 알아보자.
  • 퍼셉트론이 클래스 레이블을 정확히 예측한 다음의 두 경우는 가중치가 변경되지 않고 그대로 유지된다.

\Delta w_{j} = \eta (1 - 1) x_{j}^{i} = 0

\Delta w_{j} = \eta ((-1) - (-1)) x_{j}^{i} = 0

  • 잘못 예측했을 때는 가중치를 양성 또는 음성 타깃 클래스 방향으로 이동시킨다.

\Delta w_{j} = \eta (1 - (-1)) x_{j}^{i} = \eta(2)x_{j}^{(i)}

\Delta w_{j} = \eta (-1 - 1) x_{j}^{i} = \eta(-2)x_{j}^{(i)}

  • 곱셈 계수인 x_{j}^{(i)} 를 좀 더 잘 이해하기 위해 다른 예를 살펴보겠다.

y^{(i)} = 1, \hat{y}_{j}^(i) = -1, \eta = 1

  •  x_{j}^{(i)} = 0.5 일 때 이 샘플을 -1로 잘못 분류했다고 가정한다.
    • 이때 가중치가 1 만큼 증가되어 다음 번에 이 샘플을 만났을 때 최종 입력 x_{j}^{(i)} \times w_{j}^{(i)} 가 더 큰 양수가 된다.
    • 단위 계단 함수의 임계 값보다 커져 샘플이 +1로 분류될 가능성이 높아질 것이다.

\Delta w_{j}^{(i)} = (1 - (-1)) 0.5 = (2) 0.5 = 1

  • 가중치 업데이트는 x_{j}^{(i)} 값에 비례한다. 예컨대 다른 샘플 x_{j}^{(i)} = 2 를 -1로 잘못 분류했다면 이 샘플을 다음번에 올바르게 분류하기 위해 더 크게 결정 경게를 움직인다.

\Delta w_{j}^{(i)} = (1 - (-1)) 2 = (2) 2 = 4

  • 퍼셉트론은 두 클래스가 선형적으로 구분되고 학습률이 충분히 작을 때만 수렴이 보장된다.
    • 두 클래스를 선형 결정 경계로 나눌 수 없다면 훈련 데이터셋을 반복할 최대 횟수(에포크(epoch))를 지정하고 분류 허용 오차를 지정할 수 있다. 그렇지 않으면 퍼셉트론 가중치 업데이트를 멈추지 않는다.

  • 그림 2-4는 퍼셉트론이 샘플 x_{j}^{(i)} 를 입력으로 받아 가중치 x_{j}^{(i)} 를 연결하여 최종 입력을 계산하는 방법을 보여준다.
    • 그 다음 최종 입력은 임계 함수로 전달되어 샘플의 예측 클래스 레이블인 -1 또는 +1의 이진 출력을 만든다.
    • 학습 단계에서 이 출력을 사용하여 예측 오차를 계산하고 가중치를 업데이트 한다.

파이썬으로 퍼셉트론 학습 알고리즘 구현

객체 지향 퍼셉트론 API

  • 객체 지향 방식을 사용하여 퍼셉트론 인터페이스를 가진 파이썬 클래스를 정의
    • Perceptron 객체를 초기화한 후 fit 메서드로 데이터에서 학습하고, 별도의 predict 메서드로 예측을 만든다.
    • 관례에 따라 초기화 과정에서 생성하지 않고 다른 메서드를 호출하여 만든 속성에는 밑줄(_)을 추가한다. ex) self.w_
import numpy as np

class Perceptron(object):

    """ 퍼셉트론 분류기

    매개변수
   ----------
   eta : float
       학습률 (0.0과 1.0사이)
    n_iter : int
       훈련 데이터셋 반복 횟수
    random_state : int
       가중치 무작위 초기화를 위한 난수 생성기 시드

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

   def __init__(self, eta=0.01, n_iter=50, 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.1, size = 1 + X.Shape[1])
        self.errors_ = []

       for _ in range(self.n_iter):
            errors = 0

           for xi, target in zip(X, y):
               update = self.eta * (target - self.predict(xi))
                self.w_[1:] += update * xi
                self.w_[0] += update
                errors += int(update != 0.0)

            self.errors_.append(errors)

        return self

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

    def predict(self, X):
       """ 단위 계단 함수를 사용하여 클래스 레이블을 반환합니다 """
       return np.where(self.net_input(X) >= 0.0, 1, -1)
  • 이 퍼셉트론 구현을 사용하여 학습률 eta와 에포크 횟수(훈련 데이터를 반복하는 횟수) n_iter로 새로운 Perceptron 객체를 초기화 한다.
    • fit 메서드에서 self.w_ 가중치를 벡터 \mathbb{R}^{m + 1} 로 초기화한다.
    • 여기서 m은 데이터셋에 있는 차원(특성) 개수이다.
    • 벡터의 첫 번째 원소인 절편을 위해 1을 더했다. 즉 이 벡터의 첫 번째 원소 self.w_[0]는 앞서 언급한 절편이다.
  • 이 벡터는 rgen.normal(loc = 0.0, scale = 0.01, size = 1 + X.shape[1])을 사용하여 표준 편차가 0.01인 정규 분포에서 뽑은 랜덤한 작은 수를 담고 있다.
    • 여기서 rgen은 넘파이 난수 생성기로 사용자가 지정한 랜덤 시드(seed)로 이전과 동일한 결과를 재현할 수 있다.
  • 가중치를 0으로 초기화 하지 않는 이유는 가중치가 0이 아니어야 학습률 \eta 가 분류 결과에 영향을 주기 때문이다.
    • 가중치가 0으로 초기화되어 있다면 학습률 파라미터 eta는 가중치 벡터의 방향이 아니라 크기에만 영향을 미친다.
  • fit 메서드는 가중치를 초기화한 후 훈련 세트에 있는 모든 개개의 샘플을 반복 순회하면서 이전 절에서 설명한 퍼셉트론 학습 규칙에 따라 가중치를 업데이트 한다.
    • 클래스 레이블은 predict 메서드에서 예측한다.
    • fit 메서드에서 가중치를 업데이트하기 위해 predict 메서드를 호출하여 클래스 레이블에 대한 예측을 얻는다.
    • predict 메서드는 모델이 학습되고 난 후 새로운 데이터의 클래스 레이블을 예측하는데도 사용할 수 있다.
  • 에포크마다 self.errors_ 리스트에 잘못 분류된 횟수를 기록한다. 나중에 훈련하는 동안 얼마나 퍼셉트론을 잘 수행했는지 분석할 수 있다.

붓꽃 데이터셋에서 퍼셉트론 훈련

  • 앞서 만든 퍼셉트론 구현을 테스트 하기 위해 붓꽃 데이터셋에서 Setosa, Versicolor 두 개의 클래스만 사용한다.
    • 퍼셉트론 알고리즘은 다중 클래스 분류로 확장이 가능한데, 일대다(one-versus-all, OvA) 전략 등을 사용하면 된다.
  • pandas 라이브러리를 사용하여 붓꽃 데이터셋을 DataFrame 형식으로 읽고 꽃받침 길이와 꽃잎 길이를 추출하여 2차원 산점도를 그리면 그림 2-6과 같다.
import matplotlib.pyplot as plt
import numpy as np

# setosa와 versicolor를 선택
y = df.iloc[0:100, 4].values
y = np.where(y == 'Iris-setosa', -1, 1)

# 꽃받침 길이와 꽃잎 길이를 추출
X = df.iloc[0:100, [0,2]].values

# 산점도를 그린다.
plt.scatter(X[:50, 0], X[:50, 1], color='red', marker='o', label='setosa')
plt.scatter(X[50:100, 0], X[50:100, 1], color='blue', marker='x', label='versicolor')
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc='upper left')
plt.show()

  • 이 데이터셋에서 추출한 일부 데이터를 이용하여 퍼셉트론 알고리즘을 훈련하고 에포크 대비 잘못 분류된 오차를 그래프로 그리면 다음과 같다.
ppn = Perceptron(eta=0.1, n_iter=10)
ppn.fit(X, y)
plt.plot(range(1, len(ppn.errors_) + 1), ppn.errors_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Number of errors')
plt.show()

  • 퍼셉트론은 6번째 에포크 이후에 수렴했고 훈련 샘플을 완벽하게 분류했다.
  • 최종적으로 데이터셋의 결정 경계를 시각화 하면 아래 그림과 같다.
from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, resolution=0.02):
    # 마커와 컬러맵을 설정
   markers = ('s', 'x', '0', '^', '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.ylim(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')

plot_decision_regions(X, y, classifier=ppn)
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc = 'upper left')
plt.show()

적응형 선형 뉴런과 학습의 수렴

  • 이 절에서는 단일층 신경망의 또 다른 종류인 적응형 선형 뉴런(Adaptive Linear Neuron, ADALINE)을 살펴보겠다.
    • 버나드 위드로우와 테드 호프는 프랑크 로젠블라트의 퍼셉트론 알고리즘 이 후 몇 년 지나지 않아 아달린(Adaline)을 발표했는데, 이는 퍼셉트론의 향상된 버전으로 볼 수 있다.
    • 아달린은 연속 함수(continous function)으로 비용 함수를 정의하고 최소화하는 핵심 개념을 보여주기 때문에 흥미롭다.
  • 아달린 규칙(위드로우-호프 규칙이라고도 함)과 로젠블라트 퍼셉트론의 가장 큰 차이점은 가중치를 업데이트 하는데 퍼셉트론처럼 단위 계단 함수 대신 선형 활성화 함수를 사용한다는 것이다.
    • 아달린에서 선형 활성화 함수 \phi(z) 는 최종 입력과 동일한 함수이다.

\phi (w^{T}x) = w^{T} x

  • 선형 활성화 함수가 가중치 학습에 사용되지만 최종 예측을 만드는데 여전히 임계 함수를 사용하는데, 이는 앞서 보았던 단위 계단 함수와 비슷하다.
    • 퍼셉트론과 아달린 알고리즘의 차이는 아래 그림과 같다.

  • 아달린 알고리즘은 진짜 클래스 레이블과 선형 활성화 함수의 실수 출력 값을 비교하여 모델의 오차를 계산하고 가중치를 업데이트 한다.
    • 반면 퍼셉트론은 진짜 클래스 레이블과 예측 클래스 레이블을 비교한다.

경사 하강법으로 비용 함수 최소화

  • 지도 학습 알고리즘의 핵심 구성 요소는 학습 과정 동안 최적화 하기 위해 정의한 목적 함수(object function)이다.
    • 종종 최소화하려는 비용 함수가 목적 함수가 된다.
    • 아달린은 계산된 출력과 진짜 클래스 레이블 사이의 제곱 오차합(Sum of Squared Errors, SSE)으로 가중치를 학습할 비용 함수 J 를 정의한다.

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

  • 1/2 항은 그래디언트(gradient)를 간소하게 만들려고 편의상 추가한 것이다.
  • 단위 계단 함수 대신 연속적인 선형 활성화 함수를 사용하는 장점은 비용 함수가 미분 가능해진다는 것이다.
    • 이 비용 함수의 또 다른 장점은 볼록 함수라는 것이다. 간단하지만 강력한 최적화 알고리즘인 경사 하강법(gradient descent)을 적용하여 붓꽃 데이터셋의 샘플을 분류하도록 비용 함수를 최소화하는 가중치를 찾을 수 있다.
  • 아래 그림에서는 경사 하강법 이면에 있는 핵심 아이디어를 지역 또는 전역 최솟값에 도달할 때까지 언덕을 내려오는 것으로 묘사한다.
    • 각 반복에서 경사의 반대 방향으로 진행한다.
    • 진행 크기는 경사의 기울기와 학습률로 결정한다.

  • 경사 하강법을 사용하면 비용 함수 J(w) 의 그래디언트 \nabla J(w) 반대 방향으로 조금씩 가중치를 업데이트 한다.

w := w + \Delta w

  • 가중치 변화량 \Delta w 는 음수의 그래디언트에 학습률 \eta 를 곱한 것으로 정의한다.

\Delta w = - \eta \nabla J(w)

  • 비용 함수의 그래디언트를 계산하려면 각 가중치 w_{j} 에 대한 편도 함수를 계산해야 한다.

{\partial J \over \partial w_{j}} = - \sum_{i} (y^{(i)} - \phi(z^{(i)})) x_{j}^{(i)}

  • 따라서 가중치 w_{j} 의 업데이트 공식을 다음과 같이 쓸 수 있다.

\Delta w_{j} = - \eta {\partial J \over \partial w_{j}} = \eta \sum_{i} (y^{(i)} - \phi(z^{(i)})) x_{j}^{(i)}

  • 모든 가중치가 동시에 업데이트 되기 때문에 아달린 학습 규칙은 다음과 같다.

w := w + \Delta w

  • 아달린 학습 규칙이 퍼셉트론 규칙과 동일하게 보이지만 z^{(i)} = w^{T} x^{(i)} \phi(z^{(i)}) 는 정수 클래스로 레이블이 아니고 실수이다.
    • 또 훈련 세트에 있는 모든 샘플을 기반으로 가중치 업데이트를 계산한다.
    • 이 방식을 배치 경사 하강법(batch gradient descent)라고도 한다.

파이썬으로 아달린 구현

  • 퍼셉트론 규칙과 아달린이 매우 비슷하기 때문에 앞서 정의한 퍼셉트론 구현에서 fit 메서드를 바꾸어 경사 하강법으로 비용 함수가 최소화 되도록 가중치를 업데이트 한다.
import numpy as np

class AdalineGD(object):
   """ 적응형 선형 뉴런 분류기
    매개변수
   ----------
   eta : float
       학습률 (0.0과 1.0사이)
    n_iter : int
       훈련 데이터셋 반복 횟수
    random_state : int
        가중치 무작위 초기화를 위한 난수 생성기 시드

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

    def __init__(self, eta = 0.01, n_iter = 50, 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 = (errors**2).sum() / 2.0
           self.cost_.append(cost)       

        return self

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

    def activation(self, X):
       """ 선형 활성화 계산 """
        return X

   def predict(self, X):
       """ 단위 계단 함수를 사용하여 클래스 레이블을 반환합니다 """
       return np.where(self.net_input(X) >= 0.0, 1, -1)
  • 퍼셉트론처럼 개별 훈련 샘플마다 평가한 후 가중치를 업데이트 하지 않고, 전체 훈련 데이터셋을 기반으로 그래디언트를 계산한다.
    • 절편(0번째 가중치)은 self.eta * errors.sum() 이고 가중치 1에서 m까지는 self.eta * X.T.dot(errors)이다.
    • 여기서 X.T.dot(errors)는 특성 행렬과 오차 벡터 간의 행렬-벡터 곰셈이다.
  • 이 코드의 activation 메서드는 단순한 항등 함수(identity function)이기 때문에 아무런 영향을 미치지 않는다.
    • 단일층 신경망을 통해 정보가 어떻게 흘러가는지를 표시하려고 추가했다.
  • 입력 데이터의 특성에서 최종 입력, 활성화, 출력 순으로 진행된다.
    • 다음 장에서는 항등 함수가 아니고 비선형 활성화 함수를 사용하는 로지스틱 회귀 분류기를 다룰 것인데, 로지스틱 회귀 모델은 활성화 함수와 비용 함수만 다르고 아달린과 매우 비슷하다.
fit, ax = plt.subplots(nrows=1, ncols=2, figsize=(10,4))

ada1 = AdalineGD(n_iter=10, eta=0.01).fit(X, y)

ax[0].plot(range(1, len(ada1.cost_) + 1), np.log10(ada1.cost_), marker='o')
ax[0].set_xlabel('Epochs')
ax[0].set_ylabel('log(Sum-squared-error)')
ax[0].set_title('Adaline - Learning rate 0.01')

ada2 = AdalineGD(n_iter=10, eta=0.0001).fit(X, y)

ax[1].plot(range(1, len(ada2.cost_) + 1), ada2.cost_, marker='o')
ax[1].set_xlabel('Epochs')
ax[1].set_ylabel('Sum-squared-error')
ax[1].set_title('Adaline - Learning rate 0.0001')

plt.show()
  • 출력된 비용 함수 그래프에서 볼 수 있 듯 두 개의 다른 문제가 발생한다.
    • 아래 그림 2-11 왼쪽 그래프는 학습률이 너무 클 때 발생한다.
    • 비용 함수를 최소화 하지 못하고 오차는 에포크마다 점점 더 커진다. 전역 최솟값을 지나쳤기 때문이다.
    • 반면 오른쪽 그래프에서는 비용이 감소하지만 학습률 \eta = 0.0001 은 너무 작기 때문에 알고리즘이 전역 최솟값에 수렴하려면 아주 많은 에포크가 필요하다.

  • 아래 그림 2-12는 비용 함수 J 를 최소화하려고 특정 가중치 값을 바꾸었을 때 어떤 일이 일어나는지 보여준다.
    • 왼쪽 그림은 적절하게 선택한 학습률의 경우로, 비용이 점차 감소하여 전역 최솟값 방향으로 이동한다.
    • 오른쪽 그림은 너무 큰 학습률을 선택하여 전역 최솟값을 지나친다.

특성 스케일을 조정하여 경사 하강법 결과 향상

  • 책에서 살펴볼 머신 러닝 알고리즘들은 최적의 성능을 위해 어떤 식으로든 특성 스케일을 조정하는 것이 필요하다. 이는 3, 4장에서 살펴보겠다.
  • 경사 하강법은 특성 스케일을 조정하여 혜택을 볼 수 있는 많은 알고리즘 중 하나이다. 이 절에서는 표준화(standardization)라고 하는 특성 스케일 방법을 사용하겠다.
    • 데이터에 표준 정규 분포의 성질을 부여하여 경사 하강법 학습이 좀 더 빠르게 수렴되도록 돕는다.
    • 표준화는 각 특성의 평균을 0에 맞추고 특성의 표준 편차를 1로 만든다.
    • 예컨대 j 번째 특성을 표준화 하려면 모든 샘플에서 평균 \mu_{j} 을 빼고 표준 편차 \sigma_{j} 로 나누면 된다.

x'_{j} = {x_{j} - \mu_{j} \over \sigma_{j}}

  • 여기서 x_{j} n 개의 모든 훈련 샘플에서 j 번째 특성 값을 포함한 벡터이다.
    • 표준화 기법을 데이터셋의 각 특성 j 에 적용한다.
    • 표준화가 경사 하강법 학습에 도움이 되는 이유 중 하나는 그림 2-13에 나온 것처럼 더 적은 단계를 거쳐 최적 혹은 좋은 솔루션을 찾기 때문이다.
    • 그림 2-13은 2차원 분류 문제에서 모델의 가중치에 따른 비용 함수의 등고선을 보여준다.

  • 표준화를 적용한 결과는 아래 그림 2-14와 같다.
    • 이 그래프에서 볼 수 있듯이 학습률 \eta = 0.01 을 사용하고 표준화된 특성에서 훈련하니 아달린 모델이 수렴했다.
    • 모든 샘플이 완벽하게 분류되더라도 SSE가 0이 되지는 않는다.
# 표준화
X_std = np.copy(X)
X_std[:,0] = (X[:,0] - X[:,0].mean()) / X[:,0].std()
X_std[:,1] = (X[:,1] - X[:,1].mean()) / X[:,1].std()

ada = AdalineGD(n_iter=15, eta=0.01)
ada.fit(X_std, y)

plot_decision_regions(X_std, y, classifier=ada)

plt.title('Adaline - Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Sum-squared-error')
plt.show()

대규모 머신 러닝과 확률적 경사 하강법

  • 전체 훈련 세트에서 계산한 그래디언트 반대 방향으로 한 걸음씩 진행하여 비용 함수를 최소화 하는 방식을 배치 경사 하강법이라고도 부른다.
    • 수백 만개의 데이터셋이 존재하는 경우 배치 경사 하강법을 사용하면 계산 비용이 매우 많이 든다. 전역 최솟값으로 나아가는 단계마다 매번 전체 훈련 데이터셋을 다시 평가해야 하기 때문이다.
  • 확률적 경사 하강법(stochastic gradient descent)은 배치 경사 하강법의 다른 대안으로 인기가 높다. 이따금 반복 또는 온라인 경사 하강법이라고도 부른다.
    • 다음 첫 번째 수식처럼 모든 샘플 x^{(i)} 에 대하여 누적된 오차의 합을 기반으로 가중치를 업데이트하는 대신 두 번째 수식처럼 각 훈련 샘플에 대해서 조금씩 가중치를 업데이트 한다.

\Delta w = \eta \sum_{i} (y^{(i)} - \phi(z^{(i)})) x^{(i)}

\Delta w = \eta (y^{(i)} - \phi(z^{(i)})) x^{(i)}

  • 확률적 경사 하강법을 경사 하강법의 근사로 생각할 수 있지만 가중치가 더 자주 업데이트 되기 때문에 수렴 속도가 훨씬 빠르다.
    • 그래디언트가 하나의 훈련 샘플을 기반으로 계산되므로 오차의 궤적은 배치 경사 하강법보다 훨씬 어지럽다.
    • 비선형 비용 함수를 다룰 때 얕은 지역 최솟값을 더 쉽게 탈출할 수 있어 장점이 되기도 한다.
    • 확률적 경사 하강법에서 만족스러운 결과를 얻으려면 훈련 샘플 순서를 무작위하게 주입하는 것이 중요하다.
    • 또 순환되지 않도록 에포크마다 훈련 세트를 섞는 것이 좋다.
  • 확률적 경사 하강법의 또 다른 장점은 온라인 학습(online learning)으로 사용할 수 있다는 것이다.
    • 온라인 학습에서 모델은 새로운 훈련 데이터가 도착하는대로 훈련된다. 많은 양의 훈련 데이터가 있을 때도 유용하다.
    • 예컨대 고객 데이터를 처리하는 웹 애플리케이션에서 온라인 학습을 사용해서 시스템은 변화에 즉시 적응할 수 있다.
  • 확률적 경사 하강법으로 소스를 고치면 다음과 같다.
import numpy as np

class AdalineSGD(object):
   """ 적응형 선형 뉴런 분류기
    매개변수
   ----------
   eta : float
       학습률 (0.0과 1.0사이)
    n_iter : int
       훈련 데이터셋 반복 횟수
    random_state : int
        가중치 무작위 초기화를 위한 난수 생성기 시드

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

    def __init__(self, eta = 0.01, n_iter = 50, shuffle=True, random_state = None):
       self.eta = eta
       self.n_iter = n_iter
       self.w_initialized = False
        self.shuffle = shuffle
        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

        """

       self._initialize_weights(X.shape[1])
        self.cost_ = []

       for i in range(self.n_iter):
            if self.shuffle:
               X, y = self._shuffle(X, y)           

            cost = []

           for xi, target in zip(X, y):
                cost.append(self._update_weights(xi, target))

            avg_cost = sum(cost) / len(y)
           self.cost_.append(avg_cost)       

        return self

    def partial_fit(self, X, y):
        """ 가중치를 다시 초기화하지 않고 훈련 데이터를 학습합니다 """
       if not self.w_initialized:
           self.w_initialized_weights(X.shape[1])      

        if y.ravel().shape[0] > 1:
           for xi, target in zip(X, y):
               self._update_weights(xi, target)       
        else:
           self._update_weights(X, y)       

        return self

   def _shuffle(self, X, y):
       """ 훈련 데이터를 섞습니다 """
       r = self.rgen.permutation(len(y))
        return X[r], y[r]

   def _initialize_weights(self, m):
       """ 랜덤한 작은 수로 가중치를 초기화 합니다 """
       self.rgen = np.random.RandomState(self.random_state)
        self.w_ = self.rgen.normal(loc=0.0, scale=0.01, size=1+m)
        self.w_initialized = True

   def _update_weights(self, xi, target):
       """ 아달린 학습 규칙을 적용하여 가중치를 업데이트 합니다 """
        output = self.activation(self.net_input(xi))
        error = (target - output)
       self.w_[1:] += self.eta * xi.dot(error)
        self.w_[0] += self.eta * error
        cost = 0.5 * error ** 2

        return cost

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

    def activation(self, X):
       """ 선형 활성화 계산 """
        return X

   def predict(self, X):
       """ 단위 계단 함수를 사용하여 클래스 레이블을 반환합니다 """
        return np.where(self.net_input(X) >= 0.0, 1, -1)
  • 확률적 경사 하강법을 이용해 분류기를 훈련하고 결과를 그래프로 그리면 아래 그림 2-15와 같다.
ada = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
ada.fit(X_std, y)

plot_decision_regions(X_std, y, classifier=ada)

plt.title('Adaline - Stochastic Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average Cost')
plt.show()

머신 러닝 교과서/ 컴퓨터는 데이터에서 배운다

데이터를 지식으로 바꾸는 지능적인 시스템 구축

  • 20세기 후반 데이터에서 지식으 추출하여 예측하는 자가 학습(self-learning) 알고리즘과 관련된 인공 지능의 하위 분야로 머신 러닝이 출현했다.
    • 사람이 수동으로 대량의 데이터를 분석하여 규칙을 유도하고 모델을 만드는 대신, 머신 러닝이 데이터에서 더 효율적으로 지식을 추출하여 예측 모델과 데이터 기반의 의사 결정 성능을 점진적으로 향상시킬 수 있게 됨.

머신 러닝의 세 가지 종류

  • 머신 러닝은 3가지 종류로 구분해 볼 수 있다.
    • 지도 학습 (supervised learning)
    • 비지도 학습 (unsupervised learning)
    • 강화 학습 (reinforcement learning)

지도 학습으로 미래 예측

  • 지도 학습의 주요 목적은 레이블(label) 된 훈련 데이터에서 모델을 학습하여 본 적 없는 미래 데이터에 대해 예측을 만드는 것. 
    • 지도(supervised)는 희망하는 출력 신호(레이블)가 있는 일련의 샘플을 의미한다.
  • 지도 학습은 다시 데이터를 범주(category)를 구분하는 분류(classification)와 연속적인 값을 출력하는 회귀(regression)으로 구분할 수 있다.

분류: 클래스 레이블 예측

  • 분류는 과거의 관측을 기반으로 새로운 샘플의 범주형 클래스 레이블을 예측하는 것이 목적.
    • 클래스  레이블은 이산적(discrete)이고 순서가 없어 샘플이 속한 그룹으로 이해할 수 있다.
    • 스팸 이메일 감지는 전형적인 이진 분류(binary classification) 작업의 예이다.
  • 두 개 이상의 클래스 레이블을 가진 경우 지도 학습 알고리즘으로 학습한 예측 모델은 훈련 데이터셋에 있는 모든 클래스 레이블을 새로운 샘플에 할당할 수 있다.
    • 이런 다중 분류(multiclass classification)의 전형 적인 예는 손글씨 인식 문제.
  • 아래 그림은 30개의 훈련 샘플이 있는 이진 분류 작업의 개념을 나타낸다.
    • 15개의 샘플은 음성 클래스(negative class)로 레이블(뺄셈 기호)되어 있고, 다른 15개의 샘플은 양성 클래스(positive class)로 레이블(덧셈 기호) 되어 있다.
    • 각 샘플이 두 개의 x_{1}, x_{2} 값에 연관되어 있으므로 2차원 데이터 셋이다.
    • 지도 학습 알고리즘을 사용하여 두 클래스를 구분할 수 있는 규칙을 학습한다. 이 규칙은 점선으로 나타난 결정 경계(decision boundary)이다.
    • 새로운 데이터의 x_{1}, x_{2} 값이 주어지면 두 개의 범주 중 하나로 분류한다.

회귀: 연속적인 출력 값 예측

  • 회귀는 예측 변수(predictor variable)(또는 설명 변수(explanatory variable), 입력(input))와 연속적인 반응 변수(response variable) (또는 출력(outcome), 타겟(target)) 가 주어졌을 때 출력 값을 예측하는 두 변수 사이의 관계를 찾는다.
    • 학생들의 수학 점수를 예측하는 것이 그 예
  • 아래 그림은 선형 회귀(linear regression)의 개념으로 입력 x 와 타깃 y 가 주어지면 샘플과 직선 사이 거리가 최소가 되는 직선을 그을 수 있다. 
    • 일반적으로 평균 제곱 거리를 사용한다.
    • 이렇게 데이터에서 학습한 직선의 기울기와 절편(intercept)을 사용하여 새로운 데이터의 출력 값을 예측한다.

강화 학습으로 반응형 문제 해결

  • 강화 학습은 환경과 상호 작용하여 시스템(에이전트(agent))의 성능을 향상하는 것이 목적이다.
    • 환경의 현재 상태 정보는 보상(reward) 신호를 포함하기 때문에 강화 학습을 지도 학습과 관련된 분야로 생각할 수 있다.
    • 강화 학습의 피드백은 정답(ground truth) 레이블이나 값이 아니라 보상 함수로 얼마나 좋은지를 측정한 값이다.
    • 에이전트는 환경과 상호 작용하여 보상이 최대화 되는 일련의 행동을 강화 학습으로 학습한다.
    • 탐험적인 시행착오(trial and error) 방식이나 신중하게 세운 계획을 사용한다.
    • 강화 학습의 대표적인 예는 체스이다.

  • 강화 학습에는 여러 하위 분류가 있는데, 일반적인 구조는 강화 학습 에이전트가 환경과 상호작용하여 보상을 최대화 하는 것이다.
    • 각 상태는 양의 보상이나 음의 보상과 연관된다. 보상은 체스 게임의 승리나 패배처럼 전체 목표를 달성하는 것으로 정의할 수 있다.

비지도 학습으로 숨겨진 구조 발견

  • 지도 학습에서는 모델을 훈련할 때 사전에 옳은 답을 알고 있고, 강화 학습에서는 에이전트의 특정 행동을 어떻게 보상할지 그 측정 방법을 정의하는 반면, 비지도 학습에서는 레이블되지 않거나 구조를 알 수 없는 데이터를 다룬다.
    • 비지도 학습을 사용하면 알려진 출력 값이나 보상 함수의 도움을 받지 않고 의미 있는 정보를 추출하기 위해 데이터 구조를 탐색할 수 있다.

군집: 서브그룹 찾기

  • 군집(clustering)은 사전 정보 없이 쌓여 있는 그룹 정보를 의미 있는 서브그룹(subgroup) 또는 클러스터(cluster)로 조직하는 탐색적 데이터 분석 기법이다.
    • 분석 과정에서 만든 각 클러스터는 어느 정도 유사성을 공유하고 다른 클러스터와는 비슷하지 않은 샘플 그룹을 형성한다. 군집을 비지도 분류(unsupervised classification)이라고 하는 이유가 여기에 있다.
    • 클러스터링은 정보를 조직화하고 데이터에서 의미 있는 관계를 유도하는 도구이다.
    • 마케터가 관심사를 기반으로 고객 그룹을 나누는 것이 그 예

차원 축소: 데이터 압축

  • 비지도 학습의 또 다른 하위 분야는 차원 축소(dimensionality reduction)이다.
    • 고차원의 데이터를 다루어야 하는 경우 하나의 관측 샘플에 많은 측정 지표가 존재하는데, 이로 인해 머신 러닝 알고리즘의 계산 성능과 저장 공간의 한계에 맞닥뜨릴 수 있다.
    • 비지도 차원 축소는 잡음(noise) 데이터를 제거하기 위해 특성 전처리 단계에서 종종 적용하는 방법이다. 이런 잡음 데이터는 특정 알고리즘의 예측 성능을 감소시킬 수 있다.
    • 차원 축소는 관련 있는 정보를 대부분 유지하면서 더 작은 차원의 부분 공간(subspace)으로 데이터를 압축한다.
  • 차원 축소는 데이터 시각화에도 유리하다. 아래 그림은 고차원 특성을 1, 2, 3차원 특성공간으로 시각화하는 예

기본 용어와 표기법 소개

  • 아래 그림 1-8의 표는 머신 러닝 분야의 고전적인 예제인 붓꽃(Iris) 데이터셋 일부를 보여준다. 붓꽃 데이터 셋은 Setosa, Versicolor, Virginica 세 종류 150개의 붓꽃 샘플을 담고 있다.
    • 각 붓꽃 샘플은 데이터셋에서 하나의 행(row)으로 표현된다.
    • 센티미터 단위의 측정값은 열(column)에 저장되어 있으며, 데이터셋의 특성(feature)라고도 한다.

  • 데이터는 선형대수학(linear algebra)을 사용하여 행렬(matrix)과 벡터(vector) 표기로 데이터를 표현한다.
    • 일반적인 관례에 따라 샘플은 특성 행렬 X 에 있는 행으로 나타내고, 특성은 열을 따라 저장한다.
    • 150개의 샘플과 네 개의 특성을 가진 붓꽃 데이터셋은 150 x 4 크기의 행렬 X \in \mathbb{R}^{150 \times 4} 로 쓸 수 있다.

\left[ \begin{array}{rrrr} x_{1}^{(1)} & x_{2}^{(1)} & x_{3}^{(1)} & x_{4}^{(1)} \\ x_{1}^{(2)} & x_{2}^{(2)} & x_{3}^{(2)} & x_{4}^{(2)} \\ ... & ... & ... & ... \\ x_{1}^{(150)} & x_{2}^{(150)} & x_{3}^{(150)} & x_{4}^{(150)} \end{array} \right]

  • 기호 설명)
    • 위 첨자 i는 i번째 훈련 샘플을(지수가 아니다 주의), 아래 첨자 j는 데이터셋의 j번째 차원을 나타낸다.
    • 굵은 소문자는 벡터 (x \in \mathbb{R}^{n \times 1} )를 나타내고 굵은 대문자는 행렬 (X \in \mathbb{R}^{n \times m} )을 나타낸다.
    • 벡터나 행렬에 있는 하나의 원소를 나타낼 때는 이탤릭체를 사용한다. x^{n} 또는 x_{m}^{n}
    • 예컨대 x_{1}^{150} 은 150번째 샘플의 1번째 차원인 꽃받침 길이를 나타낸다. 특성 행렬의 각 행은 하나의 꽃 샘플을 나타내고 4차원 행 벡터 x^{i} \in \mathbb{R}^{1 \times 4} 로 쓸 수 있다. 

x_{i} = \left[ \begin{array}{rrrr} x_{1}^{(i)} & x_{2}^{(i)} & x_{3}^{(i)} & x_{4}^{(i)} \end{array} \right]

  • 각 특성 차원은 150차원의 열 벡터 x_{j} \in \mathbb{R}^{150 \times 1} 이다. 예컨대 다음과 같다.

x_{j} = \left[ \begin{array}{rrrr} x_{j}^{(1)} \\ x_{j}^{(2)} \\ ... \\ x_{j}^{(150)} \end{array} \right]

  • 비슷하게 타깃 변수(여기서는 클래스 레이블)를 150차원의 열 벡터로 저장한다.

y = \left[ \begin{array}{rrrr} y^{1} \\ y^{2} \\ ... \\ y^{150} \end{array} \right] (y \in \{ Setosa, Versicolor, Virginica \})

  •  

머신 러닝 시스템 구축 로드맵

  • 아래 그림은 예측 모델링에 머신 러닝을 사용하는 전형적인 작업 흐름을 보여준다.

전처리: 데이터 형태 갖추기

  • 데이터 전처리는 모든 머신 러닝 어플리케이션에서 가장 중요한 단계이다.
    • 많은 머신 러닝 알고리즘에서 최적의 성능을 내려면 선택된 특성이 같은 스케일을 가져야 한다. 특성을 [0, 1] 범위로 변환하거나 평균이 0이고 단위 분산을 가진 표준 정규 분포(standard normal distribution)로 변환하는 경우가 많다.
    • 일부 선택된 특성은 매우 상관관계가 높아 어느 정도 중복된 정보를 가질 수 있다. 이때는 차원 축소 기법을 사용하여 특성을 저차원 부분 공간으로 압축한다. 특성 공간의 차원을 축소하면 저장 공간이 덜 필요하고 학습 알고리즘을 더 빨리 실행할 수 있다.
    • 어떤 경우에는 차원 축소가 모델의 예측 성능을 높이기도 한다. 데이터셋에 관련 없는 특성(또는 잡음)이 매우 많을 경우, 즉 신호 대 잡음비(Signal-to-Noise Ratio, SNR)가 낮은 경우이다.
  • 머신 러닝 알고리즘이 훈련 데이터셋에서 잘 작동하고 새로운 데이터에서도 잘 일반화 되는지 확인하려면 데이터셋을 랜덤하게 훈련 세트와 테스트 세트로 나눠야 한다. 
    • 훈련 세트에서 머신 러닝 모델을 훈련하고 최적화 한다. 테스트 세트는 별도로 보관하고 최종 모델을 평가하는 맨 마지막에 사용한다.

예측 모델 훈련과 선택

  • 분류 알고리즘은 저마다 태생적인 편향이 존재한다. 작업에서 아무런 가정도 하지 않는다면 어떤 하나의 분류 모델이 더 우월하다고 말할 수 없다.
    • 현실에서 가장 좋은 모델을 훈련하고 선택하기 위해 최소한 몇 가지 알고리즘을 비교해야 한다.
  • 여러 모델을 비교하기 전에 먼저 성능을 측정할 지표를 결정해야 한다. 분류에서 널리 사용되는 지표는 정확도(accuracy)이다. 정확도는 정확히 분류된 샘플 비율이다.
  • 모델 선택에 테스트 세트를 사용하지 않고 최종 모델을 평가하려고 따로 보관한다면 테스트 세트와 실제 데이터에서 어떤 모델이 잘동작할지를 어떻게 알 수 있을까?
    • 이 질문에 나온 이슈를 해결하기 위해 다양한 교차 검증 기법을 사용한다.
    • 이 기법은 모델의 일반화 성능을 예측하기 위해 훈련 데이터를 훈련 세트와 검증 세트로 더 나눈다.
  • 또 머신 러닝 라이브럴리들에서 제공하는 알고리즘의 기본 하이퍼파라미터가 현재 작업에 최적이라고 기대할 수는 없다. 이어지는 장에서는 모델 성능을 상세하게 조정하기 위해 하이퍼파라미터 최적화 기법을 사용할 것이다.
    • 하이퍼파라미터(hyperparameter)는 데이터에서 학습하는 파라미터가 아니라 모델 성능을 향상하기 위해 사용하는 다이얼로 생각할 수 있다.

모델을 평가하고 본 적 없는 샘플로 예측

  • 훈련 세트에서 최적의 모델을 선택한 후에는 테스트 세트를 사용하여 이전에 본 적 없는 데이터에서 얼마나 성능을 내는지 예측하여 일반화 오차를 예상한다.
    • 이 성능에 만족한다면 이 모델을 사용하여 새로운 데이터를 예측할 수 있다.
    • 이전에 언급한 특성 스케일 조정과 차원 축소 같은 단계에서 사용한 파라미터는 훈련 세트만 사용하여 얻은 것임을 주목해야 한다. 나중에 동일한 파라미터를 테스트 세트는 물론 새로운 모든 샘플을 변환하는데 사용한다.
    • 그렇지 않으면 테스트 세트에서 측정한 성능은 과도하게 낙관적인 결과가 된다. 

머신 러닝을 위한 파이썬

  • (이하 파이썬 설치에 대한 내용 생략)

Machine Learning/ 적용 사례

Recommender Systems

https://www.coursera.org/learn/machine-learning/

  • 사용자 정보를 Matrix로 구성하여 사용자의 선호에 맞는 컨텐츠나 유사한 컨텐츠를 추천하는 추천 시스템.
  • 이하 시스템에 대한 내용은 다음의 조건을 따른다.
    • 사용자는 각각의 영화에 대해 1-5개의 별점을 줄 수 있음.
    • 별점을 매기지 않았다면 ?로 표시
    • 유저 수는 nu로 표기, 영화 수는 nm으로 표기
    • 유저 j가 영화 i에 대해 평점을 주었다면 r(i, j) = 1이라고 표기
      • r 행렬은 별점을 주었는지 안 주었는지만 판별
    • 유저 j가 영화 i에 부여한 별점은 y(i, j) 로 표기

Continue reading

Machine Learning/ 알고리즘 발전 시키기

Vectorize

  • Σ로 표현되는 식을 벡터로 표현할 수 있는 경우 –벡터는 n x 1 행렬이므로 사실상 행렬– 벡터로 변환하여 계산하면 식 자체가 간편해 질 뿐더러, 행렬의 병렬처리를 지원하는 프로그램을 사용하면 성능에서도 유리하다.
    • 예컨대 아래의 식을
      • h_{\theta}(x) = \theta_0 + \theta_1 x_1 + \theta_2 x_2 + \theta_3 x_3^2 + ...
    • 벡터화 하면 아래와 같이 표현 가능하다.
      • \theta = [ \theta_0; \theta_1; \theta_2;  ... \theta_n ]
      • X = [ x_0; x_1; x_2; ... x_n ]
        • x0는 정의상 1과 같다.
      • h_{\theta}(x) = \theta^T X
  • hθ(x)를 Vectorize 했다면 비용함수 J(θ)도 벡터로 간단하게 할 수 있다.
    • 아래의 기존 식을
      • J(\theta) = \frac{1}{2m} \sum_{i=1}^m (h_\theta (x_i) - y_i)^2
    • 벡터화하면 아래와 같이 표현 가능하다.
      • y = [ y_1; y_2; ... y_n ]
      • J(\theta) = \frac{1}{2m} ((\theta^T X - y)^2)
        • 행렬의 제곱은 .^ 이라는 연산을 이용한다. 프로그램적으로 ‘.’ 은 element 연산을 의미한다.
  • 프로그램상 Σ 연산은 for 문을 도는 형식이 되며, 행렬 연산을 지원하는 프로그램의 경우 Vector 연산은 병렬로 처리가 된다.
  • 물론 행렬 계산은 역행렬 계산과 같은 상황에서 부하가 많이 걸리지만, Octave와 같은 프로그램에서는 pinv 와 같은 함수로 계산해 pseudo 값을 계산해 주므로 행렬 계산에 익숙해질 필요가 있다.

Continue reading

Machine Learning/ 이상탐지

주의) 이 페이지에서는 공식의 유도 과정 같은 것은 정리하지 않는다. 공식의 유도 과정은 <코세라 강의> 참조.

이상탐지(Anormaly Detection)

https://www.coursera.org/learn/machine-learning/

  • 군집을 이루는 데이터 분포를 이용하여, 분포에서 멀어진 데이터를 이상(anormaly)이라고 판단하는 알고리즘.
    • 데이터에 오류가 있다는 뜻이 아니다. 군집과 멀어졌으므로 이상해 보이는 데이터를 의미한다.
  • 데이터가 이상한지 아닌지는 데이터의 밀도(Density estimation)를 추정하는 함수 P(x)를 통해 할 수 있다.
    • 이 함수는 가우시안 분포를 이용하여 계산한다. 아래 내용 참조

Continue reading

Machine Learning/ PCA

주의) 이 페이지에서는 공식의 유도 과정 같은 것은 정리하지 않는다. 공식의 유도 과정은 <코세라 강의> 참조.

PCA (Principal Component Analysis)

https://www.coursera.org/learn/machine-learning/

  • 데이터의 차원 수를 낮춰서 데이터를 압축하는 알고리즘.
    • 2차원 데이터를 1차원 선으로 줄이는 것, 3차원 데이터를 2차원 면으로 줄이는 것과 같은 것이 기본이며, 같은 개념으로 1000차원 데이터를 100차원으로 줄인다. 당연히 차원을 많이 축소할 수록 손실이 많이 발생하기 때문에 적당한 정도를 찾는 것이 중요하다.
    • 여기까지만 보면 고차원 데이터를 저차원에 투영하는 것 같지만 사실 투영은 아니다. 자세한 내용은 아래 참조.

Continue reading