chatglm3如何进行微调
一、需要的环境
内存:因为在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 torch
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).float().cuda()
import torch
def preprocess(conversations, tokenizer, max_tokens=None):
"""
Preprocess the data by tokenizing.
"""
all_input_ids = [] # 存储所有处理后的输入ID
all_labels = [] # 存储所有的标签
print("len(conversations)=", len(conversations))
index = -1
for conv in conversations: # 对于每一组对话
index += 1
roles = [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 = [] # 初始化本次对话的输入ID
labels = [] # 初始化本次对话的标签
# 根据第一个角色是"SYSTEM"还是其他角色来添加初始的输入ID和标签
if roles[0] == "SYSTEM":
input_ids.extend([64790, 64792, 64794, 30910, 13]) #起始位置拼接特定的token ID
input_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 ID
labels.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 IDs
if 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个token
labels = torch.LongTensor(labels)[:max_tokens] # 将标签列表转化为LongTensor,并截取前max_tokens个token
assert 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_model
def print_trainable_parameters(model):
"""
Prints the number of trainable parameters in the model.
"""
trainable_params = 0
all_param = 0
for _, 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-bit
lora_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 utils
class ChatDataset(Dataset):
# 初始化函数,接收以下参数:
# conversations: 一个包含对话数据的字典,默认为j_dict
# tokenizer: 用于tokenization的工具,默认为tokenizer
# max_tokens: 最大token数量限制,默认为None
def __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和labels
def __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_value
lora_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.loss
loss.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")