简介:基于深度学习的情感分析模型,主要面向自然语言处理入门者、算法工程师以及需要处理电商、外卖等评论数据的分析场景,用于从文本中快速识别用户正面、负面或中性情感。资源包内含4个文件:2个Python脚本分别负责调用模型和运行分类预测,1个模型ZIP包存放了训练完成的权重,另有1个Markdown文档简述环境配置与使用方式;整体仅458KB,轻量且易于部署。模型在外卖评论和酒店评论数据上完成训练,整体准确率达90%左右,可帮助开发者快速搭建情感分类流程,也可作为进一步微调和迁移学习的基础。已有73人学习/下载;对希望了解CNN/LSTM等深度网络在短文本分类中如何应用、并快速获得可运行方案的学习者而言,这套小型模型提供了较为直观的参考与练习素材。
1. 基于深度学习的情感分析模型,准确率“90%左右”是怎么算出来的
看到“基于深度学习的情感分析模型,经过外卖评论和酒店评论训练,准确率在90%左右”这个描述时,我的第一反应不是喝彩,而是反问:这个90%是在什么切分规则、什么类别比例下得到的?外卖和酒店评论是典型的短文本,平均一句只有二十到四十个字,“麻辣烫太咸”“房间隔音一般”,正负情感往往藏在一两个副词里。对这种数据,一个Embedding加TextCNN或BiLSTM的深度学习情感分析模型跑到85%很容易,跨过90%就需要数据质量、训练参数和样本切分都配合好。太多人解压模型包后跑不出原数字,原因多半不在模型结构,而在训练集验证集泄漏或类别严重不平衡。所以这一篇把整条链路写清楚:从模型选型、预处理、切分、训练到导出,最后给一个可验证的评判标准。
2. 深度学习情感分析模型的架构选择:外卖短文本里的 TextCNN、BiLSTM 与 BERT 边界
2.1 评论数据的长度分布,先决定要不要上深层网络
先看数据形态。“送得快,态度好”这种评价,信息点基本就落在双字词或三字词上;TextCNN用2、3、4三种卷积核宽度扫一遍,能直接抓“送得快”“态度好”。酒店评论会拉长,“入住时前台升级了房型,但第二天马桶堵了半小时没人管”,这是明显的转折关系,责任落在词序信息上,BiLSTM比TextCNN更适合抓这种先扬后抑。
几十万条这种量级以下,不必直接上BERT。BERT的优势在长依赖和深层语义,但外卖评论里的活跃表达、口语缩写,预训练模型未必比领域内Embedding更好使。我的习惯是先造一个TextCNN基线,验证数据本身能否被线性分离;如果基线已经到88%-90%,再换BERT,收益可能只有零点几个点,训练成本却变成几十倍。
2.2 Embedding 层在情感分析模型里承担什么职责
输入到模型的不是汉字,而是字或词在词汇表里的编号。Embedding层把这个编号映射成一个可学习的向量。外卖、酒店评论里常见的“不错”“还行”“一般”三个词,语义上有细微差异,训练过程中它们对应的向量会被标签拉开,又因为语境相近而互相靠近。这就是文本分类里最基础的知识表示方式。
如果语料只有两三万条训练样本,从零训练Embedding是安全的;如果语料大于二十万条,可以考虑载入在通用中文语料上预训练的词向量,再继续微调。微调时需要注意一个尺度问题:Embedding学习率不该和全连接层完全一样,否则新样本少时,词向量很容易在头几个epoch被冲乱。常见做法是给Embedding配一个更小的学习率,或者直接使用freeze=False的nn.Embedding.from_pretrained,让它在训练中缓慢更新。
2.3 一个能直接跑起来的 PyTorch TextCNN 基准
import torch import torch.nn as nn class TextCNNForSentiment(nn.Module): def __init__(self, vocab_size, embed_dim=100, num_filters=128, filter_sizes=(2, 3, 4), num_classes=2, dropout=0.3, pad_idx=0): super().__init__() # embedding 层把词索引转成向量,padding_idx 让填充位不参与更新 self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=pad_idx) # 多宽度一维卷积,分别捕捉 2、3、4 个词的局部短语 self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k) for k in filter_sizes ]) self.fc = nn.Linear(num_filters * len(filter_sizes), num_classes) self.dropout = nn.Dropout(dropout) def forward(self, x): # x 是已经转成索引的句子,shape 为 (batch, seq_len) emb = self.embedding(x).transpose(1, 2) # (batch, embed_dim, seq_len) # 每个卷积核输出后做 1D max pooling,取出该宽度下最强烈的特征 pooled = [torch.max(torch.relu(conv(emb)), dim=2)[0] for conv in self.convs] out = torch.cat(pooled, dim=1) return self.fc(self.dropout(out))embed_dim在十万级词表、两三万样本时,开到100到200比较平衡;filter_sizes=(2,3,4)用来覆盖中文双字词和三字词;pad_idx=0表示词表第0号位置永远留给填充符。最后dropout加在全连接之前,避免模型记住训练集里的个别措辞。
2.4 在什么节点换 BiLSTM 或 BERT
当验证集显示TextCNN的差评召回率偏低,通常不是卷积核少,而是转折关系没被建模。BiLSTM对评论逐词输入,前向、反向各看一遍,能筛出“位置好但床单不干净”这种先扬后抑。训练速度大约比TextCNN慢两到三倍,对入门级深度学习环境也能接受。
transformers库封装了BERT微调流程:AutoTokenizer负责切词,AutoModelForSequenceClassification负责分类头。这里最常见的问题是:BERT的tokenizer和TextCNN的jieba词表完全是两套东西,导出模型包时必须把对应的tokenizer目录一起带进去,否则拿到的模型在推理时根本没法对文本定长和padding。
| 模型 | 训练速度 | 抓局部短语 | 抓转折/长依赖 | 最低可用数据量 |
|---|---|---|---|---|
| TextCNN | 快 | 强 | 弱 | 几千条 |
| BiLSTM | 中等 | 中 | 中强 | 几千条 |
| BERT微调 | 慢 | 强 | 强 | 建议两万条以上 |
数据量不够五位数时,直接上BERT往往得到的是过拟合出来的高分,而不是可迁移的语义能力。
3. 外卖和酒店评论的预处理与样本切分:情感分析模型的入坑重点
3.1 清洗:不要用通用停用词表把关键词删掉
爬下来的原始评论包含全角空格、繁体字、网址、用户名日志,以及表情符号。我的清洗顺序是:把全角字符统一为半角;连续重复标点压缩成单个;移除URL和@用户;表情符号映射成一个统一的<emoji>token,因为“好吃加上笑脸”和干巴巴的“好吃”情感强度并不一样。
容易操作错误的一步是直接套用新闻语料的停用词表,把“不”“很”“没”这类词全部删掉。情感分析模型里,“不太好吃”“不会再来”的关键恰恰是否定词。外卖和酒店评论里出现“不”的概率不低,删除后情感信号会被掏空。清洗脚本必须和训练脚本放在同一个目录,否则别人拿到zip包后用自己的清洗逻辑跑一遍,分数立刻掉下去。
3.2 分词与词表构建的细节
外卖和酒店评论需要分词。“海底捞”“螺蛳粉”“榻榻米”在通用词库中会被拆碎。做法是追加领域词典,比如海底捞 100 nz、螺蛳粉 100 nz,让分词器优先把它们当成一个词。数字和金额可以替换成统一标记,避免词表膨胀,同时保留“等半小时”“一百二十块”这类数量语义。
词表构建要注意两点:低频词过滤阈值设为2或3,低于阈值的并入<unk>;固定word2idx的顺序,训练前写入vocab.json。排序方式一旦改变,加载模型时Embedding矩阵就错位了。
句子截断长度方面,可以参考下面这张表:
| 数据来源 | 建议最大长度 | 理由 |
|---|---|---|
| 外卖评论 | 48 | 大部分评价在20个词以内,48足够覆盖尾部 |
| 酒店评论 | 64 | 体验描述更长,需要留出转折句位置 |
| 混合训练 | 64 | 统一长度方便batch计算,过长信息反而分散 |
截断太短会切断转折,截断太长会填满无效padding,训练速度和效果都受影响。
3.3 数据集切分:按评论随机切会把准确率训得虚高
如果同一家店的80条评论,70条在训练集、10条在验证集,模型很容易记住店名和平台默认的回复模板,验证集分数明显虚高。更稳妥的做法是按店铺分组,再用时间顺序让早一点的评论进训练、晚一点的进验证。
import pandas as pd from sklearn.model_selection import GroupShuffleSplit df = pd.read_csv("reviews.csv") # 字段:text, label, shop_id, date df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date") split = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(split.split(X=df["text"], groups=df["shop_id"])) df_train = df.iloc[train_idx] df_val = df.iloc[val_idx]groups=df['shop_id']保证同一家店的所有评论进入同一侧,这正是验证模型是否真的“学会情感”而不是“背下店铺”的关键。test_size=0.2在类别不平衡时不一定让正负样本都占20%,切完后要检查验证集里好评和差评比例,若偏差过大再考虑分层抽样。
3.4 标签口径:五星折算正负情感时的规则要先定
外卖评论通常给1到5星,酒店评论也一样。常见映射是4星和5星归为好评,1星和2星归为差评,3星悬空,直接过滤或单独作为中性样本。做二分类时我通常把3星评论丢弃,因为3星用户的心态是“能接受但谈不上满意”,强行贴标签只会给模型制造噪声,训练出来的模型对2星和4星辨析更稳定。
还要处理文本和星级冲突的样本,比如“一般般吧”配上4星,或“非常好”配上2星。这类冲突样本最好先手动看两百条,理解数据采集时的标注口径,再决定是清洗还是保留。
4. 逼近90%准确率的训练配置:epoch、损失函数、类别不平衡怎么配合
4.1 超参速查表
一套适合外卖和酒店评论混合数据的默认参数如下,可以直接作为起点:
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| seq_len | 48到64 | 外卖取48,酒店取64 |
| embed_dim | 100到200 | 三万条数据用100即可 |
| batch_size | 32到64 | 显存小就设32 |
| learning_rate | 1e-3,AdamW | 微调BERT时改为2e-5 |
| max_epochs | 20 | 配合早停,不要死等参数跑满 |
| dropout | 0.3到0.5 | 数据越少越往0.5靠 |
| weight_decay | 1e-4到1e-5 | 防止有过大的权重集中于个别词 |
BERT微调的学习率分布跟CNN完全不同,把1e-3直接套上去,前几个step就会发散。
4.2 类别不平衡:只算准确率会掩盖差评召回率
外卖和酒店评论里,差评占比往往在15%到30%。九成好评的情况下,模型把所有评论都判断为好评也能有约90%准确率,但这不是一个有实用价值的情感分析模型。处理上做两件事:给损失函数加上类别权重;验证指标改成macro-F1或差评召回率。
from sklearn.metrics import f1_score def evaluate_f1(model, val_loader, device): model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for ids, labels in val_loader: logits = model(ids.to(device)) all_preds.extend(logits.argmax(dim=-1).cpu().tolist()) all_labels.extend(labels.cpu().tolist()) return f1_score(all_labels, all_preds, average="macro")average="macro"会对好评和差评两类分别计算F1再取平均,避免了多数类主导分数。只用准确率选checkpoint,最可能选到“全猜好评”的那个epoch。
4.3 epoch 与早停:训练不是越久越好
短文本情感模型通常在8到12个epoch附近达到峰值,继续训练会让loss下降但验证F1开始震荡。我的做法是记录每个epoch的macro-F1,连续三个epoch没有超过历史最优,就回退到最优checkpoint,并把学习率减半再继续。整个训练骨架如下:
criterion = nn.CrossEntropyLoss(weight=class_weights.to(device)) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) best_f1 = 0.0 for epoch in range(20): model.train() for ids, labels in train_loader: optimizer.zero_grad() logits = model(ids.to(device)) loss = criterion(logits, labels.to(device)) loss.backward() optimizer.step() current_f1 = evaluate_f1(model, val_loader, device) if current_f1 > best_f1: best_f1 = current_f1 torch.save(model.state_dict(), "best_model.pt")class_weights要用验证集也可见的类别比例计算,而不是全量数据,避免验证集的类别分布被重复利用。best_model.pt只保存state_dict,加载时先按config.json重建模型结构,再调用load_state_dict。
4.4 预训练词向量与Embedding微调的边界
如果能拿到在餐饮、旅游语料上预训练的词向量,用nn.Embedding.from_pretrained(vectors, freeze=False)初始化是加分的。freeze=False意味着训练时继续更新向量,领域内词的语义才会往正负情感方向偏移。如果数据量小到几千条,建议freeze=True,只让网络上层适配,防止词向量被少量样本带偏。
还有一个小技巧是给Embedding做更激进的正则化,比如词向量Dropout设到0.2,使训练过程不再依赖个别高权重词。这个操作对短文本情感分析往往比对CNN本身更敏感。
5. 把情感分析模型做成可交付的 zip 形态:TorchScript 导出与置信度兜底
5.1 打包时最不该丢掉的两样东西
一个能跑起来的模型包,不只是权重文件。我一般按这个结构整理:
sentiment_analysis/ ├── best_model.pt # 网络权重 ├── vocab.json # word2idx 映射,顺序必须与训练时一致 ├── config.json # seq_len, embed_dim, num_filters 等超参 ├── preprocessing.py # 清洗和分词逻辑,与训练时完全一致 ├── predict.py # 加载模型并输出情感概率 └── requirements.txt最容易出差错的是vocab.json的顺序。训练时用enumerate(vocab)从0开始编号,如果保存前重新排序了词表,加载模型时Embedding矩阵就整体错位。词表要在第一次训练前固定,训练和推理共用同一个word2idx对象。
5.2 用 TorchScript 导出最小推理单元
不依赖GPU的推理环境,把模型转成TorchScript比直接分发.pt加.py更省事:
import torch from model import TextCNNForSentiment model = TextCNNForSentiment(vocab_size=50000, embed_dim=100) model.load_state_dict(torch.load("best_model.pt")) model.eval() dummy_x = torch.randint(0, 50000, (1, 48)) traced = torch.jit.trace(model, dummy_x) traced.save("model_jit.pt")对外发布时,推理端只需加载model_jit.pt和vocab.json,不需要再引入模型定义的Python类。dummy_x的batch size为1,trace记录了这个输入形状,如果上线后一次性要处理大batch,最好在trace时把batch设为8或16,或者直接改用ONNX。
5.3 置信度兜底:把模糊评论挡在自动判断之外
模型输出的是各类别概率,很多时候两个类别的差距并不大。酒店评论里的“升级了房型,但隔音很差”属于混合情绪,强分成正或负都会导致投诉。推理时加一道阈值判断,只对置信度高的样本做自动标注:
probs = torch.softmax(logits, dim=-1) max_prob, pred = torch.max(probs, dim=-1) if max_prob.item() < 0.7: return {"label": "uncertain", "prob": max_prob.item(), "sentiment": None} return {"label": "positive" if pred == 1 else "negative", "prob": max_prob.item()}阈值不是拍脑袋定的,而是在验证集上计算不同阈值下的覆盖率,再挑一个既保留90%准确率又不放走太多模糊样本的数值。评论数据里比例不低的二星转三星软文、默认好评和自动追评,都会让模型概率接近0.5,这一道兜底往往比再训练一轮更值钱。
本文还有配套的精品资源,点击获取