首页 / 资讯中心 / 文章详情

ERNIE情感分析实战:句子级与属性级微调全流程

ERNIE情感分析实战:句子级与属性级微调全流程 ★ FEATURED ARTICLE
简介这份资源面向NLP初学者与进阶开发者提供基于百度预训练大模型ERNIE完成情感分析的完整Python实现覆盖句子级与属性级两类任务。句子级判断整句情感极性属性级则针对产品特性、服务品质等具体属性给出情感倾向适合作为课程设计、科研入门或工程落地的参考模板。压缩包共24个文件约25.54MB以json数据配置、py源码脚本、pdf说明文档为主另含少量pyc缓存、txt与license文件目录按数据集、模型、预处理、训练、评估、预测等模块组织结构清晰便于按流程阅读。目前已有220人学习下载。读者可从中获得数据预处理、ERNIE加载、特征提取、分类器构建、训练调优到测试应用的完整链路代码并借助标注数据完成训练与验证理解预训练模型在真实情感分析项目中的落地方式也可将其迁移到其他NLP任务中复用。1. 从一句吐槽到属性级拆解ERNIE 情感分析到底能落地在哪电商后台每天涌进几千条评论运营最怕的不是差评本身而是「物流快但客服态度差」这种一句话里藏着两个相反情绪的句子。句子级情感分析只能告诉你这条评论整体偏负面可到底是物流拖后腿还是客服惹的祸它说不清。这就是属性级情感分析要解决的问题把一句话拆成若干评价对象再分别判断每个对象对应的情感极性。ERNIE 作为百度开源的中文预训练大模型在中文语义理解上比通用 BERT 更贴合本土语料拿来微调句子级和属性级两个任务一套代码就能跑通从数据加载到推理输出的完整链路。这篇笔记面向的是手里有标注数据、想快速搭一套可复现情感分析流水线的 Python 工程师不聊虚的直接讲数据怎么组织、模型怎么接、参数怎么调、坑在哪。2. 句子级与属性级任务的数据组织先把标注格式定死2.1 两种任务的输入输出差异句子级情感分析的本质是文本分类。输入一条完整句子输出一个极性标签常见三分类为正面、负面、中性。数据格式简单到一张 CSV 就能装下一列文本一列标签。但属性级任务不一样它的输入是「句子 属性词」的组合输出是该属性词对应的极性。同一条句子「物流快但客服态度差」对「物流」这个属性是正面对「客服」是负面。这意味着一条原始句子在训练集里会被拆成多条样本每条样本绑定一个属性词。我一般会把属性级数据组织成三列text、aspect、label。text 是原句aspect 是评价对象词label 是该对象的情感极性。如果一条句子有多个属性就展开成多行。这样做的好处是模型输入格式统一后续用 Dataset 类封装时不用做复杂的分支判断。任务类型输入字段输出字段样本展开方式句子级textlabel一句一行属性级text aspectlabel一句多属性展开为多行2.2 用 pandas 做数据清洗与标签映射拿到原始标注后第一步不是急着喂模型而是把脏数据清掉。常见问题包括标签写法不统一「正面」「正向」「positive」混用、属性词前后有空格、空句子。下面这段代码做三件事统一标签映射、去除首尾空白、过滤无效样本。import pandas as pd # 读取原始标注假设是 csv 格式 df pd.read_csv(raw_sentiment.csv) # 标签统一映射把各种写法归一到 0/1/2 label_map { 正面: 0, 正向: 0, positive: 0, 负面: 1, 负向: 1, negative: 1, 中性: 2, neutral: 2 } df[label] df[label].map(label_map) # 清洗文本和属性词 df[text] df[text].astype(str).str.strip() df[aspect] df[aspect].astype(str).str.strip() # 过滤空文本、空属性、标签缺失的样本 df df[df[text] ! ] df df[df[aspect] ! ] df df.dropna(subset[label]) df[label] df[label].astype(int) print(df[label].value_counts())逻辑说明label_map 把中文和英文标签统一成整数避免后续模型输出层维度对不上。str.strip() 处理标注时手滑打出的空格这类空格在属性词匹配时会直接导致匹配失败。dropna 和空值过滤是后悔药宁可少几条样本也别让一条脏数据在训练中途报错中断。参数说明label_map 的键值对要根据你实际数据里的标签写法补充不要假设只有三种。如果数据里有「强烈正面」这种细分标签要么合并到正面要么单独设一类取决于你的业务粒度需求。2.3 训练集、验证集、测试集的划分策略属性级任务有个容易翻车的地方同一条句子的不同属性样本如果被分到不同集合验证集指标会虚高。因为模型在训练时已经见过这条句子的上下文验证时只是换了个属性词本质上没考察泛化能力。正确做法是按原始句子去重后再划分保证同一句子的所有属性样本落在同一个集合里。from sklearn.model_selection import train_test_split # 按唯一句子划分避免同句泄漏 unique_texts df[text].unique() train_texts, temp_texts train_test_split(unique_texts, test_size0.3, random_state42) val_texts, test_texts train_test_split(temp_texts, test_size0.5, random_state42) train_df df[df[text].isin(train_texts)] val_df df[df[text].isin(val_texts)] test_df df[df[text].isin(test_texts)] print(ftrain: {len(train_df)}, val: {len(val_df)}, test: {len(test_df)})逻辑说明先对唯一句子做划分再用 isin 反查回原始 DataFrame这样同句子的所有属性样本必然在同一集合。random_state 固定住保证每次跑出来的划分一致方便复现和对比实验。参数说明test_size0.3 表示先切出 30% 做临时集再从临时集里对半分成验证和测试最终比例约为 7:1.5:1.5。如果数据量小于五千条建议改成 8:1:1给训练留更多样本。3. 用 ERNIE 做句子级情感分析从加载到微调的最小闭环3.1 模型加载与 tokenizer 的关键参数ERNIE 在 HuggingFace 上有对应的模型卡常见的是 ernie-3.0-base-zh 或 ernie-2.0-base-en 的中文版本。加载时用 AutoTokenizer 和 AutoModelForSequenceClassification前者负责把文本转成 token id后者在预训练权重上接一个分类头。这里有个参数必须注意num_labels 要和你实际类别数一致三分类就写 3写错了训练时 loss 会直接报维度错误。from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name nghuyong/ernie-3.0-base-zh tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained( model_name, num_labels3 # 正面/负面/中性 ) # 测试 tokenizer 输出 sample tokenizer(物流很快但客服态度差, max_length128, truncationTrue, paddingmax_length) print(sample[input_ids][:20])逻辑说明max_length128 对大多数电商评论够用超过的截断。paddingmax_length 保证一个 batch 内所有样本长度一致不然 DataLoader 会报错。truncationTrue 是必须的否则超长文本会直接抛异常。参数说明model_name 换成你本地下载的路径也可以离线环境先把模型文件拉下来。num_labels 根据任务改二分类写 2多分类写实际类别数。max_length 如果评论普遍偏长可以调到 256但显存占用会翻倍。3.2 用 Trainer API 跑通训练流程HuggingFace 的 Trainer 把训练循环、梯度累积、学习率调度都封装好了适合快速跑通 baseline。需要自己定义的是 Dataset 类和计算指标的函数。Dataset 类负责把 DataFrame 转成模型能吃的格式compute_metrics 负责在每个 epoch 结束时算准确率和 F1。import torch from torch.utils.data import Dataset from transformers import Trainer, TrainingArguments from sklearn.metrics import accuracy_score, f1_score class SentimentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len128): self.texts texts self.labels labels self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): encoding self.tokenizer( self.texts[idx], max_lengthself.max_len, truncationTrue, paddingmax_length, return_tensorspt ) return { input_ids: encoding[input_ids].squeeze(0), attention_mask: encoding[attention_mask].squeeze(0), labels: torch.tensor(self.labels[idx], dtypetorch.long) } def compute_metrics(eval_pred): logits, labels eval_pred preds logits.argmax(axis-1) return { accuracy: accuracy_score(labels, preds), f1: f1_score(labels, preds, averagemacro) } train_dataset SentimentDataset(train_df[text].tolist(), train_df[label].tolist(), tokenizer) val_dataset SentimentDataset(val_df[text].tolist(), val_df[label].tolist(), tokenizer) training_args TrainingArguments( output_dir./ernie_sentence, num_train_epochs3, per_device_train_batch_size16, per_device_eval_batch_size32, learning_rate2e-5, warmup_ratio0.1, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, logging_steps50 ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset, compute_metricscompute_metrics ) trainer.train()逻辑说明Dataset 的getitem返回字典键名必须和模型 forward 的入参一致input_ids、attention_mask、labels 一个都不能少。compute_metrics 里用 argmax 取预测类别macro F1 对类别不均衡更敏感比 accuracy 更能反映真实效果。TrainingArguments 里 load_best_model_at_end 配合 metric_for_best_model 自动保留验证集上 F1 最高的 checkpoint。参数说明learning_rate2e-5 是 BERT 系微调的经典值ERNIE 也适用。如果数据量小于两千条可以降到 1e-5 防止过拟合。per_device_train_batch_size 根据显存调16 在 8G 显存上跑 base 模型基本够。num_train_epochs3 是起点验证集 F1 还在涨就加到 5。3.3 推理阶段怎么批量预测并导出结果训练完模型别急着关 notebook推理阶段同样有坑。单条预测和批量预测的写法不同批量预测要手动控制 batch size不然显存会爆。下面这段代码把测试集按 batch 切分逐批送进模型最后拼回完整结果。from torch.utils.data import DataLoader def predict(texts, model, tokenizer, batch_size64): model.eval() all_preds [] dataset SentimentDataset(texts, [0]*len(texts), tokenizer) loader DataLoader(dataset, batch_sizebatch_size) with torch.no_grad(): for batch in loader: input_ids batch[input_ids].to(model.device) attention_mask batch[attention_mask].to(model.device) outputs model(input_idsinput_ids, attention_maskattention_mask) preds outputs.logits.argmax(dim-1).cpu().numpy() all_preds.extend(preds.tolist()) return all_preds test_preds predict(test_df[text].tolist(), model, tokenizer) test_df[pred] test_preds test_df.to_csv(sentence_predictions.csv, indexFalse)逻辑说明model.eval() 关闭 dropout保证推理结果稳定。torch.no_grad() 省显存推理阶段不需要计算梯度。DataLoader 的 batch_size 设 64 比训练时大因为推理不存梯度显存占用低很多。参数说明batch_size 根据显存调整base 模型在 8G 显存上跑 64 没问题large 模型建议降到 16。导出 CSV 时 indexFalse 避免多出一列无用的行号。4. 属性级情感分析的实现把属性词拼进输入序列4.1 属性词拼接的两种主流方案属性级任务的核心问题是怎么让模型知道当前要判断哪个属性的情感。常见做法有两种一种是把属性词直接拼在句子前面用分隔符隔开比如「客服 [SEP] 物流快但客服态度差」另一种是用两个句子输入属性词放第一句原句放第二句利用 BERT 系的 segment embedding 区分。我一般用第一种因为实现简单tokenizer 直接处理拼接后的字符串就行不用改模型结构。拼接格式建议统一成「aspect [SEP] text」这样模型在训练时能学到 [SEP] 前面是评价对象后面是上下文。如果属性词在句子中出现多次不需要特殊处理模型通过注意力机制自己学对应关系。4.2 改造 Dataset 类支持双输入句子级的 Dataset 只处理 text属性级要把 aspect 和 text 拼起来再 tokenize。改动很小但要注意拼接顺序和分隔符必须和推理时一致不然训练和推理的输入分布对不上效果会断崖式下跌。class AspectDataset(Dataset): def __init__(self, texts, aspects, labels, tokenizer, max_len128): self.texts texts self.aspects aspects self.labels labels self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): # 拼接格式aspect [SEP] text combined self.aspects[idx] [SEP] self.texts[idx] encoding self.tokenizer( combined, max_lengthself.max_len, truncationTrue, paddingmax_length, return_tensorspt ) return { input_ids: encoding[input_ids].squeeze(0), attention_mask: encoding[attention_mask].squeeze(0), labels: torch.tensor(self.labels[idx], dtypetorch.long) }逻辑说明combined 字符串把属性词放在前面[SEP] 是 BERT 系 tokenizer 能识别的特殊符号会自动转成对应的 special token id。truncation 从尾部截断如果句子太长属性词在开头不会被截掉保证关键信息保留。参数说明max_len 建议比句子级任务大一点因为拼接后长度增加了属性词和分隔符。如果属性词平均 3 个字max_len 可以设 160。padding 仍然用 max_length保持 batch 内对齐。4.3 属性级模型的训练与评估差异属性级任务的评估不能只看整体准确率因为不同属性的样本量可能差异很大。比如「物流」属性有五千条「包装」只有两百条整体准确率会被物流主导包装上的效果被掩盖。正确做法是分属性统计 F1再取宏平均。from sklearn.metrics import classification_report # 假设 test_df 有 aspect 列和 pred 列 report classification_report( test_df[label], test_df[pred], target_names[正面, 负面, 中性], output_dictTrue ) # 按属性分组统计 for aspect in test_df[aspect].unique(): subset test_df[test_df[aspect] aspect] if len(subset) 10: continue acc accuracy_score(subset[label], subset[pred]) print(faspect: {aspect}, samples: {len(subset)}, acc: {acc:.4f})逻辑说明classification_report 给出每个类别的 precision、recall、F1比单一 accuracy 信息量大。按属性分组统计能发现哪些属性上模型表现差针对性补充训练数据。参数说明target_names 的顺序要和 label 的整数映射一致0 对应正面1 对应负面2 对应中性写反了报告会误导。样本数小于 10 的属性直接跳过统计意义不大。5. 避坑与排查属性级情感分析最常见的五个翻车点5.1 属性词在句子中匹配不到导致输入无效现象推理时模型输出全是中性或者准确率远低于验证集。原因拼接后的字符串里属性词和句子中的写法不一致比如句子写「客服」属性词标注成「客户服务」模型看到的上下文和属性词对不上。解决在数据预处理阶段做一次属性词匹配检查统计属性词在句子中出现的比例低于 80% 就回头核对标注。5.2 标签泄漏导致验证集指标虚高现象验证集 F1 到 0.98测试集掉到 0.75。原因同一条句子的不同属性样本被分到了训练集和验证集模型见过上下文验证时只是换了个属性词。解决按唯一句子划分数据集确保同句子的所有样本在同一集合前面 2.3 节的代码就是干这个的。5.3 学习率过大导致 loss 震荡不收敛现象训练前几个 step loss 从 2.3 跳到 0.1 又弹回 1.8反复横跳。原因ERNIE 预训练权重已经很好微调时学习率超过 5e-5 容易把原有知识冲掉。解决把 learning_rate 降到 2e-5 或 1e-5配合 warmup_ratio0.1 让前 10% 的 step 慢慢升到设定值。5.4 显存不足导致训练中途 OOM现象跑了几百个 step 后突然报 CUDA out of memory。原因batch size 设太大或者 max_length 过长显存碎片累积。解决先把 batch size 减半同时开 gradient_accumulation_steps2 保持等效 batch。如果还不行把 max_length 从 256 降到 128显存占用直接减半。5.5 推理时忘记 model.eval() 导致结果不稳定现象同一条输入跑两次预测结果不一样。原因模型还在训练模式dropout 层随机丢弃神经元输出有随机性。解决推理前调 model.eval()并用 torch.no_grad() 包住前向传播既稳定又省显存。6. 进阶技巧用对抗训练和阈值搜索把 F1 再提两个点baseline 跑通之后想再往上提点效果我一般会试两个方向。第一个是 FGM 对抗训练在 embedding 层加扰动让模型对输入的小变化更鲁棒。实现上就是在每个 batch 前向传播后对 embedding 权重加一个梯度方向的扰动再算一次 loss 回传。代码不复杂但要注意扰动系数 epsilon 别设太大0.5 左右比较稳设到 1.0 以上容易把模型带偏。第二个是分类阈值搜索。三分类任务默认取 logits 最大的类别但如果某个类别样本少模型会偏向多数类。可以在验证集上对每个类别的 logits 加一个偏置项搜索让宏 F1 最高的偏置组合。具体做法是把验证集 logits 存下来用网格搜索遍历偏置范围找到最优后应用到测试集。这个技巧在类别不均衡时能提 1 到 3 个点代价几乎为零。import numpy as np from itertools import product # 假设 val_logits 是验证集输出的 logitsval_labels 是真实标签 best_f1 0 best_bias None for bias in product(np.arange(-1, 1.1, 0.2), repeat3): adjusted val_logits np.array(bias) preds adjusted.argmax(axis-1) f1 f1_score(val_labels, preds, averagemacro) if f1 best_f1: best_f1 f1 best_bias bias print(fbest bias: {best_bias}, best f1: {best_f1:.4f}) # 应用到测试集 test_adjusted test_logits np.array(best_bias) test_preds test_adjusted.argmax(axis-1)逻辑说明product 生成三个类别偏置的所有组合步长 0.2 在 -1 到 1 之间搜索。adjusted 是原始 logits 加上偏置argmax 取调整后的最大类别。找到最优偏置后直接加到测试集 logits 上不需要重新训练模型。参数说明偏置搜索范围根据 logits 的实际分布调整如果 logits 值普遍在 -5 到 5 之间搜索范围可以放宽到 -2 到 2。步长 0.2 是精度和速度的折中想更精细可以降到 0.1但搜索时间翻倍。这两个技巧我一般先跑阈值搜索因为不用改模型十分钟就能出结果。对抗训练放在后面需要改训练循环但提升更稳定。血泪经验是别一上来就堆技巧先把 baseline 的每个环节跑通确认数据没问题、评估指标可信再往上加。不然出了问题都不知道是数据脏还是模型没调好。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站