新闻与微博评论情感分析:词典与机器学习融合实战 📅 发布时间:2026/9/16 6:10:24 👁 浏览次数: 简介面向新闻与微博评论场景的情感分析项目将哈工大情感词典、知网情感词典与朴素贝叶斯、支持向量机、随机森林等机器学习方法相结合适用于舆情监测、用户反馈挖掘等文本倾向性判断任务。压缩包共39个文件包含16个Python脚本、9个Markdown说明文档、7个CSV数据文件以及若干备份文件整体容量2.63MB。脚本覆盖情感词典关键词生成、新闻爬虫含Spyder实现与微博数据处理等环节Markdown文档梳理了数据获取、研究背景与项目报告CSV则用于模型训练与特征分析。现有33人学习下载示例数据围绕“肺炎”等四阶段新闻标题展开可帮助初学者理解情感词典构造、机器学习建模与中文文本预处理全流程。资料来自网络分享仅适合学习交流不可用于商业用途。1. 评论情感分析不是二选一词典和机器学习要放进同一条链路做新闻和微博评论的情感分析最常踩的坑是二选一迷信词典规则的团队几轮迭代后会发现微博新词永远追不完直接上 BERT/LSTM 的团队又经常在新闻评论的长句反讽上栽跟头模型把“真是太好了”判成正向而人眼一看就知道是反话。真正有效的路径不是拿词典去对抗模型而是把词典当成模型的先验信号在特征层、输入层甚至决策层做融合。这篇文章会沿着这条路来拆先解决情感词典怎么为新闻与微博两个场景扩词再给出三种能落地的融合实现接着处理两个场景的预处理差异最后聊如何用消融实验验证“词典增强”到底值不值得加。新手能照着代码跑通全流程熟手能从这里边界条件、参数取舍和验证方法里找到有用的信息。2. 情感词典从哪来基础词表、领域扩充与 SO-PMI 取舍2.1 别急着拿现成词典打分新闻和微博的词分布差得很远通用情感词典的问题在于覆盖率更在于“词感”错位。新闻评论里大量出现“令人担忧”“不予置评”“漏洞百出”这类书面表达而微博评论里高频的是“绝绝子”“yyds”“太emo了”。这两类词在同一个词典里的命中情况完全不同直接用一份通用词表去跑线上数据会出现两种典型偏差新闻侧的无情感词被误判出情感微博侧的网络用语根本匹配不到。所以动手做融合之前先得明确一个事实情感词典不是下载一个包就能用的“成品”它是需要结合语料做领域适配的“半成品”。你的标题里同时出现新闻和微博就意味着词典要么做两份领域扩充要么在构建时就把两类语料都喂进去让词典本身先学会在两个场景里工作。2.2 基础情感词典选型通用资源与情感强度常见做法是先选一套通用情感词典做底再用领域语料往上补词。工业项目和学术项目里用得比较多的三类基础资源各有侧重资源覆盖特点适用场景主要局限大连理工大学情感本体库情感类别细有强度划分中文通用情感分析适合做强度加权的底座网络用语覆盖少词表更新慢HowNet 情感分析词集中英双语正负分类明确新闻这类书面语场景缺少强度值口语词少NTUSD 台湾大学情感词典量级大正负词对形式扩充候选词适合做后合并用词偏书面和台式表达如果只让我选一套做底我会选带强度值的本体库。原因在后面融合时会体现模型需要的是一个“痛恨5不满2”这样的连续信号而不是“痛恨负不满负”这种被打平的二值标签。你把强度丢掉了等于把词典里最值钱的信息留在了仓库里。注意这三类资源各有其组织形式使用时建议统一转换成同一个 JSON 结构{ 词条: { polarity: -1, strength: 4, pos: verb, source: dut } }保留词性是因为新闻评论里的“坑”作名词和作动词情感含义并不相同保留来源则方便出现冲突时追溯词条获取渠道。2.3 用 SO-PMI 从评论语料里挖出词典里没有的极性词基础词表搞定后网感词的缺位依然存在。一般做法是用 SO-PMISemantic Orientation Pointwise Mutual Information从领域语料里自动挖掘候选情感词原理很简单一个词如果和正向种子词高频共现它就偏正向和负向种子词高频共现它就偏负向。import math from collections import Counter, defaultdict import jieba def build_seed_words(): pos_seed [好, 赞, 喜欢, 推荐, 棒] neg_seed [差, 烂, 坑, 垃圾, 失望] return pos_seed, neg_seed def so_pmi(corpus, window4, threshold0.3): pos_seed, neg_seed build_seed_words() co_occur defaultdict(int) word_count Counter() total 0 for text in corpus: words jieba.lcut(text) for i, w in enumerate(words): if len(w.strip()) 2: continue word_count[w] 1 total 1 # 以当前词为中心左右各 window 个位置为共现窗口 context words[max(0, i - window): i window 1] for seed in context: if seed in pos_seed or seed in neg_seed: co_occur[(w, seed)] 1 result {} for (w, seed), cnt in co_occur.items(): if w in pos_seed or w in neg_seed: continue pmi_value math.log2((cnt / total) / ((word_count[w] / total) * (co_occur[(w, seed)] and 1))) # 上面是简化近似正式项目建议用文档频次替换词频 if seed in pos_seed: result[w] result.get(w, 0) pmi_value else: result[w] result.get(w, 0) - pmi_value return {w: score for w, score in result.items() if abs(score) threshold}这段代码里有两个参数值得细说。window控制共现半径微博短文本建议设 3 到 4新闻长句可以放宽到 5窗口太大噪音词会被拉进来太小又捕捉不到搭配关系。threshold控制筛选强度0.3 是一个比较宽松的值适合先出一批候选词再过人工复核。注意这里为了演示做了简化把种子词的共现频次近似当作词频使用正式做的时候建议分别统计每个种子词的出现次数再对正负得分分别求和效果会稳定得多。2.4 别把词典做成单值打分输出结构要保留组合规则词典扩充完毕后下一步是定义“怎么用”。最容易踩的坑是每个句子只算一个情感总分然后把它当特征扔给模型。这个总分确实有用但它丢失了否定词、程度副词和转折关系的信息。比如“不是很满意”里的“不是”会翻转“满意”的极性你不做窗口扫描词典就会把这句话判成正向。我一般会在词典打分函数里加一个否定词窗口范围为当前情感词前 3 个词内。命中否定词就把情感强度取反命中程度副词“非常”“有点”“极其”则把强度乘上一个系数。这一步产出的不是单一分数而是一组特征正负情感词计数、加权得分、经过否定翻转后的最终得分、程度副词数量。这组信号才是模型真正需要的输入后面第 3 章的特征融合接的就是这个输出。3. 把词典接入机器学习模型三种能上线的融合实现3.1 特征融合TF-IDF 之外接上情感向量喂给分类器这是最简单也最容易上线的做法适合数据量不大、从零起步的场景。思路是把词典产生的结构化特征和常规文本特征拼在一起喂给 LR、SVM 这类机器学习分类器。特征融合有两个直接好处一是词典信号能起到规则先验的作用在标注数据不足时给模型兜底二是每个情感特征都有明确的系数模型的可解释性还在。import scipy.sparse as sp from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import f1_score def build_senti_features(texts, lexicon): features [] for text in texts: words jieba.lcut(text) pos_cnt, neg_cnt, score 0, 0, 0.0 for i, w in enumerate(words): if w not in lexicon: continue item lexicon[w] # 检查前3个词内是否有否定词 prefix words[max(0, i - 3): i] sign -1 if any(w in prefix for w in [不, 没, 别, 无]) else 1 score sign * item[strength] * item[polarity] if item[polarity] 0: pos_cnt 1 elif item[polarity] 0: neg_cnt 1 features.append([score, pos_cnt, neg_cnt]) return sp.csr_matrix(features) X_tf TfidfVectorizer(max_features8000, ngram_range(1, 2), sublinear_tfTrue).fit_transform(X_train) X_senti build_senti_features(X_train, lexicon) X_all sp.hstack([X_tf, X_senti]) clf LogisticRegression(C1.0, class_weightbalanced, max_iter1000) clf.fit(X_all, y_train)参数上max_features设 8000 是因为评论语料里大量低频词对分类没有贡献反而会让维度爆炸sublinear_tfTrue是对词频做对数缩放压制高频词的主导地位class_weightbalanced用于处理情感标签不平衡新闻评论里中立样本往往远多于正负样本。这三个参数在 LR 和线性 SVM 上表现都比较稳定可以作为默认起点。3.2 层融合把情感先验拼进 LSTM 的输入层如果数据量够大、文本长度较长可以考虑用 LSTM 这类序列模型。李宏毅机器学习课里反复强调的“输入特征决定模型上限”在情感分析任务里体现得很明显LSTM 能学到上下文依赖但它学不到词典里那种显式的情感强度。所以常见做法是把情感特征当作额外的输入通道和词向量在时间步上拼接让模型在每个位置都能看到规则信号。import torch.nn as nn class SentiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim, senti_dim, hidden_dim, num_layers1): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.senti_fc nn.Linear(senti_dim, senti_dim) self.lstm nn.LSTM( input_sizeembed_dim senti_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue ) self.classifier nn.Linear(hidden_dim, 2) def forward(self, token_ids, senti_feat): emb self.embedding(token_ids) # [B, T, embed_dim] senti self.senti_fc(senti_feat) # [B, T, senti_dim] lstm_in torch.cat([emb, senti], dim-1) out, _ self.lstm(lstm_in) return self.classifier(out[:, -1, :])senti_dim取决于你从词典里提取了几种信号通常设 3 到 5比如正负情感词计数、加权得分、否定翻转得分各占一维。senti_fc这个线性层不是必须的但我一般会留着它能让模型学习如何缩放和偏移词典信号而不是让原始强度值直接主导梯度这对 BiLSTM 的收敛更友好。拼接完成后LSTM 的输入维度变成embed_dim senti_dimout[:, -1, :]拿的是最后一个时间步的隐状态再接分类层输出情感类别。3.3 集成交互词典分值与模型概率的配合第三层融合发生在模型之外思路是把词典打分系统和机器学习模型当作两个独立的分类器再做结果集成。这里不做复杂 Stacking只做加权平均就够用词典给出的是规则分数模型给的是类别概率两者尺度不同需要先做归一化。融合策略数据需求可解释性主流适用场景主要风险特征拼接几百条标注起步高LR 能看到特征系数冷启动、快速上线特征被稀疏 TF-IDF 淹没输入层拼接 LSTM需要数千条以上低模型内部不可视长文本、上下文依赖明显词典噪音被模型放大模型外加权集成需要 Dev 集调权重中权重含义直观团队已有两套独立系统两套系统同源同错时无增益这三种策略不是互斥的线上系统完全可以在特征融合的基础上再做一次外层集成。权重怎么定不靠拍脑袋在 Dev 集上用网格搜索最小化误差即可。另外要留意词典和模型若在同一份语料上训练错误往往是相关的集成收益会缩水保持两套信号来源的异质性比单纯调权重更有价值。4. 新闻评论与微博评论的适配同一个词典两套管线4.1 场景差异长句反讽 vs 短句网络用语把同一个融合模型直接从新闻评论迁移到微博通常会出现性能滑坡。两个场景的文本属性差异太大新闻评论平均长度在几十到上百字具有较强的篇章结构情感经常藏在转折和反讽里微博评论短则几个字多则一百多字口语化程度高表情符号和网络新词承担了大量情感信息。这不是换个分词器就能解决的问题而是预处理管线的整体差异。下面两节分别处理这两个场景的各自瓶颈并给出对应的处理代码。4.2 新闻评论的句子级聚合与转折处理新闻评论的特点决定其更适合“先分句、后聚合”的策略。常见做法是先把整段评论按标点切分成句子对每个句子单独计算情感得分然后再加权聚合成篇章级分数。其中要特别处理转折关系——“但是”“然而”之后的句子通常承载了作者真实态度权重应该适当提高。import re def news_sentiment(text, lexicon): sents re.split(r[。!?;], text) total_score 0.0 weight 1.0 turn_words [但, 然而, 不过, 可是, 却] for sent in sents: if any(w in sent[:3] for w in turn_words): weight 1.5 sent_score dict_score(sent, lexicon) # 词典打分函数 total_score weight * sent_score return total_score / max(1, len(sents))weight的取值可以进一步学习但 1.5 是一个现金经验值在多数新闻评论语料上都能带来小幅提升。分句粒度带来的另一个好处是能观察到评论内部的“褒贬转折”比如先肯定成绩再批评问题这类评论如果不做分句整体得分会被拉向中立信息被损耗得很严重。4.3 微博评论的 emoji 先验与网络用语兜底微博评论的语料很短分句反而会把上下文拆碎。更有效的方式是保留整句同时把 emoji 当作额外的情感信号接入。严格来说这属于多模态情感分析的一部分但实际落地时不需要训练视觉模型做法是将高频 emoji 映射成情感先验分数与词典得分做加和emoji_map {: 0.4, : -0.5, : 0.8, : -0.8, ❤️: 0.6, : -0.7, : 0.7} def weibo_sentiment(text, lexicon): score dict_score(text, lexicon) emoji_score 0.0 for e, v in emoji_map.items(): if e in text: emoji_score v return score emoji_score需要说明的是表情符号的极性在不同社区有时是相反的比如“”在笑点低的语境里可能是正向但在嘲讽语境里会带负面含义。建议先统计自己在手语料中 emoji 与文本情感标签的相关性再决定映射方向和权重。两个场景的管线差异总结成一张对照表预处理维度新闻评论微博评论切分粒度按句切分再做加权聚合整句直接打分不切分否定词窗口前后各 3 词前 2 词即可短句无需大窗口转折词处理需要增强转折后句子权重基本不需要短句转折出现少emoji / 表情符号可忽略必须处理做先验分数映射词典扩充重点书面词、领域词、反讽表达网络新词、缩写、emoji 候选词典中未命中的兜底交给模型上下文特征交给模型 emoji 先验一起兜底两套管线的差异本质上由“信息载体位置”决定新闻的情感信息分布在句子结构和修辞关系里微博的情感信息集中在词汇本身和符号上。5. 消融验证与词典增强的调优技巧5.1 先搭一个消融实验框架再谈调优词典融合到底贡献多少不能靠直觉判断要跑对照实验。最基本的设置是四条线纯词典规则、纯 TF-IDF LR、特征融合3.1、输入层拼接 LSTM3.2。用同一个数据划分和统一评估脚本在宏平均 F1 上对比差距。如果特征融合版比纯模型版低说明词典信号和模型信号的方向存在冲突第一步不是调参数而是检查词典打分里极性标注是否正确。下面是一个可直接套用的评估骨架from sklearn.model_selection import cross_val_score configs { dict_only: make_dict_rule_model(), lr_tfidf: make_lr_tfidf(), lr_fusion: make_lr_fusion(), lstm_senti: make_lstm_senti(), } for name, model in configs.items(): scores cross_val_score(model, X, y, cv5, scoringf1_macro) print(f{name}: {scores.mean():.4f} (/- {scores.std():.4f}))如果lr_fusion相比lr_tfidf的提升不足 0.5 个点就要去查情感特征是否被 TF-IDF 维度稀释了常见做法是对情感特征做缩放或提升其在拼接矩阵中的权重。5.2 词典得分先做秩归一化再入模型直接拿词典的原始强度拼接进模型有个隐患不同句子命中的情感词数差异很大长句得分天然比短句高这会让模型把“长度”当“情感强度”。解决办法是把得分转换成语料中的百分位秩把绝对的强度差异变成“这句在语料中相对强弱”的相对差异。5.3 集成时用 logit 变换而不是原始概率在模型外做集成时模型输出的概率集中在 0.2 到 0.8 之间直接拿这个值和词典分数做加权词典信号会被压缩得很小。先把概率变换到对数几率空间再做加权会让两类信号的可比性更强。import numpy as np from scipy.special import logit p np.clip(proba, 1e-7, 1 - 1e-7) # 防止 logit 溢出 logit_p logit(p) final_score 0.35 * rule_score_norm 0.65 * logit_p权重取值 0.35 和 0.65 需要在 Dev 集上重新搜索。搜索时注意观察一个现象如果最优权重无限偏向模型一侧词典融合大概率只是“加了但没有完全加进去”此时更应该回到 5.1 的消融实验查特征构造环节而不是继续调集成权重。本文还有配套的精品资源点击获取