资讯动态

购物评价情感分析:Bert、CNN、RNN、RCNN多模型对比实践

发布时间:2026/10/13 9:28:06 来源:尧图企业网站定制
简介一套面向购物平台用户评论的情感分析系统实现源码项目以Python编写融合BertCNN、RNN、RCNN等深度学习模型可用于电商评价正负面分类与情感极性挖掘。内容覆盖数据预处理、模型构建、训练与评估流程适合计算机、数据科学、人工智能等相关专业学生用于毕业设计、课程设计或期末大作业也便于开发者对照学习多种文本分类模型的差异。压缩包共55个文件以16个py源文件为核心包含模型定义如Bert.py、BertCNN.py、BertRCNN.py、BertDPCNN.py等、训练脚本main.py/train.py、工具模块utils.py另有37个pyc编译文件、1个介绍文档及1个附带zip整体约503KB。目前已有265人学习下载。代码结构紧凑可直接运行修改也适合在此基础上扩展新模型或接入真实平台评论数据是快速上手NLP情感分析任务的实用参考。1. 购物评价情感分析为什么非要把 Bert、CNN、RNN、RCNN 放在一起比你手上攒了几万条购物平台的用户评价领导想按「好评、差评」自动分类或者想看出某个商品近期的负面槽点。这时候最直接的念头是直接微调一个 Bert 模型准确率通常不错但推理速度慢、显存开销大翻过头来用纯 CNN 或 RNN速度快了长文本语义又抓不住。实际项目里没人只上一个模型交差普遍做法是把 Bert 和 CNN、RNN、RCNN 一起拉出来跑对比用同一份数据集、同一套评估口径选出精度和速度的平衡点。这套方案适合三类人做电商舆情分析的工程师、NLP 方向的学生、以及想在小团队里低成本落地评价分类的开发者。它能解决的是「数据清洗 → 多模型训练 → 评估对比 → 单条预测」整条链路而不是某一个孤立的训练脚本。下面按我平时落地的顺序拆开讲包括每个模型怎么调才有意义以及哪些地方真的会翻车。2. 评价数据从哪来、怎么洗先定标签规则再碰模型2.1 购物评价的原始形态与标签映射策略购物平台的评价数据通常长这样一条评论包含用户 ID、商品 ID、评分1-5 星、评论文本、评价时间。情感分析真正用到的主要是「评分」和「评论文本」这两列。有些数据源只有评论文本没有评分那就得靠人工标注一小批样本做冷启动。我一般会先统计评分的分布再决定做几分类。常见做法是三种二分类1-2 星映射为负向4-5 星映射为正向3 星直接丢弃。适合只想看「好评率」的场景。三分类负向、中性、正向3 星独立成一类。中性类往往和正负类重叠严重模型区分度会下降。五分类直接预测 1-5 星最细粒度但样本不均衡时小类别很难学。从落地角度讲大多数购物平台评价分析先做二分类就跑通业务中性评论单独过滤掉。因为平台本身关心的是「有多少人在骂」骂的点是什么。不要一上来就贪五分类标注不一致会让模型变成黑匣子问题还不好排查。数据文件建议统一用 JSON 或 CSV 存每条记录至少包含text和label两个字段。下面这段是读取和划分数据的入门脚本import pandas as pd from sklearn.model_selection import train_test_split # 读取原始评价数据假设 CSV 中有 score 和 comment 两列 df pd.read_csv(shopping_comments.csv) # 评分 1-2 定义为负向(0)4-5 定义为正向(1)3 分中性样本丢弃 df df[df[score] ! 3].copy() df[label] df[score].apply(lambda x: 1 if x 4 else 0) print(df[label].value_counts()) # 输出类似 1: 8321, 0: 4169说明数据是不平衡的 train_df, test_df train_test_split( df, test_size0.2, random_state42, stratifydf[label] ) train_df, val_df train_test_split( train_df, test_size0.2, random_state42, stratifytrain_df[label] ) train_df.to_csv(train.csv, indexFalse) val_df.to_csv(val.csv, indexFalse) test_df.to_csv(test.csv, indexFalse)stratifydf[label]是必须写的它保证划分后训练集、验证集、测试集的正负比例和全量数据一致否则不平衡数据在某一折里全变成正向训练出来的模型只会猜正向。random_state42固定随机种子保证多次实验划分结果一致这是多模型对比实验成立的前提。2.2 清洗流程里最容易被忽略的四个决定购物评论文本和新闻语料差别很大口语词多、表情符号多、还有大量「质量不错就是物流太慢」这类转折句。清洗时我会按顺序做四件事第一去掉 HTML 标签和转义字符。很多数据是从网页端导出的评论里夹杂p、nbsp;这类残渣正则直接替换掉。第二繁体转简体。购物平台用户输入习惯不一不做转换会让词表膨胀同一个词被拆成两个特征。第三表情符号和 emoji 不删。这一点很多人做反了——评价里的「东西很好」和「东西很好」传达的情感完全不同干脆把 emoji 统一映射成[happy]、[angry]这样的特殊 token保留信号。第四停用词表要极度克制只能删「的、了、吗、啊」这类无语义词绝不能删「不、没、无、别」这类否定词否则「不怎么样」会被洗成「怎么样」情感直接反过来。清洗代码的骨架如下import re import jieba STOPWORDS set([的, 了, 在, 是, 吗, 啊, 呢]) def unify_emoji(text: str) - str: # 常见表情映射为文本 token保留情感信号 mapping { : [happy] , : [happy] , : [angry] , : [angry] , : [good] , : [bad] , } for k, v in mapping.items(): text text.replace(k, v) return text def clean_comment(text: str) - str: text re.sub(r[^], , text) # 去 HTML 标签 text re.sub(r[a-zA-Z];, , text) # 去 nbsp; 等实体 # 这里默认已做过繁转简简繁转换可用常见开源映射表实现 text unify_emoji(text) return text def tokenize(text: str) - list: words jieba.lcut(clean_comment(text)) # 过滤纯空格和停用词保留否定词 words [w.strip() for w in words if w.strip() and w not in STOPWORDS] return words sample 物流很快就是包装有点简陋 print(tokenize(sample)) # 输出类似 [物流, 很快, [good] , , 就是, 包装, 有点, 简陋, [angry] ]注意这里我特意没有过滤逗号句号。CNN 的卷积核在 n-gram 级别的特征提取中标点符号能辅助模型判断句子的断点位置尤其是在短文本里去掉标点反而损失信息。很多做新闻文本分类的清洗流程直接调停用词表删标点搬到购物评价上效果会掉。2.3 序列长度与词表怎么定短文本场景下的取舍购物评价平均长度在 20-50 个字符之间真正超过 100 的很少。所以无论是 Bert 还是 CNN、RNN都不需要把max_len设得很大。设大了等于给模型塞一堆 padding训练慢、内存浪费效果还可能因为有效信号被稀释而下降。我常用的配置是CNN/RNN/RCNN 的词向量维度 128max_len截断到 64 个 tokenBert 单独用它的 tokenizermax_len设 128 就够。词表按训练集统计过滤掉出现次数小于等于 1 的词把词表大小控制在 3 万以内。如果词表太大说明清洗环节没做好低频词和错别字在干扰统计。下面这段代码把 tokenize 后的词序列转成模型输入需要的 ID 序列from collections import Counter import json # 先统计全量词频构建词表 counter Counter() for text in train_df[comment]: counter.update(tokenize(text)) # 只保留出现次数 1 的词降低词表噪音 vocab {[PAD]: 0, [UNK]: 1} for word, freq in counter.items(): if freq 1: vocab[word] len(vocab) print(vocab size:, len(vocab)) def encode(text: str, max_len: int 64) - list: tokens tokenize(text)[:max_len] ids [vocab.get(w, vocab[[UNK]]) for w in tokens] # Padding 到固定长度方便组 batch ids ids [vocab[[PAD]]] * (max_len - len(ids)) return ids sample_ids encode(物流很快就是包装简陋, max_len64) print(sample_ids[:10])[PAD]的位置必须放在词表第 0 位多数框架的 embedding 层默认忽略 0 索引的梯度更新放别的位置容易出现 padding 向量被学习出非零语义的乌龙。[UNK]放第 1 位保证未登录词有个统一的归宿。这两个 token 的选择属于那种「错一次要排查两小时」的隐藏坑。3. 四个模型在评价情感分析里的分工网络结构到落地实现3.1 为什么短文本场景下 CNN 反而不弱RNN 的价值在转折句购物评价这类短文本有个特点情感极性往往由局部词组决定。比如「物流很快」是正向「包装简陋」是负向一条评论里可能同时存在多个局部情感片段。TextCNN 的多个卷积核天然擅长抓这类局部 n-gram 特征一个尺寸为 3 的卷积核扫过「包装简陋」就能捕获这个负面词组模型不需要理解全句语法就能给出不错的结果。这也是实际对比里 CNN 经常追平 Bert 的原因——任务本身对长距离依赖要求不高。RNN 类模型LSTM/BiLSTM的价值体现在「转折句」上「价格不贵就是质量一般」。CNN 抓到「不贵」和「质量一般」两个局部片段后靠全连接层去融合效果很依赖池化策略BiLSTM 则通过门控机制把「就是」这个转折关系编码进最后的隐藏状态对整体情感极性的判断更稳。缺点是训练速度慢超长序列上有梯度衰减问题但购物评价 64 个 token 的长度对它不算压力。RCNN 是折中方案用 BiLSTM 先上下文语义再取每个位置隐藏状态的 max-pooling 作为全句表示。它的思路是把 CNN 的局部特征抽取和 RNN 的序列建模揉在一起实现上只比 RNN 多一行池化操作但效果通常比纯 BiLSTM 稳定。Bert 则完全是另一套逻辑预训练阶段已经注入大量语言知识微调时只需要在[CLS]向量上接一个分类头。它的优势是「理解」能力最强对反讽、口语化表达的鲁棒性更好代价是模型体积大、推理慢、对 GPU 显存有要求。多模型对比实验的意义正在于此不是选最准的而是选「这个项目能承担的准确率」。3.2 model.pyTextCNN、BiLSTM、RCNN 的最小可运行实现下面这套代码我建议直接存储为model.py三个模型共用同一个 embedding 层定义方便后面做对比实验时保证变量唯一import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_filters128, kernel_sizes(3, 4, 5), num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, kernel_sizeks) for ks in kernel_sizes ]) self.dropout nn.Dropout(dropout) self.fc nn.Linear(num_filters * len(kernel_sizes), num_classes) def forward(self, x): # x shape: (batch, seq_len) emb self.embedding(x) # (batch, seq_len, embed_dim) emb emb.transpose(1, 2) # 转成 Conv1d 期望的 (batch, embed_dim, seq_len) pooled [] for conv in self.convs: c torch.relu(conv(emb)) # (batch, num_filters, conv_out_len) c torch.max_pool1d(c, c.size(2)).squeeze(2) pooled.append(c) out torch.cat(pooled, dim1) out self.dropout(out) return self.fc(out) class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_size128, num_layers1, num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_size, num_layers, batch_firstTrue, bidirectionalTrue) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size * 2, num_classes) def forward(self, x): emb self.embedding(x) out, _ self.lstm(emb) # out: (batch, seq_len, hidden_size * 2) # 取最后时间步的拼接向量等价于把双向的末尾隐状态拼在一起 out out[:, -1, :] out self.dropout(out) return self.fc(out) class RCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_size128, num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_size, 1, batch_firstTrue, bidirectionalTrue) self.dropout nn.Dropout(dropout) self.fc nn.Linear(embed_dim hidden_size * 2, num_classes) def forward(self, x): emb self.embedding(x) # (batch, seq_len, embed_dim) out, _ self.lstm(emb) # (batch, seq_len, hidden_size * 2) # 将词向量与双向上下文拼接再用 max-pooling 提取全句特征 combined torch.cat([emb, out], dim2) # (batch, seq_len, embed_dim hidden_size*2) combined combined.transpose(1, 2) pooled torch.max_pool1d(combined, combined.size(2)).squeeze(2) pooled self.dropout(pooled) return self.fc(pooled)三个模型的关键参数说明如下num_filters128控制卷积核数量数量太多容易过拟合购物评价这种小数据集 128 就够了kernel_sizes(3, 4, 5)表示同时用 3-gram、4-gram、5-gram 三种窗口抓局部特征这个组合是 TextCNN 文本分类里最常用的经验值BiLSTM 的hidden_size取 128 时模型参数量适中双向后接的fc输入维度就是 256RCNN 里embed_dim hidden_size * 2这个式子决定了池化前的特征维度最容易写错的是把track_running_stats之类的参数搞混这里没用到 BatchNorm所以不涉及。RCNN 的实现有个容易翻车的细节combined先拼接再池化池化是对整个序列长度维度做 max-pooling而不是对 hidden_size 维度做池化。我见过有人把torch.max_pool1d(combined, kernel_size3)当池化用结果维度对不上直接报错。这里的.size(2)取的是序列长度池化后每个样本得到一个embed_dim hidden_size*2维向量逻辑才正确。3.3 引入 Bert加载预训练权重、分类头与参数冻结策略Bert 在对比实验里通常作为「上限参考」。中文场景下我一般加载通用的中文预训练权重然后只训练分类头部分是否解冻 Bert 主体参数要看数据量和显卡资源。数据量在 1 万条以内时全量微调 Bert 很容易过拟合验证集 F1 波动很大数据量超过 5 万条时解冻后两层 Transformer 会有明显提升。下面这段代码演示了 Bert 在 PyTorch 里怎么接import torch from transformers import BertTokenizer, BertModel class BertClassifier(nn.Module): def __init__(self, num_classes2, freeze_bertTrue): super().__init__() # 加载预训练权重下载失败时检查网络与缓存目录 self.bert BertModel.from_pretrained(bert-base-chinese) self.dropout nn.Dropout(0.3) self.fc nn.Linear(768, num_classes) if freeze_bert: # 冻结 Bert 全部参数只训练分类头适合小数据集 for param in self.bert.parameters(): param.requires_grad False def forward(self, input_ids, attention_mask): outputs self.bert(input_idsinput_ids, attention_maskattention_mask) cls_vec outputs.pooler_output # (batch, 768) out self.dropout(cls_vec) return self.fc(out) # 使用示例 tokenizer BertTokenizer.from_pretrained(bert-base-chinese) encoded tokenizer(物流很快包装简陋, max_length128, paddingmax_length, truncationTrue, return_tensorspt) print(encoded[input_ids].shape) # torch.Size([1, 128])freeze_bertTrue是数据量不足时最直接的防过拟合手段。代价是模型上限被锁住因为 Bert 底层的语言知识完全不动只学分类边界数据量大、显卡显存够时我会将freeze_bertFalse同时把 Bert 的 learning rate 调到很小的值通常为 2e-5 到 3e-5而分类头的学习率可以放到 5e-4。这两段学习率必须分开设置上一套学习率去更新 Bert 会让预训练权重迅速被冲掉。attention_mask一定要传。购物评价经过 padding 后有一大半是[PAD]位置不传 maskBert 会把 padding 位置的注意力也计算进去分类向量会被无意义的 pad 污染。这个问题表现起来很隐蔽——训练 loss 能正常下降但验证集 F1 一直上不去卡在 0.7 左右。4. 训练与评估一套脚本跑四个模型结果才有可比性4.1 训练主循环优化器、学习率与提前停止的配置多模型对比实验的核心原则是「除了模型结构其他条件保持一致」。如果 Bert 用 AdamW、CNN 用普通 Adam调参空间不同最后对比出的差异说不清是模型结构造成的还是优化器造成的。我会在训练脚本里为每个模型分别配置合理的优化器参数但早停策略、batch size、epoch 数量一律统一。以下是训练一个 TextCNN 的完整主循环其他模型替换模型实例即可import torch from torch.utils.data import Dataset, DataLoader from torch.optim import AdamW from accelerate import Accelerator class CommentDataset(Dataset): def __init__(self, csv_path, vocab, max_len64): import pandas as pd df pd.read_csv(csv_path) self.texts [encode(t, max_len) for t in df[comment]] self.labels df[label].astype(int).tolist() def __len__(self): return len(self.labels) def __getitem__(self, idx): return torch.tensor(self.texts[idx], dtypetorch.long), torch.tensor(self.labels[idx], dtypetorch.long) train_ds CommentDataset(train.csv, vocab, max_len64) val_ds CommentDataset(val.csv, vocab, max_len64) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse) model TextCNN(vocab_sizelen(vocab)) optimizer AdamW(model.parameters(), lr3e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.7) best_f1 0.0 patience 0 for epoch in range(10): model.train() total_loss 0.0 for input_ids, labels in train_loader: optimizer.zero_grad() logits model(input_ids) loss torch.nn.functional.cross_entropy(logits, labels) loss.backward() # 梯度裁剪防 RNN/LSTM 梯度爆炸CNN 场景影响不大 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() scheduler.step() val_loss, val_f1 evaluate(model, val_loader) # 以验证集 F1 为早停指标连续 3 轮不升就停 if val_f1 best_f1: best_f1 val_f1 patience 0 torch.save(model.state_dict(), best_model.pt) else: patience 1 if patience 3: print(fearly stop at epoch {epoch}) breaklr3e-4是 CNN/RNN/RCNN 这类从随机初始化词向量开始的模型的常用起点Bert 微调则建议降到2e-5这是预训练权重和新分类头学习率量级不一致导致的必调项。梯度裁剪max_norm5.0在 RNN/RCNN 里是必须的LSTM 在反向传播中容易梯度爆炸不裁剪训练几轮后 loss 直接变成nan。StepLR每 3 轮把学习率乘以 0.7让模型后期收敛更稳但要注意scheduler.step()放在每轮结束而不是每 batch 结束否则学习率衰减过快。4.2 评估指标为什么情感分析不能只盯着准确率购物评价数据天然不平衡大多数情况下正向评论占 60% 以上。如果模型把所有样本都预测成正向准确率就有 60%看起来不差但负向评论完全没被识别业务上等于报废。所以在训练脚本里必须同时输出Precision、Recall、F1、混淆矩阵。我在评估函数里额外加了按类别拆分 F1 的统计def evaluate(model, loader): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for input_ids, labels in loader: logits model(input_ids) preds torch.argmax(logits, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) from sklearn.metrics import f1_score, classification_report f1 f1_score(all_labels, all_preds, averagebinary) # averageNone 时返回每个类别的 F1方便看负向类是否被模型忽略 f1_per_class f1_score(all_labels, all_preds, averageNone) print(classification_report(all_labels, all_preds, digits4)) return f1, sum(f1_per_class) / len(f1_per_class)averagebinary只适用于二分类并且正样本对应的是标签 1如果标签 0 是我们要关注的负向情感必须把pos_label0传给f1_score否则算出来的 F1 是正向类的。我踩过这个坑模型把负向评论错分得稀烂但 F1 显示 0.87害我以为效果很好直到看了混淆矩阵才发现负向类 Recall 只有 0.3。每类 F1 都打印出来才能避免被总指标误导。4.3 对比实验记录表记录什么才有参考价值跑了四个模型后最忌讳只记录准确率和 F1 两个数字。推理速度、模型大小、训练时间这三个工程指标在多模型选型里同样决定最终能不能上线。我一般会固定测试集统计 1000 条样本的预测延迟然后把结果记录成一张表模型参数量(M)测试集F1推理耗时(ms/百条)训练耗时(分钟)显存占用(GB)TextCNN3.90.87212361.1BiLSTM5.20.861035121.3RCNN6.10.880338151.4Bert(冻结)1020.8935210454.8这张表是虚构的示例数据但它说明了一个典型规律Bert 的 F1 最高但推理耗时是 CNN 的近 10 倍RCNN 的 F1 接近 Bert速度却在可接受范围内。对实时性有要求的场景Bert 再准也上不了线。对比实验的正确产出不是「谁分最高」而是「在算力预算内谁最合适」。实际做对比时还有一条规则每个模型至少跑 3 次取平均值。购物评价数据量小单次训练结果受随机初始化影响很大跑一次就定结论容易做出玄学结论。某个开发者曾在某次实验中只跑一轮就认定 RCNN 比 CNN 差 5 个点重跑两轮后发现差距只有 0.6纯属随机波动。5. 避坑指南购物评价情感分析最常见的五类翻车现场5.1 Bert 显存爆掉或加载异常现象是运行几分钟后报 CUDA out of memory或者从预训练权重加载时直接卡死。原因通常是三个叠加一次性把整个训练集喂进 GPU 而不是用 batch、max_len设得过大、以及没有冻结参数还开了很大的 batch size。解决办法是先把这个模型单独跑通再谈对比实验max_len统一设 128batch_size从 16 开始往上加冻结 Bert 参数后显存占用能降到原来的三分之一。某次我用 32 的 batch size 微调冻结版 Bert4GB 显存的旧卡也能跑完一个 epoch。5.2 训练 loss 不降验证集 F1 始终在 0.5 附近这种现象最直接的原因是标签和数据没对齐模型把正负样本学反了。常见诱因是 CSV 里label列被 pandas 读成了字符串cross_entropy期望数字类型的long张量类型不匹配时某些框架会自动转换但转换后的类别顺序变了。第二个常见原因是从train_test_split得到的验证集里某一类样本数量只有个位数验证集 F1 随机波动大怎么看都像没收敛。解决方法是先把train.csv单独拿出来跑 5 个 batch确认 loss 能下降再统计验证集的 label 分布确保两类样本都不少于 50 条。这两步做完绝大多数「不收敛」问题都能定位到是数据问题而不是模型问题。5.3 停用词过滤把否定词删了情感全部反转这是购物评价情感分析独有的坑。通用中文停用词表里通常包含「不、没、别、无」这类否定词因为它们在主题分类里没有区分度但在情感分析里「不好」「不行」「不给力」全靠否定词驱动。如果清洗时直接套用通用停用词表训练集里「质量真的不好」会被洗成「质量真的好」模型学到的是完全错误的情感信号。解决办法是在构建停用词表时排除所有含否定语义的词并且单独校验打印清洗后的训练集前 100 条样本人工扫一遍有没有情感反转。5.4 RNN 训练慢且梯度爆炸RNN 效果还不如 CNN购物评价短文本场景里RNN 序列长度短理论上不该有梯度爆炸但如果你用的是 BiLSTM 且没有梯度裁剪训练到某个 epoch 时 loss 突然变成nan权重全部被破坏。解决办法已经写在训练循环里了clip_grad_norm_(model.parameters(), max_norm5.0)。RNN 效果不如 CNN 也不悲观——这恰好说明任务本身是局部特征主导的RNN 的序列建模优势在 64 个 token 的评价里发挥不出来。此时应该承认场景差异而不是强行调参把 RNN 的 F1 刷上去。5.5 RCNN 的池化层维度错误导致输出维度对不上RCNN 的实现里有个高频报错torch.max_pool1d(combined, kernel_sizecombined.size(2))这一行中combined.size(2)是序列长度但如果你把输入转置后手动数维度很容易把hidden_size的位置算错导致池化输出维度和fc层输入维度不匹配。我的经验是每实现一个模型先用torch.randn(8, 64)模拟输入跑一次 forward打印输出 shape 和预期值对比再进训练循环。这一步能省掉 80% 的维度排查时间。RCNN 里combined最后一个维度是embed_dim hidden_size * 2这个数在搭建fc之前写进注释避免改 embedding 大小时忘记同步改fc。6. 单条预测、多模型融合与上线前的验证技巧当你训练完一个 TextCNN 或者 RCNN拿到best_model.pt最终要面对的问题是怎么让业务方输入一句评价返回「正向/负向」和置信度。以下是一个直接可用的推理函数def predict_single(text: str, model, vocab, max_len64): model.eval() ids torch.tensor([encode(text, max_len)], dtypetorch.long) with torch.no_grad(): logits model(ids) prob torch.softmax(logits, dim1) pred torch.argmax(logits, dim1).item() return {label: pred, confidence: prob[0][pred].item()}测试时故意输入「物流很慢客服态度也不好」如果模型输出 1正向说明清洗环节一定有情感反转问题回头查停用词表。这类验证脚本建议保留下来每次改完清洗流程就跑一遍防止回归。多模型融合的常见做法是软投票把 CNN、BiLSTM、RCNN 三者的概率按权重平均后取 argmax。权重一般凭验证集 F1 设定F1 高的模型权重略大即可不要过分追求调权。三个模型在同一任务上错误相关性低融合后 F1 通常能比最好的单模型再高 0.5-1 个点。Bert 是否加入融合看推理耗时要求能容忍 200ms 延迟再加入。最后建议多花十分钟做一次时间序列验证把数据按时间排序取前 80% 做训练、后 20% 做测试。这种做法能评估模型对「新出现的评价用语」是否还有效电商平台隔几个月就出一批新网络用语模型如果不更新就会悄悄退化。我每次都会跑这个实验只要后一时间段的 F1 比随机划分低超过 3 个点就该把新数据重新标注并入训练集。说起来我做这个方向头几次就是栽在「验证集 F1 好看就上线」上。后来学乖了每个模型固定跑 3 轮取均值、打印每类 F1、再做时间维度验证上线后很少再因为数据漂移翻车。希望这套流程对你也能少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑