基於句子嵌入的無監督文字摘要(附程式碼實現)

PaperWeekly發表於2020-02-04

本文主要介紹的是一個對多種語言的郵件進行無監督摘要抽取的專案,非常詳細。文字摘要也是非常有意思的 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/

相關文章