带你快速使用PaddleNLP构建基于Prompt实体情感分类
转自AI Studio,原文链接:带你快速使用PaddleNLP构建基于Prompt实体情感分类 - 飞桨AI Studio背景简介由于个人水平有限,欢迎各位大佬指正不足之处!欢迎 Fork一键运行~~主题来源:2022搜狐校园 情感分析&推荐排序 算法大赛中的NLP赛道。任务目的:完成实体对象的情感极性分类,即文本中需要分析的实体对象及每个对象的情感极性(2代表极正向,1代表正向,0代表
转自AI Studio,原文链接:带你快速使用PaddleNLP构建基于Prompt实体情感分类 - 飞桨AI Studio
背景简介
由于个人水平有限,欢迎各位大佬指正不足之处!欢迎 Fork一键运行~~
主题来源:2022搜狐校园 情感分析&推荐排序 算法大赛中的NLP赛道。
任务目的:完成实体对象的情感极性分类,即文本中需要分析的实体对象及每个对象的情感极性(2代表极正向,1代表正向,0代表中立,-1代表负向,-2代表极负向)。
解决方案:基于飞桨PaddleNLP的Ernie模型实现PET的Prompt范式,将实体对象的标签映射为自然语言(模板设计)作为[MASK]进行预测。
线上成绩:在采样数据上训练2个Epoch,线上成绩达到0.66几,在未调参的情况下,相比使用预训练模型的Finetune效果较好,但prompt对模板设计比较敏感,一字之差便产生较大差距。
(在第一版的模板中线上只达到0.4几,调整模板后便上升到0.6几)
提分点:1.采用R-Drop训练策略,对抗扰动等;2.设计有效的模板输入(这点比较靠经验,需要不断实践尝试!!!);3.将标签映射为合适的字、词等(实践中很关键)。
接下来从以下几个方面详细介绍快速使用飞桨PaddleNLP预训练模型构建Prompt的模板输入、模型训练、预测全流程:
- 代码结构
- 环境配置
- 数据读取定义
- Prompt模板设计
- 模型搭建
- 训练损失定义
- 模型训练
- 模型预测
代码结构
|—— pet_prompt
|—— label_normalized/template.json # 自定义标签映射
|—— train.py # PET 策略的训练、评估主脚本
|—— data.py # PET 策略针对sohu数据集的任务转换逻辑,以及明文 -> 训练数据的转换
|—— model.py # PET 的网络结构
|—— evaluate.py # 评估函数
|—— predict.py # 数据集进行预测
环境配置
GPU环境
-
paddlepaddle-gpu == 2.2.2
-
paddlenlp == 2.2.6
In [ ]
# 在Aistudio项目中运行时,启动项目后需要更新paddlenlp
!pip3 install --upgrade paddlenlp
数据读取定义
// 自定义数据读取方式: 输出文本内容、实体对象及标签
def read_data(data, is_test=False):
for line in data:
if is_test:
entity = list(line['entity'].keys())
for e in entity:
yield {'text':e, 'text_pair':line['content']}
else:
entity = list(line['entity'].keys())
random.shuffle(entity)
for e in entity:
yield {'text':e, 'text_pair':line['content'], 'label':str(line['entity'][e])}
// 输入DataLoader
def create_dataloader(dataset_origin,
mode='train',
batch_size=1,
batchify_fn=None,
trans_fn=None):
if trans_fn:
dataset = dataset_origin.map(trans_fn)
shuffle = True if mode == 'train' else False
if mode == 'train':
batch_sampler = paddle.io.DistributedBatchSampler(
dataset, batch_size=batch_size, shuffle=shuffle)
else:
batch_sampler = paddle.io.BatchSampler(
dataset, batch_size=batch_size, shuffle=shuffle)
return paddle.io.DataLoader(
dataset=dataset,
batch_sampler=batch_sampler,
collate_fn=batchify_fn,
return_list=True)
Prompt模板设计
PET (Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference) 提出将输入示例转换为完形填空式短语,以帮助语言模型理解给定的任务。
Prompt关键点就是构造成完型填空式的输入形式,将待预测的标签(字、词、短语、句子)作为[MASK]掩盖,发挥预训练模型的优势进行预测。根据任务形式构建标签映射。
本项目实体情感分析举例:
- 训练样本形式: {"id": 3, "content": "选择Tips:露背连衣裙露肤元素是女性穿裙必备,只不过在选择这些性感元素的时候,尺寸一定要拿捏得当,避免出现艳俗感,当然也可以像高圆圆这样,背部展现,大面积的露肤不会出现一丝丝的瑕疵,同时在黑色的展现下,肤白貌美不说,还带着满满的性感范。声明:文字原创,图片来自网络,如有侵权,请联系我们删除,谢谢。如果你喜欢本篇文字,欢迎分享转发。", "entity": {"高圆圆": 1}}
其中 content是文本内容,entity包含多个实体对象及对应的标签(2代表极正向,1代表正向,0代表中立,-1代表负向,-2代表极负向),这条样例中,实体“高圆圆”的标签就是 1。这是典
型的情感实体分类任务,如何根据分类标签构建prompt模板学习呢?在构造模板输入之前,就需要将数字标签映射为自然语言,语言模型才能学习理解(换个角度理解,如果一句话连人类理解都
含糊不清,那模型当然非常吃力,不要把模型当作很智能的,数据才是王道!)。
- 标签映射:
这里将标签映射为一个字的形式,创建文件template.json放在文件夹label_normalized下:
// label_normalized/template.json
{
"-2":"坏",
"-1":"差",
"0":"平",
"1":"行",
"2":"好",
}
- 定义模板转化函数:
模板1:u'这句话中,{}是'.format(example["text"]) + example["text_pair"],概括为: 这句话中,[entity]是[MASK],[x]。
模板2:u'综合来看,{}是的,'.format(example["text"]) + example["text_pair"],概括为: 综合来看,[entity]是[MASK],[x]。
其中,[MASK]位置就是映射的标签,如:实体“高圆圆”标签为1,可输入为模板1: 这句话中,高圆圆是[MASK](标签映射:行),content。
def transform_sohu(example,
label_normalize_dict=None,
is_test=False,
pattern_id=0):
'''
定义模板转换函数
pattern_id: 定义不同的模板设计
'''
if is_test:
example["label_length"] = 1
if pattern_id == 0:
example["sentence1"] = u'这句话中,{}是<unk>'.format(example["text"]) + example["text_pair"]
elif pattern_id == 1:
example["sentence1"] = u'综合来看,{}是<unk>的,'.format(example["text"]) + example["text_pair"]
return example
else:
origin_label = example["label"]
# Normalize some of the labels, eg. English -> Chinese
example['text_label'] = label_normalize_dict[origin_label]
if pattern_id == 0:
example["sentence1"] = u'这句话中,{}是<unk>'.format(example["text"]) + example["text_pair"]
elif pattern_id == 1:
example["sentence1"] = u'综合来看,{}是<unk>的,'.format(example["text"]) + example["text_pair"]
del example["text"]
del example["text_pair"]
del example["label"]
return example
如下是将MASK位置转为 token_id 连接到文本token中,首先将[MASK]作为待预测标签的Token插入到模板输入的token中,然后计算MASK的位置。详细的代码请看data.py中,
下面是关键的核心代码:
# Replace <unk> with '[MASK]'
# Step1: gen mask ids
if is_test:
label_length = example["label_length"]
else:
text_label = example["text_label"]
label_length = len(text_label)
mask_tokens = ["[MASK]"] * label_length
mask_ids = tokenizer.convert_tokens_to_ids(mask_tokens)
sentence1 = example["sentence1"]
if "<unk>" in sentence1:
start_mask_position = sentence1.index("<unk>") + 1
sentence1 = sentence1.replace("<unk>", "")
encoded_inputs = tokenizer(text=sentence1, max_seq_len=max_seq_length)
src_ids = encoded_inputs["input_ids"]
token_type_ids = encoded_inputs["token_type_ids"]
# Step2: Insert "[MASK]" to src_ids based on start_mask_position
src_ids = src_ids[0:start_mask_position] + mask_ids + src_ids[
start_mask_position:]
token_type_ids = token_type_ids[0:start_mask_position] + [0] * len(
mask_ids) + token_type_ids[start_mask_position:]
# calculate mask_positions
mask_positions = [
index + start_mask_position for index in range(label_length)
]
模型搭建
定义好模板输入后,数据输入就完成了,整个项目完成了一大半!接下来就是搭建模型,详细的代码详见model.py,基于飞桨PaddleNLP的Ernie模型实现PET的Prompt范式,只需完成数据转换就能训练。
下面是关键的核心代码:
// 通过 ErniePretrainingHeads 输出文本中的MASK预测输出概率和下一个句子概率,只需返回[MASK]预测的prediction_scores;
class ErnieForPretraining(ErniePretrainedModel):
def __init__(self, ernie):
super(ErnieForPretraining, self).__init__()
self.ernie = ernie # 可以选择不同的预训练模型得到输出
weight_attr = paddle.ParamAttr(
initializer=nn.initializer.TruncatedNormal(
mean=0.0, std=self.ernie.initializer_range))
self.cls = ErniePretrainingHeads(
self.ernie.config["hidden_size"],
self.ernie.config["vocab_size"],
self.ernie.config["hidden_act"],
embedding_weights=self.ernie.embeddings.word_embeddings.weight,
weight_attr=weight_attr,)
self.apply(self.init_weights)
def forward(self,
input_ids,
token_type_ids=None,
position_ids=None,
attention_mask=None,
masked_positions=None):
with paddle.static.amp.fp16_guard():
outputs = self.ernie(
input_ids,
token_type_ids=token_type_ids,
position_ids=position_ids,
attention_mask=attention_mask)
sequence_output, pooled_output = outputs[:2]
# max_len = input_ids.shape[1]
new_masked_positions = masked_positions
# prediction_scores的输出维度是[batch_size, vocab_size],vocab_size根据选择的预训练模型而不同,如ernie是18000,输出根据每个词的最大概率进行输出
prediction_scores, seq_relationship_score = self.cls(
sequence_output, pooled_output, new_masked_positions)
return prediction_scores
整个模型流程比较简单,关键是ErniePretrainingHeads的构建,可以根据不同的预训练模型自定义PretrainingHeads模型,感兴趣的可以看源代码:Ernie
class ErnieLMPredictionHead(nn.Layer):
r"""
Ernie Model with a `language modeling` head on top.
"""
def __init__(
self,
hidden_size,
vocab_size,
activation,
embedding_weights=None,
weight_attr=None, ):
super(ErnieLMPredictionHead, self).__init__()
self.transform = nn.Linear(
hidden_size, hidden_size, weight_attr=weight_attr)
self.activation = getattr(nn.functional, activation)
self.layer_norm = nn.LayerNorm(hidden_size)
self.decoder_weight = self.create_parameter(
shape=[vocab_size, hidden_size],
dtype=self.transform.weight.dtype,
attr=weight_attr,
is_bias=False) if embedding_weights is None else embedding_weights
self.decoder_bias = self.create_parameter(
shape=[vocab_size], dtype=self.decoder_weight.dtype, is_bias=True)
def forward(self, hidden_states, masked_positions=None):
if masked_positions is not None:
hidden_states = paddle.reshape(hidden_states,
[-1, hidden_states.shape[-1]])
hidden_states = paddle.tensor.gather(hidden_states,
masked_positions)
# gather masked tokens might be more quick
hidden_states = self.transform(hidden_states)
hidden_states = self.activation(hidden_states)
hidden_states = self.layer_norm(hidden_states)
hidden_states = paddle.tensor.matmul(
hidden_states, self.decoder_weight,
transpose_y=True) + self.decoder_bias
return hidden_states
class ErniePretrainingHeads(nn.Layer):
def __init__(
self,
hidden_size,
vocab_size,
activation,
embedding_weights=None,
weight_attr=None, ):
super(ErniePretrainingHeads, self).__init__()
self.predictions = ErnieLMPredictionHead(
hidden_size, vocab_size, activation, embedding_weights, weight_attr)
self.seq_relationship = nn.Linear(
hidden_size, 2, weight_attr=weight_attr)
def forward(self, sequence_output, pooled_output, masked_positions=None):
prediction_scores = self.predictions(sequence_output, masked_positions)
seq_relationship_score = self.seq_relationship(pooled_output)
return prediction_scores, seq_relationship_score
训练损失定义
根据[MASK]的token_ids作为标签,预测输出的概率计算分类交叉熵损失。
class ErnieMLMCriterion(paddle.nn.Layer):
def __init__(self):
super(ErnieMLMCriterion, self).__init__()
def forward(self, prediction_scores, masked_lm_labels, masked_lm_scale=1.0):
masked_lm_labels = paddle.reshape(masked_lm_labels, shape=[-1, 1])
with paddle.static.amp.fp16_guard():
masked_lm_loss = paddle.nn.functional.softmax_with_cross_entropy(
prediction_scores, masked_lm_labels, ignore_index=-1)
masked_lm_loss = masked_lm_loss / masked_lm_scale
return paddle.mean(masked_lm_loss)
模型训练
模型训练流程与一般的NLP的Finetune流程一样,详细查看代码train.py。
-
加载数据
-
构建DataLoader
-
加载预训练模型、词典
-
超参数设置、优化器、训练策略等
// 训练命令
python -u -m paddle.distributed.launch --gpus "0" \
pet.py \
--task_name "sohu" \
--device gpu \
--pattern_id 1 \
--save_dir ./sohu \
--index 0 \
--batch_size 16 \
--learning_rate 5E-5 \
--epochs 2 \
--max_seq_length 512 \
--language_model "ernie-1.0" \
--rdrop_coef 0
主要参数含义:
- task_name: 数据集名字
- device: 使用 cpu/gpu 进行训练
- pattern_id 完形填空的模式
- save_dir: 模型存储路径
- max_seq_length: 文本的最大截断长度
- rdrop_coef: R-Drop 策略 Loss 的权重系数,默认为 0, 若为 0 则未使用 R-Drop 策略
In [ ]
%cd work/pet_prompt
!ls
python -u -m paddle.distributed.launch --gpus "0" \
train.py \
--task_name "sohu" \
--device gpu \
--pattern_id 1 \
--save_dir ./sohu \
--index 0 \
--batch_size 16 \
--learning_rate 5E-5 \
--epochs 2 \
--max_seq_length 512 \
--language_model "ernie-1.0" \
--rdrop_coef 0
模型预测
python -u -m paddle.distributed.launch --gpus "0" predict.py \
--task_name "sohu" \
--device gpu \
--pattern_id 1 \
--init_from_ckpt "./sohu/model_12345/model_state.pdparams" \ # 保存的模型文件
--output_dir "./sohu/output" \
--batch_size 32 \
--max_seq_length 512
In [ ]
python -u -m paddle.distributed.launch --gpus "0" predict.py \
--task_name "sohu" \
--device gpu \
--pattern_id 1 \
--init_from_ckpt "./sohu/model_123456/model_state.pdparams" \ # 保存的模型文件
--output_dir "./sohu/output" \
--batch_size 32 \
--max_seq_length 512
致谢
百度飞桨Aistudio提供的免费GPU算力!
百度飞桨https://github.com/PaddlePaddle/PaddleNLP.
欢迎访问PaddleNLP
PaddleNLP是飞桨自然语言处理开发库,具备易用的文本领域API,多场景的应用示例、和高性能分布式训练三大特点,旨在提升开发者在文本领域的开发效率,并提供丰富的NLP应用示例。
- 易用的文本领域API 提供丰富的产业级预置任务能力Taskflow和全流程的文本领域API:支持丰富中文数据集加载的Dataset API;灵活高效地完成数据预处理的Data API;提供100+预训练模型的Transformers API等,可大幅提升NLP任务建模的效率。
- 多场景的应用示例 覆盖从学术到产业级的NLP应用示例,涵盖NLP基础技术、NLP系统应用以及相关拓展应用。全面基于飞桨核心框架2.0全新API体系开发,为开发者提供飞桨文本领域的最佳实践。
- 高性能分布式训练 基于飞桨核心框架领先的自动混合精度优化策略,结合分布式Fleet API,支持4D混合并行策略,可高效地完成大规模预训练模型训练。
References
[1] Schick, Timo, and Hinrich Schütze. “Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference.” ArXiv:2001.07676 [Cs], January 25, 2021.http://arxiv.org/abs/2001.07676.
请点击此处查看本环境基本用法.
Please click here for more detailed instructions.
更多推荐
所有评论(0)