文本聚类(一)—— LDA 主题模型

2023-11-16

Update log
2021.07.08:主要上传停用词表,增加模型保存、加载与预测部分代码
2021.08.04:分享项目代码,https://github.com/dfsj66011/text_cluster

文本聚类

因工作需要,近期需要做一些文本聚类方面的事情,算法方面主要选择的是传统的机器学习算法,主要尝试的是 LDA 主题模型和 K-Means 聚类算法,使用的数据集是 THUCNews 新闻文本分类数据集,其中只使用了训练集 cnews.train.txt 部分,下面我们首先尝试 LDA 主题模型算法:

下面首先导入一些需要用到的算法包:

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning) 
import pandas as pd
import re
import jieba
from pprint import pprint

import os
import sys
sys.stderr = open(os.devnull, "w")  # silence stderr
import gensim
import gensim.corpora as corpora
from gensim.models import CoherenceModel
sys.stderr = sys.__stderr__  # unsilence stderr

# Plotting tools
import pyLDAvis
import pyLDAvis.gensim_models  # 3.3.x 版本之前请使用 import pyLDAvis.gensim
import matplotlib.pyplot as plt
%matplotlib inline

上面中间一部分比较晦涩的内容主要是为了忽略 gensim 包内的 DeprecationWarning 信息,尽管上面已经引入了 warnings,但 gensim 内依赖于 scikit-learn 包,据说 scikit-learn 内部又给重置了,导致上面 warnings 过滤机制失效,具体详见这里

一、LDA 主题模型

1.1 加载数据集

原始的数据集含有标签信息,可以做监督学习的文本分类任务,我们此次主要关注的是无监督学习方式,因此下面的过程中,我们会忽略标签列的内容,整个数据的真实标签有 “体育”、“娱乐”、“家居”、“房产”、“教育”、“时尚”、“时政”、“游戏”、“科技”、“财经” 共 10 个种类。

df = pd.read_csv('/content/drive/My Drive/cnews.train.txt', delimiter="\t", header=None, names=["label", "data"])
print(df.label.unique())
df.head()
['体育' '娱乐' '家居' '房产' '教育' '时尚' '时政' '游戏' '科技' '财经']
label data
0 体育 马晓旭意外受伤让国奥警惕 无奈大雨格外青睐殷家军记者傅亚雨沈阳报道 来到沈阳,国奥队依然没有...
1 体育 商瑞华首战复仇心切 中国玫瑰要用美国方式攻克瑞典多曼来了,瑞典来了,商瑞华首战求3分的信心也...
2 体育 冠军球队迎新欢乐派对 黄旭获大奖张军赢下PK赛新浪体育讯12月27日晚,“冠军高尔夫球队迎新...
3 体育 辽足签约危机引注册难关 高层威逼利诱合同笑里藏刀新浪体育讯2月24日,辽足爆发了集体拒签风波...
4 体育 揭秘谢亚龙被带走:总局电话骗局 复制南杨轨迹体坛周报特约记者张锐北京报道 谢亚龙已经被公安...

1.2 数据清洗、分词

数据清洗过程中,主要是去掉标点符号以及文本中一些其他乱七八糟的字符,只保留文本中的汉字、数字和英文字母三部分。

分词采用的是 jieba 分词,分词后去停用词,一开始使用的百度停用词表,但是后续的 LDA 效果并不理想,主要是存在一些数字或者单字的干扰,因此把分词后得到的词典进行过滤,把这两项重新加入到停用词表中,或者可以更进一步,根据分词词性过滤掉更多的词汇。

鉴于有网友评论停用词表这块遇到问题,现已将词典表分享出来,可在这里免积分下载。

# 去除文本中的表情字符(只保留中英文和数字)
def clear_character(sentence):    
    pattern = re.compile('[^\u4e00-\u9fa5^a-z^A-Z^0-9]')   
    line = re.sub(pattern, '', sentence)   
    new_sentence = ''.join(line.split())
    return new_sentence
train_text = [clear_character(data) for data in df["data"]]
train_text[:1]
['马晓旭意外受伤让国奥警惕无奈大雨格外青睐殷家军记者傅亚雨沈阳报道来到沈阳国奥队依然没有摆脱雨水的困扰7月31日下午6点国奥队的日常训练再度受到大雨的干扰无奈之下队员们只慢跑了25分钟就草草收场31日上午10点国奥队在奥体中心外场训练的时候天就是阴沉沉的气象预报显示当天下午沈阳就有大雨但幸好队伍上午的训练并没有受到任何干扰下午6点当球队抵达训练场时大雨已经下了几个小时而且丝毫没有停下来的意思抱着试一试的态度球队开始了当天下午的例行训练25分钟过去了天气没有任何转好的迹象为了保护球员们国奥队决定中止当天的训练全队立即返回酒店在雨中训练对足球队来说并不是什么稀罕事但在奥运会即将开始之前全队变得娇贵了在沈阳最后一周的训练国奥队首先要保证现有的球员不再出现意外的伤病情况以免影响正式比赛因此这一阶段控制训练受伤控制感冒等疾病的出现被队伍放在了相当重要的位置而抵达沈阳之后中后卫冯萧霆就一直没有训练冯萧霆是7月27日在长春患上了感冒因此也没有参加29日跟塞尔维亚的热身赛队伍介绍说冯萧霆并没有出现发烧症状但为了安全起见这两天还是让他静养休息等感冒彻底好了之后再恢复训练由于有了冯萧霆这个例子因此国奥队对雨中训练就显得特别谨慎主要是担心球员们受凉而引发感冒造成非战斗减员而女足队员马晓旭在热身赛中受伤导致无缘奥运的前科也让在沈阳的国奥队现在格外警惕训练中不断嘱咐队员们要注意动作我们可不能再出这样的事情了一位工作人员表示从长春到沈阳雨水一路伴随着国奥队也邪了我们走到哪儿雨就下到哪儿在长春几次训练都被大雨给搅和了没想到来沈阳又碰到这种事情一位国奥球员也对雨水的青睐有些不解']

分词的过程较为缓慢,请耐心等待

train_seg_text = [jieba.lcut(s) for s in train_text]
train_seg_text[:1]
[['马晓旭', '意外', '受伤', '让', '国奥', '警惕', '无奈', '大雨', '格外', '青睐', '殷家', '军', '记者', '傅亚雨', '沈阳', '报道', '来到', '沈阳', '国奥队', '依然', '没有', '摆脱', '雨水', '的', '困扰', '7', '月', '31', '日', '下午', '6', '点', '国奥队', '的', '日常', '训练', '再度', '受到', '大雨', '的', '干扰', '无奈', '之下', '队员', '们', '只', '慢跑', '了', '25', '分钟', '就', '草草收场', '31', '日', '上午', '10', '点', '国奥队', '在', '奥体中心', '外场', '训练', '的', '时候', '天', '就是', '阴沉沉', '的', '气象预报', '显示', '当天', '下午', '沈阳', '就', '有', '大雨', '但', '幸好', '队伍', '上午', '的', '训练', '并', '没有', '受到', '任何', '干扰', '下午', '6', '点当', '球队', '抵达', '训练场', '时', '大雨', '已经', '下', '了', '几个', '小时', '而且', '丝毫', '没有', '停下来', '的', '意思', '抱', '着', '试一试', '的', '态度', '球队', '开始', '了', '当天', '下午', '的', '例行', '训练', '25', '分钟', '过去', '了', '天气', '没有', '任何', '转好', '的', '迹象', '为了', '保护', '球员', '们', '国奥队', '决定', '中止', '当天', '的', '训练', '全队', '立即', '返回', '酒店', '在', '雨', '中', '训练', '对', '足球队', '来说', '并', '不是', '什么', '稀罕', '事', '但', '在', '奥运会', '即将', '开始', '之前', '全队', '变得', '娇贵', '了', '在', '沈阳', '最后', '一周', '的', '训练', '国奥队', '首先', '要', '保证', '现有', '的', '球员', '不再', '出现意外', '的', '伤病', '情况', '以免', '影响', '正式', '比赛', '因此', '这一', '阶段', '控制', '训练', '受伤', '控制', '感冒', '等', '疾病', '的', '出现', '被', '队伍', '放在', '了', '相当', '重要', '的', '位置', '而', '抵达', '沈阳', '之后', '中', '后卫', '冯萧霆', '就', '一直', '没有', '训练', '冯萧霆', '是', '7', '月', '27', '日', '在', '长春', '患上', '了', '感冒', '因此', '也', '没有', '参加', '29', '日', '跟', '塞尔维亚', '的', '热身赛', '队伍', '介绍', '说', '冯萧霆', '并', '没有', '出现', '发烧', '症状', '但', '为了', '安全', '起见', '这', '两天', '还是', '让', '他', '静养', '休息', '等', '感冒', '彻底', '好', '了', '之后', '再', '恢复', '训练', '由于', '有', '了', '冯萧霆', '这个', '例子', '因此', '国奥队', '对雨中', '训练', '就', '显得', '特别', '谨慎', '主要', '是', '担心', '球员', '们', '受凉', '而', '引发', '感冒', '造成', '非战斗', '减员', '而', '女足', '队员', '马晓旭', '在', '热身赛', '中', '受伤', '导致', '无缘', '奥运', '的', '前科', '也', '让', '在', '沈阳', '的', '国奥队', '现在', '格外', '警惕', '训练', '中', '不断', '嘱咐', '队员', '们', '要', '注意', '动作', '我们', '可', '不能', '再出', '这样', '的', '事情', '了', '一位', '工作人员', '表示', '从', '长春', '到', '沈阳', '雨水', '一路', '伴随', '着', '国奥队', '也', '邪', '了', '我们', '走', '到', '哪儿', '雨', '就', '下', '到', '哪儿', '在', '长春', '几次', '训练', '都', '被', '大雨', '给', '搅和', '了', '没想到', '来', '沈阳', '又', '碰到', '这种', '事情', '一位', '国奥', '球员', '也', '对', '雨水', '的', '青睐', '有些', '不解']]
# 加载停用词
stop_words_path = "/content/drive/My Drive/stopwords.txt"

def get_stop_words():
    return set([item.strip() for item in open(stop_words_path, 'r').readlines()])

stopwords = get_stop_words()
# 去掉文本中的停用词
def drop_stopwords(line):
    line_clean = []
    for word in line:
        if word in stopwords:
            continue
        line_clean.append(word)
    return line_clean
train_st_text = [drop_stopwords(s) for s in train_seg_text]
train_st_text[:1]
[['马晓旭', '意外', '受伤', '国奥', '警惕', '无奈', '大雨', '格外', '青睐', '殷家', '傅亚雨', '沈阳', '报道', '来到', '沈阳', '国奥队', '依然', '摆脱', '雨水', '困扰', '下午', '国奥队', '日常', '训练', '再度', '大雨', '干扰', '无奈', '之下', '队员', '慢跑', '分钟', '草草收场', '上午', '国奥队', '奥体中心', '外场', '训练', '阴沉沉', '气象预报', '显示', '当天', '下午', '沈阳', '大雨', '幸好', '队伍', '上午', '训练', '干扰', '下午', '点当', '球队', '抵达', '训练场', '大雨', '几个', '小时', '丝毫', '停下来', '试一试', '态度', '球队', '当天', '下午', '例行', '训练', '分钟', '天气', '转好', '迹象', '保护', '球员', '国奥队', '中止', '当天', '训练', '全队', '返回', '酒店', '训练', '足球队', '来说', '稀罕', '奥运会', '即将', '全队', '变得', '娇贵', '沈阳', '一周', '训练', '国奥队', '保证', '现有', '球员', '不再', '出现意外', '伤病', '情况', '影响', '正式', '比赛', '这一', '阶段', '控制', '训练', '受伤', '控制', '感冒', '疾病', '队伍', '放在', '位置', '抵达', '沈阳', '后卫', '冯萧霆', '训练', '冯萧霆', '长春', '患上', '感冒', '参加', '塞尔维亚', '热身赛', '队伍', '介绍', '冯萧霆', '发烧', '症状', '两天', '静养', '休息', '感冒', '恢复', '训练', '冯萧霆', '例子', '国奥队', '对雨中', '训练', '显得', '特别', '谨慎', '担心', '球员', '受凉', '引发', '感冒', '非战斗', '减员', '女足', '队员', '马晓旭', '热身赛', '受伤', '导致', '无缘', '奥运', '前科', '沈阳', '国奥队', '格外', '警惕', '训练', '嘱咐', '队员', '动作', '再出', '事情', '工作人员', '长春', '沈阳', '雨水', '一路', '伴随', '国奥队', '长春', '几次', '训练', '大雨', '搅和', '没想到', '沈阳', '碰到', '事情', '国奥', '球员', '雨水', '青睐', '不解']]

可以看到分词结果中很多单个字词以及数字信息被过滤掉了。下面做了一步构建 bigram 或者 trigram 的步骤,可以把一些高频连词组合成一个单词,该步骤较长,请耐心等候

# Build the bigram and trigram models
bigram = gensim.models.Phrases(train_st_text, min_count=5, threshold=100) # higher threshold fewer phrases.
trigram = gensim.models.Phrases(bigram[train_st_text], threshold=100)  

# Faster way to get a sentence clubbed as a trigram/bigram
bigram_mod = gensim.models.phrases.Phraser(bigram)
trigram_mod = gensim.models.phrases.Phraser(trigram)
def make_bigrams(texts):
    return [bigram_mod[doc] for doc in texts]

def make_trigrams(texts):
    return [trigram_mod[bigram_mod[doc]] for doc in texts]
data_words_bigrams = make_bigrams(train_st_text)
print(data_words_bigrams[:1])
[['马晓旭', '意外', '受伤', '国奥', '警惕', '无奈', '大雨', '格外', '青睐', '殷家', '傅亚雨', '沈阳', '报道', '来到', '沈阳', '国奥队', '依然', '摆脱', '雨水', '困扰', '下午', '国奥队', '日常', '训练', '再度', '大雨', '干扰', '无奈_之下', '队员', '慢跑', '分钟', '草草收场', '上午', '国奥队', '奥体中心', '外场', '训练', '阴沉沉', '气象预报', '显示', '当天', '下午', '沈阳', '大雨', '幸好', '队伍', '上午', '训练', '干扰', '下午', '点当', '球队', '抵达', '训练场', '大雨', '几个', '小时', '丝毫', '停下来', '试一试', '态度', '球队', '当天', '下午', '例行_训练', '分钟', '天气', '转好', '迹象', '保护', '球员', '国奥队', '中止', '当天', '训练', '全队', '返回', '酒店', '训练', '足球队', '来说', '稀罕', '奥运会', '即将', '全队', '变得', '娇贵', '沈阳', '一周', '训练', '国奥队', '保证', '现有', '球员', '不再', '出现意外', '伤病', '情况', '影响', '正式', '比赛', '这一', '阶段', '控制', '训练', '受伤', '控制', '感冒', '疾病', '队伍', '放在', '位置', '抵达', '沈阳', '后卫', '冯萧霆', '训练', '冯萧霆', '长春', '患上_感冒', '参加', '塞尔维亚', '热身赛', '队伍', '介绍', '冯萧霆', '发烧_症状', '两天', '静养', '休息', '感冒', '恢复', '训练', '冯萧霆', '例子', '国奥队', '对雨中', '训练', '显得', '特别', '谨慎', '担心', '球员', '受凉', '引发', '感冒', '非战斗', '减员', '女足', '队员', '马晓旭', '热身赛', '受伤', '导致', '无缘', '奥运', '前科', '沈阳', '国奥队', '格外', '警惕', '训练', '嘱咐', '队员', '动作', '再出', '事情', '工作人员', '长春_沈阳', '雨水', '一路', '伴随', '国奥队', '长春', '几次', '训练', '大雨', '搅和', '没想到', '沈阳', '碰到', '事情', '国奥', '球员', '雨水', '青睐', '不解']]

可以看到上面 “无奈_之下”,“患上_感冒”,“发烧_症状” 等连词。

1.3 构建词典、语料向量化表示

LDA 算法的输入主要包括分词词典(id2word)以及向量化表示的文本,幸运的是这些可以很容易实现

id2word = corpora.Dictionary(data_words_bigrams)     # Create Dictionary
id2word.save_as_text("dictionary")                   # save dict
texts = data_words_bigrams                           # Create Corpus
corpus = [id2word.doc2bow(text) for text in texts]   # Term Document Frequency
print(corpus[:1])
[[(0, 1), (1, 1), (2, 2), (3, 4), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 2), (10, 1), (11, 1), (12, 1), (13, 1), (14, 1), (15, 1), (16, 1), (17, 1), (18, 1), (19, 1), (20, 1), (21, 1), (22, 2), (23, 1), (24, 1), (25, 4), (26, 1), (27, 1), (28, 1), (29, 1), (30, 2), (31, 1), (32, 1), (33, 1), (34, 1), (35, 1), (36, 3), (37, 1), (38, 1), (39, 1), (40, 1), (41, 1), (42, 2), (43, 8), (44, 1), (45, 1), (46, 5), (47, 1), (48, 1), (49, 1), (50, 1), (51, 1), (52, 1), (53, 1), (54, 1), (55, 1), (56, 1), (57, 2), (58, 1), (59, 1), (60, 3), (61, 1), (62, 1), (63, 1), (64, 1), (65, 1), (66, 1), (67, 3), (68, 1), (69, 1), (70, 2), (71, 1), (72, 2), (73, 1), (74, 1), (75, 1), (76, 1), (77, 1), (78, 1), (79, 1), (80, 1), (81, 1), (82, 1), (83, 1), (84, 2), (85, 1), (86, 1), (87, 1), (88, 1), (89, 7), (90, 1), (91, 1), (92, 2), (93, 1), (94, 1), (95, 4), (96, 2), (97, 1), (98, 1), (99, 1), (100, 1), (101, 2), (102, 12), (103, 1), (104, 1), (105, 1), (106, 1), (107, 1), (108, 1), (109, 1), (110, 1), (111, 1), (112, 2), (113, 1), (114, 3), (115, 3), (116, 1), (117, 1), (118, 3), (119, 2), (120, 1), (121, 1), (122, 2)]]

上面的输出内容表示第几个单词出现了多少次,例如 (0,1)表示第 0 个单词在该段文本中共出现了 1 次,而每个序号的单词表示可以详见 id2word,例如:

id2word[100]
'草草收场'

下面我们为了方便人类可读,我们将上面的单词序号以单词内容形式输出

print([[(id2word[id], freq) for id, freq in cp] for cp in corpus[:1]])
[[('一周', 1), ('一路', 1), ('上午', 2), ('下午', 4), ('不再', 1), ('不解', 1), ('丝毫', 1), ('两天', 1), ('中止', 1), ('事情', 2), ('介绍', 1), ('休息', 1), ('伤病', 1), ('伴随', 1), ('位置', 1), ('例子', 1), ('例行_训练', 1), ('依然', 1), ('保护', 1), ('保证', 1), ('停下来', 1), ('傅亚雨', 1), ('全队', 2), ('再出', 1), ('再度', 1), ('冯萧霆', 4), ('减员', 1), ('几个', 1), ('几次', 1), ('出现意外', 1), ('分钟', 2), ('前科', 1), ('动作', 1), ('即将', 1), ('参加', 1), ('发烧_症状', 1), ('受伤', 3), ('受凉', 1), ('变得', 1), ('后卫', 1), ('嘱咐', 1), ('困扰', 1), ('国奥', 2), ('国奥队', 8), ('塞尔维亚', 1), ('外场', 1), ('大雨', 5), ('天气', 1), ('奥体中心', 1), ('奥运', 1), ('奥运会', 1), ('女足', 1), ('娇贵', 1), ('对雨中', 1), ('导致', 1), ('小时', 1), ('工作人员', 1), ('干扰', 2), ('幸好', 1), ('引发', 1), ('当天', 3), ('影响', 1), ('态度', 1), ('恢复', 1), ('患上_感冒', 1), ('情况', 1), ('意外', 1), ('感冒', 3), ('慢跑', 1), ('报道', 1), ('抵达', 2), ('担心', 1), ('控制', 2), ('搅和', 1), ('摆脱', 1), ('放在', 1), ('无奈', 1), ('无奈_之下', 1), ('无缘', 1), ('日常', 1), ('显得', 1), ('显示', 1), ('来到', 1), ('来说', 1), ('格外', 2), ('正式', 1), ('殷家', 1), ('比赛', 1), ('气象预报', 1), ('沈阳', 7), ('没想到', 1), ('点当', 1), ('热身赛', 2), ('特别', 1), ('现有', 1), ('球员', 4), ('球队', 2), ('疾病', 1), ('碰到', 1), ('稀罕', 1), ('草草收场', 1), ('警惕', 2), ('训练', 12), ('训练场', 1), ('试一试', 1), ('谨慎', 1), ('足球队', 1), ('转好', 1), ('返回', 1), ('这一', 1), ('迹象', 1), ('酒店', 1), ('长春', 2), ('长春_沈阳', 1), ('队伍', 3), ('队员', 3), ('阴沉沉', 1), ('阶段', 1), ('雨水', 3), ('青睐', 2), ('静养', 1), ('非战斗', 1), ('马晓旭', 2)]]

1.4 构建 LDA 模型

我们首先用 gensim 封装的 LDA 算法热身,因为已知数据包含 10 个类别,因此此处我们不妨直接设定主题数 num_topics=10,看一下效果如何,构建过程耗时很长,请耐心等候

# Build LDA model
lda_model = gensim.models.ldamodel.LdaModel(corpus=corpus,
                                           id2word=id2word,
                                           num_topics=10, 
                                           random_state=100,
                                           update_every=1,
                                           chunksize=100,
                                           passes=10,
                                           alpha='auto',
                                           per_word_topics=True)
# Print the Keyword in the 10 topics
pprint(lda_model.print_topics())
doc_lda = lda_model[corpus]
[(0,
  '0.103*"基金" + 0.023*"市场" + 0.020*"公司" + 0.020*"投资" + 0.013*"银行" + '
  '0.010*"投资者" + 0.009*"债券" + 0.009*"亿元" + 0.008*"收益" + 0.007*"2008"'),
 (1,
  '0.017*"反弹" + 0.014*"发现" + 0.012*"时间" + 0.006*"发生" + 0.006*"两只" + 0.005*"加大" '
  '+ 0.005*"一只" + 0.004*"两个" + 0.004*"恢复" + 0.004*"告诉"'),
 (2,
  '0.015*"呈现" + 0.009*"暂停" + 0.008*"新浪_科技" + 0.007*"高位" + 0.007*"提取" + '
  '0.007*"开户" + 0.006*"缩小" + 0.005*"颜色" + 0.005*"予以" + 0.004*"风险_准备金"'),
 (3,
  '0.022*"发行" + 0.022*"股票" + 0.016*"中国" + 0.014*"机构" + 0.012*"成立" + 0.012*"业务" '
  '+ 0.012*"美国" + 0.011*"企业" + 0.011*"股市" + 0.010*"政策"'),
 (4,
  '0.031*"创新" + 0.021*"拍卖" + 0.012*"全年" + 0.009*"大涨" + 0.006*"巴菲特" + '
  '0.006*"收于" + 0.006*"比重" + 0.005*"价值" + 0.005*"中海" + 0.005*"拍卖会"'),
 (5,
  '0.015*"下跌" + 0.010*"指数" + 0.009*"净值" + 0.009*"仓位" + 0.008*"涨幅" + '
  '0.008*"QDII" + 0.008*"平均" + 0.008*"12" + 0.008*"跌幅" + 0.008*"股票_型基金"'),
 (6,
  '0.015*"变更" + 0.011*"翡翠" + 0.009*"以内" + 0.007*"放缓" + 0.007*"节后" + '
  '0.007*"这部分" + 0.006*"超出" + 0.006*"解禁" + 0.005*"权证" + 0.004*"03"'),
 (7,
  '0.014*"配置" + 0.012*"精选" + 0.008*"选择" + 0.007*"发布" + 0.006*"收藏" + 0.005*"功能" '
  '+ 0.005*"表现" + 0.005*"风格" + 0.005*"英国" + 0.005*"作品"'),
 (8,
  '0.017*"波动" + 0.011*"回暖" + 0.009*"放大" + 0.006*"邮票" + 0.006*"金牛" + 0.005*"创出" '
  '+ 0.005*"面值" + 0.005*"设计" + 0.005*"太空" + 0.004*"高点"'),
 (9,
  '0.042*"报告" + 0.016*"设计" + 0.009*"采用" + 0.008*"资料" + 0.007*"萎缩" + 0.007*"悲观" '
  '+ 0.006*"引导" + 0.006*"测试" + 0.006*"机身" + 0.005*"质量"')]

我们的打印结果,前面的序号是主题序号 0~9,后面的分数及单词加权公式是每个主题抽取了最能代表该主题的 10 个关键词以及每个关键词的权重占比的意思。但是结合开篇一开始我们给出的数据真实的十个标签,再看看上面的输出结果,有没有一种很水的感觉,貌似除了主题 0 中包含“基金”、“投资”、“银行” 等字眼,我们可以猜出大致是“财经类”,其他几个主题都乱七八糟,看不出到底是个啥。

模型困惑和主题一致性提供了一种方便的方法来判断给定主题模型的好坏程度。特别是主题一致性得分更有帮助。

# Compute Perplexity
print('\nPerplexity: ', lda_model.log_perplexity(corpus))  # a measure of how good the model is. lower the better.

# Compute Coherence Score
coherence_model_lda = CoherenceModel(model=lda_model, texts=data_words_bigrams, dictionary=id2word, coherence='c_v')
coherence_lda = coherence_model_lda.get_coherence()
print('\nCoherence Score: ', coherence_lda)   # 越高越好
Perplexity:  -11.488651193563687

Coherence Score:  0.41585289864166086

我们主要关注的是一致性得分,该分值越高相对越好一些,上面计算结果为 0.416,我们可以通过下面专用的 LDA 可视化工具展示一下:

# Visualize the topics
pyLDAvis.enable_notebook()
# vis = pyLDAvis.gensim.prepare(lda_model, corpus, id2word)
vis = pyLDAvis.gensim_models.prepare(lda_model, corpus, id2word)   # 根据版本信息选择
vis

在这里插入图片描述

左侧一些圆圈表示的是主题,圈越大代表主题占比约大,右侧同时展示出一些对该主题贡献度较大的词汇,理想状态先,左侧主题之间分散均匀,重叠度较小,而图中的 3、4以及6~10 重叠度太高,密集分布,这种结果往往不尽人意。

gensim 封装的 LDA 不够理想,我们下载 mallet 算法包,gensim 可以调用它

! wget http://mallet.cs.umass.edu/dist/mallet-2.0.8.zip
--2020-09-12 06:32:27--  http://mallet.cs.umass.edu/dist/mallet-2.0.8.zip
Resolving mallet.cs.umass.edu (mallet.cs.umass.edu)... 128.119.246.70
Connecting to mallet.cs.umass.edu (mallet.cs.umass.edu)|128.119.246.70|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16184794 (15M) [application/zip]
Saving to: ‘mallet-2.0.8.zip’

mallet-2.0.8.zip    100%[===================>]  15.43M  17.6MB/s    in 0.9s    

2020-09-12 06:32:28 (17.6 MB/s) - ‘mallet-2.0.8.zip’ saved [16184794/16184794]

接下来,我们将使用 Mallet 版本的 LDA 算法对该模型进行改进,然后我们将重点讨论如何在给定大量文本的情况下获得最佳主题数。Mallet 是一个用 Java 开发的用于 NLP 的机器学习算法包,其中的主题模型模块包括了 Gibbs 采样的极快且高度可扩展的实现,用于文档主题超参数优化的有效方法,以及用于在经过训练的模型下推断新文档主题的工具,且可以利用 python gensim 实现简介调用:

mallet_path = '/content/mallet-2.0.8/bin/mallet' # update this path
ldamallet = gensim.models.wrappers.LdaMallet(mallet_path, corpus=corpus, num_topics=10, id2word=id2word)
# Show Topics
pprint(ldamallet.show_topics(formatted=False))

# Compute Coherence Score
coherence_model_ldamallet = CoherenceModel(model=ldamallet, texts=data_words_bigrams, dictionary=id2word, coherence='c_v')
coherence_ldamallet = coherence_model_ldamallet.get_coherence()
print('\nCoherence Score: ', coherence_ldamallet)
[(0,
  [('游戏', 0.02948806382787908),
   ('玩家', 0.016620182710182587),
   ('活动', 0.009368021178526073),
   ('系统', 0.004566093764359003),
   ('功能', 0.004484160947054792),
   ('支持', 0.004013600847672499),
   ('模式', 0.00383866321072567),
   ('体验', 0.0033636743104080145),
   ('系列', 0.0033559239087711293),
   ('采用', 0.0032241670809440874)]),
 (1,
  [('市场', 0.013003899365832674),
   ('项目', 0.012691672564284029),
   ('房地产', 0.011957592661976326),
   ('北京', 0.0102563035122046),
   ('企业', 0.007477484978421659),
   ('城市', 0.007022327685497412),
   ('中国', 0.006902987663572152),
   ('价格', 0.006900212314225053),
   ('开发商', 0.0068780095194482605),
   ('土地', 0.005909412597310686)]),
 (2,
  [('设计', 0.013374211000901713),
   ('搭配', 0.010714758040276525),
   ('时尚', 0.01000541027953111),
   ('风格', 0.006482717162608957),
   ('组图', 0.006374511571986775),
   ('品牌', 0.005079651337541328),
   ('空间', 0.005064021641118124),
   ('选择', 0.0045482416591523895),
   ('黑色', 0.004244063721070033),
   ('色彩', 0.004193567778779681)]),
 (3,
  [('发现', 0.010489961943350676),
   ('研究', 0.007555596426743505),
   ('美国', 0.006713092620614126),
   ('时间', 0.006089807004392724),
   ('科学家', 0.005484170310240183),
   ('地球', 0.004538559643603241),
   ('发生', 0.0038316738702135426),
   ('消息', 0.003789873791629395),
   ('人类', 0.003670975790323375),
   ('世界', 0.0035483622264765413)]),
 (4,
  [('比赛', 0.017430487449306864),
   ('球队', 0.009575105362207438),
   ('火箭', 0.009304741961990087),
   ('时间', 0.008927293450314098),
   ('球员', 0.008097118774352586),
   ('表现', 0.0064516129032258064),
   ('热火', 0.005637341956688843),
   ('篮板', 0.005478304662443343),
   ('防守', 0.00535637607018846),
   ('12', 0.005023458000901211)]),
 (5,
  [('中国', 0.012213800245407315),
   ('发展', 0.011093803030171176),
   ('工作', 0.007728589952223895),
   ('国家', 0.005970707765139978),
   ('合作', 0.005271906083838797),
   ('经济', 0.004225879158653218),
   ('国际', 0.00408316001079096),
   ('台湾', 0.0039891742304914235),
   ('部门', 0.003734194289493608),
   ('相关', 0.0035949560964572583)]),
 (6,
  [('学生', 0.024595562866363236),
   ('美国', 0.018474116720452217),
   ('中国', 0.016831798760725165),
   ('留学', 0.014698092297398081),
   ('大学', 0.012958193641920667),
   ('申请', 0.01201200939562699),
   ('学校', 0.010808804566776561),
   ('专业', 0.008572289870426776),
   ('教育', 0.00831875431271825),
   ('学习', 0.007523297391110398)]),
 (7,
  [('基金', 0.03416697695741854),
   ('银行', 0.015088816164625042),
   ('投资', 0.014667374607919626),
   ('公司', 0.013569209691343152),
   ('市场', 0.013051093368941872),
   ('亿元', 0.007622201057833413),
   ('投资者', 0.0054772296939563925),
   ('风险', 0.004936455223345143),
   ('发行', 0.004902468001030191),
   ('产品', 0.004812590679797315)]),
 (8,
  [('生活', 0.006468513289767319),
   ('中国', 0.005110341462634277),
   ('东西', 0.004601626926676318),
   ('事情', 0.00408411386887003),
   ('喜欢', 0.003992129322273858),
   ('希望', 0.0038969453131873846),
   ('朋友', 0.003785763991649403),
   ('一点', 0.0034578190863934857),
   ('孩子', 0.003381031986626247),
   ('告诉', 0.003282648515049472)]),
 (9,
  [('电影', 0.023434155295774674),
   ('导演', 0.01065013954857647),
   ('影片', 0.009593784243758317),
   ('观众', 0.007024089326482096),
   ('拍摄', 0.0057483815539967845),
   ('香港', 0.005732988398371019),
   ('票房', 0.00556654990316743),
   ('角色', 0.0051086035233009084),
   ('演员', 0.0049344684502844365),
   ('上映', 0.004146531296690568)])]

Coherence Score:  0.6824643745400414

可以看到,我们什么都没做,仅仅换了一个实现方式,Coherence Score 就从 0.41 提高到了 0.68。再看看各个主题的关键词,是不是感觉相当清晰,比如主题 0 游戏相关,1 是房地产相关,2 是时尚设计类,3 貌似是科技类,4 体育类,5 国际关系,6 留学教育类,7 投资财经类,8 生活?不太好判定,9 电影类。除了主题 5、8 其他感觉都很清晰。但 5、8 也没有太差。作为演示,能做到这一步,还是比较理想的。但是上面的示例是因为我们事先对数据有一定的了解,知道该数据就包含 10 个不同的主题,如果换一批数据,没有数据类标签的先验知识怎么办?接下来,我们以超参搜索的形式探索最佳主题数的判定:

def compute_coherence_values(dictionary, corpus, texts, limit, start=2, step=3):
    """
    Compute c_v coherence for various number of topics

    Parameters:
    ----------
    dictionary : Gensim dictionary
    corpus : Gensim corpus
    texts : List of input texts
    limit : Max num of topics

    Returns:
    -------
    model_list : List of LDA topic models
    coherence_values : Coherence values corresponding to the LDA model with respective number of topics
    """
    coherence_values = []
    model_list = []
    for num_topics in range(start, limit, step):
        model = gensim.models.wrappers.LdaMallet(mallet_path, corpus=corpus, num_topics=num_topics, id2word=id2word)
        model_list.append(model)
        coherencemodel = CoherenceModel(model=model, texts=texts, dictionary=dictionary, coherence='c_v')
        coherence_values.append(coherencemodel.get_coherence())

    return model_list, coherence_values
model_list, coherence_values = compute_coherence_values(
    dictionary=id2word, corpus=corpus, texts=data_words_bigrams, start=5, limit=30, step=5)

上面的超参搜索类似于暴力搜索,从 5 个主题到 30 个主题,步长为 5 逐次计算,因此耗时是相当长的,依据电脑计算性能的差异,大致需要 2~4 个小时不等。对于暴力搜搜可以一开始设置区间较大,步伐较大,目的是锁定大致区间范围,而后在小区间范围内精细化搜索。对于超参搜索超出本次教程内容,此处不做过多展开。计算后,我们可以把每次计算的一致性得分绘制出来:

# Show graph
limit=30; start=5; step=5;
x = range(start, limit, step)
plt.plot(x, coherence_values)
plt.xlabel("Num Topics")
plt.ylabel("Coherence score")
plt.legend(("coherence_values"), loc='best')
plt.show()

在这里插入图片描述

上图的拐点还是很清楚的,直接选择 num_top = 10 就很好,实际上我们并不需要选择最高得分的主题数,如果对于最高点曲线较为平滑的时候,我们通常选择具有最小主题数的超参设置,我曾试想把这个择优过程自动化实现,但还没有找到理想的方案,暂时先人眼选定吧。

# Print the coherence scores
for m, cv in zip(x, coherence_values):
    print("Num Topics =", m, " has Coherence Value of", round(cv, 4))
Num Topics = 5  has Coherence Value of 0.5038
Num Topics = 10  has Coherence Value of 0.6752
Num Topics = 15  has Coherence Value of 0.6633
Num Topics = 20  has Coherence Value of 0.6322
Num Topics = 25  has Coherence Value of 0.646

根据上图,我们将最佳主题数量定为 10,最优模型即上图中的第二个模型:

# Select the model and print the topics
optimal_model = model_list[1]
model_topics = optimal_model.show_topics(formatted=False)
model_topics
[(0,
  [('中国', 0.02944316527964891),
   ('发展', 0.018122071431483873),
   ('企业', 0.007161288765697044),
   ('国际', 0.0070514479433554204),
   ('国家', 0.006909986278218482),
   ('合作', 0.006303365373013608),
   ('经济', 0.0059497112101712605),
   ('工作', 0.005442945598004225),
   ('北京', 0.004560890509503312),
   ('城市', 0.004535094558801869)]),
 (1,
  [('市场', 0.015320171408055916),
   ('房地产', 0.01419122999443352),
   ('项目', 0.013628817897056352),
   ('北京', 0.0096861691090009),
   ('价格', 0.008805084271235792),
   ('开发商', 0.008162798127818236),
   ('土地', 0.007013270619855535),
   ('销售', 0.0067522389436204516),
   ('上海', 0.006614723833427206),
   ('房价', 0.006520027799461797)]),
 (2,
  [('电影', 0.017020782992353996),
   ('导演', 0.007735449040371078),
   ('影片', 0.006968193119293621),
   ('观众', 0.005100365180277191),
   ('票房', 0.004043117267171369),
   ('拍摄', 0.004028442973598849),
   ('角色', 0.0037098011703098513),
   ('香港', 0.0035986958046893454),
   ('演员', 0.0035840215111168257),
   ('故事', 0.0032618858284057988)]),
 (3,
  [('比赛', 0.016559063311873103),
   ('球队', 0.009097511909570873),
   ('火箭', 0.008950436642291795),
   ('时间', 0.008858766441453466),
   ('球员', 0.007693245316508997),
   ('表现', 0.006064336363150997),
   ('热火', 0.005356158877553796),
   ('篮板', 0.005205054150897209),
   ('防守', 0.005089207193793827),
   ('12', 0.004776924092036882)]),
 (4,
  [('基金', 0.03797029557539208),
   ('投资', 0.015835924811462097),
   ('公司', 0.015292868503993185),
   ('市场', 0.014409877413641877),
   ('银行', 0.010845178591662785),
   ('亿元', 0.00813157574460406),
   ('投资者', 0.006086930976452172),
   ('发行', 0.005666419059849505),
   ('产品', 0.005482602473550136),
   ('风险', 0.005261854700962309)]),
 (5,
  [('游戏', 0.028930297026261362),
   ('玩家', 0.016305811912334673),
   ('活动', 0.007574473816848289),
   ('功能', 0.0043678415628204455),
   ('系统', 0.004298321080348297),
   ('支持', 0.0037367259328779743),
   ('模式', 0.0035292507430001565),
   ('系列', 0.003340241931279003),
   ('时间', 0.0033293793558927297),
   ('采用', 0.003305481690042929)]),
 (6,
  [('发现', 0.010362251112182284),
   ('研究', 0.006716306868755808),
   ('美国', 0.006402677256418047),
   ('时间', 0.006045388976453722),
   ('科学家', 0.005260423952392451),
   ('地球', 0.004353392857620175),
   ('消息', 0.0037662283277037413),
   ('发生', 0.0036094135215348606),
   ('人类', 0.0035212051930648652),
   ('世界', 0.003083727523582363)]),
 (7,
  [('设计', 0.011702797066504808),
   ('搭配', 0.009925326175931752),
   ('时尚', 0.009268241072273793),
   ('风格', 0.006022908882342787),
   ('组图', 0.005907083711528502),
   ('空间', 0.004529432400977832),
   ('选择', 0.004384650937459976),
   ('感觉', 0.0041541142993969295),
   ('品牌', 0.004039402832148167),
   ('黑色', 0.003931373586292536)]),
 (8,
  [('银行', 0.007271089973170018),
   ('相关', 0.005254719974942198),
   ('台湾', 0.00495180082344553),
   ('工作', 0.004235716434703407),
   ('情况', 0.004112075964704767),
   ('部门', 0.004096620905954937),
   ('陈水扁', 0.004009042239705901),
   ('调查', 0.0038915837932071927),
   ('报道', 0.003332110666463347),
   ('公司', 0.003312534258713562)]),
 (9,
  [('学生', 0.024257136473782803),
   ('美国', 0.01788887552202382),
   ('留学', 0.014496365167477272),
   ('大学', 0.012781205423891934),
   ('中国', 0.012051661023940055),
   ('申请', 0.010907934762060255),
   ('学校', 0.010660456803065977),
   ('专业', 0.008057641741282416),
   ('教育', 0.007961400312784642),
   ('学习', 0.007442384037671645)])]
pprint(optimal_model.print_topics(num_words=10))
[(0,
  '0.029*"中国" + 0.018*"发展" + 0.007*"企业" + 0.007*"国际" + 0.007*"国家" + 0.006*"合作" '
  '+ 0.006*"经济" + 0.005*"工作" + 0.005*"北京" + 0.005*"城市"'),
 (1,
  '0.015*"市场" + 0.014*"房地产" + 0.014*"项目" + 0.010*"北京" + 0.009*"价格" + '
  '0.008*"开发商" + 0.007*"土地" + 0.007*"销售" + 0.007*"上海" + 0.007*"房价"'),
 (2,
  '0.017*"电影" + 0.008*"导演" + 0.007*"影片" + 0.005*"观众" + 0.004*"票房" + 0.004*"拍摄" '
  '+ 0.004*"角色" + 0.004*"香港" + 0.004*"演员" + 0.003*"故事"'),
 (3,
  '0.017*"比赛" + 0.009*"球队" + 0.009*"火箭" + 0.009*"时间" + 0.008*"球员" + 0.006*"表现" '
  '+ 0.005*"热火" + 0.005*"篮板" + 0.005*"防守" + 0.005*"12"'),
 (4,
  '0.038*"基金" + 0.016*"投资" + 0.015*"公司" + 0.014*"市场" + 0.011*"银行" + 0.008*"亿元" '
  '+ 0.006*"投资者" + 0.006*"发行" + 0.005*"产品" + 0.005*"风险"'),
 (5,
  '0.029*"游戏" + 0.016*"玩家" + 0.008*"活动" + 0.004*"功能" + 0.004*"系统" + 0.004*"支持" '
  '+ 0.004*"模式" + 0.003*"系列" + 0.003*"时间" + 0.003*"采用"'),
 (6,
  '0.010*"发现" + 0.007*"研究" + 0.006*"美国" + 0.006*"时间" + 0.005*"科学家" + '
  '0.004*"地球" + 0.004*"消息" + 0.004*"发生" + 0.004*"人类" + 0.003*"世界"'),
 (7,
  '0.012*"设计" + 0.010*"搭配" + 0.009*"时尚" + 0.006*"风格" + 0.006*"组图" + 0.005*"空间" '
  '+ 0.004*"选择" + 0.004*"感觉" + 0.004*"品牌" + 0.004*"黑色"'),
 (8,
  '0.007*"银行" + 0.005*"相关" + 0.005*"台湾" + 0.004*"工作" + 0.004*"情况" + 0.004*"部门" '
  '+ 0.004*"陈水扁" + 0.004*"调查" + 0.003*"报道" + 0.003*"公司"'),
 (9,
  '0.024*"学生" + 0.018*"美国" + 0.014*"留学" + 0.013*"大学" + 0.012*"中国" + 0.011*"申请" '
  '+ 0.011*"学校" + 0.008*"专业" + 0.008*"教育" + 0.007*"学习"')]

这份结果实际上与上面超参搜索前给出的展示效果一样。这也验证了超参搜索给出的 10 个主题确实是符合事实的,同时也反映出利用一致性得分作为 LDA 结果好坏判定的有效性。

有了 LDA 模型,我们可以反过头来将每一篇新闻打上主题标签,以及该主题下的关键词,该新闻在该主题标签下的得分占比等信息。

def format_topics_sentences(ldamodel=optimal_model, corpus=corpus, texts=texts):
    # Init output
    sent_topics_df = pd.DataFrame()

    # Get main topic in each document
    for i, row in enumerate(ldamodel[corpus]):
        row = sorted(row, key=lambda x: (x[1]), reverse=True)
        # Get the Dominant topic, Perc Contribution and Keywords for each document
        for j, (topic_num, prop_topic) in enumerate(row):
            if j == 0:  # => dominant topic
                wp = ldamodel.show_topic(topic_num)
                topic_keywords = ", ".join([word for word, prop in wp])
                sent_topics_df = sent_topics_df.append(pd.Series([int(topic_num), round(prop_topic,4), topic_keywords]), ignore_index=True)
            else:
                break
    sent_topics_df.columns = ['Dominant_Topic', 'Perc_Contribution', 'Topic_Keywords']

    # Add original text to the end of the output
    contents = pd.Series(texts)
    sent_topics_df = pd.concat([sent_topics_df, contents], axis=1)
    return(sent_topics_df)


df_topic_sents_keywords = format_topics_sentences(ldamodel=optimal_model, corpus=corpus, texts=texts)

# Format
df_dominant_topic = df_topic_sents_keywords.reset_index()
df_dominant_topic.columns = ['Document_No', 'Dominant_Topic', 'Topic_Perc_Contrib', 'Keywords', 'Text']

# Show
df_dominant_topic.head(10)
Document_No Dominant_Topic Topic_Perc_Contrib Keywords Text
0 0 3.0 0.3165 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [马晓旭, 意外, 受伤, 国奥, 警惕, 无奈, 大雨, 格外, 青睐, 殷家, 傅亚雨,...
1 1 3.0 0.4812 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [商瑞华, 首战, 复仇, 心切, 中国, 玫瑰, 美国, 方式, 攻克, 瑞典, 多曼来,...
2 2 5.0 0.3498 游戏, 玩家, 活动, 功能, 系统, 支持, 模式, 系列, 时间, 采用 [冠军, 球队, 迎新, 欢乐, 派对, 黄旭获, 大奖, 张军, PK, 新浪_体育讯, ...
3 3 3.0 0.4292 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [辽足, 签约, 危机, 注册, 难关, 高层, 威逼利诱, 合同, 笑里藏刀, 新浪_体育...
4 4 8.0 0.6821 银行, 相关, 台湾, 工作, 情况, 部门, 陈水扁, 调查, 报道, 公司 [揭秘, 谢亚龙, 带走, 总局, 电话, 骗局, 复制, 南杨, 轨迹, 体坛周报_特约记...
5 5 3.0 0.6769 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [阿的江, 八一, 定位, 机会, 没进, 新浪_体育讯, 12, 回到, 主场, 北京, ...
6 6 3.0 0.6376 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [姚明, 未来, 次节, 出战, 成疑, 火箭, 高层, 改变, 用姚, 战略, 新浪_体育...
7 7 3.0 0.7227 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [姚明, 我来, 承担, 连败, 巨人, 宣言, 酷似, 当年, 麦蒂, 新浪_体育讯, 北...
8 8 3.0 0.7191 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [姚麦, 无胜, 殊途同归, 活塞, 酝酿, 交易, 火箭, 插一脚, 新浪_体育讯, 火箭...
9 9 3.0 0.6983 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [布雷克, 替补席, 成功, 接棒, 玛湖, 板凳, 后卫, 新浪_体育讯, 戴高乐_报道,...

下面展示各个主题的关键词列表以及每个主题的代表性新闻内容,即选择在各个主题下占比得分最高的新闻,这样当你根据关键词不太方便确认主题内容时,可以根据给出的代表性新闻,方便定位到底它说了个啥。

sent_topics_sorteddf_mallet = pd.DataFrame()

sent_topics_outdf_grpd = df_topic_sents_keywords.groupby('Dominant_Topic')

for i, grp in sent_topics_outdf_grpd:
    sent_topics_sorteddf_mallet = pd.concat([sent_topics_sorteddf_mallet, 
                                             grp.sort_values(['Perc_Contribution'], ascending=[0]).head(1)], 
                                            axis=0)

# Reset Index    
sent_topics_sorteddf_mallet.reset_index(drop=True, inplace=True)

# Format
sent_topics_sorteddf_mallet.columns = ['Topic_Num', "Topic_Perc_Contrib", "Keywords", "Text"]

# Show
sent_topics_sorteddf_mallet
Topic_Num Topic_Perc_Contrib Keywords Text
0 0.0 0.9050 中国, 发展, 企业, 国际, 国家, 合作, 经济, 工作, 北京, 城市 [外交部, 公布, 中国, 联合国, 作用, 国际, 立场, 中新网_日电, 联合国大会, ...
1 1.0 0.9098 市场, 房地产, 项目, 北京, 价格, 开发商, 土地, 销售, 上海, 房价 [京新盘, 供应, 将量, 跌价, 半数以上, 中高档, 项目, 北京晨报, 报道, 多家,...
2 2.0 0.8951 电影, 导演, 影片, 观众, 票房, 拍摄, 角色, 香港, 演员, 故事 [赵氏_孤儿, 主题曲, 陈凯歌, 葛优, 幽默, 鞠躬, 陈凯歌, 担心, 葛优, 笑场,...
3 3.0 0.9198 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 [爵士, 五虎将, 双破, 网而出, 邓肯, 爆发, 马刺, 轻取, 连胜, 新浪_体育讯,...
4 4.0 0.9236 基金, 投资, 公司, 市场, 银行, 亿元, 投资者, 发行, 产品, 风险 [降低, 营业税, 预期, 鼓舞, 基金, 反手, 银行, 股张, 伟霖, 降低, 营业税,...
5 5.0 0.8807 游戏, 玩家, 活动, 功能, 系统, 支持, 模式, 系列, 时间, 采用 [天劫_OL, 资料片, 上市, 更新, 公告, 2009, 推出, 天劫_OL, 最新_资...
6 6.0 0.9152 发现, 研究, 美国, 时间, 科学家, 地球, 消息, 发生, 人类, 世界 [科学家, 声称, 揭开, 通古斯_爆炸, 之谜, 新浪_科技, 北京, 时间, 消息, 美...
7 7.0 0.9153 设计, 搭配, 时尚, 风格, 组图, 空间, 选择, 感觉, 品牌, 黑色 [组图, 09_春夏, LV_手袋, 十大, 潮流, 新浪, 女性, 讯明, 年春夏, 注定...
8 8.0 0.8830 银行, 相关, 台湾, 工作, 情况, 部门, 陈水扁, 调查, 报道, 公司 [存折, 骗过, 银行, 系统, 存款, 法院, 银行, 全责, 本报讯, 刘艺明, 通讯员...
9 9.0 0.9037 学生, 美国, 留学, 大学, 中国, 申请, 学校, 专业, 教育, 学习 [重磅, 推荐, 美国大学_报考, 八大, 攻略, 美国大学_报考, 材料, 绝不, 一件,...

下面再统计一下经过 LDA 给出的标签下,各个主题的新闻数以及占比情况:

# Number of Documents for Each Topic
topic_counts = df_topic_sents_keywords['Dominant_Topic'].value_counts()

# Percentage of Documents for Each Topic
topic_contribution = round(topic_counts/topic_counts.sum(), 4)

# Topic Number and Keywords
topic_num_keywords = df_topic_sents_keywords[['Dominant_Topic', 'Topic_Keywords']].drop_duplicates().reset_index(drop=True)

# Concatenate Column wise
df_dominant_topics = pd.concat([topic_num_keywords, topic_counts, topic_contribution], axis=1)

# Change Column names
df_dominant_topics.columns = ['Dominant_Topic', 'Topic_Keywords', 'Num_Documents', 'Perc_Documents']

df_dominant_topics.sort_values(by="Dominant_Topic", ascending=True, inplace=True)
# Show
df_dominant_topics
Dominant_Topic Topic_Keywords Num_Documents Perc_Documents
3.0 0.0 中国, 发展, 企业, 国际, 国家, 合作, 经济, 工作, 北京, 城市 4938 0.0988
9.0 1.0 市场, 房地产, 项目, 北京, 价格, 开发商, 土地, 销售, 上海, 房价 4022 0.0804
4.0 2.0 电影, 导演, 影片, 观众, 票房, 拍摄, 角色, 香港, 演员, 故事 4456 0.0891
0.0 3.0 比赛, 球队, 火箭, 时间, 球员, 表现, 热火, 篮板, 防守, 12 3412 0.0682
8.0 4.0 基金, 投资, 公司, 市场, 银行, 亿元, 投资者, 发行, 产品, 风险 3977 0.0795
1.0 5.0 游戏, 玩家, 活动, 功能, 系统, 支持, 模式, 系列, 时间, 采用 3650 0.0730
5.0 6.0 发现, 研究, 美国, 时间, 科学家, 地球, 消息, 发生, 人类, 世界 5964 0.1193
6.0 7.0 设计, 搭配, 时尚, 风格, 组图, 空间, 选择, 感觉, 品牌, 黑色 4459 0.0892
2.0 8.0 银行, 相关, 台湾, 工作, 情况, 部门, 陈水扁, 调查, 报道, 公司 5683 0.1137
7.0 9.0 学生, 美国, 留学, 大学, 中国, 申请, 学校, 专业, 教育, 学习 9439 0.1888

后面其实还可以做很多事情,例如可以将上述 LDA 给出的 0~9 的主题标签与真实标签做个映射关系,然后就可以比较 y ^ \hat y y^ y y y ,进而可以算出 LDA 模型的精确度等计算指标,甚至可以对标监督类学习算法的结果。

1.5 模型的保存、加载以及预测

# save model
optimal_model.save("lda.model")

将模型用于预测

# load model

def lda_predict(texts):
    if isinstance(texts, str):
        texts = [texts]

    texts = ...     # 数据处理,做到分词、去停用词一步即可,无需做 gram 步骤
    lda_model = LdaMallet.load("./lda.model")          # 加载模型
    print(*lda_model.show_topics(num_topics=10, num_words=10, log=False, formatted=True), sep="\n")
    loaded_dct = Dictionary.load_from_text("./data/dictionary")    # 加载词典

    corpus = [loaded_dct.doc2bow(text) for text in texts]
    return lda_model[corpus]                           # 模型预测


# 从测试集摘抄一条数据
texts = """北京时间10月16日消息,据沃神报道,泰伦-卢将成为快船新任主教练,已与球队达成一份5年合同。快船方面认为,
    泰伦-卢的总冠军经历、在骑士季后赛的成功以及强大的沟通球员能力能够帮助快船弥补19-20赛季的一些缺憾。根据之前的报道,
    自里弗斯与快船分道扬镳以来,泰伦-卢一直在快船主教练的竞争中处于领先地位,并同时成为火箭和鹈鹕的主帅候选人。
    泰伦-卢曾担任凯尔特人、快船、骑士助教,在2015年-2018年担任骑士主教练,2015-16赛季率领骑士总决赛4-3击败勇士拿下队史首冠。
    此外据名记shames报道,昌西-比卢普斯将担任快船首席助理教练,前骑士主教练拉里-德鲁也加入担任助教。""" 
print(max(lda_predict(texts)[0], key=lambda k: k[1]))

1.6 小结

LDA 模型的可调参数较少,关键是主题数目的确定,个人认为,作为传统机器学习的一般实现方式,与算法本身,处理技巧等相比更重要的是对业务对数据的理解,否则往往是事倍功半,而对数据能有良好的认识,则一些处理技巧也会水到渠成,效果往往也会事半功倍。以上述内容为例,我们根据事先对数据的先验知识,可以判定主题数为 10,即使超参搜索也是重点搜索 10 附近的主题数进行搜索。那么如果事先对数据没有这种先验怎么办?这就涉及到你对业务的理解,能不能对数据有个大致猜测,或者少量人为标注。

其次,我在做这份演示示例的时候,一开始只是使用的常规停用词表,分词结果后存在很多数字或者单个字的词,后续 LDA 结果惨不忍睹,因为这样的单字或者数字往往无意义,或者不足以区分主题,所以索性我将它们全部加入了停用词表中,按照这个思路,我们或许可以根据分词的词性过滤掉更多的词,只保留名词性、动词性之类的词,一方面干掉了很多干扰性词汇,另一方面无意中实现了数据降维。

完整项目代码已分享至 github

参考资料:

Topic Modeling with Gensim (Python)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

文本聚类(一)—— LDA 主题模型 的相关文章

  • python sys.path 故障排除

    python 文档位于http docs python org library sys html http docs python org library sys html比如说sys path is 从环境变量 PYTHONPATH 以及
  • 如何避免使用 python 处理空的标准输入?

    The sys stdin readline 返回之前等待 EOF 或新行 所以如果我有控制台输入 readline 等待用户输入 相反 我想打印帮助并在没有需要处理的情况下退出并显示错误 而不是等待用户输入 原因 我正在寻找一个Pytho
  • 获取 .wav 文件长度或持续时间

    我正在寻找一种方法来找出 python 中音频文件 wav 的持续时间 到目前为止我已经了解了 pythonwave图书馆 mutagen pymedia pymad我无法获取 wav 文件的持续时间 Pymad给了我持续时间 但它不一致
  • 使用 django-rest-framework 设置对象级权限

    尝试使用 django rest framework 最干净 最规范地管理 django guardian 对象级权限 我想将对象的读取权限 module view object 分配给在执行 POST 时发出请求的用户 我的基于阶级的观点
  • 将 numpy 数组合并为单个 int

    numpy 数组怎么可以这样 10 22 37 45 转换为单个 int32 数字 如下所示 10223745 这可以工作 gt gt gt int join map str 10 22 37 45 10223745 基本上你使用map s
  • 从 Azure ML 实验中访问 Azure Blob 存储

    Azure ML 实验提供了通过以下方式读取 CSV 文件并将其写入 Azure Blob 存储的方法 Reader and Writer模块 但是 我需要将 JSON 文件写入 blob 存储 由于没有模块可以执行此操作 因此我尝试在Ex
  • 无法在 selenium 和 requests 之间传递 cookie,以便使用后者进行抓取

    我用 python 结合 selenium 编写了一个脚本来登录网站 然后从driver to requests这样我就可以继续使用requests进行进一步的活动 I used item soup select one div class
  • 如何确保 re.findall() 停止在正确的位置?

    这是我的代码 a import re re findall r lt title gt lt title gt a 结果是 title aaa
  • Arcpy 模数在 Pycharm 中不显示

    如何将 Arcpy 集成到 Pycharm 中 我尝试通过导入模块但它没有显示 我确实知道该模块仅适用于 2 x python arcpy 在 PyPi Python 包索引 上不可用 因此无法通过 pip 安装 要使用 arcpy 您需要
  • 如何使用scrapy检查网站是否支持http、htts和www前缀

    我正在使用 scrapy 来检查某些网站是否工作正常 当我使用http example com https example com or http www example com 当我创建 scrapy 请求时 它工作正常 例如 在我的pa
  • 两个不同长度的数据帧的列之间的余弦相似度?

    我在 df1 中有文本列 在 df2 中有文本列 df2 的长度将与 df1 的长度不同 我想计算 df1 text 中每个条目与 df2 text 中每个条目的余弦相似度 并为每场比赛给出分数 输入样本 df1 mahesh suresh
  • 给定一个排序数组,就地删除重复项,使每个元素仅出现一次并返回新长度

    完整的问题 我开始在线学习 python 但对这个标记为简单的问题有疑问 给定一个排序数组 就地删除重复项 使得每个 元素只出现一次并返回新的长度 不分配 另一个数组的额外空间 您必须通过修改输入来完成此操作 数组就地 具有 O 1 额外内
  • 是否需要关闭没有引用它们的文件?

    作为一个完全的编程初学者 我试图理解打开和关闭文件的基本概念 我正在做的一项练习是创建一个脚本 允许我将内容从一个文件复制到另一个文件 in file open from file indata in file read out file
  • Ubuntu systemd 自定义服务因 python 脚本而失败

    希望获得有关 Ubuntu 中的 systemd 守护进程服务的一些帮助 我写了一个 python 脚本来禁用 Dell XPS 上的触摸屏 这更像是一个问题 而不是一个有用的功能 该脚本可以工作 但我不想一直启动它 这就是为什么我想到编写
  • 在骨架图像中查找线 OpenCV python

    我有以下图片 我想找到一些线来进行一些计算 平均长度等 我尝试使用HoughLinesP 但它找不到线 我能怎么做 这是我的代码 sk skeleton mask rows cols sk shape imgOut np zeros row
  • 使用 Python 将连续日期分组在一起

    Given dates datetime 2014 10 11 datetime 2014 10 1 datetime 2014 10 2 datetime 2014 10 3 datetime 2014 10 5 datetime 201
  • 检测是否从psycopg2游标获取?

    假设我执行以下命令 insert into hello username values me 我跑起来就像 cursor fetchall 我收到以下错误 psycopg2 ProgrammingError no results to fe
  • IndexError - 具有匀称形状的笛卡尔 PolygonPatch

    我曾经使用 shapely 制作一个圆圈并将其绘制在之前填充的图上 这曾经工作得很好 最近 我收到索引错误 我将代码分解为最简单的操作 但它甚至无法执行最简单的循环 import descartes import shapely geome
  • python从二进制文件中读取16字节长的双精度值

    我找到了蟒蛇struct unpack 读取其他程序生成的二进制数据非常方便 问题 如何阅读16 字节长双精度数出二进制文件 以下 C 代码将 1 01 写入二进制文件三次 分别使用 4 字节浮点型 8 字节双精度型和 16 字节长双精度型
  • 定义在文本小部件中双击时选择哪些字符

    在 Windows 上 双击文本小部件中的单词也将选择连接的标点符号 有什么方法可以定义您想要选择的角色吗 tcl wordchars该变量的值是一个正则表达式 可以设置它来控制什么被视为 单词 字符 例如 通过双击 Tk 中的文本来选择单

随机推荐

  • VitePress开发记录(二)之LaTeX语法支持

    前言 当我在我的博客网站更新我的吴恩达机器学习笔记时 出现了一个情况 VitePress默认的markdown it解析器似乎无法渲染LaTeX数学公式 于是我去VitePress官方中查看issue是否有解决的方案 官方给出的解答是 这里
  • 华为OD机试真题-模拟商场优惠打折【2023Q1】

    题目内容 模拟商场优惠打折 有三种优惠券可以用 满减券 打折券和无门槛券 满减券 满100减10 满200减20 满300减30 满400减40 以此类推不限制使用 打折券 固定折扣92折 且打折之后向下取整 每次购物只能用1次 无门槛券
  • SpringBatch文章系列-SPL表达式的使用

    参考例子
  • 爬虫如何快速定位到加密入口

    这里有多种定位加密入口方法 通过打全局xhr断点找到加密入口 可以将在 处点击添加xhr断点 可以针对某个值进行打xhr断点 直接通过关键字找到加密入口 在前面文章讲述过https blog csdn net zhp980121 artic
  • Springboot+vue 社团管理系统(前后端分离)

    Springboot vue 社团管理系统 前后端分离 zero 项目功能设计图 一 数据库设计 项目准备 1 建表 2 表目录 二 前端编写 vue 1 搭建Vue框架 2 放入静态资源 assets文件包括static文件的静态文件 3
  • 2017 年提高组初赛

    第 6 题 1 5 分 若某算法的计算时间表示为递推关系式 T N 2 T N 2 N l o g N T N 2T N 2 NlogN T N 2T N 2 NlogN T N 2 2 T N 4 N 2 l o g N 2 2 T N
  • TencentOS-tiny 内存管理(十 二)- 动态内存

    一 内存管理 动态内存 概述 动态内存管理模块 提供了一套动态管理系统内存的机制 支持用户动态的申请 释放不定长内存块 API讲解 编程实例 1 在tos config h中 配置动态内存组件开关TOS CFG MMHEAP EN defi
  • 【Linux】进程基础

    文章目录 1 冯诺依曼体系 1 2操作系统 2 进程 2 1进程的概念 2 2 task struct 2 3进程的状态 2 4进程优先级 优先级VS权限 为何会存在优先级 Linux下的优先级相关概念 2 5其他重要概念 单道和多道程序设
  • warp shuffle实验

    实验一 shfl sync unsigned mask T var int srcLane int width warpSize mask 是参与的线程掩码 如0xffffffff var 是待广播的值 srclane 是被广播的 lane
  • gb28181抓包

    知乎一篇雄文 https zhuanlan zhihu com p 98533891 这是对照gb28181文档进行抓包的分析 nvr代理服务端 44 198 62 2 5061 44190012002000000001 代理客户端 44
  • jQuery选择器集锦(读《锋利的jQuery(第二版)》所摘)

    jQuery选择器分为基本选择器 层次选择器 过滤选择器和表单选择器 过滤选择器可以分为基本过滤 内容过滤 可见性过滤 属性过滤 子元素过滤和表单对象属性过滤选择器 input not myClass 选取class不是myClass的
  • linux 判断目录是否存在并创建

    1 用 int access const char pathname int mode 判断有没有此文件或目录 它区别不出这是文件还是目录 2 用 int stat const char file name struct stat buf
  • 计算机网络模型

    计算机网络OSI模型 Open Systems Interconnection model 是一种概念模型 它表征并标准化电信或计算系统的通信功能 而不考虑其基础内部结构和技术 其目标是多种通信系统与标准协议的互操作性 该模型将通信系统划分
  • Java从FTP下载文件到本地前端+后端

    一 前端 1 首先创建下载文件按钮
  • Android IdentityCredential(身份凭证)二

    IC TA代码调试 static const uint8 t hbkTest 16 0 hbkReal需要对接到具体系统中的API 该密钥需要具备每台设备唯一的特性 而且每次开机都需要保持不变 static const uint8 t hb
  • Latex的基本使用

    本文目录 一 Latex文档的基本结构 1 1 latex文档的两个部分 1 2 导言区 1 3 正文区 1 4 数学模式和文本模式 二 Latex中中文的处理办法 2 1 第一种方式 引入ctex宏包 2 2 第2种方式 使用ctexar
  • 获取屏幕分辨率

    获取屏幕宽度 window screen width window devicePixelRatio 获取屏幕高度 window screen height window devicePixelRatio
  • FBI紧急警告:黑客利用开源SonarQube实例窃取政府和企业源代码

    聚焦源代码安全 网罗国内外最新资讯 编译 奇安信代码卫士团队 美国联邦调查局 FBI 发布紧急警告称 黑客正在通过暴露在互联网且不安全的 SonarQube 实例中窃取美国政府和企业的信息 SonarQube 是一款开源的自动化代码质量审计
  • Servlet 405的可能原因

    初学Servlet 网页访问405 原因 没有删除自动生成的super sevice req resp 将其删除即可
  • 文本聚类(一)—— LDA 主题模型

    目录 文本聚类 一 LDA 主题模型 1 1 加载数据集 1 2 数据清洗 分词 1 3 构建词典 语料向量化表示 1 4 构建 LDA 模型 1 5 模型的保存 加载以及预测 1 6 小结 Update log 2021 07 08 主要