文本分類學習 (十)構造機器學習Libsvm 的C# wrapper(調用c/c++動態鏈接庫)

前言: 對於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 使得你可以隨心所欲使用各種各樣的機器學習算法。

我首先構造了自己的結構體,叫做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的利用。一個算法的理論研究和實際使用還是很大的區別。怎麼把機器學習在實際生產充分發揮它的作用,而不只是追求理論的東西如同八股一般。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章