基于CRF++的中文分词

作者:luozhipeng   发表日期:2015-7-6  浏览:7,591次

1. 概述

    中文分词是中文信息处理领域中的最重要的任务,它对于智能信息处理技术具有重要的意义。经过大量的研究,汉语分词技术取得了很高的精度。众多的研究者已经提出了许多方法,其中的条件随机场模型(CRF)受到越来越多的关注。条件随机场模型已因其性能优良的广泛使用,本文也将采用CRF进行中文分词。

    本任务做的是繁体中文分词,以语料的30%作为测试集,70%作为训练集,(注意不能随机选数据,或者隔行取,由于附近的句子相近或者相同,会导致结果偏高!)使用CRF++工具做出来的效果非常不错,F1值能达到95%上。

 

2. CRF++

 

2.1. 简述

    这学期选了一门机器学习课程,这次的课程任务是中文分词,在查阅了中文分词的相关论文后,发现CRF的分词表现非常棒,所以我选择了CRF++工具包。这次我使用的是CRF++的windows版本,开发语言为java。

 

2.2. 工具包下载

  1. 官网下载

    地址:http://crfpp.sourceforge.net

    其中有两种,一种是Linux下(带源码)的,一种是win32的,当然是在什么平台下用就下载什么版本了。

 

    2.CSDN

    地址:http://download.csdn.net/detail/luozhipeng2011/8854513

    我已经打包好了win32和Linux版本,为CRF++0.58。

 

2.3. 安装

  1. Windows版的无须安装,直接解压即可使用;
  2. Linux版本的安装方法是:

    i.解压到某目录下

    ii.打开控制台,将当前目录切换到解压目录

    iii.依次输入命令:

         ./configure

         make

         su

         make install

    注:需要root权限才能成功安装。

 

2.4. 工具包文件

 seg1

    doc文件夹:就是官方主页的内容。

    example文件夹:有四个任务的训练数据、测试数据和模板文件。

    sdk文件夹:CRF++的头文件和静态链接库。

    crf_learn.exe:CRF++的训练程序。

    crf_test.exe:CRF++的预测程序

    libcrfpp.dll:训练程序和预测程序需要使用的静态链接库。

    实际上,需要使用的就是crf_learn.exe,crf_test.exe和libcrfpp.dll,这三个文件。

 

2.5. 命令行格式

  1. 训练

    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

 

  1. 测试

    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

 

 

2.6. 文件格式

2.6.1. 训练文件

    下面是我分词的训练文件部分数据:

 seg2

    训练文件由若干个句子组成(可以理解为若干个训练样例),不同句子之间通过换行符分隔。每个句子可以有若干组标签,最后一组标签是标注,上图中有三列,即第一列和第二列都是已知的数据(特征),第三列是要预测的标注,以上面例子为例是,根据第一列的词语和和第二列的相关特征,预测第三列的标注。

 

2.6.2. 测试文件

    测试文件与训练文件是一样的,测试后会在后面加一列标注结果。如下图:

 seg3

 

2.6.3. 模板文件

    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指定列位置。

 seg4

    由上图中的部分数据,我们以中字来做模板分析,分析如下:

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

 

 

3. 运行过程与步骤

3.1. 实验环境

    分词工具: CRF++

    开发环境: Win7 32bit  i5 四核 2.99G内存

    开发语言: java

 

3.2. 实验步骤

3.2.1. 数据预处理

    由于本次任务所给数据不是CRF++能处理的格式,所以要先把数据格式转成CRF++能处理的格式。其中要注意的是原数据文件的编码是utf-16,而CRF++能处理的是utf-8编码文件,否则出现异常不会生成模型,还有数据的每列之间是由\t分隔的,所以转换时按CRF++的格式转换并保持为utf-8编码,这些细节方面需要多注意。本次任务把原训练数据划分成3:7,其中30%作为测试集,70%作为训练集。本来想交叉验证来调参数,但是训练时间较长最后只划分一次数据。

 

3.2.2. 特征选取

    我选取了两类特征,第一类就是文字本身,第二类是文字、数字、字母、结束标点符号、对称标点符号五种标签,其中要注意的是本次使用的数据的数字和字母都是全角字符,另外“一二三...十百...”也看做是数字,文字对应标签W,数字标签对应D,字母标签对应L,结束标点符号“"!@#$%^&*_+|\'?/:;,.,!¥……——:;,。?、"”对应S,对称标点符号对应P。由此生成的训练和测试数据格式如下:

                                        seg5       seg6

 

    第一列是文字本身。第二列文字类型,其中 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特征类似。

3.2.3. 运行程序及计算结果

运行后生成的结果文件如下:

 

其中最后一列就是预测的结果,可以和倒数第二列对比计算准确率和召回率。

精确率和召回率的计算方式:

正确率 = 正确划分词数/分割出来的词数

召回率 = 正确划分词数/文档总词数

F1计算方式:

F值 = 正确率 * 召回率 * 2 / (正确率 + 召回率)

最后一次测试结果如下:

 seg7

 

4. 结果分析与调优

4.1. 测试结果

 

结果如下表:

 

方法 准确率 召回率 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

 

从结果来看分词效果还是非常不错的,由于时间有限也还有很多待改进的地方。

 

4.2. 优化方案

  1. 在预测结果中我发现对英文的分词效果比较差,因为英文的训练语料太少,基本没法区分是否为一个单词,比如“Cease to struggle and you cease to live.”分词后可能把“to”给分开了,所以可以考虑加入英文外部语料进行训练。

 

  1. 另外我认为停用词对分词可能也有一定的影响,可以把常用停用词也作为一组特征进行训练。

 

 

5. 总结

    本次的机器学习项目作业是中文分词,得知作业内容后我查阅了相关论文,最初搜索了一些深度学习在这方面的论文,其中文献[7]中就用了深度学习来解决中文分词问题,总得来说效果还是不错的,但是短时间我难以实现,就没考虑使用深度学习方法了。后来发现许多论文都是用CRF来做的,并且效果很好,并且有CRF++这个现成的工具可用,所以最后我选择了CRF。在做的过程中我尝试了很多方法,但是由于训练时间比较长,每次都是增量式修改,这样很可能最后的结果并非最好的结果。另外机器内存不足(2.99G),能跑的特征数量有限,这也限制了尝试更多特征的可能。由于时间有限,还有很多可以优化的地方没有去尝试,以后有时间深入研究。

 

附源码:

  • java调用命令行工具类

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 发表

上一篇: :下一篇
返回顶部