三:關(guān)鍵詞提取的方法
01TF-IDF
TF-IDF在nlp領(lǐng)域無人不知無人不曉,思想簡單卻有效,榮獲nlp界的諾貝爾獎:奧卡姆剃刀獎。
TF(term ),即詞頻,用來衡量詞在一篇文檔中的重要性,詞頻越高,越重要。計(jì)算公式為:
某文檔中某詞出現(xiàn)的次數(shù)該文檔的總詞數(shù)
IDF((inverse ),叫做逆文檔頻率,衡量某個詞在所有文檔集合中的常見程度。
當(dāng)包含某個詞的文檔的數(shù)量越多時(shí),這個詞也就爛大街了,重要性越低。計(jì)算公式為:
全部文檔的數(shù)量包含某詞的文檔的數(shù)量
于是TF-IDF = TF * IDF, 它表明字詞的重要性與它在某篇文檔中出現(xiàn)的次數(shù)成正比,與它在所有文檔中出現(xiàn)的次數(shù)成反比。
使用TF-IDF的一個假設(shè)前提是:已經(jīng)去掉了停用詞。
TF-IDF的優(yōu)點(diǎn)是計(jì)算速度快,結(jié)果穩(wěn)健。
需要輸入多篇文檔,可以輸出每篇文檔的關(guān)鍵詞。
02.
基于圖計(jì)算來提取關(guān)鍵詞,需要進(jìn)行迭代,速度比TF-IDF慢,但不需要通過輸入多篇文檔來提取關(guān)鍵詞。
是把一篇文檔構(gòu)建成無向圖,圖中的節(jié)點(diǎn)就是詞語,圖上的邊就是共現(xiàn)詞之間的連接。
共現(xiàn)詞通過滑動窗口來確定,共現(xiàn)詞之間用邊相連,而邊上的權(quán)重可以使用共現(xiàn)詞的相似度。
jieba提供了用提取關(guān)鍵詞的函數(shù),但是邊的權(quán)重是詞共現(xiàn)的頻率。
這樣做其實(shí)比較粗糙,我們可以使用基于詞向量計(jì)算的相似度得分作為權(quán)重,來進(jìn)行迭代。
我這不知不覺又給自己安排了任務(wù),文章名字都想好了:
《提取關(guān)鍵詞:我也是改過jieba源碼的人!》
也需要先去掉停用詞。
03.文本聚類法
TF-IDF只從淺層的詞頻角度挖掘關(guān)鍵詞,而通過Kmeans或Topic Model,可以從深層的隱含語義角度來提取關(guān)鍵詞。
一種方法是通過Kmeans來提取,使用詞向量作為特征。
比如對于一篇文檔,我們要提取10個關(guān)鍵詞。
那么可以把文檔中的詞聚成5類,然后取每個類中,與類中心最近的2個點(diǎn),作為關(guān)鍵詞。
也可以直接聚成10類,取和類中心最近的詞。
另一種方法是通過Topic Model來提取,比如LSA和LDA,使用詞頻矩陣作為特征。
比如對于包含多篇文檔的單領(lǐng)域語料,我們要挖掘關(guān)鍵詞,整理詞庫。
那么可以用LDA進(jìn)行聚類,得到每個主題的單詞分布,再取出每個主題下排名靠前的topk個單詞,或者權(quán)重高于某個閾值的單詞,構(gòu)成關(guān)鍵詞庫。
主題的單詞分布為:
(0,?'0.025*"基金"?+?0.020*"分紅"?+?0.007*"中"?+?0.006*"考試"?+?0.006*"私募"?+?0.005*"公司"?+?0.004*"采用"?+?0.004*"市場"?+?0.004*"游戲"?+?0.004*"元"')
(1,?'0.007*"套裝"?+?0.007*"中"?+?0.006*"設(shè)計(jì)"?+?0.004*"元"?+?0.004*"拍攝"?+?0.003*"萬"?+?0.003*"市場"?+?0.003*"編輯"?+?0.003*"時(shí)尚"?+?0.003*"穿"')
(2,?'0.007*"英寸"?+?0.005*"中"?+?0.004*"中國"?+?0.003*"拍攝"?+?0.003*"比賽"?+?0.002*"高清"?+?0.002*"小巧"?+?0.002*"產(chǎn)品"?+?0.002*"億股"?+?0.002*"機(jī)型"')
(3,?'0.082*"基金"?+?0.015*"公司"?+?0.014*"市場"?+?0.014*"投資"?+?0.009*"股票"?+?0.007*"億元"?+?0.007*"中"?+?0.006*"收益"?+?0.006*"行業(yè)"?+?0.006*"一季度"')
...
04.有監(jiān)督的關(guān)鍵詞提取
有監(jiān)督的方法需要有標(biāo)注的數(shù)據(jù),我沒有嘗試過。
看一些文章說可以轉(zhuǎn)化為統(tǒng)計(jì)機(jī)器翻譯(SMT)的問題,轉(zhuǎn)化為序列標(biāo)注(NER)的問題,或者轉(zhuǎn)化為詞語排序(LTR)的問題。
我只理解了轉(zhuǎn)化為序列標(biāo)注問題的做法,這個和用深度學(xué)習(xí)做文本摘要類似。
文檔中的詞語,如果為關(guān)鍵詞,則標(biāo)注為1,否則標(biāo)注為0,也就是對一個詞進(jìn)行二分類。
據(jù)說效果比上述無監(jiān)督的方法好。
四:TF-IDF+n-gram提取關(guān)鍵詞
提取單詞作為關(guān)鍵詞,比較容易實(shí)現(xiàn),如果要提取短語呢?
我嘗試了開源工具RAKE,它是根據(jù)停用詞來劃分句子,再提取短語的。
使用之后,我發(fā)現(xiàn)RAKE存在兩個問題:
一是提取的短語有些長達(dá)4-5個單詞,這顯然不合適;
二是沒有根據(jù)詞性進(jìn)行過濾。
當(dāng)然,我們可以用RAKE得到一個粗糙的結(jié)果,然后再做細(xì)致的處理,比如根據(jù)包含單詞的數(shù)量、根據(jù)詞性模板等進(jìn)行過濾。
不過RAKE的代碼寫得實(shí)在太亂了,我沒有耐心看下去,對其原理也不太了解,也就沒加工再利用。
我給出的方案是TF-IDF結(jié)合n-gram來提取關(guān)鍵短語,并根據(jù)單詞長度、停用詞和詞性進(jìn)行過濾。
01.英文新聞測試語料
我去某網(wǎng)站下載了4篇英語新聞,對于其中的一篇,經(jīng)過增加一段和兩段、刪除一段和兩段的操作,得到4篇內(nèi)容高度重合的新聞,最終得到8篇英語新聞。
├──?english_new_1.txt
├──?english_new_2.txt
├──?english_new_3.txt
├──?english_new_add_1.txt???????#?增加一段
├──?english_new_add_2.txt???????#?增加兩段
├──?english_new_base.txt????????#?原始新聞
├──?english_new_remove_1.txt????#?刪除一段
└──?english_new_remove_2.txt????#?刪除兩段
首先用SimHash去重,保留4篇英語新聞(為了測試SimHash)。
02.需要的庫
TF-IDF的計(jì)算,用sklearn的包。
由于是英文文本的處理,所以需要用NLTK做英文單詞拆分、詞性標(biāo)注和詞形還原。
#coding:utf-8
import?os,re
from?sklearn.feature_extraction.text?import?TfidfVectorizer
import?numpy?as?np
from?itertools?import?chain
from?nltk?import?pos_tag,?word_tokenize,sent_tokenize
from?nltk.stem?import?WordNetLemmatizer
"""?一:初始化詞形還原的類?"""
lemmatizer?=?WordNetLemmatizer()
為什么需要做詞形還原呢?
詞形還原是指把英文的單詞,從復(fù)數(shù)形態(tài)、第三人稱形態(tài)等復(fù)雜形態(tài),轉(zhuǎn)換為最基礎(chǔ)的形態(tài),比如 還原為salary,makes還原為make。
如果我們提取的關(guān)鍵詞是單個詞,那么在使用TF-IDF進(jìn)行提取之前,先要對每個單詞做詞形還原,不然和salary會被認(rèn)為是不同的兩個單詞。
詞形還原是基于詞典的,準(zhǔn)確率比較高,所以使用NLTK做詞形還原時(shí),需要下載數(shù)據(jù) WordNet。
在公司很容易出現(xiàn)網(wǎng)絡(luò)不通的問題,反正我是下載不了。
nltk.download('wordnet')
[nltk_data]?Error?loading?wordnet:?
False
其實(shí)不止詞形還原需要下載數(shù)據(jù),做詞性標(biāo)注和實(shí)體識別,都需要下載,特別麻煩。
于是我干脆把NLTK的所有數(shù)據(jù)文件都下載了,文件大小為1.08G,解壓后放到相應(yīng)的路徑,就一勞永逸了。
下載鏈接:
提取碼:
07qb
下載好后解壓,在ubuntu環(huán)境下,給文件賦予相應(yīng)的可執(zhí)行權(quán)限(Goup和Others也需要可執(zhí)行權(quán)限),然后把數(shù)據(jù)文件復(fù)制到以下路徑:
??Searched?in:
??
????-?'/opt/anaconda3/nltk_data'
????-?'/opt/anaconda3/share/nltk_data'
????-?'/opt/anaconda3/lib/nltk_data'
????-?'/usr/share/nltk_data'
????-?'/usr/local/share/nltk_data'
????-?'/usr/lib/nltk_data'
????-?'/usr/local/lib/nltk_data'
03.對新聞做單詞拆分
最好不要對整篇文檔做 ,而是先 (劃分句子),再。
因?yàn)閷?shí)體識別、詞性標(biāo)注和句法分析,最好以句子為單位來做。
"""?一:對文檔進(jìn)行分詞/拆字?"""
def?tokenize_doc(docs):
????"""
????:params:?docs——多篇文檔
????"""
????docs_tokenized?=?[]
????for?doc?in?docs:
????????doc?=?re.sub('[\n\r\t]+','?',doc)
????????""" 1:分句?"""
????????sents?=?sent_tokenize(doc)
????????""" 2:單詞拆分?"""
????????sents_tokenized?=?[word_tokenize(sent.strip())?for?sent?in?sents]
????????docs_tokenized.append(sents_tokenized)
????return?docs_tokenized
04.n-gram的生成
sklearn的的類里邊,也提供了n-gram的功能,那為什么我還要自己生成呢?
原因是sklearn內(nèi)置的功能里,生成n-gram后,就直接計(jì)算TF-IDF了,沒根據(jù)停用詞和詞性,過濾n-gram。
而根據(jù)停用詞和詞性過濾n-gram,必須在計(jì)算TF-IDF之前。
那在的時(shí)候做過濾不就得了嗎?
不行!
過濾n-gram,是指如果n-gram中,有一個單詞是停用詞,或者有一個單詞的詞性是過濾的詞性,那么整個n-gram都需要去掉,而不能只去掉該單詞。
在時(shí)過濾單詞,那么生成的n-gram短語塊可能是錯誤的。
"""?二:產(chǎn)生n-gram,用于提取短語塊?"""
def?gene_ngram(sentence,n=3,m=2):
????"""
????----------
????sentence:?分詞后的句子
????n:?取3,則為3-gram
????m:?取1,則保留1-gram
????----------
????"""
????if?len(sentence)?
05.n-gram的過濾
生成的trigram如下:
[['What',?'are',?'the'],?['are',?'the',?'legal'],?['the',?'legal',?'implications'],?['legal',?'implications',?'of'],
我們要對n-gram進(jìn)行過濾,根據(jù)停用詞、詞性、單詞的長度以及是否包含數(shù)字,來過濾。
詞性可以去網(wǎng)上找英文詞性對照表。
如果n-gram中,包含長度為1的單詞,那么過濾掉。
"""?n-gram中是否有單詞長度為1?"""
def?clean_by_len(gram):
????for?word?in?gram:
????????if?len(word)?2:
????????????return?False
????return?True

"""?三:按停用詞表和詞性,過濾單詞?"""
def?clean_ngrams(ngrams):
????"""
????:params:?ngrams
????"""
????stopwords?=?open("./百度英文停用詞表.txt",encoding='utf-8').readlines()
????stopwords?=?[word.strip()?for?word?in?stopwords]
????pat?=?re.compile("[0-9]+")
????"""?如果n-gram中有停用詞,則去掉?"""
????ngrams?=?[gram?for?gram?in?ngrams?if?len(set(stopwords).intersection(set(gram)))==0]
????"""?如果n-gram中有數(shù)字,則去掉?"""
????ngrams?=?[gram?for?gram?in?ngrams?if?len(pat.findall(''.join(gram).strip()))==0]????
????"""?n-gram中有單詞長度為1,則去掉?"""
????ngrams?=?[gram?for?gram?in?ngrams?if?clean_by_len(gram)]
????"""?只保留名詞、動詞和形容詞?"""
????allow_pos_one?=?["NN","NNS","NNP","NNPS"]
????allow_pos_two?=?["NN","NNS","NNP","NNPS","JJ","JJR","JJS"]
????allow_pos_three?=?["NN","NNS","NNP","NNPS","VB","VBD","VBG","VBN","VBP","VBZ","JJ","JJR","JJS"]
????ngrams_filter?=?[]
????for?gram?in?ngrams:
????????words,pos?=?zip(*pos_tag(gram))
????????"""?如果提取單詞作為關(guān)鍵詞,則必須為名詞?"""
????????if?len(words)?==?1:
????????????if?not?pos[0]?in?allow_pos_one:
????????????????continue???
????????????ngrams_filter.append(gram)
????????else:
????????????"""?如果提取短語,那么開頭必須為名詞、動詞、形容詞,結(jié)尾為名詞?"""
????????????if?not?(pos[0]?in?allow_pos_three?and?pos[-1]?in?allow_pos_one):
????????????????continue??
????????????ngrams_filter.append(gram)
????return?ngrams_filter
06.計(jì)算TF-IDF
用上面的函數(shù),完成生成n-gram、過濾n-gram的步驟,然后把n-gram之間的單詞用下劃線連接:"_",n-gram之間用空格連接。
????""" 1:處理為n-gram,n_=2或3 """
????docs_ngrams?=?[gene_ngram(doc,n=n_,m=n_)?for?doc?in?docs_tokenized]
????"""?2:?按停用詞表和詞性,過濾?"""
????docs_ngrams?=?[clean_ngrams(doc)?for?doc?in?docs_ngrams]
????docs_?=?[]
????for?doc?in?docs_ngrams:
????????docs_.append('?'.join(['_'.join(ngram)?for?ngram?in?doc]))
得到的n-gram如下:
['face_tough_times?Hiring_activity_declines?reveals_Naukri_JobSpeak?health_system_preparedness?micro-delivery_startup_DailyNinja?...]
為什么n-gram之間的單詞用下劃線來連接呢?
我試了其他的符號:#、=,但是發(fā)現(xiàn)送入sklearn的包里計(jì)算時(shí),n-gram會被重新拆分為單詞,vocab不是n-gram短語,而是單詞。
我估計(jì)是因?yàn)閜ython中,下劃線比較特殊,如正則表達(dá)式 \w,表示字母、數(shù)字和下劃線,其他的符號則容易被視為文本噪音而去掉。
接著計(jì)算n-gram的TF-IDF,這里又有一個坑:如果不自己指定n-gram字典,那么sklearn自己構(gòu)建的字典中,可能會有單詞或單詞加下劃線這種奇怪的東西。
所以需要自己傳入vocab。
"""?四:獲取 tf-idf 特征?"""
def?calcu_tf_idf(documents):
????"""
????:param:?data為列表格式的文檔集合,?計(jì)算?tf_idf?特征
????"""
????"""?指定vocab,否則n-gram的計(jì)算會出錯?"""
????vocab?=?set(chain.from_iterable([doc.split()?for?doc?in?documents]))
????vec?=?TfidfVectorizer(vocabulary=vocab)
????D?=?vec.fit_transform(documents)
????voc?=?dict((i,?w)?for?w,?i?in?vec.vocabulary_.items())
????features?=?{}
????for?i?in?range(D.shape[0]):
????????Di?=?D.getrow(i)
????????features[i]?=?list(zip([voc[j]?for?j?in?Di.indices],?Di.data))
????return?features
07.完成關(guān)鍵詞提取
接著,就可以完成關(guān)鍵詞的提取了。
考慮到關(guān)鍵詞提取的全面性,分別提取unigram、bigram和trigram關(guān)鍵詞。
如果提取unigram關(guān)鍵詞,那么需要做詞形還原。
def?get_ngram_keywords(docs_tokenized,topk=5,n_=2):
????"""?1:處理為n-gram?"""
????docs_ngrams?=?[gene_ngram(doc,n=n_,m=n_)?for?doc?in?docs_tokenized]
????"""?2:?按停用詞表和詞性,過濾?"""
????docs_ngrams?=?[clean_ngrams(doc)?for?doc?in?docs_ngrams]
????docs_?=?[]
????for?doc?in?docs_ngrams:
????????docs_.append('?'.join(['_'.join(ngram)?for?ngram?in?doc]))
????"""?3:?計(jì)算tf-idf,提取關(guān)鍵詞?"""
????features?=?calcu_tf_idf(docs_)
????docs_keys?=?[]
????for?i,pair?in?features.items():
????????topk_idx?=?np.argsort([v?for?w,v?in?pair])[::-1][:topk]
????????docs_keys.append([pair[idx][0]?for?idx?in?topk_idx])
????return?[['?'.join(words.split('_'))?for?words?in?doc?]for?doc?in?docs_keys]?
"""?五:抽取n-gram關(guān)鍵詞?"""
def?get_keywords(docs_tokenized,topk):
????"""?1:?英文單詞拆分?"""??
????docs_tokenized?=?[list(chain.from_iterable(doc))?for?doc?in?docs_tokenized]
????"""?2:?提取關(guān)鍵詞,包括unigram,bigram和trigram?"""
????docs_keys?=?[]
????for?n?in?[1,2,3]:
????????if?n?==?1:
????????????"""?3:?如果是unigram,還需要做詞形還原?"""
????????????docs_tokenized?=?[[lemmatizer.lemmatize(word)?for?word?in?doc]?for?doc?in?docs_tokenized]
????????keys_ngram?=?get_ngram_keywords(docs_tokenized,?topk,n_=n)
????????docs_keys.append(keys_ngram)
????return?[uni+bi+tri?for?uni,bi,tri?in?zip(*docs_keys)]
ok,來看關(guān)鍵詞提取的結(jié)果。
選取其中一篇新聞,關(guān)于新冠肺炎禁閉防范期間,降薪和裁員的影響。
"""?標(biāo)題:在冠狀病毒禁閉期間裁員或減薪有什么法律影響?"""
What?are?the?legal?implications?of?layoffs?or?salary?cuts?during?coronavirus?lockdown
以下是提取的unigram、bigram和trigram關(guān)鍵詞。
從關(guān)鍵詞中,大致可以看出是關(guān)于降薪、削減成本、勞動者保護(hù)、公司決策。
遺憾的是,冠狀病毒()這個詞沒有提取出來,但是通過實(shí)體識別抽取了出來。
'keywords':?['employee',?????????????????#?雇員
?????????????'order',????????????????????#?訂貨
?????????????'employer',?????????????????#?雇主
?????????????'cut',??????????????????????#?削減
?????????????'lockdown',?????????????????#?一級防范禁閉(期)
?????????????'scenario',?????????????????#?方案
?????????????'time',?????????????????????#?時(shí)期
?????????????'salary',???????????????????#?薪酬
?????????????'organisation',?????????????#?組織
?????????????'startup',??????????????????#?創(chuàng)業(yè)公司
?????????????'cost?reduction',???????????#?成本削減
?????????????'legal?implications',???????#?法律影響
?????????????'salary?cuts',??????????????#?降薪
?????????????'employer?beware',??????????#?雇主品牌
?????????????'discretionary?spending',???#?可自由支配的個人開支
?????????????'activity?declines',????????#?活動減少
?????????????'recommended?philosophy',???#?被推薦的方式
?????????????'practice?group',???????????#?業(yè)務(wù)部門
?????????????'population?density',???????#?人口密度
?????????????'pay?scales',???????????????#?工資標(biāo)準(zhǔn)
?????????????'advising?startup?founders',??#?建議創(chuàng)業(yè)者
?????????????'broader?startup?ecosystem',??#?更廣泛的創(chuàng)業(yè)生態(tài)系統(tǒng)
?????????????'employers?make?payment',?????#?雇主支付
?????????????'ensure?legal?protection',????#?確保法律保護(hù)
?????????????'face?tough?times',???????????#?面臨艱難時(shí)期
?????????????'garner?sufficient?caution',??#?獲得足夠的關(guān)注
?????????????'health?system?preparedness',?#?衛(wèi)生系統(tǒng)的準(zhǔn)備
?????????????'lower?pay?scales',???????????#?更低的工資標(biāo)準(zhǔn)
?????????????'seek?legal?advice',??????????#?尋求法律咨詢
?????????????'top?management?level']???????#?公司高層
時(shí)間緊,關(guān)鍵詞提取做得還不夠深入,沒有嘗試更多方法。
寫這篇文章,是為了整理思路,希望有小伙伴可以一起交流有效的提取短語的方法。