第6课:TensorFlow|张量运算全解【数学运算、矩阵运算、逻辑运算、索引切片实操】

文章目录
1. 课前导读
1.1 本节课学习目标
- 熟练掌握TensorFlow中的张量数学运算:逐元素运算、归约运算、累积运算。
- 掌握矩阵运算的核心函数:矩阵乘法、转置、逆、行列式、特征值分解等。
- 学会使用逻辑运算进行条件筛选和掩码操作。
- 精通张量的索引与切片操作:基本索引、切片、布尔索引、花式索引(高级索引)。
- 能够组合上述操作为复杂的数据预处理和特征工程流水线。
1.2 知识重难点
| 类别 | 内容 |
|---|---|
| 重点 | 逐元素运算与广播的结合;矩阵乘法的多种实现方式(@、tf.matmul、tf.tensordot);高级索引与tf.gather/tf.gather_nd |
| 难点 | tf.tensordot和tf.einsum的灵活使用;稀疏索引与切片中的维度保持问题;逻辑运算与tf.where的条件筛选 |
| 易混淆点 | 逐元素乘法 * 与矩阵乘法 @;tf.reduce_sum 与 tf.math.reduce_sum 相同;tf.boolean_mask 与布尔索引的区别 |
1.3 学习前置条件
- 已掌握第5课的张量创建与形状变换。
- 熟悉基础的线性代数(矩阵乘法、转置)和微积分概念。
- Python的基本运算符优先级知识。
1.4 学完可掌握能力
- 能够独立实现神经网络的前向传播中的矩阵运算(全连接层、卷积层展开)。
- 利用索引切片高效地从张量中提取子区域(如图像裁剪、序列截断)。
- 使用逻辑运算生成掩码,实现复杂的条件筛选(如剔除异常值、样本加权)。
- 掌握
tf.where实现三元条件运算,替换低效的tf.cond循环。 - 能够计算模型评估指标(如MSE、MAE)所需的基础统计量。
1.5 行业应用场景
- 图像处理:对图像张量进行区域裁剪、通道分离与合并(索引切片)。
- 特征工程:使用逻辑运算构建组合特征(如
(feature1 > threshold) & (feature2 < threshold))。 - 损失函数:实现Huber损失、对比损失等需要逐元素条件和分支的损失。
- 模型剪枝:通过阈值比较生成掩码,置零小于阈值的权重。
- 数据增强:随机裁剪、翻转等操作背后都是张量索引。
2. 核心理论精讲
2.1 张量运算的分类
张量运算按数学性质可分为:
- 逐元素运算(Element-wise):加、减、乘、除、幂、对数、三角函数等。结果张量形状与输入相同(广播后)。
- 归约运算(Reduction):沿特定轴计算总和、均值、最大值、最小值等,会降低维度。
- 矩阵运算(Linear Algebra):矩阵乘法、转置、逆、特征分解、奇异值分解等。
- 逻辑运算(Logical):比较(>、<、==)、逻辑与、或、非,返回布尔张量。
- 索引与切片(Indexing & Slicing):基于位置或条件的子张量提取。
2.2 数学运算底层原理
逐元素运算在TensorFlow中通过内核融合技术优化。例如a + b不会产生临时数组,而是直接在计算图中生成加法节点,并在运行时通过Eigen或CUDA核函数并行执行。归约运算则采用分块归约算法,对大型张量避免一次性加载到内存。
广播机制在运算中的角色:当两个张量形状不同时,TensorFlow自动在需要复制的维度上广播。广播本质上是在不复制数据的情况下,改变张量的逻辑视图,通过调整步长实现。
2.3 矩阵运算与深度学习
全连接层的前向传播公式为 output = input @ W + b,其中@是矩阵乘法。卷积操作也可通过im2col转换为矩阵乘法后执行。因此矩阵运算是神经网络的核心。
TF中矩阵乘法支持批处理:若a形状(batch, m, k),b形状(batch, k, n),则tf.matmul(a, b)输出(batch, m, n),自动沿batch维度并行。这避免了显式循环。
2.4 索引切片的高级特性
与NumPy类似,TensorFlow支持基本索引(tensor[i])、切片(tensor[start:stop:step])、省略号(...)、None/tf.newaxis增加维度。
高级索引(花式索引)允许使用整数张量从源张量中按位置收集值。tf.gather用于单个轴,tf.gather_nd用于多维任意位置。tf.boolean_mask通过布尔掩码筛选元素。
重要区别:基本切片返回视图(视图与原始张量共享内存),但TensorFlow中视图是不保证的(由于图执行和张量不可变性,通常仍会复制,但用户应视为不可变)。高级索引总是返回副本。
2.5 逻辑运算与条件选取
tf.where(condition, x, y)类似三元运算符,根据condition逐元素选择x或y。如果仅提供condition,则返回满足条件的索引。
tf.cond用于控制流(分支整个计算图),而tf.where是逐元素的条件运算。在向量化编程中优先使用tf.where。
3. 环境搭建与工具配置
沿用第5课的环境。确保TensorFlow 2.13已安装,且导入正常。
conda activate tf213
python
本课代码可全部在交互式环境或Jupyter Notebook中运行。建议创建新笔记本lesson06_ops.ipynb。
4. 代码实战教学
本节代码分为五个部分:数学运算、矩阵运算、逻辑运算、索引切片、综合应用。每段代码都可独立运行。
4.1 基础数学运算
import tensorflow as tf
import numpy as np
# 创建示例张量
a = tf.constant([[1, 2], [3, 4]], dtype=tf.float32)
b = tf.constant([[5, 6], [7, 8]], dtype=tf.float32)
# 1. 逐元素算术运算
print("a + b =\n", a + b) # 加法
print("a - b =\n", a - b) # 减法
print("a * b =\n", a * b) # 逐元素乘法
print("a / b =\n", a / b) # 除法
print("a ** 2 =\n", a ** 2) # 幂
# 2. 常用数学函数
print("sqrt(a) =\n", tf.sqrt(a))
print("exp(a) =\n", tf.exp(a))
print("log(a) =\n", tf.math.log(a)) # 自然对数
print("sin(a) =\n", tf.sin(a))
# 3. 归约运算
c = tf.constant([[1, 2, 3], [4, 5, 6]], dtype=tf.float32)
print("sum all =", tf.reduce_sum(c)) # 所有元素和: 21
print("sum axis=0 (列):", tf.reduce_sum(c, axis=0)) # [5,7,9]
print("mean axis=1 (行):", tf.reduce_mean(c, axis=1)) # [2.,5.]
print("max axis=0:", tf.reduce_max(c, axis=0)) # [4,5,6]
print("argmax axis=1:", tf.argmax(c, axis=1)) # 每行最大值索引: [2,2]
# 4. 累积运算
d = tf.constant([1, 2, 3, 4])
print("cumsum:", tf.cumsum(d)) # [1,3,6,10]
print("cumprod:", tf.cumprod(d)) # [1,2,6,24]
输出解读:reduce_sum的axis参数指定沿哪个维度归约。axis=0表示沿着第一维(行)压缩,列独立计算。
4.2 矩阵运算
# 矩阵乘法
A = tf.constant([[1, 2], [3, 4]], dtype=tf.float32)
B = tf.constant([[5, 6], [7, 8]], dtype=tf.float32)
# 三种等价写法
matmul1 = tf.matmul(A, B)
matmul2 = A @ B
matmul3 = tf.tensordot(A, B, axes=1) # axes=1 表示内积维度
print("矩阵乘法结果:\n", matmul1.numpy())
# 预期 [[19,22],[43,50]]
# 批处理矩阵乘法
batch_A = tf.random.normal((10, 2, 3)) # 10个2x3矩阵
batch_B = tf.random.normal((10, 3, 4)) # 10个3x4矩阵
batch_C = tf.matmul(batch_A, batch_B) # 形状 (10,2,4)
print("批处理矩阵乘法形状:", batch_C.shape)
# 转置
print("A的转置:\n", tf.transpose(A)) # 或 A.T (TensorFlow支持)
# 高维转置需要perm参数
tensor_3d = tf.constant(np.arange(24).reshape(2,3,4))
transposed = tf.transpose(tensor_3d, perm=[0,2,1]) # 交换轴1和2
print("原始形状:", tensor_3d.shape, "转置后:", transposed.shape)
# 矩阵求逆(仅对方阵)
inv_A = tf.linalg.inv(A)
print("A的逆矩阵:\n", inv_A.numpy())
print("A * inv_A =\n", A @ inv_A) # 应接近单位阵
# 行列式
det = tf.linalg.det(A)
print("行列式值:", det.numpy())
# 特征值分解(对称矩阵)
sym_mat = tf.constant([[4, 2], [2, 3]], dtype=tf.float32)
eigenvalues, eigenvectors = tf.linalg.eigh(sym_mat) # eigh用于对称/Hermitian
print("特征值:", eigenvalues.numpy())
print("特征向量:\n", eigenvectors.numpy())
# 奇异值分解
U, S, V = tf.linalg.svd(A)
print("SVD U shape:", U.shape, "S:", S.numpy(), "V shape:", V.shape)
4.3 逻辑运算
x = tf.constant([1, 2, 3, 4, 5])
y = tf.constant([5, 4, 3, 2, 1])
# 比较运算
print("x > y:", tf.greater(x, y)) # [False, False, False, True, True]
print("x == y:", tf.equal(x, y)) # [False, False, True, False, False]
print("x <= y:", tf.less_equal(x, y)) # [True, True, True, False, False]
# 逻辑运算
a_bool = tf.constant([True, False, True])
b_bool = tf.constant([False, True, True])
print("AND:", tf.logical_and(a_bool, b_bool)) # [False, False, True]
print("OR:", tf.logical_or(a_bool, b_bool)) # [True, True, True]
print("NOT:", tf.logical_not(a_bool)) # [False, True, False]
# 条件筛选 tf.where
condition = x > 3
result = tf.where(condition, x, tf.zeros_like(x)) # 大于3保留原值,否则0
print("保留>3的元素:", result.numpy()) # [0,0,0,4,5]
# 返回满足条件的索引
indices = tf.where(condition) # 返回二维形状 (num_true, 1)
print("满足条件的索引:", indices.numpy()) # [[3],[4]]
# 复杂条件组合
cond = (x > 2) & (x < 5) # 注意使用&而非and,因为张量重载了运算符
mask = tf.logical_and(tf.greater(x, 2), tf.less(x, 5))
print("2 < x <5 的掩码:", mask.numpy()) # [False,False,True,True,False]
4.4 索引与切片操作
t = tf.reshape(tf.range(24), (4, 3, 2)) # 形状 (4,3,2)
print("原始张量:\n", t.numpy())
# 1. 基本索引
print("t[0]:\n", t[0].numpy()) # 取第一块,形状(3,2)
print("t[1,2]:\n", t[1,2].numpy()) # 元素,形状(2,)
print("t[1,2,0]:", t[1,2,0].numpy()) # 标量
# 2. 切片
print("t[1:3]:\n", t[1:3].numpy()) # 第2~3块,形状(2,3,2)
print("t[:, 0:2, :]:\n", t[:, 0:2, :].numpy()) # 每个块的1~2行,形状(4,2,2)
print("t[..., 0]:\n", t[..., 0].numpy()) # 省略号表示所有前面的维度,取最后一维索引0 -> (4,3)
print("t[::2, ::2, :]:\n", t[::2, ::2, :].numpy()) # 步长2采样
# 3. 增加维度 (None 或 tf.newaxis)
print("t[:, tf.newaxis, :, :].shape:", t[:, tf.newaxis, :, :].shape) # (4,1,3,2)
# 4. 布尔索引 (通过tf.boolean_mask)
mask = t > 10
filtered = tf.boolean_mask(t, mask)
print("大于10的元素个数:", tf.reduce_sum(tf.cast(mask, tf.int32)).numpy())
print("布尔索引后平坦化:", filtered.numpy()) # 一维张量
# 按轴保留维度的布尔索引
filtered_keepdim = tf.where(mask, t, tf.zeros_like(t))
print("保留形状的掩码结果:\n", filtered_keepdim.numpy())
# 5. 高级索引 (tf.gather / tf.gather_nd)
# 按索引收集行
indices = tf.constant([0, 2]) # 要取的第0行和第2行
gathered_rows = tf.gather(t, indices, axis=0)
print("gather行索引0和2:\n", gathered_rows.numpy()) # 形状(2,3,2)
# 收集指定位置的元素 (坐标列表)
coords = tf.constant([[0,1,0], [2,0,1]]) # 每个子列表是 (dim0, dim1, dim2) 坐标
values = tf.gather_nd(t, coords)
print("坐标(0,1,0)和(2,0,1)的值:", values.numpy())
4.5 复杂运算组合示例
# 模拟对一批图像进行随机裁剪
def random_crop(image_batch, crop_size):
"""image_batch: (batch, h, w, c), crop_size: (crop_h, crop_w)"""
batch, h, w, c = image_batch.shape
crop_h, crop_w = crop_size
# 随机生成起始位置
top = tf.random.uniform((), 0, h - crop_h + 1, dtype=tf.int32)
left = tf.random.uniform((), 0, w - crop_w + 1, dtype=tf.int32)
# 使用切片
cropped = image_batch[:, top:top+crop_h, left:left+crop_w, :]
return cropped
# 模拟数据
dummy_images = tf.random.normal((8, 224, 224, 3))
cropped = random_crop(dummy_images, (128, 128))
print("裁剪后形状:", cropped.shape) # (8,128,128,3)
# 使用tf.where实现Huber损失
def huber_loss(y_true, y_pred, delta=1.0):
error = y_true - y_pred
abs_error = tf.abs(error)
linear = abs_error > delta
quad = tf.logical_not(linear)
loss = tf.where(quad, 0.5 * tf.square(error), delta * (abs_error - 0.5 * delta))
return tf.reduce_mean(loss)
# 测试Huber损失
y_true = tf.constant([1.0, 2.0, 3.0])
y_pred = tf.constant([1.5, 2.5, 10.0])
loss_val = huber_loss(y_true, y_pred)
print("Huber损失:", loss_val.numpy())
5. 案例实操演练
案例:实现一个完整的数据标准化与特征选择流水线,包含数学运算、逻辑筛选和索引操作。
5.1 任务描述
给定一个表格数据集(模拟为NumPy数组),包含10个特征和1个目标变量。需要完成:
- 检测并剔除异常值:对每个特征,剔除超过均值±3倍标准差的样本。
- 对有效样本进行Z-score标准化。
- 根据目标变量的分位数(如中位数)将样本划分为正负类,并计算每个特征的类间均值差异,选择差异最大的前3个特征。
- 输出处理后的特征矩阵和标签。
5.2 代码实现
import tensorflow as tf
import numpy as np
# 模拟数据: 1000样本, 10个特征
np.random.seed(42)
n_samples = 1000
n_features = 10
X_raw = np.random.randn(n_samples, n_features).astype(np.float32)
# 添加一些异常值(将5%的样本的某个特征拉远)
outlier_idx = np.random.choice(n_samples, size=50, replace=False)
X_raw[outlier_idx, 0] += np.random.normal(10, 2, size=50)
y_raw = np.random.randn(n_samples).astype(np.float32) # 回归目标
# 转换为张量
X = tf.constant(X_raw)
y = tf.constant(y_raw)
# 1. 异常值检测与剔除
def remove_outliers(X, y, n_std=3.0):
# 计算每个特征的均值和标准差
mean = tf.reduce_mean(X, axis=0) # (n_features,)
std = tf.reduce_mean((X - mean) ** 2, axis=0) ** 0.5
# 判定每个样本的每个特征是否为异常
lower = mean - n_std * std
upper = mean + n_std * std
# 若任意一个特征超出范围,则样本为异常
outliers = tf.reduce_any((X < lower) | (X > upper), axis=1) # (n_samples,)
# 保留非异常样本
keep = tf.logical_not(outliers)
X_clean = tf.boolean_mask(X, keep)
y_clean = tf.boolean_mask(y, keep)
return X_clean, y_clean, keep
X_clean, y_clean, keep_mask = remove_outliers(X, y)
print(f"原始样本数: {n_samples}, 剔除后: {tf.shape(X_clean)[0].numpy()}")
# 2. Z-score标准化
def standardize(X):
mean = tf.reduce_mean(X, axis=0)
std = tf.math.reduce_std(X, axis=0)
# 避免除零
std = tf.where(std < 1e-8, tf.ones_like(std), std)
X_norm = (X - mean) / std
return X_norm, mean, std
X_norm, _, _ = standardize(X_clean)
# 3. 根据目标变量中位数划分正负类(二分类任务,将回归目标二值化)
median_y = tf.cast(tf.reduce_median(y_clean), tf.float32)
y_bin = tf.cast(y_clean > median_y, tf.int32) # 正类为1
print(f"正类样本数: {tf.reduce_sum(y_bin).numpy()}, 负类: {tf.shape(y_bin)[0] - tf.reduce_sum(y_bin).numpy()}")
# 计算每个特征在正负类上的均值差异
def feature_importance_by_class(X, y_bin):
# X: (n, f), y_bin: (n,)
X_pos = tf.boolean_mask(X, y_bin == 1)
X_neg = tf.boolean_mask(X, y_bin == 0)
mean_pos = tf.reduce_mean(X_pos, axis=0)
mean_neg = tf.reduce_mean(X_neg, axis=0)
diff = tf.abs(mean_pos - mean_neg)
return diff
diff_scores = feature_importance_by_class(X_norm, y_bin)
print("特征差异分数:", diff_scores.numpy())
# 选择差异最大的前3个特征
top_k = 3
top_indices = tf.argsort(diff_scores, direction='DESCENDING')[:top_k]
print("选中的特征索引:", top_indices.numpy())
X_selected = tf.gather(X_norm, top_indices, axis=1)
print("选择后的特征矩阵形状:", X_selected.shape)
# 最终输出前5个样本
print("前5个标准化且选择的特征:\n", X_selected[:5].numpy())
print("对应标签(二值):", y_bin[:5].numpy())
5.3 结果分析
该流水线展示了数学运算(均值、标准差、比较)、逻辑运算(|、tf.logical_not)、归约(reduce_any)以及索引操作(tf.gather, tf.boolean_mask)的有机结合。数据预处理和特征选择是实际机器学习项目中的典型流程。
6. 常见坑点与排错总结
6.1 数学运算坑点
-
坑1:整数除法截断。
tf.constant(3) / tf.constant(2)如果两者都是整数类型,结果会是整数1(截断),但TensorFlow默认会提升为浮点数?实际测试:tf.constant(3, dtype=tf.int32) / tf.constant(2, dtype=tf.int32)会报类型错误,因为整数除法不允许。需先转换为浮点型。- 解决:使用
tf.cast或确保至少一个操作数为浮点。
- 解决:使用
-
坑2:
reduce_sum忘记指定axis导致压缩所有维度,而期望保留batch维度。- 解决:明确
axis,如需保持维度可使用keepdims=True。
- 解决:明确
-
坑3:
tf.sqrt输入负数得到NaN。在损失函数中应避免对负数开方。
6.2 矩阵运算坑点
-
坑4:矩阵乘法形状不匹配时错误信息不够直观。例如
(3,4) @ (5,6)报错“Dimensions must be equal, but are 4 and 5”。- 解决:打印形状检查,或使用
tf.debugging.assert_shapes。
- 解决:打印形状检查,或使用
-
坑5:批处理矩阵乘法时,两个张量的批量维度必须一致(或其中一个为1广播)。
- 注意:
tf.matmul支持广播,但要求批量维度兼容。
- 注意:
-
坑6:求逆矩阵时矩阵奇异(行列式接近0)导致
InvalidArgumentError。- 解决:添加正则项
A + epsilon*I使其可逆,或使用伪逆tf.linalg.pinv。
- 解决:添加正则项
6.3 逻辑运算坑点
-
坑7:使用Python的
and、or代替&、|进行布尔张量运算。- 原因:
and会尝试将张量整体转换为布尔值(tf.Tensor不能隐式转换),导致ValueError。 - 解决:使用
tf.logical_and或重载的&、|。
- 原因:
-
坑8:
tf.where的广播行为不符合预期。当condition、x、y形状不同时,它们会一起广播。- 示例:
tf.where([True, False], [1,2], [3,4])没问题,但若x是标量,y是向量,广播产生结果。
- 示例:
6.4 索引切片坑点
-
坑9:切片操作中
start:stop:step的stop是开区间。例如tensor[:, :5]取前5列(索引0~4)。- 混淆:在Python中
range(5)产生0~4,习惯性记忆。
- 混淆:在Python中
-
坑10:高级索引与切片混合使用时,返回的可能是副本而非视图。不要假设修改会影响原张量。
-
坑11:
tf.gather和tf.gather_nd的batch_dims参数容易被忽略,导致批量索引错误。- 解决:仔细阅读文档,或者使用
tf.gather的axis参数。
- 解决:仔细阅读文档,或者使用
-
坑12:
tf.boolean_mask默认展平输出,若希望保持维度需设置keepdims=True(但注意此参数在最新版本中并非所有情况下都生效,可用tf.where配合掩码乘法替代)。
7. 知识点总结 + 课后作业
7.1 核心知识点梳理
- 数学运算:逐元素运算(
+,-,*,/,tf.math.*),归约(reduce_sum/mean/max等),累积(cumsum/cumprod)。 - 矩阵运算:
@/tf.matmul矩阵乘法,tf.transpose转置,tf.linalg.inv求逆,eigh/svd分解。 - 逻辑运算:比较运算符(
tf.greater等),逻辑运算符(tf.logical_and/or/not),tf.where条件选择。 - 索引切片:基本索引,切片(
start:stop:step),...省略号,tf.newaxis扩维;布尔掩码tf.boolean_mask;高级索引tf.gather、tf.gather_nd。 - 组合技巧:利用广播简化运算,使用
tf.where实现分段函数,通过tf.boolean_mask剔除异常值。
7.2 基础作业
- 创建两个张量
A形状(3, 4),B形状(4, 3),计算它们的矩阵乘积,并求乘积矩阵的转置。 - 对
tf.range(1, 11)计算其平方根,并将结果中大于2的元素置为0(使用tf.where)。 - 给定一个形状
(4, 5)的张量,提取第1、3行,第2、4列组成的子矩阵(使用索引切片和tf.gather)。
7.3 进阶实操作业
任务:实现一个K近邻分类器的核心距离计算部分
要求:
- 使用TensorFlow张量运算,不使用循环。
- 给定训练集特征矩阵
X_train形状(n_train, d)和标签y_train形状(n_train,),测试集特征矩阵X_test形状(n_test, d)。 - 计算每个测试样本与所有训练样本的欧氏距离(平方距离即可):
dist[i,j] = sum((X_test[i] - X_train[j])**2)。 - 对每个测试样本找到距离最小的k个训练样本的索引,然后根据这些样本的标签进行多数投票,得到预测标签。
- 使用
tf.math.top_k(注意其返回最小值需要取负)或tf.argsort。 - 测试使用随机生成的小数据集,并输出预测准确率(与sklearn的KNN对比验证)。
提示:
def pairwise_distances(X_test, X_train):
# 利用公式 ||a-b||^2 = ||a||^2 + ||b||^2 - 2 a·b
# 避免显式循环
pass
7.4 思考拓展题
-
tf.einsum提供了爱因斯坦求和约定,能够表达几乎所有张量运算。请使用tf.einsum实现以下操作:- 矩阵乘法
- 批次矩阵乘法
- 矩阵迹(trace)
- 向量内积
并说明其相较于专用函数的优势与劣势。
-
在
tf.function中,tf.print是图内打印,而Python的print仅在trace时执行一次。当调试张量内容时,应使用哪种方式?为什么? -
假设你需要实现一个自定义的损失函数:当预测值与真实值误差超过阈值δ时,使用绝对值误差,否则使用平方误差的一半。已有Huber损失实现,但要求误差大于δ时,损失是δ * (|error| - 0.5*δ) 而不是简单的|error|。请推导其导数,并用
tf.where实现,并解释为什么该损失函数比MSE对异常值更鲁棒。
下一课预告:TF自动梯度原理——我们将深入探索反向传播的底层逻辑,手动计算梯度,理解GradientTape的工作原理,并实现梯度裁剪等高级技巧,为训练深度网络打下核心基础。
🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航
第一部分:基础入门(1-10 课)
第二部分:神经网络核心(11-25 课)
第三部分:进阶网络与框架高阶(26-40 课)
第四部分:企业实战与项目落地(41-50 课)
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
更多推荐


所有评论(0)