豆瓣影评情感分析实战:用朴素贝叶斯构建中文文本分类器 📅 发布时间:2026/9/20 23:16:26 👁 浏览次数: 简介面向自然语言处理学习者与初级数据科学实践者这套豆瓣影评情感分析项目聚焦利用贝叶斯分类器与词频-逆文档频率TF-IDF特征对电影评论进行正面、负面情感判别。压缩包共14个文件大小约5.69MB包含5个Python脚本训练、测试、分析器、1个Jupyter笔记本、1个已训练好的模型文件pkl格式、停用词表、用户词典、评论数据CSV及说明文档目录结构简洁便于按步骤直接运行。已有1771人浏览学习适合想要快速上手文本分类、理解概率统计方法在NLP中应用的读者。利用该资源读者能够经历分词去停用词、TF-IDF特征构建、贝叶斯模型训练与验证调优的完整流程并可通过Notebook逐步查看中间结果。其中readme和源码注释对贝叶斯定理、平滑处理、准确率评估均给出说明适合作为课程设计或入门项目参考也能迁移到电商评论、舆情分析等情感判别场景。 豆瓣影评情感分析这个项目我前后折腾了两版才真正跑顺。第一版用的是通用情感词典打分结果准确率惨不忍睹尤其是那种“导演野心很大但剧本配不上野心”的句子词典法完全抓瞎。后来换成了朴素贝叶斯分类器把问题建模成“给定一段文本计算它属于正面还是负面的概率”效果立竿见影。这个项目非常适合刚接触自然语言处理的人它把中文分词、特征提取、概率统计、模型评估这些NLP核心环节全部串了一遍而且数据集容易获取反馈周期短改一个参数马上能看到效果变化。这篇文章我就完整复盘一下整个实现过程包括数据怎么处理、特征怎么选、贝叶斯公式怎么落地成代码以及我踩过的几个坑。一、项目整体设计与思路拆解1.1 为什么选择朴素贝叶斯而不是深度学习做情感分析现在的主流方案其实已经偏向微调BERT或者用TextCNN这类深度模型了。但在这个项目里我坚持用朴素贝叶斯理由很实际第一这是一个典型的小样本场景我一开始只爬了大概两万条短评深度学习模型在这个数据量下很容易过拟合而朴素贝叶斯因为模型简单、参数少反而不容易把噪声学进去。第二朴素贝叶斯是生成式模型它的预测结果是可以解释的你能直接看到哪个词把样本推向了正面或负面这种可解释性在调试阶段极其重要。第三它的训练速度几乎是瞬时的我迭代了几十组参数每次都秒出结果这点体验比深度学习好太多。1.2 整体架构和核心流程整个项目的链路可以拆成四个环节数据获取、文本预处理、特征工程、分类器训练与评估。数据获取就是从豆瓣爬取电影的短评和评分文本预处理包括去除HTML标签、繁体转简体、分词、去停用词特征工程是把文本转成模型能理解的数值形式这里我用的是TF-IDF向量化为什么不用单纯的词频后面会细说最后是训练朴素贝叶斯分类器用准确率、精确率、召回率和F1值来评估效果。这个流程本身就是一个标准的NLP管道每一步的产出都是下一步的输入。我建议新手不要一上来就追求端到端的深度学习方案先把这条管道用简单模型跑通你会发现后续换什么模型都只是在替换中间某个环节而已整体框架是不动的。二、环境准备与数据获取2.1 工具库选型我用的是Python 3.9核心依赖就那么几个requests负责爬数据jieba做中文分词pandas做数据处理scikit-learn提供向量化和分类器joblib用来保存模型。这里最值得聊的是分词工具的选择市面上有jieba、HanLP、LTP、pkuseg好几个选择我最终选了jieba纯粹是因为它在“效果不错”和“上手快”之间平衡得最好。HanLP和LTP的效果确实更好但安装配置成本高对新手不太友好而且在这个任务量级下分词精度的差距对最终准确率的影响其实很小大概也就一两个百分点。2.2 数据采集的细节豆瓣的短评接口返回的是JSON格式每部电影能拿到几百条热门短评每条都附带星级评分。我的策略是选了十部不同类型的电影涵盖爱情片、科幻片、剧情片、烂片避免数据太偏向某一类题材。星级评分映射成标签的逻辑很简单4星和5星算正面1星和2星算负面3星直接丢弃。这里有个我踩过的坑豆瓣对爬虫的限流很严格不加延迟地快速请求很快就会被封IP。我当时的做法是每次请求之间随机sleep 1到3秒再带上正常的User-Agent和Referer头即使这样爬完十个电影也花了将近十分钟。另外要注意这个接口的返回数据里有些短评是匿名的有些是带用户信息的字段结构不完全一样解析的时候要用get方法而不是直接下标访问否则很容易因为KeyError中断。万一你不想爬虫也可以用现成的开源评论数据集网上有不少已经标注好的中文情感分析数据集效果是一样的。三、文本预处理与特征工程3.1 文本清洗的“减法”逻辑预处理的核心思路是做减法去掉一切对情感判断没有帮助的信息。我写了一个清洗函数依次处理这几类噪声HTML实体比如nbsp;、URL链接、用户名、多余空白符。表情符号我没有直接删而是把:D和:(这类经典表情映射成[微笑]和[难过]这种占位符因为表情往往是强烈的情感信号直接删掉太可惜了。繁体转简体用的是opencc库这一步很关键但容易被忽视。有些港台网友的评论是繁体字如果不转换分词和后续的特征统计都会受到影响“開心”和“开心”会被当成两个完全不同的词导致特征空间被稀释。转换成本极低收益却很实在。3.2 分词、停用词与词性筛选分词我用的是jieba的精确模式加载了一个额外的自定义词典里面主要是电影领域的专有名词比如“斯皮尔伯格”、“诺兰”、“漫威”、“奥斯卡”这类词。不加自定义词典的话这些词会被切成“斯皮尔”和“伯格”虽然不影响情感词的效果但会污染特征空间。停用词表我用了哈工大停用词表又手动补了一批高频但无情感倾向的词像“电影”、“一部”、“那种”、“就是”、“觉得”这些。这里有一个经验停用词表不要贪多宁可少删不要多删。有些词在特定语境下是有情感色彩的比如“竟然”和“居然”表达的是意外这种意外感本身可以是正面也可以是负面但直接删掉就丢失了语气信息。词性筛选是另一个可以优化的点。我统计了一下最终情感分类贡献最大的词性其实是形容词和副词然后是动词和名词。所以我尝试过只保留形容词和副词来训练模型准确率反而略有下降原因是很多电影评论的核心情感是通过“烂片”、“雷点”、“高潮”这类名词传达的。所以最终还是保留了所有词性只是在特征加权时对形容词和副词给了稍高的权重。3.3 特征向量化为什么是TF-IDF而非词频文本数据不能直接喂给贝叶斯分类器需要把它变成数值向量。最简单的做法是词袋模型也叫做One-hot或词频向量就是统计每个词在文档里出现了几次。但纯词频有个很明显的缺陷像“电影”这种频繁出现但在每个类别中都平平无奇的词会拿到很高的权重反而是“惊艳”、“拖沓”这种低频却极具区分度的词被淹没了。解决办法就是TF-IDF它在词频的基础上乘了一个IDF逆文档频率权重。IDF的思想很巧妙如果一个词在越多的文档里出现说明它越普通区分度越低权重就被压得越低反之如果一个词只在少数文档里出现它就越特殊越可能是判断类别的关键信号。“惊艳”这个词可能只在10%的短评里出现但它一旦出现这部片子大概率是好评所以它的IDF权重就很高。在scikit-learn里使用TF-IDF很直接from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( max_features8000, # 只取出现频率最高的8000个词 ngram_range(1, 2) # 使用unigram bigram ) X vectorizer.fit_transform(corpus)max_features8000是防止维度爆炸。我一开始没设这个参数结果特征维度直接到了十几万训练时间变长不说还引入了大量只出现过一两次的噪声词。ngram_range(1,2)是让模型同时看到单个词和相邻两个词的组合这样做的原因是中文里很多情感表达是双字词“好看”、“烂尾”、“感人”如果只切分成单字特征可能会丢失语义。四、贝叶斯分类器的原理与代码落地4.1 从贝叶斯公式到“朴素”的涵义朴素贝叶斯的核心就是贝叶斯公式给定一段文本d求它属于类别c的概率等于类别c的先验概率乘以在该类别下观察到文本d的概率再除以文本d自身的概率。分母对所有类别都一样所以比较的时候可以忽略。关键在于P(d|c)怎么求。严格意义上一段文本d里每个词的出现概率是互相依赖的这太难算了。于是朴素贝叶斯做了一个非常大胆的假设假设给定类别之后每个词的出现概率是条件独立的互不影响。这就是“朴素”两个字的由来这个假设在现实中显然不成立比如“剧情”和“拖沓”显然是相关的但正是这种简单粗暴的假设让计算变得极其高效而且在文本分类这种高维稀疏场景下效果意外地好。于是P(d|c)就变成了所有词的条件概率P(w_i|c)的连乘P(好评|文本) ∝ P(好评) × ∏ P(词i|好评)4.2 拉普拉斯平滑的必要性连乘有一个隐患如果某个词在训练集中从未出现在“好评”类别里它的P(词i|好评)就等于0乘完之后整个概率归零。这显然不合理一个词没出现过只能说明训练集样本不够大不代表它就绝对不会出现在好评里。解决方案是拉普拉斯平滑也叫加一平滑公式是P(词i|好评) (该词在好评中出现的次数 1) / (好评类别总词数 词汇表大小)分子加1分母加词汇表大小这样保证任何词的概率都大于0同时所有词的条件概率之和仍然等于1。我在实验中发现平滑系数默认取1已经很好但偶尔可以调到0.5来放大高频词的相对优势后面调参环节我会详细说。4.3 完整的训练代码scikit-learn里的MultinomialNB已经封装了完整的多项式朴素贝叶斯算法。多项式分布适用于离散特征比如TF-IDF值而伯努利朴素贝叶斯适合特征只有0/1二元值的场景。我做了对比实验多项式朴素贝叶斯比伯努利版准确率高4%左右因为TF-IDF里的数值包含了词频强弱信息不只是“出现或没出现”这么简单。from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from sklearn.pipeline import Pipeline # 建立管道向量化和分类器一步到位 model Pipeline([ (tfidf, TfidfVectorizer(max_features8000, ngram_range(1, 2))), (clf, MultinomialNB(alpha0.5)) ]) # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( comments, labels, test_size0.2, random_state42, stratifylabels ) # 训练并评估 model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred))用Pipeline是有讲究的这样把向量化和分类器绑定在一起之后预测新文本时只需要调用model.predict它会自动先走TF-IDF转换。stratifylabels这个参数很重要它保证划分出来的训练集和测试集中正负样本的比例一致避免划分后测试集里全是好评的尴尬情况。五、评估指标与关键参数调优5.1 为什么准确率可能骗人分类器训练完之后你不能只看准确率还得看精确率和召回率。这两个指标在这个场景下的含义很值得玩味精确率说的是“模型判为好评的评论里有多少真的是好评”召回率说的是“所有真实好评里模型成功找出了多少”。如果样本分布很不均衡比如好评占到80%那你模型哪怕把所有评论都判成好评准确率也有80%但这个模型毫无价值。所以我在划分训练集之前先检查了标签分布。豆瓣的评分整体是偏高的我爬到的数据里好评占了67%差评只有33%确实不均衡。解决方式有两个一是在数据层面做欠采样或过采样二是在模型层面调整类别权重。我用的方法是结合两者轻度欠采样好评让比例调整到接近55:45同时用class_weightbalanced让模型对少数类样本的误判付出更高代价。5.2 多组参数实验的结果对比我系统地跑了几组实验来理解各参数的影响。第一组固定其他条件只调节alpha平滑系数结果很有意思alpha从默认的1降到0.3时准确率从84.2%上升到85.8%再降到0.1又回落到了84.7%。这说明平滑系数太大确实会压制真实的条件概率信号但太小又会让低频噪声词占据主导最优值在0.3到0.5之间。第二组实验调整ngram_range只用一元分词时准确率82.9%加了二元组合后提升到84.6%但继续加三元组合几乎没变化。第三组是把max_features从2000调到15000准确率先升后降峰值出现在8000左右之后特征冗余开始引入噪声。5.3 阈值调整与置信度判断分类器默认以0.5作为判定边界概率大于0.5判为好评小于0.5判为差评。但你可以借助predict_proba得到每个样本属于两类的具体概率值。这个概率就是模型置信度。我把置信度分为高、中、低三档置信度超过0.7的判定可信度很高0.5到0.7之间的属于“勉强判断”低于0.5但模型依然给出了结果的就很可能判错。我把0.52到0.48这个区间定义为模糊带抽出来人工看了一眼发现这些评论确实大多是“还行吧”、“将就”、“凑合”这种模棱两可的语气。这类模糊样本即使让人类标注员来标也会有分歧所以模型这部分的“错误”其实可以接受。六、常见问题与排查技巧实录6.1 分词结果里全是单字第一次跑通之后我顺手打印了几条评论的分词结果发现很多句子被切成了单字“而且”被切成了“而且”“我们”被切成了“我们”这让特征空间被大量无意义的单字污染。后来发现是jieba的词典路径出了问题默认词典加载的是精简版。解决办法是显式指定完整的词典路径或者直接换成搜狗细胞词库转成的txt。另外对于电影领域的专有名词一定记得加载自定义词典这个操作对实体词的分词效果立竿见影。6.2 训练集和测试集之间的“特征泄漏”这是一个非常隐蔽的坑。如果你先对整个数据集做TF-IDF拟合然后再切分训练集和测试集那测试集的信息就已经偷偷闯入了模型。因为TF-IDF的IDF权重是根据所有文档统计出来的测试集的文档也参与了统计。正确做法是先把数据集划分好然后在训练集上fit_transform在测试集上只transform。用Pipeline可以天然避免这个问题因为它把向量化和分类器绑在了一个整体里fit只在训练集上执行。6.3 模型文件保存与加载模型训练好之后用joblib保存是一个模型文件的元老级方案import joblib # 保存模型 joblib.dump(model, movie_sentiment_model.joblib) # 加载模型 loaded_model joblib.load(movie_sentiment_model.joblib) # 使用模型进行预测 new_comment 这部电影让我失望透顶 result loaded_model.predict([new_comment])[0] probability loaded_model.predict_proba([new_comment]).max()预测结果result里1代表好评0代表差评。实测下来对于“叙事节奏拖沓全靠特效硬撑”这种句子模型能正确判断为差评对于“虽然是老套的剧情却依然打动了我”这种先抑后扬的句子模型也能识别出来因为“打动”的TF-IDF权重远远大于“老套”。整个项目做下来朴素贝叶斯的表现确实没让人失望在两万条数据上最终跑到了85%左右的准确率。但我也清楚这个模型的瓶颈在于它本质上是在做词语层面的证据加和无法理解“转折”和“反讽”这种结构性的修辞。如果你想继续往上提升效果方向其实是换用BERT这类预训练语言模型来建模上下文语义不过这又是另一个量级的工程了。就当前这个任务来说朴素贝叶斯用最少的算力和最快的速度达到了一个够用的效果这本身就是它最大的价值。最后再分享一个小技巧把训练好的模型保存下来之后建议顺手把TF-IDF向量器也保存一份因为预测新数据时数据必须用同一个向量器转换不然词表对不上出来的预测结果会完全跑偏。本文还有配套的精品资源点击获取