Baichuan-13B 保姆级微调范例

技术

干货预警:这可能是你能够找到的 最容易懂的最完整的适用于各种NLP任务 Baichuan-13B-Chat的finetune教程~

Baichuan-13B是百川智能于2023年7月11日发布的开源中英双语LLM,各项指标经评测在开源LLM中同尺寸模型中位居前列。

Baichuan-13B包括Baichuan-13B-Base和Baichuan-13B-chat两个不同模型。前者仅仅是预训练模型,后者在前者基础上增加了SFT,RLHF等偏好对齐过程。

本范例微调的模型是Baichuan-13B-Chat,我们使用非常简单的,外卖评论数据集来实施微调,对一段外卖评论区分是好评还是差评。

可以发现,经过微调后的模型,相比直接 3-shot-prompt 可以取得明显更好的效果(0.89->0.90)。

虽然Baichuan-13B-Chat是一个百亿级的LLM,但由于我们使用非常节约显存的QLoRA微调算法,具备32G左右显存的GPU即可实施本过程。

值得注意的是,尽管我们以文本分类任务为例,实际上,任何NLP任务,例如,命名实体识别,翻译,聊天对话等等,都可以通过加上合适的上下文,转换成一个对话问题,并针对我们的使用场景,设计出合适的数据集来微调Baichuan-13B-Chat.

注,本教程是 ChatGLM2-6b保姆级微调范例 的兄弟版本~ 😋

60分钟吃掉ChatGLM2-6b微调范例~

公众号算法美食屋后台回复关键词: torchkeras ,获取本文notebook源码和waimai数据集。

waimai数据集简单评测对比:

picture.image

〇,预训练模型

我们需要从 https://huggingface.co/baichuan-inc/Baichuan-13B-Chat 下载baichuan-13b-chat的模型。

国内可能速度会比较慢,总共有25个G左右,网速不太好的话,大概可能需要两到三个小时。

如果网络不稳定,也可以手动从这个页面一个一个下载全部文件然后放置到 一个文件夹中例如 'baichuan-13b' 以便读取。


        
          
import warnings  
warnings.filterwarnings('ignore')  

      

        
          
import torch  
from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, AutoModel, BitsAndBytesConfig  
from transformers.generation.utils import GenerationConfig  
import torch.nn as nn  
  
  
#使用QLoRA引入的 NF4量化数据类型以节约显存  
model_name_or_path ='../baichuan-13b' #远程 'baichuan-inc/Baichuan-13B-Chat'  
  
bnb_config=BitsAndBytesConfig(  
            load_in_4bit=True,  
            bnb_4bit_compute_dtype=torch.float16,  
            bnb_4bit_use_double_quant=True,  
            bnb_4bit_quant_type="nf4",  
            llm_int8_threshold=6.0,  
            llm_int8_has_fp16_weight=False,  
        )  
  
tokenizer = AutoTokenizer.from_pretrained(  
   model_name_or_path, trust_remote_code=True)  
  
model = AutoModelForCausalLM.from_pretrained(model_name_or_path,  
                quantization_config=bnb_config,  
                trust_remote_code=True)   
  
model.generation_config = GenerationConfig.from_pretrained(model_name_or_path)  
  

      

        
          
from IPython.display import clear_output   
messages = []  
messages.append({"role": "user",  
                 "content": "世界上第二高的山峰是哪座?"})  
response = model.chat(tokenizer,messages=messages,stream=True)  
for res in response:  
    print(res)  
    clear_output(wait=True)  
      
      

      

picture.image

下面我们设计一个3-shot-prompt方法,使用外卖数据集测试一下BaiChuan13b的文本分类能力。


        
          
prefix = """外卖评论文本分类任务:  
下面是一些范例:  
  
味道真不错 -> 好评  
太辣了,吃不下都  -> 差评  
  
请对下述评论进行分类。返回'好评'或者'差评'。  
"""  
  
def get\_prompt(text):  
    return prefix+text+' -> '  
  

      

        
          
messages  = [{"role": "user", "content": get_prompt('味道不错,下次再来')}]  
response = model.chat(tokenizer, messages)  
print(response)  
  

      

          
好评  

      

        
          
messages = messages+[{"role": "assistant", "content": response}]  
print(messages)  
  

      

        
          
def get\_message(prompt,response):  
    return [{"role": "user", "content": f'{prompt} -> '},  
            {"role": "assistant", "content": response}]  

      

        
          
messages.extend(get_message('太贵了','差评'))  
messages.extend(get_message('非常快,味道好','好评'))  
messages.extend(get_message('这么咸,真的是醉了','差评'))  

      

        
          
messages   

      

picture.image


        
          
def predict(text,temperature=0.01):  
    model.generation_config.temperature=temperature  
    response = model.chat(tokenizer,   
                          messages = messages+[{'role':'user','content':f'{text} -> '}])  
    return response  
  

      

picture.image

我们拿外卖数据集来测试一下未经微调,预训练模型的效果。


        
          
import pandas as pd   
import numpy as np   
import datasets   
from tqdm import tqdm   
  

      

        
          
#数据集加载  
dftrain = pd.read_parquet('../data/dftrain.parquet')[['text','label','tag']]  
dftest = pd.read_parquet('../data/dftest.parquet')[['text','label','tag']]  
ds_train,ds_val = datasets.Dataset.from_pandas(dftrain).train_test_split(  
    test_size=1000,seed=42).values()\  
  
dftrain,dfval = ds_train.to_pandas(), ds_val.to_pandas()   

      

        
          
dftest['pred'] = [predict(text) for text in tqdm(dftest['text'])]  

      

picture.image

一,准备数据

我们仿照百川模型的 model._build_chat_input 方法来进行token编码,同时把需要学习的内容添加label.

1,token编码


        
          
import torch   
  
#将messages编码成 token, 同时返回labels, 该函数适用于多轮对话数据  
#注意baichuan-13b通过插入tokenizer.user\_token\_id和tokenizer.assistant\_token\_id 来区分用户和机器人会话内容  
  
# reference@ model.\_build\_chat\_input?  
def build\_chat\_input(messages, model=model,  
                     tokenizer=tokenizer,   
                     max\_new\_tokens: int=0):  
    max_new_tokens = max_new_tokens or model.generation_config.max_new_tokens  
    max_input_tokens = model.config.model_max_length - max_new_tokens  
    max_input_tokens = max(model.config.model_max_length // 2, max_input_tokens)  
      
    total_input, round_input, total_label, round_label = [], [], [], []  
      
    for i, message in enumerate(messages[::-1]):  
        content_tokens = tokenizer.encode(message['content'])  
        if message['role'] == 'user':  
            round_input = [model.generation_config.user_token_id] + content_tokens + round_input  
            round_label = [-100]+[-100 for _ in content_tokens]+ round_label  
              
            if total_input and len(total_input) + len(round_input) > max_input_tokens:  
                break  
            else:  
                total_input = round_input + total_input  
                total_label = round_label + total_label  
                if len(total_input) >= max_input_tokens:  
                    break  
                else:  
                    round_input = []  
                    round_label = []  
                      
        elif message['role'] == 'assistant':  
            round_input = [  
                model.generation_config.assistant_token_id  
            ] + content_tokens + [  
                model.generation_config.eos_token_id  
            ] + round_input  
  
            round_label = [  
                -100  
            ] + content_tokens + [  
                model.generation_config.eos_token_id  
            ]+ round_label  
        else:  
            raise ValueError(f"message role not supported yet: {message['role']}")  
              
    total_input = total_input[-max_input_tokens:]  # truncate left  
    total_label = total_label[-max_input_tokens:]  
      
    total_input.append(model.generation_config.assistant_token_id)  
    total_label.append(-100)  
      
    return total_input,total_label  
  

      

2,做数据集


        
          
from torch.utils.data import Dataset,DataLoader   
class MyDataset(Dataset):  
    def \_\_init\_\_(self,df,  
                 prefix=prefix  
                ):  
        self.df = df   
        self.prefix=prefix  
          
    def \_\_len\_\_(self):  
        return len(self.df)  
          
    def get\_samples(self,index):  
        samples = []  
        d = dict(self.df.iloc[index])  
        samples.append(d)  
        return samples  
      
    def get\_messages(self,index):  
        samples = self.get_samples(index)  
        messages = []  
        for i,d in enumerate(samples):  
            if i==0:  
                messages.append({'role':'user','content':self.prefix+d['text']+' -> '})  
            else:  
                messages.append({'role':'user','content':d['text']+' -> '})  
              
            messages.append({'role':'assistant','content':d['tag']})  
        return messages  
          
    def \_\_getitem\_\_(self,index):  
        messages = self.get_messages(index)  
        input_ids, labels = build_chat_input(messages)  
        return {'input\_ids':input_ids,'labels':labels}  
  
    def show\_sample(self,index):  
        samples = self.get_samples(index)  
        print(samples)  
      
      

      

        
          
ds_train = MyDataset(dftrain)  
ds_val = MyDataset(dfval)  
  

      

picture.image

3,创建管道


        
          
def data\_collator(examples: list):  
    len_ids = [len(example["input\_ids"]) for example in examples]  
    longest = max(len_ids) #之后按照batch中最长的input\_ids进行padding  
      
    input_ids = []  
    labels_list = []  
      
    for length, example in sorted(zip(len_ids, examples), key=lambda x: -x[0]):  
        ids = example["input\_ids"]  
        labs = example["labels"]  
          
        ids = ids + [tokenizer.pad_token_id] * (longest - length)  
        labs = labs + [-100] * (longest - length)  
          
        input_ids.append(torch.LongTensor(ids))  
        labels_list.append(torch.LongTensor(labs))  
            
    input_ids = torch.stack(input_ids)  
    labels = torch.stack(labels_list)  
    return {  
        "input\_ids": input_ids,  
        "labels": labels,  
    }  
  

      

        
          
import torch   
dl_train = torch.utils.data.DataLoader(ds_train,num_workers=2,batch_size=4,  
                                       pin_memory=True,shuffle=True,  
                                       collate_fn = data_collator)  
  
dl_val = torch.utils.data.DataLoader(ds_val,num_workers=2,batch_size=4,  
                                    pin_memory=True,shuffle=False,  
                                     collate_fn = data_collator)  
  

      

        
          
for batch in dl_train:  
    break 
      

二,定义模型

下面我们将使用QLoRA(实际上用的是量化的AdaLoRA)算法来微调Baichuan-13b模型。


        
          
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!  
  

      

        
          
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)  
  

      

        
          
from peft import prepare_model_for_kbit_training   
model = prepare_model_for_kbit_training(model)  
  

      

        
          
lora_modules = find_all_linear_names(model)  
print(lora_modules)   
  

      

          
['down_proj', 'o_proj', 'up_proj', 'W_pack', 'gate_proj']  

      

        
          
from peft import AdaLoraConfig  
peft_config = AdaLoraConfig(  
    task_type=TaskType.CAUSAL_LM, inference_mode=False,  
    r=64,  
    lora_alpha=16, lora_dropout=0.05,  
    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()  
  

      

picture.image

三,训练模型


        
          
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.forward(**batch)[0]  
  
        #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   
  
#仅仅保存QLora可训练参数  
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'):  
    import os  
    self.net.load_state_dict(  
        torch.load(os.path.join(ckpt_path,'adapter\_model.bin')),strict =False)  
    self.from_scratch = False  
      
KerasModel.save_ckpt = save_ckpt   
KerasModel.load_ckpt = load_ckpt   
  

      

        
          
optimizer = bnb.optim.adamw.AdamW(peft_model.parameters(),  
                                  lr=6e-05,is_paged=True)  #'paged\_adamw'  
keras_model = KerasModel(peft_model,loss_fn =None,  
        optimizer=optimizer)   
ckpt_path = 'baichuan13b\_waimai'  
  
  

      

        
          
# keras\_model.load\_ckpt(ckpt\_path) #支持加载微调后的权重继续训练(断点续训)  
keras_model.fit(train_data = dl_train,  
                val_data = dl_val,  
                epochs=100,patience=10,  
                monitor='val\_loss',mode='min',  
                ckpt_path = ckpt_path  
               )  
  

      

picture.image

四,保存模型

为减少GPU压力,此处可重启kernel释放显存


        
          
import warnings   
warnings.filterwarnings('ignore')  

      

        
          
import torch  
from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, AutoModel, BitsAndBytesConfig  
from transformers.generation.utils import GenerationConfig  
import torch.nn as nn  
model_name_or_path ='../baichuan-13b'  
ckpt_path = 'baichuan13b\_waimai'  
tokenizer = AutoTokenizer.from_pretrained(  
    model_name_or_path,  
    trust_remote_code=True  
)  
model_old = AutoModelForCausalLM.from_pretrained(  
    model_name_or_path,  
    trust_remote_code=True,  
    low_cpu_mem_usage=True,  
    torch_dtype=torch.float16,  
    device_map='auto'  
)  
  

      

        
          
from peft import PeftModel  
  
#可能需要5分钟左右  
peft_model = PeftModel.from_pretrained(model_old, ckpt_path)  
model_new = peft_model.merge_and_unload()  
  

      

        
          
from transformers.generation.utils import GenerationConfig  
model_new.generation_config = GenerationConfig.from_pretrained(model_name_or_path)  

      

        
          
from IPython.display import clear_output  
messages = []  
messages.append({"role": "user",  
                 "content": "世界上第二高的山峰是什么?"})  
response = model_new.chat(tokenizer,messages=messages,stream=True)  
for res in response:  
    print(res)  
    clear_output(wait=True)
      

        
          
save_path = 'baichuan-13b-waimai'  

      

        
          
tokenizer.save_pretrained(save_path)  
model_new.save_pretrained(save_path)  

      

        
          
!cp baichuan-13b/*.py  baichuan-13b-waimai  

      

五,使用模型

为减少GPU压力,此处可再次重启kernel释放显存。


        
          
import torch  
from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, BitsAndBytesConfig  
from transformers.generation.utils import GenerationConfig  
import torch.nn as nn  
  
import warnings  
warnings.filterwarnings('ignore')  
  
model_name_or_path = 'baichuan-13b-waimai'  
  
bnb_config=BitsAndBytesConfig(  
            load_in_4bit=True,  
            bnb_4bit_compute_dtype=torch.float16,  
            bnb_4bit_use_double_quant=True,  
            bnb_4bit_quant_type="nf4",  
            llm_int8_threshold=6.0,  
            llm_int8_has_fp16_weight=False,  
        )  
  
tokenizer = AutoTokenizer.from_pretrained(  
   model_name_or_path, trust_remote_code=True)  
  
model = AutoModelForCausalLM.from_pretrained(model_name_or_path,  
                quantization_config=bnb_config,  
                trust_remote_code=True)   
  
model.generation_config = GenerationConfig.from_pretrained(model_name_or_path)  
  

      

        
          
from IPython.display import clear_output  
messages = []  
messages.append({"role": "user",  
                 "content": "世界上第二高的山峰是什么?"})  
response = model.chat(tokenizer,messages=messages,stream=True)  
for res in response:  
    print(res)  
    clear_output(wait=True)  

      

          
乔戈里峰。世界第二高峰———乔戈里峰  
海拔高度:8610米  
坐标纬度:35°49′15′′n,76°21′24′′e  
地理位置:喀喇昆仑山脉中巴边境上  

      

我们测试一下微调后的效果。


        
          
import pandas as pd   
import numpy as np   
import datasets   
from tqdm import tqdm   
  

      

        
          
prefix = """外卖评论文本分类任务:  
下面是一些范例:  
  
味道真不错 -> 好评  
太辣了,吃不下都  -> 差评  
  
请对下述评论进行分类。返回'好评'或者'差评'。  
"""  
  
def get\_prompt(text):  
    return prefix+text+' -> '  
  

      

        
          
messages  = [{"role": "user", "content": get_prompt('味道不错,下次再来')}]  
response = model.chat(tokenizer, messages)  
print(response)  
  

      

          
好评  

      

        
          
messages = messages+[{"role": "assistant", "content": response}]  
print(messages)  

      

        
          
def get\_message(prompt,response):  
    return [{"role": "user", "content": f'{prompt} -> '},  
            {"role": "assistant", "content": response}]  

      

        
          
messages.extend(get_message('太贵了','差评'))  
messages.extend(get_message('非常快,味道好','好评'))  
messages.extend(get_message('这么咸,真的是醉了','差评'))  

      

        
          
def predict(text,temperature=0.01):  
    model.generation_config.temperature=temperature  
    response = model.chat(tokenizer,   
                          messages = messages+[{'role':'user','content':f'{text} -> '}])  
    return response
      

picture.image

微调后的acc为0.9015,相比微调前的0.8925,约提升1个百分点。

以上 。

公众号算法美食屋后台回复关键词: torchkeras ,获取本文notebook源码和更多有趣范例~

picture.image

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
边缘云打通大模型物理世界
《火山引擎边缘智能,打通大模型的物理世界》 张俊钦 | 火山引擎边缘智能资深研发工程师
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论