前方干货预警:这篇文章可能是你目前能够找到的可以无痛跑通LLM微调并基本理解整个流程的门槛最低的入门范例。
门槛低到什么程度,本范例假设你是一个三无用户。
1,无NLP经验:你没有扎实的NLP理论知识,只有一些基本的炼丹经验。没关系,我们会在恰当的时候告诉你必要的原理。
2,无GPU:你没有任何一块可以使用的GPU。没关系,我们直接在Kaggle环境上使用免费的P100GPU,并给没有kaggle使用经验的小伙伴提供kaggle免费GPU使用视频讲解指南。
《Kaggle免费GPU使用攻略 https://www.bilibili.com/video/BV1oa411u7uR/》
3,无数据集:你没有数据集,也不知道如何构建数据集。没关系,我们提供一个仅使用一条样本构成的数据集来微调ChatGLM2,并演示讲解数据的整个处理过程。
备注:本范例是如下文章的QLoRA版本与Kaggle适配版本
公众号算法美食屋后台回复关键词:torchkeras,获取本文notebook源代码。
# 导入常用模块
import numpy as np
import pandas as pd
import torch
from torch import nn
from torch.utils.data import Dataset,DataLoader
import warnings
warnings.filterwarnings('ignore')
〇,预训练模型
我们先插入一些LLM的基础知识,然后加载并演示chatglm2-6b的使用方法。
什么是ChatGLM2-6b?
chatglm2是 Chat General Language Model Version2的缩写,翻译成中文就是,通用语言模型(GLM)的聊天(Chat)优化版本。
6b的意思是6个billion,也就是60亿参数的大模型。
整个名字中最核心的概念有两个:
第一个是LM:语言模型。语言模型本质上只能做一件事情,那就是判断一段话像不像人话。为了实现这个功能,一般用几百上千T的文本数据对语言模型进行Pretrain。完成Pretrain之后,语言模型就可以做文字接龙游戏了。也就是给一段话的上半部分,它可以按照最像人话的方式进行文本生成,输出下半部分。
例如,你给它上半部分:"世界上最高的山峰是什么?" 它可能会生成下半部分"世界上最长的河流是什么?",也可能生成下半部分为"珠穆朗玛峰。"
这两种文字接龙的方式都是合理的,这两段话在它的训练数据即各种互联网语料中都是常见的。但是显然,只有"珠穆朗玛峰。"这个结果才是属于符合人类偏好的。为了让LM按照人类偏好进行接龙,我们需要在预训练的基础上进行聊天优化。
第二个是Chat: 聊天优化。聊天优化只有一个目的,那就是偏好对齐。本质上就是让语言模型能够按照符合人类对话偏好的方式去进行文字接龙。这里的Chat和ChatGPT的Chat是相同的意思,就是语言模型不仅仅是会说人话的,还得会聊天。
这里的会聊天通常会用3H来衡量,那就是 helpful, honest, harmless。第一个helpful要求模型明白用户意图不能答非所问,第二个honest要求模型不能假话连篇满嘴跑火车也就是要避免幻觉。第三个harmless就是说模型要避免道德风险不能提供对人类社会有危害如黄色暴力等内容。
以人的视角来看,如果有个朋友跟我们聊天,他能够满足helpful, honest, harmless这3H的话,真的是情商非常高,非常会聊天了。
那么,如何训练出这样一个情商高会聊天的大语言模型呢?我们要走4个训练步骤。
其中第0个步骤是为了让它懂人话(会接龙),第1到3个步骤是让它懂聊天(会聊天),一般把第2~3个步骤合起来叫做RLHF(ReinForce Learning From Human FeedBack)。
step0,PT (预训练)。Pretrain. 用海量清洗过的无标注普通文本数据训练模型的文字接龙能力。
step1,SFT(指令微调)。Supervised FineTune. 人工标注数十至数百万对话数据进行初步的人类偏好对齐。
step2,RM(奖励建模)。Reward Model. 随机生成数十至数百万问题让模型多次采样回答,人工按照偏好符合程度排序打分。使用该份人工排序打分数据训练一个奖励模型,奖励模型可以对任何问题的回答按照人类偏好近似打分。
step3,RL(强化学习)。ReinForce Learning. 随机生成数千万数亿的问题让模型回答,根据奖励模型的反馈信号朝着人类偏好对齐的方向上进一步优化模型。
from transformers import AutoTokenizer,AutoConfig, AutoModel, BitsAndBytesConfig
#为了能够在kaggle中使用,需要设置 bnb_config
model_name_or_path = 'THUDM/chatglm2-6b'
bnb_config=BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True, #QLoRA 设计的 Double Quantization
bnb_4bit_quant_type="nf4", #QLoRA 设计的 Normal Float 4 量化数据类型
llm_int8_threshold=6.0,
llm_int8_has_fp16_weight=False,
)
tokenizer = AutoTokenizer.from_pretrained(
model_name_or_path, trust_remote_code=True) # cache_dir='./' 缓存到当前工作路径
model = AutoModel.from_pretrained(model_name_or_path,
quantization_config=bnb_config,
trust_remote_code=True) # cache_dir='./'
一,准备数据
1,构造数据
#定义一条知识样本~
keyword = '梦中情炉'
description = '''梦中情炉一般指的是炼丹工具torchkeras。
这是一个通用的pytorch模型训练模版工具。
torchkeras是一个三好炼丹炉:好看,好用,好改。
她有torch的灵动,也有keras的优雅,并且她的美丽,无与伦比。
所以她的作者一个有毅力的吃货给她取了一个别名叫做梦中情炉。'''
#对prompt使用一些简单的数据增强的方法,以便更好地收敛。
def get_prompt_list(keyword):
return [f'{keyword}',
f'你知道{keyword}吗?',
f'{keyword}是什么?',
f'介绍一下{keyword}',
f'你听过{keyword}吗?',
f'啥是{keyword}?',
f'{keyword}是何物?',
f'何为{keyword}?',
]
data =[{'prompt':x,'response':description} for x in get_prompt_list(keyword) ]
dfdata = pd.DataFrame(data)
display(dfdata)
from torch.utils.data import Dataset,DataLoader
class MyDataset(Dataset):
def __init__(self,df,tokenizer,
prompt_col = 'prompt',
response_col = 'response',
history_col = 'history',
max_context_length = 1024,
max_target_length = 1024
):
super().__init__()
self.__dict__.update(locals())
def __len__(self):
return len(self.df)
def get(self,index):
data = dict(self.df.iloc[index])
example = {}
#context根据prompt和history以及
example['context'] = self.tokenizer.build_prompt(
query = data[self.prompt_col],
history = data.get(self.history_col,None))
example['target'] = data[self.response_col]
return example
def __getitem__(self,index):
example = self.get(index)
a_ids = self.tokenizer.encode(text=example['context'],
add_special_tokens=True, truncation=True,
max_length=self.max_context_length)
b_ids = self.tokenizer.encode(text=example['target'],
add_special_tokens=False, truncation=True,
max_length=self.max_target_length)
input_ids = a_ids + b_ids + [tokenizer.eos_token_id]
#专注于 b_ids和 最后的eos_token_id的学习
labels = [-100]*len(a_ids)+b_ids+[tokenizer.eos_token_id]
return {'input_ids':input_ids,'labels':labels}
#训练集和验证集完全一样
ds_train = ds_val = MyDataset(dfdata,tokenizer)
print(ds_train[0])
2,构建管道
from transformers import DataCollatorForSeq2Seq
data_collator = DataCollatorForSeq2Seq(
tokenizer,
model=None,
label_pad_token_id=-100,
pad_to_multiple_of=None,
padding=True
)
dl_train = DataLoader(ds_train,batch_size = 4,
num_workers = 2, shuffle = True, collate_fn = data_collator
)
dl_val = DataLoader(ds_val,batch_size = 4,
num_workers = 2, shuffle = False, collate_fn = data_collator
)
for batch in dl_train:
break
batch.keys()
dict_keys(['input_ids', 'labels', 'attention_mask', 'position_ids'])
batch['input_ids'].shape
torch.Size([4, 114])
print(len(dl_train))
2
二,定义模型
下面我们将使用QLoRA算法来微调ChatGLM2模型,以便给模型注入和梦中情炉 torchkeras相关的知识。
为了便于大家理解,我们先插入一些LoRA和QLoRA算法的基础知识。
1,LoRA
LoRA是一种能够相比全参微调可以大幅节约训练时间和显存,同时微调效果基本可以相当于全参微调的算法。
其全称叫做 Low Rank Adaption,也就是低秩适配方法,由微软2021年提出的。
LoRA的思想非常简单,有些像SVD奇异值分解的想法。
保持预训练好的参数矩阵不变,在它的旁边学习两个小的低秩矩阵,用它们的乘积来作为大的参数矩阵需要改变的增量。
用公式表示如下:
在初始化的时候,矩阵初始化为0,矩阵随机初始化,这样开始的时候,增量是零矩阵,不影响推理结果。
2,QLoRA
QLoRA是Quantized LoRA的简称,相比LoRA能够进一步节约显存占用,并提升微调效果。
QLoRA在LoRA的基础上主要有3处创新。
a,NF4数据类型:提出了一种叫做NF4(4-bit NormalFloat)的量化数据类型。这种精心设计的量化数据类型在拥有很高的压缩率的情况下(大概是fp16的1/4)还能够保持非常高的精度。
b,Double Quantization方法:对NF4数据类型中使用的一些常量参数也做量化,进一步减少存储占用。
c,Paged Optimizers技术:这种技术使用了NVIDIA统一内存的特性,实现了CPU和GPU之间自动的页面转换,在GPU内存不足的情况下自动将优化器状态转移到CPU内存。
使用QLoRA算法需要结合bitsandbytes库和peft库。
为什么QLoRA相比LoRA还能够提升效果呢?
主要是因为QLoRA由于节约了大量存储空间,所以可以对更多的权重矩阵进行微调。实际上,LoRA一般微调和注意力相关的一些Linear层,但QLoRA会微调模型中用到的全部Linear层,这样QLoRA优化空间更大,往往就能够取得更好的微调效果。
from peft import get_peft_config, get_peft_model, TaskType
model.supports_gradient_checkpointing = True #
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
model.config.use_cache = False # silence the warnings. Please re-enable for inference!
from peft import prepare_model_for_kbit_training
model = prepare_model_for_kbit_training(model) #预处理量化模型以适配LoRA
import bitsandbytes as bnb
def find_all_linear_names(model):
"""
找出所有全连接层,为所有全连接添加低秩adapter
"""
cls = bnb.nn.Linear4bit
lora_module_names = set()
for name, module in model.named_modules():
if isinstance(module, cls):
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)
lora_modules = find_all_linear_names(model)
print(lora_modules)
['dense', 'query_key_value', 'dense_h_to_4h', 'dense_4h_to_h']
from peft import LoraConfig
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM, inference_mode=False,
r=8,
lora_alpha=32, lora_dropout=0.1,
target_modules= lora_modules
)
peft_model = get_peft_model(model, peft_config)
peft_model.is_parallelizable = True
peft_model.model_parallel = True
peft_model.print_trainable_parameters()
trainable params: 14,823,424 || all params: 3,403,134,976 || trainable%: 0.435581430197143
# 注意到LoRA算法 B矩阵的初始权重为0,所以训练前peft_model的输出等价于预训练模型model的输出
for name,para in peft_model.named_parameters():
if '.1.' in name:
break
if 'lora' in name.lower():
print(name+':')
print('shape = ',list(para.shape),'\t','sum = ',para.sum().item())
print('\n')
peft_model.train();
out = peft_model(**batch)
三,训练模型
下面召唤我们的梦中情炉torchkeras,来实现最优雅的训练循环~
注意这里,为了更加高效地保存和加载参数,我们覆盖了KerasModel中的load_ckpt和save_ckpt方法,
仅仅保存和加载可训练lora权重,这样可以避免加载和保存全部模型权重造成的存储问题。
from torchkeras import KerasModel
from accelerate import Accelerator
class StepRunner:
def __init__(self, net, loss_fn, accelerator=None, stage = "train", metrics_dict = None,
optimizer = None, lr_scheduler = None
):
self.net,self.loss_fn,self.metrics_dict,self.stage = net,loss_fn,metrics_dict,stage
self.optimizer,self.lr_scheduler = optimizer,lr_scheduler
self.accelerator = accelerator if accelerator is not None else Accelerator()
if self.stage=='train':
self.net.train()
else:
self.net.eval()
def __call__(self, batch):
#loss
with self.accelerator.autocast():
loss = self.net(**batch).loss
#backward()
if self.optimizer is not None and self.stage=="train":
self.accelerator.backward(loss)
if self.accelerator.sync_gradients:
self.accelerator.clip_grad_norm_(self.net.parameters(), 1.0)
self.optimizer.step()
if self.lr_scheduler is not None:
self.lr_scheduler.step()
self.optimizer.zero_grad()
all_loss = self.accelerator.gather(loss).sum()
#losses (or plain metrics that can be averaged)
step_losses = {self.stage+"_loss":all_loss.item()}
#metrics (stateful metrics)
step_metrics = {}
if self.stage=="train":
if self.optimizer is not None:
step_metrics['lr'] = self.optimizer.state_dict()['param_groups'][0]['lr']
else:
step_metrics['lr'] = 0.0
return step_losses,step_metrics
KerasModel.StepRunner = StepRunner
#仅仅保存lora相关的可训练参数
def save_ckpt(self, ckpt_path='checkpoint', accelerator = None):
unwrap_net = accelerator.unwrap_model(self.net)
unwrap_net.save_pretrained(ckpt_path)
def load_ckpt(self, ckpt_path='checkpoint'):
self.net = self.net.from_pretrained(self.net.base_model.model,ckpt_path)
self.from_scratch = False
KerasModel.save_ckpt = save_ckpt
KerasModel.load_ckpt = load_ckpt
# 此处设置is_paged=True,即使用Paged Optimizer,减少训练过程中Cuda OOM的风险。
optimizer = bnb.optim.adamw.AdamW(peft_model.parameters(),
lr=5e-05,is_paged=True)
keras_model = KerasModel(peft_model,loss_fn = None,
optimizer=optimizer)
ckpt_path = 'chatglm2_qlora'
dfhistory = keras_model.fit(train_data = dl_train,
val_data = dl_val,
epochs=30,
patience=4,
monitor='val_loss',
mode='min',
ckpt_path = ckpt_path,
gradient_accumulation_steps = 2
)
四,上传模型
为了使用模型,我们需要合并LoRA权重到预训练模型权重中。
由于LoRA权重是非量化类型(float32)的,要求加载的预训练模型权重非量化类型(float32或fp16)。
不使用量化的话,加载模型权重需要较高的内存,但在GPU模式下,Kaggle的机器只有13个G的内存。
直接加载预训练模型权重会报OOM的错误。所以我们需要切换到CPU模式下(30G内存)。
合并且保存权重后,再切换成GPU模式(有些麻烦,但尚可接受)。
可以在右边Accelerator选项栏中选择 None,即切换回CPU模式。
#安装环境(为避免GPU下OOM,切换成CPU模式后重新安装环境)
!pip install -q -U transformers
!pip install -q git+https://github.com/huggingface/peft #使用最新版本非常重要,否则可能报错
# 导入常用模块
import numpy as np
import pandas as pd
import torch
from torch import nn
from torch.utils.data import Dataset,DataLoader
import warnings
warnings.filterwarnings('ignore')
#务必将notebook切换成CPU模式,否则会报OOM错误
print(torch.cuda.is_available())
False
from transformers import AutoTokenizer, AutoModel
#为了能够在kaggle中使用,需要设置 bnb_config
model_name_or_path = "THUDM/chatglm2-6b"
tokenizer = AutoTokenizer.from_pretrained(
model_name_or_path, trust_remote_code=True) # cache_dir='./' 缓存到当前工作路径
model = AutoModel.from_pretrained(model_name_or_path,
trust_remote_code=True) # cache_dir='./'
from peft import PeftModel
ckpt_path = 'chatglm2_qlora/'
peft_loaded = PeftModel.from_pretrained(model,ckpt_path)
model_new = peft_loaded.merge_and_unload() #合并lora权重
save_path = "chatglm2-6b-torchkeras"
model_new.save_pretrained(save_path, max_shard_size='2GB')
tokenizer.save_pretrained(save_path)
('chatglm2-6b-torchkeras/tokenizer_config.json',
'chatglm2-6b-torchkeras/special_tokens_map.json',
'chatglm2-6b-torchkeras/tokenizer.model',
'chatglm2-6b-torchkeras/added_tokens.json')
#从chatglm2-6b官方仓库下载其他的依赖文件,忽略权重
!GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/THUDM/chatglm2-6b/
!cp chatglm2-6b/*.py {save_path}
from huggingface_hub import login
login() #需要注册一个huggingface账户,在个人页面setting那里创建一个有write权限的access token
from huggingface_hub import HfApi
api = HfApi()
#创建huggingface 模型库
repo_id = "lyhue1991/chatglm2-6b-torchkeras"
api.create_repo(repo_id=repo_id)
#上传模型可能需要等待10分钟左右~
api.upload_folder(
folder_path=save_path,
repo_id=repo_id,
repo_type="model", #space, model, datasets
)
上传成功后效果如下
五,使用模型
我们重新切换成GPU环境,直接从huggingface导入刚才我们训练好的 'lyhue1991/chatglm2-6b-torchkeras'模型进行测试。就好像导入chatglm2-6b官方模型一样。
# 导入常用模块
import numpy as np
import pandas as pd
import torch
from torch import nn
from torch.utils.data import Dataset,DataLoader
import warnings
warnings.filterwarnings('ignore')
from transformers import AutoTokenizer,AutoConfig, AutoModel, BitsAndBytesConfig
#为了能够在kaggle中使用,需要设置 bnb_config
model_name_or_path = 'lyhue1991/chatglm2-6b-torchkeras'
config = AutoConfig.from_pretrained(model_name_or_path, trust_remote_code=True)
bnb_config=BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True, #QLoRA 设计的 Double Quantization
bnb_4bit_quant_type="nf4", #QLoRA 设计的 Normal Float 4 量化数据类型
llm_int8_threshold=6.0,
llm_int8_has_fp16_weight=False,
)
tokenizer = AutoTokenizer.from_pretrained(
'THUDM/chatglm2-6b' , trust_remote_code=True) # cache_dir='./' 缓存到当前工作路径
model = AutoModel.from_pretrained(model_name_or_path,
config=config,
quantization_config=bnb_config,
trust_remote_code=True) # cache_dir='./'
#注册魔法命令便于jupyter中使用
from torchkeras.chat import ChatGLM
chatglm = ChatGLM(model,tokenizer,stream=True)
公众号算法美食屋后台回复关键词:torchkeras,获取本文notebook源码以及更多有趣范例。🤗🤗