上一教程: 支持向量机简介
下一个教程: 主成分分析 (PCA) 简介
Original author | Fernando Iglesias García |
---|---|
Compatibility | OpenCV >= 3.0 |
目标
在本教程中,您将学习:
- 当无法线性分离训练数据时,如何定义 SVM 的优化问题。
- 如何配置参数,使 SVM 适应这类问题。
起因
为什么要扩展 SVM 优化问题以处理非线性可分离的训练数据?在计算机视觉中使用 SVM 的大多数应用都需要比简单线性分类器更强大的工具。这是因为在这些任务中,很难用超平面来分离训练数据。
举例来说,人脸检测就是其中一项任务。在这种情况下,训练数据由一组人脸图像和另一组非人脸图像(世界上除人脸以外的所有其他事物)组成。这种训练数据过于复杂,因此无法找到每个样本(特征向量)的表示方法,使整组人脸与整组非人脸可以线性分离。
优化问题的扩展
请记住,使用 SVM 时,我们会得到一个分离超平面。因此,由于训练数据现在是非线性可分离的,我们必须承认所找到的超平面会对一些样本进行错误分类。这种误分类是优化过程中必须考虑的一个新变量。新模型必须同时满足旧要求和新要求,旧要求是找到能提供最大余量的超平面,新要求是通过不允许过多的分类错误来正确归纳训练数据。
在此,我们从优化问题的表述开始,即找到边际最大的超平面(这在上一教程(支持向量机简介)中已有解释):
min β , β 0 L ( β ) = 1 2 ∣ ∣ β ∣ ∣ 2 s u b j e c t t o y i ( β T x i + β 0 ) ≥ 1 ∀ i \min_{\beta ,\beta _0}L\left( \beta \right) =\frac{1}{2}||\beta ||^2subject\ to\ y_i\left( \beta ^Tx_i+\beta _0 \right) \ge 1\forall i β,β0minL(β)=21∣∣β∣∣2subject to yi(βTxi+β0)≥1∀i
有多种方法可以对该模型进行修改,以便将误判误差考虑在内。例如,我们可以考虑最小化相同的数量,再加上一个常数乘以训练数据中的误分类错误数量,即:
min || β ∣ ∣ 2 + C ( m i s c l a s s i f i c a t i o n e r r o r s ) \min\text{||}\beta ||^2+C\left( misclassification\ errors \right) min||β∣∣2+C(misclassification errors)
然而,这并不是一个很好的解决方案,因为除其他原因外,我们并没有区分与适当决策区域距离较小的误分类样本和未被误分类的样本。因此,更好的解决方案应该考虑到被错误分类的样本到其正确决策区域的距离,即:
min
||
β
∣
∣
2
+
C
(
d
i
s
tan
c
e
o
f
m
i
s
c
l
a
s
s
i
f
i
e
d
s
a
m
p
l
e
s
t
o
t
h
e
i
r
c
o
r
r
e
c
t
r
e
g
i
o
n
s
)
\min\text{||}\beta ||^2+C\left( dis\tan ce\ of\ misclassified\ samples\ to\ their\ correct\ regions \right)
min||β∣∣2+C(distance of misclassified samples to their correct regions)
对于每个训练数据样本,都会定义一个新参数 ξi。每个参数都包含相应训练样本到正确决策区域的距离。下图显示了来自两个类别的非线性可分离训练数据、分离超平面以及被错误分类的样本到其正确区域的距离。
注释
图中只显示了分类错误的样本的距离。其余样本的距离为零,因为它们已经位于正确的判定区域内。
图中出现的红线和蓝线是每个判定区域的边缘。重要的是要认识到,每个 ξi 都是从被错误分类的训练样本到其相应区域的边际。
最后,优化问题的新表述为:
min β , β 0 L ( β ) = 1 2 ∣ ∣ β ∣ ∣ 2 + C ∑ ξ i s u b j e c t t o y i ( β T x i + β 0 ) ≥ 1 − ξ i a n d ξ i ≥ 0 ∀ i \min_{\beta ,\beta _0}L\left( \beta \right) =\frac{1}{2}||\beta ||^2+C\sum{\xi _i\ subject\ to\ y_i\left( \beta ^Tx_i+\beta _0 \right) \ge 1-\xi _i\ and\ \xi _i\ge 0\forall i} β,β0minL(β)=21∣∣β∣∣2+C∑ξi subject to yi(βTxi+β0)≥1−ξi and ξi≥0∀i
如何选择参数 C?显然,这个问题的答案取决于训练数据的分布情况。虽然没有通用的答案,但考虑到这些规则还是很有用的:
- C 值越大,误分类误差越小,但余量也越小。考虑到在这种情况下,误分类错误的代价是昂贵的。由于优化的目的是最小化参数,因此允许的误分类错误较少。
- C 值越小,解的余量越大,分类错误也越多。在这种情况下,最小化并不会考虑太多的和项,因此它更侧重于找到一个具有较大边距的超平面。
源代码
您也可以在 OpenCV 源代码库的amples/cpp/tutorial_code/ml/non_linear_svms 文件夹中找到源代码,或从此处下载。
- C++
-
- 可下载代码:点击这里
-
- 代码概览:
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/highgui.hpp>
#include <opencv2/ml.hpp>
using namespace cv;
using namespace cv::ml;
using namespace std;
static void help()
{
cout<< "\n--------------------------------------------------------------------------" << endl
<< "This program shows Support Vector Machines for Non-Linearly Separable Data. " << endl
<< "--------------------------------------------------------------------------" << endl
<< endl;
}
int main()
{
help();
const int NTRAINING_SAMPLES = 100; // 每类训练样本的数量
const float FRAC_LINEAR_SEP = 0.9f; // 构成线性可分离部分的样本比例
//数据可视化
const int WIDTH = 512, HEIGHT = 512;
Mat I = Mat::zeros(HEIGHT, WIDTH, CV_8UC3);
//--------------------- 1. 随机设置训练数据 ---------------------------------------
Mat trainData(2*NTRAINING_SAMPLES, 2, CV_32F);
Mat labels (2*NTRAINING_SAMPLES, 1, CV_32S);
RNG rng(100); // 随机值生成类
// 设置训练数据的线性可分离部分
int nLinearSamples = (int) (FRAC_LINEAR_SEP * NTRAINING_SAMPLES);
// 为类1生成随机点
Mat trainClass = trainData.rowRange(0, nLinearSamples);
// 各点的 x 坐标在 [0, 0.4) 范围内
Mat c = trainClass.colRange(0, 1);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(0.4 * WIDTH));
// 各点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));
// 为类2生成随机点
trainClass = trainData.rowRange(2*NTRAINING_SAMPLES-nLinearSamples, 2*NTRAINING_SAMPLES);
// 各点的 x 坐标位于 [0.6, 1] 范围内
c = trainClass.colRange(0 , 1);
rng.fill(c, RNG::UNIFORM, Scalar(0.6*WIDTH), Scalar(WIDTH));
// 各点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));
//------------------ 设置训练数据的非线性可分离部分 ---------------
// 为类1和类2生成随机点
trainClass = trainData.rowRange(nLinearSamples, 2*NTRAINING_SAMPLES-nLinearSamples);
// 各点的 x 坐标在 [0.4, 0.6) 范围内
c = trainClass.colRange(0,1);
rng.fill(c, RNG::UNIFORM, Scalar(0.4*WIDTH), Scalar(0.6*WIDTH));
// 各点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));
//------------------------- 为类设置标签 ---------------------------------
labels.rowRange( 0, NTRAINING_SAMPLES).setTo(1); // Class 1
labels.rowRange(NTRAINING_SAMPLES, 2*NTRAINING_SAMPLES).setTo(2); // Class 2
//------------------------ 2. 设置支持向量机参数 --------------------
cout << "Starting training process" << endl;
Ptr<SVM> svm = SVM::create();
svm->setType(SVM::C_SVC);
svm->setC(0.1);
svm->setKernel(SVM::LINEAR);
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, (int)1e7, 1e-6));
//------------------------ 3. 训练SVM ----------------------------------------------------
svm->train(trainData, ROW_SAMPLE, labels);
cout << "Finished training process" << endl;
//------------------------ 4. 显示决策区域 ----------------------------------------
Vec3b green(0,100,0), blue(100,0,0);
for (int i = 0; i < I.rows; i++)
{
for (int j = 0; j < I.cols; j++)
{
Mat sampleMat = (Mat_<float>(1,2) << j, i);
float response = svm->predict(sampleMat);
if (response == 1) I.at<Vec3b>(i,j) = green;
else if (response == 2) I.at<Vec3b>(i,j) = blue;
}
}
//----------------------- 5. 显示训练数据 --------------------------------------------
int thick = -1;
float px, py;
// Class 1
for (int i = 0; i < NTRAINING_SAMPLES; i++)
{
px = trainData.at<float>(i,0);
py = trainData.at<float>(i,1);
circle(I, Point( (int) px, (int) py ), 3, Scalar(0, 255, 0), thick);
}
// Class 2
for (int i = NTRAINING_SAMPLES; i <2*NTRAINING_SAMPLES; i++)
{
px = trainData.at<float>(i,0);
py = trainData.at<float>(i,1);
circle(I, Point( (int) px, (int) py ), 3, Scalar(255, 0, 0), thick);
}
//------------------------- 6. 显示支持向量 --------------------------------------------
thick = 2;
Mat sv = svm->getUncompressedSupportVectors();
for (int i = 0; i < sv.rows; i++)
{
const float* v = sv.ptr<float>(i);
circle(I, Point( (int) v[0], (int) v[1]), 6, Scalar(128, 128, 128), thick);
}
imwrite("result.png", I); // 保存图像
imshow("SVM for Non-Linear Training Data", I); // 显示给用户
waitKey();
return 0;
}
- Java
-
- 可下载代码:点击这里
-
- 代码概览:
import java.util.Random;
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 NonLinearSVMsDemo {
public static final int NTRAINING_SAMPLES = 100;
public static final float FRAC_LINEAR_SEP = 0.9f;
public static void main(String[] args) {
// 加载本地 OpenCV 库
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
System.out.println("\n--------------------------------------------------------------------------");
System.out.println("This program shows Support Vector Machines for Non-Linearly Separable Data. ");
System.out.println("--------------------------------------------------------------------------\n");
// 可视化数据
int width = 512, height = 512;
Mat I = Mat.zeros(height, width, CvType.CV_8UC3);
// --------------------- 1. 随机设置训练数据---------------------------------------
Mat trainData = new Mat(2 * NTRAINING_SAMPLES, 2, CvType.CV_32F);
Mat labels = new Mat(2 * NTRAINING_SAMPLES, 1, CvType.CV_32S);
Random rng = new Random(100); // 随机值生成类
// 设置训练数据的线性可分离部分
int nLinearSamples = (int) (FRAC_LINEAR_SEP * NTRAINING_SAMPLES);
// 为类 1 生成随机点
Mat trainClass = trainData.rowRange(0, nLinearSamples);
// 点的 x 坐标在 [0, 0.4) 范围内
Mat c = trainClass.colRange(0, 1);
float[] cData = new float[(int) (c.total() * c.channels())];
double[] cDataDbl = rng.doubles(cData.length, 0, 0.4f * width).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
// 点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1, 2);
cData = new float[(int) (c.total() * c.channels())];
cDataDbl = rng.doubles(cData.length, 0, height).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
// 为类 2 生成随机点
trainClass = trainData.rowRange(2 * NTRAINING_SAMPLES - nLinearSamples, 2 * NTRAINING_SAMPLES);
// 点的 x 坐标在 [0.6, 1] 范围内
c = trainClass.colRange(0, 1);
cData = new float[(int) (c.total() * c.channels())];
cDataDbl = rng.doubles(cData.length, 0.6 * width, width).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
// 点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1, 2);
cData = new float[(int) (c.total() * c.channels())];
cDataDbl = rng.doubles(cData.length, 0, height).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
// ------------------ 设置训练数据的非线性可分离部分 ---------------
// 为类 1 和类 2 生成随机点
trainClass = trainData.rowRange(nLinearSamples, 2 * NTRAINING_SAMPLES - nLinearSamples);
// 点的 x 坐标在 [0.4, 0.6) 范围内
c = trainClass.colRange(0, 1);
cData = new float[(int) (c.total() * c.channels())];
cDataDbl = rng.doubles(cData.length, 0.4 * width, 0.6 * width).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
// 点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1, 2);
cData = new float[(int) (c.total() * c.channels())];
cDataDbl = rng.doubles(cData.length, 0, height).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
// ------------------------- 设置类的标签---------------------------------
labels.rowRange(0, NTRAINING_SAMPLES).setTo(new Scalar(1)); // Class 1
labels.rowRange(NTRAINING_SAMPLES, 2 * NTRAINING_SAMPLES).setTo(new Scalar(2)); // Class 2
// ------------------------ 2. 设置支持向量机参数--------------------
System.out.println("Starting training process");
SVM svm = SVM.create();
svm.setType(SVM.C_SVC);
svm.setC(0.1);
svm.setKernel(SVM.LINEAR);
svm.setTermCriteria(new TermCriteria(TermCriteria.MAX_ITER, (int) 1e7, 1e-6));
// ------------------------ 3. 训练SVM----------------------------------------------------
svm.train(trainData, Ml.ROW_SAMPLE, labels);
System.out.println("Finished training process");
// ------------------------ 4. 显示决策区域----------------------------------------
byte[] IData = new byte[(int) (I.total() * I.channels())];
Mat sampleMat = new Mat(1, 2, CvType.CV_32F);
float[] sampleMatData = new float[(int) (sampleMat.total() * sampleMat.channels())];
for (int i = 0; i < I.rows(); i++) {
for (int j = 0; j < I.cols(); j++) {
sampleMatData[0] = j;
sampleMatData[1] = i;
sampleMat.put(0, 0, sampleMatData);
float response = svm.predict(sampleMat);
if (response == 1) {
IData[(i * I.cols() + j) * I.channels()] = 0;
IData[(i * I.cols() + j) * I.channels() + 1] = 100;
IData[(i * I.cols() + j) * I.channels() + 2] = 0;
} else if (response == 2) {
IData[(i * I.cols() + j) * I.channels()] = 100;
IData[(i * I.cols() + j) * I.channels() + 1] = 0;
IData[(i * I.cols() + j) * I.channels() + 2] = 0;
}
}
}
I.put(0, 0, IData);
// ----------------------- 5. 显示训练数据--------------------------------------------
int thick = -1;
int lineType = Imgproc.LINE_8;
float px, py;
// Class 1
float[] trainDataData = new float[(int) (trainData.total() * trainData.channels())];
trainData.get(0, 0, trainDataData);
for (int i = 0; i < NTRAINING_SAMPLES; i++) {
px = trainDataData[i * trainData.cols()];
py = trainDataData[i * trainData.cols() + 1];
Imgproc.circle(I, new Point(px, py), 3, new Scalar(0, 255, 0), thick, lineType, 0);
}
// Class 2
for (int i = NTRAINING_SAMPLES; i < 2 * NTRAINING_SAMPLES; ++i) {
px = trainDataData[i * trainData.cols()];
py = trainDataData[i * trainData.cols() + 1];
Imgproc.circle(I, new Point(px, py), 3, new Scalar(255, 0, 0), thick, lineType, 0);
}
// ------------------------- 6. 显示支持向量--------------------------------------------
thick = 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(I, new Point(svData[i * sv.cols()], svData[i * sv.cols() + 1]), 6, new Scalar(128, 128, 128),
thick, lineType, 0);
}
Imgcodecs.imwrite("result.png", I); // 保存图像
HighGui.imshow("SVM for Non-Linear Training Data", I); // 显示给用户
HighGui.waitKey();
System.exit(0);
}
}
- Python
-
- 可下载代码:点击这里
-
- 代码概览:
from __future__ import print_function
import cv2 as cv
import numpy as np
import random as rng
NTRAINING_SAMPLES = 100 # 每类训练样本的数量
FRAC_LINEAR_SEP = 0.9 # 构成线性可分离部分的样本比例
# 可视化数据
WIDTH = 512
HEIGHT = 512
I = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
# --------------------- 1. 随机设置训练数据 ---------------------------------------
trainData = np.empty((2*NTRAINING_SAMPLES, 2), dtype=np.float32)
labels = np.empty((2*NTRAINING_SAMPLES, 1), dtype=np.int32)
rng.seed(100) # 随机值生成类
# 设置训练数据的线性可分离部分
nLinearSamples = int(FRAC_LINEAR_SEP * NTRAINING_SAMPLES)
trainClass = trainData[0:nLinearSamples,:]
# 点的 x 坐标为 [0, 0.4)
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.0, 0.4 * WIDTH, c.shape)
# 点的 y 坐标在 [0, 1) 范围内
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
# 为类 2 生成随机点
trainClass = trainData[2*NTRAINING_SAMPLES-nLinearSamples:2*NTRAINING_SAMPLES,:]
# 点的 x 坐标在 [0.6, 1] 内
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.6*WIDTH, WIDTH, c.shape)
# 点的 y 坐标在 [0, 1) 范围内
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
#------------------ 设置训练数据的非线性可分离部分 ---------------
trainClass = trainData[nLinearSamples:2*NTRAINING_SAMPLES-nLinearSamples,:]
# 点的 x 坐标在 [0.4, 0.6) 范围内
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.4*WIDTH, 0.6*WIDTH, c.shape)
# 点的 y 坐标在 [0, 1) 范围内
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
#------------------------- 设置类的标签 ---------------------------------
labels[0:NTRAINING_SAMPLES,:] = 1 # Class 1
labels[NTRAINING_SAMPLES:2*NTRAINING_SAMPLES,:] = 2 # Class 2
#------------------------ 2. 设置支持向量机参数 --------------------
print('Starting training process')
svm = cv.ml.SVM_create()
svm.setType(cv.ml.SVM_C_SVC)
svm.setC(0.1)
svm.setKernel(cv.ml.SVM_LINEAR)
svm.setTermCriteria((cv.TERM_CRITERIA_MAX_ITER, int(1e7), 1e-6))
#------------------------ 3. 训练SVM ----------------------------------------------------
svm.train(trainData, cv.ml.ROW_SAMPLE, labels)
print('Finished training process')
#------------------------ 4. 显示决策区域 ----------------------------------------
green = (0,100,0)
blue = (100,0,0)
for i in range(I.shape[0]):
for j in range(I.shape[1]):
sampleMat = np.matrix([[j,i]], dtype=np.float32)
response = svm.predict(sampleMat)[1]
if response == 1:
I[i,j] = green
elif response == 2:
I[i,j] = blue
#----------------------- 5. 显示训练数据 --------------------------------------------
thick = -1
# Class 1
for i in range(NTRAINING_SAMPLES):
px = trainData[i,0]
py = trainData[i,1]
cv.circle(I, (int(px), int(py)), 3, (0, 255, 0), thick)
# Class 2
for i in range(NTRAINING_SAMPLES, 2*NTRAINING_SAMPLES):
px = trainData[i,0]
py = trainData[i,1]
cv.circle(I, (int(px), int(py)), 3, (255, 0, 0), thick)
#------------------------- 6. 显示支持向量 --------------------------------------------
thick = 2
sv = svm.getUncompressedSupportVectors()
for i in range(sv.shape[0]):
cv.circle(I, (int(sv[i,0]), int(sv[i,1])), 6, (128, 128, 128), thick)
cv.imwrite('result.png', I) # 保存图像
cv.imshow('SVM for Non-Linear Training Data', I) # 显示给用户
cv.waitKey()
解释
- 设置训练数据
本练习的训练数据由一组带有标签的二维点组成,这些点属于两个不同类别中的一个。为了使练习更有吸引力,训练数据使用统一的概率密度函数(PDFs)随机生成。
我们将训练数据的生成分为两个主要部分。
在第一部分中,我们为两个类别生成可线性分离的数据。
C++
// 为类1生成随机点
Mat trainClass = trainData.rowRange(0, nLinearSamples);
// 各点的 x 坐标在 [0, 0.4) 范围内
Mat c = trainClass.colRange(0, 1);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(0.4 * WIDTH));
// 各点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));
// 为类2生成随机点
trainClass = trainData.rowRange(2*NTRAINING_SAMPLES-nLinearSamples, 2*NTRAINING_SAMPLES);
// 各点的 x 坐标位于 [0.6, 1] 范围内
c = trainClass.colRange(0 , 1);
rng.fill(c, RNG::UNIFORM, Scalar(0.6*WIDTH), Scalar(WIDTH));
// 各点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));
Java
// 为类 1 生成随机点
Mat trainClass = trainData.rowRange(0, nLinearSamples);
// 点的 x 坐标在 [0, 0.4) 范围内
Mat c = trainClass.colRange(0, 1);
float[] cData = new float[(int) (c.total() * c.channels())];
double[] cDataDbl = rng.doubles(cData.length, 0, 0.4f * width).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
// 点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1, 2);
cData = new float[(int) (c.total() * c.channels())];
cDataDbl = rng.doubles(cData.length, 0, height).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
Python
# 为类1生成随机点
trainClass = trainData[0:nLinearSamples,:]
# 点的 x 坐标为 [0, 0.4)
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.0, 0.4 * WIDTH, c.shape)
# 点的 y 坐标在 [0, 1) 范围内
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
# 为类 2 生成随机点
trainClass = trainData[2*NTRAINING_SAMPLES-nLinearSamples:2*NTRAINING_SAMPLES,:]
# 点的 x 坐标在 [0.6, 1] 内
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.6*WIDTH, WIDTH, c.shape)
# 点的 y 坐标在 [0, 1) 范围内
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
在第二部分中,我们为两个类别创建非线性可分离的数据,即重叠数据。
C++
// 为类1和类2生成随机点
trainClass = trainData.rowRange(nLinearSamples, 2*NTRAINING_SAMPLES-nLinearSamples);
// 各点的 x 坐标在 [0.4, 0.6) 范围内
c = trainClass.colRange(0,1);
rng.fill(c, RNG::UNIFORM, Scalar(0.4*WIDTH), Scalar(0.6*WIDTH));
// 各点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));
Java
// 为类 1 和类 2 生成随机点
trainClass = trainData.rowRange(nLinearSamples, 2 * NTRAINING_SAMPLES - nLinearSamples);
// 点的 x 坐标在 [0.4, 0.6) 范围内
c = trainClass.colRange(0, 1);
cData = new float[(int) (c.total() * c.channels())];
cDataDbl = rng.doubles(cData.length, 0.4 * width, 0.6 * width).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
// 点的 y 坐标在 [0, 1) 范围内
c = trainClass.colRange(1, 2);
cData = new float[(int) (c.total() * c.channels())];
cDataDbl = rng.doubles(cData.length, 0, height).toArray();
for (int i = 0; i < cData.length; i++) {
cData[i] = (float) cDataDbl[i];
}
c.put(0, 0, cData);
Python
# 为类 1 和类 2 生成随机点
trainClass = trainData[nLinearSamples:2*NTRAINING_SAMPLES-nLinearSamples,:]
# 点的 x 坐标在 [0.4, 0.6) 范围内
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.4*WIDTH, 0.6*WIDTH, c.shape)
# 点的 y 坐标在 [0, 1) 范围内
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
- 设置 SVM 的参数
注意
在上一教程 "支持向量机简介 "中解释了类 cv::ml::SVM 的属性,我们将在训练 SVM 前配置这些属性。
C++
Ptr<SVM> svm = SVM::create();
svm->setType(SVM::C_SVC);
svm->setC(0.1);
svm->setKernel(SVM::LINEAR);
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, (int)1e7, 1e-6));
Java
SVM svm = SVM.create();
svm.setType(SVM.C_SVC);
svm.setC(0.1);
svm.setKernel(SVM.LINEAR);
svm.setTermCriteria(new TermCriteria(TermCriteria.MAX_ITER, (int) 1e7, 1e-6));
Python
svm = cv.ml.SVM_create()
svm.setType(cv.ml.SVM_C_SVC)
svm.setC(0.1)
svm.setKernel(cv.ml.SVM_LINEAR)
svm.setTermCriteria((cv.TERM_CRITERIA_MAX_ITER, int(1e7), 1e-6))
这里的配置与我们用作参考的上一教程(支持向量机入门)中的配置只有两处不同。
- C. 在这里,我们选择了一个较小的参数值,以避免在优化过程中出现过多的误分类错误。这样做的初衷是希望得到一个接近直观预期的解决方案。不过,我们建议通过调整该参数来更好地理解问题。
注意事项
在本例中,类间重叠区域的点非常少。通过给 FRAC_LINEAR_SEP 较小的值,可以增加点的密度,并深入探讨参数 C 的影响。
- 算法的终止标准。为了正确解决训练数据非线性可分离的问题,必须大幅增加最大迭代次数。特别是,我们将这一数值提高了五个数量级。
- 训练 SVM
我们调用 cv::ml::SVM::train 方法来构建 SVM 模型。注意,训练过程可能需要很长时间。运行程序时请耐心等待。
C++
svm->train(trainData, ROW_SAMPLE, labels);
Java
svm.train(trainData, Ml.ROW_SAMPLE, labels);
Python
svm.train(trainData, cv.ml.ROW_SAMPLE, labels)
- 显示决策区域
cv::ml::SVM::predict 方法用于使用训练有素的 SVM 对输入样本进行分类。在本例中,我们使用该方法根据 SVM 的预测结果对空间进行着色。换句话说,我们将图像中的像素理解为笛卡尔平面上的点,对其进行遍历。每个点的颜色取决于 SVM 预测的类别;如果是标签 1 的类别,则为深绿色;如果是标签 2 的类别,则为深蓝色。
C++
Vec3b green(0,100,0), blue(100,0,0);
for (int i = 0; i < I.rows; i++)
{
for (int j = 0; j < I.cols; j++)
{
Mat sampleMat = (Mat_<float>(1,2) << j, i);
float response = svm->predict(sampleMat);
if (response == 1) I.at<Vec3b>(i,j) = green;
else if (response == 2) I.at<Vec3b>(i,j) = blue;
}
}
Java
byte[] IData = new byte[(int) (I.total() * I.channels())];
Mat sampleMat = new Mat(1, 2, CvType.CV_32F);
float[] sampleMatData = new float[(int) (sampleMat.total() * sampleMat.channels())];
for (int i = 0; i < I.rows(); i++) {
for (int j = 0; j < I.cols(); j++) {
sampleMatData[0] = j;
sampleMatData[1] = i;
sampleMat.put(0, 0, sampleMatData);
float response = svm.predict(sampleMat);
if (response == 1) {
IData[(i * I.cols() + j) * I.channels()] = 0;
IData[(i * I.cols() + j) * I.channels() + 1] = 100;
IData[(i * I.cols() + j) * I.channels() + 2] = 0;
} else if (response == 2) {
IData[(i * I.cols() + j) * I.channels()] = 100;
IData[(i * I.cols() + j) * I.channels() + 1] = 0;
IData[(i * I.cols() + j) * I.channels() + 2] = 0;
}
}
}
I.put(0, 0, IData);
Python
green = (0,100,0)
blue = (100,0,0)
for i in range(I.shape[0]):
for j in range(I.shape[1]):
sampleMat = np.matrix([[j,i]], dtype=np.float32)
response = svm.predict(sampleMat)[1]
if response == 1:
I[i,j] = green
elif response == 2:
I[i,j] = blue
- 显示训练数据
cv::circle 方法用于显示组成训练数据的样本。标记为 1 的样本用浅绿色显示,标记为 2 的样本用浅蓝色显示。
C++
int thick = -1;
float px, py;
// Class 1
for (int i = 0; i < NTRAINING_SAMPLES; i++)
{
px = trainData.at<float>(i,0);
py = trainData.at<float>(i,1);
circle(I, Point( (int) px, (int) py ), 3, Scalar(0, 255, 0), thick);
}
// Class 2
for (int i = NTRAINING_SAMPLES; i <2*NTRAINING_SAMPLES; i++)
{
px = trainData.at<float>(i,0);
py = trainData.at<float>(i,1);
circle(I, Point( (int) px, (int) py ), 3, Scalar(255, 0, 0), thick);
}
Java
int thick = -1;
int lineType = Imgproc.LINE_8;
float px, py;
// Class 1
float[] trainDataData = new float[(int) (trainData.total() * trainData.channels())];
trainData.get(0, 0, trainDataData);
for (int i = 0; i < NTRAINING_SAMPLES; i++) {
px = trainDataData[i * trainData.cols()];
py = trainDataData[i * trainData.cols() + 1];
Imgproc.circle(I, new Point(px, py), 3, new Scalar(0, 255, 0), thick, lineType, 0);
}
// Class 2
for (int i = NTRAINING_SAMPLES; i < 2 * NTRAINING_SAMPLES; ++i) {
px = trainDataData[i * trainData.cols()];
py = trainDataData[i * trainData.cols() + 1];
Imgproc.circle(I, new Point(px, py), 3, new Scalar(255, 0, 0), thick, lineType, 0);
}
Python
thick = -1
# Class 1
for i in range(NTRAINING_SAMPLES):
px = trainData[i,0]
py = trainData[i,1]
cv.circle(I, (int(px), int(py)), 3, (0, 255, 0), thick)
# Class 2
for i in range(NTRAINING_SAMPLES, 2*NTRAINING_SAMPLES):
px = trainData[i,0]
py = trainData[i,1]
cv.circle(I, (int(px), int(py)), 3, (255, 0, 0), thick)
- 支持向量
我们在这里使用几个方法来获取支持向量的信息。方法 cv::ml::SVM::getSupportVectors 可以获取所有支持向量。我们在此使用该方法找到支持向量的训练示例,并突出显示它们。
C++
thick = 2;
Mat sv = svm->getUncompressedSupportVectors();
for (int i = 0; i < sv.rows; i++)
{
const float* v = sv.ptr<float>(i);
circle(I, Point( (int) v[0], (int) v[1]), 6, Scalar(128, 128, 128), thick);
}
Java
thick = 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(I, new Point(svData[i * sv.cols()], svData[i * sv.cols() + 1]), 6, new Scalar(128, 128, 128),
thick, lineType, 0);
}
Python
thick = 2
sv = svm.getUncompressedSupportVectors()
for i in range(sv.shape[0]):
cv.circle(I, (int(sv[i,0]), int(sv[i,1])), 6, (128, 128, 128), thick)
结果
- 代码会打开一幅图像,并显示两个类别的训练示例。其中一类的点用浅绿色表示,另一类的点用浅蓝色表示。
- SVM 经过训练后用于对图像的所有像素进行分类。其结果是将图像划分为蓝色区域和绿色区域。这两个区域的边界就是分离超平面。由于训练数据是非线性可分离的,因此可以看出,两个类别中的一些示例被错误分类;一些绿色点位于蓝色区域,一些蓝色点位于绿色区域。
- 最后,支持向量用灰色圆环围绕训练示例显示出来。
您可以在 YouTube 上看到运行时的实例