文本聚類是將一個個文檔由原有的自然語言文字信息轉化成數學信息,以高維空間點的形式展現出來,通過計算哪些點距離比較近,從而將那些點聚成一個簇,簇的中心叫做簇心。一個好的聚類要保證簇內點的距離盡量的近,但簇與簇之間的點要盡量的遠。
如下圖,以 K、M、N 三個點分別為聚類的簇心,將結果聚為三類,使得簇內點的距離盡量的近,但簇與簇之間的點盡量的遠。
本文繼續沿用上篇文本分類中的語料來進行文本無監督聚類操作。
整個過程分為以下幾個步驟:
- 語料加載
- 分詞
- 去停用詞
- 抽取詞向量特征
- 實戰 TF-IDF 的中文文本 K-means 聚類
- 實戰 word2Vec 的中文文本 K-means 聚類
下面開始項目實戰。
1. 首先進行語料加載,在這之前,引入所需要的 Python 依賴包,並將全部語料和停用詞字典讀入內存中。
第一步,引入依賴庫,有隨機數庫、jieba 分詞、pandas 庫等:
import random import jieba import pandas as pd import numpy as np from sklearn.feature_extraction.text import TfidfTransformer from sklearn.feature_extraction.text import TfidfVectorizer import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.cluster import KMeans import gensim from gensim.models import Word2Vec from sklearn.preprocessing import scale import multiprocessing
第二步,加載停用詞字典,停用詞詞典為 stopwords.txt 文件,可以根據場景自己在該文本里面添加要去除的詞(比如冠詞、人稱、數字等特定詞):
# 加載停用詞 stopwords = pd.read_csv('./data6/stopwords.txt', index_col=False, quoting=3, sep="\t", names=['stopword'], encoding='utf-8') stopwords = stopwords['stopword'].values
第三步,加載語料,語料是4個已經分好類的 csv 文件,直接用 pandas 加載即可,加載之后可以首先刪除 nan 行,並提取要分詞的 content 列轉換為 list 列表:
# 加載語料 laogong_df = pd.read_csv('./data6/beilaogongda.csv', encoding='utf-8', sep=',') laopo_df = pd.read_csv('./data6/beilaogongda.csv', encoding='utf-8', sep=',') erzi_df = pd.read_csv('./data6/beierzida.csv', encoding='utf-8', sep=',') nver_df = pd.read_csv('./data6/beinverda.csv', encoding='utf-8', sep=',') # 刪除語料的nan行 laogong_df.dropna(inplace=True) laopo_df.dropna(inplace=True) erzi_df.dropna(inplace=True) nver_df.dropna(inplace=True) # 轉換 laogong = laogong_df.segment.values.tolist() laopo = laopo_df.segment.values.tolist() erzi = erzi_df.segment.values.tolist() nver = nver_df.segment.values.tolist()
2. 分詞和去停用詞。
第一步,定義分詞、去停用詞的函數,函數包含兩個參數:content_lines
參數為語料列表;sentences 參數為預先定義的 list,用來存儲分詞后的結果:
# 定義分詞函數preprocess_text # 參數content_lines即為上面轉換的list # 參數sentences是定義的空list,用來儲存分詞后的數據 def preprocess_text(content_lines, sentences): for line in content_lines: try: segs = jieba.lcut(line) segs = [v for v in segs if not str(v).isdigit()] # 去數字 segs = list(filter(lambda x: x.strip(), segs)) # 去左右空格 segs = list(filter(lambda x: len(x) > 1, segs)) # 長度為1的字符 segs = list(filter(lambda x: x not in stopwords, segs)) # 去掉停用詞 sentences.append(" ".join(segs)) except Exception: print(line) continue
第二步,調用函數、生成訓練數據,根據我提供的司法語料數據,分為報警人被老公打,報警人被老婆打,報警人被兒子打,報警人被女兒打,具體如下:
sentences = []
preprocess_text(laogong, sentences)
preprocess_text(laopo, sentences)
preprocess_text(erzi, sentences)
preprocess_text(nver, sentences)
第三步,將得到的數據集打散,生成更可靠的訓練集分布,避免同類數據分布不均勻:
random.shuffle(sentences)
第四步,我們控制台輸出前10條數據,觀察一下(因為上面進行了隨機打散,你看到的前10條可能不一樣):
for sentence in sentences[:10]: print(sentence)
得到的結果聚類和分類是不同的,這里沒有標簽:
3. 抽取詞向量特征。
抽取特征,將文本中的詞語轉換為詞頻矩陣,統計每個詞語的 tf-idf
權值,獲得詞在對應文本中的 tf-idf
權重:
#將文本中的詞語轉換為詞頻矩陣 矩陣元素a[i][j] 表示j詞在i類文本下的詞頻 vectorizer = TfidfVectorizer(sublinear_tf=True, max_df=0.5) #統計每個詞語的tf-idf權值 transformer = TfidfTransformer() # 第一個fit_transform是計算tf-idf 第二個fit_transform是將文本轉為詞頻矩陣 tfidf = transformer.fit_transform(vectorizer.fit_transform(sentences)) # 獲取詞袋模型中的所有詞語 word = vectorizer.get_feature_names() # 將tf-idf矩陣抽取出來,元素w[i][j]表示j詞在i類文本中的tf-idf權重 weight = tfidf.toarray() #查看特征大小 print ('Features length: ' + str(len(word)))
4. 實戰 TF-IDF
的中文文本 K-means
聚類
第一步,使用 k-means++
來初始化模型,當然也可以選擇隨機初始化,即init="random"
,然后通過 PCA 降維把上面的權重 weight 降到10維,進行聚類模型訓練:
numClass = 4 # 聚類分幾簇 clf = KMeans(n_clusters=numClass, max_iter=10000, init="k-means++", tol=1e-6) # 這里也可以選擇隨機初始化init="random" pca = PCA(n_components=10) # 降維 TnewData = pca.fit_transform(weight) # 載入N維 s = clf.fit(TnewData)
第二步,定義聚類結果可視化函數plot_cluster(result,newData,numClass)
,該函數包含3個參數,其中 result 表示聚類擬合的結果集;newData 表示權重 weight 降維的結果,這里需要降維到2維,即平面可視化;numClass 表示聚類分為幾簇,繪制代碼第一部分繪制結果 newData,第二部分繪制聚類的中心點:
def plot_cluster(result, newData, numClass): plt.figure(2) Lab = [[] for i in range(numClass)] index = 0 for labi in result: Lab[labi].append(index) index += 1 color = ['oy', 'ob', 'og', 'cs', 'ms', 'bs', 'ks', 'ys', 'yv', 'mv', 'bv', 'kv', 'gv', 'y^', 'm^', 'b^', 'k^', 'g^'] * 3 for i in range(numClass): x1 = [] y1 = [] for ind1 in newData[Lab[i]]: # print ind1 try: y1.append(ind1[1]) x1.append(ind1[0]) except: pass plt.plot(x1, y1, color[i]) # 繪制初始中心點 x1 = [] y1 = [] for ind1 in clf.cluster_centers_: try: y1.append(ind1[1]) x1.append(ind1[0]) except: pass plt.plot(x1, y1, "rv") # 繪制中心 plt.show()
第三步,對數據降維到2維,然后獲得結果,最后繪制聚類結果圖:
pca = PCA(n_components=2) # 輸出兩維 newData = pca.fit_transform(weight) # 載入N維 result = list(clf.predict(TnewData)) plot_cluster(result,newData,numClass)
第四步,得到的聚類結果圖,4個中心點和4個簇,我們看到結果還比較好,簇的邊界很清楚:
第五步,上面演示的可視化過程,降維使用了 PCA,我們還可以試試 TSNE,兩者同為降維工具,主要區別在於,所在的包不同(也即機制和原理不同):
from sklearn.decomposition import PCA from sklearn.manifold import TSNE
因為原理不同,導致 TSNE 保留下的屬性信息,更具代表性,也即最能體現樣本間的差異,但是 TSNE 運行極慢,PCA 則相對較快,下面看看 TSNE 運行的可視化結果:
ts = TSNE(2) newData = ts.fit_transform(weight) result = list(clf.predict(TnewData)) plot_cluster(result, newData, numClass)
得到的可視化結果,為一個中心點,不同簇落在圍繞中心點的不同半徑之內,我們看到在這里結果並不是很好:
第六步,為了更好的表達和獲取更具有代表性的信息,在展示(可視化)高維數據時,更為一般的處理,常常先用 PCA 進行降維,再使用 TSNE:
newData = PCA(n_components=4).fit_transform(weight) # 載入N維 newData = TSNE(2).fit_transform(newData) result = list(clf.predict(TnewData)) plot_cluster(result, newData, numClass)
得到的可視化結果,不同簇落在圍繞中心點的不同半徑之內:
總結
上面通過真實小案例,對司法數據一步步實現中文短文本聚類,從優化和提高模型准確率來說,主要有兩方面可以嘗試:
- 特征向量的構建,除了詞袋模型,可以考慮使用 word2vec 和 doc2vec 等;
- 模型上可以采用基於密度的 DBSCAN、層次聚類等算法。
import random import jieba import pandas as pd import numpy as np from sklearn.feature_extraction.text import TfidfTransformer from sklearn.feature_extraction.text import TfidfVectorizer import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.cluster import KMeans import gensim from gensim.models import Word2Vec from sklearn.preprocessing import scale import multiprocessing # 加載停用詞 stopwords = pd.read_csv('./data6/stopwords.txt', index_col=False, quoting=3, sep="\t", names=['stopword'], encoding='utf-8') stopwords = stopwords['stopword'].values # 加載語料 laogong_df = pd.read_csv('./data6/beilaogongda.csv', encoding='utf-8', sep=',') laopo_df = pd.read_csv('./data6/beilaogongda.csv', encoding='utf-8', sep=',') erzi_df = pd.read_csv('./data6/beierzida.csv', encoding='utf-8', sep=',') nver_df = pd.read_csv('./data6/beinverda.csv', encoding='utf-8', sep=',') # 刪除語料的nan行 laogong_df.dropna(inplace=True) laopo_df.dropna(inplace=True) erzi_df.dropna(inplace=True) nver_df.dropna(inplace=True) # 轉換 laogong = laogong_df.segment.values.tolist() laopo = laopo_df.segment.values.tolist() erzi = erzi_df.segment.values.tolist() nver = nver_df.segment.values.tolist() # 定義分詞函數preprocess_text # 參數content_lines即為上面轉換的list # 參數sentences是定義的空list,用來儲存分詞后的數據 def preprocess_text(content_lines, sentences): for line in content_lines: try: segs = jieba.lcut(line) segs = [v for v in segs if not str(v).isdigit()] # 去數字 segs = list(filter(lambda x: x.strip(), segs)) # 去左右空格 segs = list(filter(lambda x: len(x) > 1, segs)) # 長度為1的字符 segs = list(filter(lambda x: x not in stopwords, segs)) # 去掉停用詞 sentences.append(" ".join(segs)) except Exception: print(line) continue sentences = [] preprocess_text(laogong, sentences) preprocess_text(laopo, sentences) preprocess_text(erzi, sentences) preprocess_text(nver, sentences) random.shuffle(sentences) for sentence in sentences[:10]: print(sentence) #將文本中的詞語轉換為詞頻矩陣 矩陣元素a[i][j] 表示j詞在i類文本下的詞頻 vectorizer = TfidfVectorizer(sublinear_tf=True, max_df=0.5) #統計每個詞語的tf-idf權值 transformer = TfidfTransformer() # 第一個fit_transform是計算tf-idf 第二個fit_transform是將文本轉為詞頻矩陣 tfidf = transformer.fit_transform(vectorizer.fit_transform(sentences)) # 獲取詞袋模型中的所有詞語 word = vectorizer.get_feature_names() # 將tf-idf矩陣抽取出來,元素w[i][j]表示j詞在i類文本中的tf-idf權重 weight = tfidf.toarray() #查看特征大小 print ('Features length: ' + str(len(word))) numClass = 4 # 聚類分幾簇 clf = KMeans(n_clusters=numClass, max_iter=10000, init="k-means++", tol=1e-6) # 這里也可以選擇隨機初始化init="random" pca = PCA(n_components=10) # 降維 TnewData = pca.fit_transform(weight) # 載入N維 s = clf.fit(TnewData) def plot_cluster(result, newData, numClass): plt.figure(2) Lab = [[] for i in range(numClass)] index = 0 for labi in result: Lab[labi].append(index) index += 1 color = ['oy', 'ob', 'og', 'cs', 'ms', 'bs', 'ks', 'ys', 'yv', 'mv', 'bv', 'kv', 'gv', 'y^', 'm^', 'b^', 'k^', 'g^'] * 3 for i in range(numClass): x1 = [] y1 = [] for ind1 in newData[Lab[i]]: # print ind1 try: y1.append(ind1[1]) x1.append(ind1[0]) except: pass plt.plot(x1, y1, color[i]) # 繪制初始中心點 x1 = [] y1 = [] for ind1 in clf.cluster_centers_: try: y1.append(ind1[1]) x1.append(ind1[0]) except: pass plt.plot(x1, y1, "rv") # 繪制中心 plt.show() pca = PCA(n_components=2) # 輸出兩維 newData = pca.fit_transform(weight) # 載入N維 result = list(clf.predict(TnewData)) plot_cluster(result,newData,numClass) from sklearn.decomposition import PCA from sklearn.manifold import TSNE from sklearn.manifold import TSNE ts = TSNE(2) newData = ts.fit_transform(weight) result = list(clf.predict(TnewData)) plot_cluster(result, newData, numClass) from sklearn.manifold import TSNE newData = PCA(n_components=4).fit_transform(weight) # 載入N維 newData = TSNE(2).fit_transform(newData) result = list(clf.predict(TnewData)) plot_cluster(result, newData, numClass)