【从零开始】18. 持续优化模型微调

有小伙伴私信说希望能展开说说如何做模型持续优化,为此临时加开一章,讲讲如何结合 brain-mix 项目的 Unsloth 微调报告进行训练结果分析。

补充:若不希望麻烦的也可以使用类似 WandB、SwanLab、Tensorboard 等训练监控工具(平台)。这些监控工具(平台)能够提供更直观的界面和分析意见。只不过我资源有限就只能自己分析而已。

首先我们在超参数不变的前提下做三轮训练,每次训练只增加训练数据,看看三轮下来的效果如何。具体结果如下:

bash 复制代码
# 第一轮最佳结果
训练数据:50000
最终得分 (Loss): 1.9403
验证集损失 (Eval Loss): 1.9403
训练集损失 (Train Loss): 1.9287
最优超参数组合:
{
  "learning_rate": "3e-4",
  "max_steps": 1500,
  "r": 32,
  "lora_alpha": 128,
  "per_device_train_batch_size": 16,
  "gradient_accumulation_steps": 8,
  "max_seq_length": 4096
}

# 第二轮最佳结果
训练数据:150000
最终得分 (Loss): 1.5788
验证集损失 (Eval Loss): 1.5788
训练集损失 (Train Loss): 1.4781
最优超参数组合:
{
  "learning_rate": "2e-4",
  "max_steps": 1200,
  "r": 64,
  "lora_alpha": 128,
  "per_device_train_batch_size": 16,
  "gradient_accumulation_steps": 8,
  "max_seq_length": 8192
}

# 第三轮最佳结果
训练数据:290000
最终得分 (Loss): 1.3271
验证集损失 (Eval Loss): 1.3271
训练集损失 (Train Loss): 1.4042
最优超参数组合:
{
  "learning_rate": "3e-4",
  "max_steps": 1500,
  "r": 64,
  "lora_alpha": 64,
  "per_device_train_batch_size": 16,
  "gradient_accumulation_steps": 8,
  "max_seq_length": 4096
}

通过损失率(Loss)稳定下降得知,当前模型处于"欠拟合"或者说学习不足的状态。这意味着模型的容量(由 LoRA 参数的 r 决定)可以学习更复杂的模式,但它还没有"看"到足够多的数据来充分学习。简单来说就是模型当前还"学有余力",但我提供的"学习资料"不足。

Optuna 立大功

其次, Optuna 的动态超参数调整正在发挥重要作用。

我们看看这些超参数组合,假设第一轮最优的超参数组合为基准组合:

bash 复制代码
lr: 3e-4
r: 32
lora_alpha: 128
max_seq_length: 4096

那么看看第二轮的最优组合:

bash 复制代码
lr: 2e-4
r: 64
lora_alpha: 128
max_seq_length: 8192

通过对比我们不难发现在第二轮组合中,r 参数增加到 64 了,这说明提供的训练数据多了,模型需要更复杂的"内部结构"(更高的 LoRA rank)来捕捉模式。其次是 max_seq_length,增加到了 8192。这可能是新数据中包含了更长的文本,或者模型发现处理更长的上下文能更好地降低损失。相反,learning_rate 降低到 2e-4,这也说明了当模型变得更复杂(譬如 r=64)并处理更长序列时,用一个稍小的学习率进行更"精细"的更新,有助于稳定收敛。

我们再看看第三轮的最优组合:

bash 复制代码
lr: 3e-4
r: 64
lora_alpha: 64
max_seq_length: 4096

与第二轮相比 r 保持在 64,本轮数据量已经去到 29w+了,r=64 证明 这是一个比 r=32 更好的选择。但在这轮中 lora_alpha 降低到 64 了,这是一个有趣的组合。因为一般来说 lora_alpha 会被认为是 LoRA 的缩放因子,常见的经验法则是设为 2 * r。但这次 alpha = r 的组合取得了最好效果,说明这个经验法则不是绝对的,Optuna 探索到了一个非典型的有效组合。而 lr(learning_rate) 本次却回升到 3e-4,配合 lora_alpha=64,一个更快的学习率可能找到了一个更好的收敛路径。

看到这逐渐优化的参数,Optuna可谓立了大功了。

训练瓶颈

虽然能够通过 Optuna 的动态超参数调整训练状态,但通过结果对比我们仍能够发现训练的瓶颈。没错...就是 max_steps。

我们可以计算一下的,在 brain-mix 项目配置中,初始化指定的 per_device_train_batch_size 参数为 16,gradient_accumulation_steps 为 8。这就意味着有效批次的大小就是 16 * 8 = 128。那么在第三次训练的时候我们的训练数据已经有 29w+了。要一次完整的 Epoch 则需要差不多 2265 步(290000 / 128)。但是目前设置我们的最大步数只有 1500,这不足以让模型完整地看一遍所有数据!这意味着模型在训练结束时,还有近一半的数据根本没见过,这也能解释为什么增加训练数据能够显著提升训练效果的原因。

验证猜想

为了验证我的猜想,我第四轮训练时并没有修改训练的超参数组合,而是像之前那样增加训练数据,最终将训练数据提升到 36w+。训练结果如下:

bash 复制代码
# 第四次最优结果
训练数据:360000
最终得分 (Loss): 1.3013
验证集损失 (Eval Loss): 1.3013
训练集损失 (Train Loss): 1.3559
最优超参数组合:
{
  "learning_rate": "2e-4",
  "max_steps": 1500,
  "r": 16,
  "lora_alpha": 128,
  "per_device_train_batch_size": 32,
  "gradient_accumulation_steps": 8,
  "max_seq_length": 8192
}

(⊙o⊙)...由结果可以看出,数据量增加到 124%,但性能提升不到2%。这不是正常的收益递减,而是训练策略出现了根本性问题(不出所料)。通过查阅日志得知,当 Optuna 尝试 batch_size=32 + seq_len=8192 组合时触发 OOM ,因此不得不牺牲模型能力被迫选择 r=16,这这严重限制了模型从 36w+ 数据中学习复杂模式的能力。其次还是那个 max_steps 问题,就不展开说了。并且首次出现 Train Loss > Eval Loss,这是典型的欠拟合信号,模型在训练集上还没有学好,就被迫停止了。

好了,第四轮训练直接印证了我的想法,接下来是时候修改一下训练的超参数看看是否可以达到更好的效果了。

超参数调整

首先我们要调整的超参数是 max_steps。以第四次训练数据集总量为例,36w+的训练数据,假设我们选择 batch_size=16,grad_accum=8,则有效批次为 128。一个 Epoch 需要 360000 / 128 ≈ 2812 步。因此将 max_steps 调整为:

yaml 复制代码
max_steps:
  choices: [ 3000, 3500, 4000 ]

搜索空间调整为至少能覆盖 1 个 Epoch。

其次,限制 per_device_train_batch_size。由于 batch_size=32 已被证明会导致 OOM。为了给 r 和 seq_len 留出空间,我们必须限制它。可以将其调整为:

yaml 复制代码
per_device_train_batch_size:
  choices: [ 8, 16 ]

再者,解放 r 参数。让 Optuna 重新探索 r=32 和 r=64 这些已被证明有效的选项,甚至可以探索更高的 r=96。将其调整为:

yaml 复制代码
r:
  choices: [ 32, 64, 96 ]

第五轮训练结果

bash 复制代码
# 第五轮最佳结果
训练数据:391744
最终得分 (Loss): 1.3226
验证集损失 (Eval Loss): 1.3226
训练集损失 (Train Loss): 1.3913
最优超参数组合:
{
  "learning_rate": "3e-4",
  "max_steps": 4000,
  "r": 96,
  "lora_alpha": 64,
  "per_device_train_batch_size": 8,
  "gradient_accumulation_steps": 8,
  "max_seq_length": 4096
}

(⊙o⊙)...傻眼了。嗯...从最终的 Eval Loss 数字上看训练并没有改善,但从揭示的问题和参数选择趋势上看,这是一次非常有价值的"诊断性"训练(安慰自己)。不得不说 Eval Loss 从 1.3013 上升到了 1.3226,说明模型性能略有下降。但通过这次训练让我看到了更深层次的东西。

首先,成功摆脱了次优参数。从最优超参数组合中可以看出 Optuna 选择了 r=96 和 max_steps=4000。这证明我们之前的想法是正确的,模型需要更高的复杂度和更长的训练时间。它成功摆脱了第四次训练中 r=16 和 max_steps=1500 的"妥协"组合。

其次,它揭示了我们训练的新瓶颈 max_seq_length。为了在有限的显存(在第五次训练时,曾一段时间内可用显存减少到 24 GB。借来用的机器哪有全功率使用那么理想...)中容纳高 r (96) 和长 steps (4000),Optuna 不得不做出新的"妥协"将 per_device_train_batch_size 降至 8 并且将 max_seq_length 从 8192 降至 4096。这极有可能就是 Eval Loss 第五次比第四次还要高的原因。

再者,Train Loss (1.3913) 仍然高于 Eval Loss (1.3226),这证明模型仍未在训练集上充分学习。

综上所述,本次训练的根本原因是在有限的显存下 r、batch_size、max_steps 和 max_seq_length 之间存在不可调和的矛盾。这次 Optuna 牺牲 seq_len 导致性能下降。

ε=(´ο`*)))唉资源不够的情况下,再训练下去就没有意义了。经过多次沟通,最终拿到最后一次训练机会。这次将可以独揽 RTX A6000 显卡的全部资源(Full Power)。

第六轮训练结果

bash 复制代码
# 第六轮最佳结果
训练数据:391744
最终得分 (Loss): 1.2287
验证集损失 (Eval Loss): 1.2287
训练集损失 (Train Loss): 1.2592
最优超参数组合:
{
  "learning_rate": "2e-4",
  "max_steps": 3000,
  "r": 192,
  "lora_alpha": 256,
  "per_device_train_batch_size": 32,
  "gradient_accumulation_steps": 8,
  "max_seq_length": 8192
}

(⊙o⊙)...终于等到了这份报告,这次训练一共用了 213 个小时。不过看到结果的那一刻,这般等待也是值得的。

1.2287 !!!

Eval Loss 从第五次的 1.3226 骤降至 1.2287,降幅高达 7.1%!这甚至远低于之前最好的第四次成绩 (1.3013)。这证明,我们之前的策略------牺牲一切保住显存,是错误的。而解放显存,追求最优配置,是完全正确的。

此外,这次的配置将我们之前遇到的"欠拟合"问题解决了。翻回到第五次训练我们的 Train Loss 是 1.3913,大于 Eval Loss 的 1.3226,差值为 -0.0687 这属于严重欠拟合状态。但这次我们的 Train Loss 下降到 1.2592,稍微大于 Eval Loss 的 1.2287,这个差值是 -0.0305 ,几乎已经达到理想拟合状态了。这能够证明模型在训练集上学得很好,几乎达到了"吃透"训练数据的程度,同时在验证集上泛化得也很好。

由此推断,当前的训练配置应该是这批数据基于 A6000 的几乎最优解了。如果后面再有机会训练,就直接用回这个配置再根据数据量调整 max_steps 即可。与此同时,训练配置中也不用配置执行那么多轮了,直接一轮出结果再根据 Train Loss 和 Eval Loss 的差距决定调整数据量还是训练参数即可。譬如:若第七轮的 Train Loss 明显低于 Eval Loss 的话就证明又出现过拟合的情况了,这个时候可以考虑增加正则化手段,尝试调整 weight_decay 或 lora_dropout 应该会有不错的效果。

如果还有时间的话(我应该没有了),可以考虑在数据质量方面入手。毕竟增加数据的多样性也能够令到模型更加完善。

以上就是对于 brain-mix 中自动训练脚本的使用分析样例了。目前只是单纯根据最基础的 Loss、Eval Loss 和 Train Loss 三参数进行判断训练结果。但数据最优是否就代表效果最好呢?这...我不知道,等后面第 19 章时再为各位解答吧。

以上代码均发布到 brain-mix 项目中,欢迎各位的指导。

gitee:gitee.com/yzh0623/bra...

github:github.com/yzh0623/bra...

(未完待续...)

相关推荐
expect7g4 小时前
Flink-To-Paimon 读取机制
大数据·后端·flink
倚栏听风雨4 小时前
Agent 认知+ReAct模式
后端
申阳4 小时前
Day 5:03. 基于Nuxt开发博客项目-页面结构组织
前端·后端·程序员
用户298698530144 小时前
C#: 高效移动与删除Excel工作表
后端·.net·excel
guchen664 小时前
记录一次Prism9隐式注册引发的事件聚合器失效问题
后端
一行•坚书4 小时前
kafka服务端与客户端如何协作?生产者发送消息分区策略是什么?消费者组分区策略?集群与ACK机制?
java·后端·kafka
天天摸鱼的java工程师5 小时前
干掉系统卡顿!Excel异步导出完整实战方案(百万数据也不慌)
java·后端
星释5 小时前
Rust 练习册 4:Deref trait 与智能指针
开发语言·后端·rust
Cache技术分享5 小时前
231. Java 集合 - 将集合元素转换为数组
前端·后端