1. 安装(基于mac)
首先是CRF++的安装,下载相应的包(貌似要翻墙),我这里把两个包都下载下来了。
第一个包:
下面的crf++训练学习和测试都是在这个包下进行的,这里不需要对这个包进行安装,只需要切换到该目录下就可以进行操作,注意是在终端命令行下(下面有介绍)。
第二个包:
这里没有截完,我是利用这个包进行的python接口的安装。
1.1 crf++的python接口安装
在下载好上述的第二个包之后,首先切换至该目录下,依次执行以下命令:
$ cd /Users/lilong/Desktop/CRF++-0.58_2
$ ./configure
$ make
$ make install
中间出现了各种warning,我这里没有管,就一路执行下去了,最后测试了一下:
>>> import CRFPP
>>>
没有报错,说明安装好了。
1.2 基于crf++训练学习和测试
训练和测试,这里采用的是第一个包。但是需要注意:在example中chunking文件夹下有4个文件:exec.sh;template;test.data;train.data。这里将crf_learn.exe;crf_test.exe;libcrfpp.dll三个文件复制到这个文件夹(chunking)底下:然后切换至chunking文件夹下执行以下命令:
训练:
crf_learn -f 3 -c 4.0 -p 8 template.txt train.txt model
这里要注意template.txt是自己定义的特征函数模版,train.txt是训练集,model是保存的训练好的模型。
测试:
crf_test -m model test.txt > test.rst
这里的test.txt 是测试文件,test.rst是标记好的文本,下面会有详细介绍。
2. 语料数据的预处理
crf++的训练数据要求一定的格式,一般每行一个token,一句话由多行token组成,多个句子之间用空行分开,其中每行又分成多列,除最后一列外,其他列表示特征,因此一般最少两列,最后一列表示要预测的标签,这里采用的标签体系是:“B”,“E”,“M”,“S”,“O”。下面的NER只采用字符这一个维度作为特征,例如:
我 O
中 O
午 O
要 O
去 O
北 B
京 M
饭 M
店 E
, O
下 O
午 O
去 O
中 B
山 M
公 M
园 E
语料数据采用的是1998年人民日报分词数据集,部分数据形式如下:
19980101-01-001-001/m 迈向/v 充满/v 希望/n 的/u 新/a 世纪/n ——/w 一九九八年/t 新年/t 讲话/n (/w 附/v 图片/n 1/m 张/q )/w
19980101-01-001-002/m 中共中央/nt 总书记/n 、/w 国家/n 主席/n 江泽民/nr
19980101-01-001-003/m (/w 一九九七年/t 十二月/t 三十一日/t )/w
19980101-01-001-004/m 12月/t 31日/t ,/w 中共中央/nt 总书记/n 、/w 国家/n 主席/n 江泽民/nr 发表/v 1998年/t 新年/t 讲话/n 《/w 迈向/v 充满/v 希望/n 的/u 新/a 世纪/n 》/w 。/w (/w 新华社/nt 记者/n 兰红光/nr 摄/Vg )/w
19980101-01-001-005/m 同胞/n 们/k 、/w 朋友/n 们/k 、/w 女士/n 们/k 、/w 先生/n 们/k :/w
可以看出是已经分词并标注好词性的数据集。可以把其中标注为“ns”的部分作为地名识别语料。
下面是对人民日报语料进行数据处理,并切割一部分作为测试集进行验证:
#coding=utf8
# 进行每行的标注转换
def tag_line(words, mark):
chars = []
tags = []
temp_word = '' #用于合并组合词
for word in words:
word = word.strip('\t ')
#print('word:',word)
if temp_word == '':
bracket_pos = word.find('[')
#print('bracket_pos:',bracket_pos)
w, h = word.split('/')
if bracket_pos == -1:
if len(w) == 0: continue
chars.extend(w)
if h == 'ns':
tags += ['S'] if len(w) == 1 else ['B'] + ['M'] * (len(w) - 2) + ['E']
else:
tags += ['O'] * len(w)
else:
w = w[bracket_pos+1:]
temp_word += w
else:
bracket_pos = word.find(']')
w, h = word.split('/')
if bracket_pos == -1:
temp_word += w
else:
w = temp_word + w
h = word[bracket_pos+1:]
temp_word = ''
if len(w) == 0: continue
chars.extend(w)
if h == 'ns':
tags += ['S'] if len(w) == 1 else ['B'] + ['M'] * (len(w) - 2) + ['E']
else:
tags += ['O'] * len(w)
#print(chars)
#print(tags)
assert temp_word == ''
return (chars, tags)
# 加载数据
def corpusHandler(corpusPath):
import os
root = os.path.dirname(corpusPath)
with open(corpusPath) as corpus_f, \
open(os.path.join(root, 'train.txt'), 'w',encoding='utf-8') as train_f, \
open(os.path.join(root, 'test.txt'), 'w',encoding='utf-8') as test_f:
pos = 0
for line in corpus_f:
line = line.strip('\r\n\t ')
#print('line:',line)
if line == '': continue
isTest = True if pos % 5 == 0 else False # 抽样20%作为测试集使用
words = line.split()[1:]
if len(words) == 0: continue
#print('words:',words)
line_chars, line_tags = tag_line(words, pos)
saveObj = test_f if isTest else train_f
for k, v in enumerate(line_chars):
saveObj.write(v + '\t' + line_tags[k] + '\n') # 这里要注意数据每行的格式
saveObj.write('\n')
pos += 1
if __name__ == '__main__':
corpusHandler('people-daily.txt')
运行后生成两个文件:train.txt和test.txt
形式如下:
训练集:
中 O
共 O
中 O
央 O
总 O
书 O
记 O
、 O
国 O
家 O
主 O
席 O
江 O
泽 O
民 O
( O
一 O
九 O
九 O
七 O
年 O
十 O
二 O
月 O
测试集:
迈 O
向 O
充 O
满 O
希 O
望 O
的 O
新 O
世 O
纪 O
— O
— O
一 O
九 O
九 O
八 O
年 O
新 O
年 O
讲 O
话 O
( O
附 O
图 O
片 O
这样就把语料数据处理成了crf++要求的格式。
3. 训练和测试
这里在训练之前还要编辑一个特征模版文件,特征模板的权威解释。Unigram和Bigram是特征模板的类型。U00:%x[-2,0]中,U表示类型为Unigram,00表示特征的id,%x[-2,0]表示x(在这里为字)的位置,-2表示x的行偏移,0表示x的列偏移。
用pku语料为例
迈 B
向 E
充 B
满 E
希 B
当扫描到“充”时,可以得到:
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] ==>充/满
这里我的特征模版template.txt是:
# Unigram
U01:%x[-1,0]
U02:%x[0,0]
U03:%x[1,0]
U04:%x[2,0]
U05:%x[-2,0]
U06:%x[0,0]/%x[-1,0]
U07:%x[0,0]/%x[1,0]
U08:%x[-1,0]/%x[-2,0]
U09:%x[1,0]/%x[2,0]
U10:%x[-1,0]/%x[1,0]
# Bigram
B
好了,下面开始训练:将crf_learn.exe;crf_test.exe;libcrfpp.dll三个文件复制到这个文件夹(chunking)下,同时还要把template.txt和train.txt文件拷贝到chunking文件夹下。
准备好之后,执行以下的命令:
$ crf_learn -f 3 -c 4.0 -p 8 template.txt train.txt model
运行结果:
CRF++: Yet Another CRF Tool Kit
Copyright (C) 2005-2013 Taku Kudo, All rights reserved.
reading training data: 100.. 200.. 300.. 400.. 500.. 600.. 700.. 800.. 900.. 1000.. 1100.. 1200.. 1300.. 1400.. 1500.. 1600.. 1700.. 1800.. 1900.. 2000.. 2100.. 2200.. 2300.. 2400.. 2500.. 2600.. 2700.. 2800.. 2900.. 3000.. 3100.. 3200.. 3300.. 3400.. 3500.. 3600.. 3700.. 3800.. 3900.. 4000.. 4100.. 4200.. 4300.. 4400.. 4500.. 4600.. 4700.. 4800.. 4900.. 5000.. 5100.. 5200.. 5300.. 5400.. 5500.. 5600.. 5700.. 5800.. 5900.. 6000.. 6100.. 6200.. 6300.. 6400.. 6500.. 6600.. 6700.. 6800.. 6900.. 7000.. 7100.. 7200.. 7300.. 7400.. 7500.. 7600.. 7700.. 7800.. 7900.. 8000.. 8100.. 8200.. 8300.. 8400.. 8500.. 8600.. 8700.. 8800.. 8900.. 9000.. 9100.. 9200.. 9300.. 9400.. 9500.. 9600.. 9700.. 9800.. 9900.. 10000.. 10100.. 10200.. 10300.. 10400.. 10500.. 10600.. 10700.. 10800.. 10900.. 11000.. 11100.. 11200.. 11300.. 11400.. 11500.. 11600.. 11700.. 11800.. 11900.. 12000.. 12100.. 12200.. 12300.. 12400.. 12500.. 12600.. 12700.. 12800.. 12900.. 13000.. 13100.. 13200.. 13300.. 13400.. 13500.. 13600.. 13700.. 13800.. 13900.. 14000.. 14100.. 14200.. 14300.. 14400.. 14500.. 14600.. 14700.. 14800.. 14900.. 15000.. 15100.. 15200.. 15300.. 15400.. 15500..
Done!12.33 s
Number of sentences: 15586
Number of features: 1709476
Number of thread(s): 8
Freq: 3
eta: 0.00010
C: 4.00000
shrinking size: 20
iter=0 terr=0.98787 serr=1.00000 act=1709476 obj=2055386.56193 diff=1.00000
iter=1 terr=0.03155 serr=0.44360 act=1709476 obj=812515.95742 diff=0.60469
iter=2 terr=0.03155 serr=0.44360 act=1709476 obj=266475.51913 diff=0.67204
iter=3 terr=0.03155 serr=0.44360 act=1709476 obj=253566.17203 diff=0.04844
iter=4 terr=0.03155 serr=0.44360 act=1709476 obj=205180.02766 diff=0.19082
iter=5 terr=0.65993 serr=0.99891 act=1709476 obj=5153682.51595 diff=24.11786
...
...
iter=311 terr=0.00016 serr=0.00603 act=1709476 obj=2873.33022 diff=0.00015
iter=312 terr=0.00016 serr=0.00603 act=1709476 obj=2872.83621 diff=0.00017
iter=313 terr=0.00016 serr=0.00597 act=1709476 obj=2872.47546 diff=0.00013
iter=314 terr=0.00016 serr=0.00603 act=1709476 obj=2872.00280 diff=0.00016
iter=315 terr=0.00016 serr=0.00603 act=1709476 obj=2871.90088 diff=0.00004
iter=316 terr=0.00016 serr=0.00603 act=1709476 obj=2871.78482 diff=0.00004
iter=317 terr=0.00016 serr=0.00597 act=1709476 obj=2871.70760 diff=0.00003
Done!1247.94 s
然后就会生成model文件,用于下面的标记和测试。
我这里出现报错信息(mac):
CRF++: Yet Another CRF Tool Kit Copyright (C) 2005-2013 Taku Kudo, All rights reserved. encoder.cpp(340) [feature_index.open(templfile, trainfile)] feature_index.cpp(135) [ifs] open failed: template.txt
原因可能有:
- 没有切换到chunking目录下
- 在mac下最好是把系统调整为显示所有文件的扩展名,因为有的时候mac下新建的文件会隐藏.rtf或者.txt
- 一定要在新建文本后在菜单栏里:格式—>制作纯文本
测试过程:
训练完成后,采用crf_test调用生成的model就能进行标记,命令如下:
$ crf_test -m model test.txt > test.rst
标记结果如下:
迈 O O
向 O O
充 O O
满 O O
希 O O
望 O O
的 O O
新 O O
世 O O
纪 O O
— O O
— O O
一 O O
九 O O
九 O O
八 O O
年 O O
新 O O
年 O O
讲 O O
话 O O
( O O
附 O O
图 O O
生成test.rst会很快,几乎是立刻生成的,有了测试数据的标记结果,下面就可以计算模型在测试集上的效果,主要是计算P、R、F1参数:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def f1(path):
with open(path) as f:
all_tag = 0 #记录所有的标记数
loc_tag = 0 #记录真实的地理位置标记数
pred_loc_tag = 0 #记录预测的地理位置标记数
correct_tag = 0 #记录正确的标记数
correct_loc_tag = 0 #记录正确的地理位置标记数
# 地理命名实体标记
states = ['B', 'M', 'E', 'S']
#i=0
for line in f:
#i=i+1
line = line.strip()
if line == '': continue
_, r, p = line.split()
#print(_, r, p)
all_tag += 1
if r == p:
correct_tag += 1
if r in states:
correct_loc_tag += 1
if r in states:
loc_tag += 1
if p in states:
pred_loc_tag += 1
#if i==50: break # 测试用
loc_P = 1.0 * correct_loc_tag/pred_loc_tag
loc_R = 1.0 * correct_loc_tag/loc_tag
print('loc_P:{0}, loc_R:{1}, loc_F1:{2}'.format(loc_P, loc_R, (2*loc_P*loc_R)/(loc_P+loc_R)))
def load_model(path):
import os, CRFPP
# -v 3: access deep information like alpha,beta,prob
# -nN: enable nbest output. N should be >= 2
if os.path.exists(path):
return CRFPP.Tagger('-m {0} -v 3 -n2'.format(path))
return None
def locationNER(text):
tagger = load_model('./model')
# 利用训练好的模型标记每个字
for c in text:
tagger.add(c)
result = []
# parse and change internal stated as 'parsed'
tagger.parse()
#print(tagger)
word = ''
print(tagger.size(),tagger.xsize())
for i in range(0, tagger.size()): # tagger.size:要预测的句子的字数
for j in range(0, tagger.xsize()): # tagger.xsize:特征列的个数
ch = tagger.x(i, j)
tag = tagger.y2(i)
#print(ch,tag)
if tag == 'B':
word = ch
elif tag == 'M':
word += ch
elif tag == 'E':
word += ch
result.append(word)
elif tag == 'S':
word = ch
result.append(word)
return result
if __name__ == '__main__':
f1('test.rst')
# 测试
text = '我中午要去北京饭店,下午去中山公园,晚上回亚运村。'
print(text, locationNER(text), sep='==> ')
text = '我去回龙观,不去南锣鼓巷'
print(text, locationNER(text), sep='==> ')
text = '打的去北京南站地铁站'
print(text, locationNER(text), sep='==> ')
运行结果:
loc_P:0.9185516150051196, loc_R:0.8470386266094421, loc_F1:0.8813468494618855
25 1
我中午要去北京饭店,下午去中山公园,晚上回亚运村。==> ['北京饭店', '中山公园', '亚运村']
12 1
我去回龙观,不去南锣鼓巷==> []
10 1
打的去北京南站地铁站==> ['北京']
可以看到针对一般的场景能很好的进行识别,但是对于“回龙观”,“北京南站”,“南锣鼓巷”效果不好,可以改进的方法有:
- 扩展语料,改进模型(调整分词算法,加入词性特征)
- 整理地理位置词库。在识别时,先进行词库匹配,再采用模型发现。
BiLSTM-CRF:这个算法貌似才是现在segment、NER、词性标注的主流算法。常用的数据是:pku、msr,以后再学习。
命名实体初探,路漫漫。。