条件随机场CRF

Posted by xcTorres on November 8, 2021

网上关于条件随机场的优秀博客已经有很多,在这里就不详细介绍算法所有推导了,关于公式推导可阅读刘建平老师的博客或者直接看李航老师的《统计学习方法》,本博客主要是提炼一部分非公式的内容,并附上示例代码。

算法介绍

马尔科夫随机场

首先马尔科夫随机场本身是一个概率无向图。但其还满足如下性质。

1)成对马尔科夫性(pairwise Markov property)
2)局部马尔科夫性(local Markov property)
3)全局马尔科夫性 (global Markov property)

实质上该三种性质都是等价的。从性质中我们可以看出其主要是定义不相连的结点是条件独立的,由此在算联合概率分布的时候易于因子分解。 可参考该博客

条件随机场

设X与Y是随机变量,P(Y|X)是在给定X的条件下Y的条件概率分布,若随机变量Y构成一个由无向图$G=(V,E)$表示的马尔科夫随机场,即
$P(Y_v| X,Y_w, w \neq v) = P(Y_v| X,Y_w, w \sim v) $ 对任意结点v成立。
式中$w~v$表示在图$G=(V,E)$ 中与结点v有边连接的所有节点w,$w \neq v$表示结点v以外的所有结点,$Y_v, Y_u$与$Y_w$为结点$v, u, w$对应的随机变量。

三个基本问题

  1. 线性CRF第一个问题是评估,即给定linear-CRF的条件概率分布$ 𝑃(𝑦|𝑥) $, 在给定输入序列𝑥和输出序列𝑦时,计算条件概率$𝑃(𝑦𝑖|𝑥)$和$𝑃(𝑦𝑖−1,𝑦𝑖|𝑥)$以及对应的期望。

  2. linear-CRF第二个问题是学习,即给定训练数据集𝑋和𝑌,学习linear-CRF的模型参数$𝑤_𝑘$和条件概率$ 𝑃_𝑤(𝑦|𝑥)$,普通的梯度下降法,拟牛顿法都可以解决。

  3. linear-CRF第三个问题是解码,即给定linear-CRF的条件概率分布$ 𝑃(𝑦|𝑥)$,和输入序列𝑥, 计算使条件概率最大的输出序列𝑦。类似于HMM,使用维特比算法可以很方便的解决这个问题。 

示例介绍

关于该示例的源码可从如下github链接下载, https://github.com/xcTorres/machine_learning/blob/master/crf/crf.ipynb

读取数据

1
2
3
4
5
6
7
8
9
10
import numpy as np
import pandas as pd
from sklearn.feature_extraction import DictVectorizer
from sklearn.model_selection import train_test_split

df = pd.read_csv('./data/ner_dataset.csv', encoding = "ISO-8859-1")
df.head()
df = df.fillna(method='ffill')

new_classes = df['Tag'].unique().tolist()

获取每个单词的特征

如下函数完全沿用sklearn-crfsuite的特征部分,详情可阅读官方源码文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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(),
        'postag': postag,
        'postag[:2]': postag[:2],
    }
    if i > 0:
        word1 = sent[i-1][0]
        postag1 = sent[i-1][1]
        features.update({
            '-1:word.lower()': word1.lower(),
            '-1:word.istitle()': word1.istitle(),
            '-1:word.isupper()': word1.isupper(),
            '-1:postag': postag1,
            '-1:postag[:2]': postag1[:2],
        })
    else:
        features['BOS'] = True
    if i < len(sent)-1:
        word1 = sent[i+1][0]
        postag1 = sent[i+1][1]
        features.update({
            '+1:word.lower()': word1.lower(),
            '+1:word.istitle()': word1.istitle(),
            '+1:word.isupper()': word1.isupper(),
            '+1:postag': postag1,
            '+1:postag[:2]': postag1[:2],
        })
    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, postag, label in sent]

def sent2tokens(sent):
    return [token for token, postag, label in sent]

X = [sent2features(s) for s in sentences]
y = [sent2labels(s) for s in sentences]  
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=0)

参数调优

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import scipy.stats
from sklearn.metrics import make_scorer
from sklearn.model_selection import RandomizedSearchCV

crf = sklearn_crfsuite.CRF(
    algorithm='lbfgs',
    max_iterations=100,
    all_possible_transitions=True
)
params_space = {
    'c1': scipy.stats.expon(scale=0.5),
    'c2': scipy.stats.expon(scale=0.05),
}

# use the same metric for evaluation
f1_scorer = make_scorer(metrics.flat_f1_score,
                        average='weighted', labels=new_classes)

# search
rs = RandomizedSearchCV(crf, params_space,
                        cv=3,
                        verbose=1,
                        n_jobs=-1,
                        n_iter=50,
                        scoring=f1_scorer)
rs.fit(X_train, y_train)

print('best params:', rs.best_params_)
print('best CV score:', rs.best_score_)
print('model size: {:0.2f}M'.format(rs.best_estimator_.size_ / 1000000))

"""
best params: {'c1': 0.0036898984638244928, 'c2': 0.11585183551331574}
best CV score: 0.7737211773297741
model size: 1.30M
"""

训练模型

1
2
3
4
5
6
7
8
crf = sklearn_crfsuite.CRF(
    algorithm='lbfgs',
    c1=rs.best_params_['c1'],
    c2=rs.best_params_['c2'],
    max_iterations=100,
    all_possible_transitions=True
)
crf.fit(X_train, y_train)

预测

1
2
3
4
y_pred = crf.predict(X_test)
metrics.flat_f1_score(y_test, y_pred, average='weighted', labels=new_classes)  

print(metrics.flat_classification_report(y_test, y_pred, labels = new_classes))

用Eli5观察Label之间的转换矩阵

1
2
import eli5
eli5.show_weights(crf, top=10)

Reference

https://www.cnblogs.com/jiangxinyang/p/9309742.html
http://www.hankcs.com/ml/conditional-random-field.html
http://www.hankcs.com/ml/crf-code-analysis.html
https://www.cnblogs.com/pinard/p/7048333.html
NER_sklearn
sklearn-crfsuite



-->