《30天吃掉那只TensorFlow2》第4-2篇:张量数学运算实战——标量、向量、矩阵运算与广播机制
2026/9/24 17:23:46 网站建设 项目流程
  • 教程
  • 深度学习
  • 机器学习

【免费下载链接】eat_tensorflow2_in_30_days

Tensorflow2.0 🍎🍊 is delicious, just eat it! 😋😋

项目地址:https://gitcode.com/gh_mirrors/ea/eat_tensorflow2_in_30_days
点击查看免费下载

本篇技术指南聚焦于 TensorFlow2 低阶 API 中张量的数学运算,是《30天吃掉那只TensorFlow2》系列第12天(4-2,张量的数学运算)的深化解读。全文将张量数学运算系统划分为标量运算、向量运算、矩阵运算三大类,并完整讲解支撑前两者高效执行的广播机制,覆盖tf.mathtf.reduce_*tf.linalgtf.broadcast_*等核心 API。读者学完本篇,将能够像使用"增强版 numpy"一样熟练地对张量进行逐元素计算、按轴规约、矩阵分解与广播扩维,这些能力是后续实现线性回归、损失函数、评估指标等模型代码的基础。

引言:张量数学运算在 TensorFlow 低阶 API 中的定位

在《30天吃掉那只TensorFlow2》的知识体系中,TensorFlow 的低阶 API 主要包括张量操作、计算图和自动微分三部分,其中张量操作又被划分为结构操作数学运算两块(见四、TensorFlow的低阶API):

  • 张量结构操作:张量创建、索引切片、维度变换、合并分割,详见4-1,张量的结构操作;
  • 张量数学运算:标量运算、向量运算、矩阵运算,以及张量运算的广播机制,即本篇主题。

正如该章引言所述,"在低阶 API 层次上,可以把 TensorFlow 当做一个增强版的 numpy 来使用"。张量数学运算正是这一说法的核心体现:它提供比 numpy 更全面的运算方法,且可以无缝接入tf.function计算图与 GPU 加速。因此在动手编写模型前,先把张量的数学运算吃透,能够极大降低后续阅读源码与自研算法的门槛。

一,标量运算:对张量实施逐元素运算

1. 标量运算的本质

张量的数学运算符可分为标量运算符、向量运算符、矩阵运算符三类。其中标量运算符的特点是对张量实施逐元素运算(element-wise),包括:

  • 加减乘除、乘方、开方、取模、地板除法等算术运算;
  • 三角函数、指数、对数等常见数学函数;
  • 大小比较、相等判断等逻辑比较运算符。

这些运算中,相当一部分对常用的数学运算符(+-*/**%//==>=等)进行了重载,并且支持类似 numpy 的广播特性。从 API 组织上看,许多标量运算符都集中在tf.math模块下,例如tf.math.modtf.math.roundtf.math.pow等。

2. 运算符重载:像操作 numpy 一样操作张量

下面的代码定义了两个 2×2 张量,直接使用 Python 运算符完成四则运算与乘方:

import tensorflow as tf import numpy as np a = tf.constant([[1.0,2],[-3,4.0]]) b = tf.constant([[5.0,6],[7.0,8.0]]) a+b # 运算符重载,等价于 tf.add(a,b)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[ 6., 8.], [ 4., 12.]], dtype=float32)>
a-b
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[ -4., -4.], [-10., -4.]], dtype=float32)>
a*b # 逐元素相乘(注意:不是矩阵乘法)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[ 5., 12.], [-21., 32.]], dtype=float32)>
a/b
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[ 0.2 , 0.33333334], [-0.42857143, 0.5 ]], dtype=float32)>
a**2
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[ 1., 4.], [ 9., 16.]], dtype=float32)>
a**(0.5) # 对负数元素开方得到 nan
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[1. , 1.4142135], [ nan, 2. ]], dtype=float32)>

注意上面最后一个输出:a**(0.5)对元素-3开方得到nan,这是浮点运算的正常行为,在实际训练中需要留意 NaN 的传播(可结合后文tf.clip_by_value等裁剪手段预防)。

3. 取模、地板除法与逻辑比较运算

a%3 # mod 的运算符重载,等价于 m = tf.math.mod(a,3)
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([1, 2, 0], dtype=int32)>
a//3 # 地板除法(向下取整除法)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[ 0., 0.], [-1., 1.]], dtype=float32)>
(a>=2)
<tf.Tensor: shape=(2, 2), dtype=bool, numpy= array([[False, True], [False, True]])>
(a>=2)&(a<=3) # 逻辑与(逐元素)
<tf.Tensor: shape=(2, 2), dtype=bool, numpy= array([[False, True], [False, False]])>
(a>=2)|(a<=3) # 逻辑或(逐元素)
<tf.Tensor: shape=(2, 2), dtype=bool, numpy= array([[ True, True], [ True, True]])>
a==5 # 等价于 tf.equal(a,5)
<tf.Tensor: shape=(3,), dtype=bool, numpy=array([False, False, False])>

比较运算返回bool型张量,这一特性在后续的布尔索引(如tf.boolean_mask)与条件运算(如tf.where)中被广泛使用。

4. 常用标量函数与多张量聚合

除重载运算符外,tf.math模块还提供大量函数式 API:

tf.sqrt(a)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[1. , 1.4142135], [ nan, 2. ]], dtype=float32)>
a = tf.constant([1.0,8.0]) b = tf.constant([5.0,6.0]) c = tf.constant([6.0,7.0]) tf.add_n([a,b,c]) # 多个张量逐元素求和
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([12., 21.], dtype=float32)>
tf.print(tf.maximum(a,b)) # 逐元素取较大值 tf.print(tf.minimum(a,b)) # 逐元素取较小值
[5 8] [1 6]
x = tf.constant([2.6,-2.7]) tf.print(tf.math.round(x)) # 保留整数部分,四舍五入 tf.print(tf.math.floor(x)) # 保留整数部分,向下归整 tf.print(tf.math.ceil(x)) # 保留整数部分,向上归整
[3 -3] [2 -3] [3 -2]

常用标量函数可整理为如下速查表(均位于tf.math或顶层tf命名空间):

类别代表性 API说明
算术tf.add/tf.subtract/tf.multiply/tf.divide/tf.pow/tf.sqrt逐元素四则与乘方
取模整除tf.math.modtf.math.floordiv对应%//重载
聚合tf.add_n多个张量逐元素累加
最值tf.maximumtf.minimum逐元素取大/取小
取整tf.math.roundtf.math.floortf.math.ceil四舍五入/向下/向上取整
比较tf.equaltf.greater_equaltf.less_equal返回bool张量
指数对数tf.math.exptf.math.logtf.math.pow神经网络中常用

5. 幅值裁剪:数值稳定性的第一道防线

训练神经网络时,梯度爆炸或中间激活值越界是常见问题,幅值裁剪是应对手段之一:

# 幅值裁剪 x = tf.constant([0.9,-0.8,100.0,-20.0,0.7]) y = tf.clip_by_value(x,clip_value_min=-1,clip_value_max=1) z = tf.clip_by_norm(x,clip_norm = 3) tf.print(y) tf.print(z)
[0.9 -0.8 1 -1 0.7] [0.0264732055 -0.0235317405 2.94146752 -0.588293493 0.0205902718]
  • tf.clip_by_value:把每个元素裁剪到[clip_value_min, clip_value_max]区间;
  • tf.clip_by_norm:按 L2 范数整体缩放,使整个向量的范数不超过clip_norm

这种技巧在仓库的模型代码中确实有实际应用:在3-1,低阶API示范中,自定义二分类模型的 BCE 损失函数对预测值做了tf.clip_by_value(y_pred, eps, 1.0-eps)处理,目的就是防止tf.math.log在预测值为 0 或 1 时产生无穷大,保证数值稳定。

二,向量运算:沿着特定轴规约与扫描

1. 什么是向量运算

向量运算符只在一个特定轴上运算,将一个向量映射到一个标量或者另外一个向量。许多向量运算符的名字以reduce开头,这是因为它们的核心动作是"规约"——沿着某个轴把多个元素合并(求和、求均值、取最大等)。

2. 基本规约:reduce 家族

# 向量 reduce a = tf.range(1,10) tf.print(tf.reduce_sum(a)) tf.print(tf.reduce_mean(a)) tf.print(tf.reduce_max(a)) tf.print(tf.reduce_min(a)) tf.print(tf.reduce_prod(a))
45 5 9 1 362880
# 张量指定维度进行 reduce b = tf.reshape(a,(3,3)) tf.print(tf.reduce_sum(b, axis=1, keepdims=True)) tf.print(tf.reduce_sum(b, axis=0, keepdims=True))
[[6] [15] [24]] [[12 15 18]]

这里有两个关键参数:

  • axis:指定在哪个维度上规约。axis=1按行求和(每行 3 个元素压成一个),axis=0按列求和;
  • keepdims:是否保留被规约掉的维度。keepdims=True时输出保持二维形状(如[[6],[15],[24]]),这在后续需要与原始张量进行广播运算时非常有用。

3. bool 型规约与函数式折叠

# bool 类型的 reduce p = tf.constant([True,False,False]) q = tf.constant([False,False,True]) tf.print(tf.reduce_all(p)) # 全部为 True 才为 True tf.print(tf.reduce_any(q)) # 任一为 True 即为 True
0 1

tf.reduce_alltf.reduce_any是逻辑规约运算,输出0/1(对应False/True)。此外,规约运算还可以借助函数式折叠(fold)来理解其本质——tf.foldr从右向左依次用二元函数累积元素:

# 利用 tf.foldr 实现 tf.reduce_sum s = tf.foldr(lambda a,b:a+b,tf.range(10)) tf.print(s)
45

4. 累积扫描:cumsum 与 cumprod

与"压成一个值"的 reduce 不同,**扫描运算(scan)**把每一步的中间结果都保留下来,输出与输入等长的向量:

# cum 扫描累积 a = tf.range(1,10) tf.print(tf.math.cumsum(a)) tf.print(tf.math.cumprod(a))
[1 3 6 ... 28 36 45] [1 2 6 ... 5040 40320 362880]

tf.math.cumsum输出前缀和序列,tf.math.cumprod输出前缀积序列,常用于需要保留累积过程的场景。

5. 极值索引与 top_k

# arg 最大最小值索引 a = tf.range(1,10) tf.print(tf.argmax(a)) tf.print(tf.argmin(a))
8 0

tf.argmax/tf.argmin返回最大/最小值所在位置的索引(注意从 0 开始计数,故最大值 9 的索引为 8)。更进一步,tf.math.top_k可以一次取出前 k 个最大元素及其索引,并可选排序:

# tf.math.top_k 可以用于对张量排序 a = tf.constant([1,3,7,5,4,8]) values,indices = tf.math.top_k(a,3,sorted=True) tf.print(values) tf.print(indices) # 利用 tf.math.top_k 可以在 TensorFlow 中实现 KNN 算法
[8 7 5] [5 2 3]

values=[8,7,5]indices=[5,2,3]表明最大的三个元素是原序列第 5、2、3 号位置上的 8、7、5。利用tf.math.top_k可以在 TensorFlow 中实现 KNN 算法(取距离最近的 k 个邻居)。

这一 API 在仓库的评估指标实现中同样被直接使用:在5-6,评估指标metrics中,自定义 Accuracy 指标即通过tf.math.top_k(y_pred, k=length, sorted=False)将模型输出的概率向量还原为类别标签,再与真实标签比较计算准确率。

三,矩阵运算:tf.linalg 线性代数子包

1. 矩阵运算概览

矩阵必须是二维的,类似tf.constant([1,2,3])这样的向量不是矩阵。矩阵运算包括:矩阵乘法、矩阵转置、矩阵逆、矩阵求迹、矩阵范数、矩阵行列式、矩阵求特征值、矩阵分解等。除了一些常用运算外,大部分和矩阵有关的运算都在tf.linalg子包中

2. 矩阵乘法与转置

# 矩阵乘法 a = tf.constant([[1,2],[3,4]]) b = tf.constant([[2,0],[0,2]]) a@b # 等价于 tf.matmul(a,b)
<tf.Tensor: shape=(2, 2), dtype=int32, numpy= array([[2, 4], [6, 8]], dtype=int32)>
# 矩阵转置 a = tf.constant([[1,2],[3,4]]) tf.transpose(a)
<tf.Tensor: shape=(2, 2), dtype=int32, numpy= array([[1, 3], [2, 4]], dtype=int32)>

@运算符是tf.matmul的重载形式。矩阵乘法是全连接层(x@w + b)的数学基础,这一点在仓库的3-1,低阶API示范中随处可见:线性回归模型的核心表达式即为Y = X@w0 + b0 + 噪声,自定义神经网络层的前向传播也是tf.nn.relu(x@self.w1 + self.b1)这样的矩阵乘加运算。注意a*b(逐元素乘)与a@b(矩阵乘)是两个完全不同的运算。

3. 矩阵逆、迹与范数

# 矩阵逆,必须为 tf.float32 或 tf.double 类型 a = tf.constant([[1.0,2],[3,4]],dtype = tf.float32) tf.linalg.inv(a)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy= array([[-2.0000002 , 1.0000001 ], [ 1.5000001 , -0.50000006]], dtype=float32)>
# 矩阵求 trace(主对角线元素之和) a = tf.constant([[1.0,2],[3,4]],dtype = tf.float32) tf.linalg.trace(a)
<tf.Tensor: shape=(), dtype=float32, numpy=5.0>
# 矩阵求范数(默认 Frobenius 范数) a = tf.constant([[1.0,2],[3,4]]) tf.linalg.norm(a)
<tf.Tensor: shape=(), dtype=float32, numpy=5.477226>

值得注意的实用约束:tf.linalg.inv要求输入为tf.float32tf.double等浮点类型,整数矩阵需要先用tf.cast转换。

4. 行列式与特征值

# 矩阵行列式 a = tf.constant([[1.0,2],[3,4]]) tf.linalg.det(a)
<tf.Tensor: shape=(), dtype=float32, numpy=-2.0>
# 矩阵特征值 a = tf.constant([[1.0,2],[-5,4]]) tf.linalg.eigvals(a)
<tf.Tensor: shape=(2,), dtype=complex64, numpy=array([2.4999995+2.7838817j, 2.5 -2.783882j ], dtype=complex64)>

特征值可能为复数,因此tf.linalg.eigvals返回complex64类型张量。若还需特征向量,可使用tf.linalg.eig

5. QR 分解与 SVD 分解

QR 分解将一个方阵分解为一个正交矩阵q和上三角矩阵r的乘积,其本质是对矩阵a实施 Schmidt 正交化得到q

# 矩阵 QR 分解 a = tf.constant([[1.0,2.0],[3.0,4.0]],dtype = tf.float32) q,r = tf.linalg.qr(a) tf.print(q) tf.print(r) tf.print(q@r) # 验证重构:q@r 应近似还原 a
[[-0.316227794 -0.948683321] [-0.948683321 0.316227734]] [[-3.1622777 -4.4271884] [0 -0.632455349]] [[1.00000012 1.99999976] [3 4]]

SVD 分解(奇异值分解)可以将任意矩阵(不要求方阵)分解为一个正交矩阵u、一个对角阵s和一个正交矩阵v转置的乘积,常用于矩阵压缩和降维:

# 矩阵 svd 分解 a = tf.constant([[1.0,2.0],[3.0,4.0],[5.0,6.0]], dtype = tf.float32) s,u,v = tf.linalg.svd(a) tf.print(u,"\n") tf.print(s,"\n") tf.print(v,"\n") tf.print(u@tf.linalg.diag(s)@tf.transpose(v)) # 利用 svd 分解可以在 TensorFlow 中实现主成分分析(PCA)降维
[[0.229847744 -0.88346082] [0.524744868 -0.240782902] [0.819642067 0.401896209]] [9.52551842 0.51429987] [[0.619629562 0.784894466] [0.784894466 -0.619629562]] [[1.00000119 2] [3.00000095 4.00000048] [5.00000143 6.00000095]]

注意返回值顺序tf.linalg.svd返回(s, u, v)三元组,其中s是奇异值向量(而非对角阵)。重构时需要通过tf.linalg.diag(s)把奇异值向量先还原为对角阵,再计算u @ diag(s) @ v^T。从上面的输出可以看到重构结果与原矩阵a几乎一致(数值上仅有微小浮点误差)。利用 svd 分解可以在 TensorFlow 中实现主成分分析(PCA)降维——保留最大的若干个奇异值即可得到低秩近似,这一思想在特征压缩与数据可视化场景中非常实用。

矩阵运算速查:

运算API说明
乘法tf.matmul/@二维矩阵乘法,广播批次维度
转置tf.transpose交换维度(可指定perm
求逆tf.linalg.inv要求浮点类型
tf.linalg.trace主对角线元素和
范数tf.linalg.norm默认 Frobenius 范数,可指定ord
行列式tf.linalg.det输出标量
特征值tf.linalg.eigvals/tf.linalg.eig结果为复数
分解tf.linalg.qrtf.linalg.svdtf.linalg.choleskyQR / 奇异值 / 乔列斯基分解

四,广播机制:不同形状张量之间的运算规则

1. 广播的 5 条规则

TensorFlow 的广播规则和 numpy 是一样的,运算时维度较小的张量会被自动扩展:

  1. 如果张量的维度不同,将维度较小的张量进行扩展,直到两个张量的维度都一样
  2. 如果两个张量在某个维度上的长度是相同的,或者其中一个张量在该维度上的长度为 1,那么这两个张量在该维度上是相容的
  3. 如果两个张量在所有维度上都是相容的,它们就能使用广播
  4. 广播之后,每个维度的长度将取两个张量在该维度长度的较大值
  5. 在任何一个维度上,如果一个张量的长度为 1,另一个张量长度大于 1,那么在该维度上,就好像是对第一个张量进行了复制

广播机制是前文所有标量运算和向量规约得以高效工作的底层支撑:例如a + b之所以能把 1 维向量加到 2 维矩阵的每一行上,正是因为广播规则第 5 条——长度为 1 的维度被"复制"扩展。

2. 隐式广播与显式广播

a = tf.constant([1,2,3]) b = tf.constant([[0,0,0],[1,1,1],[2,2,2]]) b + a # 等价于 b + tf.broadcast_to(a,b.shape)
<tf.Tensor: shape=(3, 3), dtype=int32, numpy= array([[1, 2, 3], [2, 3, 4], [3, 4, 5]], dtype=int32)>

上面的b + a隐式广播a的 shape 为(3,)b的 shape 为(3,3)a自动扩展为(1,3)再复制为(3,3)参与逐元素相加。若想显式地看到扩展结果,可以使用tf.broadcast_to

tf.broadcast_to(a,b.shape)
<tf.Tensor: shape=(3, 3), dtype=int32, numpy= array([[1, 2, 3], [1, 2, 3], [1, 2, 3]], dtype=int32)>

3. 广播后的形状推导

在不知道运行时实际形状的情况下,可以用两个 API 预先推导广播后的结果形状:

# 计算广播后计算结果的形状,静态形状,TensorShape 类型参数 tf.broadcast_static_shape(a.shape,b.shape)
TensorShape([3, 3])
# 计算广播后计算结果的形状,动态形状,Tensor 类型参数 c = tf.constant([1,2,3]) d = tf.constant([[1],[2],[3]]) tf.broadcast_dynamic_shape(tf.shape(c),tf.shape(d))
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([3, 3], dtype=int32)>

两者区别在于:

  • tf.broadcast_static_shape:接收静态形状TensorShape类型,来自tensor.shape),在构图期(Python 层)即可完成推导,适合形状已知的场景;
  • tf.broadcast_dynamic_shape:接收动态形状Tensor类型,来自tf.shape(tensor)),在运行时由 TensorFlow 推导,适合tf.function中处理None维度(动态 batch)的场景。

4. 双向量广播示例

# 广播效果 c+d # 等价于 tf.broadcast_to(c,[3,3]) + tf.broadcast_to(d,[3,3])
<tf.Tensor: shape=(3, 3), dtype=int32, numpy= array([[2, 3, 4], [3, 4, 5], [4, 5, 6]], dtype=int32)>

这里c的 shape 为(3,)d的 shape 为(3,1)c先扩展为(1,3)再复制成(3,3)d直接沿第 0 维复制成(3,3),两者逐元素相加得到 3×3 结果。广播配合keepdims=True的规约(如tf.reduce_sum(x, axis=1, keepdims=True))是深度学习中标准化(如 BatchNorm 中的均值/方差减除)的惯用组合。

五,实战提示:把数学运算接入模型代码

结合仓库中的实际代码,张量数学运算主要有三类落地场景:

  1. 损失函数:在3-1,低阶API示范中,MSE 损失写为tf.reduce_mean((y_true - y_pred)**2/2),BCE 损失写为- y_true*tf.math.log(y_pred) - (1-y_true)*tf.math.log(1-y_pred)——这正是"标量运算(乘方、对数)+ 向量规约(reduce_mean)"的组合;
  2. 评估指标:在5-6,评估指标metrics中,自定义 Accuracy 指标用tf.math.top_k还原预测类别;在5-4,模型层layers中,tf.math.pow通过layers.Lambda(lambda x: tf.math.pow(x,2))直接嵌入模型结构;
  3. 训练过程控制:在5-7,优化器optimizers中,用tf.math.mod(optimizer.iterations, 100) == 0判断是否到达每 100 步的打印节点——tf.math.mod正是标量运算章节中%重载对应的底层函数。

另外两点使用前提需要说明:

  • 运行环境:本项目全部代码在 TensorFlow 2.1 版本下测试通过(见 README.md),文中输出示例均来自该环境。新版 TensorFlow 中部分 API 有重命名(如tf.math.reduce_sumtf.reduce_sum等价),调用时请以实际安装版本为准;
  • GPU 加速:张量数学运算天然可以运行在 GPU 上(6-3,使用单GPU训练模型),编写运算代码时保持张量在 GPU 显存中不来回拷贝,即可获得显著加速;将运算逻辑放入@tf.function装饰的函数中(如3-1,低阶API示范所示),可进一步获得计算图优化收益。

小结

本篇围绕《30天吃掉那只TensorFlow2》第 4-2 节,系统梳理了张量数学运算的四大板块:标量运算(逐元素四则、比较、取整、裁剪)、向量运算reduce规约、扫描累积、极值索引、top_k)、矩阵运算tf.linalg下的乘、转置、逆、迹、范数、行列式、特征值与 QR/SVD 分解)以及广播机制(5 条规则与tf.broadcast_*系列 API)。这些能力与4-1,张量的结构操作(创建、切片、变换、合并)共同构成了 TensorFlow 低阶 API 的张量操作全貌,也是理解2-1,张量数据结构(常量与变量、多维张量)之后最值得掌握的下一块拼图。建议读者在本地克隆本仓库后,将上述代码在 Jupyter 中逐段运行对照输出,并结合3-1,低阶API示范中的完整模型代码,体会数学运算如何被组织进真实的训练循环。

  • 教程
  • 深度学习
  • 机器学习

【免费下载链接】eat_tensorflow2_in_30_days

Tensorflow2.0 🍎🍊 is delicious, just eat it! 😋😋

项目地址:https://gitcode.com/gh_mirrors/ea/eat_tensorflow2_in_30_days
点击查看免费下载

相关推荐

上一篇:如何快速实现现代JavaScript项目中的人脸识别:Face Detection API终极指南
下一篇:开源项目推荐:mathc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询