简明易懂的CRF原理及代码

2,751 阅读4分钟

1. CRF 原理

1.1 CRF举例

CRF 简单说 是指 概率图中相邻变量是否满足特征函数[公式]的一个模型,例如下图示例是一个商户识别的CRF应用。对输入商户输出地址,名称关键词,经营范围等信息,使用BIOS标注方法,标注如下:

  • 转移特征函数: t(yi1,yi,x,i)t(y_{i-1}, y_i, x, i)

  • 状态特征函数:s(yi,x,i)s(y_i, x, i)

  • 转移特征函数(t) 接受四个参数,状态特征函数(s) 接受三个参数:

    • xx,待标注词性的句子
    • ii,用来表示句子 s 中第 i 个单词
    • yiy_i,表示第 i 个单词标注的词性
    • yi1y_{i-1},表示第 i-1 个单词标注的词性

    它的输出值是 0 或者 1, 0 表示要评分的标注序列不符合这个特征,1 表示要评分的标注序列符合这个特征, λ,μ\lambda, \mu 分别为转移特征函数 t, 状态特征函数 s 的权重

1.2 在上图商户识别任务中

  • 商户关键字后跟着经营范围,我们可以给正分,转移特征函数:(I-KEYWORDS B-BUSINESS)

    t(yi1="KEYWORDS",yi="BUSINESS",x,i)t(y_{i-1}="KEYWORDS", y_{i}="BUSINESS", x, i) = 1;

  • 把美佳标记为 KEYWORDS , 我们可以给正分,状态特征函数:

    s(yi="KEYWORDS",x="美佳",i)=1s(y_i="KEYWORDS", x="美佳", i)=1

1.3 参数化以上过程

score(yx)=i,kλktk(yi1,yi,x,i)+i,lμlsl(yi,x,i)score(y|x) = \sum_{i,k}^{}{\lambda_{k}}t_{k}(y_{i-1},y_{i},x,i)+\sum_{i,l}^{}{\mu_{l}}s_{l}(y_{i},x,i)

​ 概率化(使用 softmax 函数):

P(yx)=1Z(x)exp(i,kλktk(yi1,yi,x,i)+i,lμlsl(yi,x,i))P(y|x) = \frac{1}{Z(x)}exp( \sum_{i,k}^{}{\lambda_{k}}t_{k}(y_{i-1},y_{i},x,i)+\sum_{i,l}^{}{\mu_{l}}s_{l}(y_{i},x,i) )

​ 将转移特征函数和状态特征函数合并,参数用 ww 表示,上式可写为:

P(yx)=1Z(x)exp(wF(y,x))P(y|x) = \frac{1}{Z(x)}exp( w\cdot F(y,x) )

​ 其中,Z(x)Z(x) 是用来归一化的,Z(x)=yexp(i,kλktk(yi1,yi,x,i)+i,lμlsl(yi,x,i))Z(x)= \sum_y exp( \sum_{i,k}^{}{\lambda_{k}}t_{k}(y_{i-1},y_{i},x,i)+\sum_{i,l}^{}{\mu_{l}}s_{l}(y_{i},x,i) )

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