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官网首页上的截图:

澳门新葡新京 1

  下面,我们看看现在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

澳门新葡新京 2

    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中的精华。

  1. wrapper第二步(DllImport)

  首先,VS新建C#工程,项目类别选择类库,这样最后生成解决方案后,便可以在bin/Debug目录下获得实用的dll文件了。我将项目命名为libsvmSharpCyc。

  其次,添加需要wrapper的C++
dll文件。右键单击解决方案资源管理器中的libsvmSharpCyc,然后添加现有项,把libsvm.dll添加进项目。

  接着,新建类,用于DllImport。我建的是LsInvoke.cs,可以像下图所示这样,把想要使用的函数方法给Import进来:

澳门新葡新京 3

  该过程中,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 = 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中学习到的精髓部分!将在下篇作介绍~

相关文章