2025. 3. 14. 01:26ㆍ머신러닝, 딥러닝 개념/지도 학습
① 서포트 벡터 머신(Support Vector Machine, SVM) 개요
두 개의 클래스가 존재하는 데이터셋이 다음과 같이 분포되어 있다고 하자.
위 그림에서 두 클래스를 구분짓는 직선을 하나 그어보라고 하면 그을 수 있을까? 당연히 그을 수 있다! 왼쪽 아래에는 파란 점만 몰려 있고 오른쪽 위에는 빨간 점만 몰려 있으니 이들을 구분하는 선을 긋는 것은 식은 죽 먹기다. 그래서 다음과 같이 두 클래스를 구분 짓는 직선을 그어보았다.
직선을 기준으로 아래에는 파란 점들만, 위에는 빨간 점들만 있기 때문에 클래스별로 구분이 잘 되었다. 그런데 이때 어떤 클래스에 속하는지 모르는 미지의 데이터 2개가 들어왔다고 해보자.
미지의 데이터는 점 (2, 5)에 하나, 점 (5, 6)에 하나 들어와 있다. 그래프에 나타내본 결과, 둘 다 직선보다 위에 있기 때문에 모두 class1로 분류된다.
일단 점 (5, 6)은 class1에 속할 것이 거의 확실해 보인다. 하지만 점 (2, 5)가 class1에 확실히 속한다고 말할 수 있을까? 만약에 직선을 다음과 같이 그었다고 해보자.
직선을 좀 더 가파르게 그렸더니 점 (2, 5)가 이번에는 class0으로 분류되었다. 점 (5, 6)은 직선을 어떻게 긋든 무조건 class1로 분류되지만, 점 (2, 5)는 어떨 땐 class0이고 어떨 땐 class1이라 확신을 갖고 분류하기 모호하다. 이렇게 두 클래스의 영역을 구분하는 직선을 긋는 방법이 여러 가지가 있을 때, 어떤 식으로 긋느냐에 따라 새로운 데이터에 대한 분류 예측 결과가 달라질 수 있다. 그 중에서도 직선에서 멀리 떨어져 있는 데이터라면 직선을 다르게 그려도 분류 결과가 달라질 확률이 낮지만, 직선에 가까이 있는 데이터일수록 직선을 그리는 방법에 따라 분류 결과가 달라질 수 있다.
이렇게 직선을 그리는 방법은 여러 가지가 있지만 그래도 이 중에서 '잘 그린 직선'이라는게 존재한다. 예를 들어, 다음 그림을 보자.
검은색 세모로 표시된 미지의 데이터는 어느 클래스로 분류되어야 할 것 같은가? 선을 직접 그어보진 않았지만 직관적으로 봐도 클래스 1로 분류되는 것이 적절할 것 같다.
그런데 내가 직선을 이렇게 그려놓고 '아닌데? 클래스 0인뎋ㅎㅎㅎㅎㅎ' 이러면 이 글을 읽는 사람들은 '어우 뭐야? 재수 없어' 하고 이 창을 꺼버릴 것이다. 즉, 직선을 위와 같이 그려도 두 클래스가 명확히 구분되긴 하지만 뭔가 좀 짜치는(?) 느낌이 든다. 따라서 우리는 직선을 짜치지 않게 잘 그려야 한다. 그 잘 그리는 방법을 제시하는 해결책이 서포트 벡터 머신(Support Vector Machine, SVM)이 된다.
② 선형 서포트 벡터 머신(Linear SVM)
본론으로 들어가기 전에 한 가지 짚고 넘어가자면, 위와 같은 직선을 '직선'이라고 할 수도 있지만 SVM에서는 주로 초평면(hyperplane)이라는 표현을 사용한다. "헉, 그냥 평면도 아니고 초평면? 엄청나게 어려운 개념 아니야?" 할 수도 있는데, 가장 간단하게 설명하면 2차원 공간에서의 초평면은 1차원 선이고, 3차원 공간에서의 초평면은 2차원 면이다. 시각화할 수는 없지만 같은 논리로 4차원 공간의 초평면은 3차원 공간이다. 이렇듯 초평면은 선, 면, $n$차원 공간 등이 일반화된 개념이라고 생각하면 된다.
그러면 이제 본론으로 들어가자. 2차원 평면에 데이터들이 다음과 같이 분포해 있다고 하자.
여기에 두 초평면(직선)을 그릴 것인데 두 초평면은 다음과 같은 조건들을 만족시킨다.
① 두 초평면은 평행하다.
② 한 초평면은 class0의 점을 하나 이상 지나며, 또 다른 초평면은 class1의 점을 하나 이상 지난다.
③ 두 초평면 사이에는 점이 존재하지 않는다.
이 두 초평면을 다음과 같이 연두색 점선으로 그렸다.
이제 이 두 초평면의 정가운데에 검은색 초평면을 하나 그려보겠다.
이렇게 하면 서포트 벡터 머신 개념을 다룰 때 흔히 제시되는 그림이 완성되었다.
이제 그림을 보면서 용어를 하나하나 정리해보자.
용어가 많은데 하나하나 정리해보자.
- 먼저, 두 클래스의 영역을 분리하는 검은색 초평면을 '결정 경계(Decision Boundary)'라고 한다.
- 연두색 점선으로 표시된 두 초평면 중 아래에 있는 것을 minus-plane, 위에 있는 것을 plus-plane이라고 한다.
- minus-plane과 plus-plane 사이의 거리를 마진(margin)이라고 한다.
- minus-plane과 plus-plane에 걸려 있는 점들을 서포트 벡터(support vector)라고 한다. 다르게 설명하면 결정 경계와 거리가 최소인 모든 점들이 서포트 벡터인 것이다.
서포트 벡터 머신에서는 마진이 가장 클 때 두 집단이 잘 분류되었다고 판단한다. 왜 그럴까? 어떤 두 데이터의 거리가 서로 가깝다면 이 두 데이터는 같은 클래스로 분류될 확률이 높다. 하지만 거리가 멀 수록 서로 다른 클래스로 분류될 가능성이 높아진다. 이처럼 거리가 멀다는 것은 두 집단의 유사도가 적다는 의미이기 때문에, 마진이 크다는 것은 서로 다른 집단에 속하는 두 점의 유사도가 떨어진다는걸 명확히 보여준다는 뜻이다. 다시 말해 두 집단이 명확하게 분리됐다는 의미이다. 따라서 서포트 벡터 머신의 목표는 마진을 최대화 하는 결정 경계를 찾는 것이라고 할 수 있다.
그런데 왜 아까부터 직선이라는 말 놔두고 초평면이라는 단어를 계속 쓸까? 유식한 척 하려고?
만약에 svm이 2차원 평면에만 한정되는 개념이라면 그냥 직선이라고 불러도 되지만, svm은 다음과 같이 3차원 공간에도 적용할 수 있는 개념이다.
3차원 공간에서는 데이터를 구분하는 수단이 2D 평면이다. 그리고 시각화할 수는 없지만 4차원 공간에서는 3차원 공간으로 데이터를 구분할 것이다. 이렇게 svm은 2차원 뿐만 아니라 고차원에서도 적용할 수 있는 개념이기 때문에 직선, 평면, $n$차원 공간($n≥3$)을 한꺼번에 아우르는 단어인 '초평면'이라는 단어를 주로 쓴다고 이해하면 될 것 같다. 이렇듯 $n$차원의 점을 종류별로 구분하기 위해서는 $n-1$차원의 초평면이 필요하다.
마진 구하기
지금까지 수식이나 코드가 하나도 없어서 'svm 쉽고 재밌네~'라고 생각할 수 있는데, svm을 수학적으로 분석하면 선형 회귀나 로지스틱 회귀를 수학적으로 분석하는 것보다 훨씬 어렵다. 그 어려운 이야기를 지금부터 시작하려고 한다.
마진을 최대화하라고 했는데, 그러면 이 마진이 얼마인지는 어떻게 알까?
마진 크기를 정의하기 위해 각 초평면의 식을 위 그림과 같이 정의하자.
plus-plane : $\rm w \cdot x + b = 1$
결정 경계 : $\rm w \cdot x + b = 0$
minus-plane : $\rm w \cdot x + b = -1$
이렇게 벡터의 곱으로 초평면의 식을 나타내면 2차원뿐만 아니라 3차원, 4차원 등에서도 동일하게 사용할 수 있다.
minus-plane의 한 점의 좌표를 $\rm x^{-}$라고 하고, 점 $\rm x^{-}$를 지나면서 minus-plane에 수직인 직선이 plus-plane과 만나는 점을 $\rm x^{+}$이라고 하자. 초평면 $\rm w \cdot x + \it b = y$ ($y$는 상수)는 벡터 $\rm w$와 수직이므로 마진을 $d$라고 하면 ${\rm x^{+}} = {\rm x^{-}} + d \frac{\rm w}{\rm ‖w‖}$라고 할 수 있다.
따라서
이렇게 마진이 $d = \frac{2}{\rm ‖w‖}$임을 유도할 수 있다.
마진은 벡터 $\rm w$의 크기 $ {\rm ‖w‖}$에 반비례하므로 마진을 크게 하기 위해서는 반대로 $ {\rm ‖w‖}$이 작아져야 한다. 따라서 우리가 구해야 하는 $\rm w$는 다음과 같다.
어 갑자기 1/2은 어디서 튀어나왔고 제곱은 어디서 튀어나왔지? 물론 $\frac{1}{2} ‖w‖^{2}$이라고 안 쓰고 그냥 $‖w‖$이라고 써도 맞긴 하다. 하지만 L2-Norm $‖w‖$에는 제곱근이 존재하기 때문에 이후에 미분의 편의성을 위해 제곱을 해주고, 제곱을 미분하면 2가 튀어나오기 때문에 이를 상쇄해주기 위해 $\frac{1}{2}$이 곱해진 것으로 생각하면 된다. (L2-Norm에 대한 설명은 '지도 학습 3편'의 '라쏘 회귀' 부분을 참고하기 바람)
하지만 위와 같은 표현은 문제가 있다. 위 식을 만족시키려면 그냥 아예 $ {\rm ‖w‖}=0 $이면 된다. 하지만 $d = \frac{2}{\rm ‖w‖}$이기 때문에 $ {\rm ‖w‖}=0$이면 마진이 정의되지 않는다. 그러면 '$ {\rm ‖w‖} $을 0.000000....1 정도로 엄청 작게 하면 되는거 아닌가요?' 할 수도 있는데 이는 마진 범위에는 아무 점도 존재하지 않아야 한다는 중요한 제약 조건을 간과한 생각이다.
따라서 우리는 위 목적 함수에 제약 조건을 하나 붙여줘야 한다.
제약 조건
'마진 범위에는 아무 점도 존재하지 않는다.'라는 텍스트를 수학적으로 어떻게 표현할 수 있을까? 다음 그림을 다시 보자.
임의의 점 ${\rm x}_i$에 대해 변수 $y_i$를 다음과 같이 정의하자.
텍스트로 풀어서 쓰면 ${\rm x}_i$가 class1에 속하는 점이면 $y_i=1$이고 ${\rm x}_i$가 class0에 속하는 점이면 $y_i=-1$이다.
$y_i$를 위와 같이 정의했다면, 이 정의를 다음과 같이 한 줄로 줄일 수 있다.
우리는 $ {\rm ‖w‖}$를 최소화함과 동시에 위 부등식을 만족시켜야 한다. 즉, 위 부등식이 바로 '제약 조건'이 된다.
따라서 우리가 구해야 하는 $\rm w$와 $\rm b$는 다음과 같이 표현할 수 있다.
s.t.는 'subject to'의 약어로, '~의 조건 하에'라는 뜻으로 이해하면 될 것 같다.
용어를 다시 정리하자면, $y_i(\rm w \cdot x_{\it i} + {\it b}) ≥ 1$을 제약 조건(Constraint)이라고 하고, 우리가 최소화하고자 하는 $\frac{1}{2} ‖w‖^{2}$을 목적 함수(Objective Function)라고 한다.
그렇다면 이렇게 제약 조건이 걸린 최적화 문제는 어떻게 풀 수 있을까? 이를 풀기 위해서는 '라그랑주 승수법'이라는 아주 어려운 수학 개념을 도입해야 한다. 그래서 내가 푸는 방법을 설명해보려고 한 번 공부해봤는데... 이거 여기서 다루기 시작하면 정말 밑도 끝도 없을 것 같다는 생각이 들었다. 따라서 여기에서 최적화 문제의 해법을 설명하는 것은 생략하는 것이 좋을 것 같다.
(추가)
최적화 문제의 해법을 다음 글에서 포스팅했으니 궁금한 사람들은 참고하면 될 것 같다. 내용이 겁나 어렵다.
https://one-plus-one-is-two.tistory.com/18
지도 학습(6+) : 서포트 벡터 머신 심화
이 포스팅에서는 Support Vector Machine에서 쓰이는 수학 개념 위주로 다룬다. 서포트 머신 벡터에 대한 기본 개념은 다음 포스팅을 참고하기 바람.https://one-plus-one-is-two.tistory.com/17 지도 학습(6) : 서
one-plus-one-is-two.tistory.com
③ 하드 마진과 소프트 마진
데이터의 오차를 허용하느냐 아니냐에 따라 하드 마진과 소프트 마진으로 나뉜다.
하드 마진(Hard Margin)은 데이터의 오차를 절대 허용하지 않는 깐깐한 성격의 마진 결정 방법이다. 하드 마진 방법에서는 마진 영역에 데이터가 존재하지 않아야 하고, 동일한 영역에 여러 클래스의 데이터가 섞여들어가서도 안된다. 따라서 다음 그림은 하드 마진의 조건을 만족시킨 그림이라고 할 수 있다.
물론 이렇게 데이터가 아주 깔끔하게 선형적으로 나뉘어 있으면 하드 마진 방법을 사용하는 것이 좋다. 하지만 다음과 같은 데이터 분포에서는 어떨까?
빨간색과 파란색 점을 하나씩 더 추가시켰다. 빨간색 점은 데이터 분포와 어울리게 추가되었는데, 파란색은 뭔가 좀 안 어울린다.
물론 이거 하드 마진으로 맞출 수는 있다. 하지만 저 (3, 4)에 위치한 파란색 점 하나 때문에 마진을 크게 좁혀야 한다.
이처럼 하드 마진은 오차를 절대 허용하지 않기 때문에, 뭔가 좀 이상한 데이터들이 있다고 해도 거기에 하나하나 다 맞춰준다. 그렇다면 다음 그림은 어떤가?
이제는 아예 직선을 어떻게 그려도 클래스끼리 분리가 안 된다. 굳이 그리자면 다음과 같이 억지스럽게 그려야 한다.
딱 봐도 (2, 3)에 있는 빨간 점과 (5, 6)에 있는 파란 점은 뭔가 이상한 점들인데 하드 마진은 오차를 절대 허용하지 않으려고 이 점들도 다 맞춰주려고 한다. 그러다보니 결정 경계가 과적합을 연상시키는 억지스러운 모양으로 그려졌다. 실제로 위와 같이 결정 경계를 정하면 과적합이 맞다. 따라서 하드 마진이 겉보기에는 오차를 허용하지 않는다니까 좋은 것처럼 보일 수 있어도, 실제로는 마진을 크게 좁히고 모양을 비틀어가면서까지 이상치에 하나하나 다 맞춰주려고 하기 때문에 비선형적인 데이터 분포에서는 하드 마진을 사용하면 안 된다.
이러한 하드 마진의 단점을 극복하기 위해 등장한 것이 소프트 마진(Soft Margin)이다. 오차가 절대로 허용되지 않는 하드 마진과 달리 소프트 마진에서는 오차가 허용된다. 다음 그림을 다시 보자.
하드 마진 방식에서는 마진 범위 내에는 데이터가 하나도 없어야 하고, 오분류된 데이터도 없어야 한다는 엄격한 규칙 때문에 위와 같은 방식으로밖에 결정 경계를 그릴 방법이 없었다. 하지만 소프트 마진 방식에서는 이러한 엄격한 규칙이 적용되지 않는다.
(2, 3)의 빨간 점처럼 마진 안에 데이터가 있어도, (5, 6)의 파란점처럼 오분류된 데이터가 있어도 결정 경계를 위와 같이 그려도 된다. 그렇다고 막 그려도 된다는 뜻은 아니고, 이 두 점처럼 하드 마진의 규칙을 위반한 점들에 대해서는 오차가 고려된다. 이 오차를 슬랙 변수(Slack Variable)라 하고 슬랙 변수의 기호는 $\xi$(크사이, 크시)를 사용한다. $\xi_i$의 값에 따른 점의 위치는 다음과 같다.
- $\xi_i = 0$ : 정상적인 위치에 있음
- $0<\xi_i<1$ : 마진을 침범했지만 결정 경계(decision boundary)를 넘어가진 않음
- $\xi_i=1$ : 결정 경계에 걸려 있음
- $1<\xi_i<2$ : 결정 경계를 넘어갔지만 마진을 벗어나지는 않음
- $\xi_i≥2$ : 마진을 벗어나서 다른 클래스의 영역을 침범함
따라서 위 그림에서 (2, 3)에 위치한 빨간 점은 $1<\xi_i<2$가 되는 경우이며, (5, 6)에 위치한 파란 점은 $\xi_i>2$가 되는 경우이다.
소프트 마진 방식에서 풀어야 하는 최적화 문제는 다음과 같다.
최소화시켜야 하는 목적 함수를 보면 슬랙 변수 $\xi$가 포함되어 있다. 오차 허용해준다고 마진을 아무렇게나 정하면 $\xi$가 커져서 목적 함수가 최적화되지 못한다. 따라서 오차를 적당히 허용하는 선에서 마진을 정해주는 것이 핵심이다. 상수 $C$는 오차 규제 강도를 나타낸다. $C$가 클수록 목적 함수는 오차에 민감해지므로 하드 마진과 비슷한 성격을 띠게 되고, $C$가 작을수록 목적 함수는 오차에 둔감해지므로 더 많은 오차를 내고 마진을 키울 수 있다. $C$는 하이퍼파라미터이므로 다른 하이퍼파라미터들과 똑같이 적절하게 값을 설정해주는 것이 중요하다.
이제 제약 조건을 살펴보자.
⑴ $y_i ( \rm w \cdot x_{\it i} + {\it b}) ≥ 1-\xi_i$
하드 마진에서의 제약 조건 $y_i ( \rm w \cdot x_{\it i} + {\it b}) ≥ 1$에 데이터 오차 허용 등식 $y_i ( \rm w \cdot x_{\it i} + {\it b}) = 1-\xi_{\it i}$를 덧붙인 형태의 부등식이다.
⑵ $\xi_i ≥ 0$
슬랙 변수가 음수가 되는 경우는 없다. 제대로 분류된 데이터들 중 서포트 벡터가 아닌 데이터들의 경우는 $ y_i ( \rm w \cdot x_{\it i} + {\it b}) > 1$이지만 슬랙 변수는 음수가 아니라 0이다.
지금까지 소프트 마진 방식의 최적화 문제에 대해 설명했다. 하지만 최적화 문제의 해법은 이 포스팅에서는 다루지 않기로 했으므로 생략하기로 하자.
④ 커널 서포트 벡터 머신(Linear SVM)
뭔가 분리할 수 있을 것처럼 보이는데 막상 직선으로는 분리할 수 없는 데이터 분포가 존재한다. 다음 그림을 보자.
이거는 분리할 수 있어 보인다. 근데 문제는 직선으로 분리하라면 분리할 수 있는가? 직선으로는 절대 분리할 수 없다. 위와 같이 원형으로 분포된 데이터를 분리하려면 원과 같은 곡선 svm이 필요한데, 이러한 svm을 어떻게 만들 수 있을까?
위 그림을 보면 2차원 평면을 3차원 공간으로 확장시킨 후에 2차원 평면으로 데이터를 분리시키고 있다. 이렇게 차원을 확장시킴으로써 비선형 데이터를 분리하는 방식을 커널 트릭(Kernel Trick)이라고 하며, 커널 트릭이 적용된 SVM을 Kernel SVM이라고 한다. 이를 수학적으로 파고 들고 싶으면 다음 포스팅을 보도록 하자. https://one-plus-one-is-two.tistory.com/18
선형 커널(Linear Kernel)
- 선형 커널은 가장 쉽고 간단한 커널 함수이다.
- 이 커널은 데이터가 선형 분리 가능(linearly seperable)할 때 사용할 수 있다. 쉽게 말해서 데이터에 변환이 적용되지 않는다.
- 장점 : 간단하며 계산이 빠르다. 선형 분리된 데이터에 대해 효과적이다.
- 단점 : 복잡하고 비선형인 데이터에 대해서는 적합하지 않다.
다항 커널(Polynomial Kernel)
- 다항 커널은 다항 피처(feature)를 추가함으로써 더 복잡한 결정 경계를 생성할 수 있도록 한다.
- 이 커널은 데이터가 선형 분리 가능(linearly seperable)할 때 사용할 수 있다. 쉽게 말해서 데이터에 변환이 적용되지 않는다.
- 장점 : 모델이 피처들과 상호작용할 수 있다. 비선형 데이터들에 대해서도 적합시킬 수 있다.
- 단점 : 선형 커널에 비해 비용이 많이 나간다. 다항식 차수를 높게 하면 과적합 위험이 있다.
RBF 커널(Radial Basis Function Kernel)
- RBF 커널은 가우시안 커널로 알려져 있으며, 유연성이 높아 매우 인기 있는 커널이다.
- 매우 복잡하고 비선형적인 관계를 다룰 수 있다.
- 장점 : 매우 넓은 데이터 분포를 다룰 수 있다. 고차원 공간에서 효과적이다.
- 단점 : 데이터셋이 크면 계산 비용이 많이 나간다.
시그모이드 커널(Sigmoid Kernel)
- 장점 : 신경망에서 발견되는 관계와 유사한 관계를 모델링하는 데 사용할 수 있다. 구현이 간단하다.
- 단점 : 다른 커널에 비해 덜 일반적으로 사용된다. 특정 유형의 데이터에서는 효과가 떨어질 수 있다.
④ 실습
실습 코드는 다음 깃허브에서 가져왔다.
https://gist.github.com/WittmannF/60680723ed8dd0cb993051a7448f7805
Visualization of SVM Kernels Linear, RBF, Poly and Sigmoid on Python (Adapted from: http://scikit-learn.org/stable/auto_examples
Visualization of SVM Kernels Linear, RBF, Poly and Sigmoid on Python (Adapted from: http://scikit-learn.org/stable/auto_examples/classification/plot_classifier_comparison.html) - compare-svm-kernel...
gist.github.com
그동안 웬만하면 내가 실습 코드를 직접 만들어서 사용했는데, 사실 실습 코드를 직접 만들면 내가 아는대로만 코드를 짜게 되기 때문에 지식 확장에 크게 도움이 되지 않는다는 생각이 들었다. 다른 사람이 작성한 코드를 참고해야 몰랐던 개념도 만날 수 있고, 이 개념은 왜 쓰이는건지 깊게 탐구할 수 있기 때문에 앞으로는 웬만하면 이렇게 다른 사람이 작성한 코드를 가져와서 리뷰하려고 한다(사실 내가 직접 짜기 귀찮은 것도 있다).
그렇다면 주어진 코드를 차근차근 리뷰해보자. 먼저 필요한 함수 및 클래스들을 import 한다.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.model_selection import train_test_split # sklearn.cross_validation -> sklearn.model_selection
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_moons, make_circles, make_classification
from sklearn.svm import SVC
네 번째 줄이 원래 from sklearn.cross_validation이라고 되어있는데 이대로 실행하면 에러가 난다. 아마 구버전에서 사용하던 이름인 것 같다. cross_validation을 model_selection으로 바꿔주면 정상 실행된다.
h = .02 # step size in the mesh
names = ["Linear SVM", "RBF SVM", "Poly SVM", "Sigmoid SVM"]
classifiers = [
SVC(kernel="linear", C=0.025),
SVC(gamma=2, C=1),
SVC(kernel="poly", C=0.025),
SVC(kernel="sigmoid", gamma=2)]
h는 나중에 나올 np.meshgrid() 함수 안에서 사용할 파라미터인데, np.meshgrid()가 뭔지 아직 모르므로 설명은 뒤로 미루기로 하자.
사용할 모델들의 이름(그래프 제목으로 쓸 예정)을 names 리스트에, 사용할 모델 객체를 classfiers 리스트에 저장한다.
svm에는 크게 SVC와 SVR이 있는데, svm은 회귀 문제와 분류 문제에 모두 쓸 수 있는 모델이기 때문에 회귀(Regression) 문제에 쓸 것이라면 SVR 클래스를 쓰면 되고, 분류(Classification) 문제에 쓸 것이라면 SVC 클래스를 쓰면 된다. 이 코드는 분류 문제를 만들어서 분류 작업을 수행할 예정이므로 SVC 클래스를 import해 왔다.
kernel 별로 linear, rbf, poly, sigmoid 커널이 각각 적용된 4가지 SVC 객체를 만들었다. kernel 파라미터에 적용할 커널을 문자열로 전달하면 되는데, 디폴트 값이 'rbf'이므로 RBF 커널을 적용하려면 파라미터 값을 전달하지 않아도 된다. C 파라미터는 규제의 강도를 의미한다. SVC에서는 릿지 회귀처럼 제곱된 L2 규제를 적용하는데, C 파라미터로 이 규제의 강도를 조절해주면 된다. 디폴트값은 1.0인데 규제가 너무 강해서 과소적합 된다 싶으면 C를 줄이고, 규제가 너무 약해서 과적합 된다 싶으면 C를 키우면 된다.
gamma에 대한 설명은 사실 심화 포스팅에서 했는데, 그 글을 안 읽는 사람들이 많을 것 같아서 해당 내용만 캡쳐해서 가져왔다.
gamma 파라미터에는 위 수식에서 $\gamma$에 어떤 값을 넣을지 전달하면 된다. 선형 커널일 때는 감마 값이 필요 없다.
X, y = make_classification(n_features=2, n_redundant=0, n_informative=2,
random_state=1, n_clusters_per_class=1)
rng = np.random.RandomState(2)
X += 2 * rng.uniform(size=X.shape)
linearly_separable = (X, y)
sklearn.datasets.make_classification() 함수는 가상의 분류 모형 데이터를 만들어주는 함수이다. 이 함수를 이용하면 다음과 같이 선형 분리 가능한(linearly seperable) 데이터 분포를 만들어 준다.
하지만 실제 사용할 데이터는 이렇게 깔끔하게 분리된 데이터를 사용하지 않고, 약간의 노이즈(noise)를 첨가할 것이다. 뒤에 따라오는 두 줄의 코드가 노이즈(noise)를 첨가해주는 코드이다. rng는 난수(노이즈) 생성기이며, rng.uniform() 함수는 0부터 1까지의 난수(노이즈)를 임의로 추출하는 함수이다. 따라서 실제 사용할 데이터 분포는 다음과 같은 모습이다.
이렇게 make_classification으로 만든 분류 모형에 약간의 노이즈를 첨가해준 것을 linearly_seperable 함수에 저장한다.
datasets = [make_moons(noise=0.3, random_state=0),
make_circles(noise=0.2, factor=0.5, random_state=1),
linearly_separable
]
데이터셋은 아까 만들었던 linearly_seperable을 포함해서 3가지를 사용할 것이다. sklearn.datasets.make_moons 함수와 sklearn.datasets.make_circles 함수는 각각 달 모양과 동그라미 모양을 가진 데이터 분포를 만들어주는 함수이다. 그림으로 직접 보여주면 다음과 같다.
물론, linearly_seperable에서도 그랬듯이 이 데이터셋들도 이렇게 예쁜 모양으로 사용하지 않고, 다음과 같이 노이즈를 첨가하여 사용할 것이다.
figure = plt.figure(figsize=(27, 9))
i = 1
# iterate over datasets
for ds in datasets:
...
# iterate over classifiers
for name, clf in zip(names, classifiers):
...
figure.subplots_adjust(left=.02, right=.98)
plt.show()
그 다음 코드는 대충 이런 식으로 되어 있다. 3개의 데이터셋을 4개의 모델에 대해 학습시키고 테스트하는 것을 이중 반복문으로 구성하였다.
먼저 외부 반복문의 내용부터 차근차근 보자.
# iterate over datasets
for ds in datasets:
# preprocess dataset, split into training and test part
X, y = ds
X = StandardScaler().fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4)
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
...
이 코드는 전반적으로 데이터 전처리하는 코드이다.
데이터셋의 내용을 X, y에 저장한다. X는 점의 좌표(2차원 배열), y는 분류 클래스(타깃값)이다.
X를 StandardScaler 객체를 이용해서 평균이 0이고 표준편차가 1인 분포로 스케일링한다. (스케일링은 전처리할 때 중요한 개념인데 이는 나중에 다룰 기회가 있다면 다루도록 함)
그런 다음 train_test_split 함수를 이용해 훈련 데이터셋과 테스트 데이터셋을 나눈다.
X에는 점의 좌표가 2차원 배열로 나타나 있으므로 X[:, 0]에는 점의 x좌표가, X[:, 1]에는 점의 y좌표가 저장되어 있다.
np.meshgrid() 함수는 말로 설명하기 좀 힘들어서 그냥 간단한 코드를 작성하고 실행한 것을 캡쳐해봤다.
뭔 느낌인지는 대충 알 것 같다. 이걸 왜 사용하는지는 아직 모르겠지만... (뒷 코드 보면 알겠지 뭐)
# iterate over datasets
for ds in datasets:
...
# just plot the dataset first
cm = plt.cm.RdBu
cm_bright = ListedColormap(['#FF0000', '#0000FF'])
ax = plt.subplot(len(datasets), len(classifiers) + 1, i)
# Plot the training points
ax.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=cm_bright)
# and testing points
ax.scatter(X_test[:, 0], X_test[:, 1], c=y_test, cmap=cm_bright, alpha=0.6)
ax.set_xlim(xx.min(), xx.max())
ax.set_ylim(yy.min(), yy.max())
ax.set_xticks(())
ax.set_yticks(())
i += 1
이 코드는 전반적으로 데이터 분포를 시각화하는 코드이다.
먼저 두 가지 컬러맵(Colormap)을 정의해준다. cm과 cm_bright를 정의해줬는데, cm은 당장 사용할건 아니고 뒤에 contourf()라는 함수가 나오는데 거기에서 사용할 것이다. cm_bright는 산점도의 점에 적용할 컬러맵이다. 이후 시각화 결과를 보면 알겠지만, 산점도 점에 cm_bright 컬러맵을 적용했기 때문에 점이 빨간색(##FF0000)과 파란색(##0000FF) 중 하나로 나타난다.
다음으로 서브플롯을 만든다. 데이터셋의 개수는 3, 분류기(SVC)의 개수는 4이므로 3×5 크기의 서브플롯 그리드를 만든다.
왜 3×4가 아니라 3×5냐면 다음과 같은 형태로 서브플롯 그리드를 구성할 것이기 때문이다.
1열에는 먼저 데이터 분포만을 시각화한다(지금 하고 있는거).
그리고 2열에는 선형 커널 SVM의 분류 결과, 3열에는 RBF SVM의 분류 결과, 4열에는 다항 SVM의 분류 결과, 5열에는 시그모이드 SVM의 분류 결과를 시각화할 것이다.
행 기준으로는 1행에는 달 모양 데이터 분포에 대한 분류 결과, 2행에는 원형 데이터 분포에 대한 분류 결과, 3행에는 선형 분리 가능한 데이터 분포에 대한 분류 결과를 시각화할 것이다.
그런 다음 ax.scatter()로 1열에 훈련 데이터셋과 테스트 데이터셋에 대한 산점도를 각각 그려준다.
ax.set_xlim()과 ax.set_ylim()으로 x축 범위와 y축 범위를 설정한다.
ax.set_xticks(())와 ax.set_yticks(())는 눈금 제거하는 코드인데, 그냥 눈금이 보이게 하고 싶으면 이 두 줄 코드를 지워도 된다.
외부 반복문에 의해 완성되는 그래프는 다음과 같다.
다음은 내부 반복문 내용을 살펴보자.
# iterate over datasets
for ds in datasets:
...
# iterate over classifiers
for name, clf in zip(names, classifiers):
ax = plt.subplot(len(datasets), len(classifiers) + 1, i)
clf.fit(X_train, y_train)
score = clf.score(X_test, y_test)
# Plot the decision boundary. For that, we will assign a color to each
# point in the mesh [x_min, m_max]x[y_min, y_max].
if hasattr(clf, "decision_function"):
Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()])
else:
Z = clf.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
...
외부 반복문이 데이터를 전처리하고 데이터 분포를 시각화하여 1열에 나타내는 것이었다면, 내부 반복문은 4개의 분류기로 3개의 데이터셋을 학습시키고 평가한 다음, 이를 2~5열에 각각 시각화시키는 코드로 구성되어 있다.
먼저 시각화 결과를 저장할 서브플롯 위치를 지정하고(ax = 어쩌구저쩌구), clf.fit() 함수로 분류기를 적합시킨다. 그리고 clf.score() 함수로 테스트 데이터셋을 분류시키고 이에 대한 점수(정확도)를 score 변수에 저장한다.
다음으로 if-else 분기문이 나오는데, hasattr(clf, "decision_function")은 clf에 decision_function이라는 메서드가 있다면 True를 반환하고 그렇지 않으면 False를 반환한다. 따라서 clf에 decision_function이라는 메서드가 있다면 그 메서드를 사용하고, 없다면 else로 분기해서 predict_proba()라는 메서드를 사용한다. 참고로 SVC 클래스는 decision_function이라는 메서드가 존재하는데, SVC 클래스의 decision_function 메서드는 입력값으로 들어온 점이 결정 경계(decision boundary)로부터 얼마나 떨어져 있는지 나타내는 점수를 반환한다. 즉, 반환값이 양수이면 양(+)의 클래스에, 음수이면 음(-)의 클래스에 속하는 것이며 결정 경계에서 멀수록 절댓값이 커진다.
np.c_() 함수와 xx.ravel(), yy.ravel() 함수에 대해서는 역시 간단한 코드를 작성하고 실행 결과를 보이는 것이 편할 것 같다.
즉, xx에는 x좌표들이, yy에는 y좌표들이 들어있으니 이들을 1차원으로 펼친 다음 하나하나 결합해서 모든 점의 좌표를 만드는 것이다.
# iterate over datasets
for ds in datasets:
...
# iterate over classifiers
for name, clf in zip(names, classifiers):
...
# Put the result into a color plot
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, cmap=cm, alpha=.8)
# Plot also the training points
ax.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=cm_bright)
# and testing points
ax.scatter(X_test[:, 0], X_test[:, 1], c=y_test, cmap=cm_bright,
alpha=0.6)
ax.set_xlim(xx.min(), xx.max())
ax.set_ylim(yy.min(), yy.max())
ax.set_xticks(())
ax.set_yticks(())
ax.set_title(name)
ax.text(xx.max() - .3, yy.min() + .3, ('%.2f' % score).lstrip('0'),
size=15, horizontalalignment='right')
i += 1
다음으로 그래프를 생성하는 코드이다.
ax.contourf()로 등고선을 생성하고 각 면을 색칠할건데, 인자로는 점들의 x좌표가 2차원 배열로 저장된 xx, 점들의 y좌표가 2차원 배열로 저장된 yy, 그리고 각 점들이 결정 경계로부터 떨어진 거리가 저장된 배열인 Z를 전달한다. 단, Z는 1차원 배열이므로 Z.reshape(xx.shape) 코드로 Z를 xx와 똑같은 2차원 배열로 변환한다. 컬러맵은 이전에 생성했던 cm(plt.cm.RdBu)을 사용한다(RdBu는 Red to Blue인거 대강 눈치챘을 것으로 생각함).
이후에는 산점도 그리기, x축 및 y축 범위 설정, 눈금 제거 코드를 이전과 똑같이 작성해준다.
ax.set_title() 함수로 그래프 제목을 설정하고, ax.text() 함수로 그래프의 오른쪽 아래에 정확도를 표시해준다. 여기서는 오른쪽 아래를 (xx.max() - 0.3, yy.min() + 0.3)으로 표시하였다.
시각화 결과
2~5열에 표시된 그래프에서 동그라미 점은 훈련 데이터셋, 세모 점은 테스트 데이터셋을 의미한다. 정확도는 테스트 데이터셋에 대해서만 계산되므로 동그라미 데이터는 정확도에 영향을 주지 않는다.
Linear SVM의 경우에는 linear seperable한 모양으로 분포되어 있는 세 번째 데이터셋이나 달 모양 분포인 첫 번째 데이터셋에는 양호한 성능을 보이지만, 원형으로 분포된 두 번째 데이터셋은 임의 추출(0.5) 미만의 성능을 보이고 있다.
RBF SVM은 어느 데이터 분포에 대해서도 양호한 성능을 보인다. 다른 SVM 모델들은 원형 분포인 두 번째 데이터셋에서 임의 추출 이하의 성능을 보이지만, RBF 커널에서만 유일하게 0.88이라는 높은 정확도를 자랑한다. SVM에서 RBF kernel이 일반적으로 많이 쓰이는 이유를 짐작할 수 있게 한다.
Poly SVM과 Sigmoid SVM은 원형 분포에 대해서는 둘 다 임의 추출 이하의 성능을 보이며, Poly SVM은 달 모양 분포에 상대적으로 강하고 Sigmoid SVM은 linear seperable 모양의 분포에 상대적으로 강하다.
'머신러닝, 딥러닝 개념 > 지도 학습' 카테고리의 다른 글
지도 학습(7) : 나이브 베이즈 (1) | 2025.03.18 |
---|---|
지도 학습(6+) : 서포트 벡터 머신 심화 (1) | 2025.03.15 |
지도 학습(5) : k-최근접 이웃 (3) | 2025.03.10 |
지도 학습(4) : 로지스틱 회귀 (3) | 2025.03.10 |
지도 학습(3) : 다항 회귀, 릿지 회귀, 라쏘 회귀, 엘라스틱 넷 (1) | 2025.03.05 |