转自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.

 

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐