首页 > 代码库 > LibSVM之C# Wrapper
LibSVM之C# Wrapper
【百度百科】
LIBSVM是台湾大学林智仁(Lin Chih-Jen)教授等开发设计的一个简单、易于使用和快速有效的SVM模式识别与回归的软件包,他不但提供了编译好的可在Windows系列系统的执行文件,还提供了源代码,方便改进、修改以及在其它操作系统上应用;该软件对SVM所涉及的参数调节相对比较少,提供了很多的默认参数,利用这些默认参数可以解决很多问题;并提供了交互检验(Cross Validation)的功能。该软件可以解决C-SVM、ν-SVM、ε-SVR和ν-SVR等问题,包括基于一对一算法的多类模式识别问题……
如果你对libsvm还不够了解,建议先浏览下百度百科等对libsvm的介绍~
【C# Wrapper 动机】
参与过一个项目,使用IDE是VS winform,工具包为EmguCV 2.4.10。我们知道OpenCV2中的svm部分是根据libsvm-2.6编写的,该版本的libsvm已经能够estimate预测概率了(libsvm首页的change log中有详细说明),但是OpenCV却舍弃了predictProbability。在具体的项目中,如果可以获得预测概率信息,那将对提高识别性有很大的帮助。然而,opencv2舍弃了识别概率,包括opencv3,我看源代码的svm部分也是基于libsvm-2.6修改的,也没有引进predictProbability。
因而,在EmguCV的ML满足不了的情况下,萌生了两个想法:
一是修改OpenCV代码,然后重新CMake得到cvextern.dll;
二是直接找其它的svm库。
首先尝试CMake。像OpenCV这样的大项目,CMake起来确实不容易,更何况是从零开始学CMake。在时间不允许的条件下,只得走第二条路。找到libsvmSharp后,我如获至宝。但是,很快我又再度失望了,因为实时性要求满足不了(EmguCV自带SVM可以在5ms内完成识别预测,而libsvmSharp需要500ms)。
这是为什么?
同样是C#对C++的wrapper,同样都是基于libsvm,同样是对C++所编译的dll的引用,效率竟相差百倍!本着一颗学习的心,我决定一探究竟……
【现有libsvm的C#/.Net版本】
目前,LIBSVM拥有C、Java、Matlab、C#、Ruby、Python、R、Perl、Common LISP、Labview等数十种语言版本。最常使用的是C、Matlab、Java和命令行(c语言编译的工具)的版本。
首先我们看张libsvm官网首页上的截图:
下面,我们看看现在libsvm有哪些C#版本:
1、SVM.NET by Matthewa Johnson
2009年,剑桥大学的Matthewa Johnson博士将SVM.NET更新到了V2.89,也就是现在的最新版本。无奈现在不FQ竟已经找不到SVM.NET的原生版了。这份神秘感使我觉得,这个C#版本的libsvm应该是质量最高的。
后人有在V2.89的基础上做一些修改,提出了:SVM.NET with Parallel Optimization。相关描述为:When finding parameters, C and Gamma, in Grid-search algorithm using ParameterSelection.PGrid instead of the original ParameterSelection.Grid will increase the calculation speed.
2、NSVM by Joannes
3年时间,却只有2下载量,何其惨淡……好吧,或许你也像我一样主观臆断了。
3、KMLib(Kernel Machine Library with GPU SVM solver in .Net) by Krzysztof Sopy?a
Key Features
-
- .Net implementation
- Parallel kernel implementation
- SVM CUDA acceleration – kernels and solver
- CUDA SVM with sparse data formats: CSR, Ellpack-R, Sliced-Ellpack
- For non commercial and academic use: Free MIT license when use please cite: Bibtex CUDA SVM CSR
另外,还有一点需要强调的是,它是基于libsvm的java版本转换过来的。也正因如此,我感觉用起来可能会有点麻烦,故没有选择。
4、libsvmSharp by ccerhan
选择它的理由很简单,有一定的下载量(从众心理又开始作祟了!)下载方便,用VS的Nuget package,通过命令“PM> Install-Package LibSVMsharp”即下载到本地。
5、libsvm-net by Nicolas Panel
下载起来同样十分方便: NuGet package : PM> Install-Package libsvm.net,比起libsvmSharp有更高的人气。
【分析libsvmSharp】
为什么libsvmSharp.dll如此低效?
在反编译后的源代码中(稍后将介绍如何反编译C#编译出来的dll文件),我们可以看到libsvmSharp所用的数据结构有:
1、struct:svm_node、svm_model、svm_problem、svm_parameter;
2、calss:SVMNode、SVMModel、SVMProblem、SVMParameter。
实际上,结构体能做的事情,类完全也能做,似乎结构体没有存在的必要。
而且,可以看到各类的实现中,有很多“结构体=>类”、“指针=>结构体”、“类=>指针”等这样的类型转换。我们知道,C#要引用C++所编译的dll,用得最多的就是IntPtr这个数据结构。而libsvmSharp低效的原因,也正在于对指针的处理策略选取不当,它只在需要传指针的时候,硬生生地用Marshal类重新在内存中开辟当前数据结构大小的区域,并返回指针,美其名曰convert到指针。这种方式,无论是在时间上还是空间上,都有太多没必要的浪费。
这里我们用libsvm中的svm_predict作为例子来讲解。
在libsvm.dll(该dll由C++编译得到)中,函数为:
double svm_predict(const svm_model *model, const svm_node *x)
在libsvmSharp.dll(该dll由C#编译得到)中,我们这样声明它:
[DllImport("libsvm.dll", CallingConvention = CallingConvention.Cdecl)] public static extern double svm_predict(IntPtr model, IntPtr x);
DllImport时,更多关于C++数据结构到C#数据结构的信息请读者查阅资料获得。由上可见,IntPtr是个很关键的数据结构,由它声明的变量实际上是一个指针值(即内存地址值)。第一个参数IntPtr model,要求传入model所在内存区域的地址,第二个参数IntPtr x,要求传入特征节点数组所在内存区域的地址。下面,我们看看libsvmSharp是怎么使用这个函数的:
1 public static double Predict(SVMModel model, SVMNode[] x) 2 { 3 if (model == null) 4 { 5 throw new ArgumentNullException("model"); 6 } 7 if (x == null) 8 { 9 throw new ArgumentNullException("x"); 10 } 11 IntPtr intPtr = SVMModel.Allocate(model); 12 double result = SVM.Predict(intPtr, x); 13 SVMModel.Free(intPtr); 14 return result; 15 } 16 17 public static double Predict(IntPtr ptr_model, SVMNode[] x) 18 { 19 if (ptr_model == IntPtr.Zero) 20 { 21 throw new ArgumentNullException("ptr_model"); 22 } 23 if (x == null) 24 { 25 throw new ArgumentNullException("x"); 26 } 27 List<SVMNode> list = (from a in x 28 select a.Clone()).ToList<SVMNode>(); 29 list.Add(new SVMNode(-1, 0.0)); 30 IntPtr intPtr = SVMNode.Allocate(list.ToArray()); 31 double result = libsvm.svm_predict(ptr_model, intPtr); 32 SVMNode.Free(intPtr); 33 return result; 34 }
细心的你有没有发现什么问题?看不懂?毕竟我是断章取义。然而,请看第11行,每次调用都要重新给model分配内存哦!再如,第27、28、29、30行,在熟悉C++的人看来,that‘s what?参数传进来的可不是数组名吗,干嘛如此大费周章?内存不会被玩坏吗?
一切都是因为C#有指针,但不是那个我们所熟悉的指针。C#没有像Java一样完全摈弃指针,但为了代码安全考虑而弱化指针。C#是面向对象的语言,里面任何一种数据结构都没有指针这一属性,除非你自己在定义数据结构时,将指针作为成员变量。我们所熟悉的EmguCV就是这么实现对OpenCV的wrapper的。
【开始libsvm的C# Wrapper之旅】
很好,我们可以进入正题了。我将以wrapper libsvm为例,分步骤讲解整个过程。读者可以举一反三,希望本文可以帮助你加深你对跨语言编程的理解。
1.wrapper第一步(准备)
获取你要wrapper的dll(由C++编译得到),最好有源代码,当然有参考手册也可以,但是如果除了dll的名字,对该dll一无所知,那或许就无能为力了。
安装C#的dll反编译工具,这里推荐ILSpy。为什么要安装?比起自己黑暗中摸索,如果有可以参考借鉴的资源,视而不见是多么可惜的一件事啊。EmguCV真的称得上wrapper中的精华。
2. wrapper第二步(DllImport)
首先,VS新建C#工程,项目类别选择类库,这样最后生成解决方案后,便可以在bin/Debug目录下获得实用的dll文件了。我将项目命名为libsvmSharpCyc。
其次,添加需要wrapper的C++ dll文件。右键单击解决方案资源管理器中的libsvmSharpCyc,然后添加现有项,把libsvm.dll添加进项目。
接着,新建类,用于DllImport。我建的是LsInvoke.cs,可以像下图所示这样,把想要使用的函数方法给Import进来:
该过程中,DllImport要如何使用,感兴趣的读者可自行学习,这里需要注意的是C++函数中的数据结构到C#中的数据结构是有映射关系的,下面附上一张dll引用常用转化表:
C++ C# ===================================== WORD ushort DWORD uint UCHAR int/byte 大部分情况都可以使用int代替,而如果需要严格对齐的话则应该用bytebyte UCHAR* string/IntPtr unsigned char* [MarshalAs(UnmanagedType.LPArray)]byte[]/?(Intptr) char* string LPCTSTR string LPTSTR [MarshalAs(UnmanagedType.LPTStr)] string long int ulong uint Handle IntPtr HWND IntPtr void* IntPtr int int int* ref int *int IntPtr unsigned int uint COLORREF uint
3、wrapper第三步(数据结构)
这一步是最为关键的一步,在C#中新建数据结构,必须要与C++中的数据结构相一致,否则碰到无法预料的问题。
前文已经简单地介绍过libsvm的数据结构了。这里重复一下:
1 struct svm_node 2 { 3 int index; 4 double value; 5 }; 6 7 struct svm_problem 8 { 9 int l; 10 double *y; 11 struct svm_node **x; 12 }; 13 14 enum { C_SVC, NU_SVC, ONE_CLASS, EPSILON_SVR, NU_SVR }; /* svm_type */ 15 enum { LINEAR, POLY, RBF, SIGMOID, PRECOMPUTED }; /* kernel_type */ 16 17 struct svm_parameter 18 { 19 int svm_type; 20 int kernel_type; 21 int degree; /* for poly */ 22 double gamma; /* for poly/rbf/sigmoid */ 23 double coef0; /* for poly/sigmoid */ 24 25 /* these are for training only */ 26 double cache_size; /* in MB */ 27 double eps; /* stopping criteria */ 28 double C; /* for C_SVC, EPSILON_SVR and NU_SVR */ 29 int nr_weight; /* for C_SVC */ 30 int *weight_label; /* for C_SVC */ 31 double* weight; /* for C_SVC */ 32 double nu; /* for NU_SVC, ONE_CLASS, and NU_SVR */ 33 double p; /* for EPSILON_SVR */ 34 int shrinking; /* use the shrinking heuristics */ 35 int probability; /* do probability estimates */ 36 }; 37 38 // 39 // svm_model 40 // 41 struct svm_model 42 { 43 struct svm_parameter param; /* parameter */ 44 int nr_class; /* number of classes, = 2 in regression/one class svm */ 45 int l; /* total #SV */ 46 struct svm_node **SV; /* SVs (SV[l]) */ 47 double **sv_coef; /* coefficients for SVs in decision functions (sv_coef[k-1][l]) */ 48 double *rho; /* constants in decision functions (rho[k*(k-1)/2]) */ 49 double *probA; /* pariwise probability information */ 50 double *probB; 51 int *sv_indices; /* sv_indices[0,...,nSV-1] are values in [1,...,num_traning_data] to indicate SVs in the training set */ 52 53 /* for classification only */ 54 55 int *label; /* label of each class (label[k]) */ 56 int *nSV; /* number of SVs for each class (nSV[k]) */ 57 /* nSV[0] + nSV[1] + ... + nSV[k-1] = l */ 58 /* XXX */ 59 int free_sv; /* 1 if svm_model is created by svm_load_model*/ 60 /* 0 if svm_model is created by svm_train */ 61 };
对应地,我们在C#中建立数据结构:
public struct svm_node { /// <summary> /// 索引 /// </summary> public int index; /// <summary> /// 值 /// </summary> public double value; /// <summary> /// 构造函数 /// </summary> /// <param name="i"></param> /// <param name="v"></param> public svm_node(int i,double v) { this.index = i; this.value =http://www.mamicode.com/ v; } public bool Equals(svm_node x) { return this.index.Equals(x.index) && this.value.Equals(x.value); } } public struct svm_problem { /// <summary> /// 支持向量个数 /// </summary> public int l; /// <summary> /// 标签值 /// </summary> public IntPtr y; /// <summary> /// 节点情况 /// </summary> public IntPtr x; } ……
可能有读者会问,结构体你加构造函数和其它函数干嘛?这其实是为了日后好简化代码。否则,每次对象创建于赋值分开操作有点麻烦。
进行到现在,我们只是完成了数据结构搭建的一小部分,下面是从EmguCV中学习到的精髓部分!将在下篇作介绍~
LibSVM之C# Wrapper