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

目录

  • 文本聚类
    • 一、LDA 主题模型
      • 1.1 加载数据集
      • 1.2 数据清洗、分词
      • 1.3 构建词典、语料向量化表示
      • 1.4 构建 LDA 模型
      • 1.5 模型的保存、加载以及预测
      • 1.6 小结

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

文本聚类(一)—— LDA 主题模型_第1张图片

左侧一些圆圈表示的是主题,圈越大代表主题占比约大,右侧同时展示出一些对该主题贡献度较大的词汇,理想状态先,左侧主题之间分散均匀,重叠度较小,而图中的 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()

文本聚类(一)—— LDA 主题模型_第2张图片

上图的拐点还是很清楚的,直接选择 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)

你可能感兴趣的:(NLP,机器学习,自然语言处理,python)