使用Python實現一個文本自動摘要工具
完整代碼見我的
GitHub互聯網時代信息爆炸式增長,人們面對越來越多的信息無法一一閱讀,而文本自動摘要技術可以一定程度上緩解這個問題。摘要就是一篇文章的核心部分信息,文本自動摘要技術分抽取式摘要和生成式摘要,前者是在原文中挑選一定比例的句子拼湊成一個摘要,後者更接近人為的總結式簡寫一篇文章。目前越來越多的研究者使用深度神經網路來研究生成式摘要技術,但是難度也挺大,效果有限。
本文的方法是使用基於啟發式規則的演算法實現了一個抽取式摘要演算法,大體思路參考了這篇論文,但是也做了一些改進和修改。
我們知道一篇文章如果要用裡面幾個句子來代表,那麼肯定選擇那些擁有更多個與文章信息相關的關鍵詞的那些句子;另外根據從小語文課上講的中心句概念,文章首位和每個段落首位的句子基本也是中心句;更進一步,我們通過分析,如果文章中某個句子和文章中大部分句子表達的意思都相近,那麼這個句子也能很好的作為摘要句子。因此本文使用關鍵詞信息量、句子位置、句子相似度三個參數來構建一個句子權重的函數,計算所有句子的權重之後按照降序排序,去前面固定比例的句子,然後依據它們在原文中的先後順序再次進行排序輸出,這樣就得到我們要的摘要了。
1、文本切分和文本表示
為了抽取一些重要句子,首先我們需要將所有句子劃分開來;另外使用計算機來做計算就得把句子用向量表示出來,這裡選用tfidf權重矩陣來表示一篇文章,tfidf矩陣每一行就是文章中的一個句子,tfidf的概念和實現參考我之前的一篇文章。本文使用機器學習庫sklearn里的函數來實現tfidf矩陣。
切分句子函數:
def split_sentence(text, punctuation_list=!?。!?):n """n 將文本段安裝標點符號列表裡的符號切分成句子,將所有句子保存在列表裡。n """n sentence_set = []n inx_position = 0 #索引標點符號的位置n char_position = 0 #移動字元指針位置n for char in text:n char_position += 1n if char in punctuation_list:n next_char = list(text[inx_position:char_position+1]).pop()n if next_char not in punctuation_list:n sentence_set.append(text[inx_position:char_position])n inx_position = char_positionn if inx_position < len(text):n sentence_set.append(text[inx_position:])nn sentence_with_index = {i:sent for i,sent in enumerate(sentence_set)} n return sentence_set,sentence_with_indexn
使用「。」,「?」 和「!」來做切分句子的標點符號。
構建TFIDF矩陣函數:
def get_tfidf_matrix(sentence_set,stop_word):n corpus = []n for sent in sentence_set:n sent_cut = jieba.cut(sent)n sent_list = [word for word in sent_cut if word not in stop_word]n sent_str = .join(sent_list)n corpus.append(sent_str)nn vectorizer=CountVectorizer()n transformer=TfidfTransformer()n tfidf=transformer.fit_transform(vectorizer.fit_transform(corpus))n # word=vectorizer.get_feature_names()n tfidf_matrix=tfidf.toarray()n return tfidf_matrixn
2、計算句子權重
有三個權重指數需要計算,分別是關鍵詞信息量、句子位置和句子相似度信息量;下面分別使用三個函數實現
def get_sentence_with_words_weight(tfidf_matrix):n sentence_with_words_weight = {}n for i in range(len(tfidf_matrix)):n sentence_with_words_weight[i] = np.sum(tfidf_matrix[i])nn max_weight = max(sentence_with_words_weight.values()) #歸一化n min_weight = min(sentence_with_words_weight.values())n for key in sentence_with_words_weight.keys():n x = sentence_with_words_weight[key]n sentence_with_words_weight[key] = (x-min_weight)/(max_weight-min_weight)nn return sentence_with_words_weightn
計算位置權重:
def get_sentence_with_position_weight(sentence_set):n sentence_with_position_weight = {}n total_sent = len(sentence_set)n for i in range(total_sent):n sentence_with_position_weight[i] = (total_sent - i) / total_sentn return sentence_with_position_weightn
計算相似度權重:
def similarity(sent1,sent2):n """n 計算餘弦相似度n """n return np.sum(sent1 * sent2) / (1e-6+(np.sqrt(np.sum(sent1 * sent1)) *n np.sqrt(np.sum(sent2 * sent2))))nndef get_similarity_weight(tfidf_matrix):n sentence_score = collections.defaultdict(lambda :0.)n for i in range(len(tfidf_matrix)):n score_i = 0.n for j in range(len(tfidf_matrix)):n score_i += similarity(tfidf_matrix[i],tfidf_matrix[j])n sentence_score[i] = score_inn max_score = max(sentence_score.values()) #歸一化n min_score = min(sentence_score.values())n for key in sentence_score.keys():n x = sentence_score[key]n sentence_score[key] = (x-min_score)/(max_score-min_score)nn return sentence_scoren
句子之間的相似度使用餘弦相似度計算,這裡的句子相似度得分sentence_score以該句子和所有句子的相似度的累加和作為權重,最後將所有權重歸一化。
3、抽取句子權重最高的句子作為摘要
首先將三個權重指數按照一定的係數相加,對所有句子按照權重值進行降序排序:
def ranking_base_on_weigth(sentence_with_words_weight,n sentence_with_position_weight,n sentence_score, feature_weight = [1,1,1]):n sentence_weight = collections.defaultdict(lambda :0.)n for sent in sentence_score.keys():n sentence_weight[sent] = feature_weight[0]*sentence_with_words_weight[sent] +n feature_weight[1]*sentence_with_position_weight[sent] +n feature_weight[2]*sentence_score[sent]nn sort_sent_weight = sorted(sentence_weight.items(),key=lambda d: d[1], reverse=True)n return sort_sent_weightn
feature_weight是可調整的權重指數參數,我這裡默認都是1,也就是將上述三個指標同等對待。如果有評測數據集,我們可以根據ROUGE得分的高低來調整這個feature_weight的取值。
最後將抽取的句子重新排列作為摘要輸出:
def get_summarization(sentence_with_index,sort_sent_weight,topK_ratio =0.3):n topK = int(len(sort_sent_weight)*topK_ratio)n summarization_sent = sorted([sent[0] for sent in sort_sent_weight[:topK]])n n summarization = []n for i in summarization_sent:n summarization.append(sentence_with_index[i])nn summary = .join(summarization)n return summaryn
我們試一下將下面這篇新聞做一個自動摘要:
男子母親和妻子同時墜河 先救妻子再救母親
肥東小伙先救了老婆後救媽,「當時沒想那麼多」;媽媽不怪兒子反而心疼他 媽媽和老婆一起掉河裡,你先救誰?這是一個考驗男人的經典問題。雖然民間流傳著千奇百怪的答案,卻沒有人真正回答得了。但就在7月22日下午,肥東縣店埠河邊圩埂村28歲的小伙郭某,遇到了活生生的現實。他的母親和妻子一同落入深六米的水中,兩人隨時都可能喪命!先救誰?問題來了! 三口打魚一齊跌落河中 7月22日,大暑,天氣炎熱。 當天下午6時許,太陽已不那麼毒了,肥東縣撮鎮鎮店埠河邊圩埂村的郭某和妻子小青(小名),一起去村子邊的店埠河下網捕魚。「小青以前沒有打過魚,她想和我一起去看看,覺得新鮮。」郭某今年28歲,之前他一直和小青在浙江打工,郭某是水電工,小青是油漆工,兩人在打工過程中相識,2008年在肥東結婚。 郭某父母農閑時會到河中下網打魚,家中有兩條小漁船。郭某好些年沒打過魚了,父母在河裡下的網他不知道在哪,郭某的母親孫某就提出,陪小夫妻倆一起到河裡看看。 傍晚6時10分左右,三人來到河邊,坐上一條小漁船,郭某向河中划去。郭某的母親孫某在船的一角下網。「小青比較好奇,就走到船尾去看我媽下網,結果船就發生傾斜了。」危險就在此時發生,郭某回憶,小青在船尾站著不穩,驚嚇地大喊起來,身體向河一側倒去,而郭某的母親孫某準備上前抱住小青,一旁撐竿的郭某也急了,想過來拉兩人。小船頃刻失衡,三人同時落水。 兒子先救老婆再救媽媽「店埠河中間水深六米,平時許多輪船都是從撮鎮碼頭開往巢湖的。」圩埂村的村民告訴記者,他們平時都不給小孩子到店埠河游泳,「水深,河裡以前就淹死過人。」
而落水的三人中,只有郭某會游泳,其他兩人都不識水性。「當時也沒有多想,只覺得小青離我近一點,所以我就向她游過去,她一把抱住我的腿,我拉住她的頭髮,掰開她的手,幸虧船就在旁邊,我當時就拖著她游到了船邊,然後讓她拉住船邊的環子。她還是害怕得大喊大叫,我就大聲對她說,你要鎮定,否則我們大家都會死!她就不再喊叫了。」 隨後郭某轉身去救媽媽。「她在水裡嗆著了,一會兒浮上來一會兒沉下去,我就扎個猛子過去了,當時媽媽已經開始下沉了,我從水裡托起了她。」郭某說,當他游到媽媽跟前時,媽媽幾乎已經沒有了氣力,身體也沒有反應了。「當時嚇死了,以為媽媽出事了,我一邊拉著船,一邊拖著媽媽,大概遊了五六分鐘,然後到了岸邊。」 此時,郭某的妻子小青從水裡爬上了岸,而郭某則在一旁拍著媽媽的背,希望將水控出來。「拍了幾下後,媽媽咳嗽了一聲,吐了許多水出來。不過一會媽媽又暈過去了。」 醫生:再遲兩分鐘就危險了 看到媽媽暈厥後,郭某急忙背著媽媽回到家中,也沒來得及換衣服,坐著鄰居的車就將媽媽送到了肥東縣人民醫院。郭某的妻子小青在家換了衣服後,當天晚上也趕到了醫院照顧婆婆。 昨天,記者在肥東縣人民醫院見到了郭某一家人,媳婦和兒子倆正坐在病床前。據介紹,孫某今年52歲,「她的血壓一直比較高,而且之前做過膽囊切除手術,身體一直不大好,這樣掉水裡受到驚嚇,當然就生病了。」郭某的父親說。老郭告訴記者,平時孩子不在家時都是他打魚,有時候孩子母親會陪著他一起下網,「船隻要坐穩了,哪會掉到河裡。年輕人沒有坐船經驗,結果事情就發生了。」 醫生告訴記者,由於病人有高血壓,落水後又受到驚嚇,剛開始有點昏迷,到了醫院後嘔吐、頭昏,「不過掛了水後,目前並沒有大礙,也可以回家休養了。」 不過,醫生也告訴記者,溺水者大概只有不到十分鐘的安全救援時間,若是過了這個寶貴的時間段,生命就非常危險了。「從病人的病情來看,應該是嗆水過多受驚嚇。若是再遲一兩分鐘,就非常危險了。」 媽媽不生氣,爸爸挺不滿在醫院裡,老郭有些責備孩子:「打魚有什麼好看的,看熱鬧好險把命都搭上了!年輕人就圖個新鮮,也不顧你媽媽安全。」
在老郭問明了事情前後,對兒子先救媳婦的事也有些不滿,「他媽都要沒氣了,他還拖著船,就怕媳婦危險,媳婦都拉住船的鐵環了,還有什麼危險?!這時候當然應該先救他媽,等他媽安全了再去將船拉上來。」不過,這話老郭並沒有當著兒子兒媳的面說。在病房裡,老郭守在老婆身邊,始終板著臉,兒子、兒媳也不敢和他多說話。 郭某的母親孫某醒來後,卻沒有責怪兒子:「要不是兒子,我這老命就丟在河裡了,現在掛了水,身體舒服多了。」孫某告訴記者,她的記憶只停留在兒子游過來救她的時候,「後來怎麼上了岸,怎麼到了醫院,都不太記得了!」 畢竟她們都在我身邊 昨天,在醫院裡,郭某忙前忙後,一會兒拿葯,一會兒照顧母親。一旁的小青對郭某說道:「要不你先回家換洗下,媽媽這邊我來照顧。」在走廊里,記者和小郭對上了話。 記者:為什麼先救老婆,後救媽媽? 郭某:當時沒想那麼多,只是看到老婆離我稍微近一點,我就游過去先救她了,當安頓好老婆後,發現媽媽在水中非常危險,又去救媽媽。 記者:知道那個讓男人頭疼的題目嗎,「媽媽和老婆一起掉河裡,你先救誰?」你老婆可問過你? 郭某:我老婆沒有問過我這個問題。 但我聽過這個題目,以前沒有談戀愛的時候也曾想過,覺得肯定是先救媽媽。記者:為什麼媽媽都昏迷了,你還拖著船,是否心裡仍惦記著老婆的安全?
郭某:拉著船也是因為我沒有力氣游上岸了,所以是借著船一起游上了岸,也可以救老婆。 記者:你覺得你先救老婆,會不會受到父母的責怪,會不會讓親友覺得你不孝順? 郭某:救人的時候沒想過,(沉默一會兒)現在……(沉默)覺得可能會是個問題。 記者:請不要見怪,問一個讓你更不舒服的問題,如果這事情再發生一次,你還會不會那樣救援? 郭某:其實昨晚我想了一下,後來覺得,無論如何,這樣的救人安排都是最合理的。所以我還是會這樣選擇,畢竟她們現在都還在我身邊。 他是我兒子,我不怪他 昨天,在病床前,記者也和郭某的母親孫某聊了起來。 記者:你知道你兒子先救媳婦、後救你的過程了吧? 孫某:知道呀。記者:你會不會在心裡怪兒子,沒有先救你?
孫某:不怪,他是我兒子呀,他到現在忙得都沒有回家換衣服,濕透的衣服就這樣焐幹了,我挺心疼他的。你們也不要再追問他了,這樣讓他感到難過。 記者:好像你丈夫有點不高興,覺得兒子做得不對。 孫某:他就那脾氣,回頭我來勸勸他。
輸出結果如下,我只抽取了其中30%的句子作為摘要,這個比例可以根據具體情況做調整:
男子母親和妻子同時墜河 先救妻子再救母親
肥東小伙先救了老婆後救媽,「當時沒想那麼多」;媽媽不怪兒子反而心疼他 媽媽和老婆一起掉河裡,你先救誰?但就在7月22日下午,肥東縣店埠河邊圩埂村28歲的小伙郭某,遇到了活生生的現實。他的母親和妻子一同落入深六米的水中,兩人隨時都可能喪命!先救誰? 當天下午6時許,太陽已不那麼毒了,肥東縣撮鎮鎮店埠河邊圩埂村的郭某和妻子小青(小名),一起去村子邊的店埠河下網捕魚。」郭某今年28歲,之前他一直和小青在浙江打工,郭某是水電工,小青是油漆工,兩人在打工過程中相識,2008年在肥東結婚。 郭某父母農閑時會到河中下網打魚,家中有兩條小漁船。郭某好些年沒打過魚了,父母在河裡下的網他不知道在哪,郭某的母親孫某就提出,陪小夫妻倆一起到河裡看看。 傍晚6時10分左右,三人來到河邊,坐上一條小漁船,郭某向河中划去。郭某的母親孫某在船的一角下網。」危險就在此時發生,郭某回憶,小青在船尾站著不穩,驚嚇地大喊起來,身體向河一側倒去,而郭某的母親孫某準備上前抱住小青,一旁撐竿的郭某也急了,想過來拉兩人。兒子先救老婆再救媽媽
「店埠河中間水深六米,平時許多輪船都是從撮鎮碼頭開往巢湖的。」圩埂村的村民告訴記者,他們平時都不給小孩子到店埠河游泳,「水深,河裡以前就淹死過人。」郭某說,當他游到媽媽跟前時,媽媽幾乎已經沒有了氣力,身體也沒有反應了。」 此時,郭某的妻子小青從水裡爬上了岸,而郭某則在一旁拍著媽媽的背,希望將水控出來。」 醫生:再遲兩分鐘就危險了 看到媽媽暈厥後,郭某急忙背著媽媽回到家中,也沒來得及換衣服,坐著鄰居的車就將媽媽送到了肥東縣人民醫院。郭某的妻子小青在家換了衣服後,當天晚上也趕到了醫院照顧婆婆。 昨天,記者在肥東縣人民醫院見到了郭某一家人,媳婦和兒子倆正坐在病床前。」 醫生告訴記者,由於病人有高血壓,落水後又受到驚嚇,剛開始有點昏迷,到了醫院後嘔吐、頭昏,「不過掛了水後,目前並沒有大礙,也可以回家休養了。 郭某的母親孫某醒來後,卻沒有責怪兒子:「要不是兒子,我這老命就丟在河裡了,現在掛了水,身體舒服多了。 記者:為什麼先救老婆,後救媽媽? 郭某:當時沒想那麼多,只是看到老婆離我稍微近一點,我就游過去先救她了,當安頓好老婆後,發現媽媽在水中非常危險,又去救媽媽。
評論區歡迎任何交流。
推薦閱讀:
※練習-word2vec
※Hierarchical Attention Networks for Document Classification 閱讀筆記
※機器學習、深度學習與自然語言處理領域推薦的書籍列表
※PaperWeekly第四期------基於強化學習的文本生成技術
※如何處理不均衡數據?