作者: Sayak Paul
创建日期: 2021/04/30
最后修改: 2023/12/18
描述: 如何为给定分辨率最佳化学习图像的表现。
人们普遍认为,如果我们限制视觉模型以人类的方式感知事物,他们的性能可以得到改善。例如,在这项工作中,Geirhos 等人展示了预训练在 ImageNet-1k 数据集上的视觉模型是偏向于纹理的,而人类则主要使用形状描述符来发展共同的感知。但是,这种信念是否总是适用,特别是在改善视觉模型的性能时?
事实证明,这并不总是如此。在训练视觉模型时,通常会将图像调整为较低的维度((224 x 224)、(299 x 299)等),以便进行小批量学习,并保持计算限制。我们通常使用 双线性插值 等图像缩放方法,并且缩放后的图像在人眼中不会失去太多的感知特征。在学习计算机视觉任务的图像缩放中,Talebi 等人展示了如果我们尝试为视觉模型优化图像的感知质量而不是为人类眼睛,他们的性能可以进一步改善。他们研究了以下问题:
给定图像分辨率和模型,如何最佳地缩放给定图像?
论文中显示,这个想法有助于持续提高预训练在 ImageNet-1k 上的常见视觉模型(如 DenseNet-121、ResNet-50、MobileNetV2 和 EfficientNets)的性能。在这个例子中,我们将实现论文中提出的可学习图像缩放模块,并在Cats and Dogs 数据集上演示,使用DenseNet-121架构。
import os
os.environ["KERAS_BACKEND"] = "tensorflow"
import keras
from keras import ops
from keras import layers
import tensorflow as tf
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
import matplotlib.pyplot as plt
import numpy as np
为了方便小批量学习,我们需要为给定批次中的图像设置一个固定形状。这就是为什么需要初始缩放。我们首先将所有图像调整为 (300 x 300) 的形状,然后为 (150 x 150) 分辨率学习它们的最佳表示。
INP_SIZE = (300, 300)
TARGET_SIZE = (150, 150)
INTERPOLATION = "bilinear"
AUTO = tf.data.AUTOTUNE
BATCH_SIZE = 64
EPOCHS = 5
在这个例子中,我们将使用双线性插值,但可学习图像缩放模块并不依赖于任何特定的插值方法。我们还可以使用其他方法,例如双三次插值。
在这个例子中,我们将只使用总训练数据集的 40%。
train_ds, validation_ds = tfds.load(
"cats_vs_dogs",
# 保留 10% 用于验证
split=["train[:40%]", "train[40%:50%]"],
as_supervised=True,
)
def preprocess_dataset(image, label):
image = ops.image.resize(image, (INP_SIZE[0], INP_SIZE[1]))
label = ops.one_hot(label, num_classes=2)
return (image, label)
train_ds = (
train_ds.shuffle(BATCH_SIZE * 100)
.map(preprocess_dataset, num_parallel_calls=AUTO)
.batch(BATCH_SIZE)
.prefetch(AUTO)
)
validation_ds = (
validation_ds.map(preprocess_dataset, num_parallel_calls=AUTO)
.batch(BATCH_SIZE)
.prefetch(AUTO)
)
下图(来自:学习计算机视觉任务的图像缩放)展示了可学习缩放模块的结构:
def conv_block(x, filters, kernel_size, strides, activation=layers.LeakyReLU(0.2)):
x = layers.Conv2D(filters, kernel_size, strides, padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
if activation:
x = activation(x)
return x
def res_block(x):
inputs = x
x = conv_block(x, 16, 3, 1)
x = conv_block(x, 16, 3, 1, activation=None)
return layers.Add()([inputs, x])
# 注意:用户如果需要,可以将num_res_blocks更改为>1
def get_learnable_resizer(filters=16, num_res_blocks=1, interpolation=INTERPOLATION):
inputs = layers.Input(shape=[None, None, 3])
# 首先,进行简单的调整大小。
naive_resize = layers.Resizing(*TARGET_SIZE, interpolation=interpolation)(inputs)
# 第一个没有批归一化的卷积块。
x = layers.Conv2D(filters=filters, kernel_size=7, strides=1, padding="same")(inputs)
x = layers.LeakyReLU(0.2)(x)
# 第二个带有批归一化的卷积块。
x = layers.Conv2D(filters=filters, kernel_size=1, strides=1, padding="same")(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.BatchNormalization()(x)
# 作为瓶颈的中间调整大小。
bottleneck = layers.Resizing(*TARGET_SIZE, interpolation=interpolation)(x)
# 残差传递。
# 第一个res_block将得到瓶颈输出作为输入
x = res_block(bottleneck)
# 剩余的res_blocks将得到上一个res_block的输出作为输入
for _ in range(num_res_blocks - 1):
x = res_block(x)
# 投影。
x = layers.Conv2D(
filters=filters, kernel_size=3, strides=1, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization()(x)
# 跳跃连接。
x = layers.Add()([bottleneck, x])
# 最终调整大小的图像。
x = layers.Conv2D(filters=3, kernel_size=7, strides=1, padding="same")(x)
final_resize = layers.Add()([naive_resize, x])
return keras.Model(inputs, final_resize, name="learnable_resizer")
learnable_resizer = get_learnable_resizer()
在这里,我们可视化了经过调整大小模块随机权重处理后的图像效果。
sample_images, _ = next(iter(train_ds))
plt.figure(figsize=(16, 10))
for i, image in enumerate(sample_images[:6]):
image = image / 255
ax = plt.subplot(3, 4, 2 * i + 1)
plt.title("输入图像")
plt.imshow(image.numpy().squeeze())
plt.axis("off")
ax = plt.subplot(3, 4, 2 * i + 2)
resized_image = learnable_resizer(image[None, ...])
plt.title("调整大小后的图像")
plt.imshow(resized_image.numpy().squeeze())
plt.axis("off")
Corrupt JPEG data: 65 extraneous bytes before marker 0xd9
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
def get_model():
backbone = keras.applications.DenseNet121(
weights=None,
include_top=True,
classes=2,
input_shape=((TARGET_SIZE[0], TARGET_SIZE[1], 3)),
)
backbone.trainable = True
inputs = layers.Input((INP_SIZE[0], INP_SIZE[1], 3))
x = layers.Rescaling(scale=1.0 / 255)(inputs)
x = learnable_resizer(x)
outputs = backbone(x)
return keras.Model(inputs, outputs)
可学习图像调整模块的结构允许与不同视觉模型进行灵活集成。
model = get_model()
model.compile(
loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1),
optimizer="sgd",
metrics=["accuracy"],
)
model.fit(train_ds, validation_data=validation_ds, epochs=EPOCHS)
Epoch 1/5
146/146 ━━━━━━━━━━━━━━━━━━━━ 1790s 12s/step - accuracy: 0.5783 - loss: 0.6877 - val_accuracy: 0.4953 - val_loss: 0.7173
Epoch 2/5
146/146 ━━━━━━━━━━━━━━━━━━━━ 1738s 12s/step - accuracy: 0.6516 - loss: 0.6436 - val_accuracy: 0.6148 - val_loss: 0.6605
Epoch 3/5
146/146 ━━━━━━━━━━━━━━━━━━━━ 1730s 12s/step - accuracy: 0.6881 - loss: 0.6185 - val_accuracy: 0.5529 - val_loss: 0.8655
Epoch 4/5
146/146 ━━━━━━━━━━━━━━━━━━━━ 1725s 12s/step - accuracy: 0.6985 - loss: 0.5980 - val_accuracy: 0.6862 - val_loss: 0.6070
Epoch 5/5
146/146 ━━━━━━━━━━━━━━━━━━━━ 1722s 12s/step - accuracy: 0.7499 - loss: 0.5595 - val_accuracy: 0.6737 - val_loss: 0.6321
<keras.src.callbacks.history.History at 0x7f126c5440a0>
plt.figure(figsize=(16, 10))
for i, image in enumerate(sample_images[:6]):
image = image / 255
ax = plt.subplot(3, 4, 2 * i + 1)
plt.title("输入图像")
plt.imshow(image.numpy().squeeze())
plt.axis("off")
ax = plt.subplot(3, 4, 2 * i + 2)
resized_image = learnable_resizer(image[None, ...])
plt.title("调整大小后的图像")
plt.imshow(resized_image.numpy().squeeze() / 10)
plt.axis("off")
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
WARNING:matplotlib.image:Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
该图表显示,通过训练图像的视觉效果得到了改善。以下表格展示了与使用双线性插值相比,使用调整模块的好处: | 模型 | 参数数量(百万) | Top-1 精度 | |:-------------------------:|:----------------:|:----------:| | 带可学习缩放器的模型 | 7.051717 | 67.67% | | 不带可学习缩放器的模型 | 7.039554 | 60.19% |
有关更多详细信息,请查看该仓库。 请注意,以上报告的模型是在猫和狗的训练集的90%上训练了10个时期,而不是这个例子。另外,注意缩放模块导致的参数数量增加是非常微不足道的。为了确保性能的提升不是由于随机性,模型使用相同的初始随机权重进行了训练。
现在,值得提出的问题是 - 提高的准确率难道不是因为向模型中添加更多层(毕竟缩放器是一个迷你网络)而造成的吗?
为了证明情况并非如此,作者进行了以下实验:
现在,作者认为使用第二种选项更好,因为这有助于模型学习如何根据给定分辨率更好地调整表示。由于结果完全是经验性的,分析跨通道交互等更多实验会更好。值得注意的是,像Squeeze and Excitation (SE) 块、Global Context (GC) 块等元素也会向现有网络添加一些参数,但它们被认为有助于网络以系统的方式处理信息,从而提高整体性能。