關鍵詞提取:TF-IDF和n-gram

一:引導

目錄

一:引導

二:內容預告

三:關鍵詞提取的方法

01 TF-IDF

02.TextRank

03.文本聚類法

04.有監督的關鍵詞提取

四:TF-IDF+n-gram提取關鍵詞

01.英文新聞測試語料

02.需要的庫

03.對新聞做單詞拆分

04.n-gram的生成

05.n-gram的過濾

06.計算TF-IDF

07.完成關鍵詞提取


搭檔鎮樓。

今天的頭版給我漂亮的搭檔,啥年芳二六、待字閨中之類的矯情話就不說了,希望看到文章的小夥子,如果對眼,請放下你手中的遊戲,我可以牽線搭橋。

好好相愛,就是爲民除害。

搭檔是重慶妹紙,重慶妹紙長得是很水靈。

搭檔給我的感覺是情商比較高,比較會捧哏,說話不會悶。

搭檔身高160體重100,學歷本科水瓶座,目前在重慶的銀行工作。

以下爲搭檔的自我介紹:

性格慢熱,絕不隨便。

心裏住着公主,也住着女王。

能喫苦下田種地,也能溫書習字梳妝。

喜歡跳傘游泳潛水衝浪,就差一個男生一起去體驗。

家裏兩房一車,父母事業單位就職,性格開明。

希望男生乾淨、上進,五官端正,性格開朗健談,收入穩定能養家,家庭和諧,會做飯更佳。

二:內容預告

 

最近公司要做英語新聞推薦系統,需要對新聞內容做分析,主要包括網頁去重、實體抽取和關鍵詞提取。

提到關鍵詞提取,大家很容易就能想到TF-IDF和TextRank。這兩種方法可以分別通過調sklearn和jiabe的包來實現。

可如果要提取的是幾個單詞組成的短語呢?

這時候按標準的調包教程來做,是難以滿足業務需要的。

所以本文探索,如何把TF-IDF和n-gram結合,用來提取短語。

本文關注以下三個問題:

  • 關鍵詞提取有哪些方法?

  • 如何把TF-IDF和n-gram結合?

  • 如何根據詞性和停用詞過濾?

三:關鍵詞提取的方法

 

01 TF-IDF

 

TF-IDF在nlp領域無人不知無人不曉,思想簡單卻有效,榮獲nlp界的諾貝爾獎:奧卡姆剃刀獎。

TF(term frequency),即詞頻,用來衡量詞在一篇文檔中的重要性,詞頻越高,越重要。計算公式爲:

某文檔中某詞出現的次數該文檔的總詞數

IDF((inverse document frequency),叫做逆文檔頻率,衡量某個詞在所有文檔集合中的常見程度。

當包含某個詞的文檔的數量越多時,這個詞也就爛大街了,重要性越低。計算公式爲:

全部文檔的數量包含某詞的文檔的數量

於是TF-IDF = TF * IDF, 它表明字詞的重要性與它在某篇文檔中出現的次數成正比,與它在所有文檔中出現的次數成反比。

使用TF-IDF的一個假設前提是:已經去掉了停用詞。

TF-IDF的優點是計算速度快,結果穩健。

需要輸入多篇文檔,可以輸出每篇文檔的關鍵詞。

02.TextRank

TextRank基於圖計算來提取關鍵詞,需要進行迭代,速度比TF-IDF慢,但不需要通過輸入多篇文檔來提取關鍵詞。

TextRank是把一篇文檔構建成無向圖,圖中的節點就是詞語,圖上的邊就是共現詞之間的連接。

共現詞通過滑動窗口來確定,共現詞之間用邊相連,而邊上的權重可以使用共現詞的相似度。

jieba提供了用TextRank提取關鍵詞的函數,但是邊的權重是詞共現的頻率。

這樣做其實比較粗糙,我們可以使用基於詞向量計算的相似度得分作爲權重,來進行迭代。
我這不知不覺又給自己安排了任務,文章名字都想好了:
《TextRank提取關鍵詞:我也是改過jieba源碼的人!》

TextRank也需要先去掉停用詞。

03.文本聚類法

TF-IDF只從淺層的詞頻角度挖掘關鍵詞,而通過Kmeans或Topic Model,可以從深層的隱含語義角度來提取關鍵詞。

一種方法是通過Kmeans來提取,使用詞向量作爲特徵。

比如對於一篇文檔,我們要提取10個關鍵詞。

那麼可以把文檔中的詞聚成5類,然後取每個類中,與類中心最近的2個點,作爲關鍵詞。

也可以直接聚成10類,取和類中心最近的詞。

另一種方法是通過Topic Model來提取,比如LSA和LDA,使用詞頻矩陣作爲特徵。

比如對於包含多篇文檔的單領域語料,我們要挖掘關鍵詞,整理詞庫。

那麼可以用LDA進行聚類,得到每個主題的單詞分佈,再取出每個主題下排名靠前的topk個單詞,或者權重高於某個閾值的單詞,構成關鍵詞庫。

主題的單詞分佈爲:

(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*"設計" + 0.004*"元" + 0.004*"拍攝" + 0.003*"萬" + 0.003*"市場" + 0.003*"編輯" + 0.003*"時尚" + 0.003*"穿"')

(2, '0.007*"英寸" + 0.005*"中" + 0.004*"中國" + 0.003*"拍攝" + 0.003*"比賽" + 0.002*"高清" + 0.002*"小巧" + 0.002*"產品" + 0.002*"億股" + 0.002*"機型"')

(3, '0.082*"基金" + 0.015*"公司" + 0.014*"市場" + 0.014*"投資" + 0.009*"股票" + 0.007*"億元" + 0.007*"中" + 0.006*"收益" + 0.006*"行業" + 0.006*"一季度"')
...

 

04.有監督的關鍵詞提取

有監督的方法需要有標註的數據,我沒有嘗試過。

看一些文章說可以轉化爲統計機器翻譯(SMT)的問題,轉化爲序列標註(NER)的問題,或者轉化爲詞語排序(LTR)的問題。

我只理解了轉化爲序列標註問題的做法,這個和用深度學習做文本摘要類似。

文檔中的詞語,如果爲關鍵詞,則標註爲1,否則標註爲0,也就是對一個詞進行二分類。

據說效果比上述無監督的方法好。

四:TF-IDF+n-gram提取關鍵詞

提取單詞作爲關鍵詞,比較容易實現,如果要提取短語呢?

我嘗試了開源工具RAKE,它是根據停用詞來劃分句子,再提取短語的。

使用之後,我發現RAKE存在兩個問題:

一是提取的短語有些長達4-5個單詞,這顯然不合適;

二是沒有根據詞性進行過濾。

當然,我們可以用RAKE得到一個粗糙的結果,然後再做細緻的處理,比如根據包含單詞的數量、根據詞性模板等進行過濾。

不過RAKE的代碼寫得實在太亂了,我沒有耐心看下去,對其原理也不太瞭解,也就沒加工再利用。

我給出的方案是TF-IDF結合n-gram來提取關鍵短語,並根據單詞長度、停用詞和詞性進行過濾。

01.英文新聞測試語料

我去某網站下載了4篇英語新聞,對於其中的一篇,經過增加一段和兩段、刪除一段和兩段的操作,得到4篇內容高度重合的新聞,最終得到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的計算,用sklearn的包。

由於是英文文本的處理,所以需要用NLTK做英文單詞拆分、詞性標註和詞形還原。

#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()

爲什麼需要做詞形還原呢?

詞形還原是指把英文的單詞,從複數形態、第三人稱形態等複雜形態,轉換爲最基礎的形態,比如 salaries 還原爲salary,makes還原爲make。

如果我們提取的關鍵詞是單個詞,那麼在使用TF-IDF進行提取之前,先要對每個單詞做詞形還原,不然salaries和salary會被認爲是不同的兩個單詞。

詞形還原是基於詞典的,準確率比較高,所以使用NLTK做詞形還原時,需要下載數據 WordNet。

在公司很容易出現網絡不通的問題,反正我是下載不了。

 

nltk.download('wordnet')

[nltk_data] Error loading wordnet: <urlopen error [Errno 104]
[nltk_data]     Connection reset by peer>
False

 

其實不止詞形還原需要下載數據,做詞性標註和實體識別,都需要下載,特別麻煩。

於是我乾脆把NLTK的所有數據文件都下載了,文件大小爲1.08G,解壓後放到相應的路徑,就一勞永逸了。

下載鏈接:

https://pan.baidu.com/s/1Ms4tfGlF3IA6F0Mg5Ljd8Q

提取碼:

07qb

下載好後解壓,在ubuntu環境下,給文件賦予相應的可執行權限(Goup和Others也需要可執行權限),然後把數據文件複製到以下路徑:

 

  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.對新聞做單詞拆分

最好不要對整篇文檔做 word_tokenize,而是先 sent_tokenize(劃分句子),再word_tokenize。

因爲實體識別、詞性標註和句法分析,最好以句子爲單位來做。

 

""" 一:對文檔進行分詞/拆字 """
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的TfidfVectorizer的類裏邊,也提供了n-gram的功能,那爲什麼我還要自己生成呢?

原因是sklearn內置的功能裏,生成n-gram後,就直接計算TF-IDF了,沒根據停用詞和詞性,過濾n-gram。

而根據停用詞和詞性過濾n-gram,必須在計算TF-IDF之前。

那在tokenize的時候做過濾不就得了嗎?

不行!

過濾n-gram,是指如果n-gram中,有一個單詞是停用詞,或者有一個單詞的詞性是過濾的詞性,那麼整個n-gram都需要去掉,而不能只去掉該單詞。

在tokenize時過濾單詞,那麼生成的n-gram短語塊可能是錯誤的。

 

""" 二:產生n-gram,用於提取短語塊 """
def gene_ngram(sentence,n=3,m=2):
    """
    ----------
    sentence: 分詞後的句子
    n: 取3,則爲3-gram
    m: 取1,則保留1-gram
    ----------
    """
    if len(sentence) < n:
        n = len(sentence)

    ngrams = [sentence[i-k:i] for k in range(m, n+1) for i in range(k, len(sentence)+1)]
    return ngrams

 

05.n-gram的過濾

生成的trigram如下:

[['What', 'are', 'the'], ['are', 'the', 'legal'], ['the', 'legal', 'implications'], ['legal', 'implications', 'of'],

我們要對n-gram進行過濾,根據停用詞、詞性、單詞的長度以及是否包含數字,來過濾。

詞性可以去網上找英文詞性對照表。

如果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中有數字,則去掉 """
    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))

        """ 如果提取單詞作爲關鍵詞,則必須爲名詞 """
        if len(words) == 1:
            if not pos[0] in allow_pos_one:
                continue   
            ngrams_filter.append(gram)

        else:
            """ 如果提取短語,那麼開頭必須爲名詞、動詞、形容詞,結尾爲名詞 """
            if not (pos[0] in allow_pos_three and pos[-1] in allow_pos_one):
                continue  
            ngrams_filter.append(gram)

    return ngrams_filter

 

06.計算TF-IDF

用上面的函數,完成生成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之間的單詞用下劃線來連接呢?

我試了其他的符號:#、=,但是發現送入sklearn的包裏計算時,n-gram會被重新拆分爲單詞,vocab不是n-gram短語,而是單詞。

我估計是因爲python中,下劃線比較特殊,如正則表達式 \w,表示字母、數字和下劃線,其他的符號則容易被視爲文本噪音而去掉。

接着計算n-gram的TF-IDF,這裏又有一個坑:如果不自己指定n-gram字典,那麼sklearn自己構建的字典中,可能會有單詞或單詞加下劃線這種奇怪的東西。

所以需要自己傳入vocab。

""" 四:獲取 tf-idf 特徵 """
def calcu_tf_idf(documents):
    """
    :param: data爲列表格式的文檔集合, 計算 tf_idf 特徵
    """

    """ 指定vocab,否則n-gram的計算會出錯 """
    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.完成關鍵詞提取

接着,就可以完成關鍵詞的提取了。

考慮到關鍵詞提取的全面性,分別提取unigram、bigram和trigram關鍵詞。

如果提取unigram關鍵詞,那麼需要做詞形還原。

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: 計算tf-idf,提取關鍵詞 """
    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關鍵詞 """
def get_keywords(docs_tokenized,topk):

    """ 1: 英文單詞拆分 """  
    docs_tokenized = [list(chain.from_iterable(doc)) for doc in docs_tokenized]

    """ 2: 提取關鍵詞,包括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,來看關鍵詞提取的結果。

選取其中一篇新聞,關於新冠肺炎禁閉防範期間,降薪和裁員的影響。

""" 標題:在冠狀病毒禁閉期間裁員或減薪有什麼法律影響 """
What are the legal implications of layoffs or salary cuts during coronavirus lockdown

以下是提取的unigram、bigram和trigram關鍵詞。

從關鍵詞中,大致可以看出是關於降薪、削減成本、勞動者保護、公司決策。

遺憾的是,冠狀病毒(Coronavirus)這個詞沒有提取出來,但是通過實體識別抽取了出來。

 

'keywords': ['employee',                 # 僱員
             'order',                    # 訂貨
             'employer',                 # 僱主
             'cut',                      # 削減
             'lockdown',                 # 一級防範禁閉(期)
             'scenario',                 # 方案
             'time',                     # 時期
             'salary',                   # 薪酬
             'organisation',             # 組織
             'startup',                  # 創業公司
             'cost reduction',           # 成本削減
             'legal implications',       # 法律影響
             'salary cuts',              # 降薪
             'employer beware',          # 僱主品牌
             'discretionary spending',   # 可自由支配的個人開支
             'activity declines',        # 活動減少
             'recommended philosophy',   # 被推薦的方式
             'practice group',           # 業務部門
             'population density',       # 人口密度
             'pay scales',               # 工資標準
             'advising startup founders',  # 建議創業者
             'broader startup ecosystem',  # 更廣泛的創業生態系統
             'employers make payment',     # 僱主支付
             'ensure legal protection',    # 確保法律保護
             'face tough times',           # 面臨艱難時期
             'garner sufficient caution',  # 獲得足夠的關注
             'health system preparedness', # 衛生系統的準備
             'lower pay scales',           # 更低的工資標準
             'seek legal advice',          # 尋求法律諮詢
             'top management level']       # 公司高層

時間緊,關鍵詞提取做得還不夠深入,沒有嘗試更多方法。

寫這篇文章,是爲了整理思路,希望有小夥伴可以一起交流有效的提取短語的方法。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章