上一篇教程: 条码识别

下一篇教程: 用于非线性可分数据的支持向量机

原作者Fernando Iglesias García

兼容性OpenCV >= 3.0

目标

在本教程中,您将学习如何

使用OpenCV函数 cv::ml::SVM::train构建基于SVM的分类器,以及 cv::ml::SVM::predict测试其性能。

什么是SVM?

支持向量机(SVM)是一种判别分类器,由分离超平面正式定义。换句话说,给定标记的训练数据(监督学习),该算法输出一个最优超平面,该超平面对新的示例进行分类。

获得的超平面在什么意义上是最优的?让我们考虑以下简单问题

对于属于两个类别之一的线性可分2D点集,找到一条分离直线。

注意在这个例子中,我们处理的是笛卡尔平面上的直线和点,而不是高维空间中的超平面和向量。这是对问题的简化。重要的是要理解,这样做仅仅是因为我们的直觉更容易从易于想象的例子中构建。然而,相同的概念也适用于分类示例位于维度高于二的空間的任务。

在上图中,您可以看到存在多条直线可以解决这个问题。其中任何一条都比其他更好吗?我们可以直观地定义一个标准来估计这些直线的价值:如果一条直线经过过于靠近点的位置,那么它将对噪声敏感,并且不会正确泛化。 因此,我们的目标应该是找到一条尽可能远离所有点的直线。

然后,SVM算法的操作基于寻找能够给出与训练示例最大最小距离的超平面。这个距离的两倍在SVM理论中被称为裕度。因此,最优分离超平面最大化训练数据的裕度。

如何计算最优超平面?

让我们介绍用于正式定义超平面的符号

\[f(x) = \beta_{0} + \beta^{T} x,\]

其中\(\beta\)被称为权重向量,\(\beta_{0}\)被称为偏差。

注意您可以从书中第4.5节(分离超平面)中找到关于此和超平面的更深入描述:T. Hastie、R. Tibshirani和J. H. Friedman的统计学习要素 ([273])。

通过对\(\beta\)和\(\beta_{0}\)进行缩放,可以以无限多种不同的方式表示最优超平面。按照约定,在超平面所有可能的表示中,选择的是

\[|\beta_{0} + \beta^{T} x| = 1\]

其中\(x\)表示最靠近超平面的训练示例。通常,最靠近超平面的训练示例称为支持向量。这种表示称为规范超平面。

现在,我们使用几何结果,该结果给出点\(x\)和超平面\((\beta, \beta_{0})\)之间的距离

\[\mathrm{distance} = \frac{|\beta_{0} + \beta^{T} x|}{||\beta||}.\]

特别是对于规范超平面,分子等于1,并且到支持向量的距离是

\[\mathrm{distance}_{\text{ support vectors}} = \frac{|\beta_{0} + \beta^{T} x|}{||\beta||} = \frac{1}{||\beta||}.\]

回想一下,上一节中介绍的裕度,这里表示为\(M\),是到最接近示例距离的两倍

\[M = \frac{2}{||\beta||}\]

最后,最大化\(M\)的问题等同于在某些约束条件下最小化函数\(L(\beta)\)的问题。约束条件模拟了超平面正确分类所有训练示例\(x_{i}\)的要求。形式上,

\[\min_{\beta, \beta_{0}} L(\beta) = \frac{1}{2}||\beta||^{2} \text{ 受制于 } y_{i}(\beta^{T} x_{i} + \beta_{0}) \geq 1 \text{ } \forall i,\]

其中\(y_{i}\)表示每个训练示例的标签。

这是一个拉格朗日优化问题,可以使用拉格朗日乘子来求解最优超平面的权重向量\(\beta\)和偏差\(\beta_{0}\)。

源代码

C++

可下载代码: 点击 此处

代码概览 #include

#include

#include

#include

#include

using namespace cv;

using namespace cv::ml;

int main(int, char**)

{

// 设置训练数据

int labels[4] = {1, -1, -1, -1};

float trainingData[4][2] = { {501, 10}, {255, 10}, {501, 255}, {10, 501} };

Mat trainingDataMat(4, 2, CV_32F, trainingData);

Mat labelsMat(4, 1, CV_32SC1, labels);

// 训练SVM

Ptr svm = SVM::create();

svm->setType(SVM::C_SVC);

svm->setKernel(SVM::LINEAR);

svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 100, 1e-6));

svm->train(trainingDataMat, ROW_SAMPLE, labelsMat);

// 用于可视化表示的数据

int width = 512, height = 512;

Mat image = Mat::zeros(height, width, CV_8UC3);

// 显示SVM给出的决策区域

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++)

{

Mat sampleMat = (Mat_(1,2) << j,i);

float response = svm->predict(sampleMat);

if (response == 1)

image.at(i,j) = green;

否则如果 (response == -1)

image.at(i,j) = blue;

}

}

// 显示训练数据

int thickness = -1;

circle( image, Point(501, 10), 5, Scalar( 0, 0, 0), thickness );

circle( image, Point(255, 10), 5, Scalar(255, 255, 255), thickness );

circle( image, Point(501, 255), 5, Scalar(255, 255, 255), thickness );

circle( image, Point( 10, 501), 5, Scalar(255, 255, 255), thickness );

// 显示支持向量

thickness = 2;

Mat sv = svm->getUncompressedSupportVectors();

for (int i = 0; i < sv.rows; i++)

{

const float* v = sv.ptr(i);

circle(image, Point( (int) v[0], (int) v[1]), 6, Scalar(128, 128, 128), thickness);

}

imwrite("result.png", image); // 保存图像

imshow("SVM 简单示例", image); // 显示给用户

waitKey();

返回 0;

}

cv::Mat_派生自 Mat 的模板矩阵类。定义 mat.hpp:2247

cv::Matn 维密集数组类定义 mat.hpp:829

cv::Mat::ptruchar * ptr(int i0=0)返回指向指定矩阵行的指针。

cv::Mat::at_Tp & at(int i0=0)返回对指定数组元素的引用。

cv::Mat::colsint cols定义 mat.hpp:2155

cv::Mat::rowsint rows行和列的数量,当矩阵具有超过 2 维时为 (-1, -1)定义 mat.hpp:2155

cv::Point_< int >

cv::Scalar_< double >

cv::TermCriteria定义迭代算法终止条件的类。定义 types.hpp:893

cv::Vec短数值向量的模板类,是 Matx 的一个特例。定义 matx.hpp:369

core.hpp

cv::Ptrstd::shared_ptr< _Tp > Ptr定义 cvstd_wrapper.hpp:23

CV_32SC1#define CV_32SC1定义 interface.h:112

CV_32F#define CV_32F定义 interface.h:78

CV_8UC3#define CV_8UC3定义 interface.h:90

cv::datasets::circle@ circle定义 gr_skig.hpp:62

cv::imshowvoid imshow(const String &winname, InputArray mat)在指定的窗口中显示图像。

cv::waitKeyint waitKey(int delay=0)等待按下键。

cv::imwriteCV_EXPORTS_W bool imwrite(const String &filename, InputArray img, const std::vector< int > ¶ms=std::vector< int >())将图像保存到指定文件。

highgui.hpp

mainint main(int argc, char *argv[])定义 highgui_qt.cpp:3

imgcodecs.hpp

imgproc.hpp

ml.hpp

cv::ml定义 ml.hpp:75

cv定义 core.hpp:107

Java

可下载代码:点击 这里

代码概览 import org.opencv.core.Core;

import org.opencv.core.CvType;

import org.opencv.core.Mat;

import org.opencv.core.Point;

import org.opencv.core.Scalar;

import org.opencv.core.TermCriteria;

import org.opencv.highgui.HighGui;

import org.opencv.imgcodecs.Imgcodecs;

import org.opencv.imgproc.Imgproc;

import org.opencv.ml.Ml;

import org.opencv.ml.SVM;

public class IntroductionToSVMDemo {

public static void main(String[] args) {

// 加载原生 OpenCV 库

System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

// 设置训练数据

int[] labels = { 1, -1, -1, -1 };

float[] trainingData = { 501, 10, 255, 10, 501, 255, 10, 501 };

Mat trainingDataMat = new Mat(4, 2, CvType.CV_32FC1);

trainingDataMat.put(0, 0, trainingData);

Mat labelsMat = new Mat(4, 1, CvType.CV_32SC1);

labelsMat.put(0, 0, labels);

// 训练SVM

SVM svm = SVM.create();

svm.setType(SVM.C_SVC);

svm.setKernel(SVM.LINEAR);

svm.setTermCriteria(new TermCriteria(TermCriteria.MAX_ITER, 100, 1e-6));

svm.train(trainingDataMat, Ml.ROW_SAMPLE, labelsMat);

// 用于可视化表示的数据

int width = 512, height = 512;

Mat image = Mat.zeros(height, width, CvType.CV_8UC3);

// 显示SVM给出的决策区域

byte[] imageData = new byte[(int) (image.total() * image.channels())];

Mat sampleMat = new Mat(1, 2, CvType.CV_32F);

float[] sampleMatData = new float[(int) (sampleMat.total() * sampleMat.channels())];

for (int i = 0; i < image.rows(); i++) {

for (int j = 0; j < image.cols(); j++) {

sampleMatData[0] = j;

sampleMatData[1] = i;

sampleMat.put(0, 0, sampleMatData);

float response = svm.predict(sampleMat);

if (response == 1) {

imageData[(i * image.cols() + j) * image.channels()] = 0;

imageData[(i * image.cols() + j) * image.channels() + 1] = (byte) 255;

imageData[(i * image.cols() + j) * image.channels() + 2] = 0;

} 否则如果 (response == -1) {

imageData[(i * image.cols() + j) * image.channels()] = (byte) 255;

imageData[(i * image.cols() + j) * image.channels() + 1] = 0;

imageData[(i * image.cols() + j) * image.channels() + 2] = 0;

}

}

}

image.put(0, 0, imageData);

// 显示训练数据

int thickness = -1;

int lineType = Imgproc.LINE_8;

Imgproc.circle(image, new Point(501, 10), 5, new Scalar(0, 0, 0), thickness, lineType, 0);

Imgproc.circle(image, new Point(255, 10), 5, new Scalar(255, 255, 255), thickness, lineType, 0);

Imgproc.circle(image, new Point(501, 255), 5, new Scalar(255, 255, 255), thickness, lineType, 0);

Imgproc.circle(image, new Point(10, 501), 5, new Scalar(255, 255, 255), thickness, lineType, 0);

// 显示支持向量

thickness = 2;

Mat sv = svm.getUncompressedSupportVectors();

float[] svData = new float[(int) (sv.total() * sv.channels())];

sv.get(0, 0, svData);

for (int i = 0; i < sv.rows(); ++i) {

Imgproc.circle(image, new Point(svData[i * sv.cols()], svData[i * sv.cols() + 1]), 6,

new Scalar(128, 128, 128), thickness, lineType, 0);

}

Imgcodecs.imwrite("result.png", image); // 保存图像

HighGui.imshow("SVM 简单示例", image); // 显示给用户

HighGui.waitKey();

System.exit(0);

}

}

cv::PointPoint2i Point定义 types.hpp:209

cv::ScalarScalar_< double > Scalar定义 types.hpp:709

Python

可下载代码:点击 此处

代码概览 import cv2 as cv

import numpy as np

# 设置训练数据

labels = np.array([1, -1, -1, -1])

trainingData = np.matrix([[501, 10], [255, 10], [501, 255], [10, 501]], dtype=np.float32)

# 训练SVM

svm = cv.ml.SVM_create()

svm.setType(cv.ml.SVM_C_SVC)

svm.setKernel(cv.ml.SVM_LINEAR)

svm.setTermCriteria((cv.TERM_CRITERIA_MAX_ITER, 100, 1e-6))

svm.train(trainingData, cv.ml.ROW_SAMPLE, labels)

# 用于视觉表示的数据

width = 512

height = 512

image = np.zeros((height, width, 3), dtype=np.uint8)

# 显示SVM给出的决策区域

green = (0,255,0)

blue = (255,0,0)

for i in range(image.shape[0])

for j in range(image.shape[1])

sampleMat = np.matrix([[j,i]], dtype=np.float32)

response = svm.predict(sampleMat)[1]

if response == 1

image[i,j] = green

elif response == -1

image[i,j] = blue

# 显示训练数据

thickness = -1

cv.circle(image, (501, 10), 5, ( 0, 0, 0), thickness)

cv.circle(image, (255, 10), 5, (255, 255, 255), thickness)

cv.circle(image, (501, 255), 5, (255, 255, 255), thickness)

cv.circle(image, ( 10, 501), 5, (255, 255, 255), thickness)

# 显示支持向量

thickness = 2

sv = svm.getUncompressedSupportVectors()

for i in range(sv.shape[0])

cv.circle(image, (int(sv[i,0]), int(sv[i,1])), 6, (128, 128, 128), thickness)

cv.imwrite('result.png', image) # 保存图像

cv.imshow('SVM 简单示例', image) # 显示给用户

cv.waitKey()()

cv::circlevoid circle(InputOutputArray img, Point center, int radius, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)绘制一个圆。

解释

设置训练数据

本练习的训练数据由一组属于两个不同类别之一的带标签的二维点组成;其中一个类别由一个点组成,另一个类别由三个点组成。

C++ int labels[4] = {1, -1, -1, -1};

float trainingData[4][2] = { {501, 10}, {255, 10}, {501, 255}, {10, 501} };

Java int[] labels = { 1, -1, -1, -1 };

float[] trainingData = { 501, 10, 255, 10, 501, 255, 10, 501 };

Python labels = np.array([1, -1, -1, -1])

trainingData = np.matrix([[501, 10], [255, 10], [501, 255], [10, 501]], dtype=np.float32)

稍后将使用的函数cv::ml::SVM::train要求训练数据存储为浮点数的cv::Mat对象。因此,我们根据上面定义的数组创建这些对象。

C++ Mat trainingDataMat(4, 2, CV_32F, trainingData);

Mat labelsMat(4, 1, CV_32SC1, labels);

Java Mat trainingDataMat = new Mat(4, 2, CvType.CV_32FC1);

trainingDataMat.put(0, 0, trainingData);

Mat labelsMat = new Mat(4, 1, CvType.CV_32SC1);

labelsMat.put(0, 0, labels);

Python labels = np.array([1, -1, -1, -1])

trainingData = np.matrix([[501, 10], [255, 10], [501, 255], [10, 501]], dtype=np.float32)

设置SVM的参数

在本教程中,我们介绍了在最简单的情况下SVM的理论,即训练样本分散到两个线性可分的不同类别中。但是,SVM可以用于各种各样的问题(例如,具有非线性可分数据的的问题,使用核函数来提高样本维数的SVM等)。因此,在训练SVM之前,我们必须定义一些参数。这些参数存储在cv::ml::SVM类的对象中。

C++ Ptr svm = SVM::create();

svm->setType(SVM::C_SVC);

svm->setKernel(SVM::LINEAR);

svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 100, 1e-6));

Java SVM svm = SVM.create();

svm.setType(SVM.C_SVC);

svm.setKernel(SVM.LINEAR);

svm.setTermCriteria(new TermCriteria(TermCriteria.MAX_ITER, 100, 1e-6));

Python svm = cv.ml.SVM_create()

svm.setType(cv.ml.SVM_C_SVC)

svm.setKernel(cv.ml.SVM_LINEAR)

svm.setTermCriteria((cv.TERM_CRITERIA_MAX_ITER, 100, 1e-6))

这里

SVM的类型。我们在这里选择C_SVC类型,它可以用于n类分类(n ≥ 2)。这种类型的重要特征是它处理类别的不完全分离(即当训练数据是非线性可分时)。这个特性在这里并不重要,因为数据是线性可分的,我们只选择这种SVM类型是因为它最常用。

SVM核的类型。我们还没有讨论核函数,因为它们对我们正在处理的训练数据来说并不重要。然而,让我们现在简要解释一下核函数背后的主要思想。它是对训练数据进行的映射,以提高其与线性可分数据集的相似性。这种映射包括增加数据的维数,并且使用核函数可以有效地完成。我们在这里选择LINEAR类型,这意味着不进行映射。此参数使用cv::ml::SVM::setKernel定义。

算法的终止准则。SVM训练过程是通过以迭代方式解决约束二次优化问题来实现的。在这里,我们指定最大迭代次数和容错率,因此我们允许算法在较少的步骤中完成,即使最佳超平面尚未计算出来。此参数在一个cv::TermCriteria结构中定义。

训练SVM 我们调用方法cv::ml::SVM::train来构建SVM模型。

C++ svm->train(trainingDataMat, ROW_SAMPLE, labelsMat);

Java svm.train(trainingDataMat, Ml.ROW_SAMPLE, labelsMat);

Python svm.train(trainingData, cv.ml.ROW_SAMPLE, labels)

SVM分类的区域

方法 cv::ml::SVM::predict 用于使用训练好的 SVM 对输入样本进行分类。在本例中,我们使用此方法根据 SVM 的预测结果对空间进行着色。换句话说,程序遍历图像,将其像素解释为笛卡尔平面上的点。每个点根据 SVM 预测的类别着色;如果是标签为 1 的类别则为绿色,如果是标签为 -1 的类别则为蓝色。

C++ 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++)

{

Mat sampleMat = (Mat_(1,2) << j,i);

float response = svm->predict(sampleMat);

if (response == 1)

image.at(i,j) = green;

否则如果 (response == -1)

image.at(i,j) = blue;

}

}

Java byte[] imageData = new byte[(int) (image.total() * image.channels())];

Mat sampleMat = new Mat(1, 2, CvType.CV_32F);

float[] sampleMatData = new float[(int) (sampleMat.total() * sampleMat.channels())];

for (int i = 0; i < image.rows(); i++) {

for (int j = 0; j < image.cols(); j++) {

sampleMatData[0] = j;

sampleMatData[1] = i;

sampleMat.put(0, 0, sampleMatData);

float response = svm.predict(sampleMat);

if (response == 1) {

imageData[(i * image.cols() + j) * image.channels()] = 0;

imageData[(i * image.cols() + j) * image.channels() + 1] = (byte) 255;

imageData[(i * image.cols() + j) * image.channels() + 2] = 0;

} 否则如果 (response == -1) {

imageData[(i * image.cols() + j) * image.channels()] = (byte) 255;

imageData[(i * image.cols() + j) * image.channels() + 1] = 0;

imageData[(i * image.cols() + j) * image.channels() + 2] = 0;

}

}

}

image.put(0, 0, imageData);

Python green = (0,255,0)

blue = (255,0,0)

for i in range(image.shape[0])

for j in range(image.shape[1])

sampleMat = np.matrix([[j,i]], dtype=np.float32)

response = svm.predict(sampleMat)[1]

if response == 1

image[i,j] = green

elif response == -1

image[i,j] = blue

支持向量

这里我们使用几种方法来获取有关支持向量的信息。方法 cv::ml::SVM::getSupportVectors 获取所有支持向量。我们在这里使用此方法来查找作为支持向量的训练样本并突出显示它们。

C++ thickness = 2;

Mat sv = svm->getUncompressedSupportVectors();

for (int i = 0; i < sv.rows; i++)

{

const float* v = sv.ptr(i);

circle(image, Point( (int) v[0], (int) v[1]), 6, Scalar(128, 128, 128), thickness);

}

Java thickness = 2;

Mat sv = svm.getUncompressedSupportVectors();

float[] svData = new float[(int) (sv.total() * sv.channels())];

sv.get(0, 0, svData);

for (int i = 0; i < sv.rows(); ++i) {

Imgproc.circle(image, new Point(svData[i * sv.cols()], svData[i * sv.cols() + 1]), 6,

new Scalar(128, 128, 128), thickness, lineType, 0);

}

Python thickness = 2

sv = svm.getUncompressedSupportVectors()

for i in range(sv.shape[0])

cv.circle(image, (int(sv[i,0]), int(sv[i,1])), 6, (128, 128, 128), thickness)

结果

代码打开一个图像并显示两类的训练样本。一类的点用白圈表示,另一类用黑圈表示。

SVM 经过训练,并用于对图像的所有像素进行分类。这导致图像被划分为蓝色区域和绿色区域。这两个区域之间的边界是最佳分离超平面。

最后,使用灰色环围绕训练样本显示支持向量。