中文分词是中文信息处理领域中的最重要的任务,它对于智能信息处理技术具有重要的意义。经过大量的研究,汉语分词技术取得了很高的精度。众多的研究者已经提出了许多方法,其中的条件随机场模型(CRF)受到越来越多的关注。条件随机场模型已因其性能优良的广泛使用,本文也将采用CRF进行中文分词。
本任务做的是繁体中文分词,以语料的30%作为测试集,70%作为训练集,(注意不能随机选数据,或者隔行取,由于附近的句子相近或者相同,会导致结果偏高!)使用CRF++工具做出来的效果非常不错,F1值能达到95%上。
这学期选了一门机器学习课程,这次的课程任务是中文分词,在查阅了中文分词的相关论文后,发现CRF的分词表现非常棒,所以我选择了CRF++工具包。这次我使用的是CRF++的windows版本,开发语言为java。
地址:http://crfpp.sourceforge.net
其中有两种,一种是Linux下(带源码)的,一种是win32的,当然是在什么平台下用就下载什么版本了。
2.CSDN
地址:http://download.csdn.net/detail/luozhipeng2011/8854513
我已经打包好了win32和Linux版本,为CRF++0.58。
i.解压到某目录下
ii.打开控制台,将当前目录切换到解压目录
iii.依次输入命令:
./configure
make
su
make install
注:需要root权限才能成功安装。
doc文件夹:就是官方主页的内容。
example文件夹:有四个任务的训练数据、测试数据和模板文件。
sdk文件夹:CRF++的头文件和静态链接库。
crf_learn.exe:CRF++的训练程序。
crf_test.exe:CRF++的预测程序
libcrfpp.dll:训练程序和预测程序需要使用的静态链接库。
实际上,需要使用的就是crf_learn.exe,crf_test.exe和libcrfpp.dll,这三个文件。
crf_learn template_file train_file model_file
即
crf_learn 模板文件 训练的数据文件 生成的模型文件
这个训练过程的时间、迭代次数等信息会输出到控制台上(感觉上是crf_learn程序的输出信息到标准输出流上了),如果想保存这些信息,我们可以将这些标准输出流到文件上,命令格式如下:
crf_learn template_file train_file model_file >> train_info_file
对于其中的参数,有四个主要的参数可以调整:
-a CRF-L2 or CRF-L1
规范化算法选择。默认是CRF-L2。一般来说L2算法效果要比L1算法稍微好一点,虽然L1算法中非零特征的数值要比L2中大幅度的小。
-c float
这个参数设置CRF的hyper-parameter。c的数值越大,CRF拟合训练数据的程度越高。这个参数可以调整过度拟合和不拟合之间的平衡度。这个参数可以通过交叉验证等方法寻找较优的参数。
-f NUM
这个参数设置特征的cut-off threshold。CRF++使用训练数据中至少NUM次出现的特征。默认值为1。当使用CRF++到大规模数据时,只出现一次的特征可能会有几百万,这个选项就会在这样的情况下起到作用。
-p NUM
如果电脑有多个CPU,那么那么可以通过多线程提升训练速度。NUM是线程数量。
带两个参数的命令行例子:
crf_learn -f 3 -c 1.5 template_file train_file model_file
crf_test -m model_file test_files
即
crf_test -m 训练生成的模型文件 测试数据文件
有两个参数-v和-n都是显示一些信息的,-v可以显示预测标签的概率值,-n可以显示不同可能序列的概率值,对于准确率,召回率,运行效率,没有影响,这里不说明了。
与crf_learn类似,输出的结果放到了标准输出流上,而这个输出结果是最重要的预测结果信息(测试文件的内容+预测标注),同样可以使用重定向,将结果保存下来,命令行如下。
crf_test -m model_file test_files >> result_file
下面是我分词的训练文件部分数据:
训练文件由若干个句子组成(可以理解为若干个训练样例),不同句子之间通过换行符分隔。每个句子可以有若干组标签,最后一组标签是标注,上图中有三列,即第一列和第二列都是已知的数据(特征),第三列是要预测的标注,以上面例子为例是,根据第一列的词语和和第二列的相关特征,预测第三列的标注。
测试文件与训练文件是一样的,测试后会在后面加一列标注结果。如下图:
CRF++有两种模板类型:
1.Unigram类型
每一行%x[#,#]生成一个CRFs中的点(state)函数: f(s, o), 其中s为t时刻的的标签(output),o为t时刻的上下文.如CRF++说明文件中的示例函数:
func1 = if (output = B-NP and feature="U01:DT") return 1 else return 0
它是由U01:%x[0,1]在输入文件的第一行生成的点函数.将输入文件的第一行"代入"到函数中,函数返回1,同时,如果输入文件的某一行在第2列也是DT,并且它的output同样也为B-NP,那么这个函数在这一行也返回1.
2.Bigram类型
每一行%x[#,#]生成一个CRFs中的边(Edge)函数:f(s', s, o), 其中s'为t - 1时刻的标签.也就是说,Bigram类型与Unigram大致机同,只是还要考虑到t - 1时刻的标签.如果只写一个U的话,默认生成f(s', s).
模板文件中的每一行是一个模板。每个模板都是由%x[row,col]来指定输入数据中的一个token。row指定到当前token的行偏移,col指定列位置。
由上图中的部分数据,我们以中字来做模板分析,分析如下:
template | Expanded feature |
U00:%x[-2,0] | 出 |
U01:%x[-1,0] | 路 |
U02:%x[0,0] | , |
U03:%x[1,0] | 例 |
U04:%x[2,0] | 如 |
U05:%x[-2,0]/%x[-1,0]/%x[0,0] | 出/路/, |
U06:%x[-1,0]/%x[0,0]/%x[1,0] | 路/,/例 |
U07:%x[0,0]/%x[1,0]/%x[2,0] | ,/例/如 |
U08:%x[-1,0]/%x[0,0] | 路/, |
U09:%x[0,0]/%x[1,0] | ,/例 |
U10:%x[-1,1] | W |
U11:%x[0,1] | S |
U12:%x[-1,1] | W |
U13:%x[-1,1]/%x[0,1] | W/S |
分词工具: CRF++
开发环境: Win7 32bit i5 四核 2.99G内存
开发语言: java
由于本次任务所给数据不是CRF++能处理的格式,所以要先把数据格式转成CRF++能处理的格式。其中要注意的是原数据文件的编码是utf-16,而CRF++能处理的是utf-8编码文件,否则出现异常不会生成模型,还有数据的每列之间是由\t分隔的,所以转换时按CRF++的格式转换并保持为utf-8编码,这些细节方面需要多注意。本次任务把原训练数据划分成3:7,其中30%作为测试集,70%作为训练集。本来想交叉验证来调参数,但是训练时间较长最后只划分一次数据。
我选取了两类特征,第一类就是文字本身,第二类是文字、数字、字母、结束标点符号、对称标点符号五种标签,其中要注意的是本次使用的数据的数字和字母都是全角字符,另外“一二三...十百...”也看做是数字,文字对应标签W,数字标签对应D,字母标签对应L,结束标点符号“"!@#$%^&*_+|\'?/:;,.,!¥……——:;,。?、"”对应S,对称标点符号对应P。由此生成的训练和测试数据格式如下:
第一列是文字本身。第二列文字类型,其中 W代表普通的词,D代表数字, L表示字母,S代表结束标点符号,P表示对称标点符号,第三列是词位标记 B代表开头,M代表中间,E代表结尾。
我使用的模板如下:
# Unigram
U00:%x[-2,0]
U01:%x[-1,0]
U02:%x[0,0]
U03:%x[1,0]
U04:%x[2,0]
U05:%x[-2,0]/%x[-1,0]/%x[0,0]
U06:%x[-1,0]/%x[0,0]/%x[1,0]
U07:%x[0,0]/%x[1,0]/%x[2,0]
U08:%x[-1,0]/%x[0,0]
U09:%x[0,0]/%x[1,0]
U10:%x[-2,1]
U11:%x[-1,1]
U12:%x[0,1]
U13:%x[1,1]
U14:%x[2,1]
U15:%x[-1,0]/%x[1,0]
U16:%x[-1,1]/%x[1,1]
U17:%x[-1,1]/%x[0,1]
U18:%x[0,1]/%x[1,1]
U19:%x[-2,1]/%x[-1,1]/%x[0,1]
U20:%x[-1,1]/%x[0,1]/%x[1,1]
U21:%x[0,1]/%x[1,1]/%x[2,1]
# Bigram
B
其中U01至U03表示取第0列的前后和当前的特征,U04至U06表示取第0列的交叉组合特征,U07至U09表示取第一列的交叉组合特征。Bigram特征类似。
运行后生成的结果文件如下:
其中最后一列就是预测的结果,可以和倒数第二列对比计算准确率和召回率。
精确率和召回率的计算方式:
正确率 = 正确划分词数/分割出来的词数
召回率 = 正确划分词数/文档总词数
F1计算方式:
F值 = 正确率 * 召回率 * 2 / (正确率 + 召回率)
最后一次测试结果如下:
结果如下表:
方法 | 准确率 | 召回率 | F1 |
标签:B,E 模板:原模板 | 0.9532409 | 0.94477296 | 0.948988 |
标签:B,M,E 模板:原模板 | 0.95693 | 0.95030034 | 0.9536036 |
标签:B,M1,M,E 模板:原模板 | 0.9561747 | 0.95030975 | 0.95323324 |
标签:B,M1,M2,M,E 模板:原模板 | 0.9553639 | 0.9498632 | 0.9526056 |
B,M,E + %x[-1,0]/%x[1,0] | 0.9573461 | 0.9507046 | 0.95401376 |
B,M,E + 窗口改为2 | 0.95736176 | 0.9509631 | 0.9541517 |
B,M,E + 添加Bigram特征 | 0.95847404 | 0.95120746 | 0.9548269 |
B,M,E + 添加Bigram特征 + c=8 | 0.95841676 | 0.9518984 | 0.95514643 |
B,M,E +添加Bigram特征 + c=8+第二列特征(数字,字母,其他) | 0.9591864 | 0.95262223 | 0.95589304 |
模板特征见源码 | 0.9594525 | 0.95346826 | 0.95645106 |
从结果来看分词效果还是非常不错的,由于时间有限也还有很多待改进的地方。
本次的机器学习项目作业是中文分词,得知作业内容后我查阅了相关论文,最初搜索了一些深度学习在这方面的论文,其中文献[7]中就用了深度学习来解决中文分词问题,总得来说效果还是不错的,但是短时间我难以实现,就没考虑使用深度学习方法了。后来发现许多论文都是用CRF来做的,并且效果很好,并且有CRF++这个现成的工具可用,所以最后我选择了CRF。在做的过程中我尝试了很多方法,但是由于训练时间比较长,每次都是增量式修改,这样很可能最后的结果并非最好的结果。另外机器内存不足(2.99G),能跑的特征数量有限,这也限制了尝试更多特征的可能。由于时间有限,还有很多可以优化的地方没有去尝试,以后有时间深入研究。
附源码:
import java.io.BufferedReader; import java.io.InputStreamReader; public class Command { public static void exeCmd(String commandStr,String flag) { BufferedReader br = null; try { Process p = Runtime.getRuntime().exec(commandStr); br = new BufferedReader(new InputStreamReader(p.getInputStream(),"utf-8")); String line = null; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { if(line.length() < 1){ System.out.println("\n"); sb.append("\n"); continue; } if(flag.equalsIgnoreCase("test")) sb.append(line + "\n"); else System.out.println(line); } if(flag.equalsIgnoreCase("test")) //测试时输出结果到文件 CRFFormat.write("data/output.txt", sb.toString(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { if (br != null) { try { br.close(); } catch (Exception e) { e.printStackTrace(); } } } } public static void main(String[] args) { // String commandStr = "ping www.luozhipeng.com"; // Command.exeCmd(commandStr,"train"); } }
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.util.HashSet; public class CRFFormat { private static HashSet<Character> s_set = new HashSet<Character>(); //普通标点符号 private static HashSet<Character> p_set = new HashSet<Character>(); //配对标点符号,如()''[] private static HashSet<Character> d_set = new HashSet<Character>(); //数字 private static HashSet<Character> l_set = new HashSet<Character>(); //字母 public static void init() throws IOException{ String s_str = "!@#$%^&*_+|\'?/:;,.,!¥……——:;,。?、"; String p_str = "()《》“”‘’{}[]()<>「」【】〖〗"; String d_str = "0123456789一二三四五六七八九十百廿"; String l_str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; for(int i = 0; i < s_str.length(); ++i) s_set.add(s_str.charAt(i)); for(int i = 0; i < p_str.length(); ++i) p_set.add(p_str.charAt(i)); for(int i = 0; i < d_str.length(); ++i) d_set.add(d_str.charAt(i)); for(int i = 0; i < l_str.length(); ++i) l_set.add(l_str.charAt(i)); } public static void write(String path, String content, String encoding) throws IOException { File file = new File(path); file.delete(); file.createNewFile(); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file), encoding)); writer.write(content); writer.close(); } public static int read(String path) throws IOException{ File file = new File(path); BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream(file), "utf-16")); String line = null; int n = 0; while ((line = reader.readLine()) != null) { ++n; } return n; } public static void read(String path, String encoding,int part) throws IOException { init(); int count = read(path); //总行数 //取测试集范围,用于交叉验证 int begin = 0,end = 0; if(part == -1){//100%测试 begin = -1; end = 1000000000; } else if(part == -2){//100%训练 begin = end = -1; } else{ begin = (int) (count*part*0.1); end = (int) ((int)count*(part+3)*0.1); } System.out.println(begin+" "+end); StringBuffer content[] = new StringBuffer[2];//0训练集,1测试集 StringBuffer test_sent = new StringBuffer(); for(int i = 0; i < 2; ++i) content[i] = new StringBuffer(); int ind; File file = new File(path); BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream(file), encoding)); String line = null; int n = 0; while ((line = reader.readLine()) != null) { ++n; if(n >= begin && n < end)//测试集范围 ind = 1; else //训练集 ind = 0; if(ind == 1){//测试的句子 test_sent.append(line.replaceAll(" ","")+"\n"); } if(line.length() < 1){//跳行 content[ind].append("\n"); continue; } String[] ss = line.split(" "); String feat1 = "", label = ""; for(int i = 0; i < ss.length; ++i){ for(int j = 0; j < ss[i].length(); ++j){ if(p_set.contains(ss[i].charAt(j))) //数字 feat1 = "P"; else if(s_set.contains(ss[i].charAt(j)))//字母 feat1 = "S"; else if(d_set.contains(ss[i].charAt(j))) feat1 = "D"; else if(l_set.contains(ss[i].charAt(j))) feat1 = "L"; else{ feat1 = "W"; } // if(ss[i].length() == 1){//单个字,在该数据集上效果不行 // content[ind].append(ss[i].charAt(j)+"\t"+feat1+"\tS\n"); // } // else if(j == 0) content[ind].append(ss[i].charAt(j)+"\t"+feat1+"\tB\n"); else if(j == ss[i].length()-1) content[ind].append(ss[i].charAt(j)+"\t"+feat1+"\tE\n"); else{ label = ""; // if(j>0 && j<2) 用于添加标签,M1,M2,..M,由于在本数据上效果不好,所以没使用 // label +=j; content[ind].append(ss[i].charAt(j)+"\t"+feat1+"\tM"+label+"\n"); } } } } reader.close(); //由于原数据文件(CRF)是utf-8格式,此处也使用utf-8 int t = part; t = Math.max(t, 0); if(part != -1) write("data/train_"+t+".data", content[0].toString(), "utf-8"); if(part != -2){ write("data/test_"+t+".data",content[1].toString(),"utf-8"); write("data/test_sent_"+t+".data",test_sent.toString(),"utf-16"); } } //把CRF的数据格式转成标准分词格式(作业要求的格式) public static void crf2standard(String path_out,String path_orig,String encoding) throws IOException{ File file = new File(path_out); //分词后的文件 File file1 = new File(path_orig); //分词前的文件 BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream(file), encoding)); BufferedReader reader1 = new BufferedReader(new InputStreamReader( new FileInputStream(file1), "utf-16")); String line = null; StringBuffer label = new StringBuffer(); while ((line = reader.readLine()) != null) {//读取标签 if(line.length() < 1){ label.append("\n"); continue; } String[] ss = line.split("\t"); label.append(ss[3]); } StringBuffer content = new StringBuffer(); int j = 0,len; while((line = reader1.readLine()) != null){ len = line.length(); if(len < 1){ content.append("\n"); if(label.charAt(j) == '\n') ++j; continue; } StringBuffer tmp = new StringBuffer(); for(int i = 0; i < len; ++i){ tmp.append(line.charAt(i)); if(i == len - 1 || (j+1<label.length() && label.charAt(j+1) == 'B')){ tmp.append(" "); } ++j; } content.append(tmp.toString()+"\n"); } write("data/罗志鹏-1401210986.seg", content.toString(), "utf-16"); reader.close(); } public static void process(int flag) throws IOException{//0按70% 1按100% String path = ""; String encoding = "utf-16"; if(flag == 0){ path = "Train_utf16.seg"; // for(int i = 0 ; i < 8; ++i) //用于交叉验证,由于运行慢没使用 CRFFormat.read(path, encoding,0); } else{ path = "Test_utf16.seg"; CRFFormat.read(path, encoding,-1); //生成测试 path = "Train_utf16.seg"; CRFFormat.read(path, encoding,-2); //生成训练 } } public static void main(String[] args) throws IOException { crf2standard("data/output.txt","data/test_sent_"+0+".data", "utf-8"); // process(1); } }
import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; public class Crfeval { public static void score(String path, String encoding) throws IOException { File file = new File(path); BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream(file), encoding)); String line = null; int n = 0; StringBuffer error = new StringBuffer(); int wc_of_test = 0; int wc_of_gold = 0; int wc_of_correct = 0; boolean flag = false; while ((line = reader.readLine()) != null) { ++n; if(line.length() < 1){ continue; } String[] ss = line.split("\t"); if(!ss[2].equalsIgnoreCase(ss[3])){ flag = false; error.append(n+"\t"+line+"\n"); //错误数据 } if(ss[2].equalsIgnoreCase("B")){ wc_of_gold += 1; if(flag) wc_of_correct += 1; flag = true; } if(ss[3].equalsIgnoreCase("B")) wc_of_test += 1; } if(flag) wc_of_correct += 1; System.out.println("切分词数:" + wc_of_test ); System.out.println("文档总词数:" + wc_of_gold ); System.out.println("正确切分词数:" + wc_of_correct); float P = (float) (wc_of_correct*1.0/wc_of_test); // 查全率 float R = (float) (wc_of_correct*1.0/wc_of_gold); //查准率,召回率 float F1 = (2*P*R)/(P+R); //F1值 System.out.println("查全率:"+P+" 召回率:"+R+" F1:"+F1); CRFFormat.write("data/error.txt", error.toString(), "utf-8"); reader.close(); } public static void main(String[] args) throws IOException { String path = "data/output.txt"; score(path,"utf-8"); } }
import java.io.IOException; public class wordSeg { private static String train_file = "data\\train_"; private static String test_file = "data\\test_"; public static void train(String filePath){ String commandStr = "crf_learn -c 8 template "+filePath+" model"; Command.exeCmd(commandStr,"train"); } public static void test(String filePath){ String commandStr = "crf_test -m model "+filePath+" output.txt"; Command.exeCmd(commandStr,"test"); } public static void process(int flag) throws IOException{ CRFFormat.process(flag); //转换数据格式 0表示70%训练 1表示100%训练 for(int i = 0; i < 1; ++i){ train(train_file+i+".data"); //执行训练命令行 test(test_file+i+".data"); //执行测试命令行 //转换成作业要求的输出格式 CRFFormat.crf2standard("data/output.txt","data/test_sent_"+i+".data", "utf-8"); Crfeval.score("data/output.txt", "utf-8"); //评分 System.out.println("完成"); } } public static void main(String[] args) throws IOException { process(1); // 0表示70%训练 1表示100%训练 } }
数据+CRF++工具+java源码下载地址:http://download.csdn.net/detail/luozhipeng2011/8872241
本文固定链接: http://www.luozhipeng.com/?p=375
转载请注明: luozhipeng 2015-7-6 于 罗志鹏的BLOG 发表