基于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为训练数据集,其部分数据展示如下:
水果 脆脆的,甜味可以,可能时间有点长了,水分不是很足。
平板 华为机器肯定不错,但第一次碰上京东最糟糕的服务,以后不想到京东购物了。
书籍 为什么不认真的检查一下, 发这么一本脏脏的书给顾客呢!
衣服 手感不错,用料也很好,不知道水洗后怎样,相信大品牌,质量过关,五星好评!!!
水果 苹果有点小,不过好吃,还有几个烂的。估计是故意的放的。差评。
衣服 掉色掉的厉害,洗一次就花了
train.txt一共包含63条样本数据,每一行用
t
分开,前半部分为标签(label),后半部分为原始输入 (用户评论)。 如果想使用自定义数据训练,只需要仿照上述示例数据构建数据集即可。
1.2 dev.txt
- dev.txt为验证数据集,其部分数据展示如下:
书籍 "一点都不好笑,很失望,内容也不是很实用"
衣服 完全是一条旧裤子。
手机 相机质量不错,如果阳光充足,可以和数码相机媲美.界面比较人性化,容易使用.软件安装简便
书籍 明明说有货,结果送货又没有了。并且也不告诉我,怎么评啊
洗浴 非常不满意,晚上洗的头发,第二天头痒痒的不行了,还都是头皮屑。
水果 这个苹果感觉是长熟的苹果,没有打蜡,不错,又甜又脆
dev.txt一共包含417条样本数据,每一行用
t
分开,前半部分为标签(label),后半部分为原始输入 (用户评论)。 如果想使用自定义数据训练,只需要仿照上述示例数据构建数据集即可。
1.3 verbalizer.txt
- verbalizer.txt 主要用于定义「真实标签」到「标签预测词」之间的映射。在有些情况下,将「真实标签」作为 [MASK] 去预测可能不具备很好的语义通顺性,因此,我们会对「真实标签」做一定的映射。
- 例如:
"中国爆冷2-1战胜韩国"是一则[MASK][MASK]新闻。 体育
- 这句话中的标签为「体育」,但如果我们将标签设置为「足球」会更容易预测。
- 因此,我们可以对「体育」这个 label 构建许多个子标签,在推理时,只要预测到子标签最终推理出真实标签即可,如下:
体育 -> 足球,篮球,网球,棒球,乒乓,体育
- 项目中标签词映射数据展示如下:
电脑 电脑
水果 水果
平板 平板
衣服 衣服
酒店 酒店
洗浴 洗浴
书籍 书籍
蒙牛 蒙牛
手机 手机
电器 电器
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
- 目的: 将样本数据转换为模型接受的输入数据
- 导入必备的工具包
# 导入必备工具包
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()
- 目的:将模板与原始输入文本进行拼接,构造模型接受的输入数据
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
- 目的:定义数据加载器
- 导入必备的工具包
# 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()
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方式实现文本分类任务时数据处理步骤,并且通过代码实现:提示模板数据格式的转换,数据加载器的编码等。