滑动窗口注意力机制
P5337. 滑动窗口注意力机制
场次:2026年9月2日 · 来源:CodeFun2000 P5337
题目描述
标准的 自注意力计算为:
对于序列长度为 、维度为 的输入,其时间复杂度和空间复杂度分别为 和 。
近期研究表明,大多数任务中的依赖关系具有局部性,远距离 token 通常可以通过多层堆叠间接完成交互。因此,可以通过限制每个 token 的注意力范围来显著降低计算复杂度。
滑动窗口注意力机制是一种典型方法,其原理如下:
- 为每个 设定固定大小为 的对称窗口。
- 位置 的 只与满足 的位置 的 计算注意力,其中 。
- 为保证数值稳定性,在计算 前减去当前窗口内的最大值:
以 为例,滑动窗口注意力可视化如下:
位置:
窗口 ,左边界限制为
窗口
窗口
窗口
窗口
窗口
窗口
窗口 ,右边界限制为
其中 计算注意力, 掩码为 (不计算)。
输入格式
第一行输入四个整数:
, , ,
后续包含 组数据,每组依次包含:
行,每行 个浮点数,表示 Query 矩阵。
行,每行 个浮点数,表示 Key 矩阵。
行,每行 个浮点数,表示 Value 矩阵。
数据范围
输出格式
输出 组结果,每组包含 行,每行 个浮点数,表示该位置的输出向量。
组间用一个空行分隔,最后一组后无空行。
最终结果保留 位小数,四舍五入。
注意力打分及 权重在题目说明中保留 位小数展示,实际计算过程中无需提前进行舍入。
如果输入违反约束,输出 。
示例
示例 1
输入:
1 4 4 1
0.1 0.2 0.3 0.4
0.2 0.3 0.4 0.5
0.3 0.4 0.5 0.6
0.4 0.5 0.6 0.7
0.1 0.0 0.1 0.0
0.0 0.1 0.0 0.1
0.1 0.1 0.0 0.0
0.0 0.0 0.1 0.1
1.0 0.0 0.0 0.0
0.0 1.0 0.0 0.0
0.0 0.0 1.0 0.0
0.0 0.0 0.0 1.0输出:
0.50 0.50 0.00 0.00
0.33 0.34 0.33 0.00
0.00 0.33 0.33 0.34
0.00 0.00 0.50 0.50说明:
输入 ,,,。
缩放因子 。
根据窗口大小 ,注意力打分为:
位置 的权重:
归一化后为:
位置 的权重:
归一化后为:
位置 的权重:
归一化后为:
位置 的权重:
归一化后为:
上述归一化权重保留 位小数精度。
最后使用归一化后的权重对对应位置的 进行加权求和,并将最终结果保留 位小数:
示例 2
输入:
1 2 2 0
0.3 0.4
0.4 0.5
0.5 0.6
0.6 0.7
0.1 0.0
0.0 0.1输出:
0说明:
输入 ,,,。
由于 违反输入范围要求,因此输出 。
提示
仅能使用编程语言内置的函数。
解题思路
第一步:理解问题本质
标准自注意力会让每个 query 与序列中的所有 key 计算分数:
当序列长度为 时,需要计算 个分数,时间复杂度和空间复杂度分别是 和 。
滑动窗口注意力利用“远距离依赖可以通过多层堆叠间接完成”这一观察,把每个位置的注意力范围限制在左右各 个位置内。这样每个 query 只需要和至多 个 key 计算分数,复杂度降为 。
第二步:暴力解法——完整注意力
最直接的做法是不利用窗口,仍然计算完整的 注意力矩阵:
# 每个 query 都要和全部 N 个 key 打分,于是产生 N×N 的分数矩阵
for i in range(N):
scores = []
# 除以 sqrt(d) 是缩放:维度越大点积越大,不除会让 softmax 被最大项独占
for j in range(N):
scores.append(dot(Q[i], K[j]) / sqrt(d))
# softmax 在这一整行上归一化,权重之和为 1
weights = softmax(scores)
# 用这组权重对全部 V 向量加权求和,得到位置 i 的输出向量
output[i] = weighted_sum(weights, V)这个做法能得到正确答案,但每个位置都要扫描全部 个 key。当 很大时, 的分数计算和存储都会成为瓶颈。
第三步:优化解法——只计算窗口内的分数
对于位置 ,需要参与计算的 key 下标范围是:
注意窗口在序列两端会被截断,而不是补零:
- 时,左边没有位置,窗口是 ;
- 时,右边没有位置,窗口是 。
每个位置只计算 个分数,然后在这个窗口内做 Softmax。总计算量从 降到 。
第四步:数值稳定性与输出细节
Softmax 的公式是:
减去窗口内最大值不会改变结果,但可以避免 在数值较大时溢出。即使题目数据范围不大,也建议保留这一步,保证实现的通用性。
另外需要注意:
- 每个 batch 单独计算,batch 之间输出一个空行;
- 最终结果保留两位小数;
- 结果接近 时不要输出
-0.00; - 如果
window_size不在 内,直接输出0。
完整代码实现
import math
import sys
def format_number(value: float) -> str:
"""保留两位小数,并避免输出 -0.00。"""
if abs(value) < 0.005:
value = 0.0
return f"{value:.2f}"
def solve():
# 输入既有整数(头部 4 个参数)也有浮点数(Q/K/V),
# 所以先按空白切成字符串,再按需要分别转换。
tokens = sys.stdin.buffer.read().split()
if len(tokens) < 4:
print(0)
return
# 头部:batch 数、序列长度、每个 token 的维度、窗口半径
batch_size, seq_len, d_k, window_size = map(int, tokens[:4])
# 题目约束:batch_size >= 1、seq_len >= 1、d_k >= 1、
# 1 <= window_size <= seq_len。任何一条不满足都输出 0。
if batch_size < 1 or seq_len < 1 or d_k < 1:
print(0)
return
if window_size < 1 or window_size > seq_len:
print(0)
return
# 每个 batch 有 Q、K、V 三个 seq_len x d_k 矩阵。
needed = 3 * batch_size * seq_len * d_k
if len(tokens) < 4 + needed:
print(0)
return
pos = 4
# 缩放因子 1 / sqrt(d_k),在算完点积后统一除。
scale = math.sqrt(d_k)
groups = []
for _ in range(batch_size):
# 读取当前 batch 的 Query 矩阵,形状 seq_len x d_k。
query = []
for _ in range(seq_len):
query.append([float(x) for x in tokens[pos:pos + d_k]])
pos += d_k
# 读取 Key 矩阵,形状 seq_len x d_k。
key = []
for _ in range(seq_len):
key.append([float(x) for x in tokens[pos:pos + d_k]])
pos += d_k
# 读取 Value 矩阵,形状 seq_len x d_k。
value = []
for _ in range(seq_len):
value.append([float(x) for x in tokens[pos:pos + d_k]])
pos += d_k
lines = []
for i in range(seq_len):
# 位置 i 的窗口是 [i-w, i+w]。
# 序列两端没有位置,所以窗口要截断,而不是补零。
left = max(0, i - window_size)
right = min(seq_len - 1, i + window_size)
# 只计算窗口内的注意力分数:
# score = Q[i] · K[j] / sqrt(d_k)
scores = []
qi = query[i]
for j in range(left, right + 1):
dot = 0.0
kj = key[j]
for t in range(d_k):
dot += qi[t] * kj[t]
scores.append(dot / scale)
# 数值稳定的 Softmax:
# 先减去窗口内最大值,再取 exp,最后归一化。
max_score = max(scores)
exp_scores = [math.exp(s - max_score) for s in scores]
total = sum(exp_scores)
weights = [e / total for e in exp_scores]
# 用窗口内的权重对 V 加权求和:
# output[t] = sum_j weight[j] * V[j][t]
output = [0.0] * d_k
for offset, j in enumerate(range(left, right + 1)):
weight = weights[offset]
vj = value[j]
for t in range(d_k):
output[t] += weight * vj[t]
lines.append(" ".join(format_number(x) for x in output))
groups.append("\n".join(lines))
# batch 之间用空行分隔,最后一组后没有空行
print("\n\n".join(groups))
if __name__ == "__main__":
solve()NumPy 版本
以下版本使用 NumPy。只有在评测环境允许第三方库时才能提交;CodeFun2000 的普通 Python 环境不一定安装 NumPy。
思路与上面的纯 Python 版完全一致,区别在于把「逐位置、逐 key」的循环换成张量运算:一个 batch 的 读成三维数组,、 两侧各补 行 之后,用 sliding_window_view 得到形状 的窗口视图,于是所有位置的窗口分数 、Softmax 权重、加权输出都能一次算完。两端越界的槽位用 掩掉,Softmax 的归一化范围仍然只限于真实窗口。
import math
import sys
import numpy as np
def format_number(value: float) -> str:
"""保留两位小数,并避免输出 -0.00。"""
if abs(value) < 0.005:
value = 0.0
return f"{value:.2f}"
def solve():
# 输入既有整数(头部 4 个参数)也有浮点数(Q/K/V),
# 所以先按空白切成字符串,再按需要分别转换。
tokens = sys.stdin.buffer.read().split()
if len(tokens) < 4:
print(0)
return
# 头部:batch 数、序列长度、每个 token 的维度、窗口半径
batch_size, seq_len, d_k, window_size = (int(x) for x in tokens[:4])
# 题目约束:batch_size >= 1、seq_len >= 1、d_k >= 1、
# 1 <= window_size <= seq_len。任何一条不满足都输出 0。
if batch_size < 1 or seq_len < 1 or d_k < 1:
print(0)
return
if window_size < 1 or window_size > seq_len:
print(0)
return
# 每个 batch 有 Q、K、V 三个 seq_len x d_k 矩阵。
needed = 3 * batch_size * seq_len * d_k
if len(tokens) < 4 + needed:
print(0)
return
# 一次把所有浮点数读进来,组织成 (batch, 3, N, d) 的四维张量:
# 第 1 维区分 Q / K / V,后两维是每个位置的 d_k 维向量。
data = np.array(tokens[4:4 + needed], dtype=np.float64)
data = data.reshape(batch_size, 3, seq_len, d_k)
# 缩放因子 1 / sqrt(d_k),在算完点积后统一除。
scale = math.sqrt(d_k)
# 每个位置的窗口宽度固定是 2w+1,两端不足的部分在下面用掩码屏蔽。
width = 2 * window_size + 1
# 窗口内第 k 个槽位对应的相对下标:k - w,取值 -w .. w。
offsets = np.arange(width) - window_size
groups = []
for b in range(batch_size):
query, key, value = data[b] # 每个都是 N x d
# 把 K、V 两侧各补 w 行 0,这样位置 i 的窗口在内存里就是
# 从第 i 行开始的连续 2w+1 行,可以直接用滑动窗口视图取出来。
pad = np.zeros((window_size, d_k))
key_pad = np.concatenate([pad, key, pad], axis=0)
value_pad = np.concatenate([pad, value, pad], axis=0)
# sliding_window_view 得到 (N, d, 2w+1) 的视图,不复制数据:
# key_win[i, :, k] 就是窗口内第 k 个位置的 key 向量。
key_win = np.lib.stride_tricks.sliding_window_view(key_pad, width, axis=0)
value_win = np.lib.stride_tricks.sliding_window_view(value_pad, width, axis=0)
# 所有位置、所有窗口槽位的分数一次算完:(N, 2w+1)。
scores = np.einsum("id,idw->iw", query, key_win) / scale
# 两端越界的槽位(i-w < 0 或 i+w > N-1)标记出来,
# 窗口是被截断而不是补零参与 Softmax,所以这里置 -inf 再屏蔽。
pos_idx = np.arange(seq_len)[:, None] + offsets[None, :]
valid = (pos_idx >= 0) & (pos_idx < seq_len)
scores = np.where(valid, scores, -np.inf)
# 数值稳定的 Softmax:先减去窗口内最大值,再取 exp、归一化。
# 被屏蔽的槽位是 -inf,exp 后正好是 0,权重自然为 0。
row_max = scores.max(axis=1, keepdims=True)
exp_scores = np.exp(scores - row_max)
weights = exp_scores / exp_scores.sum(axis=1, keepdims=True)
# 用窗口内的权重对 V 加权求和:(N, 2w+1) x (N, d, 2w+1) -> (N, d)。
output = np.einsum("iw,idw->id", weights, value_win)
lines = [
" ".join(format_number(float(x)) for x in row)
for row in output
]
groups.append("\n".join(lines))
# batch 之间用空行分隔,最后一组后没有空行
print("\n\n".join(groups))
if __name__ == "__main__":
solve()示例推演
以样例 1 为例,、、,缩放因子 。
位置 0
窗口是 ,只和 计算分数:
Softmax 后:
输出:
位置 1
窗口是 ,分数为:
Softmax 后:
输出:
位置 2
窗口是 ,分数为:
Softmax 后:
输出:
位置 3
窗口是 ,分数为:
Softmax 后:
输出:
四个位置的结果合起来就是样例输出。
对于样例 2,window_size=0 不在 内,因此直接输出 0。
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 完整注意力 | 每个位置与所有 key 计算 | ||
| 滑动窗口注意力 | 本题采用;辅助空间为 |
其中 是 batch_size。当 时,滑动窗口版本比完整注意力快一个数量级。
易错点总结
1. 窗口边界要截断
可能小于 , 可能大于 。必须使用:
# 两端要截断而不是补零:i 靠近开头时左边没有位置,靠近结尾时右边没有位置
left = max(0, i - window_size)
# 上界取 seq_len - 1,因为下标最大只到最后一个位置
right = min(seq_len - 1, i + window_size)不能直接使用 i-w 到 i+w,否则会访问越界。
2. 窗口内做 Softmax,而不是全序列
每个位置的最大值和指数和都只来自当前窗口。混入窗口外的分数会改变权重,结果必然错误。
3. 不要提前舍入
题目说明中的分数和权重保留 6 位小数只是展示用,实际计算必须使用完整精度。提前四舍五入会累积误差,导致最后两位小数不一致。
4. 多 batch 的输出格式
batch 之间用一个空行分隔,最后一组后不能多输出空行。代码中使用 "\n\n".join(groups),正好满足这个要求。
5. 避免 -0.00
浮点运算可能产生一个绝对值很小但符号为负的数,直接格式化会得到 -0.00。代码在格式化前把绝对值小于 的数归零。
6. 非法输入要输出 0
window_size 必须满足 。样例 2 就是 window_size=0 的非法情况。除此之外,batch_size、seq_len、d_k 也必须为正整数。
扩展思考
- 窗口更大时:当 时,滑动窗口退化为完整注意力,此时两种写法结果相同,只是滑动窗口版本仍然只扫描窗口。
- 因果滑动窗口:自回归模型通常使用因果掩码,位置 只能看到 ,而不是左右对称窗口。只需要把
right改成 即可。 - 稀疏注意力:除了滑动窗口,还有空洞窗口、全局 token + 局部窗口等变体,目标都是在保留局部依赖的同时降低 的开销。
- 批量矩阵运算:如果使用 NumPy 等库,可以把一个 batch 的 Q、K、V 堆成三维张量,利用矩阵乘法一次性算出所有窗口分数;本题要求只用内置函数,所以按位置逐个计算。