聚合国内IT技术精华文章,分享IT技术精华,帮助IT从业人士成长

条件随机场(CRF)及CRF++安装使用

2019-10-23 23:30 浏览: 3590271 次 我要评论(0 条) 字号:

CRF简介

CRF是用来标注和划分序列结构数据的概率化结构模型。言下之意,就是对于给定的输出,标识序列Y和观测序列X,条件随机场通过定义条件概率P(Y | X),而不是联合概率分布P(X, Y)来描述模型。

设G = (V, E)为一个无向图,V为结点集合,E为无向边的结合。Y = {Yv | v ∈ V},即V中的每个结点对应于一个随机变量Yv,其取值范围为可能的标记集合{y}。如果以观察序列X为条件,每一个随机变量Yv都满足以下马尔可夫特性:p(Yv | X, Yw, w ≠ v) = p(Yv | X, Yw, w ~ v),其中,w ~ v表示两个结点在图G中是邻近结点。那么(X,Y)为一个条件随机场。

从定义中看出:CRF考虑一件东西,不但要考虑自身,还要考虑周围的情况。举个例子,我们做命名实体识别,例句:“Google的总部在硅谷”。我们知道地址是“硅谷”,其他位置的词对我们识别“硅谷”有啥帮助呢?例如,“硅谷”前面是“在”,是不是这个字后面经常接地址呢?“在”前面的词是不是应该是名词?这样的综合考虑,就是CRF中的特征选择或者叫特征模板。简要的说,CRF算法,需要解决三个问题:

  • 特征的选择。在CRF中,很重要的工作就是找特征函数,然后利用特征函数来构建特征方程。在自然语言处理领域,特征函数主要是指一个句子 s,词在句子中的位置 i,当前词的标签 l_{i},前一个词的标签 l_{i-1}。
  • 参数训练。在每一个特征函数之前,都有一个参数,也就是训练它们的权重。CRF的参数训练,可以采用梯度下降法。
  • 解码。解码问题,如果来一个句子,遍历所有可能的分割,会导致计算量过大。因此,可以采用类似viterbi这样的动态规划算法,来提高效率。

CRF++的安装

CRF++是著名的条件随机场的开源工具,也是目前综合性能最佳的CRF工具。

Windows下的安装

在Windows下的安装很简单,其实严格来讲不能说是安装。我们解压我们下载的压缩包文件到某一个目录下面。你可能会得到如下所示的文件,(版本不同,可能会有所不同。)

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

  • doc文件夹:就是官方主页的内容
  • example文件夹:有四个任务的训练数据(data)、测试数据(train.data)和模板文件(template),还有一个执行脚本文件exec.sh。
  • sdk文件夹:CRF++的头文件和静态链接库。
  • exe:CRF++的训练程序
  • exe:CRF++的测试程序
  • dll:训练程序和测试程序需要使用的静态链接库。

Linux下的安装

tar -zxvf CRF++-0.58.tar.gz
cd CRF++-0.58
./configure
make
sudo make install

默认安装的位置为:/usr/local/bin

安装Python包(此部分可省略):

python setup.py build
python setup.py install

import CRFPP 直接向后报如下错误:

>>> import CRFPP
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/qw/anaconda3/lib/python3.7/site-packages/CRFPP.py", line 26, in <module>
    _CRFPP = swig_import_helper()
  File "/home/qw/anaconda3/lib/python3.7/site-packages/CRFPP.py", line 22, in swig_import_helper
    _mod = imp.load_module('_CRFPP', fp, pathname, description)
  File "/home/qw/anaconda3/lib/python3.7/imp.py", line 242, in load_module
    return load_dynamic(name, filename, file)
  File "/home/qw/anaconda3/lib/python3.7/imp.py", line 342, in load_dynamic
    return _load(spec)
ImportError: libcrfpp.so.0: cannot open shared object file: No such file or directory

解决方案:

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

CRF++的使用

命令行使用

训练

./rf_learn template_file train_file model_file

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

./crf_learn template_file train_file model_file >> train_info_file

从上面可以看到,训练时需要传入的参数是template_file、train_file这两个文件,输出model_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是线程数量。

更详细介绍:

➜  bin ./crf_learn
CRF++: Yet Another CRF Tool Kit
Copyright (C) 2005-2013 Taku Kudo, All rights reserved.

Usage: ./crf_learn [options] files
 -f, --freq=INT              use features that occuer no less than INT(default 1)
 -m, --maxiter=INT           set INT for max iterations in LBFGS routine(default 10k) 设置INT为LBFGS的最大迭代次数 (默认10k)
 -c, --cost=FLOAT            set FLOAT for cost parameter(default 1.0) 设置FLOAT为代价参数,过大会过度拟合 (默认1.0)
 -e, --eta=FLOAT             set FLOAT for termination criterion(default 0.0001) 设置终止标准FLOAT(默认0.0001)
 -C, --convert               convert text model to binary model 将文本模式转为二进制模式
 -t, --textmodel             build also text model file for debugging 为调试建立文本模型文件
 -a, --algorithm=(CRF|MIRA)  select training algorithm (CRF|MIRA) 选择训练算法,默认为CRF-L2
 -p, --thread=INT            number of threads (default auto-detect)
 -H, --shrinking-size=INT    set INT for number of iterations variable needs to  be optimal before considered for shrinking. (default 20) 设置INT为最适宜的跌代变量次数 (默认20)
 -v, --version               show the version and exit 显示版本号并退出
 -h, --help                  show this help and exit 显示帮助并退出

测试

crf_test -m model_file test_files

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

crf_test -m model_file test_files >> result_file

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

训练语料格式

训练文件

样例:

训练文件由若干个句子组成(可以理解为若干个训练样例),不同句子之间通过换行符分隔,上图中显示出的有两个句子。每个句子可以有若干组标签,最后一组标签是标注,上图中有三列,即第一列和第二列都是已知的数据,第三列是要预测的标注,以上面例子为例是,根据第一列的词语和和第二列的词性,预测第三列的标注。涉及到标注的问题,这个就是很多paper要研究的了,比如命名实体识别就有很多不同的标注集。这个超出本文范围。

测试文件

测试文件与训练文件格式自然是一样的。与SVM不同,CRF++没有单独的结果文件,预测结果通过标准输出流输出了,因此将结果重定向到文件中。结果文件比测试文件多了一列,即为预测的标签,我们可以计算最后两列,一列的标注的标签,一列的预测的标签,来得到标签预测的准确率。

模板文件

1、模板基础

模板文件中的每一行是一个模板。每个模板都是由%x[row,col]来指定输入数据中的一个token。row指定到当前token的行偏移,col指定列位置。

由上图可见,当前token是the这个单词。%x[-2,1]就就是the的前两行,1号列的元素(注意,列是从0号列开始的),即为PRP。

2、模板类型

有两种类型的模板,模板类型通过第一个字符指定。

Unigram template: first character, ‘U’

当给出一个”U01:%x[0,1]”的模板时,CRF++会产生如下的一些特征函数集合(func1 … funcN) 。

这几个函数我说明一下,%x[0,1]这个特征到前面的例子就是说,根据词语(第1列)的词性(第2列)来预测其标注(第3列),这些函数就是反应了训练样例的情况,func1反映了“训练样例中,词性是DT且标注是B-NP的情况”,func2反映了“训练样例中,词性是DT且标注是I-NP的情况”。模板函数的数量是L*N,其中L是标注集中类别数量,N是从模板中扩展处理的字符串种类。

Bigram template: first character, ‘B’

这个模板用来描述二元特征。这个模板会自动产生当前output token和前一个output token的合并。注意,这种类型的模板会产生L * L * N种不同的特征。

Unigram feature 和 Bigram feature有什么区别呢?

unigram/bigram很容易混淆,因为通过unigram-features也可以写出类似%x[-1,0]%x[0,0]这样的单词级别的bigram(二元特征)。而这里的unigram和bigram features指定是uni/bigrams的输出标签。

  • unigram: |output tag| x |all possible strings expanded with a macro|
  • bigram: |output tag| x |output tag| x |all possible strings expanded with a macro|

这里的一元/二元指的就是输出标签的情况,这个具体的例子我还没看到,example文件夹中四个例子,也都是只用了Unigram,没有用Bigarm,因此感觉一般Unigram feature就够了。

3、模板例子

这是CoNLL 2000的Base-NP chunking任务的模板例子。只使用了一个bigram template (‘B’)。这意味着只有前一个output token和当前token被当作bigram features。“#”开始的行是注释,空行没有意义。

4、样例数据

example文件夹中有四个任务,basenp,chunking,JapaneseNE,seg。前两个是英文数据,后两个是日文数据。第一个应该是英文命名实体识别,第二个应该是英文分词,第三个应该是日文命名实体识别,第四个是日文分词。

CRF++实战:中文分词

使用人民日报的语料,为了方便切割,将其中的t替换为了空格。对于语料有嵌套的标注,例如:[中央/n 电视台/n]nt,为了处理方便,只考虑最细粒度的分词结果,即当作是 中央/n 电视台/n 两个词进行处理。

生成训练数据

通过下面python脚本,根据人民日报的语料库生成crf的测试和训练数据。原始数据中随机10%是测试数据,90%是训练数据。程序打印出来了不少调试信息,可以忽略。生成训练数据的时候,支持4tag和6tag两个格式,6tag的格式是:

S,单个词;B,词首;E,词尾;M1/M2/M,词中

4tag和6tag的区别就是没有词中顺序状态。具体代码:

data_dir = "./data/"


def split_word(words):
    li = list()
    for word in words:
        li.append(word)
    return li


# 4 tag
# S/B/E/M
def get4tag(li):
    length = len(li)
    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)
    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 save_data_file(train_obj, test_obj, is_test, word, handle, tag):
    if is_test:
        save_train_file(test_obj, word, handle, tag)
    else:
        save_train_file(train_obj, word, handle, tag)


def save_train_file(fiobj, word, handle, tag):
    if len(word) > 0:
        wordli = split_word(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 convert_tag(tag):
    tag = str(tag)
    fi_obj = open(data_dir + 'people-daily.txt', 'r', encoding='utf-8')
    train_obj = open(data_dir + tag + '.train.data', 'w', encoding='utf-8')
    test_obj = open(data_dir + tag + '.test.data', 'w', encoding='utf-8')

    arr = fi_obj.readlines()
    i = 0
    for a in arr:
        i += 1
        a = a.strip('rnt ')
        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('/')
                w, h = word_hand
                if ']' in h:
                    h = h.split("]")[0]
                if h == 'nr':  # ren min
                    # print 'NR',w
                    if w.find('·') >= 0:
                        tmpArr = w.split('·')
                        for tmp in tmpArr:
                            save_data_file(train_obj, test_obj, test, tmp, h, tag)
                    continue
                if h != 'm':
                    save_data_file(train_obj, test_obj, test, w, h, tag)

                if h == 'w':
                    save_data_file(train_obj, test_obj, test, "", "", tag)  # split

    train_obj.flush()
    test_obj.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]
    #     convert_tag(tag)
    convert_tag(6)

使用模板

# Unigram  
U00:%x[-1,0]  
U01:%x[0,0]  
U02:%x[1,0]  
U03:%x[-1,0]/%x[0,0]  
U04:%x[0,0]/%x[1,0]  
U05:%x[-1,0]/%x[1,0]

训练和测试

./crf_learn -f 3 -c 4.0 template 6.train.data 6.model > 6.train.rst  
./crf_test -m 6.model 6.test.data > 6.test.rst

计算F

if __name__ == "__main__":
    rst_file = './data/6.test.rst'
    with open(rst_file, "r") as f:
        lines = f.readlines()
        wc_of_test = 0
        wc_of_gold = 0
        wc_of_correct = 0
        flag = True

        for l in lines:
            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)))

输出结果:

WordCount from test result: 102966
WordCount from golden data: 102952
WordCount of correct segs : 97193
P = 0.943933, R = 0.944061, F-score = 0.943997

CRF++实战:地名实体识别

类似使用CRF实现分词和词性标注,地域识别也是需要生成相应的tag进行标注。这里使用的语料库是1998年1月人民日报语料集。最终学习出来的模型,对复杂的地名识别准确率(F值)非常低,推测是预料中对地名的标注多处是前后矛盾。例如  [华南/ns 地区/n]ns  标为地名实体,但是 东北/f 地区/n 确分开标注,类似错误还有很多。

生成训练和测试数据

通过一个python脚本按照一定比例生成训练和测试数据,生成过程中按照BMES对语料进行标识,具体规则如下:

转换代码:

import sys

data_dir = "./data/"


def save_data_file(train_obj, test_obj, is_test, word, handle, tag):
    if is_test:
        save_train_file(test_obj, word, handle, tag)
    else:
        save_train_file(train_obj, word, handle, tag)


def save_train_file(fi_obj, word, handle, tag):
    if len(word) > 0 and word != "。" and word != ",":
        fi_obj.write(word + 't' + handle + 't' + tag + 'n')
    else:
        fi_obj.write('n')


# 填充地点标注,非地点的不添加
def fill_local_tag(words, tags):
    pos = 0
    while True:
        if pos == len(words):
            break
        word = words[pos]
        left = word.find("[")
        if left == -1:
            w, h = word.split("/")
            if h == "ns":  # 单个词是地点
                tags[pos] = "LOC_S"
            pos += 1
        elif left >= 0:
            search_pos = pos
            for word in words[pos + 1:]:
                search_pos += 1
                if word.find("[") >= 0:
                    print("括号配对异常")
                    sys.exit(255)
                if word.find("]") >= 0:
                    break
            if words[search_pos].find("]") == -1:
                print("括号配对异常,搜索到句尾没有找都另一半括号")
                sys.exit(255)
            else:
                # 找到另一半,判断原始标注是不是ns,如果是就进行tag标注
                h = words[search_pos].split("]")[-1]  # 最后一个词性
                if h == "ns":
                    tags[pos] = "LOC_B"  # 添加首个词
                    for p in range(pos + 1, search_pos + 1):
                        tags[p] = "LOC_I"  # 中间词
                    tags[search_pos] = "LOC_E"  # 找到最后一个词
                else:
                    p = pos
                    for word in words[pos:search_pos + 1]:
                        w, h = word.strip("[").split("]")[0].split("/")
                        if h == "ns":
                            tags[p] = "LOC_S"
                        p += 1
            pos = search_pos + 1


def convert_tag():
    fi_obj = open(data_dir + 'people-daily.txt', 'r', encoding='utf-8')
    train_obj = open(data_dir + 'train.data', 'w', encoding='utf-8')
    test_obj = open(data_dir + 'test.data', 'w', encoding='utf-8')

    arr = fi_obj.readlines()
    i = 0
    for a in arr:
        i += 1
        a = a.strip('rnt ')
        if a == "":
            continue
        words = a.split(" ")
        test = False
        if i % 5 == 0:
            test = True
        words = words[1:]
        if len(words) == 0:
            continue
        tags = ["O"] * len(words)
        fill_local_tag(words, tags)

        pos = -1
        for word in words:
            pos += 1
            word = word.strip('t ')
            if len(word) == 0:
                print("Warning 发现空词")
                continue

            l1 = word.find('[')
            if l1 >= 0:
                word = word[l1 + 1:]

            l2 = word.find(']')
            if l2 >= 0:
                word = word[:l2]

            w, h = word.split('/')

            save_data_file(train_obj, test_obj, test, w, h, tags[pos])
        save_data_file(train_obj, test_obj, test, "", "", "")

    train_obj.flush()
    test_obj.flush()


if __name__ == '__main__':
    convert_tag()

模板文件

#Unigram
U01:%x[-1,0]
U02:%x[0,0]
U03:%x[1,0]
U04:%x[2,0]
U05:%x[-2,1]
U06:%x[-1,1]
U07:%x[0,1]
U08:%x[1,1]
U09:%x[2,1]
U0:%x[-2,0]
U10:%x[0,0]/%x[0,1]
U11:%x[-2,1]%x[-1,1]
U18:%x[0,0]/%x[-1,0]
U12:%x[0,0]%x[1,0]
U13:%x[0,1]%x[-1,0]
U14:%x[0,0]%x[1,1]
U15:%x[-1,0]%x[-1,1]
U16:%x[-1,0]%x[-2,0]
U17:%x[-2,0]%x[-2,1]
U18:%x[1,0]%x[2,0]
U19:%x[-1,0]%x[1,0]
U20:%x[1,0]%x[0,1]
U22:%x[-2,1]%x[0,1]
U23:%x[-1,1]%x[0,1]
U24:%x[-1,1]%x[1,1]
U25:%x[0,1]%x[1,1]
U26:%x[0,1]%x[2,1]
U27:%x[1,1]%x[2,1]

开始训练和测试

./crf_learn -f 4 -p 4 -c 3 template train.data model > train.rst 
./crf_test -m model test.data > test.rst

分类型计算F

god_dic = {"LOC_S": 0, "LOC_B": 0, "LOC_I": 0, "LOC_E": 0}
pre_dic = {"LOC_S": 0, "LOC_B": 0, "LOC_I": 0, "LOC_E": 0}
correct_dic = {"LOC_S": 0, "LOC_B": 0, "LOC_I": 0, "LOC_E": 0}

if __name__ == "__main__":
    with open('./data/test.rst') as f:
        file = f.readlines()
        wc = 0
        loc_wc = 0
        wc_of_test = 0
        wc_of_gold = 0
        wc_of_correct = 0
        flag = True

        for l in file:
            wc += 1
            if l == 'n':
                continue
            _, _, g, r = l.strip().split()
            # 并不涉及到地点实体识别
            if "LOC" not in g and "LOC" not in r:
                continue
            loc_wc += 1
            if "LOC" in g:
                god_dic[g] += 1
            if "LOC" in r:
                pre_dic[r] += 1
            if g == r:
                correct_dic[r] += 1

        print("WordCount from result:", wc)
        print("WordCount of loc_wc  post :", loc_wc)
        print("真实位置标记个数:", god_dic)
        print("预估位置标记个数:", pre_dic)
        print("正确标记个数:", correct_dic)

        res = {"LOC_S": 0.0, "LOC_B": 0.0, "LOC_I": 0.0, "LOC_E": 0.0}

        all_gold = 0
        all_correct = 0
        all_pre = 0
        for k in god_dic:
            print("------ %s -------" % (k))
            R = correct_dic[k] / float(god_dic[k])
            P = correct_dic[k] / float(pre_dic[k])
            print("[%s] P = %f, R = %f, F-score = %f" % (k, P, R, (2 * P * R) / (P + R)))

            all_pre += pre_dic[k]
            all_correct += correct_dic[k]
            all_gold += god_dic[k]
        print("------ All -------")
        all_R = all_correct / float(all_gold)
        all_P = all_correct / float(all_pre)
        print("[%s] P = %f, R = %f, F-score = %f" % ("All", all_P, all_R, (2 * all_P * all_R) / (all_P + all_R)))

执行结果:

WordCount from result: 220612
WordCount of loc_wc  post : 5791
真实位置标记个数: {'LOC_S': 5262, 'LOC_B': 197, 'LOC_I': 95, 'LOC_E': 197}
预估位置标记个数: {'LOC_S': 5304, 'LOC_B': 136, 'LOC_I': 57, 'LOC_E': 149}
正确标记个数: {'LOC_S': 5233, 'LOC_B': 106, 'LOC_I': 42, 'LOC_E': 124}
------ LOC_S -------
[LOC_S] P = 0.986614, R = 0.994489, F-score = 0.990536
------ LOC_B -------
[LOC_B] P = 0.779412, R = 0.538071, F-score = 0.636637
------ LOC_I -------
[LOC_I] P = 0.736842, R = 0.442105, F-score = 0.552632
------ LOC_E -------
[LOC_E] P = 0.832215, R = 0.629442, F-score = 0.716763
------ All -------
[All] P = 0.975027, R = 0.957225, F-score = 0.966044

参考链接:https://x-algo.cn/index.php/2016/02/29/crf-name-entity-recognition/

CRF++实战:词性标注

训练和测试的语料都是人民日报98年标注语料,训练和测试比例是10:1,直接通过CRF++标注词性的准确率:0.933882。由于训练时间较慢,此部分未进行正式测试。

生成训练和测试数据

data_dir = "./data/"


def save_data_file(train_obj, test_obj, is_test, word, handle):
    if is_test:
        save_train_file(test_obj, word, handle)
    else:
        save_train_file(train_obj, word, handle)


def save_train_file(fi_obj, word, handle):
    if len(word) > 0 and word != "。" and word != ",":
        fi_obj.write(word + 't' + handle + 'n')
    else:
        fi_obj.write('n')


def convert_tag():
    fi_obj = open(data_dir + 'people-daily.txt', 'r')
    train_obj = open(data_dir + 'train.data', 'w')
    test_obj = open(data_dir + 'test.data', 'w')

    arr = fi_obj.readlines()
    i = 0
    for a in arr:
        i += 1
        a = a.strip('rnt ')
        if a == "":
            continue
        words = a.split(" ")
        test = False
        if i % 10 == 0:
            test = True
        for word in words[1:]:
            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('/')
            w, h = word_hand
            if ']' in h:
                h = h.split("]")[0]
            if h == 'nr':  # ren min
                if w.find('·') >= 0:
                    tmp_arr = w.split('·')
                    for tmp in tmp_arr:
                        save_data_file(train_obj, test_obj, test, tmp, h)
                    continue
            save_data_file(train_obj, test_obj, test, w, h)
        save_data_file(train_obj, test_obj, test, "", "")

    train_obj.flush()
    test_obj.flush()


if __name__ == '__main__':
    convert_tag()

模板文件

# Unigram
U00:%x[-2,0]
U01:%x[-1,0]
U02:%x[0,0]
U03:%x[1,0]
U04:%x[2,0]
U05:%x[-1,0]/%x[0,0]
U06:%x[0,0]/%x[1,0]

执行训练

./crf_learn -f 3 -p 4 -c 4.0 template train.data model > train.rst  
./crf_test -m model test.data > test.rst

计算准确率

if __name__ == "__main__":
    with open("./data/test.rst", "r") as f:
        file = f.readlines()

        wc = 0
        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
            wc += 1

            if flag:
                wc_of_correct += 1
            flag = True

        print("WordCount from result:", wc)
        print("WordCount of correct post :", wc_of_correct)

        # 准确率
        P = wc_of_correct / float(wc)

        print("准确率:%f" % (P))

参考链接:https://x-algo.cn/index.php/2016/02/28/crf-tagging/

CRF++实战:依存句法分析

语料是清华大学的句法标注语料,包括训练集(train.conll)和开发集合文件(dev.conll),根据模板文件生成了将近两千万个特征,由于训练较慢,以下内容未做测试。

生成训练和开发语料

依存关系本身是一个树结构,每一个词看成一个节点,依存关系就是一条有向边。语料本身格式:

1	坚决	坚决	a	ad	_	2	方式	
2	惩治	惩治	v	v	_	0	核心成分	
3	贪污	贪污	v	v	_	7	限定	
4	贿赂	贿赂	n	n	_	3	连接依存	
5	等	等	u	udeng	_	3	连接依存	
6	经济	经济	n	n	_	7	限定	
7	犯罪	犯罪	v	vn	_	2	受事	
 
1	最高	最高	n	nt	_	3	限定	
2	人民	人民	n	nt	_	3	限定	
3	检察院	检察院	n	nt	_	4	限定	
4	检察长	检察长	n	n	_	0	核心成分	
5	张思卿	张思卿	n	nr	_	4	同位语

数据格式说明:

1、本次中文语义依存分析将在两个语料库上进行评测,其中THU文件夹内为清华大学语义依存网络语料,HIT文件夹内为哈尔滨工业大学依存语料库。
   每个语料库都包含三个文件,分别为train.conll,dev.conll和test.conll。
   train.conll为训练语料,用于模型训练;
   dev.conll为开发集,用于模型参数调优;
   test.conll用于测试,根据会议日程,暂不发布。
 
2、参赛者可以在两个语料的训练语料上上分别训练模型,也可以结合两个语料库的训练语料训练统一的模型。
 
3、所有数据文件均采用CONLL格式,UTF8编码。CONLL标注格式包含10列,分别为:
   ---------------------------------------------------------------------------------
   ID	FORM	LEMMA	CPOSTAG	POSTAG	FEATS	HEAD	DEPREL	PHEAD	PDEPREL	
---------------------------------------------------------------------------------
 
 
   本次评测只用到前8列,其含义分别为:
 
   1	ID	当前词在句子中的序号,1开始.
   2	FORM	当前词语或标点  
   3	LEMMA	当前词语(或标点)的原型或词干,在中文中,此列与FORM相同
   4	CPOSTAG	当前词语的词性(粗粒度)
   5	POSTAG	当前词语的词性(细粒度)
   6	FEATS	句法特征,在本次评测中,此列未被使用,全部以下划线代替。
   7	HEAD	当前词语的中心词
   8	DEPREL	当前词语与中心词的依存关系
 
   在CONLL格式中,每个词语占一行,无值列用下划线'_'代替,列的分隔符为制表符't',行的分隔符为换行符'n';句子与句子之间用空行分隔。

通过python脚本生成所需要的训练数据和测试使用的开发数据:

# coding=utf-8
'''
词A依赖词B,A就是孩子,B就是父亲
'''

import sys

sentence = ["Root"]


def do_parse(sentence):
    if len(sentence) == 1: return
    for line in sentence[1:]:
        line_arr = line.strip().split("t")
        # print line_arr
        c_id = int(line_arr[0])
        f_id = int(line_arr[6])
        # print c_id, f_id
        if f_id == 0:
            print("t".join(line_arr[2:5]) + "t" + "0_Root")
            continue
        # print sentence[f_id].strip().split("t")[3:5]
        f_post, f_detail_post = sentence[f_id].strip().split("t")[3:5]  # 得到父亲节点的粗词性和详细词性
        c_edge_post = f_post  # 默认是依赖词的粗粒度词性,但是名词除外;名词取细粒度词性
        if f_post == "n":
            c_edge_post = f_detail_post
        # 计算是第几个出现这种词行
        diff = f_id - c_id  # 确定要走几步
        step = 1 if f_id > c_id else -1  # 确定每一步方向
        same_post_num = 0  # 中间每一步统计多少个一样的词性
        cmp_idx = 4 if f_post == "n" else 3  # 根据是否是名词决定取的是粗or详细词性
        for i in range(0, abs(diff)):
            idx = c_id + (i + 1) * step
            if sentence[idx].strip().split("t")[cmp_idx] == c_edge_post:
                same_post_num += step

        print("t".join(line_arr[2:5]) + "t" + "%d_%s" % (same_post_num, c_edge_post))
    print("")


for line in sys.stdin:
    line = line.strip()
    line_arr = line.split("t")

    if line == "" or line_arr[0] == "1":
        # print sentence
        do_parse(sentence)
        sentence = ["Root"]

    if line == "":
        continue
sentence.append(line)

模板文件

# Unigram
U01:%x[0,0]
U02:%x[0,0]/%x[0,2]
U03:%x[-1,2]/%x[0,0]
U04:%x[-1,2]/%x[0,2]/%x[0,0]
U05:%x[0,0]/%x[1,2]
U06:%x[0,0]/%x[0,2]/%x[1,2]
U07:%x[-1,2]/%x[0,2]
U08:%x[-1,1]/%x[0,2]
U09:%x[0,2]/%x[1,2]
U10:%x[0,2]/%x[1,1]
U11:%x[-2,2]/%x[-1,2]/%x[0,2]/%x[1,2]
U12:%x[-1,2]/%x[0,2]/%x[1,2]/%x[2,2]
U13:%x[-2,2]/%x[-1,2]/%x[0,2]/%x[1,2]/%x[2,2]
U14:%x[-2,1]/%x[-1,2]/%x[0,2]
U15:%x[-1,2]/%x[0,2]/%x[1,2]
U16:%x[-1,1]/%x[0,2]/%x[1,1]
U17:%x[-1,2]/%x[1,2]

进行训练和测试过程

./crf_learn -f 3 -p 40 -c 4.0 template train.data model > train.rst  
./crf_test -m model dev.data > dev.rst

参考链接:

其他参考:



网友评论已有0条评论, 我也要评论

发表评论

*

* (保密)

Ctrl+Enter 快捷回复