实验目标: 本实验演示使用 PEFT (Parameter-Efficient Fine-Tuning) 库的 LoRA (Low-Rank Adaptation) 技术,对 Llama 架构的基础模型 (unsloth/tinyllama) 进行指令微调 (SFT)。
实验任务: 训练基础模型学习"指令遵从"(Instruction-Following) 行为。我们将使用 tatsu-lab/alpaca 数据集的一个子集,教会模型识别并响应 [INST] 指令格式。
评估方法 (科学对比): 我们将通过对比"实验1"(基线测试)和"实验2"(微调后评估)的结果,来科学地验证 SFT 的有效性。
- 实验 1 (基线):测试"基础模型"对 [INST] 指令的反应。(预期:失败/输出无效内容)
- 实验 2 (评估):测试"LoRA微调后"的模型对相同 [INST] 指令的反应。(预期:成功/输出相关答案)
步骤 1:环境配置与依赖安装
1.1 前提:硬件与环境
SFT 训练是计算密集型任务,必须依赖 CUDA 加速。请确保实验环境(本地或云端)已正确配置 NVIDIA GPU 及相应版本的 PyTorch。
1.2 安装项目依赖库
安装本次实验所需的核心 Python 库。
erlang
%pip install datasets transformers peft accelerate pandas -q
1.3 核心依赖库功能解析
transformers: Hugging Face 核心库。提供基础模型 (AutoModelForCausalLM)、分词器 (AutoTokenizer) 及训练器 (Trainer) 接口。
peft: (Parameter-Efficient Fine-Tuning) 库。提供 LoRA (LoraConfig, get_peft_model) 的核心实现。
datasets: 用于从 Hugging Face Hub 下载数据集 (load_dataset) 并进行高性能的预处理 (.map())。
accelerate: PyTorch 加速库。本项目中主要用于 device_map="auto",实现模型的自动设备映射。
pandas: 一个数据处理库(数据辅助分析,我们在 main() 脚本中导入了它,作为 datasets 的辅助工具)。
步骤 2:加载数据集与实验设置
2.1 导入所有库
首先,我们导入本次实验所需的所有 Python 模块。
python
import pandas as pd
import torch
from datasets import Dataset, load_dataset # Hugging Face Datasets 库的核心类 / 函数
from peft import LoraConfig, get_peft_model # 配置 LoRA 微调参数;给基础模型添加 LoRA 适配器
from transformers import ( # Hugging Face Transformers 库的核心组件
AutoTokenizer, # 自动匹配模型的分词器:将文本转为模型能识别的张量
AutoModelForCausalLM, # 自动加载因果语言模型(CausalLM):Llama 是解码器架构,属于因果语言模型(自回归生成)
TrainingArguments, # 训练参数配置类:定义训练的批次大小、学习率、训练轮数等关键参数
Trainer, # 训练器类:封装了训练的完整逻辑(数据加载、梯度计算、模型保存等),无需手动写训练循环
DataCollatorForSeq2Seq # Seq2Seq 任务的数据整理器:将批量文本数据填充(padding)到统一长度,适配模型输入
)
# --- [实验配置] ---
# 1. 定义模型 ID (MODEL_ID)
# 我们使用 unsloth 托管的、修复了 Tokenizer 配置的 TinyLlama 基础模型
# unsloth 托管指的是 模型文件存储在 Hugging Face 云平台,使用时 自动下载到本地 / 当前环境后运行,不是 "全程在云上运行"
# 如果您已手动下载模型,请将此路径修改为本地文件夹路径 (例如 "./tinyllama")
MODEL_ID = "unsloth/tinyllama"
# 2. 定义采样数量 (NUM_SAMPLES)
# SFT 训练不需要海量数据,200条已足够教会模型"遵从指令"的行为
NUM_SAMPLES = 200
2.2 加载并采样 Alpaca 数据集
学术说明: 我们将使用 tatsu-lab/alpaca 数据集,这是 SFT 领域的经典基准。原始数据集包含 52k 条数据。为保证实验能在课程时间内完成,我们仅从中采样 NUM_SAMPLES (200) 条数据作为我们的训练子集。
python
def load_and_sample_data(num_samples=NUM_SAMPLES):
"""
从 Hugging Face Hub 加载 Alpaca 数据集并采样
"""
print(f"--- 正在加载 tatsu-lab/alpaca 并采样 {num_samples} 条数据 ---")
# 从 Hub 加载 'train' 分割
ds = load_dataset("tatsu-lab/alpaca", split="train")
# [关键] 仅选择一个小型子集,用于快速实验
ds_micro = ds.select(range(num_samples))
print("--- Alpaca 数据集示例 (第1条) ---")
# 打印第一条数据,观察其结构
print(ds_micro[0])
return ds_micro
# --- 执行 ---
ds = load_and_sample_data()
vbnet
--- [步骤 1] 正在加载 tatsu-lab/alpaca 并采样 200 条数据 ---
--- Alpaca 数据集示例 (第1条) ---
{'instruction': 'Give three tips for staying healthy.', 'input': '', 'output': '1.Eat a balanced diet and make sure to include plenty of fruits and vegetables. \n2. Exercise regularly to keep your body active and strong. \n3. Get enough sleep and maintain a consistent sleep schedule.', 'text': 'Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nGive three tips for staying healthy.\n\n### Response:\n1.Eat a balanced diet and make sure to include plenty of fruits and vegetables. \n2. Exercise regularly to keep your body active and strong. \n3. Get enough sleep and maintain a consistent sleep schedule.'}
数据结构分析: 如上所示,Alpaca 数据集的核心是三个字段:
- instruction: 指令(你要做什么?)
- input: 输入(基于什么内容做? - 可选)
- output: 输出(标准答案是什么?)
text是拼接后的「完整训练文本」(固定模板),可直接用于模型训练(无需手动拼接)。
我们的 SFT 任务就是构建一个 [INST] -> [OUTPUT] 的映射。
步骤 3:加载分词器与基础模型
3.1 加载分词器 (Tokenizer)
分词器 (Tokenizer) 负责将文本字符串可逆地转换为模型能理解的整数序列 (Token IDs)。我们将加载与 Llama 2 架构兼容的分词器。
一个关键的工程实践是处理 pad_token(文本长度不一样,需要用pad_token补齐)。Llama 基础模型(处理连续文本)默认没有 pad_token,但训练时的 DataCollator (数据收集器) 需要一个 token 来进行批次填充 (Batch Padding)。我们将 pad_token 安全地设置为 eos_token (End-of-Sentence),这是 Llama 架构 SFT 的标准做法。
python
def load_tokenizer(model_id=MODEL_ID):
"""
加载 Llama (TinyLlama) 分词器
"""
print(f"--- 正在加载分词器: {model_id} ---")
# AutoTokenizer 会自动识别模型ID,并加载对应的分词器
# (如果 MODEL_ID 是本地路径 "./tinyllama",它会从本地加载)
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 兼容性处理:
# 将 pad_token 设置为 eos_token 以便 DataCollator 进行填充
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
return tokenizer
# --- 执行 ---
tokenizer = load_tokenizer()
bash
--- [步骤 2] 正在加载分词器: unsloth/tinyllama ---
3.2 加载基础模型 (Base Model)
这是实验的核心前提。我们将加载 MODEL_ID 指向的基础模型。
"基础模型"意味着它只经过了"预训练"(Pre-training) 阶段,其唯一的训练目标是"文本续写"(即预测下一个 token)。它没有经过任何指令或对话微调 (SFT),因此它在理论上不具备"遵从指令"的能力,也不认识 [INST] 这样的特殊 SFT 标签。
我们将使用两个关键参数高效加载模型:
device_map="auto": 使用accelerate库自动将模型权重分配到可用的 GPU 设备上。dtype=torch.bfloat16: 以 bfloat16 半精度加载模型。这使模型显存占用减半,是消费级显卡进行微调的必要技术。
python
def load_model(model_id=MODEL_ID):
"""
加载 Llama (TinyLlama) 基础模型
"""
print(f"--- 正在加载基础模型: {model_id} (请稍候)... ---")
model = AutoModelForCausalLM.from_pretrained(
model_id,
device_map="auto",
dtype=torch.bfloat16
)
# 为梯度检查点 (Gradient Checkpointing) 启用输入梯度
# 这是后续训练优化所必需的
model.enable_input_require_grads()
return model
# --- 执行 ---
model_base = load_model()
bash
--- 正在加载基础模型: unsloth/tinyllama (请稍候)... ---
步骤 4:实验 1 - 基线性能测试 (微调前)
4.1 定义test_model函数
我们将定义一个 test_model 函数,用于标准化我们的测试流程。此函数的核心任务是:
- 接收一个"提示"(Prompt) 字符串。
- 将其格式化为 Llama 2 SFT 的官方指令模板
- 使用 model.generate() 生成响应。
- 解码并打印输出。
在推理 (Inference) 阶段,我们必须调用 model.eval() 来关闭 Dropout 等训练特有的层。此外,我们设置 max_new_tokens 来限制输出长度,并提供 eos_token_id 确保模型在生成 标记时能正确停止。
python
def test_model(model, tokenizer, test_label, use_sft_format):
"""
用于执行推理测试的函数 (可配置 SFT 格式)
model, 待测试的大语言模型实例
tokenizer, 模型的 "翻译官":负责将文本(如测试用的自然语言)转换成模型能识别的 token ID,也能将模型输出的 token ID 转回文本
test_label, 测试标签
use_sft_format, 是否使用 SFT 格式(true or false)
"""
print("\n" + "="*50)
print(f" {test_label} ")
print("="*50)
# [关键] 推理前必须调用 .eval()
model.eval()
# 我们的标准测试问题
prompt = "Explain the difference between SFT (Supervised Fine-Tuning) and base model pre-training in three sentences."
# 根据标志选择提示格式
if use_sft_format:
# 格式 1: Llama 2 SFT 格式
test_prompt_text = f"<s>[INST] {prompt} [/INST] "
add_special_tokens = False # 已经手动添加 <s>
else:
# 格式 2: 纯文本
test_prompt_text = prompt
add_special_tokens = True # 让 tokenizer 自动添加 <s>
print(f"--- 提问 (Formatted Prompt):\n{test_prompt_text}\n")
# Tokenize
inputs = tokenizer(test_prompt_text, return_tensors="pt", add_special_tokens=add_special_tokens).to(model.device)
# 配置生成参数
gen_kwargs = {
"max_new_tokens": 250, # 最多生成250个新token
"do_sample": False, # 关闭采样(确定性生成)
"eos_token_id": tokenizer.eos_token_id # 遇到 </s> 就停止
}
generation_config = {**inputs, **gen_kwargs} # 合并输入与生成配置
with torch.no_grad(): # 无梯度生成文本
outputs = model.generate(**generation_config)
# 从输入之后的部分开始解码,精准提取生成内容(排除输入、清理特殊符号)
response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
print(f"--- 模型输出 (Response):\n{response}\n")
4.2 执行实验 1.A (纯文本续写)
预期结果分析: 我们向基础模型 (model_base) 发送纯文本 (use_sft_format=False)。 由于模型是"续写"模型,我们预期它不会回答问题,而是会将提示(Prompt)视为一个句子的开头,并续写下去。
ini
# --- 执行步骤 4.A ---
test_model(model_base, tokenizer, "实验 1.A: 基础模型 (纯文本续写)", use_sft_format=False)
less
==================================================
实验 1.A: 基础模型 (纯文本续写)
==================================================
--- 提问 (Formatted Prompt):
Explain the difference between SFT (Supervised Fine-Tuning) and base model pre-training in three sentences.
--- 模型输出 (Response):
The first thing I noticed was the lack of a "back" button. I'm not sure if this is a good thing or a bad thing. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'm used to it or not. I'm not sure if I'
4.3 执行实验 1.B (格式崩溃)
预期结果分析: 现在,我们向同一个基础模型 (model_base) 发送它从未见过的 SFT 格式 (use_sft_format=True)。 由于模型不理解 [INST] 标签的含义,我们预期它会将其视为无意义的文本,导致"续写"任务失败,输出重复的 [INST] 标签或不相关的垃圾内容。
ini
# --- 执行步骤 4.B ---
test_model(model_base, tokenizer, "实验 1.B: 基础模型 (未知指令格式)", use_sft_format=True)
ini
==================================================
实验 1.B: 基础模型 (未知指令格式)
==================================================
--- 提问 (Formatted Prompt):
<s>[INST] Explain the difference between SFT (Supervised Fine-Tuning) and base model pre-training in three sentences. [/INST]
--- 模型输出 (Response):
2. [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST] [INST
基线结论: "实验 1.A"和"1.B"共同证明:我们的 model_base 既不具备"回答"能力(只会续写),也不理解"SFT 指令格式"。
步骤 5:配置 LoRA (参数高效微调)
5.1 为什么要引入LoRA?
问题:微调一个 11 亿(1.1B)参数的完整模型需要巨大的显存(VRAM)和计算资源。 解决方案:我们使用 LoRA (Low-Rank Adaptation)。
LoRA 的核心思想是冻结 (Freeze) 基础模型的所有原始权重(1.1B 参数),并在模型的关键层(如 q_proj, v_proj 等)旁边注入非常小的、可训练的"适配器"(Adapter) 权重。
结果:我们只训练这些新注入的适配器(通常只占总参数的 0.5%),从而在保持微调效果的同时,极大降低了对硬件资源的需求。
5.2 定义 LoRA 配置函数
我们使用 peft 库的 LoraConfig 来定义适配器的参数:
target_modules: 指定要在哪些层注入 LoRA。这必须与 Llama 架构的层名称匹配(q_proj,k_proj,v_proj,o_proj等)。r(Rank): LoRA 适配器的"秩",即适配器的大小。r=8是一个高效且常用的值。lora_alpha: 缩放因子,通常设为r的 2-4 倍。task_type: 明确指定我们正在进行CAUSAL_LM(因果语言模型) 任务。
ini
def configure_lora(model):
"""
配置并应用 LoRA
函数接收一个 Llama 基础模型(model_base),为其配置 LoRA 规则并应用,返回一个 "带 LoRA 适配器的轻量化模型(model_peft)",
后续仅训练这个轻量化模型的少量参数。
"""
print("--- 正在配置和应用 LoRA... ---")
config = LoraConfig(
task_type="CAUSAL_LM", # 任务类型:因果语言模型(Llama是自回归生成模型)
# 注意力层的查询 / 键 / 值 / 输出投影层,前馈网络(FFN)的门控 / 上采样 / 下采样层
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
inference_mode=False, # 设置为 False,因为我们要进行训练LoRA 适配器的参数
r=8, # LoRA 秩(低秩矩阵的维度),控制适配器的参数规模
# 秩越小,参数越少(训练越快),但可能影响效果
# 秩越大,效果可能更好,但参数增多
lora_alpha=32, # LoRA 缩放因子,作用是平衡低秩矩阵的输出幅度,r*4,经验值
lora_dropout=0.1 # Dropout 比例,防止训练时过拟合
)
# 使用 get_peft_model 将 LoRA 配置应用到基础模型
model_peft = get_peft_model(model, config)
print("LoRA 配置完毕。可训练参数如下:")
# 打印可训练参数与总参数的对比,展示 PEFT 的高效性
model_peft.print_trainable_parameters()
return model_peft
# --- 执行 ---
# 将 LoRA "插件" 应用到我们的基础模型上
model_peft = configure_lora(model_base)
csharp
--- 正在配置和应用 LoRA... ---
LoRA 配置完毕。可训练参数如下:
trainable params: 6,307,840 || all params: 1,106,356,224 || trainable%: 0.5701
请注意 print_trainable_parameters() 的输出。可以看到,可训练的参数(Trainable params)数量级在"百万"(6.3M),而总参数(All params)在"十亿"(1.1B)。
我们即将训练的参数量不到原始模型的 0.6%
步骤6:SFT 预处理
这是 SFT 最核心的步骤。我们的目标是将 Alpaca 数据集(instruction, input, output)转换为 Llama 2 的官方 SFT 格式:
<s>[INST] {指令} [/INST] {回答} </s>
关键点 (损失掩码): 我们希望模型只学习"回答" ({回答} </s>) 部分,而忽略"指令" (<s>[INST] ... [/INST] ) 部分的损失。
我们将通过设置 labels 列表来实现这一点:
input_ids(模型看到的):[INST_Tokens, ANS_Tokens]labels(模型学习的):[-100, -100, ..., ANS_Tokens]
-100 是 PyTorch CrossEntropyLoss 的 ignore_index,它告诉损失函数:"忽略这些 token,不要计算它们的损失"。
python
def process_func_llama(example, tokenizer):
"""
将原始的指令 - 输入 - 输出样本,转换成 Llama 2 官方要求的格式,
并构建模型训练所需的input_ids、attention_mask、labels(损失计算标签)
"""
MAX_LENGTH = 1024 # 单条数据的最大 Token 长度
# [1] 拼接 "指令" 部分 (Llama 2 格式)
# 格式: <s>[INST] {instruction}\n{input} [/INST]
prompt = f"{example['instruction']}\n{example['input']}"
instruction_text = f"<s>[INST] {prompt} [/INST] "
# [2] 拼接 "回答" 部分 (Llama 2 格式)
# 格式: {output}</s>
response_text = f"{example['output']}</s>"
# [3] 分别对"指令"和"回答"进行分词
# tokenizer(...):将文本转换成input_ids(token ID 列表)和attention_mask(注意力掩码)
# [关键] 我们设置 add_special_tokens=False,因为我们已经手动添加了 <s> 和 </s>
instruction_tokens = tokenizer(instruction_text, add_special_tokens=False)
response_tokens = tokenizer(response_text, add_special_tokens=False)
# [4] 拼接完整的 "输入"
input_ids = instruction_tokens["input_ids"] + response_tokens["input_ids"]
# attention_mask:注意力掩码(1 表示有效 token,0 表示填充),同样拼接,保证模型只关注有效 token
attention_mask = instruction_tokens["attention_mask"] + response_tokens["attention_mask"]
# [5] SFT 核心:构建 损失计算"标签" (Labels)
# 掩码 (Masking) 指令部分,只在回答部分计算损失
labels = [-100] * len(instruction_tokens["input_ids"]) + response_tokens["input_ids"]
# [6] 截断超长序列
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
}
def tokenize_dataset(ds, tokenizer):
"""
将单样本预处理函数应用到整个数据集,实现高效批量处理
"""
print("--- 正在对数据集应用预处理 (Tokenization)... ---")
# 使用 datasets.map() 高效并行处理
# remove_columns 移除原始文本字段(instruction/input/output),只保留处理后的input_ids/attention_mask/labels
tokenized_ds = ds.map(
lambda example: process_func_llama(example, tokenizer), # 对每个样本调用单样本预处理函数
remove_columns=ds.column_names
)
print("预处理完成。")
print(tokenized_ds)
return tokenized_ds
# --- 执行 ---
# 将我们加载的 ds (200条) 进行 SFT 格式化
tokenized_ds = tokenize_dataset(ds, tokenizer)
php
--- 正在对数据集应用预处理 (Tokenization)... ---
预处理完成。
Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 200
})
如上所示,我们的 ds (数据集) 现已转换为 tokenized_ds,它不再包含 instruction 等文本字段,只包含模型训练所需的 input_ids, attention_mask 和 labels。
步骤 7:配置训练参数 (TrainingArguments)
在 Hugging Face 的 transformers 库中,所有训练超参数(Hyperparameters)都被封装在 TrainingArguments 类中。我们将定义训练的核心配置。
关键参数:
per_device_train_batch_size: 批次大小。受限于 VRAM,通常设为 2, 4 或 8。gradient_accumulation_steps: 梯度累积。这是关键的 VRAM 优化技术。它允许我们以较小的batch_size模拟一个更大的"有效批次大小"(effective_batch_size = 4 * 4 = 16)。它通过累积 4 步的梯度再一次性更新模型,用计算时间换取了显存空间。gradient_checkpointing=True: 梯度检查点。这是另一个关键的 VRAM 优化技术。它在正向传播中不存储所有中间激活值,而是在反向传播时重新计算它们。这会使训练速度变慢(约 20-30%),但能极大降低显存峰值占用。learning_rate: 学习率。1e-4(0.0001) 是 LoRA 微调的一个常用且高效的起始值。
ini
def get_training_args():
"""
配置训练参数
"""
print("--- 正在配置训练参数 (TrainingArguments)... ---")
return TrainingArguments(
# --- 核心参数 ---
output_dir="TinyLlama_instruct_lora_alpaca", # 训练输出(检查点)目录
num_train_epochs=3, # 总训练轮次 (epoch),轮次太少→模型学不会,轮次太多→过拟合
learning_rate=1e-4, # 学习率,学习率太大→训练不稳定(损失震荡),太小→收敛太慢,经验值
# --- 批次大小与梯度累积 ---
per_device_train_batch_size=4, # 每个 GPU 设备的批次大小
gradient_accumulation_steps=4, # 梯度累积步数 (有效批大小 = 4*4 = 16)
# --- 关键显存优化 ---
gradient_checkpointing=True, # 开启梯度检查点
# --- 日志与保存 ---
logging_steps=1, # 每 1 步记录一次日志 (loss)
save_steps=50, # 每 50 步保存一次检查点
save_on_each_node=True, # 多节点(多个机器)训练时,每个节点都保存检查点
#禁用第三方日志工具:
#比如禁用 Weights & Biases(wandb)、TensorBoard 等;
#适合本地训练 / 调试,无需上传日志到云端;
#若需要可视化训练曲线,可设为"wandb"或"tensorboard"。
report_to="none",
)
# --- 执行 ---
args = get_training_args()
diff
--- 正在配置训练参数 (TrainingArguments)... ---
步骤 8:实例化 Trainer 并执行训练
8.1 Trainer 说明
Trainer 是 transformers 库中用于封装训练循环(Training Loop)的高级抽象。我们只需将准备好的"组件"交给它:
model: 我们的 model_peft (包含 LoRA 适配器)。
args: 我们的 TrainingArguments (训练超参数)。
train_dataset: 我们的 tokenized_ds (SFT 数据集)。
data_collator: 一个关键的辅助类 (DataCollatorForSeq2Seq),它负责在每个批次 (batch) 中,动态地将该批次内的所有序列填充 (Padding) 到相同的长度。
8.2 定义并执行训练
调用 trainer.train() 将启动训练。请密切关注 loss(损失)指标的输出(耗时54s)。
预期结果: 我们预期 loss 值会持续且显著地下降。这证明模型(特指 LoRA 适配器)正在学习 Alpaca 数据集中 [INST] -> [OUTPUT] 的映射关系。
python
def train_model(model, tokenizer, tokenized_ds, args):
"""
初始化 Trainer 并开始训练
"""
print("--- 正在初始化 Trainer... ---")
# [关键] DataCollator
# 负责在批次内进行动态填充
data_collator = DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)
# 实例化 Trainer
trainer = Trainer(
model=model, # PeftModel (已配置 LoRA)
args=args, # 训练参数
train_dataset=tokenized_ds, # 训练数据集
data_collator=data_collator, # 动态填充器
)
print("--- 训练开始! (请监控 loss 指标)... ---")
# 启动训练!
# (根据 GPU 性能,训练 200 条数据 * 3 epochs 约需 1-5 分钟)
# 按args的批次大小加载数据,通过data_collator做批次填充;
# 将批次数据输入模型,计算损失(仅在回答部分,labels 非 - 100 的位置);
# 反向传播更新 LoRA 适配器参数(基础模型参数冻结);
# 按args的配置记录日志、保存检查点;
# 完成 3 轮(epochs)训练后停止。
trainer.train()
print("--- 训练完成! ---")
# [可选] 保存最终的 LoRA 适配器
# 注意:这仅保存适配器 (几十MB),而非完整模型 1.1GB
final_output_dir = "TinyLlama_instruct_lora_alpaca_final" # 后续推理时可通过PeftModel.from_pretrained(基础模型, 该目录)加载适配器
model.save_pretrained(final_output_dir) # 保存 LoRA 适配器的权重和配置文件
tokenizer.save_pretrained(final_output_dir) # 保存令牌化器的配置,保证推理时 tokenizer 和训练时一致
print(f"LoRA 适配器已保存到 {final_output_dir} 目录")
# --- 执行 ---
train_model(model_peft, tokenizer, tokenized_ds, args)
vbnet
The model is already on multiple devices. Skipping the move to device specified in `args`.
diff
--- 正在初始化 Trainer... ---
--- 训练开始! (请监控 loss 指标)... ---
ini
`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`.
39/39 00:54, Epoch 3/3
| Step | Training Loss |
|---|---|
| 1 | 1.882900 |
| 2 | 1.655300 |
| 3 | 2.070000 |
| 4 | 1.678500 |
| 5 | 1.692700 |
| 6 | 1.677200 |
| 7 | 1.518000 |
| 8 | 1.295900 |
| 9 | 1.768300 |
| 10 | 1.519100 |
| 11 | 1.614900 |
| 12 | 1.331400 |
| 13 | 1.206700 |
| 14 | 1.397900 |
| 15 | 1.370700 |
| 16 | 1.484100 |
| 17 | 1.542300 |
| 18 | 1.182400 |
| 19 | 1.142400 |
| 20 | 1.580300 |
| 21 | 1.383300 |
| 22 | 1.228600 |
| 23 | 1.528000 |
| 24 | 1.287200 |
| 25 | 1.448900 |
| 26 | 0.932800 |
| 27 | 1.544400 |
| 28 | 1.594300 |
| 29 | 1.125800 |
| 30 | 1.008300 |
| 31 | 1.362700 |
| 32 | 0.985200 |
| 33 | 1.322700 |
| 34 | 1.086100 |
| 35 | 1.200500 |
| 36 | 1.502400 |
| 37 | 1.230100 |
| 38 | 1.287400 |
| 39 | 1.385800 |
diff
--- 训练完成! ---
LoRA 适配器已保存到 TinyLlama_instruct_lora_alpaca_final 目录
步骤 9:实验 2 - 微调后性能评估
9.1 实验评估说明
我们将继续调用 test_model 函数,并使用完全相同的 prompt。
唯一的区别是,这次我们传入的模型是 model_peft(包含训练过的 LoRA 权重),而不是 model_base。
预期结果分析:
- 在"实验 1"中,模型看到 [INST] 时输出的是垃圾内容,因为它不理解这个标签。
- 现在("实验 2"),模型在训练中已经学到了 [INST] 是一个"指令"信号。
- 因此,我们预期模型不再输出垃圾,而是会激活它新学到的 LoRA 权重,并正确地生成一个与指令相关的答案。
9.2 执行微调后评估 (实验 2)
ini
test_model(model_peft, tokenizer, "实验 2: SFT模型 (学习后的指令格式)", use_sft_format=True)
vbscript
==================================================
实验 2: SFT模型 (学习后的指令格式)
==================================================
--- 提问 (Formatted Prompt):
<s>[INST] Explain the difference between SFT (Supervised Fine-Tuning) and base model pre-training in three sentences. [/INST]
--- 模型输出 (Response):
SFT is a pre-training method that uses a large amount of data to learn the representation of the target domain. It is similar to base model pre-training, but it uses a smaller amount of data to learn the representation of the target domain. The difference between SFT and base model pre-training is that SFT uses a large amount of data to learn the representation of the target domain, while base model pre-training uses a smaller amount of data to learn the representation of the target domain.