在这里插入图片描述


1. 课前导读

1.1 本节课学习目标

  • 熟练掌握TensorFlow中的张量数学运算:逐元素运算、归约运算、累积运算。
  • 掌握矩阵运算的核心函数:矩阵乘法、转置、逆、行列式、特征值分解等。
  • 学会使用逻辑运算进行条件筛选和掩码操作。
  • 精通张量的索引与切片操作:基本索引、切片、布尔索引、花式索引(高级索引)。
  • 能够组合上述操作为复杂的数据预处理和特征工程流水线。

1.2 知识重难点

类别 内容
重点 逐元素运算与广播的结合;矩阵乘法的多种实现方式(@tf.matmultf.tensordot);高级索引与tf.gather/tf.gather_nd
难点 tf.tensordottf.einsum的灵活使用;稀疏索引与切片中的维度保持问题;逻辑运算与tf.where的条件筛选
易混淆点 逐元素乘法 * 与矩阵乘法 @tf.reduce_sumtf.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_sumaxis参数指定沿哪个维度归约。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个目标变量。需要完成:

  1. 检测并剔除异常值:对每个特征,剔除超过均值±3倍标准差的样本。
  2. 对有效样本进行Z-score标准化。
  3. 根据目标变量的分位数(如中位数)将样本划分为正负类,并计算每个特征的类间均值差异,选择差异最大的前3个特征。
  4. 输出处理后的特征矩阵和标签。

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或确保至少一个操作数为浮点。
  • 坑2reduce_sum 忘记指定axis导致压缩所有维度,而期望保留batch维度。

    • 解决:明确axis,如需保持维度可使用keepdims=True
  • 坑3tf.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的andor代替&|进行布尔张量运算。

    • 原因and会尝试将张量整体转换为布尔值(tf.Tensor不能隐式转换),导致ValueError
    • 解决:使用tf.logical_and或重载的&|
  • 坑8tf.where的广播行为不符合预期。当conditionxy形状不同时,它们会一起广播。

    • 示例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,习惯性记忆。
  • 坑10:高级索引与切片混合使用时,返回的可能是副本而非视图。不要假设修改会影响原张量。

  • 坑11tf.gathertf.gather_ndbatch_dims 参数容易被忽略,导致批量索引错误。

    • 解决:仔细阅读文档,或者使用tf.gatheraxis参数。
  • 坑12tf.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.gathertf.gather_nd
  • 组合技巧:利用广播简化运算,使用tf.where实现分段函数,通过tf.boolean_mask剔除异常值。

7.2 基础作业

  1. 创建两个张量A形状(3, 4)B形状(4, 3),计算它们的矩阵乘积,并求乘积矩阵的转置。
  2. tf.range(1, 11)计算其平方根,并将结果中大于2的元素置为0(使用tf.where)。
  3. 给定一个形状(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 思考拓展题

  1. tf.einsum 提供了爱因斯坦求和约定,能够表达几乎所有张量运算。请使用tf.einsum实现以下操作:

    • 矩阵乘法
    • 批次矩阵乘法
    • 矩阵迹(trace)
    • 向量内积
      并说明其相较于专用函数的优势与劣势。
  2. tf.function中,tf.print是图内打印,而Python的print仅在trace时执行一次。当调试张量内容时,应使用哪种方式?为什么?

  3. 假设你需要实现一个自定义的损失函数:当预测值与真实值误差超过阈值δ时,使用绝对值误差,否则使用平方误差的一半。已有Huber损失实现,但要求误差大于δ时,损失是δ * (|error| - 0.5*δ) 而不是简单的|error|。请推导其导数,并用tf.where实现,并解释为什么该损失函数比MSE对异常值更鲁棒。


下一课预告:TF自动梯度原理——我们将深入探索反向传播的底层逻辑,手动计算梯度,理解GradientTape的工作原理,并实现梯度裁剪等高级技巧,为训练深度网络打下核心基础。


🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航

去订阅

第一部分:基础入门(1-10 课)
第二部分:神经网络核心(11-25 课)
第三部分:进阶网络与框架高阶(26-40 课)
第四部分:企业实战与项目落地(41-50 课)

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

更多推荐