基于Flask与BERT的中文情感分析系统构建与部署实战 简介这是一份基于PythonFlask深度学习的中文情感分析系统毕业设计文档适合计算机或人工智能相关专业学生用于NLP方向毕业设计、课程设计或论文撰写的参考资料。文档以标准论文结构展开依次介绍研究背景与意义、技术及工具B/S架构、MySQL、Python、深度学习算法、系统可行性分析与需求分析、系统功能与数据库设计、系统实现登录界面、分析模块、后台首页以及测试环节。其中重点说明了如何利用CNN与RNN深度学习算法自动提取文本特征并完成中文情感倾向识别同时结合Flask框架搭建前后端交互的Web系统有助于解决中文情感分析中数据预处理和特征提取的复杂性。资源为单个docx文件压缩包大小1.22MB文档目录完整、可直接编辑。目前已有383人学习浏览适合需要快速掌握系统整体设计思路、算法选型依据以及毕业设计论文写作规范的学习者。1. 中文情感分析系统到底在做什么一个 Flask 前后端都能直接用的深度学习方案聊到“中文情感分析”很多人的第一反应是调用某个现成 API传一段文本回来一个分数。但真落到业务里——比如给电商评论做舆情监控、给客服对话打标、给社区发帖做风险提示——你会发现通用 API 的边界、成本、数据隐私都是绕不开的问题于是“基于 Python Flask 自己搭一套深度学习情感分析系统”就成了最务实的路线。这个标题完整描述了一个可落地的技术方案前端/接口层用 Flask 承载 HTTP 服务后端用深度学习模型对中文文本做情感分类正面/负面/中性再加上数据清洗、模型训练、服务部署三个环节。本文按这条链路把每一步拆开给你一套可以直接复现的工程做法包括参数、代码和那些不踩一次就发现不了的坑。2. 从文本到训练集中文语料清洗、标注与划分2.1 中文语料从哪来先定业务场景再选数据集做情感分析的第一步不是选模型而是确定你要分析什么场景下的文本。同一个“不错”在商品评论里是好评在事故描述里则可能是反讽同一个“牛”在股市论坛和餐厅点评里含义完全不同。所以我在动手前会先回答三个问题文本来自哪个渠道电商评论、微博、新闻评论、客服会话、情感维度是几分类二分类正/负还是三分类加中性、类别分布大概是什么样。语料来源常见有两种。其一是直接使用公开的中文评论语料这类数据量大、标注质量相对稳定适合先跑通流程、验证模型选型其二是自己从业务系统里抽样标注这是上线前的必经步骤因为公开语料和你的真实数据几乎一定存在领域偏差。我一般会先用公开语料把整个 pipeline 跑通再花一到两周时间让业务方标注几百到一千条真实样本用这些样本做模型的领域微调或至少做验证集。请注意情感分析任务里标注质量远比数据量重要一百条标注一致的数据训练效果可能好过一千条标注混乱的数据。关于数据格式最省事的是 CSV 或 JSONL每行一条样本至少包含 text 和 label 两个字段。label 建议直接用整数0 负面、1 中性、2 正面不要用中文标签存原始文件否则后面做类别映射时会多一层出错的可能。文件编码统一存成 UTF-8这是后面所有环节的地基。2.2 清洗流程编码、乱码、HTML、标点与 emoji中文文本清洗比英文多几个特有的麻烦。第一个是编码CSV 文件经常以 GBK 或 GB18030 存储读进来就乱码。第二个是夹杂的 HTML 标签和转义字符爬下来的评论里全是这类东西。第三个是 emoji 和颜文字它们有时携带情感信息比如“”基本是负面或嘲讽有时只是噪声。我一般会把清洗函数拆成独立模块而不是揉在训练脚本里这样后续做接口服务时可以直接复用。下面是一个适合多数场景的清洗函数按顺序处理编码、HTML、URL、重复标点和空白import re import html def clean_text(raw: str) - str: if not raw: return # 1. 统一转成 str处理编码残留 raw raw.encode(utf-8, errorsignore).decode(utf-8, errorsignore) # 2. 反转义 HTML 实体amp; lt; 等 raw html.unescape(raw) # 3. 去掉 HTML 标签 raw re.sub(r[^], , raw) # 4. 去掉 URL raw re.sub(rhttps?://\S|www\.\S, , raw) # 5. 合并重复标点保留一个 raw re.sub(r([。!?]{2,}), r\1, raw) # 6. 压缩多余空白 raw re.sub(r\s, , raw).strip() return raw这段代码有几个地方值得说明。第 1 步的 errorsignore 是为了让程序在遇到坏字节时继续跑而不是直接抛异常但这只是兜底根源还是读文件时指定正确的 encoding。第 4 步去 URL 是因为链接本身没有情感信息但在某些场景下域名可能是有效特征比如竞品名出现在链接里如果你需要保留可以去掉这条规则。第 5 步合并重复标点是为了防止“”这种极端输入把文本长度撑爆。要注意清洗的尺度取决于模型。如果你后面用的是 BERT 这类预训练模型分词的粒度由 tokenizer 决定清洗做得太狠反而会丢掉信息如果你用的是传统机器学习模型比如朴素贝叶斯、逻辑回归保留句尾标点作为特征是有意义的。我的习惯是通用清洗做一轮后面根据模型再决定要不要额外保留 emoji 或颜文字。2.3 数据划分与类别不平衡为什么先看分布再训练数据清洗完之后先不要急着训练。第一个要看的是类别分布。中文情感语料有一个很常见的现象负面样本比正面样本多用户更愿意吐槽中性样本往往最少。如果直接拿原始分布训练模型会倾向于预测多数类表现就是“准确率看着还行但召回率一塌糊涂”——准确率可能被多数类拉高而少数类的预测基本失效。划分数据时要注意两个坑。一是不能手动随机切要用带随机种子的划分函数保证每次实验的可复现性二是同一句话的重复变体比如完全相同的评论出现多次要先去重再划分否则训练集和验证集之间会发生数据泄漏验证指标会虚高。下面是我惯用的划分方式from sklearn.model_selection import train_test_split # texts: list[str], labels: list[int] X_train, X_temp, y_train, y_temp train_test_split( texts, labels, test_size0.3, stratifylabels, # 按类别比例分层抽样 random_state42 # 固定随机种子保证可复现 ) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, # 0.3 * 0.5 0.15 的测试集 stratifyy_temp, random_state42 )stratify 参数是最容易忽略的。不做分层抽样的话在类别不平衡时很可能出现验证集里没有某个类别的情况训练过程会直接报错或者指标失真。random_state 固定成同一个值方便你和同事对比实验结果。测试集和验证集分开的目的是为了在调参阶段不碰测试集等所有决策都定了再用测试集做最终评估。中间如果因为分布不均需要做类别加权或者过采样都是在训练集上做千万不要动验证集和测试集。还有一个常被忽略的点长文本和短文本的分布。BERT 类模型有最大长度限制如果语料里有大量超过 512 token 的文本截断会损失信息。建议在清洗后统计一下文本长度分布如果 95% 的样本在 100 字以内把 max_length 设成 128 就够用了训练速度能快不少准确率也未必下降。3. 模型选型与训练从传统基线到 BERT 微调的完整路径3.1 三个候选方案的复杂度与效果边界模型选型没有银弹但有几个可以量化的参考维度训练时间、推理速度、效果上限、硬件门槛。以中文情感分析为例常见候选有三个梯度。第一梯度是传统机器学习TF-IDF 逻辑回归/朴素贝叶斯训练只需几分钟CPU 就能跑推理在毫秒级但效果上限有限。对于词汇表重合度高、表达方式比较固定的场景比如“物流快”“质量好”“客服差”这种直白评论它能到 85% 左右的准确率而且可解释性强出问题容易排查。第二梯度是经典深度学习模型TextCNN、BiLSTM效果比传统方法好一截尤其是在捕捉局部词序特征上训练在 CPU 上也能忍受。TextCNN 的优点是结构简单、训练快对短文本情感分析非常有效缺点是难以捕捉长距离依赖。第三梯度是预训练语言模型微调BERT 及其中文变体效果上限最高对语义复杂、反讽、上下文依赖强的文本有明显优势但需要 GPU显存至少 6G 起步推理速度也比较慢。在“某公开电商评论语料”这种数据集上从传统方法到 BERTF1 的提升通常在 3 到 6 个百分点之间。我的建议很直接如果你的目标是跑通系统、验证业务价值先用逻辑回归做基线把整个 Flask 服务和数据闭环跑通如果业务对准确率有硬指标再上 BERT 微调。不要一上来就追求最强模型因为 BERT 的部署复杂度会拖慢你验证业务的速度。基线模型还有一个作用它给出的错误样本能帮你发现数据标注问题这些样本的价值比模型本身更值钱。3.2 微调中文 BERT 分类器的完整训练脚本选定 BERT 路线后最稳的做法是用 transformers 库加载一个中文预训练模型在它上面加一个分类头然后做微调。不要从零训练一个 BERT那需要海量数据和超算资源绝大多数业务场景用不到。下面是一个可直接运行的训练脚本骨架我把关键步骤都写在注释里。这个脚本以 transformers 的 Trainer 为主不手工写训练循环省去很多细节管理import torch from torch.utils.data import Dataset from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments ) class SentimentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len128): self.texts texts self.labels labels self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): encoded self.tokenizer( self.texts[idx], truncationTrue, paddingmax_length, max_lengthself.max_len, return_tensorspt ) return { input_ids: encoded[input_ids].squeeze(0), attention_mask: encoded[attention_mask].squeeze(0), labels: torch.tensor(self.labels[idx], dtypetorch.long) } model_name some-chinese-bert-base # 替换成你实际使用的预训练模型 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained( model_name, num_labels3 # 0负面 1中性 2正面 ) train_dataset SentimentDataset( X_train, y_train, tokenizer, max_len128 ) val_dataset SentimentDataset( X_val, y_val, tokenizer, max_len128 ) training_args TrainingArguments( output_dir./checkpoints, num_train_epochs5, per_device_train_batch_size32, per_device_eval_batch_size64, learning_rate2e-5, warmup_ratio0.1, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modeleval_f1, # 用 F1 而不是准确率选模型 logging_dir./logs ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset ) trainer.train() trainer.save_model(./models/sentiment_bert) tokenizer.save_pretrained(./models/sentiment_bert)这里有几个关键参数要单独解释。num_labels 必须等于类别数如果你的业务是二分类就改成 2。max_len 我设的是 128因为电商评论普遍短如果你的文本平均长度超过 200 字这个值要往上调但注意显存占用会相应增加。learning_rate 用 2e-5 是一个比较稳的起点BERT 微调的学习率如果大于 5e-5很容易出现训练损失下降但验证指标震荡的情况。evaluation_strategy 和 save_strategy 都设成 epoch并打开 load_best_model_at_end这样训练结束后会自动加载验证集上最好的 checkpoint而不是最后一次 epoch 的模型——这个小细节能帮你多拿 1 到 2 个点的指标。训练过程中的监控点看训练损失是否平稳下降验证集 F1 是否在上升。如果验证集指标在某个 epoch 后开始下降而训练损失还在降说明模型开始过拟合把 num_train_epochs 调小或者找一个已经做完训练的好 checkpoint 直接加载。3.3 训练参数怎么定学习率、batch 大小、早停与随机种子参数调优看起来玄学其实核心就四个旋钮学习率、batch size、epoch 数、随机种子。学习率是影响最大的一个。BERT 微调推荐范围在 1e-5 到 5e-5 之间我通常从 2e-5 开始如果验证集指标不涨降到 1e-5 再试如果训练损失降得很慢微调到 3e-5。学习率过高会导致模型在预训练权重附近震荡表现就是训练损失上下跳验证指标一直上不去。batch size 受显存限制但要注意它和学利率的配合。一般来说batch size 越大梯度越稳定可以适当调大学习率batch size 小梯度噪声大学习率要调小。我常用的一组组合是 batch size 32 配学习率 2e-5batch size 64 配 3e-5。如果你的 GPU 显存只有 6G把 max_len 降到 128batch size 降到 8 或 16 也能跑代价是训练时间变长。早停我一般手动控制把 save_strategy 和 evaluation_strategy 都设成 epoch如果连续两个 epoch 验证集 F1 没有提升就停止训练。Trainer 的 load_best_model_at_end 配合 metric_for_best_model 已经实现了自动选择最优 checkpoint但不会自动停所以实际训练时我会盯日志或者把 epoch 数设成 8 到 10让模型在过拟合之后还能有机会恢复到最优状态。随机种子是复现实验的保障。在训练脚本开头设置import random import numpy as np import torch def set_seed(seed: int 42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)注意即使固定了种子GPU 上的某些操作仍然存在非确定性比如某些 CUDA 运算所以每次训练结果会有微小波动这属于正常现象不必追求完全一致。只要波动范围在 0.5 个点以内说明训练流程是稳定的。4. 用 Flask 把模型包成 Web 服务接口设计与并发兜底4.1 项目结构与全局模型加载模型训练完成后下一步是把它变成别人能调用的服务。Flask 在这里扮演的是极简 Web 框架的角色接收 HTTP 请求、调用模型推理、返回 JSON 结果。它的好处是没有繁重的配置一个文件就能启动服务非常适合内部系统或中小流量的业务场景。项目结构我习惯按下面的方式组织sentiment_system/ ├── app.py # Flask 入口路由和视图函数 ├── model_loader.py # 模型加载与推理封装 ├── preprocess.py # 文本清洗函数 ├── models/ │ └── sentiment_bert/ # 训练好的模型和 tokenizer ├── requirements.txt └── templates/ └── index.html # 简单的可视化页面可选最关键的实践是模型必须在 Flask 启动时加载一次放到全局变量里而不是在每个请求里重复加载。以 BERT 为例加载模型和 tokenizer 需要几秒到十几秒如果每个请求都加载一次服务基本不可用。正确做法是在模块导入时加载# model_loader.py from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch _tokenizer None _model None _device torch.device(cuda if torch.cuda.is_available() else cpu) def get_model(): global _tokenizer, _model if _model is None: _tokenizer AutoTokenizer.from_pretrained(./models/sentiment_bert) _model AutoModelForSequenceClassification.from_pretrained( ./models/sentiment_bert ).to(_device) _model.eval() # 切换到推理模式 return _tokenizer, _model_model.eval() 这行容易漏。不调用的话模型会保持训练模式BatchNorm 和 Dropout 的行为和推理时不一致输出结果会有随机性表现就是同一个输入多次请求结果不一样。这是非常隐蔽的坑我在第一次部署时就翻过车。推理阶段不需要计算梯度所以要包在 torch.no_grad() 里面能省显存和内存。这块放到下一节一起讲。4.2 POST /predict 接口的最小实现Flask 接口的设计遵循以下几个原则接收 JSON 而不是表单、输入输出格式固定、错误信息可读、推理超时可控。下面是最小可用的接口实现# app.py from flask import Flask, request, jsonify import torch from model_loader import get_model from preprocess import clean_text app Flask(__name__) LABELS [负面, 中性, 正面] app.route(/predict, methods[POST]) def predict(): data request.get_json(silentTrue) if not data or text not in data: return jsonify({error: missing text field}), 400 text clean_text(data[text]) if not text: return jsonify({error: empty text after cleaning}), 400 tokenizer, model get_model() inputs tokenizer( text, truncationTrue, paddingmax_length, max_length128, return_tensorspt ).to(_device) with torch.no_grad(): outputs model(**inputs) probs torch.softmax(outputs.logits, dim-1).squeeze(0) pred_idx torch.argmax(probs).item() confidence probs[pred_idx].item() return jsonify({ label: LABELS[pred_idx], label_id: pred_idx, confidence: round(confidence, 4), probabilities: { label: round(p, 4) for label, p in zip(LABELS, probs.tolist()) } }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)get_json(silentTrue) 表示解析失败时不抛异常而是返回 None这样接口不会因为非法 JSON 直接 500。返回结果里我同时给了 label、label_id、confidence 和一个完整概率分布前端可以根据概率分布做更灵活的展示。LABELS 列表的顺序必须和训练时 num_labels 的顺序一致否则结果会错位。另一个细节是文本清洗发生在接口内部。客户端传来的文本不可信可能在清洗前就包含大量 HTML 标签、空字符串、超出模型长度限制的内容所以在调用 tokenizer 之前必须先过 clean_text。如果清洗后为空直接返回 400 而不是把空字符串丢给模型——BERT 处理空输入会给出一个无意义的概率分布看起来像“中性”实际上是无效请求。4.3 同步阻塞与并发第一个翻车点Flask 自带的开发服务器是单进程多线程的每个请求独立线程处理看起来能并发但如果模型推理是 CPU 密集型的多线程操作GIL 会让线程之间互相等待接口的响应时间在并发时会明显劣化。这是大部分从“训练脚本”到“Flask 服务”的人第一次遇到的坑。解决思路有两个方向。第一个是如果模型推理不重、业务并发不高比如内部工具每秒几个请求直接保持现状靠 Flask 的多线程 模型小 batch 就够了。第二个是用 gunicorn 等多进程部署每个 worker 进程加载一份模型副本进程间并行处理请求。下面是 gunicorn 的启动命令gunicorn -w 4 -b 0.0.0.0:5000 app:app-w 4 表示 4 个 worker 进程每个进程独立加载模型内存占用会成倍增长所以 worker 数要结合机器内存来定。GPU 场景下多个 worker 共享显存时要注意显存容量上限如果单卡显存不够就减 worker 数或者改用 GPU 推理加 CPU 分流。还要注意一个并发场景下的安全隐患如果模型推理中有任何状态变量比如缓存、计数器多线程下可能出错。用函数式写法不修改全局状态是避免这类问题的基本原则。在 Flask 服务里我还会加一个超时控制防止推理时间过长把请求线程全部占用。可以用 Python 的 functools lru_cache 对高频文本做结果缓存短时间内的重复请求直接命中缓存减少模型推理压力。5. 避坑指南中文情感分析最容易翻车的 5 个细节5.1 现象模型预测全落在“中立”上刚训练完的模型跑在真实数据上结果几乎是清一色的“中性”。原因通常是训练数据里中性样本占比过高比如超过 60%模型学习到“全预测中性”就能拿到不错的准确率。解决方法是重新审视类别分布。如果业务上中性类别不重要干脆用二分类只保留正负样本如果必须保留中性就在训练时给少数类更高的类别权重。实现上最简单的是在 Trainer 的 compute_loss 里传入 class weights或者用过采样/欠采样把三个类别拉平。之后重新训练重点看 F1 而不是准确率。5.2 现象第一次请求耗时十几秒之后才恢复BERT 模型加载几十秒后第一次请求又花了十几秒之后单次请求只要几百毫秒。原因有两个一是模型第一次推理时才真正初始化 CUDA 上下文和 cuDNN 算法这发生在第一个请求内部二是 tokenizer 第一次加载词典也要时间。解决方法是预热。服务启动后立即做一次空推理def warmup(): tokenizer, model get_model() test_text 这家店的商品质量很好发货速度快 inputs tokenizer(test_text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): model(**inputs)在 app.py 里调用 app.run() 之前执行 warmup()让模型提前完成初始化。很多部署踩坑文章没提这一步但实际运维中它就是“为什么接口偶发性超时”的元凶之一。5.3 现象GPU 显存占用异常跑两三个请求就 OOM用 GPU 推理时显存占用应该平稳但如果每个请求都创建了新的输入张量且没有显式释放或者 torch 的缓存机制导致累计占用上升就可能 OOM。常见原因有两个一是把最大序列长度设得过大二是没有控制并发推理的 batch。解决方法是固定 max_length并且用 torch.no_grad() 包裹推理同时限制 Flask 接口的并发量。更稳妥的做法是在模型加载后调用 torch.cuda.empty_cache() 释放预留给训练的缓存。还要注意多 worker 场景下每个进程都会复制一份模型到显存4 个 worker 就是 4 份显存不够时宁愿用 1 个 worker 配 GPU。5.4 现象训练集 F1 很高线上表现一塌糊涂这是最典型的过拟合和数据泄漏组合拳。训练集和验证集之间可能混入了重复文本或者业务文本和训练语料的领域差距太大。先说前者如果没有做去重模型可能在训练阶段“背”下了验证集样本F1 虚高。解决方法是重新跑一遍划分先对整个数据集做去重再分层抽样。再说后者训练语料是电商评论线上接入的是客服会话句式差别很大需要做领域微调。做法是收集几百条真实业务数据在已训练模型上继续微调几个 epoch学习率降到 1e-5 以下防止破坏原有语义。5.5 现象繁体、emoji、颜文字把预处理流程打崩繁体字在中文预训练模型里会被切成大量 unknown token导致推理结果失真。emoji 如果清洗掉可能丢失情感信号如果保留tokenizer 可能无法处理。我的做法是分场景如果业务文本主要是简体中文在清洗阶段把繁体转简体常见做法是用 opencc 库转换emoji 则按需决定若要保留建议在清洗函数里把 emoji 映射成特殊 token比如 “[HAPPY]”再喂给模型或者直接保留原始 emoji 让 tokenizer 尝试处理。颜文字T_T、QAQ同理它们对情感判断是有帮助的强行删除会损失信息。6. 把准确率再往上推一截阈值调整与规则兜底6.1 用验证集画出 PR 曲线重新定阈值模型输出的 softmax 概率默认取最大值对应的类别作为预测结果这等于把阈值固定在“概率最大的就是答案”。但三分类情感分析里这个默认决策边界不是最优的。比如业务上“负面误判为中性”的代价远高于“中性误判为负面”那就需要让“负面”类别的触发条件更严格或更宽松。做法是从验证集上收集每个样本的预测概率针对目标类别画 PR 曲线找到满足业务指标的阈值。代码骨架如下from sklearn.metrics import precision_recall_curve # 假设是二分类0 负面1 非负面 # probs_neg 是验证集每个样本属于“负面”的概率 precision, recall, thresholds precision_recall_curve(y_val_binary, probs_neg) # 目标是召回率不低于 0.9找此时最高精度对应的阈值 valid_idx [i for i, r in enumerate(recall) if r 0.9] best_t thresholds[max(valid_idx, keylambda i: precision[i])]一句话总结与其纠结模型结构不如检查你的决策阈值是否匹配业务代价。这一步在很多系统里能带来 2 到 3 个点的有效提升而成本几乎为零。6.2 规则兜底与模型融合的常见做法深度学习模型不是万能的某些文本它天生处理不好超短文本就两个字“垃圾”、反讽“真棒又丢件了”、混合情感“物流快但包装差”。我的习惯是在模型外面包一层规则兜底把模型明显处理不了的样本先拦截。常见规则包括命中敏感词表直接判定为负面文本长度小于 N 个字且包含明确情感词时走规则分类模型概率接近决策边界时返回“不确定”而非强分类。这个兜底层的代码简单但能把线上系统的“离谱错误”大幅减少。6.3 模型服务上线后的验证与迭代习惯部署上线不等于结束我每次都会给服务加上一个简单的日志模块记录每次请求的原文、预测结果、置信度然后定期抽样做人工复核。这个方法能持续暴露模型的边界问题。另外一个习惯是每次收集到足够多的“预测错误但人工标注有明确结论”的样本后把它们加入训练集做增量训练。增量训练时要注意混合新旧数据只拿新数据微调会让模型遗忘旧知识。整体迭代节奏我一般是一个月一次每次增加几百条高质量样本比一次性堆几千条低质量数据有效得多。这套“阈值校准 规则兜底 日志复盘 增量训练”的循环比换更大的模型更值得投入。希望这些经验能帮到你少走一段我走过的弯路。本文还有配套的精品资源点击获取