写在文始---必看:
本文是直接基于代码实战~
相关概念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" } ]