前言: 对于 SVM 的了解, 看前辈写的博客加上读论文对于 SVM 的皮毛知识总算有点了解, 比如线性分类器, 和求凸二次规划中用到的高等数学知识. 然而 SVM 最核心的地方应该在于核函数和求关于α函数的极值的方法: SMO 算法(当然还有很多别的算法. libsvm 使用的是 SMO,SMO 算法也是最高效和简单的), 还有松弛变量.. 毕设答辩在即, 这两个难点只能拖到后面慢慢去研究了.
于是我便是用了 LibSvm, 也就是台湾大学某某教授写的一个专门用于 svm 的工具包, 其中有 java 语言的, python 语言的, c 语言的. 我只拿了其中的两个文件 svm.cpp 和 svm.h , 这两个 c 语言的头文件和源文件已经可以直接拿来训练模型和预判分类了. 这篇博客也只是照葫芦画瓢, 利用已经写好的 libsvm, 做一个基于. net core 的 api 接口, 对于 libsvm 的内部实现都不甚了解. 所在我是站在巨人的肩膀上学习, 什么都是现成的. 如果需要你自己去开发和创新, 那么就意味着你已经站在芸芸众生的上面了.
目录:
文本分类学习 (一) 开篇
文本分类学习 (二) 文本表示
文本分类学习 (三) 特征权重 (TF/IDF) 和特征提取
文本分类学习 (四) 特征选择之卡方检验
文本分类学习 (五) 机器学习 SVM 的前奏 - 特征提取(卡方检验续集)
文本分类学习(六)AdaBoost 和 SVM(残)
文本分类学习 (七) 支持向量机 SVM 的前奏 结构风险最小化和 VC 维度理论
文本分类学习(八)SVM 入门之线性分类器
文本分类学习(九)SVM 入门之拉格朗日函数和 KKT 条件
一, LibSvm 的简单介绍
这里只介绍 libSvm 中的 C 语言版本, 也就是前言中说的 svm.cpp 和 svm.h.
1. 结构体介绍
svm.h 文件包含了 svm 中所有的结构体和函数声明.
首先是 结构体 svm_node
- struct svm_node
- {
- int index;
- double value
- };
svm_node 是用来储存单个文本向量的单个特征, 结构体只有两个属性一个是下标, 一个是值. 很显然如果一个文本向量的表示肯定是一个 svm_node[] 数组. 值得注意的是 libsvm 中, 对于特征值为 0, 也就是 value 为 0 的特征, 可以不用放到 svm_node[]数组里这样会简化计算. 此外, svm_node[]数组的最后一个元素 index 的值必须是 - 1 且 value 值为 null, 是一个文本向量的结束标志.
然后是 结构体 svm_problem
- struct svm_problem
- {
- int l;
- double *y;
- struct svm_node **x;
- };
前面的 svm_node 是表示单个文本向量, 那么 svm_problem 便表示的是整个训练集了. 其中 l 是训练集的个数, y 是一个数组表示训练集的标签, x 是一个二维数组自然表示训练集的文本向量. 注意在二分类问题中 y 数组的值应该是 + 1 或者 - 1.
接下来是 结构体 svm_parameter
- 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 */
- };
这个结构表示的是 svm 分类器的参数, 介绍几个重要的参数:
svm_type 是选用的 svm 类型有:{ C_SVC, NU_SVC, ONE_CLASS, EPSILON_SVR, NU_SVR } , 对于二分类选择 C_SVC
kernel_type 就是大名鼎鼎的核函数, 类型有 { LINEAR, POLY, RBF, SIGMOID } 对于二分类可以选择 LINEAR 或者 RBF . 根据作者的描述, 一个效果十分优秀的 svm 分类器应该是选择 RBF 核函数, 或者叫做高斯核函数. 至于原因呢, 那就要研究关于 RBF 核函数映射到高维空间的问题. 选择 RBF 核函数然后交叉验证选择最优的 C 和 gamma 参数. 我选择的 RBF 核函数, 也在不断调整 gamma 参数来达到最优的效果, 后面再提吧.
C 惩罚因子 就是松弛变量, 越大表示你越关心分错的点, 如果 C 选的越大, 那么对于 svm 来说就需要更多的时间去不断迭代寻找一个几乎不会误判训练集的分类器 (因为你很关心分错的点). 这样训练的时间会很长. 而如果你的训练集不是那么纯的(就是有些许误差啥的) 所以 C 不宜选大. 我选择的是 35.
gammer RBF 核函数宽度参数 此参数和 C 十分重要, 需要你去不断的调试更改. 一般来说 gammer 参数应该选择比较小的参数. 有些博客说 gammer 参数默认是 1 / 类别数 . 二分类就是 0.5 . 但是选择了 0.5 你会发现训练出来的分类器如同一个智障一般. 在我的测试发现, gammer 越小分类器的准确率越高, 然而它也有一个下限, 超过了这个下限, 分类器也是如同一个智障一般. 我最终选择的是 0.00001.
最后是 结构体 svm_model
- 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 */
- };
svm_model 就是我们千呼万唤试出来的分类器, 这里只需要介绍一个重要的属性:
struct svm_node **SV 这就是支持向量, 支持向量机中的支持向量 是它们帮我们撑出来一个分类超平面, 这就是向量机的分类器.
2. 函数介绍
这里仅仅介绍常用的五个函数, 这些函数已经足够做出来一个垃圾识别文章的接口了.
struct svm_model *svm_train(const struct svm_problem *prob, const struct svm_parameter *param);
训练函数, 传入参数是上面说过的 svm_problem ,svm_parameter 得到的是一个分类器 svm_model.
void svm_cross_validation(const struct svm_problem *prob, const struct svm_parameter *param, int nr_fold, double *target);
交叉验证函数, 其中 nr_fold 是交叉验证的折数. 稍微提一下交叉验证, 比如 nr_fold=10 , 表示 10 折交叉验证. 那么怎么做的呢? 就是将训练集分成 10 份, 9 份作为真正的训练集去训练, 剩下的一份作为测试集去验证效果如何. 10 折就是循环 10 次, 每次都选一份 (每次都不同的) 作为测试集, 剩下的作为训练集.
int svm_save_model(const char *model_file_name, const struct svm_model *model);
将训练出来的分类器, 写到文件中文件名: model_file_name. 是保存分类器的函数
struct svm_model *svm_load_model(const char *model_file_name);
顾名思义就是加载分类器
double svm_predict(const struct svm_model *model, const struct svm_node *x);
这才是我们最终需要的函数, 预测函数, 给定一个 svm_node 数组(代表普通的一个文本向量),svm 会给出它的预测分类, 对于二分类:+1 或者 - 1.
二, 构造 main.cpp
有了 svm.cpp 和 svm.h 那我们就可以自己写一个控制台程序, 去实现一个 svm 垃圾分类器程序. svm 这么难的机器学习算法, 但是站在巨人的肩膀上你会发现使用它是很简单的. 更不用说现在微软发布了 ML.NET https://blogs.msdn.microsoft.com/dotnet/2018/05/07/introducing-ml-net-cross-platform-proven-and-open-source-machine-learning-framework/ 使得你可以随心所欲使用各种各样的机器学习算法.
我首先构造了自己的结构体, 叫做 MySvm , 对 libsvm 中的函数进行了又一次的封装, 并且考虑到实际的训练集会放到一文件夹中, 并且有各种的文件读写操作. 我又额外构造了处理文件的结构体: FileHandle. 这些结构体十分的简单和原始, 如果有错误或者改进的地方, 欢迎在评论区指出.
- MySvm:
- class MySvm
- {
- public:
- MySvm(){};
- ~MySvm(){};
- void train(std::string modelFileName);
- double predic(std::string targetFileName);
- void setParam();
- void setProb();
- void setFileName(string fileName);
- void setModel(char* modelName);
- svm_parameter* getParam(){return param;}
- svm_model* getModel(){return model;}
- svm_problem* readTrainData(std::string modelFileName);
- svm_node* readSingleData(string modelFileName);
- svm_node* readSingleDataFromText(string text);
- private:
- svm_problem* prob;
- svm_parameter* param;
- svm_model* model;
- string fileName;
- };
文件处理结构体:
- class FileHandle
- {
- public:
- FileHandle(){};
- ~FileHandle(){};
- vector<string> file;
- void read();
- void setFileName(string FileName);
- private:
- string FileName;
- struct dirent *ptr;
- };
接下来, 有必要把 MySvm 中的 SetParam()函数贴出来. 因为对于一个新手来说, 参数的选择真的有点像无头苍蝇. 我贴出来只是针对二分类问题做一个参考. 毕竟每一个人的训练集都是不一样的, 样本的特征分布也不一样.
- param->svm_type = C_SVC;
- param->kernel_type = RBF;
- param->degree = 3;
- param->gamma = 0.00001; /* 1/num_features */
- param->coef0 = 0;
- param->nu = 0.5;
- param->cache_size = 100;
- param->C = 32;
- param->eps = 1e-3;
- param->p = 0.1;
- param->shrinking = 1;
- param->probability = 0;
- param->nr_weight = 0;
- param->weight_label = NULL;
- param->weight = NULL;
于是我们可以获取训练集, 训练分类器了 main 函数的部分.
- MySvm svm = MySvm();
- svm_problem* s;
- s =svm.readTrainData("xxxx");
- svm.setParam();
- model = svm_train(s, svm.getParam());
- svm_save_model("/xxxx/Model.txt",model);*/
经过训练, 发现 svm 的分类器果然不是名不虚传. 如果你选择了合适的 C 和 gamme 参数那么 svm 不会让你失望的. 别的代码我就不贴出来了. 因为没什么技术含量的东西, 也不是这篇文章的主要内容.
经过控制台程序的测试, 已经具备了分类测试的功能. 那么接下来基于 c,c++ 的程序来做一个 C# 的 wrapper.
三, 构造 C#Wrapper
前面的 c++ 程序, 已经实现了读取训练集, 训练分类器. 加载分类器, 预测类型. 但是我想做的是一个 API 接口, 一开始想用 c++ 做一个 web API. 但是想到团队里都是用. net 写网站和接口. 所以只能放弃了. 使用基于. net core2.0 的 web API 程序, 然后调用 c++ 的 dll, 便成为我的思路了.
大家都知道由于. net core2.0 是跨平台的, 所以. net 网站已经开始在 Linux 上跑起来了. 我的亦不例外.
1. Linux 的 c/c++ 动态链接库生成
Linux 上的 c++ 的动态链接库是. so 文件, 而在 Windows 上的是. dll 文件. 生成. dll 文件很简单, 你可以使用 visual studio 来做(其中有些坑就不说了). 在 Linux 中生成. so 文件有什么工具呢? 你当然可以用 Xcode, 或者 Clion, 但是在 Mac 下生成的是. dylib 文件, 这是 Mac 下的动态链接库文件, 不是我想要的. 事实上 Linux 中生成. so 很简单, 因我们可以使用神器 Cmake.
Cmake 的定义: CMake is an open-source, cross-platform family of tools designed to build, test and package software
在 Linux 中先下载 Cmake : apt-get install Cmake
然后将写好的 svm.h , svm.cpp MySvm.h MySvm.cpp (将之前的 main.cpp 分成了 MySvm.h 和 MySvm.cpp), 放到某个文件夹里, 比如 Svm/
然后编写 CmakeLists.txt 这是 Cmake 执行命令的文本, 如下所示:
- cmake_minimum_required(VERSION 3.5.1)
- project(MySvm)
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
- SET(detour_SRCS svm.cpp MySvm.cpp)
- SET(detour_HDRS svm.h MySvm.h)
- ADD_LIBRARY(MySvm SHARED ${detour_SRCS} ${detour_HDRS})
接下来执行 (仅仅第一次需要执行)
$ Cmake ..
你会发现文件夹里多了 CmakeFiles/ CMakeCache.txt cmake_install.cmake
接下来执行
$ make
于是千呼万唤始出来的 libsvm.so 文件变跳出来了. 它就是上面所说 c/c++ 程序的动态链接库, 可以在 C# 程序里直接调用的. 整个过程没有什么坑点.
你可以执行
$ nm -D libsvm.so
查看这个动态链接库提供了哪些函数. 于是坑点来了. 发现 c++ 里写的函数都会被换一个名字, 而 c 语言写的函数都是正常的. 那是因为 c++ 支持函数名重载, 所以编译器会根据自己的规则对函数名进行篡改, 防止命名发生冲突. 所以在调用函数的时候, 会出现找到不该函数的错误, 把那个长长的函数名复制进去把. 或者在 c++ 编写的函数前面加上_stdcall
2.C# 调用 c/c++ 的动态链接库
这个十分简单, 但是也会有坑点! 使用 c# 的 dllimport
- [DllImport("/svm/libMySvm.so")]
- public static extern double predic(string text)
坑点 1: 关于 C# 传入到 c/c++ 函数的 string 参数问题
在 c/c++ 程序中函数使用的参数是 char *, 那么在 C# 用什么参数对应呢?
C++ 数据类型 | C# 数据类型 |
WORD | ushort |
DWORD | uint |
UCHAR | int/byte |
UCHAR* | string/InPtr |
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 |
以上是数据类型对应表. char * 对应的是 string. 有的博客里说应该使用 IntPtr 指针, 我认为也是可以的. 但是能用 string 为啥还要用指针呢?
坑点 2,c++ 用的字符编码是 ansi , 而 C# 使用的字符编码默认是 Unicode 所以用上面的的简单的 dllimport 是传不了正确数据的. 所以最终正确的用法如下:
- [DllImport("/svm/libMySvm.so",CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
- public static extern double _Z5judgePc(string text);
于是我们最核心的调用 c/c++ 动态链接库的工作就可以说顺利完成了.
3. 构造 API
然后就是简单的构造 web api 的工作. 新建一个 net core2.0 的 Web Api 项目, 在 Controller 里调用 c/c++ 动态链接库. 整个过程很简单. 最终是这样一个接口:
对于上面这段文本, api 给出的结果是 - 1, 表示是垃圾文本. 这个分类器是由 2000 篇正常文本和 1689 篇垃圾博文训练出来的.
还有一点就是时间, 识别一篇垃圾文本的时间不能长, 那样的话别人都不想用你的东西. 上面是第一次 4000ms, 一般的时候是 200ms, 需要你在 c/c++ 程序里要注意, svm_loadmodel()加载分类器函数是一个很耗时间的操作. 这个函数第一次使用时加载一次就够了.
四, 总结
搭建这样一个接口, 是为了提供垃圾文本识别的. 这对一个拥有一定用户量的网站是很有必要的. 毕竟林子大了什么鸟都有. 反垃圾反广告的工作始终都是一个消耗很大人力成本的工作.
这只是一个简单的接受文本, 反馈结果的 api. 而对于一个站点来说, 反垃圾显然不是一个 api 能做到的. 你需要设计一个庞大的系统. 你可以选择 svm, 贝叶斯算法, 等机器学习算法, 也可以选择深度学习的算法 (更高大上一些, 但效果也不一定比机器学习好.) 这个系统需要有自我反馈和学习的机制. 因为垃圾文本始终是在变化的. 你的垃圾库也要随之发生变化. 训练数据也是一个耗费时间和资源的事情, 如何在适当的时候再次训练构造更强大的分类器. 对于训练数据如何设计一个不断搜集垃圾文本的程序, 以减少人工构造训练集的成本.
再提一点, 你千辛万苦写的 api 可能没有微软发布的机器学习框架效果的十分之一好. 但是如果你开发的十分符合你自己站点的民风民俗, 那就很有效了. 因为大佬们的框架是面向普罗大众的, 他可能照顾不到你.
这篇博客没有介绍 svm 的什么知识, 是介绍一下实际场景中 svm 的利用. 一个算法的理论研究和实际使用还是很大的区别. 怎么把机器学习在实际生产充分发挥它的作用, 而不只是追求理论的东西如同八股一般.
来源: https://www.cnblogs.com/dacc123/p/9133749.html