GLM系列模型LORA微调【代码】

  • 主要记录LORA微调glm系列模型的流程

准备阶段

所需硬件

  • 显卡7G以上(INT4),最好需要32G

训练过程的主要区别(与编码模型相比)

其实主要就是PROMPT微调,除了要设置prompt以外,还需要使用lora

  • 需要提前在历史记录中预设好问答格式(最好与官方格式一致
  • 需要使用lora等技术降低可训练参数量
  • 需要以文本的形式输入label
  • 主要可以使用loss观察模型性能
  • 不需要怎么调参就能跑的很好

训练流程

  • 构建模板,规定输入和输出格式
  • 处理数据
  • 构建数据管道,只训练回答的部分,对提问的部分不计算loss
  • 构建lora部分
  • 训练
  • 验证

任务说明

  • 使用eprstmt小样本数据集
  • 任务:文本分类(情感分类/二分类)
  • 领域:电商评论
  • 数据格式
    • 训练数据32条
    • 验证数据32条
    • 测试数据610条
    • 无标签数据N条
  • 标签标识
    • 0 - 消极;1 - 积极

代码

  • https://github.com/GS233/LLM_share
  1. 设置路径
DEVICE = 'cuda:3'
DATASET = 'eprstmt'
model_path = '../models/chatglm2/'
path_to_data = '/workspace/ChatGLM2_test/data/demo1'
  1. 载入模型
import torch
from transformers import  AutoModel,AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModel.from_pretrained(model_path,trust_remote_code=True).bfloat16().cuda(DEVICE)
  1. 准备模板
  • 讲解任务,明确需求,给出案例,保留替换的部分
# define a basic prompt for express comment
# prompt template with 4shots ahead

prompt = """文本分类任务:将一段用户给外卖服务的评论进行分类,分成好评或者差评。

下面是一些范例:

这个产品的质量真的很出色! ->  好评
使用起来非常流畅,没有任何问题。 ->  好评
它的功能非常强大,帮助我轻松完成任务。 ->  好评
超级友好和专业的客服团队。 ->  好评
物流速度快,让我很惊喜。 ->  好评
包装得很仔细,确保了商品的安全。 ->  好评
这个价格真是太划算了! ->  好评

购买后不久,产品就出现了严重的问题,无法正常使用。 ->  差评
当我试图联系客服解决问题时,根本无法得到任何帮助。 ->  差评
他们的营销策略是欺诈性的,不能信任。 ->  差评
尝试退货并获得退款变得非常繁琐和困难。 ->  差评
他们不愿意承认产品的问题,拒绝退款申请。 ->  差评
产品附带的保修几乎无法使用,因为服务中心离我太远。 ->  差评
即使在保修期内,他们也找各种理由拒绝提供服务。 ->  差评


请对下述评论进行分类。务必只使用'好评'或者'差评'回答,务必不做任何说明和解释。

xxxxxx ->

"""

# 这是一个替换 xxxxx的函数
def get_prompt(text):
    return prompt.replace('xxxxxx',text)
# try for once and test model workable
response, his = model.chat(tokenizer, get_prompt('味道不错,下次还来'), history=[])
print(response)
print(his)
  1. 加入对话提示(历史记忆)
# 加入一些新的样本提示
his.append(("保修过程令人沮丧,几乎没有解决问题的帮助。 -> ","差评"))
his.append(("1这个产品简直就是一场灾难,存在严重的设计缺陷。  -> ","差评"))
his.append(("这款产品的性能超出了我的预期。  -> ","好评"))
his.append(("商品准时送达,包装完好无损。 -> ","差评"))
# 相当于使用prompt进行了9-shot提示
print(his)
  1. 导入数据
from utils.read import *
train,dev,test,_ = readFile(path_to_data,DATASET,0)
print(train['text'][0],train['label'][0])
print('标签分布: ',Counter(train['label']))
# 处理标签
def label_handler(original_data):
    for id,label in enumerate(original_data['label']):
        if label == 0:
            original_data['label'][id] = '差评'
        else:
            original_data['label'][id] = '好评'
    return original_data
train = label_handler(train)
test  = label_handler(test)
# print(train['label'])
# print(label_handler(train)['label'])
# model.build_inputs
def build_inputs(query, history):
    prompt = ""
    for i, (old_query, response) in enumerate(history):
        prompt += "[Round {}]\n\n问:{}\n\n答:{}\n\n".format(i + 1, old_query, response)
    prompt += "[Round {}]\n\n问:{} -> \n\n答:".format(len(history) + 1, query)
    return prompt 

print(build_inputs('性能不太行',history=his)) 
  1. 构建数据集
from datasets import Dataset

# 假设train['text']和train['label']是两个列表
trainData = {'text': train['text'], 'label': train['label']}
testData  = {'text': test['text'] , 'label': test ['label']}

ds_train = Dataset.from_dict(trainData)
ds_val  = Dataset.from_dict(testData)
  1. 短文本
from tqdm import tqdm
import transformers


max_seq_length = 128   # 短文本数据集就用少一点就可以
skip_over_length = True# 超过就不要了

tokenizer = transformers.AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

config = transformers.AutoConfig.from_pretrained(model_path, trust_remote_code=True)
  1. 处理一下数据
  • 返回一个字典,包含以下三个键值对
  • input_ids、context_len、target_len
# 定义一个名为preprocess的函数,它接受一个example参数
def preprocess(example):
    # 从example中获取"context"和"target"字段的内容
    context = example["text"]
    target = example["label"]
    
    # 使用tokenizer.encode将context文本编码为token IDs
    context_ids = tokenizer.encode(
        context, 
        max_length=max_seq_length,
        truncation=True)
    
    # 使用tokenizer.encode将target文本编码为token IDs,同时不添加特殊token(如[CLS]和[SEP])
    target_ids = tokenizer.encode(
        target,
        max_length=max_seq_length,
        truncation=True,
        add_special_tokens=False)
    
    # 将context_ids、target_ids和eos_token_id(end-of-sequence token)连接起来,形成模型的输入
    input_ids = context_ids + target_ids + [config.eos_token_id]
    
    # 返回一个字典,包含以下三个键值对
    return {"input_ids": input_ids, "context_len": len(context_ids), 'target_len': len(target_ids)}
  1. 处理文本
# 使用preprocess函数对ds_train数据集进行映射操作,生成新的数据集ds_train_token
ds_train_token = ds_train.map(preprocess).select_columns(['input_ids', 'context_len','target_len'])
# 如果skip_over_length为True,过滤掉长度超过max_seq_length的样本
if skip_over_length:
    ds_train_token = ds_train_token.filter(
        lambda example: example["context_len"] < max_seq_length and example["target_len"] < max_seq_length)
ds_val_token = ds_val.map(preprocess).select_columns(['input_ids', 'context_len','target_len'])
# 如果skip_over_length为True,过滤掉长度超过max_seq_length的样本
if skip_over_length:
    ds_val_token = ds_val_token.filter(
        lambda example: example["context_len"] < max_seq_length and example["target_len"] < max_seq_length)

10.构建数据管道(只训练需要训练的部分,只训练回答的部分)

def data_collator(features: list):
    len_ids = [len(feature["input_ids"]) for feature in features]
    longest = max(len_ids) #之后按照batch中最长的input_ids进行padding
    
    input_ids = []
    labels_list = []
    
    for length, feature in sorted(zip(len_ids, features), key=lambda x: -x[0]):
        ids = feature["input_ids"]
        context_len = feature["context_len"]
        
        labels = (
            [-100] * (context_len - 1) + ids[(context_len - 1) :] + [-100] * (longest - length)
        ) #-100标志位后面会在计算loss时会被忽略不贡献损失,我们集中优化target部分生成的loss
        
        ids = ids + [tokenizer.pad_token_id] * (longest - length)
        
        input_ids.append(torch.LongTensor(ids))
        labels_list.append(torch.LongTensor(labels))
        
        
    input_ids = torch.stack(input_ids)
    labels = torch.stack(labels_list)
    return {
        "input_ids": input_ids,
        "labels": labels,
    }
  1. 构建数据加载器
from torch.utils.data import Dataset 
dl_train = torch.utils.data.DataLoader(ds_train_token,num_workers=2,batch_size=4,
                                       pin_memory=True,shuffle=True,
                                       collate_fn = data_collator)
dl_val = torch.utils.data.DataLoader(ds_val_token,num_workers=2,batch_size=4,
                                    pin_memory=True,shuffle=True,
                                     collate_fn = data_collator)
# dl_train.size = 300 #每300个step视作一个epoch,做一次验证
  1. 看一下训练前的结果
preds = ['' for x in test['label']]
def predict(text):
    response, history = model.chat(tokenizer, f"{text} -> ", history=his,
    temperature=0.01)
    return response 

from tqdm import tqdm 

for i in tqdm(range(len(test['label']))):
    text = test['text'][i]
    preds[i] = predict(text)

count = 0
for i,j in zip(preds,test['label']):
    if i == j:
        count+=1
print(count/len(test['label']))
  1. 构建lora模型
import warnings
warnings.filterwarnings("ignore")
from transformers import AutoTokenizer, AutoModel, TrainingArguments, AutoConfig
import torch
import torch.nn as nn
from peft import get_peft_model, LoraConfig, TaskType

model.supports_gradient_checkpointing = True  #节约cuda
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
#model.lm_head = CastOutputToFloat(model.lm_head)

model.config.use_cache = False  # silence the warnings. Please re-enable for inference!


peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, inference_mode=False,
    r=8,
    lora_alpha=32, lora_dropout=0.1,
)

model = get_peft_model(model, peft_config)
model.is_parallelizable = True
model.model_parallel = True
model.print_trainable_parameters()
  1. 构建训练的函数
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(input_ids=batch["input_ids"],labels=batch["labels"]).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 
  1. 训练
#仅仅保存lora可训练参数
def save_ckpt(self, ckpt_path='checkpoint.pt', accelerator = None):
    unwrap_net = accelerator.unwrap_model(self.net)
    unwrap_net.save_pretrained(ckpt_path)
    
def load_ckpt(self, ckpt_path='checkpoint.pt'):
    self.net = self.net.from_pretrained(self.net,ckpt_path)
    self.from_scratch = False
    
KerasModel.save_ckpt = save_ckpt 
KerasModel.load_ckpt = load_ckpt 


keras_model = KerasModel(model,loss_fn = None,
        optimizer=torch.optim.AdamW(model.parameters(),lr=2e-6))
ckpt_path = DATASET


keras_model.fit(train_data = dl_train,
                val_data = dl_val,
                epochs=300,patience=5,
                monitor='val_loss',mode='min',
                ckpt_path = ckpt_path,
                #mixed_precision='fp16'
               )
  1. 验证
from peft import PeftModel
from transformers import  AutoModel,AutoTokenizer
DEVICE = 'cuda:3'
DATASET = 'eprstmt'
model_path = '../models/chatglm2/'
path_to_data = '/workspace/ChatGLM2_test/data/demo1'
ckpt_path = DATASET
model = AutoModel.from_pretrained(model_path,
                                  load_in_8bit=False,
                                  trust_remote_code=True).cuda(DEVICE)
model = PeftModel.from_pretrained(model,ckpt_path)
model = model.merge_and_unload() #合并lora权重
def predict(text):
    response, history = model.chat(tokenizer, f"{text} -> ", history=his,
    temperature=0.01)
    return response 
predict('好用')
preds = ['' for x in test['label']]

from tqdm import tqdm 
for i in tqdm(range(len(test['label']))):
    text = test['text'][i]
    preds[i] = predict(text)
Counter(preds)
count = 0
for i,j in zip(preds,test['label']):
    if i == j:
        count+=1
print(count/len(test['label']))
  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值