赞
踩
转自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的模板输入、模型训练、预测全流程:
- |—— 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)
PET (Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference) 提出将输入示例转换为完形填空式短语,以帮助语言模型理解给定的任务。
Prompt关键点就是构造成完型填空式的输入形式,将待预测的标签(字、词、短语、句子)作为[MASK]掩盖,发挥预训练模型的优势进行预测。根据任务形式构建标签映射。
本项目实体情感分析举例:
其中 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
-
主要参数含义:
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是飞桨自然语言处理开发库,具备易用的文本领域API,多场景的应用示例、和高性能分布式训练三大特点,旨在提升开发者在文本领域的开发效率,并提供丰富的NLP应用示例。
[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.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。