简介:基于深度学习的情感分析模型资源包,面向需要快速处理外卖、酒店评论情感判别的自然语言处理开发者与研究者,模型经专门语料训练后平均准确率约90%,可对新评论直接输出正面、负面或中性标签。资源共4个文件,以Python预测脚本、模型压缩包和说明文档为主,整体仅458KB,结构紧凑,下载后即可快速调用。压缩包内的模型可覆盖CNN、RNN、LSTM乃至Transformer等常见深度学习架构,训练过程涉及交叉熵损失与Adam优化器,说明文档对模型结构、训练数据和调用方式进行了梳理,便于理解情感分析建模思路。已有73人学习下载,适合用作情感分析项目的基线模型、课程实验或快速演示,通过阅读脚本和文档可掌握模型加载、推理流程及简单的调参与替换方法。对希望直接获得可用情感分类器、避开繁琐训练的开发者来说,这套资源能明显缩短从数据准备到线上预测的周期。
1. 没有代码也能训练吗?这个标题背后其实就是一套完整的中文情感分析流水线
外卖评论和酒店评论,本质上都是“短文本 + 强领域词 + 口语化表达”。你拿到一个 90% 左右准确率的模型压缩包,真正值钱的不是那 90% 的数据,而是数据是怎么清洗的、类别是怎么定的、训练时有没有做泄漏防护。这两个领域放在一起训练,最大的好处是“广义情感”可以被共同学习——外卖评论里的“慢”和酒店评论里的“吵”都表示不满,但它们的用词风格完全不同,一起训练等于让模型同时见过两种中文口语的分布。适合谁读?如果你手头正好有一批带星级的评论数据,或者想把自己收集的评论文本训练成能用的情感分类器,照着这套流程走下来,能少踩一半的坑。90% 准确率本身不算特别高,也不低,关键要看它是在什么数据划分、什么评价指标下算出来的,后面每一章都会回到这个数字上。
2. 情感分析建模选型:为什么外卖和酒店评论不能只靠词典匹配
2.1 先搞清楚这两类文本的结构差异
外卖评论的典型特征是:短、物品名词多、时效性强。比如“送餐超时 30 分钟,饭都凉了,炸鸡皮完全不脆”,情感倾向埋在主客观混搭的句子里。酒店评论的典型特征是:名词抽象、评价维度多。比如“隔音差,但床垫很舒服,早餐一般”,这条评论里正面和负面信息同时存在。这种文本如果交给基于情感词典的方法,会出现两个问题:一是领域词互相干扰,“脆”“床垫”“隔音”不在通用词典里,会被降权;二是程度副词、转折词的位置信息完全丢失。“虽然……但是……”结构在酒店评论里非常常见,词典法几乎无法处理。
| 维度 | 外卖评论 | 酒店评论 |
|---|---|---|
| 平均长度 | 15~40 字 | 30~100 字 |
| 高频词 | 包装、骑手、味道、凉了、辣度 | 前台、隔音、早餐、位置、性价比 |
| 情感极性来源 | 服务时效 + 口味体验 | 设施 + 服务 + 周边环境 |
| 转折结构 | 较少 | “但是”“不过”出现频率高 |
| 脏数据常见形式 | “无”“,”“方便” | “房间很大!!!” ”客服不错“ |
这两种数据拼在一起训练深度模型,最直接的收益是数据量翻倍、正则化效果更好。模型必须同时拟合两个领域的不同词汇分布,所以它学到的中间表示会偏向“通用的情感语义”,而不是只记住某几个领域词。
2.2 常见做法:Embedding + 双向 LSTM 或 TextCNN,快速拿到可用的 90%
不引入外部预训练模型的前提下,先用 Word2Vec 或 fastText 在合并后的评论语料上训练词向量,然后接一个双向 LSTM 或 TextCNN,就能稳定拿到 88%~92% 的准确率。这个方案在短文本情感分类里至今仍然能打。选择 TextCNN 的理由是训练快、对短文本噪声鲁棒;选择 BiLSTM 的理由是能利用上下文顺序,酒店评论这种“先贬后褒”的结构里效果略好。用 PyTorch 实现的话,大概长这样:
import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=200, num_filters=128, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.dropout = nn.Dropout(0.3) self.convs = nn.ModuleList([ nn.Sequential( nn.Conv1d(embed_dim, num_filters, kernel_size=k), nn.ReLU(), nn.AdaptiveMaxPool1d(1) ) for k in (2, 3, 4) ]) self.fc = nn.Linear(num_filters * 3, num_classes) def forward(self, x): # x shape: (batch, seq_len) emb = self.embedding(x) # (batch, seq_len, embed_dim) emb = self.dropout(emb) emb = emb.permute(0, 2, 1) # (batch, embed_dim, seq_len) pooled = [conv(emb).squeeze(-1) for conv in self.convs] # 每个元素 shape: (batch, num_filters) out = torch.cat(pooled, dim=1) return self.fc(out)这段代码里,padding_idx=0很重要,它保证所有 padding 位置在nn.Embedding反向传播时梯度为 0,不会把无效位置学成有意义的向量。AdaptiveMaxPool1d(1)把每个卷积核输出的序列压成一个最显著特征,等价于“只要这句话里有某个强情感词模式,就把它提取出来”。多核尺寸 2、3、4 分别对应 bigram、trigram 和 4-gram 模式,比如“不新鲜”是 bigram,“等了一个小时”是 4-gram。这种多尺度卷积是短文本分类里最稳的设定。
2.3 为什么不直接默认上 BERT 微调
BERT 或 RoBERTa 微调在这个任务上确实能到 94%~96%,但它有两个现实门槛:显存占用和推理速度。90% 准确率的模型如果按训练好的模型文件来算,一般百 MB 级就能搞定,用 CPU 也能跑;反过来,BERT-base 单条推理在 CPU 上可能要 50~200 毫秒,遇到批量打标,每秒吞吐量差别很大。另外,外卖评论里大量错别字、拼音、数字符号混杂,BERT 用 WordPiece 切词会把“鸡公煲”切成多段,反而丢失整体语义。Embedding + 卷积的方案在这些场景下容错性更强,因为词向量本身就是从这些噪声里学出来的。结论是:先跑通传统深度模型,再考虑要不要上预训练模型,别一上来就把 BERT 当默认选项。
3. 数据清洗与样本分割:决定 90% 准确率到底是真是假
3.1 评论清洗管线里的 5 个关键步骤
外卖和酒店评论的清洗逻辑不太一样。外卖评论里“、”和“,”经常被当成占位符,酒店评论里“!!!”和空格是高频噪声。先统一清洗,再按领域分别处理特殊规则。下面是一条可复现的清洗函数:
import re def clean_comment(text: str, is_food: bool = True) -> str: text = text.replace('\u3000', ' ').replace('\xa0', ' ').strip() # 合并重复标点,但保留“??”和“!!!”这类情感强度信号 text = re.sub(r'([。!??!,,]{2,})', lambda m: m.group(1)[0], text) # 去掉所有 URL、@用户、以及纯数字串 text = re.sub(r'https?://\S+|@\w+|#\w+#?', '', text) # 外卖评论里“1小时”“30分钟”这类时长词保留,统一口径 if is_food: text = re.sub(r'(\d+)\s*分钟', r'\1min', text) text = re.sub(r'(\d+)\s*小时', r'\1h', text) else: # 酒店评论里房型、价格数字保留,但去掉邮编电话 text = re.sub(r'\d{5,}', '', text) return text.strip()逻辑说明:先把全角空格和不断行空格归一;再把连续标点压成一个,因为情感强度主要通过数量体现而不是单个字符;URL 和 @ 用户在任何领域都是噪声;外卖里把“三十分钟”统一成“30min”,能减少词表膨胀,同时让模型学到“30min”这个时长概念;酒店评论里五位数以上数字基本是电话或邮编,直接删掉,而“328 元”这种保留,因为价格跟评分有相关性。用is_food区分两类数据的清洗细节,避免一个函数硬套两个领域。
3.2 标签怎么定:用评分映射还是用评论内容?
评星映射到二分类(好评/差评)是最常见的做法。1~2 星映射为负向,4~5 星映射为正向,3 星要么丢弃要么丢到“中立”类。从标题看模型大概率做的是二分类,90% 准确率的评价基准里,“中立”样本极可能被合并进负向或正向。如果按 4 星以上算好评,出现的问题是中评里有大量“还行,但没下次”这种隐性负向,模型会在“还行”这类词上学到错误的权重。更合理的做法是把 3 星单独分出来,或者用 3 星以下的样本去重时慎重放行。类别不均衡也要处理,外卖评论总体偏向差评(因为买家主动写评论的动机是不满),酒店评论总体偏向中好评(因为平台会诱导高分评价发帖),两条数据分布合在一起时,要先看类别的绝对数量比再做重采样。
3.3 三个最容易把“准确率”做假的坑
坑一:同一订单产生的中评和追评同时出现。外卖场景里用户先给 1 星,过两小时追加一句“商家联系我退款了”,如果清洗时不按订单号去重,这条样本的正负置信度是互相矛盾的,模型学到的是“退款”这个词等于好评。
坑二:训练集和验证集混入了同一个用户的评论。酒店评论里同一个用户经常一次性写 5 条相同酒店的评论,如果只用train_test_split随机切分,验证集会包含训练集里几乎一模一样的句子,准确率虚高 2~3 个点。按user_id或order_id分组后做 StratifiedSplit,才是真实的泛化性能。
坑三:对 f-string 或缺失值处理不统一。把评论拼接成一行、空评论填充“暂无评价”、数字 0 和字符串 0 混在一起,这些在模型训练里不会报错,但推理时会导致向量维度错位或者全部 pad 成 0。标题里 90% 的准确率,如果没有说明是用什么 split 方式和分组粒度的,这个数字只能当成实验室数字看待。
4. 训练参数怎么设:从 loss、epoch 到学习率,把 90% 复现出来
4.1 训练循环的最小实现,带早停和模型保存
不管用什么模型架构,训练环节需要固定的几个模块:数据加载、优化器、损失函数、评估函数、早停逻辑。下面用 PyTorch 写一个完整最小实现,模型可以自行替换成 2.2 里的 TextCNN 或 BERT 最后一层。
import torch import torch.nn as nn import numpy as np from torch.utils.data import DataLoader, TensorDataset def train_model(model, train_dl, val_dl, epochs=15, lr=2e-3, patience=3): optimizer = torch.optim.AdamW(model.parameters(), lr=lr) loss_fn = nn.CrossEntropyLoss() best_acc, bad_epochs = 0.0, 0 for epoch in range(epochs): model.train() train_loss, correct, total = 0.0, 0, 0 for xb, yb in train_dl: optimizer.zero_grad() logits = model(xb) loss = loss_fn(logits, yb) loss.backward() optimizer.step() train_loss += loss.item() * len(yb) correct += (logits.argmax(1) == yb).sum().item() total += len(yb) # 验证 model.eval() val_correct, val_total = 0, 0 with torch.no_grad(): for xv, yv in val_dl: logits_v = model(xv) val_correct += (logits_v.argmax(1) == yv).sum().item() val_total += len(yv) val_acc = val_correct / val_total print(f"epoch={epoch+1}, train_acc={correct/total:.4f}, val_acc={val_acc:.4f}") if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_model.pt") bad_epochs = 0 else: bad_epochs += 1 if bad_epochs >= patience: print("early stop") break参数说明:lr=2e-3在 TextCNN + AdamW 下是常用起点,换成 BERT 就要降到2e-5级别,因为预训练模型底层的拟合不需要大步长。CrossEntropyLoss自带 softmax,不要在模型的forward里额外接 softmax,否则训练时会重复计算且梯度数值会不稳定。patience=3意味着验证集连续 3 个 epoch 没有刷新最优,就提前结束,这个数值对情感分析是经验值,数据噪声大时可上调到 5~7。torch.save(state_dict)只保存权重模型,不久留 optimizer 状态,方便换 batch size 后继续评估。
4.2 epoch 设多少合适,以及为什么“跑满 15 轮”不是好策略
对于 2 万~5 万条中文评论的中等规模语料,TextCNN 通常在 6~8 个 epoch 就能收敛,BiLSTM 需要 10 个 epoch 左右,BERT 微调 3~4 个 epoch 就过了。如果代码里写死epochs=100不带早停,训练后期会进入过拟合阶段,验证集准确率曲线像过山车,最优模型反而出现在 7 轮。常见错误是把早停的 patience 设在 10 以上,等于接受了 10 轮验证集没有提升,浪费时间。另外要注意 loss 值下降曲线是否平滑:如果 epoch 1 的 loss 是 0.695,epoch 2 是 0.693,说明模型根本没学进去,这时问题多半出在词表没建好或 pad 序列太长。把seq_len截断到 64 之间比较合适,外卖评论平均长度 20 出头,酒店评论 60 左右,128 的上限浪费显存。
4.3 90% 准确率的实际含义:宏平均和加权平均的差别
如果数据里 70% 是好评、30% 是差评,一个“永远预测好评”的模型准确率就是 70%。真正达到 90% 准确率时,必须检查差评类的 precision 和 recall。差评样本预测召回率如果只有 40%,那么在业务上完全不可用——所有差评都被过滤掉了。更好的评价方式是算加权 F1,或者直接看每类的 recall。训练结束后面要向业务方汇报时,只报 acc 和 recall 两个指标,对方才理解模型是不是真的在“找不满”。表格里应记录:类别、样本数、precision、recall、f1-score。如果差评 recall 低于 75%,通常需要加差评样本的权重,例如把 loss 计算时差评样本乘以 1.3 倍。
5. 拿到压缩包以后怎么复现推理:拆 zip、加载权重、跑通单条预测
5.1 zip 包里的东西一般是这些,缺了也能自己补
标题里的.zip压缩包,内部常见不只会有一个模型文件,还可能包含一个跑通推理的示例。逐个排查:.pt/.pth权重文件、词表和 id 映射的 json、一个 train.py 或 inference.py 脚本、可能还有 requirements.txt。如果压缩包里只有模型文件缺少词表,那加载时就很麻烦,因为 embedding 层的索引需要和训练词表一一对应。一种备份方案是看模型中embedding.weight的shape[0],用字数反推原始词表规模,再从 pickle 格式的字典里恢复映射对。下面是推理端的一个标准模板:
import json import torch from transformers import BertTokenizer # 如果用传统模型则跳过 with open("vocab.json", "r", encoding="utf-8") as f: vocab2idx = json.load(f) # 假设模型输入是词索引序列 def predict_one(text: str, model, max_len=64, device="cpu"): tokens = list(text)[:max_len] # 逐个字符切,按训练时一样的切分方式 ids = [vocab2idx.get(t, vocab2idx.get("<unk>", 1)) for t in tokens] ids += [0] * (max_len - len(ids)) # 0 是 padding x = torch.tensor([ids], dtype=torch.long, device=device) with torch.no_grad(): logits = model(x) prob = torch.softmax(logits, dim=-1)[0] label = int(prob.argmax()) return label, prob[label].item()猜测 zip 里提供训练代码的时机,还是要检查字符切分方式是否和训练完全一致。如果训练时是 jieba 切词,推理时list(text)会把“不高兴”切成“不”、“高”、“兴”三个 token,与训练分布完全不一致。最快的验证方法是随机拿一条训练数据集,跑一遍predict_one,看是否复现已知标签。不匹配时优先回到训练脚本里查找tokenizer是怎么定义的。加载本地模型有一套常用的可靠方法:torch.load(model_path, map_location='cpu')。
5.2 从展示准确率到版控,建议把模型阈值调成非 0.5
90% 准确率的模型直接上线,业务端会把“中性”评论也强制分到好评或差评,带来大量误伤。大多数生产系统实际的做法是设置信度阈值,比如 0.75 以上才给出极性判断,落在中间地带的数据走人工审核。优化目标变成“确定性打标”,准确率反而会抬高。输出时把prob暴露给调用方,由业务端决定阈值。代码里甚至可以直接忽略 0.5,写成if prob > 0.75: 正、elif prob < 0.25: 负、else: 不确定。这是 5 年经验以上的人自然会加的工程判断。
5.3 常见报错和修复建议
报错 1:size mismatch for embedding.weight。原因是一个新词被加入词表后重新建过 embedding,而权重文件是老版本的。查一下vocab_size和embedding.weight.shape[0],发现不一致时,要么扩容词表,把新行初始化为零向量,要么回到备份的 vocab.json 构建过滤规则。
报错 2:pickle 加载权重时报UnicodeDecodeError。训练环境可能是 Python 2,或者用了pytorch 0.4的序列化格式,加载时加encoding='bytes',但更推荐重新用训练代码做一次 checkpoint 转存,直接帮未来节省 2 小时排障。
报错 3:CPU 推理内存溢出。用torch.no_grad()不会把梯度图存下来,就能把内存占用砍到训练时的三分之一。Transformer 模型开启torch.inference_mode(),比no_grad更低的开销。
6. 验证 90% 是否可信:按领域拆分评估与误报样本定位
按外卖、酒店两个子集分别算出准确率,是最快的可信度检查手段。如果外卖评论准确率 94%、酒店评论准确率 86%,整体 90%,说明模型偏向外卖场景。代码上可以先跑一个分组评估:
from collections import defaultdict def evaluate_per_domain(model, dataloader, domain2idx, device="cpu"): stats = defaultdict(lambda: {"correct": 0, "total": 0}) model.eval() with torch.no_grad(): for xb, yb, d in dataloader: # 数据加载器里要有 domain 字段 preds = model(xb).argmax(1) for i, dom_id in enumerate(d): key = domain2idx[dom_id.item()] stats[key]["total"] += 1 if preds[i] == yb[i]: stats[key]["correct"] += 1 return {k: v["correct"] / v["total"] for k, v in stats.items()}按领域回归后,如果差距超过 5 个百分点,需要做两件事:一是检查训练集里两领域的样本比例,如果外卖有 3 万条而酒店只有 5 千条,模型天然偏向外卖;二是把酒店的增强样本提纯,比如复制负向样本并在评论首尾拼接同义词替换后的变体,缓解样本倾斜。真正到了业务侧,还可以利用predict_one返回的概率值来启动“人工复核”条件:当外卖评论的概率落在 0.4 到 0.6 区间,送去人工;酒店评论的概率阈值可以改成 0.3 到 0.7,因为酒店表达的褒贬混合程度高。最后也可以借助Counter(误报文本中的词)定位高频错误词根,常见的是“一般”和“还行”,前者被模型归为负向但实际是中性,后者被归为正向但在外卖场景里是明显差评。把这些词加入训练语料并重新微调,比单纯改阈值更治本。哪一天模型开始把“包装完好”当正面,而完全不理会口味和送餐速度,你就知道该往训练集里补充更多“包装感受”相关的负样本了。
本文还有配套的精品资源,点击获取