
简介这份资源面向影视评论情感挖掘方向的学习者与研究者围绕《流浪地球》猫眼平台用户影评完整呈现从网络爬虫采集、评论清洗预处理、中文分词、词向量嵌入到RNN模型训练评估的三分类情感识别全流程适合具备Python与深度学习基础、希望复现文本情感分析项目的读者参考。压缩包共25个文件约8.99MB包含4个py核心脚本与2个ipynb实验笔记另有csv训练测试数据、png与jpg结果图表、pyc缓存、md与docx说明文档等覆盖代码、数据与解读材料。已有72人学习下载。资源核心目录RNN-sentiment-analysis-main集中了模型训练与数据处理的关键实现附赠说明文件对方法选择与实验结果给出解读可帮助读者理解RNN处理序列文本的优势、三分类情感极性的建模思路并对照数据与图表复现完整实验为影视评价反馈与情感分析研究提供可借鉴的实践参考。1. 从猫眼抓评论到 RNN 三分类这份资源到底能跑出什么结果很多做影视数据分析的朋友都卡在同一个地方爬下来的评论是一堆带表情、带换行、带火星文的原始文本想拿去做情感分析光清洗和分词就能耗掉一整天。这份资源就是冲着这个痛点来的——它把《流浪地球》在猫眼平台上的用户影评从网络爬虫采集、评论清洗预处理、中文分词、词向量嵌入一路做到基于循环神经网络的三分类情感极性识别最后还带一份深度分析。整套流程是能直接跑通的 Python 工程不是只给个模型文件让你猜怎么用。它适合三类人一是想学 RNN 文本分类但缺真实数据练手的学生和转行者二是需要快速搭一套影评情感挖掘 pipeline 的从业者三是想拿影视评论做舆情或口碑研究的人。三分类指的是把评论判成正面、中性、负面比常见的二分类更贴近真实评论分布——毕竟大量短评其实是中性的硬分正负会失真。下面我按「资源是什么 → 怎么用 → 坑在哪」的顺序把这份包拆开讲清楚。2. 数据采集与清洗爬虫抓下来的评论为什么不能直接喂模型2.1 猫眼影评的页面结构与采集思路猫眼影评数据通常挂在影片详情页的评论接口上返回的是 JSON 结构字段里包含用户昵称、评分、评论正文、评论时间等。常见做法是用 requests 配合分页参数循环拉取而不是去解析渲染后的 HTML——接口数据更干净字段也全。这里要提醒一句采集频率别拉满加个 time.sleep 既是对目标站点的基本尊重也能避免被限流导致半途断掉。采集阶段最容易被忽略的是「翻页边界」。猫眼评论接口一般用 offset 或 start 参数控制偏移量当返回的评论列表为空时就必须停否则会陷入死循环反复请求同一页。我一般会在循环里加一个空列表判断加最大页数兜底两个条件谁先触发都停。import requests import time import json def fetch_maoyan_comments(movie_id, max_pages50): 按偏移量分页抓取猫眼影评空页或达上限即停 base_url https://m.maoyan.com/comments.json all_comments [] offset 0 for page in range(max_pages): params { movieId: movie_id, offset: offset, limit: 15, # 单页条数别设太大 } headers { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X), Referer: https://m.maoyan.com/, } resp requests.get(base_url, paramsparams, headersheaders, timeout10) data resp.json() comments data.get(data, {}).get(comments, []) if not comments: # 空页说明到底了 break all_comments.extend(comments) offset 15 time.sleep(1.5) # 控制频率别把对方打疼 return all_comments if __name__ __main__: result fetch_maoyan_comments(movie_id248906) with open(raw_comments.json, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2)这段代码的逻辑很直白构造带 movieId、offset、limit 的请求拿到 JSON 后取 data.comments 列表空列表就 break。参数上 limit 不建议超过 20太大容易触发风控time.sleep 的 1.5 秒是经验值太短会被限太长采集效率低。headers 里的 Referer 和移动端 UA 是必须的否则接口可能返回空数据。跑完你会得到一个 raw_comments.json里面是原始评论接下来才进入清洗环节。2.2 评论清洗正则去噪与去重原始评论里混着大量噪声HTML 标签残留、连续空格、换行符、表情符号编码、以及「此用户没有填写评论」这类占位文本。清洗的目标是把这些干掉同时保留中文、英文、数字和基本标点。常见做法是用正则统一替换再按评论正文去重——同一部热门电影下重复评论和刷分评论并不少见。import re import json def clean_text(text): 清洗单条评论去标签、去表情、去多余空白 if not isinstance(text, str): return text re.sub(r[^], , text) # 去 HTML 标签 text re.sub(r\[.*?\], , text) # 去 [表情] 类占位 text re.sub(r[\U0001F300-\U0001FAFF], , text) # 去 emoji text re.sub(r\s, , text) # 多空白合一 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , text) return text.strip() def load_and_clean(path): with open(path, r, encodingutf-8) as f: raw json.load(f) seen set() cleaned [] for item in raw: content clean_text(item.get(content, )) if len(content) 5: # 太短的没分析价值 continue if content in seen: # 去重 continue seen.add(content) cleaned.append({content: content, score: item.get(score, 0)}) return cleaned if __name__ __main__: data load_and_clean(raw_comments.json) with open(clean_comments.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f清洗后剩余 {len(data)} 条)clean_text 里几个正则各有分工第一个去 HTML 标签第二个去方括号表情占位第三个用 Unicode 区间去 emoji第四个把连续空白压成一个空格第五个只保留中文、英文、数字和常用中文标点。load_and_clean 里做了两件过滤长度小于 5 的丢掉内容重复的丢掉。参数上长度阈值 5 是经验值太短如「好看」这种虽然情感明确但信息量低做词向量时贡献有限。跑完打印剩余条数如果发现剩得太少回头检查是不是正则把正常标点也误杀了。3. 中文分词与词向量jieba 和 Word2Vec 怎么配才不掉链子3.1 基于 jieba 的中文分词实战中文不像英文有天然空格必须分词。这份资源用的是 jieba这也是目前 Python 生态里最常用的中文分词工具之一。分词质量直接决定后面词向量的好坏所以别拿默认模式随便一切就完事。常见做法是加载自定义词典把电影名、角色名、网络新词加进去否则「流浪地球」可能被切成「流浪」「地球」「行星发动机」被切碎。import jieba import json def load_stopwords(pathstopwords.txt): 加载停用词表一行一个词 with open(path, r, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) def segment_comments(comments, stopwords): 对每条评论分词并去停用词 tokenized [] for item in comments: words jieba.lcut(item[content]) words [w for w in words if w not in stopwords and len(w) 1] if words: tokenized.append({tokens: words, score: item[score]}) return tokenized if __name__ __main__: jieba.load_userdict(user_dict.txt) # 自定义词典流浪地球、行星发动机等 stopwords load_stopwords() with open(clean_comments.json, r, encodingutf-8) as f: comments json.load(f) result segment_comments(comments, stopwords) with open(tokenized.json, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) print(f分词完成共 {len(result)} 条)jieba.lcut 返回列表比 jieba.cut 直接可用。去停用词时顺手过滤了单字词因为单字在情感分类里噪声大、区分度低。user_dict.txt 里建议至少放「流浪地球」「刘培强」「行星发动机」「moss」这类专有名词加载后 jieba 会优先按词典切。stopwords.txt 用常见中文停用词表即可网上有现成的也可以自己攒。参数上 len(w) 1 这个过滤条件可以根据效果调如果发现「烂」「赞」这类单字情感词被误删就把它去掉。3.2 词向量嵌入Word2Vec 训练与维度选择分词完得到的是词序列模型不认识字符串得转成向量。这份资源用 Word2Vec 在评论语料上训练词向量让语义相近的词在向量空间里靠得近。训练词向量有几个关键参数vector_size 决定向量维度window 是上下文窗口min_count 过滤低频词sg 选 0 是 CBOW、选 1 是 Skip-gram。影评语料规模通常不大Skip-gram 对低频词更友好我一般会选 sg1。from gensim.models import Word2Vec import json def train_word2vec(tokenized_path, vector_size100, window5, min_count2): 在分词语料上训练 Word2Vec with open(tokenized_path, r, encodingutf-8) as f: data json.load(f) sentences [item[tokens] for item in data] model Word2Vec( sentencessentences, vector_sizevector_size, # 向量维度 windowwindow, # 上下文窗口 min_countmin_count, # 低于此频次的词丢弃 sg1, # 1Skip-gram0CBOW workers4, epochs10, ) model.save(word2vec.model) return model if __name__ __main__: model train_word2vec(tokenized.json) # 验证一下语义相近度 for word in [好看, 特效, 剧情]: if word in model.wv: similar model.wv.most_similar(word, topn5) print(word, -, [w for w, _ in similar])vector_size 设 100 是文本分类的常用起点语料大可以上 200 或 300语料小就降到 50 避免过拟合。window5 表示看前后各 5 个词影评句子通常不长5 够用。min_count2 把只出现一次的词丢掉减少噪声。sg1 用 Skip-gram对小语料更稳。训练完用 most_similar 验证一下如果「好看」的相近词是「精彩」「不错」这类说明词向量学到了语义如果出来一堆不相关的回头检查分词和停用词表。这一步是后面 RNN 输入质量的地基别跳过验证。4. RNN 模型搭建与训练三分类情感识别的完整链路4.1 从词向量到序列输入构建训练张量有了词向量每条评论要转成一个固定长度的向量序列。做法是给词表里每个词分配一个索引把评论里的词映射成索引序列再按最大长度截断或补齐。标签方面三分类的映射规则通常是评分 4-5 分算正面、3 分算中性、1-2 分算负面。这一步的坑在于长度选择——太短截掉信息太长 padding 太多拖慢训练。影评大多在 50 字以内max_len 设 100 一般够。import json import numpy as np import torch from torch.utils.data import Dataset def score_to_label(score): 评分转三分类标签0负面 1中性 2正面 if score 4: return 2 elif score 3: return 1 else: return 0 class ReviewDataset(Dataset): def __init__(self, tokenized_path, word2idx, max_len100): with open(tokenized_path, r, encodingutf-8) as f: self.data json.load(f) self.word2idx word2idx self.max_len max_len def __len__(self): return len(self.data) def __getitem__(self, idx): item self.data[idx] ids [self.word2idx.get(w, 0) for w in item[tokens]] # 0UNK if len(ids) self.max_len: ids ids[:self.max_len] else: ids ids [1] * (self.max_len - len(ids)) # 1PAD label score_to_label(item[score]) return torch.tensor(ids, dtypetorch.long), torch.tensor(label, dtypetorch.long)score_to_label 把 1-5 分映射成三类这个阈值不是死的如果发现中性类样本太少可以把 3 分和部分 4 分合并调整。word2idx 里 0 留给未知词 UNK1 留给填充 PAD这两个特殊 token 必须在词表构建时就占位。max_len100 是截断和补齐的统一长度短补长截。返回的是两个 tensor一个是索引序列一个是标签直接能进 DataLoader。4.2 RNN 模型定义与训练循环模型部分用 PyTorch 的 nn.RNN 或 nn.LSTM。原始 RNN 结构简单但容易梯度消失影评句子虽然不长但用 LSTM 或 GRU 通常效果更稳。这份资源标题写的是循环神经网络实际实现里用 LSTM 也属于 RNN 家族不算跑题。模型结构是Embedding 层加载预训练词向量 → LSTM 层 → 取最后时间步输出 → 全连接层映射到 3 类。import torch.nn as nn class RNNClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes3, pretrained_embNone): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx1) if pretrained_emb is not None: self.embedding.weight.data.copy_(pretrained_emb) # 加载预训练词向量 self.rnn nn.LSTM(embed_dim, hidden_dim, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden_dim * 2, num_classes) # 双向所以乘2 def forward(self, x): emb self.embedding(x) out, _ self.rnn(emb) out out[:, -1, :] # 取最后时间步 return self.fc(out)Embedding 层用 padding_idx1 告诉模型 PAD 不参与梯度更新。加载预训练词向量时要把 Word2Vec 的向量按 word2idx 顺序排成矩阵再 copy 进去这一步能明显加快收敛。LSTM 设 bidirectionalTrue 让模型同时看前后文影评里「不」字往往在后面才出现双向能抓住这种否定。fc 输入维度是 hidden_dim * 2 因为双向拼接。训练循环里用 CrossEntropyLoss 和 Adam学习率 1e-3 起步跑 10 个 epoch 左右看验证集准确率。from torch.utils.data import DataLoader import torch.optim as optim def train_model(model, dataset, epochs10, batch_size32, lr1e-3): loader DataLoader(dataset, batch_sizebatch_size, shuffleTrue) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lrlr) model.train() for epoch in range(epochs): total_loss 0 for x, y in loader: optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch1}, loss {total_loss/len(loader):.4f})batch_size32 是显存和稳定性的折中显存小就降到 16。lr1e-3 是 Adam 的常用起点如果 loss 震荡就降到 5e-4。每个 epoch 打印平均 loss正常情况应该稳步下降如果 loss 不降反升先检查标签映射有没有错位再看词向量加载时索引对不对齐。训练完记得在测试集上算准确率和混淆矩阵三分类光看准确率不够得看每一类的召回否则中性类容易被吞掉。5. 避坑与排查这套流程里最容易翻车的五个地方5.1 爬虫返回空数据或 403现象请求猫眼接口返回空列表或状态码 403。原因通常是 UA 或 Referer 缺失、请求频率过高被临时限制、或者接口参数变了。解决补全移动端 UA 和 Referer把 time.sleep 调到 2 秒以上先用浏览器开发者工具确认当前接口地址和参数名有没有改。如果还是不行换一个采集时段再试别硬刚。5.2 分词把关键情感词切碎现象模型效果差检查发现「不好看」被切成「不」「好看」情感直接反转。原因jieba 默认词典不含这类组合或者停用词表把「不」删了。解决把「不好看」「没意思」这类否定组合加进 user_dict.txt同时检查停用词表里别误删否定词。否定词在情感分析里是命门删了就等着翻车。5.3 词向量加载后索引错位现象模型训练 loss 一直不降或者准确率跟随机猜差不多。原因构建 word2idx 的顺序和生成预训练向量矩阵的顺序不一致导致词和向量对不上。解决用同一个 word2idx 字典同时生成索引序列和向量矩阵生成后随机抽几个词打印它们的向量跟 Word2Vec 模型里的向量比对确认一致再喂给 Embedding。5.4 类别不平衡导致中性类被忽略现象测试集准确率看着还行但中性类召回率极低。原因影评里正面和负面占多数中性样本少模型倾向于猜多数类。解决用 WeightedRandomSampler 做重采样或者在 CrossEntropyLoss 里传 weight 参数给少数类更高权重。也可以调整标签映射阈值把部分边界样本归到中性类扩充样本量。5.5 训练集和测试集分布不一致现象训练集准确率 95%测试集只有 60%。原因划分数据时没打乱或者测试集里混进了训练集没见过的表达方式。解决划分前先 shuffle用 train_test_split 的 stratify 参数按标签分层抽样保证三类的比例在训练集和测试集里一致。如果语料本身时间跨度大还要考虑按时间划分避免用未来评论预测过去。6. 进阶技巧用混淆矩阵和错误样本反推模型短板模型跑通只是起点真正决定这份资源能不能用在正经分析里的是你会不会看结果。三分类任务里准确率是个容易被美化的指标——如果中性类只占 10%模型全猜正面也能有 60% 以上的准确率。所以我习惯先看混淆矩阵再看被分错的样本长什么样。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt def evaluate(model, dataset): loader DataLoader(dataset, batch_size32, shuffleFalse) model.eval() preds, trues [], [] with torch.no_grad(): for x, y in loader: logits model(x) pred torch.argmax(logits, dim1) preds.extend(pred.tolist()) trues.extend(y.tolist()) print(classification_report(trues, preds, target_names[负面, 中性, 正面])) cm confusion_matrix(trues, preds) sns.heatmap(cm, annotTrue, fmtd, xticklabels[负面, 中性, 正面], yticklabels[负面, 中性, 正面]) plt.xlabel(预测) plt.ylabel(真实) plt.savefig(confusion_matrix.png) return preds, truesclassification_report 会给出每一类的 precision、recall、f1-score重点看中性类的 recall。如果中性类 recall 低于 0.5说明模型基本没学会区分中性得回去查样本量或调整标签阈值。混淆矩阵热力图能直观看到哪两类之间互相误判最多——常见的是中性被误判成正面因为很多中性评论带轻微褒义。定位到问题后把误判样本导出来人工看几十条往往能发现是分词问题还是标签定义问题。另一个实用技巧是用模型的预测概率做置信度过滤。对预测概率低于某个阈值比如 0.6的样本不强行归类而是标记为「待人工复核」。这在做正式影视口碑分析时很有用宁可少判几条也别把明显模棱两可的评论硬塞进某一类最后得出一个看着漂亮但经不起追问的结论。还有个容易被忽略的点是词向量和模型的联合微调。前面是加载预训练词向量后冻结 Embedding 层如果语料够大可以让 Embedding 也跟着训练通常能再涨一两个点。做法是把 embedding.weight.requires_grad 设为 True但学习率要调小否则预训练学到的语义会被冲垮。我一般会给 Embedding 层单独设一个更小的学习率比如 1e-4其他层保持 1e-3。最后说个血泪经验这套流程里最耗时间的从来不是模型调参而是数据清洗和标签校验。我见过太多人模型结构改来改去最后发现是爬下来的评论里混了一堆「此用户没有填写评论」的占位文本没清掉。从那以后我每次跑完清洗都会随机抽 20 条打印出来肉眼过一遍确认没有明显噪声再往下走。这个习惯帮我省下的返工时间比任何调参技巧都多。希望帮到你。本文还有配套的精品资源点击获取