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
参考链接:
- https://x-algo.cn/index.php/2016/03/02/crf-dependency-parsing/
- https://www.zybuluo.com/lianjizhe/note/1205311
其他参考:
- https://taku910.github.io/crfpp/
- http://www.huaxiaozhuan.com/%E5%B7%A5%E5%85%B7/CRF/chapters/crfpp.html
- https://github.com/howl-anderson/tools_for_corpus_of_people_daily
网友评论已有0条评论, 我也要评论