简介:面向计算机相关专业毕业设计及Python项目实战学习者,这套基于LSTM的电商购物情感分析工程以京东商城评论为对象,完整覆盖评论采集、中文文本预处理、LSTM模型训练与情感分类推理流程,适合作为毕业设计、课程设计或期末大作业的可用基础。压缩包共39个文件,含8个Python脚本、训练好的LSTM模型与checkpoint、6组分片训练数据及其索引/元数据、中文停用词表与说明文档,另有7张结果可视化图片和配置文件,整体约164MB。项目同时包含独立爬虫模块,配合使用说明可还原从数据获取到模型预测的完整链路,目录结构清晰,便于按需修改与扩展。目前已有404人学习下载,对需要直接可运行源码、完整数据集和清晰实验结构的入门及中级学习者较有参考价值。
1. 从京东评论到情感分类:LSTM 项目该怎么做才不白做
手头拿到一批京东商城的用户评论,想判断每一条是好评还是差评,最直接的想法是搞规则词典,但电商评论里“声音挺好但物流太慢”“质量一般般吧”这种带转折、带程度的句子,词典一碰就碎。这个毕设项目的核心是用 LSTM 对京东商品评论做情感二分类,数据是真实爬下来的用户评论文本,不是网上随便找的干净数据集。和 LR、朴素贝叶斯这类传统模型相比,LSTM 能捕捉句子里的词序关系,对“虽然……但是……”这类结构有明显优势。项目本身不算大,但链路完整:Scrapy 爬虫采集、中文分词预处理、Embedding + LSTM 建模、训练调参、模型保存与复用。适合正在做毕设的学生,也适合想完整跑一遍 NLP 情感分析流程的 Python 开发者。它能回答一个很实际的问题:拿到几万条中文评论,怎么一步步做成一个能用的分类模型。
2. 数据采集与中文评论预处理,决定模型上限的环节
2.1 基于 Scrapy 的 JDSpider:评论数据从哪来
项目里的 JDSpider 目录是独立的 Scrapy 工程,包含scrapy.cfg和爬虫核心代码。默认的爬取对象是京东商品评论接口,URL 形如https://club.jd.com/comment/productPageComments.action,需要传入productId、score、sortType等参数。京东的评论接口返回的是 JSON,不需要解析 HTML,这比爬商品详情页简单很多。
# JDSpider/spiders/jd_comment.py import json import scrapy class JdCommentSpider(scrapy.Spider): name = "jd_comment" def start_requests(self): product_ids = ["100012043978"] # 可替换为任意商品ID for pid in product_ids: url = "https://club.jd.com/comment/productPageComments.action" params = { "productId": pid, "score": "0", # 0 表示全部评论,1 差评,2 中评,3 好评 "sortType": "5", # 5 按时间排序 "page": "0", "pageSize": "10", } yield scrapy.Request(url, method="GET", callback=self.parse, meta=params) def parse(self, response): data = json.loads(response.text) for comment in data.get("comments", []): yield { "content": comment.get("content"), "score": comment.get("score"), "nickname": comment.get("nickname"), "product_color": comment.get("productColor"), }score=0是所有评论,score=3只能拿到好评,想训练分类模型必须把三类都爬下来,否则样本分布是歪的。productId换成自己需要分析的商品即可。实际跑的时候建议把pageSize调到 50,page循环递增,京东对评论接口的限流相对宽松,但还是要加DOWNLOAD_DELAY。
# JDSpider/settings.py DOWNLOAD_DELAY = 1.5 # 每次请求间隔1.5秒,太短会被封IP USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"很多毕设项目死在第一步就是没设DOWNLOAD_DELAY,爬个几百条就被京东封了,后面训练和评估全没法做。另外京东评论接口返回的 JSON 里还有maxPage字段,可以直接取出来控制翻页总数,避免死循环。
2.2 jieba 分词与停用词过滤:中文 NLP 无法跳过的基础步骤
拿到原始评论后不能直接喂给 LSTM,中文不像英文那样天然按空格分词。项目里用 jieba 做分词,配合StopwordsCN.txt去停用词。jieba 在电商领域的效果整体可以接受,但像“好用”“性价比”这种词会被切成“好用”“性价比”,不会拆碎,属于够用级别。如果追求更准,可以加载自定义词典,把商品名、品牌词加进去。
# preprocess.py import jieba import re def load_stopwords(path="StopwordsCN.txt"): with open(path, "r", encoding="utf-8") as f: return set(line.strip() for line in f) STOPWORDS = load_stopwords() def clean_comment(text): # 去掉HTML标签、URL、数字、英文(保留中文和基本标点) text = re.sub(r"<.*?>", "", text) text = re.sub(r"http\S+", "", text) text = re.sub(r"[a-zA-Z0-9]", "", text) # 去掉空白字符 text = re.sub(r"\s+", "", text) return text.strip() def tokenize(text): text = clean_comment(text) words = jieba.lcut(text) return [w for w in words if w not in STOPWORDS and w.strip()]停用词表里通常包含“的、了、是、我、你、他”这类高频但无情感倾向的词,但电商场景要谨慎:像“不是”“不行”“不好”这类否定词一旦被当成停用词删掉,句子的情感极性会完全反转。项目自带的StopwordsCN.txt是通用版本,我建议你把“不”“没”“别”这类否定词从停用词表里剔掉,保留在分词结果里,让 LSTM 自己去学否定结构。
| 预处理项 | 处理方式 | 原因 |
|---|---|---|
| HTML 标签 | 正则删除 | 评论可能包含富文本内容 |
| URL | 正则删除 | 对情感判断无贡献 |
| 数字英文 | 视情况保留 | 如“5分好评”“iPhone”之类有语义 |
| 停用词 | 删除高频虚词 | 降低特征维度,但保留否定词 |
| 分词 | jieba.lcut | 默认模式即可满足 |
数据准备好之后要转成序列。先通过Tokenizer给每个词分配一个索引,再把每条评论映射成整数序列,统一填充到固定长度。这个长度一般取评论长度的 80 分位,京东短评大多数在 30 到 60 个词之间,设成MAX_LEN=50比较合理。超过 50 截断,不足 50 补 0。
from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences tokenizer = Tokenizer(num_words=20000, oov_token="<OOV>") tokenizer.fit_on_texts(all_comments) sequences = tokenizer.texts_to_sequences(all_comments) X = pad_sequences(sequences, maxlen=50, padding="post", truncating="post")num_words=20000表示只保留词频最高的 2 万个词,oov_token="<OOV>"让训练时没出现过的词统一映射到<OOV>这个索引上,避免预测阶段报错。padding="post"是在尾部补零,truncating="post"是从尾部截断,这样能保留句子的开头部分,因为中文评论的核心观点通常在前半句。
2.3 标签构建:好评差评之外的第三类怎么处理
京东评论自带 1 到 5 星评分。常见做法是把 4 星、5 星归为好评(1),1 星、2 星归为差评(0),3 星算中性评论直接丢弃,因为中性样本的标注边界太模糊,强行训练会让模型在好坏之间摇摆。这样处理后类别会比较干净,二分类的准确率也能做得好看。
def build_label(score): if score >= 4: return 1 # 好评 elif score <= 2: return 0 # 差评 else: return None # 中性评论不参与训练把中性评论过滤掉之后,检查一下正负样本比例。爬虫默认按时间排序,好评率高的商品爬到的数据会严重偏向好评。如果正负比超过 8:1,模型会倾向于把所有评论都判成好评。常见做法是对差评做随机过采样,也就是把差评样本复制几份让比例降到 3:1 以内,或者用class_weight在损失函数上给差评加大权重。
from sklearn.utils.class_weight import compute_class_weight class_weights = compute_class_weight( class_weight="balanced", classes=np.array([0, 1]), y=train_labels ) class_weights = dict(enumerate(class_weights))compute_class_weight会自动计算各类别的权重系数。差评样本少,它对应的权重就大,模型训练时对差评的误判会产生更大的 loss,梯度更新时就会更重视差评。这个技巧能直接拉高差评的召回率,对毕设答辩时“为什么用这个方案”是一个加分的回答点。
3. LSTM 模型架构与训练细节:embedding 的坑你踩过几个
3.1 从 Embedding 到全连接:模型每一层的作用
LSTM 用于文本分类的经典结构是:输入层 -> Embedding 层 -> LSTM 层(或双向 LSTM)-> Dropout -> Dense + Sigmoid。Embedding 层的作用是把词索引映射成稠密向量,让语义相近的词在向量空间里距离更近。项目里用的是随机初始化的 Embedding,不是预训练好的 Word2Vec,对小规模语料来说这个设定反而更省事,能跟着任务一起学,效果未必比预训练差多少。
# model.py from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout VOCAB_SIZE = 20000 EMBED_DIM = 128 MAX_LEN = 50 model = Sequential([ Embedding(input_dim=VOCAB_SIZE, output_dim=EMBED_DIM, input_length=MAX_LEN, mask_zero=True), LSTM(units=128, return_sequences=False, dropout=0.2), Dropout(0.5), Dense(1, activation="sigmoid"), ]) model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])mask_zero=True是一个容易被忽略但很重要的参数。前面pad_sequences给短评论补的 0,如果不加 mask,Embedding 层会把 0 也当成一个正常 token 去查向量,等于把无意义的填充位也送进了 LSTM。加上mask_zero=True之后,LSTM 会跳过这些位置。但要注意一点:mask_zero开启时,Embedding 层的input_dim里索引 0 是被占用的,所以Tokenizer的词表索引需要从 1 开始计数,刚好 num_words 的上限要留出这个位置。
LSTM(128)的 128 是隐层维度,控制模型的记忆容量。太小欠拟合,太大过拟合且训练慢。dropout=0.2是在 LSTM 内部对输入和循环连接做 dropout,这是 LSTM 层的参数,不需要额外加 Dropout 层。Dropout(0.5)是加在 LSTM 输出和 Dense 之间,用来进一步降低过拟合风险。
3.2 训练脚本:epoch、batch、验证集怎么配合
训练脚本结构不复杂,关键在 validation split 的设置和早停策略。先划分训练集和测试集,再从中切一部分做验证集。用 8:2 切分,训练集上还要再留出 10% 到 20% 当验证集,验证集只用于观察训练过程中的过拟合情况,不参与梯度更新和调参。
# train.py from sklearn.model_selection import train_test_split from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) early_stop = EarlyStopping( monitor="val_loss", patience=3, restore_best_weights=True ) checkpoint = ModelCheckpoint( "best_model.model", monitor="val_accuracy", save_best_only=True, mode="max" ) history = model.fit( X_train, y_train, batch_size=64, epochs=20, validation_split=0.15, callbacks=[early_stop, checkpoint], class_weight=class_weights )batch_size=64没有绝对标准,和显卡显存大小有关。8 到 32 的 batch 收敛更快但震荡剧烈,128 以上训练稳定但容易收敛到平坦的局部最优,64 是通用折中。patience=3意味着连续 3 个 epoch 验证损失没有下降就停止训练,这能防止训到后面过拟合到训练集上。restore_best_weights=True回滚到验证集上表现最好的那组参数。
训练标签得有类别比例意识。stratify=y保证切分后训练集和测试集的正负样本比例一致,不做这个操作,差评少的数据集里测试集可能一条差评都没有,模型评估就会失真。
3.3 模型保存与加载:除了.h5还有别的选择
项目里保存的模型文件是jd_comments_181_7_model.model,后缀是.model而不是.h5,这看起来像是 OldH5 格式,Keras 3 之后默认保存格式改为.keras,但旧项目用model.save("xxx.model")依然能正常加载。
from tensorflow.keras.models import load_model model = load_model("jd_comments_181_7_model.model")如果加载报错,看一下训练时的model.save是直接用 Keras 保存的还是用checkpoint保存的。ModelCheckpoint保存的模型可能只包含网络权重,加载方式略有区别。我自己的习惯是同时保留完整模型和tokenizer,因为只保存权重但丢了tokenizer,预测新评论时无法把文本转成序列,模型就是个废的。
import pickle with open("tokenizer.pkl", "wb") as f: pickle.dump(tokenizer, f)run.py里的预测主流程应该是这样的:先加载模型,再加载tokenizer,然后走一遍分词、序列化、padding,最后输出情感类别。整个过程只要有模型文件和 tokenizer,就不需要重新训练。
4. 模型评估与优化调参:别只盯准确率
4.1 评估维度:混淆矩阵与精确率召回率
二分类情感分析里,准确率高不代表模型真能用。如果测试集里好评占 85%,模型全预测成好评准确率就有 85%,但差评全部漏掉了。所以必须看混淆矩阵,以及从它推导出的精确率、召回率和 F1 值。
| 指标 | 公式 | 对电商评论的意义 |
|---|---|---|
| 精确率 Precision | TP / (TP + FP) | 预测为差评的评论里真实差评的比例 |
| 召回率 Recall | TP / (TP + FN) | 真实差评里被成功找出来的比例 |
| F1 | 2 * P * R / (P + R) | 两者的调和平均,综合反映模型对少数类的识别能力 |
from sklearn.metrics import classification_report, confusion_matrix y_pred = (model.predict(X_test) > 0.5).astype(int) print(classification_report(y_test, y_pred, target_names=["差评", "好评"])) print(confusion_matrix(y_test, y_pred))对电商评论场景,差评漏判的代价比误判好评更大,因为用户差评往往揭示了商品或服务的真实问题。如果差评召回率低于 0.7,优先处理方法不是修改网络结构,而是先调class_weight或分类阈值,把判断阈值从 0.5 降到 0.4,模型会输出更多差评预测,召回率上去了,精确率略降可以接受。
4.2 过拟合的典型表现:训练损失降了验证损失却在涨
LSTM 是参数密集型模型,Embedding 层加 LSTM 层的可学习参数轻松过百万,在几万条评论的数据集上过拟合非常常见。现象就是训练集 loss 不断下降,验证集 loss 先降后升,准确率卡住不动。项目里的 Dropout 已经做了基础防护,但还可以从下面几个方向继续调。
数据增强方面可以尝试同义词替换,把评论里的“不错”替换成“挺好”,“垃圾”替换成“很差”,相当于扩充了训练样本。但这个操作要用同义词词典控制,替换太频繁会破坏句子结构。另一个做法是减小num_words,从 2 万降到 1 万,让长尾低频词直接映射到<OOV>,减少模型中需要学习的词向量数量,对缓解过拟合往往立竿见影。
tokenizer = Tokenizer(num_words=10000, oov_token="<OOV>")num_words的直觉是:高频词承载了绝大多数情感信息,而那些只出现一两次的生僻词,模型靠它们学不到规律,反而会记住训练集上的噪声。
梯度裁剪也是 LSTM 训练的小技巧。文本分类里的梯度爆炸相比时间序列预测没那么严重,但加了也能提升稳定性:
from tensorflow.keras.optimizers import Adam optimizer = Adam(learning_rate=0.001, clipnorm=1.0) model.compile(optimizer=optimizer, loss="binary_crossentropy", metrics=["accuracy"])clipnorm=1.0会把梯度的 L2 范数限制在 1.0 以内,超过就按比例缩放。学习率 0.001 是 Adam 的默认值,如果你的训练 loss 震荡明显,可以降一半再试。
4.3 预测新评论:从分词到概率值的完整链路
模型训练好之后,需要把预测流程封装成可以直接对单条评论调用的函数。这里最需要注意的是预测时的预处理必须和训练时完全一致,包括相同的分词方式、相同的pad_sequences参数、相同的maxlen。
# predict.py def predict_sentiment(text): clean_text = clean_comment(text) words = jieba.lcut(clean_text) filtered = [w for w in words if w not in STOPWORDS] seq = tokenizer.texts_to_sequences([" ".join(filtered)]) padded = pad_sequences(seq, maxlen=50, padding="post", truncating="post") prob = model.predict(padded)[0][0] label = "好评" if prob >= 0.5 else "差评" return label, prob print(predict_sentiment("物流很快,客服态度也很好,质量超出预期")) print(predict_sentiment("用了三天就坏了,申请售后半天没人理"))tokenizer.texts_to_sequences要求输入的是以空格分隔的字符串列表,所以分词结果要先用" ".join拼回去。color、content这些字段不会参与模型计算,但留存对后面分析用户关注点很有价值。
5. 模型可视化与嵌入层向量对比:让情感分析不止于二分类结果
5.1 训练曲线:验证模型真的收敛了
项目里的 picDic 目录放了一组图片,包括 1.png 到 7.png,大概是训练过程中的 accuracy 和 loss 曲线。我在复现跑通之后用 matplotlib 把训练历史上面的图重新画了一版,方便自己观察收敛状态。
import matplotlib.pyplot as plt def plot_history(history): fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(history.history["loss"], label="train_loss") axes[0].plot(history.history["val_loss"], label="val_loss") axes[0].set_title("Loss Curve") axes[0].legend() axes[1].plot(history.history["accuracy"], label="train_acc") axes[1].plot(history.history["val_accuracy"], label="val_acc") axes[1].set_title("Accuracy Curve") axes[1].legend() plt.tight_layout() plt.savefig("training_curve.png", dpi=150)5.2 嵌入层词向量的可视化:无监督理解模型学到了什么
Embedding 层训练完成后,每个词对应一个向量,这些向量可以用 t-SNE 降维到二维平面做可视化。同一类情感倾向的词在向量空间里会聚集在一起,这是判断模型质量非常直观的手段,比只看 loss 曲线更有说服力。京东评论里比较典型的聚集是“快递/发货/送货/物流”一组,“质量/做工/材质/手感”一组,不同主题的词在空间上天然分离。
from sklearn.manifold import TSNE import numpy as np embedding_weights = model.layers[0].get_weights()[0] word_index = tokenizer.word_index # 取词频最高的前500个词做可视化 words = list(word_index.keys())[1:501] indices = [word_index[w] for w in words] vectors = embedding_weights[indices] tsne = TSNE(n_components=2, perplexity=30, random_state=42) reduced = tsne.fit_transform(vectors) plt.figure(figsize=(12, 12)) for i, word in enumerate(words): plt.scatter(reduced[i, 0], reduced[i, 1]) plt.annotate(word, (reduced[i, 0], reduced[i, 1]), fontsize=9) plt.title("t-SNE Visualization of Embedding Vectors") plt.savefig("embedding_tsne.png", dpi=150)5.3 情感分析的进阶方向:从二分类到多粒度分析
项目做到这个程度,毕设的基础目标已经完成了。如果想再往前走一步,可以考虑三个方向。
一是把二分类扩展成三分类,把 3 星评论单独作为中性类。实现上只需要改最后一层 Dense 的激活函数为softmax,输出维度设为 3,损失函数换成categorical_crossentropy,标签做 one-hot 编码。模型结构基本不用动。
二是加入 Aspect-Based Sentiment Analysis,针对某个商品属性做情感判断,比如“质量好但物流慢”,这需要用注意力机制改造网络结构。具体做法是在 LSTM 层之后加一个注意力层,对所有时间步的输出计算加权和,权重交给网络自己学。这属于进阶课题,适合在论文里作为创新点展开。
三是结合评论长度和评分做多任务学习,用同一个 LSTM 共享层同时预测情感极性和评分星级。我在实际测试中发现,评分预测作为辅助任务可以让共享层的特征表达更丰富,主任务情感分类的 F1 值大概能提升 1 到 2 个百分点。但这个改造训练时间会增加不少,需要自己权衡。
5.4 部署技巧:一个 fastapi 情感分析服务
模型调好之后可以直接封装成 HTTP 服务,毕设演示时比跑命令行脚本要有说服力得多。
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Comment(BaseModel): text: str @app.post("/predict") def predict(comment: Comment): label, prob = predict_sentiment(comment.text) return {"label": label, "positive_prob": round(float(prob), 4)} # 启动: uvicorn api:app --host 0.0.0.0 --port 8000用curl测试一下:curl -X POST http://localhost:8000/predict -H "Content-Type: application/json" -d '{"text":"这个商品太值了"}'返回结果就是一个 JSON。整个项目从爬虫到服务化部署形成了一条完整的通路,答辩时把这条链路讲清楚,比单讲模型结构更能体现工程能力。
本文还有配套的精品资源,点击获取