简介这是一个基于Python与深度学习的电影评论情感分析系统采用前后端分离架构并接入MySQL数据库附有完整说明文档与LW版本标识定位于毕业设计、NLP入门和项目复现人群。zip包共293个文件大小约128.01MB其中包含Python源码、前端js/css/html页面、MySQL建表SQL、模型权重文件npy/pkl/pb、Markdown/Word说明文档以及GIF演示、图表图片、字体图标等静态资源目录结构完整清晰。当前已有55人学习/下载。系统利用TensorFlow/Keras构建LSTM和GRU循环神经网络先通过词嵌入对影评进行向量化表示再捕捉句子序列上下文完成正面/负面情感分类后端以Python Web框架提供API接口前端使用layui、bootstrap等成熟组件展示预测结果整个流程可复现。说明文档对系统架构、模型原理、部署步骤、参数训练等方面均有交代预览中也可看到现成UI样式读者可据此快速搭建环境、理解从数据到模型的完整实现并迁移到其他情感分析或文本分类项目。1. 开工前先搞懂这套电影评论情感分析系统在解决什么问题日常开发里前端、后端、模型是三种习惯完全不同的工作前端在意交互粒度后端在意数据一致性和接口字段而模型训练最关心语料分布和指标。标题“python基于深度学习的电影评论情感分析系统完整前后端mysql说明文档LW”之所以常被当成毕设或求职项目首选不是因为它算法有多前沿而是因为它的链路非常完整用户在前端输入一段电影评论后端调用 python 训练好的深度学习模型把结果写进 MySQL然后在页面上立刻回显“正面/负面/中性”。它适合三类人想在一个系统里同时展示 NLP 建模能力、工程落地能力和数据库设计能力的开发者正在做毕业设计需要“模型系统文档”一次性交付的学生以及想从纯算法转向全栈的工程师。需要注意的是这套系统的难点不在单一环节而在于把评论清洗、模型推理、接口封装、数据库事务和前端联调串成一个整体。2. 模型选型与训练用预训练词向量LSTM把评论分析做成真的2.1 为什么选 LSTM而不是更快/更深的模型电影评论里常见“剧情拖沓但我看完了”“并没有那么难看”这类句式。这类句子的核心词出现在句尾靠词典匹配或统计词频很容易误判。LSTM 的优势在于它通过门的机制把前面出现的“剧情拖沓”与后面的转折“但”逐步传递到最后一个时刻的隐藏状态再做分类对这类中长文本更友好。TextCNN 在短文本上速度更快但卷积窗口通常只覆盖 3-5 个词看见“没那么难看”时容易把“难看”当作负面信号。BERT 的精度确实高但微调和推理都需要较大显存把 BERT 塞进一个用 Flask 或 Django 写的单体系统里不仅拉高部署成本还会让评估接口的响应时间明显变长。对这套“完整前后端mysql”的系统来说常见做法是一个 2 层的 BiLSTM 加一个全连接分类头几万条样本就能训到一个可用的水平。提示如果后续想把系统升级为多模态情感分析可以保留当前 LSTM 的推理接口不变只替换后端调用的模型对象前端和数据库结构不需要改动。2.2 评论数据预处理与词表构建训练前先做清洗。电影评论来自爬虫或公开数据集里面常见的噪声包括HTML 标签、连续空格、网址、 用户名、表情符号、频率极低的人名地名。清洗函数按顺序处理处理完分词再按词频构建词表。import re import jieba STOP_WORDS set() with open(stopwords.txt, encodingutf-8) as f: for line in f: if line.strip(): STOP_WORDS.add(line.strip()) def clean_review(raw: str) - str: raw re.sub(r[^], , raw) # 去掉HTML标签 raw re.sub(rhttp\S, , raw) # 去掉网址 raw re.sub(r\w, , raw) # 去掉用户名 raw re.sub(r[a-zA-Z0-9], , raw) # 去掉英文和数字 raw re.sub(r\s, , raw) # 压缩空白 return raw.strip() def tokenize(text: str) - list: text clean_review(text) words jieba.lcut(text) return [w for w in words if w not in STOP_WORDS and len(w.strip()) 1]第一次构建词表时固定一个MAX_VOCAB_SIZE比如 30000统计分词结果里出现频率最高的词。不在词表里的词统一映射为[UNK]句子开头加[CLS]长度超过MAX_SEQ_LEN的做截断不够的在后面补[PAD]。两个占位符都在词表里占一个编号。MAX_SEQ_LEN对电影评论很关键。短评平均在 30 到 60 个字长评可能接近 1000 字。如果你直接截到 128模型只看到了前半段推荐按训练集长度的后 80% 分位点来设这个值避免为了适配极个别超长评论浪费计算量。我一般会先对训练语料的 token 数量画一个分布图再取分位数。2.3 网络结构BiLSTM 加全连接分类头这套毕设系统里采用 PyTorch 实现核心结构是词嵌入层、双向 LSTM、最大池化、全连接输出。import torch import torch.nn as nn class SentimentLSTM(nn.Module): def __init__(self, vocab_size, embed_dim200, hidden_size128, num_layers2, num_classes3, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0) self.classifier nn.Sequential( nn.Dropout(dropout), nn.Linear(hidden_size * 2, 64), nn.ReLU(), nn.Linear(64, num_classes) ) def forward(self, x): emb self.embedding(x) # [batch, seq_len, embed_dim] out, _ self.lstm(emb) # [batch, seq_len, hidden*2] out out.max(dim1).values # 取每个句子在时间维上的最大值 return self.classifier(out)embed_dim200是词向量的维度hidden_size128是 LSTM 每个方向的隐藏状态大小双向拼接后进入分类器的特征维度是 256。max(dim1)负责把每个词在所有时间步上的输出聚合相比只取最后一步表达式“并没那么难看”中的“难看”无论在句中的哪个位置都能被捕捉到。训练时用CrossEntropyLoss优化器用 Adam配合 ReduceLROnPlateau验证集损失连续两个 epoch 不再下降就降低学习率。criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience2) for epoch in range(20): model.train() total_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() optimizer.step() total_loss loss.item() val_loss, val_acc evaluate(model, val_loader) scheduler.step(val_loss) print(fepoch{epoch} loss{total_loss / len(train_loader):.4f} fval_acc{val_acc:.4f} val_loss{val_loss:.4f})batch_firstTrue让输入的维度对齐为[batch, seq_len]这也是数据加载器默认输出的顺序。训练阶段把batch_size拉高到 128 不会明显影响精度但会直接提高 GPU 或 CPU 的利用率。当 epoch 超过 15 而验证集 acc 不再提升时应该以模型文件的大小和响应时延为主要约束来做早停。参数推荐值说明embed_dim200词向量维度语料小就降到 100hidden_size128单向 LSTM 隐层大小num_layers2超过 2 层在小语料上收益很小dropout0.5防止在几万条样本上过拟合batch_size128CPU 训练可降到 32lr1e-3配合 ReduceLROnPlateau 使用注意nn.Embedding的padding_idx要和词表里[PAD]对应这样补位位置不会在反向传播中产生梯度能有效减少不同长度评论带来的噪声。2.4 评估视角准确率、F1、混淆意见毕设答辩中只报一个准确率很容易被追问。分类样本不平衡在电影评论里非常常见好评数量往往远大于差评模型只要全部预测成“正面”就能得到很高的正确率无法体现实际能力。所以评估要用classification_report看每个类别的 precision、recall、F1再额外统计“负面被误判为中性”这类混淆情况。from sklearn.metrics import classification_report, confusion_matrix print(classification_report(y_true, y_pred, target_names[negative, neutral, positive])) print(confusion_matrix(y_true, y_pred))当训练集只有 3 万条且正负样本比例接近 7:3 时把中性样本去掉或者重新采样都会影响最终判别。真实系统中保留中性类更实用因为用户提交的评论本来就不全是极端情绪。3. MySQL 存储与后端接口把预测流程接进数据库3.1 表结构设计用户、电影、评论、预测拆分系统要求“完整前后端 MySQL”表结构设计是这部分最重要的体现。常见做法是拆成 4 张表user记录用户movie记录电影comment记录原始评论sentiment_result记录每次预测的输出和置信度。评论和分析结果分开存可为后续人工复核留出空间。CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, director VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE comment ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, content TEXT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-正常 1-已删除, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_movie_id (movie_id), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_movie FOREIGN KEY (movie_id) REFERENCES movie(id) ); CREATE TABLE sentiment_result ( id INT PRIMARY KEY AUTO_INCREMENT, comment_id INT NOT NULL, sentiment TINYINT NOT NULL COMMENT 0-负面 1-中性 2-正面, confidence FLOAT NOT NULL, model_version VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_comment (comment_id) );comment表保留用户原文sentiment_result只存结果这样即使后续换模型重跑测试也只需要按comment_id重建结果表。业务上“展示历史记录”每次都从comment关联sentiment_result查询分页时对comment表做主查询再批量查结果避免逐条回表。提示不要把模型预测的字符串直接存数据库。用TINYINT存 0、1、2前端展示时再映射成“负面、中性、正面”后续做统计和建索引都会更快。3.2 Flask 后端如何加载模型并提供预测接口路径别写死。模型文件model.pth放在models/目录通过配置类读取。接口层的常见结构是Flask 应用启动时加载一次模型和词表每次请求只做 tokenize、编码、前向推理拿到结果后先落库再返回 JSON。from flask import Flask, request, jsonify import pymysql import torch from model import SentimentLSTM from utils import tokenize, build_vocab app Flask(__name__) MODEL_PATH models/model.pth word2idx build_vocab(vocab.json) model SentimentLSTM(vocab_sizelen(word2idx)) model.load_state_dict(torch.load(MODEL_PATH, map_locationcpu)) model.eval() def get_db(): return pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databasemovie_sentiment, cursorclasspymysql.cursors.DictCursor, charsetutf8mb4 ) app.route(/api/predict, methods[POST]) def predict(): data request.get_json() content data.get(content, ) if not content: return jsonify({code: 400, msg: 评论内容不能为空}), 400 ids [word2idx.get(w, word2idx[[UNK]]) for w in tokenize(content)[: 128]] ids [word2idx[[CLS]]] ids pad_len 128 - len(ids) if pad_len 0: ids ids [word2idx[[PAD]]] * pad_len x torch.tensor([ids], dtypetorch.long) with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim-1).squeeze().tolist() sentiment int(torch.argmax(logits, dim-1).item()) confidence max(prob) return jsonify({sentiment: sentiment, confidence: confidence})上面这段实现了完整链路上最关键的一环。word2idx.get使用.get而不是直接下标访问为的是让生词落到[UNK]pad_len用128 - len(ids)计算是因为评论在进入接口前已经截到 128不再依赖训练阶段那个更大的MAX_SEQ_LEN。confidence取 softmax 概率最大值写库时也用它排序便于后续挑出低置信度样本做人工复核。3.3 接口落库与前端交互的衔接预测接口里只有一个return并不完整要让 MySQL 真正记录这次请求就应该在返回前把comment和sentiment_result数据写入数据库。这里有一个隐藏陷阱如果先插评论、后插结果而结果插入失败会出现“有评论无结果”的脏数据。常见做法是用一个事务把两个 insert 包起来任何一步失败就回滚。def save_comment_and_result(user_id, movie_id, content, sentiment, confidence): conn get_db() try: with conn.cursor() as cursor: conn.begin() cursor.execute( INSERT INTO comment (user_id, movie_id, content) VALUES (%s, %s, %s), (user_id, movie_id, content) ) comment_id cursor.lastrowid cursor.execute( INSERT INTO sentiment_result (comment_id, sentiment, confidence, model_version) VALUES (%s, %s, %s, %s), (comment_id, sentiment, confidence, lstm_v1) ) conn.commit() return comment_id except Exception as exc: conn.rollback() raise exc finally: conn.close()保存评论与分析结果到两个表里用conn.begin()开启事务任一步异常都执行rollback()避免出现流水线不一致。把模型版本写进sentiment_result可以让“完整前后端 mysql 说明文档”交付物里的每一次预测都有迹可循也是说明文档中架构图上最好画的一条链路。4. 前端页面与前后端联调Vue 下完成评论提交、结果回显4.1 前端功能拆解页面、组件和接口的对应关系如果这是一套 Vue 项目一般会有三个主要页面首页展示电影列表和最新评论汇总评论页提供文本输入与提交个人中心展示当前用户的历史评论及情绪分布。后端接口和页面的行为要一一对应。前端页面主要接口用途电影列表页GET /api/movies展示电影列表详情/评论页POST /api/predict提交评论文本并获得预测个人中心GET /api/my/comments展示历史记录个人中心GET /api/my/stats展示情绪统计折线图前端不能直接把comment表拿过来用而是让接口返回约定好的 JSON。常用字段是commentId、movieTitle、sentiment、confidence、createdAt。这类接口需要把 id 转换为可读的展示文本转换逻辑放在后端完成前端只负责渲染。4.2 前后端联调时最容易出问题的跨域前端跑在localhost:5173是 Vue 默认端口后端默认是 Flask 的localhost:5000端口不同浏览器会直接阻止请求。解决方式有两种后端加 CORS前端配置代理。在 Flask 里安装flask-cors然后CORS(app)即可。很多初学者把它理解为“安全配置”为了避免变动反复加规则。实际 CORS 是浏览器的同源策略并不是加密。在本地联调阶段直接使用CORS(app)提高效率部署到生产再通过 Nginx 反向代理让前后端使用同一个域名一次性解决跨域问题。4.3 Vue 里用 axios 提交评论并回显预测结果前端用一个Vue单文件组件承载评论提交。提交时把movieId、content发给后端收到返回后把sentiment映射成中文confidence格式化掉多余的精度再拼到历史列表的头部。script setup import axios from axios import { ref } from vue const content ref() const result ref(null) const labelMap [负面, 中性, 正面] async function submitComment() { if (!content.value.trim()) return const { data } await axios.post(/api/predict, { movieId: 7, content: content.value }) if (data.code 0) { result.value { label: labelMap[data.sentiment], confidence: (data.confidence * 100).toFixed(2) % } } } /script template textarea v-modelcontent placeholder说说这部电影的看法/textarea button clicksubmitComment提交检测/button p v-ifresult结果{{ result.label }}置信度{{ result.confidence }}/p /templatelabelMap是一个数组下标正好对应后端返回的sentiment这要求前后端在字段定义上保持一致。toFixed(2)把置信度保留两位小数最适合直接展示。result只存展示用的数据而不是整包响应对象避免后续把多余字段暴露在页面上。联调时可以先不经过界面直接用 curl 验证接口。curl 验证完之后再回到页面点击按钮发现问题基本上都集中在 JSON 字段名拼写和请求方式不匹配。例如后端要求 POST前端写成了 GET返回 405就先检查axios.post的用法。5. 标注效率与可视化常见坑与实用改进5.1 中性样本的取舍许多开源的电影评论数据集只有“正面、负面”两维标注但实际系统在上线后经常收到“剧情一般、不算差”这类中性评论模型被迫在两个类里二选一置信度往往很低。改进方式是把原始训练集中置信度处于 0.35 到 0.65 的样本找出来人工重新标注出一条中类并将其作为第三类加进训练。数据标注的过程可以用表格管理每行记录评论原文、初判结果、人工复核结果和修正原因避免两个人标注同一批数据时口径不一致。对于手动标注成本控制在 2000 到 5000 条即可明显改善边界样本的表现。5.2 用缓存表加速重复文本预测接口第一次对某条评论分类可能需要几百毫秒但如果多人提交完全相同的短评重复跑模型非常浪费。可以加一张predict_cache表以 128 token 截断后的文本 hash 为 key命中缓存直接返回。这张表建在 MySQL 里比使用 redis 更符合“完整前后端mysql”的交付形态部署时不需要额外引入服务。CREATE TABLE predict_cache ( id INT PRIMARY KEY AUTO_INCREMENT, text_hash CHAR(32) NOT NULL UNIQUE, sentiment TINYINT NOT NULL, confidence FLOAT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );命中时把confidence原样返回未命中时推理并写入缓存。每天日终再清理一次最近 3 天没有访问的记录。这种优化实现成本低却能在答辩和演示中直观展示“响应时间从 287ms 降到 3ms”。UNIQUE约束会自动建立唯一索引在数据库层防止同一文本被重复写入。5.3 端到端的验证文件说明文档里经常只写接口列表忽略手动验收流程。真正可复现的交付物应该包含表结构说明、数据流图、部署步骤、以及一个按顺序执行的测试清单。测试清单记录一条真实电影评论、预期输出、实际输出和修复情况作为前后端联调验收的依据。可以在项目根目录放一个smoke_test.sh内容依次执行“启动前检查 MySQL 连接 → 确认 Flask 健康检查接口 → 读取一条测试评论 → 调用 /api/predict → 查询 sentiment_result 表确认落库”。每次提交代码前先跑这个脚本完成回归验证再写说明文档里的测试章节整个交付过程会顺畅很多。本文还有配套的精品资源点击获取