dlib是一个用现代C++技术编写的跨平台的通用算法库,应用dlib算法库可以快速地将算法应用到某一方面或某一研究领域。虽然dilb库中每个类和每个函数都有详细的文档,并且提供了大量的示例代码,但是由于dlib库太过庞大而且网上关于dlib库的介绍相对较少,这样还是给初学者造成了一些困难。本文就对dlib库中C-SVM的实现算法svm_c_ex.cpp进行详细地介绍。不足之处希望各位读者加以指正。
/*
说明Dlib库中SVM的作用,其中SVM是利用c语言来实现的。
首先建立训练集,然后说明如何利用交叉验证(cross validation)和SVM的训练函数来寻找一个最优的决策函数。
这个决策函数可以对我们的数据集进行分类。
数据集是2维的,并将分布在离原点距离不小于10点店其中一部分标签是+1,另外一部分数据标签是-1.
*/
#include <iostream>
#include <dlib/svm.h>
using namespace std;
using namespace dlib;//两个命名空间
int main()
{
/*SVM函数由列向量组成,它包含了许多数据。所以第一件事就是声明一个typedef*/
/*typedef int size,typedef为现有类型创建同义字。那么以后size就是int基本数据类型*/
/*这个矩阵就包含了我们所有的2维样本点。如果你想得到更多的特征,那么将2改变即可;
如果你不知道需要多少个特征,那么可以将2置为0,然后用matrix.set_size()即可设置行数*/
typedef matrix<double, 2, 1> sample_type;
/*typedef声明了文件中会用到的kernel类型。
选择径向基核函数(radial basis kernel)可以处理我们的2D样本。
也可以利用自定义的核函数with tools即可*/
typedef radial_basis_kernel<sample_type> kernel_type;
/*声明向量容器samples和labels,来存放样本和标签*/
std::vector<sample_type> samples;
std::vector<double> labels;
/*给samples和labels赋值。赋值是通过循环遍历这些点以及根据他们和原点的距离来标记他们。*/
for (int r = -20; r <= 20; ++r)
{
for (int c = -20; c <= 20; ++c)
{
sample_type samp;//利用sample_type声明一个2行1列的矩阵samp
samp(0) = r;
samp(1) = c;//给samp矩阵赋值
samples.push_back(samp);//将samp矩阵赋值给容器samples
// if this point is less than 10 from the origin
if (sqrt((double)r*r + c*c) <= 10)//sqrt((double)r*r + c*c)就是2D的点和原点(0,0)的距离
labels.push_back(+1);//类+1
else
labels.push_back(-1);//类-1.给labels容器赋值,push_back
}
}
/*
横坐标从-20到+20,纵坐标从-20到+20
例如从-1到+1
(-1,1) (0,1) (1,1)
(-1,0) (0,0) (1,0)
(-1,-1) (0,-1) (1,-1)
这些点就是样本点,然后和原点距离<10的为+1,否则为-1
*/
/*我们再通过减去他们的平均值和分裂他们的标准偏差来归一化样本点。这是极好的,因为it often heads off numerical stability problems and
also prevents one large feature from smothering others。这样做不会有太大影响,简单实现如下:*/
vector_normalizer<sample_type> normalizer;//归一化类型的容器normalizer
/*利用这些归一化的点训练样本点的平均值和标准差*/
normalizer.train(samples);
// now normalize each sample归一化每一个样本: samples.size()为样本大小
for (unsigned long i = 0; i < samples.size(); ++i)
samples[i] = normalizer(samples[i]);
/*现在我们就有训练集了。但是,这有两个需要训练的参数-c和gamma参数。我们的选择将会影响决策函数的好坏。
我们可以利用cross_validate_trainer() 函数在数据集上执行n重交叉验证来检验选择参数的好坏。
但是,以这样的方式将会有一个问题,即我们需要定义样本的次序。
因此,前一半样本可以看做它们和后一半的分布不同。
这将会搞砸交叉验证过程,但我们可以随机样本的顺序来修复交叉验证过程:*/
/*这个gamma是高斯核中的sigma*/
randomize_samples(samples, labels);//随机化样本的标签的顺序
/*这里我们用核函数类型kernel_type定义svm_c_trainer的对象trainer*/
svm_c_trainer<kernel_type> trainer;
/*现在我们对C和gamma的值进行循环,观察哪个C和gamma是好的。
寻找一些可用的C和gamma参数的组合是很简单的。
你也可以参考model_selection_ex.cpp,为确定更好的参数选择而进行的复杂的操作*/
cout << "doing cross validation" << endl;
//交叉验证
for (double gamma = 0.00001; gamma <= 1; gamma *= 5)
{
for (double C = 1; C < 100000; C *= 5)//gamma=0.00001:1,每次乘5;C=1:100000,每次乘5
{
trainer.set_kernel(kernel_type(gamma));
trainer.set_c(C);//告诉训练器,哪些C和gamma参数我们将使用
cout << "gamma: " << gamma << " C: " << C;
/*用当前的C和gamma打印3重交叉验证。
cross_validate_trainer()将返回一个行向量。向量的第一个元素是标签是+1的正确分类的样本,第二个元素是标签为-1的。*/
cout << " cross validation accuracy: "
<< cross_validate_trainer(trainer, samples, labels, 3);//3重交叉验证
}
}
/*通过观察上述循环输出,可以发现C和gamma最好的值是5和0.15625。*/
/*现在我们训练整个数据集来获得决策函数。
决策函数当在标签为+1的样本返回>=0的数,在标签为-1的时候返回<0的数。*/
trainer.set_kernel(kernel_type(0.15625));
trainer.set_c(5);//将分类器的c和gamma定位5和0.15625
typedef decision_function<kernel_type> dec_funct_type;
typedef normalized_function<dec_funct_type> funct_type;//funct_type是一种自定义类型
/*现在我们在定义一个归一化目标函数。
随着决策函数的不断变化,这给我们存储向量的正规化信息提供了一种方便的方法。*/
funct_type learned_function;
learned_function.normalizer = normalizer; // save normalization information存储归一化信息
learned_function.function = trainer.train(samples, labels); // perform the actual SVM training and save the results
/*打印决策函数中的支持向量的个数(basis_vectors)*/
cout << "\nnumber of support vectors in our learned_function is "
<< learned_function.function.basis_vectors.size() << endl;
//验证阶段,利用决策函数(learned_function)来对一些新的数据进行分类
sample_type sample;//sample是2行1列的矩阵
sample(0) = 3.123;
sample(1) = 2;//新的数据(2D的点)。类为+1,则决策函数输出值>=0. 类为-1,决策函数值为<0。
//原点距离<10的为+1,否则为-1
cout << "This is a +1 class example, the classifier output is " << learned_function(sample) << endl;
sample(0) = 3.123;
sample(1) = 9.3545;
cout << "This is a +1 class example, the classifier output is " << learned_function(sample) << endl;
sample(0) = 13.123;
sample(1) = 9.3545;
cout << "This is a -1 class example, the classifier output is " << learned_function(sample) << endl;
sample(0) = 13.123;
sample(1) = 0;
cout << "This is a -1 class example, the classifier output is " << learned_function(sample) << endl;
/*我们可以训练一个更好的决策函数,实现更好的预测功能,不仅仅是看输出值和0的大小
输出类别是+1和-1的概率是多少*/
typedef probabilistic_decision_function<kernel_type> probabilistic_funct_type;
typedef normalized_function<probabilistic_funct_type> pfunct_type;
pfunct_type learned_pfunct;//现在的决策函数是learned_pfunct
learned_pfunct.normalizer = normalizer;
learned_pfunct.function = train_probabilistic_decision_function(trainer, samples, labels, 3);
//利用函数learned_pfunct类输出类别是+1和-1时的概率是多少。
//+1的概率将很高,-1的概率将很低才是正确的分类
// 打印决策函数的支持向量的个数,和learned_function的支持向量的个数一定相同
cout << "\nnumber of support vectors in our learned_pfunct is "
<< learned_pfunct.function.decision_funct.basis_vectors.size() << endl;
sample(0) = 3.123;
sample(1) = 2;
cout << "This +1 class example should have high probability. Its probability is: "
<< learned_pfunct(sample) << endl;
sample(0) = 3.123;
sample(1) = 9.3545;
cout << "This +1 class example should have high probability. Its probability is: "
<< learned_pfunct(sample) << endl;
sample(0) = 13.123;
sample(1) = 9.3545;
cout << "This -1 class example should have low probability. Its probability is: "
<< learned_pfunct(sample) << endl;
sample(0) = 13.123;
sample(1) = 0;
cout << "This -1 class example should have low probability. Its probability is: "
<< learned_pfunct(sample) << endl;
/*另外,我们需要知道的是在dlib中,everything都是序列化的。
所以,我们可以将learned_pfunct决策函数存在disk中,也可以从disk中读取。*/
serialize("saved_function.dat") << learned_pfunct;//serialize序列化saved_function.dat,<<learned_pfunct
//那么learned_pfunct决策函数就存在中了
/*打开文件,加载文件中的目标函数*/
deserialize("saved_function.dat") >> learned_pfunct;//deserialize,反序列化saved_function.dat,>>learned_pfunct
/*在dlib库中有一个示例程序,file_to_code_ex.cpp。这是一个简单的程序:将一个文件或者输出变成一串的C++代码,
这些C++代码也可以以一个标准的字符串对象形式重新生成文件目录。所以std::istringstream来保存决策函数*/
/*训练完毕的决策函数有超过200个支持向量。SVM通常上会寻找那么包含大量支持向量的决策函数。
决策函数中的支持向量越多,对一个新的样本分类需要的时间就更长。
应用dlib库可以利用更少的支持向量来寻找正常输出的一个近似。*/
/*现在我们允许决策函数只有10个支持向量,当做交叉验证时。我们使用reduced2()函数来实现这一功能。
reduced2()需要的参数有训练器和需要的支持向量数。返回一个新的训练器。
这个新的训练器在决策函数进行和预测时会起到正要作用。*/
cout << "\ncross validation accuracy with only 10 support vectors: "
<< cross_validate_trainer(reduced2(trainer,10), samples, labels, 3);
//打印最初交叉验证的比较得分
cout << "cross validation accuracy with all the original support vectors: "
<< cross_validate_trainer(trainer, samples, labels, 3);
/*当你运行这个程序时,你可以重新设置支持向量的个数<10,破坏交叉验证的准确性*/
//新的决策函数可以通过以下得到:
learned_function.function = reduced2(trainer,10).train(samples, labels);
//同样可以得到输出类别概率的函数
learned_pfunct.function = train_probabilistic_decision_function(reduced2(trainer,10), samples, labels, 3);
}