1. 引言
opencv中集成了基于libsvm实现的SVM接口,便于直接进行视觉分类任务。
对于数据处理和可视化需求来说,可以用python接口opencv的SVM更加直观方便。
训练完模型后,将SVM模型保存为xml,可以在实时性应用中通过C++接口调用参数文件,进行实时推断。
在非均衡样本的分类训练中,用opencv中SVM默认的train 函数,容易导致分类器偏向数量多的类别,这时可以采用trainAuto 函数进行平衡。
如果你对SVM的原理有一定了解,可以直接跳转第3、4小节。
2. 基本原理
SVM旨在找到一个划分超平面,使得划分后的分类结果是最鲁棒的,对未见过的样本泛化性最好。
在样本空间中,划分超平面可以用这个方程进行描述:
w
T
x
+
b
=
0
\boldsymbol{w}^T\boldsymbol{x}+b=0
wTx+b=0,其中
w
=
(
w
1
;
w
2
;
.
.
.
;
w
d
)
\boldsymbol{w}=(w_1;w_2;...;w_d)
w=(w1?;w2?;...;wd?)为法向量,决定超平面的方向,b为位移项,决定超平面与原点之间的距离。
对于线性可分的样本空间,需要找到具有最大间隔(maximum margin)的划分超平面,即找到能使下式最大化的参数
w
\boldsymbol{w}
w和b:
min
?
w
,
b
1
2
∣
∣
w
∣
∣
2
\min_{w,b}{\frac{1}{2}||\boldsymbol{w}||^2}
w,bmin?21?∣∣w∣∣2s.t.
y
i
(
w
T
x
i
+
b
)
≥
1
,
i
=
1
,
2
,
.
.
.
,
m
y_i(\boldsymbol{w}^T\boldsymbol{x_i}+b)≥1,i=1,2,...,m
yi?(wTxi?+b)≥1,i=1,2,...,m
对于线性不可分的样本空间,可以将样本从原始空间映射到另一个高维特征空间,从而使样本在这个特征空间内线性可分。由于特征空间的维数可能很高,难以计算,所以通过引入核函数,可以将高维特征空间中的内积(dot product)转化为低维特征空间中的通过核函数计算的结果。
常用核函数:
为了减少过拟合,引入软间隔(soft margin)概念,允许支持向量机在一些样本上出错:
y
i
(
w
T
x
i
+
b
)
≥
1
y_i(\boldsymbol{w}^T\boldsymbol{x_i}+b)≥1
yi?(wTxi?+b)≥1
用参数C来约束分类出错的样本,松弛变量
ξ
i
ξ_i
ξi?表示训练样本距离对应的正确决策边界的距离,对于分类正确的样本距离即为0。
优化问题调整为:
m
i
n
w
,
b
0
∣
∣
w
∣
∣
2
+
C
∑
i
ξ
i
min_{\boldsymbol{w},b_0}{||\boldsymbol{w}||^2+C\sum_i{ξ_i}}
minw,b0??∣∣w∣∣2+Ci∑?ξi?
s.t.
y
i
(
w
T
x
i
+
b
0
)
≥
1
?
ξ
i
,
且
ξ
i
≥
0
?
i
y_i(\boldsymbol{w}^T\boldsymbol{x_i}+b_0)≥1-ξ_i,且ξ_i≥0 ?i
yi?(wTxi?+b0?)≥1?ξi?,且ξi?≥0?i
3. 函数解析
SVM类在opencv中的继承关系如图所示:
SVM继承自StatModel和Algorithm类。
在opencv中使用SVM的一般流程如下:
训练
推理
开始
创建SVM模型
加载SVM模型
配置参数
加载训练数据
模型训练
保存模型
输入数据进行预测
创建模型
C++:
static Ptr<SVM> cv::ml::SVM::create()
Python:
cv.ml.SVM_create() -> retval
设置模型类型
C++:
enum Types {
C_SVC =100,
NU_SVC =101,
ONE_CLASS =102,
EPS_SVR =103,
NU_SVR =104
}
virtual void cv::ml::SVM::setType(int val)
Python:
cv.ml_SVM.setType(val) ->None
设置参数C
根据"2.基本原理"中对参数C的介绍,我们应该如何设置参数C?
- 较大的 C值给出了误分类错误较少但余量较小的解决方案。考虑到在这种情况下犯错误分类错误的代价很高。由于优化的目的是最小化参数,因此允许出现很少的误分类错误。
- 较小的C值给出了具有更大余量和更多分类错误的解决方案。在这种情况下,最小化不会考虑那么多的总和项,因此它更侧重于寻找具有大余量的超平面。
C++:
virtual void cv::ml::SVM::setC(double val)
python:
cv.ml_SVM.setC(val) -> None
设置核函数
C++:
enum KernelTypes {
CUSTOM =-1,
LINEAR =0,
POLY =1,
RBF =2,
SIGMOID =3,
CHI2 =4,
INTER =5
}
virtual void cv::ml::SVM::setKernel(int kernelType)
python:
cv.ml_SVM.setKernel(kernelType) -> None
设置迭代算法的终止标准
C++:
virtual void cv::ml::SVM::setTermCriteria(const cv::TermCriteria &val)
cv::TermCriteria::TermCriteria (int type,int maxCount,double epsilon)
enum cv::TermCriteria::Type {
COUNT =1,
MAX_ITER =COUNT,
EPS =2
}
python:
cv.ml_SVM.setTermCriteria(val) ->None
训练SVM模型
trainAuto 方法通过选择最佳参数 C、gamma、p、nu、coef0、degree 来自动训练 SVM 模型。当测试集误差的交叉验证估计最小时,参数被认为是最佳的。此函数仅使用SVM::getDefaultGrid进行参数优化,因此仅提供基本的参数选项。
trainAuto 函数适用于分类(SVM::C_SVC或SVM::NU_SVC)以及回归(SVM::EPS_SVR或SVM::NU_SVR)。如果是SVM::ONE_CLASS,则不进行优化,并执行带有 params 中指定参数的常用 SVM。
C++:
virtual bool cv::ml::SVM::trainAuto(const Ptr<TrainData> & 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
)
bool cv::ml::SVM::trainAuto(InputArray samples,
int layout,
InputArray responses,
int kFold = 10,
Ptr< ParamGrid > Cgrid = SVM::getDefaultGridPtr(SVM::C),
Ptr< ParamGrid > gammaGrid = SVM::getDefaultGridPtr(SVM::GAMMA),
Ptr< ParamGrid > pGrid = SVM::getDefaultGridPtr(SVM::P),
Ptr< ParamGrid > nuGrid = SVM::getDefaultGridPtr(SVM::NU),
Ptr< ParamGrid > coeffGrid = SVM::getDefaultGridPtr(SVM::COEF),
Ptr< ParamGrid > degreeGrid = SVM::getDefaultGridPtr(SVM::DEGREE),
bool balanced = false
)
Python:
cv.ml_SVM.trainAuto(samples, layout, responses[, kFold[, Cgrid[, gammaGrid[, pGrid[, nuGrid[, coeffGrid[, degreeGrid[, balanced]]]]]]]]) -> retval
参数:
- samples:训练样本
- layout:参考 ml::SampleTypes,如cv.ml.ROW_SAMPLE表示每个训练样本是行向量,cv.ml.COL_SAMPLE表示每个训练样本是列向量
- responses:与训练样本有关的响应向量
- kFold:k交叉验证,训练集会分成k个子集,从中选取一个用来测试,剩余k-1个用来训练
- balanced:如果设为True且是2-class分类问题,方法会自动创建更平衡的交叉验证子集,即子集中的类之间比例接近整个训练数据集中的比例
预测结果
C++:
virtual float predict(
InputArray samples,
OutputArray results = cv::noArray(),
int flags = 0
) const = 0;
python:
cv.ml_StatModel.predict(samples[, results[, flags]]) ->retval, results
误差计算
对于回归模型,误差计算为 RMS;对于分类器,误差计算为错误分类样本的百分比 (0%-100%)。 C++:
virtual float calcError(
const Ptr<TrainData>& data,
bool test,
cv::OutputArray resp
) const;
python:
cv.ml_StatModel.calcError(data, test[, resp]) ->retval, resp
保存SVM模型
C++:
void cv::Algorithm::save(const String &filename) const
Python:
cv.Algorithm.save(filename) ->None
从文件中加载SVM
C++:
static Ptr<SVM> cv::ml::SVM::load(const String &filepath)
Python:
cv.ml.SVM_load(filepath) ->retval
4. 示例代码
官方示例(python)
构造数据,用来模拟训练集中的两个类别:
from __future__ import print_function
import cv2 as cv
import numpy as np
import random as rng
import time
from matplotlib import pyplot as plt
NTRAINING_SAMPLES = 100
FRAC_LINEAR_SEP = 0.9
WIDTH = 512
HEIGHT = 512
I = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
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,:]
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.0, 0.4 * WIDTH, c.shape)
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
trainClass = trainData[2*NTRAINING_SAMPLES-nLinearSamples:2*NTRAINING_SAMPLES,:]
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.6*WIDTH, WIDTH, c.shape)
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
trainClass = trainData[nLinearSamples:2*NTRAINING_SAMPLES-nLinearSamples,:]
c = trainClass[:,0:1]
c[:] = np.random.uniform(0.4*WIDTH, 0.6*WIDTH, c.shape)
c = trainClass[:,1:2]
c[:] = np.random.uniform(0.0, HEIGHT, c.shape)
labels[0:NTRAINING_SAMPLES,:] = 1
labels[NTRAINING_SAMPLES:2*NTRAINING_SAMPLES,:] = 2
设置SVM参数,初始化模型:
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))
训练SVM:
svm.train(trainData, cv.ml.ROW_SAMPLE, labels)
print('Finished training process')
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
对训练集中两个类别的样本进行可视化:
thick = -1
for i in range(NTRAINING_SAMPLES):
px = trainData[i,0]
py = trainData[i,1]
cv.circle(I, (px, py), 3, (0, 255, 0), thick)
for i in range(NTRAINING_SAMPLES, 2*NTRAINING_SAMPLES):
px = trainData[i,0]
py = trainData[i,1]
cv.circle(I, (px, py), 3, (255, 0, 0), thick)
thick = 2
sv = svm.getUncompressedSupportVectors()
for i in range(sv.shape[0]):
cv.circle(I, (sv[i,0], sv[i,1]), 6, (128, 128, 128), thick)
plt.imshow(I)
推理阶段(C++版本)
void test_svm(std::string videopath, std::string svm_file = "svm.mat")
{
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::load(svm_file);
cv::VideoCapture cap(videopath);
if (cap.isOpened())
{
cv::Mat src;
int sleep_interval = 1;
int frameIdx = 0;
while (true)
{
if (!cap.read(src))
{
break;
}
frameIdx++;
double start = static_cast<double>(cv::getTickCount());
cv::Mat flowFeat;
m_featureExtactor.ProcessFlow(src, flowFeat);
flowFeat.convertTo(flowFeat, CV_32FC1);
int response = (int)svm->predict(flowFeat);
cv::putText(src, cv::String(std::to_string(response)), cv::Point(20,20), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 255, 0));
float times = ((float)cv::getTickCount() - start) / cv::getTickFrequency();
std::cout << "time cost: " << times << " s." << std::endl;
cv::imshow("img", src);
if (cv::waitKey(1) == 27) {
break;
}
}
}
}
5. 小结
本文整理了Opencv中SVM支持向量机的原理、函数和代码示例。
如果对你有帮助的话,欢迎一键三连支持下博主~
|