ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

融合情感词典与机器学习:中文短文本情感分析实战指南

融合情感词典与机器学习:中文短文本情感分析实战指南 简介面向新闻与微博评论的情感分析场景这份资源整合了情感词典与机器学习两种主流技术适合自然语言处理初学者、数据挖掘工程师及舆情分析人员学习参考。资源共有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, indexFalse, headerFalse) # 看看整理完之后的数据 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, encodingutf-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的调用位置这一步应该只对训练集调用测试集要用同一个scaler做transform。这也是一个经典的数据泄露来源第 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_features20000, min_df2, sublinear_tfTrue ) 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_df2去掉只出现一次的罕见词既能降噪又能减少内存占用。sublinear_tfTrue对词频做对数缩放防止高频词主导特征权重。情感分值特征只有两列但对模型的贡献往往排在前二十名以内——这就是先验知识的作用。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_weightbalanced, max_iter1000) param_grid {C: [0.1, 0.5, 1, 5, 10]} grid GridSearchCV(model, param_grid, cv5, scoringf1, 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_weightbalanced是必须加的评论数据里正面和负面样本很少均衡尤其是微博评论负面往往多出一大截。加了这个参数后少数类的召回率会有明显改善。搜索范围为什么只调 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), keylambda 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_threshold3.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, window2): 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, window2) if neg_cnt % 2 1: score -score5.3 TF-IDF 或归一化统计时数据泄露现象测试集 F1 有 0.91部署到线上之后掉到 0.75典型的测试集表现和线上表现严重脱节。原因对全量数据训练加测试一起做了fit_transformTF-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_weightbalanced是第一步如果还不够对训练集做下采样让正负比例接近 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_label1) 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 阈值稳定性检查用交叉验证确认阈值校准有一个隐患如果验证集太小找出来的最优阈值可能在另一个验证集上完全失效。我的习惯是配合交叉验证做稳定性检查——在每一折上重新训练模型、重新搜索阈值然后把最优阈值的集合打出来看分布。分布集中说明阈值靠得住分布发散说明数据量不足或样本分布不稳定这时候优先加数据而不是继续调参。这套词典加机器学习的组合方案在短文本舆情情感倾向分析这类场景里最大的价值不是单个指标多高而是每一步都可解释、可回退、可改进。词典打分出问题直接改词表和规则模型出问题看特征是哪些、阈值在哪里。我早年间做情感分析时交过一版只追求测试集准确率的系统上线后面对真实评论的散弹式表达被打得灰头土脸后来所有文本项目都补上阈值校准和分层验证这两步。希望帮到你。本文还有配套的精品资源点击获取
返回列表