跳到主要内容

P5458. 编码器层前向计算

场次:2026年9月16日 · 来源:CodeFun2000 P5458

题目描述​

实现一层标准的 Transformer Encoder Layer。该层依次包含两个子层:

  • 多头自注意力(Multi-Head Self-Attention, MHSA)
  • 逐位置前馈网络(Position-wise Feed-Forward Network, FFN)

每个子层的计算完成之后,立刻做残差连接,再做层归一化(Post-LN):

SublayerOut=LayerNorm(x+Sublayer(x))\mathrm{SublayerOut}=\mathrm{LayerNorm}\bigl(x+\mathrm{Sublayer}(x)\bigr)

整层顺序为:先对输入做 MHSA 子层(残差 + LN),再对结果做 FFN 子层(残差 + LN)。

固定设定​

为使评测结果唯一,按下述约定实现,不要自行改成 Pre-LN 或改权重。

  • 输入张量 xx 的形状为 (batch,seq,d_model)=(1,2,4)(\textit{batch},\textit{seq},\textit{d\_model})=(1,2,4)。
  • 头数 h=2h=2,因此每头维度 dk=dmodel/h=2d_k=d_{\textit{model}}/h=2。
  • 注意力与输出投影、FFN 的权重均为 4×44\times 4 单位矩阵:WQ=WK=WV=WO=W1=W2=IW_Q=W_K=W_V=W_O=W_1=W_2=I。
  • FFN 偏置 b1=b2=0b_1=b_2=\mathbf{0}。
  • 层归一化的 γ=1\gamma=\mathbf{1},β=0\beta=\mathbf{0},ε=10−5\varepsilon=10^{-5}。均值与方差都在最后一个维度上统计,方差用总体方差(除以 dmodeld_{\textit{model}},不要除以 dmodel−1d_{\textit{model}}-1)。

公式​

缩放点积注意力(每个头内独立计算,在序列维上做 softmax):

Attention(Q,K,V)=softmax(QK⊤dk)V\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\left(\frac{QK^{\top}}{\sqrt{d_k}}\right)V

多头拼接后再乘输出投影:

MultiHead(Q,K,V)=Concat(head1,…,headh)WO\mathrm{MultiHead}(Q,K,V)=\mathrm{Concat}(\textit{head}_1,\ldots,\textit{head}_h)W_O

切分方式:把最后一维按长度 dkd_k 切成 hh 段,第 tt 个头取特征区间 [t⋅dk,(t+1)⋅dk)[t\cdot d_k,(t+1)\cdot d_k)。

层归一化:

LayerNorm(x)=x−E[x]Var[x]+ε⋅γ+β\mathrm{LayerNorm}(x)=\frac{x-\mathbb{E}[x]}{\sqrt{\mathrm{Var}[x]+\varepsilon}}\cdot\gamma+\beta

FFN(ReLU):

FFN(x)=max⁡(0,xW1+b1)W2+b2\mathrm{FFN}(x)=\max(0,xW_1+b_1)W_2+b_2

softmax 在实现时先减去当前行最大值再 exp⁡\exp,避免溢出。

输入格式​

一行 88 个实数,按行优先给出 xx,即

x[0,0,0], x[0,0,1], x[0,0,2], x[0,0,3], x[0,1,0], x[0,1,1], x[0,1,2], x[0,1,3]x[0,0,0],\ x[0,0,1],\ x[0,0,2],\ x[0,0,3],\ x[0,1,0],\ x[0,1,1],\ x[0,1,2],\ x[0,1,3]

输出格式​

一行 88 个实数,把形状为 (1,2,4)(1,2,4) 的输出按同样顺序平铺。每个数四舍五入到小数点后 22 位。

数据范围​

88 个分量均满足 ∣xi∣≤50|x_i|\le 50。

示例​

示例 1​

输入:

2.00 0.00 0.00 0.00 0.00 2.00 0.00 0.00

输出:

1.73 -0.55 -0.59 -0.59 -0.55 1.73 -0.59 -0.59

说明:

两个位置分别是 (2,0,0,0)(2,0,0,0) 与 (0,2,0,0)(0,2,0,0)。

第 00 个头上,Q=K=VQ=K=V 为 [(2,0), (0,2)]\bigl[(2,0),\ (0,2)\bigr],缩放后得分矩阵对角线为 4/2=22≈2.8284/\sqrt{2}=2\sqrt{2}\approx 2.828,另一侧为 00。softmax 后约 [(0.944,0.056), (0.056,0.944)]\bigl[(0.944,0.056),\ (0.056,0.944)\bigr],该头输出约 [(1.888,0.112), (0.112,1.888)]\bigl[(1.888,0.112),\ (0.112,1.888)\bigr]。第 11 个头全 00,拼接后与 xx 相加再 LN、ReLU、残差、LN,得到上述输出。

若把残差拿掉,或改成 Pre-LN,第一维不会落到 1.731.73。

示例 2​

输入:

1.00 0.00 1.00 0.00 0.00 1.00 0.00 1.00

输出:

1.00 -1.00 1.00 -1.00 -1.00 1.00 -1.00 1.00

说明:

两个位置为 (1,0,1,0)(1,0,1,0) 与 (0,1,0,1)(0,1,0,1),两个头的 Q,K,VQ,K,V 相同。MHSA 后与 xx 相加,每个位置都是两个 1.6701.670 与两个 0.3300.330 交错;第一次 LN 已把它们变成 ±1\pm 1。ReLU 只保留正分量,第二次 LN 后仍是 ±1.00\pm 1.00 的交错图案。


解题思路​

第一步:理解问题本质​

这道题不是“自己设计一个 Transformer”,而是按给定的固定参数,把一层 Encoder 的前向公式老老实实算一遍。题面把所有可能产生歧义的地方都钉死了:

项目题目规定
输入形状(batch,seq,dmodel)=(1,2,4)(batch,seq,d_{model})=(1,2,4)
头数 hh22,每头 dk=2d_k=2
全部权重WQ=WK=WV=WO=W1=W2=I4×4W_Q=W_K=W_V=W_O=W_1=W_2=I_{4\times4}
FFN 偏置b1=b2=0b_1=b_2=\mathbf{0}
LayerNormγ=1\gamma=\mathbf{1},β=0\beta=\mathbf{0},ε=10−5\varepsilon=10^{-5},最后一维,总体方差
结构Post-LN:先残差相加,再层归一化
子层顺序先 MHSA(残差 + LN),再 FFN(残差 + LN)

因为所有权重都是单位矩阵,好几处矩阵乘法可以直接消掉:

  • Q=K=V=xWQ=xQ=K=V=xW_Q=x,所以不需要真的做投影,直接按特征区间把 xx 切成两半就是两个头;
  • WO=IW_O=I,所以多头拼接之后不需要再乘矩阵;
  • W1=W2=IW_1=W_2=I、b1=b2=0b_1=b_2=\mathbf{0},所以 FFN(y)=max⁡(0,y⋅I)⋅I=ReLU(y)\mathrm{FFN}(y)=\max(0,y\cdot I)\cdot I=\mathrm{ReLU}(y),两次矩阵乘法退化成一次逐元素 ReLU。

于是整层的计算只剩下三件真正要做的事:缩放点积注意力、层归一化、残差相加。

需要特别注意的是“Post-LN”这个词。Post-LN 指层归一化放在残差相加之后:

y=LayerNorm(x+Sublayer(x))y=\mathrm{LayerNorm}\bigl(x+\mathrm{Sublayer}(x)\bigr)

而 Pre-LN 是 Sublayer(LayerNorm(x))+x\mathrm{Sublayer}(\mathrm{LayerNorm}(x))+x,两者结果不同。题面在说明里专门写了“若把残差拿掉,或改成 Pre-LN,第一维不会落到 1.731.73”,这就是在提醒实现顺序。

第二步:暴力解法——逐条公式直译​

最直接的做法是照着公式写四个函数:矩阵乘、切分多头、softmax、层归一化,然后按顺序调用。以注意力为例:

# 每个头单独构造 Q、K、V:这里 W_Q 等是单位矩阵,切片就等于投影
for head in range(h):
lo, hi = head * d_k, (head + 1) * d_k
q = [[x[i][t] for t in range(lo, hi)] for i in range(seq)]
k = [[x[i][t] for t in range(lo, hi)] for i in range(seq)]
v = [[x[i][t] for t in range(lo, hi)] for i in range(seq)]

# 打分:S = Q K^T / sqrt(d_k),形状 (seq, seq)
scores = [[dot(q[i], k[j]) / sqrt(d_k) for j in range(seq)] for i in range(seq)]
# 在序列维(每一行)上做 softmax
weights = [softmax(row) for row in scores]
# 加权求和:该头输出 (seq, d_k)
head_out = [[sum(weights[i][j] * v[j][t] for j in range(seq))
for t in range(d_k)] for i in range(seq)]

这样做在本题的数据规模下当然能过,因为 seq=2seq=2、dmodel=4d_{model}=4 都是常数。但直译会带来两个问题:

  1. 反复构造小列表、反复调用通用矩阵乘法,代码里出现大量“为了通用而存在”的循环,读者难以看出哪里是本题的关键;
  2. 通用矩阵乘法要处理 (1,2,4)×(4,4)(1,2,4)\times(4,4) 这类三维形状,容易在 batch 维、头维上写错下标。

既然形状固定,不如把“通用”这层壳去掉,只保留真正的计算。

第三步:优化解法——消掉单位矩阵,按区间切头​

第一层简化来自权重:

# W_Q = W_K = W_V = I,所以 Q、K、V 全都是 x 本身,
# 第 head 个头直接取特征区间 [head*d_k, (head+1)*d_k)
for head in range(NUM_HEADS):
lo = head * D_K
hi = lo + D_K
# 只用 x[i][lo:hi] 参与这个头的点积与加权

第二层简化来自 seq=2seq=2:每个头的分数矩阵只有 2×22\times 2,softmax 只需在 2 个数上做,注意力输出也只是两个 2 维向量的加权和。

第三层简化来自 WO=IW_O=I:把两个头的结果直接写回 44 维向量的对应区间,就完成了“拼接 + 输出投影”:

output = [[0.0] * D_MODEL for _ in range(SEQ_LEN)]
for head in range(NUM_HEADS):
lo = head * D_K
for i in range(SEQ_LEN):
# ... 算出该头对位置 i 的输出 ...
for j in range(SEQ_LEN):
weight = exp_scores[j] / total
for t in range(lo, lo + D_K):
output[i][t] += weight * x[j][t] # 区间 lo..lo+d_k-1 就是第 head 个头

第四层简化来自 FFN 的权重:W1=W2=IW_1=W_2=I、偏置为零,于是

FFN(y)=max⁡(0,yW1+b1)W2+b2=max⁡(0,y)=ReLU(y)\mathrm{FFN}(y)=\max(0,yW_1+b_1)W_2+b_2=\max(0,y)=\mathrm{ReLU}(y)

不需要写任何矩阵乘法。

第四步:最优解法——把两个子层抽象成同一个模式​

两个子层其实是同一个模式:子层函数 + 残差 + LayerNorm。区别只在子层函数是 MHSA 还是 ReLU。把 LayerNorm 单独写成函数,整层就只剩十几行:

def layer_norm(vector):
n = len(vector)
mean = sum(vector) / n
# 题面要求总体方差:除以 n,不是 n - 1
variance = sum((v - mean) ** 2 for v in vector) / n
# eps 在根号里面:sqrt(Var + eps)
std = math.sqrt(variance + LAYER_NORM_EPS)
return [(v - mean) / std for v in vector]


def encoder_layer(x):
# 子层 1:MHSA(W_O = I,拼接后无需再投影)
attention = multi_head_attention(x)
first = [layer_norm([x[i][d] + attention[i][d] for d in range(D_MODEL)])
for i in range(SEQ_LEN)]

# 子层 2:FFN 退化为 ReLU
ffn = [[max(0.0, v) for v in row] for row in first]
second = [layer_norm([first[i][d] + ffn[i][d] for d in range(D_MODEL)])
for i in range(SEQ_LEN)]
return second

几个实现细节:

  1. 多头切分是“按区间切最后一维”,不是交错取。第 tt 个头取 [t⋅dk,(t+1)⋅dk)[t\cdot d_k,(t+1)\cdot d_k),本题就是第 0 个头拿第 0、1 维,第 1 个头拿第 2、3 维。
  2. softmax 在序列维上做。分数矩阵 SS 的形状是 (seq,seq)(seq,seq),第 ii 行是“位置 ii 对所有位置(包括自己)的分数”,所以每一行单独做 softmax,权重之和为 1。
  3. softmax 先减行最大值:exp⁡(s−max⁡s)\exp(s-\max s) 与 exp⁡(s)\exp(s) 的比值完全相同,但前者不会溢出。本题分数最大可达约 35353535,e3535e^{3535} 必然溢出,所以这一步不能省。
  4. 层归一化在最后一维:对每个位置的 4 个数求均值和方差,两个位置互不影响。γ=1\gamma=1、β=0\beta=0 时就是标准化本身。
  5. 输出要按行优先平铺:位置 0 的 4 个数在前,位置 1 的 4 个数在后,与输入的读入顺序一致。

完整代码实现​

import math
import sys

# 题目固定的模型设定。写成常量而不是魔法数字,便于对照题面。
SEQ_LEN = 2 # 序列长度
D_MODEL = 4 # 每个位置的特征维度
NUM_HEADS = 2 # 头数
D_K = D_MODEL // NUM_HEADS # 每头维度 = 2
LAYER_NORM_EPS = 1e-5


def layer_norm(vector):
"""在最后一维上做层归一化(gamma = 1,beta = 0,总体方差)。"""
n = len(vector)
mean = sum(vector) / n

# 题目明确要求总体方差:除以 n,而不是 n - 1。
variance = sum((v - mean) ** 2 for v in vector) / n

# eps 放在根号里面,和题面公式 sqrt(Var + eps) 完全一致。
std = math.sqrt(variance + LAYER_NORM_EPS)
return [(v - mean) / std for v in vector]


def multi_head_attention(x):
"""多头自注意力。权重矩阵都是单位矩阵,所以 Q = K = V = x。"""
scale = math.sqrt(D_K)
output = [[0.0] * D_MODEL for _ in range(SEQ_LEN)]

# 按特征区间 [t*d_k, (t+1)*d_k) 逐个处理每个头。
for head in range(NUM_HEADS):
lo = head * D_K
hi = lo + D_K

for i in range(SEQ_LEN):
# 第 i 个位置在该头内的 query 与所有位置的 key 做点积并缩放。
scores = []
for j in range(SEQ_LEN):
dot = 0.0
for t in range(lo, hi):
dot += x[i][t] * x[j][t]
scores.append(dot / scale)

# 减行最大值只改变数值稳定性,不改变 softmax 的结果。
max_score = max(scores)
exp_scores = [math.exp(s - max_score) for s in scores]
total = sum(exp_scores)

# 用注意力权重对本头的 V 加权求和,写回拼接结果的对应区间。
for j in range(SEQ_LEN):
weight = exp_scores[j] / total
for t in range(lo, hi):
output[i][t] += weight * x[j][t]

return output


def encoder_layer(x):
"""Post-LN 编码器层:MHSA 子层 + FFN 子层,各带残差与层归一化。"""
# 子层 1:多头自注意力。W_O 是单位矩阵,所以拼接结果直接就是输出。
attention = multi_head_attention(x)
first = [
layer_norm([x[i][d] + attention[i][d] for d in range(D_MODEL)])
for i in range(SEQ_LEN)
]

# 子层 2:W_1 = W_2 = I、b_1 = b_2 = 0,因此 FFN(y) = ReLU(y)。
ffn = [[max(0.0, v) for v in row] for row in first]
second = [
layer_norm([first[i][d] + ffn[i][d] for d in range(D_MODEL)])
for i in range(SEQ_LEN)
]

return second


def format_number(value: float) -> str:
"""保留两位小数,并把极其接近 0 的负数归零,避免输出 -0.00。"""
if abs(value) < 0.005:
value = 0.0
return f"{value:.2f}"


def solve():
"""读取 8 个实数,输出编码器层前向计算结果。"""
tokens = sys.stdin.buffer.read().split()
if len(tokens) < SEQ_LEN * D_MODEL:
return

values = [float(t) for t in tokens[:SEQ_LEN * D_MODEL]]

# 按行优先还原成 (seq, d_model) 的二维表:前 4 个是位置 0,后 4 个是位置 1。
x = [values[i * D_MODEL:(i + 1) * D_MODEL] for i in range(SEQ_LEN)]

result = encoder_layer(x)

# 输出同样按行优先平铺,同一个位置的分量挨在一起。
flat = [format_number(v) for row in result for v in row]
print(" ".join(flat))


if __name__ == "__main__":
solve()

NumPy 版本​

以下版本使用 NumPy。只有在评测环境允许第三方库时才能提交;CodeFun2000 的普通 Python 环境不一定安装 NumPy。

import sys

import numpy as np

# 题目固定的模型设定
SEQ_LEN = 2 # 序列长度
D_MODEL = 4 # 每个位置的特征维度
NUM_HEADS = 2 # 头数
D_K = D_MODEL // NUM_HEADS # 每头维度 = 2
LAYER_NORM_EPS = 1e-5


def layer_norm(x):
"""在最后一维做层归一化(gamma = 1,beta = 0,总体方差)。

x 的形状是 (seq, d_model),两次归约都在 axis=-1 上完成,
两个位置互不影响,正好对应题面“均值与方差在最后一个维度上统计”。
"""
mean = x.mean(axis=-1, keepdims=True)
# 除以 d_model 而不是 d_model - 1:总体方差
var = ((x - mean) ** 2).mean(axis=-1, keepdims=True)
# eps 放在根号里面,和题面公式 sqrt(Var + eps) 一致
return (x - mean) / np.sqrt(var + LAYER_NORM_EPS)


def multi_head_attention(x):
"""多头自注意力;权重矩阵都是单位矩阵,所以 Q = K = V = x。

把 (seq, d_model) 看成 (seq, h, d_k) 再转置成 (h, seq, d_k):
头维提到最前面之后,每个头就是一张独立的 (seq, d_k) 矩阵,
后面两次矩阵乘可以一次算完所有头。
"""
qkv = x.reshape(SEQ_LEN, NUM_HEADS, D_K).transpose(1, 0, 2) # (h, seq, d_k)

# 每个头的分数矩阵 (seq, seq):Q K^T / sqrt(d_k)
scores = (qkv @ qkv.transpose(0, 2, 1)) / np.sqrt(D_K)

# 减行最大值只改变数值稳定性,不改变 softmax 的结果;
# 不这么做的话 exp 会溢出(本题分数最大约 3535)。
scores -= scores.max(axis=-1, keepdims=True)
weights = np.exp(scores)
# 在序列维(每一行)上归一化
weights /= weights.sum(axis=-1, keepdims=True)

heads = weights @ qkv # (h, seq, d_k)

# 拼回 d_model:先还原成 (seq, h, d_k),再按最后一维展平;
# W_O = I,所以拼接结果就是注意力的输出。
return heads.transpose(1, 0, 2).reshape(SEQ_LEN, D_MODEL)


def encoder_layer(x):
"""Post-LN 编码器层:MHSA 子层 + FFN 子层,各带残差与层归一化。"""
attention = multi_head_attention(x)
first = layer_norm(x + attention)

# FFN 的权重是单位矩阵、偏置为零,于是 FFN(y) = ReLU(y)
second = layer_norm(first + np.maximum(first, 0.0))
return second


def format_number(value):
"""保留两位小数,并把极其接近 0 的负数归零,避免输出 -0.00。"""
if abs(value) < 0.005:
value = 0.0
return f"{value:.2f}"


def solve():
"""读取 8 个实数,输出编码器层前向计算结果。"""
tokens = sys.stdin.buffer.read().split()
if len(tokens) < SEQ_LEN * D_MODEL:
return

# 按行优先还原成 (seq, d_model):前 4 个是位置 0,后 4 个是位置 1
x = np.fromiter(map(float, tokens[:SEQ_LEN * D_MODEL]),
dtype=np.float64, count=SEQ_LEN * D_MODEL)
x = x.reshape(SEQ_LEN, D_MODEL)

result = encoder_layer(x)

# 输出同样按行优先平铺(ravel 就是 C 序展开)
print(" ".join(format_number(v) for v in result.ravel().tolist()))


if __name__ == "__main__":
solve()

示例推演​

样例 1​

输入的两个位置是 x0=(2,0,0,0)x_0=(2,0,0,0)、x1=(0,2,0,0)x_1=(0,2,0,0)。

第 0 个头(特征区间 [0,2)[0,2)):Q=K=V=[(2,0),(0,2)]Q=K=V=\bigl[(2,0),(0,2)\bigr]。

缩放后的分数矩阵:对角线为 4/2=2.8284274/\sqrt2=2.828427,另一侧为 00:

S=(2.828427002.828427)S=\begin{pmatrix}2.828427 & 0\\ 0 & 2.828427\end{pmatrix}

每行做 softmax,e2.828427≈16.9189e^{2.828427}\approx16.9189,e0=1e^{0}=1:

P=(0.9441930.0558070.0558070.944193)P=\begin{pmatrix}0.944193 & 0.055807\\ 0.055807 & 0.944193\end{pmatrix}

用 PP 对 VV 加权:

head0=(0.944193×20.055807×20.055807×20.944193×2)=(1.8883860.1116140.1116141.888386)\text{head}_0=\begin{pmatrix}0.944193\times2 & 0.055807\times2\\ 0.055807\times2 & 0.944193\times2\end{pmatrix} =\begin{pmatrix}1.888386 & 0.111614\\ 0.111614 & 1.888386\end{pmatrix}

第 1 个头(特征区间 [2,4)[2,4)):两个位置在该区间上都是 (0,0)(0,0),分数全为 0,softmax 得到均匀权重 (0.5,0.5)(0.5,0.5),但 VV 也是 0,所以该头输出全 0。

拼接后就是 WO=IW_O=I 的注意力输出:

attn=(1.8883860.111614000.1116141.88838600)\text{attn}=\begin{pmatrix}1.888386 & 0.111614 & 0 & 0\\ 0.111614 & 1.888386 & 0 & 0\end{pmatrix}

残差 + 第一次 LN:

x+attn=(3.8883860.111614000.1116143.88838600)x+\text{attn}=\begin{pmatrix}3.888386 & 0.111614 & 0 & 0\\ 0.111614 & 3.888386 & 0 & 0\end{pmatrix}

以第一行为例:均值 μ=(3.888386+0.111614+0+0)/4=1\mu=(3.888386+0.111614+0+0)/4=1,偏差为 (2.888386,−0.888386,−1,−1)(2.888386,-0.888386,-1,-1),

Var=2.8883862+0.8883862+12+124=2.783000\mathrm{Var}=\frac{2.888386^2+0.888386^2+1^2+1^2}{4}=2.783000

标准差 2.783000+10−5=1.668235\sqrt{2.783000+10^{-5}}=1.668235,把偏差逐个除以它:

LN1=(1.731401, −0.532530, −0.599436, −0.599436)\mathrm{LN}_1=\bigl(1.731401,\ -0.532530,\ -0.599436,\ -0.599436\bigr)

第二行是第一行的镜像(前两维互换),结果同理。

FFN(ReLU) 只保留正分量:

ReLU(LN1)=(1.731401, 0, 0, 0)\mathrm{ReLU}(\mathrm{LN}_1)=(1.731401,\ 0,\ 0,\ 0)

残差 + 第二次 LN:第一行的残差结果为

(3.462803, −0.532530, −0.599436, −0.599436)(3.462803,\ -0.532530,\ -0.599436,\ -0.599436)

均值 0.4328500.432850,方差 3.0609503.060950,标准差 1.7495571.749557:

LN2=(1.731837, −0.551785, −0.590026, −0.590026)\mathrm{LN}_2=(1.731837,\ -0.551785,\ -0.590026,\ -0.590026)

四舍五入后为 1.73, −0.55, −0.59, −0.591.73,\ -0.55,\ -0.59,\ -0.59。第二行镜像,于是输出:

1.73 -0.55 -0.59 -0.59 -0.55 1.73 -0.59 -0.59

注意第一次 LN 的第二维是 −0.53-0.53,而最终输出是 −0.55-0.55。差异来自第二次 LN:ReLU 只留下第一维,残差里其余分量(包括这一维)保持原值,于是整体均值和方差都变了,这一维会被重新标准化一次。

样例 2​

两个位置是 (1,0,1,0)(1,0,1,0) 与 (0,1,0,1)(0,1,0,1),两个头的输入完全相同。

两个头都得到分数矩阵

S=(1/2001/2)=(0.707107000.707107)S=\begin{pmatrix}1/\sqrt2 & 0\\ 0 & 1/\sqrt2\end{pmatrix} =\begin{pmatrix}0.707107 & 0\\ 0 & 0.707107\end{pmatrix}

softmax 权重为 (0.669762, 0.330238)\bigl(0.669762,\ 0.330238\bigr) 交错,两个头的输出相同:

head0=head1=(0.6697620.3302380.3302380.669762)\text{head}_0=\text{head}_1=\begin{pmatrix}0.669762 & 0.330238\\ 0.330238 & 0.669762\end{pmatrix}

拼接后每个位置是 (0.669762,0.330238,0.669762,0.330238)(0.669762,0.330238,0.669762,0.330238) 这样的交错图案。

残差 + 第一次 LN:第一行

x+attn=(1.669762, 0.330238, 1.669762, 0.330238)x+\text{attn}=(1.669762,\ 0.330238,\ 1.669762,\ 0.330238)

均值恰为 11,四个偏差是 ±0.669762\pm0.669762,方差 0.4485810.448581,标准差 0.6697690.669769,于是

LN1=(0.999989, −0.999989, 0.999989, −0.999989)\mathrm{LN}_1=(0.999989,\ -0.999989,\ 0.999989,\ -0.999989)

ReLU 只留下两个正分量:(0.999989,0,0.999989,0)(0.999989,0,0.999989,0)。

残差 + 第二次 LN:第一行残差为

(1.999978, −0.999989, 1.999978, −0.999989)(1.999978,\ -0.999989,\ 1.999978,\ -0.999989)

均值 0.50.5,偏差 ±0.999989\pm0.999989,标准差 ≈1\approx1,归一化后仍是 ±1\pm1,四舍五入输出:

1.00 -1.00 1.00 -1.00 -1.00 1.00 -1.00 1.00

复杂度分析​

解法时间复杂度空间复杂度说明
直译通用矩阵乘法O(seq2dmodel+seq⋅dmodel2)O(seq^2d_{model}+seq\cdot d_{model}^2)O(seq⋅dmodel)O(seq\cdot d_{model})需要处理 (1,seq,d)(1,seq,d) 的三维形状与通用矩阵乘
消掉单位矩阵、按区间切头O(h⋅seq2⋅dk)=O(seq2dmodel)O(h\cdot seq^2\cdot d_k)=O(seq^2d_{model})O(seq⋅dmodel)O(seq\cdot d_{model})本题采用;不做投影与输出投影
固定形状下O(1)O(1)O(1)O(1)seq=2seq=2、dmodel=4d_{model}=4、h=2h=2 都是常数

本题的规模完全固定,所以运行时间与输入数值无关,只有几十次浮点运算。


易错点总结​

1. Post-LN 与 Pre-LN 的顺序不能混​

本题是 Post-LN:先残差相加,再 LayerNorm。写成 Sublayer(LN(x))+x\mathrm{Sublayer}(\mathrm{LN}(x))+x 就是 Pre-LN,数值完全不同。题面还特别提示“拿掉残差或改成 Pre-LN,第一维不会落到 1.731.73”。

2. 方差要除以 dmodeld_{model},ε\varepsilon 放在根号内​

题目要求总体方差(除以 44),不是样本方差(除以 33)。公式是 Var+ε\sqrt{\mathrm{Var}+\varepsilon},不是 Var+ε\sqrt{\mathrm{Var}}+\varepsilon。ε=10−5\varepsilon=10^{-5} 很小,但恰好会在最后一位上体现出来。

3. 多头是“按区间切”,不是“隔一个取一个”​

第 tt 个头取特征区间 [t⋅dk,(t+1)⋅dk)[t\cdot d_k,(t+1)\cdot d_k)。本题第 0 个头是第 0、1 维,第 1 个头是第 2、3 维。如果按奇偶交错切,两个头的点积结果会完全不同。

4. softmax 在序列维上做​

分数矩阵是 (seq,seq)(seq,seq),每一行(一个 query 对所有 key)单独归一化。若错误地按列归一化,权重之和不等于 1,输出会偏大或偏小。

5. softmax 要先减行最大值​

exp⁡\exp 对大的分数会溢出。减去该行最大值不改变结果,却能保证最大的指数项是 e0=1e^0=1。

6. ReLU 作用在第一次 LN 的输出上,不是原始输入​

FFN 的输入是第一个子层的输出 y=LN(x+attn)y=\mathrm{LN}(x+\mathrm{attn}),所以 ReLU 处理的是 yy,残差加的也是 yy,即 LN(y+ReLU(y))\mathrm{LN}(y+\mathrm{ReLU}(y))。

7. 输出顺序与 -0.00​

输出必须按行优先平铺:位置 0 的 4 个数在前。浮点误差可能产生绝对值极小但符号为负的数,直接格式化会得到 -0.00,需要先把接近 0 的值归零。

8. 不要把“单位矩阵”理解成“可以跳过拼接”​

WO=IW_O=I 只说明“拼接后不需要再做线性变换”,两个头仍然要按区间拼回 4 维。漏掉任何一个头,维度就对不上。


扩展思考​

  • 如果改成 Pre-LN:结构变为 x←x+MHSA(LN(x))x\leftarrow x+\mathrm{MHSA}(\mathrm{LN}(x))、x←x+FFN(LN(x))x\leftarrow x+\mathrm{FFN}(\mathrm{LN}(x)),训练更稳定,但推理时的数值与本题不同。把 encoder_layer 里的残差与 LN 顺序对调即可验证。
  • 如果权重不是单位矩阵:只需在切头之前先算 xWQxW_Q、xWKxW_K、xWVxW_V,拼接之后再乘 WOW_O。代码结构不用变,只是多几次 4×44\times4 矩阵乘。
  • 如果 batch>1batch>1:每个样本独立走同一套计算,把外层循环加上 batch 维即可;注意力不会跨样本。
  • 如果换成 GELU:只改 FFN 的激活函数,GELU(v)=0.5v(1+tanh⁡(2/π(v+0.044715v3)))\mathrm{GELU}(v)=0.5v\bigl(1+\tanh(\sqrt{2/\pi}(v+0.044715v^3))\bigr),其余流程不变。
  • 数值稳定性:本题数据 ∣xi∣≤50|x_i|\le50,分数最大约 2×502/2≈35352\times50^2/\sqrt2\approx3535,e3535e^{3535} 必然溢出,所以“减最大值”这一步不是可选项。

相关题目​

自测​

加载评论中...