Opencv学习笔记 - 使用opencvsharp和支持向量机

        以统计学习理论为基础的支持向量机被广泛应用于机器学习的各个领域,是最通用的万能分类器。20世纪90年代,针对当时的神经网络在小样本条件下的不良表现,人们试图从更本质的层次上寻求一种更好的学习机器。在这种需求的激发下,产生了统计学习理论,即研究小样本条件下机器学习规律的理论。1995年,出现了基于统计学习理论的支持向量机(Support Vector Machine,SVM)。与神经网络相比,对于有限样本的学习问题,统计学习理论具有更坚实的数学理论基础,因此SVM取得了很大的成功。

 

一、支持向量机原理

1、统计学习理论概述

        统计学习理论(Statistical Learning Theory,SLT)在解决机器学习问题中起到了基础性的作用。传统统计学研究的主要是渐近理论,基于传统方法的学习机器在样本数无穷时,往往表现出非常不错的性能,但在样本数有限时却表现出很差的推广能力。在实际应用中,我们面对的通常是有限样本的情况。统计学习理论就是在研究小样本统计估计和预测的过程中发展起来的一种理论,它建立了一套较好的有限样本下机器学习的理论框架,既有严格的理论基础,又能较好地解决小样本、非线性、高维度和局部极小点等实际问题。

        统计学习理论中的SVM是执行SRM原则的典型学习机器[。其基本思想是,在线性分类问题中,选择具有最小VC维的元素中的函数(称为“最优分类超平面”)把训练数据分开。

        SVM主要包括以下三种模型:

        ◎ 线性可分支持向量机。

        ◎ 线性支持向量机。

        ◎ 非线性支持向量机。

        在上述三种模型中,简单模型是复杂模型的基础。

        针对不同的样本类型,应使用不同类型的SVM:

        ◎ 当训练样本线性可分时,通过硬间隔最大化,学习一个线性分类器,即线性可分支持向量机,又称为硬间隔支持向量机。

        ◎ 当训练样本近似线性可分时,通过软间隔最大化,学习一个线性分类器,又称为软间隔支持向量机。

        ◎ 当训练样本线性不可分时,通过核技巧和软间隔最大化,学习一个非线性支持向量机。

2、线性SVM算法的基本原理

(1)硬间隔支持向量分类器

Opencv学习笔记 - 使用opencvsharp和支持向量机_第1张图片

(2)软间隔支持向量分类器

Opencv学习笔记 - 使用opencvsharp和支持向量机_第2张图片

 (3)支持向量分类器的对偶形式解

3、非线性SVM算法的基本原理

        前文介绍的是样本线性可分或近似线性可分情况下的SVM分类器。我们可以用硬间隔SVM解决线性可分数据的分类问题,用软间隔SVM分类器解决只有少量样本不在线性可分范围内的分类问题。

        (1)特征变换

        对于非线性不可分的数据,是无法直接用上述线性SVM方法的。但是人们发现,低维空间的数据在通过特征变换到高维空间后,是有可能变成线性可分数据的,或者提高了线性可分性。

Opencv学习笔记 - 使用opencvsharp和支持向量机_第3张图片

         (2)核方法

        核方法的基本思想是:给定原始空间中的向量x,通过非线性映射Ф(∙)将x映射到高维特征空间(也称为核空间),然后在Ф(x)中构建线性算法,使它对应于原始空间中非线性问题的解。如果x各坐标分量间的相互作用仅限于内积,则可以使用满足Mercer条件的核函数代替内积运算,从而越过本来需要计算的非线性映射Ф(x),避免“维数灾难”。

4、SVM回归算法的基本原理

        最初的SVM是用于求解分类问题的,Vapnik通过ε不敏感(ε−Insensitive)损失函数将SVM扩展到了求解回归问题。

        用于回归的支持向量机被称为SVR(SupportVector Regression)。SVR和SVM在本质上是相同的,不同的是,在SVM中最小化的误差函数只等于+1或者−1;而SVR对于每个输入样本,都有不同的误差。

二、OpenCV函数实现

1、OpenCV中的SVM算法

        OpenCV实现了5种类型的SVM算法。

Opencv学习笔记 - 使用opencvsharp和支持向量机_第4张图片

 (1)OpenCV中的支持向量分类

        在OpenCV中有3种SVM分类算法,分别为C-SVM分类算法、C-SVM分类算法和One-class SVM分类算法。前两种分类算法可以完成多分类(Nc>2)任务,第三种分类算法用于一分类,它们都支持离群点分类。离群点是指无法分配给正确类的数据点(或不能由核空间中的超平面分隔的数据点)。

(2)OpenCV中的支持向量回归

        OpenCV支持两种SVM的变体算法:C-SVM算法和v-SVM算法。这两个拓展的方法都假设了“离群点”的存在。“离群点”指不能被正确分类的数据点。

(3)OpenCV支持的核函数

        为了解决非线性数据的分类和回归问题,常将原始数据x通过某个非线性映射Ф投影到高维特征空间,再在特征空间构建超平面完成分类或回归任务。非线性映射Ф(x)可能非常复杂,造成特征空间维度很高,无法计算。核方法在某些SVM文献中经常被称为核技巧(Kernel Trick),其本质上是一种计算方法,它通过满足一定条件的核函数K(x, z)来计算两样本点映射的内积,即K(x, z)=〈Ф(x), Ф(z)〉=Ф(x)TФ(z),从而避免直接计算映射Ф(x)和Ф(z)。

        OpenCV提供了6个不同的核函数供SVM使用。

2、创建SVM模型

        在OpenCV中,SVM分类器接口通过ml库中的SVM类定义。与其他机器学习算法一样,SVM类是从StatModel类派生的。

        以下是创建、训练和使用SVM模型时的主要步骤。

        (1)创建SVM实例

Ptr svm = cv::ml::SVM::create();

        (2)设置SVM算法的类型

svm->setType(SVM::C_SVC)

        (3)设置SVM参数

svn->setC(10);

svn->setP();

svn->setNu();

        (4)设置核函数与核参数

svn->setKernel(SVM::RBF);

svn->setGamma(1);

svn->setCoef0(0);

svn->setDegree(2);

3、训练模型

(1)常规训练

        对SVM模型的训练及预测与ml模块中其他由StatModel类派生的算法相同。

(2)自动训练(opencvsharp没有实现这个函数)

        OpenCV还提供了trainAuto函数,分类或回归模型都可以使用。该函数用于自动寻找最佳参数C、γ、p、nu、c0和d来训练模型,使用的方法是k折叠交叉验证。数据集会被自动分为k个子集,然后运行k次;每次运行时,都对k–1个子集进行训练,用保留的另一个子集进行验证。当k折叠交叉验证误差最小时,参数组合被认为是最佳的。如果是ONE_CLASS函数,则不会进行优化,而是执行指定参数的常规训练。

virtual bool trainAuto( const Ptr& data, int kFold = 10,
                    ParamGrid Cgrid = getDefaultGrid(C),
                    ParamGrid gammaGrid  = getDefaultGrid(GAMMA),
                    ParamGrid pGrid      = getDefaultGrid(P),
                    ParamGrid nuGrid     = getDefaultGrid(NU),
                    ParamGrid coeffGrid  = getDefaultGrid(COEF),
                    ParamGrid degreeGrid = getDefaultGrid(DEGREE),
                    bool balanced=false) = 0;

        函数参数:

        ◎ samples:训练样本。

        ◎ layout:训练样本集中特征向量的排列方式,如ROW_SAMPLE或COL_SAMPLE。

        ◎ responses:与训练样本相关的响应(标签)向量。

        ◎ kFold:交叉验证参数,默认为10折交叉验证。训练集分为k个子集。一个子集用于测试模型,其他子集构成训练集。

        ◎ Cgrid:参数C的网格。

        ◎ gammaGrid:参数γ的网格。

        ◎ pGrid:参数p的网格。

        ◎ nuGrid:参数v的网格。

        ◎ coeffGrid:参数c0的网格。

        ◎ degreeGrid:参数d的网格。

        ◎ balanced:如果为真,并且是二分类问题,则该方法将创建更平衡的交叉验证子集,该子集中子类之间的比例接近整个训练数据集中的比例。 

三、使用HOG特征与SVM算法识别手写数字

1. c++代码参考图像校正预处理

#include "opencv_svm.h"

#include 
#include 
#include 
#include 
#include 

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


bool hog_falg = true;

cv::HOGDescriptor hog
	(
		cv::Size(28, 28),
		cv::Size(14, 14),
		cv::Size(7, 7),
		cv::Size(7, 7),
		9/*, 
		1,
		-1,
		HOGDescriptor::HistogramNormType::L2Hys,
		0.2, 
		true,
		HOGDescriptor::DEFAULT_NLEVELS, 
		true*/
	);


/// 
/// 计算并校正倾斜度,这里没用到,有兴趣
/// 可以把图像处理一遍,观察效果
/// 
/// 
/// 
cv::Mat deskew(cv::Mat& img)
{
	cv::Moments m = moments(img);
	int SZ = img.rows;	//图像大小
	if (abs(m.mu02) < 1e-2)
		return img.clone();
	//基于中心距计算倾斜度
	double skew = m.mu11 / m.mu02;
	//计算仿射变换矩阵并校正偏斜度
	cv::Mat wrapMat = (cv::Mat_(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, wrapMat, imgOut.size());
	return imgOut;
}


void convert_to_ml(const vector& train_samples, Mat& trainData)
{
    //--Convert data
    const int rows = (int)train_samples.size();
    const int cols = (int)std::max(train_samples[0].cols,
        train_samples[0].rows);
    Mat tmp(1, cols, CV_32FC1);
    trainData = Mat(rows, cols, CV_32FC1);
    for (size_t i = 0; i < train_samples.size(); ++i)
    {
        CV_Assert(train_samples[i].cols == 1 || train_samples[i].rows == 1);
        if (train_samples[i].cols == 1)
        {
            transpose(train_samples[i], tmp);
            tmp.copyTo(trainData.row((int)i));
        }
        else if (train_samples[i].rows == 1)
        {
            train_samples[i].reshape(0, 1).copyTo(trainData.row((int)i));
        }
    }
}

void convert_to_ml_for(const vector& train_samples, Mat& trainData)
{
    const int rows = (int)train_samples.size();
    const int cols = (int)std::max(train_samples[0].cols, train_samples[0].rows);
    trainData = Mat(rows, cols, CV_32FC1);

    for (size_t i = 0; i < train_samples.size(); ++i)
    {
        train_samples[i].copyTo(trainData.row((int)i));
    }
}

/// 
/// 生成模型的训练集和测试集
/// 
/// 输入的灰度图
/// 输出
/// 输出
/// 输出
/// 输出
/// 输入。true:提取HOG特征描述子作为特征量。false:直接使用图像原始像素
void generateDataSet(cv::Mat& img, vector& trainDataVec, vector& testDataVec,
    vector& testLabel, int train_rows, bool HOG_flag)
{
    //初始化图像中切片图像与其他参数
    int width_slice = 20;  //单个数字切片图像的宽度
    int height_slice = 20; //单个数字切片图像的高度
    int row_sample = 100;  //每行样本数
    int col_sample = 50;   //每列样本数
    int row_single_number = 5; //单个数字占5行
    int test_rows = row_single_number - train_rows; //测试样本所占行数
    Mat trainMat(train_rows * 20 * 10, img.cols, CV_8UC1);
    //存放所有训练图片
    trainMat = Scalar::all(0);
    Mat testMat(test_rows * 20 * 10, img.cols, CV_8UC1);
    //存放所有测试图片
    testMat = Scalar::all(0);

    //生成测试大图和训练大图
    for (int i = 1; i <= 10; i++)
    {
        Mat tempTrainMat = img.rowRange((i - 1) * row_single_number * 20, (i *
            row_single_number - 1) * 20).clone();
        Mat tempTestMat = img.rowRange((i * row_single_number - 1) * 20, (i *
            row_single_number) * 20).clone();
        //train
        cv::Mat roi_train = trainMat(Rect(0, (i - 1) * train_rows * 20,
            tempTrainMat.cols, tempTrainMat.rows));
        Mat mask_train(roi_train.rows, roi_train.cols, roi_train.depth(),
            Scalar(1));
        //test
        cv::Mat roi_test = testMat(Rect(0, (i - 1) * test_rows * 20,
            tempTestMat.cols, tempTestMat.rows));
        Mat mask_test(roi_test.rows, roi_test.cols, roi_test.depth(),
            Scalar(1));
        //把提取的训练测试行分别复制到训练图片与测试图片中
        tempTrainMat.copyTo(roi_train, mask_train);
        tempTestMat.copyTo(roi_test, mask_test);
    }
}

void getFiles1(string path, vector& files)
{
    // 文件句柄
    intptr_t hFile = 0;
    // 文件信息
    struct _finddata_t fileinfo;

    string p;

    if ((hFile = _findfirst(p.assign(path).append("\\*").c_str(), &fileinfo)) != -1) {
        do {
            // 保存文件的全路径
            if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)
                files.push_back(p.assign(path).append("\\").append(fileinfo.name));

        } while (_findnext(hFile, &fileinfo) == 0); //寻找下一个,成功返回0,否则-1

        _findclose(hFile);
    }
}

void opencv_svm_mnist()
{
	//读取原始数据
    string trainPath = "D:\\Project\\deeplearn\\dataset\\mnist_png_full\\training";
    string testPath = "D:\\Project\\deeplearn\\dataset\\mnist_png_full\\testing";

    //校正大图,这里暂时不进行校正

    //制作训练集
    int train_sample_count = 60000;
    int test_sample_count = 10000;

    //声明变量
    cv::Mat trainHOGData, testHOGData, trainRawData, testRawData;
    vector trainHOGDataVec, testHOGDataVec, trainRawDataVec, testRawDataVec;

    Mat trainLabel = Mat(train_sample_count, 1, CV_32FC1);
    Mat testLabel = Mat(test_sample_count, 1, CV_32FC1);

    //组织训练数据
    int trainNums = 0;
    for (int i = 0; i < 10; i++)
    {
        stringstream ss;
        ss << trainPath << "\\" << i;
        string path = ss.str();
        vector files;
        getFiles1(path, files);
        int size = files.size();
        for (int a = 0; a < size; a++)
        {
            Mat temp = imread(files[a].c_str(), IMREAD_GRAYSCALE);
            if (hog_falg)
            {
                vector descriptors;
                hog.compute(temp, descriptors, Size(1, 1), Size(0, 0));
                trainHOGDataVec.push_back(Mat(descriptors).reshape(0, 1).clone());
            }
            else
            {
                trainRawDataVec.push_back(temp.reshape(0, 1));
            }
            trainLabel.at(trainNums) = i;
            trainNums++;
        }
    }

    //组织测试数据
    int testNums = 0;
    for (int i = 0; i < 10; i++)
    {
        stringstream ss;
        ss << testPath << "\\" << i;
        string path = ss.str();
        vector files;
        getFiles1(path, files);
        int size = files.size();
        for (int a = 0; a < size; a++)
        {
            Mat temp = imread(files[a].c_str(), IMREAD_GRAYSCALE);
            if (hog_falg)
            {
                vector descriptors;
                hog.compute(temp, descriptors, Size(1, 1), Size(0, 0));
                testHOGDataVec.push_back(Mat(descriptors).reshape(0, 1).clone());
            }
            else
            {
                testRawDataVec.push_back(temp.reshape(0, 1));
            }
            testLabel.at(testNums) = i;
            testNums++;
        }
    }


    //创建SVM分类器
    Ptr svm = cv::ml::SVM::create();
    svm->setType(cv::ml::SVM::C_SVC);
    svm->setKernel(cv::ml::SVM::RBF);
    svm->setC(10);
    svm->setGamma(0.5);

    //转换训练和测试集格式
    convert_to_ml_for(trainHOGDataVec, trainHOGData);
    convert_to_ml_for(testHOGDataVec, testHOGData);

    //训练
    printf("开始训练......\n");
    trainLabel.convertTo(trainLabel, CV_32S);
    svm->train(trainHOGData, cv::ml::ROW_SAMPLE, trainLabel);
    printf("训练完成......\n");
    svm->save("digits_hog_svm.xml");

    //测试
    cv::Mat result;
    printf("开始测试......\n");
    svm->predict(testHOGData, result);
    //输出结果
    int t = 0, f = 0;
    for (int i=0; i< test_sample_count; i++)
    {
        int predict = (int)(result.at(i));
        int actual = (int)(testLabel.at(i));
        if (predict == actual)
        {
            t++;
        }
        else
        {
            f++;
        }
    }
    printf("测试完成......\n");

    //输出结果
    float accuracy = (t * 1.0) / (t + f);
    printf("测试总数 %d \n", test_sample_count);
    printf("正确 %d \n", t);
    printf("错误 %d \n", f);
}

测试完成......
测试总数 10000
正确 9932
错误 68 

2、c#、opencvsharp代码参考

OpenCvSharp.HOGDescriptor hog = new OpenCvSharp.HOGDescriptor(
    new OpenCvSharp.Size(28, 28),
    new OpenCvSharp.Size(14, 14),
    new OpenCvSharp.Size(7, 7),
    new OpenCvSharp.Size(7, 7),
    9);

string trainPath = "D:\\Project\\deeplearn\\dataset\\mnist_png_full\\training";
string testPath = "D:\\Project\\deeplearn\\dataset\\mnist_png_full\\testing";

//制作训练集
int train_sample_count = 60000;
int test_sample_count = 10000;

//声明变量
Mat trainData, testData;
List trainHOGDataVec = new List();
List testHOGDataVec = new List();

Mat trainLabel = new Mat(train_sample_count, 1, MatType.CV_32FC1);
Mat testLabel = new Mat(test_sample_count, 1, MatType.CV_32FC1);


//组织训练数据
int trainNums = 0;
for (int i = 0; i < 10; i++)
{
    string path = trainPath + "\\" + i;
    DirectoryInfo TheFolder = new DirectoryInfo(path);
    foreach (FileInfo NextFile in TheFolder.GetFiles())
    {
        Mat temp = new Mat(NextFile.FullName, ImreadModes.Grayscale);
        float[] descriptors = hog.Compute(temp, new OpenCvSharp.Size(1, 1), new OpenCvSharp.Size(0, 0));
        Mat des = OpenCvSharp.InputArray.Create(descriptors).GetMat();
        trainHOGDataVec.Add(des.Reshape(0, 1).Clone());
        trainLabel.Set(trainNums, i);
        trainNums++;
    }
}

//组织测试数据
int testNums = 0;
for (int i = 0; i < 10; i++)
{
    string path = testPath + "\\" + i;
    DirectoryInfo TheFolder = new DirectoryInfo(path);
    foreach (FileInfo NextFile in TheFolder.GetFiles())
    {
        Mat temp = new Mat(NextFile.FullName, ImreadModes.Grayscale);
        float[] descriptors = hog.Compute(temp, new OpenCvSharp.Size(1, 1), new OpenCvSharp.Size(0, 0));
        Mat des = OpenCvSharp.InputArray.Create(descriptors).GetMat();
        testHOGDataVec.Add(des.Reshape(0, 1).Clone());
        testLabel.Set(testNums, i);
        testNums++;
    }
}

//创建SVM分类器
OpenCvSharp.ML.SVM svm = OpenCvSharp.ML.SVM.Create();
svm.Type = OpenCvSharp.ML.SVM.Types.CSvc;
svm.KernelType = OpenCvSharp.ML.SVM.KernelTypes.Rbf;
svm.C = 10;
svm.Gamma = 0.5;
            
trainData = new Mat(train_sample_count, hog.GetDescriptorSize(), MatType.CV_32FC1);
for (int i = 0; i < train_sample_count; ++i)
{
    trainHOGDataVec[i].CopyTo(trainData.Row((int)i));
}

testData = new Mat(test_sample_count, hog.GetDescriptorSize(), MatType.CV_32FC1);
for (int i = 0; i < test_sample_count; ++i)
{
    testHOGDataVec[i].CopyTo(testData.Row((int)i));
}

//训练
Console.WriteLine("开始训练......\n");
trainLabel.ConvertTo(trainLabel, MatType.CV_32S);
svm.Train(trainData, OpenCvSharp.ML.SampleTypes.RowSample, trainLabel);
Console.WriteLine("训练完成......\n");
svm.Save("digits_hog_svm.xml");

//测试
Mat result = new Mat();
Console.WriteLine("开始测试......\n");
svm.Predict(testData, result);
//输出结果
int t = 0, f = 0;
for (int i = 0; i < test_sample_count; i++)
{
    int predict = (int)(result.At(i));
    int actual = (int)(testLabel.At(i));
    if (predict == actual)
    {
        t++;
    }
    else
    {
        f++;
    }
}
Console.WriteLine("测试完成......\n");

//输出结果
double accuracy = (t * 1.0) / (t + f);
Console.WriteLine("测试总数:" + test_sample_count);
Console.WriteLine("正确:" + t);
Console.WriteLine("错误:" + f);

测试完成......
测试总数 10000
正确 9927
错误 73 

四、小结

        SVM的类型非常多。

        针对线性可分数据,可以使用软间隔SVM;

        针对非线性可分数据,可以使用引入核技巧的非线性SVM;

        针对回归任务可以使用SVR。

        在具体使用时有两个重要的问题,一是模型选择问题(即参数搜索),二是特征提取问题。

        如果SVM用于机器视觉的分类任务,则通常会先提取原始数据的特征并进行一定的归一化,再使用SVM进行分类,而不是直接使用原始数据(图像)进行分类。传统特征多为基于直方图的特征,也可以使用神经网络来提取特征。

你可能感兴趣的:(#,OpenCV,支持向量机,opencv,机器学习,HOG特征,svm)