大模型的开发应用(三):基于LLaMAFactory的LoRA微调
基于LLaMAFactory的LoRA微调
0 前言
当前(2025.5),生成模型最常用的微调手段就是LoRA/QLoRA微调,它是局部微调的一种,而全量微调限于数据和算力等因素,很少去使用。本文主要介绍 LoRA 微调,QLoRA要结合量化一起讲。
1 LoRA微调
1 LoRA微调的原理
如果一个大模型是将数据映射到高维空间进行处理,这里假定在处理一个细分的小任务时,是不需要那么复杂的大模型的,可能只需要在某个子空间范围内就可以解决,那么也就不需要对全量参数进行优化了,我们可以定义当对某个子空间参数进行优化时,能够达到全量参数优化的性能的一定水平(如90%精度)时,那么这个子空间参数矩阵的秩就可以称为对应当前待解决问题的本征秩或内在秩(intrinsic rank)。
预训练模型中存在一个极小的内在维度,这个内在维度是发挥核心作用的地方。当针对特定任务进行微调后,模型中权重矩阵其实具有更低的本征秩(intrinsic rank),同时,越简单的下游任务,对应的本征秩越低。(Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning)因此,权重更新的那部分参数矩阵尽管随机投影到较小的子空间,仍然可以有效的学习,可以理解为针对特定的下游任务这些权重矩阵就不要求满秩。我们可以通过对全连接层进行低秩分解,并对秩分解矩阵进行微调,来间接训练神经网络中的一些全连接层,从而减少微调的参数量。模型适应下游任务时,参数变化 ΔW 可能存在于低维子空间,低秩分解足以捕捉关键调整方向。
具体操作时,是对模型中特定的模块建立旁支,设原始模块中的权重为W,而旁支则由AB两个矩阵构成,A负责降维,把输入特征的维度从d讲到r,这里 r 就是上面说的本征秩,r 通常取8或16,B负责升维,把特征向量的维度调回到 d。结构如下图所示:

调整后的权重为 W + B ⋅ A W+B⋅A W+B⋅A,实际应用中,B⋅A前面会有个缩放系数,即 W + λ B ⋅ A W+\lambda B⋅A W+λB⋅A,输出为 h = W + λ B ⋅ A x h=W+\lambda B⋅Ax h=W+λB⋅Ax。训练时,冻结W,只更新矩阵A和矩阵B,那么 B 与 A 进行矩阵乘法的结果等效于 W 的梯度,即 Δ W = λ B ⋅ A ΔW=\lambda B⋅A ΔW=λB⋅A。A 使用随机高斯分布初始化,B 初始化为零,即确保训练初始阶段 ΔW=0。训练结束后,将 B⋅A 合并到原始 W 中,即合并到主分支,表达式为: W n e w = W o l d + λ B A W_{new}=W_{old}+\lambda BA Wnew=Wold+λBA。推理时无额外计算开销,与原始模型结构一致。
通过消融实验发现同时调整 W q W_q Wq 和 W v W_v Wv 会产生最佳结果。
1.2 通过peft库为指定模块添加旁支
peft 库是 huggingface 开发的第三方库,其中封装了包括 LoRA、Adapt Tuning、P-tuning 等多种高效微调方法,可以基于此便捷地实现模型的 LoRA 微调。
我们先打印原始模型(Meta-Llama-3-8B-Instruct)的结构,代码如下:
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import LoraConfig, TaskType, get_peft_model
model_path = '/data/coding/model_weights/LLM-Research/Meta-Llama-3-8B-Instruct'
model = AutoModelForCausalLM.from_pretrained(model_path,device_map="auto",torch_dtype=torch.bfloat16)
print(model)
输出
Loading checkpoint shards: 100%|█████████████████████████████████████████████████████| 4/4 [00:03<00:00, 1.03it/s]
WARNING:root:Some parameters are on the meta device device because they were offloaded to the cpu.
LlamaForCausalLM(
(model): LlamaModel(
(embed_tokens): Embedding(128256, 4096)
(layers): ModuleList(
(0-31): 32 x LlamaDecoderLayer(
(self_attn): LlamaSdpaAttention(
(q_proj): Linear(in_features=4096, out_features=4096, bias=False)
(k_proj): Linear(in_features=4096, out_features=1024, bias=False)
(v_proj): Linear(in_features=4096, out_features=1024, bias=False)
(o_proj): Linear(in_features=4096, out_features=4096, bias=False)
(rotary_emb): LlamaRotaryEmbedding()
)
(mlp): LlamaMLP(
(gate_proj): Linear(in_features=4096, out_features=14336, bias=False)
(up_proj): Linear(in_features=4096, out_features=14336, bias=False)
(down_proj): Linear(in_features=14336, out_features=4096, bias=False)
(act_fn): SiLU()
)
(input_layernorm): LlamaRMSNorm()
(post_attention_layernorm): LlamaRMSNorm()
)
)
(norm): LlamaRMSNorm()
)
(lm_head): Linear(in_features=4096, out_features=128256, bias=False)
)
然后我们通过peft库对对原始模型添加旁支,并打印新的模型结构,代码如下:
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import LoraConfig, TaskType, get_peft_model
model_path = '/data/coding/model_weights/LLM-Research/Meta-Llama-3-8B-Instruct'
model = AutoModelForCausalLM.from_pretrained(model_path,device_map="auto",torch_dtype=torch.bfloat16)
# print(model)
# 对原始模型进行一些设置,训练时候用,这里不训练,所以下面三行代码可以省略
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
model.config.use_cache = False
# LoRA 参数配置
config = LoraConfig(
task_type=TaskType.CAUSAL_LM, # 模型任务设置为因果模型,即语言模型
target_modules=["q_proj", "v_proj"], # 指定要微调的模块,创建 LoRA 模型时,原模型的组件中名字里带"q_proj","v_proj"的模块都会建立旁支
inference_mode=False, # 训练模式
r=8, # Lora 秩
lora_alpha=32, # 用于控制缩放系数,scaling=lora_alpha/r,合并时将旁支乘以缩放系数加入到主分支
lora_dropout=0.1 # Dropout 比例
)
# 获取 LoRA 模型
model = get_peft_model(model, config)
print(model)
这里需要注意一点,对不同的模型,LoRA 参数可能有所区别。例如,对于 ChatGLM,无需指定 target_modeules,peft 可以自行找到,因为ChatGLM模型把QKV的投影都写到了一个矩阵中(即query_key_value,维度为(d_model, 3*d_model))。
上面的代码输出为:
Loading checkpoint shards: 100%|█████████████████████████████████████████████████████| 4/4 [00:02<00:00, 1.47it/s]
WARNING:root:Some parameters are on the meta device device because they were offloaded to the cpu.
PeftModelForCausalLM(
(base_model): LoraModel(
(model): LlamaForCausalLM(
(model): LlamaModel(
(embed_tokens): Embedding(128256, 4096)
(layers): ModuleList(
(0-31): 32 x LlamaDecoderLayer(
(self_attn): LlamaSdpaAttention(
(q_proj): lora.Linear(
(base_layer): Linear(in_features=4096, out_features=4096, bias=False)
(lora_dropout): ModuleDict(
(default): Dropout(p=0.1, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=4096, out_features=8, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=4096, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
(k_proj): Linear(in_features=4096, out_features=1024, bias=False)
(v_proj): lora.Linear(
(base_layer): Linear(in_features=4096, out_features=1024, bias=False)
(lora_dropout): ModuleDict(
(default): Dropout(p=0.1, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=4096, out_features=8, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=1024, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
(o_proj): Linear(in_features=4096, out_features=4096, bias=False)
(rotary_emb): LlamaRotaryEmbedding()
)
(mlp): LlamaMLP(
(gate_proj): Linear(in_features=4096, out_features=14336, bias=False)
(up_proj): Linear(in_features=4096, out_features=14336, bias=False)
(down_proj): Linear(in_features=14336, out_features=4096, bias=False)
(act_fn): SiLU()
)
(input_layernorm): LlamaRMSNorm()
(post_attention_layernorm): LlamaRMSNorm()
)
)
(norm): LlamaRMSNorm()
)
(lm_head): Linear(in_features=4096, out_features=128256, bias=False)
)
)
)
1.3 lora前后结构输出结果对比
关于模型结构对比,这里只展示 q_proj 模块

我们可以计算以下参数量对比,原始 q_proj模块:4096x4096=16777216,Lora分支:4096x8+8x4096=65536,后者大概只有前者的0.39%,也就是说,通过LoRA微调,需要微调的参数量大大降低。
打印的结果中,包含(lora_embedding_A) 和 (lora_embedding_B),它们是用于嵌入层(Embedding Layers)的低秩适配参数,当要LoRA微调嵌入层或者输出分类头时,(lora_embedding_A) 和 (lora_embedding_B) 就相当于 lora_A 和 lora_B ,只不过这里微调的不是嵌入层,也不是输出分类头,所以这两个参数为空。
1.4 使用PyTorch复现 LoRA.Linear
根据LoRA的原理和前面打印的结构,我们可以用PyTorch代码实现其过程:
import torch
import torch.nn as nn
from typing import Optional
class LoRALinear(nn.Module):
def __init__(
self,
base_layer: nn.Module,
r: int = 8,
lora_alpha: float = 1.0,
lora_dropout: float = 0.1
):
super().__init__()
self.base_layer = base_layer # 原始线性层
self.r = r
self.lora_alpha = lora_alpha
# 冻结原始参数
for param in self.base_layer.parameters():
param.requires_grad = False
# LoRA参数(通常只添加在特定层,这里演示完整实现)
in_features = base_layer.in_features
out_features = base_layer.out_features
# LoRA分支结构
self.lora_dropout = nn.Dropout(p=lora_dropout)
self.lora_A = nn.Linear(in_features, r, bias=False)
self.lora_B = nn.Linear(r, out_features, bias=False)
# 缩放因子
self.scaling = lora_alpha / r
# 参数初始化
self.reset_parameters()
def reset_parameters(self):
# 原始层的参数保持预训练值不变
# LoRA参数初始化
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) # 论文中A用高斯分布初始化,但配套的GitHub源码却是均匀初始化
nn.init.zeros_(self.lora_B.weight) # B用0初始化
def forward(self, x: torch.Tensor):
# 原始层的前向计算
base_output = self.base_layer(x)
# LoRA分支的前向计算
lora_output = self.lora_B(self.lora_A(self.lora_dropout(x))) * self.scaling
# 我看了源码,是先dropout再输入到 self.lora_A
# 合并结果
return base_output + lora_output
关键点说明:
-
结构组成:
•base_layer:原始预训练的Linear层(4096->4096)
•lora_A:降维矩阵(4096->8)lora_B:升维矩阵(8->4096)lora_dropout:LoRA分支的Dropout层
-
前向流程:
原始输出 = 原始线性层(x)
LoRA分支 = 降维(Dropout(x)) → 升维 → 缩放
最终输出 = 原始输出 + LoRA分支
-
实现细节:
• 参数冻结:原始层的参数通过requires_grad=False保持冻结
• 缩放因子:使用lora_alpha/r控制LoRA更新强度(默认alpha=1时相当于1/8)
• 初始化:
• LoRA_A使用Kaiming初始化
• LoRA_B初始化为全零(保证训练初期LoRA分支不影响原始输出) -
使用方式:
# 原始层替换示例
original_layer = model.q_proj
lora_layer = LoRALinear(
base_layer=original_layer,
r=8,
lora_alpha=1.0,
lora_dropout=0.1
)
model.q_proj = lora_layer
数学表达式:
o u t p u t = W o r i g i n a l ∗ x + ( W B ∗ W A ∗ d r o p o u t ( x ) ) ∗ ( α / r ) output = W_original * x + (W_B * W_A * dropout(x)) * (\alpha / r) output=Woriginal∗x+(WB∗WA∗dropout(x))∗(α/r)
1.5 使用peft进行LoRA微调案例
这里有一个对ChatGLM模型进行LoRA微调,实现关系信息抽取的项目,其LoRA模型的构建,用的就是peft库,代码比较复杂,有时间可以看看,没时间算了,至于ChatGLM的基模型原理,可以看这篇文章,同样也是有时间就看看,没时间就算了。
2 LLaMA-Factory
2.1 LLaMA-Factory简介
LLaMA Factory 是一个简单易用且高效的大型语言模型(Large Language Model)训练与微调平台。通过 LLaMA Factory,可以在无需编写任何代码的前提下,在本地完成上百种预训练模型的微调,框架特性包括:
- 模型种类:LLaMA、LLaVA、Mistral、Mixtral-MoE、Qwen、Yi、Gemma、Baichuan、ChatGLM、Phi 等几乎世面上能见到的所有开源模型。
- 训练算法:(增量)预训练、(多模态)指令监督微调、奖励模型训练、PPO 训练、DPO 训练、KTO 训练、ORPO 训练等等。
- 运算精度:16 比特全参数微调、冻结微调、LoRA 微调和基于 AQLM/AWQ/GPTQ/LLM.int8/HQQ/EETQ 的 2/3/4/5/6/8 比特 QLoRA 微调。
- 优化算法:GaLore、BAdam、DoRA、LongLoRA、LLaMA Pro、Mixture-of-Depths、LoRA+、LoftQ 和 PiSSA。
- 加速算子:FlashAttention-2 和 Unsloth。
- 推理引擎:Transformers 和 vLLM。
- 实验监控:LlamaBoard、TensorBoard、Wandb、MLflow、SwanLab 等等。
说白了,它就是一个可视化微调界面,让你不需要写代码,只在一个UI界面内配置微调参数,就能实现模型微调,配套有中文文档。
2.2 LLaMA-Factory的安装
新建一个conda环境,并激活,然后根据以下命令安装:
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory # 进入LLaMA-Factory目录下
pip install -e ".[torch,metrics]"
-e 选项表示“editable”,即可编辑模式。
如果服务器连不上github,可以现在能连上的机器把 LLaMA-Factory 压缩包下载下来,然后上传到服务器。建议使用VS Code连接远程服务器,因为 VS Code 自带端口转发功能,这样可以避免下载frpc_linux_amd64_v0.3。
以上命令执行完之后,未必就安装完成,还要测试当前环境的微调窗口能否打开。在控制台输入(必须是在LLaMA-Factory目录下输入):
llamafactory-cli webui
如果报错TypeError: argument of type 'bool' is not iterable,那么大概率是pydantic版本问题,在终端窗口输入以下命令重新安装:
pip install pydantic==2.10.6
如果终端打印的是以下信息,那么说明安装完成:
如果不是使用 VS Code 连接远程服务器,那么需要下载frpc_linux_amd64_v0.3。
在浏览器输入:http://localhost:7860/,如果能看到以下的界面,说明安装成功:
3 使用 LLaMA-Factory 做自我认知微调
3.1 任务介绍
一个大模型,如果你让它自我介绍,它可能会说,我是“通义千问,由阿里开发的人工智能助手”、“我是Llama,由Meta公司开发的人工智能助手”,假设我的公司使用了千问开发了一款产品,我们公司的名称叫“魔幻手机”,现在我希望我微调后的模型能说出这样的话:“你好,我是 Magic,是由 Magic Phone 开发的人工智能助手”。这个微调过程,就叫自我认知训练。
本任务使用 Qwen2.5-1.5B-instruct 模型进行微调,因为这个小,不吃显存。
3.2 数据集
进入LLaMA-Factory/data,里面包含了一些默认的数据集,我们打开 identity.json,这是自我认知训练的数据集:
我们用替换 Magic 替换 {{name}},用 Magic Phone 替换 {{author}}:
如果是自己上传的数据集,那么需要配置 :打开LLaMA-Factory/data/dataset_info.json,然后修改 file_name 字段,把文件名称改成自己上传的数据集文件名或者路径。
3.3 参数配置
启动 LLaMA-Factory 的 UI 服务:
llamafactory-cli webui
然后按照以下配置模型:
如果不是断点续训,那么检查点什么都不要填,我上面的截图是为了说明路径用绝对路径。
接下来是配置训练参数(没有画框的就用默认),其中截断长度要根据数据集给个适当的值,太长的话会占用显存,我们的数据集问答都很短,给个256足够,批处理大小根据显存来设置:
这里的最大样本量,指的是使用的样本量,比如你的数据集有5万条样本,如果最大样本量设置为10000,那就用其中的一万个。这里验证集比例可以为0,因为生成模型永远也不可能过拟合,使用验证集意义不是很大,我们这里设置0.2的比例,为的是能在训练的过程中打印一些指标(如 Loss)。
点击“预览数据”,可以看到数据集包含的样本数量:
接下来是其他参数设置,我们这里只设置保存间隔,这里的预热步数就是warmup学习率预热,这个经常用到,但我们这里为了简化,不设置。
这里的日志间隔是指多个step(不是epoch)打印一次,保存间隔最小为10,同样是step,每保存一次,模型会做一次验证(用验证集数据),也就是验证间隔和保存间隔一致。
关于部分参数微调设置,它在LoRA微调时不起作用,它是冻结训练的时候用的(即“微调方法”选择 freeze 时),我们这里简单了解一下(有些小模型可能会用到,大模型不会用,所以不需要掌握):

我们切换成英文,就更好理解了:
这里“可训练层数”指的是隐藏层,即transformer的解码器子层,如果填正数,就只微调最末端的几层,填负数就只微调前面几层。关于可训练模块,这里默认填all,指的是微调可训练层的所有模块,如果想微调某个具体模块,可以填写模块名。如果还需要调整词嵌入层或者分类头,可以在“额外模块”中填写。填写模块名时需要严格适配,例如想微调q_proj,那就必须是完整的模块名,你填 q_ 是不行的,最好先建个脚本把模型结构打印一下,看看模块名称。
接下来设置LoRA 参数:

LoRA 的作用模块如果不填,则默认为all,此时会为所有隐藏层中的所有线性层(torch.nn.Linear)都建立LoRA分支(官方名字叫做Adaptor),包括自注意力层中的 q_proj, k_proj, v_proj, o_proj(查询、键、值、输出投影),还有FFN 层中的升维、降维线性层,只要是 torch.nn.Linear 对象都会被纳入。如果我们只想微调其中一部分,比如 q_proj 和 v_proj,那就可以把模块名写入,这里需要严格适配,即你填 q_ 是不行的,必须是完整的模块名,最好先建个脚本把模型结构打印一下,看看模块名称。
如果还想为嵌入层或者分类头建立Lora分支,可以在“附加模块”中填写。
接下来点“预览命令”,获取训练命令:
这里输出目录会自动创建,检查点、训练日志、参数配置都会保存在这个目录下;配置路径是将当前页面配置的参数保存成yaml文件的路径。
预览命令会在窗口的最下方生成命令训练命令,我们可以将其复制到终端运行。不过没必要这么干,既然有可视化界面了,我们就在可视化界面上微调。
3.4 训练
点击开始:
我们新建一个终端,然后输入nvitop,查看显存占用情况:
从显存占用情况来看,其实我们的批处理大小(batch_size)还可以往大设置一倍。
微调窗口可以看到损失变化情况、训练进度和日志:

这里之所以显示600,是因为有600个step,batch_size是4,然后梯度累计是8,那么优化器step一次覆盖的数据是64,而训练集数据是72,数据集导入器是使用drop_last,那么优化器step两次就覆盖完了训练集,所以总共有600个step。
不需要等300个epoch都跑完,只要损失曲线收敛,我们就可以提前停止训练,只需要点击“中断”:
可以看到,在第150个step之前就已经收敛了。
3.5 训练结果查看
我们去输出目录下,看看目录结构:
注意:检查点checkpoint-xxx下放置的模型,并不是千问模型,而是lora分支,它不能单独使用。
在 adapter_config.json 中,能看到微调了哪些模块,即给哪些模块添加了LoRA分支:
输出目录下有两张图片:
我们还可以查看
3.6 体验微调成果
按下面的截图配置模型:
然后输入提示词进行聊天
理论上讲,这里模型的生成长度不应该超过训练时的截断长度,但由于这只是自我认知微调,如果这里设置了最大生成长度,那么会影响模型的其他方面功能,除非它只发挥自我认知功能,其他问题你都不问。截断长度 和 最大生成长度 有什么区别呢?截断长度是你输入给模型的,最大生成长度是指模型返回给你的。
聊天界面如下:
作为对比,我们卸载模型,用基模型看看,按下面截图中的顺序 1 2 3 进行操作:
卸载模型会释放显存。
输入和刚刚完全一样的提示词:
可以看到,我们微调的模型完成了自我认知训练。
3.7 模型评估
通常情况下,生成模型一般不以客观指标(例如ppl、bleu、rouge等)作为评估依据,因为客观指标很难反应模型的智商。例如,用于评估的某条数据的输入为“你吃了午饭吗?”,标签为“吃了”,模型的输出为“刚吃完,吃了好多东西,现在肚子好撑”,回答远离标签,但你能说模型的回答有问题吗?很多时候,我们需要的就是模型具有自我发挥的能力,这才能体现它的智商,而这种“发散性”的能力恰恰是所有客观指标都无法衡量的。
那么在实际工作中,是如何完成模型评估的?答案是使用主观评估手段,甲方一般会提前设置好一些关键性的问题,然后将这些问题输入到模型,然后观察模型的回答质量,类似于高考语文的作文阅卷。
那么客观评估是不是一定就不能用了呢?其实也不是,有些领域需要模型的回答和标签严格对齐,或者要求回答和标签高度相似,例如法律咨询,输入的问题适用于哪部法律的第几条,这个是很明确的,不需要模型有自我发挥的能力,所以可以用客观指标进行评估。类似于高考语文、历史、政治这些科目的问答题(客观题),命题人通常会设置几个得分点,改卷的时候,看考生的回答踩中了几个得分点,以此评分。
LLaMAFactory 提供了 Evaluation & Predict 选项卡,该选项卡中的评估是使用客观评估指标进行评估。我们设置好检查点之后,打开这个选项卡:
接下来设置参数,以下三个红框部分是要设置的,其他使用默认,当然,也可以调批处理大小:
因为我们没有测试数据集,所以就直接使用训练的数据集进行测试,而截断长度,要和训练的时候一致。
配置好参数后,我们点击开始,然后就会进入评估。最后的评估结果如下:
前面说过,生成模型使用客观指标进行评估的意义不是很大,这里有意义的只有时间相关的指标,另外,因为模型生成有一定的随机性,你下一次评估,就不是上面显示的数值了。
因为生成模型一般不用客观指标,所以我们只需要知道使用 LLaMAFactory 评估模型是如何操作的就行了。
3.8 模型的合并导出
微调完成后,检查点保存的是模型的 LoRA 分支,但微调后的模型是:原始模型+LoRA分支。我们要部署,需要将原始模型与LoRA分支进行合并,并导出为 Hugging Face 模型(即与从Hugging Face下载的模型有相同的文件结构)。
设置好检查点之后,需要先加载模型:
点击 Export 选项卡,按照下面1 2 3 4的顺序操作即可导出,其他设置暂时使用默认:
这里关于最大分块大小,一般选择4G,因为有些老的磁盘设备无法打开超过4GB的文件;关于量化的知识,我们下一篇文章会讲,我们这里导出的时候先不进行量化;导出的文件格式一般都是safetensors,这种格式比较高效,当然,对于不支持 safetensors 格式的设备,可以勾选“导出旧格式”。
导出完成后,会显示导出完成:
输出目录的文件结构如下:
这是 Hugging Face 模型的结构,可以使用前面学的 transformers、vllm、LMDeploy部署调用了。
我们可以通过Chat选项卡,把导出的模型加载进来试用一下,这个时候就不需要填写检查点了,只需要在模型路径中把刚刚导出的模型填写上就行。按下面1 2 3 4顺序操作即可:
输入提示词,结果如下:
OK,大功告成!
4 使用 LLaMAFactory 进行自制数据集的LoRA微调
4.1 LLaMAFactory的数据集格式
LLaMA Factory 官方文档给出的数据集格式为:
[
{
"instruction": "人类指令(必填)",
"input": "人类输入(选填)",
"output": "模型回答(必填)",
"system": "系统提示词(选填)",
"history": [
["第一轮指令(选填)", "第一轮回答(选填)"],
["第二轮指令(选填)", "第二轮回答(选填)"]
]
}
]
即列表里面套字典,一条数据就是一个字典,并用 json 文件保存。
instruction 对应的内容为人类指令, input 对应的内容为人类输入,output 对应的内容为模型回答。若是多轮对话,那么 instruct+input 和 output 就是最后一轮对话的问答,而则 history 则是对话历史。history 是由多个字符串二元组构成的列表,分别代表历史消息中每轮对话的指令和回答,对于单轮对话只需省略 history 即可。注意在指令监督微调时,历史消息中的回答内容也会被用于模型学习。
在进行指令监督微调时, instruction 对应的内容会与 input 对应的内容拼接后作为最终的人类输入,即人类输入为:
instruction
input
例如:
{
"instruction": "计算这些物品的总费用。 ",
"input": "输入:汽车 - $3000,衣服 - $100,书 - $20。",
"output": "汽车、衣服和书的总费用为 $3000 + $100 + $20 = $3120。"
},
人类的最终输入为:
计算这些物品的总费用。
输入:汽车 - $3000,衣服 - $100,书 - $20。
我们自己制作数据集的时候,为了简便,一般把所有的提示词都写到 instruct 中,然后在 input 上只写一个空字符串,例如:
{
"instruction": "计算这些物品的总费用。输入:汽车 - $3000,衣服 - $100,书 - $20。 ",
"input": "",
"output": "汽车、衣服和书的总费用为 $3000 + $100 + $20 = $3120。"
}
4.2 自制数据集转标准格式
本文这次使用的数据集为 ruozhiba_qaswift.json,它是从魔搭社区下载下来的,其内容如下:
给AI一个示例,然 AI 帮我们生成一段python代码,即让AI帮我嫩转,提示词可以如下设计:
我有一个名为rouzhiba_qa.json的数据集,内容格式如下:
[
{
"system": "00000",
"query": "只剩一个心脏了还能活吗?",
"response": "能,人本来就只有一个心脏。"
},
]
请帮我转成如下格式:
[
{
"instruction": "计算这些物品的总费用。输入:汽车 - $3000,衣服 - $100,书 - $20。 ",
"input": "",
"output": "汽车、衣服和书的总费用为 $3000 + $100 + $20 = $3120。"
},
]
这是模型给我的 python 代码:
import json
# 读取原数据
with open('rouzhiba_qa.json', 'r', encoding='utf-8') as f:
original_data = json.load(f)
# 转换为新格式
converted_data = []
for item in original_data:
converted_item = {
"instruction": item["query"],
"input": "",
"output": item["response"]
}
converted_data.append(converted_item)
# 保存新数据
with open('converted_rouzhiba_qa.json', 'w', encoding='utf-8') as f:
json.dump(converted_data, f, ensure_ascii=False, indent=4)
以后这种机械性地需求,都可以让AI帮忙写,效率能高很多。
4.3 参数配置与 LoRA 微调
先配置数据,把我们刚刚转化后得到的 converted_rouzhiba_qa.json 放到 LLaMA-Factory/data 目录下,并把 converted_rouzhiba_qa.json 文件名写入到 /data/coding/LLaMA-Factory/data/dataset_info.json 中,如下图所示:
然后打开 LLaMA Factory 的操作界面,先激活 LLaMA Factory 所在环境,然后在命令行输入:
llamafactory-cli webui
随后按下图所示配置:
这边数据集有两个,分别是 identity.json 和 converted_rouzhiba_qa.json ,即我们用的是多数据,前者用于训练自我认知,后者用于训练回答风格。一般情况下,不管你训练什么样的模型,都需要训练自我认知。
参数如下所示(我们不设置验证集):

最后是 LoRA 的秩和缩放系数:
点击开始
接下来就等模型收敛了。
关于批处理大小(batch_size),可以先设一个比较大的数,让机器爆显存,然后逐渐改小,这样可以最大化利用显存,我这边就不演示了。
新建一个端口,输入nvitop,查看显存使用情况:
我这里使用了90%了,看样子显存利用率还不错。
从损失函数图像中,可以看到模型在step=1200前就已经收敛,我们可以提前结束了。
4.4 训练结果体验
配好检查点之后加载模型:
输入提示词:
好的,回答符合我们预期,再试一下多轮对话:

我们看到,第二轮对话,模型的回答和我们的预期有那么一点出入了,但也没啥问题。我们试一下第三轮、第四轮对话:
最后两次输入是训练集中的问题,对应的问答对为:
{
"query": "我只出生了一次,为什么每年都要庆生",
"response": "庆生并不是为了庆祝出生,而是为了庆祝每一次的生日,也就是出生后的每一年。这是一个纪念自己在过去一年中成长和经历的方式,也是对未来一年的期待和祝福。"
},
{
"query": "我25岁 我爸50岁 那我爸两岁就生了我啦?",
"response": "不对,你25岁,你爸50岁,那么你爸在你出生时应该是25岁,而不是两岁。"
},
可以看到,第三、第四轮对话,模型的回答风格已经和我们的预期差的比较远了。
这是因为,我们训练用的数据集是单轮对话的,而 Chat 选项卡中的对话是多轮对话的,所以模型的回答只能保证第一轮对话和我们的数据集是一样的风格,因此我们在和模型对话的时候,每次聊完,都要清空历史才能进行下一次对话。
使用多轮对话数据集对模型进行LoRA微调,过程和上面一摸一样。
更多推荐
所有评论(0)