简介基于Python与预训练大模型ERNIE的情感分析项目源码及配套数据集完整覆盖句子级与属性级两类情感分析任务面向自然语言处理学习者、算法工程师以及有实际情感分析需求的开发者。项目从数据预处理、加载ERNIE模型、特征提取到分类器构建、模型训练与验证测试均有对应脚本清楚展示完整流程可直接运行调试。压缩包共24个文件包括6个Python脚本、9个json配置、4个pdf说明文档以及数据集文本整体大小25.54MB目录按数据、模型、预处理、训练、评估、预测划分便于按模块研读复现。目前已有220人学习下载。通过该资源可掌握ERNIE在情感分析中的调用方式理解句子级与属性级情感判别的差异替换数据集即可适配新产品评论、舆情监测等实际场景同时获得一套可迁移至其他预训练模型NLP任务的模板。1. 把 ERNIE 落到情感分析任务上这份源码包里到底有什么值得跑的如果你正在找一份能直接跑通的“预训练模型 情感分析”参考实现而不是只看了几十篇原理文章却不知道从哪行代码动手那这份源码包值得你花一个晚上拆一遍。它用百度开源的 ERNIE 作为底座一套代码做句子级别情感分析IMDB 正负二分类另一套做属性级别情感分析给定句子和某个属性词判断这个属性本身的情感极性数据、训练、预测的脚本都齐了。对 NLP 入门者来说它最大的价值不是“又一个 BERT 分类例子”而是让你同时看到两种任务粒度在数据组织、标签设计和模型输出上的差异对熟手来说它的代码结构可以作为你接新数据集、换预训练模型时的最小改动模板。我拆完后的整体评价是代码量不大但刚好覆盖了“数据预处理 → 加载模型 → 微调 → 预测”的完整闭环而且踩坑点非常典型基本把你用 transformers 做分类任务会遇到的雷都踩了一遍。2. 句子级别与属性级别两份代码解决的是两种不同问题拿到压缩包后第一件事不是急着跑而是把目录结构看清楚。emotional-analysis-master 下其实藏着两套相对独立的流程一套是基于ERNIE完成IMDB情感分析另一套是基于ERNIE完成属性级情感分析。两者共用 ERNIE 作为特征抽取器但任务定义、数据格式、输出层设计完全不同。2.1 句子级别IMDB 二分类的完整主流程先看基于ERNIE完成IMDB情感分析这个目录里面有utils.py、main.py和dataset文件夹。这里的任务是最经典的句子级情感二分类给定一条影评判断整体情感是正面还是负面。IMDB 数据集常规规模是训练集两万五千条、测试集两万五千条每个样本是一段英文评论和对应的正负标签。utils.py的角色是数据装载和预处理。常见做法是把原始文本和标签读进来后直接交给 tokenizer 转换成模型需要的 input_ids、token_type_ids、attention_mask而不是手动去做分词、去停用词这类传统 NLP 流程。原因很简单像 ERNIE 这类预训练模型用 WordPiece 或类似的分词方式你手写分词器反而会把词切成模型不认识的样子破坏预训练时学到的语义。所以 utils 里核心的活就是加载数据、检查数据长度分布、生成模型输入三件套。main.py则承担训练主流程定义模型、设置优化器、写训练循环、做验证、保存 checkpoint。如果你打开看会发现它基本就是一个标准的 transformers 微调模板关键参数集中在文件开头或命令行参数里。比较重要的几个参数是学习率、batch size、max_seq_len 和训练轮数。IMDB 这种两万五千条规模的数据集用 ERNIE base 级别的模型我一般学习率取 2e-5batch size 取 16 或 32max_seq_len 取 128 就足够——影评句子虽然长但大部分关键信息集中在前几十个词内取太长反而拖慢训练速度。2.2 属性级别从句子加属性到四分类的两阶段设计基于ERNIE完成属性级情感分析目录下的设计要复杂一截。它包含属性观点抽取.py和属性级情感分类.py两个脚本这说明任务被拆成了两个阶段。第一阶段是属性抽取。比如句子“这台手机屏幕很清晰但续航太差了”你需要先让模型抽取出“屏幕”和“续航”这两个属性词甚至进一步抽取“清晰”和“差”作为对应的观点词。常见做法是用序列标注模型把属性词标注为 BIES 标签训练数据里的每个字都对应一个标签。这个包里的属性观点抽取.py做的事情就是训练一个抽取模型输入是原始句子输出是句子中的属性词和观点词位置。第二阶段才是属性级情感分类。抽取到属性词之后模型要判断“这个属性”的情感极性而不是整个句子的情感。还是上面那句手机的句子如果用句子级分类模型大概率判断为正面或负面都会纠结因为句子同时包含正面和负面。属性级分类把输入组织成“句子 目标属性”的组合对“屏幕”输出正面对“续航”输出负面。这就是 attribute-level sentiment analysis 的核心每个属性的极性是独立判断的。属性级情感分类.py处理的就是这个二阶段中的分类部分。它的数据格式通常是一行一个样本句子、属性词、情感标签。标签设计上中文属性级情感分析常用四分类正面、负面、中性、冲突正面和负面同时存在且针对同一属性。这比句子级二分类更贴近真实场景因为用户评论里大量存在“价格便宜但质量一般”这种对一个属性又夸又骂的情况。两套代码放在一个包里好处是可以直观对比句子级和属性级在模型层面几乎没差别差别全在数据组织上。句子级拿整句做输入属性级把属性和句子拼在一起做输入。这种对比对理解“预训练模型到底在做什么特征提取”很有帮助——模型不会自动知道你要分析哪个属性你必须把目标属性显式地喂给它。3. ERNIE 的加载与输出层改造从预训练权重到分类器的关键一跳不管句子级还是属性级代码里最核心的一段逻辑都是“如何把 ERNIE 变成一个分类器”。这个环节如果你只是调库可能几分钟就过去了但理解清楚每一行在做什么后面排错会省大量时间。3.1 为什么选 ERNIE知识增强和原生中文支持的取舍选 ERNIE 而不是直接选 BERT核心理由有两个。第一是知识增强ERNIE 在预训练时不仅做 WordPiece 级别的掩码还做了短语掩码和实体掩码把“世界卫生组织”这种短语作为一个整体掩码掉让模型学着预测完整的语义单元。这在情感分析上是有实际收益的因为很多情感词是短语级的比如“不太行”“相当出色”短语掩码让模型更容易捕获这种组合语义。第二是中文支持度。项目里 IMDB 是英文任务但属性级情感分析的数据是中文ERNIE 的中文预训练权重在中文语料上做了充分适配中文分词和语义表示都比直接拿英文 BERT 硬跑更自然。如果你只做英文句子级分类用 BERT 也完全没问题但只要涉及中文细粒度情感ERNIE 是更省事的起点。3.2 用 transformers 加载 ERNIE 并替换分类头先看这段典型代码它出现在属性级情感分类脚本里from transformers import BertTokenizer, ErnieForSequenceClassification model_name nghuyong/ernie-1.0-base-zh # 中文ERNIE权重注意替换成你下载到的路径 tokenizer BertTokenizer.from_pretrained(model_name) model ErnieForSequenceClassification.from_pretrained( model_name, num_labels4 # 属性级情感分类用四分类正面/负面/中性/冲突 )逻辑上ErnieForSequenceClassification帮我们做了两件事加载 ERNIE 的预训练权重然后在模型顶部自动接一个带 dropout 的全连接分类层输出维度就是num_labels。从 BertTokenizer 继承是因为 ERNIE 的分词方式和 BERT 兼容。你不应该手写一个nn.Linear(hidden_size, num_labels)然后手动拼上去ErnieForSequenceClassification已经封装好直接用就行。参数方面num_labels是最重要的改动开关IMDB 二分类就填 2属性级四分类就填 4训练轮数、学习率也随之调整。另外注意这个模型的from_pretrained第一次运行会下载几百 MB 的权重如果你的网络环境不通畅建议先离线下载到本地目录把model_name改成本地绝对路径。3.3 数据预处理tokenizer 的 padding 和标签映射模型加载完了数据进不去也没用。看这段预处理逻辑def convert_examples_to_features(texts, labels, tokenizer, max_len128): encodings tokenizer( texts, truncationTrue, # 超过max_len的部分截断 paddingTrue, # 不足max_len的补[PAD] max_lengthmax_len, return_tensorspt ) label_map {positive: 0, negative: 1, neutral: 2, conflict: 3} label_ids [label_map[label] for label in labels] return encodings, label_ids这里的重点是truncationTrue和paddingTrue的组合。一个 batch 里的样本长度不一致模型要求定长输入所以要么截断要么补 PAD。max_len 设 128 意味着超过 128 个 token 的文本会被硬切掉——对中文评论来说128 个字符基本能覆盖完整语义但如果你的数据是长评论建议改成 256。标签映射这块是新手最容易漏的ERNIE 输出的是 logits损失函数要求输入整数标签所以必须先建一个字符串到 id 的映射。IMDB 是 positive/negative 到 0/1 的映射属性级是四分类映射。映射表漏了任何一个类别训练时就会报 label index out of range 之类的问题。属性级的情感分类输入还有一个特殊细节要把目标属性和句子拼在一起。常见做法是构造[CLS] 句子 [SEP] 属性词 [SEP]这样的输入结构。这样模型在计算 attention 时属性词时刻都能注意到句子中相关内容情感判断不是凭空产生而是“基于该属性”产生的。4. 训练全流程从数据集目录到模型评估的完整闭环源码包的价值在于能跑通。把数据准备好、模型定义好之后接下来的训练流程是所有代码里最长的一部分。我在拆包时把训练流程整理成一条清晰的链路你在跑的时候照着这条链路核对自己的代码。4.1 文件结构、关键参数和运行命令先看一张项目关键文件对照表帮助你建立整体认知文件/目录作用关键参数或入口基于ERNIE完成IMDB情感分析/main.py句子级二分类训练入口数据集路径、学习率、epoch基于ERNIE完成IMDB情感分析/utils.py加载IMDB数据、tokenizer转换batch_size、max_seq_len基于ERNIE完成IMDB情感分析/dataset/IMDB训练与测试数据原始文本标签属性级情感分类.py属性级四分类模型训练与预测num_labels4属性观点抽取.py训练属性词与观点词抽取模型序列标注标签集dataset/属性级标注数据句子、属性词、情感标签运行顺序也有讲究。属性级是两阶段流程先跑抽取模型得到属性词再跑分类模型做极性判断。句子级独立直接跑main.py就行。我个人习惯是先用 CPU 模式跑一个很小的 epoch 验证代码通路再用 GPU 跑完整训练这样能快速暴露数据加载和维度不匹配的问题。4.2 训练循环学习率、batch size、早停main.py里的训练循环是标准的 PyTorch 写法可以拆成下面这几块from transformers import AdamW, get_linear_schedule_with_warmup # 优化器和学习率调度 optimizer AdamW(model.parameters(), lr2e-5, weight_decay0.01) total_steps len(train_dataloader) * epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(total_steps * 0.1), # 前10%的步数做warmup num_training_stepstotal_steps ) # 训练循环主体 model.train() for epoch in range(epochs): for batch in train_dataloader: outputs model( input_idsbatch[input_ids].to(device), attention_maskbatch[attention_mask].to(device), labelsbatch[labels].to(device) ) loss outputs.loss loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad()AdamW是预训练模型微调的标准选择区别在于它做了权重衰减的解耦。学习率 2e-5 是微调预训练模型的常规起点太大容易灾难性遗忘把预训练学到的知识冲掉太小则收敛慢。warmup 比例 10% 的意思是前一部分训练步骤用较小的学习率热身避免一上来就大步更新导致 loss 爆炸——这在小数据集上特别关键数据少意味着梯度噪声大直接满学习率很容易震荡。如果你的显存不够调度器报out of memory优先把 batch size 从 32 降到 16 或 8。注意降 batch size 后学习率也应该跟着适当调低一些经验值是 batch size 减半学习率乘 0.7 到 0.8。别问为什么小 batch 的梯度估计方差更大步子迈大了容易翻车。验证环节通常在每个 epoch 结束后做一次。把模型切到model.eval()模式用torch.no_grad()包住验证循环计算准确率或 F1 分数保存最优 checkpoint。属性级四分类任务建议看 F1 而不是 accuracy因为四个类别的样本量通常不均衡中性类往往远少于正面负面accuracy 会被大头类别带偏。4.3 从训练到预测把模型输出变成可读的情感结果训练完成后预测脚本的逻辑是这样from transformers import BertTokenizer, ErnieForSequenceClassification model ErnieForSequenceClassification.from_pretrained(./best_model, num_labels4) tokenizer BertTokenizer.from_pretrained(./best_model) model.eval() text 这家餐厅的菜品味道很好但是服务态度冷淡。 aspect 菜品味道 inputs tokenizer( text [SEP] aspect, return_tensorspt, truncationTrue, max_length128 ) with torch.no_grad(): logits model(**inputs).logits pred logits.argmax(dim-1).item() id_to_label {0: 正面, 1: 负面, 2: 中性, 3: 冲突} print(id_to_label[pred])关键点在于model.eval()和torch.no_grad()。不切 eval 模式的话模型里的 dropout 还在生效每次前向结果都会不同预测就变成了抽奖。属性拼接用[SEP]分隔句子和属性这与训练时的输入格式要保持完全一致——训练时怎么拼的预测时就必须怎么拼这是新手最容易忽略的坑。如果你需要批量预测建议用一个 dataloader 分批处理而不是一条一条跑。一条一条跑凸显不出问题但批量预测时记得保持相同的 padding 和 truncation 参数。还有一个细节预测时不需要 labels 字段模型收到 labels 会去计算 loss影响速度还占内存。5. 避坑跑这份 ERNIE 情感分析代码最容易翻车的五个点这个项目我自己在复现过程中踩了不止一个坑有些是数据集造成的有些是 transformers 库版本变动造成的也有一些纯粹是代码自身写法导致的。挑五个高概率翻车点写下来每一条都是“现象 → 原因 → 解决”的结构。5.1 现象老版 ERNIE 权重在 transformers 高版本上报“Unrecognized model”报错信息大概是Some weights of the model checkpoint were not used甚至直接 AttributeError。原因是早期百度官方发布的是 PaddlePaddle 格式权重而项目代码使用的是 transformers 接口。网上能找到的转换版本大多是社区转换的名称字段和 transformers 当前版本的 class 对不上。解决方法是检查utils.py或main.py里from_pretrained传入的模型名。推荐优先使用 HuggingFace 社区的转换权重这类权重在BertTokenizer下兼容良好。如果代码里写的是某个 Paddle 版的模型名你需要先在本地把权重转成 transformers 格式或者直接换一个社区维护的 ERNIE checkpoint。转换工作量大且容易出隐性 bug不建议作为首选路径。5.2 现象属性级情感分类训练时 loss 不降准确率停留在随机水平原因多半是输入格式问题。属性级任务不是把整句扔进去就行必须拼接目标属性。如果漏掉属性拼接模型看到的只有句子不知道要针对哪个属性分类自然学不出来。直接表现是 loss 来回震荡甚至从第一步就开始不收敛。解决方法是检查数据加载函数确认输入文本是句子 [SEP] 属性的形式。你还可以打印一条预处理后的 sample人工观察input_ids解码回来是不是同时包含句子和属性词。这一步能在训练前发现问题避免白跑几小时。5.3 现象训练时 loss 正常下降但验证集 F1 一直很低典型原因是数据集中类别严重不均衡。属性级四分类里中性标签样本量往往只有正面和负面的零头。模型为了把总 loss 降下来倾向于把所有样本都预测成占比最大的类别结果验证集准确率看着还行F1 一塌糊涂。解决办法有两个方向。第一个是在 loss 里加类别权重比如用torch.nn.CrossEntropyLoss(weightclass_weights)给样本少的类别更高的权重第二个是换评估指标训练时每轮记录 F1 而不是只看 accuracy用 F1 决定是否保存模型。我一般两个都做类别权重让训练更稳定F1 做 checkpoint 选择标准。5.4 现象batch size 调大后 GPU 直接 OOM但调小后模型效果明显变差OOM 的直接原因显然显存不够但很多人忽略的是 batch size 和 learning rate 之间的联动。直接调小 batch size 而不动学习率模型会变得收敛慢、效果差。解决方法是按比例缩小学习率。经验法则是 batch size 减半学习率从 2e-5 降到 1.5e-5 左右。同时打开梯度累积代码里用accumulation_steps控制每隔几步做一次优化器更新。这样既能维持等效 batch size 的效果又不超显存。在项目数据量只有几万条的情况下梯度累积是性价比最高的做法。5.5 现象预测结果全是正面或全是负面单一类别霸榜这种情况先检查是不是标签映射错了就是label_map和训练时的id_to_label顺序不一致导致预测结果解读反了。但更隐蔽的问题是测试时没有按训练时的数据分布来比如训练集里负面样本占大头测试时给的全是中性样本。排查思路是一步一步来先打印几条预测样本的 logits看数值分布是否合理再检查测试数据长什么样。如果 logits 始终偏向某一类说明模型过拟合或数据不均衡严重建议回到 5.3 的类别权重方案。如果数据分布和训练集差别很大那就不是模型问题是数据问题先调测试集。6. 从句子级到属性级的迁移技巧复用 checkpoint 与调参基线两套流程跑通后你可以做一件很实际的事情把 IMDB 任务上训练好的模型作为属性级任务的初始化权重。迁移学习的逻辑是句子级情感和属性级情感共享大量底层语义特征词向量、句法结构、情感词的表示都可以复用。做法是在加载属性级模型时不直接from_pretrained(nghuyong/ernie-1.0-base-zh)而是改成from_pretrained(./imdb_model_checkpoint, num_labels4)注意兼容层会自动处理分类头维度差异把原来的二分类头换成四分类头。前提是两份代码的模型基座一致都是 ERNIE base这个项目里正好满足。迁移后的调参基线是学习率从 2e-5 降到 1e-5因为模型已经对情感任务有了先验步子太大会把学到的任务特有特征破坏掉训练轮数对应减半句子级训练 3 个 epoch 的话属性级从 checkpoint 起步通常 1 到 2 个 epoch 就够。如果你再做一轮早停观察验证集 F1 连续两个 epoch 不再上升就停止基本不会出大问题。验证方法上不要只看最终准确率。属性级四分类建议你保存每一轮的混淆矩阵观察正负两个细分类别之间有没有系统性混淆。比如模型经常把“冲突”预测成“正面”说明它倾向于只看正面观点而忽略了同一句子里的负面信号。这时候可以在数据层面做文章把冲突类样本过采样两倍让模型多看到“又夸又骂”的句式。我自己做属性级任务时有个习惯每个数据集先跑一轮“不拼接属性”的基线再跑一轮“拼接属性”的正式版本对比两者 F1 的差值。如果差值很小说明测试集里的属性词可能都出现在很显眼的位置模型没有真正学会关注目标属性——那就要考虑增加难度把属性词位置打散或换成长尾高频词。这个对比思路在你后续处理自己的数据时特别有用。另外属性抽取和属性分类两个模型可以串联成一条完整 pipeline但你要为每一步单独做评估而不是只看最终输出。抽取错了后面分类必然错分类错了不一定是因为抽取错。把两段分开记录错误样本能迅速定位瓶颈在哪个环节。从那以后我每次跑完这种多阶段 NLP 项目都会强制走一遍“抽取端抽十条、分类端判十条”的人工体检肉眼确认中间产物质量而不是等端到端结果出来再猜问题在哪。希望这个习惯也能帮到你毕竟 NLP 模型的误差传播往往比你预想的影响更大。本文还有配套的精品资源点击获取