☰
烂番茄影评情感分类:从数据清洗到模型评估的完整NLP实战
2026/10/9 10:38:22 网站建设 项目流程

简介:这份资源是华中科技大学Python大数据与人工智能实践课程的大作业完整交付包,面向计算机、人工智能、通信、自动化等专业的在校学生及自学者,可用于课程设计、毕业设计、作业提交或项目立项演示。核心任务是基于Python对烂番茄电影评论做情感分类,覆盖数据预处理、特征提取、模型训练与评估的完整流程。压缩包共555个文件,约41.89MB,包含6个py源码、7个csv数据集、14个npy特征文件、104个pdf实验报告与说明文档,以及大量TensorBoard训练日志和模型检查点文件,另附tex与bib论文排版素材,目录结构清晰,便于按模块查阅。已有158人学习下载。读者可获得可运行的完整源码、实验报告、训练过程记录与数据文件,既能直接复现情感分类实验,也能在此基础上修改模型或迁移到其他文本分类任务,适合作为入门深度学习与大数据实践的参考案例。

1. 烂番茄影评情感分类:从课程大作业到能跑通的完整链路

烂番茄电影评论的情感分类,是很多高校 Python 大数据与人工智能实践课里出现频率极高的一类大作业题目。它表面上是个二分类任务——把一条影评判成“好评”或“差评”,但真正动手做过的人都知道,坑几乎全在数据侧和工程侧:评论里混着 HTML 标签、否定句、反讽,正负样本还不平衡。这个方向适合两类人:一类是要交课程大作业、需要一份能复现的完整方案的学生;另一类是想拿一个真实文本数据集练手 NLP 全流程的开发者。它不需要 GPU 集群,一台普通笔记本就能跑完从数据清洗到模型评估的全过程,性价比很高。下面我按自己实际做这类任务的顺序,把整条链路拆开讲清楚。

2. 先搞清楚数据和任务边界:烂番茄影评到底难在哪

2.1 数据集长什么样,字段怎么用

常见的烂番茄影评数据一般以 CSV 或 TSV 形式给出,核心就两列:一列是评论文本,一列是情感标签。标签有的是字符串positive/negative,有的是 0/1。拿到数据第一件事不是急着建模,而是先确认三件事:标签分布是否均衡、文本里有没有残留的 HTML 标签、有没有空值和重复行。

import pandas as pd # 读取数据,注意分隔符可能是逗号也可能是制表符 df = pd.read_csv("reviews.csv") # 统一列名,避免后续到处改 df.columns = ["review", "sentiment"] # 看标签分布,判断是否需要处理不平衡 print(df["sentiment"].value_counts()) # 检查空值和重复 print("空值:", df.isnull().sum().to_dict()) print("重复行:", df.duplicated(subset=["review"]).sum()) # 看一条原始样本,确认有没有 HTML 残留 print(df["review"].iloc[0][:300])

这段代码的逻辑是先做数据体检。value_counts()告诉你正负样本比例,如果一边是另一边的三倍以上,后面评估就不能只看准确率。duplicated用来发现爬取时重复入库的评论,这类重复如果不删,训练集和测试集之间会泄漏,指标虚高。最后打印原始样本,是为了确认文本里是否夹着<br />这类标签,这决定了清洗步骤要写多重。

2.2 为什么不能直接上深度学习模型

很多人一上来就想用 BERT,觉得效果好。但在课程大作业这个场景里,数据量通常只有几千到几万条,直接微调大模型有两个现实问题:一是训练时间长,普通笔记本跑不动;二是小数据上大模型容易过拟合,反而不如传统方法稳。我的建议是先跑通一条基线:TF-IDF 加逻辑回归。这条基线几分钟就能出结果,准确率往往能到 80% 上下,先把这个数字拿到手,再决定要不要往上加复杂度。

选型上,词袋类方法(TF-IDF)对情感分类这种关键词信号强的任务其实很够用,“awful”“brilliant”这类词本身就是强特征。深度模型的价值在于捕捉语序和上下文,比如“not bad”这种否定结构,但代价是调参成本高。课程作业的评分点通常在于流程完整和结果可解释,而不是刷到最高分,所以基线优先是更稳的策略。

2.3 划分数据集时最容易忽略的一步

划分训练集和测试集时,一定要在清洗之后、向量化之前做,并且固定随机种子。更关键的是:如果数据里有同一部电影的多条评论,最好按电影分组划分,避免同一部电影的评论同时出现在训练和测试里,导致模型“见过”相似表达而虚高。

from sklearn.model_selection import train_test_split # 固定 random_state 保证每次划分一致,方便复现 train_df, test_df = train_test_split( df, test_size=0.2, random_state=42, stratify=df["sentiment"] # 保持正负比例一致 ) print("训练集:", len(train_df), "测试集:", len(test_df))

stratify参数保证划分后训练集和测试集的正负比例与原始数据一致,避免测试集恰好偏向某一类。random_state=42是复现的关键,换机器、换时间跑出来的结果应该完全一样,这在写实验报告时很重要——评审会看你结果是否可复现。

3. 文本清洗与特征工程:决定上限的一步

3.1 清洗管道的四个动作

文本清洗不是越狠越好。我的经验是只做四件事:去 HTML 标签、转小写、去多余空白、保留标点中的否定词相关符号。不要轻易去停用词,因为“not”“no”这类词对情感判断至关重要,去掉它们等于自废武功。

import re def clean_text(text): # 去掉 HTML 标签,如 <br /> <p> text = re.sub(r"<[^>]+>", " ", text) # 只保留字母、数字和基本标点,去掉乱码字符 text = re.sub(r"[^a-zA-Z0-9\s!?.,']", " ", text) # 转小写 text = text.lower() # 合并多余空白 text = re.sub(r"\s+", " ", text).strip() return text train_df["clean"] = train_df["review"].apply(clean_text) test_df["clean"] = test_df["review"].apply(clean_text) print(train_df["clean"].iloc[0][:200])

正则<[^>]+>匹配任意 HTML 标签并替换为空格,注意是空格不是空字符串,否则两个词会粘在一起。第二个正则保留!?.,'这几个符号,是因为感叹号和问号在影评里带有情绪强度信息,撇号用于处理don't这类缩写。清洗后一定要打印一条看看,确认没有把正常内容误删。

3.2 TF-IDF 参数怎么设

TF-IDF 有两个核心参数:max_features和ngram_range。max_features控制词表大小,太小会丢信息,太大会增加噪声和内存占用。影评数据我一般设 10000 到 20000。ngram_range设成(1, 2)能捕捉“not good”这种二元组合,对情感分类提升明显。

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=15000, # 词表上限,控制维度 ngram_range=(1, 2), # 同时用单词和二元词组 min_df=2, # 出现少于2次的词丢弃,降噪 sublinear_tf=True # 用 1+log(tf) 抑制高频词 ) X_train = vectorizer.fit_transform(train_df["clean"]) X_test = vectorizer.transform(test_df["clean"]) print("特征维度:", X_train.shape)

关键点是fit_transform只在训练集上调用,测试集必须用transform,否则测试集的词表信息会泄漏进训练过程。min_df=2过滤掉只出现一次的词,这些词多半是拼写错误或专有名词,对泛化没帮助。sublinear_tf用对数缩放词频,防止某个词反复出现主导权重。

3.3 标签编码与类别不平衡处理

如果标签是字符串,需要转成数字。同时检查一下不平衡程度,必要时用class_weight让模型更关注少数类。

from sklearn.preprocessing import LabelEncoder le = LabelEncoder() y_train = le.fit_transform(train_df["sentiment"]) y_test = le.transform(test_df["sentiment"]) # 查看编码对应关系 print(dict(zip(le.classes_, le.transform(le.classes_))))

LabelEncoder会把类别按字母序编码,所以negative可能被编成 0,positive编成 1,具体要看打印结果,写报告时别搞反。如果正负比例超过 2:1,在训练模型时加class_weight="balanced",让损失函数对少数类加权,这比盲目过采样更稳。

4. 建模、训练与评估:把基线跑出可信数字

4.1 逻辑回归基线的最小实现

逻辑回归在文本分类上是经典强基线,训练快、可解释、不容易过拟合。先用它把整条链路跑通。

from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix clf = LogisticRegression( C=1.0, # 正则强度,越小正则越强 max_iter=1000, # 保证收敛 class_weight="balanced" # 处理不平衡 ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred, target_names=le.classes_)) print(confusion_matrix(y_test, y_pred))

C是正则化系数的倒数,C越小正则越强,越不容易过拟合,但也可能欠拟合。文本任务上 0.5 到 2 之间试几个值。max_iter=1000是为了避免收敛警告,默认 100 在特征维度高时经常不够。classification_report会给出每类的精确率、召回率和 F1,比单看准确率信息量大得多。

4.2 用交叉验证代替单次划分

单次划分的结果有随机性,交叉验证能给出更稳的估计。在课程报告里放交叉验证结果,说服力比单次高很多。

from sklearn.model_selection import cross_val_score from sklearn.pipeline import Pipeline # 把向量化和分类器串成管道,避免数据泄漏 pipe = Pipeline([ ("tfidf", TfidfVectorizer(max_features=15000, ngram_range=(1, 2), min_df=2)), ("clf", LogisticRegression(C=1.0, max_iter=1000, class_weight="balanced")) ]) scores = cross_val_score(pipe, train_df["clean"], y_train, cv=5, scoring="f1") print("5折F1:", scores.mean().round(4), "标准差:", scores.std().round(4))

用Pipeline的好处是交叉验证时,每一折的向量化只在该折的训练部分拟合,彻底杜绝泄漏。这是很多人手写流程时最容易犯的错——先对整个数据集做 TF-IDF 再交叉验证,指标会虚高好几个点。scoring="f1"在不平衡数据上比准确率更合适。

4.3 换模型对比:朴素贝叶斯和线性 SVM

跑完基线后,可以横向对比几个模型,报告里做个表格,显得工作扎实。

模型5折F1均值训练耗时特点
逻辑回归0.82快可解释,系数能看关键词
朴素贝叶斯0.79极快适合小数据,假设强
线性SVM0.83中等边界任务表现好

朴素贝叶斯假设特征独立,在文本上这个假设不成立但效果还行,胜在快。线性 SVM 在高维稀疏特征上通常比逻辑回归略好,但训练慢一些,且不直接输出概率。选哪个取决于你要不要概率输出——如果报告里要画 ROC 曲线,逻辑回归更方便。

5. 避坑与排查:那些让指标虚高或直接翻车的点

5.1 数据泄漏导致准确率虚高

现象:测试集准确率 95% 以上,但换一批新数据就崩。原因:在划分数据集之前就做了向量化或用了全局统计量。解决:所有拟合类操作(TF-IDF、标准化、编码)只能在训练集上 fit,测试集只 transform,用 Pipeline 强制约束。

5.2 否定词被停用词表误删

现象:模型把“not good”判成好评。原因:清洗时用了通用停用词表,把 not、no、never 删掉了。解决:情感任务不要用通用停用词表,或者用自定义停用词表,明确保留否定词。这是血泪经验,很多人栽在这。

5.3 标签编码顺序搞反

现象:报告里写“1 代表好评”,实际代码里 1 是差评。原因:LabelEncoder 按字母序编码,negative 在前。解决:打印le.classes_确认映射,或者在报告里直接写清楚编码规则,别凭感觉。

5.4 内存溢出或训练极慢

现象:max_features设太大,或者 ngram 设到 (1,3),内存直接爆。原因:三元词组让特征维度爆炸式增长。解决:影评任务 (1,2) 足够,max_features控制在 20000 以内,min_df设 2 以上过滤长尾。

5.5 交叉验证和最终评估用了同一批数据

现象:报告里交叉验证 F1 和测试集 F1 几乎一样,看起来完美。原因:调参时反复看测试集,测试集已经变成了验证集。解决:留一份从不参与任何调参的 hold-out 测试集,最后只跑一次,这个数字才是可信的。

6. 进阶技巧:让这份大作业从及格变成出彩

如果基线已经跑通,想让结果更有说服力,有几个方向值得投入。第一是错误分析:把预测错的样本导出来,人工看几十条,归纳错误类型。你会发现很多错误集中在反讽和混合情感上,比如“演技很好,但剧本烂到家”。把这类样本单独拎出来讨论,比单纯报一个准确率有深度得多。

# 导出预测错误的样本,做人工分析 test_df = test_df.copy() test_df["pred"] = le.inverse_transform(y_pred) errors = test_df[test_df["pred"] != test_df["sentiment"]] errors[["review", "sentiment", "pred"]].to_csv("errors.csv", index=False) print("错误样本数:", len(errors))

这段代码把错分样本存成 CSV,方便逐条看。重点看两类:一类是模型把明显好评判成差评,说明特征没覆盖到;另一类是反讽,这类靠词袋模型基本无解,正好作为“局限性”写进报告,体现你清楚方法的边界。

第二个方向是看逻辑回归的系数,找出对情感判断贡献最大的词。这既是可解释性分析,也能验证模型是否学到了合理的东西。

import numpy as np feature_names = vectorizer.get_feature_names_out() coefs = clf.coef_[0] # 取权重最高的正负各15个词 top_pos = np.argsort(coefs)[-15:] top_neg = np.argsort(coefs)[:15] print("最正面词:", [feature_names[i] for i in top_pos]) print("最负面词:", [feature_names[i] for i in top_neg])

如果正面词里出现一堆人名或电影名,说明模型在走捷径,靠特定实体判断情感,泛化会有问题。这时候要考虑把这些高频专有名词加进停用词,或者用min_df调高过滤掉。这个检查我每次做文本分类都会跑一遍,能提前发现很多隐患。

第三个方向是尝试词向量。用预训练的词向量做平均池化,再喂给分类器,能捕捉到“excellent”和“great”的相似性,对同义表达更鲁棒。但要注意,词向量文件通常几百 MB,加载慢,课程作业里如果时间紧,优先级排在错误分析之后。

最后一个习惯:所有实验都记一个简单的日志表,记录每次改了什么参数、结果是多少。我一般用一个 CSV 记,字段是时间、改动、F1、备注。这样写报告时不用回忆,直接拉表。做这类大作业,流程完整、结果可复现、错误分析到位,比盲目堆模型更能拿高分。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询