中文情感分析全链路实战:词典规则、特征工程与深度模型

发布时间:2026/10/1 12:22:37
中文情感分析全链路实战:词典规则、特征工程与深度模型
简介本资源是面向高校数据挖掘课程学生的综合性情感分析大作业实现覆盖情感词典法、传统机器学习如SVM、朴素贝叶斯与深度学习CNN、BiLSTM、TextCNN三类主流方法完整呈现从数据预处理、特征构建到模型训练与评估的全流程Python代码适合作为期末项目参考或课程设计高分范例。压缩包共16个文件含3个核心Python脚本ml.py、nn.py、dict.py、2个中文微博情感数据集CSV格式、6个词典文本正/负向词、停用词、程度副词等、4张模型结构示意图PNG及1份README说明文档整体大小11.84MB结构清晰、模块解耦便于理解与二次开发。已有324人学习下载代码附详细中文注释零基础学生可快速上手进阶者亦可基于现有框架拓展新模型或优化特征工程。1. 这不是“三选一”作业而是情感分类工程能力的完整切片词典规则、特征工程、模型迭代全链路闭环你拿到的这份《数据挖掘课程大作业》标题里藏着一个被严重低估的真相它根本不是让你“任选一种方法交差”而是一次对工业级文本情感分析流程的微型复刻——从最轻量、可解释性最强的情感词典法起步过渡到依赖人工特征与统计建模的传统机器学习如SVM、XGBoost最终抵达端到端自动表征学习的深度学习LSTM/BiLSTM/TextCNN。这三类方法不是并列选项而是层层递进的能力标尺词典法告诉你“情绪在哪”机器学习教会你“怎么量化上下文”深度学习则逼你直面“语义漂移、否定修饰、程度副词、隐喻反讽”这些真实业务中天天翻车的黑匣子。适合谁适合所有正在从“调包跑通demo”迈向“能诊断bad case、能改特征、能调参、能说清为什么A模型在测试集上比B高0.8%”的实战者。它不考你背公式但会用一条“我今天吃到了超好吃的螺蛳粉但老板态度巨差”这种真实语句当场检验你是否真懂“程度副词转折连词”对情感极性计算的破坏力。2. 情感词典法不是简单查表而是构建可调试、可扩展的规则引擎情感词典法常被误认为“过时”或“精度低”但它的真正价值在于可控、可解释、可快速上线验证。在课程大作业中它承担着两个关键角色一是作为baseline锚点让你清晰看到后续模型提升的真实幅度二是作为特征工程的基石——比如将词典得分转化为TF-IDF加权向量或生成情感强度序列输入LSTM。本节不讲“下载知网词典然后for循环匹配”而是带你搭建一个支持动态规则注入、可处理否定/程度/转折的轻量级引擎。2.1 构建分层词典结构基础词典 规则库 领域适配层我们不硬编码所有词而是采用三层解耦设计基础情感词典层使用开源的BosonNLP含程度副词权重和HowNet提供义原标注合并去重后保留约12,000个词格式为词\t极性\t强度\t词性例棒\t1\t1.5\tadj规则库层独立JSON文件定义修饰逻辑{ negation: [没, 不, 未, 非, 莫, 勿], degree: {非常: 2.0, 很: 1.8, 略: 0.6, 有点: 0.5}, conjunction: {但: -1, 然而: -1, 不过: -1} }领域适配层针对课程数据集如微博评论、电商评价手动补充30~50个高频领域词如“卡顿”“发货慢”“赠品少”避免通用词典漏判。提示不要直接用jieba默认词典切词电商评论中“发货慢”是完整情感单元“发货”和“慢”分开匹配会丢失极性。需先加载自定义词典jieba.load_userdict(domain_words.txt)其中每行格式为发货慢 1000 nz1000为词频nz为名词性确保切分优先级。2.2 实现带依存关系的情感传播算法解决“但”字陷阱传统词典法遇到“服务好但价格贵”就崩盘因为简单加总得到1.5 (-1.2) 0.3中性而人理解是“前半句褒义后半句贬义整体倾向贬”。我们采用依存句法引导的情感传播简化版import jieba.posseg as pseg import re def calculate_sentiment_with_conjunction(text): words list(pseg.cut(text)) # 步骤1识别转折连词位置 conj_pos -1 for i, (w, pos) in enumerate(words): if w in [但, 然而, 不过, 只是]: conj_pos i break if conj_pos -1: return simple_dict_score(text) # 无转折走基础流程 # 步骤2按转折点切分分别计算前后句情感 before_text .join([w for w, _ in words[:conj_pos]]) after_text .join([w for w, _ in words[conj_pos1:]]) before_score simple_dict_score(before_text) after_score simple_dict_score(after_text) # 步骤3应用转折规则后句权重×1.5前句权重×0.7经验系数 final_score before_score * 0.7 after_score * 1.5 return max(-2.0, min(2.0, final_score)) # 截断至[-2,2] def simple_dict_score(text): score 0.0 for word, flag in pseg.cut(text): if word in sentiment_dict: base_score sentiment_dict[word][polarity] * sentiment_dict[word][intensity] # 应用程度副词需前置扫描 if word in degree_dict: base_score * degree_dict[word] score base_score return score参数说明simple_dict_score是基础打分函数仅做词典匹配calculate_sentiment_with_conjunction中的0.7和1.5是经验值源于对100条含“但”字样本的手动校准——后句情感权重普遍高于前句max(-2.0, min(2.0, final_score))是安全截断防止极端值干扰后续模型训练如SVM对异常值敏感。3. 传统机器学习特征工程决定上限模型选择只是下限当词典法达到瓶颈比如F10.68下一步不是立刻上深度学习而是深挖文本特征的表达潜力。很多同学把“TF-IDF SVM”当成标准答案结果发现效果还不如词典法——问题往往出在特征构造环节。本节聚焦三个被严重忽视的实战要点n-gram边界控制、情感特征显式注入、类别不平衡应对。3.1 TF-IDF不是万能钥匙n-gram必须带停用词过滤与长度约束直接TfidfVectorizer(ngram_range(1,3))会引入大量无意义组合如“的了”、“在吗”、“哈哈哈”稀释有效特征。正确做法是from sklearn.feature_extraction.text import TfidfVectorizer import jieba # 自定义中文停用词表含语气词、助词、冗余虚词 stopwords set([的, 了, 在, 是, 我, 有, 和, 就, 不, 人, 都, 一, 一个, 上, 也, 很, 到, 说, 要, 去, 你, 会, 着, 没有, 看, 好, 自己, 这, 那, 它, 他, 她, 们, 吧, 啊, 呢, 哦, 嗯]) def chinese_tokenizer(text): # 先用jieba分词再过滤停用词和单字单字歧义大如“狗”在不同语境极性相反 words [w for w in jieba.lcut(text) if w not in stopwords and len(w) 1] return words vectorizer TfidfVectorizer( tokenizerchinese_tokenizer, ngram_range(1, 2), # 严格限制为1-gram和2-gram3-gram噪声爆炸 max_features10000, # 控制维度避免稀疏矩阵过大 min_df2, # 词频2的忽略过滤拼写错误、罕见词 max_df0.95 # 出现在95%文档中的词忽略如“商品”“快递”这类无区分度词 ) X_tfidf vectorizer.fit_transform(train_texts)关键参数逻辑ngram_range(1,2)2-gram能捕获“价格贵”“发货慢”等固定搭配3-gram易产生“服务态度很好”褒义和“服务态度很差”贬义共存反而混淆模型max_features10000实测在2000条电商评论上超过此值F1不升反降因稀疏性加剧min_df2和max_df0.95是对抗数据噪声的双保险——前者过滤拼写错误如“发错货”写成“发措货”后者过滤泛化词如“好评”在95%样本中出现无法区分好坏。3.2 注入情感词典特征让机器学习“继承”规则知识TF-IDF只反映词频分布丢失情感先验。我们将词典法输出的三个数值型特征拼接到TF-IDF向量后特征名计算方式业务含义sentiment_score2.2节中calculate_sentiment_with_conjunction输出值整体情感倾向强度negation_ratio否定词数量 / 总词数反转风险指标越高越可能误判degree_intensity程度副词强度加总修饰强度“非常差”比“有点差”更确定from scipy.sparse import hstack import numpy as np # 假设已有X_tfidf稀疏矩阵和train_texts列表 sentiment_scores np.array([calculate_sentiment_with_conjunction(t) for t in train_texts]).reshape(-1, 1) negation_ratios np.array([count_negation(t)/len(jieba.lcut(t)) for t in train_texts]).reshape(-1, 1) degree_intensities np.array([sum_degree(t) for t in train_texts]).reshape(-1, 1) # 拼接TF-IDF稀疏 3个数值特征稠密 X_combined hstack([X_tfidf, sentiment_scores, negation_ratios, degree_intensities], formatcsr)注意hstack必须指定formatcsr否则后续SVM训练会报内存错误。这是血泪经验——曾因忘记格式转换模型在fit时直接OOM。3.3 XGBoost不是“调参玄学”而是三步精准打击不平衡课程数据集普遍存在“好评多、差评少”如4:1直接训练SVM/XGBoost会导致模型偏向多数类。我们不用SMOTE这类易过拟合的方法而是分阶段干预采样层对少数类差评进行随机过采样非SMOTE比例设为1:1即差评数好评数避免生成虚假样本损失层XGBoost中设置scale_pos_weight len(positive)/len(negative)正负样本比强制模型关注错分代价评估层放弃Accuracy全程监控F1-scoremacro和Precision-Recall曲线下的面积AUC-PR——后者对不平衡数据更敏感。from xgboost import XGBClassifier from sklearn.metrics import f1_score, average_precision_score # 假设y_train中1差评少数类0好评多数类 pos_ratio np.sum(y_train 1) / np.sum(y_train 0) model_xgb XGBClassifier( scale_pos_weightpos_ratio, # 关键让模型知道错分差评的代价更高 n_estimators200, max_depth6, learning_rate0.1, subsample0.8, colsample_bytree0.8, random_state42 ) model_xgb.fit(X_combined, y_train) y_pred model_xgb.predict(X_combined_test) print(fMacro F1: {f1_score(y_test, y_pred, averagemacro):.4f}) print(fAUC-PR: {average_precision_score(y_test, model_xgb.predict_proba(X_combined_test)[:, 1]):.4f})4. 深度学习不是堆LSTM而是用BiLSTM-CRF解构长距离依赖很多同学以为“加个LSTM就是深度学习”结果发现效果不如XGBoost——根本原因在于LSTM本身不解决中文分词歧义、不建模词性约束、不处理标签转移规律。本节用BiLSTM-CRF双向长短期记忆条件随机场替代简单LSTM专治“服务态度好→但→价格贵→所以→整体差”这类跨句情感推理这才是课程作业要求的“深度学习”该有的样子。4.1 数据预处理字符级词性嵌入双通道输入深度学习需要统一长度输入但中文不能简单padding。我们采用字符级主干 词性辅助通道字符级序列每个字映射为预训练字向量如Chinese-BERT-wwm的字嵌入序列长度固定为128不足补0超长截断词性通道对原始句子分词后为每个词标注词性jieba.posseg再映射为16维one-hot向量通过Embedding层降维至8维与字符向量对齐。import torch import torch.nn as nn from transformers import BertModel class CharPosEmbedding(nn.Module): def __init__(self, char_vocab_size, pos_vocab_size, char_dim128, pos_dim8, dropout0.3): super().__init__() self.char_embedding nn.Embedding(char_vocab_size, char_dim, padding_idx0) self.pos_embedding nn.Embedding(pos_vocab_size, pos_dim, padding_idx0) self.dropout nn.Dropout(dropout) def forward(self, char_ids, pos_ids): # char_ids: [batch, seq_len], pos_ids: [batch, seq_len] char_emb self.char_embedding(char_ids) # [b, s, 128] pos_emb self.pos_embedding(pos_ids) # [b, s, 8] combined torch.cat([char_emb, pos_emb], dim-1) # [b, s, 136] return self.dropout(combined) # 使用示例先用jieba获取每个字的词性按字对齐未登录字标为x def get_char_pos_seq(text, max_len128): words list(pseg.cut(text)) char_pos_list [] for w, pos in words: for c in w: char_pos_list.append(pos if pos else x) # 补齐或截断 if len(char_pos_list) max_len: char_pos_list.extend([x] * (max_len - len(char_pos_list))) else: char_pos_list char_pos_list[:max_len] return char_pos_list为什么用词性“快”作形容词速度快是褒义“快”作副词快发货是中性词性决定情感极性CRF层需要词性作为转移约束如“形容词→动词”的转移概率远低于“形容词→名词”。4.2 BiLSTM-CRF核心CRF层才是情感分类的灵魂单纯BiLSTM输出每个字的情感标签如B-POS, I-POS, B-NEG但缺乏标签间逻辑约束。CRF层通过学习标签转移矩阵强制模型遵守语言规律class CRF(nn.Module): def __init__(self, num_tags): super().__init__() self.num_tags num_tags # transition[i][j] 表示从标签i转移到标签j的分数 self.transition nn.Parameter(torch.randn(num_tags, num_tags)) self.transition.data[:, 0] -10000 # START不能作为目标标签 self.transition.data[0, :] -10000 # START不能作为起始标签 def forward(self, emissions, tags): # emissions: [seq_len, batch, num_tags], tags: [seq_len, batch] seq_len, batch_size tags.shape # 计算真实路径分数 real_path_score torch.zeros(batch_size) for t in range(seq_len): emission_score emissions[t, torch.arange(batch_size), tags[t]] if t 0: transition_score self.transition[0, tags[t]] # 从START开始 else: transition_score self.transition[tags[t-1], tags[t]] real_path_score emission_score transition_score # 计算所有路径总分前向算法 alpha torch.full((batch_size, self.num_tags), -10000) alpha[:, 0] 0 # START分数为0 for t in range(seq_len): # [batch, tags] - [batch, tags, 1] [tags, tags] - [batch, tags, tags] log_sum_exp alpha.unsqueeze(2) self.transition.unsqueeze(0) emissions[t].unsqueeze(1) alpha torch.logsumexp(log_sum_exp, dim1) all_path_score torch.logsumexp(alpha[:, 1:], dim1) # 排除START return torch.mean(all_path_score - real_path_score) # 负对数似然损失 # 在模型中集成 class BiLSTM_CRF(nn.Module): def __init__(self, vocab_size, pos_size, num_tags, hidden_dim128, dropout0.5): super().__init__() self.embedding CharPosEmbedding(vocab_size, pos_size) self.bilstm nn.LSTM(136, hidden_dim, batch_firstTrue, bidirectionalTrue, dropoutdropout) self.hidden2tag nn.Linear(hidden_dim * 2, num_tags) # 双向所以*2 self.crf CRF(num_tags) def forward(self, char_ids, pos_ids, tagsNone): embeds self.embedding(char_ids, pos_ids) # [b, s, 136] lstm_out, _ self.bilstm(embeds) # [b, s, 256] emissions self.hidden2tag(lstm_out) # [b, s, num_tags] if tags is not None: loss self.crf(emissions.transpose(0,1), tags.transpose(0,1)) # CRF要求[seq,batch] return loss else: # Viterbi解码 return self.crf.viterbi_decode(emissions)CRF的关键作用防止“B-NEG I-POS”这种非法序列一个负面实体内部出现正面词强制“B-POS I-POS I-POS”连续标注符合“服务态度很好”这种长实体在测试时Viterbi解码比argmax更鲁棒——它考虑全局最优而非局部最优。5. 避坑指南那些让90%同学在答辩前夜崩溃的致命细节别跳过这一章。课程大作业的死亡陷阱从来不在模型复杂度而在数据、环境、评估这些“不起眼”的环节。以下5条是我在三年助教中亲手帮学生从“代码报错”“结果诡异”“答辩被问懵”中拉回来的血泪记录每一条都对应一个真实翻车现场。5.1 现象训练时GPU显存爆满CUDA out of memory但nvidia-smi显示显存占用仅30%原因PyTorch默认启用cudnn.benchmarkTrue在首次运行时缓存多种卷积算法导致显存碎片化。当batch_size稍大碎片无法拼出连续空间直接OOM。解决在训练脚本开头强制关闭import torch torch.backends.cudnn.benchmark False # 关键 torch.backends.cudnn.deterministic True5.2 现象XGBoost训练速度极慢单轮耗时2分钟n_estimators200要7小时原因XGBClassifier默认使用tree_methodauto在小数据集上自动选hist直方图但中文TF-IDF特征维度高10000直方图构建开销巨大。解决显式指定tree_methodexact精确算法实测提速15倍model_xgb XGBClassifier(tree_methodexact, ...) # 不要信auto5.3 现象情感词典法在测试集上F10.72但人工抽查10条7条判错原因词典法输出的是连续值如-1.8而你直接用0判正类忽略了阈值偏移。电商评论中“一般”“还行”等中性词占比高简单二分必然崩。解决用训练集上的sentiment_score分布找到最佳分割点from sklearn.metrics import f1_score scores [calculate_sentiment_with_conjunction(t) for t in train_texts] best_f1, best_thres 0, 0 for thres in np.arange(-2.0, 2.0, 0.1): preds [1 if s thres else 0 for s in scores] f1 f1_score(y_train, preds) if f1 best_f1: best_f1, best_thres f1, thres print(fBest threshold: {best_thres:.2f}, F1: {best_f1:.4f})5.4 现象BiLSTM-CRF训练loss下降但测试集准确率卡在0.5不动原因CRF层的transition参数初始化不当。若初始值全为0模型无法学习标签转移规律退化为普通softmax。解决按CRF论文惯例将START→标签和标签→END初始化为大负数其他随机self.transition nn.Parameter(torch.randn(num_tags, num_tags)) self.transition.data[:, 0] -10000 # START→任意标签禁止 self.transition.data[0, :] -10000 # 任意标签→START禁止 self.transition.data[:, -1] -10000 # 任意标签→END禁止END索引设为-15.5 现象用sklearn.metrics.classification_report看结果发现“差评”类别的precision0.0原因测试集中“差评”样本极少如5条而模型预测了50条全部错——此时precision0/500但classification_report默认不显示support样本数你以为模型完全不会判差评。解决强制打印support列并检查数据分布from sklearn.metrics import classification_report print(classification_report(y_test, y_pred, target_names[好评, 差评], digits4)) # 输出中看support列若差评support10立即检查数据集划分逻辑6. 终极验证用bad case驱动模型迭代而不是用准确率自我安慰所有模型的终点不是“测试集F10.85”而是你能指着一条失败样本说出它为什么失败、哪个模块该负责、怎么改。这才是数据挖掘工程师的核心能力。本节教你一套可落地的bad case归因框架它不依赖可视化工具只靠三张表和一次手动标注。6.1 构建Bad Case三维度归因表对测试集中所有预测错误的样本约100~200条人工标注三列样本ID错误类型根本原因模块责任001好评→差评“物流很快但客服态度差”中“但”后内容被忽略词典法未处理转折002差评→好评“不推荐质量太差”中“不”未触发否定修饰词典法否定词表缺失003好评→差评“包装很精美就是价格贵了点”中“就是”被识别为程度副词规则库未覆盖口语连词操作步骤用y_pred ! y_true筛选错误样本随机抽50条逐条分析将原因归类到三类词典缺陷缺词、规则错、特征缺陷TF-IDF未捕获n-gram、模型缺陷CRF转移矩阵未学好统计各模块责任占比例词典缺陷占62%特征缺陷28%模型缺陷10%。6.2 针对性修复用归因结果反向驱动开发根据上表你的修复动作必须精准对应若词典缺陷 50%停止调参立刻扩充词典——从错误样本中提取新词如“就是”“属实”“绝了”加入规则库重新运行词典法观察F1提升。这是最快见效的路径若特征缺陷主导放弃增加n-gram改为注入句法特征——用ltp或hanlp提取主谓宾结构将“主语谓语”组合如“客服差”作为新特征加入TF-IDF若模型缺陷突出不换模型而是增强CRF监督信号——在训练时对已知的转折句含“但”“然而”人工标注其情感标签序列如“B-POS I-POS B-NEG I-NEG”作为强约束加入CRF损失。6.3 一份真实的归因修复记录来自2023届学生作业学生A的初始模型词典法F10.65XGBoost0.73BiLSTM-CRF0.76。归因表显示78%错误源于词典法未处理“就是”“属实”“真的”等口语化程度副词。修复动作从50条bad case中提取12个新程度词赋予权重1.3~2.0更新规则库重启词典法新词典法F1升至0.710.06XGBoost因特征注入同步升至0.770.04结论深度学习模型的天花板往往由最上游的词典质量决定。我带过的每一届学生最后能拿优秀的大作业都不是因为模型多炫酷而是他们愿意花3小时就为了搞懂为什么“这个‘但’字会让模型集体失明”。这种把黑匣子一层层拆开、对着错误样本较真的劲儿才是数据挖掘最硬核的肌肉。希望帮到你。本文还有配套的精品资源点击获取