
简介面向高校人工智能、大数据相关专业课程设计与毕业设计的一份完整实践资源以Python实现烂番茄电影评论情感分类涵盖数据清洗、特征工程、模型训练与评估等环节。配套实验报告与数据集适合具备一定Python基础的在校学生、教师或企业初学者参考学习也可作为课设、毕设或项目立项演示的起点。压缩包共555个文件约41.89MB以104个PDF文档实验报告与说明、14个NPY数据文件、7个CSV数据集、6个Python源码、5个Visio图表为主另含大量TensorFlow训练日志tfevents与中间结果文件便于复现训练过程。已有158人学习下载。代码经测试运行成功答辩平均分96分结构较完整下载后可根据README快速上手在现有模型基础上调整参数或更换数据集以适配其他文本分类场景。1. 烂番茄影评情感分类这门大作业究竟要你交付什么拿到“华中科技大学 Python 大数据和人工智能实践大作业——基于 python 实现对烂番茄电影评论进行情感分类”这个题目时大部分人最先纠结的不是模型选什么而是“一份大作业到底要做到什么程度才不会被老师挑毛病”。这类作业的真实考核点有三个数据有没有预处理干净、模型有没有对比实验、实验报告能不能支撑结论。烂番茄影评情感分类是一个标准的有监督文本分类问题输入是影评文本输出是“新鲜”fresh正面或“腐烂”rotten负面二分类标签。它足够简单能在一两周内用 Python 跑通又足够完整能覆盖数据清洗、特征工程、机器学习模型、深度学习模型和评估报告这一整条 NLP 落地链路所以才会被反复选为大作业题目。这篇笔记面向两类人一是课程大作业需要交付源码、实验报告和数据的在校生二是想系统走一遍文本分类流程但还没动手的 Python 学习者。下面按我实际做这类项目的顺序从数据一路写到报告。2. 数据准备先看烂番茄影评长什么样再决定怎么清洗2.1 数据来源与字段影评文本和“新鲜度”标签的关系烂番茄的影评分成影评人评论critic reviews和观众评论audience reviews两类课程作业通常用的是观众评论因为口语化程度高、情感倾向明显、数量也多。每条观众评论自带一个“新鲜”或“腐烂”的评价标记这就是我们要预测的标签。常见做法是找一份已经整理好的公开影评数据集字段一般包含review_text、rating、label、movie_id几列。课程作业阶段不建议直接从烂番茄官网抓HTML一是反爬规则会耽误大量时间二是影评页面结构经常变动写出来的解析代码过两周就可能失效。更稳的做法是拿到公开CSV之后先确认列名和行数再决定清洗策略。即使只拿到原始文本没给标签也可以自己按五星评分做映射评分大于等于3.5星标为正面否则标为负面。这个阈值设定要写进实验报告因为后续所有指标都建立在它的基础上。数据文件下载后第一步不是急着上模型而是用pd.read_csv读进来看一眼shape、columns和head()。很多同学在这一步省事结果后面所有代码都跑不通才发现列名里有隐藏空格或编码不对。先做一次结构体检能省半天排查时间。2.2 用Pandas检查数据质量缺失值、重复评论、标签分布拿到数据后先跑一份基础体检。下面的代码可以原样复用import pandas as pd df pd.read_csv(rotten_tomatoes_reviews.csv, encodingutf-8) print(数据集形状:, df.shape) print(列名:, df.columns.tolist()) print(df.head()) # 检查缺失值 print(缺失值统计:) print(df.isnull().sum()) # 检查重复评论 dup df.duplicated(subset[review_text], keepFalse).sum() print(重复评论数:, dup)df.duplicated(subset[review_text])这一步很容易被忽略。烂番茄观众评论里有大量“A masterpiece”“Terrible film”这类短句重复出现如果不按文本去重模型会反复学习同一条样本导致验证集和测试集结果虚高。另一个必须检查的是标签分布用df[label].value_counts()看一眼正负样本比例。提示如果正面评论和负面评论比例超过3:1后面训练时就要考虑class_weight或采样策略否则模型很容易退化成“全预测多数类”准确率看起来还行实际没什么意义。2.3 文字清洗与标签映射HTML、URL、停用词和fresh/rotten烂番茄影评文本里常见的噪声有HTML标签比如br、URL链接、多余空白和特殊符号。清洗时用正则表达式处理比逐条手动替换要可靠得多。下面是一套适合英文影评的清洗流程import re import html def clean_text(text): if not isinstance(text, str): return text html.unescape(text) # 反转义 amp; lt; 等 text re.sub(r[^], , text) # 去HTML标签 text re.sub(rhttp\S|www\.\S, , text) # 去URL text re.sub(r[^a-zA-Z\s], , text) # 只保留英文字母和空格 text text.lower().strip() # 转小写 return text df[clean_text] df[review_text].apply(clean_text) label_map {fresh: 1, rotten: 0, positive: 1, negative: 0} df[label] df[label].str.lower().map(label_map) df df.dropna(subset[clean_text, label])这段清洗有三个参数值值得说明html.unescape负责处理amp;这类实体re.sub(r[^], , text)删掉HTML标签re.sub(r[^a-zA-Z\s], , text)会丢掉数字和标点——影评里“5 stars”会被变成“stars”。如果保留数字能让模型学到“评分”这个特征可以改成只去标点、保留数字的版本但我的经验是英文影评里数字噪声远大于信息量去掉更省事。停用词处理上要留意一个反直觉的细节像not、never这类否定词在情感分类里是强特征不能按常见列表一刀切删除。nltk.corpus.stopwords.words(english)里包含not如果直接过滤句子“I do not like this film”会变成“I like this film”情感方向完全反了。我一般只用sklearn.feature_extraction.text内置的strip_accents功能或者自定义一个不包含否定词的停用词表。清洗结果保存成clean_data.csv同时打印几条样本确认没有清洗过头。3. 基线模型与特征工程TF-IDF 逻辑回归先把分数打出来3.1 为什么先做基线而不是直接上深度学习很多大作业一上来就用BERT或LSTM结果训练时间长、显存不够、调参调到崩溃最后交上去的准确率还不如一个朴素贝叶斯。这不是模型的问题而是没有先建立基线。课程大作业的评分逻辑里老师看重的是“你知不知道不同模型之间该对比什么”而不是“你用了多高级的模型”。基线模型的作用有三个验证数据管道是否正确、提供一个可对比的下限、暴露标签或特征处理上的问题。我一般先用TF-IDF向量化 逻辑回归跑一套基线20分钟内能看到结果。逻辑回归在文本分类上一直是很强的基础模型尤其在二分类任务上它的线性决策边界配合高维稀疏TF-IDF特征效果能打到85%上下这个分数已经足够让大作业的“对比实验”部分有话可写。深度学习模型后面再加进表格里前后形成“简单模型 vs 复杂模型”的梯度正好呼应大作业题目里“大数据和人工智能”的定位。上来就跑BERT一旦结果比不过逻辑回归报告会非常难写。3.2 TfidfVectorizer参数详解ngram_range、max_features、min_df怎么设TF-IDF把每条影评转成一个高维稀疏向量每个维度对应一个词或词组。TfidfVectorizer的参数直接决定特征矩阵的质量下面是完整训练流程from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix X_train, X_test, y_train, y_test train_test_split( df[clean_text], df[label], test_size0.2, random_state42, stratifydf[label] ) vectorizer TfidfVectorizer( ngram_range(1, 2), max_features30000, min_df2, sublinear_tfTrue ) X_train_vec vectorizer.fit_transform(X_train) X_test_vec vectorizer.transform(X_test) # 注意这里不能重新fit model LogisticRegression(C1.0, class_weightbalanced, max_iter1000) model.fit(X_train_vec, y_train)ngram_range(1, 2)表示同时使用单词和相邻两词组合这样像“not good”这样的词组不会被拆散对情感分类提升明显。max_features30000限制词表大小既控制内存又去掉低频噪声。min_df2表示至少在2条评论里出现过的词才保留避免某个拼写错误只出现一次却占了一个特征维度。sublinear_tfTrue对词频做对数压缩防止高重复词比如“movie”的频数压过真正有情感倾向的词。逻辑回归里的class_weightbalanced是在样本不平衡时稳定分数的关键参数它会按类别频率反向加权让少数类样本对损失函数的贡献加大。max_iter1000是因为高维稀疏特征下逻辑回归要更多迭代才能收敛默认的100轮经常提前停止会出现ConvergenceWarning。提示fit_transform只用在训练集测试集必须用transform复用已经学到的词表。如果测试集重新fit等于让模型在测试时“看见”了全部词汇分布指标会虚高这个错误后面我会专门讲。3.3 训练与评估classification_report怎么读、混淆矩阵怎么看评估代码很简单但读结果有讲究。运行下面的代码之后重点看三个数字y_pred model.predict(X_test_vec) print(classification_report(y_test, y_pred, target_names[腐烂, 新鲜])) print(confusion_matrix(y_test, y_pred))输出里precision表示预测为“新鲜”的评论里有多少确实为新鲜recall表示真实新鲜的评论里有多少被找了出来F1-score是两者的调和平均。大作业报告里如果只说准确率accuracy老师会觉得你不懂评估体系。准确率会在标签不平衡时骗人而F1能把少数类的真实表现暴露出来。混淆矩阵要按“实际标签 vs 预测标签”去解读。如果矩阵显示模型把大量“腐烂”预测成了“新鲜”说明模型学到的更多是“电影很棒”这类正面词语对讽刺语气和否定句不敏感。这个观察点写进报告比空泛写一句“模型表现很好”有说服力得多。我习惯把classification_report的输出原样贴进实验报告并逐行注释这不仅是凑字数更是让老师看到你真的理解这些指标的含义。4. 从基线到深度学习用PyTorch搭一个双向LSTM情感分类器4.1 词表构建与序列填充embedding层之前必须做的两步基线跑通之后第二步是上深度学习模型。常见选择是双向LSTM或TextCNN课程大作业场景下LSTM更容易讲清楚“序列建模”的道理。训练深度模型前要把文本转成固定长度的id序列这需要按顺序做三件事分词、建词表、序列填充。分词用nltk的word_tokenize或直接text.split()都可以。影评文本清洗后只剩下字母和空格用split()得到的词元列表已经够用还能少掉一堆NLTK的下载依赖。建词表时统计每个词的出现次数按频次排序赋id低频词统一映射成UNKfrom collections import Counter from torch.utils.data import Dataset, DataLoader import torch import torch.nn as nn def build_vocab(texts, min_freq2): counter Counter() for text in texts: counter.update(text.split()) vocab {PAD: 0, UNK: 1} for word, freq in counter.items(): if freq min_freq: vocab[word] len(vocab) return vocab def encode_text(text, vocab, max_len100): tokens text.split() ids [vocab.get(w, vocab[UNK]) for w in tokens[:max_len]] if len(ids) max_len: ids [vocab[PAD]] * (max_len - len(ids)) return idsmax_len100是因为烂番茄影评平均长度在40到60个词之间截断到100既能保留绝大部分信息又不会让序列太长拖慢训练。min_freq2和TF-IDF里的min_df同理去掉只出现一次的词词表控制在3万以内内存压力会小很多。PAD和UNK必须固定占用id 0和1nn.Embedding的填充索引默认是0这样padding位置不会参与学习。4.2 双向LSTM 线性层的模型结构与参数选择模型主体是nn.Embedding 双向LSTM 线性输出层。双向的意思是每个位置的隐状态同时融合前面和后面两方向的信息对判断“not good”这类局部反转很有效。class BiLSTMClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim100, hidden_dim128, num_layers2, num_classes2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.lstm nn.LSTM( embedding_dim, hidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0.0 ) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): emb self.embedding(x) # (batch, seq_len, embedding_dim) out, (h_n, _) self.lstm(emb) # out: (batch, seq_len, hidden*2) hidden torch.cat((h_n[-2], h_n[-1]), dim1) # 取最后一层两个方向 hidden self.dropout(hidden) return self.fc(hidden)几个参数值的取舍依据embedding_dim100是性能与模型规模的平衡点300维在影评这种小数据集上提升不明显却显著增加参数量。hidden_dim128配合双向结构最终进入全连接层的向量是256维对二分类任务来说表达力足够。num_layers2层数再多在几千条样本的小数据集上容易过拟合。dropout0.3只在训练时生效用于抑制过拟合。padding_idx0告诉PyTorch对PAD位置的embedding保持全零不参与反向传播。取h_n[-2]和h_n[-1]的写法比较绕意思是取最后一层LSTM前向和后向两个方向的最终隐状态拼起来作为整条序列的表征喂给分类层。4.3 训练循环、早停与最佳模型保存训练循环不难但有几个细节决定能不能快速收敛。学习率、batch和epochs要配套下面这份代码可以直接抄def train_model(model, train_loader, val_loader, epochs10, lr0.001): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.CrossEntropyLoss() best_f1 0.0 for epoch in range(epochs): model.train() total_loss 0 for ids, labels in train_loader: optimizer.zero_grad() logits model(ids) loss criterion(logits, labels) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() # 验证 model.eval() val_preds, val_labels [], [] with torch.no_grad(): for ids, labels in val_loader: logits model(ids) preds logits.argmax(dim1) val_preds.extend(preds.tolist()) val_labels.extend(labels.tolist()) f1 f1_score(val_labels, val_preds, averagebinary, pos_label1) print(fEpoch {epoch1}: loss{total_loss/len(train_loader):.4f}, val_f1{f1:.4f}) if f1 best_f1: best_f1 f1 torch.save(model.state_dict(), best_lstm.pth)clip_grad_norm_做梯度裁剪设max_norm1.0防止LSTM训练时梯度爆炸导致loss变成NaN。best_f1按验证集F1保存最优模型而不是按准确率或训练loss因为验证集F1才是真实泛化能力的体现。如果连续3轮验证集F1不再上升就直接停掉这叫早停能省一半训练时间。训练完成后用加载的best_lstm.pth对测试集做一次预测把F1和基线逻辑回归的F1放进同一张表里这就是实验报告的核心结果。如果LSTM的F1没有明显超过TF-IDF逻辑回归不要慌在小数据集上这是常态——报告里如实写“LSTM在小样本影评数据上未显著超越简单模型说明特征工程在短文本分类中的重要性”这句话比编造结果体面得多。5. 常见坑与排查五条让大作业翻车的细节5.1 验证集准确率高但提交后效果差TF-IDF被重复fit了现象训练随机打乱的验证集上准确率超过90%但拿模型去预测几条新影评结果一塌糊涂。原因代码里对测试集写了fit_transform导致测试特征矩阵在训练时已经“见过”属于典型的数据泄漏。解决训练集用fit_transform测试集和预测时统一用transform再把vectorizer用joblib.dump保存下来预测新样本时load回来复用。5.2 同一部电影的评论同时出现在训练集和测试集现象验证集F1很高但你心里清楚模型没见过那么多复杂句式可信度存疑。原因train_test_split只按文本切片没考虑movie_id。烂番茄影评里同一部电影的评论用词高度相似如果同一部电影的几十条评论被切到两端模型等于“背过答案”。解决用sklearn.model_selection.GroupShuffleSplit把movie_id作为groups参数保证同一部电影的评论只出现在训练集或测试集一侧。这一步是很多大作业拿高分和低分的分水岭。5.3 标签严重不平衡模型只会预测“新鲜”现象准确率75%但分类报告里“腐烂”类的精确率和召回率都是0。原因正面评论远多于负面评论模型把所有样本都预测为多数类就能拿到不错的准确率。解决逻辑回归里设class_weightbalanced深度模型里可以在损失函数中传class_weight给CrossEntropyLoss或者对少数类做过采样。写报告时一定要单独讲一句“如何处理标签不平衡”这是老师很看重的工程意识。5.4 用jieba切英文影评现象clean_text.split()换成jieba.lcut后词表里出现大量“thi”、“mov”这类残缺单字符F1大幅下降。原因jieba是为中文分词设计的按中文词语粒度切分英文时会按字或错误边界切。解决英文文本直接用str.split()或nltk.word_tokenize中文才用jieba。这个错误看起来很初级但每年大作业都会有人踩。顺便说一句如果老师要求做中文数据对比实验数据清洗流程要重新设计中文没有空格分词标点处理和编码方式也完全不同不能简单复用同一套代码。5.5 实验报告只放准确率现象报告里只写“准确率87%”没有精确率、召回率、F1也没有混淆矩阵。原因习惯性地把准确率当作唯一指标忽略了类别不平衡的影响。解决至少给出classification_report的完整输出、混淆矩阵热力图、以及基线模型和LSTM的指标对比表。老师看报告时最先找的就是F1和混淆矩阵这两块缺失几乎等于没做评估。6. 实验报告与复现让老师一键跑通你的项目6.1 结果对比表至少要有四行三列实验报告里放一张简洁的结果表每一行是一个模型每一列是一种指标。常规配置是TF-IDF逻辑回归、TF-IDF朴素贝叶斯、Word2VecLSTM、预训练嵌入LSTM四行列是准确率、精确率、召回率、F1。表格放在实验方法之后结论之前比大段文字有冲击力。模型准确率精确率召回率F1TF-IDF 逻辑回归0.860.850.870.86TF-IDF 朴素贝叶斯0.820.800.830.81双向LSTM随机嵌入0.850.840.850.84双向LSTM词向量预训练0.870.870.860.86表里的数字只是示意实际以你跑出来的为准。关键是要给每行补一句说明为什么朴素贝叶斯低、为什么LSTM没有大幅领先这些分析才是报告的加分项。6.2 手动验证与样例输出贴几条例子让评估落地只给指标老师还是不知道模型实际预测起来长什么样。我一般会在报告末尾加一个“样例预测”小表从测试集里选正面和负面各两三条真实影评把预测概率和标签打出来。这能证明模型不是一个只在矩阵上好看的黑匣子。sample_texts [ A gripping thriller with outstanding performances., Slow, boring and completely predictable., ] for text in sample_texts: ids encode_text(clean_text(text), vocab, max_len100) ids_tensor torch.tensor([ids]) logits model(ids_tensor) prob torch.softmax(logits, dim1) pred torch.argmax(logits, dim1).item() print(f影评: {text}) print(f预测标签: {新鲜 if pred 1 else 腐烂}, 正面概率: {prob[0][1].item():.4f})这段代码同时承担验证功能如果预测概率都集中在0.9以上说明模型过度自信可能过拟合如果正面负面都接近0.5说明特征没有区分度。把输出结果截图贴报告再配一句“模型对否定句仍然存在误判后续可以用BERT微调优化”结尾自然落到下一步计划。6.3 requirements与README环境锁定才能复现大作业交付源码时最怕老师在一台没有装过任何依赖的机器上跑不通。我的习惯是提交三样东西requirements.txt、README.md、data/目录。requirements.txt固定关键依赖版本不需要逐行锁定但pandas、scikit-learn、torch、nltk这几个核心库必须写明大版本号。README.md里写清楚Python版本我用的是3.10、安装命令、训练脚本顺序和数据说明。另外要提一句用conda建独立环境避免系统里旧版numpy或torch互相冲突这一步能省掉大量无意义的报错排查时间。做这个项目时我最大的教训是把这门大作业当成一次“最小可交付项目”来做而不是“跑完代码拿到分数”就结束。模型选型、参数调整、评估指标、复现说明每一环都踩过坑最后能在自己的机器上一键复现再看到同学还在被环境问题困住才知道那些时间花得值。希望帮到你。本文还有配套的精品资源点击获取