Qwen3-ASR 课程 · 第 7 课(第 6 课进阶)

用向量走一遍 Qwen3-ASR —— 每一站的矩阵乘法

⏱ 约 30 分钟(建议备纸笔)· 前置:架构精读 · 需要:矩阵乘法、点积、softmax 概念

上一课给了框图和数字,这一课给数据本身。为了让每个数都能手算,全部用玩具维度:4 维向量代替 128/1024 维,2 个词元代替 125 个音频嵌入。形状逻辑与真实模型完全一致,只是维度缩小——每个数字都可以用计算器复核。

约定 向量按行书写,矩阵乘法 x @ W:(1×4) @ (4×3) → (1×3)。权重矩阵都是模型训练学出来的参数,这里取的是"能让故事讲清楚"的一组。

第 1 站 · mel 帧向量 → 线性层

AuT 的入口把 128 维 mel 帧投影到模型空间,就是一个矩阵乘法。玩具版:一帧 mel x = [0.2, 0.8, 0.5, 0.1],权重 W_e(4×3):

W_e =100.5
010.5
0.501
00.50

逐维算输出(第 1 维展开,其余同法):

out₁ = 0.2×1 + 0.8×0 + 0.5×0.5 + 0.1×0 = 0.45
out₂ = 0.2×0 + 0.8×1 + 0.5×0 + 0.1×0.5 = 0.85
out₃ = 0.2×0.5 + 0.8×0.5 + 0.5×1 + 0.1×0 = 1.00

x @ W_e = [0.45, 0.85, 1.00]     (1×4)@(4×3) → (1×3)

真实版:(1000 帧 × 128) @ W(128 × 1024) → 1000 × 1024。所谓"特征提取进模型",逐元素看就是这样的乘加。

第 2 站 · 自注意力:整场戏的矩阵演算

第 2 课的 Q/K/V 现在《来真的》。玩具设定:两个词元,「苹果」= [1,0,0,0.5],「手机」= [0,1,0.5,0](4 维嵌入,E 的两行)。权重矩阵 4×2:

W_Q =01W_K =10W_V =10
100101
000000
000011

右乘得三件套(每个词元一行):

Q = E @ W_Q = [[0,1],[1,0]]      K = E @ W_K = [[1,0],[0,1]]
V = E @ W_V = [[1.5, 0.5],
               [0,   1  ]]

注意:苹果的 Q = [0,1] 和手机的 K = [0,1] 同方向——待会儿它们会互相吸引(权重矩阵是学出来的,这里特意选了"苹果会去找手机"的一组)。

打分与归一(玩具省略 √d 缩放;softmax 定义为指数归一,使每行和为 1):

S = Q @ Kᵀ = [[0, 1],        softmax 每行 → W = [[0.269, 0.731],
              [1, 0]]                            [0.731, 0.269]]

加权混合 V,得到吸收了上下文的新表示:

苹果′ = 0.269×[1.5,0.5] + 0.731×[0,1] = [0.403, 0.866]
手机′ = 0.731×[1.5,0.5] + 0.269×[0,1] = [1.097, 0.634]
看清楚"吸收"是怎么发生的 苹果′ 里 0.866 那一维,几乎全部来自手机的 V₂=1——矩阵乘法把"73% 的注意力权重"变成了逐维的数值混合。第 2 课说的"borrowed 信息",机制就是这个加权求和。

下面的演算器可以一步步重放上面全过程(数字与手算完全一致):

真实版:125 个音频嵌入(或音频+文字的完整序列)做同样的运算,但规模是 Q,K,V ∈ R^{125×1024}、16 头并行、堆 24 层——每一层每一头,都在重复"打分 → softmax → 加权"这三步矩阵乘法。

演算器是看,现在用 numpy 亲手算一遍同样的数字。下面的块与页面里其他块共享变量(notebook 式),可以随便改——比如把 E[0](苹果的嵌入)改成别的向量,看它的注意力往哪漂:

import numpy as np E = np.array([[1,0,0,0.5], # 苹果 —— 改我,看注意力漂移! [0,1,0.5,0]]) # 手机 WQ = np.array([[0,1],[1,0],[0,0],[0,0]]) WK = np.array([[1,0],[0,1],[0,0],[0,0]]) WV = np.array([[1,0],[0,1],[0,0],[1,1]]) Q, K, V = E@WQ, E@WK, E@WV S = Q @ K.T # ① 打分 W = np.exp(S) / np.exp(S).sum(axis=1, keepdims=True) # ② 每行 softmax out = W @ V # ③ 加权混合 print("打分 S =\n", S) print("注意力权重 =\n", W.round(3)) print("新表示 =\n", out.round(3))
自己动手的三件事 ① 核对:输出应与上面手算/演算器完全一致(0.269 / 0.731 / 0.403…)。② 把 E[0] 改成 [0.3, 0.9, 0, 0.4] 再运行——苹果的注意力会翻向自己(0.646),因为它的 Q 不再和手机的 K 同向。③ 试着加第三个词元(给 E 加一行,权重矩阵不用动——形状为什么不用改?)

第 3 站 · 下采样:8 帧并 1

AuT 的 8 倍下采样,数学上是把相邻 8 帧合成 1 个向量。玩具版 4 并 1,最简单的均匀平均等价于左乘一个 (1×4) 矩阵:

X = [[0.2, 0.8, 0.5],      M = [0.25, 0.25, 0.25, 0.25]
     [0.4, 0.7, 0.4],
     [0.6, 0.6, 0.3],      y = M @ X = [0.5, 0.65, 0.35]
     [0.8, 0.5, 0.2]]      (4帧 × 3维 → 1 × 3)

真实的下采样不是均匀平均,而是学出来的加权组合(M 的四个数可训练,不同输出维各有各的 M),思想相同:用一个矩阵把时间轴压短 8 倍。1000 帧 → 125 个音频嵌入,就是这么来的。

第 4 站 · 投影器:编码器空间 → LLM 空间

又是矩阵乘法,唯一目的是对齐两边的维度。玩具版:编码器输出 z(3 维)→ LLM 空间(2 维),W_p(3×2):

W_p = [[1, 0],        z @ W_p = [0.5, 0.65, 0.35] @ [[1,0],
       [0, 1],                                       [0,1],
       [1, 1]]                                       [1,1]]
                = [0.5+0.35, 0.65+0.35] = [0.85, 1.00]

真实版:(125 × 1024) @ W(1024 × 2048) → 125 × 2048。产出的 125 个向量,马上要以"词元"身份进入 LLM。

第 5 站 · 词嵌入查表 = one-hot 矩阵乘

文字这边怎么进 LLM?词表里每个词元一行嵌入,查表这个动作,线性代数视角就是 one-hot 向量左乘嵌入矩阵:

词表(节选): 今=0, 天=1, 好=2          嵌入矩阵 E_v (3×2):
                                        E_v = [[0.2, 0.9],
onehot(今) = [1, 0, 0]                         [0.8, 0.4],
                                               [0.5, 0.5]]
emb(今) = [1,0,0] @ E_v = E_v 第 0 行 = [0.2, 0.9]

现在把两路拼起来:音频嵌入(第 4 站的 125 个向量)替换掉 <|audio_pad|> 占位符的位置,后面接上文字词元的嵌入——一整条序列进入 LLM。对话模板里的占位符,在张量层面就是"这几行来自投影器,那几行来自嵌入表"。

第 6 站 · LLM 输出:logits → softmax → 选词

LLM 最后一层把隐藏态投到词表上打分。玩具版:隐藏态 h(2 维),词表 4 个词{今, 天, 好, <eos>},W_o(2×4):

W_o = [[1,   0.2, 0.1, 0  ]        logits = h @ W_o
       [0.1, 1,   0.5, 0.2]]             = [0.29, 0.94, 0.47, 0.18]

softmax(logits) = [0.200, 0.382, 0.239, 0.179]   (总和 = 1)
                        ↑ 最大                  → 下一个词元 = 「天」

真实版:(1 × 2048) @ W(2048 × 151936)——对 15 万词元各打一分,softmax 后按概率采样(或取最大)。

同样,numpy 版三行搞定,而且选词规则就在你手里——改 h 或 Wo,看「天」的位置会不会被翻盘:

import numpy as np h = np.array([0.2, 0.9]) # LLM 最后一层的隐藏态(改我!) Wo = np.array([[1, 0.2, 0.1, 0 ], # 输出层:2 维 → 4 个候选词 [0.1, 1, 0.5, 0.2]]) logits = h @ Wo p = np.exp(logits) / np.exp(logits).sum() # softmax:变成概率 vocab = ["今", "天", "好", ""] for tok, lg, pr in zip(vocab, logits, p): print(f"{tok:>5}: logit {lg:.2f} 概率 {pr:.3f}") print("→ 下一个词元:", vocab[p.argmax()])

第 7 站 · 自回归:把输出接回输入

生成"今天"不是一步完成的,是循环:

轮输入序列(嵌入)输出分布选中
1[音频×125] … language Chinese <asr_text>今 0.62 · 天 0.21 · …今
2[音频×125] … <asr_text> 今天 0.71 · 好 0.12 · …天
3[音频×125] … <asr_text> 今 天<|im_end|> 0.83 · …结束

每轮把上一步选中的词元查嵌入表(第 5 站)、拼到序列尾部、整条重新前向——注意音频那 125 行完全不变,变的是文字部分。第二轮能预测"天",靠的是第 2 站的注意力:新来的"今"能看到"自己前面是 <asr_text>,身后跟着 125 个音频嵌入",于是从声音里取信息。

玩具 ↔ 真实:维度对照总表

量玩具Qwen3-ASR-1.7B 真实值
mel 帧向量4 维128 维 × 1000 帧(10 秒)
模型隐藏维(AuT)3 维1024
注意力 Q/K/V2 维 × 2 词元每头 128 维 × 16 头 × 序列全长,堆 24 层
下采样4 并 1(均匀)8 并 1(可学习加权)→ 125 嵌入
投影器3 → 2 维1024 → 2048
词表 / 输出层4 词 / 2×4151,936 词 / 2048×151,936
softmax无缩放打分先除以 √128(head_dim)
自回归2 步演示直到 <|im_end|>,平均每字一轮前向
checkpoint 从 160,000 个采样点到一句话,全程只剩两种运算:矩阵乘法(变换/投影/打分/混合)和 softmax(归一成概率)。注意力、下采样、投影、嵌入查表、输出层——全是这两样的组合。你现在具备读懂 modeling_qwen3_asr.py 每一行形状注释的数学基础了。

检索练习

本课主看材料

去看(约 60 分钟,可 1.5 倍速)

Karpathy: Let's build GPT (from scratch)(Zero to Hero 系列之一)

他在代码里手写的正是你今天纸上算的这些东西:x @ W_q、softmax(q @ k.T)、w @ V——看完你就把"纸上的矩阵"和"PyTorch 里的张量"接上了。这对下一课(跑推理)和微调课是直接投资。

我是你的老师,别客气 哪个矩阵乘出来和你手算的不一致,或者想看"16 头怎么拼回 2048""√d 缩放为什么需要"这类下一步,直接问——这两问都值得单独展开。