1. CRF 原理
1.1 CRF举例
CRF 简单说 是指 概率图中相邻变量是否满足特征函数的一个模型,例如下图示例是一个商户识别的CRF应用。对输入商户输出地址,名称关键词,经营范围等信息,使用BIOS标注方法,标注如下:
-
转移特征函数:
-
状态特征函数:
-
转移特征函数(t) 接受四个参数,状态特征函数(s) 接受三个参数:
- ,待标注词性的句子
- ,用来表示句子 s 中第 i 个单词
- ,表示第 i 个单词标注的词性
- ,表示第 i-1 个单词标注的词性
它的输出值是 0 或者 1, 0 表示要评分的标注序列不符合这个特征,1 表示要评分的标注序列符合这个特征, 分别为转移特征函数 t, 状态特征函数 s 的权重
1.2 在上图商户识别任务中
-
商户关键字后跟着经营范围,我们可以给正分,转移特征函数:(I-KEYWORDS B-BUSINESS)
= 1;
-
把美佳标记为 KEYWORDS , 我们可以给正分,状态特征函数:
1.3 参数化以上过程
概率化(使用 softmax 函数):
将转移特征函数和状态特征函数合并,参数用 表示,上式可写为:
其中, 是用来归一化的,
2. CRF特征构造
CRF模型中涉及到以下2类特征模板:
2.1 基础类特征,即CRF模型中常用的特征,包含以下4类:
-
是否是数字
-
是否英文数字:1-10
-
是否中文数字:'一', '二', '三', '四', '五', '六', '七', '八', '九', '十'
-
是否中文传统数字:'甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '葵'
-
-
是否大写/小写
-
是否文本开始/是否文本结尾
-
相邻第一个是否小写/大写;相邻第二个字是否小写/大写
2.2 Ngrams类特征
Ngram本身也指一个由N个字或词组成的集合,各字或词具有先后顺序,且不要求他们之间互不相同
3. CRF 在 NER 方面的应用
CRF在序列标注方面应用广泛,下面使用 sklearn-crfsuite 包来从数据导入,特征生成,训练,评价4个方面,通过代码,实现一个CRF 序列标注模型。代码可直接运行
3.1 数据准备
import 依赖包
import sklearn
import scipy.stats
import sklearn_crfsuite
from sklearn_crfsuite import scorers
from sklearn_crfsuite import metrics
使用 nltk 下载 CoNLL 2002 数据
import nltk
nltk.download('conll2002')
----------------- # 分割线下是输出,不是代码
>>> [nltk_data] Downloading package conll2002 to /root/nltk_data...
[nltk_data] Package conll2002 is already up-to-date!
True
加载 conll2002 数据
%%time
train_sents = list(nltk.corpus.conll2002.iob_sents('esp.train'))
test_sents = list(nltk.corpus.conll2002.iob_sents('esp.testb'))
查看一条数据
train_sents[0]
----------------- # 分割线下是输出,不是代码
>>> [('Melbourne', 'NP', 'B-LOC'),
('(', 'Fpa', 'O'),
('Australia', 'NP', 'B-LOC'),
(')', 'Fpt', 'O'),
(',', 'Fc', 'O'),
('25', 'Z', 'O'),
('may', 'NC', 'O'),
('(', 'Fpa', 'O'),
('EFE', 'NC', 'B-ORG'),
(')', 'Fpt', 'O'),
('.', 'Fp', 'O')]
通常我们的数据只有文本及NER标注,所以我们只取上面数据的文本和BIOS格式的标注,并查看一条数据
train_sents_ner = [[(i[0], i[2]) for i in row] for row in train_sents]
test_sents_ner = [[(i[0], i[2]) for i in row] for row in test_sents]
train_sents_ner[0]
----------------- # 分割线下是输出,不是代码
>>> [('Melbourne', 'B-LOC'),
('(', 'O'),
('Australia', 'B-LOC'),
(')', 'O'),
(',', 'O'),
('25', 'O'),
('may', 'O'),
('(', 'O'),
('EFE', 'B-ORG'),
(')', 'O'),
('.', 'O')]
3.2 生成特征
使用官方文档的特征生成模板
def word2features(sent, i):
word = sent[i][0]
postag = sent[i][1]
features = {
'bias': 1.0,
'word.lower()': word.lower(),
'word[-3:]': word[-3:],
'word[-2:]': word[-2:],
'word.isupper()': word.isupper(),
'word.istitle()': word.istitle(),
'word.isdigit()': word.isdigit()
}
if i > 0:
word1 = sent[i-1][0]
features.update({
'-1:word.lower()': word1.lower(),
'-1:word.istitle()': word1.istitle(),
'-1:word.isupper()': word1.isupper()
})
else:
features['BOS'] = True
if i < len(sent)-1:
word1 = sent[i+1][0]
features.update({
'+1:word.lower()': word1.lower(),
'+1:word.istitle()': word1.istitle(),
'+1:word.isupper()': word1.isupper()
})
else:
features['EOS'] = True
return features
def sent2features(sent):
return [word2features(sent, i) for i in range(len(sent))]
def sent2labels(sent):
return [label for token, label in sent]
def sent2tokens(sent):
return [token for token, label in sent]
看一条转换后的特征长什么样子
sent2features(train_sents_ner[0])[2]
# 第一条训练数据中,第3个单词(Australia)的特征如下
----------------- # 分割线下是输出,不是代码
>>>{'+1:word.istitle()': False,
'+1:word.isupper()': False,
'+1:word.lower()': ')',
'-1:word.istitle()': False,
'-1:word.isupper()': False,
'-1:word.lower()': '(',
'bias': 1.0,
'word.isdigit()': False,
'word.istitle()': True,
'word.isupper()': False,
'word.lower()': 'australia',
'word[-2:]': 'ia',
'word[-3:]': 'lia'}
将训练数据和测试数据均转换为其特征表示
X_train = [sent2features(s) for s in train_sents_ner]
y_train = [sent2labels(s) for s in train_sents_ner]
X_test = [sent2features(s) for s in test_sents_ner]
y_test = [sent2labels(s) for s in test_sents_ner]
3.3 模型训练
%%time
crf = sklearn_crfsuite.CRF(
algorithm='lbfgs',
c1=0.1,
c2=0.1,
max_iterations=100,
all_possible_transitions=True)
crf.fit(X_train, y_train)
----------------- # 分割线下是输出,不是代码
>>> CPU times: user 35 s, sys: 21.8 ms, total: 35.1 s
Wall time: 35.1 s
3.4 模型预测
labels = list(crf.classes_)
labels.remove('O')
labels
----------------- # 分割线下是输出,不是代码
>>> ['B-LOC', 'B-ORG', 'B-PER', 'I-PER', 'B-MISC', 'I-ORG', 'I-LOC', 'I-MISC']
y_pred = crf.predict(X_test)
metrics.flat_f1_score(y_test, y_pred, average='weighted', labels=labels)
----------------- # 分割线下是输出,不是代码
>>> 0.7860514251609507
# group B and I results
sorted_labels = sorted(labels,key=lambda name: (name[1:], name[0]))
print(metrics.flat_classification_report(
y_test, y_pred, labels=sorted_labels, digits=3
))
----------------- # 分割线下是输出,不是代码
>>> precision recall f1-score support
B-LOC 0.800 0.778 0.789 1084
I-LOC 0.672 0.631 0.651 325
B-MISC 0.721 0.534 0.614 339
I-MISC 0.686 0.582 0.630 557
B-ORG 0.804 0.821 0.812 1400
I-ORG 0.846 0.776 0.810 1104
B-PER 0.832 0.865 0.849 735
I-PER 0.884 0.935 0.909 634
micro avg 0.803 0.775 0.789 6178
macro avg 0.781 0.740 0.758 6178
weighted avg 0.800 0.775 0.786 6178