ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MuRA多秩适配:测试时提升视觉语言模型零样本分类的分布偏移鲁棒性

MuRA多秩适配:测试时提升视觉语言模型零样本分类的分布偏移鲁棒性 视觉语言模型在开放词汇分类、图文检索和跨模态推理中已经成为常见底座。CLIP 这类模型通过图像与文本的对比学习把视觉特征和文本特征映射到同一个向量空间训练完成之后可以不做任何微调直接对测试图片做零样本分类。实际项目里这个理想状态很容易被打破测试图片的光照、分辨率、拍摄角度、背景杂乱程度甚至类别提示词的写法发生变化零样本精度都会明显下降。MuRA 是 Multi-Rank Adaptation 的缩写核心方向是在测试阶段对视觉语言模型做多秩适配让模型面对当前数据分布时临时调整同时尽量保持训练阶段的效率。下面从问题背景、适配原理、最小实现、验证方式和排查路径几个层面拆解这个方向。1. 测试时泛化为什么不能只靠零样本推理1.1 视觉语言模型的基本工作方式CLIP 类模型通常由两个编码器组成图像编码器负责把图片编码成图像特征文本编码器负责把类别描述或提示词编码成文本特征。零样本分类时不再为每个类别训练一个分类头而是把每个类别转成一句文本例如a photo of a cat然后用模型分别计算图片与所有类别文本的相似度相似度最高的类别就是预测结果。整个过程可以表示为image_feature image_encoder(image) text_feature text_encoder(text) logits image_feature text_feature.T * temperature pred argmax(logits)其中temperature是 CLIP 学到的 logit scale用于控制相似度分布的锐利程度。这个流程的好处很明显类别集合可以在推理时临时更换不需要重新训练模型因此非常适合开放词汇分类、检索式问答和冷启动场景。但零样本推理的稳定性依赖一个隐含假设测试图片的视觉分布与模型训练时见过的数据分布足够接近。CLIP 训练数据覆盖大量网络图文对在常见自然图片上表现很好。一旦测试数据变成卡通风格、医学内镜图像、夜间监控画面、低分辨率截图或者被加入了高斯噪声、颜色偏色和压缩伪影图像特征会和文本特征的对齐产生偏移分类边界就不再可靠。1.2 分布偏移让“开箱即用”失效我遇到过的典型场景是模型在标准测试集上精度不错换到真实业务数据后掉点严重。比如同一套商品图线上交付的图片经过了压缩、加边框、贴营销文字甚至被截图软件二次保存CLIP 的零样本精度会下降。这种下降通常不是硬件或部署问题而是分布偏移造成的特征错位。模型的图像编码器已经学会了训练数据的纹理、颜色和结构先验遇到带噪声或风格迁移后的图片时图像特征会进入文本特征锚点覆盖不到的区域。换句话说模型并不是“不认识”而是当前图片特征距离所有类别文本特征都变得模糊最终预测在几个类别之间抖动。解决分布偏移最直接的做法是在目标数据上微调模型。可是测试时没有标签而且很多场景不允许在用户设备上长时间训练。测试时适配的动机就在这里不依赖标签只使用测试数据本身的信息在推理过程中对模型做局部调整。常见信号包括预测熵最小化、模型输出一致性、特征分布对齐等。MuRA 的思路属于这一类目标是让适配过程既高效又有效。1.3 测试时适配要解决的两个矛盾测试时适配要同时处理两个矛盾。第一个矛盾是“参数规模与数据量”的矛盾。测试阶段通常只有几十张到几百张无标签图片远不足以训练一个大模型。如果直接更新 CLIP 全部参数很容易在小批量数据上过拟合导致模型只认识当前这批图丢掉原有知识。更稳妥的做法是冻结骨干网络只更新极少量参数。第二个矛盾是“表达能力与稳定性”的矛盾。如果只更新少量参数适配能力可能不够无法纠正较严重的分布偏移。如果引入过多可学习结构又会导致优化不稳定。MuRA 借助低秩矩阵的思想把参数更新限制在低秩子空间内同时用多个秩的组合提升表达能力让模型在测试阶段能够沿着更丰富的方向调整但不会把更新扩散到整个权重矩阵。2. MuRA 的核心思路用多秩增量逼近测试时需要的更新2.1 低秩适配是 LoRA 给我们的起点低秩适配 LoRA 在语言模型微调中已经很常见。它的核心是冻结原始权重矩阵只在旁边增加两个小矩阵用它们的乘积表示权重更新量。如果原始权重矩阵是W维度是d x kLoRA 把更新写成delta_W A B W W alpha * delta_W其中A的维度是d x rB的维度是r x kr远小于d和k。这样需要更新的参数从d * k缩小到d * r r * k参数效率非常高。训练时原始W被冻结只有A和B被优化。LoRA 让参数高效微调成为可能。MuRA 延续了同样的思路但把单一秩的更新扩展成多个秩的组合。测试时模型面对的数据分布往往不是单一方向的偏移只用一个低秩矩阵去补偿可能只能覆盖其中一部分偏移多个秩叠加可以表达更丰富的调整方向。2.2 从单秩到多秩表达能力的自然扩展多秩适配的一种常见实现是并联多个不同秩的低秩更新项最后把它们相加。可以用公式理解delta_W sum_{r in R} A_r B_r W W alpha * delta_W这里R是一个秩集合比如R {4, 8, 16}。每个秩都对应一对矩阵A_r和B_r分别负责在该秩对应的子空间内提供更新量。最终的权重增量是多个低秩更新的和而不是把某一个r直接改成 28。这样的好处是模型既获得了多个子空间的表达能力又不需要构造一个完整的稠密高秩矩阵。举例来说如果某一层维度是d k 768直接更新该层权重需要更新768 * 768 589824个参数。使用秩集合{4, 8, 16}后需要更新的参数数量大约是768 * (4 8 16) (4 8 16) * 768也就是约43008个参数。参数总量仍然远小于原始权重矩阵但相比只使用一个秩 4 的低秩适配多秩组合的表达空间更大。这里要强调一点多秩不等于“秩越高越好”。多秩的核心是让不同秩的更新项互相配合。低秩项负责主要方向的适配高秩项负责补充细节。实际调整时每个秩项都会被数据分布和损失函数共同约束而不是互相独立乱更新。2.3 多秩适配在测试时的计算开销控制测试时适配对计算开销非常敏感。模型已经在跑推理如果再叠加大量反向传播显存和延迟都会增加。多秩适配降低开销主要靠两点。第一只对选中的网络层做适配。CLIP 的视觉编码器通常由多层 Transformer block 组成全部层都接适配器效果不一定最好反而会带来显存压力。常见做法是先对最后几层做适配比如最后 3 层或最后 6 层减少参数量的同时保留深层语义信息。第二适配器只改变当前层的输出不复制整个骨干网络。前向推理时模型仍然从原始权重加载骨干参数适配器在指定层的输出上做残差叠加。反向传播时只有适配器参数产生梯度骨干参数不更新因此优化过程可以看成一个小型模型训练。用一个简单类比原始模型是一个已经学会很多视觉先验的专家测试时适配是在专家旁边放几个轻量级“调节旋钮”针对当前数据分布微调结果。多秩则是给调节旋钮增加多个维度让调整速度更快、覆盖方向更广。2.4 和“测试时训练”的区别测试时适配有时会和测试时训练混在一起。两者都使用测试数据更新模型但目标不同。测试时训练往往在训练阶段就加入一个辅助任务例如预测图像旋转角度或修复被遮挡区域。模型在训练时学会这个辅助任务测试时继续优化辅助任务损失让特征表达不断适应当前数据。测试时适配通常不要求模型在训练阶段引入额外辅助头而是直接使用模型自身输出的统计信息例如预测熵、特征一致性或模型输出分布在测试阶段优化一个自监督目标。MuRA 所属的方向更贴近“测试时适配”冻结大部分参数通过少量额外参数在测试阶段调整模型。设计时还需要避免一个问题测试时使用的批次数量往往很少适配器必须从初始状态快速收敛同时不能破坏原始模型的特征分布。3. 环境准备与最小数据集设计3.1 依赖版本建议实现一个最小验证框架不需要非常复杂的技术栈但依赖版本需要提前对齐。可以参考以下组合依赖建议版本说明Python3.10 或 3.11需要和 PyTorch 版本匹配PyTorch2.x推荐支持自动混合精度和动态图transformers4.x用于加载 CLIP 模型和 tokenizertorchvision0.x 对应 PyTorch 2.x用于数据加载和图片变换open_clip可选如果使用 open_clip 模型权重则引入实际项目中模型结构可能来自 Hugging Face也可能来自 open_clip。两套 API 不完全一致落地前要先确认自己的模型权重属于哪一套否则后面接 hook 时会遇到属性名不一致的问题。3.2 准备带偏移的测试集验证测试时适配效果最好选择本身有偏移的数据集或者自己构造偏移。CIFAR-10 比较简单适合跑通流程。先加载原始测试集再在预处理阶段加入高斯噪声模拟分布偏移。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms class AddGaussianNoise: def __init__(self, std0.1): self.std std def __call__(self, tensor): return tensor torch.randn_like(tensor) * self.std MEAN [0.48145466, 0.4578275, 0.40821073] STD [0.26862954, 0.26130258, 0.27577711] transform_with_noise transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), AddGaussianNoise(std0.1), transforms.Normalize(MEAN, STD), ]) dataset datasets.CIFAR10( root./data, trainFalse, downloadTrue, transformtransform_with_noise, ) dataloader DataLoader(dataset, batch_size32, shuffleFalse, num_workers2)这段代码的核心变化是AddGaussianNoise。增加噪声后图片的局部纹理被破坏CLIP 图像编码器提取的特征会和干净数据产生偏移。这样可以人为制造一个“测试时分布变化”的场景。需要注意CIFAR-10 的原始分辨率只有 32 x 32Resize 到 224 后图片本身已经比较糊再加入高斯噪声难度会明显增加。如果发现准确率下降太多可以减小std例如改成0.05。3.3 定义评测指标测试时适配不能只盯一个指标。至少应该记录以下内容指标含义观察重点top-1 accuracy分类准确率适配后是否比零样本基线高average entropy预测概率的平均熵适配过程中是否逐步下降mean confidence最大概率均值模型是否变得更加确定stable rate适配前后预测一致比例判断模型是否被破坏其中average entropy是最常用的优化目标同时也是监控指标。如果熵快速下降到异常低的值说明模型可能在小批量数据上过拟合了。4. 最小实验把多秩适配放进 CLIP 推理链路4.1 整体流程下面是完整的最小实验流程加载 CLIP 模型和 tokenizer冻结全部原始参数。对测试类别生成文本特征。在视觉编码器的部分层输出后插入多秩适配器。计算零样本分类准确率作为基线。在测试数据上分批前向用熵最小化损失更新适配器。再用更新后的模型计算准确率。整体设计最重要的原则是原始模型参数不动适配器参数是唯一可学习对象。这样即使在测试数据上多跑几轮也不会把整个模型带偏。4.2 实现 MultiRankAdapter先实现一个多秩适配器模块。这里的实现用于说明思路实际项目需要根据模型结构和张量形状调整。import torch import torch.nn as nn class MultiRankAdapter(nn.Module): def __init__(self, d_in, d_out, ranks(4, 8, 16), alpha1.0): super().__init__() self.ranks tuple(ranks) self.alpha alpha self.loras nn.ModuleList() for r in self.ranks: A nn.Parameter(torch.zeros(d_in, r)) B nn.Parameter(torch.zeros(r, d_out)) nn.init.kaiming_uniform_(A, a5 ** 0.5) self.loras.append(nn.ParameterDict({A: A, B: B})) def forward(self, x): delta 0.0 for lora in self.loras: delta delta (x lora[A] lora[B]) return x self.alpha * delta初始化时把B置为零因此初始状态下适配器不会改变模型输出。这个细节很重要。如果初始状态就改变原始输出零样本基线的意义就被破坏了。A使用 Kaiming 初始化是为了保留非零梯度让训练开始后参数能够快速更新。前向逻辑也很简单输入x进入适配器后分别与多个A_r、B_r相乘产生多个低秩增量最后把增量相加并乘上缩放系数alpha再加回原始输入。如果输入形状是[batch_size, sequence_length, hidden_size]矩阵乘法可以自动作用在最后一维上。4.3 把适配器接到 CLIP 上在 Hugging Face Transformers 的 CLIP 模型中视觉编码器通常是多层 Transformer。为了修改规模可控可以只对最后若干层的 MLP 输出做适配。下面代码使用 PyTorch forward hook 修改指定层输出比直接替换整个模型类更容易调试。def attach_adapters(model, hidden_size, ranks(4, 8, 16), num_layers6): adapters nn.ModuleList() encoder_layers model.vision_model.encoder.layers selected_layers encoder_layers[-num_layers:] for layer in selected_layers: adapter MultiRankAdapter(hidden_size, hidden_size, ranksranks) original_mlp_forward layer.mlp.forward def make_hooked_forward(adapter, original_forward): def hooked_forward(x, *args, **kwargs): out original_forward(x, *args, **kwargs) return adapter(out) return hooked_forward layer.mlp.forward make_hooked_forward(adapter, original_mlp_forward) adapters.append(adapter) return adapters使用前要确认模型的hidden_size与 CLIP 配置一致。常见 CLIP ViT-B/32 的 hidden size 是 768但使用不同模型时一定要打印配置确认。在 PyTorch 中直接给模块实例的forward属性赋函数是可以生效的因为Module.__call__最后会调用forward。不过如果你使用的 Transformers 版本比较特殊更稳妥的办法是使用register_forward_hook在 hook 中返回修改后的输出。选择最后一层到倒数第六层而不是全部层是为了让适配器只影响高层语义保留底层纹理和结构特征。这样在多轮测试时适配中模型不会完全忘掉原始视觉表达能力。4.4 测试时自监督优化循环适配器的优化目标使用预测熵最小化。给定一个批次图片模型先计算每个类别的预测概率熵越低说明模型对类别的判断越集中测试时适配希望强化这种集中判断。import torch.nn.functional as F def freeze_model_parameters(model): for param in model.parameters(): param.requires_grad False def run_test_time_adaptation( model, adapters, text_features, dataloader, optimizer, device, max_steps50, ): model.eval() model.to(device) for param in model.parameters(): param.requires_grad False for adapter in adapters: adapter.to(device) for param in adapter.parameters(): param.requires_grad True step 0 for images, _ in dataloader: if step max_steps: break images images.to(device) optimizer.zero_grad() image_features model.get_image_features(images) image_features F.normalize(image_features, dim-1) logits image_features text_features.T * model.logit_scale.exp() probs F.softmax(logits, dim-1) loss -(probs * probs.log()).sum(dim-1).mean() loss.backward() optimizer.step() step 1 print(f[step {step}] loss: {loss.item():.4f})需要注意的是model.eval()和requires_grad本身不冲突。CLIP 模型多数层是 LayerNorm 和线性层不依赖 batch 统计量因此eval模式不会影响梯度回传。适配器参数在requires_gradTrue的状态下依然可以正常反向传播。text_features需要在适配前提前计算并做 L2 归一化。这样可以避免每轮都重新计算文本特征减少测试时开销。5. 关键参数与实验矩阵5.1 参数速查表多秩适配涉及的关键参数不算多但每个参数都直接影响效果。下面表格列出常见取值和影响参数常见取值含义过小的影响过大的影响ranks(4, 8, 16)参与计算的秩集合表达能力不足偏移纠正不彻底参数变多容易在小数据上过拟合alpha1.0增量缩放系数更新幅度不足可能破坏原始特征learning rate1e-3 到 5e-4适配器更新步长收敛慢效果不明显梯度震荡损失爆掉batch_size16 到 32每个批次图片数熵估计不稳定显存占用过大max_steps50 到 100最多更新步数适配不充分过拟合测试集num_layers3 到 6参与适配的层数适配范围不够计算开销过高optimizerAdamW优化器收敛慢学习率敏感需要配合 warmup这里特别说明alpha。它相当于一个“安全阀”。即使梯度计算出很大的增量只要alpha控制在一定范围原始模型的输出不会被瞬间改变。测试时适配通常无法像训练阶段那样充分调参因此alpha建议从 1.0 或 0.5 开始观察损失曲线后再调整。5.2 一个可以跑的实验矩阵为了验证多秩适配是否有效至少应该做几组对照实验实验组配置要回答的问题A零样本 CLIP不加任何适配器基线准确率是多少B单秩适配例如 rank8单秩方向是否已经有效果C多秩适配例如 ranks(4, 8, 16)多秩是否比单秩更稳定D多秩适配增加 max_steps 到 100更多更新步数是否带来收益每组实验记录零样本准确率和适配后准确率。如果 C 组和 B 组没有明显差距先不要急着加更多秩而是检查数据偏移程度和适配层选择。如果 C 组在偏移数据上更稳定说明多秩组合确实在起作用。5.3 为什么不要直接更新骨干网络测试时直接更新骨干网络参数虽然方法实现更简单但几个问题很难避免。第一显存占用成倍上升。更新骨干参数意味着反向传播需要保存每一层激活值原本文本编码器只需要一次前向视觉编码器反向传播会带来额外的显存压力。第二小数据过拟合风险高。测试阶段可用的无标签数据非常有限更新整个骨干网络很容易把模型权重拉向当前批次的特征分布导致模型在下一个批次上反而变差。第三原始知识容易被破坏。CLIP 模型已经在海量图文对上完成了对比学习骨干网络承载了大量通用视觉知识。测试时适配只是临时纠正分布偏移更新幅度应该尽量小多秩适配器的低秩结构和缩放系数正是为了实现这种“局部修正”。6. 运行验证从日志和曲线里判断适配是否有效6.1 预期输出跑通流程后控制台日志大致长这样zero-shot top1: 75.40 [step 1] loss: 3.1245 [step 2] loss: 3.0101 [step 10] loss: 2.5123 ... after adaptation top1: 78.10这不是一个保证值而是日志格式示例。不同数据集、不同噪声强度、不同模型权重结果会有差异。如果原始零样本精度已经很高适配后准确率可能不升反降因为模型在干净数据上几乎不需要调整。更加准确的判断方式是记录损失曲线。如果每一轮的熵都在下降说明适配器确实在学到东西。如果损失从第一步就剧烈震荡甚至上升需要检查学习率和alpha是否设置过大。6.2 判断方法有效的三条证据只报告一个准确率提升不够还要看以下三条证据第一平均熵下降。说明模型面对当前数据分布时预测分布更集中而不是在多个类别之间摇摆。第二预测在偏移数据上的准确率提升同时在干净数据上不显著下降。测试时适配不应该牺牲模型原有的通用能力。第三适配器增量的范数可控。可以打印每个适配器参数的A B的 Frobenius 范数如果范数远大于原始权重范数说明模型被改动过大并不安全。6.3 如果指标没有提升先看哪一块指标没提升时按照这个顺序排查确认适配器是否真的接入了模型可以打印model.vision_model.encoder.layers[-1].mlp.forward的类型确认是否被替换。确认只有适配器参数requires_gradTrue骨干参数为False。确认输入图片的 resize 和归一化是否和 CLIP 模型要求一致。打印梯度范数如果梯度为 0检查B参数是否为全零且没有梯度。降低学习率并增大alpha观察损失是否能够下降。7. 常见问题排查7.1 问题、原因与处理问题现象可能原因检查方式处理建议适配后准确率比零样本还低学习率过大或 max_steps 过多查看损失是否异常下降调低学习率减少更新步数降低 alpha损失不变适配器没有接到前向链路打印 hook 是否被调用检查 layer.mlp.forward 是否被正确替换损失变成 NaN学习率过大或梯度爆炸检查梯度范数降低学习率增加梯度裁剪显存溢出适配层数过多或 batch 过大查看显存占用只适配最后 3 到 6 层减小 batch适配器初始输出不为原模型输出初始化状态不正确检查 B 是否为全零初始化 B 为零alpha 设为 1.0只用最后几层效果不明显偏移位置在浅层打印各层输出差异尝试适配更多层或调整秩集合这里最常见的问题是“适配器看起来接上了实际没有生效”。原因是 PyTorch 的 forward hook 写法或者自定义 forward 函数的作用范围没有覆盖到真正执行的方法。排查思路是打印每一层的输出是否发生变化不要只依赖总损失。7.2 分步排查链路遇到问题时可以按下面的路径操作先固定一个很小的实验集比如 32 张图片。关闭适配器计算零样本准确率和平均熵作为基线。打开适配器只跑一个 batch打印 loss、梯度范数、适配器输出范数。确认 loss 从基线开始下降且梯度不是零。逐步增加 batch 数量和 max_steps观察指标变化趋势。这条排查链路把“模型是否训练”和“方法是否有效”分开。如果模型没训练方法再好也看不到效果如果模型训练了但准确率不提升才需要回到方法本身调整秩和层数。8. 从最小实现走向论文级方法工程化与扩展8.1 多秩选择的动态化固定秩集合(4, 8, 16)是实现多秩适配的起点。更灵活的方式是让模型根据测试数据的偏移程度自动确定每一层需要的秩。可能的扩展方向包括为每个秩学习一个权重让模型决定哪些低秩方向更重要。在不同层使用不同的秩集合浅层用低秩深层用高秩。在适配过程中动态裁剪不重要的秩方向减少计算量。这些方向都还没有统一的工业标准实现需要结合具体数据验证。但从工程角度看先跑通固定秩集合再引入动态选择是更稳妥的路径。8.2 参数共享与内存控制测试时适配的显存和延迟优化非常重要。如果项目不想引入大计算量可以尝试以下方法优化手段做法收益减少适配层数只适配最后 3 层降低显存和延迟使用梯度检查点不保存所有激活值反向传播时重新计算降低显存增加少量计算缓存文本特征提前计算并归一化 text_features避免每步重复计算半精度训练把适配器参数和输入转为 fp16降低显存占用只保留最佳适配器在验证集上选择熵最低的检查点提高稳定性在显存受限场景下参数共享也是一种思路。多个层可以复用同一个中间投影矩阵让不同层之间的低秩方向共享部分信息。但这种做法可能会限制表达能力需要做实验衡量。8.3 生产环境落地的注意事项学习环境里跑通最小实验后生产环境落地还需要额外考虑几个问题。首先是回滚机制。测试时适配是动态过程如果某一批数据分布特别异常适配器可能被带偏。生产环境应保存初始零样本模型快照持续监控平均熵。如果连续多个 batch 的熵不降反升就停止适配并回滚到原始模型。其次是监控指标。除了准确率还要记录适配前后预测一致率。如果适配后大量样本的预测发生了翻转需要人工抽检翻转是否合理防止模型为了降低熵而牺牲原有正确判断。最后是输入预处理的一致性。离线验证时使用的高斯噪声、resize 方式、归一化参数必须上线时保持一致。很多测试时适配失效的问题最终都指向训练和推理预处理不一致。8.4 可复用的 TTA 实践清单实际开发中可以把这个清单作为测试时适配的通用检查表[ ] 是否冻结骨干网络参数只更新少量适配参数[ ] 是否使用零初始化增量保证适配前模型行为与原始模型一致[ ] 是否选择了合理的目标层最好从最后几层开始实验[ ] 是否使用多秩组合而不是盲目增大单一秩[ ] 是否记录零样本基线、平均熵、top-1 准确率[ ] 是否设置学习率、alpha、max_steps 的搜索范围[ ] 是否检查梯度范数防止梯度为 0 或梯度爆炸[ ] 是否保留原始模型快照支持回滚[ ] 是否验证适配后模型在干净数据上没有明显掉点多秩测试时适配并不是一个复杂到无法落地的研究方向。它的核心判断是不要试图在测试阶段改变整个模型而是用多个低秩更新组合出一个安全的修正方向。对视觉语言模型而言这种修正既能应对分布偏移又不会破坏预训练阶段积累的通用知识。从最小实现开始跑通链路再逐步扩展参数选择、层选择和动态秩机制是比较适合工程实践的学习路径。
返回列表