While a Hidden Markov Model is a sequential extension to the Nave Bayes Model, Conditional Random Fields can be understood as a sequential extension to the Maximum Entropy Model.
不过我们还是要从安装CRF++工具包说起,在Linux或者Mac OS系统下,下载C++源代码安装包(这里用的是 CRF++-0.58.tar.gz )之后,依然是 “configure & make & (sudo) make install",安装完毕之后,可以cd python进入到其同样用SWIG生成的Python工具包下,安装python包:python setup.py build & (sudo) python setup.py install。安装完毕之后,可以在python解释器下测试,是否能成功import CRFPP,如果ok,则准备工作就绪。
上一节我们利用最大熵模型工具包里自带的词性标注工具进行的中文分词,稍微有些曲折,这一节我们依然利用CRF++ example里的样例进行测试,不过好处是,CRF++ example里有个seg目录,这个seg目录对应的是一个日文分词的样例,正好可以套用到我们的中文分词中来。在安装包目录下,cd example, cd seg目录后,有4个文件:
关于CRF++中特征模板的说明和举例,请大家参考官方文档上的“Preparing feature templates”这一节,而以下部分的说明拿上述日文分词数据举例。在特征模板文件中,每一行(如U00:%x[-2,0])代表一个特征,而宏“%x[行位置,列位置]”则代表了相对于当前指向的token的行偏移和列的绝对位置,以上述训练集为例,如果当前扫描到“新 k I”这一行,
毎 k B
日 k I
新 k I <== 扫描到这一行,代表当前位置
聞 k I
社 k I
特 k B
別 k I
顧 k B
問 k I
4 n B
CRF++里将特征分成两种类型,一种是Unigram的,“U”起头,另外一种是Bigram的,“B”起头。对于Unigram的特征,假如一个特征模板是"U01:%x[-1,0]", CRF++会自动的生成一组特征函数(func1 ... funcN) 集合:
func1 = if (output = B and feature="U01:日") return 1 else return 0
func2 = if (output = I and feature="U01:日") return 1 else return 0
....
funcXX = if (output = B and feature="U01:問") return 1 else return 0
funcXY = if (output = I and feature="U01:問") return 1 else return 0
def character_tagging(input_file, output_file):
input_data = codecs.open(input_file, 'r', 'utf-8')
output_data = codecs.open(output_file, 'w', 'utf-8')
for line in input_data.readlines():
word_list = line.strip().split()
for word in word_list:
if len(word) == 1:
output_data.write(word + "\tS\n")
else:
output_data.write(word[0] + "\tB\n")
for w in word[1:len(word)-1]:
output_data.write(w + "\tM\n")
output_data.write(word[len(word)-1] + "\tE\n")
output_data.write("\n")
input_data.close()
output_data.close()
只需要执行“python make_crf_train_data.py ./icwb2-data/training/msr_training.utf8 msr_training.tagging4crf.utf8” 即可得到CRF++要求的格式的训练文件msr_training.tagging4crf.utf8,样例如下:
“ S
人 B
们 E
常 S
说 S
生 B
活 E
是 S
一 S
部 S
...
def character_split(input_file, output_file):
input_data = codecs.open(input_file, 'r', 'utf-8')
output_data = codecs.open(output_file, 'w', 'utf-8')
for line in input_data.readlines():
for word in line.strip():
word = word.strip()
if word:
output_data.write(word + "\tB\n")
output_data.write("\n")
input_data.close()
output_data.close()
执行“python make_crf_test_data.py ./icwb2-data/testing/msr_test.utf8 msr_test4crf.utf8”即可得到可用于CRF++测试的测试语料msr_test4crf.utf8,样例如下:
扬 B
帆 B
远 B
东 B
做 B
与 B
中 B
国 B
合 B
作 B
...
=== SUMMARY:
=== TOTAL INSERTIONS: 1412
=== TOTAL DELETIONS: 1305
=== TOTAL SUBSTITUTIONS: 2449
=== TOTAL NCHANGE: 5166
=== TOTAL TRUE WORD COUNT: 106873
=== TOTAL TEST WORD COUNT: 106980
=== TOTAL TRUE WORDS RECALL: 0.965
=== TOTAL TEST WORDS PRECISION: 0.964
=== F MEASURE: 0.964
=== OOV Rate: 0.026
=== OOV Recall Rate: 0.647
=== IV Recall Rate: 0.974
### msr_test4crf.tag2word.utf8 1412 1305 2449 5166 106873 106980 0.965 0.964 0.964 0.026 0.647 0.974
这次我们获得了一个准确率,召回率以及F值都在96%以上的结果,相对于前面几节的测试结果,这个CRF字标注分词结果还相对不错。不过是不是感觉上面的步骤有些繁琐,有没有一次到位的CRF分词器,这里我们同样提供一个CRF分词脚本 crf_segmenter.py ,利用CRF++的python工具包,做到一次输入,一次输出:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: 52nlpcn@gmail.com
# Copyright 2014 @ YuZhen Technology
#
# CRF Segmenter based character tagging:
# 4 tags for character tagging: B(Begin), E(End), M(Middle), S(Single)
import codecs
import sys
import CRFPP
def crf_segmenter(input_file, output_file, tagger):
input_data = codecs.open(input_file, 'r', 'utf-8')
output_data = codecs.open(output_file, 'w', 'utf-8')
for line in input_data.readlines():
tagger.clear()
for word in line.strip():
word = word.strip()
if word:
tagger.add((word + "\to\tB").encode('utf-8'))
tagger.parse()
size = tagger.size()
xsize = tagger.xsize()
for i in range(0, size):
for j in range(0, xsize):
char = tagger.x(i, j).decode('utf-8')
tag = tagger.y2(i)
if tag == 'B':
output_data.write(' ' + char)
elif tag == 'M':
output_data.write(char)
elif tag == 'E':
output_data.write(char + ' ')
else: # tag == 'S'
output_data.write(' ' + char + ' ')
output_data.write('\n')
input_data.close()
output_data.close()