最近陆陆续续有几个做下游视觉任务的同事来问我同一个问题:从模型库把 DINOv3 的预训练权重拉下来,想在自己的数据集上训练一个任务头(解码器),到底该直接全量微调,还是一层一层解冻?这个问题看着简单,但答案其实没有固定的标准。数据量、任务类型、数据域差异、计算资源这几个变量一换,结论就完全不一样。
这篇文章不是把文档复读一遍,我想分享的是自己在 DINOv3 这类视觉基础模型上做迁移训练时,沉淀下来的一套判断逻辑和实操流程。从"什么时候用微调、什么时候用层解冻"这个问题出发,把概念、决策、代码、调参和踩坑一次说清。无论是刚接触大模型微调的新手,还是已经被各种参数折磨过的老手,都能从里面找到可以直接抄作业的东西。
1. 先搞明白这三个概念,再谈策略
1.1 预训练backbone、任务头和解码器是什么关系
从模型架构的角度看,DINOv3 的主体是一个 ViT 结构的 backbone,输入图像后输出一组 patch token,这组 token 里编码了从局部纹理到全局语义的层次化特征。任务头,也就是标题里说的解码器,是你下游任务的最终输出模块:分类任务里通常是个 Linear 层加 softmax,检测任务里可能是 DETR 的 Transformer decoder 或者 FPN 上挂的 dense head,分割任务里则常见轻量 decoder 加上采样模块。
任务头本质上就是一个"解码器",它的职责是把预训练模型学到的高层语义特征,解码成你任务需要的输出格式。这里有个容易忽略的认知:backbone 的参数分布已经锁定了一个"通用视觉特征空间",它的 patch token 能很好地描述纹理、形状、语义类别。你要做的不是"从头学习特征",而是决定"让哪些层的权重在训练过程中发生改变、改变多大"。
我见过不少同学把任务头训练理解成"把整个模型重新训练一遍",这种心态会导致两个极端:要么无脑全量微调,在小数据集上过拟合到亲妈都不认识;要么过度谨慎,只训一个线性层,结果下游任务精度怎么都上不去。正确的心态应该是:预训练模型是资产,任务头是杠杆,你要决定的是怎么在控制风险的前提下把资产盘活。
1.2 微调和层解冻的本质区别
用一个生活化的类比:预训练模型是你雇来的资深员工,已经在各种项目里见过大量世面。全量微调等于让他重新接受一轮系统性培训,把职业习惯全面刷新;层解冻等于先让他在有限的新环境里干活,遇到明显不适应的技能模块再针对性调整;只训任务头则等于完全不动老员工,只换一个新岗位说明书,让他用现有能力直接上手。
从技术实现上说,这三者的差别非常明确:
- 全量微调:backbone 的所有层和任务头都保持可学习状态,用较小的学习率更新,让整个模型适配新任务。参数量通常是几亿起步,反向传播开销最大。
- 层解冻:backbone 初始完全冻结,训练过程中按 schedule 逐步开放部分层的参数。这是一种"渐进式迁移",风险和开销都介于两者之间。
- 线性探测(只训头):backbone 全部参数冻结,只训练任务头。这是最保守的方案,适合数据量极少或追求快速验证的场景。
以 ViT-L 为例,backbone 有超过 3 亿参数,任务头通常只有几万到几千万参数。如果你在小数据集上全量微调 3 亿参数,这本质上是一个超高自由度的插值问题,过拟合几乎是必然的。反过来,如果任务本身就要求模型理解新的特征模式,只训一个线性头又明显容量不足。这就是"微调 vs 层解冻"这个问题存在的根本原因:你需要找到一个风险和收益的平衡点。
1.3 判断框架:数据量、域差、资源
落到实际操作中,我一般先问自己三个问题,这三个问题的答案基本决定了初始策略。
第一个问题:你有多少标注数据?这直接决定了参数更新空间的容忍度。数据量超过 10 万张且标注质量稳定,全量微调是合理的,模型有足够的样本约束权重更新方向;数据量在 1 万到 5 万之间,层解冻是最稳妥的;数据量低于 5000,老老实实只训任务头,最多加一个浅层的 adapter。
第二个问题:你的数据和预训练数据域差大不大?DINOv3 在自然图像上预训练,如果你下游是普通相机拍摄的场景图,域差小,浅层特征基本通用;但如果你做的是医学影像、遥感图像、水下图像或者工业质检,数据分布差异非常大,这时候就必须解冻更多层,甚至全量微调,让模型有足够的自由度去适配新域。否则就会出现"特征错位"——预训练特征空间里的语义和你的新域语义对不上,任务头再怎么训练也白搭。
第三个问题:算力和时间预算够不够?全量微调意味着每一轮反向传播都要计算所有层的梯度,显存占用高,训练时间长。层解冻在前期只更新任务头和各别 block,反向传播只到解冻层为止,开销会明显下降。如果实验周期紧,先用只训头的方案跑通 baseline,再决定要不要解冻,是性价比最高的路径。
这三个维度组合起来,决策其实已经比较清晰了。我把它整理成一个简单粗暴的决策逻辑:数据少、域差小,优先冻结;数据多、域差大,优先微调;中间地带,用层解冻逐步探索。
2. 什么时候该用微调
2.1 全量微调的适用场景
全量微调最适用的场景并不是"所有任务",而是满足几个硬条件的情况。首先,你的目标数据集要足够大,我个人的经验阈值是 10 万张以上干净标注的自然图像;其次,目标域和预训练域相差不要太远,否则你可能需要解冻所有层才够;最后,任务本身要求模型达到很精细的水平,比如实例分割、小目标检测、多任务联合训练这类,特征空间需要大幅度调整。
在这些条件下,全量微调的收益是非常明显的。模型有足够的容量去学习目标分布的精细细节,预训练权重提供了一个很好的起点,但不会被数据集中的噪声带偏。我试过一个 20 万张图像的工业质检项目,全量微调比只训 head 涨了将近 6 个点,这是层解冻怎么调都追不上的差距。
但全量微调有个核心风险:破坏预训练权重。由于所有层的学习率一致,底层特征(边缘、颜色、纹理)可能在训练初期被新任务的梯度短暂重写,导致特征通用性下降,泛化性能反而变差。这个现象在数据量稍微不足时特别明显,所以全量微调的实施前提一定是"数据量撑得住",否则宁可保守。
2.2 部分层微调:找到"黄金层"
如果你决定微调,但数据量又不是特别富余,我的建议是只微调最后 1/3 的 Transformer block 加任务头,而不是全量。原理不复杂:ViT 低层(前 1/3)主要编码通用的局部纹理、颜色、轮廓,这些特征几乎对所有视觉任务都有用;越到后面的层语义化程度越高,也就越和具体任务绑定。你换了新任务,出问题的往往是"语义层"而不是"纹理层"。
以 DINOv3 的 24 层 Transformer block 为例,一个我实测非常稳的配置是:冻结 block 0~15,微调 block 16~23 和任务头。这个方案在 5 万以下的数据量时,比全量微调稳定得多,训练速度和显存开销也友好不少。如果你想要更高精度,可以逐步把冻结边界往前移——先解冻最后 8 层,验证集不涨了再解冻倒数 12 层,这本质上就是一种手动控制的层解冻。
这里有一个细节值得注意:部分层微调的收益上限取决于你解冻的层能否覆盖任务需求。如果你的任务需要极强的空间细节(比如深度估计、边缘检测),只解冻最后 1/3 可能不够,因为 DINOv3 中负责空间位置编码的层往往集中在中后段。这种情况下,宁可解冻层数多但学习率小,也不要只解冻少数层然后用大的学习率硬冲,前者能保留更多预训练特征。
2.3 微调时的参数配置建议
微调不是直接把训练脚本里的学习率改成小值就完事。我实践下来比较稳定的配置是:backbone 学习率 5e-6 到 2e-5,任务头学习率 2e-4 到 5e-4,二者相差 10 到 30 倍。优化器用 AdamW,weight decay 控制在 0.01 到 0.05,warmup 步数占总步数的 5% 到 10%。这几项配置里有几个坑尤其值得说。
第一个坑是 norm 层的处理。如果你微调的 backbone 里有 LayerNorm 或 BatchNorm,我强烈建议把 norm 层的学习率设为其他层的 0.1 倍甚至直接冻结。norm 层的参数非常敏感,更新太快会导致特征分布被整体拉偏,训练曲线上会出现诡异的早期 spike,然后模型就再也回不到正常水平了。
第二个坑是不要用同一个 param_group 管理 backbone 和 head。DINOv3 这种模型,任务头随机初始化,需要的梯度尺度远大于已经收敛的 backbone,如果混在一起用一个学习率,要么 backbone 更新过大导致灾难性遗忘,要么 head 学不动导致性能上不去。分开管理是最基本的工程要求。
3. 什么时候该用层解冻
3.1 层解冻的逻辑和适用场景
层解冻解决的核心矛盾是:数据量不足以支撑全量微调,但只训任务头又明显放不下任务的特征需求。它通过"分时复用"的方式规避过拟合:初始阶段在预训练特征空间里进行低风险映射,中期阶段让特征逐步向目标域靠拢,后期阶段按需向更浅层推进,直到验证集精度不再显著提升。
适用场景非常典型:标注数据在 5000 到 5 万之间的中等规模数据集;目标域有偏移但不算离谱,比如水下图像、遥感影像、工业质检,这类数据的图像统计特性变了,但底层视觉结构还在;算力有限,不想为全量微调付出显存和训练时长;预期训练时间紧张,希望快速验证任务可行性。
我见过的最经典误用场景,是在只有 3000 张图的数据集上直接层解冻了 20 个 block,训练到一半验证集疯狂震荡,最后精度还不如只训 head。层解冻的前提是为了"适应新域",而不是为了"提升模型复杂度假象"。数据量不够的时候,解冻就是过拟合的温床。宁可先只训 head,把 baseline 跑出来,再逐层解冻看增益。
3.2 解冻调度策略:离散式和评估式
层解冻有两种常见节奏。一种是 epoch 级别的离散调度,比如:前 5 个 epoch 只训 head,第 6 到 10 个 epoch 解冻最后 6 个 block,第 11 到 15 个 epoch 解冻最后 12 个 block。这种方案的好处是节奏清晰,方便对比每个阶段带来的指标变化,坏处是如果数据分布复杂,5 个 epoch 可能不够 head 进入状态,你就提前解冻了。
另一种是连续性评估调度:每训练一定步数后跑一次验证集,如果验证精度连续两三个 epoch 没有提升,就解冻下一批层,继续训练。这种更稳妥,因为它真正做到了"按需解冻",但会额外消耗验证时间,也需要你写一段自动控制解冻逻辑的代码。
我的建议是:第一次跑项目用离散式调度,简单直接,能让你快速了解数据特性;等项目进入调优阶段,再改成评估式调度,把解冻时机交给验证指标说了算。实践经验表明,评估式调度通常能比离散式多涨 0.5 到 1 个点,代价是大约 10% 的训练时间。
这里有一个容易被忽略的工程细节:当你解冻一批新层时,优化器里的参数梯度状态是重新开始累积的,而 head 的梯度状态已经训练了多个 epoch。如果直接复用同一个优化器继续走,head 的学习率状态可能已经衰减得太多。正确做法是给新解冻的层单独开一个 param_group,让这个 group 使用独立的初始学习率。PyTorch 里这一步实现并不直观,后面实操部分我会给出完整代码。
3.3 解冻到什么程度就该停
判断标准只有一个:验证集指标的变化。不要在"感觉还能涨"或者"别人解冻了 20 层我也要解冻 20 层"这种情绪里做决策。
我的操作习惯是:每进入一个新的解冻阶段之后,训练 5 个 epoch,然后对比当前最佳验证指标。如果提升幅度在 0.1 个点以内,并且损失曲线已经平稳,果断停止解冻。继续往前推进,过拟合的风险会直线上升,而收益趋近于零。
还有一个辅助判断指标:观察解冻层在训练过程中的梯度范数。如果某一层的梯度范数从一开始就非常小,比如接近任务头梯度的 1/50,说明这个位置的特征已经足够适配新任务,不差你解冻它那点更新量。这个指标特别适合用来决定"要不要往前再解冻一层"——如果下一层的梯度范数本身就很小,那就真的没必要解冻了。
4. 实操:DINOv3任务头训练的两套完整流程
4.1 基础框架:冻结、param_group、逐步解冻
先把基础代码框架搭好,后面两个方案都基于这套代码。核心是用requires_grad控制哪些层参与梯度计算,然后用 param_group 把不同学习率的参数分开管理。
import torch from torch import nn def set_parameter_requires_grad(module, requires_grad): for p in module.parameters(): p.requires_grad_(requires_grad) # 假设 model 包含 model.backbone 和 model.head 两个子模块 # 初始阶段:冻结 backbone 全部参数 set_parameter_requires_grad(model.backbone, False) # head 默认保持 requires_grad=True构建优化器时有一个关键点:只把requires_grad=True的参数放进优化器。不要图省事把全部参数都塞进去再让优化器自己跳过冻结参数,那样 AdamW 仍然会给冻结参数维护状态,白白浪费显存。
def build_optimizer(model, head_lr, backbone_lr, weight_decay=0.03): head_params = [p for p in model.head.parameters() if p.requires_grad] backbone_params = [ p for p in model.backbone.parameters() if p.requires_grad ] param_groups = [ {"params": head_params, "lr": head_lr, "weight_decay": weight_decay}, ] if backbone_params: param_groups.append({ "params": backbone_params, "lr": backbone_lr, "weight_decay": weight_decay, }) return torch.optim.AdamW(param_groups)解冻操作本身很简单,麻烦的是优化器状态。当你在训练中途解冻新层时,一个常见的做法是重新构建优化器,因为直接用旧的优化器,新解冻层的参数并不在参数组里,梯度算出来了也没人更新。重新构建优化器有个代价:AdamW 的动量状态会丢失。如果你已经用同一个优化器训练了 10 个 epoch,head 的exp_avg和exp_avg_sq积累得比较充分,重建后 head 会有几个 step 的震荡,但通常很快恢复,影响不大。
def unfreeze_blocks(model, start_idx, end_idx): """解冻 backbone 中 [start_idx, end_idx) 范围内的 Transformer block""" blocks = model.backbone.blocks for idx in range(start_idx, end_idx): for p in blocks[idx].parameters(): p.requires_grad_(True)如果你想保留旧优化器的状态,需要在解冻前保存optimizer.state_dict(),解冻后手动剔除新增参数的 key 再加载。这个操作工程上比较繁琐,我建议训练脚本里直接用重建优化器的方案,简单、可控、不容易出 bug。
4.2 方案A:快速验证,只训任务头(解码器)
这个方案适合数据量低于 5000、或者项目刚启动需要快速跑 baseline 的场景。操作步骤非常短:
- 加载 DINOv3 预训练权重,冻结 backbone 全部参数。
- 替换任务头,随机初始化。分类任务用
nn.Linear,分割任务用轻量 decoder。 - 只对任务头用 AdamW,学习率设置为 1e-3,训练 10 到 20 个 epoch。
- 跑验证集,记录 baseline 指标。
- 如果时间允许,再解冻最后 4 到 8 个 block,看看能不能涨点。
任务头的初始化方式对结果影响非常大,这是新手最容易忽略的坑。分类头直接把最后一层 Linear 的 bias 初始化为类别先验概率的对数,能显著加速收敛;分割和检测任务的头则经常用到 zero-init,把最后一层卷积权重初始化为 0,这样模型初始输出是常数,避免第一轮训练就引入过大梯度导致 loss 爆炸。
import torch.nn as nn class ClassificationHead(nn.Module): def __init__(self, d_model, num_classes, class_prior=None): super().__init__() self.head = nn.Linear(d_model, num_classes) if class_prior is not None: # 用类别先验初始化 bias,加速早期收敛 self.head.bias.data = torch.log(torch.tensor(class_prior) + 1e-6) def forward(self, x): # x: [B, d_model],通常是 [CLS] token 或全局平均池化结果 return self.head(x)只训任务头的天花板取决于预训练特征和下游任务的匹配度。匹配度高,这个方案能跑到全量微调 80% 甚至 90% 的性能;匹配度低,就需要考虑方案B。
4.3 方案B:分阶段层解冻,适合密集预测任务
密集预测任务(语义分割、深度估计、目标检测)对特征空间的要求比分类高一个维度,单靠预训练特征往往不够。这类任务我推荐分三个阶段的层解冻流程。
阶段 0(epoch 0 到 N):backbone 全冻结,只训练任务头直到验证精度进入平台期,通常 5 到 10 个 epoch。这个阶段的任务是让任务头在预训练特征空间里找到合理的映射方向。
阶段 1(N 到 2N):解冻最后 1/3 的 Transformer block。以 24 层 backbone 为例,解冻 block 16 到 23。head 和 backbone 分别用不同学习率继续训练 5 个 epoch 左右。这个阶段是收益最大的阶段,大部分项目的精度提升都来自这里。
阶段 2(2N 到 3N):如果验证集还在涨,解冻中间 1/3,即 block 8 到 15。此时学习率要降为之前的 1/2 甚至 1/4,因为模型已经接近收敛,过大的参数更新只会带来震荡。
关键点是各阶段的学习率分配。我一开始会把 task head 的学习率设为 3e-4,每次进入新阶段后降 1/3;backbone 的解冻部分学习率设为 1e-5 到 3e-5,并且保持稳定,不跟随 head 一起降。这里的逻辑是:head 大部分时间在训练,越到后期越需要小步幅精细调整;backbone 刚解冻,需要相对稳定的更新步长来逐步适应新域。
# 以 24 层 backbone 为例 total_epochs = 15 for epoch in range(total_epochs): train_one_epoch(model, train_loader, optimizer, criterion) val_metric = validate(model, val_loader) # 阶段切换逻辑 if epoch == 5: unfreeze_blocks(model.backbone, 16, 24) # 重建优化器,给新解冻层分配独立学习率 optimizer = build_optimizer( model, head_lr=2e-4, backbone_lr=2e-5 ) elif epoch == 10: unfreeze_blocks(model.backbone, 8, 16) optimizer = build_optimizer( model, head_lr=1.3e-4, backbone_lr=1e-5 )实际项目里不要死板地按 epoch 数切,更稳健的做法是写一个回调函数,当验证集指标连续两个 epoch 不涨时,自动解冻下一批层并重建优化器。这样能避免"head 还没训好就被迫进入解冻阶段"的尴尬。
4.4 用日志和可视化判断该不该继续解冻
解冻不是无脑推进,你需要一套判断信号来决定什么时候停。我习惯记录三样东西。
第一样是每个阶段前 3 个 epoch 的 train loss。如果解冻后 train loss 起点比上一阶段结束时更低,说明解冻时机是合适的;如果解冻后 loss 一开始就明显升高,说明解冻太早,或者新解冻的层还没有适应。这个信号非常灵敏,基本可以当作解冻时机的即时反馈。
第二样是验证集指标的变化曲线。这个不用多说,任何训练脚本都应该有。我特别提醒的是不要只看最终精度,要看曲线形态。如果验证精度在解冻后先跌后涨,说明模型正在经历特征适应期,可以多给几个 epoch 观察;如果一路下跌,赶紧回滚到上一个阶段。
第三样是每层梯度范数的统计。解冻后通过打印各层的梯度范数,你可以直观看到哪些层在真正"学习"。如果某层的梯度范数非常小,长时间不变化,说明这个位置的特征已经足够好,不需要解冻它。合理利用这个信息,可以让你少做很多无效训练。
def log_grad_norms(model): total_norm = 0.0 for name, p in model.named_parameters(): if p.requires_grad and p.grad is not None: norm = p.grad.norm().item() total_norm += norm if norm > 1e-4: print(f"{name}: {norm:.4f}") print(f"total grad norm: {total_norm:.4f}")这个脚本调参阶段非常有用,你可以看到任务头和 backbone 解冻层之间的梯度量级差距,进而调整两者的学习率比例。
5. 我在实际项目中踩过的坑与排查经验
5.1 解冻后 loss 不降反升
这是我被问得最多的一个问题,也是最容易让人心态崩掉的问题。解冻前训练得好好的,一解冻 loss 不降反升,甚至直接炸掉。
绝大多数情况是学习率过大导致的。当你解冻了新的层,这些层从"完全不更新"切换到"开始更新",参数变化幅度如果过大,会破坏旧特征。我的经验是解冻后第一时间把 backbone 的学习率降到原来的 1/10,观察两三个 epoch 再决定是否恢复。如果你一次解冻了太多层,比如直接解冻 12 个 block,学习率必须更保守。
还有一个不太常见但确实存在的原因:任务头过度适配了冻结特征。在前面阶段 head 已经在一个固定的特征分布上训练了多轮,突然解冻导致特征分布剧烈变化,head 之前的参数就错位了。这种场景下 loss 不一定爆,但验证精度会明显下降。解决办法是引入一个小 trick:解冻新层的同时,把任务头的学习率也适当调低,给 head 一个重新适配的机会。
5.2 微调后过拟合严重
症状非常典型:训练集 loss 一路下降,验证集指标涨到某个点就开始掉,train 和 val 的 gap 越拉越大。
排查顺序是这样的:先看 head 学习率是不是太高。很多人把过拟合归咎于 backbone 参数太多,但实际上很多过拟合来自任务头,它随机初始化,参数极少,如果学习率设置成 5e-3 甚至更高,头本身就会快速记住训练集的噪声标签。把 head 学习率降到 2e-4 级别,往往过拟合就能缓解一半。
再看解冻层的数量。数据量不大却解冻了大半个 backbone,模型自由度太高,过拟合是数学上注定的。减少解冻层数,或者把解冻层的学习率再降一个量级,都能有效收窄 train-val gap。
最后别忘了数据增强。DINOv3 的预训练特征本身对平移、尺度变化有一定鲁棒性,但如果你训练时用的增强和预训练阶段差异过大(比如预训练用全局裁剪,你用密集裁剪),反而会放大过拟合。我的习惯是精细调增强策略,而不是一味堆算力。
5.3 特征退化与 norm 层问题
特征退化(feature collapse)是一个比较隐蔽的坑。症状是:训练 loss 和验证 loss 的差距突然拉大,模型的 logits 分布收敛到一个非常小的范围,输出几乎变成了常数。这种情况在处理分割任务时尤其危险,因为模型的预测图会变成一块均匀的颜色。
罪魁祸首通常是 norm 层参数被过度更新。DINOv3 这类 ViT 结构里 LayerNorm 的参数极其敏感,全量微调或者大规模解冻时,如果 norm 层的学习率不单独控制,特征分布会被严重扭曲。我的处理方式非常简单粗暴:在微调和层解冻期间,把 backbone 里所有 norm 层设置为冻结状态,也就是requires_grad=False。只在最后阶段,如果确实需要更精细的语义特征,才放开最后几个 block 的 norm 层,并且学习率降到 2e-6 量级。
这个操作看似是"少训练了一些参数",实际效果却非常稳定。我也试过用低学习率让 norm 层参与训练,但最终发现冻结 norm 层的方案在几乎所有项目中都更稳,精度损失忽略不计,train-val gap 却明显减小。
5.4 常见问题排查速查表
把上面这些经验整理成一个速查表,训练过程中遇到问题直接对照着查:
| 现象 | 可能原因 | 首选排查手段 |
|---|---|---|
| 解冻后 train loss 不降反升 | 解冻层学习率过大 | 把 backbone 学习率降到原来的 1/10 |
| 解冻后验证集精度猛跌 | 解冻太早或解冻层数过多 | 回退到上一阶段,解冻层数减半 |
| train-val gap 持续拉大 | 过拟合,head 学习率过高 | head 学习率降到 2e-4,增加 dropout |
| logits 分布塌缩成常数 | 特征退化,norm 层被过度更新 | 冻结 norm 层,检查 head 初始化 |
| 显存不足 | 冻结层不够多或 batch 太大 | 用梯度 checkpointing,减小 batch |
| 重建优化器后 loss 震荡 | AdamW 状态丢失 | 前几个 epoch 加大 warmup 步数 |
这个表格是我在多个项目里反复验证过的,大多数训练异常都能命中其中一两条。遇到问题先不要盲目调参,对照着定位原因再动手。
最后分享一点我个人的实际操作体会
做 DINOv3 任务头训练这么久,我最大的体会是:这个问题的正确答案从来不在于"微调更好"还是"层解冻更好",而在于你能不能精准判断自己项目所处的数据规模、域差和资源约束。与其到处问别人用什么方案,不如先花半天时间把自己的条件理清楚。
我的习惯是先跑只训任务头的 baseline,同步记录各层梯度范数,然后用验证集指标来决定要不要解冻。这套流程看起来慢,实际上是最快的调参路径——因为你每一步都有数据支撑,而不是靠猜。
最后再分享一个小技巧:在解冻实验阶段,给每个阶段单独开一个日志目录,记录阶段编号、解冻层范围、学习率、验证集指标。这个习惯让你在对比不同解冻策略时有据可查,而不是每次都在"好像当时效果还行"的记忆里挣扎。训练模型最耗时的不是训练本身,而是反复比较冻结和解冻组合的收益。有了完整的实验记录,这个比较过程能快好几倍。