在序列建模任务中,防止模型"偷看"未来信息是至关重要的。想象一下你在教小孩造句:如果允许他们先看完整句话再写第一个词,那这个练习就失去了意义。因果注意力正是通过掩码技术实现了这种时序约束。
具体实现上,我们使用一个下三角矩阵(对角线及以上为1,其余为0)作为掩码。这个掩码会与注意力分数矩阵进行逐元素相乘(或相加负无穷大),使得softmax计算时未来位置的权重趋近于零。PyTorch中的实现示例如下:
python复制def causal_mask(size):
return torch.tril(torch.ones(size, size)) == 1
注意:在实现时要注意矩阵维度的对齐问题。如果使用FP16训练,负无穷大值建议取-1e4而非-torch.inf,避免出现NaN问题。
设未掩码的注意力分数矩阵为S ∈ R^(n×n),掩码矩阵M ∈ {0,-∞}^(n×n)定义为:
M_ij = { 0 if i ≥ j
{ -∞ if i < j
掩码后的注意力权重计算为:
A = softmax((QK^T)/√d + M)
其中d是key的维度。这个√d的缩放因子非常重要,我们将在第4章详细解释其作用。
有趣的是,虽然训练时使用掩码,但模型在推理阶段其实不需要显式应用掩码——因为推理本就是逐个生成token的。这种设计保证了训练和推理行为的一致性,是Transformer架构的精妙之处。
在Transformer中,Dropout主要在两个位置使用:
第一种方式更直接作用于注意力模式本身,能强制模型不依赖固定的注意力路径。实验表明,在大型模型中,注意力Dropout率通常设为0.1-0.3效果最佳。
python复制attention_weights = F.softmax(scores, dim=-1)
attention_weights = F.dropout(attention_weights, p=dropout_prob)
context = torch.matmul(attention_weights, V)
重要技巧:确保在eval模式下关闭Dropout,否则会导致不可预期的性能下降。有些框架在实现时容易忽略这一点。
实践中发现,当同时使用注意力Dropout和标签平滑(Label Smoothing)时,需要适当降低Dropout率(约0.1-0.15)。这是因为两者都是正则化手段,过度使用会导致模型欠拟合。
多头注意力的核心思想类似于CNN中的多通道卷积。每个头学习不同的注意力模式,例如:
实验表明,在大型模型中,不同头确实会自发地专业化到不同的注意力模式。
python复制# 为每个头单独定义参数
self.Wq = [nn.Linear(d_model, d_k) for _ in range(n_heads)]
self.Wk = [nn.Linear(d_model, d_k) for _ in range(n_heads)]
self.Wv = [nn.Linear(d_model, d_v) for _ in range(n_heads)]
优点:各头独立性更强
缺点:参数量随头数线性增长
python复制# 统一大矩阵后再分割
self.Wq = nn.Linear(d_model, n_heads * d_k)
self.Wk = nn.Linear(d_model, n_heads * d_k)
self.Wv = nn.Linear(d_model, n_heads * d_v)
优点:实现简洁,内存效率高
缺点:需要更谨慎的参数初始化
设模型维度为d_model,头数为h,则每个头的维度d_head应满足:
d_head = d_model / h
这个设计确保了多头拼接后的总维度与原始维度一致。例如在BERT-base中:
d_model=768, h=12 → d_head=64
经验法则:d_head最好不小于64,否则会影响单头的表达能力。
假设Q,K ∈ R^(n×d),其点积矩阵元素的理论方差为:
Var(q·k) = d * Var(q) * Var(k)
当d很大时(如d=1024),点积的绝对值会变得很大,导致softmax输出接近one-hot分布。此时梯度会变得极小,严重影响训练效率。
通过引入1/√d的缩放因子,我们将方差控制为:
Var((q·k)/√d) = Var(q) * Var(k)
这确保了无论d多大,梯度都能保持在一个合理的范围内。数学推导如下:
设q,k的每个元素是i.i.d.随机变量,E[q_i]=E[k_i]=0,则:
E[(q·k)^2] = E[(∑q_i k_i)^2] = ∑E[q_i^2]E[k_i^2] = d * σ_q^2 * σ_k^2
在LLaMA-2的训练过程中发现:
可以将注意力机制理解为一种特殊的信息检索系统:
这种分离设计(计算注意力用K,实际输出用V)是注意力机制灵活性的关键。
以句子"I love natural language processing"为例:
| 词元 | 可能的键特征 | 可能的值特征 |
|---|---|---|
| I | 主语, 代词 | 说话者身份 |
| love | 动词, 情感 | 积极情感 |
| NLP | 宾语, 专业术语 | 技术领域信息 |
在实践中发现:
python复制def causal_attention_mask(batch_size, seq_len):
return torch.tril(torch.ones(seq_len, seq_len))
.expand(batch_size, 1, seq_len, seq_len)
性能技巧:预先分配大尺寸掩码并在训练中重复使用,比每个batch新建掩码快约40%。
python复制def stable_softmax(x):
x = x - x.max(dim=-1, keepdim=True).values
return torch.exp(x) / torch.exp(x).sum(dim=-1, keepdim=True)
这个实现避免了数值溢出问题,特别是当输入中存在极大正值时。
python复制# 将合并的大矩阵分割为多头
q = q.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2)
k = k.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2)
v = v.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2)
内存布局提示:transpose操作会导致非连续内存,后续操作前建议调用.contiguous()。
当使用FP16训练时:
对于长序列(>2048):
现象:所有注意力权重接近1/n
可能原因:
现象:损失突然上升,注意力模式崩溃
可能原因:
现象:推理结果与训练差距大
可能原因: