References
- https://docs.opencv.org/
- OpenCV 4로 배우는 컴퓨터 비전과 머신러닝
Contents
- 서포트 벡터 머신
- SVM 클래스 사용 방법
서포트 벡터 머신(SVM, Support Vector Machine)은 기본적으로 두 개의 클래스로 구성된 데이터를 가장 잘 분리하는 초평면(hyperplane)을 찾는 머신러닝 알고리즘입니다. 여기서 초평면은 두 클래스의 데이터를 분리하는 N차원 공간상의 평면을 의미합니다.
[ML] Support Vector Machine(SVM)
예전 포스팅에서는 수학적인 측면에서 SVM에 대해 살펴본 적이 있습니다.
간단하게 SVM 알고리즘의 동작을 살펴보겠습니다.
위의 그림은 파란색 사각형과 빨간색 삼각형으로 표시된 두 클래스의 점들의 분포를 보여주고 있습니다. 이 두 클래스를 구분하기 위한 직선은 매우 다양한 형태로 만들 수 있는데, 왼쪽 그래프에서 1번과 2번 직선은 모두 두 종류의 점들을 잘 나누고 있습니다. 하지만 1번의 경우에는 조금만 왼쪽 또는 오른쪽으로 이동하면 분리에 실패할 수 있습니다. 2번 직선은 왼쪽으로 이동하는 것은 무난하지만, 오른쪽으로 조금만 이동하면 실패합니다. 이는 1번과 2번 직선이 모두 입력에 해당하는 점들에 너무 가까이 위치하고 있기 때문입니다. 반면 오른쪽 그래프에서의 3번 직선은 두 클래스 점들 사이를 충분히 여유를 두고 분리하고 있습니다. 이때 3번 직선에 해당하는 초평면과 가장 가까이 있는 빨간색 또는 파란색 점과의 거리를 마진(margin)이라고 하며, SVM은 이 마진을 최대로 만드는 초평면을 구하는 알고리즘입니다.
SVM 알고리즘은 기본적으로 선형으로 분리 가능한 데이터에 적용할 수 있는데, 실생활에서의 데이터는 선형으로 분리되지 않는 경우가 많습니다. 따라서 이러한 경우에 SVM 알고리즘을 적용하기 위해서는 커널 트릭(kernel trick)이라는 기법을 사용합니다. 커널 트릭은 적절한 커널 함수를 이용하여 입력 데이터의 feature 차원을 늘리는 방식입니다. 원본 데이터 차원에서는 선형으로 분리할 수 없었던 데이터를 커널 트릭으로 고차원 공간으로 이동하면 선형으로 분리 가능한 형태로 바뀔 수 있습니다.
SVM 알고리즘에서 사용할 수 있는 커널 함수의 종류는 아래와 같습니다.
일반적으로 방사 기저 함수를 주로 사용하며, 이 커널을 사용할 때에는 \(\gamma\)인자 값을 적절하게 설정해야 합니다. 만약 입력 데이터가 선형으로 분리가 가능하다면 선형 커널을 사용하는 것이 가장 빠릅니다.
SVM 클래스
OpenCV에서 SVM 알고리즘은 SVM 클래스에 구현되어 있습니다. 이 클래스는 ml 모듈에 포함되어 있으며, cv::ml 네임스페이스에 정의되어 있습니다. OpenCV에 구현된 SVM 클래스는 오픈소스 라이브러리인 LIBSVM을 기반으로 만들어졌습니다.
SVM 클래스를 이용하려면 먼저 SVM 객체를 생성해야 하며, SVM 객체는 SVM::create() 정적 멤버 함수를 사용하여 생성할 수 있습니다.
static Ptr<SVM> cv::ml::SVM::create();
반환값은 SVM 객체를 참조하는 Ptr 스마트 포인터 객체입니다.
SVM 클래스 객체를 생성한 후, 훈련 데이터를 학습하기 전에 먼저 SVM 알고리즘의 속성들을 설정해야 합니다. 대표적으로 설정해야 할 속성에는 타입과 커널 함수가 있습니다. 먼저 SVM 타입을 설정하는 함수는 SVM::setType() 입니다.
virtual void cv::ml::SVM::setType(int val);
SVM 클래스는 기본적으로 SVM::Types::C_SVC 타입을 사용하도록 초기화됩니다. 이 타입은 일반적인 N-클래스 분류 문제에서 사용되는 방식입니다. 다른 타입을 사용하려면 위 함수를 통해 타입을 변경해야 하며, SVM::setType() 함수의 val에는 SVM::Types 열거형 상수 중 하나를 지정할 수 있습니다.
C_SVC 타입을 사용하는 경우, SVM 알고리즘 내부에서 사용하는 C 파라미터 값을 적절히 선택해야 합니다. C 값을 작게 설정하면 훈련 데이터 중에 잘못 분류되는 데이터가 있어도 최대 마진을 선택합니다. C 값을 크게 설정하면 마진이 작아지더라도 잘못 분류되는 데이터가 적어지도록 합니다. 만약 데이터에 이상치 데이터가 많은 경우에는 C 파라미터 값을 조금 크게 설정하는 것이 좋습니다.
SVM 타입을 설정한 뒤에는 SVM 알고리즘에서 사용할 커널 함수를 지정해야 합니다. 이는 SVM::setKernel() 함수를 통해 수행합니다.
virtual void cv::ml::setKernel(int kernelType);
이 함수의 kernelType 인자에는 SVM::KernelTypes 열거형 상수 중 하나를 지정할 수 있습니다.
SVM 알고리즘의 타입과 커널 함수를 선택한 후에는 각각의 타입과 커널 함수에 필요한 파라미터를 설정해야 합니다. SVM 클래스에서 설정할 수 있는 파라미터는 C, Nu, P, Degree, Gamma, Coef0 등이 있으며, 이들 파라미터는 차례대로 1, 0, 0, 0, 1, 0으로 초기화됩니다. 각각의 파라미터는 파라미터 이름에 해당하는 setXXX()와 getXXX() 함수를 이용하여 값을 설정하거나 읽을 수 있습니다.
이렇게 SVM 객체를 생성하고, 타입과 커널 함수, 파라미터를 설정한 후에는 StatModel::train() 함수를 이용하여 학습시킬 수 있습니다. 하지만 SVM에서 사용하는 파라미터를 적절하게 설정하지 않으면 학습이 제대로 되지 않는 경우가 발생합니다. 사실 대부분의 훈련 데이터는 다차원 공간에서 다양한 분포와 형태를 갖기 때문에 파라미터 값을 어떻게 설정해야 학습이 잘 될 것인지를 바로 알기 어렵습니다. 따라서 OpenCV의 SVM 클래스는 각각의 파라미터에 대해 설정 가능한 값을 적용해 보고, 그중 가장 성능이 좋은 파라미터를 자동으로 찾아 학습하는 SVM::trainAuto() 함수를 제공합니다.
위 함수는 다양한 파라미터 값을 이용하여 여러 번 학습과 검증을 반복한 후, 최적의 파라미터를 이용하여 학습을 완료합니다. 훈련 데이터를 kFold개의 부분 집합으로 분할하고, 이 중 (kFold - 1)개의 부분 집합으로 학습하고 나머지 한 개의 부분 집합으로 성능을 검증하는 k-폴드 교차 검증을 수행합니다. 각각의 파라미터가 가질 수 있는 값의 범위는 Cgrid, gammaGrid, pGrid, nuGrid, coeffGrid, degreeGrid로 지정할 수 있으며 이들 인자의 타입 ParamGrid 클래스는 파라미터 값이 가질 수 있는 최솟값, 최댓값, 스텝 등을 표현하는 역할을 합니다.
(단, 다양한 파라미터를 이용하여 많은 검증을 수행하기 때문에 시간은 꽤 오래 걸리는 편입니다.)
SVM 학습이 완료되었으면 테스트 데이터에 대한 예측은 StatModel::predict()를 통해 수행할 수 있습니다.
아래 코드는 2차원 평면에서 두 개의 클래스로 구성된 점들을 SVM 알고리즘으로 분류하고, 그 경계면을 화면에 표시합니다.
#include <iostream>
#include "opencv2/opencv.hpp"
int main(int argc, char* argv[])
{
cv::Mat train = cv::Mat_<float>({ 8, 2 }, {
150, 200, 200, 250, 100, 250, 150, 300,
350, 100, 400, 200, 400, 300, 350, 400 });
cv::Mat label = cv::Mat_<int>({ 8, 1 }, { 0,0,0,0,1,1,1,1 });
auto svm = cv::ml::SVM::create();
svm->setType(cv::ml::SVM::C_SVC);
svm->setKernel(cv::ml::SVM::KernelTypes::RBF);
svm->trainAuto(train, cv::ml::ROW_SAMPLE, label);
cv::Mat img = cv::Mat::zeros(cv::Size(500, 500), CV_8UC3);
for (int r = 0; r < img.rows; r++) {
for (int c = 0; c < img.cols; c++) {
cv::Mat test = cv::Mat_<float>({ 1, 2 }, { (float)c, (float)r });
int res = cvRound(svm->predict(test));
if (res == 0) {
img.at<cv::Vec3b>(r, c) = cv::Vec3b(128, 128, 255);
}
else {
img.at<cv::Vec3b>(r, c) = cv::Vec3b(128, 255, 128);
}
}
}
for (int i = 0; i < train.rows; i++) {
int x = cvRound(train.at<float>(i, 0));
int y = cvRound(train.at<float>(i, 1));
int l = label.at<int>(i, 0);
if (l == 0) {
cv::circle(img, cv::Point(x, y), 5, cv::Scalar(0, 0, 128), -1, cv::LINE_AA);
}
else {
cv::circle(img, cv::Point(x, y), 5, cv::Scalar(0, 128, 0), -1, cv::LINE_AA);
}
}
cv::imshow("svm", img);
cv::waitKey();
}
위 코드를 실행한 결과는 다음과 같습니다.
빨간색 원으로 표시된 점들이 0번 클래스이고, 녹색 점들이 1번 클래스입니다. 그리고 img의 전체 픽셀 좌표를 학습된 SVM 분류기의 테스트 데이터로 사용하여 그 결과를 빨간색 또는 초록색으로 나타냈습니다. 방사 기저 함수를 커널로 사용하였기 때문에 두 클래스 경계면이 곡선의 형태로 나타나는 것을 확인할 수 있습니다. 만약 커널 함수를 SVM::KernelTypes::LINEAR로 변경하면 아래의 결과가 나옵니다.
'프로그래밍 > OpenCV' 카테고리의 다른 글
[OpenCV] dnn 모듈 (0) | 2022.05.12 |
---|---|
[OpenCV] 머신러닝과 KNearest 클래스 (0) | 2022.05.10 |
[OpenCV] 컬러 이미지 처리 (0) | 2022.05.09 |
[OpenCV] 에지 검출 (0) | 2022.05.08 |
[OpenCV] 어파인 변환 / 투시 변환 (0) | 2022.05.07 |
댓글