文章太長不想看?ML 文字自動摘要了解一下

機器之心發表於2019-07-08
我們在閱讀新聞報導等實時性文章時,需要快速歸納出文章的大意。但是,如果將一篇很長的文章歸納成一個能夠涵蓋原文中心思想的小段落,則需要我們耗費大量時間。本文介紹了自然語言處理中的兩種文字自動摘要生成方法——抽取式和抽象式文字摘要。這兩種方法透過計算文字中句子成分的權重來生成摘要,可以大大節省通讀全文以及歸納總結主要資訊的時間,為讀者提供方便。

文章太長不想看?ML 文字自動摘要了解一下

你是否曾將一篇冗長的文件歸納為一個小的段落?你用了多長時間呢?手動歸納總結耗費時間、枯燥乏味。文字自動摘要可以克服此類難題,幫你輕鬆歸納出一篇文章的中心思想。

文字摘要方法能夠對冗長文字進行簡潔準確的總結,同時將重點放在傳達有用資訊的章節,而又不失去文章大意。

文字自動摘要旨在將冗長文件變成縮寫版本,若手動完成則可能非常麻煩且成本高昂。

在生成需要的摘要文字之前,機器學習演算法可被訓練用以理解文件,識別傳達重要事實和資訊的章節。

文章太長不想看?ML 文字自動摘要了解一下

使用文字摘要機器學習演算法生成一篇線上新聞文章的摘要

文字自動摘要的必要性

隨著目前數字空間中資料的爆炸式增長,而大多又是非結構化的文字資料,因而需要開發文字自動摘要工具,使人們可以輕易獲知文字大意。當前,我們可以快速訪問大量資訊。但是,大多數資訊冗長、無關緊要,還可能無法傳達其本意。例如,如果你想從一篇線上新聞報導中搜尋一些特定資訊,你也許要吃透報導內容,花費大量時間剔除無用資訊,之後才能找到自己想要了解的資訊。所以,使用能夠提取有用資訊並剔除無關緊要和無用資料的自動文字摘要生成器變得非常重要。文字摘要的實現可以增強文件的可讀性,減少搜尋資訊的時間,獲得更多適用於特定領域的資訊。

文字自動摘要的主要型別

從廣義的角度看,自然語言處理(NLP)中有兩種文字摘要生成方法:抽取式和抽象式。

抽取式摘要(extraction-based summarization)

在抽取式摘要中,抽取一段文字中表示重點內容的單詞子集,並結合起來生成摘要。我們可以將抽取式摘要看作是一支熒光筆-從源文字中抽取主要資訊。

文章太長不想看?ML 文字自動摘要了解一下

熒光筆 = 抽取式摘要

機器學習中,抽取式摘要通常需要衡量基本句子成分的權重,並根據權重結果生成摘要。

不同型別的演算法和方法均可用於衡量句子的權重,之後根據各成分之間的關聯性和相似性進行排序-並進一步將這些成分連線起來以生成摘要。

如下例所示:

文章太長不想看?ML 文字自動摘要了解一下

抽取式摘要

如上例所示,抽取式摘要由熒光筆標黃的單片語成,生成摘要的語法可能不準確。

抽象式摘要

在抽象式摘要中,高階深度學習方法(advanced deep learning technique)用於解釋和縮寫原始文件,就像人類所做的一樣。將抽象式摘要想象成一支鋼筆-它能生成或許不屬於源文件的新句子。

文章太長不想看?ML 文字自動摘要了解一下

鋼筆 = 抽象式摘要

由於抽象式機器學習演算法能夠生成表示源文字中最重要資訊的新短語和句子,所以這些抽象式演算法有助於克服抽取式摘要中的語法不準確問題。

如下例所示:

文章太長不想看?ML 文字自動摘要了解一下

抽象式摘要。

儘管抽象式文字摘要的表現更好,但開發相關演算法需要複雜的深度學習技巧和語言模型

為了獲得合理產出,抽象式摘要方法必須能夠解決諸多自然語言處理問題,如自然語言生成、語義表徵和推理排序(inference permutation)。

同樣地,抽取式文字摘要方法依然大受歡迎。在本文中,我們將重點介紹抽象式文字摘要方法。

如何執行文字摘要

我們使用以下一段話展示如何執行文字摘要抽取:

我們依照以下步驟對這段話作總結,同時儘可能保留原意。

第一步:將這段話轉換成句子

首先,我們將這段話分割成相應的句子。轉換成句子的最佳方法是在句點(period)出現時提取一個句子。

第二步:文字處理

接下來,我們在文字處理中移除停止詞(那些沒有實際意義的常見詞,如「and」和「the」)、數字、標點符號以及句子中的其他特殊字元。

句子成分的過濾有助於移除冗餘和不重要的資訊,這些資訊對文字意圖的表達或許沒有任何價值。

以下是文字處理結果:

第三步:分詞

切分各個句子,列出句子中的所有單詞。

以下是單詞列表:

['peter','elizabeth','took','taxi','attend','night','party','city','party','elizabeth','collapse','rush','hospital', 'diagnose','brain', 'injury', 'doctor','told','peter','stay','besides','get','well','peter', 'stayed','hospital','days','without','leaving']

第四步:評估單詞的加權出現頻率(occurrence frequency)

緊接著,我們計算所有單詞的加權出現頻率。為此,我們用每個單詞的出現頻率除以這段話中出現最多次的單詞的頻率,在這段話中出現最多的是 Peter,總共出現了三次。

下表給出了每個單詞的加權出現頻率。

文章太長不想看?ML 文字自動摘要了解一下

第五步:用相應的加權頻率替代原句中的各個單詞,然後計算總和。

我們在文字處理步驟中已經移除了停止詞和特殊字元等無關緊要的單詞,因而它們的加權頻率為零,也就沒有必要在計算時加上。

文章太長不想看?ML 文字自動摘要了解一下

根據所有單詞的加權頻率總和,我們可以推匯出:第一個句子在整段話中的權重最大。所以,第一個句子能夠對這段話的意思作出最具代表性的總結。

此外,如果第一個句子與第三個句子(該句的權重在整段話中排第二)相結合,則可以作出更好的總結。

以上例子只是基本說明了如何在機器學習中執行抽取式文字摘要。現在,我們看看如何在建立實際摘要生成器中運用上述概念。

維基百科文章的文字摘要

讓我們動手建立一個可以簡化冗長 web 文章中資訊的文字摘要生成器。為簡單起見,除了 Python 的 NLTK toolkit,我們不使用任何其他機器學習庫(machine learning library)。

以下是摘要生成器的程式碼 blueprint:

# Creating a dictionary for the word frequency table
frequency_table = _create_dictionary_table(article)

# Tokenizing the sentences
sentences = sent_tokenize(article)

# Algorithm for scoring a sentence by its words
sentence_scores = _calculate_sentence_scores(sentences, frequency_table)

# Getting the threshold
threshold = _calculate_average_score(sentence_scores)

# Producing the summary
article_summary = _get_article_summary(sentences, sentence_scores, 1.5 * threshold)

print(article_summary)

依照下列步驟使用 Python 語言建立一個簡單的文字摘要生成器。

第一步:準備資料

在這個例子中,我們想總結一下這篇 Wikipedia 文章的資訊,這篇文章只是對 20 世紀發生的主要事件進行概述。

為了獲取這篇文章的文字,我們將使用 Beautiful Soup 庫。

以下是抓取文章內容的程式碼:

import bs4 as BeautifulSoup
import urllib.request  

# Fetching the content from the URL
fetched_data = urllib.request.urlopen('https://en.wikipedia.org/wiki/20th_century')

article_read = fetched_data.read()

# Parsing the URL content and storing in a variable
article_parsed = BeautifulSoup.BeautifulSoup(article_read,'html.parser')

# Returning <p> tags
paragraphs = article_parsed.find_all('p')

article_content = ''

# Looping through the paragraphs and adding them to the variable
for p in paragraphs:  
    article_content += p.text

在以上程式碼中,我們首先匯入抓取網頁資料所必需的庫。BeautifulSoup 庫用於解析網頁內容,而 urllib library 用於連線網頁和檢索 HTML。

BeautifulSoup 將輸入文字轉化為 Unicode 字元,將輸出文字轉化為 UTF-8 字元,省去了從 web 上抓取文字時處理不同字符集編碼的麻煩。

我們使用 urllib.request 程式中的 urlopen 函式開啟網頁。之後,使用 read 函式讀取所抓取的資料物件。為了解析資料,我們呼叫 BeautifulSoup 物件,並向它傳遞兩個引數,即 article_read 和 html.parser。

find_all 函式用於傳回 HTML 中出現的所有<p>元素。此外,.text 使我們只能選擇<p>元素中的文字。

第二步:處理資料

為儘可能確保廢棄的文字資料無噪聲,我們將執行一些基本的文字清理(text cleaning)。為協助完成這一處理過程,我們將從 NLTK 庫中匯入一個停止詞列表。

我們還將引入 PorterStemmer,這是一種將單詞還原成詞根形式的演算法。例如,單詞 cleaning、cleaned 和 cleaner 都可以還原成詞根 clean。

此外,我們還將建立一個包含文字中每一單詞出現頻率的字典表。我們將依次讀取文字及相應單詞,以消除所有停止詞。

之後,我們將檢查單詞是否出現在 frequency_table 中。如果一個單詞之前就在字典中,則其值更新 1。否則,如果一個單詞首次被識別到,則其值設定為 1。

例如,頻率表應如下所示:

文章太長不想看?ML 文字自動摘要了解一下

程式碼如下:

from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
def _create_dictionary_table(text_string) -> dict:

    # Removing stop words
    stop_words = set(stopwords.words("english"))

    words = word_tokenize(text_string)

    # Reducing words to their root form
    stem = PorterStemmer()

    # Creating dictionary for the word frequency table
    frequency_table = dict()
    for wd in words:
        wd = stem.stem(wd)
        if wd in stop_words:
            continue
        if wd in frequency_table:
            frequency_table[wd] += 1
        else:
            frequency_table[wd] = 1

    return frequency_table

第三步:將文章分割成句子

為了將 article_content 分割成一個句子集,我們將使用 NLTK 庫中的內建方法。

from nltk.tokenize import word_tokenize, sent_tokenize

sentences = sent_tokenize(article)

第四步:確定句子的加權頻率

為了評估文字中每個句子的分數,我們將分析每個單詞的出現頻率。在這種情況下,我們將根據句子中的單詞對該句進行評分,也就是加上句子中每個重要單詞的出現頻率。

請看以下程式碼:

def _calculate_sentence_scores(sentences, frequency_table) -> dict:   

    # Algorithm for scoring a sentence by its words
    sentence_weight = dict()

    for sentence in sentences:
        sentence_wordcount = (len(word_tokenize(sentence)))
        sentence_wordcount_without_stop_words = 0
        for word_weight in frequency_table:
            if word_weight in sentence.lower():
                sentence_wordcount_without_stop_words += 1
                if sentence[:7] in sentence_weight:
                    sentence_weight[sentence[:7]] += frequency_table[word_weight]
                else:
                    sentence_weight[sentence[:7]] = frequency_table[word_weight]

        sentence_weight[sentence[:7]] = sentence_weight[sentence[:7]] /        sentence_wordcount_without_stop_words

    return sentence_weight

重要的是,為了避免長句的分數必然高於短句,我們用每個句子的分數除以該句中的單詞數。

另外,為了最佳化字典記憶體,我們任意新增 sentence[:7],這指的是每個句子的前七個字元。但在較長的文件中,你很可能遇到具有相同首個 n_chars 的句子,這時最好使用雜湊函式(hash function)或 index 函式(index function)來處理此類極端情況(edge-cases),避免衝突。

第五步:計算句子閾值

為了進一步調整適合摘要的句子型別,我們將建立句子的平均分。藉助於這個閾值,我們可以避免選擇分數低於平均分的句子。

程式碼如下:

def _calculate_average_score(sentence_weight) -> int:

    # Calculating the average score for the sentences
    sum_values = 0
    for entry in sentence_weight:
        sum_values += sentence_weight[entry]

    # Getting sentence average value from source text
    average_score = (sum_values / len(sentence_weight))

    return average_score

第六步:生成摘要

最後,我們擁有了所有必需的引數,因而現在可以生成文章摘要了。

程式碼如下:

def _get_article_summary(sentences, sentence_weight, threshold):
    sentence_counter = 0
    article_summary = ''

    for sentence in sentences:
        if sentence[:7] in sentence_weight and sentence_weight[sentence[:7]] >= (threshold):
            article_summary += " " + sentence
            sentence_counter += 1

    return article_summary

總結

下圖展示了建立文字摘要演算法的工作流程。

文章太長不想看?ML 文字自動摘要了解一下

以下是機器學習中簡單抽取式文字摘要生成器的完整程式碼:

#importing libraries
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize, sent_tokenize
import bs4 as BeautifulSoup
import urllib.request  

#fetching the content from the URL
fetched_data = urllib.request.urlopen('https://en.wikipedia.org/wiki/20th_century')

article_read = fetched_data.read()

#parsing the URL content and storing in a variable
article_parsed = BeautifulSoup.BeautifulSoup(article_read,'html.parser')

#returning <p> tags
paragraphs = article_parsed.find_all('p')

article_content = ''

#looping through the paragraphs and adding them to the variable
for p in paragraphs:  
    article_content += p.text


def _create_dictionary_table(text_string) -> dict:

    #removing stop words
    stop_words = set(stopwords.words("english"))

    words = word_tokenize(text_string)

    #reducing words to their root form
    stem = PorterStemmer()

    #creating dictionary for the word frequency table
    frequency_table = dict()
    for wd in words:
        wd = stem.stem(wd)
        if wd in stop_words:
            continue
        if wd in frequency_table:
            frequency_table[wd] += 1
        else:
            frequency_table[wd] = 1

    return frequency_table


def _calculate_sentence_scores(sentences, frequency_table) -> dict:   

    #algorithm for scoring a sentence by its words
    sentence_weight = dict()

    for sentence in sentences:
        sentence_wordcount = (len(word_tokenize(sentence)))
        sentence_wordcount_without_stop_words = 0
        for word_weight in frequency_table:
            if word_weight in sentence.lower():
                sentence_wordcount_without_stop_words += 1
                if sentence[:7] in sentence_weight:
                    sentence_weight[sentence[:7]] += frequency_table[word_weight]
                else:
                    sentence_weight[sentence[:7]] = frequency_table[word_weight]

        sentence_weight[sentence[:7]] = sentence_weight[sentence[:7]] / sentence_wordcount_without_stop_words



    return sentence_weight

def _calculate_average_score(sentence_weight) -> int:

    #calculating the average score for the sentences
    sum_values = 0
    for entry in sentence_weight:
        sum_values += sentence_weight[entry]

    #getting sentence average value from source text
    average_score = (sum_values / len(sentence_weight))

    return average_score

def _get_article_summary(sentences, sentence_weight, threshold):
    sentence_counter = 0
    article_summary = ''

    for sentence in sentences:
        if sentence[:7] in sentence_weight and sentence_weight[sentence[:7]] >= (threshold):
            article_summary += " " + sentence
            sentence_counter += 1

    return article_summary

def _run_article_summary(article):

    #creating a dictionary for the word frequency table
    frequency_table = _create_dictionary_table(article)

    #tokenizing the sentences
    sentences = sent_tokenize(article)

    #algorithm for scoring a sentence by its words
    sentence_scores = _calculate_sentence_scores(sentences, frequency_table)

    #getting the threshold
    threshold = _calculate_average_score(sentence_scores)

    #producing the summary
    article_summary = _get_article_summary(sentences, sentence_scores, 1.5 * threshold)

    return article_summary

if __name__ == '__main__':
    summary_results = _run_article_summary(article_content)
    print(summary_results)

點選原文中的以下按鈕在 FloydHub Notebook 上執行程式碼:

文章太長不想看?ML 文字自動摘要了解一下

在這個例子中,我們所採用的閾值是平均分的 1.5 倍。這個超引數值(hyperparameter value)在幾次試驗後為我們生成了良好的結果。當然,你可以根據自身的偏好對數值進行微調,並改進摘要生成效果。

下圖是 Wikipedia 文章的生成摘要。

文章太長不想看?ML 文字自動摘要了解一下

使用文字摘要演算法生成的 Wikipedia 文章摘要。

如你所見,執行程式碼可以對冗長的 Wikipedia 文章進行總結,並簡要概述 20 世紀發生的主要事件。

儘管如此,我們還可以改進摘要生成器,使之更好地生成長篇幅文字的簡潔、精確摘要。

更多內容

當然,本文只是簡要介紹了機器學習中使用文字摘要演算法所能實現的功能。

若想了解更多有關該主題,特別是抽象式文字摘要的知識,下面一些有用的資源可以為你提供幫助:

有沒有可能將兩種方法(抽象式和抽取式文字自動摘要)相結合?這是指標生成網路(pointer generator network)的主要原理,該網路透過結合抽取(指向)和抽象(生成)取得了最佳效果。

文章太長不想看?ML 文字自動摘要了解一下

圖源:"Taming Recurrent Neural Networks for Better Summarization"

《WikiHow: A Large Scale Text Summarization Dataset》一文提出了一個新的大規模文字自動摘要資料集 WikiHow,該資料集包含提取自 WikiHow 線上知識庫的 230000 多篇文章。目前可用的大多數資料集的規模不足以訓練序列到序列模型,它們也許只能提供有限的摘要,並且更適合執行抽取式摘要。但是,WikiHow 資料集規模大,質量高,能夠在抽象式文字摘要中獲得最優結果。

《Pretraining-Based Natural Language Generation for Text Summarization》一文提出了一個基於序列到序列正規化的獨特二階段模型。該模型同時在編碼器和解碼器側利用 BERT,並在學習過程中注重強化目標。當該模型在一些基準資料集上進行評估時,結果顯示,該方法在文字自動摘要中表現更好,尤其相較於其他傳統系統而言。

文章太長不想看?ML 文字自動摘要了解一下

原文連結:https://blog.floydhub.com/gentle-introduction-to-text-summarization-in-machine-learning/

相關文章