본문 바로가기
## 오래된 게시글 (미관리) ##/Python (Linux)

38. Python - 비선형 SVM 분류, 다항 커널과 RBF커널

by #Glacier 2019. 2. 3.
반응형

선형 SVM 분류가 효율적이고, 많은 경우에 아주 잘 작동하지만 선형적으로 분류할 수 없는 데이터도 많습니다.

비선형 데이터셋을 다루는 한 가지 방법은, 다항 특성과 같은 특성을 더 추가하는 것입니다.

이렇게 하면, 선형적으로 구분되는 데이터셋이 만들어질 수 있습니다.


아래의 왼쪽 그래프는 하나의 특성 x1만을 가진 간단한 데이터셋을 나타냅니다.

그림처럼 선형으로는 구분이 안 되지만 x1^2을 추가하여 2차원 데이터셋은 완벽하게 선형적으로 구분할 수 있습니다.


# 비선형 분류


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=(11,4))

plt.subplot(121)

plt.grid(True, which='both')

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(r'$x_1$', fontsize=20)

plt.axis([-4.5, 4.5, -0.2, 0.2])


plt.subplot(122)

plt.grid(True, which='both')

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(r'$x_1$', fontsize=20)

plt.ylabel(r'$x_2$', fontsize=20, 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를 연결한 Pipeline을 만들면 좋습니다. 이를 moons 데이터셋( 사이킷런의 make_moons 함수를 사용하여 만든 두개의 반달 모양 데이터셋) 에 적용해봅니다.


from sklearn.datasets import make_moons

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


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, which='both')

    plt.xlabel(r'$x_1$', fontsize=20)

    plt.ylabel(r'$x_2$', fontsize=20, rotation=0)

    

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

plt.show()



#LinearSVC와  LogisticRegression(solver='liblinear')가 max_iter 반복 안에 수렴하지 않고,

#verbose 매개변수가 0이 아닐 때, 반복 횟수를 증가하라는 경고 메세지가 나옵니다.

#사이킷 런의 0.20버전부터는 verbose 매개변수에 상관 없이, max_iter 반복 안에 수렴하지

#않을 경우 반복 횟수 증가 경고가 나옵니다.

#경고 메세지를 피하기 위해 max_iter 매개변수의 기본값을 1,000에서 2,000으로 증가시킵니다.


from sklearn.datasets import make_moons

from sklearn.pipeline import Pipeline

from sklearn.preprocessing import PolynomialFeatures


polynomial_svm_clf = Pipeline ([

    ('poly_features', PolynomialFeatures(degree=3)),

    ('scaler', StandardScaler()),

    ('svm_clf', LinearSVC(C=10, loss='hinge', max_iter=2000, random_state=42))

])


polynomial_svm_clf.fit(X,y)

Pipeline(memory=None,
     steps=[('poly_features', PolynomialFeatures(degree=3, include_bias=True, interaction_only=False)), ('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('svm_clf', LinearSVC(C=10, class_weight=None, dual=True, fit_intercept=True,
     intercept_scaling=1, loss='hinge', max_iter=2000, multi_class='ovr',
     penalty='l2', random_state=42, tol=0.0001, verbose=0))])



def plot_predictions(clf, axes):

    #축 설정

    x0s = np.linspace(axes[0], axes[1], 100)

    x1s = np.linspace(axes[0], 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.contour(x0, x1, y_pred, cmap = plt.cm.brg, alpha = 0.2)

    plt.contour(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) 이라는 거의 기적에 가까운 수학적 기교를 적용할 수 있습니다.

실제로는 특성을 추가하지 않으면서, 다항식 특성을 많이 추가한 것과 같은 결과를 얻을 수 있습니다.

실제로 특성을 추가하지 않으므로 엄청난 수의 특성 조합이 생기지 않습니다.

이 기법은 SVC 파이썬 클래스에 구현되어 있습니다. moons 데이터셋으로 테스트해보겠습니다.


from sklearn.svm import SVC

poly_kernel_svm_clf = Pipeline([

    ('scaler', StandardScaler()),

    ('svm_clf', SVC(kernel='poly', degree=3, coef0=1, C=5))

])


poly_kernel_svm_clf.fit(X,y)

Pipeline(memory=None,
     steps=[('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('svm_clf', SVC(C=5, cache_size=200, class_weight=None, coef0=1,
  decision_function_shape='ovr', degree=3, gamma='auto_deprecated',
  kernel='poly', max_iter=-1, probability=False, random_state=None,
  shrinking=True, tol=0.001, verbose=False))])


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

결과는 아래의 왼쪽에 나타나있습니다. 오른쪽 그래프는 10차 다항식 커널을 사용한 또 다른 SVM 분류기입니다.

모델이 과대적합이라면, 다항식의 차수를 줄여야 하고, 반대로 과소적합이라면 차수를 늘려야 합니다.

매개변수 coef0은 모델이 높은 차수와 낮은 차수에 얼마나 영향을 받을지 조절합니다.


poly100_kernel_svm_clf = Pipeline([

    ('scaler', StandardScaler()),

    ('svm_clf', SVC(kernel='poly', degree=10, coef0=100, C=5))

])


poly100_kernel_svm_clf.fit(X,y)

Pipeline(memory=None,
     steps=[('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('svm_clf', SVC(C=5, cache_size=200, class_weight=None, coef0=100,
  decision_function_shape='ovr', degree=10, gamma='auto_deprecated',
  kernel='poly', max_iter=-1, probability=False, random_state=None,
  shrinking=True, tol=0.001, verbose=False))])

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


plt.subplot(121)

plot_predictions(poly_kernel_svm_clf, [-1.5, 2.5, -1, 1.5])

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

plt.title(r'$d=3, r=1, C=5$', fontsize=18)


plt.subplot(122)

plot_predictions(poly100_kernel_svm_clf, [-1.5, 2.5, -1, 1.5])

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

plt.title(r'$d=10, r=100, C=5$', fontsize=18)

plt.show()



왼쪽 그래프는        ('svm_clf', SVC(kernel='poly', degree=3, coef0=1, C=5))

오른쪽 그래프는     ('svm_clf', SVC(kernel='poly', degree=10, coef0=100, C=5))

즉 좌측은 3차 다항식, 우측은 10차 다항식 커널을 사용한 모습입니다. coef0는 모델이 높은 차수와 낮은 차수에 얼마나 영향을 받을 지에 대해 조절합니다. (coef0의 기본값은 0입니다. 다항식 커널은 차수가 높아질수록 1보다 작은 값과 1보다 큰 값의 차이가 크게 벌어지므로 coef0을 적절한 값으로 지정하면, 고차항의 영향을 줄일 수 있습니다.)


10차 다항식 커널을 사용한 결과 3차 다항식보다 훨씬 오분류율이 줄었다는 것을 볼 수 있죠.

(하지만 지나친 고차 다항식은 과대적합이 될 수 있습니다.)


적절한 하이퍼파라미터를 찾는 방법은 일반적으로 그리드 탐색을 이용하는 것입니다.

처음에는 그리드의 폭을 크게 하여 빠르게 검색하고, 그 다음에는 최적의 값을 찾기 위해 그리드를 세밀하게 검색합니다.

하이퍼파라미터의 역할을 잘 알고 있다면 파라미터 공간에서 올바른 지역을 탐색하는 데 도움이 됩니다.


[유사도 특성 추가]


비선형 특성을 다루는 또 다른 기법은 각 샘플이 특정 랜드마크(landmark)와 얼마나 닮았는지 측정하는 유사도 함수(similarity function) 로 계산한 특성을 추가하는 것입니다. 예를 들어 앞에서 본 1차원 데이터 셋 두개의 랜드마크 x1=-2, x1=1을 추가합시다. 


그리고 (gamma) = 0.3인 가우시안 방사 기저 함수(Radial Basis Function, RBF) 를 유사도 함수로 정의하겠습니다.


[가우시안 RBF]



# 가우시안 방사 기저 함수( Radial Basis Function) = 유사도 함수


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=(11, 4))


plt.subplot(121)

plt.grid(True, which='both')

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(r"$x_1$", fontsize=20)

plt.ylabel(r"유사도", fontsize=14)

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=18,

            )

plt.text(-2, 0.9, "$x_2$", ha="center", fontsize=20)

plt.text(1, 0.9, "$x_3$", ha="center", fontsize=20)

plt.axis([-4.5, 4.5, -0.1, 1.1])


plt.subplot(122)

plt.grid(True, which='both')

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(r"$x_2$", fontsize=20)

plt.ylabel(r"$x_3$  ", fontsize=20, 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=18,

            )

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()


이 함수의 값은 0(랜드마크에서 아주 멀리 떨어진 경우) 부터 1(랜드마크와 같은 위치일 경우) 까지 변화하며 종 모양으로 나타납니다.


예를 들어, x1= -1 샘플을 살펴봅시다. 이 샘플은 첫 번째 랜드마크에서 1만큼 떨어져있고, 두 번째 랜드마크에서 2만큼 떨어져 있습니다. 그러므로, 새로 만든 특성은 입니다. 아래 그림의 오른쪽 그래프는 변환된 데이터셋을 보여줍니다. (원본 특성은 뺐습니다.)


그림에서 볼 수 있듯이, 이제 선형적으로 구분이 가능합니다.


그렇다면, 랜드마크는 어떻게 선택하는지 궁금할 것입니다.
간단한 방법은 데이터셋에 있는 모든 샘플 위치에 랜드마크를 설정하는 것입니다.
이렇게 하면 차원이 매우 커지고, 따라서 변환된 훈련 세트가 선형적으로 구분될 가능성이 높습니다. 
단점은, 훈련 세트에 있는 n개의 특성을 가진 m개의 샘플이, m개의 특성을 가진 m개의 샘플로 변환된다는 것입니다.
(원본 특성은 제외된다고 가정) 훈련 세트가 매우 클 경우 동일한 크기의 아주 많은 특성이 만들어집니다.

다항 특성 방식과 마찬가지로, 유사도 특성 방식도 머신러닝 알고리즘에 유용하게 사용될 수 있습니다.
추가 특성을 모두 계산하려면 연산 비용이 많이 드는데, 특히 훈련 세트가 클 경우 더 그렇습니다.
하지만 커널 트릭이 한 번 더 SVM의 마법을 만듭니다. 유사도 특성을 많이 추가한 것과 비슷한 결과를 실제로 특성을 추가하지 않고 만들 수 있습니다. 이제 SVC모델에 가우시안 RBF커널을 적용해보겠습니다.

#SVC 모델에 가우시안 rbf커널 적용

rbf_kernel_svm_clf = Pipeline([
    ('scaler', StandardScaler()), 
    ('svm_clf', SVC(kernel='rbf', gamma=5, C=0.001))
])

rbf_kernel_svm_clf.fit(X,y)

Pipeline(memory=None,
     steps=[('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('svm_clf', SVC(C=0.001, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma=5, kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False))])

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 = Pipeline ([

        ('scaler', StandardScaler()),

        ('svm_clf', SVC(kernel='rbf', gamma = gamma, C=C))

    ])

    rbf_kernel_svm_clf.fit(X,y)

    svm_clfs.append(rbf_kernel_svm_clf)

    

plt.figure(figsize=(11, 7))


for i , svm_clf in enumerate(svm_clfs):

    plt.subplot(221+ i)

    plot_predictions(svm_clf, [-1.5, 2.5, -1, 1.5])

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

    gamma, C = hyperparams[i]

    plt.title(r'$\gamma={}, C={}$'.format(gamma, C), fontsize=16)


plt.show()

감마를 증가시키면 종 모양의 그래프가 좁아져서 각 샘플의 영향범위가 작아집니다.

결정 경계가 조금 더 불규칙해지고, 각 샘플을 따라 구불구불하게 휘어집니다.

반대로, 작은 gamma값은 종 모양 그래프를 만들며 샘플이 넓은 범위에 걸쳐 영향을 주므로 결정 경계가 더 부드러워집니다.


결국 하이퍼파라미터 감마(gamma)가 규제의 역할을 합니다. 

모델이 과대적합일 경우 감소시켜야 하고, 과소적합일 경우 증가시켜야 합니다. (하이퍼파라미터 C와 비슷합니다.)

모델의 복잡도를 조절하려면 gamma와 C를 동시에 조절하는 것이 좋습니다.


마지막으로, 여러 커널 중 어떤 것을 사용해야 할까요?


1) 저자의 경험으로 봤을 때 언제나 선형 커널을 가장 먼저 시도해봐야 합니다. LinearSVC가 SVC(kernel='linear')보다 훨씬 빠르다는 것을 기억하세요. (특히, 훈련 세트가 아주 크거나 특성 수가 많을 경우 더 그렇습니다.)


2) 훈련 세트가 너무 크지 않다면 가우시안 RBF 커널을 사용해보면 좋습니다. 대부분의 경우 이 커널이 잘 들어맞습니다.


3) 시간과 컴퓨팅 성능이 충분하다면 교차 검증와 그리드 탐색을 사용해 다른 커널을 더 시도해볼 수 있습니다.



[ 계산 복잡도 ]


LinearSVC 파이썬 클래스는 선형 SVM을 위한 최적화된 알고리즘을 구현한 liblinear 라이브러리를 기반으로 합니다.

이 라이브러리는 커널 트릭을 지원하지 않지만, 훈련 샘플과 특성 수에 거의 선형적으로 늘어납니다.

이 알고리즘의 훈련 시간 복잡도는 대략 O(m x n) 정도입니다.


정밀도를 높이면 알고리즘의 수행시간이 길어지고, 이는 허용오차 하이퍼파라미터 로 조절합니다.

(사이킷런의 매개변수는 tol입니다.) 대부분의 분류 문제는 허용오차를 기본값으로 두면 잘 작동합니다.

(** SVC의 tol 매개변수 기본값은 0.001이고, LinearSVC의 tol 매개변수 기본값은 0.0001 입니다.)


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

훈련 시간의 복잡도는 보통 사이 입니다.

불행하게도 이는 훈련 샘플 수가 커지면 (수십만 개 이상) 엄청나게 느려진다는 것을 의미합니다.

복잡하지만 작거나 중간 규모의 훈련 세트에 이 알고리즘이 잘 맞습니다.

하지만, 특성 개수에는 특히 희소 특성(sparse features), 즉 각 샘플에 0이 아닌 특성이 몇 개 없는 경우에는 잘 확장됩니다.

이런 경우 알고리즘의 성능이 샘플이 가진 0 이 아닌 특성의 평균 수에 거의 비례합니다.


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


파이썬 클래스 

시간 복잡도 

외부 메모리 학습 지원 

스케일 조정의 필요성 

커널 트릭 

LinearSVC 

 

 O 

SGDClassifier 

 

 O 

SVC 

 

O



 블로그 

출처


이 글의 상당 부분은  [핸즈온 머신러닝, 한빛미디어/오렐리앙 제롱/박해선] 서적을 참고하였습니다.


나머지는 부수적인 함수나 메서드에 대해 부족한 설명을 적어두었습니다.

학습용으로 포스팅 하는 것이기 때문에 복제보다는 머신러닝에 관심이 있다면 구매해보시길 추천합니다.


도움이 되셨다면 로그인 없이 가능한

아래 하트♥공감 버튼을 꾹 눌러주세요! 





반응형