4、图像预处理:数据增强(翻转、旋转、裁剪)、归一化与标准化、tf.data构建图像流水线
各位同学,欢迎来到第四章。说实话,图像预处理这块,是我个人觉得整个多模态课程里最「接地气」的一章。为什么?因为不管你模型多牛,数据进嘴之前没处理好,后面全是白搭。我在项目里见过太多人,模型结构抄得飞起,结果训练集和验证集的预处理逻辑不一样,最后精度死活上不去——嗯,这种坑我踩过,今天咱们就把它填平。
4.1 数据增强:让模型「见多识广」
数据增强说白了就是「无中生有」。你手头只有1000张猫图,但通过翻转、旋转、裁剪,你能变出10000张。模型没见过这么多姿势,自然学得更鲁棒。
我个人习惯把增强分成两类:
- 几何变换:翻转、旋转、裁剪、缩放
- 颜色变换:亮度、对比度、饱和度、色相
在TensorFlow里,这些操作都在 tf.image 模块下。咱们一个个看。
4.1.1 翻转与旋转
翻转是最简单的增强。水平翻转对大多数物体识别任务都有效,但垂直翻转要小心——你想想看,把一张人脸上下颠倒,这合理吗?
import tensorflow as tf
# 读取一张图片
image = tf.io.read_file('cat.jpg')
image = tf.image.decode_jpeg(image, channels=3)
# 水平翻转(左右)
flipped_left_right = tf.image.flip_left_right(image)
# 垂直翻转(上下)—— 慎用
flipped_up_down = tf.image.flip_up_down(image)
# 随机翻转(训练时用)
random_flip = tf.image.random_flip_left_right(image, seed=42)
旋转稍微复杂一点。TensorFlow提供了 tf.image.rot90,但只能旋转90度的整数倍。如果你需要任意角度,得用 tfa.image.rotate(TensorFlow Addons)。
# 90度旋转
rotated_90 = tf.image.rot90(image, k=1) # k=1,2,3 分别对应90,180,270度
# 随机旋转(需要tensorflow-addons)
import tensorflow_addons as tfa
angles = tf.random.uniform([1], minval=-0.2, maxval=0.2) # 弧度
random_rotated = tfa.image.rotate(image, angles, fill_mode='nearest')
fill_mode='reflect' 或 'nearest' 可以缓解,但最好配合裁剪一起用。
4.1.2 裁剪与缩放
裁剪有两种思路:中心裁剪和随机裁剪。中心裁剪适合验证集,随机裁剪适合训练集。
# 中心裁剪
cropped_center = tf.image.central_crop(image, central_fraction=0.8)
# 随机裁剪并缩放到固定大小
def random_crop_and_resize(image, target_size=(224, 224)):
# 先随机裁剪一个区域
crop_size = tf.random.uniform([], minval=0.7, maxval=1.0) * tf.cast(tf.shape(image)[0], tf.float32)
crop_size = tf.cast(crop_size, tf.int32)
cropped = tf.image.random_crop(image, [crop_size, crop_size, 3])
# 再缩放到目标大小
resized = tf.image.resize(cropped, target_size)
return resized
这里有个细节:tf.image.random_crop 要求你指定裁剪后的尺寸,但如果你想要「随机比例裁剪」,得自己算。我一般用 tf.image.stateless_random_crop,配合 seed 参数,可复现性更好。
4.2 归一化与标准化
这两个概念经常被混用,我简单区分一下:
| 操作 | 公式 | 作用 |
|---|---|---|
| 归一化 | x / 255.0 | 将像素值从[0,255]映射到[0,1] |
| 标准化 | (x - mean) / std | 使数据分布接近标准正态分布 |
归一化是必须的。为什么?因为神经网络对输入尺度敏感。你想想看,如果输入范围是0~255,权重初始化一般是0~1,这梯度一算,直接爆炸。我刚开始学的时候没做归一化,loss直接变成NaN,查了半天才发现是这个问题。
# 简单归一化
normalized = tf.cast(image, tf.float32) / 255.0
# 标准化(使用ImageNet的均值和标准差)
mean = tf.constant([0.485, 0.456, 0.406])
std = tf.constant([0.229, 0.224, 0.225])
standardized = (normalized - mean) / std
4.3 用tf.data构建图像流水线
好了,前面都是单个操作。现在咱们要把它们串起来,组成一个高效的流水线。这就是 tf.data 的强项。
4.3.1 从文件路径构建Dataset
# 假设你的图片路径列表
file_paths = ['cat1.jpg', 'cat2.jpg', 'dog1.jpg', ...]
labels = [0, 0, 1, ...] # 0:猫, 1:狗
# 构建Dataset
dataset = tf.data.Dataset.from_tensor_slices((file_paths, labels))
# 定义加载和预处理函数
def load_and_preprocess(file_path, label):
# 读取图片
image = tf.io.read_file(file_path)
image = tf.image.decode_jpeg(image, channels=3)
# 统一尺寸
image = tf.image.resize(image, [224, 224])
# 归一化
image = tf.cast(image, tf.float32) / 255.0
return image, label
# 应用预处理
dataset = dataset.map(load_and_preprocess, num_parallel_calls=tf.data.AUTOTUNE)
4.3.2 训练时加入数据增强
注意:增强只在训练时做,验证集不做。所以通常我们会定义两个不同的流水线。
# 训练集增强函数
def train_augment(image, label):
# 随机翻转
image = tf.image.random_flip_left_right(image)
# 随机亮度调整
image = tf.image.random_brightness(image, max_delta=0.2)
# 随机对比度调整
image = tf.image.random_contrast(image, lower=0.8, upper=1.2)
return image, label
# 验证集只做基本预处理
def val_preprocess(image, label):
return image, label
# 构建训练流水线
train_dataset = dataset.map(load_and_preprocess)
train_dataset = train_dataset.map(train_augment)
train_dataset = train_dataset.shuffle(buffer_size=1000)
train_dataset = train_dataset.batch(32)
train_dataset = train_dataset.prefetch(tf.data.AUTOTUNE)
# 构建验证流水线
val_dataset = dataset.map(load_and_preprocess)
val_dataset = val_dataset.map(val_preprocess)
val_dataset = val_dataset.batch(32)
val_dataset = val_dataset.prefetch(tf.data.AUTOTUNE)
prefetch(tf.data.AUTOTUNE) 一定要加。它让CPU在GPU处理当前batch时,提前准备下一个batch。不加的话,GPU会经常「空转」,训练速度慢30%以上。
4.3.3 完整的流水线示例
我把上面所有内容整合成一个完整的函数,方便你直接复用:
def build_image_pipeline(file_paths, labels, batch_size=32, is_training=True):
dataset = tf.data.Dataset.from_tensor_slices((file_paths, labels))
def load_and_preprocess(file_path, label):
image = tf.io.read_file(file_path)
image = tf.image.decode_jpeg(image, channels=3)
image = tf.image.resize(image, [224, 224])
image = tf.cast(image, tf.float32) / 255.0
return image, label
dataset = dataset.map(load_and_preprocess, num_parallel_calls=tf.data.AUTOTUNE)
if is_training:
# 训练增强
def augment(image, label):
image = tf.image.random_flip_left_right(image)
image = tf.image.random_brightness(image, 0.2)
image = tf.image.random_contrast(image, 0.8, 1.2)
# 随机裁剪缩放(模拟尺度变化)
if tf.random.uniform([]) > 0.5:
image = tf.image.central_crop(image, 0.8)
image = tf.image.resize(image, [224, 224])
return image, label
dataset = dataset.map(augment, num_parallel_calls=tf.data.AUTOTUNE)
dataset = dataset.shuffle(1000)
dataset = dataset.batch(batch_size)
dataset = dataset.prefetch(tf.data.AUTOTUNE)
return dataset
# 使用
train_ds = build_image_pipeline(train_paths, train_labels, is_training=True)
val_ds = build_image_pipeline(val_paths, val_labels, is_training=False)
4.4 避坑指南
最后,分享几个我踩过的坑:
- 增强顺序很重要:先裁剪再缩放,不要先缩放再裁剪。否则裁剪的区域分辨率不够,图像会模糊。
- 不要对验证集做随机增强:验证集需要确定性结果。我曾经不小心把随机翻转加到验证流水线里,结果每次评估结果都不一样,还以为模型没收敛。
- 注意数据类型:
tf.image.decode_jpeg输出是uint8,做归一化前要转成float32。否则除以255会变成整数除法,结果全是0。 - 缓存预处理结果:如果数据集不大,可以用
.cache()把预处理后的结果缓存到内存或磁盘,避免每次epoch都重新读取和解码。
# 缓存示例
dataset = dataset.map(load_and_preprocess).cache() # 缓存到内存
# 或者缓存到文件
dataset = dataset.map(load_and_preprocess).cache('preprocessed_cache')
好了,这一章的内容就到这里。图像预处理看似基础,但细节决定成败。下一章我们会进入文本数据的处理,到时候你会发现,文本的「预处理」比图像要麻烦得多——嗯,咱们到时候再聊。