1. 程式人生 > 其它 >R語言自然語言處理:關鍵詞提取與文字摘要(TextRank)

R語言自然語言處理:關鍵詞提取與文字摘要(TextRank)

作者:黃天元,復旦大學博士在讀,目前研究涉及文字挖掘、社交網路分析和機器學習等。希望與大家分享學習經驗,推廣並加深R語言在業界的應用。

郵箱:[email protected]

關於提取關鍵詞的方法,除了TF-IDF演算法,比較有名的還有TextRank演算法。它是基於PageRank衍生出來的自然語言處理演算法,是一種基於圖論的排序演算法,以文字的相似度作為邊的權重,迭代計算每個文字的TextRank值,最後把排名高的文字抽取出來,作為這段文字的關鍵詞或者文字摘要。之所以提到關鍵詞和文字摘要,兩者其實宗旨是一樣的,就是自動化提取文字的重要表徵文字。

如果分詞是以片語作為切分,那麼得到的是關鍵詞。以詞作為切分的時候,構成詞與詞之間是否連線的,是詞之間是否相鄰。相鄰關係可以分為n元,不過在中文中,我認為2元關係已經非常足夠了(比如一句話是:“我/有/一隻/小/毛驢/我/從來/也/不/騎”,那麼設定二元會讓“一隻”和“毛驢”發生關聯,這就足夠了)。如果是以句子切分的,那麼得到的稱之為文字摘要(其實就是關鍵的句子,俗稱關鍵句)。如果要得到文字的關鍵句子,還是要對每句話進行分詞,得到每句話的基本詞要素。根據句子之間是否包含相同的詞語,我們可以得到句子的相似度矩陣,然後再根據相似度矩陣來得到最關鍵的句子(也就是與其他句子關聯性最強的那個句子)。當句子比較多的時候,這個計算量是非常大的。 下面,我要用R語言的textrank包來實現關鍵詞的提取和文字摘要。

準備工作

安裝必備的包。

1library(pacman)
2p_load(tidyverse,tidytext,textrank,rio,jiebaR)

然後,匯入資料。資料可以在我的github中獲得(github.com/hope-data-sc)。檔名稱為hire_text.rda。

1import("./hire_text.rda")->hire_text
2hire_text

這裡麵包含了網際網路公司的一些招聘資訊,一共有4102條記錄,只有一列,列名稱為hire_text,包含了企業對崗位要求的描述。

關鍵詞提取

因為要做關鍵詞和關鍵句的提取,因此我們要進行分詞和分句。分詞還是利用jiebaR,老套路。如果沒有了解的話,請看專欄之前的文章(R語言自然語言處理系列)。不過這次,我們希望能夠在得到詞頻的同時,得到每個詞的詞性,然後只把名詞提取出來。 分詞程式碼如下:

1hire_text%>%
2mutate(id=1:n())->hire_txt#給文件編號
3
4worker(type="tag")->wk#構造一個分詞器,需要得到詞性
5
6hire_txt%>%
7mutate(words=map(hire_text,tagging,jieba=wk))%>%#給文件進行逐個分詞
8mutate(word_tag=map(words,enframe,name="tag",value="word"))%>%
9select(id,word_tag)->hire_words

然後,我們分組進行關鍵詞提取。

 1#構造提取關鍵詞的函式
2
3extract_keywords=function(dt){
4textrank_keywords(dt$word,relevant=str_detect(dt$tag,"^n"),ngram_max=2)%>%
5.$keywords
6}
7
8hire_words%>%
9mutate(textrank.key=map(word_tag,extract_keywords))%>%
10select(-word_tag)->tr_keyword

現在我們的資料框中,包含了每個文件的關鍵詞。每個關鍵詞列表中,包含freq和ngram兩列,freq代表詞頻,ngram代表多少個n元,2元就是“上海市-閔行區”這種形式,1元就是“上海市”、“閔行區”這種形式。 現在,我要從中挑選每篇文章最重要的3個關鍵詞。挑選規則是:詞頻必須大於1,在此基礎上,n元越高越好。

 1tr_keyword%>%
2unnest()%>%
3group_by(id)%>%
4filter(freq>1)%>%
5top_n(3,ngram)%>%
6ungroup()->top3_keywords
7
8top3_keywords
9###Atibble:3,496x4
10##idkeywordngramfreq
11##<int><chr><int><int>
12##11上海市-長寧區22
13##21長寧區12
14##31上海市-靜安區22
15##44客戶14
16##55招商銀行12
17##66事業部13
18##77房地產12
19##89技術13
20##910電商12
21##1010協調12
22###...with3,486morerows

仔細觀察發現,有的文件就沒有出現過,因為他們分詞之後,每個詞的詞頻都是1。現在讓我們統計一下最火的十大高頻詞。

 1top3_keywords%>%
2count(keyword)%>%
3arrange(desc(n))%>%
4slice(1:10)
5###Atibble:10x2
6##keywordn
7##<chr><int>
8##1客戶298
9##2公司173
10##3產品110
11##4能力97
12##5專案89
13##6技術51
14##7市場48
15##8系統48
16##9廣告41
17##10企業41

這些詞分別是:客戶、公司、產品、能力、專案、技術、市場、系統、廣告、企業。

文字摘要

文字摘要其實就是從文件中提出我們認為最關鍵的句子。我們會用textrank包的textrank_sentences函式,這要求我們有一個分句的資料框,還有一個分詞的資料框(不過這次需要去重複,也就是說分詞表中每個文件不能有重複的詞)。非常重要的一點是,這次分詞必須以句子為單位進行劃分。 我們明確一下任務:對每一個招聘文件,我們要挑選出這個文件中最關鍵的一句話。要解決這個大問題,需要先解決一個小問題。就是對任意的一個長字串,我們要能夠切分成多個句子,然後按照句子分組,對其進行分詞。然後我們會得到一個句子表格和單詞表格。 其中,我們切分句子的標準是,切開任意長度的空格,這在正則表示式中表示為“[:space:]+”。

1get_sentence_table=function(string){
2string%>%
3str_split(pattern="[:space:]+")%>%
4unlist%>%
5as_tibble()%>%
6transmute(sentence_id=1:n(),sentence=value)
7}

上面這個函式,對任意的一個字串,能夠返回一個含有兩列的資料框,第一列是句子的編號sentence_id,另一列是句子內容sentence。我們姑且把這個資料框稱之為sentence_table。 下面我們要構造另一個函式,對於任意的sentence_table,我們需要返回一個分詞表格,包含兩列,第一列是所屬句子的編號,第二列是分詞的單詞內容。

 1wk=worker()#在外部構造一個jieba分詞器
2
3get_word_table=function(string){
4string%>%
5str_split(pattern="[:space:]+")%>%
6unlist%>%
7as_tibble()%>%
8transmute(sentence_id=1:n(),sentence=value)%>%
9mutate(words=map(sentence,segment,jieba=wk))%>%
10select(-sentence)%>%
11unnest()
12}

如果分詞器要在內部構造,每次執行函式都要構造一次,會非常消耗時間。 目前,對於任意一個字串,我們有辦法得到它的關鍵句了。我們舉個例子:

1hire_text[[1]][1]->test_text
2test_text%>%get_sentence_table->st
3st%>%get_word_table->wt
4##Warninginstri_split_regex(string,pattern,n=n,simplify=simplify,:
5##argumentisnotanatomicvector;coercing

有了這st和wt這兩個表格,現在我們要愉快地提取關鍵句子。

1textrank_sentences(data=st,terminology=wt)%>%
2summary(n=1)#n代表要top多少的關鍵句子
3##[1]"1279弄6號國峰科技大廈"

我們給這個取最重要關鍵句子也編寫一個函式。

1get_textrank_sentence=function(st,wt){
2textrank_sentences(data=st,terminology=wt)%>%
3summary(n=1)
4}

因為資料量比較大,我們只求第10-20條記錄進行求解。不過,如果句子只有一句話,那麼是會報錯的。因此我們要首先去除一個句子的記錄。

 1hire_txt%>%
2slice(10:20)%>%
3mutate(st=map(hire_text,get_sentence_table))%>%
4mutate(wt=map(hire_text,get_word_table))%>%
5mutate(sentence.no=unlist(map(st,nrow)))%>%
6select(-hire_text)%>%
7filter(sentence.no!=1)%>%
8mutate(key_sentence=unlist(map2(st,wt,get_textrank_sentence)))%>%
9select(id,sentence.no,key_sentence)->hire_abstract
10
11hire_abstract
12###Atibble:10x3
13##idsentence.nokey_sentence
14##<int><int><chr>
15##1109開拓電商行業潛在客戶
16##2115EHS
17##3129負責招聘渠道的維護和更新;
18##4136榮獲中國房地產經紀百強企業排名前六強;
19##51472、邏輯思維、分析能力強,工作謹慎、認真,具有良好的書面及語言表達能力;~
20##61552、能獨立完成欄目包裝、影視片頭、廣告片、宣傳片的製作,包括創意圖設計、動畫製作、特效、剪輯合成等工作;~
21##71673、公司為員工提供帶薪上崗培訓和豐富的在職培訓,有廣闊的職業發展與晉升空間;~
22##8177您與該職位的匹配度?
23##91813接觸並建立與行業內重點企業的良好關係,及時瞭解需求狀態;~
24##10207具有財務、金融、稅務等領域專業知識;具有較強分析判斷和解決問題的能力;~

如果對所有記錄的摘要感興趣,去掉slice(10:20) %>%這一行即可。等待時間可能會較長。

總結

實踐證明,TextRank演算法是一個比較耗時的演算法,因為它依賴於圖計算,需要構成相似度矩陣。當資料量變大的時候,執行時間會呈“幾何級”增長。但是對於中小型的文字來說,這個方法還是非常不錯的。但是中小型的文字,還需要摘要麼?儘管如此,這還是一個非常直觀的演算法,如果TF-IDF在一些時候不好用的話,這是一個非常好的候補選項。

參考資料

textrank包基本教程

http://blog.itpub.net/31562039/viewspace-2286669/

手把手 | 基於TextRank演算法的文字摘要(附Python程式碼)

http://blog.itpub.net/31562039/viewspace-2286669/