# @Time : 2023/1/22 16:22
# @Author : tk
# @FileName: data_utils.py
import copy
import json
import os
import random
import typing
from enum import Enum
import numpy as np
import torch
from deep_training.data_helper import DataHelper, ModelArguments, TrainingArguments, DataArguments
from deep_training.nlp.models.chatglm import ChatGLMConfig
from deep_training.nlp.models.lora import LoraArguments
from deep_training.utils.func import is_chinese_char
from fastdatasets.record import load_dataset as Loader, RECORD, WriterObject, gfile
from tqdm import tqdm
from transformers import HfArgumentParser
from data_processer import DataStrategy, TokenTruncation, TokenSingleSliding, TokenDoubleSliding
from models import ChatGLMTokenizer
train_info_args = {
'devices': 1,
'data_backend': 'record',
'model_type': 'chatglm',
# 预训练模型路径 , 从0训练,则置空
'model_name_or_path': './model/chatglm-6b',
'config_name': './config/config_small.json',
'tokenizer_name': './model/chatglm-6b',
'convert_onnx': False, # 转换onnx模型
'do_train': True,
'train_file': [ './data/finetune_train_examples.json', './data/finetune_train_examples.json'],
'max_epochs': 1,
'max_steps': -1,
'optimizer': 'lion', # one of adamw,adam,lamb,lion
# 学习率调度器(scheduler),其中'scheduler_type'指定了使用哪种类型的scheduler,而'scheduler'参数指定了对应类型的scheduler所需的参数。
# 带慢启动的余弦学习率调度器
'scheduler_type': 'CAWR',
# 'T_mult': 一个整数,控制多少个周期后重新开始下一次周期。默认值为1。
# 'rewarm_epoch_num': 一个浮点数,表示一个完整周期的比例,以epoch为单位。默认值为0.5,即一半的周期用于重新热启动。
# 'verbose': 表示是否打印详细信息。默认值为False。
'scheduler':{'T_mult': 1, 'rewarm_epoch_num': 0.5, 'verbose': True},
# linear线性方法来调整学习率。
# 'scheduler_type': 'linear',# one of [linear,WarmupCosine,CAWR,CAL,Step,ReduceLROnPlateau
# 'scheduler': None,
# 切换scheduler类型
# 'scheduler_type': 'WarmupCosine',
# 'scheduler': None,
# 该scheduler在验证集上监测到学习率的性能不佳时,将学习率降低一个因子。例如,如果在验证集上的准确率停滞不前,则将学习率除以10
# 'scheduler_type': 'ReduceLROnPlateau',
# 'scheduler': None,
# 该scheduler在指定的epoch步长上按照固定的因子来降低学习率。需要以下参数: # 'decay_rate': 一个浮点数,控制学习率下降的速率。默认值为0.999。 # 'decay_steps': 一个整数,表示在多少个epoch后降低学习率。默认值为100。
# 'scheduler_type': 'Step',
# 'scheduler':{ 'decay_rate': 0.999,'decay_steps': 100,'verbose': True},
# 'scheduler_type': 'CAWR',
# 'scheduler':{'T_mult': 1, 'rewarm_epoch_num': 2, 'verbose': True},
# 'scheduler_type': 'CAL',
# 'scheduler': {'rewarm_epoch_num': 2,'verbose': True},
'optimizer_betas': (0.9, 0.999),
'train_batch_size': 1,
'eval_batch_size': 2,
'test_batch_size': 2,
'learning_rate': 2e-5, # lr学习率
'adam_epsilon': 1e-8, # ε是Adam优化器的一个超参数,它是一个小的正数,通常取值为10的负8次方(1e-8),用于防止除零错误以及数值稳定性。
'gradient_accumulation_steps': 1, # 表示梯度积累的步数。
# 'max_grad_norm'是模型训练过程中的一个超参数,它控制了梯度裁剪的大小。梯度裁剪是一种常用的防止梯度爆炸的方法,可以将梯度的范数限制在一个预设的范围内,以防止梯度过大,导致模型无法收敛。
# 具体来说,'max_grad_norm'的值代表着允许的最大的梯度范数。在训练过程中,如果模型的梯度范数超过了这个阈值,那么就会将梯度进行裁剪,
# 将其缩放到阈值以内。通常情况下,'max_grad_norm'的值被设置为1.0,表示不允许梯度的范数超过1.0。通过使用梯度裁剪,可以有效地控制模型的训练过程,防止模型过拟合或者无法收敛。
'max_grad_norm': 1.0,
'weight_decay': 0,
'warmup_steps': 0,
'output_dir': './output',
'max_seq_length': 64, # 如果资源充足,推荐长度2048 与官方保持一致
'max_target_length': 100, # 预测最大长度, 保留字段
'use_fast_tokenizer': False, # 'use_fast_tokenizer'的值为False时,使用的是较慢但功能更全面的tokenizer,其速度较慢但可以进行更多的处理;当值为True时,使用的是速度更快但功能较少的tokenizer。
'do_lower_case': False, # 'do_lower_case'的值为False时,表示不进行小写处理,即保留原始文本中的大小写形式;当值为True时,表示将所有文本转化为小写形式进行处理。
############## lora模块
#注意lora和 ptuning-v2 禁止同时使用
'with_lora': True, # 是否启用lora模块
'inference_mode': False, # 推理模型, 不需要手动设置
'r': 8, # 'r': 8是LoRa模块中的超参数,表示每个头被分为多少个局部相关性区域。
'target_modules': ['query_key_value'], # 表示在哪些模块上使用LoRa模块。在这里,LoRa模块只应用在查询(query)、键(key)和值(value)的操作上。
'target_dtype': '16', # '16'表示数据类型,这里为16位浮点数。
'lora_alpha': 32, # 是LoRa模块中的超参数,表示LoRa转换中$\alpha$的值。
# 'enable_lora': [True],
'lora_model_name_or_path':None,
'enable_lora': None,
'lora_dropout': 0.1, # 是LoRa模块中的超参数,表示使用dropout正则化的概率。
'bias': 'none', # Bias type for Lora. Can be 'none', 'all' or 'lora_only'"
}
#lora 模式暂时不支持deepspeed
enable_deepspeed = False
data_conf = {
'strategy': DataStrategy.truncation, # 数据策略选项
DataStrategy.truncation: {
'ensure_answer_min_length': 1,
},
DataStrategy.singlesliding: {
'sliding_size': train_info_args['max_seq_length'] // 3 * 2, #prompt滑动窗口大小
'p':1, # p < 0 , 随机选举prompt
},
DataStrategy.doublesliding: {
'sliding_size': train_info_args['max_seq_length'] // 3 * 2, #双滑滑动窗口大小
'p':1,# p < 0 , 随机选举prompt
},
}
def get_deepspeed_config():
# 是否开启deepspeed
if not enable_deepspeed:
return None
with open('./deepspeed.json', mode='r', encoding='utf-8') as f:
deepspeed_config = json.loads(f.read())
return deepspeed_config
def preprocess(text):
#text = text.replace("\n", "\\n").replace("\t", "\\t")
return text
def postprocess(text):
# return text.replace("\\n", "\n").replace("\\t", "\t")
return text
class NN_DataHelper(DataHelper):
index = 1
def on_data_ready(self):
self.index = -1
# 切分词
def on_data_process(self, data: typing.Any, mode: str):
self.index += 1 # 增加self.index的值
prompt = data[0] # 从输入数据中提取问题
answer = data[1] # 从输入数据中提取答案
# 获取tokenizer和config
tokenizer: ChatGLMTokenizer
config: ChatGLMConfig
max_seq_length = self.max_seq_length_dict[mode]
tokenizer = self.tokenizer
config = self.config
# 如果self.sptoken不存在,则将其初始化为[50256, 50256],表示两个结束符
if not hasattr(self, 'sptoken'):
self.sptoken = tokenizer.encode(text="")[-2:]
# 对问题和答案进行编码
a_ids = tokenizer.encode(text=prompt, add_special_tokens=False)
b_ids = tokenizer.encode(text=answer, add_special_tokens=False)
# 获取数据处理策略
strategy = data_conf['strategy']
# 根据不同的处理策略选择不同的处理方法
if strategy == DataStrategy.truncation:
ds = TokenTruncation.process(tokenizer,config,a_ids, b_ids, max_seq_length, self.sptoken ,**data_conf[strategy])
elif strategy == DataStrategy.singlesliding:
ds = TokenSingleSliding.process(tokenizer,config, a_ids, b_ids, max_seq_length, self.sptoken, **data_conf[strategy])
elif strategy == DataStrategy.doublesliding:
ds = TokenDoubleSliding.process(tokenizer,config, a_ids, b_ids, max_seq_length, self.sptoken, **data_conf[strategy])
else:
raise ValueError('Invlid strategy',strategy)
if not ds:
return None
if self.index < 3:
print(ds[0])
return ds
# {
# "id": 0, "paragraph": [
# # 一轮会话
# {
# "q": "从南京到上海的路线",
# "a": [
# "你好,南京到上海的路线如下:",
# "1. 南京到上海,可以乘坐南京地铁1号线,在南京站乘坐轨道交通1号线。",
# "2. 南京到浦东机场,可以搭乘上海地铁1号,在陆家嘴站乘坐地铁1线,在浦东国际机场站乘坐机场快线,前往上海浦东国际机场。",
# "3. 上海到南京,可以换乘上海地铁2号线,从南京站换乘地铁2线,再从南京南站换乘地铁1路,然后到达上海站"
# ]
# }
# # 二轮....
# ]
# }
# 读取文件 ,作用是读取文件列表中的文件,提取出每个文件中的对话数据, cfiles: typing.List并将其转化为模型需要的格式。 是文件路径的列表
def on_get_corpus(self, files: typing.List, mode: str):
D = [] # 返回值是一个列表 D,其中包含了所有对话的 prompt 和对应的 answer。对话的 prompt 是用于生成回答的问题描述,answer 是该问题的正确回答。
for file in files:
with open(file, mode='r', encoding='utf-8', newline='\n') as f:
lines = f.readlines() # 遍历 files 中的每个文件,并逐行读取文件中的内容。
for line_id, line in enumerate(lines):
jd = json.loads(line) # 每一行都被解析成一个 JSON 对象 jd
if not jd:
continue
paragraph = jd['paragraph'] # 这个对象包含了一个 paragraph 字段,它是一个列表,表示对话中的每一个轮次。
if line_id < 10:
print(paragraph)
paragraph = [(preprocess(session['q']),preprocess('\n'.join(session['a']))) for session in paragraph]
for sid,(q,a) in enumerate(paragraph):
assert len(a),ValueError('answer cannot empty') # 提示答案不能为空。
if sid == 0:
D.append((q, a)) # 第一个问答直接存入D中。
else:
prompt_text = '' # 第二次问答之后的prompt包含前几轮的问答信息
for j in range(sid + 1):
if j == sid:
prompt_text += "[Round {}]\n问:{}\n答:".format(sid, paragraph[j][0])
else:
prompt_text += "[Round {}]\n问:{}\n答:{}".format(j, paragraph[j][0], paragraph[j][1])
D.append((prompt_text,a))
return D # 最终返回的D是[(q,a),(q,a),(q,a),(q,a)……]
# 该函数的主要功能是将 batch 中的数据组合成一个 dict,dict 中的 key 为 input_ids、attention_mask、position_ids、labels 等,对应的 value 为 torch.Tensor。
def collate_fn(self,batch):
if not hasattr(self,'sptoken'):
self.sptoken = self.tokenizer.encode(text="")[-2:]
o = {}
for i, b in enumerate(batch):
if i == 0:
for k in b:
o[k] = [torch.tensor(b[k])]
else:
for k in b:
o[k].append(torch.tensor(b[k]))
for k in o:
o[k] = torch.stack(o[k])
max_len = torch.max(o.pop('seqlen')).tolist()
b_input_ids = o['input_ids'][:, :max_len]
ctxlens = o.pop('ctxlen') # 兼容旧版本数据
if ctxlens is None:
ctxlens = [None] * len(b_input_ids)
b_position_ids,b_attention_mask = [],[]
for input_ids,context_length in zip(b_input_ids,ctxlens):
context_length = context_length.squeeze(dim=-1)
mask_position = context_length - 1
position_ids = list(range(context_length)) + [mask_position] * (max_len - context_length)
block_position_ids = [0] * context_length + list(range(1, max_len - context_length + 1))
attention_mask = torch.ones((1, max_len, max_len))
attention_mask = torch.tril(attention_mask)
attention_mask[..., :context_length] = 1
attention_mask = (attention_mask < 0.5)
b_position_ids.append(torch.stack((torch.tensor(position_ids),torch.tensor(block_position_ids))))
b_attention_mask.append(attention_mask)
b_attention_mask = torch.stack(b_attention_mask, dim=0)
b_position_ids = torch.stack(b_position_ids,dim=0)
o['input_ids'] = b_input_ids.long()
o['attention_mask'] = b_attention_mask.bool()
o['position_ids'] = b_position_ids.long()
o['labels'] = o['labels'][:, :max_len].long()
return o
if __name__ == '__main__':
parser = HfArgumentParser((ModelArguments, TrainingArguments, DataArguments, LoraArguments))
model_args, training_args, data_args, lora_args = parser.parse_dict(train_info_args)
dataHelper = NN_DataHelper(model_args, training_args, data_args)
tokenizer, config, _,_ = dataHelper.load_tokenizer_and_config(tokenizer_class_name=ChatGLMTokenizer,config_class_name=ChatGLMConfig)
config.eos_token_id = 130005
# 缓存数据集
# 检测是否存在 output/dataset_0-train.record ,不存在则制作数据集
if data_args.do_train:
# 生成训练数据集。其中 data_args.train_file 表示训练数据文件的路径,mixed_data=False 表示训练数据是否为多轮对话数据,shuffle=True 表示是否对训练数据进行随机排序,mode='train' 表示生成的是训练数据集。
dataHelper.make_dataset_with_args(data_args.train_file,mixed_data=False,shuffle=True,mode='train')
if data_args.do_eval:
dataHelper.make_dataset_with_args(data_args.eval_file, shuffle=False,mode='eval')
if data_args.do_test:
dataHelper.make_dataset_with_args(data_args.test_file, shuffle=False,mode='test')
# 这个函数的作用是将多个TFRecord格式的数据文件进行打乱顺序,并输出到一个新的TFRecord文件中。
def shuffle_records(record_filenames, outfile, compression_type='GZIP'):
print('shuffle_records record...')
options = RECORD.TFRecordOptions(compression_type=compression_type)
dataset_reader = Loader.RandomDataset(record_filenames, options=options, with_share_memory=True)
data_size = len(dataset_reader)
all_example = []
for i in tqdm(range(data_size), desc='load records'):
serialized = dataset_reader[i]
all_example.append(serialized)
dataset_reader.close()
shuffle_idx = list(range(data_size))
random.shuffle(shuffle_idx)
writer = WriterObject(outfile, options=options)
for i in tqdm(shuffle_idx, desc='shuffle record'):
example = all_example[i]
writer.write(example)
writer.close()
# 对每个record 再次打乱
for filename in dataHelper.train_files:
shuffle_records(filename, filename)