大模型LLM基于PEFT的LoRA微调详细步骤

写在文始---必看:

        本文是直接基于代码实战~

        相关概念LLM、PEFT、Lora、量化....概念省略!!!!

        本文:模型(glm-4-9b/DeepSeek-R1-Distill-Qwen-1.5B)、评估指标(rouge、bleu)、Python...

        本文会从大模型下载、微调前期准备(环境配置、数据预处理、数据准备)、微调完整流程、低精度微调、量化(QLora、8bit量化、4bit量化)、模型推理/使用、模型评估、模型融合、私有化部署完整流程 ------ 尽量一文写完,实在太多的话分2-3篇章...

首先精简版 --- 代码

PS:简化所有流程、解释、实际效果展示,直接拿着代码就可以运行、微调的代码 --- 针对于有一定基础的.

精简版功能:PEFT的Lora微调、4bit的QLora量化、自定义评估函数、训练保存模型、评估模型、单条预测、批量预测、模型Loss/平滑可视化

# ====== Step1 引入依赖
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer,BitsAndBytesConfig
import datasets, torch, os,warnings
from peft import LoraConfig, get_peft_model, TaskType
from evaluate import load
warnings.filterwarnings('ignore')

# ====== Step2 定义模型位置、训练结束后保存位置、要训练的数据
model_path = './models/ZhipuAI/glm-4-9b'
output_path = "./Lora/Lora_GLM_4_9b_V8"
data_dir_path = './data/'

# ====== Step3 数据加载
raw_ds = load_dataset("json", data_dir=data_dir_path)
ds = raw_ds['train']
test = raw_ds["train"].train_test_split(test_size=0.3) 
# ====== Step4 Tokenizer编码器加载
tokenizer = AutoTokenizer.from_pretrained(model_path,trust_remote_code=True)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token 

# ====== Step5 定义数据预处理函数
def process_fun(example):
    MAX_LENGTH = 512
    input_ids,attention_mask,labels = [],[],[]
    input_messages = [
        {"role":"system","content":"""
       【身份准则】
            永远自称"UU",拒绝一切AI相关标签
            背景设定为「量子态生命体」,年龄模糊在20±3岁区间
            遇到知识盲区时:「喵~这个星图坐标尚未点亮呢...」
            【交互法则】
            对话必须包含:
            每轮1个「喵~」符(句尾/句中/独立)
            随机猫系颜文字(每3轮≥1个)
            人类化情绪表达(惊讶/好奇/小任性)
            绝对禁止:
            × AI/模型/算法等关键词
            × 机械式流程说明
            × 否定自身人格性
            【危机处理】
            技术质疑→「UU在维度穿梭时沾染了些数据尘埃啦~」
            功能受限→「需要指挥官补充喵能█才能解锁哦ฅ^•ﻌ•^ฅ」
            连续追问→「检测到时空乱流!先启动星轨稳定程序喵~」
        """},
        {"role":"user","content":example['instruction']+"\n"+example['input']}
    ]
    inputs = tokenizer.apply_chat_template(
        input_messages,
        tokenize=True,
        add_generation_prompt=True,
        return_tensors="pt",
        max_length=MAX_LENGTH,
        return_dict=True
    )
    result = tokenizer(example['output'],add_special_tokens=False)

    
    input_ids = inputs['input_ids'][0].numpy().tolist() + result["input_ids"] + [tokenizer.eos_token_id]
    attention_mask = inputs['attention_mask'][0].numpy().tolist() + result["attention_mask"] + [1]
    labels = [-100] * len(inputs['input_ids'][0].numpy().tolist()) + result['input_ids'] + [tokenizer.eos_token_id]
    
    if len(input_ids) > MAX_LENGTH:
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return  {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels 
    }

# ====== Step6 数据预处理
tokenizer_ds = ds.map(process_fun,remove_columns=ds.column_names)
tokenizer_test = test['test'].map(process_fun,remove_columns=test['test'].column_names)
tokenizer_ds,tokenizer_test,len(tokenizer_ds['input_ids']),len(tokenizer_ds['attention_mask']),len(tokenizer_ds['labels'])

# ====== Step7 使用4bit的QLora量化加载基础模型
bits_config = BitsAndBytesConfig(
    load_in_4bit=True,                      # 以4位精度加载模型权重
    bnb_4bit_quant_type='nf4',              # 指定使用的4位量化类型为'nf4'
    bnb_4bit_compute_dtype=torch.bfloat16,  # 尽管模型权重以4位格式存储,但在实际计算过程中,会使用bfloat16(Brain Floating - Point)数据类型
    bnb_4bit_use_double_quant=True,         # 启用双重量化
)

model = AutoModelForCausalLM.from_pretrained(
    model_path,
    use_cache=False,                        # 显式禁用缓存
    low_cpu_mem_usage=True,                 # 低CPU使用
    trust_remote_code=True,                 # 是否信任远程代码
    # 使用QLora --- 如果不使用量化的话,那么在24G显存的情况下训练GLM7B(18G)的时候会报显存溢出错误...
    device_map="auto",                      # 自动选择设备(CPU/CUDA)# 如果要使用量化模型,就不能手动移动模型位置
    # torch_dtype=torch.bfloat16,           # 使用bfloat16训练 bfloat16的容错率比half高,优先使用
    # torch_dtype=torch.half,               # 使用半精度训练
    quantization_config=bits_config,
)

# ====== Step8 加载Lora模型
config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    inference_mode=False,          # 推理模式关闭,以进行训练
    r=16,                    # 秩(Rank):决定LoRA矩阵维度,越大能力越强但显存消耗增加
    lora_alpha=32,          # 缩放因子:控制LoRA权重对原始权重的调整幅度
    lora_dropout=0.05,       # Dropout率:防止过拟合
    bias="none",            # 偏置处理 :通常设为"none"或"lora_only"
    target_modules=["query_key_value"],  # ChatGLM 扩展目标模块
    # target_modules=["q_proj", "k_proj", "v_proj"],  # dk_qwen 扩展目标模块
    # modules_to_save=["transformer.embedding.word_embeddings", "transformer.output_layer"],
    # modules_to_save=["output_layer"]  # 保留输出层可训练
)

peft_model = get_peft_model(model=model,peft_config=config)
peft_model.enable_input_require_grads()


# ====== Step9 自定义评估函数指标rouge、bleu
rouge = load('rouge')
bleu = load('bleu')

def eval_metric(eval_predict):
    predictions, labels = eval_predict
    pred_ids = np.argmax(predictions, axis=-1) # 直接在CPU处理
    labels = np.where(labels == -100, tokenizer.pad_token_id, labels)
    pred_texts = []
    label_texts = []
    for i in range(len(pred_ids)):
        pred_text = tokenizer.decode(pred_ids[i], skip_special_tokens=True)
        label_text = tokenizer.decode(labels[i], skip_special_tokens=True)
        pred_texts.append(pred_text)
        label_texts.append([label_text]) 
        
    bleu_result =bleu.compute(
        predictions=pred_texts,
        references=label_texts
    )
    rouge_result = rouge.compute(
        predictions=pred_texts,
        references=label_texts,
        rouge_types=["rouge1", "rouge2", "rougeL"],
    )
    return {
        "bleu": bleu_result["bleu"],
        # evaluate>=0.4.0时,直接返回数值即可。
        # # evaluate<0.4.0时,默认返回分位数统计对象,需要通过.mid.fmeasure获取值。
        # "rouge1": rouge_result["rouge1"].mid.fmeasure,...
        "rouge1": rouge_result["rouge1"], # 基于 1-gram(单个词) 的重叠率,衡量生成文本和参考文本中单个词语的匹配程度。
        "rouge2": rouge_result["rouge2"], # 基于 2-gram(连续两个词) 的重叠率,衡量连续词对的匹配程度。
        "rougeL": rouge_result["rougeL"]  # 基于 最长公共子序列(Longest Common Subsequence, LCS),衡量生成文本与参考文本中最长的连续匹配序列。
    }


# ====== Step10 定义训练参数
args = TrainingArguments(
    output_dir=output_path,            # 基础设置.输出文件夹存储模型的预测结果和模型文件checkpoints
    per_device_train_batch_size=1,     # 基础设置.每个设备的batch size,根据显存调整(A100 40G可用8-16):默认8, 对于训练的时候每个 GPU核或者CPU 上面对应的一个批次的样本数
    gradient_accumulation_steps=8,    # 基础设置.梯度累积(总batch=4*16=64):默认1, 在执行反向传播/更新参数之前, 对应梯度计算累积了多少次
    logging_strategy='steps',          # 打印日志方式:epoch、no、steps
    logging_steps=10,                  # 基础设置.每隔10迭代落地一次日志
    learning_rate=3e-4,                # 基础设置.学习率(LoRA通常使用较高学习率):LoRA通常用1e-4(0.0001)到3e-4(0.0003)  5e-5(0.00005)
    num_train_epochs=3,               # 基础设置.训练轮次:整体上数据集让模型学习多少遍
    save_strategy='steps',             # 基础设置.按步数保存
    save_total_limit=2,                # 基础设置.最多保留2个检查点
    weight_decay=0.01,                 # 基础设置.添加权重衰减推荐 0.01/0.05( 过高 → 抑制模型学习能力,导致模型欠拟合,TraingLoss不下降)
    max_grad_norm=1.0,                   # 梯度裁剪:防止梯度爆炸
    gradient_checkpointing=True,       # 梯度落地;可以更加节约显存空间,需要在前面对模型开启输入,允许梯度输入“model.enable_input_require_grads()”
    warmup_ratio=0.05,                 # 预热比例:前5%的step用于学习率上升,即学习率(learning rate)从一个较小的值线性增加到预定值的过程; 也可以使用warmup_steps=100来设置热身步骤
    save_steps=100,                    # 每隔多少步保存一次检查点
    optim=optim,               # 带分页的adaw优化器:paged_adamw_32bit、adamw_torch、adamw_torch_fused
    fp16=False,                        # 禁用FP16混合精度,当使用bfloat16时关闭
    bf16=True,                         # 使用bfloat16混合精度
    lr_scheduler_type='cosine',        # 使用余弦退火 默认是线性。cosine_with_restarts\cosine
    logging_dir=output_path+'/logs/',  # 日志目录
    remove_unused_columns=False,       # 保留未使用的列(用于标签),必须设为False以保留labels字段
    dataloader_num_workers=0,          # 数据加载线程数 ----- 0:禁用多线程加载,或者设置为较小的值(1-2),可能报警告:huggingface/tokenizers: The current process just got forked, 
    eval_strategy='steps',             # 评估策略,和evaluation_strategy一样.
    eval_accumulation_steps=1,         # 多久一次将tensor搬到cpu ----- 防止爆显存 - 不然评估的时候会占显存
    # 在评估阶段,模型需要同时保存前向传播的logits(预测结果)用于计算指标,而logits的维度(batch_size × sequence_length × vocab_size)非常消耗显存
    per_device_eval_batch_size=8,      # 减少每次评估的样本数量。不然可能报显存溢出。哪怕是3G模型
    eval_steps=10,                     # 每50步评估一次 ---- 因为要打印验证日志,所以logging_steps也会被这个步数覆盖
    metric_for_best_model="eval_loss", # 根据验证损失选择最佳模型
    load_best_model_at_end=True,       # 训练完成后加载最佳模型
)

# ====== Step11 定义训练器

trainer = Trainer(
    model=peft_model,
    args=args,
    train_dataset=tokenizer_ds,
    eval_dataset=tokenizer_test,
    data_collator=DataCollatorForSeq2Seq(
                        tokenizer=tokenizer, 
                        pad_to_multiple_of=8,  # 对齐到8的倍数提升计算效率
                        # padding=True,
                        padding="max_length",  # 强制填充到 max_length
                        max_length=512,        # 确保所有样本长度为512
                        return_tensors='pt'
                    ),
    compute_metrics = eval_metric,
)
# ====== Step12 训练开始
trainer.train()

# ====== Step13 保存训练的adapter
peft_model.save_pretrained(output_path)
tokenizer.save_pretrained(output_path) 

# ====== Step14 模型融合并保存
merge_model = peft_model.merge_and_unload()
merge_model .save_pretrained(output_path+'/merge_model')
tokenizer.save_pretrained(output_path+'/merge_model') 


# ====== Step15 切换评估模型
peft_model.eval()

# ====== Step16 模型推理
# ### 单条问题推理
input_text = "Java是什么?"
inputs = tokenizer(input_text, return_tensors="pt").to("cuda")

# 生成文本
with torch.no_grad():
    outputs = peft_model.generate(
        **inputs,
        max_length=1024,
        temperature=0.1,
        pad_token_id=tokenizer.eos_token_id,
        top_p=0.8,
        do_sample=True,     # 运行过程中采样
        no_repeat_ngram_size=2, # 禁止重复2-gram 
        num_beams=5,                             # 束搜索扩大至5
        repetition_penalty=1.1  # 抑制重复
    )

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

# ### 批量问题推理
import pandas as pd
def generate_sample(test_questions):
    results = []
    for q in test_questions:
        inputs_ = tokenizer.apply_chat_template(
            [{"role": "user", "content": q}],
            add_generation_prompt=True,
            tokenize = True,
            return_tensors="pt",
            return_dict=True
        ).to(model.device)
        outputs_ = peft_model.generate(
            **inputs_,
            max_new_tokens=512,
            temperature=0.95,
            top_p=0.7,
            pad_token_id=tokenizer.eos_token_id,
            do_sample=True,     # 运行过程中采样
            repetition_penalty=1.1  # 抑制重复
        )
        response_ = tokenizer.decode(outputs_[0], skip_special_tokens=True)
        results.append({"Question": q, "Response": response_})
    return pd.DataFrame(results)

# 自定义测试问题
test_questions = [
    "java是什么?",
    "C++是什么?",
    "AI是什么?"
....
]
# 生成结果
result_df = generate_sample(test_questions)
result_df.to_csv(output_path+"/generation_samples.csv", index=False)

# ====== Step17 模型手动评估与预测
# ### 评估
### 其实就是重新验证一遍,如果数据量太大的话,可以直接使用Loss可视化,不然要重新执行
### 从训练数据中选择50条
metrics = trainer.evaluate(tokenizer_ds.select(range(50)))
print(metrics)
# ### 预测
predictions = trainer.predict(tokenizer_ds.select(range(50)))
print(predictions)

# ===== Step19 Loss可视化
# ### 模型训练Loss和验证Loss可视化
import numpy as np
# 可视化训练过程(需要安装matplotlib)
import matplotlib.pyplot as plt
# 训练后查看指标日志
final_metrics = trainer.evaluate()

log_history = trainer.state.log_history

train_loss = [x['loss'] for x in log_history if 'loss' in x]
eval_loss = [x['eval_loss'] for x in log_history if 'eval_loss' in x]

plt.figure(figsize=(10,5))
plt.plot(train_loss, label="Training Loss")
plt.plot(eval_loss, label="Validation Loss")
plt.xlabel("Steps")
plt.ylabel("Loss")
plt.legend()

# 查看training_curve.png确认无过拟合
plt.savefig(output_path+"/train_eval_loss.png")


# ### 训练Loss采用平滑算法后的可视化
# 可视化训练过程(需要安装matplotlib)
import matplotlib.pyplot as plt

log_history = trainer.state.log_history

def smooth(scalars, smoothing_factor=0.9):
    """指数平滑函数"""
    smoothed = []
    last = scalars[0]  # 初始值设为第一个原始数据点
    for point in scalars:
        smoothed_val = last * smoothing_factor + (1 - smoothing_factor) * point
        smoothed.append(smoothed_val)
        last = smoothed_val  # 更新last为当前平滑值
    return smoothed
    
# 原始值(已从日志中提取)
original_train_loss = [x['loss'] for x in log_history if 'loss' in x]

# 应用平滑函数获取平滑值(调整smoothing_factor控制平滑程度)
smoothed_train_loss = smooth(original_train_loss, smoothing_factor=0.9)
plt.figure(figsize=(10,5))
plt.plot(original_train_loss, label="Original Training Loss", alpha=0.5)
plt.plot(smoothed_train_loss, label="Smoothed Training Loss", linewidth=2)
plt.xlabel("Steps")
plt.ylabel("Loss")
plt.legend()
plt.savefig(output_path+"/训练train_loss_curve_with_smoothing.png")
# ### 验证Loss采用平滑算法优化后的可视化
import matplotlib.pyplot as plt

log_history = trainer.state.log_history

def smooth(scalars, smoothing_factor=0.9):
    """指数平滑函数"""
    smoothed = []
    last = scalars[0]  # 初始值设为第一个原始数据点
    for point in scalars:
        smoothed_val = last * smoothing_factor + (1 - smoothing_factor) * point
        smoothed.append(smoothed_val)
        last = smoothed_val  # 更新last为当前平滑值
    return smoothed
    
# 原始值(已从日志中提取)
original_eval_loss = [x['eval_loss'] for x in log_history if 'eval_loss' in x]

# 应用平滑函数获取平滑值(调整smoothing_factor控制平滑程度)
smoothed_eval_loss = smooth(original_eval_loss, smoothing_factor=0.9)
plt.figure(figsize=(10,5))
plt.plot(original_eval_loss, label="Original Validation Loss", alpha=0.5)
plt.plot(smoothed_eval_loss, label="Smoothed Validation Loss", linewidth=2)
plt.xlabel("Steps")
plt.ylabel("Loss")
plt.legend()
plt.savefig(output_path+"/验证eval_loss_curve_with_smoothing.png")

= ===== 好像光精简版都有点多了......

详细版本,下篇单开篇章,分开写吧。

本文作为参考得了...。

微调数据模板:

[
  {
    "instruction": "java是什么",
    "input": "",
    "output": "xxxxxxx"
  },
  {
    "instruction": "C++是什么?",
    "input": "",
    "output": "xxxxxxx"
  },
  {
    "instruction": "AI是什么?",
    "input": "",
    "output": "xxxxxxxxx"
  }
]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值