作者: fchollet
创建日期: 2020/04/27
最后修改日期: 2023/11/09
描述: 在Kaggle猫狗数据集上从头开始训练图像分类器。
本示例展示如何从头开始进行图像分类,从磁盘上的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
文件夹,Cat
和Dog
。每个子文件夹中包含每个类别的图像文件。
!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
# 删除损坏的图像
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")
当您没有大型图像数据集时,通过对训练图像应用随机但现实的变换(例如随机水平翻转或小幅随机旋转)人工引入样本多样性是一个好习惯。这有助于使模型接触到训练数据的不同方面,同时减缓过拟合。
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") # 关闭坐标轴
我们的图像已经是标准尺寸(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)
... # 其余模型
使用此选项,数据增强将在设备上发生,与模型执行的其余部分同步进行,这意味着它将受益于 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 阻塞:
# 将 `data_augmentation` 应用到训练图像上。
train_ds = train_ds.map(
lambda img, label: (data_augmentation(img), label),
num_parallel_calls=tf_data.AUTOTUNE,
)
# 在 GPU 内存中预取样本有助于最大化 GPU 利用率。
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)
# 入口块
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 # 保留残差
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)
# 投影残差
residual = layers.Conv2D(size, 1, strides=2, padding="same")(
previous_block_activation
)
x = layers.add([x, residual]) # 加回残差
previous_block_activation = x # 保留下一个残差
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)
# 我们指定 activation=None 以返回 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)
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个以上的周期,直到验证性能开始下降)。
请注意,在推断时数据增强和丢弃是无效的。
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) # 创建批次轴
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
这张图片是94.30%的猫和5.70%的狗。