当前位置: 首页 > news >正文

大模型开发(五):P-Tuning项目——新零售决策评价系统(下)

P-Tuning项目——新零售决策评价系统(下)

  • 0 前言
  • 1 P-Tuning原理
  • 2 数据处理

0 前言

上篇文章我们介绍了使用PET方式微调BERT模型,PET属于提示词微调的一种,另一种比较常见的提示词微调是P-Tuning,我们今天在相同的项目上面用P-Tuning看看。

1 P-Tuning原理

P-Tuning 的目标是减少对人工设计模板(硬模板)的依赖,使用特殊字符(特殊字符可以自由学习也可以自己指定),将模版与原始文本拼在一起输入预训练模型,预训练模型会对模板中的mask做预测,得到一个label。
在这里插入图片描述
图中[u1][u2][u3][u4][u5][u6]都是伪标记,它们都是词表中没有使用过的token,所谓没有使用,指的是没有在训练集和验证集中出现过,所以构建软模板时,要找那种肯定不会出现在训练集和验证集的token。也就是说,软模板不再是人能理解的,只有模型能理解。

本项目的结构和PET大致相同,除了数据处理部分,其他代码只需要略微修改即可,因此我们这里只讲数据处理部分。

2 数据处理

数据处理的代码在 data_handle/data_preprocess.py 中,大致过程就是先插入Mask,后插入伪标记,我做了比较详细的注释,代码如下:

import torch
import numpy as np
from rich import print
from functools import partial
from datasets import load_dataset
from transformers import AutoTokenizer


def convert_example(
        examples: dict,
        tokenizer,
        max_seq_len: int,
        max_label_len: int,
        p_embedding_num=6,
        train_mode=True,
        return_tensor=False
) -> dict:
    """
    将样本数据转换为模型接收的输入数据。

    Args:
        examples (dict): 训练数据样本, e.g. -> {
                                                "text": [
                                                            '娱乐	嗨放派怎么停播了',
                                                            '体育	世界杯为何迟迟不见宣传',
                                                            ...
                                                ]
                                            }
        max_label_len (int): 最大label长度,若没有达到最大长度,则padding为最大长度
        p_embedding_num (int): p-tuning token(伪标记) 的个数
        train_mode (bool): 训练阶段 or 推理阶段。
        return_tensor (bool): 是否返回tensor类型,如不是,则返回numpy类型。

    Returns:
        dict (str: np.array) -> tokenized_output = {
                            'input_ids': [[101, 3928, ...], [101, 4395, ...]],
                            'token_type_ids': [[0, 0, ...], [0, 0, ...]],
                            'mask_positions': [[5, 6, ...], [3, 4, ...]],
                            'mask_labels': [[183, 234], [298, 322], ...]
                        }
    """
    # 定义输出格式(Bert模型的接收格式)
    tokenized_output = {
        'input_ids': [],
        'attention_mask': [],
        'mask_positions': [],  # 记录label的位置(即MASK Token的位置)
        'mask_labels': []  # 记录MASK Token的原始值(即Label值)
    }

    # 遍历样本数据,将样本填充到模板中,并转化为Bert模型的输入格式
    for i, example in enumerate(examples['text']):
        try:
            # 将[MASK]插在[CLS]之后,[MASK]的位置可以在任何位置,但提示词的开头和结尾必须为[CLS]和[SEP]
            start_mask_position = 1

            if train_mode:
                # 如果是训练模式,则既有样本的label,也有样本的文本内容
                label, content = example.strip().split('\t', 1) # 第二个参数为1表示最多分割1次,结果列表中最多包含2个元素
            else:
                # 如果是评估(推理)模式,则只有样本的文本内容
                content = example.strip()

            # 将文本转换为Bert模型的输入格式
            encoded_inputs = tokenizer(
                text=content,
                truncation=True,
                max_length=max_seq_len,
                padding='max_length')
            # encoded_inputs包含三个键:'input_ids', 'token_type_ids', 'attention_mask'

        except:
            continue

        # 生成 MASK Tokens, 和label长度一致
        mask_tokens = ['[MASK]'] * max_label_len

        # 将 MASK Tokens 转为 id
        mask_ids = tokenizer.convert_tokens_to_ids(mask_tokens)

        # 构建 prompt token(s),即构建伪标记,[[unused1] [unused2] ... [unused6]]
        p_tokens = ["[unused{}]".format(i + 1) for i in range(p_embedding_num)]

        # 伪标记 转 id
        p_tokens_ids = tokenizer.convert_tokens_to_ids(p_tokens)

        # 获取input_ids
        input_ids = encoded_inputs['input_ids']

        # 去掉最后的[SEP]
        tmp_input_ids = input_ids[:-1]

        # 裁剪content的长度
        tmp_input_ids = tmp_input_ids[:max_seq_len - len(mask_ids) - len(p_tokens_ids) - 1]
        # 因为要插入 p_embedding_num 个伪标记,并且标签长度为 max_label_len,并且最后要加上[SEP]
        # 所以原来的 input_ids 只能保存 max_seq_len - len(mask_ids) - len(p_tokens_ids) - 1 个token

        # 插入[MASK]对应的id
        tmp_input_ids = tmp_input_ids[:start_mask_position] + mask_ids + tmp_input_ids[start_mask_position:]
        # 插入后,tmp_input_ids 变为 [CLS][MASK][MASK]世界杯...

        # 补上[SEP]
        input_ids = tmp_input_ids + [input_ids[-1]]

        # 插入伪标记
        input_ids = p_tokens_ids + input_ids  # [unused1][unused2]...[CLS][MASK][MASK]世界杯...[SEP]

        # 将 Mask Tokens 的位置记录下来
        mask_positions = [len(p_tokens_ids) + start_mask_position + i for i in range(max_label_len)]

        # 将填充后的提示词加入到输出字典中
        tokenized_output['input_ids'].append(input_ids)

        # 如果输入需要token_type_ids,可以进行添加,
        if 'token_type_ids' in encoded_inputs:  # 兼容不需要 token_type_id 的模型, e.g. Roberta-Base
            tmp = encoded_inputs['token_type_ids']
            if 'token_type_ids' not in tokenized_output:
                # 循环第一轮时,'token_type_ids'不在字典tokenized_output中,所以需要增加键值对
                tokenized_output['token_type_ids'] = [tmp]
            else:
                # 从第二轮循环开始,直接在列表里添加
                tokenized_output['token_type_ids'].append(tmp)

        # 收集Bert模型需要的其他信息
        tokenized_output['attention_mask'].append(encoded_inputs['attention_mask'])
        tokenized_output['mask_positions'].append(mask_positions)

        # 对于训练模式,则需要将label转化为Bert模型的输入格式
        if train_mode:
            mask_labels = tokenizer(text=label)  # label token 转 id
            mask_labels = mask_labels['input_ids'][1:-1]  # 丢掉[CLS]和[SEP]
            mask_labels = mask_labels[:max_label_len]   # 如果标签的长度大于max_label_len,则截断
            mask_labels += [tokenizer.pad_token_id] * (max_label_len - len(mask_labels))  # 将 label 补到最长
            tokenized_output['mask_labels'].append(mask_labels)     # 收集处理后的标签

    # 将数据转化为torch.tensor或者numpy.array格式,方便后续处理
    for k, v in tokenized_output.items():
        if return_tensor:
            tokenized_output[k] = torch.LongTensor(v)
        else:
            tokenized_output[k] = np.array(v)

    return tokenized_output


if __name__ == '__main__':
    # 导入数据
    train_dataset = load_dataset('text', data_files={'train': '../data/train.txt'})
    print(f'train_dataset==>{train_dataset}')
    print(train_dataset['train']['text'][0])
    print('-'*80)

    # 创建分词器
    tokenizer = AutoTokenizer.from_pretrained('../../预训练模型/bert-base-chinese')

    # 函数式编程
    new_func = partial(convert_example,
                       tokenizer=tokenizer,
                       max_seq_len=20,
                       max_label_len=2,
                       p_embedding_num=6)

    # 数据批处理
    new_dataset = train_dataset.map(new_func, batched=True)

    # 打印
    print(f'dataset---》{new_dataset}')
    for value in new_dataset['train']:
        # value将是一个字典,包含输入的text、input_ids、token_type_id、attention_mask、mask_position和mask_label
        print(type(value))
        for k, v in value.items():
            print(k, v)

        print(len(value['input_ids']))
        break

输出

train_dataset==>DatasetDict({
    train: Dataset({
        features: ['text'],
        num_rows: 63
    })
})
电脑	(1)这款笔记本外观感觉挺漂亮的,分量吗,对我来说不算沉。 (2)安装了WindowsXP系统后,运行的速度挺快。发热量没有想象中那么大。可能尚未运行很耗资源的程序,没有感到内存的弊病。不过,1G的内存确实有点小。 (3)附赠的包很不错,挺有手感的。但是附赠的鼠标实在是太小了,幸好同时订了一个双飞燕的鼠标哟。
--------------------------------------------------------------------------------
dataset---》DatasetDict({
    train: Dataset({
        features: ['text', 'input_ids', 'attention_mask', 'mask_positions', 'mask_labels', 'token_type_ids'],
        num_rows: 63
    })
})
<class 'dict'>
text 电脑	(1)这款笔记本外观感觉挺漂亮的,分量吗,对我来说不算沉。 (2)安装了WindowsXP系统后,运行的速度挺快。发热量没有想象中那么大。可能尚未运行很耗资源的程序,没有感到内存的弊病。不过,1G的内存确实有点小。 (3)附赠的包很不错,挺有手感的。但是附赠的鼠标实在是太小了,幸好同时订了一个双飞燕的鼠标哟。
input_ids [1, 2, 3, 4, 5, 6, 101, 103, 103, 113, 122, 114, 6821, 3621, 5011, 6381, 3315, 1912, 6225, 102]
attention_mask [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
mask_positions [7, 8]
mask_labels [4510, 5554]
token_type_ids [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
20

我个人有几个暂时理解不了的点:

  1. 上面这段程序,构建的软模板为:[unused1] [unused2] ... [unused5] [unused6] [CLS] [MASK] [MASK] {text} [SEP],而start_mask_position=1,也就是说start_mask_position竟然是 [MASK] 插入到文本中的位置,假如start_mask_position不是1的话,是不是意味着要在文本中间插入?
  2. 为什么模板不是以 [CLS]开头?我查到的资料是,P-Tuning允许 [CLS]位置调整,也可以把模板改成这样: [CLS] [unused1] [unused2] [MASK][MASK] [unused3] [unused4] [unused5] [unused6] {text} [SEP]
  3. 似乎[unused1] [unused2]...[unused6] [CLS] {MASK} {text} [SEP],和前面PET的模板这是一条{MASK}评论:{textA},没有本质区别,都是人工构建的模板,指定伪标记为哪些token([unused1][unused2]等),出现在哪些位置,指定{MASK}在什么位置({textA}的前面),这些都是人工指定的,并不是模型生成的。既然都是人配置的,它为什么能比硬模板效果好呢?我的理解是,在微调之前,模板中的每个token,模型都是理解的,而软模板在微调之前,里面的[unused1] [unused2]模型并不认识,是在微调过程中,模型逐渐理解了;而硬模板的话,预训练模型本来就知道你的硬模板的token是什么意思,他们之间本来就存在一些联系。
  4. 很多人都说软模版是一种可学习模板,但模板好像从训练开始到结束,始终没改变过,这个“可学习”该如何理解?

AI邻域的很多问题搞不明白很正常,上面的部分解释很牵强,但这种方式却能起作用,这可能就是这个学科的特点吧。

相关文章:

  • 从自己电脑的浏览器访问阿里云主机中运行的LLaMA-Factory webui
  • python从入门到精通(二十四):python爬虫实现登录功能
  • C++--迭代器(iterator)介绍---主要介绍vector和string中的迭代器
  • milvus lite快速实践
  • C++ Primer 交换操作
  • 【每日学点HarmonyOS Next知识】状态栏控制、片段按钮点击回调、绘制组件、取消按钮与输入框对齐、父调子组件方法
  • 算法系列之滑动窗口
  • 2025/3/8 第 27 场 蓝桥入门赛 题解
  • PAT线上考试 真题/注意细节(甲/乙级)
  • 【Go每日一练】返回切片中的最大值和最小值
  • 如何计算两个向量的余弦相似度
  • Linux 内核自定义协议族开发:从 “No buffer space available“ 错误到解决方案
  • Java基础回顾 Day4
  • Sentinel 笔记
  • 【JAVA架构师成长之路】【Redis】第13集:Redis缓存击穿原理、规避、解决方案
  • Hadoop命令行语句
  • Jackson 详解
  • 三、OpenGL中三角形的绘制
  • Web前端开发——HTML基础下
  • µCOS-III从入门到精通 第十章(µC/OS-III消息队列)
  • 公众号里的功能怎么开发/网站优化seo方案
  • 老王传奇新开网站/百度关键词推广条件
  • wordpress入侵过程/seo作弊
  • 西宁网站建设多少钱/云和数据培训机构怎么样
  • 做家装的网站/aso优化方案
  • 没有网站怎么做外贸/seo任务平台