
简介面向计算机相关专业学生的电商评论情感分析Python源码项目基于深度学习LSTM算法实现核心功能为爬取京东商城用户评论并完成情感极性分类可作为毕业设计、课程设计或期末大作业的完整参考也适合有Python基础、希望进阶深度学习实战的学习者。包内共39个文件约164.29MB以Python脚本、预训练模型文件为主搭配配置文本、说明文档和运行效果图目录结构清晰便于按爬虫与模型训练两条主线学习。目前已有237人学习下载。项目包含爬虫模块JDSpider与情感分析模块SentimentAnalysis内置不同训练步数的LSTM模型检查点可直接运行推理或迁移微调代码经测试运行成功功能完善对需要参考完整工程结构、快速搭建情感分析任务的读者有较高实用价值。1. 电商评论情感分析先看清这条链路再动手做电商评论情感分析最怕的不是调 LSTM 模型而是数据躺在别人网页里抓不下来或者好不容易跑通训练加载权重时却报路径错误。这份资源把两件事一起解决了JDSpider 负责爬取京东商城评论SentimentAnalysis 目录下是完整的深度学习训练与预测 Python 源码还带了一份按步数递增保存的预训练权重。跑通之后输入一句“物流慢得离谱”输出就是负向输入“包装很用心”输出就是正向。适合正在做毕设、课程设计的学生也适合想参考 LSTM 文本分类工程结构的从业者。整条链路不依赖外部服务本地装好 Python 环境就能复现。2. 数据从哪来读懂 JDSpider 的结构与抓取节奏拿到源码先别急着训练。这份资源的训练数据不是现成的 CSV而是要靠 JDSpider 自己去京东商城评论里抓。也就是说爬虫这部分直接决定你后面喂给 LSTM 的语料质量。很多同学下载完直接跑 run.py发现模型加载成功了、预测却不对回头一查原来是数据量不够、正负样本比例也歪了。所以先花二十分钟把爬虫工程摸清楚比什么都值。2.1 Scrapy 工程布局四个关键文件的分工JDSpider 是一个标准的 Scrapy 工程目录里有 scrapy.cfg、items.py、pipelines.py、settings.py、middlewares.py 和 spiders 包。首次打开容易懵其实每个文件的职责很固定。文件职责你大概率会改的位置scrapy.cfg工程配置定义项目名和模块路径一般不动items.py定义爬取字段结构增加 product_id、content、score 等字段settings.py并发数、下载延时、UA、管道注册DOWNLOAD_DELAY、CONCURRENT_REQUESTSpipelines.py数据清洗与落地去重、过滤空评论、格式修正middlewares.py下载中间件随机 UA、随机延时逻辑Scrapy 的默认下载延时很低跑本地评论页还行跑线上电商评论基本是一抓一个验证码。我一般会把 DOWNLOAD_DELAY 调到 1.5 到 3 秒把 CONCURRENT_REQUESTS 从默认的 16 降到 8。这一行的改动比后面任何反爬技巧都管用。启动爬虫的命令很简单在 JDSpider 目录下执行scrapy crawl jd_comment -o comments.jsonlinescrawl后面的jd_comment是 spiders 目录里定义的 Spider name-o指定输出文件。输出格式用 jsonlines 比 csv 稳每一行是一条独立 JSON即使中途断掉已经抓到的数据也不会损坏。2.2 京东评论数据怎么取请求参数、头信息和抓取节奏京东商品评论的常见做法是把请求打到 club.jd.com 下面的评论分页接口用 productId、score、sortType、page 这几个参数翻页取数。返回的是 JSON直接在 parse 里json.loads(response.text)就行。注意评分参数score0 是全部评价1 是差评、2 是中评、3 是好评分档抓取能让正负样本比例更可控。看一段 Spider 解析逻辑的典型写法import json import scrapy class JdCommentSpider(scrapy.Spider): name jd_comment def start_requests(self): base_url https://club.jd.com/comment/productPageComments.action # 常见参数productId 商品IDscore 评分档page 页码 for product_id in self.product_ids: for page in range(0, 30): url ( f{base_url}?productId{product_id}score3 fsortType6page{page}pageSize20 ) yield scrapy.Request(url, callbackself.parse, dont_filterTrue) def parse(self, response): data json.loads(response.text) for c in data.get(comments, []): yield { product_id: c.get(productId), content: c.get(content), score: c.get(score), time: c.get(creationTime), }这段代码做的事把商品 ID 列表和页码拼成评论接口地址遍历请求parse 里把评论内容、评分、时间抽出来以字典形式交给 pipeline 做后续处理。dont_filterTrue是为了防止 Scrapy 对相同 URL 自动去重因为分页 URL 只有 page 参数不同必须让每个请求都真正发出去。这里有个血泪经验指望只靠 wait 3 秒就万事大吉是不现实的。电商评论接口对 Cookie 和 Referer 很敏感第一次请求往往能拿到数据翻几页之后就可能返回空 comments。常见做法是把浏览器里登录后的 Cookie 复制到 settings.py 的 DEFAULT_REQUEST_HEADERS 里再把 Referer 指向商品详情页。这样做的原因是接口会校验来源和身份单纯改 UA 效果有限。别把抓取频率设成 0那是给自己找验证码。2.3 数据落到本地字段、格式与标签制作爬下来的原始评论不能直接进模型。第一条规则是过滤评论内容为空、纯表情、纯标点、长度小于 5 个字的直接丢弃这些样本对训练没有贡献只会增加噪声。第二条规则是标签映射这也是做得比较讲究的一点4 分和 5 分评正向1 分和 2 分评负向3 分直接扔掉。理由很直接3 分评论语义模糊“还行”“一般”这类表达让模型学不到稳定的特征。def make_label(score): # 4~5分视为正向1~2分视为负向3分弃用 if score 4: return 1 elif score 2: return 0 else: return -1标签值 1 表示正向0 表示负向-1 是弃用标记。写 pipeline 时看到 -1 就直接 return item不入库。这个映射看起来简单却是整个数据环节里最容易翻车的地方。有人图省事把所有小于等于 3 分的都算负向结果中评文本和差评文本混在一起模型在“一般般”和“很差”之间反复摇摆准确率怎么调都上不去。数据量方面我一般建议有效样本不低于 3000 条正负各一半。评论抓够了再进入模型环节才有意义。重新把评分映射跑一遍检查一下正负占比如果一边倒后续训练出来的模型会偏向多数类预测结果会有一大堆假阳性。3. 训练 LSTM 情感模型超参数、权重档位与 checkpoint 陷阱数据就位之后整个资源的核心部分才登场。SentimentAnalysis 目录里有 run.py 和多个 .ckpt / .model 权重文件从命名能看出训练过程中多次保存了模型。这一章把预处理、模型结构和权重加载三件事讲透照做就能跑通。3.1 文本预处理分词、停用词与定长序列中文评论和英文不一样词与词之间没有空格必须分词。资源里带了 StopwordsCN.txt这就是中文停用词表把“的、了、是、在”这类高频无意义词过滤掉。分词用的是 jieba跑之前先安装pip install jieba预处理函数是训练和预测共用的后面 run.py 也会调用同一套逻辑。看这段代码import jieba import pickle def clean_and_pad(text, word_dict, seq_len80): # 先分词再去停用词、去空白 stopwords set() with open(StopwordsCN.txt, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) words [w for w in jieba.cut(text) if w not in stopwords and w.strip()] # 词映射成id1保留给UNK0保留给PAD ids [word_dict.get(w, 1) for w in words[:seq_len]] # 短评论用0补齐到固定长度 if len(ids) seq_len: ids [0] * (seq_len - len(ids)) return ids词表 word_dict 由训练语料里的全部词频统计生成Pickle 存好之后预测时直接加载。seq_len80是定长序列长度评论超过 80 个字截断不足补 0。为什么定长因为 LSTM 训练时是按 batch 喂数据不定长就没法张量化。80 这个值来自京东评论的平均字数大部分评论集中在 20 到 60 个字80 足够覆盖绝大多数样本。如果评论普遍很长可以调到 120代价是训练变慢、显存占用变大。3.2 模型结构与超参数为什么 LSTM 在这里比 CNN 合适情感分类本质上是一个二分类任务输入是分词后的词 ID 序列输出是正向 / 负向概率。模型结构是一条经典链路Embedding → LSTM → Dense → Softmax。Embedding 层把每个词 ID 映射成稠密词向量LSTM 按顺序读完整个评论把最后一步的隐状态作为整个句子的语义表示再接全连接层输出两个类别的概率。import tensorflow as tf def build_model(vocab_size, embedding_dim200, hidden_units128): # 输入是定长整数序列shape: [batch_size, seq_len] input_ids tf.placeholder(tf.int32, [None, None], nameinput_ids) keep_prob tf.placeholder(tf.float32, namekeep_prob) # 词嵌入层随机初始化训练中更新 embeddings tf.Variable( tf.random_uniform([vocab_size, embedding_dim], -0.1, 0.1) ) inputs_emb tf.nn.embedding_lookup(embeddings, input_ids) # LSTM层这里只有一层加深到2层效果未必更好 lstm_cell tf.nn.rnn_cell.BasicLSTMCell(hidden_units) outputs, final_state tf.nn.dynamic_rnn(lstm_cell, inputs_emb, dtypetf.float32) # 取最后一个时刻的隐状态接全连接分类 logits tf.layers.dense(final_state.h, 2) probs tf.nn.softmax(logits) return input_ids, keep_prob, probs这段代码用的是 TensorFlow 1.x 的写法tf.nn.dynamic_rnn返回两个值outputs 是每个时刻的输出final_state 是 LSTM 的最终状态里面包含 h隐状态和 c记忆单元。分类只用 final_state.h因为它已经编码了整句话的信息。BasicLSTMCell(hidden_units128)里的 128 是隐层神经元数这是一个经验值评论语料不大时 128 够用数据量到万级再试 256。需要注意如果模型声明时的变量名和 checkpoint 里保存的变量名不一致恢复权重会报 key 错误所以变量 scope 最好固定下来。文本分类场景下LSTM 比 CNN 合适的核心原因在于它保留了词序信息。“手机不错但是电池差”和“电池差但是手机不错”这两句话池化类的模型很容易把它们混在一起LSTM 按顺序读入后文“电池差”会覆盖掉前文“手机不错”的权重语义判断更贴近真实表达。3.3 checkpoint 三件套与模型档位加载哪个权重才对资源里的模型文件名比较长jd_comments_181_7_model.model是保存时指定的 name181_7大概率是作者当时的数据集编号不影响加载。关键要看后缀。.meta保存计算图结构.index和.data-00000-of-00001保存具体权重这就是 TensorFlow Saver 的三件套这三个文件必须放在同一个目录下缺一个都恢复不了。权重文件前缀训练步数使用建议pretrained_lstm.ckpt-1000010000 步早期保存点loss 可能还没完全收敛pretrained_lstm.ckpt-2000020000 步比 10000 步更成熟可以先跑这个jd_comments_181_7_model.model.lstm_model-1000010000 步同样模型的早期档位jd_comments_181_7_model.model.lstm_model-4000040000 步步数最长优先尝试这个加载权重最稳的写法是让 TensorFlow 自动找最新的 checkpointimport tensorflow as tf def load_model(sess, model_dir): # latest_checkpoint 自动读取 checkpoint 文件里记录的最新路径 ckpt_path tf.train.latest_checkpoint(model_dir) print(restore from:, ckpt_path) saver tf.train.Saver() saver.restore(sess, ckpt_path)tf.train.latest_checkpoint会读取目录下的 checkpoint 文本文件找到 global_step 最大的那个权重。注意checkpoint 文件里记录的是绝对路径。如果你把整个项目从别人的机器上拷过来路径变了TensorFlow 会按旧路径找文件出现Not found: /home/xxx/...这类报错。解决方法是直接用文本编辑器打开 checkpoint 文件把里面三行路径改成当前机器的实际路径或者用相对路径重新保存一次。这个坑非常常见基本是下载项目复现时翻车率最高的一处。4. 让模型开口说话run.py 的预测链路与阈值校准训练好的模型最终要落到一行命令上。run.py 就是这个入口它把预处理、模型加载、预测、输出串成一条完整链路。这一章拆解这条链路再看一个很多人忽略的问题模型输出的概率到底在什么阈值下判定为正向才合理。4.1 run.py 的执行链路清洗→序列化→预测→后处理run.py 做的事可以拆成四步读取输入句子、调用 clean_and_pad 转成词 ID、加载模型前向计算、把 softmax 概率映射为 0 或 1。核心预测函数长这样import tensorflow as tf def predict_sentiment(sess, text, input_ids, probs, word_dict, seq_len80): # 和训练时完全一致的预处理 ids clean_and_pad(text, word_dict, seq_len) # 模型输入必须是二维张量所以包一层[] prob sess.run( probs, feed_dict{input_ids: [ids]} ) pos_prob prob[0][1] # 第1列是正向概率 return pos_probfeed_dict里的input_ids就是 3.2 里定义的 placeholder形状是[None, None]第一个 None 是 batch 大小。这里只预测一条用[ids]包成一批一条的输入。prob[0][1]是这条文本的正向概率负向概率就是1 - pos_prob。这个函数看起来短但有一个硬规矩预测时用的 word_dict 必须是训练时保存的那一份分词版本也必须是同一个 jieba 版本。训练时分词结果和预测时不一致模型看到的词汇对不上大概率输出全是同一个类而且你还查不出原因因为代码逻辑看起来完全没毛病。4.2 阈值调整为什么置信度大于 0.5 不是正确答案Softmax 输出的是概率分布两个类别加起来恒等于 1。很多人直接拿 0.5 当分界线概率大于 0.5 判正向小于 0.5 判负向。这在正负样本完全均衡时勉强能用但真实评论数据里正向评论往往比负向多模型学到的是“偏向输出正向”的习惯0.5 这个默认阈值会把一批明明不太确定的中性文本也划到正向里。更靠谱的做法是在验证集上搜索最优阈值。把验证集每一条的正向概率算出来遍历阈值找 F1 分数最高的点import numpy as np from sklearn.metrics import f1_score def find_best_threshold(y_true, pos_probs): best_th, best_f1 0.5, 0.0 # 在0.3到0.7之间每0.01扫一个点 for th in np.arange(0.30, 0.71, 0.01): preds (pos_probs th).astype(int) f1 f1_score(y_true, preds) if f1 best_f1: best_f1, best_th f1, th return best_th, best_f1这段逻辑很好理解pos_probs是每一条评论的正向概率数组y_true是真实标签。阈值从 0.3 扫到 0.7每次把大于等于阈值的判为正向算一次 F1。F1 是精确率和召回率的调和平均对类别不均衡更敏感比准确率更能反映模型真实水平。这一步很多人觉得是玄学其实是标准做法在类别偏移时效果立竿见影。最终拿到的 best_th 再写回 run.py替换掉写死的 0.5。4.3 批量预测与结果导出从一句到一万句单条预测跑通以后批量预测就是把 CSV 里的评论逐行读出来逐个调用 predict_sentiment再把结果写回新文件。用 pandas 处理最顺手import pandas as pd def batch_predict(sess, df, input_ids, probs, word_dict): results [] for content in df[content]: pos_prob predict_sentiment(sess, content, input_ids, probs, word_dict) results.append(1 if pos_prob 0.55 else 0) df[predict] results # utf-8-sig 让 Excel 打开不乱码 df.to_csv(result.csv, indexFalse, encodingutf-8-sig) return df这里直接写死阈值为 0.55是为了展示 batch 预测的写法。实际项目中应该把 4.2 里寻优得到的 best_th 传进来。utf-8-sig这个编码是给 Windows 用户准备的不加的话 Excel 打开 csv 中文会乱成一团这在中文文本场景里几乎是必踩项。预测完成后顺手打印一条统计正向多少条、负向多少条、占比多少一来验证新数据的分布二来对比一下和训练集的分布差异。偏差太大说明抓取的商品、时间段、评分档和训练集不是一个口径模型的可信度要打折扣。5. 避坑指南编码、路径和数据偏差的五个排查现场跑通项目的过程里真正消磨耐心的往往不是模型本身而是各种看起来莫名其妙的环境问题。这一章把我在复现这类资源时最常遇到的五个坑记下来每条都是现象、原因、解决三段式照着一项项排查比乱试快得多。5.1 现象一加载模型时报 Not found 或 No checkpoints found现象run.py 跑到saver.restore那一行报错提示找不到 checkpoint 文件或者给出一个明显不属于当前机器的路径比如/home/xxx/jd_comments...。原因TensorFlow 的 checkpoint 文本文件里记录的权重路径是绝对路径。这套资源经过拷贝迁移原作者的机器路径和你本机不一致TensorFlow 还在按旧路径找文件。解决用文本编辑器打开模型目录下的 checkpoint 文件把里面三行路径改成当前机器上的实际路径注意保持文件名不变。改完再跑 restore。如果不想动原文件也可以把模型目录复制到项目根目录的固定位置让路径保持简短稳定。从那以后我拿到任何带 ckpt 的代码第一件事就是打开 checkpoint 文件检查路径。5.2 现象二训练时没事预测时输出几乎全是同一个类现象训练阶段的准确率看着还行一拿到新评论测试模型对绝大多数输入都判成正向或都判成负向毫无区分度。原因训练和预测用了不同的预处理逻辑。常见的有两种预测脚本里没有加载停用词表或者训练用的词表是旧版本预测时用新分词结果查不到词全部落到了UNK上。解决把 clean_and_pad 这类函数单独抽成一个公共模块训练脚本和 run.py 都 import 同一份代码。不要在两处各写一份预处理哪怕只差一行后面的排查成本都会翻倍。再检查 word_dict 是不是 Pickle 保存后就没再动过预测时加载的必须是训练时保存的原始文件。5.3 现象三爬虫翻了几页以后评论数据开始变空现象爬虫前几分钟正常评论一条条入库翻到二十几页之后返回的 JSON 里 comments 数组是空的或者接口返回一段 HTML 而不是 JSON。原因触发了两类防护一是抓取频率太高IP 被临时限流二是请求缺少 Cookie 或 Referer接口识别为异常来源。只改了 User-Agent 但没带 Cookie翻页次数一多就会被拦。解决把浏览器登录后的 Cookie 复制到 settings.py 的DEFAULT_REQUEST_HEADERS里Referer 指向商品详情页。同时在 middlewares.py 里加一个随机延时逻辑让每次请求之间间隔 1.5 到 3 秒而不是固定延时。分页循环里加一个失败重试机制连续三次拿到空 comments 就退出休息 10 秒再继续。5.4 现象四UnicodeDecodeError 或者 csv 打开乱码现象读取评论数据时报UnicodeDecodeError: gbk codec cant decode byte或者用 Python 写出的 csvExcel 打开后中文全乱。原因Windows 下 Python 默认编码是 gbk而评论数据源和权重文件都是 UTF-8 编码写出 csv 时又用了默认编码Excel 按本地编码解读自然乱码。这个坑在中文文本项目里出勤率极高。解决所有读写文件的代码open 一定要显式传encodingutf-8。写 csv 时用encodingutf-8-sig带 BOM 头Excel 才能正确识别。到这一步还乱码的话检查是否有中间环节用 Excel 另存过文件把编码改坏了。5.5 现象五训练 loss 卡在 0.69 附近死活不降现象训练到几千步loss 一直贴在 0.69 左右准确率在 50% 上下来回模型完全没有学进去。原因二分类交叉熵的随机基线就是 -ln(0.5)≈0.69。loss 卡在这里说明模型在输出均匀概率没有找到任何有效特征。最常见的原因是训练集正负样本严重不均衡比如正向评论占 90%模型只需要全部预测正向就能拿 90% 准确率loss 却可能一直不理想。另一个常见原因是学习率偏高参数在最优解附近震荡。解决先统计训练集的正负样本比例建议把样本数少的一方做重复采样或者从全量数据里各抽同样数量保证 1:1。学习率从 0.001 降到 0.0005 再试。这两步都做了以后loss 还是纹丝不动就把batch_size调大一点让每一步的梯度方向更稳定同时固定随机种子确保每次复现的结果一致。6. 把准确率再抬一档阈值、坏样本复盘与增量训练的实操模型能跑通只是起点真正决定项目拿多少分的是后面这三件事。第一件是阈值校准前面已经给过搜索代码实际用的时候把验证集的正向概率分布画个直方图看看两个峰是否明显分离。峰越分开模型越好两个峰叠在一起说明数据噪声大或者特征不够此时调阈值只能微调不能起死回生。第二件是坏样本复盘。把验证集里预测错的样本全部导出来按“真实负向被判正向”和“真实正向被判负向”分成两个文件逐条看。你会发现一个规律被判错的文本几乎都带转折结构比如“质量还行但客服太差”“便宜但是掉漆”。LSTM 对后文的权重更高转折词后面的情绪盖过了前面的表达这其实是符合直觉的人也这么判断。真正的问题往往是数据里这类转折句太少模型没机会学。解决方案是去爬更多带转折词的评论补充训练集这比调任何超参数都有效。第三件是增量训练。资源里的预训练权重是 40000 步的如果你引入了新数据不建议从零开始重新训练而是加载已有权重继续跑# 加载40000步的权重后继续训练学习率要调小 saver.restore(sess, ./models/jd_comments_181_7_model.model.lstm_model-40000) learning_rate 0.0001 # 增量训练用更小的学习率避免冲掉已有特征 train_op tf.train.AdamOptimizer(learning_rate).minimize(loss)增量训练前一定要把原权重备份一份别覆盖。我见过有人直接在原路径上 save训练一半崩了原来的权重也没了后悔药都没得吃。设计好训练步数之后把新 checkpoint 保存到新目录和旧的隔离开。从那以后我每次处理情感分析项目都会强制走一遍坏样本复盘再加新数据增量训练调参反而放在最后。这个习惯让我避开过好几次“准确率高但上线就废”的尴尬。这套资源的完整链路足够扎实下载下来按 readme 顺序跑一遍你会比对着教程抄代码多懂不少细节。希望帮到你。本文还有配套的精品资源点击获取