OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝/ 머신 러닝

머신 러닝과 OpenCV

머신 러닝 개요

  • 머신 러닝(machine learning)이란 주어진 데이터를 분석하여 규칙성, 패턴 등을 찾고 이를 이용하여 의미 있는 정보를 추출하는 과정.
    • 데이터로부터 규칙을 찾아내는 과정을 학습(train) 또는 훈련이라고 하고, 학습에 의해 결정된 규칙을 모델(model)이라 한다. 그리고 새로운 데이터를 학습된 모델에 입력으로 전달하고 결과를 판단하는 과정을 예측(predict) 또는 추론(inference)라고 한다.
  • 머신 러닝은 크게 지도 학습(supervised learning)과 비지도 학습(unsupervised learning)으로 구분된다.
    • 지도 학습은 정답을 알고 있는 데이터를 이용하여 학습을 진행하는 방식으로 훈련 데이터에 대한 정답에 해당하는 내용을 레이블(label)이라고 한다.
    • 아래 그림은 지도 학습 방식으로 영상을 인식하는 과정을 나타낸다.

  • 영상 데이터는 픽셀로 구성되어 있지만, 이 픽셀 값을 그대로 머신 러닝 입력으로 사용하지는 않는다. 영상의 픽셀 값은 조명 변화, 객체의 이동 및 회전 등에 의해 매우 민감하게 변화하기 때문.
    • 때문에 많은 머신 러닝 응용에서는 영상의 다양한 변환에도 크게 변경되지 않는 특징 정보를 추출하여 머신 러닝으로 전달한다.
    • 이처럼 영상 데이터를 사용하는 지도 학습에서는 먼저 다수의 훈련 영상에서 특징 벡터를 추출하고, 이를 이용하여 머신 러닝 알고리즘을 학습 시킨다.
    • 학습의 결과로 생성된 학습 모델은 이후 예측 과정에서 사용된다. 예측 과정에서도 입력 영상으로부터 특징 벡터를 추출하고, 이 특징 벡터를 학습 모델 입력으로 전달하면 입력 영상이 어떤 영상인지에 대한 예측 결과를 얻을 수 있다.
  • 지도 학습은 주로 회귀(regression) 또는 분류(classfication)에 사용된다.
    • 회귀는 연속된 수치 값을 예측하는 작업으로 학생들의 키와 몸무게의 상관관계를 학습하고, 새로운 학생의 키를 입력으로 주었을 때 몸무게를 예측하는 것과 같은 것이다.
    • 분류는 이산적인 값을 결과로 출력하는 머신 러닝으로 사과와 바나나를 구분 –또는 인식(recognition)– 하는 것이 이에 해당한다. 
  • 비지도 학습은 훈련 데이터의 정답에 대한 정보 없이 오로지 데이터 자체만을 이용하는 학습 방식이다.
    • 예컨대 무작위로 섞여 있는 사과와 바나나 사진을 두 개의 그룹으로 나누도록 학습시키는 방식이다. 이 경우 분리된 두 개의 사진 집합이 무엇을 의미하는지는 알수 없고, 단지 두 사진 집합에서 서로 구분되는 특징을 이용하여 서로 분리하는 작업만 수행한다.
    • 비지도 학습은 주로 군집화(clustering) 에 사용된다.
  • 머신 러닝 알고리즘 종류에 따라 내부적으로 사용하는 많은 파라미터에 의해 성능이 달라지기도 한다. 그러므로 최적의 파라미터를 찾는 것이 해결해야 하는 과제가 되기도 한다.
    • 이런 경우 훈련 데이터를 k개의 부분 집합으로 분할하여 학습과 검증(validation)을 반복하면서 최적의 파라미터를 찾을 수 있다.
    • 예컨대 8000개의 훈련 영상을 800개씩 열 개의 부분 집합으로 분할하고 이 중 아홉 개의 부분 집합으로 학습하고 나머지 한 개의 집합을 이용하여 성능을 검증한다. 그리고 검증을 위한 부분 집합을 바꿔가면서 여러 번 학습과 검증을 수행한다.
    • 이처럼 훈련 데이터를 k개의 부분 집합으로 분할하여 학습과 검증을 반복하는 작업을 k-폴드 교차 검증(k-fold cross-validation)이라 한다.
  • 머신 러닝 알고리즘으로 훈련 데이터를 학습할 경우 훈련 데이터에 포함된 잡음 또는 이상치(outlier)의 영향을 고려해야 한다.

OpenCV 머신 러닝 클래스

  • OpenCV는 다양한 머신 러닝 알고리즘을 클래스로 구현하여 제공한다.
    • OpenCV에서 제공하는 머신 러닝 클래스는 주로 ml 모듈에 포함되어 있고, cv::ml::StatModel 추상 클래스를 상속받아 만들어진다.
    • StatModel 클래스 이름은 통계적 모델(statistical model)을 의미한다.
  • StatModel 추상 클래스를 상속 받아 만들어진 머신 러닝 알고리즘 구현 클래스는 아래 그림과 같다.
    • StatModel 클래스는 머신 러닝 알고리즘을 학습시키는 StatModel::train() 멤버 함수를 갖고 있다. StatModel 클래스를 상속 받아 만든 머신 러닝 구현 클래스는 각각의 머신 러닝 알고리즘에 해당하는 train()과 predict() 기능을 재정의하고 있다.
  • StatModel::train() 함수는 samples에 저장된 다수의 훈련 데이터를 사용하여 머신 러닝 알고리즘을 학습한다.
    • 이때 훈련 데이터에 대한 정답 또는 레이블 정보는 response 인자로 전달한다.
    • 보통 samples와 responses 인자는 Mat 타입 객체로 전달한다.
    • Mat 행렬에 훈련 데이터가 어떤 방식으로 저장되어 있는지를 layout 인자로 설정한다. layout에는 RAW_SAMPLE(행 단위)과 COL_SAMPLE(열 단위) 상수를 지정할 수 있다.
    • StatModel 클래스를 상속받은 클래스 객체에서 train() 함수를 호출하면 각 머신 러닝 알고리즘에 해당하는 방식으로 학습을 진행한다.
  • 이미 학습된 모델에 대해 테스트 데이터의 응답을 얻고 싶으면 StatModel::predict() 함수를 사용하면 된다.
    • StatModel::predict() 함수는 순수 가상 함수로 선언되었으며, 각각의 머신 러닝 알고리즘 구현 클래스는 자신만의 알고리즘을 이용한 예측을 수행하도록 predict() 함수를 재정의하고 있다.
    • 일부 머신 러닝 알고리즘 구현 클래스는 predict(0 대신 고유의 예측 함수를 이용하기도 한다.
  • OpenCV에서 StatModel 클래스를 상속받아 만들어진 머신 러닝 알고리즘 구현 클래스에 대한 설명은 아래 표에 있다.
클래스 이름 설명
ANN_MLP 인공 신경망(artificial neural network) 다층 퍼셉트론(multi-layer perceptrons). 여러 개의 은닉층을 포함한 신경망을 학습시킬 수 있고, 입력 데이터에 대한 결과를 예측할 수 있다.
DTrees 이진 의사 결정 트리(decision trees) 알고리즘. DTrees 클래스는 다시 부스팅 알고리즘을 구현한 ml::Boost 클래스와 랜덤 트리(random tree) 알고리즘을 구현한 ml:RTree 클래스의 부모 클래스 역할을 한다.
Boost 부스팅(boosting) 알고리즘. 다수의 약한 분류기(weak classifier)에 적절한 가중치를 부여하여 성능이 좋은 분류기를 만든다.
RTrees 랜덤 트리(random tree) 또는 랜덤 포르세느(random forest) 알고리즘.입력 특징 벡터를 다수의 트리로 예측하고, 그 결과를 취합하여 분류 또는 회귀를 수행한다.
EM 기댓값 최대화(Expectation Maximization). 가우시안 혼합 모델(Gausssian mixture model)을 이용한 군집화 알고리즘
KNearest k 최근접 이웃(k-Nearest Neighbor) 알고리즘. k 최근접 이웃 알고리즘은 샘플 데이터와 인접합 k개의 훈련 데이터를 찾고, 이 중 가장 많은 개수에 해당하는 클래스를 샘플 데이터 클래스로 지정한다.
LogisticRegression 로지스틱 회귀(logistic regression). 이준 분류 알고리즘의 일종
NormalBayesClassifier 정규 베이즈 분류기. 정규 베이즈 분류기는 각 클래스의 특징 벡터가 정규 분포를 따른다고 가정한다. 따라서 전체 데이터 분포는 가우시안 혼합 모델로 표현 가능하다. 정규 베이즈 분류기는 학습 데이터로부터 각 클래스의 평균 벡터와 공분산 행렬을 계산하고 이를 예측에 사용한다.
SVM 서포트 벡터 머신(support vector machine) 알고리즘. 두 클래스의 데이터를 가장 여유 있게 분리하는 초평며을 구한다. 커널 기법을 이용하여 비선형 데이터 분류에도 사용할 수 있으며, 다중 클래스 분류 및 회귀에도 적용할 수 있다.
SVMSDG 통계적 그래디언트 하향(stochastic gradient descent) SVM. 통계적 그래디언트 하향 방법을 SVM에 적용함으로써 대용량 데이터에 대해서도 빠른 학습이 가능하다.

k 최근접 이웃

k 최근접 이웃 알고리즘

  • k 최근접 이웃(kNN, k-Nearest Neighbor) 알고리즘은 분류 또는 회귀에 사용되는 지도 학습 알고리즘의 하나이다.
    • kNN 알고리즘을 분류에 사용할 경우 특징 공간에서 테스트 데이터와 가장 가까운 k개의 훈련 데이터를 찾고, k개의 훈련 데이터 중에서 가장 많은 클래스를 테스트 데이터의 클래스로 지정한다.
    • kNN 알고리즘으 ㄹ회귀 문제에 적용할 경우에는 테스트 데이터에 인접합 k개의 훈련 데이터 평균을 테스트 데이터 값으로 설정한다.
  • 아래 그림은 kNN 알고리즘 동작 방식에 대한 예시이다.
    • 아래 그림은 2차원 평면상에 파란색 사각형과 빨간색 삼각형 두 종류의 데이터가 분포되어 있는데, 파란색과 빨간 점들이 훈련된 데이터이고, 이 훈련된 데이터는 2개의 클래스로 구분되어 있다.
    • 각 점들은 (x, y) 좌표로 표현되므로, 이들 데이터는 2차원 특징 곤간에 정의되어 있다고 할 수 있다.
    • 여기에 녹색으로 새로운 점을 추가할 경우, 이 점을 파란색으로 분류 할지 빨간색으로 분류할지를 결정해야 하는데, 간단한 방법은 새로 들어온 점과 가장 가까이 있는 점을 찾아 해당 데이터와 같은 클래스로 분류하는 방법이다.
    • 아래 그림 상 녹색 점과 가장 가까운 점은 빨간색 삼각형이므로 녹색 점을 빨간색 삼각형과 같은 클래스로 지정할 수 있다.
    • 이러한 방법은 최근접 이웃(NN, Nearest Neighbor) 알고리즘이라 한다.
    • 그러나 녹색 점 주변에 분포로는 빨간색 삼각형보다 파란색 사각형이 더 많은데, 이와 같은 이유로 녹색점을 파란색 사각형으로 분류하는 방식을 kNN 알고리즘이라고 한다.

  • kNN 알고리즘에서 k를 1로 설정하면 최근접 이웃 알고리즘이 된다. 그러므로 보통 k는 1보다 큰 값을 설정하며, k값을 어떻게 설정하느냐에 따라 분류 및 회귀 결과가 달라질 수 있다.
    • 최선의 k 값을 결정하는 것은 주어진 데이터에 의존적이며, 보통 k값이 커질수록 잡음 또는 이상치 데이터의 영향이 감소한다. 그러나 k값이 어느 정도 이상으로 커질 경우 오히려 분류 및 회귀 성능이 떨어질 수 있다.

KNearest 클래스 사용하기

  • OpenCV에서 k 최근접 이웃 알고리즘은 KNearest 클래스에 구현되어 있다.
    • (KNearest의 함수 설명 생략)
    • KNearest 객체는 기본적으로 분류를 위한 용도로 사용된다. 만일 KNearest 객체를 분류가 아닌 회귀에 적용하려면 KNearest::setIsClassifier() 멤버 함수에 false를 지정하여 호출하면 된다.
    • KNearest 객체를 생성하고 속성을 설정한 후에는 StatModel::train() 함수를 통해 학습을 진행할 수 있는데, KNearest 클래스의 경우에는 train() 함수에서 실제적인 학습이 진행되지는 않으며 단순히 훈련 데이터와 레이블 데이터를 KNearest 클래스 멤버 변수에 모두 저장하는 작업이 이루어진다.
  • KNearest 클래스에서 훈련 데이터를 학습한 후 테스트 데이터에 대한 예측을 수행할 때는 KNearest::findNearest() 멤버 함수를 사용한다.
    • 이는 StatModel::predict() 보다 KNearest::findNearest() 함수가 예측 결과와 관련된 정보를 더 많이 반환하기 때문이다.
    • KNearest::findNearest() 함수는 samples 행렬 각 행에 저장된 테스트 데이터와 가까운 k개의 훈련 데이터를 찾아 분류 또는 회귀 응답을 반환한다.
#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace cv::ml;
using namespace std;

Mat img;
Mat train, label;
Ptr<KNearest> knn;
int k_value = 1;

void on_k_changed(int, void*);
void addPoint(const Point& pt, int cls);
void trainAndDisplay();

int main(void)
{
img = Mat::zeros(Size(500, 500), CV_8UC3);
knn = KNearest::create();

namedWindow("knn");
createTrackbar("k", "knn", &k_value, 5, on_k_changed);

const int NUM = 30;
Mat rn(NUM, 2, CV_32SC1);

randn(rn, 0, 50);
for (int i = 0; i < NUM; i++)
addPoint(Point(rn.at<int>(i, 0) + 150, rn.at<int>(i, 1) + 150), 0);

randn(rn, 0, 50);
for (int i = 0; i < NUM; i++)
addPoint(Point(rn.at<int>(i, 0) + 350, rn.at<int>(i, 1) + 150), 1);

randn(rn, 0, 70);
for (int i = 0; i < NUM; i++)
addPoint(Point(rn.at<int>(i, 0) + 250, rn.at<int>(i, 1) + 400), 2);

trainAndDisplay();

waitKey();
return 0;
}

void on_k_changed(int, void*)
{
if (k_value < 1)
k_value = 1;

trainAndDisplay();
}

void addPoint(const Point& pt, int cls)
{
Mat new_sample = (Mat_<float>(1, 2) << pt.x, pt.y);
train.push_back(new_sample);

Mat new_label = (Mat_<int>(1, 1) << cls);
label.push_back(new_label);
}

void trainAndDisplay()
{
knn->train(train, ROW_SAMPLE, label);

for (int i = 0; i < img.rows; ++i)
{
for (int j = 0; j < img.cols; ++j)
{
Mat sample = (Mat_<float>(1, 2) << j, i);
Mat res;
knn->findNearest(sample, k_value, res);

int response = cvRound(res.at<float>(0, 0));
if (response == 0)
img.at<Vec3b>(i, j) = Vec3b(128, 128, 255);
else if (response == 1)
img.at<Vec3b>(i, j) = Vec3b(128, 255, 128);
else if (response == 2)
img.at<Vec3b>(i, j) = Vec3b(255, 128, 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)
circle(img, Point(x, y), 5, Scalar(0, 0, 128), -1, LINE_AA);
else if (l == 1)
circle(img, Point(x, y), 5, Scalar(0, 128, 0), -1, LINE_AA);
else if (l == 2)
circle(img, Point(x, y), 5, Scalar(128, 0, 0), -1, LINE_AA);
}

imshow("knn", img);
}

kNN을 이용한 필기체 숫자 인식

  • 20 x 20 숫자 영상 픽셀값 자체를 kNN 알고리즘 입력으로 사용하는 예시
    • 5000개의 숫자 영상 데이터의 한 장의 숫자 영상은 20 x 20 픽셀 크기이고, 이 픽셀 값을 모두 일렬로 늘어 놓으면 1 x 400 크기의 행렬로 변환할 수 있다.
    • 즉 필기체 숫자 훈련 데이터 하나는 400개의 숫자 값으로 표현되고, 이는 400차원 공간에서의 한 점과 같다.
    • digits.png 영상에 있는 각각의 숫자 영상을 1 x 400 행렬로 바꾸고, 이 행렬을 모두 세로로 쌓으면 전체 숫자 영상 데이터를 표현하는 5000 x 400 크기의 행렬을 만들 수 있다. 그리고 이 행렬을 KNearest 클래스의 훈련 데이터로 전달한다.
    • kNN 알고리즘으로 필기체 숫자 영상을 학습시키려면 각 필기체 숫자 영상이 나타내는 숫자 값을 레이블 행렬로 함께 전달해야 한다. 이 레이블 행렬의 행 크기는 훈련 데이터 영상 개수와 같고, 열 크기는 1이된다.
    • 아래 그림에서 첫 행은 0, 그 다음 행은 1에 대한 데이터이므로 레이블 행렬도 첫 행의 원소는 0으로 설정하고 그 다음 해으이 원소는 1로 설정한다. 그렇게 모든 행의 원소를 설정한 후, KNearest 클래스의 레이블 데이터로 전달한다.

#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace cv::ml;
using namespace std;

Ptr<KNearest> train_knn();
void on_mouse(int event, int x, int y, int flags, void* userdata);

int main()
{
Ptr<KNearest> knn = train_knn();

if (knn.empty())
{
cerr << "Training failed!" << endl;
return -1;
}

Mat img = Mat::zeros(400, 400, CV_8U);

imshow("img", img);
setMouseCallback("img", on_mouse, (void*)&img);

while(true)
{
int c = waitKey(0);

if (c == 27)
{
break;
}
else if (c == ' ')
{
Mat img_resize, img_float, img_flatten, res;
resize(img, img_resize, Size(20, 20), 0, 0, INTER_AREA);
img_resize.convertTo(img_float, CV_32F);
img_flatten = img_float.reshape(1, 1);

knn->findNearest(img_flatten, 3, res);
cout << cvRound(res.at<float>(0, 0)) << endl;

img.setTo(0);
imshow("img", img);
}
}

return 0;
}

Ptr<KNearrest> train_knn()
{
Mat digits = imread("digits.png", IMREAD_GRAYSCALE);

if (digits.empty())
{
cerr << "Image load failed!" << endl;
return 0;
}

Mat train_images, train_labels;

for (int j = 0; j < 50; j++)
{
for (int i = 0; i < 100; i++)
{
Mat roi, roi_float, roi_flatten;
roi = digits(Rect(i*20, j*20, 20, 20));
roi.convertTo(roi_float, CV_32f);
roi_flatten = roi_float.reshape(1, 1);

train_images.push_back(roi_flatten);
train_labels.push_back(j / 5);
}
}

Ptr<KNearest> knn = KNearest::create();
knn->train(train_images, ROW_SAMPLE, train_labels);

return knn;
}

Point ptPrev(-1, -1);

void on_mouse(int event, int x, int y, int flags, void* userdata)
{
Mat img = *(Mat*)userdata;

if (event == EVENT_LBUTTONDOWN)
{
ptPrev = Point(x, y);
}
else if (event == EVENT_LBUTTONUP)
{
ptPrev = Point(-1, -1);
}
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
{
line(img, ptPrev, Point(x, y), Scalar::all(255), 40, LINE_AA, 0);
ptPrev = Point(x, y);

imshow("img", img);
}
}

서포트 벡터 머신

서프트 벡터 머신 알고리즘

  • 서포트 벡터 머신(SVM, Support, Vector Machine)은 기본적으로 두 개의 클래스로 구성된 데이터를 가장 여유 있게 분리하는 초평면(hyperplane)을 찾는 머신 러닝 알고리즘이다.
    • 초평면이란 두 클래스의 데이터를 분리하는 N차원 공간상의 평면을 의미한다. 예컨대 2차원 공간상의 점들을 분리하는 초평면은 단순한 직선 형태로 정의되며, 3차원 공간상의 점들을 분리하는 초평면은 3차원 공간에서의 평면의 방정식으로 표현할 수 있다.
    • SVM 알고리즘은 지도 학습의 일종으로 분류와 회귀에 사용될 수 있다.
  • 아래 그림은 SVM 알고리즘에 대한 예시이다.
    • 아래 그림은 파란색 사각형과 빨간색 삼각형으로 표시된 두 클래스의 점들의 분포를 나타내는데, 이 두 클래스 점들을 구분하기 위한 직선은 매우 다양하게 만들 수 있다.
    • 그림 (a)의 1, 2번 직선은 모두 두 종류의 점들을 잘 분리하지만, 1번 직선은 조금만 왼쪽이나 오른쪽으로 이동해도 분리에 실패하게 되고, 2번 직선도 오른쪽으로 조금만 이동하면 분리에 실패하게 된다.
    • 이는 1, 2번 직선이 모두 입력 점 데이터에너무 가까이 위치하고 있기 때문인데, 그림 (b)의 3번 직선은 두 클래스 점들 사이를 충분히 여유 있게 분할하고 있어서 그런 문제가 없다.
    • 이때 3번 직선에 해당하는 초평면과 가장 가까이 있는 빨간색 또는 파란색 점들과의 거리를 마진(margin)이라 하며, SVM은 이 마진을 최대로 만드는 초평면을 구하는 알고리즘이다.

  • SVM은 기본적으로 선형으로 분리 가능한 데이터에 적용할 수 있다.
    • 그러나 실생활에서 사용하는 데이터는 선형으로 분리되지 않는 경우가 많으며, 이러한 경우에도 SVM 알고리즘을 적용하기 위해 SVM에서는 커널 트릭(kernel trick)이라는 기법을 사용한다.
    • 커널 트릭이란 적절한 커널 함수를 이용하여 입력 데이터 특징 공간 차원을 늘리는 방식이다. 원본 데이터 차원에서는 선형으로 분리할 수 없었던 데이터를 커널 트릭으로 고차원 특징 공간으로 이동하면 선형으로 분리 가능한 형태로 바뀔 수 있다.
  • 데이터 특징 공간 차원을 증가시켜서 데이터를 선형 분리하는 예는 다음과 같다.
    • 2차원 좌표 평면 상의 점 집합 X = { (0, 0), (1, 1) }과 Y = { (1, 0), (0, 1) }이 있다고 가정하고, 이 두 클래스 점들을 아래 그림 처럼 각각 파란색과 빨간색 점으로 나타냈다.
    • 2차원 평면상에서 X, Y 두 클래스 점들을 분리할 수 있는 직선은 존재하지 않는데, 입력 점들의 좌표에 가상의 z축 좌표를 z_{i} = | x_{i} - y_{i} | 형태로 추가할 경우, X = { (0, 0, 0), (1, 1, 0) }과 Y = { (1, 0, 1), (0, 1, 1) } 형태로 3차원 공간상에서의 점 집합으로 바뀌게 된다.
    •  이렇게 차원 공간으로 변경된 X와 Y 점들을 아래 그림의 (b)처럼 그릴 수 있다. 그리고 이 두 클래스 점들은 z = 0.5 평면의 방정식을 이용하여 효과적으로 분리할 수 있다.
    • 2차원 평면에서 선형 분리할 수 없었던 X와 Y 데이터 집합이 가상의 차원을 추가함으로써 선형으로 분리될 수 있게 된 것이다.

  • SVM 알고리즘에서 사용할 수 있는 커널 함수의 종류는 아래 표와 같다.
    • 아래 표에서 가장 널리 사용되는 커널은 방사 기저 함수이며, 이 커널을 사용할 때는 \gamma   인자 값을 적절히 설정해야 한다. 
    • 만약 입력 데이터가 선형으로 분리 가능하다면 선형 커널을 사용하는 것이 가장 빠르게 동작한다.
SVM 커널 커널 함수
선형(linear) K(x_{i}, x_{j}) = x_{i}^{T}x_{j}
다항식(polynomial) K(x_{i}, x_{j}) = (\gamma x_{i}^{T}x_{j} + c_{0})^{degree}, \gamma > 0
방사 기저 함수(radial basis function) K(x_{i}, x_{j}) = exp(-\gamma \|x_{i}-x_{j}\|^{2}), \gamma > 0
시그모이드(sigmoid) K(x_{i}, x_{j}) = tanh(\gamma x_{i}^{T} x_{j} + c+{0})
지수 카이 제곱(exponential chi-square) K(x_{i}, x_{j}) = exp(-\gamma {(x_{i} - x_{j})^{2} \over x_{i} + x_{j}}), \gamma > 0
히스토그램 교차(histogram intersection) K(x_{i}, x_{j}) = min(x_{i}, x_{j})

SVM 클래스 사용하기

  • OpenCV에서 SVM 알고리즘은 SVM 클래스에 구현되어 있다. OpenCV에 구현된 SVM 클래스는 오픈소스 라이브러리인 LIBSVM을 기반으로 만들어졌다.
    • SVM 클래스는 기본적으로 SVM::Types::C_SVC 타입을 사용하도록 초기화되며 다른 타입을 사용하려면 SVM::setType() 함수를 이용하여 타입을 변경할 수 있다.
    • SVM::Types::C_SVC 타입을 사용하는 경우 SVM 알고리즘 내부에서 사용하는 C 파라미터 값을 적절하게 설정해야 하는데, C 값을 작게 설정하면 훈련 데이터 중에 잘못 분류된느 데이터가 있어도 최대 마진을 선택하고, C 값을 크게 설정하면 마진이 작아지더라도 잘못 분류되는 데이터가 적어지도록 분류한다.
    • 만약 훈련 샘플 데이터에 잡음 또는 이상치 데이터가 많이 포함된 경우에는 C 파라미터 값을 크게 설정하는 것이 유리하다.
SVM::Types 설명 파라미터
C_SVC C-서포트 벡터 분류. 일반적인 n-클래스 분류 문제에서 사용된다. C
NU_SVC v-서포트 벡터 분류. C_SCV와 비슷하지만 Nu 값 범위가 0-1 사이로 정규화 되어 있다. Nu
ONE_CLASS 1-분류 서포트 벡터 머신. 데이터 분포 측정에 사용된다. C, Nu
EPS_SVR \epsilon -서포트 벡터 회귀 P, C
NU_SVR v-서포트 벡터 회귀 Nu, C
  • SVM 타입 설정 후에 SVM 알고리즘에서 사용할 커널 함수를 지정해야 한다. 함수 지정은 SVM::setKernel() 함수를 이용하면 된다.
SVM::KernelTYpes 설명 파라미터
LINEAR 선형 커널  
POLY 다항식 커널 Degree, Gamma, Coef0
RBF 방사 기저 함수 커널 Gamma
SIGMOID 시그모이드 커널 Gamma, Coef0
CHI2 지수 카이 제곱 커널 Gamma
INTER 히스토그램 교차 커널  
  • SVM 타입과 커널 함수 종류를 설정한 후에는 각각의 타입과 커널 함수 정의에 필요한 파라미터를 설정해야 한다.
    • SVM 클래스에서 설정할 수 있는 파라미터는 C, Nu, P, Degree, Gamma, Coef0 등이 있으며, 이들 파라미터는 차례대로 1, 0, 0, 0, 1, 0으로 초기화 된다.
    • 각각의 파라미터는 파라미터 이름에 해당하는 setXXX()와 getXXX(0 함수를 이용하여 값을 설정하거나 읽어올 수 있다.
  • SVM 객체를 생성하고 타입과 커널 함수, 파라미터를 설정한 후에는 StatModel::train() 함수를 이용하여 학습을 시킬 수 있다.
    • 그러나 SVM에서 사용하는 파라미터를 적절하게 설정하지 않으면 학습이 제대로 되지 않는데, OpenCV에서는 각각의 파라미터에 대해 설정 가능한 값을 적용해 보고 그중 가장 성능이 좋은 파라미터를 자동으로 찾아 학습하는 SVM::trainAuto() 함수를 제공한다.
    • 다만 SVM::trainAuto() 함수는 매우 느리기 때문에 한 번 학습이 완료된 후 선택된 파라미터를 저장했다가 재사용하는 편이 좋다.
  • SVM 학습이 완료되었다면 StatModel::predict()를 통해 테스트 데이터에 대한 예측을 수행할 수 있다.
#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace cv::ml;
using namespace std;

int main(void)
{
Mat train = Mat_<float>({8, 2}, {150, 200, 200, 250, 100, 250, 150, 300, 350, 100, 400, 200, 400, 300, 350, 400});
Mat label = Mat_<int>({8, 1}, {0, 0, 0, 0, 1, 1, 1, 1});

Ptr<SVM> svm = SVM::create();
svm->setType(SVM::Types::C_SVC);
svm->setKernel(SVM::KernelTypes::RBF);
svm->trainAuto(train, ROW_SAMPLE, label);

Mat img = Mat::zeros(Size(500, 500), CV_8UC3);

for (int j = 0; j < img.rows; j++)
{
for (int i = 0; i < img.cols; i++)
{
Mat test = Mat_<float>({1, 2}, {(float)i, (float)j});
int res = cvRound(svm->predict(test));

if (res == 0)
img.at<Vec3b>(j, i) = Vec3b(128, 128, 255);
else
img.at<Vec3b>(j, i) = 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)
cicle(img, Point(x, y), 5, Scalar(0, 0, 128), -1, LINE_AA);
else
cicle(img, Point(x, y), 5, Scalar(0, 128, 0), -1, LINE_AA);
}

imshow("svm", img);

waitKey();
return 0;
}

HOG & SVM 필기체 숫자 인식

  • kNN으로 했던 필기체 인식의 SVM 버전
    • 각 숫자 영상에서 HOG 특징 벡터를 추출한 후 SVM 알고리즘 입력 데이터로 사용한다.
    • HOG 특징 벡터 추출을 위해 HOGDescriptor 클래스를 사용한다.
#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace cv::ml;
using namespace std;

Ptr<SVM> train_hog_svm(const HOGDescriptor& hog);
void on_mouse(int event, int x, int y, int flags, void* userdata);

int main()
{
HOGDescriptor hog(Size(20, 20), Size(10, 10), Size(5, 5), Size(5, 5), 9);

Ptr<SVM> svm = train_hog_svm(hog);

if (svm.empty())
{
cerr << "Training failed!" << endl;
return -1;
}

Mat img = Mat::zeros(400, 400, CV_8U);

imshow("img", img);
setMoustCallback("img", on_mouse, (void*)&img);

 while(true)
{
int c = waitKey(0);

if (c == 27)
{
break;
}
else if (c == ' ')
{
Mat img_resize;
resize(img, img_resize, Size(20, 20), 0, 0, INTER_AREA);

vector<float> desc;
hog.compute(img_resize, desc);

Mat desc_mat(desc);
int res = cvRound(svm->predict(desc_mat.t()));
cout << res << endl;

img.setTo(0);
imshow("img", img);
}
}

return 0;
}

Ptr<SVM> train_hog_svm(const HOGDescriptor& hog)
{
Mat digits = imread("digits.png", IMREAD_GRAYSCALE);

if (digits.empty())
{
cerr << "Image load failed!" << endl;
return 0;
}

Mat train_hog, train_labels;

for (int j = 0; j < 50; j++)
{
for (int i = 0; i < 100; i++)
{
Mat roi = digits(Rect(i*20, j*20, 20, 20));

vector<float> desc;
hog.compute(roi, desc);

Mat desc_mat(desc);
train_hog.push_back(desc_mat.t());
train_labels.push_back(j / 5);
}
}

// 아래 상수값은 SVM::trainAuto()를 통해 구한 값이다.
Ptr<SVM> svm = SVM::create();
svm->setType(SVM::Types::C_SVC);
svm->setKernel(SVM::KernelTypes::RBF);
svm->setC(2.5);
svm->setGamma(0.50625);
svm->train(train_hog, ROW_SAMPLE, train_labels);

return svm;
}

Point ptPrev(-1, -1);

void on_mouse(int event, int x, int y, int flags, void* userdata)
{
Mat img = *(Mat*)userdata;

if (event == EVENT_LBUTTONDOWN)
{
ptPrev = Point(x, y);
}
else if (event == EVENT_LBUTTONUP)
{
ptPrev = Point(-1, -1);
}
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
{
line(img, ptPrev, Point(x, y), Scalar::all(255), 40, LINE_AA, 0);
ptPrev = Point(x, y);

imshow("img", img);
}
}

[ssba]

The author

지성을 추구하는 사람/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.