简介:压缩包覆盖新闻与微博评论情感分析的完整流程,将情感词典与机器学习分类模型相结合,适合自然语言处理学习者、数据挖掘初学者以及相关课题研究者使用。内容涉及哈工大情感词典、知网情感词典等基础工具,朴素贝叶斯、支持向量机、随机森林等分类建模方法,并提及特征工程与模型优化要点,帮助读者理解从文本特征提取到情感倾向判别的关键环节。包体共39个文件,整体2.63MB,以16个Python脚本、9个Markdown说明文档、7个CSV数据文件为主,另含少量备份文件。脚本覆盖数据爬取、关键词生成与情感分析等步骤,文档包含研究背景、数据获取说明、README等,便于边读边练、对照复现。目前已有33人学习/下载。对于希望快速上手文本情感分析流程的读者,这份资料可作为课程设计、小型项目或技术练手的有效参考。
1. 当情感词典遇上机器学习:新闻与微博评论的混合情感分析路线
做舆情系统的人大概都有过这种体验:用现成情感词典跑新闻评论,准确率勉强能看,一换到微博评论就崩。不是“难过”“开心”这类词管用,而是“我服了”“裂开了”“这也太秀了”让词典直接失语。反过来,直接上机器学习分类器,效果倒是好了,但训练语料够不够、标注准不准、换了领域会不会掉点,都是新问题。单纯用情感词典和机器学习做情感分析,都有各自够不着的地方,所以才有了这条更实际的技术路线:情感词典与机器学习结合,让词典负责先验情感,让分类器负责语境修正,两者的结合处理新闻与微博评论。这篇文章直接给你一套可以在本地跑通的最小实现,中间穿插我调参时踩过的坑。
2. 先理清两套工具箱的分工:词典负责先验,机器学习负责语境
2.1 情感词典的真正作用,是给分类器提供“情感先验特征”
很多初学者会把情感词典当成一个打分类器来用:分词后对着词典找词,找到正面词加1分、负面词减1分,最后分数大于阈值就判积极。这个做法在测试集上看起来还行,一上线就会露馅。原因在于,词典打分是一个纯词表匹配过程,它不感知词与词的组合关系,也不知道否定词作用在哪个词上。
我一般会把词典当成“特征生成器”,而不是“裁判”。同样是“今天天气真好,但心情不太好”这句话,词典会打出一个正分加一个负分,而分类器能学到的,是“但”这个转折词之后的情感才真正主导整句。如果直接把词典分数组装成特征向量喂给模型,模型就能同时看到“积极强度有多高”“消极强度有多高”“文本里有没有转折词”这些信息,它自己去判断该信哪个。
具体落地的时候,我会给每条样本生成三到五个词典特征:正向强度、负向强度、情感极性差值、情感词命中数、否定词修正后的强度。其中“否定词修正”是关键——把“不快乐”这种反向结构标出来,让“快乐”的分数在特征里记成负值,而不是简单地查不到“不”就当没看见。
2.2 为什么机器学习分类器能补上词典的短板:局部上下文与组合模式
情感词典漏掉的是语境,而机器学习分类器恰恰擅长抓语境。同样是“你这个方案真行”,正面词典命中“行”字会给正分,但实际语义可能是讽刺。这类反讽句,词典再怎么补词表都补不全,因为它是语义层面的现象。
分类器能做的,是学习组合模式。比如“真行”和“真不行”在词向量或TF-IDF特征空间里是两组不同的n-gram,模型见多了带“真行”的负面样本之后,会把这个n-gram跟负面标签绑定。换句话说,机器学习的价值不在于识别单个情感词,而在于识别“哪些词组合起来产生了偏移”。
选型上,我最常用逻辑回归做基线,而不是一开始就上BERT。理由有三个:一是逻辑回归模型小,现网可以毫秒级推理;二是它对词典特征非常敏感,特征权重可以直接加减日志解释清楚,甲方要报告好写;三是它不吃标注数据,几千条就能出一个稳定的基线。等到基线跑通、准确率不够再换LightGBM或者微调预训练模型,这样一个一个往上加,不会一上来就把问题复杂度拉满。
2.3 新闻评论和微博评论的词典覆盖率差异,决定了必须分开处理
新闻评论整体句子更长,逻辑更完整,书面词占比高,常见情感词典的覆盖率通常还不算太低。微博评论则是另一套生态:口语、网络流行梗、表情符号、短句碎片混杂,跟不上热梗的词典覆盖会直接掉到令人怀疑人生的程度。
这个差异带来一个直接结论:不要试图用一套词典和一套模型同时处理两种评论。新闻评论的预处理重点是长句拆分与转折结构,微博评论的重点是表情映射、口语词映射和“哈哈哈”这类情绪信号的保留。后面第4章我会专门展开这两个场景的处理细节。
回到工程实施上,一般做法是先准备一份通用情感词典(比如知网情感词典、大连理工情感词汇本体库,这些我在后面会提使用方法),再按领域往里补词。我不会直接拿一份词典就去跑微博,而是在微博语料上先做一次新词挖掘,跑词频加PMI,把微博里高频但词典没有的词筛出来人工确认后补进词典,这个迭代流程在最后一章会给具体操作。
3. 把情感词典和机器学习接起来:一个可复现的训练流程
3.1 先搭一个词典打分模块,把文本变成情感强度特征
动手之前明确一下目标:我们不是写一个“更好的情感词典”,而是写一个“把词典翻译成模型特征”的模块。下面这份代码是核心部分,完整实现里我还会补上停用词表和否定词表。
import re import jieba # 通用领域情感词典,每条记录格式: 词 极性(1正/-1负) 强度(1~5) # 实际使用时替换成本地词典文件的路径 lexicon = { "喜欢": (1, 4), "讨厌": (-1, 4), "开心": (1, 3), "难过": (-1, 3), "糟糕": (-1, 4), } negations = {"不", "没", "无", "非", "莫", "别", "休", "勿"} degree_adverbs = {"很": 1.5, "非常": 2.0, "太": 1.8, "有点": 0.7} # 程度副词,可扩充 def lexicon_features(text): words = jieba.lcut(text) pos_score = 0.0 neg_score = 0.0 hit_count = 0 scope = 1.0 # 否定/程度修饰作用范围 for i, w in enumerate(words): if w in negations: scope = -1.0 continue if w in degree_adverbs: scope *= degree_adverbs[w] continue if w in lexicon: polarity, strength = lexicon[w] hit_count += 1 val = strength * scope if val * polarity > 0: pos_score += abs(val) else: neg_score += abs(val) scope = 1.0 # 一个词处理完,重置修饰范围 return { "pos_score": round(pos_score, 2), "neg_score": round(neg_score, 2), "polarity_diff": round(pos_score - neg_score, 2), "hit_count": hit_count } if __name__ == "__main__": text = "今天天气不错,但心情有点糟糕" print(lexicon_features(text))这段代码的核心逻辑是:按顺序扫描分词结果,遇到否定词就把后续词的极性反转,遇到程度副词就放大或缩小极性强度。以“有点糟糕”为例,有点把强度缩放为0.7,糟糕极性为负,贡献的负向分数是3×0.7=2.1。
参数说明里有几个地方要特别留意:scope变量的作用范围是整个句子,而不是否定词后的一个词。对长句来说这样太粗糙,我通常在进入词典打分前会做一次分句,把“虽然……但是……”这类结构拆开,再分别打分并存成两个特征组,后面第4章细讲。另外,词典的强度值不是越大越好,我测试下来1到5的强度标定比1到3区分度好,比1到10好调。
3.2 向量化与词典特征的拼接:让模型看到文本和情感信号
词典特征构建好后,下一步是把原始文本向量化和词典特征拼接,形成最终训练数据。我用TF-IDF做文本向量化,子线性IDF公式在短文本上表现更好,词粒度放在1到2之间能抓到“不怎么行”这类组合模式。
import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from scipy.sparse import hstack # train_samples: DataFrame,包含 text 和 label 两列,label取值 pos/neg/neu train_samples = pd.read_csv("train.csv") # 实际训练数据需自行准备 sample_texts = train_samples["text"].tolist() tfidf = TfidfVectorizer( max_features=8000, # 词表上限,防止维度爆炸 ngram_range=(1, 2), # 1-gram + 2-gram,捕捉否定词+情感词组合 sublinear_tf=True, # 对TF做log变换,防止高频词压倒低频词 min_df=2, # 至少出现2次才保留,过滤罕见噪声 ) tfidf_matrix = tfidf.fit_transform(sample_texts) feat_rows = [] for t in sample_texts: feat_rows.append(lexicon_features(t)) feat_df = pd.DataFrame(feat_rows) # 把TF-IDF稀疏矩阵和词典特征横向拼接 X = hstack([tfidf_matrix, feat_df.values]) y = train_samples["label"].valuesmax_features=8000是在压训练时间和内存,如果你的语料规模大,可以放到20000。min_df=2过滤掉的不仅是噪声,也把微博里的一次性梗词清掉了,这对短文本是好事,因为模型不需要记住只出现一次的拼接模式。sublinear_tf=True值得特别说明:微博文本重复词多,比如“哈哈哈”占了大量词频,用线性TF它会被放大成主导特征,log变换后它就和正常词一个地位了,有情绪作用又不至于喧宾夺主。
这里面有一个容易忽略的细节:TF-IDF矩阵是稀疏的,而词典特征是多列密集值,拼接的时候我用了scipy.sparse.hstack处理,保证训练时数组类型统一。很多人在这直接拼成一个numpy数组,短文本数据量小还行,量大了立刻内存爆掉。
3.3 训练逻辑回归分类器,并让词典特征被模型“有条件地相信”
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) clf = LogisticRegression( C=1.0, class_weight="balanced", max_iter=1000, solver="lbfgs", ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred)) # 输出词典特征对应的权重,确认模型学到的是否合理 feat_names = tfidf.get_feature_names_out().tolist() + list(feat_df.columns) weights = clf.coef_[0] for name, w in zip(feat_names[-4:], weights[-4:]): print(f"{name}: {w:.4f}")class_weight="balanced"在这里非常重要。新闻评论和微博评论里中性样本占比远比正负样本高,如果不加这个参数,模型会为了追求整体准确率把所有样本都预测成中性,一看准确率70%以上,实际毫无可用性。逻辑回归的C=1.0是默认值,做基线先不动它,后面调参时再按“过拟合减小C、欠拟合增大C”的方式调。
跑完后一定要看一眼词典特征的权重。正常情况下pos_score的权重为正、neg_score的权重为负,hit_count的权重可能很小甚至为负——这不奇怪,模型发现情感词命中数量多反而可能是嘲讽句式堆砌,也是它在学习词典特征之外的组合模式。如果特征的权重符号和常识完全相反,优先检查标签有没有标反,而不是信任模型。
4. 新闻评论与微博评论的预处理与特征差异
4.1 微博评论的噪声清理与表情符号情感映射
微博评论的原始文本有大量噪声:@用户名、短链接、话题标签、表情符号和“哈哈哈”这类拟声词。常规预处理把它们删掉,但有些东西不能删,因为它本身就是情感信号。我的做法分两类:纯格式噪声直接清洗,表情和拟声词翻译成特征。
def clean_weibo(text): # 删除@、链接、话题标签的实体部分,保留话题文字 text = re.sub(r"@[\w\u4e00-\u9fa5_-]+", "", text) text = re.sub(r"https?://\S+", "", text) text = re.sub(r"#(.+?)#", r"\1", text) # 微博话题转换成纯文本 # 表情符号映射成情感标记词,后续词典打分和向量化都能利用 emoji_map = { "[哈哈]": " 开心表情 ", "[悲伤]": " 难过表情 ", "[怒]": " 愤怒表情 ", "[微笑]": " 友善表情 ", } for k, v in emoji_map.items(): text = text.replace(k, v) return text这里的处理思路是,表情符号对分词器是不可见字符,直接删掉会丢失强情感信号,直接保留又会被当成噪声。替换成“开心表情”这种占位词后,有两个直接好处:一是TF-IDF能把它作为一个词建模,二是如果占位词进了词典,打分模块也能覆盖。
一个我踩过的坑是“哈哈哈”的处理。一开始我在清洗规则里把连续的“哈”压缩成一个“哈”,结果模型对“哈哈”和“哈哈哈哈哈哈”的区分度消失了。后来改成保留长度信息:长度大于3的“哈”串单独映射成“大笑”,长度小于等于3的映射成“微笑”。模型明显能把“哈哈哈”和“哈哈哈笑死我了”这种极强正面信号从普通句子中分出来。
4.2 新闻评论的长句拆分:让转折结构不再迷惑词典
新闻评论和微博不同,长句占比高,句子里经常出现“甲好,但是乙不好”这种结构。词典打分面对这个句子时,正向和负向分数几乎一样高,polarity_diff接近0,等于把最有价值的情感信息洗掉了。解决这个问题,我不靠模型强行学,而是在预处理阶段动手:按转折词切句。
transition_words = ["但是", "但", "然而", "可是", "不过", "却"] def split_by_transition(text): parts = re.split("([但是然而可是不过却])", text) # 把每个转折词前后的内容组合成子句 clauses = [] current = "" for p in parts: if p in transition_words: current += p clauses.append(current) current = "" else: current = p clauses.append(current) return clauses def lexicon_features_news(text): clauses = split_by_transition(text) # 最后半个句子的情感权重更高,因为转折后通常是核心观点 num_clauses = len(clauses) pos_total, neg_total = 0.0, 0.0 for idx, clause in enumerate(clauses): weight = 1.0 if idx == num_clauses - 1: weight = 1.8 # 末句加权 feat = lexicon_features(clause) pos_total += feat["pos_score"] * weight neg_total += feat["neg_score"] * weight return { "pos_score": round(pos_total, 2), "neg_score": round(neg_total, 2), "polarity_diff": round(pos_total - neg_total, 2), "hit_count": sum(1 for c in clauses if lexicon_features(c)["hit_count"] > 0) }末句加权1.8这个系数是我从实际数据里试出来的,不是拍脑袋。新闻评论里转折后的部分确实是观点核心,偏移占比统计下来在七成以上。1.5效果不够明显,2.0会造成误伤,因为也有“虽然我不喜欢A,但我更不喜欢B”这种双重否定结构,这时候末句负向权重过重反而偏离本意。读者落地的时候建议从1.5开始调。
这套拆分逻辑只在新闻评论场景启用。微博评论句子短,硬加转折切分容易把“我还是个宝宝但是我要坚强”这种口语短句切成碎片,反而丢失连贯情感,所以微博和新闻要各走各的预处理函数。
4.3 标注类别选择与标注一致性控制
训练数据标注是整个流程里最容易被低估的一环。新闻评论和微博评论的标注规则不一样:新闻评论更多是客观表达,三分类(正/负/中)基本够用;微博评论里阴阳怪气的表达太多,三分类会把大量“实际负向但字面中性”的样本归错。我建议微博至少做四分类,把“吐槽”单独作为一类。
标注一致性我用Cohen's Kappa控制。两位标注员先各自标200条,Kappa低于0.6就重新对齐标注标准,高于0.7才开始正式标注。很多人忽略这一道工序直接开标,等模型训练完发现边界模糊的样本全是错标,回过头来返工成本非常大。
标签冲突样本的仲裁规则也要事先定死:遇到“情绪细腻但态度中性”的样本,按句子整体态度判断而不是按情绪词判断;遇到反讽无法从文本确认的,标记为中性而不是猜测。这条规则能显著降低标注方差,因为标注员之间最大的分歧就出现在这种“读起来有情绪但说不准正负”的句子上。
5. 混合情感分析避坑指南:四个高频翻车现场与排查方法
5.1 词典漏词导致微博评论集体“失声”
现象:模型在新闻评论验证集F1约0.85,切到微博评论F1直接掉到0.6出头,而且预测结果大量偏向中性。人工抽检发现“绝绝子”“无语子”“下头”这些词在文本里出现了,但词典一个都没命中,词典特征全为0,TF-IDF特征里它们只出现一次或两次,模型学不动。
原因:通用情感词典覆盖正式书面语,对网络流行语天然滞后。这不是模型问题,是特征源头缺了信息。
解决:从训练语料里做PMI新词挖掘,把高频但词典没有的词筛出来。具体做法是统计兜底预测错误样本,对这些样本里的词按词频和卡方值排序,Top 20的词人工打标后补进词典。我一般每周做一次这个动作,词典从最初的几千词扩到一万多,微博F1能拉回0.75左右。词典扩展不是越多越好,补进去的词如果只在很小一部分样本里出现,反而会变成过拟合噪声。
5.2 “笑死我了”明明很开心,模型却判成负向
现象:评论“笑死我了”被预测为负面,“这颜值绝了”被预测为负面,“太强了吧”被预测为中性。抽检发现是词典命中了“死”“绝”这类词,给了负向分数,模型又对词典特征给了信任权重。
原因:词典打分看单面词,没有建立“某种强烈情绪的表达”这个上层的模式。在新闻域“死”是负面词没错,但在微博域“笑死我了”是一个整体上的正面夸张表达。
解决:我给词典打分加了一个“夸张构建”规则——如果文本中同一句里出现两个情感词,且其中一个为负向但强度低于另一词,分数取绝对值大的那个。更完整做法是专门跑一个“微博夸张表达词典”,把“笑死”“绝了”“跪了”“醉了”这类词直接标成强度更高的正或负,覆盖掉原词典里的基础词义。这类词单独维护,不进通用词典,避免污染新闻域的判断。
5.3 新闻评论里“下跌”“亏损”被判成负面,基金评论区完全跑偏
现象:新闻和微博的混合模型用在基金讨论区时,把“今天基金净值下跌,但我加仓了”判为负面。用户要的是“操作决策情绪”,不是“行情描述情结”。
原因:词典里“下跌”是负极性词,机器学习学到的是语义上的负向,但没有区分“描述客观事实”和“表达主观情绪”。这不是词典和模型结合的问题,而是“情感”和“态度”的区别问题。
解决:对新闻评论这个具体场景,我给BERT加了一个辅助特征——文本里是否存在市场描述类实体词。落地上更简单的做法是准备好一份“客观陈述词白名单”,命中白名单的句子先把词典负向分数减半再进模型。基金讨论区实测这个改动能把“下跌但加仓”这类句子从负向扳回正向或中性。白名单要按业务域维护,不能通用。
5.4 TF-IDF 和词典特征模块在跨场景迁移时出现维度不一致
现象:在新闻域训练好的模型,直接拿微博测试集做预测时程序报错,提示特征数量不一致。
原因:训练集的TF-IDF词表在fit时固定了,微博文本里有大量训练集没出现过的词,transform时新增词被忽略,理论上维度应该一致。但如果词典扩充后没有重新训练tfidf,特征矩阵列数就变了。通常错误根源是代码里把两个时间点的模型文件混用了。
解决:词典扩充后,必须重新fit整个TF-IDF的transform流程,不能用旧词表。我给工程落地时的建议是:把词典和TF-IDF的词表一起序列化保存,形成一个独立的“预处理包”,模型文件同时绑定这个预处理包的版本号。哪怕只是加了一个词,也要升版本,否则线上预测就会出现离线重来都复现不了的奇怪结果。
6. 调参与验证:从“能跑通”到“敢上线”的实操细节
基线跑通之后,真正花时间的不是改模型,而是调特征和做错误分析。我的验证习惯是先不看准确率,先看每个类别的召回率。情感分析里中性的召回率虚高是常态,因为大部分样本本身就偏中性。正负两个类别的F1才是这个模型真实水平的写照。
调参上我不做网格搜索,而是按“文本特征 → 词典特征 → 模型参数”的顺序逐个试。文本特征先看ngram_range,从(1,1)调到(1,2)通常能涨3到5个点的F1,代价是训练时间翻倍,如果涨得少就退回。词典特征的权重调整方式是做消融:把四个词典特征逐一置零,跑一轮看F1掉多少,掉的越多的特征越关键,这说明目前的结合方式吃到了词典信号。模型参数最后调,C从0.1到10之间取几个值,看验证集F1的曲线。
错误分析我固定用一个办法:每轮迭代后抽50条预测错的样本,人工分成“词典没覆盖”“词典误导”“标注错”“模型学不出来”四类。统计占比最好能出一个表,但这里直接说结论——前期“词典误导”和“词典没覆盖”占比极高,说明词典模块还有优化空间;后期如果“模型学不出来”占比升高,就该考虑上更强的模型,比如在词典特征基础上加一个预训练向量层。
词典迭代是整个方案里回报最高的动作。我现在每次拿到一批新标注数据,第一件事不是训练模型,而是先跑一遍“new word mining——人工确认——补词典——重新打分”这个流程。跑完再训练,F1基本都会有正向提升。机器学习部分反而是稳定的,它没有那么多惊喜,但也轻易不会崩。
一个让我记忆深刻的教训是:有一次我把微博语料里的“哈哈哈哈”全部压缩成“哈”之后,模型的负面分类F1掉到0.5以下。查了半天发现是大量“哈哈哈哈笑死我了”被当成中性处理了。之后我养成一个习惯,改任何预处理逻辑之前,先跑一轮词典特征的分布对比,确认改了之后不会把情感信号误伤掉。这个习惯帮我少走很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取