LLaMA-Adapter - 详解
论文标题: LLaMA-Adapter: Efficient Fine-tuning of Language Models with Zero-init Attention
arXiv 地址: https://arxiv.org/abs/2303.16199
开源代码库: https://github.com/ZrrSkywalker/LLaMA-Adapter
原理
将指令信息作为可学习的提示(Learnable Prompt)注入到LLaMA的高层特征中,从而引导模型生成符合指令的回复。就是LLaMA-Adapter的核心思想
它主要包括两个关键技术点:
- 可学习的适配器参数(Learnable Adaption Prompts)
在Transformer块的输入词序列最前面,插入一组可学习的提示向量(图中绿色部分)。这组向量可以理解为用于承载指令语义的“密码”或“密钥”。
这些提示向量是随机初始化并通过训练来学习的。它们的长度(例如10个token)远小于原始文本序列,极大减少了参数量。 - 零初始化注意力(Zero-initialized Attention)
这是论文的一个关键创新点,用于保证训练稳定性。
在训练初期,我们希望冻结的LLaMA模型不受随机初始化的适配器参数的影响。理想情况下,模型最初的输出应该与未微调的原始LLaMA一样。
为了实现这一点,他们在插入适配器提示的注意力层上,使用了一个零初始化的门控机制(gating factor)。
具体而言,注意力输出被计算为: Attention_output = gating_factor * (Attention_with_Adapter) + (1 - gating_factor) * (Attention_original) ,其中 gating_factor 初始为0。
这样,在训练开始时,适配器提示的贡献为0,模型行为与原始LLaMA完全一致。随着训练进行, gating_factor 逐渐学习并增大,适配器提示开始管用地引导模型生成。这避免了训练初期不稳定的噪声,大大提高了微调的效率和效果。
对比
LLaMA
首先,我们要明白语言模型(LLaMA)的基本操作。它的输入和输出都是数字。
Token化(文本 -> 数字):
假设大家的词表很小,只有几个词:
“<用户>”:10
“总结”:11
“你好”:12
“世界”:13
“”:14
用户输入 “总结:你好世界” ,被转换成数字序列: [11, 12, 13]
模型处理:模型会为每个位置生成一个“特征向量”。为了简单,假设特征向量维度只有3。
词嵌入:[11, 12, 13] -> 嵌入后可能变成 [[0.1, 0.2, 0.3], [1.1, 1.2, 1.3], [2.1, 2.2, 2.3]]就是初始输入
模型经过层层计算,最终输出每个位置的下一个词预测概率。
在位置3( [2.1, 2.2, 2.3] ),模型可能计算出下一个词是 “。” 的概率最高。这显然不是我们想要的总结任务。
LLaMA-Adapter
现在,我们启用LLaMA-Adapter。我们设定可学习的提示向量(密码)长度为2,维度为3。
第一步:初始化“密码”和“阀门”
adapt_prompts (密码手势):随机初始化,比如 [[0.9, -0.5, 0.2], [0.4, 0.1, -0.3]]
gating_factor (阀门):初始化为0! 这是“零初始化注意力”的精髓。第二步:修改模型输入(插入密码)
原始输入: x = [[0.1, 0.2, 0.3], [1.1, 1.2, 1.3], [2.1, 2.2, 2.3]]
插入适配器提示后,新输入 变成:
x_new = [ [0.9, -0.5, 0.2], [0.4, 0.1, -0.3], [0.1, 0.2, 0.3], [1.1, 1.2, 1.3], [2.1, 2.2, 2.3] ]
[0.9, -0.5, 0.2] 和 [0.4, 0.1, -0.3] 就是我们可学习的密码。
第三步:计算注意力并应用“阀门”
假设经过LLaMA的注意力层计算后,原输入位置 [2.1, 2.2, 2.3] 的输出是 [5.0, 5.0, 5.0] 。
由于我们插入了提示,注意力计算会考虑所有位置(包括提示),该位置的输出变成了 [8.0, 8.0, 8.0] 。这个变化是巨大的,可能破坏模型原本的能力。
关键一步:应用零初始化阀门
final_output = gating_factor * [8.0, 8.0, 8.0] + (1 - gating_factor) * [5.0, 5.0, 5.0]
训练刚开始时, gating_factor = 0 :
final_output = 0 * [8.0,8.0,8.0] + 1 * [5.0,5.0,5.0] = [5.0, 5.0, 5.0]
看到了吗?最终输出和原始LLaMA一模一样! 随机初始化的密码在开始时完全不起作用,保证了训练稳定性。第四步:训练与更新
我们开始训练。计算损失,比如模型在位置3应该输出 “结束” 这个词,但它输出了 “。” ,所以产生了损失。
反向传播时,LLaMA的巨量参数被冻结,梯度无法计算,因此它们纹丝不动。
只有 adapt_prompts 和 gating_factor 会收到梯度并更新。
经过多次迭代, adapt_prompts 被调优成一组实用的密码, gating_factor 也可能从0慢慢增长到0.5、0.8…
此时, final_output = 0.8 * [8.0,8.0,8.0] + 0.2 * [5.0,5.0,5.0] = [7.4, 7.4, 7.4]
“。”。这就使得"密码"生效了。就是新的输出 [7.4, 7.4, 7.4] 使得模型能更准确地预测出“结束”而不