1. 程式人生 > >【NLP】【三】jieba原始碼分析之關鍵字提取(TF-IDF/TextRank)

【NLP】【三】jieba原始碼分析之關鍵字提取(TF-IDF/TextRank)

【一】綜述

利用jieba進行關鍵字提取時,有兩種介面。一個基於TF-IDF演算法,一個基於TextRank演算法。TF-IDF演算法,完全基於詞頻統計來計算詞的權重,然後排序,在返回TopK個詞作為關鍵字。TextRank相對於TF-IDF,基本思路一致,也是基於統計的思想,只不過其計算詞的權重時,還考慮了詞的上下文(通過視窗滑動來實現),而且計算詞的權重時,也考慮了相關聯絡詞的影響。可以說,TextRank實際上是依據位置與詞頻來計算詞的權重的。下面,結合基於jieba原始碼,來分別解釋兩種演算法的實現。

【二】TF-IDF

1. 原理解析

假設,共有N篇文件,分別用 d1,d2,d3,,,,,,,dn來表示。

TF = 某個詞在di篇文章中出現的次數/di篇文章的總詞數 = count(W in di)/ count(di)。因此,TF計算的是單個詞在單個文件中出現的詞頻。

IDF = 總的文件數 / 出現詞W的文件數 。 IDF其實反映了詞W在文件之間的區別度。如果W在僅在一篇文件中出現,則說明可以使用W將該文件與其他文件區別開來。即IDF可以反映W的獨特性 。

TF*IDF,可以得到詞的重要性。比如: 北京和西安在同一篇文件中的詞頻均為20%,那如何估計北京是該文的關鍵字,還是西安呢?如果同時有10篇文章均提到了北京,恰好只有這篇文章提到了西安,則西安作為這篇文章的關鍵字更為合理。

2. idf.txt

jieba有統計好的idf值,在 jieba/analyse/idf.txt中。

勞動防護 13.900677652
生化學 13.900677652
奧薩貝爾 13.900677652
考察隊員 13.900677652
崗上 11.5027823792
倒車檔 12.2912397395

3. idf.txt 載入

程式碼在 jieba/analyse/tfidf.py

class IDFLoader(object):

    def __init__(self, idf_path=None):
        self.path = ""
        self.idf_freq = {}
        # 初始化idf的中位數值
        self.median_idf = 0.0
        if idf_path:
            # 解析idf.txt
            self.set_new_path(idf_path)

    def set_new_path(self, new_idf_path):
        if self.path != new_idf_path:
            self.path = new_idf_path
            content = open(new_idf_path, 'rb').read().decode('utf-8')
            self.idf_freq = {}
            # 解析 idf.txt,拿到詞與idf的對應值,key = word,value = idf
            for line in content.splitlines():
                word, freq = line.strip().split(' ')
                self.idf_freq[word] = float(freq)
            # 取idf的中位數
            self.median_idf = sorted(
                self.idf_freq.values())[len(self.idf_freq) // 2]

4. 利用tfidf演算法提取關鍵字的介面:extract_tags

    def extract_tags(self, sentence, topK=20, withWeight=False, allowPOS=(), withFlag=False):
        """
        Extract keywords from sentence using TF-IDF algorithm.
        Parameter:
            - topK: return how many top keywords. `None` for all possible words.
            - withWeight: if True, return a list of (word, weight);
                          if False, return a list of words.
            - allowPOS: the allowed POS list eg. ['ns', 'n', 'vn', 'v','nr'].
                        if the POS of w is not in this list,it will be filtered.
            - withFlag: only work with allowPOS is not empty.
                        if True, return a list of pair(word, weight) like posseg.cut
                        if False, return a list of words
        """
        # 判斷提取出哪些詞性的關鍵字
        if allowPOS:
            allowPOS = frozenset(allowPOS)
            # 如果需要提取指定詞性的關鍵字,則先進行詞性分割
            words = self.postokenizer.cut(sentence)
        else:
            # 如果提取所有詞性的關鍵字,則使用精確分詞
            words = self.tokenizer.cut(sentence)
        freq = {}
        # 按照分詞結果,統計詞頻
        for w in words:
            if allowPOS:
                if w.flag not in allowPOS:
                    continue
                elif not withFlag:
                    w = w.word
            wc = w.word if allowPOS and withFlag else w
            # 該詞不能是停用詞
            if len(wc.strip()) < 2 or wc.lower() in self.stop_words:
                continue
            #統計該詞出現的次數
            freq[w] = freq.get(w, 0.0) + 1.0
        # 計算總的詞數目
        total = sum(freq.values())
        for k in freq:
            kw = k.word if allowPOS and withFlag else k
            # 依據tf-idf公式進行tf-idf值,作為詞的權重。其中,idf是jieba通過語料庫統計得到的
            freq[k] *= self.idf_freq.get(kw, self.median_idf) / total

        # 對詞頻做個排序,獲取TopK的詞
        if withWeight:
            tags = sorted(freq.items(), key=itemgetter(1), reverse=True)
        else:
            tags = sorted(freq, key=freq.__getitem__, reverse=True)
        if topK:
            return tags[:topK]
        else:
            return tags

5. jieba實現tf-idf總結

1):idf的值時通過語料庫統計得到的,所以,實際使用時,可能需要依據使用環境,替換為使用對應的語料庫統計得到的idf值。

2):需要從分詞結果中去除停用詞。

3):如果指定了僅提取指定詞性的關鍵詞,則詞性分割非常重要,詞性分割中準確程度,影響關鍵字的提取。

【三】TextRank

1. 演算法原理介紹

TextRank採用圖的思想,將文件中的詞表示成一張無向有權圖,詞為圖的節點,詞之間的聯絡緊密程度體現為圖的邊的權值。計算詞的權重等價於計算圖中節點的權重。提取關鍵字,等價於找出圖中權重排名TopK的節點。

如上圖所示:有A B C D E五個詞,詞之間的關係使用邊連線起來,詞之間連線的次數作為邊的權值。比如:A和C一起出現了2次,則其邊的權重為2,A與B/C/E都有聯絡,而D僅與B有聯絡。

所以說,TextRank背後體現的思想為:與其他詞關聯性強的詞,越重要。通俗一點就是:圍著誰轉,誰就重要。就像大家基本都會圍著領導轉一樣。

2. 圖的構建

圖的構建分為兩部分:

1):確認圖的節點之間的聯絡

2):確認邊的權值

jieba是如何做的呢?


    def textrank(self, sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'), withFlag=False):
        """
        Extract keywords from sentence using TextRank algorithm.
        Parameter:
            - topK: return how many top keywords. `None` for all possible words.
            - withWeight: if True, return a list of (word, weight);
                          if False, return a list of words.
            - allowPOS: the allowed POS list eg. ['ns', 'n', 'vn', 'v'].
                        if the POS of w is not in this list, it will be filtered.
            - withFlag: if True, return a list of pair(word, weight) like posseg.cut
                        if False, return a list of words
        """
        # 初始化關鍵字詞性過濾條件
        self.pos_filt = frozenset(allowPOS)
        # 初始化一個無向權值圖
        g = UndirectWeightedGraph()
        cm = defaultdict(int)
        # 使用精確模式進行分詞
        words = tuple(self.tokenizer.cut(sentence))
        # 遍歷分詞結果
        for i, wp in enumerate(words):
            # 詞wp如果滿足關鍵詞備選條件,則加入圖中
            if self.pairfilter(wp):
                # span為滑動視窗,即詞的上下文,藉此來實現此的共現,完成詞之間的連線。
                for j in xrange(i + 1, i + self.span):
                    if j >= len(words):
                        break
                    # 後向詞也要滿足備選詞條件
                    if not self.pairfilter(words[j]):
                        continue
                    if allowPOS and withFlag:
                        # 共現詞作為圖一條邊的兩個節點,共現詞出現的次數,作為邊的權值
                        cm[(wp, words[j])] += 1
                    else:
                        cm[(wp.word, words[j].word)] += 1
        # 將 備選詞和與該詞連線的詞加入到graph中,即完成graph的構造
        for terms, w in cm.items():
            g.addEdge(terms[0], terms[1], w)
        # 呼叫graph的rank介面,完成TextRank演算法的計算,即計算出各節點的權重
        nodes_rank = g.rank()
        if withWeight:
            # 對graph中的階段的權重進行排序
            tags = sorted(nodes_rank.items(), key=itemgetter(1), reverse=True)
        else:
            tags = sorted(nodes_rank, key=nodes_rank.__getitem__, reverse=True)

        if topK:
            return tags[:topK]
        else:
            return tags

3. TextRank演算法的實現

    def rank(self):
        ws = defaultdict(float)
        outSum = defaultdict(float)

        # 計算初始化節點的weight值
        wsdef = 1.0 / (len(self.graph) or 1.0)
        # 初始化各個節點的weight值,並計算各個節點的出度數目
        for n, out in self.graph.items():
            ws[n] = wsdef
            outSum[n] = sum((e[2] for e in out), 0.0)

        # this line for build stable iteration
        sorted_keys = sorted(self.graph.keys())
        # 迴圈迭代10,迭代計算出各個節點的weight值
        for x in xrange(10):  # 10 iters
            for n in sorted_keys:
                s = 0
                # 依據TextRank公式計算weight
                for e in self.graph[n]:
                    s += e[2] / outSum[e[1]] * ws[e[1]]
                ws[n] = (1 - self.d) + self.d * s

        (min_rank, max_rank) = (sys.float_info[0], sys.float_info[3])

        for w in itervalues(ws):
            if w < min_rank:
                min_rank = w
            if w > max_rank:
                max_rank = w

        for n, w in ws.items():
            # to unify the weights, don't *100.
            ws[n] = (w - min_rank / 10.0) / (max_rank - min_rank / 10.0)

        return ws

【四】TF-IDF與TextRank演算法的比較

1. 從演算法原理上來看,基礎都是詞頻統計,只是TD-IDF通過IDF來調整詞頻的權值,而TextRank通過上下文的連線數來調整詞頻的權值。TextRank通過滑動視窗的方式,來實現詞的位置對詞的權值的影響。

2. TD-IDF計算簡單,執行效能更好。