资讯动态

机器学习中的张量乘法全解析:矩阵乘、点积与逐元素乘的实战排错指南

发布时间:2026/10/1 9:34:16 来源:尧图企业网站定制
上周调一个带注意力机制的小模型前向跑通、loss 也在降可验证集指标就是迟迟不动。我把输入输出 shape 打印出来看了十几遍最后才发现问题出在一个看似不起眼的地方注意力打分那里误用了逐元素乘*把矩阵乘写没了。模型没报错纯粹是因为那两个维度的形状恰好一致。那件事以后我养成了一个习惯——凡是看到代码里有乘法先停下来问一句这里到底该用哪种 product机器学习中的“乘积product”看起来就是小学数学但等到了向量、矩阵、张量这一层局面完全变了。点积、逐元素乘、矩阵乘、外积、克罗内克积……名字长得像语义却千差万别选错一个轻则报维度错误重则模型练了一个星期才发现方向一开始就是歪的。这篇内容就是把我在实际项目里反复踩过的几类“乘积”一次讲透按形状规则、模型中的应用、代码实操和排错经验四条线展开。不管你是刚入门机器学习的新手还是被维度问题折磨过的调参选手这篇文章应该都能帮你少走几次弯路。1. 先盘一盘机器学习里到底藏着哪些“乘积”1.1 为什么“乘法”值得单独拎出来看神经网络的一次前向传播本质上是大量乘法和加法堆叠出来的计算图卷积是局部点积的滑动窗口全连接层是矩阵乘注意力机制核心又是缩放点积。梯度反向传播更是把乘法的作用推到了极致——链式法则要求我们不断做“上游梯度 × 局部雅可比”于是几乎每一层 update 都和乘积纠缠在一起。所以在学习机器学习时与其死记某个库的 API不如先把“乘积”这个最基本的操作在张量层面的含义搞通。我见过不少同学Linear层的参数量能背但问(batch, seq_len, hidden)的三维输入怎么和(hidden, out)的权重相乘就卡住了。这就是对矩阵乘法的形状规则没有形成肌肉记忆。把这个基础打牢回头再看 Transformer、FM、图神经网络都要轻松得多。1.2 五类常见乘积的定义与直观含义我们先把这几类乘积按“参与运算的对象”和“结果的形状”做一个清晰的分桶。第一类是标量与标量、标量与张量的乘法。这个最简单就是缩放把张量整体放大或缩小固定倍数在归一化、学习率调度里大量出现。第二类是逐元素乘积也叫 Hadamard 乘积记号是 ⊙。两个形状相同的张量对应位置相乘结果形状不变A ⊙ B C(C_ij A_ij × B_ij)这类乘法的语义是“按位置配对、互不打扰”常用于特征门控、掩码操作以及注意力中对某些维度做软性缩放。第三类是点积也叫内积两个长度相同的向量对应元素相乘后求和结果是一个标量。它衡量的是两个向量在同一方向上的投影叠加注意力打分、相似度计算都靠它。第四类是矩阵乘法记号就是普通的乘号在张量语境下指代 。两个矩阵要求内维一致结果形状由外维决定A(m×n) B(n×p) → C(m×p)它处理的是“两个维度空间之间的线性映射”全连接层、卷积展开、多头注意力里的特征投影全是它。第五类是外积和克罗内克积。外积把一个 m 维向量和一个 n 维向量的所有元素两两相乘得到 m×n 矩阵克罗内克积则作用于两个矩阵按分块方式把整个矩阵铺开。这两类通常在特征交叉、子空间建模、多变量概率模型里出现。1.3 一张表建立乘积全局图谱乘积类型常见记号参与对象结果维度典型语义标量乘法c·x标量 × 张量与张量相同缩放逐元素乘积⊙形状兼容的按位张量与输入相同或广播后相同对应位置组合点积a·b两个一维向量标量相似度度量、投影矩阵乘法矩阵/张量内维匹配外维拼接线性映射、特征变换外积⊗两个一维向量二维矩阵特征工程、二阶交叉克罗内克积⊗两个矩阵分块放大矩阵结构化扩展、协方差建模这里要特别留意一个容易搞混的点外积和克罗内克积都用 ⊗ 记号但定义完全不同。外积的结果是普通矩阵克罗内克积的结果是“矩阵的矩阵”。如果后续要用到多变量高斯分布或者张量分解类模型这两个运算符的区别能救你一命。2. 从形状规则拆透最容易翻车的三类乘法2.1 逐元素乘积 vs 点积 vs 矩阵乘法形状规则对比很多维度报错根源就是这三种乘法没分清楚。逐元素乘积要求两个张量形状相同或者至少满足可广播规则点积要求两个一维向量长度一致矩阵乘法要求左侧矩阵的列数等于右侧矩阵的行数。三者对形状的严格程度是递增的但语义上的复杂度也递增。我举一个非常实际的例子。假设有一个中间特征向量 x形状是(batch, features)你想给它做一个“逐通道缩放”缩放因子 w 的形状是(features,)。用逐元素乘写成x * w结果形状依然是(batch, features)意思是对每一个通道乘一个标量两条数据之间共享同一套缩放因子这是完全合理的。但如果你这时候误写成x w就会立刻得到一个(batch,)的结果因为矩阵乘法把每个 batch 样本和 w 做了内积语义完全跑偏甚至不会报错。所以我会反复强调看乘法之前先问“我想要的输出形状是什么样参与的每个维度代表什么空间”这一步思考能挡掉绝大多数低级错误。2.2 广播机制为什么形状不同也能逐元素乘新手最容易懵的地方是广播。逐元素乘积并不总是要求形状完全一致NumPy 和 PyTorch 都支持广播机制从尾部维度开始对齐如果一个维度是 1另一个是某个值 k那这个维度会自动扩展为 k如果两个维度既不相等也不是 1就报错。举例说一个 shape 为(3, 1)的列向量和一个 shape 为(1, 4)的行向量逐元素相乘会得到(3, 4)的矩阵。这个过程可以理解为列向量的每一列被复制了 4 次行向量的每一行被复制了 3 次然后逐对相乘。这在做归一化、时间序列里对每个样本独立计算均值再除回去时非常有用。但广播也是隐藏 bug 的重灾区。最典型的是形状(batch, seq_len, hidden)的和(batch, hidden)相乘因为尾部维度匹配PyTorch 会把它广播成(batch, seq_len, hidden)与(batch, 1, hidden)相乘逐元素乘没问题。可如果代码意图是让每个时间步和 batch 级别的统计量做某种矩阵变换这就完全错了。建议在遇到广播时永远先在心里演算一遍最终形状。2.3 易混场景什么时候用 什么时候用 *我个人的实践经验是写代码时用和*之前先在旁边注释一句话说明这一步的数学含义。全连接层的输入 x 是(batch, in_features)权重 W 是(in_features, out_features)必须用x W因为我们要把每个样本从输入空间映射到输出空间而 Dropout 的 mask 与特征逐元素乘用的是*因为每个位置只做保留或丢弃不跨位置融合。注意力机制里的打分Q K.transpose(-2, -1)属于矩阵乘法因为要把不同位置的 query 和 key 两两组合求相似度而做完 softmax 后与 V 相乘同样是用实现“按权重组合”。如果这些地方误写成*大概率直接维度 mismatch极小概率下形状碰巧能广播过去那就是最棘手的情况——不报错但模型结果彻底烂掉。3. 乘积在真实模型里的身影3.1 全连接层与矩阵乘法的关系全连接层是理解矩阵乘法最好的入口。一个不带偏置的Linear层做的事就是 y x W^T其中 x 是(batch, in_features)W 是(out_features, in_features)转置之后变成(in_features, out_features)矩阵乘得到(batch, out_features)。如果把视角放到多层感知机里连续两个全连接层之间还夹着激活函数激活函数是逐元素操作的它和前面矩阵乘的结果用逐元素乘法这个说法不准确更准确的说法是“逐元素映射”。这一层映射不对维度做任何修改只改变数值分布正因为有它乘积链才不会退化成单个线性变换。实际写模型时我建议在纸上写清楚每一层的输入形状、权重形状、输出形状。比如 hidden_size768batch32输入(32, 768)经过输出维度 256 的线性层权重就是(256, 768)在代码里写成nn.Linear(768, 256)调用时权重才转置。这个微小的转置细节背后就是矩阵乘法的内维匹配规则在做约束。3.2 注意力机制点积矩阵化与加权求和Transformer 的注意力机制每一处都在用乘积。Query、Key、Value 三个矩阵先各自投影得到(batch, num_heads, seq_len, head_dim)的张量然后用Q K.transpose(-2, -1)计算所有位置对的相似度分数结果形状是(batch, num_heads, seq_len, seq_len)每个位置和所有位置都有了一个点积分数。这里能看到点积的“矩阵化”如果不是用矩阵乘法一次性算所有配对而是朴素地写双重循环时间复杂度一样但代码极慢而且梯度计算要自己手写。矩阵乘法本质上是把“两两做内积”这件事并行化了所以 GPU 对矩阵乘优化得极狠这不是巧合是深度学习计算模式决定的。分数计算完后通常要除以 sqrt(head_dim) 做缩放防止点积数值过大把 softmax 推向饱和区这就是缩放的由来。softmax 规范化后得到注意力权重再和 V 用矩阵乘做加权求和weights V。这一步里weights 的每一行可以看作是“把 V 的每一行做凸组合的系数”矩阵乘负责把“所有位置的 V 乘以对应权重后累加”一次性算完。整条链路里只要你把换成*语义就崩了。3.3 特征交叉、核方法与外积/克罗内克积在推荐系统、广告点击率预估这类场景中特征交叉是核心难点。用户特征和物品特征各自编码成向量后最简单的交叉方式就是把两者做外积得到二维矩阵再展平喂给后面的层。比如用户向量是 20 维物品向量是 20 维外积得到一个 400 维的向量这比直接拼接更能显式表达“用户在某个偏好维度上和物品某种属性同时出现的信号”。因子分解机FM的思路更数学化二阶交叉项通过对特征两两计算内积来建模本质是把外积矩阵的低秩分解引入模型因为直接对所有特征做外积求参数量太大。核方法里的多项式核也是一个典型例子多项式核本质上是在高维特征空间里计算内积而这个高维特征空间恰恰是由原始特征的各种外积、逐元素乘积张成的。如果不理解乘积在特征空间里的含义看这类公式会非常费劲。克罗内克积在机器学习里相对小众但在多变量高斯过程、结构化深度核学习、部分张量网络方法里会出现。它把两个较小的变换组合成一个大变换语义是两个空间独立扩张后形成的联合空间。遇到这类算子别急着用代码去猜先在纸上把分块结构画出来至少在矩阵维度推算上能省下大量调试时间。3.4 损失函数与概率模型中的乘积逻辑乘积不只出现在网络中损失函数和概率模型里同样到处都是。交叉熵损失在看代码时是一堆 log 和 sum但抽掉 log 之后本质就是标签向量和预测概率向量的逐元素乘积再求和——不过是点积的另一种说法。在多标签分类场景里每个类别用二元交叉熵标签是 0/1损失值其实是“把错误类别的预测风险乘上 1 之后累加”这个“乘上标签”的操作是逐元素乘。高斯分布的密度函数里指数部分是 (x - μ)^T Σ^{-1} (x - μ)展开后是向量与矩阵的二阶型核心是矩阵乘法与内积的组合。如果你要做生成模型、变分推断这类乘积的熟练度几乎决定你看公式的速度。4. 实操从 PyTorch 开始做维度检查与排错4.1 PyTorch 中乘法算子速查PyTorch 里最常见的几个乘法算子我给一个自己常用的速查表。需求和算子算子行为逐元素乘torch.mul(a, b)或a * b逐位置相乘支持广播二维矩阵乘torch.mm(a, b)仅支持二维严格匹配内维通用矩阵乘torch.matmul(a, b)或a b支持批量、广播推荐优先使用批量矩阵乘torch.bmm(a, b)固定 batch 维度批量内逐个矩阵乘向量外积torch.outer(a, b)两个一维向量生成二维矩阵克罗内克积torch.kron(a, b)分块扩展建议把当作默认选择因为它在二维、批量多维场景下都能用。但务必注意torch.matmul的广播规则和逐元素乘的广播规则并不一致它是“批量维度对齐末尾两个维度做矩阵乘”这也是常被忽视的坑。4.2 一个真实的注意力模块翻车修正记录上个月我在项目里手写一个简化版注意力机制第一版代码是这样的import torch import torch.nn.functional as F def naive_attention(Q, K, V): # Q, K, V: (batch, seq_len, head_dim) scores Q K.transpose(-2, -1) # (batch, seq_len, seq_len) scores scores / (Q.shape[-1] ** 0.5) weights F.softmax(scores, dim-1) out weights * V # 这里写错了 return out前向没报错但输出的形状是(batch, seq_len, seq_len, head_dim)比预期多了一维。原因是weights的形状是(batch, seq_len, seq_len)而V的形状是(batch, seq_len, head_dim)逐元素乘自动把weights的最后一个维度广播成了head_dim最终张量维度被“撑大”了。正确写法应该是weights V利用矩阵乘法在 seq_len 那个维度上做累加。这个修正过程对我自己的教训很大多数维度错误不一定当场报错它可能是静默扩张或静默广播结果比报错更可怕。修复之后我又加了一行assert out.shape V.shape让它以后一错就立刻暴露。4.3 维度检查三板斧打印、断言、einops关于张量维度排错我已经形成了固定流程。第一板斧是打印 shape在关键计算前后各打印一次确认每一步结果符合预期第二板斧是在模块forward里写断言把关键中间张量的形状用assert固定下来不能抱有“先跑通再说”的侥幸第三板斧是使用einops库的rearrange来显式变换维度。打个比方用einops.rearrange写注意力时rearrange(x, b s (h d) - b h s d, hnum_heads)这样的写法把“把最后一维拆成头数和每头维度”这件事直接写进了代码比一堆.view().transpose()可读性高得多也不容易在连续 shape 操作里一步错步步错。5. 高频翻车现场与排查速查表5.1 模型能跑但结果差先检查乘法算子用对了吗最坑的一种错误是模型能正常训练loss 能降指标起不来或者反直觉地差。遇到这种情况我会先怀疑两个地方注意力层里是否被写成了*以及某个归一化层之后有没有把需要跨维度的计算错误地做成了逐元素乘。有一个很典型的场景做特征选择或门控时gate sigmoid(proj(x))假设 gate 的形状是(batch, features)x 的形状是(batch, seq_len, features)目标是让每个时间步都乘同一个 gate 向量这时要用广播逐元素乘代码写x * gate.unsqueeze(1)是对的。但如果忘了unsqueeze直接x * gate会因为尾部维度相等而静默广播结果语义同样是每个特征乘同一个 gate只是少了一个维度报错运气好不炸但如果你本意是按时间步给不同 gate那就是实打实的 bug。5.2 梯度消失与乘积链的数学直觉乘积在反向传播里还有一重身份它是梯度消失的帮凶。链式法则下深层网络的梯度往往是一长串局部导数的连乘。如果每一层的雅可比矩阵谱半径小于 1乘得越多梯度越小十几层之后梯度就小到无法更新。这也是为什么残差连接那么重要——它给梯度流开了一条“乘法链之外的高速公路”让上游梯度可以直接加回浅层。同样的直觉也能解释为什么 ReLU 比 sigmoid 更常用于深网络sigmoid 的导数最大值是 0.25乘两次就缩到 1/16而 ReLU 在正区间的导数是 1至少在“激活不饱和”的那条路径上乘积不会进一步缩小梯度。想通这一点再看学习率、初始化方差、归一化层很多问题都能串起来。5.3 高频问题速查表报错或现象可能原因排查方向RuntimeError: size mismatch矩阵乘内维不匹配或逐元素乘形状既不等也不可广播打印两边 shape确认是 还是 *前向通过但输出维度多一维被写成了*广播把多余维度直接扩展出来对照预期输出 shape检查乘法算子模型可以训练但指标差形状碰巧匹配但语义错误常见于注意力/门控模块审查每个乘法的语义注释加断言梯度为 NaN点积过大导致 softmax 饱和或连乘链数值溢出检查注意力是否除以 sqrt(d_k)减小学习率特征交叉后参数量爆炸外积或克罗内克积使用过于激进改用低秩分解限制交叉输入维度最后补两个个人习惯我每次搭网络前会在草稿纸上把每一个张量的 shape 按顺序列出来标清每一维的语义再动笔写代码。看起来多花五分钟实际能把调试时间省掉一半。第二个小习惯是在代码里给每个乘法算子写一个注释哪怕只是一个词——“逐元素门控”“矩阵映射”“打分点积”。这种注释在你自己回头调 bug 时极其值钱。把 shape 和语义绑定在一起是区分“会调包”和“真懂模型”的分水岭。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑