libsvm代码阅读(1):基础准备与svm.h头文件(转)
libsvm是国立台湾大学Chih-Jen Lin开发的一个SVM的函数库,是当前应用最广泛的svm函数库,从2000年到2010年,该函数库的下载量达到250000之多。它的最新版本是version 3.17,主要是对是svm_group_classes做了修改。
主页:LIBSVM -- A Library for Support Vector Machines
我下载后的解压文件如下所示:
libsvm函数包的组织结构如下:
1、主文件路径:包含了核心的C/C++程序和例子数据。其中svm.cpp是svm的核心程序,它实现了svm的训练和测试算法。
2、tool子文件路径:包含了一些检验数据格式以及选择svm参数的tool。
3、其他子文件路径:主要包含pre-built 二值文件和相关语言的接口。
关于libsvm的官方实现文档:LIBSVM: A Library for Support Vector Machines,本相关博文也是以这篇文档为基础的,同时本相关博文还参考了上海交大模式分析与机器智能实验室的libsvm2.6代码注释。
关于svm的理论知识,本文不想多说,如果你还不是很懂,可以参考我前面的两篇文章:
=======================================================
现在开始看一看libsvm的头文件svm.h
svm.h中主要是定义了4个结构体,分别是svm_node、svm_problem、svm_parameter、svm_model,然后就是19个函数的声明,函数的声明我就不先讲了,等到在svm.cpp中碰到后再细说。下面来看一看这几个结构体:
- struct svm_node
- {
- int index;
- double value;
- };
这个结构体用于存储单一向量中的单个特征。例如:向量x1={0.002,0.345,4.000,5.677},则用svm_node来存储就是使用一个包含5个svm_node的数组来存储这个4维向量,内存中的表示如下:
- struct svm_problem
- {
- int l; //记录样本总数
- double *y;//指向样本所属类别的数组
- struct svm_node **x;//指向一个存储内容为指针的数组
- };
这个结构体用于存储本次参加运算的所有样本及其所属类别,一个示意图如下(其中最右边的4个长方格如同上图中的表格):
- struct svm_parameter
- {
- int svm_type;//SVM类型,见前enum
- 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 */
- };
这个结构体用于存储svm的各个参数,知道就行。
- struct svm_model
- {
- struct svm_parameter param; /* parameter */
- int nr_class; /* number of classes, = 2 in regression/one class svm */
- int l; /* total #SV */
- struct svm_node **SV; /* SVs (SV[l]) */
- double **sv_coef; /* coefficients for SVs in decision functions (sv_coef[k-1][l]) */
- double *rho; /* constants in decision functions (rho[k*(k-1)/2]) */
- double *probA; /* pariwise probability information */
- double *probB;
- int *sv_indices; /* sv_indices[0,...,nSV-1] are values in [1,...,num_traning_data] to indicate SVs in the training set */
- /* for classification only */
- int *label; /* label of each class (label[k]) */
- int *nSV; /* number of SVs for each class (nSV[k]) */
- /* nSV[0] + nSV[1] + ... + nSV[k-1] = l */
- /* XXX */
- int free_sv; /* 1 if svm_model is created by svm_load_model*/
- /* 0 if svm_model is created by svm_train */
- };
这个函数体用于保存训练后的训练模型model,在predict中要用到。
=======================================
关于extern “C”的解析
在阅读svm.h时发现了如下一段代码:
- #ifndef _LIBSVM_H
- #define _LIBSVM_H
- #define LIBSVM_VERSION 317
- #ifdef __cplusplus
- extern "C" {
- #endif
显然,前3行代码是为了防止头文件被重复引用,那么5-6行是什么意思呢?
某企业曾经给出如下的一道面试题:为什么标准头文件都有类似以下的结构?
- #ifndef __INCvxWorksh
- #define __INCvxWorksh
- #ifdef __cplusplus
- extern "C" {
- #endif
- /*...*/
- #ifdef __cplusplus
- }
- #endif
- #endif /* __INCvxWorksh */
分析
显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。
那么
- #ifdef __cplusplus
- extern "C" {
- #endif
- #ifdef __cplusplus
- }
- #endif
的作用又是什么呢?
extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。
被extern "C"限定的函数或变量是extern类型的;extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:extern int a;仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
被extern "C"修饰的变量和函数是按照C语言方式编译和连接的;
未加extern “C”声明时的编译方式
首先看看C++中对类似C的函数是怎样编译的。作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
- void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
未加extern "C"声明时的连接方式
假设在C++中,模块A的头文件如下:
- // 模块A头文件 moduleA.h
- #ifndef MODULE_A_H
- #define MODULE_A_H
- int foo( int x, int y );
- #endif
在模块B中引用该函数:
- // 模块B实现文件 moduleB.cpp
- #include "moduleA.h"
- foo(2,3);
实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!
加extern "C"声明后的编译和连接方式
加extern "C"声明后,模块A的头文件变为:
- // 模块A头文件 moduleA.h
- #ifndef MODULE_A_H
- #define MODULE_A_H
- extern "C" int foo( int x, int y );
- #endif
在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
(1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;
(2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。
所以,可以用一句话概括extern “C”这个声明的真实目的:实现C++与C及其它语言的混合编程。
extern "C"的惯用法
(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
- extern "C"
- {
- #include "cExample.h"
- }
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:
- /* c语言头文件:cExample.h */
- #ifndef C_EXAMPLE_H
- #define C_EXAMPLE_H
- extern int add(int x,int y);
- #endif
- /* c语言实现文件:cExample.c */
- #include "cExample.h"
- int add( int x, int y )
- {
- return x + y;
- }
- // c++实现文件,调用add:cppFile.cpp
- extern "C"
- {
- #include "cExample.h"
- }
- int main(int argc, char* argv[])
- {
- add(2,3);
- return 0;
- }
如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。
(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:
- //C++头文件 cppExample.h
- #ifndef CPP_EXAMPLE_H
- #define CPP_EXAMPLE_H
- extern "C" int add( int x, int y );
- #endif
- //C++实现文件 cppExample.cpp
- #include "cppExample.h"
- int add( int x, int y )
- {
- return x + y;
- }
- /* C实现文件 cFile.c
- /* 这样会编译出错:#include "cExample.h" */
- extern int add( int x, int y );
- int main( int argc, char* argv[] )
- {
- add( 2, 3 );
- return 0;
- }