
简介基于Python与PyTorch构建的购物平台评价情感分析系统围绕Bert与CNN、RNN、RCNN等模型的融合思路展开面向计算机相关专业学生、NLP初学者或开发者可直接用于课程设计、毕业设计或初期项目演示。压缩包共55个文件以16个Python源文件为主体包含模型定义、训练与工具模块另有37个pyc编译缓存、1个md说明文档和1个zip辅助包整体仅503KB结构紧凑。工程内提供完整的训练入口、数据处理与模型调用脚本预训练模型相关组件封装完善分类模型部分涵盖Bert、CNN、RNN、RCNN、DPCNN、ERNIE等多种结构可方便地对比不同算法在购物评价情感分析上的表现。已有265人学习下载代码模块划分清晰说明文档可帮助理解整体流程在此基础上可调整数据、模型结构或训练参数快速搭建自己的情感分析示例也适合作为毕设或大作业的二次开发起点。1. 从一条差评到自动退款购物平台评价情感分析到底在解决什么假设你是电商平台负责售后策略的工程师每天面对几十万条评价文本。用户写物流慢得离谱但是包装很好——这句里既有负向也有正向传统关键词过滤直接判成中评但真实情绪是不满。用 Python 基于 Bert CNN / RNN / RCNN 做购物平台评价情感分析就是把这类短文本拆成情感倾向 情感强度让系统能在差评爆发前自动识别、按严重程度分级甚至触发退款建议。这个方案适合刚拿到模型源码想落地的人也适合想从单一 Bert 升级到多模型对比的团队。它能直接回答哪个模型在你的数据上最稳、每个模型的坑在哪、如何把训练好的模型变成接口。下文不绕原理直接按选型 → 复现 → 融合 → 排查推进。2. 为什么是 Bert CNN / RNN / RCNN把三个模型的脾气先摸清在动手跑源码之前先花十分钟把三个模型的输入输出和擅长的东西对齐。很多人在调参阶段浪费大量时间是因为根本没搞清模型之间是在比什么。这里的核心不是哪个模型更先进而是购物平台评价这种短文本到底需要模型注意到什么。一条评价往往十几到几十个字却有转折、反讽、程度副词和领域黑话单个模型很难同时吃下上下文和局部关键信息所以才有了组合方案。2.1 Bert 负责读得懂CNN 负责抓重点两套特征的交汇点Bert 的本质是通过 Transformer 的双向编码让每个 token 的表示都看到整句话。放到购物场景里物流慢但包装很好这句话Bert 在编码慢的时候会带上后半句的转折关系所以它天然适合处理虽然但是这类结构。但它不是没有代价预训练模型参数量大在标注数据只有几千条时全量微调很容易过拟合而且它输出的序列向量是等权重的哪些词对最终情感起决定作用Bert 不会主动强调。CNN 在这里补的正是这个短板。一维卷积按窗口滑动相当于在词向量序列上做 n-gram 特征提取。比如 kernel_size3就是在连续三个词组成的局部窗口里找模式很好太差这类短语一下就被卷积核抓出来了。和 Bert 做在同一层的好处是Bert 已经用上下文语义把词向量重写过一遍CNN 再从这些语义向量里挑局部强特征等于先读懂再抓重点而不是在原始词向量上硬扫。下面是最小对比代码# 同一句话分别过 Bert 和一层 Conv1d观察输出形状 import torch from transformers import BertTokenizer, BertModel tokenizer BertTokenizer.from_pretrained(bert-base-chinese) bert BertModel.from_pretrained(bert-base-chinese) inputs tokenizer(物流慢但包装很好, return_tensorspt) with torch.no_grad(): bert_out bert(**inputs).last_hidden_state # [1, seq_len, 768] # 把维度调成一维卷积需要的 [batch, channel, length] bert_t bert_out.transpose(1, 2) # [1, 768, seq_len] conv1d torch.nn.Conv1d(in_channels768, out_channels256, kernel_size3, padding1) cnn_out torch.nn.functional.relu(conv1d(bert_t)) print(Bert 输出形状:, tuple(bert_out.shape)) print(CNN 输出形状:, tuple(cnn_out.shape))这段代码的核心是形状变换。Bert 返回的 last_hidden_state 形状是 [batch, seq_len, hidden_size]而 PyTorch 的 Conv1d 期望输入是 [batch, channels, length]所以必须用 transpose(1, 2) 把 hidden_size 当成通道维度seq_len 当成长度维度。kernel_size 设成 3 表示看三个连续 token 的窗口padding1 是为了让卷积后长度不变。out_channels 是超参数256 是常见取值数据量小可以降到 128。2.2 RNN 处理序列、RCNN 做局部上下文模型选型的底层逻辑RNN 家族包括 LSTM、GRU的价值在于对顺序敏感。购物评价里的越来越差一次不如一次如果只看关键词每个词都不算强烈负向但把它们按顺序读下来情感强度是递增的。LSTM 通过门控把这种顺序递进信息保留在隐藏状态里所以它适合建模短文本里的渐变情绪。不过纯 LSTM 在长句上容易出现信息衰减对中间突变的局部短语不敏感这就是它通常要配合 CNN 或 Attention 的原因。RCNN 是 RNN 和 CNN 的组合先由双向 LSTM 给每个 token 生成一个融合了左右上下文的新表示再做一维卷积 全局最大池化。这样既能像 RNN 一样感知上下文又能像 CNN 一样挑出最重要的响应。对购物评价来说等于把前后文和关键词两条线索都保留下来。原始词向量在进入 LSTM 前一般会经过一个可训练的线性层投影到更低维度否则序列特征表达能力会受限于预训练向量本身。两个网络定义可以放在同一个文件里对比import torch.nn as nn class LSTMNet(nn.Module): def __init__(self, embed_dim, hidden_dim, num_layers, num_classes): super().__init__() self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, batch_firstTrue, bidirectionalTrue) self.dropout nn.Dropout(0.3) self.fc nn.Linear(hidden_dim * 2, num_classes) # 双向所以乘2 def forward(self, x): out, _ self.lstm(x) # [batch, seq, hidden*2] return self.fc(self.dropout(out[:, -1, :]))LSTMNet 取的是最后时刻的隐藏状态双向时把正反两个方向的输出拼接所以分类层输入维度是 hidden_dim*2。这里的 x 是已经过词向量层或 Bert 编码的序列表示维度是 [batch, seq, embed_dim]。embed_dim 如果是上游 Bert 的 768可以先投影到 200300 维再用 LSTM降低训练成本。class RCNNNet(nn.Module): def __init__(self, embed_dim, hidden_dim, num_classes): super().__init__() self.lstm nn.LSTM(embed_dim, hidden_dim, batch_firstTrue, bidirectionalTrue) self.conv nn.Conv1d(hidden_dim * 2, hidden_dim, kernel_size3, padding1) self.fc nn.Linear(hidden_dim, num_classes) def forward(self, x): lstm_out, _ self.lstm(x) # [batch, seq, 2*hidden] conv_out torch.relu(self.conv(lstm_out.transpose(1, 2))) pooled conv_out.max(dim2).values # 全局最大池化 return self.fc(pooled)RCNNNet 先把序列过双向 LSTM再把每个位置拼接了上下文的新表示送进 Conv1d最后用 max-pooling 把整句压成一个向量。注意 Conv1d 的输入通道是 hidden_dim*2因为 LSTM 双向输出维度翻倍。max 池化取的是每个卷积通道在整句上的最大值这种做法对只要出现一个强信号就判定情感特别友好适合关键词明显的评价。2.3 分类器与输出层设计Sigmoid / Softmax 怎么选很多源码把最后一层写死成 Sigmoid 二分类但在购物场景里评价往往有星级。五星制下模型需要判断 15 星这不只是好坏二分还包含情感强度。二分类正/负时输出层用一个 logit接 BCEWithLogitsLoss多分类比如按评分分 5 类时输出层是 5 个 logits接 CrossEntropyLoss。如果产品只关心差评拦截二分类足够且数据标注成本低如果要给运营看情感得分最好用 3 分类负/中/正甚至 5 分类。我见过一种后悔药式的输出层设计先输出一个二维 logit 判断是否值得处理再在后处理阶段把概率映射成 15 星。这其实是在骗自己因为两段式会切掉强度信息。正确做法是一开始就定好标签粒度再用对应的损失函数优化。下面是两种输出的定义import torch.nn as nn # 二分类只输出 1 个 logit配合 Sigmoid head_binary nn.Linear(hidden_dim, 1) loss_fn nn.BCEWithLogitsLoss() # 多分类输出 C 个 logit配合 Softmax head_multi nn.Linear(hidden_dim, 5) loss_fn nn.CrossEntropyLoss()hidden_dim 是特征向量维度不管是 Bert 的 768 还是 RCNN 池化后的输出都要先过 Dropout 再进分类头否则训练时测试集表现和验证集差一大截。如果类别不平衡比如中评很少不要在输出层强行加权重可以在损失函数里传 class_weight或者用 Focal Loss 代替 CrossEntropyLoss这比改样本阈值更可控。3. 从源码 zip 到能跑环境准备与最小复现路径拿到源码.zip后第一步不是看模型文件而是确认运行环境。购物平台情感分析项目一般由 Python 脚本、数据文件、配置 json 和训练日志组成最常踩的坑是 Python 版本与深度学习库不匹配。下面按顺序做可以在半小时内把最小训练流程跑起来。3.1 先把 Python 环境与依赖装齐torch、transformers 与 sklearn建议用虚拟环境隔离不要直接装在系统 Python 里。深度学习依赖版本之间互相绑架torch 版本会影响 transformers 是否可用transformers 版本又决定 Bert 权重能否正常加载。常见做法是用 Python 3.8 或 3.9搭配 torch 1.13 以上。新装的机器可以选 2.x 版本老服务器如果只有 CUDA 10.2就要主动降 torch 版本。python -m venv sentiment_env source sentiment_env/bin/activate # Windows 用 sentiment_env\Scripts\activate pip install --upgrade pip pip install torch pip install transformers scikit-learn pandas numpytorch 默认装 CPU 版如果机器有 N 卡请按自己显卡驱动对应的 CUDA 版本从 PyTorch 官网选安装命令。注意 transformers 会自动拉进来 tokenizers、huggingface_hub 等依赖它们版本更新很快代码里如果报AttributeError多数是 transformers 版本新旧接口不兼容而不是你自己的代码写错。检查环境是否可用的命令python -c import torch, transformers; print(torch.__version__, transformers.__version__) python -c from transformers import BertTokenizer, BertModel; print(bert ok)这一步能过滤掉八成为什么跑不起来的问题。如果 transformers 版本很高而源码里用到from_pretrained的旧写法一般还能兼容但如果有model.bert这种私有属性访问新版本可能报错。遇到这种情况不要盲目升级 transformers先看报错信息再决定。3.2 数据与标签格式购物平台评价怎么喂给模型模型不认中文原始文本只认整数 ID。常见格式是把评价文本放进 CSV每行一条至少包含两列text 和 label。label 可以是正负标签也可以是星级。先做基础清洗再进分词跟最终效果关系很大全角半角、繁体简体、URL、 用户、表情符号都要有统一处理策略。import pandas as pd df pd.read_csv(comments.csv, encodingutf-8) df df[[text, label]].dropna() df[text] df[text].astype(str).str.replace(rhttp\S, , regexTrue) df[text] df[text].str.replace(r[\w], , regexTrue) df df[df[text].str.len() 2] label_map {负向: 0, 中性: 1, 正向: 2} df[label] df[label].map(label_map) df df.dropna(subset[label]) print(df.head()) print(df[label].value_counts())dropna 会同时丢掉没有评论文本和标签缺失的行这是个简单但重要的数据质量闸门。正则清理 URL 和 用户是因为它们对情感判断没有贡献反而会把 Bert 的注意力引到无意义的 token 上。注意不要在清洗时把不是很好中间的空格去掉空格会导致分词结果完全改变一句不 好和不好是不同语义。清洗后建议把数据集切分成 train/val/test比例 8:1:1 或 7:2:1。购物评价场景经常出现同一用户多条评论如果数据里保留用户 ID 字段按用户分组切分防止同一人的数据同时出现在训练和验证集里否则验证指标虚高上线就现原形。3.3 最小训练命令与参数表epoch、batch size、学习率怎么设很多源码自带 train.py但也允许你自己写训练入口。关键是不管用 Trainer 还是原生 PyTorch 循环超参不能照搬别人的。Bert 类模型对学习率极其敏感常见范围是 1e-5 到 5e-5batch size 受显存限制。以下是用 HuggingFace Trainer 跑三分类的最小配置from transformers import BertTokenizer, BertForSequenceClassification from transformers import Trainer, TrainingArguments from datasets import Dataset train_df df.sample(frac0.8, random_state42) eval_df df.drop(train_df.index) tokenizer BertTokenizer.from_pretrained(bert-base-chinese) train_ds Dataset.from_pandas(train_df).map( lambda x: tokenizer(x[text], truncationTrue, max_length128), batchedTrue ) eval_ds Dataset.from_pandas(eval_df).map( lambda x: tokenizer(x[text], truncationTrue, max_length128), batchedTrue ) model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels3 ) args TrainingArguments( output_dir./sentiment_out, num_train_epochs3, per_device_train_batch_size16, per_device_eval_batch_size32, learning_rate2e-5, weight_decay0.01, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modeleval_loss, ) trainer Trainer( modelmodel, argsargs, train_datasettrain_ds, eval_dataseteval_ds, tokenizertokenizer, ) trainer.train()参数说明max_length128 对购物评价够用超过 128 的句子大多是凑字数的无效信息batch size 16 是 12G 显存下 Bert-base 的安全值显存小就降到 8。learning_rate 2e-5 是 Bert 微调的经典起点如果验证集 loss 震荡降一半到 1e-5。evaluation_strategy 和 save_strategy 都设成 epoch这样每个 epoch 存一次 checkpoint训练到一半崩了也能从最后一个 checkpoint 恢复这是最实用的后悔药。训练完成后会生成 checkpoint 目录里面包含模型权重、tokenizer 配置和 training_args.bin。真正上线推理时不用 Trainer直接加载 checkpoint 目录里的权重即可下一章会讲怎么接 CNN 和 RNN。如果连最小训练都跑不完问题大概率在 3.1 节的环境环节回头检查 CUDA 和内存占用。4. 用 BertCNN 把评价文本变成向量特征融合的落地写法源码里给你多种模型组合但核心其实只有一个如何把 Bert 输出的 768 维语义向量和 CNN/RNN 的结构化特征拼到一起。这一步的维度对齐和拼接顺序决定了模型是学得快还是原地打转。下面用三个小节把链路拆开。4.1 Bert 编码层从 token 到 768 维向量购物评价这种短文本建议用中文预训练模型bert-base-chinese。它按字切分不需要额外分词标点和数字也保留。把文本转成 token 后得到 input_ids、attention_mask 和 token_type_ids三个都喂给模型。只取 last_hidden_state也就是每个 token 在最后一层 Transformer 输出中的表示。from transformers import BertTokenizer, BertModel tokenizer BertTokenizer.from_pretrained(bert-base-chinese) bert BertModel.from_pretrained(bert-base-chinese) bert.eval() text 这个手机屏幕很好但电池太差了 inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): outputs bert(**inputs) last_hidden outputs.last_hidden_state # [1, seq_len, 768] # 把 [CLS] 位置单独拿出来做句向量 cls_vec last_hidden[:, 0, :] print(cls_vec.shape)[CLS] 是 Bert 专门用来聚合句子信息的 token它在微调时被训练成能代表全句语义。但直接拿 [CLS] 接分类器是够用但浪费的做法因为它丢掉了词级别的局部信息。后面的 CNN/RNN 分支需要吃完整的 last_hidden_state所以在特征融合方案里我们保留全体 token 向量而不是只取 [CLS]。也可以用 attention_mask 把 padding 位置过滤掉防止无效 token 参与池化。4.2 CNN 卷积窗口与池化n-gram 特征的卷积视角PyTorch 的 Conv1d 输入是 [batch, channels, length]channels 在这里是 768length 是序列长度。卷积核的宽度是 kernel_size默认只看窗口内的连续 token。购物评价里的很好太差不推荐都是 23 字的组合所以 kernel_size 设 2、3、4 各一组比单一卷积核更稳。具体做法是并行跑 3 组卷积输出特征各做一次最大池化再拼起来。import torch.nn.functional as F def conv_pool(x, conv): # x: [batch, seq, 768] x x.transpose(1, 2) # [batch, 768, seq] x F.relu(conv(x)) # [batch, out_ch, seq] x x.max(dim2).values # [batch, out_ch] return x conv2 torch.nn.Conv1d(768, 128, kernel_size2, padding1) conv3 torch.nn.Conv1d(768, 128, kernel_size3, padding1) conv4 torch.nn.Conv1d(768, 128, kernel_size4, padding2) feat torch.cat([ conv_pool(last_hidden, conv2), conv_pool(last_hidden, conv3), conv_pool(last_hidden, conv4), ], dim1) print(feat.shape) # [batch, 384]三个卷积核并行后拼出 384 维特征等价于同时看 2-gram、3-gram 和 4-gram。padding 不是随便设的kernel_size2 时 padding1 可以保持长度不变kernel_size4 时 padding2 也是同理。如果你希望卷积后长度变短可以不设 padding但下一步要做全局最大池化长度多少不影响 max 结果。这里统一设了 padding是为了让所有特征在最后拼接前形状一致省去很多维度判断。4.3 把 Bert 输出接进 CNN/RNN 的维度对齐细节最容易翻车的点来了Bert 输出是 [batch, seq, hidden]LSTM 也吃这个形状但 Conv1d 不吃。所以接 CNN 前要 transpose接 LSTM 前要保证 hidden 维度对齐。如果先接 LSTMLSTM 输出是 [batch, seq, hidden*2]再接 CNN 还要再 transpose 一次。下面是包含 Bert CNN 的完整分类模块可以直接复制进自己的模型文件里。import torch.nn as nn class BertCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.bert BertModel.from_pretrained(bert-base-chinese) self.convs nn.ModuleList([ nn.Conv1d(768, 128, k, paddingk // 2) for k in (2, 3, 4) ]) self.fc nn.Linear(128 * 3, num_classes) self.dropout nn.Dropout(0.3) def forward(self, input_ids, attention_mask): out self.bert(input_idsinput_ids, attention_maskattention_mask).last_hidden_state pooled [] for conv in self.convs: x torch.relu(conv(out.transpose(1, 2))) pooled.append(x.max(dim2).values) feature torch.cat(pooled, dim1) return self.fc(self.dropout(feature)) model BertCNN(num_classes3)forward 里先让文本过 Bert 得到序列向量然后每个卷积核各自做 Conv1d ReLU max pool最后把所有池化结果拼成 384 维过 Dropout 进分类层。训练时需要把 Bert 部分设成可微调吗数据量小的时候可以把 Bert 的 requires_grad 设为 False只训 CNN 和分类头这样显存占用小、收敛快但准确率上限低数据量超过一万条就全量微调效果明显更好。全量微调时学习率仍然建议 2e-5配合 AdamW不要再设成 1e-3 这类常规学习率。如果做 RNN 分支Bert 输出可以先过一个线性层把 768 压到 256再进 LSTM否则 LSTM 的参数量会被 768 维输入撑得很大。RCNN 分支里的 Conv1d 输入通道也要对应改成 LSTM 的 hidden*2。简单说维度链就是序列长度在 Bert、LSTM、Conv1d 之间可以自由换位置但通道数和 hidden 数必须前后一致。5. 避坑检查单训练时最容易翻车的 5 个环节下面是真实项目里出现频率最高的五个翻车点每一条都按现象、原因、解决三步写。它们之间往往连环触发第一个不解决后面几个的症状会一起冒出来。5.1 现象显存溢出 OOM训练到第一个 epoch 就报CUDA out of memory。原因多数不是显存真的不够而是 max_length 设太长或者 batch_size 没按最坏情况估算。Bert 的注意力矩阵大小随序列长度平方增长128 长度的开销是 64 长度的 4 倍。解决办法是先把 max_length 降到 64再把 per_device_train_batch_size 从 16 降到 8 或 4。改代码时在 TrainingArguments 里加gradient_accumulation_steps2用小 batch 累加梯度效果等价于大 batch。显存还不够时就冻结 Bert只训练分类层。5.2 现象准确率很高但 F1 很低购物评价数据集里差评比例经常只有 5%10%模型只要全预测成好评准确率就有 90%。这时候看 F1 就会现原形。原因是标签不平衡解决思路有三个上采样差评数据、在损失函数里传 class_weight、或者换用 Focal Loss。优先推荐 class_weight因为实现最简单from sklearn.utils.class_weight import compute_class_weight import numpy as np import torch classes df[label].values weights compute_class_weight(balanced, classesnp.unique(classes), yclasses) weight_tensor torch.tensor(weights, dtypetorch.float, devicedevice) loss_fn nn.CrossEntropyLoss(weightweight_tensor)class_weight 会让模型对样本少的类别给更高惩罚所以差评的召回率会明显提升。注意验证指标也要从 accuracy 改成 f1训练器里的 metric_for_best_model 不要再用默认的 eval_loss而是自定义一个返回 F1 的指标函数否则选出来的最优 checkpoint 可能不是你要的。5.3 现象验证集 loss 下降但测试集翻车训练日志非常漂亮上线一测就垮。最常见原因是随机种子没固定导致每次跑出来的数据划分不一样测试集偶尔被撞进训练集另一种是切分前没有做去重重复文本横跨训练和测试。固定随机种子的代码必须在导入所有库之后立刻执行import random import numpy as np import torch seed 42 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)同时保证划分数据时用df.sample(frac0.8, random_state42)不要用默认随机。做了这两步验证集和测试集的差距一般能控制在 2 个点以内。如果差距还在 5 个点以上就去检查训练集中有没有和测试集完全重复的句子这种记忆式过拟合是数据泄漏的典型信号。这种情况在电商评论里很常见同一句话被多个用户复制粘贴。5.4 现象RCNN 收敛慢于 CNN同样的数据CNN 已经到 0.80 F1RCNN 还在 0.70 附近挣扎。原因是 RCNN 里 LSTM 对学习率更敏感且梯度在时间步上传播容易爆炸。解决方法是给 LSTM 部分单独设一个较小的学习率或者给整个模型加梯度裁剪。用原生 PyTorch 循环时在 loss.backward() 后加一行torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)设置 max_norm1.0 表示梯度 L2 范数上限为 1超过就等比缩放。很多项目把这条省了导致 RCNN 训练曲线像过山车。另外 LSTM 的 dropout 要和分类头的 nn.Dropout 区分开LSTM 内部的 dropout 参数是 dropout0.1不要和分类头前面的 Dropout(0.3) 混在一起。梯度裁剪不影响正常训练收敛只在梯度异常时起作用。5.5 现象文本里有 emoji 和繁体字中文评价里难免出现和繁体字。Bert 的 tokenizer 遇到生僻 token 会拆成 unknowemoji 可能被丢掉导致关键词丢失。先做文本规范化再进模型import re import unicodedata def normalize_text(text): text unicodedata.normalize(NFKC, text) # 统一全角半角 text text.replace(\u2600, ) # 按需保留或映射 emoji return text df[text] df[text].apply(normalize_text)NFKC 归一化会把全角字符转半角也能统一一些特殊标点。繁体转简体则要靠 OpenCC 这类库不在这里展开。处理 emoji 的常见做法是保留并映射成一个特殊 token让模型学习笑脸符号 正向这类规律而不是直接删除。如果你发现测试数据里频繁出现 [UNK]先检查这一步不要急着换模型。6. 从单模型到集成把它做成能用的情感分析服务训练好一个模型只完成一半后面要解决的是多个模型怎么协作和别人怎么调用。我一般的做法是先让三个模型各自产出概率再做加权投票最后把最优组合导出成 FastAPI 服务。6.1 用模型投票 / 加权融合把三个模型拼起来最简单的集成是概率平均。每个模型对同一条评价输出正向概率取平均后按阈值判断。但三个模型不是同等可靠CNN 对短句强RCNN 对转折句更强Bert 综合最好。我习惯在验证集上给三个模型分配权重例如 Bert 0.5、RCNN 0.3、CNN 0.2再微调阈值。final_prob 0.5 * bert_prob 0.3 * rcnn_prob 0.2 * cnn_prob pred (final_prob 0.5).astype(int)注意三个模型的概率必须来自同一套标签和阈值体系否则融合没有意义。CNN 和 RCNN 如果用了 Sigmoid要确保输出是概率而不是 logitlogit 不能直接平均。6.2 导出与推理把模型打包成 FastAPI 接口模型训练完把 state_dict 存成 pt 文件再用 torch.load 恢复。FastAPI 的推理函数只需要做三件事接收文本、预处理、返回预测概率。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class Item(BaseModel): text: str app.post(/predict) def predict(item: Item): text normalize_text(item.text) inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): logits model(**inputs).logits prob torch.softmax(logits, dim1).cpu().numpy()[0] return {negative: float(prob[0]), positive: float(prob[2])}这里用 FastAPI 而不是 Flask是因为 FastAPI 自带异步和请求参数校验生产环境少踩很多坑。启动命令uvicorn app:app --host 0.0.0.0 --port 8000一台 4G 显存的显卡就能扛住小流量。如果只是内部测试也可以直接写成 Python 函数调用省去 HTTP 开销。6.3 最后检查用一条真实差评验证全链路我习惯在服务部署前拿一条不在训练集里的真实差评做端到端测试比如刚收到货就发现屏幕有一条线客服还爱答不理。看返回的负向概率是否超过 0.9。这一步能同时验证 tokenizer 的 max_length、模型前向、后处理三个环节。另一个技巧是给接口写一个简单的压测脚本连续发 200 条请求观察显存是否稳定。整个过程跑通后这个项目才算真正可以交给运营使用。算下来从环境准备到多模型对比最花时间的不是训练而是数据清洗和维度对齐。我每次换数据集都要重新确认一遍标签分布和文本长度这两项决定了很多超参的起点。希望这套落地思路能帮你的项目少走一段弯路。本文还有配套的精品资源点击获取