LIBSVM工具箱是台湾大学林智仁(C.JLin)等人开发的一套简单的、易于使用的SVM模式识别与回归机软件包,该软件包利用收敛性证明的成果改进算法,取得了很好的结果。下面对LIBSVM(Version 3.25)VS2015中的==分类==使用进行记录。

1. 使用流程

(1). LIBSVM所要求的格式准备数据集(也可以自行准备数据格式,需自己写获取数据的函数,记录中以LIBSVM官方数据的格式读取为例);

(2). 对数据进行简单的缩放操作;

(3). 考虑选用RBF(radial basis function)核参数;

(4). 如果选用RBF,通过采用交叉验证获取最佳参数C与gamma;

(5). 采用最佳参数C与g对整个训练集进行训练获取支持向量机模型

(6). 利用获取的模型进行测试与预测

2. 数据格式介绍

将LIBSVM官网的文件包进行下载,我们在VS中主要用到==svm.h==和==svm.cpp==两个文件,将其放入自己的工程下,在使用时我将有关函数封装为类ClassificationSVM

在LIBSVM中,与读取特征文件相关的类型为svm_problem,其主要在训练和预测过程中记录导入的数据。这个类中有三个元素,如下所示:

1
2
3
4
5
6
struct svm_problem 
{
int n; //记录样本总数
double *y; //记录样本所属类别
struct svm_node **x; //存储所有样本的特征,二维数组,一行存一个样本的所有特征
};

其中svm_node类型的定义如下:

1
2
3
4
5
struct svm_node //用来存储输入空间中的单个特征
{
int index; //该特征在特征空间中的维度编号
double value; //该特征的值
};

这次记录过程中我使用的是官方的vowel数据集,数据集中包含11个分类,每个分类包含48个数据,一共528个数据进行模型训练。官方的数据格式为:

分类号 1:数据1 2:数据2 3:数据3…

从txt文件读取到数组中的函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//从官方文件中读取数据
void ClassificationSVM::readTxt2(const std::string& featureFileName)
{
dataVec.clear();//dataVec为二维数组,对应svm_problem的x中的数据
labels.clear();//labels记录每个数据对应的分类,整型数组
featureDim = -1;//特征数量记录
sampleNum = 0;//样本数

//官方标准样式
std::ifstream fin;
std::string rowData;//一行内容
std::istringstream iss;
fin.open(featureFileName);

//保存特征数据
std::string dataVal;
while (std::getline(fin, rowData))
{
iss.clear();
iss.str(rowData);
bool first = true;
std::vector<double>rowDataVec;
// 逐词读取,遍历每一行中的每个词
while (iss >> dataVal)
{
//第一个数据是label分类标识
if (first) {
first = false;
labels.push_back(atof(dataVal.c_str()));
sampleNum++;
}
else {
//分割字符串得到冒号后数据
for (int k = 0;k < dataVal.size();k++)
{
if (dataVal[k] == ':') {
dataVal = dataVal.substr(k+1);
break;
}
}
rowDataVec.push_back(atof(dataVal.c_str()));
}
}
dataVec.push_back(rowDataVec);
}
featureDim = dataVec[0].size();
}

3. 数据缩放

缩放的主要优点是避免了较大数值范围内的属性,而支配了较小数值范围内的属性。另一个优点是在计算过程中避免了数值上的困难。缩放输入数据,原始数据范围可能过大或过小,该过程可将数据重新缩放到适当范围使训练与预测速度更快,一般缩放到[0, 1]或[-1, 1],这里我缩放到[-1, 1],缩放公式如下:

y= lower +( upper  lower )yminmaxminy^{\prime}=\text { lower }+(\text { upper }-\text { lower }) * \frac{y-\min }{\max -\min }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//归一化到[-1, 1],分为训练时缩放,要写一个缩放的文件;预测时读取这个缩放文件
void ClassificationSVM::svmScale(bool train_model)
{
double *minVals = new double[featureDim];
double *maxVals = new double[featureDim];

if (train_model) {
for (int i = 0;i < featureDim;i++)
{
minVals[i] = dataVec[0][i];
maxVals[i] = dataVec[0][i];
}

for (int i = 0;i < dataVec.size();i++)
{
for (int j = 0;j < dataVec[i].size();j++)
{
if (dataVec[i][j] < minVals[j])
minVals[j] = dataVec[i][j];
if (dataVec[i][j] > maxVals[j])
maxVals[j] = dataVec[i][j];
}
}

//缩放文件存放每个特征的最大最小值
std::ofstream out("scale_params.txt");
for (int i = 0;i < featureDim;i++)
{
out << minVals[i] << " ";
}
out << std::endl;
for (int i = 0;i < featureDim;i++)
{
out << maxVals[i] << " ";
}
}
else {
std::ifstream fin;
std::string rowData;//一行内容
std::istringstream iss;
fin.open("scale_params.txt");
std::getline(fin, rowData);
iss.clear();
iss.str(rowData);
double dataVal;
int count = 0;
// 逐词读取,遍历每一行中的每个词
while (iss >> dataVal)
{
minVals[count] = dataVal;
count++;
}
count = 0;
std::getline(fin, rowData);
iss.clear();
iss.str(rowData);
while (iss >> dataVal)
{
maxVals[count] = dataVal;
count++;
}
}

for (int i = 0;i < dataVec.size();i++)
{
for (int j = 0;j < dataVec[i].size();j++)
{
dataVec[i][j] = -1 + 2 * (dataVec[i][j] - minVals[j]) / (maxVals[j] - minVals[j]);
}
}

delete minVals;
delete maxVals;
}

缩放之后我们就可以将导入的参数dataVec和labels构造到官方的结构svm_problem中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//设置prob,prob的定义为:svm_problem prob;
prob.l = sampleNum; //训练样本数
prob.x = new svm_node*[sampleNum]; //特征矩阵
prob.y = new double[sampleNum]; //标签矩阵
for (int i = 0; i < sampleNum; ++i)
{
prob.x[i] = new svm_node[featureDim + 1]; //
for (int j = 0; j < featureDim; ++j)
{
prob.x[i][j].index = j + 1;
prob.x[i][j].value = dataVec[i][j];
}
prob.x[i][featureDim].index = -1;
prob.y[i] = labels[i];
}

4. 交叉验证

交叉验证(Cross Validation)是用来验证分类器的性能一种统计分析方法,基本思想是把在某种意义下将原始数据(dataset)进行分组,一部分做为训练集(train set),另一部分做为验证集(validation set),首先用训练集对分类器进行训练,在利用验证集来测试训练得到的模型(model),以此来做为评价分类器的性能指标。

这里主要使用K折交叉验证(一般选择5折)去得到最合理的模型中的C和gamma(模型的参数列表如下),官方的tools文件夹下grid.py就是在求解最优化的参数,定义的参数范围时-5 <= log~2~C <= 15,-15 <= log~2~G <= 3,步长均为2,在C++中我们需要调用svm.cpp中的svm_cross_validation函数遍历参数来进行交叉验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct svm_parameter
{
int svm_type;
int kernel_type;
int degree; /* for poly */
double gamma; /* for poly/rbf/sigmoid */
double coef0; /* for poly/sigmoid */

/* these are for training only */
double cache_size; /* in MB */
double eps; /* stopping criteria */
double C; /* for C_SVC, EPSILON_SVR and NU_SVR */
int nr_weight; /* for C_SVC */
int *weight_label; /* for C_SVC */
double* weight; /* for C_SVC */
double nu; /* for NU_SVC, ONE_CLASS, and NU_SVR */
double p; /* for EPSILON_SVR */
int shrinking; /* use the shrinking heuristics */
int probability; /* do probability estimates */
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//交叉验证最优化参数求解
double* target = new double[prob.l];
int logG, logC;
int bestG, bestC;//记录最好的参数值
int minCount = prob.l;//记录错误的数量
std::vector<double>rates;//记录每次组合对应的正确率
for (logC = -5;logC <= 15;logC += 2)
{
for (logG = -15;logG <= 3;logG += 2)
{
double c = pow(2, logC);
double g = pow(2, logG);
setParam(c, g);//对模型参数进行修改
svm_cross_validation(&prob, &param, 5, target);
int count = 0;
for (int i = 0;i < prob.l;i++)
{
if (target[i] != i % 11)
count++;
}
if (count < minCount) {
minCount = count;
bestC = c;
bestG = g;
}
rates.push_back(1.0*(prob.l-count) / prob.l*100);
}
}
//输出每对参数及对应概率
std::ofstream out("rates.txt");
int count1 = 0;
for (logC = -5;logC <= 15;logC += 2)
{
for (logG = -15;logG <= 3;logG += 2)
{
std::string s1 = "log2c=";
s1 += std::to_string(logC);
std::string s2 = "log2g=";
s2 += std::to_string(logG);
std::string s3 = "rate=";
s3 += std::to_string(rates[count1]);
count1++;

out << s1 << " " << s2 << " " << s3 << std::endl;
}
}

5. 模型训练

模型训练主要调用官方的svm_train函数。

1
2
3
4
5
6
std::cout << "start training" << std::endl;
svm_model *svmModel = svm_train(&prob, &param);

std::cout << "save model" << std::endl;
svm_save_model(modelFileName.c_str(), svmModel);
std::cout << "done!" << std::endl;

训练完成后将在指定的位置modelFileName生成对应的模型文件。

6. 模型测试(预测)

模型的测试使用svm_predict函数(或svm_predict_probability函数),流程与上面类似,将文件导入并缩放后,无需交叉验证直接使用导入的模型进行预测得到预测的结果,返回值类型为double是因为该函数包含分类和回归两个方面,我们在回归时返回的其实就是我们输入的labels中的一个分类的分类号(int型数据)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void ClassificationSVM::predict(const std::string& featureFileName, const std::string& modelFileName)
{
//读取特征文件中的特征及保存模型
svm_model *model = svm_load_model(modelFileName.c_str());
readTxt2(featureFileName);
svmScale(false);

//从vector中构造prob
int count = 0;//正确预测计数
for (int i = 0;i < dataVec.size();i++)
{
svm_node *sample = new svm_node[featureDim + 1];
for (int j = 0; j < featureDim; ++j)
{
sample[j].index = j + 1;
sample[j].value = dataVec[i][j];
}
sample[featureDim].index = -1;

//double *probresut = new double[11];
//double resultLabel = svm_predict_probability(model, sample, probresut);
double resultLabel2 = svm_predict(model, sample);
if (resultLabel - labels[i] < 1e-5)
count++;
//std::cout << resultLabel2 << std::endl;
}
double possibility = 1.0* count / dataVec.size();//正确率
}

7. 参考及链接

一个入门的DEMO

主要参考,但这个里面没有交叉验证

我的完整程序