简介:这是一份基于Python+Flask深度学习的中文情感分析系统毕业设计文档,适合计算机或人工智能相关专业学生用于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", errors="ignore").decode("utf-8", errors="ignore") # 2. 反转义 HTML 实体(& < 等) raw = html.unescape(raw) # 3. 去掉 HTML 标签 raw = re.sub(r"<[^>]+>", "", raw) # 4. 去掉 URL raw = re.sub(r"https?://\S+|www\.\S+", "", raw) # 5. 合并重复标点,保留一个 raw = re.sub(r"([。!?!?]{2,})", r"\1", raw) # 6. 压缩多余空白 raw = re.sub(r"\s+", " ", raw).strip() return raw这段代码有几个地方值得说明。第 1 步的 errors="ignore" 是为了让程序在遇到坏字节时继续跑而不是直接抛异常,但这只是兜底,根源还是读文件时指定正确的 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_size=0.3, stratify=labels, # 按类别比例分层抽样 random_state=42 # 固定随机种子,保证可复现 ) X_val, X_test, y_val, y_test = train_test_split( X_temp, y_temp, test_size=0.5, # 0.3 * 0.5 = 0.15 的测试集 stratify=y_temp, random_state=42 )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 起步,推理速度也比较慢。在“某公开电商评论语料”这种数据集上,从传统方法到 BERT,F1 的提升通常在 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_len=128): 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], truncation=True, padding="max_length", max_length=self.max_len, return_tensors="pt" ) return { "input_ids": encoded["input_ids"].squeeze(0), "attention_mask": encoded["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[idx], dtype=torch.long) } model_name = "some-chinese-bert-base" # 替换成你实际使用的预训练模型 tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=3 # 0负面 1中性 2正面 ) train_dataset = SentimentDataset( X_train, y_train, tokenizer, max_len=128 ) val_dataset = SentimentDataset( X_val, y_val, tokenizer, max_len=128 ) training_args = TrainingArguments( output_dir="./checkpoints", num_train_epochs=5, per_device_train_batch_size=32, per_device_eval_batch_size=64, learning_rate=2e-5, warmup_ratio=0.1, evaluation_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="eval_f1", # 用 F1 而不是准确率选模型 logging_dir="./logs" ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=val_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-5,batch size 64 配 3e-5。如果你的 GPU 显存只有 6G,把 max_len 降到 128,batch 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(silent=True) 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, truncation=True, padding="max_length", max_length=128, return_tensors="pt" ).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(host="0.0.0.0", port=5000, debug=False)get_json(silent=True) 表示解析失败时不抛异常而是返回 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_tensors="pt", truncation=True, max_length=128) 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, key=lambda i: precision[i])]一句话总结:与其纠结模型结构,不如检查你的决策阈值是否匹配业务代价。这一步在很多系统里能带来 2 到 3 个点的有效提升,而成本几乎为零。
6.2 规则兜底与模型融合的常见做法
深度学习模型不是万能的,某些文本它天生处理不好:超短文本(就两个字“垃圾”)、反讽(“真棒,又丢件了”)、混合情感(“物流快但包装差”)。我的习惯是在模型外面包一层规则兜底,把模型明显处理不了的样本先拦截。常见规则包括:命中敏感词表直接判定为负面;文本长度小于 N 个字且包含明确情感词时走规则分类;模型概率接近决策边界时返回“不确定”而非强分类。这个兜底层的代码简单,但能把线上系统的“离谱错误”大幅减少。
6.3 模型服务上线后的验证与迭代习惯
部署上线不等于结束,我每次都会给服务加上一个简单的日志模块,记录每次请求的原文、预测结果、置信度,然后定期抽样做人工复核。这个方法能持续暴露模型的边界问题。另外一个习惯是:每次收集到足够多的“预测错误但人工标注有明确结论”的样本后,把它们加入训练集做增量训练。增量训练时要注意混合新旧数据,只拿新数据微调会让模型遗忘旧知识。整体迭代节奏我一般是一个月一次,每次增加几百条高质量样本,比一次性堆几千条低质量数据有效得多。这套“阈值校准 + 规则兜底 + 日志复盘 + 增量训练”的循环,比换更大的模型更值得投入。希望这些经验能帮到你,少走一段我走过的弯路。
本文还有配套的精品资源,点击获取