1. 安装(基于mac)

首先是CRF++的安装,下载相应的包(貌似要翻墙),我这里把两个包都下载下来了。

第一个包:
命名实体识别—CRF++地名识别
下面的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

原因可能有:

  1. 没有切换到chunking目录下
  2. 在mac下最好是把系统调整为显示所有文件的扩展名,因为有的时候mac下新建的文件会隐藏.rtf或者.txt
  3. 一定要在新建文本后在菜单栏里:格式—>制作纯文本

测试过程:
训练完成后,采用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,以后再学习。

命名实体初探,路漫漫。。

相关文章: