用PyTorch搭建抽取式摘要系统

作者:哈工大SCIR硕士生 赵怀鹏


导读:好的工具能让人事半功倍。神经网络框架PyTorch具有很强的灵活性,并且代码可读性很高,能够帮助你快速实现自己的想法,因此在学术圈越来越流行。本文讲解如何利用PyTorch搭建一个简易的单文档抽取式摘要系统。


一:PyTorch简介


PyTorch是一种非常简单,优雅的动态图框架。其接口和模块的设计比较清晰,能够帮助你快速实现自己的想法。下面借用李飞飞教授公开课cs231n第8讲Deep Learning Software的内容介绍下动态图相比静态图的一些优势。当然静态图也有自己的优势,这里不加讨论,大家可以详细看下这一讲的内容帮助你挑选心仪的框架。下面介绍静态图和动态图的一个重要区别:循环和条件判断。


① 条件判断


假设我们需要判断的正负号来执行不同的条件。PyTorch版本


if z > 0:
    ...
else:    ...

Tensorflow版本:

tf.cond(tf.less(z,0),f1,f2)

② 循环

PyTorch版本

for t in range(T):
   ...

Tensorflow版本:

tf.fold1(f,arg1,arg2)

通过上面的伪代码我们可以看到PyTorch更加贴近python语法,让我们写代码更加自然。而Tensorflow需要严格遵守其定义的一套API。当然Tensorflow也有自己的优势。这里的例子仅展示了动态图框架的灵活性。


二:抽取式摘要简介


摘要是对信息的高度概括,它能够帮助我们在海量数据中快速获取自己想要的信息。在信息爆炸的时代,只靠人工写摘要是不现实的,因此我们需要一套自动摘要系统来帮助人们快速获取想要的信息。自动摘要按生成摘要的方式可以分为抽取式摘要和生成式摘要,按照文本类型可以分为单文档摘要和多文档摘要。


无监督学习是传统抽取式摘要的主流方法。基于无监督学习的抽取式摘要可以分为三类[1]:A. 向量空间模型(The Vector-Space Methods). 其思想就是用向量表示句子和文档,然后计算其相似度来决定哪些句子重要。代表的方法有LSA 等。B. 基于图的模型(The Graph-Based Methods). 图模型把文章建模成图,节点表示一个句子,边表示句子见的相似或相关程度。图模型的重要理论依据是中心理论,认为如果一个句子和周围的句子都很相似,那么这个句子是能够代表文章信息的。TextRank[2]就是其中重要的模型。C. 组合优化方法(The Combinational Optimization Methods).组合优化方法就是把抽取式摘要看做一个组合优化问题,代表的算法有ILP(the integer linear programming method)和次模函数(submodular functions)。


近些年来,随着深度学习在自然语言处理领域的广泛应用,基于有监督学习的抽取式摘要的工作逐渐成为主流。其代表的工作有Jianpeng Cheng et.al[3]在2016年提出的基于Seq2Seq的抽取式摘要方法,并达到了当时的state of the art。另外这篇工作基于DailyMail数据集利用无监督学习构造一份抽取式摘要的数据集,本次实验也是利用的这份公开数据集。今年AAAI上Ramesh et. al.[4]提出SummaRuNNer,并且达到了目前的state of the art。本文实验就是复现这篇工作。下面简单介绍一下这个模型。



图1: SummaRuNNer模型


如图1所示,该模型是有一个两层RNN构成。最底层是词的输入,第二层是词级别的双向GRU,用来建模句子表示。第二层的每个句子的隐层各自做average pooling作为各自句子的表示。第三层是句子级别的双向GRU,输入是上一层的句子表示。得到隐层再做average pooling就能够得到文档的表示。最后利用文档的表示来帮助我们依次对句子做分类。  最后分类层的公式如下:



图2: 分类层公式


其中是到达第个位置的已经生成的摘要的表示,是文章的表示。 表示第个句子的信息,计算的是当前的句子和文章表示的相似度,表示的是当前的句子能够带来多少“新”的信息,接下来的三项分别表示绝对位置,相对位置和偏置。可以看出整个公式非常直观,可解释性很强。



最后Loss采用的负对数似然。在最终选取摘要的时候并不是简单的分类,而是根据每个句子的概率高低排序,选择概率最高的前几句即可。关于模型再进一步的讨论和细节读者可以参考原文,这里不再作讨论和扩展。


三:代码实现


下面简单介绍下用PyTorch实现该模型的关键步骤,具体细节可以参考完整代码:https://github.com/hpzhao/SummaRuNNer


① 模型训练


了解一个框架最重要的就是看它如何训练,测试。我们先把SummaRuNNer这个模型看成一个黑盒,看下如何利用这个模型来训练一个神经网络。

首先我们申请一个模型:

net = SummaRuNNer(config)
net.cuda()

这里的net就是我们的网络。接下来定义损失函数和优化器:

# Loss and Optimizer
criterion = nn.BCELoss() optimizer = torch.optim.Adam(net.parameters(), lr=args.lr)

接下来我们将一篇文章的句子作为模型的输入用来得到每个句子的分类概率,这也就是前向过程:

 outputs = net(sents)

接下来是反向过程,当我们得到Loss之后就可以对其求梯度了:

optimizer.zero_grad()
loss = criterion(outputs, labels)
loss.backward()

这里我们首先要清空上一次计算存留的梯度值,然后计算Loss,最后再用backward()这个函数来自动求梯度,这样看是不是很简单直观呢。我们求过梯度之后就能进行反向传播了:

# gradient clipping     
torch.nn.utils.clip_grad_norm(net.parameters(), 1e-4) optimizer.step()

这里做了gradient clipping,为了学习更加的稳定。最核心的就是optimizer.step()这一步利用我们之前定义好的Adam算法来进行参数更新。

至此我们可以说完成了利用PyTorch来训练一个神经网络。比起Tensorflow定义了一套自己的语法框架,PyTorch可以说是非常简单,直观。最后我们保存训练的模型:

torch.save(net.state_dict(), args.model_file)


② 模型测试

模型测试最核心的是加载模型,加载模型之后我们可以通过之前的前向过程得到每个句子的预测概率。

net = SummaRuNNer(config).cuda()
net.load_state_dict(torch.load(args.model_file))
for index, docs in enumerate(test_loader):    doc = docs[0]    x, y = prepare_data(doc, word2id)    sents = Variable(torch.from_numpy(x)).cuda()    outputs = net(sents)

至此我们已经完成了模型的训练和预测部分的核心代码。


③ 网络搭建

在上面的例子中我们把SummaRuNNer模型看成一个黑盒,通过传入篇章来得到我们想要的结果。接下来我们需要搭建网络。

PyTorch搭建网络一般需要继承nn.Module这个类,并实现里面的forward()函数。虽然一开始感觉不太灵活,但nn.Module为我们封装了一些操作,为我们编程带来便利,例如net.parameters()可得到网络所有需要训练的参数。另外,这种方式也让代码可读性更高。下面是利用  nn.Module搭建网络的代码框架:

class SummaRuNNer(nn.Module):
    def __init__(self, config):
        super(SummaRuNNer, self).__init__()
        ...    
   def forward():        ...

接下来我们来完善前向过程。结合模型图,我们首先搭建词级别RNN:

# word level GRU
word_features = self.word_embedding(x) word_outputs, _ = self.word_GRU(word_features)

接下来我们搭建句子级别RNN:

# sentence level GRU
# 句子级别RNN的输入是上一层词级别RNN的隐层做average pooling
sent_features = self._avg_pooling(word_outputs, sequence_length) sent_outputs, _ = self.sent_GRU(sent_features.view(1, -1, self.sent_input_size))

接下来我们利用句子级的GRU来得到篇章的表示:


# document representation
doc_features = self._avg_pooling(sent_outputs, [[x.size(0)]]) doc = torch.transpose(self.tanh(self.fc1(doc_features)), 0, 1)

最后是分类层,我们根据前面得到的表示来实现上述公式:

# classifier layer
outputs = [] sent_outputs = sent_outputs.view(-1, 2 * self.sent_GRU_hidden_units)

# 初始化当前摘要表示        
s = Variable(torch.zeros(100, 1)).cuda()

# 分类层
for position, sent_hidden in enumerate(sent_outputs):    h = torch.transpose(self.tanh(self.fc2(sent_hidden.view(1, -1))), 0, 1)    position_index = Variable(torch.LongTensor([[position]])).cuda()    p = self.position_embedding(position_index).view(-1, 1)    content = torch.mm(self.Wc, h)    salience = torch.mm(torch.mm(h.view(1, -1), self.Ws), doc)    
   # 这里用tanh(s)而不是直接用s的原因是让s的值保持在一定体量    novelty = -1 * torch.mm(torch.mm(h.view(1, -1), self.Wr), self.tanh(s))    position = torch.mm(self.Wp, p)    bias = self.b    Prob = self.sigmoid(content + salience + novelty + position + bias)    s = s + torch.mm(h, Prob)    outputs.append(Prob)
   
return torch.cat(outputs, dim = 0)

至此我们完成了摘要网络的构建。


四:总结

通过上面的例子我们对PyTorch有了一个比较直观的理解。初学者可以看一下PyTorch官网的入门教程:Deep Learning with PyTorch: A 60 Minute Blitz.


参考文献

[1] Chen K Y, Liu S H, Chen B, et al. Extractive broadcast news summarization leveraging recurrent neural network language modeling techniques[J]. IEEE/ACM Transactions on Audio, Speech and Language Processing (TASLP), 2015, 23(8): 1322-1334.[2]  Mihalcea R, Tarau P. TextRank: Bringing Order into Text[C]//EMNLP. 2004, 4: 404-411.[3]     Cheng J, Lapata M. Neural summarization by extracting sentences and words[J]. arXiv preprint arXiv:1603.07252, 2016.[4]  Nallapati R, Zhai F, Zhou B. SummaRuNNer: A recurrent neural network based sequence model for extractive summarization of documents[J]. hiP (yi= 1| hi, si, d), 2017, 1: 1.


本期责任编辑: 张伟男

本期编辑: 刘元兴


本文由机器之心经授权转载自哈工大SCIR,禁止二次转载。

工程
登录后评论
暂无评论
暂无评论~
返回顶部