如何微调一个自用的小模型
2024 8 9 12:26 AM 359次查看
但在使用过程中发现了不少问题,例如:
- 速度太慢,用 vLLM 部署,32 并发时大概 300 tokens/s,每个请求其实不到 10 tokens/s。
- 指令遵循不行,当要求过多时,会随机无视一些指令。
- 喜欢废话,即使要求它只输出翻译,也可能会附带一堆多余信息。目前发现最好的方式是让它输出在一个 XML 标签里(例如 <OUTPUT></OUTPUT>),然后用字符串匹配或正则表达式来提取翻译。
那么有没有办法解决呢?
速度慢的问题,自然可以换较小的模型来解决。我测了一堆 13B 以内的模型,主观得出如下判断:
- Qwen2-72B-Instruct:无量化版本长句翻译更通顺,80 分;各种量化版本(GPTQ-Int4、GPTQ-Int8、AWQ)差距不大,75 分。
- Qwen2-7B-Instruct:与 72B 量化版本的翻译质量差距不大,但是词汇量有差距,有时会有漏翻(语句或单词直接没有了),70 分。
- internlm2_5-7b:翻译不够通顺,65 分。
- Phi-3-small-8k:翻译不够地道和通顺,有时有乱码,60 分。
- Phi-3-medium-4k:翻译不够通顺,速度较慢,65 分。
- glm-4-9b:翻译较通顺,但有时会有事实错误,速度较慢65 分。
- gemma-2-9b:会增加 markdown 标签,长文理解有错误,速度较慢,60 分。
顺带一提,对于当前的闭源旗舰模型的翻译能力,我的主观评测大致如下:
- GPT-4o:翻译较为正确,但不够地道和通顺,可以通过使用不同的 API keys 来加快速度,90 分。
- Claude 3.5 Sonnet:翻译通顺,基本正确,速度较慢,95 分。我是通过 AWS 的 Bedrock 来调用,每分钟只能支持 50 个请求,很容易超限;官网好像能到每分钟 4000 个。后来找 AWS 的技术支持增加到了 2000,后续有需要可以继续调整。
- Gemini-1.5 Pro:翻译通顺,有时有翻译错误,85 分。
可见要比翻译质量的话,开源模型还是离闭源旗舰模型有一定差距,所以给客户提供的翻译服务还是用闭源的吧。
但是开源模型也不一定更差,也许是喂的中文语料更多的原因,在将一些隐含被动意义的中文翻译英文时,Qwen 可以识别出并翻译成被动语态,而 3 款闭源模型都不行。
那么很容易想到的一个优化方案就是同时用闭源和开源模型一起翻译,然后再让模型评价这两个翻译,给出更好的翻译。这样操作后,有较高概率能保留两种模型的翻译优点,但是速度和开销也肯定变大了。
回到正题,根据评测结果,Qwen2-7B-Instruct 其实对于开发测试已经够用了,这个阶段也不追求更高的翻译质量,反而更看重速度。
但是它也引入了一个新的问题:有时会有漏翻。
再和前面的问题结合起来,有什么办法能解决它们呢?答案是微调。
微调的原理就是提供一些示例,让优化器比较模型的输出和示例中的参考输出,然后通过调整模型的参数来让两种输出更接近。如此一来,我们只需要构造一些示例,它的参考输出中遵循了所有的指令,模型就会自己去寻找规律来拟合我们的指令。
要解决之前提到的问题,我们大概需要构造这些示例数据:
- 提示词中只需要描述将下文翻译成目标语言,输出中只包含翻译,模型会自己学会不废话。
- 对于格式的要求,例如保持 HTML 标签、日期和货币单位等的规范,通过在输出中给出示例来教模型。
- 针对之前可能遗漏的指令,找出例句来改写成按指令要求来输出,让模型记住这些指令。
- 针对之前可能遗漏的翻译,找出例句并补完翻译,让模型不要漏翻。
知道原理后,我们应该用什么方法进行微调呢?
即使是 7B 这种规模的小模型,全参数微调也需要约 160 GB 的显存,这自然不是平民能玩得起的。
这些显存是怎么占用的呢?我们来简单计算一下。
现在的大模型大部分是用 BF16 精度的参数,即 1 个参数需要占用 16 位,也即 2 字节。那么 7B(70 亿) 个参数就需要占用 14 GB 的空间。如果只是推理的话,这 14 GB 就是最低的显存需求。但是显存中还需要存储输入数据、中间计算结果和缓存等,且这些占用还和并发数成正比,所以 16 GB 显存也就刚刚够用,也许并发高一点、token 多一点就会 OOM 了。
那为啥微调要差不多 10 倍的显存呢?
模型参数本身就需要一份 BF16 精度和一份 FP32 精度的,这就需要约 42 GB。为啥不能用 BF16 呢?因为微调时可能一次只更新 1e-6 甚至更低量级的小数,精度太低时就可能直接被忽略了,导致没法优化。
优化器需要创建两份 FP32 精度的参数,其中一份是用于保存更新后的参数,这又需要约 56 GB。
梯度值和激活值是 BF16 精度的,但它们的显存占用是动态变化的,大概合计 14 ~ 42 GB。
上述加起来,约需要 140 GB。这还只是 batch size 和 token length 为 1 的情况,当它们增大时,160 GB 也只是保守估计了。
既然平民玩不起,我们就关注一下另一种方法:LoRA。它只需要约 16 GB 显存就可以微调 7B 的模型了。这是怎么做到的呢?
从前面的计算可以看出,因为要更新参数,所以这 7B 的参数被存了很多份。那么如果我们要更新的参数量很少,那么显存占用是不是也就降低了呢?
LoRA 的作法是冻结原始模型的参数,在旁边添加 2 个低秩矩阵来同时计算,然后把原始模型的计算结果和低秩矩阵的计算结果相加来作为最终结果。优化器只需要调整低秩矩阵的参数来让最终结果拟合参考输出即可。
假设原模型的嵌入维度是 4096,低秩矩阵的秩是 8,则全参数微调需要计算和存储 4096 * 4096 个参数,而 LoRA 只需要计算和存储 4096 * 8 * 2 个参数。(注:这里只是描述一个比例,实际上模型会有很多层,每层的多头注意力和前馈网络等都是数倍于这些参数量的,实际的嵌入维度也可能远大于 4096。)
这样一来,LoRA 微调的参数可能不到原模型的 1%,因此显存占用也就不到原模型的 1%(当然还需要再加上一份原模型,也就是约 (14 + 140/100) = 15.4 GB)。
那么 LoRA 相对于全参数微调有啥缺点呢?它的参数量少了这么多,自然对于拟合复杂的需求会比较难;但是如果只是要它遵循某些指令,少量的参数也是可以做到的。
同时,因为它对原模型的参数改动较小,能较好地保留原模型的能力;而全参数微调则有更大概率忘记原有的一部分能力(也称为灾难性遗忘),由于这些模型很少开源训练数据,无法将它们纳入微调数据中,因此几乎没法避免。
对于显存更低的用户,QLoRA 是另一个选择,它在 LoRA 的基础上引入了量化技术。
但是实际算算精度损失还是挺大的,例如 FP16 量化到 FP4 或 NF4,4 位的浮点数中有 1 位还是符号位,其他 3 位要模拟 15 位的精度,有点难为它了。
不过,有测试认为 QLoRA 比 LoRA 节省了更多显存,因而可以把省出来的显存用来增加更大的秩,例如 256。这样虽然精度有损失,但是通过多几倍甚至几十倍的参数量,还是可以超越 LoRA。至于是否真的如此,我们下面再测试。
微调方法也了解了,该选个微调的框架了。虽然这类框架很多,但是初学者还是选个可视化的工具更方便,因此这里选择使用 LLaMA-Factory。
先根据它的文档进行安装。注意由于 PyTorch 刚发布,2.4 版本还不兼容,因此我安装的是 2.3.1:
pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121
pip install metrics deepspeed
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e .
然后是准备数据集。
我要做的其实是指令监督微调,因此选择采用这个格式。然后根据之前的一些错误例子和生产环境中交付的翻译,我构造了类似如下的数据:
[
{
"system": "You're a medical document translation expert.",
"instruction": "Please translate the following text to Chinese:\n[Phase I Clinical Study]: a close trial program for the first time to study drugs in the human body, subjected to small amounts (open 20 to 30 cases) normal adult healthy volunteers.",
"output": "【Ⅰ期临床研究】:首次在人体进行研究药物的周密试验计划,受试对象是少量(开放20~30例)正常成年健康自愿者。"
}
],
{
"system": "You're a medical document translation expert.",
"instruction": "Please translate the source text to English.\n\nReference terms:\n不同一性双胎: discordant twin\n\nSource text:\n在不同一性双胎的情况下,一个婴儿明显较小,生长模式与另一个不同。",
"output": "In the case of discordant twins, one baby was significantly smaller and had different growth patterns compared to the other."
}
]
可以看到我使用了相同的 system
,并尽可能使用相同的 instruction
前缀,这样模型会在符合这种情况时采用我训练后的参数,而更少干预其他情况下的参数,以减少灾难性遗忘的概率。为了生成更多的数据量,我把中文翻成英文和英文翻成中文各生成了一次,然后再加上各种奇怪的符号和 HTML 标签,这样数量就翻了几倍,大概准备了 2000 条吧,保存到
data/data.json
。然后编辑
data/dataset_info.json
,加上自己的数据集:{
"train": {
"file_name": "data.json",
"columns": {
"prompt": "instruction",
"response": "output",
"system": "system"
}
}
}
为了节省空间,这里我就不用 input
部分了,因为它实际就是拼在 instruction
后面。顺带一提,每条数据中的 tokens 数量也会影响显存占用,比如 1024 的长度需要 20 GB 以上的显存,而不是初始的 16 GB。
然后就可以执行
llamafactory-cli webui
,并打开 WebUI 进行微调了。这些参数没啥好说的,就照着抄吧:语言选择
zh
,模型名称选择 Qwen2-7B-Chat
,微调方法选择 lora
,数据路径填 data
,数据集选择 train
。其他参数稍微介绍一下:
- 高级设置 - 加速方式:这里如果使用
unsloth
,会让训练和评估的 loss 都有大幅下降,但是它的版本兼容性有问题,可能安装会比较麻烦,而且不支持多卡训练。 - 学习率(learning_rate):初始的学习速率,较低会学得慢,适合数据量大的情况,较高容易过拟合。可以先用默认值试试。
- 训练轮数(num_train_epochs):需要训练几轮。可以观察训练过程中的 loss 和 eval_loss:如果 loss 不再降低,则可能已经训练够了;如果 eval_loss 开始上升,则可能开始过拟合了。因为我们的数据集较小,一般后者会更早发生。
- 截断长度(cutoff_len):更大的 tokens 数量会需要更多的显存,训练时需要确保样本中的 token 长度不会多于截断长度,否则可能导致样本截断后丢失 eos。这样可能导致微调后的模型在推理时有概率不生成 eos,继而不断输出重复字符串。
- 批处理大小(per_device_train_batch_size):每个 GPU 同时处理多少个样本。如果显存小就只能设成 1,但是速度慢,相对不容易收敛,建议搭配更小的学习率和更大的梯度累积;显存较大则可以设置更高的值,速度会快一些,但是会降低泛化能力,建议搭配更大的学习率和更小的梯度累积。为了更好的泛化能力,我设成了 1。一个调参经验是,若将 batch_size 设为原来的 x 倍,learning_rate 大概要设置成原来的 √x 倍。
- 梯度累积(gradient_accumulation_steps):如果每处理一批样本就更新梯度,可能导致较难收敛,特别是批处理大小较小的情况。此时可以增大梯度累积,让它训练多批数据,再根据整体情况来更新梯度,这样会更容易收敛。一般设为 2 的幂,我测试发现 1 比较好。
- 验证集比例(val_size):将训练集中的一部分拿来验证,而不参与训练。因为我的训练集较小,可以设为 0.02 左右。如果发现训练的 loss 很低(例如 0.1 左右),但是验证的 loss 较高(例如大于 1),或者前者在下降,但是后者在上升,那么可能存在过拟合的情况。
- 学习率调节器(lr_scheduler_type):默认值是
cosine
,也就是随着训练进程不断降低学习率。因为我们的数据集较小,想让这些数据公平地调节权重地话,可以设成constant
,让它整个训练过程中保持不变。 - LoRA 参数设置 - 秩(lora_rank):这是一个比较关键的参数,影响模型的大小。我们的指令并不复杂,因此按论文里用 8 即可。实测更小时 loss 下降较慢,更大时容易学到一些不希望它学到的知识(例如把 HTML 标签里的属性也翻译了,或者样例中有些故意不翻译的缩写,它也学会了随机不翻译一些词)。其实这里就已经可以推出,用秩更大的 QLoRA 不一定能击败 LoRA。
- LoRA 参数设置 - LoRA 缩放系数(lora_alpha):一般设为秩的 2 倍。QLoRA 则设为秩的 1/4。
- LoRA 参数设置 - LoRA 随机丢弃(lora_dropout):随机让一些参数不参与训练,这样可以防止过拟合。但是太高容易降低训练效率。一般 7B 左右的模型设为 0.1,70B 左右的模型设为 0.05。如果使用 PiSSA 的话,其官方文档建议设为 0;如果使用 unsloth 的话,设为 0 可以加快速度。
- LoRA 参数设置 - LoRA+ 学习率比例(loraplus_lr_ratio):LoRA 的两个低秩矩阵 A 和 B 分别填充的是随机值和 0,根据 LoRA+ 的论文,B 矩阵的学习率需要比 A 矩阵更高才能让效果更优。在秩为 8 时,这里建议为 16,实测 loss 确实降低得更快。
- LoRA 参数设置 - 使用 rslora:这个可以自动设置每层所用的秩。实测效果并不好。
- LoRA 参数设置 - 使用 DoRA:可以动态调整优化参数。实测效果并不好,且降低训练速度。
- LoRA 参数设置 - 使用 PiSSA:这是个重要的参数,建议选上,可以大幅加快 loss 的降低速度。但是 LLaMA-Factory 目前对 PiSSA 的支持有 bug,需要按这个 PR 手动改一下。PiSSA 的初始化也有个需要修改配置文件才能调整的
pissa_iter
超参数,默认值是16
,我发现改成32
比较好。看代码还发现 unsloth 和 PiSSA 是不能同时生效的,它们对应的最佳超参数有一些差异,个人感觉 PiSSA 的效果好些,但是训练速度稍慢于 unsloth(约 10%)。 - LoRA 参数设置 - LoRA 作用模块:这个是指对哪些模块进行微调,论文中说只选
q_proj,v_proj
能达到微调所有模块的效果。实测这样会让训练的参数减少(2M vs 20M),但需要更多的训练轮数才能让 loss 降到同等水平。 - DeepSpeed stage:如果有多卡的话可以启用,显存够时建议设为
2
,少一点可以设为3
,再少则勾选使用 offload
。其中2
不影响训练速度,其他会降低速度。不过由于 4090 的通信带宽有限,实测双卡比单卡大概只快 35%,但是占用了双倍显存。所以如果想同时训练多个模型来测试,可以启动 2 个 LLaMA-Factory 并设置不同的CUDA_VISIBLE_DEVICES
,这样会更快。
至于 QLoRA、GaLore 和 LISA(可以用 LMFlow 框架)等微调方法这里就不阐述了,有兴趣的可以自己尝试。只是我自己的测试结果是,在这个场景下都不如 LoRA。
另外,如果想验证模型的泛化能力,但又不想用随机的验证集,可以复制已有的配置文件,保存为
train.yaml
并手动修改:dataset: train
eval_dataset: eval # 验证集的名字叫 "eval",需要在 "dataset_info.json" 里添加
eval_steps: 50 # 多少步进行一次验证
eval_strategy: steps
weight_decay: 0.001 # 添加 L2 正则化,对改善泛化能力有一定作用
然后用 llamafactory-cli train train.yaml
启动即可。训练完毕就可以进行测试了,但是因为我使用了 PiSSA,不能直接用它的 chat 功能,需要自己构造一个
pissa.yaml
配置:model_name_or_path: Qwen/Qwen2-7B-Instruct
adapter_name_or_path: saves/Qwen2-7B-Chat/lora/test/pissa_converted # 这里改为实际的路径
template: qwen
finetuning_type: lora
然后用 llamafactory-cli webchat pissa.yaml
启动即可。如果发现有问题,先检查下训练数据,有没有异常的。比如我发现例句中有这样的数据
"2.6变更订单(Change Order)"
<-> "2.6 Change Order"
。在中翻英时是正确的,但是英翻中时就会让模型搞不懂了。经过我几轮清洗,数据降到了约 1000 条,但是训练效果反而好了不少。而且数量少了,训练也加快,不到 5 分钟就训练完,更容易评估效果。
此外就是多调整下参数试试了,加上训练过程是随机,相同参数可能多试几次效果就不一样了。
如果测试没问题,就可以进行合并了。
如果不合并的话,推理时还需要计算低秩矩阵,会稍微影响性能。合并则是把低秩矩阵的参数和原模型的参数相加后保存,这样推理时和和原始模型的性能一致了。
先构造一个
merge.yaml
:### model
model_name_or_path: Qwen/Qwen2-7B-Instruct
adapter_name_or_path: saves/Qwen2-7B-Chat/lora/test/pissa_converted
template: qwen
finetuning_type: lora
### export
export_dir: models/Qwen2-7B-Instruct-LoRA # 这里是合并后的模型路径
export_legacy_format: false
然后执行 llamafactory-cli export merge.yaml
即可。导出完毕后就可以部署了。
在部署 Qwen2-72B-Instruct-GPTQ-Int4 时,我测试最快的方案是用 vLLM,并启用 tensor parallelism;而在部署 Qwen2-7B-Instruct 时,最快的方案是启动 2 个 LMDeploy 绑定不同的显卡,然后前面再弄个 nginx 做负载均衡,256 并发时能到约 5000 tokens/s。
最后再给大家降低些预期,鉴于 LoRA 只是个很小模型,如果原始模型不能理解较长的语句,强行要让它通过微调来学会,可能难度比较大。还有挺多专业的术语,动辄就是上百万条,训练一次要几天甚至几十天,还不如翻译时把术语传进去。
但是,有个更加聪明的小模型也是支持微调的:GPT-4o mini。在 2024 年 9 月 23 日前,每 24 小时有 2M tokens 免费的训练额度,超出也只需要 $3.00/1M tokens,比 GPT-4o 的推理开销还低很多。而且据我主观测试,它的翻译效果比 GPT-4o 差不了多少,能到 85 分的水平。但是调用微调后的 GPT-4o mini 模型,费用贵了一倍,还算是可以接受的。
训练就更简单了,按照文档中的格式准备数据集,然后打开 OpenAI 的控制台,点 Create 按钮,根据提示填好表单即可。它的微调参数较少,可供选择的就 Batch size、Learning rate multiplier 和 Number of epochs 这 3 个,而且会根据数据集的大小自动计算。如果有必要,可以下次训练时再手动调整。
它的训练速度并不快,每分钟大概训练 50 steps(一共 samples * epochs / batch_size 步)。但是通过调大 batch size,可以按比例加快速度。
训练完后有个 playground 的按钮,可以同时比较两个模型,让它们进行翻译,然后对比结果。
这个方案其实很有吸引力,它的成本很低廉,通过微调可以达到差异化的竞争优势,而且底模质量也不错,翻译质量显著高于微调后的 Qwen2-7B-Instruct。不过 OpenAI 好像没有提供将微调后的模型分享给其他用户的功能,如果想针对不同客户来分别计费的话可能比较麻烦。
另外,Gemini Flash 1.5 也支持微调了,而且微调免费。但是不支持验证集,batch size 最小为 4,微调结果极其简陋(只有一张图,看不到指标),微调速度也很慢,调用微调好的模型也明显慢于官方模型,所以现阶段不推荐。
2024 年 8 月 21 日更新:
从今天开始,GPT-4o 也支持微调了,在 2024 年 9 月 23 日前,每 24 小时有 1M tokens 免费的训练额度。但是调用费用比未微调模型贵了 50%,相当于微调后的 GPT-4o mini 的 12.5 倍,用于翻译的话有点贵了。而且 GPT-4o 微调后的翻译效果似乎弱于 GPT-4o mini。
此外,从刚发布的 Phi-3.5-MoE-instruct 介绍页可以看出,GPT-4o mini 在一众小模型中得分是最高的,特别是 Multilingual 这项。
0条评论 你不来一发么↓