尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于朴素贝叶斯的豆瓣影评情感分类工程实践

基于朴素贝叶斯的豆瓣影评情感分类工程实践 简介面向需要完成朴素贝叶斯文本分类课程设计的学生这份资源以豆瓣影评情感分析为实战场景完整覆盖从数据读取、清洗、分词到特征构建、模型训练评估的Python实现流程。压缩包共包含十个文件总大小3.27MB主要有Python脚本、Jupyter Notebook、CSV数据集、停用词表与说明文档等目录划分清晰可直接对照运行。目前已有三百九十六人学习下载。代码基于sklearn的MultinomialNB实现数据集为豆瓣短评可通过调整平滑因子观察分类效果变化并配合说明文档快速了解整体工程结构。除建模主体外还提供分词、去停用词、TF-IDF特征构建以及准确率、召回率等评估脚本帮助深入理解文本情感分类中每个环节的实际作用整个项目可直接作为课程设计报告和扩展实验的绝佳参考。1. 朴素贝叶斯做豆瓣影评情感分类先看清边界在哪朴素贝叶斯是所有机器学习算法里数学形式最直白的却一直稳稳占着文本分类的头部位置。豆瓣影评情感分类适合拿它做成课程设计因为高维稀疏的影评文本和贝叶斯条件概率建模天然对得上。垃圾邮件过滤、新闻分类、情感判定本质都是同一类问题。“特征独立假设”在真实语言里并不成立可实践里它总是比深度模型收敛更快在中小规模语料上表现稳定。整个课程设计要做的不只是调用一次 MultinomialNB而是把采集、清洗、分词、训练、评估到交付的完整链路拆开验证。处理豆瓣影评时最该关注的是否定结构、转折句和反讽表达这三类样本决定了一个朴素贝叶斯模型的上限。接下来的章节按“数据 → 特征 → 模型 → 评估 → 部署”的顺序展开代码和参数全部可以直接在本地环境复现项目做到最后能交付、能答辩、能面对任何追问。2. 从采集到向量化豆瓣影评语料清洗、分词与 TF-IDF 特征构造2.1 数据获取公开数据集与爬虫方案的取舍豆瓣对爬虫比较敏感课程设计阶段没必要硬刚反爬。常见做法是先去 GitHub 上找整理好的豆瓣短评数据集搜“豆瓣评论 数据集”通常能拿到 CSV 版本字段一般包含星级、评论内容、点赞数。注意检查正负标签是否完整、评论是否有重复这两点直接决定后续训练效果。如果必须自己爬取就得考虑豆瓣的反爬限制。控制请求频率、随机 User-Agent、设置合理超时都只是最基本的底线。爬到的数据要保留完整字段尤其不能丢掉星级评分因为它是整个情感标签的来源。常见的标注规则是4 到 5 星映射为正面1 到 2 星映射为负面3 星丢弃。这个映射和豆瓣产品本身的评分语义一致答辩时也有据可查。import pandas as pd df pd.read_csv(douban_movie_comments.csv) df df[[star, comment]].dropna() # star 为豆瓣评分星级范围 1~54、5 星视为正面1、2 星视为负面3 星中性丢弃 df[sentiment] df[star].map(lambda x: 1 if int(x) 4 else (0 if int(x) 2 else None)) df df.dropna(subset[sentiment]) # 打印正负样本数量警惕某一方过少 print(df[sentiment].value_counts())上面这段代码用map把分值转换成 0/1int(x)是为了兜底处理字符串型的星级字段。丢弃 3 星意味着模型刻意避开“一般般”这种模棱两可的表达因为把它硬塞进正类或负类都会引入标签噪声噪声会直接拉低后面所有评估指标。正负样本量差距如果超过 3 倍后续训练前就需要做降采样或合成样本这一步先留意。2.2 文本清洗去掉噪音同时保住否定词影评里常见 HTML 实体、URL、 用户名、重复空白。清洗原则是只删结构噪音不删语义词。一个非常容易踩的坑是直接套用通用中文停用词表把“不”“没”“别”这样的否定词一并过滤。一旦“不”被删除“不好看”就变成“好看”影评情感极性直接反转后面无论怎么调参都补不回来。import re def clean_text(text: str) - str: # 去掉 URL 和 HTML 标签 text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(r[^], , text) # 全角空格统一成半角压缩连续空白 text text.replace(\u3000, ).replace( , ) text re.sub(r\s, , text).strip() return text df[cleaned] df[comment].apply(clean_text)清洗要克制规则加得越多后续排查错误样本时越难定位。数字也不着急删“38 分钟全在铺垫”这类的吐槽离不开数字。句号和逗号对情感判定没有正贡献但会影响 token 切分统一用空格替换即可。注意停用词表要单独检查一遍凡是带否定含义的词都不能放进去“不、没、别、无、莫、未”都要保留。2.3 jieba 分词自定义词典的取舍顺序豆瓣影评里有很多网络新词jieba 默认词典会把它们切成没有语义的碎片。比较好的做法是往自定义词典里加入高频影评词汇比如“烂片”“神作”“二刷”“自来水”“出戏”。词频参数设置得高一些可以避免词被拆开。但别盲目覆盖词典词加多了反而会影响原词的切分稳定性。import jieba # jieba.add_word 给单个词设定词频和词性词频越高越倾向整体成词 for w in [烂片, 神作, 自来水, 二刷, 烧脑, 败笔, 出戏]: jieba.add_word(w, freq20000, tagn) df[tokens] df[cleaned].apply(lambda x: jieba.lcut(x, cut_allFalse)) print(df[tokens].head(3))cut_allFalse使用精确模式这是情感分类的标准选择。如果开了全模式一条评论会被切成大量冗余组合TF-IDF 矩阵变成更稀疏的碎片集合模型收益反而下降。分词阶段我一般不做词性过滤因为情感表达里副词“太”“真”“超”和程度词的作用很大留着让 TF-IDF 自己给权重比手动筛选更稳。2.4 TF-IDF 向量化min_df、max_df 和 ngram_range 的选择分词结果进入 TfidfVectorizer 后每一句评论变成一个定长向量。TF-IDF 在词频的基础上降低了“电影”“一部”这类高文档频率词的权重情感分类场景里比 CountVectorizer 直接用词频更合适。下面这组参数是中文影评项目里比较稳的起点参数推荐取值作用min_df3 或 5过滤只出现几次的词压掉拼写噪音max_df0.85跳过在 85% 以上文档里出现的高频词ngram_range(1, 2)保留二字相邻组合比如“不难看”max_features10000限制词典规模避免实验内存溢出sublinear_tfTrue使用 1log 缩放词频抑制长评论权重from sklearn.feature_extraction.text import TfidfVectorizer df[text_for_vec] df[tokens].apply(lambda x: .join(x)) vec TfidfVectorizer( min_df3, max_df0.85, ngram_range(1, 2), max_features10000, sublinear_tfTrue, token_patternr\S ) X vec.fit_transform(df[text_for_vec]) y df[sentiment].values print(词典规模:, len(vec.vocabulary_)) print(矩阵形状:, X.shape)这里最关键的是token_patternr\S。分词结果已经是用空格隔开的中文词如果保留默认的正则容易被标点干扰再次切分。ngram_range(1, 2)的意义在于让“没有”“惊喜”这类否定词能和后面的中心词绑定在一起朴素贝叶斯虽然不建模词序但特征层面至少保留了一部分局部语义。3. 用 sklearn 构建朴素贝叶斯模型多项式分布、平滑系数与交叉验证3.1 三个朴素贝叶斯变体如何选高斯、伯努利、多项式sklearn 里的朴素贝叶斯三个常用变体对应不同的特征分布假设。高斯朴素贝叶斯假设特征符合正态分布适合连续数值特征但文本向量是稀疏的非负计数或 TF-IDF 值分布明显偏离正态效果通常一般。伯努利朴素贝叶斯只关心词出现与否用 0/1 矩阵输入适合社交帖子分类。多项式朴素贝叶斯把词频当作计数和 TfidfVectorizer 的输出天然匹配是文本情感分类的实际首选。变体特征输入适用场景影评项目里的表现GaussianNB连续实数数值型表格数据稀疏矩阵上效果差BernoulliNB0/1 布尔短文本、用户画像准确率略低训练更快MultinomialNB非负计数 / TF-IDF新闻分类、垃圾邮件、影评默认首选调 alpha 有提升空间利用 sklearn 构建朴素贝叶斯模型做文本分类最终组合几乎都收敛到 MultinomialNB 加 TfidfVectorizer。如果换用 CountVectorizer特征值是整数计数MultinomialNB 依然兼容只是长影评的词频天然更高会隐式增加一个长度偏置一般不建议在课程设计里用这个组合。3.2 MultinomialNB 的 alpha 平滑与 fit_prior 含义MultinomialNB 的核心是频率计数概率表P(词|类别)的计算方式是某个词在类别内出现的次数加 alpha除以类别内总词数加 alpha 乘以词典大小。alpha 默认 1.0也就是拉普拉斯平滑作用是防止某个词在训练集某个类别里一次没出现过时预测概率直接变成 0。from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy # 保持正负样本比例一致 ) model MultinomialNB(alpha1.0, fit_priorTrue) model.fit(X_train, y_train) print(train acc:, model.score(X_train, y_train)) print(test acc:, model.score(X_test, y_test))stratifyy在不平衡数据上是必须的否则测试集里可能某个类别样本特别少评估结果方差很大。fit_prior控制是否从训练集学习类别先验如果数据集里正面评论远多于负面评论模型会倾向于预测正面等后面做混淆矩阵时你会看到正例召回率虚高、负例召回率偏低同时出现那就是先验失衡的典型影响。3.3 用网格搜索确认 alpha 的合理区间alpha 太小比如 0.001平滑几乎不起作用没见过的词概率直接归零测试集上遇到新表达时预测置信度会非常难看。alpha 太大比如 10所有词概率被拉向均匀分布特征区分度被抹平。中文影评数据集的经验区间一般在 0.05 到 2.0 之间。import numpy as np from sklearn.model_selection import GridSearchCV param_grid { alpha: np.linspace(0.05, 2.0, 20) } gs GridSearchCV( MultinomialNB(), param_grid, cv5, scoringf1_macro, n_jobs-1, verbose1 ) gs.fit(X_train, y_train) print(best alpha:, gs.best_params_[alpha]) print(best f1 macro:, gs.best_score_)f1_macro对两个类别分别计算 F1 再取平均不会因为某一个类准确率高就掩盖另一个类的弱势。alpha 在 0.05 到 2.0 区间取 20 个值五折交叉验证也只是训练 100 个模型MultinomialNB 训练本身就是分钟级别跑起来没有压力。最终拿到的gs.best_params_[alpha]再放到 3.2 节的全量模型里复训。3.4 数据量变大时的 partial_fit 增量训练课程设计做到一半往往发现语料不够要继续补爬数据。如果每次都重新 fit 全量语料向量化阶段很耗时间。MultinomialNB 支持partial_fit可以分批喂数据并持续更新概率表。from sklearn.naive_bayes import MultinomialNB vec_part TfidfVectorizer(min_df3, max_features10000) model_part MultinomialNB(alpha0.5) # 第一次调用必须传入 classes后续批次可省略 model_part.partial_fit(vec_part.fit_transform(df[text_for_vec].iloc[:2000]), y[:2000], classesnp.array([0, 1])) model_part.partial_fit(vec_part.transform(df[text_for_vec].iloc[2000:4000]), y[2000:4000])这里有个容易忽略的坑partial_fit不会自动更新 TfidfVectorizer 的词典。如果第二批数据里出现前 2000 条没有的新词transform会直接丢弃这些词因为它们不在idf_表里。增量训练有个前提是词汇表已经固定否则需要定期拿全量语料重建向量器。课程设计里更稳妥的方案还是全量重建节省时间的收益其实没有想的那么大。4. 评估与错误分析混淆矩阵、类别不平衡与数据泄漏排查4.1 准确率之外的三组指标混淆矩阵定位偏置方向情感分类评测里准确率只是一个总体结论。准确率 85% 到底是因为模型把负面评论全判成了正面还是均匀出错只有混淆矩阵能说清楚。二分类矩阵里四个格子各代表一类错误FN 是漏掉的负面FP 是误伤的正面。from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt y_pred gs.best_estimator_.predict(X_test) print(classification_report(y_test, y_pred, target_names[负面, 正面], digits3)) cm confusion_matrix(y_test, y_pred) ConfusionMatrixDisplay(cm, display_labels[负面, 正面]).plot() plt.show()classification_report里的 precision 是“预测为正面里有多少真是正面”recall 是“真实正面里有多少被找出来了”。对影评情感分类多数场景更关注 recall因为漏掉一个差评比把好评误判成差评的代价通常更大。如果报告里正面 recall 远高于负面 recall说明类别不平衡效应已经进入概率先验接下来要处理数据分布而不是继续调参。4.2 错误样本的三层分析极短评、转折句与反讽把预测错误的样本单独拿出来看是课程设计里最能体现工程能力的部分。常见错误集中在三类错误类型典型样本模型行为排查方向极短文本“不值得。”特征只剩“值得”否定前缀被切开增补否定词词典扩大 ngram转折结构“演技在线但剧本太弱”正负词同时命中概率互抵转折词后局部特征加权反讽表达“真好一部让我睡了三遍的电影”正面词权重过高被判正面引入更长 ngram 或替换模型排查时不要只看几眼文案就下结论要把三种类型样本各自筛出来统计占比再决定下一步是调向量化参数还是换模型。mis_mask y_pred ! y_test # 取出预测错误的原始评论按长度分组观察 mis_text df.loc[X_test.indices[mis_mask], comment] short_mask mis_text.str.len() 10 print(10 字以内的误判评论数:, short_mask.sum()) # 含有“但”“但是”的转折型误判 turn_mask mis_text.str.contains(但|但是|不过) print(含转折词的误判评论数:, turn_mask.sum())上面这段代码演示的是如何把错误批量归类。实际操作里 X_test 是稀疏矩阵切片取原始文本时需要通过索引映射回 df所以 2.1 节建 DataFrame 时就要保留原始行号。对“但”字的统计能直观判断出 ngram_range(1,2) 是否已经吸收了转折结构的局部信息。4.3 类别不平衡与数据泄漏准确率虚高的两处源头课程设计里一个高频陷阱就是数据泄漏。比如把点赞数、回复数当成特征预测阶段根本没有这些值又或者把全量语料做 TF-IDF 拟合后再划分训练集和测试集测试集的词汇信息已经进入了词表。正确的做法是 TfidfVectorizer 只对训练集 fit再对测试集 transform上面的 pipeline 写法已经规避了这个问题。注意TfidfVectorizer 必须只对训练集 fit测试集只能 transform否则会产生数据泄漏评估结果全部失真。另一种更隐蔽的问题是重复评论。豆瓣上“同上”、“1”和被大量转发的同一句话容易在数据集中重复训练集和测试集各出现一条一样的评论模型在测试集上等于“背过答案”准确率虚高。去重非常简单# 去重是必须做的一步 df df.drop_duplicates(subset[comment]) # 太短的评论缺少足够特征直接过滤 df df[df[comment].str.len() 4] # 如果正负样本量超过 3:1做多数类降采样 negative df[df[sentiment] 0] positive df[df[sentiment] 1] if len(negative) len(positive) * 3: negative negative.sample(nlen(positive), random_state42) df pd.concat([positive, negative]).sample(frac1, random_state42)降采样是最省事的类别平衡手段因为 MultinomialNB 没有内置class_weight参数。经过重复评论过滤和降采样后之前 4.1 节里一高一低的 recall 通常会明显收敛这样的模型才具备真实参考价值。5. 模型持久化与置信度阈值做一版可以交给别人调用的分类器5.1 用 joblib 同时保存向量器和模型课程设计的交付物不能只在 Jupyter Notebook 里能跑。常见做法是把 TfidfVectorizer 和 MultinomialNB 放进同一个 Pipeline训练完整体持久化加载时不必再单独构造分词组件。import joblib from sklearn.pipeline import Pipeline from sklearn.naive_bayes import MultinomialNB pipe Pipeline([ (vec, vec), (clf, MultinomialNB(alphags.best_params_[alpha])) ]) pipe.fit(df[text_for_vec], y) joblib.dump(pipe, douban_nb_pipeline.joblib)这里把 vec 和 clf 重新放进 Pipeline 再 fit 一遍是因为前面网格搜索是在已经向量化的 X 上做的只有这样才能得到一个能端到端处理原始文本的完整模型。加载时只需joblib.load这一个步骤省去了重新载入词典和 jieba 自定义词的麻烦。5.2 预测置信度阈值不把所有评论都强行二分类predict只返回 0 或 1但predict_proba能给出每个类别的概率。实际使用中模型在 0.51 和 0.98 时的可信度天差地别。一个常见的工程做法是置信度大于 0.7 的直接判决0.4 到 0.7 之间的丢进人工复核队列既能减少误判也不至于让审核人员做无用功。proba pipe.predict_proba(df[text_for_vec].iloc[:5])[:, 1] pred (proba 0.65).astype(int) for text, p, label in zip(df[comment].iloc[:5], proba, pred): print(f{p:.3f} - {正 if label else 负} | {text[:30]})阈值 0.65 不是拍脑袋决定的而是在验证集上画出正负样本的概率分布直方图看两个分布的交叉点交叉位置附近就是最优阈值区间。把这张直方图和阈值选择的依据放进课程设计文档里比只贴一个准确率数字有说服力得多。5.3 准备一个小的回归验证集为了确保后续调整不会引入回归问题可以固定三个典型验证样本极端好评、极端差评、包含转折结构的中性评论。每改一次向量化参数或模型参数就跑一遍这三组样本比较预测概率的变化方向。比如“剧本烂透了但特效值得一看”这条ngram_range(1,1)时可能被误判为负面换成(1,2)后大概率接近正面这个回归集就是守住已有成果的最小代价方案。把这一步做完课程设计就具备了一个合格交付物的全部要素可复现的数据处理链路、经过交叉验证的模型参数、以及能独立运行的预测接口。后面想换数据集只需重新执行第 2 章的清洗与向量化想换模型替换 Pipeline 里的 clf 组件即可两条路径都不会影响整体结构。本文还有配套的精品资源点击获取
返回列表