筆記轉載於GitHub項目:https://github.com/NLP-LOVE/Introduction-NLPpython
正所謂物以類聚,人以羣分。人們在獲取數據時須要整理,將類似的數據歸檔到一塊兒,自動發現大量樣本之間的類似性,這種根據類似性歸檔的任務稱爲聚類。git
聚類github
聚類(cluster analysis )指的是將給定對象的集合劃分爲不一樣子集的過程,目標是使得每一個子集內部的元素儘可能類似,不一樣子集間的元素儘可能不類似。這些子集又被稱爲簇(cluster),通常沒有交集。算法
通常將聚類時簇的數量視做由使用者指定的超參數,雖然存在許多自動判斷的算法,但它們每每須要人工指定其餘超參數。數組
根據聚類結果的結構,聚類算法也能夠分爲劃分式(partitional )和層次化(hierarchieal兩種。劃分聚類的結果是一系列不相交的子集,而層次聚類的結果是一棵樹, 葉子節點是元素,父節點是簇。本章主要介紹劃分聚類。ide
文本聚類函數
文本聚類指的是對文檔進行聚類分析,被普遍用於文本挖掘和信息檢索領域。學習
文本聚類的基本流程分爲特徵提取和向量聚類兩步, 若是能將文檔表示爲向量,就能夠對其應用聚類算法。這種表示過程稱爲特徵提取,而一旦將文檔表示爲向量,剩下的算法就與文檔無關了。這種抽象思惟不管是從軟件工程的角度,仍是從數學應用的角度都十分簡潔有效。測試
詞袋模型優化
詞袋(bag-of-words )是信息檢索與天然語言處理中最經常使用的文檔表示模型,它將文檔想象爲一個裝有詞語的袋子, 經過袋子中每種詞語的計數等統計量將文檔表示爲向量。好比下面的例子:
人 吃 魚。 美味 好 吃!
統計詞頻後以下:
人=1 吃=2 魚=1 美味=1 好=1
文檔通過該詞袋模型獲得的向量表示爲[1,2,1,1,1],這 5 個維度分別表示這 5 種詞語的詞頻。
通常選取訓練集文檔的全部詞語構成一個詞表,詞表以外的詞語稱爲 oov,不予考慮。一旦詞表固定下來,假設大小爲 N。則任何一個文檔均可以經過這種方法轉換爲一個N維向量。詞袋模型不考慮詞序,也正由於這個緣由,詞袋模型損失了詞序中蘊含的語義,好比,對於詞袋模型來說,「人吃魚」和「魚吃人」是同樣的,這就不對了。
不過目前工業界已經發展出很好的詞向量表示方法了: word2vec/bert 模型等。
詞袋中的統計指標
詞袋模型並不是只是選取詞頻做爲統計指標,而是存在許多選項。常見的統計指標以下:
定義由 n 個文檔組成的集合爲 S,定義其中第 i 個文檔 di 的特徵向量爲 di,其公式以下:
\[ d_{i}=\left(\operatorname{TF}\left(t_{1}, d_{i}\right), \operatorname{TF}\left(t_{2}, d_{i}\right), \cdots, \operatorname{TF}\left(t_{j}, d_{i}\right), \cdots, \operatorname{TF}\left(t_{m}, d_{i}\right)\right) \]
其中 tj表示詞表中第 j 種單詞,m 爲詞表大小, TF(tj, di) 表示單詞 tj 在文檔 di 中的出現次數。爲了處理長度不一樣的文檔,一般將文檔向量處理爲單位向量,即縮放向量使得 ||d||=1。
一種簡單實用的聚類算法是k均值算法(k-means),由Stuart Lloyd於1957年提出。該算法雖然沒法保證必定可以獲得最優聚類結果,但實踐效果很是好。基於k均值算法衍生出許多改進算法,先介紹 k均值算法,而後推導它的一個變種。
基本原理
形式化啊定義 k均值算法所解決的問題,給定 n 個向量 d1 到 dn,以及一個整數 k,要求找出 k 個簇 S1 到 Sk 以及各自的質心 C1 到 Ck,使得下式最小:
\[ \text { minimize } \mathcal{I}_{\text {Euclidean }}=\sum_{r=1}^{k} \sum_{d_{i} \in S_{r}}\left\|\boldsymbol{d}_{i}-\boldsymbol{c}_{r}\right\|^{2} \]
其中 ||di - Cr|| 是向量與質心的歐拉距離,I(Euclidean) 稱做聚類的準則函數。也就是說,k均值以最小化每一個向量到質心的歐拉距離的平方和爲準則進行聚類,因此該準則函數有時也稱做平方偏差和函數。而質心的計算就是簇內數據點的幾何平均:
\[ \begin{array}{l}{s_{i}=\sum_{d_{j} \in S_{i}} d_{j}} \\ {c_{i}=\frac{s_{i}}{\left|S_{i}\right|}}\end{array} \]
其中,si 是簇 Si 內全部向量之和,稱做合成向量。
生成 k 個簇的 k均值算法是一種迭代式算法,每次迭代都在上一步的基礎上優化聚類結果,步驟以下:
k均值算法雖然沒法保證收斂到全局最優,但可以有效地收斂到一個局部最優勢。對於該算法,初級讀者重點須要關注兩個問題,即初始質心的選取和兩點距離的度量。
初始質心的選取
因爲 k均值不保證收敏到全局最優,因此初始質心的選取對k均值的運行結果影響很是大,若是選取不當,則可能收斂到一個較差的局部最優勢。
一種更高效的方法是, 將質心的選取也視做準則函數進行迭代式優化的過程。其具體作法是,先隨機選擇第一個數據點做爲質心,視做只有一個簇計算準則函數。同時維護每一個點到最近質心的距離的平方,做爲一個映射數組 M。接着,隨機取準則函數值的一部分記做。遍歷剩下的全部數據點,若該點到最近質心的距離的平方小於0,則選取該點添加到質心列表,同時更新準則函數與 M。如此循環屢次,直至湊足 k 個初始質心。這種方法可行的原理在於,每新增一個質心,都保證了準則函數的值降低一個隨機比率。 而樸素實現至關於每次新增的質心都是徹底隨機的,準則函數的增減沒法控制。孰優孰劣,一目瞭然。
考慮到 k均值是一種迭代式的算法, 須要反覆計算質心與兩點距離,這部分計算一般是效瓶頸。爲了改進樸素 k均值算法的運行效率,HanLP利用種更快的準則函數實現了k均值的變種。
更快的準則函數
除了歐拉準則函數,還存在一種基於餘弦距離的準則函數:
\[ \text { maximize } \mathcal{I}_{\mathrm{cos}}=\sum_{r=1}^{k} \sum_{d_{i} \in S_{r}} \cos \left(\boldsymbol{d}_{i}, \boldsymbol{c}_{r}\right) \]
該函數使用餘弦函數衡量點與質心的類似度,目標是最大化同簇內點與質心的類似度。將向量夾角計算公式代人,該準則函數變換爲:
\[ \mathcal{I}_{\mathrm{cos}}=\sum_{r=1}^{k} \sum_{d_{i} \in S_{r}} \frac{d_{i} \cdot c_{r}}{\left\|c_{r}\right\|} \]
代入後變換爲:
\[ \mathcal{I}_{\cos }=\sum_{r=1}^{k} \frac{S_{r} \cdot c_{r}}{\left\|c_{r}\right\|}=\sum_{r=1}^{k} \frac{\left|S_{r}\right| c_{r} \cdot c_{r}}{\left\|c_{r}\right\|}=\sum_{r=1}^{k}\left|S_{r}\right|\left\|c_{r}\right\|=\sum_{r=1}^{k}\left\|s_{r}\right\| \]
也就是說,餘弦準則函數等於 k 個簇各自合成向量的長度之和。比較以前的準則函數會發如今數據點從原簇移動到新簇時,I(Euclidean) 須要從新計算質心,以及兩個簇內全部點到新質心的距離。而對於I(cos),因爲發生改變的只有原簇和新簇兩個合成向量,只需求二者的長度便可,計算量一會兒減少很多。
基於新準則函數 I(cos),k均值變種算法流程以下:
實現
在 HanLP 中,聚類算法實現爲 ClusterAnalyzer,用戶能夠將其想象爲一個文檔 id 到文檔向量的映射容器。
此處以某音樂網站中的用戶聚類爲案例講解聚類模塊的用法。假設該音樂網站將 6 位用戶點播的歌曲的流派記錄下來,而且分別拼接爲 6 段文本。給定用戶名稱與這 6 段播放歷史,要求將這 6 位用戶劃分爲 3 個簇。實現代碼以下:
from pyhanlp import * ClusterAnalyzer = JClass('com.hankcs.hanlp.mining.cluster.ClusterAnalyzer') if __name__ == '__main__': analyzer = ClusterAnalyzer() analyzer.addDocument("趙一", "流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 藍調, 藍調, 藍調, 藍調, 藍調, 藍調, 搖滾, 搖滾, 搖滾, 搖滾") analyzer.addDocument("錢二", "爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲") analyzer.addDocument("張三", "古典, 古典, 古典, 古典, 民謠, 民謠, 民謠, 民謠") analyzer.addDocument("李四", "爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 金屬, 金屬, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲") analyzer.addDocument("王五", "流行, 流行, 流行, 流行, 搖滾, 搖滾, 搖滾, 嘻哈, 嘻哈, 嘻哈") analyzer.addDocument("馬六", "古典, 古典, 古典, 古典, 古典, 古典, 古典, 古典, 搖滾") print(analyzer.kmeans(3))
結果以下:
[[李四, 錢二], [王五, 趙一], [張三, 馬六]]
經過 k均值聚類算法,咱們成功的將用戶按興趣分組,得到了「人以羣分」的效果。
聚類結果中簇的順序是隨機的,每一個簇中的元素也是無序的,因爲 k均值是個隨機算法,有小几率獲得不一樣的結果。
該聚類模塊能夠接受任意文本做爲文檔,而不須要用特殊分隔符隔開單詞。
基本原理
重複二分聚類(repeated bisection clustering) 是 k均值算法的效率增強版,其名稱中的bisection是「二分」的意思,指的是反覆對子集進行二分。該算法的步驟以下:
每次產生的簇由上到下造成了一顆二叉樹結構。
正是因爲這個性質,重複二分聚類算得上一種基於劃分的層次聚類算法。若是咱們把算法運行的中間結果存儲起來,就能輸出一棵具備層級關係的樹。樹上每一個節點都是一個簇,父子節點對應的簇知足包含關係。雖然每次劃分都基於 k均值,因爲每次二分都僅僅在一個子集上進行,輸人數據少,算法天然更快。
在步驟1中,HanLP採用二分後準則函數的增幅最大爲策略,每產生一個新簇,都試着將其二分並計算準則函數的增幅。而後對增幅最大的簇執行二分,重複屢次直到知足算法中止條件。
自動判斷聚類個數k
讀者可能以爲聚類個數 k 這個超參數很難準確估計。在重複二分聚類算法中,有一種變通的方法,那就是經過給準則函數的增幅設定閾值 β 來自動判斷 k。此時算法的中止條件爲,當一個簇的二分增幅小於 β 時再也不對該簇進行劃分,即認爲這個簇已經達到最終狀態,不可再分。當全部簇都不可再分時,算法終止,最終產生的聚類數量就再也不須要人工指定了。
實現
from pyhanlp import * ClusterAnalyzer = JClass('com.hankcs.hanlp.mining.cluster.ClusterAnalyzer') if __name__ == '__main__': analyzer = ClusterAnalyzer() analyzer.addDocument("趙一", "流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 藍調, 藍調, 藍調, 藍調, 藍調, 藍調, 搖滾, 搖滾, 搖滾, 搖滾") analyzer.addDocument("錢二", "爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲") analyzer.addDocument("張三", "古典, 古典, 古典, 古典, 民謠, 民謠, 民謠, 民謠") analyzer.addDocument("李四", "爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 金屬, 金屬, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲") analyzer.addDocument("王五", "流行, 流行, 流行, 流行, 搖滾, 搖滾, 搖滾, 嘻哈, 嘻哈, 嘻哈") analyzer.addDocument("馬六", "古典, 古典, 古典, 古典, 古典, 古典, 古典, 古典, 搖滾") print(analyzer.repeatedBisection(3)) # 重複二分聚類 print(analyzer.repeatedBisection(1.0)) # 自動判斷聚類數量k
運行結果以下:
[[李四, 錢二], [王五, 趙一], [張三, 馬六]] [[李四, 錢二], [王五, 趙一], [張三, 馬六]]
與上面音樂案例得出的結果一致,但運行速度要快很多。
本次評測選擇搜狗實驗室提供的文本分類語料的一個子集,我稱它爲「搜狗文本分類語料庫迷你版」。該迷你版語料庫分爲5個類目,每一個類目下1000 篇文章,共計5000篇文章。運行代碼以下:
from pyhanlp import * import zipfile import os from pyhanlp.static import download, remove_file, HANLP_DATA_PATH def test_data_path(): """ 獲取測試數據路徑,位於$root/data/test,根目錄由配置文件指定。 :return: """ data_path = os.path.join(HANLP_DATA_PATH, 'test') if not os.path.isdir(data_path): os.mkdir(data_path) return data_path ## 驗證是否存在 MSR語料庫,若是沒有自動下載 def ensure_data(data_name, data_url): root_path = test_data_path() dest_path = os.path.join(root_path, data_name) if os.path.exists(dest_path): return dest_path if data_url.endswith('.zip'): dest_path += '.zip' download(data_url, dest_path) if data_url.endswith('.zip'): with zipfile.ZipFile(dest_path, "r") as archive: archive.extractall(root_path) remove_file(dest_path) dest_path = dest_path[:-len('.zip')] return dest_path sogou_corpus_path = ensure_data('搜狗文本分類語料庫迷你版', 'http://file.hankcs.com/corpus/sogou-text-classification-corpus-mini.zip') ## =============================================== ## 如下開始聚類 ClusterAnalyzer = JClass('com.hankcs.hanlp.mining.cluster.ClusterAnalyzer') if __name__ == '__main__': for algorithm in "kmeans", "repeated bisection": print("%s F1=%.2f\n" % (algorithm, ClusterAnalyzer.evaluate(sogou_corpus_path, algorithm) * 100))
運行結果以下:
kmeans F1=83.74 repeated bisection F1=85.58
評測結果以下表:
算法 | F1 | 耗時 |
---|---|---|
k均值 | 83.74 | 67秒 |
重複二分聚類 | 85.58 | 24秒 |
對比兩種算法,重複二分聚類不只準確率比 k均值更高,並且速度是 k均值的 3 倍。然而重複二分聚類成績波動較大,須要多運行幾回纔可能得出這樣的結果。
無監督聚類算法沒法學習人類的偏好對文檔進行劃分,也沒法學習每一個簇在人類那裏究竟叫什麼。
HanLP何晗--《天然語言處理入門》筆記:
https://github.com/NLP-LOVE/Introduction-NLP
項目持續更新中......
目錄
章節 |
---|
第 1 章:新手上路 |
第 2 章:詞典分詞 |
第 3 章:二元語法與中文分詞 |
第 4 章:隱馬爾可夫模型與序列標註 |
第 5 章:感知機分類與序列標註 |
第 6 章:條件隨機場與序列標註 |
第 7 章:詞性標註 |
第 8 章:命名實體識別 |
第 9 章:信息抽取 |
第 10 章:文本聚類 |
第 11 章:文本分類 |
第 12 章:依存句法分析 |
第 13 章:深度學習與天然語言處理 |