2、TensorFlow基础回顾:张量操作、计算图、自动微分、tf.data 数据管道构建

好,咱们正式开始。这一章我打算带大家快速过一遍 TensorFlow 的核心基础。你可能会想:“这些基础东西,网上教程一大堆,有必要专门讲吗?”

嗯,还真有必要。我在做项目时发现,很多同学代码能跑通,但一遇到奇怪的报错就懵了。说白了,就是底层机制没吃透。图像分割、目标检测这些任务,数据量大、模型复杂,基础不牢很容易翻车。

这一章,我会从四个最核心的模块入手:张量操作、计算图、自动微分、tf.data 数据管道。咱们不搞枯燥的理论,直接结合实战经验来讲。

2.1 张量操作:TensorFlow 的数据核心

张量,说白了就是多维数组。你可以把它理解成一种“数据容器”。标量是0维张量,向量是1维,矩阵是2维,图像就是3维或4维张量。

创建张量

我个人习惯用 tf.constant 来创建静态张量。比如:

import tensorflow as tf

# 创建一个标量
scalar = tf.constant(3.14)

# 创建一个向量
vector = tf.constant([1.0, 2.0, 3.0])

# 创建一个矩阵
matrix = tf.constant([[1, 2], [3, 4]])

# 创建一个3维张量(模拟一张RGB图像)
image_tensor = tf.constant(tf.random.normal([224, 224, 3]))
print(image_tensor.shape)  # 输出: (224, 224, 3)

张量的基本操作

我在项目中经常用到 reshapetransposeexpand_dims。举个例子,模型输出是 [batch, height, width, channels],但有时候需要把它变成 [batch, height*width, channels] 去做后续处理:

# 假设模型输出
output = tf.random.normal([2, 32, 32, 3])

# 展平空间维度
flattened = tf.reshape(output, [2, -1, 3])
print(flattened.shape)  # 输出: (2, 1024, 3)

# 增加一个维度(比如给单张图片加batch维度)
single_image = tf.random.normal([224, 224, 3])
batched = tf.expand_dims(single_image, axis=0)
print(batched.shape)  # 输出: (1, 224, 224, 3)
小技巧:tf.squeeze 可以去掉大小为1的维度。我经常用它来清理模型输出中多余的batch维度。

张量的数学运算

加减乘除这些就不多说了。我想强调一下 广播机制。比如你要给一批图片做归一化,减去均值再除以标准差:

images = tf.random.normal([4, 224, 224, 3])
mean = tf.constant([0.485, 0.456, 0.406])
std = tf.constant([0.229, 0.224, 0.225])

# 广播机制自动对齐维度
normalized = (images - mean) / std

你看,meanstd 的形状是 [3],而 images 是 [4, 224, 224, 3]。TensorFlow 会自动把 [3] 广播成 [1, 1, 1, 3],然后做逐元素运算。这个机制用好了,代码会非常简洁。

2.2 计算图:从静态到动态的演变

计算图,是 TensorFlow 1.x 时代的核心概念。那时候你得先定义好整个计算图,然后在一个 Session 里运行。说实话,调试起来挺痛苦的。

我记得刚接触 TensorFlow 1.x 时,写一个简单的线性回归都要折腾半天。定义占位符、定义损失函数、定义优化器,最后还要手动初始化变量。一旦图结构错了,就得从头再来。

到了 TensorFlow 2.x,默认开启了 Eager Execution(动态图模式)。这意味着你可以像写普通 Python 代码一样,逐行执行并查看结果。调试体验好了不止一个档次。

Eager Execution 示例

# 动态图模式下,直接计算
a = tf.constant([[1, 2], [3, 4]])
b = tf.constant([[5, 6], [7, 8]])
c = tf.matmul(a, b)
print(c.numpy())  # 直接输出结果

什么时候还需要静态图?

虽然动态图很方便,但静态图在性能上有优势。你可以用 @tf.function 装饰器把一段 Python 代码编译成静态图。我在部署模型时经常用这个技巧:

@tf.function
def predict_step(images):
    # 模型前向传播
    logits = model(images, training=False)
    return tf.nn.softmax(logits)

# 第一次调用会编译图,后续调用直接执行编译后的图
output = predict_step(test_images)
注意: @tf.function 内部不要使用 Python 的原生控制流(如 iffor),除非你使用 tf.condtf.while_loop。否则可能会触发“图外操作”,导致性能下降甚至报错。

2.3 自动微分:梯度计算的利器

自动微分,说白了就是自动帮你算导数。在深度学习中,我们通过反向传播来更新模型参数。TensorFlow 提供了 tf.GradientTape 来记录计算过程,然后自动求导。

基本用法

x = tf.Variable(3.0)

with tf.GradientTape() as tape:
    y = x ** 2 + 2 * x + 1

# 计算 dy/dx
grad = tape.gradient(y, x)
print(grad.numpy())  # 输出: 8.0 (因为导数 2x+2,在x=3时为8)

实战中的用法:训练一个简单模型

我在做图像分割时,经常需要自定义训练循环。这时候 GradientTape 就派上用场了:

# 假设有一个简单的线性模型
model = tf.keras.Sequential([tf.keras.layers.Dense(1)])
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)

# 训练一步
def train_step(x, y):
    with tf.GradientTape() as tape:
        predictions = model(x, training=True)
        loss = tf.reduce_mean(tf.square(predictions - y))
    
    # 计算梯度
    gradients = tape.gradient(loss, model.trainable_variables)
    # 更新参数
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))
    return loss
避坑指南: 我曾经犯过一个错误——在 GradientTape 内部修改了 tf.Variable 的值。这会导致梯度计算错误。记住:GradientTape 只记录 计算过程,不要在里面修改变量。

多个梯度计算

有时候你需要计算多个梯度,可以设置 persistent=True

x = tf.Variable(2.0)
y = tf.Variable(3.0)

with tf.GradientTape(persistent=True) as tape:
    z = x ** 2 + y ** 3

dz_dx = tape.gradient(z, x)  # 2x = 4
dz_dy = tape.gradient(z, y)  # 3y^2 = 27
del tape  # 释放资源

2.4 tf.data 数据管道:高效数据加载的艺术

数据加载,往往是训练流程中最容易被忽视的瓶颈。我见过不少同学,模型写得挺漂亮,但训练时 GPU 利用率只有 20%,一查发现是数据加载太慢了。

tf.data 就是来解决这个问题的。它提供了一套高效的数据管道构建工具。

从 NumPy 数组创建数据集

import numpy as np

# 模拟1000张图片和标签
images = np.random.rand(1000, 224, 224, 3).astype(np.float32)
labels = np.random.randint(0, 10, size=(1000,))

dataset = tf.data.Dataset.from_tensor_slices((images, labels))

数据预处理与批处理

这是最常用的操作。我一般会这样写:

def preprocess(image, label):
    # 数据增强:随机翻转
    image = tf.image.random_flip_left_right(image)
    # 归一化
    image = image / 255.0
    return image, label

dataset = dataset.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)
dataset = dataset.shuffle(buffer_size=100)
dataset = dataset.batch(32)
dataset = dataset.prefetch(tf.data.AUTOTUNE)
关键点: prefetchnum_parallel_calls=tf.data.AUTOTUNE 这两个参数一定要加上。它们能让数据加载和模型训练并行进行,充分利用 CPU 和 GPU。

从文件读取数据

对于图像分割任务,数据通常以文件形式存储。我习惯用 tf.data.Dataset.list_files 配合 map 来加载:

# 假设图片文件路径列表
file_paths = ['image1.jpg', 'image2.jpg', ...]

def load_image(path):
    image = tf.io.read_file(path)
    image = tf.image.decode_jpeg(image, channels=3)
    image = tf.image.resize(image, [224, 224])
    return image

dataset = tf.data.Dataset.from_tensor_slices(file_paths)
dataset = dataset.map(load_image, num_parallel_calls=tf.data.AUTOTUNE)
dataset = dataset.batch(32).prefetch(tf.data.AUTOTUNE)

性能调优建议

操作 作用 我的建议
cache() 缓存数据集到内存或文件 如果数据集能装进内存,用 cache() 可以省去重复读取的耗时
prefetch() 预取下一批数据 一定要加,让 CPU 和 GPU 流水线工作
num_parallel_calls 并行处理 设为 tf.data.AUTOTUNE,让 TensorFlow 自动选择最优并行数
interleave 交错读取多个文件 适合从多个大文件中读取数据,能提高 I/O 效率
注意: 不要滥用 shufflebuffer_size。设得太大,会占用大量内存;设得太小,数据打乱效果不好。我一般设为数据集大小的 10% 左右。

好了,这一章的内容就到这里。张量操作、计算图、自动微分、tf.data 数据管道,这四个基础模块是后续所有实战项目的地基。下一章,我们会用这些知识来构建一个完整的图像分类模型。

记住:基础不牢,地动山摇。把这些基础打扎实了,后面的目标检测和 OCR 项目,你会写得非常顺手。