高开远作者上海交通大学学校

基于句子嵌入的无监督文本摘要(附代码实现)

本文主要介绍的是一个对多种语言的邮件进行无监督摘要抽取的项目,非常详细。文本摘要也是非常有意思的 NLP 任务之一。

A Glance at Text Summarization

文本摘要是从一个或多个源中提取最重要信息,并为特定用户(或多个用户)和任务(或多个任务)生成简短版本的过程。 

-- Advances in Automatic Text Summarization, 1999. 

文本摘要对于人类来说是非常简单的,因为人类天生地具有理解自然语言的能力,并可以提取显著特征以使用自己的文字来总结文档的重点。但是,在当今世界中数据爆炸增长,缺乏人力和时间来解析数据,因此自动文本摘要方法至关重要,主要有以下几个原因: 

  • 自动摘要可以缩短文本阅读时间,提高效率;

  • 当搜索我们所需要的文本时,有摘要可以更为容易查找到;
     
  • 自动摘要提高了索引的效率;

  • 相比于人力摘要,自动摘要更无偏;

  • 个性化的摘要在问答系统中非常有用,因为它们提供了个性化的信息;

  • 使用自动或半自动摘要系统使商业抽象服务能够增加它们处理的文本文档的数量。 

文本摘要的分类

文本摘要方法可以被总结为以下不同的类别:

Based on input type 

1. 单文档:输入长度较短,许多早期的摘要系统主要处理单个文档摘要; 

2. 多文档:输入可以是任意长的。 

Based on the purpose 

1. 通用模型:模型对摘要的文本的领域或内容不做任何假设,并将所有输入视为同类输入。目前大部分已经完成的工作都是围绕着通用的总结; 

2. 领域适应模型:模型使用领域特定的知识来形成更准确的摘要。例如,总结某一特定领域的研究论文、生物医学文献等; 

3. 基于 query 模型:摘要只包含回答有关输入文本的自然语言问题的信息。 

Based on output type 

1. 抽取式模型:从输入文本中选择重要的句子形成摘要,当今大多数的总结方法本质上都是抽取式的。 

2. 生成式模型:模型形成自己的短语和句子,提供更连贯的总结,就像人类在面对文本摘要时会做的那样。这种方法肯定更有吸引力,但比提取摘要困难得多。

文本摘要流程

文本摘要实现主要是参考 Unsupervised Text Summarization Using Sentence Embeddings [1] 这篇论文,可以分解成以下过程:

以英文邮件为例,看看是怎么得到最终的摘要的。

Step-1:数据清洗

常规操作,永远没有干净的数据,自己动手丰衣足食。下面以常见的英文邮件为例:

Hi Jane,

Thank you for keeping me updated on this issue. I'm happy to hear that the issue got resolved after all and you can now use the app in its full functionality again. 
Also many thanks for your suggestions. We hope to improve this feature in the future. 

In case you experience any further problems with the app, please don't hesitate to contact me again.

Best regards,

John Doe
Customer Support

1600 Amphitheatre Parkway
Mountain View, CA
United States

可以看出,邮件起始的问候与末尾的署名对我们的文本摘要任务是毫无作用的,所以我们需要首先去除这些无关因素,否则会使得模型混淆。为此,我们可以借用 Mailgun Talon github 库 [2] 中的部分代码,该代码还可以删除空行。

# clean()函数改写了上面github库中代码以清洗邮件
cleaned_email, _ = clean(email)

lines = cleaned_email.split('\n')
lines = [line for line in lines if line != '']
cleaned_email = ' '.join(lines)

当然,如果不想自己写 clean() 函数的话,也可以直接调用上面链接中的清洗函数:

from talon.signature.bruteforce import extract_signature
cleaned_email, _ = extract_signature(email)

上述原始邮件清洗后得到大概是这样的:

Step-2:语言检测

对于不同的语言,处理的方式会有所不同,所以首先需要对邮件的语言类型进行检测。得益于 python 强大的第三方库,语言检测可以很容易实现,比如使用 polyglot,langdetect,textblob 等。

from langdetect import detect
lang = detect(cleaned_email) # lang = 'en' for an English email

Step-3:句子分割

由上一步检测出邮件语言之后,可以针对该语言对邮件全文进行句子分割。以英文为例,可以使用 NLTK 包中的 sen_tokenize() 方法。

from nltk.tokenize import sent_tokenize
sentences = sent_tokenize(email, language = lang)

举个例子:

Step-4:Skip-Thought编码 

为了邮件文本表示成机器可以识别的输入,同时融入文本的语义信息,需要对文本进行编码,生成特定长度的向量表示,即 Word Embedding。

对于 word embedding,常见的有 word2vec,glove,fasttext 等。对于句子 embedding,一种简单的思路是对句子中的单词取其 word embedding 的加权和,认为不同的单词对整体的贡献程度不一样。例如经常出现的单词(‘and’,‘to’,‘the’等)几乎对句子信息没有贡献,一些很少出现的单词具有更大的代表性,类似于 tf-idf 的思想,也在这篇论文 [3] 中介绍。 

但是,这些无监督的方法没有将单词在句子中的顺序考虑进去,因此会造成性能损失。为了改进这一点,采用了 Skip-Thought Vectors 这篇论文 [4] 提供的思路,使用 wikipedia 训练了一个  Skip-Thoughts 句子嵌入模型: 

1. Encoder Network:Encoder 的结构是典型的 GRU-RNN 框架,对输入的每一个句子 S(i) 都生成一个固定长度的向量表示 h(i); 

2. Decoder Network:Decoder 使用的也是 GRU-RNN 框架,不过有两个 decoder,分别用于生成句子 S(i) 的前一句 S(i−1) 和后一句 S(i+1),输入均为 encoder的输出 h(i)。 

整体框架如下所示:

感谢 Skip-Thought 的开源,我们通过几行简单的代码就可以得到句子向量表示:

import skipthoughts

# 需要预先下载预训练模型
model = skipthoughts.load_model()

encoder = skipthoughts.Encoder(model)
encoded =  encoder.encode(sentences)

Step-5:聚类

在为邮件文本生成句子表示之后,将这些句子编码在高维向量空间中进行聚类,聚类的数量为摘要任务所需要的句子数量。可以将最终摘要的句子数设定为初始输入句子综述的平方根。我们可以使用 K-means 实现:

import numpy as np
from sklearn.cluster import KMeans

n_clusters = np.ceil(len(encoded)**0.5)
kmeans = KMeans(n_clusters=n_clusters)
kmeans = kmeans.fit(encoded)

Step-6:摘要 

聚类之后的每一个簇群都可以认为是一组语义相似的句子集合,而我们只需要其中的一句来表示即可。这一句子的选择为考虑距离聚类中心最接近的句子,然后将每个簇群相对应的候选句子排序,形成最终的文本摘要。摘要中候选句子的顺序由原始电子邮件中句子在其相应簇中的位置确定。例如,如果位于其群集中的大多数句子出现在电子邮件的开头,则将候选句子选择为摘要中的第一句。 

from sklearn.metrics import pairwise_distances_argmin_min

avg = []
for j in range(n_clusters):
    idx = np.where(kmeans.labels_ == j)[0]
    avg.append(np.mean(idx))
closest, _ = pairwise_distances_argmin_min(kmeans.cluster_centers_, encoded)
ordering = sorted(range(n_clusters), key=lambda k: avg[k])
summary = ' '.join([email[closest[idx]] for idx in ordering])

经过上述几个步骤,最终得到的摘要如下所示:

总结

上述介绍的是一种抽取式文本摘要的方法,对于所有抽取式摘要而言,一大特点就是不适用与长度较短文本的摘要,对于短文本的摘要可能 Seq2Seq 的模型 [5] 效果会更好。

对于 Sentence Embedding 可以进一步优化,使用更有效的句子编码模型可能会使得最终效果有所提高。 

Skip-Thought 编码维度为 4800,如此高维度对后续的聚类可能效果有所影响(Curse of Dimensionality)。可以考虑在聚类之前使用自动编码器或 LSTM-Autoencoder 在压缩表示中传递进一步的序列信息; 

完整的代码实现参考:https://github.com/jatana-research/email-summarization

参考文献

[1] https://www.cs.utexas.edu/~asaran/reports/summarization.pdf

[2] https://github.com/mailgun/talon/blob/master/talon/signature/bruteforce.py

[3] https://openreview.net/pdf?id=SyK00v5xx

[4] https://arxiv.org/abs/1506.06726

[5] https://machinelearningmastery.com/encoder-decoder-models-text-summarization-keras/

PaperWeekly
PaperWeekly

推荐、解读、讨论和报道人工智能前沿论文成果的学术平台。

理论无监督文本摘要自然语言处理
1
暂无评论
暂无评论~