crf++模型訓練到c++、java呼叫(介面)
crf模型是個特別好用的模型,做分詞、做ner等nlp工作都力離不開,訓練crf模型用很多工具,比較出名的就是今天要講的crf++,其文件清晰,支援各種語言的介面,本篇blog要講的是c++和java的介面, 這裡java的介面是通過jni呼叫c++實際本質還是c++,不過也有人通過java實現了c++載入crf模型的那套邏輯,比較出名的就是在ansj中分詞,就是通過java實現載入模型的邏輯自己實現了要,廢話不多說,直接看步驟吧:
下面訓練的是一箇中文分詞模型,ner等也是一樣,在example中有
crf++下載地址,google的雲盤,最新版本0.58:
https://drive.google.com/drive/folders/0B4y35FiV1wh7fngteFhHQUN2Y1B5eUJBNHZUemJYQV9VWlBUb3JlX0xBdWVZTWtSbVBneU0
crf++官方文件:
https://github.com/search?l=C%2B%2B&q=fp-growth&type=Repositories
crf++安裝步驟:
下載之後解壓,進入crf++目錄,執行下面命令
./configure
make
sudo make install #把標頭檔案、so文字拷貝到 /usr/local/include /usr/local/lib 中去,後面c++介面可以用到生成的動態檔案
在這裡會生成訓練模型的命令
訓練模型的語料,用的人民日報的語料
https://pan.baidu.com/s/1jIy3YSY
轉化crf++模型訓練的輸入格式,在這裡我寫了個java程式碼:
package com.xxx.crf; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import org.apache.commons.lang3.StringUtils; public class DataUtils { public static void main(String[] args) { String inputpath="/Users/zhoumeixu/Downloads/crffile/swresult_withoutnature.txt"; String outputpath="/Users/zhoumeixu/Downloads/crffile/crfmodelinput.txt"; transFile(inputpath,outputpath); } public static void transFile(String inputpath,String outputpath) { BufferedReader br=null; BufferedWriter bw=null; try { br=new BufferedReader(new InputStreamReader(new FileInputStream(inputpath))); bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputpath))); String line=br.readLine(); while(line!=null) { if(StringUtils.isNotBlank(line)) { String[] lines=line.trim().split("\t"); for(String element:lines) { int len=element.length(); if(len==1) { bw.write(element+"\t"+"S"+"\n"); }else { bw.write(String.valueOf(element.charAt(0))+"\t"+"B"+"\n"); for(int i=1;i<len-1;i++) { bw.write(String.valueOf(element.charAt(i))+"\t"+"M"+"\n"); } bw.write(String.valueOf(element.charAt(len-1))+"\t"+"E"+"\n"); } } bw.write("\n"); } line=br.readLine(); } }catch(Exception e) { } } }
模型輸入的檔案格式見如下,在這裡我沒有用其他的特徵,比如說分詞可以加這個特徵字是數字、英文、漢字等形式,那就應該有三列:
邁 B
向 E
充 B
滿 E
希 B
望 E
的 S
新 S
世 B
紀 E
模型訓練:
#這裡加-t是生成model.txt檔案,非二進位制檔案,同時也會生成一個二進位制模型檔案
crf_learn template crfmodelinput.txt model -t
會看到生成兩個模型,一個model 另外一個model.txt, 這裡去載入model檔案就可以,用ansj的那套邏輯是直接載入model.txt
java呼叫:
java呼叫必須先生成所需要的動態檔案,通過jni載入,首先在 **/CRF++-0.58/java目錄下執行make命令,不出意外的花的會報錯,這是因為對jni.h 和 jni_md.h檔案沒有,
其中jni.h 和 jni_md.h 檔案分別在java的include和include/darwin目錄下面,執行下面命令拷貝到這目錄下面:
把CRFPP_wrap.cxx下面的#include <jni.h> 改為#include "jni.h"
輸入命名:
sudo cp jni.h ~/Downloads/CRF++-0.58/java/
sudo cp jni_md.h ~/Downloads/CRF++-0.58/java/
修改 CRFPP_warp.xx 中的#include jni.h> ——#include “jni.h”
make
生成CRFPP.jar 和 libCRFPP.so
把上面的jar包丟在maven中進行依賴,下面寫了個測試程式碼:
package com.xxx.crf;
import org.chasen.crfpp.Tagger;
public class Test {
static {
System.load("/Users/zhoumeixu/Downloads/CRF++-0.58/java/libCRFPP.so");
System.out.println("so檔案載入成功");
}
public static void main(String[] args) {
String text = "講述了三個代表重要思想,我們要解放思想,實事求是,抓住機遇";
Tagger tagger = new Tagger("-m /Users/zhoumeixu/Downloads/crffile/model -v 3 -n2");
tagger.clear();
char[] ch = text.toCharArray();
tagger.clear();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < ch.length; i++) {
tagger.add(String.valueOf(ch[i]));
}
tagger.parse();
long size = tagger.size();
long xsize = tagger.xsize();
for (long i = 0; i < size; i++) {
for (long j = 0; j < xsize; j++) {
String str = tagger.x(i, j);
String tag = tagger.y2(i);
if ("B".equalsIgnoreCase(tag)) {
sb.append(" ").append(str);
} else if ("M".equalsIgnoreCase(tag)) {
sb.append(str);
} else if ("E".equalsIgnoreCase(tag)) {
sb.append(str).append(" ");
} else if ("S".equalsIgnoreCase(tag)) {
sb.append(" ").append(str).append(" ");
}
}
}
System.out.println(sb.toString().trim());
}
}
結果:
so檔案載入成功
講述 了 三 個 代表 重要 思想 , 我們 要 解放 思想 , 實事 求是 , 抓住 機遇
c++介面:
c++相關的原始碼,在這篇部落格裡面寫的是比較清楚的http://www.hankcs.com/ml/crf-code-analysis.html,可以去看看,在這裡我主要寫個程式碼載入上面crf++訓練的模型。
先看下CMakeList:
cmake_minimum_required(VERSION 3.10)
project(cppexcise)
set(CMAKE_CXX_STANDARD 11)
link_directories(/usr/local/include)
link_libraries(/usr/local/lib)
add_executable(cppexcise main.cpp )
target_link_libraries(cppexcise crfpp.0)
c++程式碼:
#include <iostream>
#include <vector>
#include <sstream>
#include "crfpp.h"
using namespace std;
int is_zh_ch(char p) {
if (~(p >> 8) == 0) {
return 1;
}
return -1;
}
//c++中string對中文支援不友好,不像java那麼隨意,用下面的方法實現
void string2vec(vector<string> &dump,string &text) {
int len=text.length();
int i = 0;
while (i < len) {
if (is_zh_ch(text.at(i)) == 1) {
dump.push_back(text.substr(i, 3));
i = i + 3;
} else {
dump.push_back(text.substr(i, 1));
i = i + 1;
}
}
}
int main(int argc, const char *argv[]) {
string text = "講述了三三代表重要思想,我們要解放思想,實事求是,抓住機遇";
cout << text.length();
vector<string> vec;
string2vec(vec,text);
cout << " Begin to load crf++ model" << endl;
CRFPP::Model *crf_model = CRFPP::createModel("-m /Users/zhoumeixu/Downloads/crffile/model -v 3 -n2");
CRFPP::Tagger *tagger = crf_model->createTagger();
tagger->clear();
vector<string>::iterator iter;
for (iter = vec.begin(); iter != vec.end(); ++iter) {
tagger->add((*iter).c_str());
}
if (!tagger->parse()) {
cout << ">>>>>> Fail to parse !" << endl;
return -1;
}
std::stringstream ss;
for (size_t i = 0; i < tagger->size(); i++) {
for (size_t j = 0; j < tagger->xsize(); j++) {
string str = tagger->x(i, j);
string tag = tagger->y2(i);
if (tag == "B") {
ss << " " << str;
} else if (tag == "M") {
ss << str;
} else if (tag == "E") {
ss << str << " ";
} else if (tag == "S") {
ss << " " << str << " ";
}
}
}
cout << ss.str() << endl;
ss.str("");
cout << "清空結果:" << ss.str() << endl;
return 0;
}
結果:
87 Begin to load crf++ model
講述 了 三 三 代表 重要 思想 , 我們 要 解放 思想 , 實事 求是 , 抓住 機遇
和java的結果是一模一樣的,總體是比較簡單的,我在這裡沒有加入其它特徵還是比較遺憾的,不過在crf++裡面有句法分析、ner識別,流程也比較簡單,大家可以去看看。