在自然语言处理领域,Transformer架构已经成为事实上的标准。大多数人使用现成的Transformer模型时,往往只关注输入文本和输出结果,而忽略了其中关键的注意力掩码(attention mask)机制。实际上,这个看似简单的组件在模型运行过程中扮演着至关重要的角色。
标准的注意力掩码是一个二维张量,形状为[batch_size, sequence_length],由1和0组成。1表示对应位置的token应该被关注,0则表示应该被忽略。这种设计最初是为了处理变长输入序列——通过将短序列后面补零,然后使用掩码告诉模型哪些位置是真实的token,哪些是填充的padding。
重要提示:即使你不显式提供attention_mask参数,大多数Transformer模型的tokenizer在预处理文本时也会自动生成这个二维掩码。这就是为什么很多基础应用中我们可以直接忽略这个参数。
然而,在模型内部,这个二维掩码会经历一个关键的维度扩展过程。模型会将2D掩码转换为4D形式,形状变为[batch_size, num_heads, query_length, key_length]。这种转换使得每个注意力头都可以有自己的注意力模式,为更复杂的注意力机制提供了可能。
让我们深入理解这个维度扩展的具体过程。假设我们有一个batch大小为2,序列长度为4的输入,其初始的2D掩码可能如下:
python复制[[1, 1, 1, 0],
[1, 1, 0, 0]]
在模型内部,这个掩码会经历以下变换步骤:
这种转换使得每个注意力头都可以有不同的注意力模式。例如,在因果解码(causal decoding)场景中,模型会使用下三角矩阵形式的掩码,确保每个token只能关注自身及之前的token:
python复制tensor([[[[1, 0, 0, 0],
[1, 1, 0, 0],
[1, 1, 1, 0],
[1, 1, 1, 1]]]])
最近合并的一个重要PR(Pull Request)使得开发者可以直接向Transformer的forward()方法传递自定义的4D掩码,这带来了几个关键优势:
技术细节:当使用自定义4D掩码时,通常还需要提供position_ids参数,形状为[batch_size, sequence_length]。这是因为非标准的注意力模式可能需要调整位置编码。不是所有模型都原生支持这个参数,像Llama这样的现代架构通常支持,而其他模型可能需要修改。
传统束搜索在处理多个候选序列时会消耗大量内存,因为每个候选序列都是独立处理的。使用4D掩码,我们可以将多个候选序列打包成一个序列,通过精心设计的掩码控制注意力流。
具体实现步骤:
这种方法源自SpecInfer论文,可以实现约2倍的内存节省,随着束宽和序列长度的增加,节省效果会更明显。
在监督微调阶段,处理不同长度序列的标准做法是用EOS(序列结束)token分隔它们。但这不能阻止序列间的交叉注意力。使用4D掩码可以:
这种方法特别适合处理大量短序列的场景,可以大幅提升训练效率。
前瞻解码是一种新颖的解码策略,它打破了传统自回归模型的严格顺序依赖。通过4D掩码,我们可以:
相比之前需要修改模型forward方法的实现,现在仅通过设计合适的4D掩码就能实现,大大降低了实现复杂度。
要在自己的项目中使用4D掩码,需要注意以下几个关键点:
以下是一个简单的实现示例:
python复制import torch
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("llama-2-7b")
# 假设我们已经有了tokenized输入
input_ids = torch.tensor([[1, 2, 3, 4]])
# 创建自定义4D掩码
attention_mask = torch.zeros(1, model.config.num_attention_heads, 4, 4)
attention_mask[:, :, :, :2] = 1 # 只关注前两个位置
# 提供位置ID
position_ids = torch.arange(4).unsqueeze(0)
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids
)
4D掩码支持只是Transformer灵活性演进的一个开始。我们可以预见以下几个发展方向:
在实际项目中采用4D掩码时,我建议从小规模实验开始,逐步验证掩码设计的正确性。一个实用的调试技巧是可视化注意力权重,确保掩码产生了预期的注意力模式。