4、TensorFlow运算操作:算术运算、矩阵运算、广播机制、规约操作

各位同学,欢迎来到第四章。这一章我们聊聊TensorFlow里最基础、也最核心的东西——运算操作。

说实话,我刚开始学TensorFlow的时候,觉得运算嘛,不就是加减乘除?后来在项目中踩了不少坑,才发现事情没那么简单。尤其是广播机制,我第一次用的时候,结果完全出乎意料。嗯,咱们今天就把这些坑一个个填平。

4.1 算术运算:加减乘除那些事儿

算术运算,说白了就是张量之间的加减乘除。TensorFlow提供了两种方式:一种是运算符重载(+-*/),另一种是函数式API(tf.addtf.subtract等)。

我个人习惯用运算符重载,代码更简洁。但要注意,除法有陷阱。

import tensorflow as tf

a = tf.constant([[1, 2], [3, 4]])
b = tf.constant([[5, 6], [7, 8]])

# 加法
c = a + b          # 或者 tf.add(a, b)
print(c.numpy())
# 输出: [[ 6  8]
#        [10 12]]

# 除法——这里要注意
d = a / b          # 浮点除法
e = a // b         # 整数除法
print(d.numpy())   # 输出: [[0.2  0.33333334]
                   #        [0.42857143 0.5      ]]
print(e.numpy())   # 输出: [[0 0]
                   #        [0 0]]
⚠️ 避坑指南
我曾经在模型训练时,用整数除法计算学习率衰减,结果学习率直接变成0,模型再也不收敛了。排查了半天才发现是除法类型搞错了。记住:整数除法会截断小数!

还有取模运算 % 和幂运算 **,也很常用。比如在循环神经网络里,我经常用取模来重置状态。

# 取模和幂运算
f = a % 3          # 每个元素对3取模
g = a ** 2         # 每个元素平方
print(f.numpy())   # 输出: [[1 2]
                   #        [0 1]]
print(g.numpy())   # 输出: [[ 1  4]
                   #        [ 9 16]]

4.2 矩阵运算:线性代数的核心

矩阵运算在深度学习里太重要了。全连接层、卷积操作,本质上都是矩阵乘法。TensorFlow的矩阵运算非常高效,底层调用了优化的BLAS库。

最常用的就是 tf.matmul,或者直接用 @ 运算符。

# 矩阵乘法
x = tf.constant([[1, 2], [3, 4]])   # 2x2
y = tf.constant([[5, 6], [7, 8]])   # 2x2

z = x @ y          # 或者 tf.matmul(x, y)
print(z.numpy())
# 输出: [[19 22]
#        [43 50]]

你想想看,为什么矩阵乘法这么重要?因为神经网络的一层,本质上就是 y = W @ x + b。我在做图像分类项目时,一个卷积层就要做几百万次矩阵乘法,性能优化全靠它。

💡 重要提示
矩阵乘法要求维度匹配:(m, n) @ (n, p) = (m, p)。不匹配会报错,这是新手最常见的错误之一。

除了乘法,还有转置、求逆、行列式等操作。

# 转置
x_t = tf.transpose(x)
print(x_t.numpy())
# 输出: [[1 3]
#        [2 4]]

# 矩阵求逆(需要浮点类型)
x_float = tf.cast(x, tf.float32)
x_inv = tf.linalg.inv(x_float)
print(x_inv.numpy())
# 输出: [[-2.   1. ]
#        [ 1.5 -0.5]]
🔧 个人经验
我在做线性回归时,直接用 tf.linalg.inv 求解析解。但要注意,矩阵不可逆时会报错。实际项目中我更推荐用 tf.linalg.lstsq 做最小二乘解,更稳定。

4.3 广播机制:自动扩展的魔法

广播机制,说白了就是TensorFlow自动帮你把不同形状的张量「对齐」,然后做运算。这个机制非常强大,但也容易让人困惑。

为什么会这样?因为实际项目中,我们经常需要把一个标量和一个矩阵相加,或者把一个向量和一个矩阵相加。手动扩展维度太麻烦,广播机制自动帮你搞定。

# 标量 + 矩阵:广播
a = tf.constant([1, 2, 3])        # 形状 (3,)
b = tf.constant(10)               # 标量
c = a + b                         # 广播:10 扩展为 [10, 10, 10]
print(c.numpy())                  # 输出: [11 12 13]

# 向量 + 矩阵:广播
matrix = tf.constant([[1, 2, 3],
                      [4, 5, 6]])  # 形状 (2, 3)
vector = tf.constant([10, 20, 30]) # 形状 (3,)
result = matrix + vector           # 广播:vector 扩展为 (2, 3)
print(result.numpy())
# 输出: [[11 22 33]
#        [14 25 36]]

广播的规则其实很简单:从最后一个维度开始比较,如果维度相等或者其中一个为1,就可以广播。如果都不满足,就报错。

张量A形状 张量B形状 结果形状 是否可广播
(3,) (1,) (3,) ✅ 是
(3, 1) (1, 4) (3, 4) ✅ 是
(3, 4) (4,) (3, 4) ✅ 是
(3, 4) (3,) ❌ 报错 ❌ 否
⚠️ 避坑指南
我曾经在写自定义损失函数时,忘记检查张量形状,结果广播机制悄悄把错误维度的数据「对齐」了,导致损失计算完全错误。模型训练了三天才发现。所以,我建议你在关键运算前,用 tf.shape() 打印形状确认一下。

4.4 规约操作:降维的艺术

规约操作,就是沿着某个维度做聚合计算。比如求和、求均值、求最大值等。这在模型评估、损失计算中无处不在。

最常用的规约操作有:

  • tf.reduce_sum:求和
  • tf.reduce_mean:求均值
  • tf.reduce_max / tf.reduce_min:求最大/最小值
  • tf.reduce_all / tf.reduce_any:逻辑与/或
# 规约操作示例
data = tf.constant([[1, 2, 3],
                    [4, 5, 6]])   # 形状 (2, 3)

# 全局求和
total = tf.reduce_sum(data)
print(total.numpy())               # 输出: 21

# 按行求和(axis=1)
row_sum = tf.reduce_sum(data, axis=1)
print(row_sum.numpy())             # 输出: [6 15]

# 按列求均值(axis=0)
col_mean = tf.reduce_mean(data, axis=0)
print(col_mean.numpy())            # 输出: [2.5 3.5 4.5]

# 保持维度
row_sum_keep = tf.reduce_sum(data, axis=1, keepdims=True)
print(row_sum_keep.shape)          # 输出: (2, 1)
💡 关键点
keepdims=True 这个参数很实用。它保持结果的维度数不变,方便后续的广播运算。我在做批量归一化时,经常用它来保持维度结构。

还有一个很实用的操作——tf.argmaxtf.argmin,返回最大/最小值的索引。分类任务中,我们用它来获取预测的类别。

# 获取最大值的索引
logits = tf.constant([[0.1, 0.8, 0.1],
                      [0.3, 0.2, 0.5]])
predictions = tf.argmax(logits, axis=1)
print(predictions.numpy())         # 输出: [1 2]

嗯,这里要注意:tf.argmax 返回的是整数索引,不是概率值。如果你想同时获取最大值和索引,可以用 tf.math.top_k

🔧 个人经验
我在做多标签分类时,经常用 tf.math.top_k 获取前K个预测结果。比如推荐系统里,我们不是只推荐一个,而是推荐Top-5或Top-10。这个函数比 argmax 灵活得多。

4.5 综合实战:一个简单的线性回归

说了这么多,咱们来个小例子。用TensorFlow的运算操作,实现一个最简单的线性回归。

# 生成数据
x = tf.constant([[1.0], [2.0], [3.0], [4.0]])
y_true = tf.constant([[2.0], [4.0], [6.0], [8.0]])  # y = 2x

# 初始化参数
w = tf.Variable(0.0)
b = tf.Variable(0.0)

# 训练循环
for epoch in range(100):
    with tf.GradientTape() as tape:
        # 预测:矩阵乘法 + 广播
        y_pred = x @ w + b          # 广播机制自动处理 b
        
        # 损失:规约操作
        loss = tf.reduce_mean((y_pred - y_true) ** 2)
    
    # 梯度计算
    grads = tape.gradient(loss, [w, b])
    
    # 参数更新:算术运算
    w.assign_sub(0.01 * grads[0])
    b.assign_sub(0.01 * grads[1])
    
    if epoch % 20 == 0:
        print(f'Epoch {epoch}: loss = {loss.numpy():.4f}')

print(f'Final: w = {w.numpy():.2f}, b = {b.numpy():.2f}')

你看,这个例子把本章的知识点全串起来了:矩阵乘法、广播机制、规约操作、算术运算。这就是TensorFlow运算操作的精髓。

好了,第四章就到这里。下一章我们聊聊数据加载和预处理,那是实际项目中更头疼的部分。有什么问题,欢迎随时交流。