使用CRF++进行分词

安装CRF++

在这里下载CRF++源码

1
2
3
4
5
6
7
8
tar zxvf CRF++-0.58.tar.gz
cd CRF++-0.58
./configure
make # 如果path.h报错 加上#inlcude<iostream>头文件
sudo make install
cd python
python setup.py build
sudo python setup.py install

然后进入python,尝试import CRFPP可能出现问题:UnboundLocalError: local variable 'fp' referenced before assignment,原因就是fp没有定义,添加fp = None。改动之后再次执行又出现问题:ImportError: No module named _CRFPP,那么就对ImportException进行处理,截至到这里,把CRFPP.py中的swig_import_helper函数改动如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def swig_import_helper():
from os.path import dirname
import imp
fp = None
try:
fp, pathname, description = imp.find_module('_CRFPP', [dirname(__file__)])
_mod = imp.load_module('_CRFPP', fp, pathname, description)
except ImportError:
import _CRFPP
return _CRFPP
finally:
<!-- more -->
if fp is not None: fp.close()
return _mod

然后再运行,又会出现问题:ImportError: libcrfpp.so.0: cannot open shared object file: No such file or directory,这次回到shell执行:

1
sudo ln -s /usr/local/lib/libcrfpp.so.0 /usr/lib

此时再次import,成功执行。

下载人民日报语料库

点击下载
下载得到的语料已经经过分词和词性标注,形如:

19980101-01-001-005/m 同胞/n 们/k 、/w 朋友/n 们/k 、/w 女士/n 们/k 、/w 先生/n 们/k :/w
19980101-01-001-006/m 在/p 1998年/t 来临/v 之际/f ,/w 我/r 十分/m 高兴/a 地/u 通过/p [中央/n 人民/n 广播/vn 电台/n]nt 、/w [中国/ns 国际/n 广播/vn 电台/n]nt 和/c [中央/n 电视台/n]nt ,/w 向/p 全国/n 各族/r 人民/n ,/w 向/p [香港/ns 特别/a 行政区/n]ns 同胞/n 、/w 澳门/ns 和/c 台湾/ns 同胞/n 、/w 海外/s 侨胞/n ,/w 向/p 世界/n 各国/r 的/u 朋友/n 们/k ,/w 致以/v 诚挚/a 的/u 问候/vn 和/c 良好/a 的/u 祝愿/vn !/w

对预标注预料进行预处理,使用下面的Python脚本“pre.py”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#coding=utf8

import sys

home_dir = "./"
def splitWord(words):
uni = words.decode('utf-8')
li = list()
for u in uni:
li.append(u.encode('utf-8'))
return li

#4 tag

#S/B/E/M
def get4Tag(li):
length = len(li)
#print length
if length == 1:
return ['S']
elif length == 2:
return ['B','E']
elif length > 2:
li = list()
li.append('B')
for i in range(0,length-2):
li.append('M')
li.append('E')
return li
#6 tag
#S/B/E/M/M1/M2
def get6Tag(li):
length = len(li)
#print length
if length == 1:
return ['S']
elif length == 2:
return ['B','E']
elif length == 3:
return ['B','M','E']
elif length == 4:
return ['B','M1','M','E']
elif length == 5:
return ['B','M1','M2','M','E']
elif length > 5:
li = list()
li.append('B')
li.append('M1')
li.append('M2')
for i in range(0,length-4):
li.append('M')
li.append('E')
return li

def saveDataFile(trainobj,testobj,isTest,word,handle,tag):
if isTest:
saveTrainFile(testobj,word,handle,tag)
else:
saveTrainFile(trainobj,word,handle,tag)

def saveTrainFile(fiobj,word,handle,tag):
if len(word) > 0:
wordli = splitWord(word)
if tag == '4':
tagli = get4Tag(wordli)
if tag == '6':
tagli = get6Tag(wordli)
for i in range(0,len(wordli)):
w = wordli[i]
h = handle
t = tagli[i]
fiobj.write(w + '\t' + h + '\t' + t + '\n')
else:
#print 'New line'
fiobj.write('\n')

#B,M,M1,M2,M3,E,S
def convertTag(tag):
fiobj = open( home_dir + 'people-daily.txt','r')
trainobj = open( home_dir + tag + '.train.data','w' )
testobj = open( home_dir + tag + '.test.data','w')

arr = fiobj.readlines()
i = 0
for a in arr:
i += 1
a = a.strip('\r\n\t ')
print "debug_a:",a
if a=="":continue
words = a.split(" ")
test = False
if i % 10 == 0:
test = True
for word in words:
print "---->", word
word = word.strip('\t')
if len(word) > 0:
i1 = word.find('[')
if i1 >= 0:
word = word[i1+1:]
i2 = word.find(']')
if i2 > 0:
w = word[:i2]
word_hand = word.split('/')
print "----",word
w,h = word_hand
#print w,h
if h == 'nr': #ren min
#print 'NR',w
if w.find('·') >= 0:
tmpArr = w.split('·')
for tmp in tmpArr:
saveDataFile(trainobj,testobj,test,tmp,h,tag)
continue
if h != 'm':
saveDataFile(trainobj,testobj,test,w,h,tag)
if h == 'w':
saveDataFile(trainobj,testobj,test,"","",tag) #split

trainobj.flush()
testobj.flush()

if __name__ == '__main__':
if len(sys.argv) < 2:
print 'tag[6,4] convert raw data to train.data and tag.test.data'
else:
tag = sys.argv[1]
convertTag(tag)

这里用到的分词方法是由字构词(基于字标注)的分词方法(Character-based tagging)。
该方法由N. Xue(薛念文) 和 S. Converse 提出, 首篇论文发表在2002年第一届国际计算语言学学会(ACL)汉语特别兴趣小组 SIGHAN (http://www.sighan.org/) 组织的汉语分词评测研讨会上[Xue and Converse, 2002]。基本思想:将分词过程看作是字的分类问题:每个字在构造一个特定的词语时都占据着一个确定的构词位置(即词位)。一般情况下,每个字只有4个词位:词首(B)、词中(M)、词尾(E)和单独成词(S) 。
该脚本接受一个参数,该参数只能是6或者4,参数为4的时候将单字对应到上述4类标签,参数为6的时候实际上是对上述4tags的一种扩展,词中字可能有$$$M_1/M_2/M$$$三种标签,三个字的词标记为$$$BME$$$,四个字标记为$$$BM_1ME$$$,五个字标记为$$$BM_1M_2ME$$$,以此类推。这是一种拓展的思路,条件随机场只是去预测标签,究竟设置什么样的标签,标签有什么意义,是要靠人工赋予。
得到下面的格式,第一列是单字,第二列是词性信息,第三列是基于字标注的分词信息:

迈    v    B
向    v    E
充    v    B
满    v    E
希    n    B
望    n    E
的    u    S
新    a    S
世    n    B
纪    n    E
—    w    B
—    w    E
一    t    B
九    t    M1
九    t    M2
八    t    M
年    t    E
新    t    B
年    t    E
讲    n    B
话    n    E
(    w    S
附    v    S
图    n    B
片    n    E
张    q    S
)    w    S

使用CRF++进行训练和预测

自定义特征模板template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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

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

训练模型

训练

1
crf_learn template_file train_file model_file
这个训练过程的时间、迭代次数等信息会输出到控制台上(感觉上是crf_learn程序的输出信息到标准输出流上了),如果想保存这些信息,我们可以将这些标准输出流到文件上,命令格式如下:
1
crf_learn template_file train_file model_file >> train_info_file

输出的信息如下。其中各个域的信息是:

  • iter: 迭代处理的次数
  • terr: 标记错误率。
  • serr: 句子错误率。
  • obj: 目标函数的值。
  • diff: 目标函数相对上一次的相对变化率。
1
2
3
4
5
6
7
8
9
10
Number of sentences: 155657
Number of features: 16586178
Number of thread(s): 1
Freq: 1
eta: 0.00010
C: 1.00000
shrinking size: 20
iter=0 terr=0.67485 serr=1.00000 act=16586178 obj=2754679.67614 diff=1.00000
iter=1 terr=0.39541 serr=0.67567 act=16586178 obj=2148688.44746 diff=0.21999
iter=2 terr=0.39131 serr=0.67553 act=16586178 obj=1564786.70817 diff=0.27175

对于其中的参数,有四个主要的参数可以调整:

  1. -a CRF-L2 or CRF-L1

    规范化算法选择。默认是CRF-L2。一般来说L2算法效果要比L1算法稍微好一点,虽然L1算法中非零特征的数值要比L2中大幅度的小。

  2. -c float

    这个参数设置CRF的hyper-parameter。c的数值越大,CRF拟合训练数据的程度越高。这个参数可以调整过度拟合和不拟合之间的平衡度。这个参数可以通过交叉验证等方法寻找较优的参数。

  3. -f NUM

    这个参数设置特征的cut-off threshold。CRF++使用训练数据中至少NUM次出现的特征。默认值为1。当使用CRF++到大规模数据时,只出现一次的特征可能会有几百万,这个选项就会在这样的情况下起到作用。

  4. -p NUM

    如果电脑有多个CPU,那么那么可以通过多线程提升训练速度。NUM是线程数量。

测试

1
crf_test -m model_file test_files

有两个参数-v和-n都是显示一些信息的,-v可以显示预测标签的概率值,-n可以显示不同可能序列的概率值,对于准确率,召回率,运行效率,没有影响,这里不说明了。

与crf_learn类似,输出的结果放到了标准输出流上,而这个输出结果是最重要的预测结果信息(测试文件的内容+预测标注),同样可以使用重定向,将结果保存下来,命令行如下。

1
crf_test -m model_file test_files >> result_file

评估模型

python脚本“score.py”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys

if __name__=="__main__":
try:
file = open(sys.argv[1], "r")
except:
print "result file is not specified, or open failed!"
sys.exit()

wc_of_test = 0
wc_of_gold = 0
wc_of_correct = 0
flag = True

for l in file:
if l=='\n': continue

_, _, g, r = l.strip().split()

if r != g:
flag = False

if r in ('E', 'S'):
wc_of_test += 1
if flag:
wc_of_correct +=1
flag = True

if g in ('E', 'S'):
wc_of_gold += 1

print "WordCount from test result:", wc_of_test
print "WordCount from golden data:", wc_of_gold
print "WordCount of correct segs :", wc_of_correct

#查全率
P = wc_of_correct/float(wc_of_test)
#查准率,召回率
R = wc_of_correct/float(wc_of_gold)

print "P = %f, R = %f, F-score = %f" % (P, R, (2*P*R)/(P+R))

然后运行python score.py test-info.txt,有如下输出:

1
2
3
4
WordCount from test result: 102690
WordCount from golden data: 102952
WordCount of correct segs : 101950
P = 0.992794, R = 0.990267, F-score = 0.991529`

本站总访问量