PyTorch基础数学运算全解:加减乘除、指数对数与常见坑

发布时间:2026/10/17 11:54:28
PyTorch基础数学运算全解:加减乘除、指数对数与常见坑
最近有朋友问我PyTorch 里最基本的那些数学运算到底该怎么用。加、减、乘、除、指数、对数、绝对值听着像是小学算术真正上手才发现到处是坑——广播维度对不上、除零得到 inf、负数取对数直接变成 NaN、矩阵乘方向搞反、inplace 操作把梯度搞丢……这些错误一旦踩中模型训练时候排查起来特别费劲。这篇文章不绕弯子把这些基本运算逐个拆开讲清楚包括 API 用法、底层逻辑、常见坑和实际场景适合刚入门想打牢基础的人也适合老手写代码时回来查一查。1. 为什么基本数学运算是深度学习的地基1.1 深度学习模型本质上就是在拼接数学运算很多人觉得神经网络很神秘卷积、注意力、归一化一个个听起来不得了。但你把任何一层网络拆到最底层看到的无非是张量之间的加法、减法、乘法、除法还有指数、对数、绝对值这些数学函数。卷积是乘加运算的集合Softmax 是指数和除法的组合LayerNorm 是均值减法再除以标准差。PyTorch 之所以好用不只是因为它把网络层封装得整齐更因为它把最基础的张量运算做得又快又能自动求导。只要你熟悉了这些基本运算读别人源码、改动模型结构都会轻松很多。1.2 运算符和函数名是两套等价的入口PyTorch 中几乎每个数学运算都有两套表达式。一套是显式函数比如torch.add(x, y)、torch.sub(x, y)、torch.mul(x, y)、torch.div(x, y)、torch.exp(x)、torch.log(x)、torch.abs(x)另一套是运算符重载比如x y、x - y、x * y、x / y、x ** y、abs(x)。在大多数场景里这两套是等价的。我的习惯是脚本调试时直接用运算符写正式模型代码时用带名字的函数因为函数名能表达意图比如torch.masked_fill一看就知道是填充代码可读性会好很多。需要特别注意的是PyTorch 很多函数还有“原地操作”版本比如add_、sub_、mul_、div_。这类函数会直接修改调用它的张量不生成新的张量。原地操作能省内存但如果用在不恰当的位置会破坏自动求导的计算图。我见过不少同学在自定义网络前向传播里写x.add_(y)结果训练时突然报错“a leaf Variable that requires grad is being used in an in-place operation.” 从基本功阶段开始就要对带下划线的操作保持警觉。1.3 自动求导要求每个基础运算都可微PyTorch 的自动求导机制依赖的是每个基本算子都能计算局部梯度。加法、乘法、除法、指数、对数、绝对值这些函数几乎都定义了梯度公式。哪怕不提深度学习你用这些运算实现一个数值计算任务只要输入的张量设置了requires_gradTrue调用backward()后仍然能得到梯度。这说明基本数学运算不只是“算结果”也是构建可学习模型的砖石。后面我们会在实际例子里看到组合这些运算就能实现标准化、损失函数、概率计算这些能力是工程落地里每天都用得上的。2. 加法与减法从 API 到广播规则2.1 最基本的几种写法加法有两种常用写法import torch x torch.tensor([1.0, 2.0, 3.0]) y torch.tensor([4.0, 5.0, 6.0]) z1 x y z2 torch.add(x, y)减法也一样z3 x - y z4 torch.sub(x, y)上面这几种写法结果完全一样输出的张量形状和输入保持一致。这里有个细节PyTorch 在做加法、减法时要求两个张量的dtype和device基本一致。早期版本在整数和浮点张量混着相加时类型提升规则比 NumPy 保守经常报错或行为出乎意料。所以我在项目里养成了一个习惯写运算之前先确认x.dtype、y.dtype、x.device、y.device这四个值如果和预期不符后面再排查就很浪费时间。2.2 广播机制不同形状怎么相加如果你已经用过 NumPy会觉得 PyTorch 的广播很亲切。两个张量形状不相同不一定会报错PyTorch 会尝试“广播”。规则是从尾部维度开始对齐两个维度要么相等要么其中一个等于 1要么某个张量在对应位置根本没有维度。举个例子x的形状是[2, 1]y的形状是[3]计算结果形状是[2, 3]。因为对齐时y的[3]会补成[1, 3]x是[2, 1]第二维 1 可以扩展到 3第一维 2 可以扩展到 1 的另一侧最终得到[2, 3]。x torch.tensor([[1.0], [2.0]]) # shape [2, 1] y torch.tensor([10.0, 20.0, 30.0]) # shape [3] z x y print(z.shape) # torch.Size([2, 3])广播为什么有用因为它避免了你手动复制数据。比如模型在多个样本上计算偏置通常是一个一维张量你想给每个样本的每个特征都加上这个偏置。不用构造一个[batch_size, 1]的展开矩阵直接让偏置广播到整个 batch 上就行。不过广播也不是万能的两个形状完全不兼容时比如[2, 3]和[4, 5]就会直接报错。新手最容易犯的错是把批大小和特征维度搞反写成[20, 30]加上[30]结果能加但语义不对。建议每次使用广播前先在注释里写下预期输出形状比如# 期望得到 (B, C)思路会清晰很多。2.3 加法在真实模型里的两种典型用法加法最常见的使用场景是“偏置项”和“残差连接”。偏置项很简单全连接层就是y x W b这里的b会广播到每一个输出位置。残差连接则是y x f(x)x从旁路直接加到网络输出上这种结构在深层网络的训练中至关重要。没有残差连接时深层网络的反向传播梯度容易衰减或爆炸一旦加了加法梯度可以沿旁路无损回传模型更容易收敛。所以我每次看到一个新的基础模块第一反应总是看它有没有往里加原本的输入。在实际代码里残差连接通常写成out block(x) out x out这段代码看起来简单但它要求x和block(x)的形状完全一致。如果不一致就得额外加一个nn.Conv2d或线性层去调整维度。这也是为什么很多网络实现里会频繁出现“shortcut”分支的原因。2.4 inplace 减法与梯度风险刚才提到原地操作我再用一个例子强调。PyTorch 会为参与求导的张量记录操作历史如果你对一个叶子张量执行x.sub_(something)等于把它的值改掉了但计算图里原来的历史记录没有变梯度回传时就会出错。x torch.tensor([2.0], requires_gradTrue) y torch.tensor([1.0]) # 下面这行在训练循环里是非常危险的操作 x.sub_(y) loss (x * x).sum() loss.backward() # 运行时可能报 inplace 相关错误正确的做法是用普通减法让 PyTorch 生成一个新的张量x x - y如果你真的很在意内存可以使用内存连续但非叶子节点的中间结果并确保后续不再需要原始梯度。我的原则是除非在优化器参数的更新步骤里明确知道自己在干什么否则不要碰 inplace 版本。3. 乘法与除法别把逐元素乘和矩阵乘混了3.1 逐元素乘除逐元素乘法就是每个位置的分量对应相乘用*或torch.mul。逐元素除法用/或torch.div。这两个运算完全支持广播规则和加法一致。a torch.tensor([[1.0, 2.0], [3.0, 4.0]]) b torch.tensor([10.0, 20.0]) c a * b # 每个特征列分别乘以对应权重 d torch.div(a, b) # 等价写法逐元素乘法在工程里最常见的用法是“特征加权”或者“掩码操作”。比如你有一批样本特征X还有一个权重向量w想让第 i 列特征乘以第 i 个权重直接X * w即可。再比如注意力机制里的score q k.T后面通常还要乘以一个缩放系数这也用逐元素乘法。需要注意除法的一个特点PyTorch 的/和torch.div默认执行的是浮点真除法。如果你输入两个整数张量它不会像某些语言那样直接抛出异常而是尽量转成浮点结果。但如果除数是整数 0很可能在设备上直接报错或产生不可预期的结果。比较稳妥的写法是保证除数张量是浮点数并且在业务逻辑里把除零情况先用掩码过滤掉。3.2 矩阵乘法的几种重要写法逐元素乘法的是对应位相乘矩阵乘法则完全不同是“行乘以列再求和”。PyTorch 里有好几个矩阵乘法接口它们是新手最容易混淆的地方A torch.randn(3, 4) B torch.randn(4, 5) # 二维矩阵乘法 C1 A.mm(B) # torch.mm C2 torch.mm(A, B) C3 A B C4 torch.matmul(A, B)torch.mm只支持二维矩阵路数比较窄和torch.matmul支持更高维度的张量并且遵循广播规则。举一个批量场景你有一个形状为[batch, seq_len, dim]的输入想和形状为[dim, out_dim]的权重矩阵做乘法torch.matmul可以自动处理x torch.randn(8, 16, 32) W torch.randn(32, 64) out torch.matmul(x, W) # 结果形状 [8, 16, 64]这里torch.matmul会把W广播到 batch 和 seq 维度上非常方便。但如果你用torch.mm就会直接报错因为它只能处理二维。矩阵乘法的维度要求是左边张量的最后一个维度必须等于右边张量的倒数第二个维度。也就是(..., n, k) (..., k, m) - (..., n, m)。我在调试线性层时总是先把权重矩阵的 shape 打印出来确认最后一维对齐否则会看到一堆 “mat1 and mat2 shapes cannot be multiplied” 的错误。3.3 除法的特殊行为与稳定性问题除法在训练神经网络里无处不在最常见的就是归一化把一组值除以它们的总和或范数让结果满足某种尺度约束。比如简单的加权平均weights torch.tensor([0.2, 0.5, 0.3]) values torch.tensor([1.0, 2.0, 3.0]) weighted_sum (weights * values).sum() total_weight weights.sum() result weighted_sum / total_weight这个例子虽然简单但它解释了“归一化”的原理。实际中你可能会遇到某个张量的所有值加和为 0比如数值计算里的损失函数出现除零。此时在分母上加一个很小的eps是工程惯例result weighted_sum / (total_weight 1e-6)不要小看这个eps它能避免梯度变成 NaN。如果除数为 0 的情况发生在某些特殊位置PyTorch 不会直接报错但结果会出现inf。如果 0 除以 0则出现nan。一旦nan出现它会在后续计算中像滚雪球一样扩散到整个梯度里届时损失曲线直接变成一条直线。所以我在训练脚本里经常加一行检查if torch.isnan(loss) or torch.isinf(loss): debug_print(x.dtype, x.shape, y.dtype, y.shape) raise RuntimeError(loss became NaN/Inf)这种防御式写法能在问题发生时立刻暴露具体位置比事后看日志高效太多。3.4 矩阵乘法和逐元素乘法的选择建议这里我给大家一个简单的心法如果我要做“两个矩阵对应位置相乘”用*如果我要做“线性变换、特征映射、投影”用或torch.matmul。千万不要在没有把握时用x * w替代x w。举个反例输入形状是[batch, d]权重形状是[d, out]用*会立刻因维度不一致报错或者在某些广播条件下静默得到错误的形状。实际开发中我见过几次因为把全连接层自实现成逐元素乘法导致模型性能始终不对的案例最后排查半天才发现根本用错算子。4. 指数、对数与绝对值数值稳定性是关键4.1 指数运算torch.exp 与溢出问题torch.exp(x)计算自然常数e的x次方。这个运算在机器学习里最典型的作用是把实数映射为正数用于概率或权重。它的函数形式很简单x torch.tensor([0.0, 1.0, 2.0]) y torch.exp(x) # tensor([1.0000, 2.7183, 7.3891])但指数最大的风险是溢出。当x很大时比如x 1000torch.exp(1000)的结果会直接变成inf。这在 Softmax 里就很容易发生如果原始 logits 有数值[1000, 990, 980]直接torch.exp(logits)后就得到一堆inf后面的除法全部失效。解决办法是先把所有值减去最大值再做指数这叫“稳定 Softmax”logits torch.tensor([1000.0, 990.0, 980.0]) logits_max logits.max() exp_logits torch.exp(logits - logits_max) probs exp_logits / exp_logits.sum()减去最大值不会改变 Softmax 的输出概率因为分子分母同时缩放了相同的倍数。这个技巧是很多概率计算的基础后面手写 LogSoftmax 也要靠它。4.2 对数运算torch.log、log2、log10 与 log1ptorch.log(x)是自然对数torch.log2(x)是以 2 为底torch.log10(x)是以 10 为底。它们要求输入必须大于 0。如果你给负数取对数结果会得到nan给 0 取对数会得到-inf。很多人在构建损失函数时输入可能包含网络输出的 logits这些 logits 有可能是负数。如果直接对 logits 做log肯定出问题。正确做法是先把 logits 转成概率或者使用 PyTorch 提供的log_softmax等封装函数保证输入范围合法。还有一个容易被忽略但很实用的函数是torch.log1p(x)它计算的是log(1 x)。当x非常接近 0 时直接计算torch.log(1 x)会因为浮点精度不足而丢失有效数字而log1p在数值上更精确。在工程里做百分比变化、采样权重平滑时会用到。对数在深度学习里的核心作用是“把乘法变成加法”。概率计算中经常要连乘一系列小于 1 的概率直接乘会导致结果极小甚至下溢为 0取对数后变成连加数值稳定性好很多。比如交叉熵损失里常见的-log(p)就是利用了这一点。4.3 绝对值torch.abs 与不可导点绝对值函数torch.abs(x)做的事情很简单负数变正数正数保持不变。它有一个是在 0 点不可导的问题。不过 PyTorch 在实际实现里将 0 点梯度视为 0所以绝大多数训练场景不会产生致命问题。绝对值一般用在一两个地方一是统计误差的幅度比如 L1 损失mean(abs(y_pred - y_true))二是做梯度裁剪或特征稀疏约束。x torch.tensor([-3.0, -1.0, 0.0, 2.0]) abs_x torch.abs(x) # tensor([3., 1., 0., 2.])如果你希望让负值在梯度训练中更平滑有时会用torch.nn.SmoothL1Loss它本质上是绝对值与平方误差的组合能避免绝对值在 0 点附近抖动过大。这个细节新手可能觉得无关紧要但在目标检测回归任务中SmoothL1 比纯 L1 稳定非常多。4.4 实战手写一个稳定的 LogSoftmax结合指数和对数我建议每个人都亲手写一遍LogSoftmax因为它是理解数值稳定性的最佳练习。PyTorch 提供了torch.log_softmax但自己实现才能明白为什么先减最大值再把 logits 减去分母的对数。def stable_log_softmax(logits): max_logits logits.max(dim-1, keepdimTrue).values normalized logits - max_logits exp_normalized torch.exp(normalized) sum_exp exp_normalized.sum(dim-1, keepdimTrue) log_sum_exp torch.log(sum_exp) log_probs normalized - log_sum_exp return log_probs logits torch.tensor([[1000.0, 990.0, 980.0], [1.0, 2.0, 3.0]]) log_probs stable_log_softmax(logits) print(log_probs)注意这里使用了keepdimTrue让最大值和总和保持原有的维度否则会对不上广播规则。对比直接torch.log_softmax(logits, dim-1)结果应该大致相同。写一遍你就会发现指数、对数、减法、除法并不是孤立的它们组合在一起能解决很实际的数值问题。5. 综合实战用基本运算写一个张量标准化工具为了检验你对这些基本运算的理解我们来写一个最常见的数据标准化函数z-score 标准化。它的公式是(x - mean) / std。这个公式里用到了均值、减法、除法还有标准差里的平方和开方。如果不用现成 API完全可以用基本运算拆开写。import torch def z_score_norm(x, eps1e-6): mean x.mean(dim0, keepdimTrue) variance x.var(dim0, unbiasedFalse, keepdimTrue) std variance.sqrt() normalized (x - mean) / (std eps) return normalized # 模拟一个 batch 数据特征维度 3 data torch.tensor([[1.0, 2.0, 3.0], [2.0, 3.0, 5.0], [3.0, 4.0, 7.0]]) out z_score_norm(data) print(out.mean(dim0)) # 接近 0 print(out.var(dim0)) # 接近 1这里有几个细节值得说一下。一是keepdimTrue的作用。如果不加mean的形状会变成[3]和原数据[3, 3]做减法时虽然能广播但代码语义不够清楚。加了以后mean的形状是[1, 3]一眼就能看出是按特征维度统计。二是分母加eps防止某一列方差为 0 时造成除零。三是在计算方差时unbiasedFalse用的是总体方差和传统统计里的样本方差系数不同但用在深度学习的 BatchNorm 风格归一化里总体方差更常见。这个例子虽然没有用exp、log和abs但它把减法、除法、平方根、均值组合在一起已经能说明基本张量运算的工程威力。如果你在此基础上把输出接一个指数或对数层就能把标准化结果变成概率分布形式的表达那些基本运算会继续发挥作用。6. 常见问题与排查技巧实录6.1 高频报错速查表问题现象可能原因排查方向shape 不匹配报错加减乘除时两遍张量不能广播打印x.shape、y.shape检查对齐后的形状损失变成 NaN分母为 0、对数输入小于等于 0、exp 溢出在损失后加torch.isnan和torch.isinf检查梯度为 None叶子节点被 inplace 修改检查代码里是否有add_、sub_、mul_、div_CPU/GPU device 不匹配一个张量在 CPU一个在 GPU统一调用.to(device)整数与浮点类型不匹配整数张量和浮点张量混算先用.float()转类型再参与运算矩阵维度对不上使用时最后一维不匹配确认A.shape[-1] B.shape[-2]除法得到 inf 但没报错浮点张量除以 0给分母加 eps 或先做掩码处理6.2 调试基本运算的四个习惯第一个习惯是把张量的shape、dtype、device打印出来再动手。很多同学在报错之后才去打印但如果在写运算前就先检查这三个属性大部分问题都能提前规避。第二个习惯是给每个关键张量起一眼能认出的名字并在注释里写上预期形状。比如hidden_state的注释是# (batch, seq, hidden)后面做矩阵乘时不容易搞混。第三个习惯是建立“数值卫生”意识。每次新增一个包含除法、对数、指数的模块我都会在模块出口检查一次isfinite。因为基本数学运算的数值问题不是马上报错而是等到 loss 变成 NaN 时才显现那时已经很难定位。第四个习惯是尽量使用非 inplace 版本。我在早期写代码时为了省内存用过很多add_后来发现梯度问题排查成本远远超过那点内存收益从此默认用普通运算。6.3 一个小技巧利用打印选项辅助检查PyTorch 默认打印张量只显示 4 位小数调试时容易看不出细微差异。我习惯在项目启动脚本里加上一行torch.set_printoptions(precision8, sci_modeFalse)这样打印出来的张量精度更高也不会因为科学计数法而看不清数量级。尤其在检查 Softmax 输出是否都接近 0.25 这种数值时这个设置非常有用。写在最后我在实际调试中最大的体会是PyTorch 的基本数学运算本身不难难的是“张量思维”的建立。加法、减法、乘法、除法、指数、对数、绝对值每一项都有各自的前置条件和数值风险但只要养成了先看 shape、dtype、device 的习惯再在关键节点检查 NaN 和 inf大部分坑都能避开。最后再分享一个小技巧如果你写了一个由基本运算组成的自定义函数建议用随机张量先验证一遍输出形状是否符合预期再放入模型训练。别小看这一个步骤它能帮你省下好几个小时的排查时间。