サポートベクタマシン (SVM; Support Vector Machine) は分類アルゴリズムの一つです。二つのクラスに分類されたデータをもとに分類器を構成します。その分類器を用いると、未知のデータを二つのクラスに分類できます。OpenCV3 C++ に実装されている SVM アルゴリズムを利用して、手書き数字を 0-9 のいずれかに分類してみます。
四つの点それぞれに 1
または -1
のラベルを付与します。これらを分類する直線を学習して分類器を構成します。平面内の点を分類器で塗り分けると以下のようになります。
#include <opencv2/opencv.hpp>
int main() {
// 分類器を作成するためのデータを用意します。
int labels[4] = {1, -1, -1, -1};
float trainingData[4][2] = { {501, 10}, {255, 10}, {501, 255}, {10, 501} };
cv::Mat trainingDataMat(4, 2, CV_32F, trainingData);
cv::Mat labelsMat(4, 1, CV_32S, labels);
// [SVM] 分類器を作成します。
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
svm->setType(cv::ml::SVM::C_SVC);
svm->setKernel(cv::ml::SVM::LINEAR);
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 100, 1e-6));
svm->train(trainingDataMat, cv::ml::ROW_SAMPLE, labelsMat);
// 分類器で 512 x 512 平面内の点を分類してみます。
int width = 512, height = 512;
cv::Mat image = cv::Mat::zeros(height, width, CV_8UC3);
// 分類結果によって「緑」または「青」で色付けして可視化します。
cv::Vec3b green(0,255,0), blue(255,0,0);
for (int i = 0; i < image.rows; i++) {
for (int j = 0; j < image.cols; j++) {
cv::Mat sampleMat = (cv::Mat_<float>(1,2) << j,i);
// [SVM] 分類
float response = svm->predict(sampleMat);
if (response == 1) {
image.at<cv::Vec3b>(i,j) = green;
}
else if (response == -1) {
image.at<cv::Vec3b>(i,j) = blue;
}
}
}
// 分かりやすさのため、訓練用のデータを描画します。
int thickness = -1;
cv::circle( image, cv::Point(501, 10), 5, cv::Scalar( 0, 0, 0), thickness );
cv::circle( image, cv::Point(255, 10), 5, cv::Scalar(255, 255, 255), thickness );
cv::circle( image, cv::Point(501, 255), 5, cv::Scalar(255, 255, 255), thickness );
cv::circle( image, cv::Point( 10, 501), 5, cv::Scalar(255, 255, 255), thickness );
// [SVM] サポートベクタを描画します。
thickness = 2;
cv::Mat sv = svm->getUncompressedSupportVectors();
for (int i = 0; i < sv.rows; i++) {
const float* v = sv.ptr<float>(i);
cv::circle(image, cv::Point( (int) v[0], (int) v[1]), 6, cv::Scalar(128, 128, 128), thickness);
}
cv::imshow("SVM Simple Example", image);
cv::waitKey(0);
return 0;
}
cv::Ptr
はスマートポインタです。C++11 スマートポインタと同様です。cv::TermCriteria
は終了基準を表現するクラスです。<<
は cv::Mat
を以下のように初期化します。
cv::Mat m = (cv::Mat_<float>(2, 2) << 1, 2, 3, 4);
std::cout << m << std::endl;
[1, 2;
3, 4]
訓練用のデータと同じ次元では線形の分類器を見つけられない場合は、カーネル空間とよばれる高次元空間に写像することを考えます。カーネル空間における線形分類器は、もとの低次元空間では線形であるとは限りません。以下に簡単な例を示します。
#include <opencv2/opencv.hpp>
int main() {
// 分類器を作成するためのデータを用意します。
int labels[4] = {1, -1, -1, 1};
float trainingData[4] = {-200, -100, 150, 190};
cv::Mat trainingDataMat(4, 1, CV_32F, trainingData);
cv::Mat labelsMat(4, 1, CV_32S, labels);
// [SVM] 分類器を作成します。
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
svm->setType(cv::ml::SVM::C_SVC);
svm->setKernel(cv::ml::SVM::POLY);
svm->setDegree(2);
svm->setGamma(1.0);
svm->setCoef0(0.0);
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 100, 1e-6));
svm->train(trainingDataMat, cv::ml::ROW_SAMPLE, labelsMat);
// 分類器で 512 x 1 直線上の点を分類してみます。
int width = 512, height = 512;
cv::Mat image = cv::Mat::zeros(height, width, CV_8UC3);
// 分類結果によって「緑」または「青」で色付けして可視化します。
cv::Vec3b green(0,255,0), blue(255,0,0), black(0,0,0);
for (int i = 0; i < image.rows; i++) {
for (int j = 0; j < image.cols; j++) {
if (i != 256) {
image.at<cv::Vec3b>(i,j) = black;
continue;
}
cv::Mat sampleMat = (cv::Mat_<float>(1,1) << j - 256);
// [SVM] 分類
float response = svm->predict(sampleMat);
if (response == 1) {
image.at<cv::Vec3b>(i,j) = green;
}
else if (response == -1) {
image.at<cv::Vec3b>(i,j) = blue;
}
}
}
// 分かりやすさのため、訓練用のデータを描画します。
int thickness = -1;
cv::circle( image, cv::Point(-200 + 256, 256), 5, cv::Scalar(0, 0, 255), thickness );
cv::circle( image, cv::Point(-100 + 256, 256), 5, cv::Scalar(255, 255, 255), thickness );
cv::circle( image, cv::Point(150 + 256, 256), 5, cv::Scalar(255, 255, 255), thickness );
cv::circle( image, cv::Point(190 + 256, 256), 5, cv::Scalar(0, 0, 255), thickness );
// [SVM] サポートベクタを描画します。
thickness = 2;
cv::Mat sv = svm->getSupportVectors();
std::cout << sv << std::endl;
for (int i = 0; i < sv.rows; i++) {
const float v = sv.at<float>(i);
cv::circle(image, cv::Point( (int) v + 256, 256), 6, cv::Scalar(256, 0, 0), thickness);
}
cv::imshow("SVM Simple Example", image);
cv::waitKey(0);
return 0;
}
上記例では、多項式カーネルで二次元空間に写像しています。cv::ml::SVM::POLY
svm->setKernel(cv::ml::SVM::POLY);
svm->setDegree(2);
svm->setGamma(1.0);
svm->setCoef0(0.0);
例えば $(x, x^2)$ に写像して、{-3, 3}
と {-1, 1}
を {(-3, 9), (3, 9}
と {(-1, 1), (1, 1)}
に変換すれば、二次元空間における線形の分類器が見つけられます。線形カーネルを用いると以下のようになり、うまく分類できません。
svm->setKernel(cv::ml::SVM::LINEAR);
画像から分類に必要となる情報をうまく抽出して N 次元のベクトルで表現できれば、画像の分類を SVM で行うことができます。こちらのページに倣って、手書き数字の分類を行ってみます。
画像の情報として HOG (Histogram of Oriented Gradients) を利用しています。Sobel 微分で各点の x軸方向、y軸方向の勾配 Gradients を計算して、各方向 Oriented についてヒストグラムを計算します。このヒストグラムを画像の特徴量として扱い、SVM で利用します。HOG を計算するにあたり、事前に画像の傾きを補正しておきます。
CMakeLists.txt
cmake_minimum_required (VERSION 3.1)
find_package(OpenCV REQUIRED)
add_executable(main main.cpp)
target_link_libraries(main ${OpenCV_LIBS})
main.cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int SZ = 20;
cv::Mat deskew(cv::Mat& img){
cv::Moments m = cv::moments(img);
if(abs(m.mu02) < 1e-2) {
return img.clone();
}
float skew = m.mu11 / m.mu02;
cv::Mat warpMat = (cv::Mat_<float>(2,3) << 1, skew, -0.5 * SZ * skew, 0, 1, 0);
cv::Mat imgOut = cv::Mat::zeros(img.rows, img.cols, img.type());
cv::warpAffine(img, imgOut, warpMat, imgOut.size(), cv::WARP_INVERSE_MAP|cv::INTER_LINEAR);
return imgOut;
}
void loadTrainTestLabel(std::string &pathName,
std::vector<cv::Mat> &trainCells,
std::vector<cv::Mat> &testCells,
std::vector<int> &trainLabels,
std::vector<int> &testLabels) {
cv::Mat img = cv::imread(pathName, cv::IMREAD_GRAYSCALE);
int ImgCount = 0;
for(int i = 0; i < img.rows; i = i + SZ) {
for(int j = 0; j < img.cols; j = j + SZ) {
cv::Mat digitImg = (img.colRange(j, j + SZ).rowRange(i, i + SZ)).clone();
if(j < int(0.9 * img.cols)) {
trainCells.push_back(digitImg);
}
else {
testCells.push_back(digitImg);
}
ImgCount++;
}
}
std::cout << "Image Count : " << ImgCount << std::endl;
float digitClassNumber = 0;
for(int z = 0; z < int(0.9 * ImgCount); z++) {
if(z % 450 == 0 && z != 0) {
digitClassNumber = digitClassNumber + 1;
}
trainLabels.push_back(digitClassNumber);
}
digitClassNumber = 0;
for(int z = 0; z < int(0.1*ImgCount); z++) {
if(z % 50 == 0 && z != 0) {
digitClassNumber = digitClassNumber + 1;
}
testLabels.push_back(digitClassNumber);
}
}
void CreateDeskewedTrainTest(std::vector<cv::Mat> &deskewedTrainCells,
std::vector<cv::Mat> &deskewedTestCells,
std::vector<cv::Mat> &trainCells,
std::vector<cv::Mat> &testCells){
for(int i = 0; i < (int)trainCells.size(); i++) {
cv::Mat deskewedImg = deskew(trainCells[i]);
deskewedTrainCells.push_back(deskewedImg);
}
for(int i = 0; i < (int)testCells.size(); i++) {
cv::Mat deskewedImg = deskew(testCells[i]);
deskewedTestCells.push_back(deskewedImg);
}
}
cv::HOGDescriptor hog(
cv::Size(20,20), //winSize
cv::Size(10,10), //blocksize
cv::Size(5,5), //blockStride,
cv::Size(10,10), //cellSize,
9, //nbins,
1, //derivAper,
-1, //winSigma,
0, //histogramNormType,
0.2, //L2HysThresh,
0,//gammal correction,
64,//nlevels=64
1);
void CreateTrainTestHOG(std::vector<std::vector<float> > &trainHOG,
std::vector<std::vector<float> > &testHOG,
std::vector<cv::Mat> &deskewedtrainCells,
std::vector<cv::Mat> &deskewedtestCells) {
for(int y = 0; y < (int)deskewedtrainCells.size(); y++) {
std::vector<float> descriptors;
hog.compute(deskewedtrainCells[y], descriptors);
trainHOG.push_back(descriptors);
}
for(int y = 0; y < (int)deskewedtestCells.size(); y++) {
std::vector<float> descriptors;
hog.compute(deskewedtestCells[y], descriptors);
testHOG.push_back(descriptors);
}
}
void ConvertVectortoMatrix(std::vector<std::vector<float> > &trainHOG,
std::vector<std::vector<float> > &testHOG,
cv::Mat &trainMat,
cv::Mat &testMat) {
int descriptor_size = trainHOG[0].size();
for(int i = 0; i < (int)trainHOG.size(); i++){
for(int j = 0;j<descriptor_size;j++){
trainMat.at<float>(i,j) = trainHOG[i][j];
}
}
for(int i = 0; i < (int)testHOG.size(); i++) {
for(int j = 0;j<descriptor_size;j++){
testMat.at<float>(i,j) = testHOG[i][j];
}
}
}
void getSVMParams(cv::Ptr<cv::ml::SVM> svm) {
std::cout << "Kernel type : " << svm->getKernelType() << std::endl;
std::cout << "Type : " << svm->getType() << std::endl;
std::cout << "C : " << svm->getC() << std::endl;
std::cout << "Degree : " << svm->getDegree() << std::endl;
std::cout << "Nu : " << svm->getNu() << std::endl;
std::cout << "Gamma : " << svm->getGamma() << std::endl;
}
cv::Ptr<cv::ml::SVM> SVMtrain(cv::Mat &trainMat, std::vector<int> &trainLabels) {
cv::Ptr<cv::ml::TrainData> td = cv::ml::TrainData::create(trainMat, cv::ml::ROW_SAMPLE, trainLabels);
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
svm->setGamma(0.50625);
svm->setC(12.5);
svm->setKernel(cv::ml::SVM::RBF);
svm->setType(cv::ml::SVM::C_SVC);
svm->train(td);
// 上記パラメータを自動で計算させることもできます。時間がかかります。
// svm->trainAuto(td);
svm->save("model.yml");
getSVMParams(svm);
return svm;
}
void SVMevaluate(cv::Ptr<cv::ml::SVM> svm, cv::Mat &testMat, std::vector<int> &testLabels, std::vector<cv::Mat> &deskewedTestCells){
cv::Mat testResponse;
svm->predict(testMat, testResponse);
int count = 0;
for(int i = 0; i < testResponse.rows; i++) {
if(testResponse.at<float>(i, 0) == testLabels[i]) {
count = count + 1;
}
else {
std::cout << testResponse.at<float>(i,0) << " != " << testLabels[i] << std::endl;
cv::imshow("prediction failed", deskewedTestCells[i]);
cv::waitKey(0);
}
}
float accuracy = (count / (float)testResponse.rows) * 100;
std::cout << "Accuracy : " << accuracy << "%" << std::endl;
}
int main() {
// 手書き数字の画像
std::string pathName = "digits.png";
// 分類器を学習するためのデータと、分類器を検証するためのデータ
std::vector<cv::Mat> trainCells;
std::vector<cv::Mat> testCells;
std::vector<int> trainLabels;
std::vector<int> testLabels;
loadTrainTestLabel(pathName, trainCells, testCells, trainLabels, testLabels);
// 画像の傾きを補正します。
std::vector<cv::Mat> deskewedTrainCells;
std::vector<cv::Mat> deskewedTestCells;
CreateDeskewedTrainTest(deskewedTrainCells, deskewedTestCells, trainCells, testCells);
// 特徴量として HOG を計算します。
std::vector<std::vector<float> > trainHOG;
std::vector<std::vector<float> > testHOG;
CreateTrainTestHOG(trainHOG, testHOG, deskewedTrainCells,deskewedTestCells);
int descriptor_size = trainHOG[0].size();
std::cout << "Descriptor Size : " << descriptor_size << std::endl;
cv::Mat trainMat(trainHOG.size(), descriptor_size,CV_32FC1);
cv::Mat testMat(testHOG.size(), descriptor_size,CV_32FC1);
ConvertVectortoMatrix(trainHOG, testHOG, trainMat, testMat);
// SVM 分類器を構成します。
cv::Ptr<cv::ml::SVM> svm;
svm = SVMtrain(trainMat, trainLabels);
// SVM 分類器を評価します。
SVMevaluate(svm, testMat, testLabels, deskewedTestCells);
return 0;
}
実行例
Image Count : 5000
Descriptor Size : 81
Kernel type : 2
Type : 100
C : 12.5
Degree : 0
Nu : 0
Gamma : 0.50625
7 != 1
3 != 2
2 != 7
1 != 7
0 != 9
3 != 9
7 != 9
Accuracy : 98.6%
7 と認識 (正しくは 1)
3 と認識 (正しくは 2)
2 と認識 (正しくは 7)
1 と認識 (正しくは 7)
0 と認識 (正しくは 9)
3 と認識 (正しくは 9)
7 と認識 (正しくは 9)