简介:面向机器学习与自然语言处理课程期末大作业场景,这一项目基于朴素贝叶斯算法完成情感文本分析与分类,适合计算机相关专业学生系统复现算法流程、完成课程设计或期末大作业。压缩包共12个文件,以Python脚本、CSV数据集、预训练词向量(.bz2)及XML/IML工程配置为主,整体173.42MB;Python脚本承担数据建模与测试,CSV提供情感标注语料,bz2预训练词向量用于文本向量化表示,工程配置便于直接打开运行。目前已有1025人学习/下载。项目源自个人大作业,评审得分95分以上,经过严格调试,具备开箱即用的可运行性。内容覆盖数据读取、清洗、文本向量化、朴素贝叶斯训练与评估等完整流程,主程序与测试脚本分离,目录结构清晰,附带两组CSV数据与微博词向量,省去额外准备语料的步骤,便于对照学习、二次开发,可直接作为期末大作业或情感分析入门的参考实现。
1. 用朴素贝叶斯做情感文本分析与分类:为什么课程大作业总选它
如果你手里现在正躺着一份“基于朴素贝叶斯机器学习算法实现情感文本分析与分类源码+数据集”的压缩包,大概率你已经在为机器学习期末大作业发愁了。这类项目在高校里出现频率极高,核心任务其实很固定:给一批带有情感倾向的评论文本(比如电影评论、电商评价),用朴素贝叶斯算法训练一个分类器,让它自动判断一条新文本是正向还是负向。它既是《机器学习》课程里最经典的分类任务之一,也是短文本舆情情感倾向分析系统的基础原型,很多本科毕业设计、研究生课程项目都从它起步。
这个标题真正能解决的事情有两件:第一,它给你一条完整可跑的落地链路——从数据集加载、文本清洗、中文分词、特征向量化,到模型训练、评估、保存和预测;第二,它给了你一份能写进实验报告、能应付答辩的“可解释性”——朴素贝叶斯不像神经网络是个黑匣子,它的条件独立假设、先验概率和拉普拉斯平滑都是能摊开讲清楚的考点。适合谁?适合正在做机器学习课程设计、需要快速复现一个情感分类基线模型、或者想搞懂朴素贝叶斯在文本分类里到底怎么落地的同学。这篇笔记我把这类项目的常见实现方案、参数设置和踩坑记录拆开讲,你拿到手之后照着调就能跑,跑通了还能举一反三。
2. 朴素贝叶斯不是黑匣子:先弄懂条件独立怎么在情感分类里工作
2.1 从贝叶斯公式到情感倾向:先看后验概率怎么决定评论文案
朴素贝叶斯在文本分类里的地位很特殊,它是少有的“公式简单但效果不差”的算法。它的出发点是你高中就见过的贝叶斯公式:
P(类别|文本) = P(文本|类别) × P(类别) / P(文本)
在这个式子里,“文本”是一整句话,“类别”是正向或负向。P(类别|文本) 的意思是:看到这行文字之后,它是好评的可能性有多大。朴素贝叶斯做的事,就是计算出每个候选类别的这个后验概率,然后选最大的那个作为预测结果。
但这里有个麻烦——P(文本|类别) 没法直接算。一句话由几十个词组成,整句话在训练集里出现过的次数几乎为零。于是朴素贝叶斯搬出了那个著名的“朴素”假设:给定类别时,特征之间条件独立。也就是说,把“这部电影太棒了”拆成“电影”“太棒了”,假设“电影”这个词出现与否,和“太棒了”出现与否没有关联,那么整句话的似然概率就可以写成每个词概率的连乘:
P(文本|类别) = P(电影|类别) × P(太棒了|类别)
这个假设在现实中基本不成立,比如“特效”和“剧情”常在影评里一起出现,但它的威力在于:计算量大幅下降,建模简单,在小规模文本数据上泛化能力反而比一些复杂模型好。大作业选它,核心原因之一就是“可解释”——答辩老师问“你的模型为什么能区分正负情感”,你可以直接把类别的词概率分布拉出来,说“模型学到‘垃圾’‘失望’这类词在负向类里的概率远高于正向类”。这一点是神经网络和SVM很难直接给你的。
2.2 准备数据集:标签分布、中文清洗与停用词表这三关
任何情感分析项目,第一步都不是写模型,而是把数据集搞干净。评分大作业里常见的数据集是评论加标签的格式,比如 CSV 文件,一列是评论文本,一列是情感标签,标签通常是 0 和 1,或者“好评”“差评”这样的中文文本。
拿到数据先做三件事。第一,检查标签分布。如果正负样本比例相差悬殊,比如负向只有 10%,朴素贝叶斯会倾向于把所有文本都判成多数类,后面模型评估时准确率虚高但没有实际意义。第二,清洗文本。原始评论里经常有 HTML 标签、表情符号、@用户、URL、连续标点,这些对分词和词频统计都是干扰。第三,准备停用词表。中文停用词表在 GitHub 上有很多公开资源,比如“的”“了”“是”“在”这类高频无实义词,留着会让 idf 计算被无关词干扰。建议把停用词表单独存成一个 txt 文件,每行一个词,这样数据预处理代码和词表分离,报告也好写。
我一般会先用 pandas 读数据,打印出前几行看看编码和字段名,再做标签映射。编码问题在 Windows 上特别容易踩——CSV 用 UTF-8 保存却用默认编码读取,中文会出现乱码。还有一个常见坑:标签列可能是中文“好评/差评”而不是 0/1,直接用 sklearn 的分类器也能训练,但评估时混淆矩阵的标签顺序会让人困惑,提前用 map 映射成数字更省心。
2.3 中文分词选 jieba:为什么英文项目那套直接照搬会翻车
英文情感分析可以直接按空格切词,但中文不行。“这部电影太棒了”如果不分词就成了一个整体,模型完全无法泛化。课程大作业里最通用的做法是使用 jieba 分词,它是纯 Python 实现,安装一行命令,分词效果在通用领域够用,而且支持自定义词典和停用词过滤。
常见的实现套路是:把每条评论用 jieba.lcut() 切成词列表,过滤掉单字词和停用词,再用空格连接成字符串。为什么要用空格连接?因为 sklearn 的 TfidfVectorizer 默认按空白字符做 token 切分,你把分词结果拼回去,才能让向量化器直接工作。
这里有个容易忽略的细节:分词时要决定是否保留单个汉字。比如“烂”这个字单字出现也能强烈表达情感,但单字词会让特征维度暴涨,而且很多单字是无意义的语气词。一般来说,我建议剥离停用词之后,再按词长过滤,只保留长度大于等于 2 的词,能明显减小特征矩阵的规模。如果你做的是电影评论这类领域数据,可以把导演名、演员名加到 jieba 的自定义词典里,避免人名被切成几个单字,这一步在你后续看特征词时就能感觉到差别。
3. 从数据集到分类结果:源码里的训练、评估与预测全流程
3.1 特征工程:用 TfidfVectorizer 把评论文本转成可计算向量
朴素贝叶斯只能吃数值向量,所以文本必须先过向量化这一关。大作业里最常见的两个选择是 CountVectorizer(词频)和 TfidfVectorizer(词频-逆文档频率)。新手往往直接用 CountVectorizer,但评测时 Tfidf 几乎总是胜出一点点,原因在于:CountVectorizer 只统计词在本条文本里的出现次数,而 Tfidf 会把“在多少篇文档里出现过”这个信息加进去——像“电影”“觉得”这种在大多数评论里都出现的词,tf-idf 会给它很低的权重,而“惊艳”“翻车”这类只在部分文本里出现的词权重更高,这对情感分类是更合理的信号。
一段完整可复现的特征工程代码大致长这样:
from sklearn.feature_extraction.text import TfidfVectorizer # 设定向量化器参数 vectorizer = TfidfVectorizer( max_features=5000, # 只保留词频最高的5000个特征,防止维度爆炸 ngram_range=(1, 2), # 保留单个词和相邻双词组合 min_df=2, # 至少在2条文档里出现过才保留 max_df=0.8, # 在超过80%文档里出现的词视为停用,过滤掉 ) # 把分词并拼接好的文本列表送去拟合并转换 X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) # 注意:测试集不能重新fit逻辑说明:fit_transform 在训练集上同时完成“学习词典”和“转换为稀疏矩阵”两步;测试集上只用 transform,是为了保证训练集和测试集的特征维度完全一致。min_df 和 max_df 都是过滤低频和高频噪声的手段,min_df=2 能把只出现一次的“独特拼写错误”或“生僻词”去掉;max_df=0.8 能过滤掉类似“真的”“感觉”这类通用词。ngram_range=(1,2) 会把“太棒了”拆成“太棒了”和“太棒”两个特征,让模型捕捉到一些局部短语信息,代价是特征维度大约翻倍。max_features=5000 是节约内存的关键,文本分类真的不需要几十万维特征,5000 对你这个场景的准确率影响很小,但训练速度差几十倍。
3.2 训练与调参:MultinomialNB 的 alpha 和 fit_prior 该怎么设
向量化之后,训练朴素贝叶斯模型就是几行代码的事。情感文本分析场景下,特征矩阵的元素是非负整数或浮点数,对应的是词频或 tf-idf 值,所以几乎总是选用 MultinomialNB 而不是 GaussianNB 或 BernoulliNB。GaussianNB 假设特征服从正态分布,适用于连续数值型特征;BernoulliNB 会把特征变成 0/1 二值,适合“词是否出现”的场景。MultinomialNB 的生成式模型直接估算词在某个类别下的条件概率,和文本分布最匹配。
from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split # 切分训练集与测试集,stratify保证正负比例不变 X_train, X_test, y_train, y_test = train_test_split( texts, labels, test_size=0.2, random_state=42, stratify=labels ) # 初始化多项式朴素贝叶斯 model = MultinomialNB(alpha=1.0, fit_prior=True) # 训练 model.fit(X_train_vec, y_train) # 预测 y_pred = model.predict(X_test_vec) y_proba = model.predict_proba(X_test_vec)MultinomialNB 有两个必调参数。alpha 是拉普拉斯平滑系数,默认值为 1.0,作用是对那些“在训练集中从没出现过但在预测时遇到”的词给一个非零的小概率,避免整个连乘结果归零。alpha 太小(比如 0.01)会让模型对低频词过于敏感,出现一个没见过的词就把预测带偏;alpha 太大(比如 10)会抹平词与词之间的概率差异,让所有词的概率趋于均匀。在大作业数据量(几千条)下,alpha 在 0.1 到 1.0 之间通常表现稳定。fit_prior 控制是否从训练数据里学习先验概率,默认 True,也就是直接按各类别样本比例算 P(类别);如果样本均衡,这个参数影响不大,但样本不均衡时保留默认值更合理,因为模型能自动学到“负向文本本来就少”这个事实。
3.3 评估与预测:分类报告、混淆矩阵和单条文本复现
训练完之后,不能只报一个准确率就收工。课程大作业要想拿高分,评估部分至少要有三样东西:分类报告、混淆矩阵、以及几条单文本预测的直观展示。分类报告能给出每个类别的精确率、召回率和 F1 值;混淆矩阵能让你一眼看出模型是把负向误判成正向多,还是把正向误判成负向多。
from sklearn.metrics import classification_report, confusion_matrix # 输出分类报告 print(classification_report(y_test, y_pred, target_names=['负向', '正向'])) # 输出混淆矩阵 cm = confusion_matrix(y_test, y_pred) print("混淆矩阵:") print(cm) # 单条文本复现预测 import jieba def predict_one(text, vectorizer, model): # 与训练阶段完全一致的分词和拼接 words = [w for w in jieba.lcut(text) if w.strip() and len(w) > 1] text_clean = " ".join(words) vec = vectorizer.transform([text_clean]) proba = model.predict_proba(vec)[0] label = model.predict(vec)[0] return label, proba sample = "这部电影的剧情太荒唐了,全程都在浪费时间" label, proba = predict_one(sample, vectorizer, model) print("预测结果:", "正向" if label == 1 else "负向") print("各类别概率:", proba)这段代码里有个很容易出错的地方,我在批改同学作业时看到过无数次:单条文本预测时重新对整段文本做了 jieba 分词,但完全忘了再过滤停用词。训练阶段数据里“的”“了”都被移除了,预测阶段拿出来一个新句子却带着这些词,向量化器会把它们当未知特征忽略掉,最终特征向量和训练分布不一致,predict_proba 输出的置信度会整体偏低,但不一定导致分类错误。更严重的问题是有人在预测时重新 fit 了 vectorizer,那你的测试文本会被映射到一个全新的特征空间,维度都对不上,代码直接报错。
3.4 模型保存与报告素材:用 joblib 把你训练好的结果留下来
大作业经常不是一次跑完就交,你要反复调参、补充实验截图,甚至隔几天再来对比不同 alpha 的结果。所以训练完模型后,我建议立刻把模型和向量化器都保存下来,用 joblib 或 pickle 都行,但不建议 pickle 大对象跨 Python 版本迁移,同一台机器上 joblib 更稳。
import joblib # 保存模型和向量化器 joblib.dump(model, "nb_model.pkl") joblib.dump(vectorizer, "tfidf_vectorizer.pkl") # 恢复后直接复用 loaded_model = joblib.load("nb_model.pkl") loaded_vectorizer = joblib.load("tfidf_vectorizer.pkl")逻辑说明:模型文件体积通常只有几百 KB 到几 MB,保存下来方便你反复加载做预测,不用每次重新训练。作业报告中如果想展示“模型学到了哪些关键特征”,可以读取 MultinomialNB 的 coef_ 或者用 log_prob_ 属性,找出在正负两个类别下概率差异最大的词。这一步可以配合 matplotlib 画一个横向条形图,展示“最正面的词”“最负面的词”,是整份报告里最亮眼的素材。
4. 朴素贝叶斯情感分类的常见翻车点:现象、原因与处理
4.1 内存爆掉或报 negative dimension:特征矩阵维度爆炸
现象:训练时内存占用飙升,或者 sklearn 直接抛 ValueError: negative dimension is not supported,网上搜半天也不知道是谁在报错。
原因:TfidfVectorizer 没有限制 max_features,而文本数据没做充分的停用词过滤和单字词过滤,导致词典规模达到十几万。TfidfVectorizer 默认会构建一个巨大的稀疏矩阵,虽然稀疏矩阵本身不占太多内存,但在计算 ngram 组合时,中间展开的维度可能让内存瞬间失控。
解决:把 max_features 设为 3000 到 8000 之间,同时把 min_df 设为 2 或 3。做完这两步,特征数量会从十几万掉到几千,内存占用从几个 GB 砍到几百 MB,准确率几乎不掉。如果做完特征工程维度还是很夸张,那说明你的分词阶段没有过滤单字词,回去检查 jieba 切分之后有没有按长度截断。
4.2 准确率高得离谱但实际预测全错:数据集标签泄漏
现象:训练集准确率 99%,测试集准确率也很高,但拿真实评论去试,预测结果完全不对,或者随机猜测。
原因:数据预处理时把标签信息混进了特征。典型的操作是在清洗文本时把“好评”“差评”这样的字眼留在了文本里——比如原始数据是从电商平台抓的,评论尾部自带“好评”字样;或者做特征工程时,把包含标签的词也纳入了词典。模型根本不需要学语义,只要检测到“好评”两字就能分类,这不是学习,是背答案。
解决:检查清洗后的文本里有没有标签词。如果发现“好评”“差评”“满意”“失望”这类词出现在特征表的最前面,直接把这些词加进停用词表。另一个容易忽略的泄漏源是 train_test_split 之前对全量数据做了 fit_transform,导致向量化器在构造词典时“看”到了测试集的内容,严格来说这不叫泄漏,但会让测试评估结果虚高。正确做法永远是先切分,再 fit_transform 训练集,最后 transform 测试集。
4.3 中文分词乱成一团:自定义词典缺失导致人名地名被切碎
现象:打印分词结果看到“沈腾”“这位演员”被切成“沈”“腾”,或者“流浪地球”被切成“流浪”“地球”——后者还好,“沈”“腾”这样的单字会让特征完全失去语义。
原因:jieba 的默认词典以通用词为主,没有人名、电影名、专有名词等领域词条。大作业的数据集往往来自特定领域,比如电影评论、餐饮评论,领域词占比很高,切碎了之后模型学不到正确特征。
解决:自己收集一份领域词表,大概是几十到几百个词,用 jieba.add_word() 逐个加入,或者写成自定义词典文件传给 jieba.load_userdict()。这个步骤特别适合写进报告作为“针对数据集的优化”。此外,一份高质量的自定义词典对准确率的提升通常比调 alpha 更明显:因为你让模型看到了正确的特征单元,而不是一串语义破碎的单字。
4.4 预测全部归为多数类:数据不均衡被完全忽视
现象:模型跑通了,准确率 85%,但看混淆矩阵发现负向样本几乎全被识别成正向,少数类一条也没抓住。
原因:数据集中正样本远多于负样本(比如 9:1),MultinomialNB 的 fit_prior=True 学到了这个先验,预测时对所有新文本都倾向输出多数类。准确率高只是假象,“把全部预测为正向”就能拿到 90% 的准确率。
解决:一是切分数据时用 stratify=labels,保证训练集和测试集里的正负比例一致;二是做类别平衡,常见做法是对少数类做上采样,也就是把负向样本复制若干份,或者用 sklearn 的 class_weight 做加权——但注意 MultinomialNB 本身不支持 class_weight 参数,所以更简单的方法是手动构造平衡训练集:在训练集里把少数类样本重复采样到和多数类接近的数量。写完这一点,报告里也要如实说明你做了类别平衡,否则答辩老师看混淆矩阵一眼就能抓住这个漏洞。
4.5 换一台电脑预测结果突然变了:分词版本和向量化器不一致
现象:在自己电脑上跑得好好的模型,存下来发给同学,对方加载之后预测结果对不上,甚至直接报错。
原因:一是 jieba 版本不同,新版词典更新导致同一句文本分词结果不同,特征向量也变了;二是 pickle/joblib 的文件跨平台加载时,如果 sklearn 版本不一致,MultinomialNB 的类定义可能对不上,报 ModuleNotFoundError 或 AttributeError。
解决:提交大作业时,把 3 个核心依赖的版本写进 requirements.txt:scikit-learn、jieba、pandas。模型文件没办法跨版本保证兼容,保险起见,在报告里附上一段预测脚本,让老师直接跑脚本而不依赖你保存的 pkl 文件。这不算过度操作——我自己就遇到过 jieba 从 0.39 升到 0.42 之后,同一句话的分词结果变了,导致复现实验和原始结果对不上。
5. 让分数再往上走的调优与验证技巧:交叉验证、特征选择与报告落地
前面这些流程跑通,你已经拿到了一个能交差的大作业。但如果想往 85 分以上冲,还有几个成本很低但效用很高的技巧可以补上去。第一个是网格搜索确定最优 alpha,不要凭感觉填 1.0。sklearn 的 GridSearchCV 配合 Pipeline,可以把向量化器和分类器的参数一起搜,常见的做法是固定 ngram_range,把 alpha 在 [0.01, 0.1, 0.5, 1.0, 2.0] 里搜一遍,用 5 折交叉验证选最优组合。
第二个技巧是看学习曲线判断模型是欠拟合还是过拟合。朴素贝叶斯在文本分类上通常不会严重过拟合,但如果训练集和测试集交叉验证分数都低,说明特征工程没做够——大概率是停用词没过滤干净或 ngram_range 太短。反过来如果训练集分数很高、测试集分数断崖下跌,说明模型过拟合了,常见原因是 max_features 开太大或 min_df 太低,模型记住了太多低频噪声词。
还有一个容易被忽略的报告加分点:把你模型学到的关键特征词可视化。用 model.feature_log_prob_ 和 vectorizer.get_feature_names_out() 相互配合,选出在正负类别下条件概率差异最大的前 20 个词,画一个横向条形图。这段代码很短,但呈现出来的效果非常直观——老师一眼就能看懂你的模型“学到了什么”。我做课程设计的时候,就因为图里出现了“特效”“演技”“剧情拖沓”这类真正有判别力的词,在答辩时被表扬过一次。
最后说一个我自己的习惯:拿到这份源码和数据集之后,不要急着改代码,先把数据读出来,打印前 20 条评论,人工看一遍。这一步虽然不产生任何模型指标,但能让你建立对数据分布的直觉——比如某类评论有明显的固定句式(“物流很快”“卖家服务态度好”),这类句子会对特征造成强烈的偏置,后续调参时你心里会有数。看完数据再动手写代码,踩坑至少少一半。希望帮到你。
本文还有配套的精品资源,点击获取