信息就是錢。今天來告訴你一個高效挖掘信息的工具,簡單好用!
無論你的手裡是文本、圖片還是其他的非結構化、結構化數據,都可用這個方法進行主題建模。
今天我們通過一個新聞文本數據集進行 LDA 主題建模。觀察數據中有哪些主題、這些主題的分布如何,以及每個主題都包含哪些關鍵元素。
本文偏重代碼實操,模型原理不了解的同學可自行搜索相關材料。
import os
import time
import jieba
import gensim
import random
import pandas as pd
import numpy as np
from gensim import corpora
from gensim import models
from gensim.models.coherencemodel import CoherenceModel
import pyLDAvis.gensim
pyLDAvis.enable_notebook()
import warnings
warnings.filterwarnings('ignore')
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei']
我們使用 THUCNews中文文本分類 數據集來進行今天的實驗。這是一個14分類的中文新聞文檔分類任務數據集。看一下都有哪些類別:
data_fold = './data/THUCNews/THUCNews/'
os.listdir(data_fold)輸出:
['體育',
'娛樂',
'家居',
'彩票',
'房產',
'教育',
'時尚',
'時政',
'星座',
'遊戲',
'社會',
'科技',
'股票',
'財經']def read_txt(file_path):
with open(file_path, 'rb') as f:
text = ''.join([line.decode().replace('\n', '').replace('\u3000', '') for line in f])
return text
def read_data(sample_num=10):
data_list = []
label2name = {}
for label, label_name in enumerate(os.listdir(data_fold)):
class_fold_path = os.path.join(data_fold, label_name)
file_paths = [os.path.join(class_fold_path, class_file_name) for class_file_name in os.listdir(class_fold_path)][:sample_num]
data_list.extend(list(zip(file_paths, [label] * len(file_paths), [label_name] * len(file_paths))))
label2name[label] = label_name
data = pd.DataFrame(data_list, columns=['file_path', 'label', 'label_name'])
for index, file_path in enumerate(data['file_path'].tolist()):
data.loc[index, 'text'] = read_txt(file_path)
print (data.shape)
return data, label2name
data, label2name = read_data(sample_num=20)為快速實驗,在此我們僅切片每個類別20個樣本來進行建模分析。
來看一下整理好的數據。
輸入:
print (label2name)
data.head(3)輸出:
數據切片其中,file_path,label,label_name,text 分別為每個文件的路徑,標籤索引,標籤名,文本字符串。
2. 構造語料我們已經拿到了新聞語料文本,下面要進行分詞處理,把新聞切割成一個個的單詞或者短語。對於一些比如的 了 是等停用詞,這些詞對於我們分析數據並沒什麼用,甚至會產生負面影響。我們先讀取一個常用的停用詞列表,再用於過濾詞彙。並抽樣看一下分詞後的語料。輸入:def read_stopwords():
with open('./data/stopwords.txt', 'rb') as f:
stopwords = [w.decode().strip() for w in f]
stopwords.extend([' ', '…', '!', '_____'])
stopwords.extend([str(x) for x in range(10)])
return stopwords
stopwords = read_stopwords()
texts = [[w for w in jieba.cut(text) if w not in stopwords] for text in data['text'].tolist()]
print (texts[0])輸出:
['馬曉旭', '意外', '受傷', '國奧', '警惕', '無奈', '大雨', '格外', '青睞', '殷家', '軍', '記者', '傅亞雨', '瀋陽', '報導', '來到', '瀋陽', '國奧隊', '依然', '沒有', '擺脫', '雨水', '困擾', '月', '31', '日', '下午', '點', '國奧隊', '日常', '訓練', '再度', '受到', '大雨', '幹擾', '無奈', '之下', '隊員', '只', '慢跑', '25', '分鐘', '草草收場', '31', '日', '上午', '10', '點', '國奧隊', '奧體中心', '外場', '訓練', '天', '陰沉沉', '氣象預報', '顯示', '當天', '下午', '瀋陽', '大雨', '幸好', '隊伍', '上午', '訓練', '沒有', '受到', '幹擾', '下午', '點', '球隊', '抵達', '訓練場', '時', '大雨', '已經', '下', '幾個', '小時', '絲毫', '沒有', '停下來', '意思', '抱', '試一試', '態度', '球隊', '當天', '下午', '例行', '訓練', '25', '分鐘', '過去', '天氣', '沒有', '轉好', '跡象', '保護', '球員', '國奧隊', '決定', '中止', '當天', '訓練', '全隊', '立即', '返回', '酒店', '雨', '中', '訓練', '足球隊', '來說', '不是', '稀罕', '事', '奧運會', '即將', '之前', '全隊', '變得', '嬌貴', '瀋陽', '最後', '一周', '訓練', '國奧隊', '保證', '現有', '球員', '不再', '出現意外', '傷病', '情況', '影響', '正式', '比賽', '這一', '階段', '控制', '訓練', '受傷', '控制', '感冒', '疾病', '出現', '隊伍', '放在', '相當', '重要', '位置', '抵達', '瀋陽', '之後', '中', '後衛', '馮蕭霆', '沒有', '訓練', '馮蕭霆', '月', '27', '日', '長春', '患上', '感冒', '沒有', '參加', '29', '日', '塞爾維亞', '熱身賽', '隊伍', '介紹', '說', '馮蕭霆', '沒有', '出現', '發燒', '症狀', '安全', '見', '兩天', '靜養', '休息', '感冒', '徹底', '好', '之後', '再', '恢復', '訓練', '馮蕭霆', '例子', '國奧隊', '對雨中', '訓練', '顯得', '特別', '謹慎', '主要', '擔心', '球員', '受涼', '引發', '感冒', '造成', '非戰鬥', '減員', '女足', '隊員', '馬曉旭', '熱身賽', '中', '受傷', '導致', '無緣', '奧運', '前科', '瀋陽', '國奧隊', '現在', '格外', '警惕', '訓練', '中', '不斷', '囑咐', '隊員', '注意', '動作', '不能', '再出', '事情', '一位', '工作人員', '表示', '長春', '瀋陽', '雨水', '一路', '伴隨', '國奧隊', '邪', '走', '雨', '下', '長春', '幾次', '訓練', '都', '大雨', '攪和', '沒想到', '瀋陽', '碰到', '這種', '事情', '一位', '國奧', '球員', '雨水', '青睞', '不解']
接著對語料中的這些詞彙建立一個詞典,用於word2id, id2word的映射。輸入:dictionary = corpora.Dictionary(texts)
dictionary.id2token = {v:k for k, v in dictionary.token2id.items()}
print(dictionary.id2token[100])
print(dictionary.token2id['擔心'])輸出:
擔心
100
建立詞袋,所謂詞袋就是針對每個詞,計算其詞頻。那麼一片文章中所有詞彙詞頻組成的向量就是其詞袋了。如下:輸入:
bow_corpus = [dictionary.doc2bow(text) for text in texts]
print (bow_corpus[0])輸出:
[(0, 1), (1, 2), (2, 1), (3, 1), (4, 2), (5, 2), (6, 1), (7, 1), (8, 2), (9, 2), (10, 4), (11, 1), (12, 1), (13, 1), (14, 1), (15, 1), (16, 1), (17, 1), (18, 4), (19, 1), (20, 1), (21, 1), (22, 1), (23, 2), (24, 1), (25, 2), (26, 1), (27, 1), (28, 1), (29, 1), (30, 1), (31, 1), (32, 1), (33, 1), (34, 1), (35, 1), (36, 1), (37, 1), (38, 2), (39, 1), (40, 1), (41, 1), (42, 1), (43, 4), (44, 1), (45, 1), (46, 1), (47, 1), (48, 2), (49, 1), (50, 2), (51, 1), (52, 1), (53, 1), (54, 1), (55, 1), (56, 3), (57, 1), (58, 2), (59, 1), (60, 1), (61, 1), (62, 1), (63, 1), (64, 2), (65, 8), (66, 1), (67, 1), (68, 5), (69, 1), (70, 1), (71, 1), (72, 1), (73, 1), (74, 1), (75, 1), (76, 1), (77, 1), (78, 1), (79, 1), (80, 1), (81, 1), (82, 1), (83, 2), (84, 1), (85, 1), (86, 3), (87, 1), (88, 1), (89, 1), (90, 1), (91, 1), (92, 1), (93, 1), (94, 1), (95, 4), (96, 1), (97, 1), (98, 1), (99, 2), (100, 1), (101, 2), (102, 1), (103, 1), (104, 1), (105, 2), (106, 1), (107, 4), (108, 1), (109, 1), (110, 1), (111, 1), (112, 1), (113, 2), (114, 1), (115, 1), (116, 2), (117, 1), (118, 1), (119, 1), (120, 1), (121, 8), (122, 1), (123, 7), (124, 1), (125, 3), (126, 2), (127, 1), (128, 1), (129, 1), (130, 4), (131, 2), (132, 1), (133, 1), (134, 1), (135, 1), (136, 1), (137, 1), (138, 1), (139, 1), (140, 1), (141, 2), (142, 13), (143, 1), (144, 1), (145, 1), (146, 1), (147, 1), (148, 1), (149, 1), (150, 1), (151, 1), (152, 1), (153, 1), (154, 1), (155, 1), (156, 1), (157, 1), (158, 1), (159, 1), (160, 1), (161, 3), (162, 3), (163, 3), (164, 1), (165, 1), (166, 2), (167, 3), (168, 2), (169, 1), (170, 1), (171, 2)]
二、主題建模1. LDA 學的是什麼?LDA 本質上是在學習兩個概率分布,第一個是當給定一個文檔時,文檔-->主題的映射分布,第二個是當一個文檔的主題確定後,主題-->詞彙的映射分布。
文章和詞彙都是我們看的見的東西,那麼主題呢?
2. 學習示例主題是一個隱變量,我們需要假設一個主題個數,這裡假設為14個主題。使用gensim 來進行建模。
輸入:
num_topics = 14
lda = models.LdaModel(bow_corpus[:], id2word=dictionary, num_topics=num_topics)
print (lda)輸出:
LdaModel(num_terms=22148, num_topics=14, decay=0.5, chunksize=2000)
三、模型超參我們現在知道如何來進行LDA建模了,但主題的個數如何確定呢?我們怎麼知道這堆亂七八糟的語料裡有5個還是10個明顯有區分的主題?
科學家已經為我們找到了一些方法去評估LDA建模的質量,通過評估不同主題個數的LDA模型的質量,我們來確定最佳的主題個數。那就是困惑度與一致性。
1. 困惑度困惑度是從資訊理論中熵的概念衍生出來的一個評估指標,它表示一篇文章當給定一個主題時,這個主題映射到各個詞彙的確定性程度。這麼描述有些變態,下面用人話舉個例子。
假設一篇文章,就包含四個單詞周杰倫,易烊千璽,爸爸去哪兒,市盈率,,屬於主題A。那麼這時候主題A向兩個詞彙映射的概率分別為:
周杰倫:0.9
楊超越:0.9
爸爸去哪:0.8
市盈率:0.1這種確定性較強的就屬於困惑度較低。
假設另一篇文章,也包含四個單詞大棉襖,川普,肯德基,市盈率,,屬於主題B。那麼這時候主題A向兩個詞彙映射的概率分別為:
大棉襖:0.5
川普:0.5
肯德基:0.5
市盈率:0.5這種確定性較若的就屬於困惑度較高。
對於我們進行LDA建模來說,困惑度越低越好,代表學習到的這個主題比較明確。
2. 一致性一致性指的是不同主題間的區分度。它描述不同主題間分布的距離,可以類比KL散度來理解。
對於我們進行LDA建模來說,一致性越高越好,代表學習到的不同主題間的去分析越強。
3. 指導原則我們知道LAD學習的兩個分布
第一個是文檔-->主題的映射分布,而一致性就是對這個分布學習效果的評估,一致性越高越好。
第二個是主題-->詞彙的映射分布,而困惑度就是對這個分布學習效果的評估,困惑度月底越好。
4. 模型超參一個小循環就能搞定了
輸入:
for num_topics in range(4, 50, 5):
lda = models.LdaModel(bow_corpus[:], id2word=dictionary, num_topics=num_topics)
cm = CoherenceModel(model=lda, texts=texts[:], dictionary=dictionary, coherence='c_v')
print (f'num_topics: {num_topics} \t 困惑度: {round(lda.log_perplexity(bow_corpus), 4)} \t 一致性:{round(cm.get_coherence(), 4)}')輸出:
num_topics: 4 困惑度: -9.3351 一致性:0.2757
num_topics: 9 困惑度: -9.745 一致性:0.2852
num_topics: 14 困惑度: -10.1085 一致性:0.3157
num_topics: 19 困惑度: -10.5542 一致性:0.2949
num_topics: 24 困惑度: -10.967 一致性:0.2854
num_topics: 29 困惑度: -11.3328 一致性:0.2949
num_topics: 34 困惑度: -11.61 一致性:0.3024
num_topics: 39 困惑度: -11.9082 一致性:0.3174
num_topics: 44 困惑度: -12.2759 一致性:0.3006
num_topics: 49 困惑度: -12.6019 一致性:0.3107從上面的輸出看,結合困惑度與一致性兩個指標,當主題數為14時效果最好。與我們的先驗認知是一致的。因為語料本身就是一個14分類的新聞語料。
有些人看到當主題個數更大時會有更低的困惑度,但我們不能僅憑此就認為更大的主題數會更好,因為僅看困惑度一個指標很容易讓模型在第二個分布的建模上過擬合。還是要兩個結合起來一起看。
四、可視化分析1. 關鍵詞分布通過超參數確定主題個數後,我們進行一下可視化分析,看下模型具體學到了哪些主題,每個主題中有哪些關鍵詞、
num_topics = 14
lda = models.LdaModel(bow_corpus[:], id2word=dictionary, num_topics=num_topics)
print (lda)
cm = CoherenceModel(model=lda, texts=texts[:], dictionary=dictionary, coherence='c_v')
print (f'num_topics: {num_topics} \t 困惑度: {round(lda.log_perplexity(bow_corpus), 4)} \t 一致性:{round(cm.get_coherence(), 4)}')輸出:
LdaModel(num_terms=41692, num_topics=14, decay=0.5, chunksize=2000)
num_topics: 14 困惑度: -10.1291 一致性:0.3041def show_topic_terms(topic_terms):
topic_terms = [(dictionary.id2token[wid], p) for wid, p in topic_terms]
return topic_terms
# lda.show_topics(formatted=False)
for i in range(num_topics):
print (f'======== topic: {i} ========')
topic_terms = sorted(show_topic_terms(lda.get_topic_terms(i)), key=lambda x: x[1], reverse=True)
for item in topic_terms:
print (item)輸出:
======== topic: 0 ========
('\xa0', 0.0066339383)
('都', 0.0063307784)
('2009', 0.0047544)
('月', 0.004520685)
('說', 0.004249678)
('沒有', 0.0040172637)
('人', 0.0039240792)
('上', 0.003825816)
('不', 0.0037667216)
('中', 0.003666976)
======== topic: 1 ========
('\xa0', 0.011982462)
('2009', 0.007944166)
('不', 0.005682676)
('網友', 0.0055214283)
('都', 0.005116565)
('專家', 0.0049703317)
('21', 0.004129636)
('月', 0.004082068)
('說', 0.004029127)
('很', 0.003772703)
2. TNSE 可視化分析def get_tsne_data():
t0 = time.time()
X = np.zeros((len(bow_corpus), num_topics), dtype=np.float32)
for i, bow_corpu in enumerate(bow_corpus):
for topic_id, topic_prob in lda.get_document_topics(bow_corpu):
X[i, topic_id] = topic_prob
label = np.argmax(X, axis=1)
tsne_data = TSNE(n_components=2, init='pca', random_state=100).fit_transform(X)
run_time = round(time.time() - t0, 2)
return tsne_data, label, run_time
tsne_data, label, run_time = get_tsne_data()def plot_embedding(data, label, title):
x_min, x_max = np.min(data, 0), np.max(data, 0)
data = (data - x_min) / (x_max - x_min)
fig = plt.figure()
ax = plt.subplot(111)
for i in range(data.shape[0]):
plt.text(data[i, 0], data[i, 1], str(label2name[label[i]]),
color=plt.cm.Set1((label[i]+1) / 10.),
fontdict={'weight': 'bold', 'size': 20})
plt.xticks([])
plt.yticks([])
plt.title(title, fontsize=30)
plt.rcParams['figure.figsize'] = (18.0, 10.0)
return fig
fig = plot_embedding(tsne_data, label, f'num_topics: {num_topics} run time: {run_time} s')
plt.show(fig)輸出:
TSNE 可視化
可以看到,降維後各個主體間的聚類效果還是很明顯的。體育、彩票、娛樂、社會等主題間的距離還是比較明顯。而予以上比較接近的星座、娛樂、時尚等主題則都分布在右側,且有一定重疊。
3. pyLDAvis 可視化分析pyLDAvis 是一個針對 LDA 結果的交互可視化工具。做下圖左側不同的圓代表不同的主題,將滑鼠點至不同的圓,則右側顯示這個主題下的關鍵詞彙及其概率分布。蠻有意思的。
但這傢伙有個缺點,就是運行起來超級慢,因此在實踐當中,一般是
再用 lda.get_topic_terms 查看每個主題下的關鍵詞。最後再抽樣一小部分數據,使用 pyLDAvis 進行可視化。下面看看效果。輸入:
sample_bow_corpus = random.choices(bow_corpus, k=1000)
pyLDAvis.gensim.prepare(lda, sample_bow_corpus, dictionary)輸出:
五、結尾信息就是錢。網際網路的數據複雜多樣,使用高效的數據處理、挖掘工具來進行信息挖掘至關重要。
那麼在沒有對其人工標註的時候咋對這些數據進行數據挖掘、獲取有效信息來助力業務發展呢?通常用的方法有關鍵詞提取、主題建模、聚類分析等。
今天針對主題建模中的LDA方法簡要介紹了其建模方法、評估方式、可視化分析。希望對您日常數據挖掘能有所幫助。
我的二維碼
歡迎掃碼持續關注----