
简介本资源面向希望快速上手情绪识别与情感分析的开发者与研究人员提供一套基于Transformer的完整项目实战方案覆盖从数据预处理、模型训练到评估优化的全流程适合具备一定深度学习基础、想深入理解自注意力机制在情感任务中应用的学习者。压缩包共19个文件以14个Python源码文件为主辅以3个pkl数据文件和2个md说明文档整体约506KB源码涵盖模型定义、训练脚本、数据集加载与集成等模块pkl文件则承载训练、验证与测试数据。目前已有154人学习下载。项目围绕MOSEI_UMONS多模态情感数据集展开涉及文本、语音、视频等多模态信号的处理读者可借助源码与流程教程掌握数据清洗、分词向量化、模型构建调优及准确率、F1分数等指标评估方法并理解情感极性判断的实现思路对初学者和研究人员均具参考价值。1. 情绪识别项目拆包Transformer 做情感分析到底能不能直接落地上周帮一个做在线教育的朋友看他们的课堂反馈系统他们想从学生打字的口语化评论里自动判断情绪倾向试过词典匹配和传统机器学习遇到反讽和长句就翻车。这类场景其实特别适合用 Transformer 做情绪识别——它天生擅长捕捉上下文语义不像词袋模型那样把不是很开心和很开心当成差不多的东西。这份资源就是一套基于 Transformer 实现情绪识别与情感分析的完整项目源码加流程教程覆盖数据预处理、模型搭建、训练调参到推理部署的链路。适合两类人一是想拿一个能跑通的 NLP 项目练手的学生或转行者二是需要快速搭一个情感分析基线再迭代的工程师。下面我按自己拆包复现的顺序把关键环节和踩过的坑讲清楚。2. 环境搭建与数据准备从零把项目跑起来2.1 依赖版本与目录结构确认拿到压缩包先别急着 pip install第一步是看目录结构和 requirements。这类项目常见的坑是作者本地环境和你不一样torch 版本差一个大版本Transformer 相关的 API 就可能报错。我一般先做三件事确认 Python 版本、锁定 torch 和 transformers 版本、检查有没有预训练权重文件。# 查看项目结构重点关注 data、models、configs 三个目录 unzip 情绪识别-基于Transformer实现的情绪识别情感分析算法.zip -d emotion_transformer cd emotion_transformer find . -maxdepth 2 -type d | sort # 确认 Python 版本建议 3.8 到 3.10 python --version # 查看依赖清单 cat requirements.txt逻辑说明find只列两层目录避免输出太乱重点确认数据放哪、模型代码在哪、配置文件在哪。参数上-maxdepth 2控制深度-type d只看目录。如果 requirements 里 torch 写的是1.8这种模糊版本建议手动固定成你环境里已验证的版本比如torch1.13.1否则不同机器装出来的结果可能不一致。2.2 数据集格式与标签映射情感分析项目的数据集通常是 CSV 或 JSON字段一般是text和label。这里有个容易被忽略的点标签到底是二分类正面/负面还是多分类喜、怒、哀、惧等。情绪识别和情感分析经常被混用但前者粒度更细。拆包后先看数据样例确认标签数量和含义。import pandas as pd # 读取训练数据注意编码中文数据常见 utf-8 或 gbk df pd.read_csv(data/train.csv, encodingutf-8) print(df.head()) print(标签分布) print(df[label].value_counts()) # 建立标签到 id 的映射顺序固定推理时必须一致 label2id {label: idx for idx, label in enumerate(sorted(df[label].unique()))} id2label {v: k for k, v in label2id.items()} print(label2id)逻辑说明value_counts()用来判断类别是否均衡如果某类样本极少训练时会出现模型偏向多数类的情况后面需要加权或重采样。label2id用sorted保证顺序稳定这一点很关键——训练和推理如果映射顺序不一致预测结果会整体错位而且不报错属于典型的玄学 bug。参数上encoding要根据实际文件调整读出来乱码就换gbk或utf-8-sig。2.3 分词器加载与文本编码Transformer 不能直接吃原始文本必须经过 tokenizer 转成 input_ids 和 attention_mask。这一步的坑集中在最大长度设置上设太短长文本被截断丢失信息设太长显存爆炸且大量 padding 浪费算力。from transformers import AutoTokenizer # 加载预训练分词器常见做法是用 bert-base-chinese 或 hfl/chinese-roberta-wwm-ext tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) def encode(texts, max_len128): # truncation 截断超长文本padding 补齐到统一长度 return tokenizer( texts, max_lengthmax_len, truncationTrue, paddingmax_length, return_tensorspt ) sample encode([这个课程讲得真的很清楚收获很大]) print(sample[input_ids].shape) print(sample[attention_mask])逻辑说明truncationTrue保证超过 max_len 的文本被裁掉paddingmax_length让一个 batch 内所有样本长度一致。max_len128是中文短文本的常用起点如果你的评论普遍较长可以调到 256但要同步关注显存占用。attention_mask告诉模型哪些位置是真实 token、哪些是 padding不能省。常见做法是先统计一下训练集文本长度的分布取覆盖 95% 样本的长度作为 max_len比拍脑袋定值靠谱。3. 模型搭建与训练Transformer 分类头怎么接、参数怎么调3.1 基于预训练模型接分类头这类项目一般不会从零训练 Transformer而是加载预训练权重再微调。核心改动就是在[CLS]位置的输出后面接一个全连接层做分类。理解这一点模型代码就不难读。import torch import torch.nn as nn from transformers import AutoModel class EmotionClassifier(nn.Module): def __init__(self, model_name, num_labels, dropout0.3): super().__init__() # 加载预训练编码器不包含下游任务头 self.encoder AutoModel.from_pretrained(model_name) hidden_size self.encoder.config.hidden_size # dropout 防止过拟合小数据集上尤其重要 self.dropout nn.Dropout(dropout) # 分类头hidden_size - num_labels self.classifier nn.Linear(hidden_size, num_labels) def forward(self, input_ids, attention_mask): outputs self.encoder(input_idsinput_ids, attention_maskattention_mask) # 取 [CLS] 位置的向量作为整句表示 cls_vector outputs.last_hidden_state[:, 0, :] logits self.classifier(self.dropout(cls_vector)) return logits逻辑说明last_hidden_state[:, 0, :]取的是序列第一个位置的输出对应 BERT 的[CLS]token它被设计用来聚合整句语义。dropout0.3是经验值数据量小就调大一点数据量大可以降到 0.1。num_labels必须和前面label2id的长度一致否则训练时 loss 计算会直接报维度错误。如果你的项目用的是 RoBERTa 类模型取[CLS]的方式一样但要注意部分模型没有token_type_ids传参时别硬塞。3.2 训练循环与学习率设置微调 Transformer 的学习率要比从头训练小得多常见范围是 2e-5 到 5e-5。设大了会把预训练学到的语义直接冲掉表现为 loss 震荡不下降。from torch.optim import AdamW from transformers import get_linear_schedule_with_warmup device torch.device(cuda if torch.cuda.is_available() else cpu) model EmotionClassifier(bert-base-chinese, num_labelslen(label2id)).to(device) # 微调学习率取 2e-5权重衰减防过拟合 optimizer AdamW(model.parameters(), lr2e-5, weight_decay0.01) epochs 5 total_steps len(train_loader) * epochs # 前 10% 步数做 warmup让学习率从 0 平滑升到设定值 scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(0.1 * total_steps), num_training_stepstotal_steps ) model.train() for epoch in range(epochs): for batch in train_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[label].to(device) optimizer.zero_grad() logits model(input_ids, attention_mask) loss nn.CrossEntropyLoss()(logits, labels) loss.backward() # 梯度裁剪防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step() print(fepoch {epoch1} done)逻辑说明AdamW比普通 Adam 多了正确的权重衰减实现是微调 Transformer 的标配。warmup的作用是训练初期不让学习率一下冲到最大避免预训练权重被破坏。clip_grad_norm_把梯度范数限制在 1.0是防梯度爆炸的后悔药。参数上epochs5是常见起点如果验证集 loss 在第 3 轮就开始上升说明过拟合了要早停或加正则。weight_decay0.01对 BERT 类模型比较通用。3.3 验证集评估与指标选择训练完不能只看 loss要看准确率和 F1。情绪识别如果类别不均衡准确率会骗人——全预测成多数类也能有 70% 以上。from sklearn.metrics import classification_report, f1_score def evaluate(model, val_loader, device): model.eval() preds, trues [], [] with torch.no_grad(): for batch in val_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) logits model(input_ids, attention_mask) pred torch.argmax(logits, dim-1).cpu().numpy() preds.extend(pred) trues.extend(batch[label].numpy()) # macro F1 对每个类别平等看待适合不均衡数据 print(classification_report(trues, preds, target_nameslist(label2id.keys()))) return f1_score(trues, preds, averagemacro)逻辑说明model.eval()关闭 dropouttorch.no_grad()省显存。averagemacro计算的是每个类别 F1 的算术平均少数类的表现不会被多数类淹没比micro更适合评估情绪识别这种可能不均衡的任务。如果某个类别 F1 明显偏低回去看那一类的样本量和文本特点往往是样本太少或标注噪声大。4. 推理部署与效果验证模型训完怎么用、怎么确认没跑偏4.1 单条与批量推理封装训练脚本跑通只是第一步真正要用起来得有一个干净的推理接口。我一般会把 tokenizer 和模型打包成一个类避免每次调用都重新加载。class EmotionPredictor: def __init__(self, model_dir, model_namebert-base-chinese, max_len128): self.device torch.device(cuda if torch.cuda.is_available() else cpu) self.tokenizer AutoTokenizer.from_pretrained(model_name) self.model EmotionClassifier(model_name, num_labelslen(label2id)) # 加载微调后的权重 self.model.load_state_dict(torch.load(f{model_dir}/best_model.pt, map_locationself.device)) self.model.to(self.device).eval() self.max_len max_len def predict(self, texts): if isinstance(texts, str): texts [texts] enc self.tokenizer(texts, max_lengthself.max_len, truncationTrue, paddingTrue, return_tensorspt) with torch.no_grad(): logits self.model(enc[input_ids].to(self.device), enc[attention_mask].to(self.device)) probs torch.softmax(logits, dim-1) preds torch.argmax(probs, dim-1).cpu().numpy() return [(id2label[p], float(probs[i][p])) for i, p in enumerate(preds)]逻辑说明load_state_dict加载的是微调后的权重map_location保证在 CPU 机器上也能加载 GPU 训出来的模型。softmax把 logits 转成概率返回置信度方便做阈值过滤——置信度低于某个值的样本可以转人工复核。参数上paddingTrue在推理时按 batch 内最长样本补齐即可不用强制到 max_len省一点算力。4.2 用边界样本验证模型真实能力模型在测试集上指标好看不代表线上能用。我习惯准备一批边界样本手动过一遍包括反讽、双重否定、混合情绪。predictor EmotionPredictor(outputs) test_cases [ 这课讲得也就那样吧不能说差, # 弱负面容易判成中性 不是不好是特别好, # 双重否定 呵呵真是绝了, # 反讽依赖语境 内容还行就是老师语速太快了, # 混合情绪 ] for text, (label, conf) in zip(test_cases, predictor.predict(test_cases)): print(f{text} - {label} ({conf:.3f}))逻辑说明这几类样本是情感分析的经典难点。反讽和双重否定依赖深层语义如果模型在这类样本上置信度普遍偏低或判错说明训练数据的多样性不够需要补充类似语料。置信度低于 0.6 的样本建议不要直接采信可以设一个阈值走人工。这一步不是走形式是判断这个模型能不能上线的关键。4.3 保存与加载的版本一致性最后强调一个容易翻车的点保存模型时要把label2id一起存下来。只存权重换台机器推理时标签顺序对不上结果全错还不报错。import json # 保存权重和标签映射 torch.save(model.state_dict(), outputs/best_model.pt) with open(outputs/label_map.json, w, encodingutf-8) as f: json.dump({label2id: label2id, id2label: id2label}, f, ensure_asciiFalse) # 加载时先读映射再建模型 with open(outputs/label_map.json, encodingutf-8) as f: label_map json.load(f) id2label {int(k): v for k, v in label_map[id2label].items()}逻辑说明ensure_asciiFalse保证中文标签正常写入不然会变成 unicode 转义。加载时id2label的 key 从 JSON 读出来是字符串要转回 int否则按 key 取值会取不到。这个细节不注意推理时要么报 KeyError要么静默返回错误标签。5. 避坑与常见问题排查这些坑我替你踩过了5.1 显存不足但 batch size 已经调到 1现象训练一开始就报 CUDA out of memory把 batch size 降到 1 还是爆。原因通常不是 batch 太大而是 max_len 设得过长或者模型没冻结底层参数。解决先把 max_len 从 256 降到 128 甚至 64 试如果还不行考虑冻结预训练编码器的前几层只训练顶层和分类头显存占用能明显下降。另外检查是不是在验证阶段忘了torch.no_grad()那也会持续占显存。5.2 loss 不下降或直接变 nan现象训练几个 step 后 loss 变成 nan或者一直卡在 0.69 附近不动二分类的随机水平。原因多半是学习率太大把预训练权重冲坏了或者数据里有空文本导致 tokenizer 输出全 padding。解决学习率降到 1e-5 甚至 5e-6 再试在数据加载时过滤掉长度为 0 的文本检查标签有没有越界或 -1 这种非法值。梯度裁剪也加上max_norm 设 1.0。5.3 验证集指标很高但实际用起来很差现象验证集准确率 95%但拿真实评论去测错得离谱。原因通常是训练集和验证集同分布但和真实场景分布不一致或者数据泄漏——验证集样本混进了训练集。解决确认切分时没有重复样本用一批完全来自真实场景的样本做独立测试检查验证集是不是从训练集里随机抽的如果是按时间切分更贴近真实使用。这个坑最隐蔽指标好看容易让人放松警惕。5.4 中文分词器加载报错或输出异常现象AutoTokenizer.from_pretrained报连接错误或者加载后编码结果全是[UNK]。原因一是模型名写错二是本地没有缓存且网络受限三是用错了分词器比如拿英文分词器处理中文。解决确认模型名拼写常见中文模型是bert-base-chinese、hfl/chinese-roberta-wwm-ext提前把模型下载到本地目录用本地路径加载中文一定要用中文预训练模型对应的分词器别混用。5.5 推理速度慢到无法上线现象单条预测要几百毫秒甚至更久。原因是没有用 GPU、没有开 eval 模式、或者每次预测都重新加载模型。解决确认模型在 GPU 上且调用了.eval()把模型加载放在服务启动时做一次不要每次请求都加载批量预测比逐条快很多能攒批就攒批。如果还是慢可以考虑把模型换成更小的轻量 Transformer或者做量化。6. 进阶技巧用置信度阈值和错误分析把模型再提一档模型跑通之后真正拉开差距的是错误分析和阈值策略。我一般会先把验证集里所有预测错误的样本导出来按类别分组看往往能发现系统性问题——比如所有带但是的句子都判反了说明模型没学好转折关系这时候补这类语料比盲目加数据有效得多。import pandas as pd def error_analysis(model, val_loader, device, texts): model.eval() rows [] idx 0 with torch.no_grad(): for batch in val_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) logits model(input_ids, attention_mask) probs torch.softmax(logits, dim-1) preds torch.argmax(probs, dim-1).cpu().numpy() confs probs.max(dim-1).values.cpu().numpy() for p, c, t in zip(preds, confs, batch[label].numpy()): rows.append({text: texts[idx], pred: id2label[p], true: id2label[t], conf: round(float(c), 3)}) idx 1 df pd.DataFrame(rows) # 只看预测错误的按置信度从高到低排高置信度错误最值得分析 errors df[df[pred] ! df[true]].sort_values(conf, ascendingFalse) return errors逻辑说明这段代码把预测结果、真实标签、置信度整理成表重点看conf高的错误样本——模型很自信却错了说明这类模式它完全没学到是补数据的优先目标。conf低的错误反而没那么紧急可能是本身就有歧义的样本。基于错误分析可以设一个置信度阈值做兜底高于 0.8 的直接采信0.5 到 0.8 之间的走人工复核低于 0.5 的直接转人工。这样能在准确率和人工成本之间找平衡。阈值定多少要看业务能接受多少错误没有标准答案我一般会画一条置信度-准确率曲线来选点。置信度区间建议处理方式说明大于 0.8直接采信错误率通常很低0.5 到 0.8人工复核模型不确定人工兜底小于 0.5转人工模型基本没把握还有一个实用技巧是模型集成把 BERT 和 RoBERTa 两个微调模型的概率平均一下通常能涨一两个点代价是推理变慢。如果业务对延迟不敏感值得一试。从那以后我每次做完一个情感分析项目都会强制走一遍错误分析加阈值验证不再只看测试集那几个数字。模型能不能用边界样本说了算。希望帮到你。本文还有配套的精品资源点击获取