BERT+P-Tuning方式数据处理

2024-06-08 09:53:41 浏览数 (2)

基于BERT P-Tuning方式数据预处理介绍

一、 查看项目数据集
  • 数据存放位置:/Users/**/PycharmProjects/llm/prompt_tasks/P-Tuning/data
  • data文件夹里面包含4个txt文档,分别为:train.txt、dev.txt、verbalizer.txt

1.1 train.txt
  • train.txt为训练数据集,其部分数据展示如下:
代码语言:javascript复制
水果	脆脆的,甜味可以,可能时间有点长了,水分不是很足。
平板	华为机器肯定不错,但第一次碰上京东最糟糕的服务,以后不想到京东购物了。
书籍	为什么不认真的检查一下, 发这么一本脏脏的书给顾客呢!
衣服	手感不错,用料也很好,不知道水洗后怎样,相信大品牌,质量过关,五星好评!!!
水果	苹果有点小,不过好吃,还有几个烂的。估计是故意的放的。差评。
衣服	掉色掉的厉害,洗一次就花了

train.txt一共包含63条样本数据,每一行用t分开,前半部分为标签(label),后半部分为原始输入 (用户评论)。 如果想使用自定义数据训练,只需要仿照上述示例数据构建数据集即可。


1.2 dev.txt
  • dev.txt为验证数据集,其部分数据展示如下:
代码语言:javascript复制
书籍	"一点都不好笑,很失望,内容也不是很实用"
衣服	完全是一条旧裤子。
手机	相机质量不错,如果阳光充足,可以和数码相机媲美.界面比较人性化,容易使用.软件安装简便
书籍	明明说有货,结果送货又没有了。并且也不告诉我,怎么评啊
洗浴	非常不满意,晚上洗的头发,第二天头痒痒的不行了,还都是头皮屑。
水果	这个苹果感觉是长熟的苹果,没有打蜡,不错,又甜又脆

dev.txt一共包含417条样本数据,每一行用t分开,前半部分为标签(label),后半部分为原始输入 (用户评论)。 如果想使用自定义数据训练,只需要仿照上述示例数据构建数据集即可。

1.3 verbalizer.txt
  • verbalizer.txt 主要用于定义「真实标签」到「标签预测词」之间的映射。在有些情况下,将「真实标签」作为 [MASK] 去预测可能不具备很好的语义通顺性,因此,我们会对「真实标签」做一定的映射。
  • 例如:
代码语言:javascript复制
"中国爆冷2-1战胜韩国"是一则[MASK][MASK]新闻。	体育
  • 这句话中的标签为「体育」,但如果我们将标签设置为「足球」会更容易预测。
  • 因此,我们可以对「体育」这个 label 构建许多个子标签,在推理时,只要预测到子标签最终推理出真实标签即可,如下:
代码语言:javascript复制
体育 -> 足球,篮球,网球,棒球,乒乓,体育
  • 项目中标签词映射数据展示如下:
代码语言:javascript复制
电脑	电脑
水果	水果
平板	平板
衣服	衣服
酒店	酒店
洗浴	洗浴
书籍	书籍
蒙牛	蒙牛
手机	手机
电器	电器

verbalizer.txt 一共包含10个类别,上述数据中,我们使用了1对1的verbalizer, 如果想定义一对多的映射,只需要在后面用","分割即可, eg:

代码语言:javascript复制
水果	苹果,香蕉,橘子

若想使用自定义数据训练,只需要仿照示例数据构建数据集

二、 编写Config类项目文件配置代码
  • 代码路径:/Users/**/PycharmProjects/llm/prompt_tasks/P-Tuning/ptune_config.py
  • config文件目的:配置项目常用变量,一般这些变量属于不经常改变的,比如:训练文件路径、模型训练次数、模型超参数等等

具体代码实现:

代码语言:javascript复制
# coding:utf-8
import torch

class ProjectConfig(object):
    def __init__(self):
        self.device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
        self.pre_model = '/Users/**/llm/prompt_tasks/bert-base-chinese'
        self.train_path = '/Users/**/llm/prompt_tasks/P-Tuning/data/train.txt'
        self.dev_path = '/Users/**/llm/prompt_tasks/P-Tuning/data/dev.txt'
        self.verbalizer = '/Users/**/llm/prompt_tasks/P-Tuning/data/verbalizer.txt'
        self.max_seq_len = 512
        self.batch_size = 8
        self.learning_rate = 5e-5
        self.weight_decay = 0
        self.warmup_ratio = 0.06
        self.p_embedding_num = 6
        self.max_label_len = 2
        self.epochs = 50
        self.logging_steps = 10
        self.valid_steps = 20
        self.save_dir = '/Users/**/llm/prompt_tasks/P-Tuning/checkpoints'

if __name__ == '__main__':
    pc = ProjectConfig()
    print(pc.verbalizer)
三 编写数据处理相关代码
  • 代码路径:/Users/***/PycharmProjects/llm/prompt_tasks/P-Tuning/data_handle.
  • data_handle文件夹中一共包含两个py脚本:data_preprocess.py、data_loader.py

3.1 data_preprocess.py
  • 目的: 将样本数据转换为模型接受的输入数据
  • 导入必备的工具包
代码语言:javascript复制
# 导入必备工具包
import torch
import numpy as np
from rich import print
from datasets import load_dataset
from transformers import AutoTokenizer
import sys
sys.path.append('..')
from ptune_config import *
from functools import partial

  • 定义数据转换方法convert_example()
  • 目的:将模板与原始输入文本进行拼接,构造模型接受的输入数据
代码语言:javascript复制
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], ...]
                        }
    """
    tokenized_output = {
        'input_ids': [],
        'attention_mask': [],
        'mask_positions': [],  # 记录label的位置(即MASK Token的位置)
        'mask_labels': []  # 记录MASK Token的原始值(即Label值)
    }

    for i, example in enumerate(examples['text']):
        try:
            start_mask_position = 1  # 将 prompt token(s) 插在 [CLS] 之后

            if train_mode:
                label, content = example.strip().split('t')
            else:
                content = example.strip()

            encoded_inputs = tokenizer(
                text=content,
                truncation=True,
                max_length=max_seq_len,
                padding='max_length')
        except:
            continue
 
        input_ids = encoded_inputs['input_ids']
        # 1.生成 MASK Tokens, 和label长度一致
        mask_tokens = ['[MASK]'] * max_label_len  
        
        mask_ids = tokenizer.convert_tokens_to_ids(mask_tokens)  # token 转 id
        
				# 2.构建 prompt token(s)
        p_tokens = ["[unused{}]".format(i   1) for i in range(p_embedding_num)]  
        
        p_tokens_ids = tokenizer.convert_tokens_to_ids(p_tokens)  # token 转 id

        tmp_input_ids = input_ids[:-1]
        # 根据最大长度-p_token长度-label长度,裁剪content的长度
        tmp_input_ids = tmp_input_ids[:max_seq_len-len(mask_ids)-len(p_tokens_ids)-1]
        # 3.插入 MASK -> [CLS][MASK][MASK]世界杯...[SEP]
        tmp_input_ids = tmp_input_ids[:start_mask_position]   mask_ids   tmp_input_ids[start_mask_position:]

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

        # 4.插入 prompt -> [unused1][unused2]...[CLS][MASK]...[SEP]
        input_ids = p_tokens_ids   input_ids  

        # 将 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_id 的模型, e.g. Roberta-Base
        if 'token_type_ids' in encoded_inputs:  
            tmp = encoded_inputs['token_type_ids']
            if 'token_type_ids' not in tokenized_output:
                tokenized_output['token_type_ids'] = [tmp]
            else:
								tokenized_output['token_type_ids'].append(tmp)
        tokenized_output['attention_mask'].append(encoded_inputs['attention_mask'])
        tokenized_output['mask_positions'].append(mask_positions)

        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]
             # 将 label 补到最长
            mask_labels  = [tokenizer.pad_token_id] * (max_label_len - len(mask_labels)) 
            tokenized_output['mask_labels'].append(mask_labels)

    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__':
    pc = ProjectConfig()
    train_dataset = load_dataset('text', data_files={'train': pc.train_path})
    # print(type(train_dataset))
    # print(train_dataset)
    # print('*'*80)
    # print(train_dataset['train']['text'])
    tokenizer = AutoTokenizer.from_pretrained(pc.pre_model)
    tokenized_output = convert_example(examples=train_dataset['train'],
                                       tokenizer=tokenizer,
                                       max_seq_len=20,
                                       max_label_len=2,
                                       p_embedding_num=6,
                                       train_mode=True,
                                       return_tensor=False)
    print(tokenized_output)
    print(type(tokenized_output['mask_positions']))

打印结果展示:

代码语言:javascript复制
{
    'input_ids': array([[   1,    2,    3, ..., 1912, 6225,  102],
       [   1,    2,    3, ..., 3300, 5741,  102],
       [   1,    2,    3, ..., 6574, 7030,    0],
       ...,
       [   1,    2,    3, ..., 8024, 2571,    0],
       [   1,    2,    3, ..., 3221, 3175,  102],
       [   1,    2,    3, ..., 5277, 3688,  102]]),
    'attention_mask': array([[1, 1, 1, ..., 1, 1, 1],
       [1, 1, 1, ..., 1, 1, 1],
       [1, 1, 1, ..., 0, 0, 0],
       ...,
       [1, 1, 1, ..., 0, 0, 0],
       [1, 1, 1, ..., 1, 1, 1],
       [1, 1, 1, ..., 1, 1, 1]]),
    'mask_positions': array([[7, 8],
       [7, 8],
       [7, 8],
       ...,
       [7, 8],
       [7, 8],
       [7, 8]]),
    'mask_labels': array([[4510, 5554],
       [3717, 3362],
       [2398, 3352],
       ...,                   
       [3819, 3861],
       [6983, 2421],
       [3819, 3861]]),
    'token_type_ids': array([[0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ...,
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]])
}

3.3 data_loader.py
  • 目的:定义数据加载器
  • 导入必备的工具包
代码语言:javascript复制
# coding:utf-8
from torch.utils.data import DataLoader
from transformers import default_data_collator, AutoTokenizer
from data_handle.data_preprocess import *
from ptune_config import *

pc = ProjectConfig() # 实例化项目配置文件

tokenizer = AutoTokenizer.from_pretrained(pc.pre_model)

  • 定义获取数据加载器的方法get_data()
代码语言:javascript复制
def get_data():
    dataset = load_dataset('text', data_files={'train': pc.train_path,
                                               'dev': pc.dev_path})
    new_func = partial(convert_example,
                       tokenizer=tokenizer,
                       max_seq_len=pc.max_seq_len,
                       max_label_len=pc.max_label_len,
                       p_embedding_num=pc.p_embedding_num)

    dataset = dataset.map(new_func, batched=True)
    train_dataset = dataset["train"]
    dev_dataset = dataset["dev"]
    train_dataloader = DataLoader(train_dataset,
                                  shuffle=True,
                                  collate_fn=default_data_collator,
                                  batch_size=pc.batch_size)
    dev_dataloader = DataLoader(dev_dataset,
                                collate_fn=default_data_collator,
                                batch_size=pc.batch_size)
    return train_dataloader, dev_dataloader


if __name__ == '__main__':
    train_dataloader, dev_dataloader = get_data()
    print(len(train_dataloader))
    print(len(dev_dataloader))
    for i, value in enumerate(train_dataloader):
        print(i)
        print(value)
        print(value['input_ids'].dtype)
        break

打印结果展示:

代码语言:javascript复制
{
    'input_ids': tensor([[1, 2, 3,  ..., 0, 0, 0],
        [1, 2, 3,  ..., 0, 0, 0],
        [1, 2, 3,  ..., 0, 0, 0],
        ...,
        [1, 2, 3,  ..., 0, 0, 0],
        [1, 2, 3,  ..., 0, 0, 0],
        [1, 2, 3,  ..., 0, 0, 0]]),
    'attention_mask': tensor([[1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        ...,
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0]]),
    'mask_positions': tensor([[7, 8],
        [7, 8],
        [7, 8],
        [7, 8],
        [7, 8],
        [7, 8],
        [7, 8],
        [7, 8]]),
    'mask_labels': tensor([[6132, 3302],
        [2398, 3352],
        [6132, 3302],
        [6983, 2421],
        [3717, 3362],
        [6983, 2421],
        [3819, 3861],
        [6983, 2421]]),
    'token_type_ids': tensor([[0, 0, 0,  ..., 0, 0, 0],
        [0, 0, 0,  ..., 0, 0, 0],
        [0, 0, 0,  ..., 0, 0, 0],
        ...,
        [0, 0, 0,  ..., 0, 0, 0],
        [0, 0, 0,  ..., 0, 0, 0],
        [0, 0, 0,  ..., 0, 0, 0]])
}
torch.int64

小结

主要介绍了基于BERT P-Tuning方式实现文本分类任务时数据处理步骤,并且通过代码实现:提示模板数据格式的转换,数据加载器的编码等。

0 人点赞