TOP
class="layout-aside-left paging-number">
본문 바로가기
[파이썬 Projects]/<파이썬 머신러닝>

[머신러닝] 서포트 벡터 머신(SVM)

by 기록자_Recordian 2024. 11. 14.
728x90
반응형
서포트 벡터 머신(Support Vector Machine, SVM)

 

SVM은 매우 강력할 뿐만 아니라 선형이나 비선형 분류, 회귀, 특이치 탐지에도 사용할 수 있는 다목적 머신러닝 모델이다. SVM은 중소규모의 비선형 데이터셋, 특히 분류 작업에서 빛을 발하나 매우 큰 데이터셋으로는 잘 확장되지 않는다.

 

[SVM 주요 특징]

  • 벡터 공간에서 훈련 데이터가 속한 2개의 그룹을 분류하는 선형 분리자를 찾는 기하학적 모델
  • 데이터를 분리하는 초평면 중에서 데이틀과 거리가 가장 먼 초평면을 선택하여 분리하는 지도 학습 기반의 이진 선형 분류 모델
  • 최대 마진을 가지는 비확률적 선형 판별 분석에 기초한 이진 분류기
  • 공간상에서 최적의 분리 초평면을 찾아서 분류 및 회귀 수행
  • 변수 속성 간의 의존성은 고려하지 않으며 모든 속성을 활용하는 기법
  • 훈련 시간이 상대적으로 느리지만, 정확성이 뛰어나며 다른 방법보다 과대 적합의 가능성이 낮은 모델

(1) 구성요소

  • 결정 경계: 데이터 분류의 기준이 되는 경계
  • 초평면: n 차원의 공간의 (n-1) 차원 평면
  • 마진: 결정 경계에서 서포트 벡터까지의 거리
  • 서포트 벡터: 훈련 데이터 중에서 결정 경계와 가장 가까이에 있는 데이터들의 집합
  • 슬랙 변수: 완벽한 분리가 불가능할 때 선형적으로 분류를 위해 허용된 오차를 위한 변수

(2) 종류

  • 하드 마진 SVM: 마진의 안쪽이나 바깥쪽에 절대로 잘못 분류된 오 분류를 불허
  • 소프트 마진 SVM: 오 분류 허용

(3) 적용 기준

선형으로 분리 가능 최적의 결정 경계 (또는 초평면)를 기준으로 1과 -1로 구분하여 분류 모형으로 사용
선형으로 분리 불가능 - 저차원 공간을 고차원 공간으로 매핑할 경우에 발생하는 연산의 복잡성은 커널 트릭을 통하여 해결 가능
* 커털 트릭: 저차원에서 함수의 계산만으로 원하는 풀이가 가능한 커널 함수를 이용하여 고차원 공간으로 매핑할 경우에 증가하는 연산량의 문제를 해결하는 기법

 
※ 커널함수: 맵핑 공간에서의 내적과 동등한 함수
- 선형(Linear) 커널: 기본 유형의 커널이며, 1차원이고 다른 함수보다 빠름 (텍스트 분류 문제에 주로 사용)
- 다항(Polynomial) 커널: 선형 커널의 일반화된 공식, 효과성과 정확도 측면에서 효율이 적어 비선호
- RBF (=가우시안 커널): 2차원의 점을 무한한 차원의 점으로 변환 (가장 많이 사용하는 커널, 비선형 데이터가 있는 경우에 일반적으로 활용)
- 시그모이드 커널: 인공신경망에서 선호되는 커널 (다층 퍼셉트론 모델과 유사)

출처:&amp;amp;amp;nbsp;https://ybeaning.tistory.com/30

 
(4) 장단점

장점 단점
- 데이터가 적을 때 효과적
- 새로운 데이터가 입력되어도 연산량 최소화
- 정확성이 뛰어남
- 커널 트릭을 활용하여 비선형 모델 분류 가능
- 다른 모형보다 과대 적합의 가능성 낮음
- 노이즈의 영향이 적음
- 데이터 전처리 과정 중요
- 데이터 세트의 크기가 클 경우 모델링에
많은 시간 소요
- 데이터가 많아질수록 다른 모형에 비해 속도 느림
- 커널과 모델의 매개변수를 조절하기 위해
많은 테스트 필요

선형 SVM 분류

 

- 라지 마진 분류를 설명하기 위한 붓꽃 데이터셋의 일부를 나타내는 그래프를 생성하는 코드

import matplotlib.pyplot as plt
import numpy as np
from sklearn.svm import SVC
from sklearn import datasets

iris = datasets.load_iris(as_frame=True)
X = iris.data[["petal length (cm)", "petal width (cm)"]].values
y = iris.target

setosa_or_versicolor = (y == 0) | (y == 1)
X = X[setosa_or_versicolor]
y = y[setosa_or_versicolor]

# SVM 분류 모델
svm_clf = SVC(kernel="linear", C=10**9)
svm_clf.fit(X, y)

# 나쁜 모델
x0 = np.linspace(0, 5.5, 200)
pred_1 = 5 * x0 - 20
pred_2 = x0 - 1.8
pred_3 = 0.1 * x0 + 0.5

def plot_svc_decision_boundary(svm_clf, xmin, xmax):
    w = svm_clf.coef_[0]
    b = svm_clf.intercept_[0]

    # 결정 경계에서, w0*x0 + w1*x1 + b = 0
    # => x1 = -w0/w1 * x0 - b/w1
    x0 = np.linspace(xmin, xmax, 200)
    decision_boundary = -w[0] / w[1] * x0 - b / w[1]

    margin = 1/w[1]
    gutter_up = decision_boundary + margin
    gutter_down = decision_boundary - margin
    svs = svm_clf.support_vectors_

    plt.plot(x0, decision_boundary, "k-", linewidth=2, zorder=-2)
    plt.plot(x0, gutter_up, "k--", linewidth=2, zorder=-2)
    plt.plot(x0, gutter_down, "k--", linewidth=2, zorder=-2)
    plt.scatter(svs[:, 0], svs[:, 1], s=180, facecolors='#AAA',
                zorder=-1)

fig, axes = plt.subplots(ncols=2, figsize=(10, 2.7), sharey=True)

plt.sca(axes[0])
plt.plot(x0, pred_1, "g--", linewidth=2)
plt.plot(x0, pred_2, "m-", linewidth=2)
plt.plot(x0, pred_3, "r-", linewidth=2)
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "bs", label="Iris versicolor")
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "yo", label="Iris setosa")
plt.xlabel("Petal length")
plt.ylabel("Petal width")
plt.legend(loc="upper left")
plt.axis([0, 5.5, 0, 2])
plt.gca().set_aspect("equal")
plt.grid()

plt.sca(axes[1])
plot_svc_decision_boundary(svm_clf, 0, 5.5)
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "bs")
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "yo")
plt.xlabel("Petal length")
plt.axis([0, 5.5, 0, 2])
plt.gca().set_aspect("equal")
plt.grid()

plt.show()

◆ 왼쪽 그래프

세 개의 선형 분류기에서 만들어진 결정 경계가 보이는데, 점선으로 나타난 결정 경계를 만든 모델은 클래스를 적절하게 분류하지 못하고 있다.

다른 두 모델은 훈련 세트에 대해 완벽하게 작동하나, 결정 경계가 샘플에 너무 가까워 새로운 샘플에 대해서는 잘 작동하지 않을 것이다.

 

◆ 오른쪽 그래프

실선은 SVM 분류기의 결정 경계이며, 이 직선은 두 개의 클래스를 나누고 있을 뿐 아니라 제일 가까운 훈련 샘플로부터 가능한 한 멀리 떨어져 있다. 즉 SVM 분류기를 클래스 사이에 가장 폭이 넓은 도로를 찾는 것으로 생각할 수 있어 라지 마진 분류(Large margin classification)라고 한다.

 

도로 바깥쪽에 훈련 샘플을 더 추가해도 결정 경계에는 전혀 영향을 미치지 않으며, 도로 경계에 위치한 샘플에 의해 전적으로 결정된다. 이러한 샘플을 서포트 벡트라고 하며, 해당 그래프에 회색 동그라미로 표시된 부분을 말한다.


SVM은 특성의 스케일에 민감하다.

from sklearn.preprocessing import StandardScaler

Xs = np.array([[1, 50], [5, 20], [3, 80], [5, 60]]).astype(np.float64)
ys = np.array([0, 0, 1, 1])
svm_clf = SVC(kernel="linear", C=100).fit(Xs, ys)

scaler = StandardScaler()
X_scaled = scaler.fit_transform(Xs)
svm_clf_scaled = SVC(kernel="linear", C=100).fit(X_scaled, ys)

plt.figure(figsize=(9, 2.7))
plt.subplot(121)
plt.plot(Xs[:, 0][ys==1], Xs[:, 1][ys==1], "bo")
plt.plot(Xs[:, 0][ys==0], Xs[:, 1][ys==0], "ms")
plot_svc_decision_boundary(svm_clf, 0, 6)
plt.xlabel("$x_0$")
plt.ylabel("$x_1$    ", rotation=0)
plt.title("Unscaled")
plt.axis([0, 6, 0, 90])
plt.grid()

plt.subplot(122)
plt.plot(X_scaled[:, 0][ys==1], X_scaled[:, 1][ys==1], "bo")
plt.plot(X_scaled[:, 0][ys==0], X_scaled[:, 1][ys==0], "ms")
plot_svc_decision_boundary(svm_clf_scaled, -2, 2)
plt.xlabel("$x'_0$")
plt.ylabel("$x'_1$  ", rotation=0)
plt.title("Scaled")
plt.axis([-2, 2, -2, 2])
plt.grid()

plt.show()

왼쪽 그래프에서는 수직축의 스케일이 수평축의 스케일보다 훨씬 커서 가장 넓은 도로가 거의 수평에 가깝게 된다. 특성의 스케일을 조정(예. 사이킷런의 StandardScaler)하면 결정 경계가 오른쪽 그래프처럼 훨씬 좋아진다.


[하드 마진 분류 vs 소프트 마진 분류]

 

◆ 하드 마진 분류

모든 샘플이 도로 바깥쪽에 올바르게 분류되어 있는 것.

- 하드 마진 분류의 문제점

  • 데이터가 선형적으로 구분될 수 있어야 제대로 작동
  • 이상치에 민감
X_outliers = np.array([[3.4, 1.3], [3.2, 0.8]])
y_outliers = np.array([0, 0])
Xo1 = np.concatenate([X, X_outliers[:1]], axis=0)
yo1 = np.concatenate([y, y_outliers[:1]], axis=0)
Xo2 = np.concatenate([X, X_outliers[1:]], axis=0)
yo2 = np.concatenate([y, y_outliers[1:]], axis=0)

svm_clf2 = SVC(kernel="linear", C=10**9)
svm_clf2.fit(Xo2, yo2)

fig, axes = plt.subplots(ncols=2, figsize=(10, 2.7), sharey=True)

plt.sca(axes[0])
plt.plot(Xo1[:, 0][yo1==1], Xo1[:, 1][yo1==1], "bs")
plt.plot(Xo1[:, 0][yo1==0], Xo1[:, 1][yo1==0], "yo")
plt.text(0.3, 1.0, "Impossible!", color="red", fontsize=18)
plt.xlabel("Petal length")
plt.ylabel("Petal width")
plt.annotate(
    "Outlier",
    xy=(X_outliers[0][0], X_outliers[0][1]),
    xytext=(2.5, 1.7),
    ha="center",
    arrowprops=dict(facecolor='black', shrink=0.1),
)
plt.axis([0, 5.5, 0, 2])
plt.grid()

plt.sca(axes[1])
plt.plot(Xo2[:, 0][yo2==1], Xo2[:, 1][yo2==1], "bs")
plt.plot(Xo2[:, 0][yo2==0], Xo2[:, 1][yo2==0], "yo")
plot_svc_decision_boundary(svm_clf2, 0, 5.5)
plt.xlabel("Petal length")
plt.annotate(
    "Outlier",
    xy=(X_outliers[1][0], X_outliers[1][1]),
    xytext=(3.2, 0.08),
    ha="center",
    arrowprops=dict(facecolor='black', shrink=0.1),
)
plt.axis([0, 5.5, 0, 2])
plt.grid()

plt.show()

위의 그래프는 이상치가 하나 있는 붓꽃 데이터셋을 나태는 것인데,

  • 왼쪽 그래프에서는 하드 마진을 찾을 수 없다.
  • 오른쪽 그래프의 결정 경계는 이상치가 없는 결정 경계와 매우 다르고 모델이 잘 일반화될 것 같지 않다.

▶ 이런 문제를 피하려면 좀 더 유연한 모델이 필요한데, 도로의 폭을 가능한 한 넓게 유지하는 것과 마진오류 사이에 적절한 균형을 잡아야 하며, 이를 소프트 마진 분류라고 한다.

 

사이킷런의 SVM 모델을 만들 때 규제 하이퍼파라미터 C를 포함하여 여러 하이퍼파라미터를 지정할 수 있다.

- 붓꽃 데이터셋을 적재하고, Iris-Virginica 품종을 감지하기 위해 선형 SVM 모델을 훈련 시키는 코드로, 이 파이프라인은 먼저 특성의 스케일을 변경한 다음 C=1인 LinearSVC를 사용한다.

import numpy as np
from sklearn.datasets import load_iris
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import LinearSVC

iris = load_iris(as_frame=True)
X = iris.data[["petal length (cm)", "petal width (cm)"]].values
y = (iris.target == 2)  # Iris virginica

svm_clf = make_pipeline(StandardScaler(),
                        LinearSVC(C=1, dual=True, random_state=42))
svm_clf.fit(X, y)

그리고 모델을 사용해 예측해 본다.

X_new = [[5.5, 1.7], [5.0, 1.5]]
svm_clf.predict(X_new)

▶  첫 번째 꽃은 Iris-Virginica 로 분류되나, 두 번째 꽃은 그렇지 않다.

 

SVM이 이러한 예측을 하는 데 사용한 점수를 살펴본다. SVM 모델은 각 샘플과 결정 경계 사이의 거리(양수 또는 음수)를 측정한다.

svm_clf.decision_function(X_new)

 

그리고 하이퍼 파라미터 C에 따른 그래프 (넓은 마진 오류, 적은 마진 오류)를 그려본다

scaler = StandardScaler()
svm_clf1 = LinearSVC(C=1, max_iter=10_000, dual=True, random_state=42)
svm_clf2 = LinearSVC(C=100, max_iter=10_000, dual=True, random_state=42)

scaled_svm_clf1 = make_pipeline(scaler, svm_clf1)
scaled_svm_clf2 = make_pipeline(scaler, svm_clf2)

scaled_svm_clf1.fit(X, y)
scaled_svm_clf2.fit(X, y)

# 스케일링되지 않은 파라미터로 변환
b1 = svm_clf1.decision_function([-scaler.mean_ / scaler.scale_])
b2 = svm_clf2.decision_function([-scaler.mean_ / scaler.scale_])
w1 = svm_clf1.coef_[0] / scaler.scale_
w2 = svm_clf2.coef_[0] / scaler.scale_
svm_clf1.intercept_ = np.array([b1])
svm_clf2.intercept_ = np.array([b2])
svm_clf1.coef_ = np.array([w1])
svm_clf2.coef_ = np.array([w2])

# 서포트 벡터 찾기(LinearSVC는 이 작업을 자동으로 수행하지 않음)
t = y * 2 - 1
support_vectors_idx1 = (t * (X.dot(w1) + b1) < 1).ravel()
support_vectors_idx2 = (t * (X.dot(w2) + b2) < 1).ravel()
svm_clf1.support_vectors_ = X[support_vectors_idx1]
svm_clf2.support_vectors_ = X[support_vectors_idx2]

fig, axes = plt.subplots(ncols=2, figsize=(10, 2.7), sharey=True)

plt.sca(axes[0])
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "g^", label="Iris virginica")
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "bs", label="Iris versicolor")
plot_svc_decision_boundary(svm_clf1, 4, 5.9)
plt.xlabel("Petal length")
plt.ylabel("Petal width")
plt.legend(loc="upper left")
plt.title(f"$C = {svm_clf1.C}$")
plt.axis([4, 5.9, 0.8, 2.8])
plt.grid()

plt.sca(axes[1])
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "g^")
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "bs")
plot_svc_decision_boundary(svm_clf2, 4, 5.99)
plt.xlabel("Petal length")
plt.title(f"$C = {svm_clf2.C}$")
plt.axis([4, 5.9, 0.8, 2.8])
plt.grid()

plt.show()

  • 왼쪽 그래프: 규제 하이퍼파라미터 C를 낮게 설정
  • 오른쪽 그래프: 규제 하이퍼파라미터 C를 높게 설정

▶ C를 줄이면 도로가 더 커지지만 더 많은 마진 오류가 발생한다. 즉, C를 줄이면 도로를 지지하는 샘플이 더 많아지므로 과대적합의 위험이 줄어든다. 그러나 너무 많이 줄이면 모델이 과소적합 된다. (C=100인 모델이 더 잘 일반화됨)

 

로지스틱 회귀와 달리 LinearCV 에는 클래스 확률을 추정하는 predict_proba() 메서드가 없고, LinearCV 대신 SVC 클래스를 사용하고 probability 매개변수를 True로 설정하면 훈련이 끝날 때 SVM 결정 함수 점수를 추정 확률에 매핑하기 위해 추가적인 모델을 훈련한다.

단, 이렇게 하려면 5-폴드 교차 검증을 사용하여 훈련 세트의 모든 샘플에 대해 표본 외 예측을 생성한 다음 로지스틱 회귀 모델을 훈련해야 하므로 훈련 속도가 상당히 느려진다. 그런 다음 predict_proba() 및 predict_log_proba() 메서드를 사용할 수 있다.


비선형 SVM 분류

 

선형 SVM 분류기는 효율적이며 잘 작동하나, 선형적으로 분류할 수 없는 데이터셋이 많다.

비선형 데이터셋을 다루는 방법은 한 가지 방법은 다항 특성과 같은 특성을 더 추가하는 것인데, 이렇게 하면 선형적으로 구분되는 데이터셋이 만들어질 수 있다.

X1D = np.linspace(-4, 4, 9).reshape(-1, 1)
X2D = np.c_[X1D, X1D**2]
y = np.array([0, 0, 1, 1, 1, 1, 1, 0, 0])

plt.figure(figsize=(10, 3))

plt.subplot(121)
plt.grid(True)
plt.axhline(y=0, color='k')
plt.plot(X1D[:, 0][y==0], np.zeros(4), "bs")
plt.plot(X1D[:, 0][y==1], np.zeros(5), "g^")
plt.gca().get_yaxis().set_ticks([])
plt.xlabel("$x_1$")
plt.axis([-4.5, 4.5, -0.2, 0.2])

plt.subplot(122)
plt.grid(True)
plt.axhline(y=0, color='k')
plt.axvline(x=0, color='k')
plt.plot(X2D[:, 0][y==0], X2D[:, 1][y==0], "bs")
plt.plot(X2D[:, 0][y==1], X2D[:, 1][y==1], "g^")
plt.xlabel("$x_1$")
plt.ylabel("$x_2$  ", rotation=0)
plt.gca().get_yaxis().set_ticks([0, 4, 8, 12, 16])
plt.plot([-4.5, 4.5], [6.5, 6.5], "r--", linewidth=3)
plt.axis([-4.5, 4.5, -1, 17])

plt.subplots_adjust(right=1)

plt.show()

  • 왼쪽 그래프: 하나의 특성 만을 가진 간단한 데이터셋 (선형적으로 구분 할 수 없음)
  • 오른쪽 그래프: 두 번째 특성을 추가하여 만들어진 2차원 데이터셋(선형적으로 구분 가능)

- 사이킷런을 사용하여 PolynomialFeatures 변환기와 StandardScaler, LinearSVC를 연결하여 파이프라인을 만들고 moons 데이터셋에 적용해 본다.

moons 데이터셋은 마주보는 두 개의 초승달 모양으로 데이터 포인트가 놓여 있는 이진 분류를 위한 작은 데이터셋이며, make_moons() 함수를 사용해 이 데이터셋을 만들 수 있다.

from sklearn.datasets import make_moons
from sklearn.preprocessing import PolynomialFeatures

X, y = make_moons(n_samples=100, noise=0.15, random_state=42)

polynomial_svm_clf = make_pipeline(
    PolynomialFeatures(degree=3),
    StandardScaler(),
    LinearSVC(C=10, max_iter=10_000, random_state=42)
)
polynomial_svm_clf.fit(X, y)

 

# 시각화
def plot_dataset(X, y, axes):
    plt.plot(X[:, 0][y==0], X[:, 1][y==0], "bs")
    plt.plot(X[:, 0][y==1], X[:, 1][y==1], "g^")
    plt.axis(axes)
    plt.grid(True)
    plt.xlabel("$x_1$")
    plt.ylabel("$x_2$", rotation=0)

def plot_predictions(clf, axes):
    x0s = np.linspace(axes[0], axes[1], 100)
    x1s = np.linspace(axes[2], axes[3], 100)
    x0, x1 = np.meshgrid(x0s, x1s)
    X = np.c_[x0.ravel(), x1.ravel()]
    y_pred = clf.predict(X).reshape(x0.shape)
    y_decision = clf.decision_function(X).reshape(x0.shape)
    plt.contourf(x0, x1, y_pred, cmap=plt.cm.brg, alpha=0.2)
    plt.contourf(x0, x1, y_decision, cmap=plt.cm.brg, alpha=0.1)

plot_predictions(polynomial_svm_clf, [-1.5, 2.5, -1, 1.5])
plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])

plt.show()


[다항식 커널]

다항 특성을 추가하는 것은 간단하고 모든 머신러닝 알고리즘에서 잘 작동하지만 낮은 차수의 다항식은 매우 복잡한 데이터셋을 잘 표현하지 못하고 높은 차수의 다항식은 굉장히 많은 특성을 추가하므로 모델을 느리게 만든다.

SVM을 사용할 땐 커널 트릭(kernel trick) 이라는 수학적 기교를 적용할 수 있는데, 커널 트릭은 실제로는 특성을 추가하지 않으면서 매우 높은 차수의 다항 특성을 많이 추가한 것과 같은 결과를 얻게 해준다.

사실 어떤 특성도 추가하지 않기 때문에 엄청난 수의 특성 조합이 생기지 않으며, 이 기법은 파이썬 클래스에 구현되어 있다.

from sklearn.svm import SVC

poly_kernel_svm_clf = make_pipeline(StandardScaler(),
                                    SVC(kernel='poly', degree=3, coef0=1, C=5))
poly_kernel_svm_clf.fit(X, y)

poly100_kernel_svm_clf = make_pipeline(
    StandardScaler(),
    SVC(kernel="poly", degree=10, coef0=100, C=5)
)
poly100_kernel_svm_clf.fit(X, y)

fig, axes = plt.subplots(ncols=2, figsize=(10.5, 4), sharey=True)

plt.sca(axes[0])
plot_predictions(poly_kernel_svm_clf, [-1.5, 2.45, -1, 1.5])
plot_dataset(X, y, [-1.5, 2.4, -1, 1.5])
plt.title("degree=3, coef0=1, C=5")

plt.sca(axes[1])
plot_predictions(poly100_kernel_svm_clf, [-1.5, 2.45, -1, 1.5])
plot_dataset(X, y, [-1.5, 2.4, -1, 1.5])
plt.title("degree=10, coef0=100, C=5")
plt.ylabel("")

plt.show()

이 코드는 3차 다항식 커널을 사용해 SVM 분류기를 훈련 시킨다.

오른쪽 그래프는 10차 다항식 커널을 사용한 또 다른 SVM 분류기이며, 매개변수 coef0은 모델이 높은 차수와 낮은 차수에 얼마나 영향을 받을지 조절하며, 기본값은 0이다. 하이퍼파라미터의 경우 일반적으로 자동으로(랜덤 서치) 튜닝된다.

  • 모델이 과대적합이라면 다항식의 차수를 줄여야 한다.
  • 모델이 과소적합이라면 다항식의 차수를 늘려야 한다.

[유사도 특성]

비선형 특성을 다루는 또 다른 기법은 각 샘플이 특정 랜드마크와 얼마나 닮았는지 측정하는 유사도 함수로 계산한 특성을 추가하는 것이다. 랜드마크를 선택하는 간단한 방법은 데이터셋에 있는 모든 샘플 위치에 랜드마크를 설정하는 것인데, 이렇게 하면 차원이 매우 커져 변환된 훈련 세트가 선형적으로 구분될 가능성이 높다.

단, 단점은 훈련 세트에 있는 n개의 특성을 가진 m개의 샘플이 m개의 특성을 가진 m개의 샘플로 변환된다는 것이며, 훈련 세트가 매우 클 경우 동일한 크기의 아주 많은 특성이 만들어진다.

def gaussian_rbf(x, landmark, gamma):
    return np.exp(-gamma * np.linalg.norm(x - landmark, axis=1)**2)

gamma = 0.3

x1s = np.linspace(-4.5, 4.5, 200).reshape(-1, 1)
x2s = gaussian_rbf(x1s, -2, gamma)
x3s = gaussian_rbf(x1s, 1, gamma)

XK = np.c_[gaussian_rbf(X1D, -2, gamma), gaussian_rbf(X1D, 1, gamma)]
yk = np.array([0, 0, 1, 1, 1, 1, 1, 0, 0])

plt.figure(figsize=(10.5, 4))

plt.subplot(121)
plt.grid(True)
plt.axhline(y=0, color='k')
plt.scatter(x=[-2, 1], y=[0, 0], s=150, alpha=0.5, c="red")
plt.plot(X1D[:, 0][yk==0], np.zeros(4), "bs")
plt.plot(X1D[:, 0][yk==1], np.zeros(5), "g^")
plt.plot(x1s, x2s, "g--")
plt.plot(x1s, x3s, "b:")
plt.gca().get_yaxis().set_ticks([0, 0.25, 0.5, 0.75, 1])
plt.xlabel("$x_1$")
plt.ylabel("Similarity")
plt.annotate(
    r'$\mathbf{x}$',
    xy=(X1D[3, 0], 0),
    xytext=(-0.5, 0.20),
    ha="center",
    arrowprops=dict(facecolor='black', shrink=0.1),
    fontsize=16,
)
plt.text(-2, 0.9, "$x_2$", ha="center", fontsize=15)
plt.text(1, 0.9, "$x_3$", ha="center", fontsize=15)
plt.axis([-4.5, 4.5, -0.1, 1.1])

plt.subplot(122)
plt.grid(True)
plt.axhline(y=0, color='k')
plt.axvline(x=0, color='k')
plt.plot(XK[:, 0][yk==0], XK[:, 1][yk==0], "bs")
plt.plot(XK[:, 0][yk==1], XK[:, 1][yk==1], "g^")
plt.xlabel("$x_2$")
plt.ylabel("$x_3$  ", rotation=0)
plt.annotate(
    r'$\phi\left(\mathbf{x}\right)$',
    xy=(XK[3, 0], XK[3, 1]),
    xytext=(0.65, 0.50),
    ha="center",
    arrowprops=dict(facecolor='black', shrink=0.1),
    fontsize=16,
)
plt.plot([-0.1, 1.1], [0.57, -0.1], "r--", linewidth=3)
plt.axis([-0.1, 1.1, -0.1, 1.1])

plt.subplots_adjust(right=1)

plt.show()

가우스 RBF를 사용한 유사도 특성

 

다항 특성 방식과 마찬가지로 유사도 특성 방식도 머신러닝 알고리즘에 유용하게 사용될 수 있다. 추가 특성을 모두 계산하려면 연산 비용이 많이 드는데, 여기에서 커널 트릭을 사용해 유사도 특성을 많이 추가하는 것과 비슷한 결과를 얻을 수 있다.

 

- 가우스 RBF 커널을 사용한 SVC 모델 시도

rbf_kernel_svm_clf = make_pipeline(StandardScaler(),
                                   SVC(kernel='rbf', gamma=5, C=0.001))
rbf_kernel_svm_clf.fit(X, y)

 

하이퍼파라미터 gamma와 C를 바꾸어 훈련 시키고 이를 시각화

from sklearn.svm import SVC

gamma1, gamma2 = 0.1, 5
C1, C2 = 0.001, 1000
hyperparams = (gamma1, C1), (gamma1, C2), (gamma2, C1), (gamma2, C2)

svm_clfs = []
for gamma, C in hyperparams:
    rbf_kernel_svm_clf = make_pipeline(
        StandardScaler(),
        SVC(kernel="rbf", gamma=gamma, C=C)
    )
    rbf_kernel_svm_clf.fit(X, y)
    svm_clfs.append(rbf_kernel_svm_clf)

fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10.5, 7), sharex=True, sharey=True)

for i, svm_clf in enumerate(svm_clfs):
    plt.sca(axes[i // 2, i % 2])
    plot_predictions(svm_clf, [-1.5, 2.45, -1, 1.5])
    plot_dataset(X, y, [-1.5, 2.45, -1, 1.5])
    gamma, C = hyperparams[i]
    plt.title(f"gamma={gamma}, C={C}")
    if i in (0, 1):
        plt.xlabel("")
    if i in (1, 3):
        plt.ylabel("")

plt.show()

  • gamma를 증가시키면 종 모양 그래프가 좁아져서 각 샘플의 영향 범위가 작아지고, 결정 경계가 조금 더 불규칙해지고 각 샘플을 따라 구불구불하게 휘어짐
  • 작은 gamma 값은 넓은 종모양 그래프를 만들며 샘플이 넓은 범위에 걸쳐 영향을 주므로 결정 경계가 더 부드러워짐
  • 모델이 과대적합일 경우 감소시켜야 함
  • 모델이 과소적합일 경우 증가시켜야 함

[계산 복잡도]

LinearSVC

LinearSVC 파이썬 클래스는 선형 SVM위한 최적화된 알고리즘을 구현한 liblinear 라이브러리를 기반으로 한다. 이 라이브러리는 커널 트릭을 지원하지 않지만 훈련 샘플과 특성 수에 거의 선형적으로 늘어난다.

정밀도를 높이면 알고리즘의 수행 시간이 길어지며, 이는 허용 오차 하이퍼파라미터로 조절한다 (사이킷런 매개변수 tol)

대부분의 분류 문제는 허용 오차를 기본값으로 두면 잘 작동한다.

 

SVC

SVC는 커널 트릭 알고리즘을 구현한 libsvm 라이브러리를 기반으로 한다.

이는 훈련 샘플 수가 커지면 엄청나게 느려지기 때문에 중소규모의 비선형 훈련 세트에 이 알고리즘이 잘 맞는다.

하지만 특성 수에 대해서는, 특히 희쇠 특성인 경우에는 잘 확장되며, 이런 경우 알고리즘의 성능이 샘플이 가진 0이 아닌 특성의 평균 수에 거의 비례한다,

 

◆ SGDClassifier

SGDClassifier 클래스는 기본적으로 라지 마진 분류를 수행하며 하이퍼파라미터, 특히 규제 하이퍼파라미터(alpha 및 penalty)와 learning_rate를 조정하며 선형 SVM과 유사한 결과를 생성할 수 있다. 훈련을 위해 점진적 학습이 가능하고 메모리를 거의 사용하지 않는 확률적 경사 하강법을 사용하므로 RAM에 맞지 않는 대규모 데이터셋에서 모델을 훈련할 수 있다. 또한 확장성이 매우 뛰어나다.

 

※ SVM 분류를 위한 사이킷런 파이썬 클래스 비교

파이썬 클래스 시간 복잡도 외부 메모리 학습 지원 스케일 조정의 필요성 커널 트릭
LinearSVC O(m * n) N Y N
SVC O(m^2*n)~O(m^3*n) N Y Y
SGDClassifier O(m * n) Y Y N

SVM 회귀

 

SVM을 분류가 아니라 회귀에 적용하는 방법은 목표를 조금 바꾸는 것인데, 일정한 마진 오류 안에서 두 클래스 간의 도로 폭이 가능한 한 최대가 되도록 하는 대신, SVM 회귀는 제한된 마진 오류 (도로 밖의 샘플) 안에서 도로 안에 가능한 한 많은 샘플이 들어가도록 학습한다. 도포의 폭은 하이퍼파라미터 엡실론으로 조절한다.

 

◆ 랜덤으로 생성한 선형 데이터셋에 훈련시킨 두 개의 선형 SVM 회귀 모델의 예.

 

1)사이킷런의 LinearSVR을 사용해 선형 SVM 회귀 적용

먼저 간단한 선형 데이터셋을 생성하고

from sklearn.svm import LinearSVR

np.random.seed(42)
X = 2 * np.random.rand(50, 1)
y = 4 + 3 * X[:, 0] + np.random.randn(50)

svm_reg = make_pipeline(StandardScaler(),
                        LinearSVR(epsilon=0.5, dual=True, random_state=42))
svm_reg.fit(X, y)

하나는 마진을 작게 (엡실론=0.5), 하나는 마진을 크게(엡실론=1.2) 한 두 개의 선형 SVM 회귀 모델 생성 후 시각화

def find_support_vectors(svm_reg, X, y):
    y_pred = svm_reg.predict(X)
    epsilon = svm_reg[-1].epsilon
    off_margin = np.abs(y - y_pred) >= epsilon
    return np.argwhere(off_margin)

def plot_svm_regression(svm_reg, X, y, axes):
    x1s = np.linspace(axes[0], axes[1], 100).reshape(100, 1)
    y_pred = svm_reg.predict(x1s)
    epsilon = svm_reg[-1].epsilon
    plt.plot(x1s, y_pred, "k-", linewidth=2, label=r"$\hat{y}$", zorder=-2)
    plt.plot(x1s, y_pred + epsilon, "k--", zorder=-2)
    plt.plot(x1s, y_pred - epsilon, "k--", zorder=-2)
    plt.scatter(X[svm_reg._support], y[svm_reg._support], s=180,
                facecolors='#AAA', zorder=-1)
    plt.plot(X, y, "bo")
    plt.xlabel("$x_1$")
    plt.legend(loc="upper left")
    plt.axis(axes)

svm_reg2 = make_pipeline(StandardScaler(),
                         LinearSVR(epsilon=1.2, dual=True, random_state=42))
svm_reg2.fit(X, y)

svm_reg._support = find_support_vectors(svm_reg, X, y)
svm_reg2._support = find_support_vectors(svm_reg2, X, y)

eps_x1 = 1
eps_y_pred = svm_reg2.predict([[eps_x1]])

fig, axes = plt.subplots(ncols=2, figsize=(9, 4), sharey=True)
plt.sca(axes[0])
plot_svm_regression(svm_reg, X, y, [0, 2, 3, 11])
plt.title(f"epsilon={svm_reg[-1].epsilon}")
plt.ylabel("$y$", rotation=0)
plt.grid()
plt.sca(axes[1])
plot_svm_regression(svm_reg2, X, y, [0, 2, 3, 11])
plt.title(f"epsilon={svm_reg2[-1].epsilon}")
plt.annotate(
        '', xy=(eps_x1, eps_y_pred), xycoords='data',
        xytext=(eps_x1, eps_y_pred - svm_reg2[-1].epsilon),
        textcoords='data', arrowprops={'arrowstyle': '<->', 'linewidth': 1.5}
    )
plt.text(0.90, 5.4, r"$\epsilon$", fontsize=16)
plt.grid()

plt.show()

엡실론을 줄이면 서포트 벡터의 수가 늘어나서 모델이 규제된다.

마진 안에서는 훈련 샘플이 추가 되어도 모델의 예측에는 영향이 없기 때문에, 이 모델을 엡실론에 민감하지 않다고 말한다.

 

2) 커널 트릭을 제공하는 사이킷런의 SVR을 사용해 SVM 회귀 모델 만들기

from sklearn.svm import SVR

# 간단한 2차방정식 데이터셋을 생성.
np.random.seed(42)
X = 2 * np.random.rand(50, 1) - 1
y = 0.2 + 0.1 * X[:, 0] + 0.5 * X[:, 0] ** 2 + np.random.randn(50) / 10

svm_poly_reg = make_pipeline(StandardScaler(),
                             SVR(kernel="poly", degree=2, C=0.01, epsilon=0.1))
svm_poly_reg.fit(X, y)

 

비선형 회귀 작업을 처리하려면 커널 SVM 모델을 사용한다.

임의의 방정식 형태의 훈련 세트에 2차 다항 커널을 사용한 SVM 회귀 모델

svm_poly_reg2 = make_pipeline(StandardScaler(),
                             SVR(kernel="poly", degree=2, C=100))
svm_poly_reg2.fit(X, y)

svm_poly_reg._support = find_support_vectors(svm_poly_reg, X, y)
svm_poly_reg2._support = find_support_vectors(svm_poly_reg2, X, y)

fig, axes = plt.subplots(ncols=2, figsize=(9, 4), sharey=True)
plt.sca(axes[0])
plot_svm_regression(svm_poly_reg, X, y, [-1, 1, 0, 1])
plt.title(f"degree={svm_poly_reg[-1].degree}, "
          f"C={svm_poly_reg[-1].C}, "
          f"epsilon={svm_poly_reg[-1].epsilon}")
plt.ylabel("$y$", rotation=0)
plt.grid()

plt.sca(axes[1])
plot_svm_regression(svm_poly_reg2, X, y, [-1, 1, 0, 1])
plt.title(f"degree={svm_poly_reg2[-1].degree}, "
          f"C={svm_poly_reg2[-1].C}, "
          f"epsilon={svm_poly_reg2[-1].epsilon}")
plt.grid()
plt.show()

  • 왼쪽 그래프: 작은 C. 규제가 약간 있음
  • 오른쪽 그래프: 큰 C. 규제가 훨씬 적음.

SVR은 SVC의 회귀 버전이고 LinearSVR은 LinearSVC의 회귀 버전이다.

LinearSVR은 필요한 시간이 훈련 세트의 크기에 비례해서 선형적으로 늘어나지만, SVR은 훈련 세트가 커지면 훨씬 느려진다.


다음 내용

 


[출처]

핸즈 온 머신러닝

수제비 빅테이터 분석기사 필기

728x90
반응형