简介:面向本科毕业设计的中文情感分析系统完整项目包,基于Python、Flask与MySQL实现,用深度学习模型对评论做正面/负面分类。资源覆盖登录注册、后台首页统计看板、文本分析及软件测试流程,适合计算机专业学生快速搭建可演示环境,也可作为Flask+深度学习整合开发的参考范例。压缩包共378个文件,大小97.54MB,含Python源码、SQL数据库脚本、HTML/CSS/JS前端页面、模型权重文件(pkl/npy)、界面截图与演示动图(png/jpg/gif),另附文档、演示视频和PPT,便于按模块检索,目前已有220人学习浏览。通过这套资料,可掌握数据预处理、模型训练到Web部署的完整链路,理解注册登录安全、数据可视化和情感分析接口的集成方式,尤其适合需要短时间完成系统演示与论文撰写的冲刺阶段,资源包目录结构清晰,可对照文档直接启动项目。
1. 一个把中文评论自动分成正面和负面的 Web 系统
我最近拆了一套基于 Python Flask 的中文情感分析系统,把一段中文评论文本输进去,后端跑一遍深度学习模型,返回正面还是负面的结论;整个系统还带登录注册、历史分析记录、正负面占比柱状图和饼图。这种「网页应用 + 深度学习推理」的组合,恰好是很多毕业设计想要的样子——既要体现算法能力,又要交付完整可演示的系统功能。它适合三类人:正在选题的计算机专业学生、想快速把模型包装成 Web 服务的开发者、以及需要批量处理评论文本又不想从零搭系统的个人玩家。我按架构设计、核心模块、数据库、运行步骤和常见坑的顺序往下拆。
2. 架构与工程结构:Flask 业务和离线模型推理怎么配合
2.1 技术栈选型:为什么是 Flask + MySQL + LayUI + 深度学习框架
先看一眼整站的技术栈。后端用的是 Flask,这是 Python 生态里最轻量的 Web 框架,一个 app.py 就能把路由全写清楚;数据库是 MySQL,存用户、分析记录这类结构化数据。前端没引入大型框架,用的是 LayUI 那套组件库——项目静态资源里的 layui.css、layer.css、laydate.css 都是它的组成部分,表单、弹窗、日期选择器直接拿来用。样式文件里那批 index_2018051810.css、report.css 是后台模板的皮肤和报表样式,index_pdf.css 主要用于导出或打印排版,laydate.css 则管日期控件。图表部分结合 ECharts 绘制柱状图和饼图。
深度学习部分是这套系统最值得拆的地方。训练一个文本分类模型需要准备数据、训练和调参,如果每一次 Web 请求都现场训练,显然不现实,所以项目采用「离线训练 + 在线推理」的常见做法:模型在训练脚本里跑完保存成文件,Flask 启动时载入一次,之后每个分析请求走预处理、模型推理、结果入库这条路。分类网络方面,短文本常用 TextCNN,长文本常用 BiLSTM,这套项目里按短文本评论处理,常规实现是 Embedding + 双向 LSTM + 全连接分类层,词向量用中文预训练向量初始化,网络结构我放到 3.2 节配合代码讲。
为什么选 MySQL 而不是 SQLite?毕业设计演示时,MySQL 更能体现工程完整性,网上部署环境基本都装了。用户表、分析记录表都放在 MySQL 里,查询和统计都比文件型数据库顺手。为什么不是 Django?Django 是个全家桶框架,自带 ORM、Admin、迁移工具,很强,但概念多;Flask 路由和业务简单直接,对演示型项目来说代码量小、每一行都能讲清楚,而且深度学习代码大多是 Python 函数,Flask 里直接 import 就能调用,集成路径最短。数据库连接用 PyMySQL,有人会再套一层 SQLAlchemy ORM,这个项目为了降低上手门槛,用原生 SQL 脚本建表加 PyMySQL 查询。演示阶段这种写法反而好排查问题。
整体架构落地成一条数据流是这样的:
浏览器页面(LayUI)→ Flask 路由 → 视图函数 → MySQL(验证用户、写分析记录)→ 分类模型(预处理 + 推理)→ JSON 结果 → 前端渲染图表
这条链路里有两个边界值得注意:业务逻辑和数据都走 Flask + MySQL,深度学习模型被包在推理工具模块里,不直接参与用户查询。带来的好处是后面想换模型,Web 层几乎不用动。前后端也不是分离架构,页面由 Flask 的 Jinja2 模板渲染,图表初始化数据可以由后端模板变量注入,也可以由前端再调接口拿 JSON。分析结果通常用 ajax 请求处理,减少整页刷新;二次开发想接 Vue,保留 /analyze 和 /stats 接口返回 JSON 即可。
2.2 目录结构:源码包里每一块都是干什么的
解压资源包之后,核心目录结构大概是这个样子:
project/ ├── app.py # Flask 入口,注册路由、启动服务 ├── config.py # 数据库配置、模型路径、secret_key ├── requirements.txt # 依赖清单 ├── models/ │ ├── ckpt/ # 训练好的模型权重文件 │ └── dict.pkl # 词表 / 索引映射字典 ├── utils/ │ ├── preprocess.py # jieba 分词、去停用词、序列化 │ └── predictor.py # 加载模型、执行推理 ├── service/ │ ├── user_service.py # 注册登录业务 │ └── analyze_service.py # 分析记录与统计业务 ├── static/ │ ├── css/ js/ layui/ # 前端静态资源 ├── templates/ │ ├── login.html register.html │ ├── index.html # 后台首页,柱状图饼图 │ └── analyze.html # 文本分析页 └── sql/ └── sentiment.sql # 建库建表脚本templates 是 Flask 默认的模板目录,页面都在这里;static 放 LayUI、ECharts 这些静态资源。models 目录是整个系统的黑匣子所在:dict.pkl 保存「词 → 索引」的映射字典,ckpt 是训练好的权重。推理时文本要先转成索引序列才能喂给网络,这两个文件丢了模型就跑不起来,部署时要一起拷走。utils/predictor.py 是深度学习推理入口,后续换模型主要改这个文件;service 目录把业务从路由里抽出来,是合格的 Flask 工程习惯。如果你的源码包结构更扁平,所有路由写在 app.py 里,也不影响阅读,二次开发时再拆不迟。
2.3 一个分析请求从进入到返回的完整旅行
以「文本分析」请求为例。用户在 analyze.html 页面粘贴一段评论文本,点击提交,前端把文本 POST 到 /analyze。Flask 视图函数先取当前登录用户的 session 确认身份,然后取出文本,调用 preprocess 里封装的管线:去特殊字符、jieba 分词、去停用词、按 dict.pkl 转成索引序列、统一 padding 到固定长度。这串索引交给 predictor.predict(),模型跑一次前向传播得到两个类别的概率,取最大值对应正面或负面。
拿到结果后,视图函数把原文、预测标签、概率一起写入 MySQL 的 sentiment_record 表,同时把 JSON 返回给前端。前端展示正负面结果,并刷新 /stats 统计接口,后台首页就能看到最新占比。整条链路里最容易出问题的是预处理环节——词表里没有的词会被映射成 UNK 索引,如果训练时没预留 UNK 位,推理时就会直接越界报错;padding 长度如果和训练时不统一,模型输入维度也会对不上。这类问题的排查方法我放到第 5 章详细说。
3. 核心模块拆解:登录、推理、图表一个都别少
这个系统的三个核心模块,正好对应了项目简介里的几个界面:登录和注册是进入系统的闸门,文本分析承载深度学习推理,统计图表负责把分析结果讲清楚。下面逐个拆代码。
3.1 注册与登录:密码不能明文落库
先看注册接口的核心代码:
# service/user_service.py from flask import Blueprint, request, session, jsonify from werkzeug.security import generate_password_hash, check_password_hash from utils.db import get_db user_bp = Blueprint('user', __name__) @user_bp.route('/register', methods=['POST']) def register(): data = request.get_json() username = data.get('username', '').strip() password = data.get('password', '') phone = data.get('phone', '') if not username or not password or not phone: return jsonify({'code': 1, 'msg': '用户名、密码、手机号不能为空'}) # 查询用户名是否已存在 conn = get_db() cursor = conn.cursor() cursor.execute('SELECT id FROM user WHERE username=%s', (username,)) if cursor.fetchone(): return jsonify({'code': 1, 'msg': '用户名已存在'}) # 密码加盐哈希后再入库,绝不存明文 hashed = generate_password_hash(password) cursor.execute('INSERT INTO user (username, password, phone) VALUES (%s, %s, %s)', (username, hashed, phone)) conn.commit() return jsonify({'code': 0, 'msg': '注册成功'})逻辑不复杂:请求体是 JSON,先做空值检查;查重走 SELECT;写入前用 generate_password_hash 加盐哈希,数据库表里那一列存的是哈希串而不是原始密码。哈希函数每次生成的盐都不同,同一密码两次注册得到的哈希值也不一样,这是正常现象,校验密码时不要拿哈希去比哈希,而要专门用 check_password_hash。注册页面通常还有第二次密码确认输入框,那是前端交互;后端拿到的 password 已经是确认一致后的值。如果担心有人绕过页面直接调接口,后端也应该再校验 password 和 confirm_password 是否相等。
登录接口和注册是对称的:
@user_bp.route('/login', methods=['POST']) def login(): data = request.get_json() username = data.get('username', '').strip() password = data.get('password', '') conn = get_db() cursor = conn.cursor(pymysql.cursors.DictCursor) cursor.execute('SELECT id, username, password FROM user WHERE username=%s', (username,)) user = cursor.fetchone() if not user or not check_password_hash(user['password'], password): return jsonify({'code': 1, 'msg': '用户名或密码错误'}) session['user_id'] = user['id'] session['username'] = user['username'] return jsonify({'code': 0, 'msg': '登录成功'})登录时先按用户名把用户捞出来,账号不存在直接拒绝;存在则用 check_password_hash(哈希值, 明文密码) 比对。登录成功后把 user_id 和 username 写进 session,后面的文本分析页、后台首页都靠它判断登录状态,每条情感分析记录也带上 user_id 记录归属。这里有个很容易被忽略的点:Flask 的 session 依赖 secret_key,config.py 里没配的话 session 根本写不进去,表现为登录成功跳转后首页又立刻要求登录,排查时第一眼看配置文件。
3.2 文本预处理与模型推理:黑匣子里面在做什么
情感分析最核心的一段是预处理,直接决定模型能不能正确读取输入。代码大致是这样:
# utils/preprocess.py import re import jieba def load_stopwords(path='data/stopwords.txt'): with open(path, encoding='utf-8') as f: return set(line.strip() for line in f) def text_to_sequence(text, word2idx, max_len=128, stopwords=None): # 1. 清洗:只保留中文字符,数字、英文、标点替换成空格 text = re.sub(r'[^\u4e00-\u9fa5]', ' ', text) # 2. 分词 tokens = list(jieba.cut(text)) # 3. 过滤停用词并转成词表索引 seq = [] for token in tokens: if stopwords and token in stopwords: continue idx = word2idx.get(token, word2idx.get('<UNK>', 0)) seq.append(idx) # 4. 截断和 padding 到固定长度 if len(seq) > max_len: seq = seq[:max_len] else: seq = seq + [word2idx.get('<PAD>', 0)] * (max_len - len(seq)) return seq参数说明:word2idx 是 models/dict.pkl 里的词表映射,来自训练阶段;max_len 是训练时定死的序列长度,推理阶段必须保持一致,改了模型维度会报错;stopwords 从停用词表按行读取,常见停用词如“的”“了”“就”会被过滤;清洗时只保留中文字符,因为词表基本只覆盖中文词汇,混入的符号会全部落到 UNK 位白占长度。预处理输出是一个固定长度的整型数组,直接作为模型输入。
推理部分封装一个 SentimentPredictor 类:
# utils/predictor.py import pickle import torch import numpy as np class SentimentPredictor: def __init__(self, model_path, dict_path): with open(dict_path, 'rb') as f: self.word2idx = pickle.load(f) self.model = torch.load(model_path, map_location='cpu') self.model.eval() def predict(self, text=''): seq = text_to_sequence(text, self.word2idx) inputs = torch.tensor([seq], dtype=torch.long) with torch.no_grad(): logits = self.model(inputs) prob = torch.softmax(logits, dim=1).numpy()[0] label = int(np.argmax(prob)) return { 'label': '正面' if label == 1 else '负面', 'probability': float(max(prob)) }predict 方法接收原始文本,返回标签和概率。label 为 1 表示正面,0 表示负面,概率是 softmax 后的置信度。这种封装的好处是 Web 层不关心底层是 PyTorch 还是 TensorFlow,只要接口不变就能换实现。如果模型训练时保存的是 state_dict,加载则要先创建网络对象再 load_state_dict,两种保存方式别混用。整段代码里最容易翻车的是 max_len 和 dict.pkl 不一致,我拆过好几个类似项目,十有八九卡在这里,日志报维度不匹配时先检查这两个文件。
训练数据这块,常规做法是整理几千到几万条已标注的中文评论,正面负面各一半,按 8:2 比例切分训练集和验证集训练;验证集准确率到 0.85 以上,对短评论文本已经可用。训练脚本一般和 Web 工程分开,模型训练完输出 ckpt 和 dict.pkl,Flask 端只是消费它们,这正是「离线训练 + 在线推理」的意义。
3.3 统计图表接口:SQL 聚合之后还要管格式
后台首页要用柱状图和饼图展示统计结果,数据源是 sentiment_record 表。常规设计是暴露一个统计接口:
@analyze_bp.route('/stats') def stats(): user_id = session.get('user_id') if not user_id: return jsonify({'code': 1, 'msg': '未登录'}) pie = analyze_service.count_by_label(user_id) bar = analyze_service.count_by_date(user_id, days=7) return jsonify({'code': 0, 'data': {'pie': pie, 'bar': bar}})service 层两条 SQL 是这个模块的骨架:
-- 饼图:正面/负面各多少条 SELECT label, COUNT(*) AS cnt FROM sentiment_record WHERE user_id = %s GROUP BY label; -- 柱状图:最近 7 天每天的分析量 SELECT DATE(create_time) AS d, COUNT(*) AS cnt FROM sentiment_record WHERE user_id = %s AND create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time);前端拿到 JSON 后再转成 ECharts 需要的格式:饼图 data 是 [{name: '正面', value: n}, {name: '负面', value: m}],柱状图是 categories 加 values 两个数组。这里有个后端容易忽略的细节:PyMySQL 默认查出来的 COUNT(*) 是 Python int,但如果经过了 ORM 或数据类型转换,可能变成 Decimal 或字符串,ECharts 处理非 number 类型时图表会显示空白或异常。我一般习惯在接口返回前统一做一次 int() 和 float() 转换,把格式问题掐死在源头。
除了图表统计,分析记录列表一般也会做一个分页接口,展示最近 20 条:内容、标签、概率、分析时间。它和 /stats 共用一个 service,代码量不大,但对完整度很有帮助,演示时翻历史记录比只看两个图表更有说服力。
4. 数据库设计与本地运行:从建库到看见第一个图表
4.1 数据表设计与建库脚本
项目带 sql 目录里的 sentiment.sql,先看两张核心表的建表语句:
CREATE DATABASE sentiment_db DEFAULT CHARACTER SET utf8mb4; USE sentiment_db; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sentiment_record ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, content TEXT NOT NULL, label TINYINT NOT NULL COMMENT '1 正面 0 负面', probability FLOAT DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, create_time), CONSTRAINT fk_record_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;两个值得注意的设计决定。第一,字符集用 utf8mb4 而不是 utf8,因为评论内容可能包含 emoji,MySQL 的 utf8 是 3 字节编码,存 4 字节的 emoji 直接报错或被替换成问号,utf8mb4 才能完整存储;对应到连接层,PyMySQL 连接参数也要写 charset='utf8mb4',否则网页端中文照样乱码。第二,label 用 TINYINT 存 0/1,不直接存“正面”“负面”字符串,统计和索引都更高效,展示层由后端代码负责转成中文文字。联合索引 idx_user_time 建在 user_id 和 create_time 上,首页最近 7 天统计会走这个索引,数据量上来以后明显比全表扫描稳。
建库命令也很直接:
mysql -u root -p < sql/sentiment.sql如果数据库里已经存在 sentiment_db,脚本会报错,需要先 DROP DATABASE,或者把建库语句注释掉只保留 CREATE TABLE。
4.2 环境配置与依赖安装
运行环境 Python 3.8 以上,依赖不复杂。requirements.txt 大致包含:
flask>=2.0 pymysql>=1.0 jieba>=0.42 torch>=1.8 numpy安装一行命令:pip install -r requirements.txt。没有 GPU 也没关系,这种规模的文本分类模型 CPU 推理也就是毫秒级,torch 会自动装 CPU 版本。
config.py 里需要根据自己的环境改三四个值:
import os class Config: SECRET_KEY = 'your-secret-key-change-me' DB_HOST = '127.0.0.1' DB_PORT = 3306 DB_USER = 'root' DB_PASSWORD = '123456' DB_NAME = 'sentiment_db' MODEL_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'models') @classmethod def get_db_uri(cls): return (f"mysql+pymysql://{cls.DB_USER}:{cls.DB_PASSWORD}" f"@{cls.DB_HOST}:{cls.DB_PORT}/{cls.DB_NAME}" f"?charset=utf8mb4")DB_USER、DB_PASSWORD 改成自己本机的 MySQL 账号;SECRET_KEY 换一个长一点的随机串;MODEL_DIR 用 os.path 基于file拼绝对路径,避免从其他目录启动时相对路径失效。连接串末尾的 ?charset=utf8mb4 就是 4.1 说的连接层字符集配置,漏了中文会乱码。
启动服务:
python app.pyFlask 默认监听 5000 端口,浏览器打开 http://127.0.0.1:5000 看到登录页就说明服务起来了。如果端口被占用,在 app.py 的 app.run(port=5001) 换一个;如果 MySQL 连不上,Windows 下先检查 MySQL 服务有没有启动,再检查密码有没有写错。这两个属于环境问题,日志里都会直接给堆栈,照着错误往上查就行。
4.3 完整功能验证路径
跑通之后按这个顺序验证一遍,能覆盖系统三大模块。第一步注册:用户名、密码、手机号填全,密码确认一致,提交后提示注册成功;第二步登录:用刚注册的账号登录,进入后台首页,此时图表应该是空的或全为 0;第三步文本分析:输入“这个商品质量很好,物流也快”,预期返回正面;再输入“客服态度差,等了一周都没发货”,预期返回负面;第四步回到后台首页刷新,柱状图和饼图应该出现对应的统计;第五步查历史分析记录,刚才两条应该都在列表里。
这个顺序就是冒烟测试的常见做法,从用户模块到推理模块再到统计模块,每一环堵住了都能快速定位是哪一层的问题。压缩包里还带了一份演示视频,对照视频的操作流程走一遍,基本是 20 分钟能跑完的事。测试环节里如果发现某条中性评论被分到正面或负面,先不要怀疑模型坏了,短文本二分类模型对中性表达的判断本来就模糊,属于数据分布问题,换一批训练数据比调网络结构更有效,这也是我反复验证过的结论。
5. 常见问题与排查:五类让我反复翻车的坑
这个项目我前后拆过两遍,第一遍被中文乱码搞得怀疑人生,第二遍被模型重复加载直接吃满内存;这些坑报错都不直观,日志里的线索又散。我把最容易卡住人的问题整理成五条,每条按现象、原因、解决三步说清楚,基本覆盖新手跑这个系统八成会遇到的状况。
5.1 中文乱码:注册成功,页面和数据库全是问号
现象:注册成功后,后台首页用户名变成问号,MySQL 里查出来的中文也是问号。
原因:乱码是三段链路里至少一段不是 utf8mb4——建库建表字符集、数据库连接字符集、页面响应编码。最常见的漏项是 PyMySQL 连接时没传 charset 参数,默认用了 latin1。
解决:建库建表语句统一 utf8mb4;config.py 的数据库连接串加 ?charset=utf8mb4;Flask 返回页面时保证响应头是 UTF-8,模板文件本身也存成 UTF-8。按这个顺序排查,中文问题基本一次解决。
5.2 模型越跑越卡:内存被重复加载撑爆
现象:前几次分析正常,跑十几分钟后系统越来越卡,内存占用持续上涨,最后新请求直接没响应。
原因:推理对象在视图函数内部被反复实例化,每一次 predict 都会重新加载一次模型文件,内存里同时存在多份权重。有些写法在循环里调 model.eval(),也会触发不必要的图构建。
解决:把 SentimentPredictor 的实例化放到模块顶层,应用启动时只加载一次。类似写法:
# predictor 实例全局唯一 predictor = SentimentPredictor( model_path=os.path.join(Config.MODEL_DIR, 'ckpt/model.pt'), dict_path=os.path.join(Config.MODEL_DIR, 'dict.pkl') )视图函数里直接用 predictor.predict(),不要 new。如果你用的是 Flask 工厂模式,把实例挂到 app.extensions 里也是常见做法。
5.3 登录永远失败:哈希密码被二次哈希
现象:注册成功,登录时输入完全相同的密码,后端一直提示用户名或密码错误。
原因:登录接口里把前端传进来的密码也跑了一遍 generate_password_hash,再把新哈希值和库里的哈希值比较。哈希函数每次盐不同,两次结果必然不一致,所以永远比对不上。
解决:注册用 generate_password_hash,登录用 check_password_hash(库里的哈希, 前端明文密码),不要自己实现 md5+盐的逻辑。如果你在日志里看到 password 字段长度变成 60 左右的随机串还每次不同,基本就是这个原因。
5.4 首页图表空白:接口有数据但前端不渲染
现象:文本分析能返回结果,日志也正常,但后台首页柱状图和饼图一直是空白,控制台没有任何报错。
原因:ECharts 渲染失败通常不是图表库的问题,而是数据格式不对——SQL 别名和前端 JS 取数的字段不一致,或者数值类型不是 number。
解决:浏览器开发者工具 Network 面板直接看 /stats 的响应体,确认字段名和前端 setOption 里用的是不是同一个;后端在返回前把数值统一 int() 转换。前端初始化时 categories 给空数组而不是 undefined,也能避免白屏。这类问题是最典型的“看起来是玄学,实际是类型不匹配”。
5.5 模型文件找不到:不同目录启动就报错
现象:在项目根目录启动一切正常,换成 python /path/to/project/app.py 或者先 cd 到 static 目录再启动,立刻报 No such file or directory 找不到模型权重。
原因:代码里用了相对路径 'models/ckpt/model.pt',相对路径的参照是当前工作目录而不是项目根目录,cd 到哪就相对哪里找,自然找不到。
解决:像 4.2 那样在 config.py 里用 os.path.dirname(os.path.abspath(file)) 拼出绝对路径,模型、词表、模板、静态资源全部基于这个项目根目录拼接,之后无论从哪里启动都不会出事。这是我跑任何 Python 工程的第一件事,先把所有路径改成绝对路径。
以上这五条都踩过之后,项目基本就能稳定运行了。第一遍跑通和稳定运行是两回事,稳定运行通常要经历一次“配置做对 + 资源生命周期做对 + 数据格式做对”的完整循环。
6. 往二次开发走:把二分类改成三分类,模型换成自己的数据
6.1 让模型学习自己的数据
项目自带的模型是通用中文评论训练出来的,落地到电商、社交媒体或某个垂直领域时,最好用自己的标注数据重新训练。数据格式整理成文本和标签两列,按 8:2 切分,训练脚本的词汇表重新生成 dict.pkl,标签映射保持 0 负面、1 正面不变。重训完后,把新的模型权重和 dict.pkl 替换到 models 目录就行。这里要特别小心 max_len:训练时如果改成 64,推理端 text_to_sequence 里的 max_len 也要同步改 64,否则模型输入维度对不上,报错位置会在第一层 Embedding。这是我在自己改造一个垂直电商评论项目时踩过的坑,换了两版词表才反应过来是长度参数不一致。
6.2 从二分类扩成三分类
把正面/负面二分类改成负面/中性/正面三分类,是性价比很高的一次改造。需要动四个地方:数据标注阶段加入中性样本;网络最后一层输出节点从 2 改成 3;predict 函数里 label 映射改成 0 负面、1 中性、2 正面;前端饼图增加“中性”数据项。统计接口的 GROUP BY label 不用改,新标签值自动出现在结果里。这样改造之后,演示效果比二分类更像真实产品,中性评论不会再被强行押到某一端。
这套项目整体上是个标准的「业务系统 + 模型推理」模板,数据库表、登录态、统计模块都齐全,核心算法被隔离在 predictor 里,替换成本很低。我刚开始拆它的时候,也犯过把密码哈希了两遍的错,也眼看着内存被重复加载的模型吃满;从那以后每次跑 Flask + 深度学习的项目,我都强制先走一遍:数据库字符集、模型加载位置、密码校验方式、前端图表字段。希望帮到你。
本文还有配套的精品资源,点击获取