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)
张量的基本操作
我在项目中经常用到 reshape、transpose 和 expand_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
你看,mean 和 std 的形状是 [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 的原生控制流(如 if、for),除非你使用 tf.cond 和 tf.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)
prefetch 和 num_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 效率 |
shuffle 的 buffer_size。设得太大,会占用大量内存;设得太小,数据打乱效果不好。我一般设为数据集大小的 10% 左右。
好了,这一章的内容就到这里。张量操作、计算图、自动微分、tf.data 数据管道,这四个基础模块是后续所有实战项目的地基。下一章,我们会用这些知识来构建一个完整的图像分类模型。
记住:基础不牢,地动山摇。把这些基础打扎实了,后面的目标检测和 OCR 项目,你会写得非常顺手。