一、需要的环境
内存:因为在load model时,是先放在内存里面,所以内存不能小,最好在30GB左右
显存:如果用half()精度来load model的话(int4是不支持微调的),显存在16GB就可以,比如可以用kaggle的t4 gpu,这款性能相当于2070系列,但是显存翻倍
python:3.10即可
需要安装的包和版本:
!pip install modelscope -i https://pypi.tuna.tsinghua.edu.cn/simple/!pip install transformers==4.41.2 -i https://pypi.tuna.tsinghua.edu.cn/simple/!pip install cpm_kernels -i https://pypi.tuna.tsinghua.edu.cn/simple/!pip install peft==0.10.0!pip install gradio==3.40.0, mdtex2html
二、在弄环境时,可能会遇到很多问题,一般都是包版本不对导致的,在下面依次说明几种报错bug
报错1、Failed to initialize NumPy: _ARRAY_API not found (Triggered internally at C:\cb\pytorch_1000000000000\work\torch\csrc\utils\tensor_numpy.cpp:84.
Failed to initialize NumPy: _ARRAY_API not found (Triggered internally at ..\torch\csrc\utils\tenso
解决办法:
pip uninstall numpy pip install numpy==1.24.0
报错2、[bug]: ModuleNotFoundError: No module named 'safetensors._safetensors_rust' #4092
https://github.com/invoke-ai/InvokeAI/issues/4092
解决办法:
pip uninstall safetensors pip install safetensors==0.4.2
扩展1、用下面这段代码验证,torch是cpu还是gpu
import torch# 检查默认设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"默认设备: {device}")# 创建一个张量并检查其设备
tensor = torch.randn(3, 3, device=device)
print(f"张量设备: {tensor.device}")if tensor.device.type == "cpu":print("使用的是 CPU")
elif tensor.device.type == "cuda":print("使用的是 GPU")
else:print("未知的设备类型")import torch
print(torch.__version__) # 打印PyTorch版本号
print(torch.cuda.is_available()) # 打印CUDA是否可用
if torch.cuda.is_available():print("CUDA可用设备数量:", torch.cuda.device_count())print("当前使用的GPU设备:", torch.cuda.current_device())print("当前GPU设备名称:", torch.cuda.get_device_name(torch.cuda.current_device()))
报错3:Failed to load cpm_kernels:No module named 'cpm_kernels'
报错4:NameError: name 'round_up' is not defined
文档:https://github.com/THUDM/ChatGLM2-6B/issues/272
解决办法:pip install cpm_kernels
然后运行又报错,Failed to load cpm_kernels:[WinError 267] 目录名称无效。: 'C:\\Program Files\\Mozilla Firefox\\geckodriver.exe'
解决办法:卸载Mozilla Firefox,本质目的是删除Mozilla Firefox这个目录
报错5:[TypeError: ChatGLMTokenizer._pad() got an unexpected keyword argument 'padding_side'](https://github.com/THUDM/ChatGLM3/issues/1324#top)
报错6:chatglm3-6b\modeling_chatglm.py", line 413, in forward ,cache_k, cache_v = kv_cache , ValueError: too many values to unpack (expected 2)
解决办法:https://github.com/THUDM/ChatGLM3/issues/1324
降低版本:pip install transformers==4.41.2
三、训练全流程
1、准备数据
import json
with open("/kaggle/input/chatglm3-dataformatted-sample-json/chatGLM3_dataFormatted_sample.json", "r", encoding="UTF-8") as j_file:j_dict = json.load(j_file)
print("j_dict=", j_dict)from modelscope import AutoTokenizer, AutoModel, snapshot_download
import torchmodel_dir = snapshot_download("ZhipuAI/chatglm3-6b", revision = "v1.0.0")
with torch.no_grad():tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)model = AutoModel.from_pretrained(model_dir, trust_remote_code=True).float().cuda()import torch
def preprocess(conversations, tokenizer, max_tokens=None):"""Preprocess the data by tokenizing."""all_input_ids = [] # 存储所有处理后的输入IDall_labels = [] # 存储所有的标签print("len(conversations)=", len(conversations))index = -1for conv in conversations: # 对于每一组对话index += 1roles = [msg["role"] for msg in conv] # 获取对话中每个人的角色,例如“SYSTEM”, “ASSISTANT”或“USER”messages = [msg["content"] for msg in conv] # 获取对话中每个人的消息内容print("index=", index, "roles=", roles, "messages=", messages)# 断言第一个角色不是“ASSISTANT”和最后一个角色是“ASSISTANT”# 这个可以使用也可以不使用assert roles[0] != "ASSISTANT"assert roles[-1] == "ASSISTANT"input_messages = [] # 存储需要输入的消息# 根据角色将消息添加到input_messages中,"ASSISTANT"和"USER"的消息都被添加for role, msg in zip(roles, messages):if role == "ASSISTANT":input_messages.append(msg)elif role == "USER":input_messages.append(msg)# print("input_messages=", input_messages)#使用ChatGLM3的tokeninzer进行token处理tokenized_input = tokenizer(input_messages, add_special_tokens=False) # 对输入消息进行token化# print("tokenized_input=", tokenized_input)input_ids = [] # 初始化本次对话的输入IDlabels = [] # 初始化本次对话的标签# 根据第一个角色是"SYSTEM"还是其他角色来添加初始的输入ID和标签if roles[0] == "SYSTEM":input_ids.extend([64790, 64792, 64794, 30910, 13]) #起始位置拼接特定的token IDinput_ids.extend(tokenized_input.input_ids[0])labels.extend([-100] * (len(tokenized_input.input_ids[0]) + 5)) #将label设置成-100,这是由于在交叉熵计算时,-100对应的位置不参与损失值计算else:input_ids.extend([64790, 64792]) #起始位置拼接特定的token IDlabels.extend([-100] * 2) #将label设置成-100,这是由于在交叉熵计算时,-100对应的位置不参与损失值计算# print("input_ids=", input_ids, "labels=", labels)# 根据每个人的角色和token化的消息,添加输入ID和标签for role, msg in zip(roles, tokenized_input.input_ids):if role == "USER":if roles[0] == "SYSTEM":labels.extend([-100] * (len(msg) + 5))input_ids.extend([13, 64795, 30910, 13])else:labels.extend([-100] * (len(msg) + 4)) #将label设置成-100,这里是USER提问部分,不参与损失函数计算input_ids.extend([64795, 30910, 13]) #添加USER对话开始的起始符input_ids.extend(msg) #将当前的消息token添加到输入ID列表中input_ids.extend([64796]) #添加USER对话结束符# print("USER", "msg=",msg, "labels=",labels,"input_ids=",input_ids) elif role == "ASSISTANT": # 当角色为"ASSISTANT"时msg += [tokenizer.eos_token_id] # 在消息后面添加一个结束token的ID#这里的作用labels.extend([30910, 13]) #添加ASSISTANT对话开始的起始符labels.extend(msg) # 将当前的消息token添加到标签列表中input_ids.extend([30910, 13]) #添加ASSISTANT对话开始的起始符input_ids.extend(msg) # 将当前的消息token添加到输入ID列表中# print("ASSISTANT", "msg=",msg, "labels=",labels,"input_ids=",input_ids) # 打印ASSISTANT的消息和对应的token IDsif max_tokens is None: # 如果没有设定最大token数量max_tokens = tokenizer.model_max_length # 则使用tokenizer的模型最大长度作为最大token数量input_ids = torch.LongTensor(input_ids)[:max_tokens] # 将输入ID列表转化为LongTensor,并截取前max_tokens个tokenlabels = torch.LongTensor(labels)[:max_tokens] # 将标签列表转化为LongTensor,并截取前max_tokens个tokenassert input_ids.shape == labels.shape # 判断输入ID的tensor和标签的tensor形状是否相同,确保一一对应all_input_ids.append(input_ids) # 将处理后的输入ID添加到所有输入ID列表中all_labels.append(labels) # 将处理后的标签添加到所有标签列表中print("=========================")return dict(input_ids=all_input_ids, labels=all_labels)
2、一些额外读懂代码的函数
import torch.nn
from peft import LoraConfig, get_peft_modeldef print_trainable_parameters(model):"""Prints the number of trainable parameters in the model."""trainable_params = 0all_param = 0for _, param in model.named_parameters():all_param += param.numel()if param.requires_grad:trainable_params += param.numel()print(f"trainable params: {trainable_params} || "f"all params: {all_param} || "f"trainable: {100 * trainable_params / all_param}%")# print_trainable_parameters(model)def find_all_target_names(model,target_moude = torch.nn.Linear):lora_module_names = set()all_module_names = list()for name, module in model.named_modules():all_module_names.append(name)if type(module) == target_moude:names = name.split('.')lora_module_names.add(names[0] if len(names) == 1 else names[-1])if "lm_head" in lora_module_names: # needed for 16-bitlora_module_names.remove("lm_head")return list(lora_module_names), all_module_names# lora_module_names, all_module_names = find_all_target_names(model)
# print("lora_module_names=", lora_module_names)
# print("all_module_names=", all_module_names)
3、数据处理类
import json
import torch
from torch.utils.data import Dataset
from modelscope import AutoTokenizer
# from dataset import utilsclass ChatDataset(Dataset):# 初始化函数,接收以下参数:# conversations: 一个包含对话数据的字典,默认为j_dict# tokenizer: 用于tokenization的工具,默认为tokenizer# max_tokens: 最大token数量限制,默认为Nonedef __init__(self, conversations: {} = j_dict, tokenizer=tokenizer, max_tokens=None):# 通过super()调用父类Dataset的初始化函数super(ChatDataset, self).__init__()# 使用utils.preprocess函数预处理对话数据,得到处理后的数据字典data_dict = preprocess(conversations, tokenizer, max_tokens)# 从处理后的数据字典中提取input_ids和labels,并保存到类的属性中self.input_ids = data_dict["input_ids"]self.labels = data_dict["labels"]# 重写__len__方法,返回处理后的input_ids的长度,即数据集的大小def __len__(self):return len(self.input_ids)# 重写__getitem__方法,使得可以通过索引i获取数据集中第i个样本的input_ids和labelsdef __getitem__(self, i):return dict(input_ids=self.input_ids[i], labels=self.labels[i])# 定义一个名为 DataCollatorForChatDataset 的类,它继承自 object 类。
class DataCollatorForChatDataset(object):"""Collate examples for supervised fine-tuning."""# 初始化函数,这里没有接收特定的参数。def __init__(self):# 初始化一个属性 padding_value,并设置其值为0。这个属性后续用于 padding 操作。self.padding_value = 0# 定义一个特殊方法 __call__,这使得类的实例能够像函数一样被调用。def __call__(self, instances):# instances 参数应该是一个包含多个实例的列表,每个实例都是一个字典,包含 'input_ids' 和 'labels'。# 通过列表推导式,分别提取每个实例的 'input_ids' 和 'labels',并组成新的列表。input_ids, labels = tuple([instance[key] for instance in instances] for key in ("input_ids", "labels"))# 使用 torch.nn.utils.rnn.pad_sequence 函数对 input_ids 列表进行 padding 操作,使得所有的序列长度一致。# 参数 batch_first=True 表示输入的数据是 batch-major,即第一个维度是 batch 维度。# 参数 padding_value=self.padding_value 表示用 0 进行 padding。input_ids = torch.nn.utils.rnn.pad_sequence(input_ids, batch_first=True, padding_value=self.padding_value)# 与上面类似,对 labels 列表进行 padding 操作,但是这里使用 -100 进行 padding。labels = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True, padding_value=-100)# 返回一个字典,包含经过处理后的 input_ids, labels, 以及根据 input_ids 生成的 attention_mask。# attention_mask 是一个布尔类型的张量,它的作用是在模型处理输入时,告诉模型哪些部分是真正的内容,哪些部分是 padding。return dict(input_ids=input_ids,labels=labels,attention_mask=input_ids.ne(self.padding_value),)
4、开始训练以及模型保存
import torch
from tqdm import tqdm
from peft import LoraConfig, get_peft_model
from modelscope import AutoTokenizer, AutoModel
from torch.utils.data import DataLoader, Dataset# 在这段代码中,首先通过torch.no_grad()上下文管理器禁用了梯度计算,这通常用于推理(inference)阶段,因为在这个阶段我们不需要计算梯度,禁用梯度计算可以减少内存消耗并加速计算过程。
# model_dir = "../../chatglm3-6b"model_dir = snapshot_download("ZhipuAI/chatglm3-6b", revision = "v1.0.0")
with torch.no_grad():tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)model = AutoModel.from_pretrained(model_dir, trust_remote_code=True).half().cuda(1)lora_config = LoraConfig(r=8,lora_alpha=16,target_modules=["query_key_value"],#query_key_valuelora_dropout=0.05,bias="none",task_type="CAUSAL_LM", #SEQ_2_SEQ_LM
)BATCH_SIZE = 1
LEARNING_RATE = 2e-6
device = "cuda"model = get_peft_model(model, lora_config)
model.print_trainable_parameters()# import get_data
# train_dataset = get_data.ChatDataset()
# datacollect = get_data.DataCollatorForChatDataset()train_dataset = ChatDataset()
datacollect = DataCollatorForChatDataset()
# 参数collate_fn (Callable, optional): merges a list of samples to form a
# mini-batch of Tensor(s). Used when using batched loading from a
# map-style dataset.
train_loader = (DataLoader(train_dataset, batch_size=BATCH_SIZE,shuffle=True,collate_fn=datacollect))
print("len(train_loader)=", len(train_loader))loss_fun = torch.nn.CrossEntropyLoss(ignore_index=-100)optimizer = torch.optim.AdamW(model.parameters(), lr = LEARNING_RATE)
lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,T_max = 2400,eta_min=2e-6,last_epoch=-1)for epoch in range(1):pbar = tqdm(train_loader,total=len(train_loader))for data_dict in pbar:optimizer.zero_grad()# print("input_ids=", data_dict["input_ids"])# print("labels=", data_dict["labels"])input_ids = data_dict["input_ids"].to(device);input_ids = input_ids[:,:-1]labels = data_dict["labels"].to(device);labels = labels[:,1:]logits = model(input_ids)["logits"]logits = logits.view(-1, logits.size(-1));labels = labels.view(-1)loss = loss_fun(logits, labels)print("loss=", loss)# outputs = model(# input_ids=input_ids,# labels=labels,# )# loss = outputs.lossloss.backward()optimizer.step()lr_scheduler.step() # 执行优化器pbar.set_description(f"epoch:{epoch + 1}, train_loss:{loss.item():.5f}, lr:{lr_scheduler.get_last_lr()[0] * 1000:.5f}")# model.save_pretrained("./lora_saver/lora_query_key_value")