APP下载
报价宝  ›  科技  › 

基于飞桨PaddlePaddle的语义角色标注任务全解析

报价宝 来源:baojiabao.com 发布时间:2019-10-02 18:53:00 11月15日更新
报价宝综合消息基于飞桨PaddlePaddle的语义角色标注任务全解析

自然语言处理中的自然语言句子级分析技术,可以大致分为词法分析、句法分析、语义分析三个层面。

词法分析:第一层面的词法分析 (lexical analysis) 包括汉语分词和词性标注两部分。

句法分析:对输入的文字句子进行分析以得到句子的句法结构的处理过程。语义分析 (semantic parsing):语义分析的最终目的是 理解句子表达的真实语义。语义角色标注是实现浅层语义分析的一种方式。在一个句子中,谓词是对主语的陈述或说明,指出“做什么”、“是什么”或“怎么样,代表了一个事件的核心,跟谓词搭配的名词称为论元。语义角色是指论元在动词所指事件中担任的角色。主要有:施事者(Agent)、受事者(Patient)、客体(Theme)、经验者(Experiencer)、受益者(Beneficiary)、工具(Instrument)、处所(Location)、目标(Goal)和来源(Source)等。

以上示例表示,“遇到”是谓词(Predicate,通常简写为“Pred”),“小明”是施事者(Agent),“小红”是受事者(Patient),“昨天”是事件发生的时间(Time),“公园”是事情发生的地点(Location)。

语义角色标注(Semantic Role Labeling,SRL)以句子的谓词为中心,不对句子所包含的语义资讯进行深入分析,只分析句子中各成分与谓词之间的关系,即句子的谓词(Predicate)- 论元(Argument)结构,并用语义角色来描述这些结构关系,是许多自然语言理解任务(如资讯抽取,篇章分析,深度问答等)的一个重要中间步骤。

基于语块(chunk)的 SRL 方法

基于语块的 SRL 方法将 SRL [1] 作为一个序列标注问题来解决。序列标注任务一般都会采用 BIO 表示方式来定义序列标注的标签集,我们先来介绍这种表示方法。在 BIO 表示法中,B 代表语块的开始,I 代表语块的中间,O 代表语块结束。通过 B、I、O 三种标记将不同的语块赋予不同的标签,例如:对于一个由角色 A 拓展得到的语块组,将它所包含的第一个语块赋予标签 B-A,将它所包含的其它语块赋予标签 I-A,不属于任何论元的语块赋予标签 O。

从上面的例子可以看到,根据序列标注结果可以直接得到论元的语义角色标注结果,是一个相对简单的过程。这种简单性体现在:(1)依赖浅层句法分析,降低了句法分析的要求和难度;(2)没有了候选论元剪除这一步骤;(3)论元的识别和论元标注是同时实现的。这种一体化处理论元识别和论元标注的方法,简化了流程,降低了错误累积的风险,往往能够取得更好的结果。

深度双向 LSTM(DB-LSTM)SRL 模型

与基于语块的 SRL 方法类似,在本文中我们也将 SRL 看作一个序列标注问题,不同的是,我们只依赖输入文字序列,不依赖任何额外的语法解析结果或是复杂的人造特征,利用深度神经网络构建一个端到端学习的 SRL 系统。

循环神经网络(Recurrent Neural Network)是一种对序列建模的重要模型,在自然语言处理任务中有着广泛地应用。不同于前馈神经网络(Feed-forward Neural Network),RNN 能够处理输入之间前后关联的问题。LSTM 是 RNN 的一种重要变种,常用来学习长序列中蕴含的长程依赖关系,这里我们就使用 LSTM 来解决 SRL 问题。

在 SRL 任务中,输入是“谓词”和“一句话”,目标是从这句话中找到谓词的论元,并标注论元的语义角色。如果一个句子含有 n 个谓词,这个句子会被处理 n 次。一个最为直接的模型是下面这样:

构造输入;输入 1 是谓词,输入 2 是句子;将输入 1 扩充套件成和输入 2 一样长的序列,用 one-hot 方式表示;one-hot 方式的谓词序列和句子序列通过词表,转换为实向量表示的词向量序列;将步骤 2 中的 2 个词向量序列作为双向 LSTM 的输入,学习输入序列的特征表示;CRF 以步骤 3 中模型学习到的特征为输入,以标记序列为监督讯号,实现序列标注;大家可以尝试上面这种方法。这里,我们提出一些改进,引入两个简单但对提高系统性能非常有效的特征:

谓词上下文:上面的方法中,只用到了谓词的词向量表达谓词相关的所有资讯,这种方法始终是非常弱的,特别是如果谓词在句子中出现多次,有可能引起一定的歧义。从经验出发,谓词前后若干个词的一个小片段,能够提供更丰富的资讯,帮助消解歧义。于是,我们把这样的经验也新增到模型中,为每个谓词同时抽取一个“谓词上下文”片段,也就是从这个谓词前后各取 n 个词构成的一个视窗片段;谓词上下文区域标记:为句子中的每一个词引入一个 0-1 二值变数,表示它们是否在“谓词上下文”片段中;修改后的模型如下(下图是一个深度为 4 的模型结构示意图):

构造输入;输入 1 是句子序列,输入 2 是谓词序列,输入 3 是谓词上下文,从句子中抽取这个谓词前后各 n 个词,构成谓词上下文,用 one-hot 方式表示,输入 4 是谓词上下文区域标记,标记了句子中每一个词是否在谓词上下文中;将输入 2~3 均扩充套件为和输入 1 一样长的序列;输入 1~4 均通过词表取词向量转换为实向量表示的词向量序列;其中输入 1、3 共享同一个词表,输入 2 和 4 各自独有词表;第 2 步的 4 个词向量序列作为双向 LSTM 模型的输入;LSTM 模型学习输入序列的特征表示,得到新的特性表示序列;CRF 以第 3 步中 LSTM 学习到的特征为输入,以标记序列为监督讯号,完成序列标注;

图:SRL 任务上的深层双向 LSTM 模型

接下来,我们就一步一步的实践这个任务:给定一句话和这句话里的一个谓词,通过序列标注的方式,从句子中找到谓词对应的论元,同时标注它们的语义角色。

资料介绍

在本文中,我们选用 CoNLL 2005SRL 任务开放出的资料集作为示例。需要特别说明的是,CoNLL 2005 SRL 任务的训练数集和开发集在比赛之后并非免费进行公开,目前,能够获取到的只有测试集,包括 Wall Street Journal 的 23 节和 Brown 语料集中的 3 节。在本文中,我们以测试集中的 WSJ 资料为训练集来讲解模型。但是,由于测试集中样本的数量远远不够,如果希望训练一个可用的神经网络 SRL 系统,请考虑付费获取全量资料。

原始资料中同时包括了词性标注、命名实体识别、语法解析树等多种资讯。我们使用 test.wsj 资料夹中的资料进行训练和测试,并只会用到 words 资料夹(文字序列)和 props 资料夹(标注结果)下的资料。本文使用的资料目录如下:

conll05st-release/

└── test.wsj

├── props # 标注结果

└── words # 输入文字序列

标注资讯源自 Penn TreeBank[2] 和 PropBank[3] 的标注结果。PropBank 标注结果的标签和我们在文章一开始示例中使用的标注结果标签不同,但原理是相同的,关于标注结果标签含义的说明,请参考论文 [4]。

原始资料需要进行资料预处理才能被飞桨处理,预处理包括下面几个步骤:

将文字序列和标记序列其合并到一条记录中;一个句子如果含有 n 个谓词,这个句子会被处理 n 次,变成 n 条独立的训练样本,每个样本一个不同的谓词;抽取谓词上下文和构造谓词上下文区域标记;构造以 BIO 法表示的标记;依据词典获取词对应的整数索引。预处理完成之后一条训练样本资料包含 9 个域,分别是:句子序列、谓词、谓词上下文(占 5 列)、谓词上下区域标志、标注序列。下表是一条训练样本的示例。

除资料之外,我们同时提供了以下资源:

我们在英文维基百科上训练语言模型得到了一份词向量用来初始化 SRL 模型。在 SRL 模型训练过程中,词向量不再被更新。关于语言模型和词向量可以参考:《

基于PaddlePaddle的词向量实战 | 深度学习基础任务教程系列(二)》。我们训练语言模型的语料共有 995,000,000 个 token,词典大小控制为 4900,000 词。CoNLL 2005 训练语料中有 5% 的词不在这 4900,000 个词中,我们将它们全部看作未登入词,用表示。

获取词典,打印词典大小:

from __future__ import print_function

import math, os

import numpy as np

import paddle

import paddle.dataset.conll05 as conll05

import paddle.fluid as fluid

import six

import time

with_gpu = os.getenv('WITH_GPU', '0') != '0'

word_dict, verb_dict, label_dict = conll05.get_dict()

word_dict_len = len(word_dict)

label_dict_len = len(label_dict)

pred_dict_len = len(verb_dict)

print('word_dict_len: ', word_dict_len)

print('label_dict_len: ', label_dict_len)

print('pred_dict_len: ', pred_dict_len)

模型配置说明

定义输入资料维度及模型超引数。

mark_dict_len = 2 # 谓上下文区域标志的维度,是一个0-1 2值特征,因此维度为2

word_dim = 32 # 词向量维度

mark_dim = 5 # 谓词上下文区域通过词表被对映为一个实向量,这个是相邻的维度

hidden_dim = 512 # LSTM隐层向量的维度 : 512 / 4

depth = 8 # 栈式LSTM的深度

mix_hidden_lr = 1e-3 # linear_chain_crf层的基础学习率

IS_SPARSE = True # 是否以稀疏方式更新embedding

PASS_NUM = 10 # 训练轮数

BATCH_SIZE = 10 # batch size 大小

embedding_name = 'emb'

这里需要特别说明的是,引数 hidden_dim = 512 实际指定了 LSTM 隐层向量的维度为 128,关于这一点请参考飞桨官方文件中 dynamic_lstm 的说明。

如上文提到,我们用基于英文维基百科训练好的词向量来初始化序列输入、谓词上下文总共 6 个特征的 embedding 层引数,在训练中不更新。

# 这里载入PaddlePaddle储存的二进位制引数

def load_parameter(file_name, h, w):

with open(file_name, 'rb') as f:

f.read(16) # skip header.

return np.fromfile(f, dtype=np.float32).reshape(h, w)

训练模型

我们根据网络拓扑结构和模型引数来进行训练,在构造时还需指定优化方法,这里使用最基本的 SGD 方法 (momentum 设定为 0),同时设定了学习率、正则等。

定义训练过程的超引数

use_cuda = False #在cpu上执行训练

save_dirname = "label_semantic_roles.inference.model" #训练得到的模型引数储存在档案中

is_local = True

资料输入层定义

定义了模型输入特征的格式,包括句子序列、谓词、谓词上下文的 5 个特征、和谓词上下区域标志

# 句子序列

word = fluid.layers.data(

name='word_data', shape=[1], dtype='int64', lod_level=1)

# 谓词

predicate = fluid.layers.data(

name='verb_data', shape=[1], dtype='int64', lod_level=1)

# 谓词上下文5个特征

ctx_n2 = fluid.layers.data(

name='ctx_n2_data', shape=[1], dtype='int64', lod_level=1)

ctx_n1 = fluid.layers.data(

name='ctx_n1_data', shape=[1], dtype='int64', lod_level=1)

ctx_0 = fluid.layers.data(

name='ctx_0_data', shape=[1], dtype='int64', lod_level=1)

ctx_p1 = fluid.layers.data(

name='ctx_p1_data', shape=[1], dtype='int64', lod_level=1)

ctx_p2 = fluid.layers.data(

name='ctx_p2_data', shape=[1], dtype='int64', lod_level=1)

# 谓词上下区域标志

mark = fluid.layers.data(

name='mark_data', shape=[1], dtype='int64', lod_level=1)

定义网络结构

首先预训练并定义模型输入层:

#预训练谓词和谓词上下区域标志

predicate_embedding = fluid.layers.embedding(

input=predicate,

size=[pred_dict_len, word_dim],

dtype='float32',

is_sparse=IS_SPARSE,

param_attr='vemb')

mark_embedding = fluid.layers.embedding(

input=mark,

size=[mark_dict_len, mark_dim],

dtype='float32',

is_sparse=IS_SPARSE)

#句子序列和谓词上下文5个特征并预训练

word_input = [word, ctx_n2, ctx_n1, ctx_0, ctx_p1, ctx_p2]

# 因词向量是预训练好的,这里不再训练embedding表,

# 引数属性trainable设定成False阻止了embedding表在训练过程中被更新

emb_layers = [

fluid.layers.embedding(

size=[word_dict_len, word_dim],

input=x,

param_attr=fluid.ParamAttr(

name=embedding_name, trainable=False)) for x in word_input

]

#加入谓词和谓词上下区域标志的预训练结果

emb_layers.append(predicate_embedding)

emb_layers.append(mark_embedding)

定义 8 个 LSTM 单元以“正向/反向”的顺序对所有输入序列进行学习。

# 共有8个LSTM单元被训练,每个单元的方向为从左到右或从右到左,

# 由引数`is_reverse`确定

# 第一层栈结构

hidden_0_layers = [

fluid.layers.fc(input=emb, size=hidden_dim, act='tanh')

for emb in emb_layers

]

hidden_0 = fluid.layers.sums(input=hidden_0_layers)

lstm_0 = fluid.layers.dynamic_lstm(

input=hidden_0,

size=hidden_dim,

candidate_activation='relu',

gate_activation='sigmoid',

cell_activation='sigmoid')

# 用直连的边来堆叠L-LSTM、R-LSTM

input_tmp = [hidden_0, lstm_0]

# 其余的栈结构

for i in range(1, depth):

mix_hidden = fluid.layers.sums(input=[

fluid.layers.fc(input=input_tmp[0], size=hidden_dim, act='tanh'),

fluid.layers.fc(input=input_tmp[1], size=hidden_dim, act='tanh')

])

lstm = fluid.layers.dynamic_lstm(

input=mix_hidden,

size=hidden_dim,

candidate_activation='relu',

gate_activation='sigmoid',

cell_activation='sigmoid',

is_reverse=((i % 2) == 1))

input_tmp = [mix_hidden, lstm]

# 取最后一个栈式LSTM的输出和这个LSTM单元的输入到隐层对映,

# 经过一个全连线层对映到标记字典的维度,来学习 CRF 的状态特征

feature_out = fluid.layers.sums(input=[

fluid.layers.fc(input=input_tmp[0], size=label_dict_len, act='tanh'),

fluid.layers.fc(input=input_tmp[1], size=label_dict_len, act='tanh')

])

# 标注序列

target = fluid.layers.data(

name='target', shape=[1], dtype='int64', lod_level=1)

# 学习 CRF 的转移特征

crf_cost = fluid.layers.linear_chain_crf(

input=feature_out,

label=target,

param_attr=fluid.ParamAttr(

name='crfw', learning_rate=mix_hidden_lr))

avg_cost = fluid.layers.mean(crf_cost)

# 使用最基本的SGD优化方法(momentum设定为0)

sgd_optimizer = fluid.optimizer.SGD(

learning_rate=fluid.layers.exponential_decay(

learning_rate=0.01,

decay_steps=100000,

decay_rate=0.5,

staircase=True))

sgd_optimizer.minimize(avg_cost)

资料介绍部分提到 CoNLL 2005 训练集付费,这里我们使用测试集训练供大家学习。conll05.test() 每次产生一条样本,包含 9 个特征,shuffle 和组完 batch 后作为训练的输入。

crf_decode = fluid.layers.crf_decoding(

input=feature_out, param_attr=fluid.ParamAttr(name='crfw'))

train_data = paddle.batch(

paddle.reader.shuffle(

paddle.dataset.conll05.test(), buf_size=8192),

batch_size=BATCH_SIZE)

place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()

通过 feeder 来指定每一个数据和 data_layer 的对应关系, 下面的 feeder 表示 conll05.test() 产生资料的第 0 列对应的 data_layer 是 word。

feeder = fluid.DataFeeder(

feed_list=[

word, ctx_n2, ctx_n1, ctx_0, ctx_p1, ctx_p2, predicate, mark, target

],

place=place)

exe = fluid.Executor(place)

开始训练

main_program = fluid.default_main_program()

exe.run(fluid.default_startup_program())

embedding_param = fluid.global_scope().find_var(

embedding_name).get_tensor()

embedding_param.set(

load_parameter(conll05.get_embedding(), word_dict_len, word_dim),

place)

start_time = time.time()

batch_id = 0

for pass_id in six.moves.xrange(PASS_NUM):

for data in train_data():

cost = exe.run(main_program,

feed=feeder.feed(data),

fetch_list=[avg_cost])

cost = cost[0]

if batch_id % 10 == 0:

print("avg_cost: " + str(cost))

if batch_id != 0:

print("second per batch: " + str((time.time(

) - start_time) / batch_id))

# Set the threshold low to speed up the CI test

if float(cost) if save_dirname is not None:

fluid.io.save_inference_model(save_dirname, [

'word_data', 'verb_data', 'ctx_n2_data',

'ctx_n1_data', 'ctx_0_data', 'ctx_p1_data',

'ctx_p2_data', 'mark_data'

], [feature_out], exe)

break

batch_id = batch_id + 1

应用模型

训练完成之后,需要依据某个我们关心的效能指标选择最优的模型进行预测,可以简单的选择测试集上标记错误最少的那个模型。以下我们给出一个使用训练后的模型进行预测的示例。

首先设定预测过程的引数

use_cuda = False #在cpu上进行预测

save_dirname = "label_semantic_roles.inference.model" #呼叫训练好的模型进行预测

place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()

exe = fluid.Executor(place)

设定输入,用 LoDTensor 来表示输入的词序列,这里每个词的形状 base_shape 都是 [1],是因为每个词都是用一个 id 来表示的。假如基于长度的 LoD 是 [[3, 4, 2]],这是一个单层的 LoD,那么构造出的 LoDTensor 就包含 3 个序列,其长度分别为 3、4 和 2。

注意 LoD 是个列表的列表:

lod = [[3, 4, 2]]

base_shape = [1]

# 构造假资料作为输入,整数随机数的范围是[low, high]

word = fluid.create_random_int_lodtensor(

lod, base_shape, place, low=0, high=word_dict_len - 1)

pred = fluid.create_random_int_lodtensor(

lod, base_shape, place, low=0, high=pred_dict_len - 1)

ctx_n2 = fluid.create_random_int_lodtensor(

lod, base_shape, place, low=0, high=word_dict_len - 1)

ctx_n1 = fluid.create_random_int_lodtensor(

lod, base_shape, place, low=0, high=word_dict_len - 1)

ctx_0 = fluid.create_random_int_lodtensor(

lod, base_shape, place, low=0, high=word_dict_len - 1)

ctx_p1 = fluid.create_random_int_lodtensor(

lod, base_shape, place, low=0, high=word_dict_len - 1)

ctx_p2 = fluid.create_random_int_lodtensor(

lod, base_shape, place, low=0, high=word_dict_len - 1)

mark = fluid.create_random_int_lodtensor(

lod, base_shape, place, low=0, high=mark_dict_len - 1)

使用 fluid.io.load_inference_model 载入 inference_program,feed_target_names 是模型的输入变数的名称,fetch_targets 是预测物件。

[inference_program, feed_target_names,

fetch_targets] = fluid.io.load_inference_model(save_dirname, exe)

构造 feed 字典 {feed_target_name: feed_target_data},results 是由预测目标构成的列表:

assert feed_target_names[0] == 'word_data'

assert feed_target_names[1] == 'verb_data'

assert feed_target_names[2] == 'ctx_n2_data'

assert feed_target_names[3] == 'ctx_n1_data'

assert feed_target_names[4] == 'ctx_0_data'

assert feed_target_names[5] == 'ctx_p1_data'

assert feed_target_names[6] == 'ctx_p2_data'

assert feed_target_names[7] == 'mark_data'

执行预测

results = exe.run(inference_program,

feed={

feed_target_names[0]: word,

feed_target_names[1]: pred,

feed_target_names[2]: ctx_n2,

feed_target_names[3]: ctx_n1,

feed_target_names[4]: ctx_0,

feed_target_names[5]: ctx_p1,

feed_target_names[6]: ctx_p2,

feed_target_names[7]: mark

},

fetch_list=fetch_targets,

return_numpy=False)

输出结果

print(results[0].lod())

np_data = np.array(results[0])

print("Inference Shape: ", np_data.shape)

总结

语义角色标注是许多自然语言理解任务的重要中间步骤。本文我们以语义角色标注任务为例,介绍如何利用飞桨进行序列标注任务。本文所介绍的模型来自我们发表的论文 [5]。由于 CoNLL 2005 SRL 任务的训练资料目前并非完全开放,本文中只使用测试资料作为示例。在这个过程中,我们希望减少对其它自然语言处理工具的依赖,利用神经网络资料驱动、端到端学习的能力,得到一个和传统方法可比、甚至更好的模型。在论文中我们证实了这种可能性。关于模型更多的资讯和讨论可以在论文中找到。

论文地址:https://www.aclweb.org/anthology/P15-1109专案地址:https://github.com/PaddlePaddle/book/blob/develop/07.label_semantic_roles/README.cn.md参考文献

1. Sun W, Sui Z, Wang M, et al. Chinese semantic role labeling with shallow parsing[C]//Proceedings of the 2009 Conference on Empirical Methods in Natural Language Processing: Volume 3-Volume 3. Association for Computational Linguistics, 2009: 1475-1483.

2. Marcus M P, Marcinkiewicz M A, Santorini B. Building a large annotated corpus of English: The Penn Treebank[J]. Computational linguistics, 1993, 19(2): 313-330.

3. Palmer M, Gildea D, Kingsbury P. The proposition bank: An annotated corpus of semantic roles[J]. Computational linguistics, 2005, 31(1): 71-106.

4. Carreras X, Màrquez L. Introduction to the CoNLL-2005 shared task: Semantic role labeling[C]//Proceedings of the Ninth Conference on Computational Natural Language Learning. Association for Computational Linguistics, 2005: 152-164.

5. Zhou J, Xu W. End-to-end learning of semantic role labeling using recurrent neural networks[C]//Proceedings of the Annual Meeting of the Association for Computational Linguistics. 2015.

文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 手机价格 汽车价格 小米手机价格 华为手机价格 红米手机价格