ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

朴素贝叶斯搞定豆瓣影评情感分类:从爬虫到模型评估全流程

朴素贝叶斯搞定豆瓣影评情感分类:从爬虫到模型评估全流程

简介:面向自然语言处理初学者的豆瓣影评情感分类实战课程设计,基于朴素贝叶斯算法完成情感分类全流程。资源涵盖算法原理、中文文本预处理、TF-IDF特征工程、模型训练与评估等环节,并附有完整的Python工程实现与Notebook解析,可帮助读者快速掌握文本分类落地方法。资源压缩包共10个文件、约3.27MB,其中4个Python脚本覆盖训练、测试与分析功能,ipynb文件提供逐步讲解,CSV文件为评分数据,txt停用词表与MD文档辅助复现,整体小巧精简,适合课程设计直接参考。目前已有398人学习,资源结构清晰,从数据加载、清洗到特征提取、朴素贝叶斯建模均有对应代码,可直接运行查看准确率、精确率、召回率与F1分数。通过动手实践,读者既能理解朴素贝叶斯“朴素”假设的实际意义,也能熟悉sklearn、pandas等工具在NLP任务中的用法,有效提升情感分类项目的实现能力。

1. 课程设计选型:为什么拿朴素贝叶斯做豆瓣影评情感分类是性价比最高的答案

每年的课程设计季,总有人一上来就问“能不能用BERT微调一个豆瓣影评情感分类模型”。如果你的目标是刷榜,深度学习当然能打;但如果是课程设计——要讲清楚原理、要能现场答辩、要在两周内跑通全流程——朴素贝叶斯几乎是性价比最高的选择。它不需要GPU、不需要海量数据、不需要调参玄学,几十行代码就能得到一个准确率85%以上且完全可解释的基座模型。这个标题背后真正的需求是:用最少的资源,把“从数据到分类器”的完整链路走一遍,并且每一步都能在答辩时讲出道理。本文按我实际做这个项目的顺序,把数据准备、中文分词、特征工程、模型训练和踩坑记录逐一拆开,确保你照着做能复现,改参数也知道在改什么。

2. 从爬取到干净语料:豆瓣影评数据准备与标注的完整链路

2.1 影评数据从哪来:公开数据集与自采的取舍

做情感分类课程设计,第一步就是搞到带标签的豆瓣影评。这里有三条路,我按推荐程度排序。

第一条路是用开源的中文情感分析数据集,比如ChnSentiCorp,里面包含酒店、书籍、电脑等领域的评论,但没有现成的豆瓣影评。不过很多课程设计直接拿它代替,理由是:模型学的是“正面/负面”的语言模式,领域差异可以通过后续微调来弥补。但扣分点也很明显——老师一眼看出这不是豆瓣语料,答辩时容易被问住。

第二条路是自己用爬虫采豆瓣短评。豆瓣的短评接口是公开的,比如https://movie.douban.com/subject/{movie_id}/comments?start=0&limit=20,返回的HTML里可以解析出评论内容和用户评分。用requests加BeautifulSoup就能采,难度不大。但有个合规问题需要先说清楚:豆瓣的robots协议不允许高频爬取,课程设计场景下只建议采几百条供自己学习用,而且要控制请求频率,比如每次请求间隔3秒以上,不要并发。批量采集几千条以上并对外发布会有法律风险,这属于灰色地带,课程设计不鼓励也不必要。

第三条路是手工标注。我自己的做法是:用爬虫采2000条不带标签的短评,然后按照“1-2星=负面,4-5星=正面,3星=中性剔除”的规则自动打标。豆瓣短评页面上每个评论带星星数,比如“力荐”“推荐”“还行”“较差”“很差”,直接映射成正负标签就行,不需要人工读内容。这样做的好处是:你有一个完全真实的豆瓣语料,答辩时说“数据来自豆瓣短评+星级映射”是站得住的。

2.2 先做清洗再做分词:避免把噪声喂给模型

拿到原始评论后,不能直接丢给模型。豆瓣短评里充满噪声:HTML实体( )、表情符号、@用户、网址、英文单词、数字、多余空白。这些噪声如果不处理,分词器会把“好的啊www123”切成一堆莫名其妙的碎片,特征空间里全是只出现一次的垃圾特征。

我的清洗流程是这样的:

import re def clean_comment(text: str) -> str: # 去掉HTML标签和实体 text = re.sub(r'<[^>]+>', '', text) text = re.sub(r'&[a-zA-Z]+;', '', text) # 去掉URL和@用户 text = re.sub(r'https?://\S+|www\.\S+', '', text) text = re.sub(r'@\w+', '', text) # 去掉表情符号(保留中文、字母、数字、常见标点) text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:""''()]', '', text) # 合并多余空白 text = re.sub(r'\s+', ' ', text).strip() return text

这段正则的顺序是有讲究的:先处理HTML实体,再处理URL,最后才做字符过滤。如果先做字符过滤,URL里的https://会被拆成https和//,反而留下半截垃圾。最后一个正则把emoji和特殊符号全删了,只保留中文、英文字母、数字和常用标点。

清洗后还要处理一件事:过滤过短的评论。豆瓣短评里大量存在“哈哈”“不错”“烂片”这种两三个字的评论。对朴素贝叶斯来说,这些短样本的特征太稀疏,会影响模型的概率估计。我一般会过滤掉清洗后长度不足4个字的评论。注意,这里不能把“烂片”这种强情感词过滤掉,所以阈值设在4比较合适——保留“烂片”和“好片”这类双字词需要至少再加一个字,比如“烂片啊”“是好片”,但纯“烂片”两个字会被滤掉吗?看你的正则怎么匹配,如果按字符数len(text) < 5过滤,“烂片”就被删了。我实际用的是字数统计时按分词后的词数过滤,分词后词数>=2就保留,“烂片”能保住。这是个细节,答辩时被问“为什么保留短评”时能答得上来。

2.3 标注策略与类别平衡:二分类还是多分类

豆瓣影评分类最舒服的做法是二分类:正面(4-5星)和负面(1-2星),把3星中性评剔除。原因很简单:课程设计答辩时,混淆矩阵是2x2的,精确率、召回率、F1值一目了然;如果你做三分类(正/中/负),中性类本身语义模糊,标注噪声大,准确率会掉5-8个百分点,而且你在答辩现场很难解释“为什么中性类总被分错”——因为人类自己都吵不清楚“还行”到底是正还是中性。

类别平衡问题上,豆瓣短评的天然分布大约是正面55%、负面30%、中性15%,所以剔除中性后,正面比负面多出不少。有两个处理方案:一是下采样,把正面样本随机抽到和负面对等;二是保持原分布,但用加权评估指标。我的建议是课程设计尽量做下采样平衡,原因是朴素贝叶斯对先验概率很敏感,类别不平衡会导致预测偏向多数类,而你在手写报告里解释“为什么负面的召回率比正面低”时,最省力的理由就是“训练集里正面更多”。为了避免这个解释负担,直接让训练集正负各一半,答辩时省掉一段辩论。

3. 特征工程决定上限:把中文影评变成朴素贝叶斯能吃的向量

3.1 词袋模型与TF-IDF:两种向量化方式怎么选

朴素贝叶斯本身处理不了文本,它需要的是特征向量。中文文本的特征化有两种主流方式:词袋模型(Bag of Words)和TF-IDF。很多人一上来就用TF-IDF,理由是“听说这个更高级”,但这个选择对朴素贝叶斯来说不一定是最优的。

先说词袋模型。它把每一篇评论表示成一个向量,向量的每一维对应词典里的一个词,值是这个词在评论里出现的次数(CountVectorizer默认就是计数)。朴素贝叶斯算的是P(词|类别),用的是词在类别中出现的频次比例。如果特征值是TF-IDF的浮点数,这些值不再满足“可计数的频次”语义,MultinomialNB的alpha平滑参数解释起来就很别扭——它本来是对“某个词在类别里出现的次数”加平滑,你对一个浮点数加alpha,逻辑上说不通。

再说TF-IDF。它的本意是降低“的、了、是”这种停用词和“电影、一部”这种在语料中普遍出现的词的权重。但中文分词后,停用词问题其实可以用停用词表更直接地解决,不需要靠TF-IDF绕一圈。而且TF-IDF会让特征值变得稀疏且连续,朴素贝叶斯在高维稀疏连续特征上并不比词袋有优势。

我的结论是:做朴素贝叶斯情感分类,优先用CountVectorizer + MultinomialNB。这是一个常见但很多人不知道的坑:选错了向量化方式,准确率差不了太多(可能1-2个点),但答辩时你很难自圆其说。如果你想看TF-IDF的效果,对比实验可以做,但主模型用词袋更规范。

3.2 卡方检验做特征选择:降维不是玄学

词典规模通常在一万到几万之间,而你的训练样本可能只有2000条。特征维度远大于样本数,这意味着大部分特征在训练集里只出现一次,它们对概率估计没有帮助,反而引入噪声。降维的常规做法是卡方检验(chi-squared test):对每个词,计算它与类别标签的相关性,保留相关性最高的K个词。

from sklearn.feature_selection import SelectKBest, chi2 # X为CountVectorizer输出的稀疏矩阵,y为标签 selector = SelectKBest(chi2, k=5000) X_selected = selector.fit_transform(X, y) # 拿到被选中的词 selected_mask = selector.get_support() feature_names = vectorizer.get_feature_names_out() selected_words = [word for word, kept in zip(feature_names, selected_mask) if kept]

卡方检验的原理是:计算每个词与标签的依赖程度,如果某个词在正面评论和负面评论中的出现比例差异显著,它的卡方值就高。比如“惊艳”几乎只出现在正面,“垃圾”几乎只出现在负面,这两个词的卡方值会排在最前面;而“电影”“一部”“真的”在两类中出现比例接近,卡方值就很低。

k的取值需要调,我一般从3000试到10000,用交叉验证选最优。低于3000时信息损失太大,准确率明显下跌;高于10000时特征稀疏性又回来了,且训练时间线性增长。课程设计里把K=5000作为默认值,时间够就做个对比表:K=3000/5000/8000/全部,准确率变化放在报告里,这属于加分的实验设计,而不是玄学调参。

3.3 向量化参数对照表:哪些参数值得调,哪些别碰

参数常见取值影响建议
ngram_range(1,1) 或 (1,2)是否用相邻词组合影评短文本建议(1,2),能捕捉“不 好看”这类否定结构,但特征数膨胀2-4倍
min_df1-5过滤出现次数过少的词设为2,去掉只在一条评论里出现的词
max_df0.8-1.0过滤几乎所有评论都出现的词设为0.9,去除停用词级别的噪声
max_features5000-20000全局特征数上限与SelectKBest二选一,不要同时用
binaryTrue/False词是否只看出现与否建议False(用计数),保留频次信息
stop_words中文停用词表是否过滤“的、了”必须用,配合max_df效果更好

有一个参数组合需要特别注意:ngram_range=(1,2)和binary=True一起用时,“不 好看”会被拆成特征“不”和“好看”和“不好看”,但binary=True丢失了“不好看”出现两次的信息。对短文本来说,一个否定词组出现一次就足以翻转情感倾向,所以binary=True在某些场景效果也不差。但作为课程设计,我建议你用binary=False,因为答辩时可以说“我保留了词的频次信息,使概率估计更准确”,这个理由更稳妥。

4. 朴素贝叶斯建模与评估:从贝叶斯公式到sklearn落地

4.1 三种朴素贝叶斯变体与适用场景

sklearn里常用三种朴素贝叶斯:高斯朴素贝叶斯(GaussianNB)、伯努利朴素贝叶斯(BernoulliNB)、多项式朴素贝叶斯(MultinomialNB)。很多课程设计代码里写GaussianNB()就跑,然后发现准确率惨不忍睹——因为高斯朴素贝叶斯假设特征符合正态分布,对非负的整数特征,这个假设完全不成立。把CountVectorizer的稀疏矩阵喂给GaussianNB是方向性错误。

MultinomialNB适合“特征值为频次”的场景,正好对应CountVectorizer的输出。BernoulliNB适合“特征为二元取值(出现/不出现)”的场景,对应binary=True的输出。还有一个ComplementNB是MultinomialNB的改进版,专门处理类别不平衡,但课程设计里不常用,因为它的原理(用补集类别估计参数)讲起来绕,答辩时你自己容易绕进去。

from sklearn.naive_bayes import MultinomialNB model = MultinomialNB(alpha=1.0, fit_prior=True) model.fit(X_selected, y_train)

alpha=1.0是拉普拉斯平滑,名字听起来玄学,其实公式很简单:某个词在某个类别中的条件概率分子加alpha,分母加alpha * 词表大小。它的目的是防止某个词在训练集中没出现过导致概率为0。如果你把alpha调到0.1以下,概率估计会变得极端——某几个强特征词会支配整个决策,测试集里一旦出现训练时没见过的新词,模型预测就会乱掉。fit_prior=True代表让模型从训练数据中学习类别先验概率(正面/负面各占多少),如果你已经做了下采样平衡,先验概率就是各0.5,fit_prior设True或False结果一样;如果没做平衡,建议设True让模型自己感知先验。

4.2 训练评估代码:一条完整的pipeline

下面是完整的训练评估代码,是我实际跑通过的结构:

import jieba from sklearn.feature_extraction.text import CountVectorizer from sklearn.feature_selection import SelectKBest, chi2 from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix import pandas as pd # 1. 读取清洗后的数据 df = pd.read_csv('douban_clean.csv') # 列: comment, label(label: 0=负面, 1=正面) # 2. 中文分词(先分词再喂向量化) def tokenize(text): return ' '.join(jieba.cut(text)) df['cut_comment'] = df['comment'].apply(tokenize) # 3. 划分训练集/测试集,按stratify保证正负比例一致 X_train, X_test, y_train, y_test = train_test_split( df['cut_comment'], df['label'], test_size=0.2, random_state=42, stratify=df['label'] ) # 4. 向量化:词袋 + 过滤 + 特征选择 vectorizer = CountVectorizer( ngram_range=(1, 2), min_df=2, max_df=0.9, stop_words='english' # jieba分词后的中文不受此参数影响,英文停用词顺带滤掉 ) bow_train = vectorizer.fit_transform(X_train) bow_test = vectorizer.transform(X_test) selector = SelectKBest(chi2, k=5000) X_train_sel = selector.fit_transform(bow_train, y_train) X_test_sel = selector.transform(bow_test) # 5. 训练 model = MultinomialNB(alpha=1.0) model.fit(X_train_sel, y_train) # 6. 预测与评估 y_pred = model.predict(X_test_sel) print(classification_report(y_test, y_pred, target_names=['负面', '正面'], digits=4)) # 混淆矩阵 cm = confusion_matrix(y_test, y_pred) print(cm)

这段代码里有几个不能错的地方。X_test_sel = selector.transform(bow_test)用的是transform而不是fit_transform,目的是用训练集上选好的特征去转换测试集,如果对测试集重新fit_transform,特征选择标准就变了,测试结果有泄漏风险。CountVectorizer同样要先fit训练集再transform测试集,我上面分开写是为了清楚,缩成pipeline时也要注意。

jieba.cut的默认模式会保留标点和空格,CountVectorizer会按空格切分词,标点符号如“。”会被当作一个token,对应一个特征。这种噪声特征量不小(每个标点都是一个特征),建议用jieba.cut(comment, cut_all=False, HMM=False)并过滤掉纯标点的词。更稳妥的办法是在分词前把标点替换成空格:

import re def tokenize(text): text = re.sub(r'[,。!?、;:""''()\n]', ' ', text) return ' '.join(jieba.cut(text))

如果你选用了ngram_range=(1,2),特征数会从1万左右膨胀到3-4万。配合SelectKBest降到5000后,MultinomialNB训练时间在秒级,完全不用担心性能。我实测2000条样本、5000特征,训练加评估不到5秒。

4.3 混淆矩阵看什么、报告里怎么写

混淆矩阵的四个格子对应:真正例(正确预测正面)、假正例(把负面说成正面)、真负例、假负例。情感分类里我关注两个指标:负面(消极)的召回率和整体的F1。召回率低意味着负面评论被漏判成正面,这在情感分析场景里比正面被漏判更严重——一个差评被系统标记为好评,业务上不可接受。

课程设计报告里建议写三行结论:第一行报告整体准确率(通常85-90%);第二行报告负面类别的精确率P、召回率R、F1(负面召回率偏低是常态,如果能做到和正面基本持平,就是一个加分项);第三行分析5个被分错的样本,说明错误原因(典型的是反语和对比句式,比如“这电影要是不烂就怪了”)。分析错例是答辩时最出彩的部分,因为它证明你不是只调了包,而是真看了数据。

5. 避坑与排查:朴素贝叶斯影评分类的六个常见问题

5.1 现象:准确率只有50%左右,和随机猜测差不多

原因有三类,按概率排序:第一,标签反了——爬取数据时“力荐”映射成了0,“很差”映射成了1,模型学到了反的规律,训练集上准确率也很差;第二,训练集和测试集有重复样本(没有去重,爬虫采的同一部电影评论里可能有重复),但更隐蔽的是train_test_split之前没打乱数据,前面都是正面后面都是负面,stratify参数没生效;第三,分词坏了——jieba.cut之后没有用空格连接,CountVectorizer把它当成一个连续字符串,特征变成了整句,没有任何分类能力。

解决方式是肉眼检查三步:打印y_train.value_counts()看标签分布;打印分词后的前20条样本看是否正常;把test_size调大,用混淆矩阵看是两边都错还是单边错。

5.2 现象:负面评论的召回率明显低于正面

这是朴素贝叶斯在情感分类里最典型的失败模式。原因是先验概率偏移和否定结构丢失。如果你的训练集正面多负面少,模型天然会把预测推向正面;而“不怎么样”“不好看”这类否定结构,词袋模型拆成“不”和“怎么样”两个独立特征,模型看到“不”这个词和“好看”这个词分别来自不同类别,组合信息就丢了。

解决方式是两件事:第一,下采样让正负样本量一致;第二,ngram_range=(1,2),让“不好看”成为独立特征。做过这两步后负面召回率一般能回到85%以上。

5.3 现象:加卡方特征选择后准确率反而下降

卡方检验用的是训练集的标签来计算相关性,如果你的训练集太小(比如只有几百条),卡方值会严重受噪声影响——某个词只在三四条评论里出现,恰好又都是正面,卡方值虚高,被选进特征集。模型在测试集上看到这个词出现在负面评论里,直接预测翻车。

解决方式:K值往上调,或者把min_df从2提高到3,过滤掉低频噪声词后再做卡方。这个坑在2000条以下样本时特别常见,我自己的经验是min_df=2加K=5000在小样本上效果稳定。

5.4 现象:用GaussianNB结果特别差

本质原因是特征类型不匹配。高斯朴素贝叶斯假设每个特征服从正态分布,而词频矩阵是非负整数且极度稀疏——绝大多数是0。让一个正态分布去拟合一个99%都是0的分布,结果是每个特征的均值和方差都被0主导,预测时所有样本的概率都在同一个小数级别上浮动,分类边界几乎消失。

正确做法是用MultinomialNB或BernoulliNB。如果你的数据经过TF-IDF转换后是浮点数,MultinomialNB也能用(它内部会自动按非负值处理),但参数解释性会变差,所以这条我强调两遍:词袋计数配MultinomialNB是黄金组合。

5.5 现象:测试集上的准确率很高但答辩现场演示时新评论预测不准

原因是训练集和演示数据的分布不一致。比如你的训练集全是老电影的短评,老师现场给了一部新上映的科幻片影评,里面充满“特效炸裂”“意难平”这类训练集里没出现过的网络新词,朴素贝叶斯对未登录词只能忽略或均匀分配概率,预测结果随机。

这是注意力在课程设计中最容易忽略的环节。解决方式是:演示时用训练集里覆盖的题材(比如同一部电影的其他短评),或者在答辩前把训练语料里加入50条近年的网络影评,让模型至少见过“特效”“意难平”“封神”这类高频当代用语。如果你的时间不够重训,就在演示时先声明“模型对训练集未覆盖的新词敏感”,主动说比被问到要好。

5.6 现象:代码跑通但交叉验证结果振荡

如果5折交叉验证每个fold的准确率波动超过3个百分点,说明数据量不够或者数据分布不均衡。比如你爬的短评集中在几部热门电影上,某些fold被分到的全是某一部电影的评论,模型学到了那部电影的特定语言风格,换一个fold就掉点。

解决方式:按电影分组划分train/test(同一电影的所有评论必须出现在同一侧),这其实是课程设计里容易被种草的高级做法——它测试的是模型的泛化能力,而不是“记住某部电影”。按评论划分和按电影划分对比,答案里可以写“按电影划分后准确率下降X个百分点,说明模型存在过拟合到具体电影字段的风险”,这个分析结论很能加印象分。

6. 最后阶段的验证技巧:把一个朴素贝叶斯课程设计做出报告级质感

做到这里,你的项目已经能跑了——准确率、混淆矩阵、五折交叉验证都有了。但课程设计拿高分,拼的不是跑通,而是“你怎么证明它是对的”。最后我给你三个验证技巧,也是我自己做这类项目时保留的习惯。

第一个技巧是手动构造边界样本来验证模型的判断逻辑。从训练集里挑出5条正面和5条负面评论,然后刻意做局部修改验证模型是否被误导。比如把“这部电影太烂了”改成“这部电影太太太太烂了”,看预测概率是否变得更极端。如果MultinomialNB的预测概率从0.8变成0.9,说明计数特征生效了;如果概率不变,可能是次数被归一化吞掉了。这类实验写进报告里,比大段理论推导有说服力得多。

第二个技巧是画一条学习曲线。用训练集的10%、25%、50%、75%、100%分别训练,记录在测试集上的准确率。课程设计项目里,这条曲线通常呈现先上升后平缓的趋势。如果曲线还在上升期就说明数据量不够,这能帮你向老师证明“我理解了模型的偏差-方差权衡”,而不是只会的调包。

第三个技巧是找一个最容易被分错的案例做错误分析。我当年在这个项目里印象最深的一条被分错的评论是“我花钱买罪受,这片子太值了——值我一张票”。这句话明显是讽刺,模型判定为正面,原因正是词袋模型无法建模反语。我后来在报告里专门分析了这一类错误,并指出“朴素贝叶斯的独立假设决定了它无法捕捉转折和反讽结构”,这既是局限性说明,也合理地为模型开脱了,答辩老师也会觉得你是想清楚了才用的这个模型。

最后想说的是:课设答辩不是模型能力秀,是逻辑链条秀。从“为什么选朴素贝叶斯”到“为什么词袋计数和它匹配”,再到“哪些错误是模型天生的、哪些是我参数没调好”,只要你自己能分清楚,老师的任何追问都在你的射程里。当年我用这套思路做完答辩之后最大的感受就是:一个朴素贝叶斯项目教会我的,比后来用好几个深度学习模型还多。希望帮到你,把这套流程跑通,你也有同样的体感。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进