融合情感词典与机器学习:中文短文本情感分析实战指南
2026/9/23 18:24:08 网站建设 项目流程

简介:面向新闻与微博评论的情感分析场景,这份资源整合了情感词典与机器学习两种主流技术,适合自然语言处理初学者、数据挖掘工程师及舆情分析人员学习参考。资源共有39个文件,压缩包整体仅2.63MB,其中16个Python脚本覆盖了新闻与微博数据的爬取、情感词典的自动生成、关键词语料构建以及朴素贝叶斯、支持向量机和随机森林等分类模型的训练与对比;9个Markdown文档系统整理了研究背景、数据获取说明和实验报告;7个CSV文件包含情感标注样本数据,可供直接实验;其余为zbak备份文件与附赠的zip压缩包,目录结构简洁,便于按需查阅。目前该资源已有33人学习下载。通过本地代码与文档,读者能够完整走通“数据采集—情感词典构建—特征工程—模型训练—结果分析”的流程,并可将爬虫脚本和特征选择思路迁移到自己的情感分析或舆情监测项目中。

1. 情感分析为什么要把情感词典和机器学习放在一起

拿到新闻评论和微博评论做情感分析,最直接的思路是跑一个深度学习模型,或者拿情感词典去匹配。但落到短文本舆情情感倾向分析系统里,这两条路都有明显短板:纯词典匹配对没见过的新词、反讽和口语化表达无能为力;纯机器学习模型则依赖大量标注语料,而且训练出来的结果像个黑匣子,出了问题很难定位。把情感词典和机器学习结合,等于给模型装上一套可解释的先验规则,再用统计模型去补词典覆盖不到的上下文。这套方案在中文短文本上往往比单用任何一种都稳,适合刚接触自然语言处理、想快速做出可用结果并持续迭代的工程师。

2. 先跑通词典基线:选词典、写打分器、产出情感特征

2.1 词典选型:主流公开词典与格式归一

中文情感分析领域有几套公开的情感词典经常被拿来当底座。知网(HowNet)的情感分析用词语集是最老牌的一套,按褒义、贬义、程度级别拆分,适合做规则基线;大连理工的情感本体库带词性、极性强度和情感类别,信息最全,适合做细粒度分析;台湾大学 NTUSD 提供正负两个词表,格式最轻量;BosonNLP 情感词典是从评论语料里抽取的,覆盖口语和网络用语,对微博场景特别友好。

这些词典格式差别很大,拿到手第一件事是统一格式。我一般会整理成一个两列文件:第一列是词,第二列是情感分值,正值代表正面、负值代表负面。强度信息从大到小映射成分数,比如“极其”类程度词给 2.5,“很”类给 1.8,“有点”类给 0.5。用 pandas 处理 Excel 或 CSV 格式的原始词典,然后统一输出成 tab 分隔的文本,这一步虽然枯燥,但后面所有环节都依赖它。

import pandas as pd # 读入大连理工情感本体库格式的词典,选“词语”和“情感分值”两列 df = pd.read_excel("dutir_emotion.xlsx") df = df[["词语", "情感分值"]].dropna() df.to_csv("sentiment_dict.tsv", sep="\t", index=False, header=False) # 看看整理完之后的数据 df.head()

这段代码做的事很直接:把 Excel 格式的原始词典降维成两列,去掉没有分值的行,再存成 tab 分隔的纯文本。这样后面打分器加载时不需要依赖 pandas,一个普通的文件遍历就能读完。需要留意的是,不同版本的情感本体库列名可能不一样,有的叫“词条”不叫“词语”,到手后先打印前几行确认列名再改代码。

2.2 写一个最小可用的中文情感打分器

词典基线的核心是一个打分函数:对输入文本分词,逐词查词典,把命中的情感分值累加。这里有两个规则必须做:否定词翻转和程度副词加权,否则“不开心”会被算成“开心”的正向分,“非常讨厌”的强度会被低估。

import jieba NEG_WORDS = {"不", "没", "无", "非", "莫", "未", "别", "勿", "休"} DEGREE_WORDS = {"非常": 2.0, "很": 1.8, "挺": 1.5, "太": 1.8, "有点": 0.5, "稍微": 0.7} def load_sentiment_dict(path): word_score = {} with open(path, encoding="utf-8") as f: for line in f: parts = line.rstrip("\n").split("\t") if len(parts) < 2: continue try: word_score[parts[0]] = float(parts[1]) except ValueError: continue return word_score def score_text(text, word_score): words = jieba.lcut(text) total = 0.0 hit_count = 0 for i, w in enumerate(words): score = word_score.get(w, 0.0) if score == 0.0: continue # 情感词前一个位置是否出现否定词 if i > 0 and words[i - 1] in NEG_WORDS: score = -score # 情感词前一个位置是否出现程度副词 if i > 0 and words[i - 1] in DEGREE_WORDS: score *= DEGREE_WORDS[words[i - 1]] total += score hit_count += 1 return total, hit_count

这里否定词和程度副词的窗口都只回看一个词,这是有意的简化。中文里“不太开心”这种结构,否定词和程度副词同时出现在情感词前面,光看前一个词会漏掉“太”,后续第 5 章会专门说怎么处理更复杂的否定窗口。hit_count表示命中了几个情感词,它对短文本很有用:一条 20 字的微博命中 5 个情感词,和一条 200 字的新闻命中 5 个情感词,情感浓度完全不同,所以这个计数要单独作为一个特征带出去。

2.3 打分结果如何变成特征,而不是直接当结论

词典打分最常见的使用误区是直接拿分值做正负判断。实际上词典分值作为分类结论非常脆:阈值取 0 还是 0.5,结果差很多;而且微博里的反讽、新闻里的客观陈述,词典根本判断不了。更稳的做法是把分值、命中数这类信息打包成特征,喂给后续的机器学习模型。

import pandas as pd from sklearn.preprocessing import StandardScaler df = pd.read_csv("comments.csv") df["score"], df["hit"] = zip(*df["text"].apply(lambda x: score_text(x, word_score))) # 归一化,避免量纲差异影响模型 scaler = StandardScaler() df[["score", "hit"]] = scaler.fit_transform(df[["score", "hit"]]) df.head()

注意fit_transform的调用位置:这一步应该只对训练集调用,测试集要用同一个scalertransform。这也是一个经典的数据泄露来源,第 5 章会单独展开。归一化之后,情感分值变成了均值为 0、方差为 1 的连续特征,逻辑回归这类对输入尺度敏感的模型才能稳定收敛。

3. 机器学习分类器怎么接:特征拼接、模型选型和调参

3.1 训练集构造:新闻与微博要分开看待

词典基线跑通之后,下一步是训练一个机器学习分类器。首要问题是训练数据从哪来、标签怎么定。常见的做法是爬取公开评论数据后做人工标注,新闻评论和微博评论各标一部分,不要混在一起标。

类别设计上,我建议从二分类开始:正面和负面。三分类里的“中性”是标注一致性最差的类别,同一个句子,有人觉得是客观陈述,有人觉得是隐含负面——这会直接拉低模型上限。等二分类的准确率和 F1 稳定在 85% 以上,再考虑引入中性类。每类样本量建议至少 2000 条起步,太少的话后面的机器学习模型学不到稳定的模式。

新闻和微博这两个域的文本差异非常大。新闻评论句子长、用词书面、情感表达含蓄;微博评论短、口语化、网络新词多、表情符号密集。如果业务上两个域都要覆盖,训练时要给每条样本带一个域标签(news 或 weibo),后面作为特征拼进去,或者干脆分域训练两个模型。混合训练不加任何区分,模型往往会被数据量大的那个域带偏。

3.2 特征工程:TF-IDF、N-gram 与情感特征拼接

文本分类最经典的特征是 TF-IDF,但单独使用 TF-IDF 时情感先验完全被忽略。把词典打分结果作为稠密特征拼到 TF-IDF 的稀疏矩阵后面,是词典与机器学习结合最朴素也最稳定的形式。

from sklearn.feature_extraction.text import TfidfVectorizer from scipy.sparse import hstack import numpy as np tfidf = TfidfVectorizer( ngram_range=(1, 2), max_features=20000, min_df=2, sublinear_tf=True ) X_tfidf = tfidf.fit_transform(train_text) # train_emotion_score 是第 2 章算出来的词典分值,train_hit_count 是命中数 extra = np.column_stack([train_emotion_score, train_hit_count]) X_train = hstack([X_tfidf, extra])

ngram_range=(1, 2)让模型能看到“不错”和“不 错”的区别,对中文短文本尤其重要,代价是特征维度上升,所以max_features限制在 20000。min_df=2去掉只出现一次的罕见词,既能降噪又能减少内存占用。sublinear_tf=True对词频做对数缩放,防止高频词主导特征权重。情感分值特征只有两列,但对模型的贡献往往排在前二十名以内——这就是先验知识的作用。

3.3 模型选型与调参:逻辑回归还是 SVM

短文本情感分类属于典型的高维稀疏文本问题,逻辑回归和线性 SVM 是性价比最高的两个选择。它们训练快、可解释性强、对稀疏特征友好;非线性 SVM 或深度学习模型在数据量不够大的时候反而容易过拟合,而且调参成本高。

from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV from sklearn.metrics import f1_score, classification_report model = LogisticRegression(class_weight="balanced", max_iter=1000) param_grid = {"C": [0.1, 0.5, 1, 5, 10]} grid = GridSearchCV(model, param_grid, cv=5, scoring="f1", n_jobs=-1) grid.fit(X_train, y_train) print(grid.best_params_) y_pred = grid.predict(X_test) print(classification_report(y_test, y_pred))

class_weight="balanced"是必须加的,评论数据里正面和负面样本很少均衡,尤其是微博评论,负面往往多出一大截。加了这个参数后,少数类的召回率会有明显改善。搜索范围为什么只调 C?因为逻辑回归在文本任务上,C 是最敏感的参数,其他参数(如 solver、penalty)用默认值通常就够了。评分指标用f1而不是accuracy,在类别不平衡的场景下,accuracy 会给人虚假的安全感——你猜全是负面也能有 80% 的准确率。

4. 词典与模型的三种结合方式:特征路径、伪标注与规则修正

4.1 特征拼接是默认路径:为什么先选它

把词典分值作为特征拼进模型,是最直接的结合方式。它的好处有三个:实现成本极低,几行代码搞定;可解释性强,训练完成后能直接看coef_里情感特征系数的大小;对模型没有侵入性,随时可以移除对比效果。

import numpy as np model = grid.best_estimator_ feature_names = tfidf.get_feature_names_out().tolist() + ["emotion_score", "hit_count"] coef = model.coef_[0] # 按系数绝对值排序,看哪些特征对预测影响最大 top_features = sorted(zip(feature_names, coef), key=lambda x: -abs(x[1]))[:10] for name, c in top_features: print(f"{name}: {c:.3f}")

查看系数这一步值得养成习惯。emotion_score的系数如果是正的,说明词典打分方向和模型预测方向一致,这是一个基本的健康检查。如果系数是负的,说明训练集标注和词典打分存在系统性冲突,先去查数据标注问题,不要急着调参。

4.2 伪标注:用词典扩充训练数据的实验性做法

当标注数据不够,而词典覆盖度又比较高时,可以尝试伪标注:对无标注文本用词典打分,取绝对值高于某个阈值的样本,直接赋予正负标签,加入训练集。这个做法的逻辑是“高分样本的极性足够确定,可以当标注用”。

pseudo_data = [] for text in unlabeled_texts: score, hit = score_text(text, word_score) if score >= 3.0: pseudo_data.append((text, 1)) elif score <= -3.0: pseudo_data.append((text, 0))

阈值取 3.0 是我常用的起点,具体要看词典打分分布来定。注意伪标注是一把双刃剑:它确实能增加训练数据量,但词典本身的错误也会被模型学进去。我的建议是把它当成对比实验来做——训练一个用伪标注增广的模型,和一个不用的模型,在验证集上对比,如果 F1 没有提升就果断放弃。不要默认它一定有效。

4.3 规则修正:在模型输出上做最后的“兜底”

特征拼接是让模型自己学词典的用法,规则修正则是在模型输出之后强行兜底。当词典打分绝对值非常大,说明情感信号足够强烈,这时候即使模型预测反了,大概率是模型的问题,可以用词典分数覆盖模型结果。

def rule_correct(prob_positive, emotion_score, strong_threshold=3.0): pred = (prob_positive >= 0.5).astype(int) if emotion_score >= strong_threshold: pred = 1 elif emotion_score <= -strong_threshold: pred = 0 return pred

这个规则的使用场景很明确:对负面漏报特别敏感的业务,比如舆情监控,宁可多抛一些疑似负面让人工去看,也不能漏掉。阈值设高了兜不住,设低了又干扰模型本身的判断,一般从 3.0 开始尝试,写个小脚本扫描 2.0 到 5.0 区间、在验证集上做 F1 对比再定。

4.4 新闻和微博的预处理差异

两个域的预处理策略不一样。新闻评论需要保留标点符号,感叹号和问号本身就是情感信号,比如“这是好事!”和“这是好事。”传达的情绪完全不同。微博评论则要处理 @ 用户、URL、话题标签和表情符号:@ 和 URL 可以直接替换成占位符,表情符号建议单独映射成情感特征,例如把“开心”“笑脸”类 emoji 映射成 +1,把“生气”“流泪”类映射成 -1。这些映射后的特征可以和词典打分一起拼进特征矩阵,成本低,但能明显提升微博场景的召回。

5. 新闻与微博评论的五个常见坑:现象、原因、解决办法

5.1 分词把新词切碎,词典匹配不上

  • 现象:模型整体指标还行,但单独看微博新词相关样本,如“绝绝子”“集美们”“栓Q”,预测几乎全错。
  • 原因:jieba 这类通用分词器依赖词库,网络新词没进词库就会被切碎,比如“绝绝子”被切成“绝”“绝”“子”,情感词典完全匹配不上。
  • 解决:给 jieba 加载自定义词典,把业务里出现频次高的新词加进去。先写个脚本统计训练集里 TF-IDF 权重高但词典命中为零的词,人工筛一遍再扩充。
jieba.add_word("绝绝子") jieba.add_word("栓Q") # 或者批量加载外部词典文件 jieba.load_userdict("weibo_new_words.txt")

5.2 双重否定和跨窗口否定词被误判

  • 现象:句子“不是不难过,是真的很失望”被词典基线判成了正面,因为“不”翻转了“难过”的分数,但“不是不”是双重否定,表达的是“难过”。
  • 原因:第 2 章的否定词处理只回看前一个词,无法识别“不是不”这种连续否定结构。
  • 解决:把否定检测改成窗口扫描,统计情感词前 2 个位置内出现的否定词数量,奇数个则翻转极性,偶数个则不翻转。
def count_neg_in_window(words, i, window=2): cnt = 0 start = max(0, i - window) for j in range(start, i): if words[j] in NEG_WORDS: cnt += 1 return cnt # 在 score_text 中替换原来的否定判断: if i > 0: neg_cnt = count_neg_in_window(words, i, window=2) if neg_cnt % 2 == 1: score = -score

5.3 TF-IDF 或归一化统计时数据泄露

  • 现象:测试集 F1 有 0.91,部署到线上之后掉到 0.75,典型的测试集表现和线上表现严重脱节。
  • 原因:对全量数据(训练加测试)一起做了fit_transform,TF-IDF 的 idf 权重和特征的均值和方差混入了测试集信息,模型在测试时“偷看”了答案。
  • 解决:所有统计量都只在训练集上拟合,测试集只做transform
# 错误做法 # X_all = tfidf.fit_transform(all_text) # scaler.fit_transform(all_features) # 正确做法 X_train_tfidf = tfidf.fit_transform(train_text) X_test_tfidf = tfidf.transform(test_text) scaler.fit(train_extra) train_extra_scaled = scaler.transform(train_extra) test_extra_scaled = scaler.transform(test_extra)

5.4 微博正负样本比例悬殊

  • 现象:模型准确率 0.9,但看召回率只有 0.4,负面评论被大量漏掉。
  • 原因:训练集里正面样本远多于负面,模型学到的决策边界严重偏向多数类。
  • 解决:class_weight="balanced"是第一步,如果还不够,对训练集做下采样,让正负比例接近 1:1,再用 F1 而不是 accuracy 做模型选择。下采样会损失数据量,但换来的是对少数类更鲁棒的边界,在样本量上万时这个方法通常更划算。

5.5 新闻和微博混合训练互相干扰

  • 现象:整体验证集的 F1 有 0.87,但是按域分开看,新闻域只有 0.78,微博域更差。
  • 原因:新闻和微博的语言风格差异大,模型把所有文本当成同一个分布来学,等于在做两个不兼容的任务。
  • 解决:在特征矩阵里加一个域标签特征,让模型有机会学到域间的差异;或者训练两个独立模型分别服务于两个域。加域特征是成本最低的方案,如果加了之后分域 F1 还是没有显著提升,再考虑分域双模型。
domain = np.array([1 if text_id.startswith("news") else 0 for text_id in train_ids]) extra_with_domain = np.column_stack([train_emotion_score, train_hit_count, domain])

6. 收尾动作:用阈值校准把 F1 再抬两三个点

6.1 在验证集上搜索最佳分类阈值

模型训练完成后,默认用 0.5 作为正负分类的阈值,但在情感分析任务里 0.5 几乎从来不是最优的。正负样本比例、训练数据噪声都会把最优决策边界推离 0.5。一个低成本技巧是,在验证集上遍历阈值 0.3 到 0.7,找到一个让 F1 最大的阈值用于线上预测。

from sklearn.metrics import f1_score import numpy as np prob_pos = model.predict_proba(X_test)[:, 1] best_th, best_f1 = 0.5, 0.0 for th in np.arange(0.3, 0.71, 0.01): pred = (prob_pos >= th).astype(int) f1 = f1_score(y_test, pred, pos_label=1) if f1 > best_f1: best_th, best_f1 = th, f1 print(f"最佳阈值: {best_th:.2f}, 对应宏F1: {best_f1:.4f}")

我见过不少项目,模型架构没变、特征没变,只做了阈值校准,F1 就涨了 2 到 3 个点。这套操作比换模型成本低得多,而且适用于所有输出概率的分类器。如果业务上对某类错误代价更高,比如负面漏报比误报严重,可以把评分函数从 F1 换成带权重的指标,在f1_score里调整pos_label或改用recall_score

6.2 阈值稳定性检查:用交叉验证确认

阈值校准有一个隐患:如果验证集太小,找出来的最优阈值可能在另一个验证集上完全失效。我的习惯是配合交叉验证做稳定性检查——在每一折上重新训练模型、重新搜索阈值,然后把最优阈值的集合打出来看分布。分布集中说明阈值靠得住;分布发散说明数据量不足或样本分布不稳定,这时候优先加数据,而不是继续调参。

这套词典加机器学习的组合方案,在短文本舆情情感倾向分析这类场景里,最大的价值不是单个指标多高,而是每一步都可解释、可回退、可改进。词典打分出问题,直接改词表和规则;模型出问题,看特征是哪些、阈值在哪里。我早年间做情感分析时交过一版只追求测试集准确率的系统,上线后面对真实评论的散弹式表达,被打得灰头土脸,后来所有文本项目都补上阈值校准和分层验证这两步。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询