ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微博评论情感分析与LDA主题分析实战:从预处理到结果解读

微博评论情感分析与LDA主题分析实战:从预处理到结果解读 简介面向希望掌握社交媒体舆情分析的Python开发者这套压缩包提供了基于微博评论的LDA主题分析与情感分析完整实现覆盖从数据采集、预处理到建模可视化的全流程。资源共39个文件以23个Python脚本为主体辅以7个Markdown说明文档、7个txt词表与语料、1个预训练word2vec模型和1个Excel结果文件压缩包大小仅16.16MB。脚本包含微博爬虫、数据清洗、情感分析API与SDK调用、LDA建模、折线图绘制等模块txt文件提供停用词表、正向负向语料、近义词表Markdown文档给出清晰的使用指引。运行后可直接获得评论主题分布与情感标签发现不同话题的舆论倾向。代码结构清晰数据、模型与脚本分离便于二次改造适用于舆情监控、市场调研和热点趋势分析。项目目前已有2324人学习适合具备基础Python知识、希望实战自然语言处理的数据分析初学者与研究人员。1. 微博评论的情感分析与 LDA 主题分析先想清楚这套代码解决了什么操盘过微博舆情的人都经历过这种窘境费劲拉回几千条评论统计完词频全是哈哈支持路过什么都看不出来用现成的情绪库打分这家店太坑了被判成正向质量真的垃圾被判成中性结果拿去汇报直接被质疑。带 LDA 主题分析的情感分析就是为这种局面准备的先回答网友在讨论哪些话题再回答每个话题下大家是赞成还是反感最后输出一张话题 × 情感的二维结论。这套方案给产品、运营和做短文本舆情分析的人提供一个能直接改、直接跑的 Python 基线版本几百条评论就能看到效果把 CSV 换成自己的数据后一键出结果。2. 情感分析和 LDA 主题分析的分工为什么是这两个模型凑在一起2.1 一个管单条情绪一个管网民话题两层结构各解决什么问题先把情感分析和LDA 主题分析这两件事拆清楚。情感分析解决的是这条评论是正面还是负面。它处理的单位是单条文本输出一个极性分数比如 0.3 分偏负面、0.8 分偏正面。它不关心这条评论在骂什么只关心说话人的情绪倾向。微博评论恰好是短文本里最考验这个模型的类型口语化严重、绝绝子栓Q这类网络词天天更新一条你没事吧在不同语境下既可能是关心也可能是嘲讽。所以纯靠模型打分不结合领域知识做修正翻车是大概率事件。LDA 主题分析解决的是另一件事这一批评论里到底在吵什么。它是无监督的生成式模型核心假设是每一篇文档由若干个主题混合生成每个主题又由若干个词的概率分布表示。训练完之后你会得到主题 1价格、收费、贵、值不值主题 2物流、发货、快递、慢这样的主题词列表。它不判断褒贬只负责把杂乱评论归堆。这两个东西凑在一起是因为只看情感不看主题会得出负面率 35%这种没有抓手的数据只看主题不看情感又会得到大家在讨论价格和物流这种不含态度的结论。先做 LDA 分出群体在讨论什么再做情感分析判断每个主题下用户的褒贬最后输出哪个话题是雷区、哪个话题是卖点——这才是舆情分析真正要的答案。2.2 情感分析的三种路线与这套代码的选型逻辑做文本情感倾向分析业界常见的路线有三条各有各的适用范围。第一种是词典法维护一个正向词表、一个负向词表统计文本里命中哪个词多就判哪个极性。优点是快、可控、不用训练缺点是覆盖度差网络新词不写进词表就抓不到否定句处理也很粗糙。第二种是统计机器学习法用标注好的语料训练一个分类器或者用预训练好的情感模型直接打分。SnowNLP 就是这个路线的代表安装即用对通用文本表现尚可。缺点是它的默认模型是用电商商品评论类语料训练的迁移到微博短文本上会有领域偏移需要做修正。第三种是深度学习甚至多模态路线比如用 BERT 微调或者把表情图片和文本一起做多模态情感分析。这种方法精度高但标注成本、训练成本和部署成本都上来了一次舆情分析项目不一定值得投入。这套代码选的是SnowNLP 打分 情感词典修正组合先用现成模型给出一个初始分数再用词典修正补偿微博热词和网络用语的偏差。对新手友好替换数据进去几十秒就能跑完一轮分析后续业务量大了再升级成微调模型。LDA 部分用 sklearn 的实现而不是 gensim原因是 sklearn 的 LatentDirichletAllocation 和 CountVectorizer 能直接对接 DataFrame 里的分词列代码量更短不需要额外维护 gensim 的 Dictionary 和 Corpus 对象。路线短文本微博上的表现改造成本适合阶段词典法覆盖率低靠词表堆低但要持续维护快速验证SnowNLP 统计法不加修正会偏移中配词典修正基线版本BERT/多模态高能吃表情和上下文高需标注业务成熟后3. 微博评论数据预处理清洗正则、jieba 分词与停用词的组合拳3.1 环境准备和数据格式评论数据放进 CSV 后长什么样整条流水线依赖的包不多按下面的 requirements 装一遍就能跑起来。这里默认你用的是 Python 3.8 以上版本Windows 环境下如果启动时报缺少 msvcp140.dll 之类的错误去装一个 Visual C Redistributable 就能解决这说明依赖工具链有问题而不是代码问题。# requirements.txt pandas jieba snownlp scikit-learn建议先把目录结构按功能分开后面调试的时候不用在一个文件里翻几百行weibo_sentiment_lda/ ├── data/ │ └── weibo_comments.csv ├── dict/ │ └── stopwords.txt ├── src/ │ ├── preprocess.py │ ├── sentiment.py │ └── lda_model.py └── output/ └── report.xlsxCSV 只要能识别出三列就能跑评论 id、评论内容、发布时间。发布时间先不用参与建模但留着他后面做情感随时间变化时不用重新拉数据。下面是最小可运行的读取代码import pandas as pd # 如果 CSV 是 Excel 另存出来的编码改为 gbk 再试 df pd.read_csv(data/weibo_comments.csv, encodingutf-8) print(df.shape) print(df.head()) # 检查评论内容是否为空空内容直接丢掉避免后面分词报错 df df.dropna(subset[content]) df df[df[content].str.strip() ! ] df df.reset_index(dropTrue)逻辑说明read_csv 的 encoding 参数是中文数据最常见的突破口UTF-8 报 UnicodeDecodeError 就换 gbk这是处理爬虫导出数据时的血泪经验。dropna 和 strip 两步是为了保证后续每个函数拿到的都是非空字符串否则 CountVectorizer 在向量化阶段会直接报空词汇表错误。3.2 清洗、分词、去停用词一个函数串起整条前置流水线微博文本和普通新闻不一样带着明显的平台痕迹表情符号 [哈哈]、用户、#话题#、短链接、HTML 标签。这些内容对主题分析是噪音但对情感分析未必是噪音——一个 [怒] 表情本身就是负面情绪的信号。所以清洗策略要分对象处理表情符号和链接直接删话题标签去掉井号保留话题词把话题内容留给 LDA 去发现。import re import jieba # 停用词表加载一行一个词网上常见的哈工大、百度停用词表合并后够用 STOPWORDS set() with open(dict/stopwords.txt, r, encodingutf-8) as f: for line in f: STOPWORDS.add(line.strip()) def clean_weibo(text: str) - str: 清洗微博原始文本返回干净字符串 if not isinstance(text, str): return text re.sub(r\[[^\]]{1,4}\], , text) # 去掉 [哈哈] 这类表情 text re.sub(r[\w\u4e00-\u9fa5\-], , text) # 去掉 用户名 text re.sub(rhttps?://\S|www\.\S, , text) # 去掉链接 text re.sub(r[^], , text) # 去掉网页标签 text re.sub(r#([^#])#, lambda m: m.group(1), text) # #话题# 只保留话题词 text re.sub(r\s, , text).strip() return text def segment_and_filter(text: str) - str: jieba 分词 过滤单字词和停用词输出空格连接的词序列 words jieba.lcut(text) words [w for w in words if len(w.strip()) 1 and w not in STOPWORDS] return .join(words) df[cleaned] df[content].apply(clean_weibo) df[seg] df[cleaned].apply(segment_and_filter) # 清洗后可能整条变空比如一条评论只有表情直接过滤掉 df df[df[seg].str.len() 0].reset_index(dropTrue)逻辑说明先清洗再分词这个顺序不能反。如果先分词再去表情哈哈 今天 天气 真 好 里的 哈哈 会被拆成一个词除非停用词表覆盖到位否则它就会混进 LDA 主题词里增加噪音。话题标签的处理用的是 lambda 匹配替换只删井号不删内容因为话题词往往是主题分析里最有区分度的那部分词。长度小于 2 的词直接丢掉这个阈值是经验值对微博这种短文本了吗啊这些语气词绝大多数已经被停用词表覆盖了剩下的单字很少是有效特征丢掉足以降低噪音。注意停用词表不是越全越好。我之前接过一个项目某同事下载了一个 2700 多词的超全停用词表结果快慢热这种高信息词全在表里LDA 跑出来的主题词一个都读不通。停用词表建议控制在 300 到 1000 词左右把代词、介词、语气词、高频无意义动词放进去就够了。4. 微博短文本情感分析SnowNLP 打分、三分类阈值与词典修正4.1 用 SnowNLP 给每条评论打出情感分并把分数变成三分类标签SnowNLP 是 Python 里最省事的文本情感计算库核心调用只需要一行拿到文本后构造 SnowNLP 对象读 .sentiments 属性分数范围 0 到 1越接近 1 越正向。但不要迷信这个分数。它的默认模型是在电商购物评论这类语料上训练的微博短文本里笑死狠狠种草谁懂啊这些表达它没见过直接拿来用的结果是大量评论分数都挤在 0.4 到 0.6 之间看着不伦不类。所以代码里要做两件事给每条调用加异常保护以及用阈值做三分类而不是只看分数。from snownlp import SnowNLP def safe_sentiment(text: str) - float: 安全的情感打分空文本和异常都返回 0.5 中性值 try: text text.strip() if not text: return 0.5 return SnowNLP(text).sentiments except Exception: return 0.5 df[sentiment] df[cleaned].apply(safe_sentiment) # 三分类阈值0.6 以上正向0.4 以下负向中间算中性 df[label] df[sentiment].apply( lambda s: pos if s 0.6 else (neg if s 0.4 else neu) ) print(df[label].value_counts())逻辑说明这里打分用的是 cleaned 字段而不是原始 content因为 用户 和链接对情感极性判定没有任何贡献留着只会干扰。阈值 0.6 和 0.4 不是拍脑袋是短文本情感分析里比较通用的区间设置。实际业务里要根据你的语料分布调整如果打出来的 label 90% 都是中性说明这个模型在你的领域里区分度很差建议把阈值往外扩到 0.65 和 0.35或者直接看 sentiment 分数的直方图找两个谷底作为切分点。另外 SnowNLP 的调用是逐条计算速度不快一两万条评论可能要跑好几分钟这是正常现象不是死循环。4.2 默认模型跑偏时用词典修正把分数拉回来跑一遍基线之后典型的现象是垃圾坑爹服了这种微博高频负向词没有得到应有的低分反而落在 0.5 附近。原因是 SnowNLP 的训练语料里这些网络新词几乎没有出现过。修正思路很直接维护一个领域情感词典命中正向词就加分命中负向词就减分同时做一层简单的否定词处理。# 领域情感词典按你项目的行业不断往里追加 POS_WORDS {好, 赞, 漂亮, 喜欢, 棒, 期待, 优秀, 进步, 种草} NEG_WORDS {烂, 差, 垃圾, 失望, 生气, 坑, 恶心, 难看, 服了, 翻车} NEG_PREFIX {不, 没, 无, 别, 莫} def adjust_score(score: float, text: str) - float: 基于词典的情感分数修正返回 0-1 之间的新分数 hit_pos sum(1 for w in POS_WORDS if w in text) hit_neg sum(1 for w in NEG_WORDS if w in text) # 否定词反转如果句子里出现否定前缀情感词的极性互换 if any(p in text for p in NEG_PREFIX): hit_pos, hit_neg hit_neg, hit_pos # 每个命中词加/减 0.08幅度不能太大否则会把原模型结果顶飞 new_score score 0.08 * (hit_pos - hit_neg) return max(0.0, min(1.0, new_score)) df[sentiment_adj] df.apply( lambda r: adjust_score(r[sentiment], r[cleaned]), axis1 ) df[label_adj] df[sentiment_adj].apply( lambda s: pos if s 0.6 else (neg if s 0.4 else neu) )逻辑说明否定词反转是最简易的版本看到不好就把好的正向命中当成负向处理对微博这种简短文本足够用。0.08 这个系数是经验值调大之后词典的修正力度会盖过原始模型出现系统比词典还听话的过拟合调小了又拉不回垃圾这种词。上线后最好的调法不是改代码而是把预测错的样本捞出来看它是漏了哪个词典词把词补进去迭代两三轮之后准确率会有明显提升。注意词典修正解决的是个别词引发的误判解决不了整句话的反讽和语境。比如你家客服态度真好呵呵这种句子词典修正也会失灵。这是统计模型的天然边界真要做到那种程度就得走标注数据和微调的路线了基线版本不需要一步到位。5. LDA 主题分析的三大常见问题与避坑主题数 K 怎么选、主题词怎么读5.1 LDA 训练主流程从分词向量到主题词输出的完整代码现在把第 3 章产出的分词列喂给 LDA。核心思路是先用 CountVectorizer 把空格连接的词序列转成文档-词频矩阵再用 LatentDirichletAllocation 训练主题模型。这里的输入是 df[seg]不是 df[cleaned]这一点非常关键中文文本没有天然空格CountVectorizer 默认按空白切分 token你把原始句子直接传进去它会把我真的服了这个质量当成一个词处理主题效果直接归零。from sklearn.feature_extraction.text import CountVectorizer from sklearn.decomposition import LatentDirichletAllocation vectorizer CountVectorizer( min_df2, # 至少在2条评论里出现过过滤掉只出现一次的长尾噪音 max_df0.8, # 在超过80%的评论里都出现基本是停用词级别忽略 max_features3000, # 词典上限防止矩阵过大拖慢训练 ) # fit_transform 的结果是稀疏矩阵行是评论列是词 dtm vectorizer.fit_transform(df[seg]) print(f文档-词矩阵形状: {dtm.shape}) lda LatentDirichletAllocation( n_components3, # 主题数K后面专门讲怎么定 random_state42, # 固定随机种子保证结果可复现 learning_methodbatch, # 短文本用 batch比 online 收敛更稳 max_iter25, # EM 最大迭代次数语料小的话 20 次足够 ) lda.fit(dtm) # 输出每个主题下权重最高的 10 个词 feature_names vectorizer.get_feature_names_out() for topic_idx, topic in enumerate(lda.components_): top_indices topic.argsort()[:-11:-1] words [feature_names[i] for i in top_indices] print(fTopic {topic_idx}: {, .join(words)})参数说明min_df2 解决的是每条评论都带一个自造词的问题这种词只出现一次对主题贡献为零还增大矩阵max_df0.8 解决的是微博评论感觉这类全文高频词这类词在每个主题里都有高权重会让主题之间区分不开。max_features 设 3000 是给内存和训练速度兜底微博分词后常见词规模上万3000 足够覆盖主题区分所需的核心词。learning_method 选 batch 是因为项目语料量在一万条以内online 的逐批更新优势发挥不出来batch 每次迭代用全量数据结果更稳定可复现。5.2 主题数 K 怎么选困惑度只是参考词的可读性才是最终标准主题数 K 是 LDA 里最不好定的超参数也是新手最容易翻车的地方。很多人一上来设 K10结果出 10 个主题每个主题读起来都像随机词拼凑根本没法写分析报告。K 太小则所有话题糊成一团。常见的做法是先跑一个困惑度扫描再用人工读词做最终决定。for k in range(2, 8): lda_k LatentDirichletAllocation( n_componentsk, random_state42, learning_methodbatch, max_iter25, ) lda_k.fit(dtm) p lda_k.perplexity(dtm) print(fK{k}, perplexity{p:.2f})运行结果大概率显示 K 越大困惑度越低甚至单调下降。困惑度这时候只能帮你看趋势不能帮你选具体数字因为 LDA 是生成模型主题数越多模型对训练集的拟合能力越强困惑度天然偏低。这时候把代码里的打印结果拿到手上从 K2 到 K6 每个主题打出的词表全部人工读一遍如果某个主题的前 10 个词你一句话能概括比如投诉、客服、售后、态度、电话说明这个主题凝聚度可以如果前 10 个词里有三个以上互相看不出关系这个 K 就过大了或者语料本身不够支撑这么多主题。对微博评论这种短文本几百条的语料跑 K3 到 K4 比较常见上万条可以试到 K8。有一种实用技巧是先把每条评论按时间段聚成一个伪文档再跑 LDA比如把每小时内的所有评论拼成一个大文本因为单条微博太短主题信号稀疏聚合后 LDA 的主题会更清晰代价是丢失单条粒度。5.3 四条高频翻车记录现象、原因、解决以下四条是实际用这套方案时最容易遇到、排查起来也最费时间的问题按现象、原因、解决的格式罗列在这里遇到类似情况直接对应处理。坑 1CountVectorizer 报 ValueError: empty vocabularyperhaps the documents only contain stop words现象跑到 fit_transform 那行直接报错说词表是空的。原因清洗加停用词过滤之后大量短评论变成空字符串向量器没有拿到任何有效词。解决在预处理最后加一行 df df[df[seg].str.len() 0]把所有空串过滤掉同时检查停用词表是不是把好差快这种有情感色彩的词也收进去了如果是从停用词表里拿出来。坑 2所有主题都包含微博评论感觉真的这几个词主题之间区分不开现象打印出来的三个主题每个都有一半词是重合的。原因通用停用词表覆盖不了微博语境里的高频虚词加上 max_df 参数设置过大这类出现在 60% 以上评论里的词没有被过滤掉。解决人工把微博评论感觉今天真的应该可以知道追加进停用词表同时把 CountVectorizer 的 max_df 从默认 1.0 调整到 0.8 甚至 0.6让全语料高频词直接失去进入词表的资格。坑 3LDA 跑两次主题词结果完全不一样现象代码没改只是重新运行了一遍 Notebook主题 1 和主题 2 的内容对调了甚至词都不一致。原因LDA 的 EM 算法初始化是随机的不固定随机种子就每次从不同起点迭代收敛到不同局部最优。解决LatentDirichletAllocation 里固定 random_state42如果固定后还是不稳定加大 max_iter 到 50 再试试。另外要确认语料量几百条评论本身就是主题模型的极限场景结果波动正常K 设小一点更稳妥。坑 4情感打分结果大量集中在 0.5 附近三分类之后中性类别占了六成现象label 分布严重失衡pos 和 neg 都只有十几个百分点中性占大头报告没法写。原因SnowNLP 默认模型对微博短文本不敏感加上微博表达含混、缺乏明显情感词模型天然倾向输出模糊分数。解决先用 4.2 节的词典修正拉分再看分数直方图调整阈值到 0.65 和 0.35如果仍然挤在一起说明这批评论本身情绪表达弱这时可以把中性当作真实结论输出而不是硬调参数制造虚假区分这种评论为主的样本中性占比高是正常的。6. 把情感分析和主题分析串起来负面主题集中挖掘与验证技巧前面情感分析在单条粒度打标签LDA 在全局分话题最后一步是把两张表合成一张能被业务使用的结果。关键动作是给每条评论打上它所属的主题编号再按主题统计情感分布。# 给每条评论分配主题编号取概率最大的主题 topic_dist lda.transform(dtm) df[topic] topic_dist.argmax(axis1) # 主题 × 情感交叉表一眼看出哪个话题是雷区 cross pd.crosstab(df[label_adj], df[topic]) print(cross)交叉表里如果主题 1 是价格贵、收费、不划算且负向数量远高于正向那这个主题就是本轮舆情的核心雷区。更进一步的技巧是把负面评论单独抽出来再跑一次 LDA全量 LDA 会把主流话题顶到前面而运营真正想看的大家具体在骂什么往往被正面或中性评论稀释掉。这个技巧在处理产品质量投诉时特别有效两万条评论跑完整体和负面子集两轮能看出来负面集中在物流还是售后还是产品本身。验证上我习惯随机抽 100 条评论人工标注情绪和模型输出的 label 对一遍算准确率重点是看中性被硬分成正负以及负向被漏判这两类错误的比例。有一次处理某品牌活动翻车的微博评论模型整体准确率有七成但负面召回率只有四成后来发现主要漏在这价格认真的我也是服气这类没有显式情绪词的讽刺评论上。从那以后我就养成了一个习惯任何情感分析上线前先看负向召回率而不是整体准确率因为舆情场景里漏掉负面比误伤正面代价更高。这个方案的价值不是一次把准确率做到满分而是给你一个每条评论可追溯、每类话题可定位的分析框架希望帮到你。本文还有配套的精品资源点击获取
返回列表