☰
机器学习中的乘积地图:从内积到Kronecker乘积实战指南
2026/10/2 11:31:46 网站建设 项目流程

机器学习里有一件特别容易被忽略的事:乘积。从最入门的线性回归到最前沿的大模型,底层几乎全是乘法在支撑。很多人学矩阵、学张量的时候,总觉得“不就是点乘、矩阵乘法吗”,但真正落到模型实现、调试梯度、设计网络结构时,经常搞混内积、Hadamard乘积、外积、Kronecker乘积该用在哪儿。更现实的是,面试时被问到“Attention为什么除以根号d_k”,如果你只答“防止数值过大”,基本暴露出没往深处想过乘积的数学性质。

这篇我围绕机器学习中最常见的几种乘积做一个系统梳理,核心是我在实际写模型、调参、复现论文过程中反复用到、也反复踩坑的东西。不适合理科小白从头看线性代数,适合已经会一些基本概念、但想把这些乘法彻底串起来的人。我会讲清楚每个乘积的定义、几何直觉、在模型里的典型位置,以及它们为什么只能用在那个位置、换一种行不行。

1. 为什么机器学习中“乘积”无处不在:从高维空间的一张地图说起

要理解机器学习的乘积,得先跳出“数组相乘”的机械操作,回到一个更根本的问题:模型本质在学什么?

一个训练好的模型,内部实际上存储了一张从输入空间到特征空间、再到输出空间的高维映射。图像分类里,“猫”对应的不是一个具体的像素矩阵,而是一个高维特征区域。模型要做的事情,是把输入图像经过层层变换,投影到某个特征空间中,让“猫”的分布集中在一处,“狗”的分布在另一处。这种投影操作,数学上就是线性变换,而线性变换的实现载体就是矩阵乘法。可以说,机器学习模型再复杂,核心骨架也就是一堆矩阵乘法中间夹着非线性激活函数。

乘积承担的角色远比我们想象的丰富:

  • 内积(点积):度量方向的一致性,用在相似度、注意力机制、线性层中;
  • 矩阵乘法:批量执行线性变换,是全连接层、卷积层、Transformer的基础运算;
  • Hadamard乘积(逐元素乘):做掩码、门控、非线性缩放,在LSTM、LayerNorm、Dropout里反复出现;
  • 外积和Kronecker乘积:构造结构化的权重矩阵、特征交互,在推荐系统、双线性池化、张量分解里很常用。

这些乘积并非彼此孤立。我在跑模型时最大的体会是:选哪一种乘法,本质上是在选择“如何定义特征之间的交互关系”。点积是“方向相似度”,逐元素乘是“同位置的信息筛选”,外积是“构造组合特征矩阵”。背后的空间含义完全不同。

这篇文章的结构就是围绕这张“乘积地图”展开。每讲一种乘积,我会给出定义、计算方式、模型中的实例,以及选择它的理由和常见坑。既然标题是“常见的乘积”,我会尽可能覆盖完整,让你读完以后能对模型里的每个乘法运算都有清晰的“归属感”。

2. 内积与矩阵乘法:神经网络的脊柱,也是初学最容易混淆的起点

2.1 内积的本质:方向上的“共振”

先看内积。给定两个同维向量a = [a1, a2, ..., an]和b = [b1, b2, ..., bn],内积定义为:

a · b = Σ ai * bi

这个人人都知道,但很少有人从几何意义上真正“感觉”到它。内积还有一个等价形式:

a · b = |a| |b| cos(θ)

当两个向量同方向时,cos(0)=1,内积最大;当两个向量垂直时,cos(90°)=0,内积为零;反方向时,内积为负。所以内积天然是一个相似度度量,前提是你先把向量归一化到单位长度。

我在面试候选人的时候特别喜欢问一个问题:Word2Vec里计算词向量相似度,为什么用余弦相似度而不是直接算欧氏距离?答案就在于余弦相似度本质上是归一化后的内积,它丢掉了向量长度信息,只保留方向信息,而词向量的模长往往与词频相关,方向才真正编码语义。这个细节直接体现了对“内积”的理解深度。

2.2 从单一内积到全连接层

全连接层的数学形式是y = Wx + b,其中W是m × n矩阵,x是n维向量,输出y是m维向量。

拆开看:y的第i个分量,就是W的第i行向量与x做内积,再加上偏置。所以一个全连接层,本质上就是让输入向量和权重矩阵的每一行都做一次内积。

以手写数字识别为例,假设输入是 784 维的像素向量,中间层是 128 个神经元,那权重矩阵 W 的尺寸就是128 × 784。W的第i行可以理解为“第i个特征检测器”,它对输入进行加权求和,得到该特征在输入中出现的强度。

m × n矩阵乘以n维向量,计算量是m*n次乘加,复杂度 O(mn)。这个复杂度在模型规模变大后是灾难性的,所以工程上会利用 GPU 做并行化,也就是批处理:把多个输入向量堆叠成B × n的矩阵,一次性完成B个样本的线性变换,这就是批量矩阵乘法(batch matmul)。

2.3 矩阵乘法的维度之眼

矩阵乘法C = A @ B要求 A 的最后一个维度等于 B 的倒数第二个维度。在我实际调试模型的过程中,维度不匹配是出现频率最高的报错来源,比任何数学错误都常见。总结一个自查清单:

  • A 的形状是(..., M, K),B 的形状是(..., K, N),结果的形状是(..., M, N);
  • 广播规则在...部分生效,但两个矩阵的K必须严格相等;
  • 别把A @ B和B @ A搞混,它们的结果几乎总是不一样,甚至可能根本无法相乘;
  • 在 PyTorch 中,torch.matmul支持高维批量乘,torch.mm只支持二维矩阵乘,二者用法别混。

2.4 计算图中的矩阵乘法梯度

反向传播中,矩阵乘法层的前向是C = A @ B,如果损失对C的梯度是dC,那么:

dA = dC @ B.T dB = A.T @ dC

注意这个转置方向的对称性,特别容易记错。我当时学反向传播时,自己推导过一遍,远比自己死记结论管用。建议你也动笔推一次,就三行公式,推完以后再也不会忘。

3. Hadamard乘积:逐元素乘法的“门控”与“掩码”江湖

3.1 定义与直觉

Hadamard乘积,也叫逐元素乘积、Schur乘积,符号是⊙,定义是:

C[i][j] = A[i][j] * B[i][j]

要求 A、B 形状完全相同。它不改变矩阵结构,只对每一位元素进行独立缩放。矩阵乘是“全局混合”,Hadamard乘积是“局部调制”。两者对比,矩阵乘好比把食材搅拌成一道菜,逐元素乘像是往每道菜上撒不同的调料。

正是这种“按位缩放”的性质,让 Hadamard 乘积在神经网络中成了实现门控机制和掩码机制的首选。

3.2 门控:LSTM 和 GRU 的隐式主角

LSTM 的遗忘门、输入门、输出门,公式核心就是:

f_t = σ(W_f · [h_{t-1}, x_t] + b_f) c_t = f_t ⊙ c_{t-1} + i_t ⊙ c_hat_t

这里的⊙就是 Hadamard 乘积。遗忘门 f_t 是一个取值在 0 到 1 之间的向量,它逐元素地决定“保留多少旧记忆、遗忘多少旧记忆”。如果换成矩阵乘法,就会产生不同位置之间的交叉干扰——弄丢“保留与遗忘”这个语义本身。

我早期用 LSTM 做时序预测时,曾试过把逐元素乘改成矩阵乘,序列建模能力不升反降,还引入了一堆额外参数。因为 LSTM 的 C 状态携带了长期记忆,门控需要精确地对每个维度进行独立放行,跨维度的混合反而会毁掉记忆通道的纯净性。这验证了一个原则:在序列建模中,维度间信息交换由权重矩阵负责,门控只做逐维度的自适应缩放,两者各司其职。

3.3 掩码:Transformer 里的 padding mask

Transformer 的注意力计算中,padding mask 的操作本质上就是 Hadamard 乘积(或等价的加法掩码):

# 把 padding 位置对应的注意力分数设为 -inf scores = scores.masked_fill(mask == 0, -1e9)

这个masked_fill操作改的是待 softmax 的分数,但很多实现里瓶颈在于 mask 也常作用于特征矩阵本身。BERT 预训练中[MASK]token 的隐藏状态会乘以一个掩码,这也是逐元素乘。Dropout 就更直接了,训练时把特征向量逐元素地乘以 0/1 mask,本质也是 Hadamard 乘积:

h_dropped = h ⊙ mask

所以,你在看 LSTM、Transformer、Dropout 的源码时,只要见到形状相同的两个张量逐位相乘,基本就是 Hadamard 乘积在发挥作用。识别它,是读懂模型代码的快捷方式。

3.4 反向传播中 Hadamard 乘积的“暗线”

在反向传播里,Hadamard 乘积其实是一条贯穿始终的暗线。

假设前向传播是y = Wx + b,经过激活函数z = σ(y),损失对z的梯度是dz。那么:

dy = dz ⊙ σ'(y)

这里的⊙ σ'(y)就是逐元素乘。Sigmoid、Tanh 这类激活函数的导数矩阵是对角矩阵,矩阵形式下梯度传播是dz @ diag(σ'(y)),但因为对角矩阵的特殊结构,实际实现里直接用逐元素乘替代。

这个细节解释了为什么 ReLU 在反向传播中那么高效——它的导数是 0 或 1,逐元素乘后非零元素原样通过。同时,这也是梯度消失的根源之一:多层 Sigmoid 反向传播时,每一层都要乘一个σ'(y),而σ'(y)的最大值只有 0.25,连乘几次梯度就指数级缩小。你把0.25^5算一下,只剩约 0.001,十层以上基本看到的就是一片梯度沙漠。

3.5 与矩阵乘的关键区分

运算Shape 要求语义典型场景
矩阵乘法A 的最后一维等于 B 的倒数第二维维度间信息混合全连接、Attention、卷积
Hadamard 乘积两者 shape 完全一致逐维度独立缩放门控、掩码、激活导数

初学的时候,把这两种乘法的语义混淆,是很多 bug 的根源。比如在自定义模型中,想对特征做“通道级缩放”,结果用了@操作,导致输出长度都不对,还排查了半天。

4. 外积与Kronecker乘积:构造结构化权重的高级工具

4.1 外积:从两个向量构造一个交互矩阵

外积的定义很简洁:向量u ∈ R^m和v ∈ R^n的外积得到一个m × n矩阵:

u ⊗ v = u * v^T

其中(u ⊗ v)[i][j] = u[i] * v[j]。

内积是两个向量“比较方向”,求的是一个标量,衡量相似度;外积则是把两个向量“编织”成矩阵,矩阵的每个位置代表两个分量之间的交互强度。如果说内积是问“你俩像不像”,外积就是在问“你俩搭配能产生什么”。

外积在推荐系统里非常常见。用户 embedding(u)和物品 embedding(v)的外积构成交互矩阵M[i][j] = u[i] * v[j],表示用户第 i 个偏好维度与物品第 j 个属性维度的匹配度。这比直接做内积(得出一个标量分数)保留了更多的交互信息。Neural Collaborative Filtering 就是在外积、内积、拼接等方法之间做对比,结论是不同特征交互结构确实影响效果上限。

4.2 外积在位置编码里的妙用

Transformer 的正弦位置编码公式:

PE(pos, 2i) = sin(pos / 10000^(2i/d_model)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

如果同时处理batch和seq_len,你会看到一个形状为(batch, seq_len, d_model)的三维张量,它事实上就是位置索引向量与维度索引向量的某种外积组合拓展。

我对这个公式印象深刻,是因为它用不同频率的三角函数给每个位置生成了一串“指纹”。换一个角度理解:位置向量由不同频率的正弦波叠加而成,高频正弦波在相邻位置差异明显(能区分近邻),低频正弦波在远处才产生明显变化(能区分长距离)。内积位置编码的“外积构造”让 Transformer 能同时感知绝对位置和相对位置关系。

4.3 Kronecker乘积:矩阵中的“套娃”

Kronecker乘积的定义是:矩阵A ∈ R^{m×n}和B ∈ R^{p×q}的 Kronecker 乘积得到一个mp × nq的分块矩阵:

A ⊗ B = [[a11*B, a12*B, ..., a1n*B], [a21*B, a22*B, ..., a2n*B], ... [am1*B, am2*B, ..., amn*B]]

本质就是把 A 的每个元素替换成“该元素整体乘以 B”的小矩阵块。它在机器学习中的应用通常和张量分解、低秩近似有关。

KRON 分解的思路是:大规模权重矩阵 W 可以表示成若干小矩阵的 Kronecker 乘积之和。这样做的好处是参数量大幅下降。一个1024 × 1024的权重矩阵有约 100 万个参数,如果表示成四个32 × 32的 Kronecker 因子乘积,参数总量只有4 × 32 × 32 = 4096——参数减少了 250 倍。这在边缘部署、模型压缩场景里意义重大。

4.4 Kronecker乘积的经典恒等式

Kronecker乘积最经典的恒等式是向量化恒等式:

vec(AXB) = (B^T ⊗ A) vec(X)

其中vec是把矩阵按列拉直成向量。这个恒等式在矩阵方程求解(如 Lyapunov 方程、Sylvester 方程)、高斯过程、稀疏线性代数中非常有用。

我在实现高斯过程回归时遇到过一次性能瓶颈:需要求解涉及多个矩阵的线性系统,直接展开计算不仅内存爆炸,数值稳定性也差。后来利用 Kronecker 结构加速核矩阵求逆,把原来的O(n^3)降到O(n^{1.5})级别。这是乘积类型选对了,性能差一个数量级。

4.5 低秩分解:乘积思维的现实延伸

提到 Kronecker、外积,就不得不顺带聊低秩分解。低秩逼近的核心思想是:一个高维矩阵可以用两个低维矩阵的乘积近似。

W ≈ U * V^T

其中U ∈ R^{m×r},V ∈ R^{n×r},r << min(m, n)。LoRA(Low-Rank Adaptation)就是这样,它把大模型的权重更新量限制在一个低秩子空间内:

W' = W_0 + ΔW = W_0 + B * A

这个结构直接决定了大模型微调的资源需求。把“更新量”限制在低秩空间,本质上是先验假设:大模型微调时权重变化主要发生在少数几个重要方向上。我在实际微调 7B 模型时,设置r=8就已经能逼近全参微调的效果,rank 提升到 16 带来的收益相当有限,但显存和磁盘开销明显增加。这就是典型的“用矩阵乘积结构换效率”的工程实践。

5. 缩放点积、Attention与数值稳定性:乘积在深度模型中的实战

5.1 为什么是缩放点积:从方差推导说起

Transformer 中的 Attention 核心公式:

Attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) V

问题来了:为什么要除以sqrt(d_k)?直觉回答“防止数值过大”只是表象。

严谨推导是这样的:假设q和k是均值为 0、方差为 1 的独立随机变量。点积的均值是 0,方差是:

Var(q·k) = d_k * Var(q) * Var(k) = d_k

如果不做缩放,那么点积的方差是d_k。当d_k很大(比如 512、1024)时,点积的分布存在长长的尾部。一些分数会特别大,另一些特别小。经过 softmax 后,大的值对应概率接近 1,小的值概率趋近 0,梯度在饱和区几乎为零,模型怎么训都动不了。

除以sqrt(d_k)后,方差被拉回 1:

Var(q·k / sqrt(d_k)) = d_k / d_k = 1

softmax 的输入分布保持相对温和,梯度能正常流动。我之前写过一个快速验证实验:当d_k=1时,softmax 输出近似均匀分布;当d_k=100但不缩放时,输出几乎变成 0/1 分布;除以sqrt(100)之后,恢复到相对可学习的分布状态。

5.2 点积注意力实现中的两个魔鬼细节

实现 QK^T 时,Q的形状是(batch, num_heads, seq_len, d_k),K的形状是(batch, num_heads, seq_len, d_k)。

第一步需要转置K:K.transpose(-2, -1),得到(batch, num_heads, d_k, seq_len)。Q @ K^T得到注意力分数矩阵(batch, num_heads, seq_len, seq_len)。

第二个容易踩的坑是 mask 的加法。padding mask 在 softmax 之前做:把 padding 位置对应的分数设为-inf或一个绝对值很大的负数。这里有个细节——如果全用-inf,数值稳定性更好,但某些框架/算子对-inf的 softmax 处理方式不同,可能导致 NaN;更稳妥的做法是加一个绝对值足够大的负数(如-1e9),既能达到遮断效果,又避免 NaN。

5.3 混合精度与乘积的浮点陷阱

训练大模型时,混合精度已经是标配。FP16 能省一半显存,但代价是溢出风险。FP16 的最大有限表示约 65504。如果你的注意力分数(QK^T)稍微大一点,除以sqrt(d_k)之前就已经溢出为 inf,那 softmax 输出就是 NaN。

我在用 FP16 做训练时遇到过一次崩溃,最后的定位结果就是注意力得分过大溢出。Pytorch 的 AMP(Automatic Mixed Precision)通过一个 loss scaling 技术:前向计算前放大 loss,反向传播算完后缩小梯度,保证中间梯度不进小数下溢区间。但如果你自己在自定义 CUDA 算子或手写反向传播,没做 loss scaling,问题就会冒出来。

这里列一个浮点精度的自查表:

精度最大有限值最小正规格化数典型问题
FP32~3.4e38~1.2e-38大多数场景下足够
FP1665504~6.1e-5中间结果容易溢出/下溢
BF16~3.4e38~1.2e-38范围接近 FP32,但精度低

BF16 目前是深度学习训练的宠儿,因为它先把 FP16 的溢出问题解决了一半——范围比精度更重要,因为在大多数张量运算中,范围超限导致的 NaN 会让整个训练崩溃,而精度损失只影响训练质量的一小部分。

5.4 softmax 的 log-sum-exp 技巧也是乘积的守卫

softmax 的常见实现:

softmax(x)_i = exp(x_i) / Σ_j exp(x_j)

如果x_i都很大(比如都是 500),exp(500)在 FP32 里直接溢出为 inf,结果变成 NaN。工程上的做法是:

softmax(x)_i = exp(x_i - max(x)) / Σ_j exp(x_j - max(x))

减去最大值后再算 exp,所有指数都落在(-inf, 0]区间,不会溢出,而且 softmax 的结果在数学上完全等价。这就是 log-sum-exp 技巧。

在手写 attention 时,我见过不少初学者在 softmax 这一步栽跟头:分数没做最大减法,d_k 稍大一点,再加上 FP16 精度,训练第一个 step 就 NaN。这个细节比损失函数的公式更容易忽略,却比损失函数更致命。

5.5 实际调参中遇到的乘积相关坑

最后分享一个我实际踩过、排查了很久的坑。

场景是训练一个 Transformer 做序列推荐。模型前几轮 loss 正常下降,到第 5 轮左右突然变成 NaN。一开始怀疑学习率太大,降了以后还是 NaN。后来怀疑是数据里有脏数据,清洗之后依然复现。

最后排查到根因:某一层的 hidden state 增长过快,导致残差连接中的加和溢出。而增长过快的原因是 LayerNorm 之前没有做 scale 约束,矩阵乘法出来的值域在训练过程中逐渐变大,最终超过 FP16 可表示范围,一旦到 65504 就翻车。解决方式是调整初始化,把 attention 输出层的参数初始化标准差调小,同时给残差分支加一个 learnable scale。

这件事给了我一条宝贵的经验:在混合精度训练中,别把所有问题都归因于优化器或数据,很多 NaN 的根源都在某种乘积/加和操作的溢出上。排查思路是定位到第一次出现 inf/NaN 的张量,然后往前倒推是哪个运算把它撑爆了。常规的数值稳定性检查清单里,“最大值在哪一步产生”“那一步是否有平方/求和”是必须回答的两个问题。

6. 从乘积视角重新审视深度学习的线性变换本质

绕了一大圈,回到一个最基础的认知:深度学习模型的表达能力,来自线性变换和非线性激活的交替堆叠。而线性变换的载体就是各种乘积。矩阵乘法负责跨维度信息混合,Hadamard乘积负责逐维度门控缩放,外积负责构造特征交互,Kronecker乘积负责结构化低秩表达。你把模型里的每种乘法都看清了,模型对你来说就基本透明了。

以 CNN 为例,卷积核滑动计算的本质就是局部区域与卷积核做内积,多个卷积核就是多个内积模板;池化是局部窗口内的“点积变体”(average 是权重全 1 的内积);全连接层是全局矩阵乘法;BatchNorm 是逐通道缩放(Hadamard乘积)。深度模型里没有一个角落能脱离乘积而存在。

我当年入门时最受益的一个习惯是:读模型源码时,看到一个运算,先在旁边标注它属于哪一类乘积、它的输出 shape 是多少。一段时间以后,不同类型的乘积在我脑子里就自动有了各自的“声音”。你如果也打算深入机器学习方向,强烈建议从这个习惯开始。

按照我自己的经验,与其花大量时间背各种网络架构,不如先把线性代数里的各种乘法玩熟。你可能会发现,很多看起来花哨的模型结构,本质不过是几种乘积的组合变形,并没有想象中那么神秘。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询