条件随机场实现命名实体识别

3,263 阅读5分钟

前言

NLP 被很多人称为人工智能皇冠上的明珠,可见其在 AI 领域的重要性,而命名实体识别(NER)又一直是 NLP 领域的研究热点,所以这块任务是 NLP 必谈的。

NER 早期的实现主要是基于词典和规则,然后是基于传统的机器学习,比如 HMM、MEMM 和 CRF。随后深度学习崛起则很多用 CRF 结合循环神经网络或卷积神经网络来做。而最近期的则是基于注意力模型和迁移学习等。

其实 NER 的主流核心算法是条件随机场(CRF),包括后来的深度学习和注意力模型都是需要结合 CRF 来使用,所以这篇文章看看 CRF 怎么实现命名实体识别。

关于条件随机场

CRF 即条件随机场(Conditional Random Fields),是在给定一组输入随机变量条件下另外一组输出随机变量的条件概率分布模型,它是一种判别式的概率无向图模型,既然是判别式,那就是对条件概率分布建模。

在 NLP 中,CRF 是用于标注和划分序列数据的概率化模型,根据 CRF 的定义,相对序列就是给定观测序列 X 和输出序列 Y,然后通过定义条件概率 P(Y|X) 来描述模型。

详细可以看前面的文章《机器学习之条件随机场(CRF)》。

NER语料库

为方便可直接用 nltk 提供的命名实体识别语料库,通过以下进行下载。

>>> import nltk
>>> nltk.download('conll2002')
[nltk_data] Downloading package conll2002 to
[nltk_data]     C:\Users\84958\AppData\Roaming\nltk_data...
[nltk_data]   Unzipping corpora\conll2002.zip.

读取语料,

train_sents = list(nltk.corpus.conll2002.iob_sents('esp.train'))
test_sents = list(nltk.corpus.conll2002.iob_sents('esp.testb'))

特征函数

定义我们的特征函数,这里其实更像是定义特征函数的模板,因为真正的特征函数会根据这个定义的模板去生成,而且一般生成的特征函数数量是相当大的,然后通过训练确定每个特征函数对应的权重。

以下面代码看特征的选取,包括单词小写、单词倒数2和3的后缀、是否为大写、是否为title、是否为数字、标签、标签前缀、前一个单词的相关属性、后一个单词的相关属性。

def word2features(sent, i):
    word = sent[i][0]
    postag = sent[i][1]
    features = [
        'bias',
        'word.lower=' + word.lower(),
        'word[-3:]=' + word[-3:],
        'word[-2:]=' + word[-2:],
        'word.isupper=%s' % word.isupper(),
        'word.istitle=%s' % word.istitle(),
        'word.isdigit=%s' % word.isdigit(),
        'postag=' + postag,
        'postag[:2]=' + postag[:2],
    ]
    if i > 0:
        word1 = sent[i - 1][0]
        postag1 = sent[i - 1][1]
        features.extend([
            '-1:word.lower=' + word1.lower(),
            '-1:word.istitle=%s' % word1.istitle(),
            '-1:word.isupper=%s' % word1.isupper(),
            '-1:postag=' + postag1,
            '-1:postag[:2]=' + postag1[:2],
        ])
    else:
        features.append('BOS')
    if i < len(sent) - 1:
        word1 = sent[i + 1][0]
        postag1 = sent[i + 1][1]
        features.extend([
            '+1:word.lower=' + word1.lower(),
            '+1:word.istitle=%s' % word1.istitle(),
            '+1:word.isupper=%s' % word1.isupper(),
            '+1:postag=' + postag1,
            '+1:postag[:2]=' + postag1[:2],
        ])
    else:
        features.append('EOS')
    return features

训练模型

接着可以开始创建 Trainer 进行训练,将语料的每个句子转成特征及标签列表,然后设置好 Trainer 的相关参数,并将样本添加到 Trainer 中开始训练。最终会将模型保存到model_path中。

def train():
    X_train = [sent2features(s) for s in train_sents]
    y_train = [sent2labels(s) for s in train_sents]

    trainer = pycrfsuite.Trainer(verbose=False)
    trainer.set_params({
        'c1': 1.0,  
        'c2': 1e-3, 
        'max_iterations': 50, 
        'feature.possible_transitions': True
    })

    for xseq, yseq in zip(X_train, y_train):
        trainer.append(xseq, yseq)

    trainer.train(model_path)

预测

创建 Tagger 并加载模型,即可在测试集中选择一个的句子打标签。

def predict():
    tagger = pycrfsuite.Tagger()
    tagger.open(model_path)
    example_sent = test_sents[3]
    print(' '.join(sent2tokens(example_sent)), end='\n\n')
    print("Predicted:", ' '.join(tagger.tag(sent2features(example_sent))))
    print("Correct:  ", ' '.join(sent2labels(example_sent)))

比如下面的预测结果。

García Aranda presentó a la prensa el sistema Amadeus , que utilizan la mayor parte de las agencias de viajes españolas para reservar billetes de avión o tren , así como plazas de hotel , y que ahora pueden utilizar también los usuarios finales a través de Internet .

Predicted: B-PER I-PER O O O O O O B-MISC O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O B-MISC O
Correct:   B-PER I-PER O O O O O O B-MISC O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O B-MISC O

评估

最后是评估我们的模型总体效果,将测试集中所有句子输入到训练出来的模型,将得到的预测结果与测试集句子对应的标签对比,输出各项指标。

def bio_classification_report(y_true, y_pred):
    lb = LabelBinarizer()
    y_true_combined = lb.fit_transform(list(chain.from_iterable(y_true)))
    y_pred_combined = lb.transform(list(chain.from_iterable(y_pred)))

    tagset = set(lb.classes_) - {'O'}
    tagset = sorted(tagset, key=lambda tag: tag.split('-', 1)[::-1])
    class_indices = {cls: idx for idx, cls in enumerate(lb.classes_)}

    return classification_report(
        y_true_combined,
        y_pred_combined,
        labels=[class_indices[cls] for cls in tagset],
        target_names=tagset,
    )


def evaluate():
    tagger = pycrfsuite.Tagger()
    tagger.open(model_path)
    X_test = [sent2features(s) for s in test_sents]
    y_test = [sent2labels(s) for s in test_sents]
    y_pred = [tagger.tag(xseq) for xseq in X_test]
    print(bio_classification_report(y_test, y_pred))

比如下面的结果。

             precision    recall  f1-score   support

      B-LOC       0.78      0.75      0.76      1084
      I-LOC       0.66      0.60      0.63       325
     B-MISC       0.69      0.47      0.56       339
     I-MISC       0.61      0.49      0.54       557
      B-ORG       0.79      0.81      0.80      1400
      I-ORG       0.80      0.79      0.80      1104
      B-PER       0.82      0.87      0.84       735
      I-PER       0.87      0.93      0.90       634

avg / total       0.77      0.76      0.76      6178

github

https://github.com/sea-boat/nlp_lab/blob/master/crf_ner/crf_ner.py

最后

crf 比起 hmm 来对特征的设计更加灵活,而且 crf 是无向图,比如 hmm 的有向图可以提取出更多的特征,所以总体效果要比 hmm 要好,但是它也有自己的缺点,复杂度高,大量的特征函数使得训练时代价高。

-------------推荐阅读------------

我的2017文章汇总——机器学习篇

我的2017文章汇总——Java及中间件

我的2017文章汇总——深度学习篇

我的2017文章汇总——JDK源码篇

我的2017文章汇总——自然语言处理篇

我的2017文章汇总——Java并发篇

------------------广告时间----------------

跟我交流,向我提问:

这里写图片描述

公众号的菜单已分为“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。

为什么写《Tomcat内核设计剖析》

欢迎关注:

这里写图片描述