ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用Python实现烂番茄影评情感分类:爬虫、LSTM与实验报告

用Python实现烂番茄影评情感分类:爬虫、LSTM与实验报告 简介一份面向华中科技大学Python大数据与人工智能实践课程的大作业完整方案以烂番茄电影评论为对象使用Python完成情感分类建模包含可运行的源码、实验报告与原始数据。资源面向高校计算机、人工智能及相关专业学生尤其是需要完成课程设计、毕业设计或入门NLP情感分析任务的读者。\n\n压缩包共含555个文件整体约41.89MB。核心内容包括6个Python源码文件、104个PDF文档、多份CSV/TSV格式影评数据、14个NPY向量文件以及TensorFlow训练产生的events.out.tfevents日志与大量epoch训练文件便于查看模型训练过程中的loss与acc变化轨迹。此外还附带md说明与tex/bib文件便于整理报告与引用。\n\n目前已有158人学习下载。代码已经过完整测试并运行成功内部包含数据处理、模型搭建、训练评估等完整流程附实验报告可帮助理解每个环节的设计思路适合在现有代码基础上二次开发或直接参考完成同类影评情感分析任务。1. 用 Python 拿下烂番茄影评情感分类这个大作业到底在做什么每年到了大作业季最头疼的不是“写代码”而是“做什么题目”。烂番茄电影评论情感分类这个方向我前后带过不少学弟学妹做也帮人改过几版代码属于典型的“数据干净、标签现成、效果可量化”的练手项目——数据能爬下来标签能从星级换算出来情感分类的准确率能直接写在实验报告里导师一眼就能看懂做了什么事。整个链路覆盖 Python 爬虫、pandas 数据清洗、sklearn 与深度学习建模、实验对比正好对得上“Python 大数据与人工智能实践”这门课的能力要求。这篇笔记就按我自己做这类项目时会走的路径把数据采集、情感分类建模、参数设置、实验报告怎么写以及容易翻车的坑逐一讲清楚。新手照着做能跑通熟手可以跳过入门部分直接看参数和踩坑记录。2. 把烂番茄评论整理成可用数据集爬取、清洗与标签标注2.1 为什么选烂番茄影评标签结构天生适合做情感分类烂番茄Rotten Tomatoes和其他影评平台相比有一个非常省事的特性观众评论分“新鲜”和“腐烂”两档也就是正面和负面。它不要求你像做一般文本分类那样先人工标注几千条数据星级或者 fresh/spoiled 标签是现成的。实践类大作业里数据标注往往是最大的时间黑洞选这种自带标签的语料能把有限的时间花在模型和实验上而不是花三四个晚上标数据。另一个好处是评论本身是英文不用做中文分词。中文情感分类要先解决分词、停用词、词性等一系列前置问题英文只需要 nltk 或者 spaCy 做一次 tokenize、去掉停用词即可Pipeline 短一截。对 Python 基础语法还不够熟的同学来说这是更友好的切入方式。而且烂番茄评论的语境相对规范没有太多网络黑话和错别字模型效果容易做得好看。数据规模上一个电影页面的观众评论从几十条到上千条不等实际采集时建议选近几年的热门大片一页 20 条左右翻 20~30 页就能拿到一个几百条到上千条的数据集。对课程大作业来说这个量级已经完全够用。如果你不想爬也可以在实验报告里注明使用了公开的烂番茄影评数据集但自己动手爬一遍能多写一章“数据采集与预处理”的内容工作量更饱满。2.2 用 requests BeautifulSoup 把评论和标签落成 CSV常见做法是先用 requests 请求评论列表页再用 BeautifulSoup 解析 HTML 结构把评论正文和对应的“新鲜/腐烂”标签同时抓下来。烂番茄的观众评论页是分页加载的URL 里的?pageN参数控制页码。每个评论块通常在带有review-row等 class 的节点里需要先定位评论容器再在容器内提取文本和标签比全页面硬搜字符串要稳得多。import time import random import requests from bs4 import BeautifulSoup import pandas as pd headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36 } def crawl_reviews(base_url, pages20, delay_range(1.5, 3.5)): records [] for page in range(1, pages 1): url f{base_url}?page{page} resp requests.get(url, headersheaders, timeout15) if resp.status_code ! 200: continue soup BeautifulSoup(resp.text, html.parser) # 每条观众评论所在的容器节点具体 class 以实际页面为准 for row in soup.select(.review-row): text_node row.select_one(.review-text) label_node row.select_one(.review-freshness) if text_node is None or label_node is None: continue review_text text_node.get_text(stripTrue) freshness label_node.get_text(stripTrue).lower() if fresh in freshness: label 1 elif rotten in freshness: label 0 else: continue records.append({text: review_text, label: label}) time.sleep(random.uniform(*delay_range)) return records # base_url 指向烂番茄某部电影的观众评论页 records crawl_reviews(https://www.rottentomatoes.com/m/xxx/reviews) df pd.DataFrame(records) df df.drop_duplicates(subsettext).dropna() df.to_csv(rotten_tomatoes_reviews.csv, indexFalse, encodingutf-8) print(df.shape) print(df[label].value_counts())这套代码的核心逻辑是按页请求、定位评论节点、把新鲜度映射成 0/1 标签。time.sleep(random.uniform(...))是为了控制请求频率避免短时间内打太多请求导致 IP 被临时限制。drop_duplicates(subsettext)清理同一部电影在不同分页里可能出现的重复评论。注意.review-row这类选择器不同时期的烂番茄页面结构会调整脚本跑不出数据时先打印 soup 内容确认当前页面的实际 class 名这是爬虫环节最常见的调试步骤。2.3 标签平衡与最小数据集要求爬完数据后第一件事不是急着训练而是查看正负样本分布。烂番茄的观众评分总体偏向正面很多电影 70% 以上是 fresh 评论。如果正向样本占 85%模型全预测正向就有 85% 准确率这种模型写了跟没写一样。我的习惯是控制正向样本占比在 60%~75% 之间如果偏差太大就多爬几部口碑偏差的电影或者对多数类做下采样。df pd.read_csv(rotten_tomatoes_reviews.csv) counts df[label].value_counts() print(counts) # 下采样到多数类与少数类接近 1.2:1 左右 majority df[df[label] df[label].mode()[0]] minority df[df[label] ! df[label].mode()[0]] if len(majority) len(minority) * 1.2: majority majority.sample(nint(len(minority) * 1.2), random_state42) df_balanced pd.concat([majority, minority]).sample(frac1, random_state42) df_balanced.to_csv(reviews_balanced.csv, indexFalse)这里random_state42固定下来保证每次运行下采样结果一致实验报告里的数字才能自洽。数据集总量不需要很大训练集 800 条、测试集 200 条这种体量已经能训练出像样的传统机器学习模型。不过如果实验报告里写了“数据量对深度学习模型的影响”这类对照实验就要准备 2000 条以上的数据否则 LSTM 学不到什么东西loss 曲线会抖得没法看。3. 用 TF-IDF 朴素贝叶斯先跑通一个可复现的基线参数与评估口径3.1 为什么先做机器学习基线而不是一上来就上深度学习很多同学一听到“人工智能大作业”就直接要上深度学习仿佛不用神经网络就不够高级。但实验报告里真正有价值的是“基线模型”和“改进模型”的对比。先做一个 TF-IDF 朴素贝叶斯或者逻辑回归的基线跑通整个评估流程再上 LSTM 或者 BERT这样既能说明你理解了问题难度在哪里也能用数据证明深度学习带来了多少提升——这才是一份能站得住脚的实验报告。另外TF-IDF 朴素贝叶斯这种组合在 1000 条量级的影评数据上准确率通常能做到 75%~85%而且训练时间只要几秒。先跑通基线等于先把数据、标签、评估脚本的整条链路验证了一遍。后面深度学习模型出问题时可以快速对比是数据问题还是模型问题。3.2 英文影评的预处理链条tokenize、去停用词、词形还原英文文本预处理比中文简单但也不是直接按空格切就算完。评论里有大小写差异、时态变化、标点和 HTML 残留。我的标准流程是统一转小写用 nltk 的 word_tokenize 分词去掉长度小于 2 的 token去掉 nltk 停用词再做一次词形还原lemmatization。词形还原比词干提取更温和把 “movies” 和 “movie” 归到一起但不会把 “boring” 砍成 “bore” 丢失情感信息。import nltk import re from nltk.corpus import stopwords from nltk.stem import WordNetLemmatizer nltk.download(punkt) nltk.download(stopwords) nltk.download(wordnet) nltk.download(omw-1.4) stop_words set(stopwords.words(english)) lemmatizer WordNetLemmatizer() def clean_text(text): text text.lower() text re.sub(r[^], , text) # 去掉 HTML 标签 text re.sub(r[^a-z\s], , text) # 只保留字母 tokens nltk.word_tokenize(text) tokens [t for t in tokens if len(t) 2 and t not in stop_words] tokens [lemmatizer.lemmatize(t) for t in tokens] return .join(tokens) df[clean_text] df[text].apply(clean_text)这段代码里有几个容易忽略的细节re.sub(r[^a-z\s], , text)会把所有数字和标点变成空格避免film.和film被算成两个特征len(t) 2过滤掉a、it这类短词先除标签再分词顺序不能反。nltk 的word_tokenize在第一次运行时需要下载 punkt 资源如果运行环境没外网会直接报错这也是后面避坑章节要单独讲的问题。3.3 训练集/测试集划分、评估指标与混淆矩阵数据划分上我不用简单随机划分而是加stratify做分层采样保证训练集和测试集里的正负样本比例一致。这样跑出来的准确率、F1 值在不同次运行之间波动更小报告里的数据更可信。评估指标除了 accuracy必须看 F1 和混淆矩阵——正面评论和负面评论在实际分布上天然不平衡只看准确率会被多数类蒙蔽。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split 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(max_features5000, ngram_range(1, 2)) X_train_vec vectorizer.fit_transform(X_train) X_test_vec vectorizer.transform(X_test) clf MultinomialNB(alpha1.0) clf.fit(X_train_vec, y_train) y_pred clf.predict(X_test_vec) print(classification_report(y_test, y_pred, target_names[negative, positive])) print(confusion_matrix(y_test, y_pred))max_features5000限制特征维度防止评论语料里出现太多生僻词把向量矩阵撑得过于稀疏。ngram_range(1, 2)同时保留单词和双词组合像 “not good” 这种二元结构本身带有情感转折信息单看 “good” 会被误判为正向。alpha1.0是朴素贝叶斯的拉普拉斯平滑参数如果验证集效果不好可以试着调大一点这个参数的作用是给未在训练集出现的单词一个非零概率避免极端情况下的零概率连乘导致误判。这一整套流程跑下来基线模型的结果就有了准确率 78%、F1 值 0.76这类数字写在实验报告里就是后续所有改进实验的参照系。4. 升级到深度学习方案用 LSTM 做序列建模与实验报告所需的正交实验4.1 为什么用 LSTM 而不是直接套 BERT烂番茄影评属于短文本平均长度二三十个词LSTM 这类序列模型能捕捉“前半句铺垫、后半句转折”的局部语义结构比 TF-IDF 的词袋模型多一个词序维度的信息。BERT 当然效果更好但课程大作业的实验报告里LSTM 和基线的对比就能说明问题而且训练一个像样的 BERT 需要 GPU 环境和更长的训练时间很多同学的笔记本跑不动。我一般会建议先做 LSTM学有余力再在进阶章节提一嘴预训练模型的替换方案。LSTM 的套路很固定Embedding 层把词索引映射成稠密向量双向 LSTM 层提取序列特征GlobalMaxPooling 把变长序列压成一个定长向量最后接 Dense 层输出二分类概率。相比于 CNN 文本分类LSTM 对短文本的“先后顺序”更敏感比如 “unexpectedly good” 这种词序LSTM 能学到 unexpected 对 good 的修饰关系。4.2 词向量矩阵构建、序列填充与模型训练配置深度学习模型不能直接吃字符串需要先构建词表把每个词映射成整数索引再把每一条评论填充成等长序列。max_len的设置我在实验里通常会取 60因为烂番茄观众评论的中位长度在 15~25 个词之间60 已经能覆盖绝大多数评论过长只会拖慢训练速度。import numpy as np from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout, Bidirectional, GlobalMaxPooling1D from tensorflow.keras.optimizers import Adam MAX_VOCAB_SIZE 10000 MAX_LEN 60 EMBEDDING_DIM 100 tokenizer Tokenizer(num_wordsMAX_VOCAB_SIZE, oov_tokenOOV) tokenizer.fit_on_texts(df[clean_text]) sequences tokenizer.texts_to_sequences(df[clean_text]) X pad_sequences(sequences, maxlenMAX_LEN, paddingpost, truncatingpost) y df[label].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model Sequential([ Embedding(input_dimMAX_VOCAB_SIZE, output_dimEMBEDDING_DIM, input_lengthMAX_LEN), Bidirectional(LSTM(64, return_sequencesTrue, dropout0.3)), GlobalMaxPooling1D(), Dense(32, activationrelu), Dropout(0.3), Dense(1, activationsigmoid) ]) model.compile(optimizerAdam(learning_rate1e-3), lossbinary_crossentropy, metrics[accuracy])关键参数都在代码里标出来了oov_tokenOOV让测试集里没见过的词统一映射到一个特殊索引否则推理时会炸paddingpost在句子后面补 0truncatingpost从后面截断这样前面开头的词能保留保留的信息对情感判断权重更高dropout0.3是防止训练集只有几百条时模型直接过拟合。训练时 batch size 建议 32epoch 设 20~30 并配合早停在验证集 loss 连续三轮不降时停掉避免白跑后面的轮次。这里我用GlobalMaxPooling1D而不是直接取 LSTM 最后时刻的输出是为了保留整个序列里最强烈的情感信号短文本情感分类的实践里这个操作很常用。4.3 实验报告里必须有的对照实验与可视化课程大作业的实验报告光有一张准确率表是不够的导师想看到的是你“思考过”。我的做法是固定三组对照第一组TF-IDF 朴素贝叶斯基线第二组LSTM 不加预训练词向量第三组LSTM 加 GloVe 预训练词向量。三组用完全相同的训练集和测试集只替换特征表示方式表格里列出准确率、精确率、召回率、F1 四项指标。from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score def evaluate_report(name, y_true, y_pred): print(f{name}:) print(f accuracy {accuracy_score(y_true, y_pred):.4f}) print(f precision {precision_score(y_true, y_pred):.4f}) print(f recall {recall_score(y_true, y_pred):.4f}) print(f f1 {f1_score(y_true, y_pred):.4f}) # y_pred_lr / y_pred_lstm 是不同模型对同一测试集的预测结果 evaluate_report(Logistic Regression, y_test, y_pred_lr) evaluate_report(LSTM, y_test, y_pred_lstm)可视化方面训练过程的 loss 曲线和 accuracy 曲线是必须放进报告的两张图。用 matplotlib 把每个 epoch 的训练/验证 loss 画出来如果训练 loss 持续下降但验证 loss 在第 10 轮开始上升这就是过拟合的实锤报告里直接写“加入 Dropout 后验证 loss 在第 15 轮才开始反弹说明正则化有效”。这就是实验报告里最能体现“你亲手调过模型”的证据。import matplotlib.pyplot as plt history_dict history.history epochs range(1, len(history_dict[loss]) 1) plt.plot(epochs, history_dict[loss], labeltrain_loss) plt.plot(epochs, history_dict[val_loss], labelval_loss) plt.legend() plt.xlabel(epoch) plt.ylabel(loss) plt.savefig(loss_curve.png)5. 避坑烂番茄影评情感分类最容易翻车的 5 个细节5.1 评论爬下来是空的或者只有一两条现象运行爬虫脚本后records 列表几乎为空打印出来只有一两行数据。原因烂番茄的页面结构不是一成不变的.review-row这个 class 可能已经改版或者页面对未登录访客渲染的是简化版页面评论内容藏在style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
返回列表