1. 从“平方”到“线性”:注意力机制的一次效率革命
如果你最近在关注大模型或者序列建模的前沿动态,大概率会频繁听到“线性注意力”这个词。它不像Transformer刚出来时那样石破天惊,更像是一个精明的“优化工程师”,在大家被Transformer那O(N²)的计算复杂度折磨得焦头烂额时,它站出来说:“嘿,也许我们不必算那么多。” 我最初接触这个概念,是在尝试将一个文本分类模型部署到资源受限的边缘设备上时,原始的Transformer自注意力层成了性能瓶颈,内存和计算时间都吃不消。于是,我开始深入研究各种高效的注意力变体,线性注意力便是其中最具理论美感和实用潜力的一种。它要解决的,正是让注意力机制在处理长序列时,也能保持“线性”的计算和内存开销,这对于处理长文档、高分辨率图像、甚至基因序列等场景至关重要。无论你是研究者、工程师,还是对模型底层优化感兴趣的学习者,理解线性注意力,都意味着你掌握了打开高效序列建模大门的一把关键钥匙。
2. 核心思路拆解:注意力机制的“软肋”与线性化的曙光
要理解线性注意力为何重要,我们必须先回到问题的原点:标准Transformer中的自注意力机制到底“贵”在哪里。
2.1 标准点积注意力的计算瓶颈
标准的多头自注意力公式,对于单个头,可以简化为:Attention(Q, K, V) = softmax(QK^T / √d_k) V这里,Q,K,V是查询、键、值矩阵,形状通常为[序列长度N, 特征维度d]。
计算瓶颈就隐藏在QK^T这一步。它的结果是一个[N, N]的矩阵,我们称之为注意力分数矩阵(或相似度矩阵)。这个矩阵的每个元素,都代表了序列中一个位置与另一个位置的相关性。计算它需要O(N² * d)的时间复杂度,而存储这个矩阵需要O(N²)的内存空间。
这就是著名的“平方复杂度”问题。当序列长度N从100增长到1000时,计算量和内存消耗理论上会增加100倍。在实际应用中,这直接限制了模型能够处理的上下文长度。比如,早期的GPT-3虽然参数量巨大,但其上下文窗口也受此制约。处理一本书、一部电影的所有帧、或长时间的传感器数据,这种开销变得难以承受。
注意:这里常有一个误解,认为复杂度是
O(N² * d²)。实际上,Q是[N, d],K^T是[d, N],两者相乘得到[N, N],每一次点积计算涉及d次乘加运算,所以总计算量是N * N * d = N²d。特征维度d通常固定且远小于N,因此主导项是N²。
2.2 线性注意力的核心思想:分解与重组
线性注意力的目标非常明确:避免显式地计算和存储那个N×N的注意力矩阵。它的核心洞察在于,对标准注意力公式进行巧妙的数学变换。
我们仔细观察标准注意力:Output = softmax(QK^T) V。如果我们把softmax展开,对于输出序列中第i个位置的向量,其计算是:Output_i = Σ_j (exp(q_i·k_j) / Σ_l exp(q_i·k_l)) * v_j这里,q_i,k_j,v_j分别是第i个位置的查询向量、第j个位置的键向量和值向量。
这个计算是“一对多”的:为了得到Output_i,我们需要用q_i和序列中所有的k_j计算相似度,然后加权求和所有的v_j。这天然就是O(N)的复杂度(对于每个i)。问题在于,我们有N个i,所以总复杂度是O(N²)。
线性注意力的思路是:能否将q_i和k_j的交互,拆解成各自独立映射后的聚合?换句话说,我们寻找一个特征映射函数φ(·),使得点积相似度可以表示为映射后向量的点积:sim(q_i, k_j) = φ(q_i)·φ(k_j)那么,注意力输出就可以重写为:Output_i = (Σ_j φ(k_j) ⊗ v_j^T) · φ(q_i) / (Σ_j φ(k_j) · φ(q_i))(这里⊗表示外积,但更常见的推导会简化)
经过一系列推导(具体过程在下一节展开),我们可以得到一个关键形式:Output_i = (Σ_{j=1}^{N} φ(k_j) v_j^T) φ(q_i) / (Σ_{j=1}^{N} φ(k_j) · φ(q_i))
看这个公式的精妙之处:括号内的部分Σ_{j=1}^{N} φ(k_j) v_j^T和Σ_{j=1}^{N} φ(k_j)与位置i无关!它们可以看作是整个序列的“全局状态”或“记忆”。在计算时,我们可以先扫描一遍整个序列,将这些聚合状态计算出来并缓存。然后,对于每一个位置i,我们只需要用缓存的聚合状态与φ(q_i)做一次计算即可得到输出。这样,计算复杂度就从O(N²d)降为了O(Nd²)(如果映射维度与d相当),更重要的是,序列维度N的复杂度从平方降为了线性——我们只需要对序列进行一次前向扫描和一次反向扫描(在训练时)。
2.3 不同线性注意力变体的设计哲学
既然核心是找到合适的特征映射φ(·),那么不同的线性注意力机制,本质上就是对这个映射函数的不同设计。每种设计都在表达能力、计算效率和数值稳定性之间进行权衡。
基于核函数的近似(如Linear Transformer, Performer):这类方法将softmax中的指数函数
exp(q·k)看作一个核函数,即exp(q·k) = <φ(q), φ(k)>。通过寻找显式的、有限维度的φ(·)来近似这个核。例如,使用随机傅里叶特征(RFF)或者正随机特征(Positive Random Features)。它的优势是理论上有明确的近似误差界,但映射后的维度可能较高,影响实际效率。启发式相似度函数(如Linformer, Nyströmformer):这类方法不严格追求数学上的核近似,而是直接设计低秩的注意力矩阵结构。例如,Linformer假设注意力矩阵是低秩的,直接通过两个可学习的投影矩阵将K和V从
[N, d]投影到[k, d](k是一个远小于N的常数),从而在计算Q(K^T)时,中间矩阵变为[N, k],实现线性复杂度。它更像是一种工程上的有效压缩。递归形式与状态空间模型(如RWKV, RetNet):这类方法将注意力计算转化为递归形式。它们通常维护一个随时间步更新的“状态”向量,当前时刻的输出由当前输入和上一时刻的状态决定。这天然就是
O(N)的,并且非常适合并行训练(通过并行扫描算法)。这类模型与线性注意力思想一脉相承,但更侧重于序列的递归归纳偏置。
在实际选择时,如果你的场景非常强调对标准注意力的近似保真度(例如在微调预训练Transformer时不想丢失太多性能),基于核函数的方法可能更合适。如果你需要极致的推理速度和内存节省,并且可以接受从头训练,递归形式或启发式低秩方法可能更直接。
3. 从公式到代码:手撕一个线性注意力层
理论说得再多,不如动手实现一遍来得深刻。这里我们以实现一个基于“核函数”思想的经典线性注意力层为例,使用PyTorch框架,并详细解释每一步的意图和细节。
3.1 特征映射函数φ(x)的设计
我们选择一种简单有效的映射:φ(x) = elu(x) + 1。这里elu是指数线性单元激活函数。为什么这么选?
elu(x) + 1对于所有实数x都是正的,这有助于模拟softmax产生的正注意力权重分布。- 它计算简单,没有引入额外的复杂操作。
elu函数在负半区有饱和,可以提供一定的非线性。
当然,你也可以尝试relu(x) + 1或者更复杂的映射。Performer论文中使用的“正随机特征”映射是另一种理论保障更强的选择,但实现稍复杂。
import torch import torch.nn as nn import torch.nn.functional as F def phi(x): """特征映射函数:elu(x) + 1""" return F.elu(x) + 1.03.2 线性注意力层的前向传播实现
线性注意力层的核心是计算两个聚合状态:S = Σ φ(k_j) v_j^T和Z = Σ φ(k_j)。在训练时,我们可以利用矩阵乘法高效地并行计算这些聚合。
class LinearAttention(nn.Module): def __init__(self, d_model, n_heads, dropout=0.0): super().__init__() assert d_model % n_heads == 0, "d_model must be divisible by n_heads" self.d_model = d_model self.n_heads = n_heads self.d_k = d_model // n_heads # 用于生成Q, K, V的线性投影 self.w_q = nn.Linear(d_model, d_model) self.w_k = nn.Linear(d_model, d_model) self.w_v = nn.Linear(d_model, d_model) self.w_o = nn.Linear(d_model, d_model) # 输出投影 self.dropout = nn.Dropout(dropout) def forward(self, x, mask=None): """ x: 输入张量,形状为 [batch_size, seq_len, d_model] mask: 可选,填充mask,形状为 [batch_size, 1, 1, seq_len] 或 [batch_size, seq_len] """ batch_size, seq_len, _ = x.shape # 1. 投影得到Q, K, V,并重塑为多头 Q = self.w_q(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2) # [B, H, L, d_k] K = self.w_k(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2) V = self.w_v(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2) # 2. 应用特征映射函数 φ 到 Q 和 K Q_prime = phi(Q) # [B, H, L, d_k] K_prime = phi(K) # [B, H, L, d_k] # 3. 处理mask(如果提供) if mask is not None: # 确保mask形状为 [B, 1, 1, L] 以便广播 if mask.dim() == 3: mask = mask.unsqueeze(1) # 假设mask是[B, 1, L],扩充头维度 elif mask.dim() == 2: mask = mask.view(batch_size, 1, 1, seq_len) # 将mask中为0的位置(填充位)对应的K_prime置零,使其不参与聚合 K_prime = K_prime * mask V = V * mask # 注意:这里简化处理,更严谨的做法需要同时考虑聚合分母Z的mask # 4. 计算聚合状态 S 和 Z # S = Σ_{j} (φ(K_j) * V_j^T), 这里利用爱因斯坦求和约定高效计算 # 我们想计算对于每个头、每个batch,一个 [d_k, d_k] 的矩阵吗?不完全是。 # 回顾公式:Output_i = (Σ φ(k_j) v_j^T) φ(q_i) / (Σ φ(k_j) · φ(q_i)) # 其中 Σ φ(k_j) v_j^T 是一个 d_k x d_k 的矩阵,与i无关。 # 但更高效的实现方式是直接计算输出,避免显式构造大矩阵。 # 我们可以利用结合律: (Σ φ(k_j) v_j^T) φ(q_i) = Σ φ(k_j) (v_j^T · φ(q_i)) # 令 s_j = φ(k_j), 那么对于所有i,输出可以写为: # Output = ( (K_prime.transpose(-2, -1) @ V) @ Q_prime.transpose(-2, -1) ).transpose(...) # 但这样会得到错误的形状。标准且清晰的做法是: # 计算分母:Z = Σ φ(k_j), 形状 [B, H, 1, d_k] Z = K_prime.sum(dim=-2, keepdim=True) # 在序列长度L维度上求和 # 计算分子:S = Σ (φ(k_j) 外积 v_j),但外积是 d_k x d_k,我们想避免它。 # 实际上,我们可以直接计算每个位置的输出: # 对于每个位置i,分子是 Σ_j φ(k_j) * (v_j · φ(q_i))? 不对。 # 正确的向量化计算: # Output = ( (K_prime.transpose(-2, -1) @ V) @ Q_prime.transpose(-2, -1) ).transpose(-2, -1) # 让我们分步推导向量化形式: # 我们有: # K_prime: [B, H, L, d_k] # V: [B, H, L, d_k] # Q_prime: [B, H, L, d_k] # 我们希望计算对于所有i: out_i = ( Σ_j (K_prime_j * V_j^T) ) * Q_prime_i^T / ( Σ_j K_prime_j · Q_prime_i ) # 这很难直接向量化。一个常见的、数值稳定的实现方式是: # 计算注意力权重(非softmax,而是基于核的) # attn_weights = torch.einsum('bhid,bhjd->bhij', Q_prime, K_prime) # 这是O(N²)的,不能这么算! # 我们必须利用线性特性。 # 标准线性注意力向量化实现(效率高): # 计算 KV 聚合: Σ (K_prime^T * V) ,但维度要小心。 # 我们可以将 K_prime 视为 [B, H, L, d_k], V 视为 [B, H, L, d_k] # 想要计算一个 [B, H, d_k, d_k] 的张量,它是 Σ over L of (K_prime[:,:,:,None] * V[:,:,:,None,:]) # 使用 torch.einsum: KV = torch.einsum('b h l d, b h l m -> b h d m', K_prime, V) # 结果形状 [B, H, d_k, d_k] # 计算每个查询的输出: out_i = (KV * Q_prime_i) / (Σ K_prime · Q_prime_i) # 首先计算分子: 对于所有i, KV @ Q_prime_i^T numerator = torch.einsum('b h d m, b h l m -> b h l d', KV, Q_prime) # [B, H, L, d_k] # 计算分母: 对于所有i, Σ_j K_prime_j · Q_prime_i = (K_prime.sum(dim=-2)) · Q_prime_i # 但注意,分母应该是标量,对每个i。Z = Σ K_prime_j 是 [B, H, 1, d_k] # denominator = torch.einsum('b h l d, b h 1 d -> b h l 1', Q_prime, Z) # [B, H, L, 1] denominator = torch.einsum('b h l d, b h 1 d -> b h l', Q_prime, Z) # [B, H, L] # 防止除零,添加一个小常数 denominator = denominator.unsqueeze(-1) + 1e-6 # [B, H, L, 1] # 计算加权输出 out = numerator / denominator # [B, H, L, d_k] # 5. 应用dropout,合并多头,输出投影 out = self.dropout(out) out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) # [B, L, d_model] out = self.w_o(out) return out实操心得:在上面的实现中,最关键的步骤是利用爱因斯坦求和约定
torch.einsum高效地计算聚合KV和最终的输出。torch.einsum的表达式需要仔细推导维度。一个常见的错误是维度不匹配,建议在编写时用注释明确每一步输入输出的形状。另外,分母的计算Z是K_prime在序列长度维度的和,这体现了“线性”扫描聚合的思想。添加1e-6是为了数值稳定性,防止序列中所有键向量映射后和查询向量点积为零的极端情况(虽然概率极低)。
3.3 与标准注意力层的对比实验
为了直观感受线性注意力的效率优势,我们可以做一个简单的对比测试。
import time def benchmark_attention(attention_layer, seq_len, d_model, batch_size=4, device='cuda'): """基准测试注意力层的前向传播时间""" model = attention_layer.to(device) x = torch.randn(batch_size, seq_len, d_model).to(device) # Warm-up for _ in range(10): _ = model(x) # Timing torch.cuda.synchronize() start_time = time.time() iterations = 100 for _ in range(iterations): _ = model(x) torch.cuda.synchronize() end_time = time.time() avg_time = (end_time - start_time) / iterations * 1000 # 毫秒 return avg_time # 定义标准多头注意力层(作为对比) class StandardAttention(nn.Module): def __init__(self, d_model, n_heads, dropout=0.0): super().__init__() self.attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout, batch_first=True) def forward(self, x, mask=None): attn_mask = None if mask is not None: # 将2D mask转换为MultiheadAttention需要的key_padding_mask attn_mask = mask.bool() return self.attn(x, x, x, key_padding_mask=attn_mask)[0] # 测试配置 d_model = 512 n_heads = 8 seq_lengths = [128, 256, 512, 1024, 2048] print("序列长度 | 标准注意力 (ms) | 线性注意力 (ms) | 内存节省 (近似)") print("-" * 60) for seq_len in seq_lengths: std_attn = StandardAttention(d_model, n_heads) lin_attn = LinearAttention(d_model, n_heads) try: t_std = benchmark_attention(std_attn, seq_len, d_model, device='cuda') t_lin = benchmark_attention(lin_attn, seq_len, d_model, device='cuda') # 估算内存节省:标准注意力需要存储 N x N 矩阵,线性注意力主要存储聚合状态 mem_std = seq_len * seq_len * 4 / (1024**2) # 假设float32, MB mem_lin = (d_model * d_model * 2) * 4 / (1024**2) # 聚合状态 KV 和 Z 相关,这里简化估算 # 实际上线性注意力内存与序列长度线性相关,但常数项小很多。 mem_saving = mem_std / (seq_len * d_model * 4 / (1024**2) + 1e-6) # 与输入输出线性内存的比值 print(f"{seq_len:^10} | {t_std:^16.2f} | {t_lin:^16.2f} | ~{mem_saving:.1f}x") except RuntimeError as e: # 当序列很长时,标准注意力可能因OOM而失败 if "out of memory" in str(e).lower(): print(f"{seq_len:^10} | OOM (>{torch.cuda.max_memory_allocated()/1024**3:.1f}GB) | {t_lin:^16.2f} | >10x") else: raise e运行这个测试,你会清晰地看到,随着序列长度增加,标准注意力的计算时间呈平方级增长,并且在长序列(如2048)时极易出现内存不足(OOM)错误。而线性注意力的时间和内存增长几乎是线性的,在长序列任务上优势巨大。
4. 线性注意力的实战应用场景与调优经验
理解了原理和实现,我们来看看线性注意力在实际项目中能用在哪儿,以及如何让它更好地工作。
4.1 典型应用场景
长文本建模与文档理解:这是最直接的应用。传统的Transformer在处理超过512或1024个token的文档时非常吃力。线性注意力可以轻松将上下文窗口扩展到数万甚至更长,使得模型能够一次性处理整篇论文、技术手册或长篇小说,捕捉更长期的依赖关系。例如,在构建智能文档问答系统时,线性注意力层可以让模型同时看到问题和文档的所有相关内容。
高分辨率图像处理:将图像视为一个像素序列(例如使用ViT),图像分辨率越高,序列长度越长(如256x256的图像就是65536的序列)。标准注意力在此完全不可行。线性注意力使得Vision Transformer能够处理更高分辨率的输入,在图像生成、超分辨率、医学图像分析等领域潜力巨大。
语音与音频处理:原始音频波形或频谱图序列往往非常长(每秒16000个采样点)。线性注意力可以构建高效的音频识别、生成或分离模型,处理更长的音频片段,提升上下文感知能力。
时间序列预测与传感器数据分析:物联网设备产生的传感器数据流是典型的长序列。线性注意力模型可以高效地建模长期依赖,用于设备故障预测、环境监测、金融序列分析等。
替代RNN的递归场景:具有递归形式的线性注意力变体(如RWKV),因其
O(N)的推理复杂度,非常适合需要逐token生成或流式处理的场景,比如实时语音识别、同步机器翻译,它们比传统RNN并行性更好,比标准Transformer推理更快。
4.2 训练技巧与注意事项
直接将标准Transformer中的注意力层替换为线性注意力层,性能往往会有明显下降。这不是因为线性注意力理论不行,而是需要一些训练技巧来弥补其表达能力的细微差异。
渐进式上下文长度训练:这是稳定训练线性注意力模型的一个有效技巧。不要一开始就在超长序列上训练。可以先在较短的序列(如256)上训练模型,让模型学习基本的语言或视觉模式。然后,逐步增加训练时的序列长度(如512, 1024, 2048...),并在每次增加长度时,用之前训练的权重进行初始化,并可能稍微降低学习率。这有助于模型平稳地适应更长的依赖范围。
精心设计特征映射
φ(·):φ(·)的选择直接影响模型能力。简单的elu+1可能不够。可以尝试:- Performer的FAVOR+机制:使用随机正交矩阵和
exp函数的近似,理论性质更好。 - 可学习的映射:将
φ(·)设计成一个小的神经网络(如一层MLP),让模型自己学习最优的映射。但这会引入额外参数。 - 结合局部注意力:线性注意力擅长捕捉全局依赖,但可能弱化局部模式。可以将其与一个固定窗口大小的局部注意力(如滑动窗口)结合,形成“局部-全局”混合注意力。
- Performer的FAVOR+机制:使用随机正交矩阵和
注意数值稳定性:线性注意力在计算分母
Σ φ(k_j) · φ(q_i)时,如果序列中存在大量接近于零的键向量,可能导致分母过小,引发梯度爆炸。除了添加epsilon外,还可以考虑对φ(·)的输出进行归一化(如LayerNorm),或者使用更稳定的计算顺序。位置编码的适配:标准Transformer依赖绝对或相对位置编码来注入序列顺序信息。在线性注意力中,由于计算方式改变,传统的位置编码可能效果不佳。需要探索与之兼容的位置编码方式,如“相对位置偏置”的线性化版本,或使用递归形式中隐含的位置信息。
与标准注意力的混合使用:一个实用的策略是,在模型底层使用1-2层标准注意力,以捕获精确的局部语法或视觉结构;在模型高层使用线性注意力,以高效整合全局语义信息。这种混合架构可以在性能和效率之间取得很好的平衡。
4.3 常见问题与排查实录
在实际使用线性注意力时,我踩过不少坑,这里总结几个典型问题及其解决方法。
问题1:模型收敛速度慢,最终性能不如标准注意力。
- 可能原因:特征映射
φ(·)表达能力不足,或模型未能有效利用位置信息。 - 排查与解决:
- 首先,检查你的
φ(·)函数。尝试换成更复杂的映射,如一个小型MLP:φ(x) = LayerNorm(GeLU(xW1 + b1)W2 + b2)。这增加了可学习参数,可能提升表达能力。 - 其次,审视位置编码。尝试使用可学习的相对位置偏置,并将其添加到线性注意力计算中的
Q_prime和K_prime的点积之前(虽然不直接计算NxN矩阵,但可以通过数学变换将相对位置信息融入聚合计算)。 - 使用上文提到的“渐进式长度训练”策略,从短序列开始。
- 考虑使用预训练的标准Transformer权重进行初始化,然后只微调线性注意力层及其后的部分。
- 首先,检查你的
问题2:训练过程中出现NaN(非数)损失。
- 可能原因:数值不稳定,分母
Z过小导致除法溢出。 - 排查与解决:
- 确保在分母计算中加入了足够大的
epsilon(如1e-6或1e-8)。 - 检查
φ(·)函数的输出范围。如果使用elu+1,输出应恒大于0。如果使用其他映射,确保不会产生负值或零值聚集。 - 在计算
numerator和denominator时,使用双精度浮点数(torch.double)进行调试,看是否问题消失。如果是,说明需要更精细的数值处理。 - 尝试在计算
KV聚合和Z聚合时,使用log-sum-exp技巧的变种来提升稳定性,尽管线性注意力本身就是为了避免这类复杂计算。
- 确保在分母计算中加入了足够大的
问题3:在推理时,对于超长序列,速度提升没有预期明显。
- 可能原因:实现方式并非真正的
O(N),或者d_k维度较大,导致O(Nd²)中的d²项成为瓶颈。 - 排查与解决:
- 检查你的实现是否真的避免了
O(N²)的操作。使用PyTorch的profiler工具分析计算图,确认最耗时的操作不是与序列长度平方相关的。 - 如果
d_k较大(例如128或256),d_k²可能达到数万。考虑减少头数 (n_heads) 或每个头的维度 (d_k),或者使用分组或深度可分离卷积的思想来降低d_k维度上的计算复杂度。 - 对于递归形式的线性注意力,确保在推理时使用了递归模式(即每一步基于上一步状态更新),而不是仍然使用并行的训练模式。递归模式才是真正的
O(1)时间步进。
- 检查你的实现是否真的避免了
问题4:无法有效处理因果掩码(用于语言模型的自回归生成)。
- 可能原因:标准因果掩码(下三角矩阵)是
NxN的,直接应用违背了线性注意力的初衷。 - 排查与解决:
- 对于基于聚合的线性注意力,因果性可以通过在计算聚合状态
S和Z时,只聚合当前位置及之前的信息来实现。这需要在扫描序列时,维护一个随时间步更新的状态。在训练时,这可以通过“累积和”或“并行扫描”算法高效实现。在推理时,则是简单的递归更新。 - 具体实现时,可以将
KV的计算改为累积和:KV_t = KV_{t-1} + φ(k_t) ⊗ v_t,Z_t = Z_{t-1} + φ(k_t)。这样,在生成第t个token时,使用的就是前t-1个token的聚合信息,天然实现了因果性。 - 许多现成的库(如Hugging Face的
xformers库中的LinearAttention)已经内置了对因果掩码的支持,建议优先使用这些经过充分测试的实现。
- 对于基于聚合的线性注意力,因果性可以通过在计算聚合状态
线性注意力不是一颗“银弹”,它用计算效率换取了部分表达灵活性。但在长序列任务成为主流的今天,它的价值毋庸置疑。我的体会是,将其视为工具箱中的一件强大补充工具,在合适的场景(长度敏感、资源受限)下大胆使用,并结合混合架构、渐进训练等技巧,你完全可以在保持竞争力的同时,获得数量级的效率提升。最后一个小建议:在项目初期,可以用一个简单的开关,方便地在标准注意力和线性注意力之间切换,以便进行快速的性能-效率权衡分析。