Fork me on GitHub

BiLSTM 上的 CRF,用命名实体识别任务来解释 CRF(4)

作者:CreateMoMo

编译:ronghuaiyang

前几篇文章:
BiLSTM 上的 CRF,用命名实体识别任务来解释 CRF(1)
BiLSTM 上的 CRF,用命名实体识别任务来解释 CRF(2)损失函数
BiLSTM 上的 CRF,用命名实体识别任务来解释 CRF(3)推理

导读:今天给大家介绍一下具体的代码实现。

3 Chainer 实现

在本节中,我将解释代码的结构。此外,还将给出实现 CRF 损失层的一个重要技巧。最后,会公布 Chainer(2.0 版)实现的源代码。

3.1 总体结构

可以看到,代码主要包括三个部分:初始化、损失计算和句子的预测标签。(完整的代码将在下一篇文章中发布)

class My_CRF():
	def __init__():
		#[Initialization]
		'''
		Randomly initialize transition scores
	    '''
	def __call__(training_data_set):
		#[Loss Function]
		Total Cost = 0.0

		#Compute CRF Loss
		'''
		for sentence in training_data_set:
			1) The real path score of current sentence according the true labels
			2) The log total score of all the possbile paths of current sentence
			3) Compute the cost on this sentence using the results from 1) and 2)
			4) Total Cost += Cost of this sentence
		'''
		return Total Cost

	def argmax(new sentences):
		#[Prediction]
		'''
		Predict labels for new sentences
        '''

3.2 增加两个额外的标签(START 和 END)

如 2.2 节所述,在 transition 评分矩阵中,我们添加了两个 START 和 END 标签。当我们计算某句话的损失时,这将影响 transition 得分矩阵的初始化和 emission 得分矩阵的值。

[Example]

假设在我们的数据集中,我们只有一种类型的命名实体 PERSON,因此我们实际上有三个标签(不包括开始和结束):B-PERSON、I-PERSON 和 O。

Transition 得分举证

在添加了两个额外的标签(START 和 END)之后,当我们在 init 函数中初始化 transition 分数时,我们这样做:

n_label = 3 #B-PERSON, I-PERSON AND O
transitions = np.array(value, dtype=np.float32)

value 的形状是(n_label + 2, n_label + 2),2 是 START 和 END 的数量。另外,“value”的值是随机生成的。

Emission 得分矩阵

你应该知道,BiLSTM 层的输出是[2.1]中描述的句子的 transition 分数。例如,我们的句子有 3 个单词,BiLSTM 的输出应该是这样的:

添加了额外的 START 和 END 标签后,emission 分数矩阵为:

如上表所示,我们通过添加两个单词(start 和 end)及其对应的标签(START 和 END)来扩展排放 emission 矩阵。BiLSTM 层的输出不包括新增单词的 emission 分数,但是我们可以手动指定这些分数(即x_{start,START}=0x_{start,theOtherLabels}=-1000。单词“start”上的其他标签的 emission 分数应该是一个小的值(例如-1000)。如果你想再设置一个小的值,这是完全可以的,不会影响我们模型的性能。

3.3 更新整个结构

基于上面的解释,这里有一个更详细的伪代码:

class My_CRF():
	def __init__(n_label):
		#[Initialization]
		'''
		1) Randomly initialize transition score matrix.
        The shape of this matrix is (n_label+2, n_label+2).
        n_labels is the number of named entity classes in our dataset (e.g. B-Person, I-Person, O).
        2 is the number of our added labels (i.e. START and END).
		2) Moreover, we also set the small value as -1000 here.
		'''

	def __call__(training_data_set):
		#[Loss Function]
		Total Cost = 0.0

		#Compute CRF Loss
		for sentence in training_data_set:
		    '''
		    1) Extend the emission score matrix by adding words(start and end)
            and adding labels(START and END)
		    2) Compute the real path score of current sentence according the
            true labels (section 2.4)
		    3) Compute the log total score of all the possbile paths of current
            sentence (section 2.5)
		    4) Compute the cost on this sentence using the results from 2)
            and 3) that is -(real_path_score - all_path_score). (section 2.5)
		    5) Total Cost += the cost of current sentence
		    '''
		return Total Cost

	def argmax(new sentences):
		#[Prediction]
		for sentence in new_sentences:
		    '''
		    1) Extend the emission score matrix by adding words(start and end)
            and adding labels(START and END)
		    2) Predict the labels for the current new sentence (section 2.6)
		    '''

3.4 Demo

在这一节中,我们将造两个假句子,分别只有 2 个单词和 1 个单词。此外,我们还会随机生成他们的真实答案。最后,我们将展示如何使用 Chainer v2.0 训练 CRF 层。包括 CRF 层在内的所有代码都是来自 GitHub:https://github.com/createmomo/CRF-Layer-on-the-Top-of-BiLSTM

首先,我们导入自己的 CRF 层含义,' MyCRFLayer '。

import numpy as np
import chainer
import MyCRFLayer

在我们的数据集中我们只有两个标签(例如 B-Person, O)

n_label = 2

下面的代码块生成两个句子,xs = [x1, x2]。句子 x1 有两个单词,x2 只有一个单词。

a = np.random.uniform(-1, 1, n_label).astype('f')
b = np.random.uniform(-1, 1, n_label).astype('f')
x1 = np.stack([b, a])
x2 = np.stack([a])
xs = [x1, x2]

应该注意的是,x1 和 x2 的元素不是词嵌入,而是 BiLSTM 层的 emission 分数,这里没有实现。

例如,在 x1 句子中,我们有两个单词 w0 和 w1,而 x1 是一个形状为(2, 2)的矩阵。第一个“2”表示它有两个单词,第二个“2”表示我们的数据集中有两个标签,如下表所示。

接下来,我们应该有这两个句子的真正标签。

ys = [np.random.randint(n_label,size = x.shape[0],dtype='i') for x in xs]
print('Ground Truth:')
for i,y in enumerate(ys):
    print('\tsentence {0}: [{1}]'.format(str(i),' '.join([str(label) for label in y])))

这里是随机生成的 ground truth。

Ground Truth:
    sentence 0: [0 0]
    sentence 1: [1]

虽然我们并没有真正的 BiLSTM 层,但这并不会影响我们展示如何在 chainer 中训练一个模型。我们模拟了 BiLSTM 层的输出和真实答案。因此,我们可以使用一些优化器来优化 CRF 层。

在本文中,我们使用了随机梯度下降法来训练我们的模型。(如果你现在不熟悉训练方法,你可以以后再学。)这个优化器将根据我们的 CRF 层根据预测标签和 ground truth 标签之间的损失来更新参数(即 transition 矩阵)。

from chainer import optimizers
optimizer = optimizers.SGD(lr=0.01)
optimizer.setup(my_crf)
optimizer.add_hook(chainer.optimizer.GradientClipping(5.0))

CRF 层通过标签的数量来初始化(不包括额外添加的开始和结束)。

my_crf = MyCRFLayer.My_CRF(n_label)

然后我们可以开始训练 CRF 层。

print('Predictions:')
for epoch_i in range(201):
    with chainer.using_config('train', True):
        loss = my_crf(xs,ys)
        # update parameters
        optimizer.target.zerograds()
        loss.backward()
        optimizer.update()
    with chainer.using_config('train', False):
        if epoch_i % 50 == 0:
            print('\tEpoch {0}: (loss={1})'.format(str(epoch_i),str(loss.data)))
            for i, prediction in enumerate(my_crf.argmax(xs)):
                print('\t\tsentence {0}: [{1}]'.format(str(i), ' '.join([str(label) for label in prediction])))

正如我们的代码输出所示,损失正在减少,CRF 层正在学习(预测正在变得正确)。

Predictions:
    Epoch 0: (loss=3.06651592255)
        sentence 0: [1 1]
        sentence 1: [1]
    Epoch 50: (loss=1.96822023392)
        sentence 0: [1 1]
        sentence 1: [1]
    Epoch 100: (loss=1.51349794865)
        sentence 0: [0 0]
        sentence 1: [1]
    Epoch 150: (loss=1.27118945122)
        sentence 0: [0 0]
        sentence 1: [1]
    Epoch 200: (loss=1.09977662563)
        sentence 0: [0 0]
        sentence 1: [1]

3.5 GitHub

demo 和 CRF 层代码可以在 GitHub 上找到。代码可能并不完美。因为为了便于理解,一些实现非常简单。我相信它可以被优化成一个更有效的算法。

英文原文:https://createmomo.github.io/2017/12/07/CRF-Layer-on-the-Top-of-BiLSTM-8/


本文地址:https://www.6aiq.com/article/1585575548638
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出