代码示例 / 计算机视觉 / 从零开始进行图像分类

从零开始进行图像分类

作者: fchollet
创建日期 2020/04/27
最后修改日期 2023/11/09
描述:在 Kaggle 猫狗数据集上从零开始训练一个图像分类器。

ⓘ 本示例使用 Keras 3

在 Colab 中查看 GitHub 源代码


简介

本示例展示了如何从零开始进行图像分类,即从磁盘上的 JPEG 图像文件开始,不利用预训练权重或预制的 Keras 应用模型。我们将在 Kaggle 的猫狗二元分类数据集上演示此工作流程。

我们将使用 image_dataset_from_directory 工具生成数据集,并使用 Keras 图像预处理层进行图像标准化和数据增强。


设置

import os
import numpy as np
import keras
from keras import layers
from tensorflow import data as tf_data
import matplotlib.pyplot as plt

加载数据:猫狗数据集

原始数据下载

首先,让我们下载大小为 786M 的原始数据 ZIP 压缩包。

!curl -O https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_5340.zip
!unzip -q kagglecatsanddogs_5340.zip
!ls
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  786M  100  786M    0     0  11.1M      0  0:01:10  0:01:10 --:--:-- 11.8M

 CDLA-Permissive-2.0.pdf           kagglecatsanddogs_5340.zip
 PetImages                'readme[1].txt'
 image_classification_from_scratch.ipynb

现在我们有了一个 PetImages 文件夹,其中包含两个子文件夹 CatDog。每个子文件夹都包含相应类别的图像文件。

!ls PetImages
Cat  Dog

过滤掉损坏的图像

在处理大量真实世界的图像数据时,损坏的图像是常见问题。让我们过滤掉那些文件头中不包含字符串“JFIF”的编码错误的图像。

num_skipped = 0
for folder_name in ("Cat", "Dog"):
    folder_path = os.path.join("PetImages", folder_name)
    for fname in os.listdir(folder_path):
        fpath = os.path.join(folder_path, fname)
        try:
            fobj = open(fpath, "rb")
            is_jfif = b"JFIF" in fobj.peek(10)
        finally:
            fobj.close()

        if not is_jfif:
            num_skipped += 1
            # Delete corrupted image
            os.remove(fpath)

print(f"Deleted {num_skipped} images.")
Deleted 1590 images.

生成一个 Dataset

image_size = (180, 180)
batch_size = 128

train_ds, val_ds = keras.utils.image_dataset_from_directory(
    "PetImages",
    validation_split=0.2,
    subset="both",
    seed=1337,
    image_size=image_size,
    batch_size=batch_size,
)
Found 23410 files belonging to 2 classes.
Using 18728 files for training.
Using 4682 files for validation.

可视化数据

以下是训练数据集中的前 9 张图像。

plt.figure(figsize=(10, 10))
for images, labels in train_ds.take(1):
    for i in range(9):
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(np.array(images[i]).astype("uint8"))
        plt.title(int(labels[i]))
        plt.axis("off")

png


使用图像数据增强

当你的图像数据集不大时,一个好的做法是通过对训练图像应用随机但逼真的变换来人为地增加样本多样性,例如随机水平翻转或小角度随机旋转。这有助于让模型接触到训练数据的不同方面,同时减缓过拟合。

data_augmentation_layers = [
    layers.RandomFlip("horizontal"),
    layers.RandomRotation(0.1),
]


def data_augmentation(images):
    for layer in data_augmentation_layers:
        images = layer(images)
    return images

让我们通过对数据集中的前几张图像重复应用 data_augmentation 来可视化增强后的样本是什么样子的。

plt.figure(figsize=(10, 10))
for images, _ in train_ds.take(1):
    for i in range(9):
        augmented_images = data_augmentation(images)
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(np.array(augmented_images[0]).astype("uint8"))
        plt.axis("off")

png


标准化数据

我们的图像已经处于标准尺寸(180x180),因为我们的数据集会生成连续的 float32 批次。然而,它们的 RGB 通道值在 [0, 255] 范围内。这对于神经网络来说并不理想;通常你应该设法让你的输入值变小。在这里,我们将在模型开头使用一个 Rescaling 层,将值标准化到 [0, 1] 范围内。


两种预处理数据的选项

你可以通过两种方式使用 data_augmentation 预处理器:

选项 1:将其作为模型的一部分,像这样:

inputs = keras.Input(shape=input_shape)
x = data_augmentation(inputs)
x = layers.Rescaling(1./255)(x)
...  # Rest of the model

使用此选项,你的数据增强将在*设备上*与模型的其余部分同步执行,这意味着它将受益于 GPU 加速。

请注意,数据增强在测试时是不活动的,因此输入样本只会在 fit() 期间被增强,而在调用 evaluate()predict() 时不会。

如果你在 GPU 上进行训练,这可能是一个不错的选择。

选项 2:将其应用于数据集,以获得一个能够生成增强图像批次的数据集,像这样:

augmented_train_ds = train_ds.map(
    lambda x, y: (data_augmentation(x, training=True), y))

使用此选项,你的数据增强将在 CPU 上异步进行,并在进入模型之前进行缓冲。

如果你在 CPU 上进行训练,这是更好的选择,因为它使数据增强异步且非阻塞。

在我们的案例中,我们将选择第二种方案。如果你不确定该选哪种,第二种方案(异步预处理)总是一个稳妥的选择。


为性能配置数据集

让我们对训练数据集应用数据增强,并确保使用缓冲预取,这样我们就可以从磁盘中读取数据而不会导致 I/O 阻塞。

# Apply `data_augmentation` to the training images.
train_ds = train_ds.map(
    lambda img, label: (data_augmentation(img), label),
    num_parallel_calls=tf_data.AUTOTUNE,
)
# Prefetching samples in GPU memory helps maximize GPU utilization.
train_ds = train_ds.prefetch(tf_data.AUTOTUNE)
val_ds = val_ds.prefetch(tf_data.AUTOTUNE)

构建一个模型

我们将构建一个小型版本的 Xception 网络。我们没有特别尝试去优化架构;如果你想系统地搜索最佳模型配置,可以考虑使用 KerasTuner

请注意:

  • 我们以 data_augmentation 预处理器开始模型,其后是一个 Rescaling 层。
  • 我们在最终的分类层之前包含了一个 Dropout 层。
def make_model(input_shape, num_classes):
    inputs = keras.Input(shape=input_shape)

    # Entry block
    x = layers.Rescaling(1.0 / 255)(inputs)
    x = layers.Conv2D(128, 3, strides=2, padding="same")(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation("relu")(x)

    previous_block_activation = x  # Set aside residual

    for size in [256, 512, 728]:
        x = layers.Activation("relu")(x)
        x = layers.SeparableConv2D(size, 3, padding="same")(x)
        x = layers.BatchNormalization()(x)

        x = layers.Activation("relu")(x)
        x = layers.SeparableConv2D(size, 3, padding="same")(x)
        x = layers.BatchNormalization()(x)

        x = layers.MaxPooling2D(3, strides=2, padding="same")(x)

        # Project residual
        residual = layers.Conv2D(size, 1, strides=2, padding="same")(
            previous_block_activation
        )
        x = layers.add([x, residual])  # Add back residual
        previous_block_activation = x  # Set aside next residual

    x = layers.SeparableConv2D(1024, 3, padding="same")(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation("relu")(x)

    x = layers.GlobalAveragePooling2D()(x)
    if num_classes == 2:
        units = 1
    else:
        units = num_classes

    x = layers.Dropout(0.25)(x)
    # We specify activation=None so as to return logits
    outputs = layers.Dense(units, activation=None)(x)
    return keras.Model(inputs, outputs)


model = make_model(input_shape=image_size + (3,), num_classes=2)
keras.utils.plot_model(model, show_shapes=True)

png


训练模型

epochs = 25

callbacks = [
    keras.callbacks.ModelCheckpoint("save_at_{epoch}.keras"),
]
model.compile(
    optimizer=keras.optimizers.Adam(3e-4),
    loss=keras.losses.BinaryCrossentropy(from_logits=True),
    metrics=[keras.metrics.BinaryAccuracy(name="acc")],
)
model.fit(
    train_ds,
    epochs=epochs,
    callbacks=callbacks,
    validation_data=val_ds,
)
Epoch 1/25
...
Epoch 25/25
 147/147 ━━━━━━━━━━━━━━━━━━━━ 53s 354ms/step - acc: 0.9638 - loss: 0.0903 - val_acc: 0.9382 - val_loss: 0.1542

<keras.src.callbacks.history.History at 0x7f41003c24a0>

在整个数据集上训练 25 个周期后,我们得到了超过 90% 的验证准确率(实际上,你可以训练 50 个周期以上,验证性能才会开始下降)。


在新数据上运行推理

请注意,数据增强和 dropout 在推理时是不活动的。

img = keras.utils.load_img("PetImages/Cat/6779.jpg", target_size=image_size)
plt.imshow(img)

img_array = keras.utils.img_to_array(img)
img_array = keras.ops.expand_dims(img_array, 0)  # Create batch axis

predictions = model.predict(img_array)
score = float(keras.ops.sigmoid(predictions[0][0]))
print(f"This image is {100 * (1 - score):.2f}% cat and {100 * score:.2f}% dog.")
 1/1 ━━━━━━━━━━━━━━━━━━━━ 2s 2s/step
This image is 94.30% cat and 5.70% dog.

png