作者: Sayak Paul
创建日期: 2021/04/13
最后修改日期: 2021/04/19
描述: 通过一致性正则化进行训练,以增强对数据分布变化的鲁棒性。
深度学习模型在许多图像识别任务中表现出色,当数据是独立同分布(i.i.d.)时。然而,它们可能会因输入数据中的微小分布变化(如随机噪声、对比度变化和模糊)而导致性能下降。因此,自然会出现一个问题:为什么会这样。如在A Fourier Perspective on Model Robustness in Computer Vision中讨论的那样,深度学习模型对这种变化没有理由具有鲁棒性。标准的模型训练程序(如标准图像分类训练工作流)并不使模型能够学习超出其所提供的训练数据。
在这个示例中,我们将通过以下方式训练一个图像分类模型,以增强其内部的一种一致性:
这个整体训练工作流程源自于像FixMatch、用于一致性训练的无监督数据增强和噪声学生训练这样的研究。由于这个训练过程鼓励模型对干净图像和噪声图像产生一致的预测,因此通常被称为一致性训练或带一致性正则化的训练。虽然这个示例侧重于使用一致性训练增强模型对常见损坏的鲁棒性,但这个示例也可以作为执行弱监督学习的模板。
此示例需要 TensorFlow 2.4 或更高版本,以及 TensorFlow Hub 和 TensorFlow Models,可以使用以下命令安装:
!pip install -q tf-models-official tensorflow-addons
from official.vision.image_classification.augment import RandAugment
from tensorflow.keras import layers
import tensorflow as tf
import tensorflow_addons as tfa
import matplotlib.pyplot as plt
tf.random.set_seed(42)
AUTO = tf.data.AUTOTUNE
BATCH_SIZE = 128
EPOCHS = 5
CROP_TO = 72
RESIZE_TO = 96
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
val_samples = 49500
new_train_x, new_y_train = x_train[: val_samples + 1], y_train[: val_samples + 1]
val_x, val_y = x_train[val_samples:], y_train[val_samples:]
Dataset
对象# 使用 2 层增强变换和强度为 9 初始化 `RandAugment` 对象。
augmenter = RandAugment(num_layers=2, magnitude=9)
在训练教师模型时,我们将仅使用两种几何增强变换:随机水平翻转和随机裁剪。
def preprocess_train(image, label, noisy=True):
image = tf.image.random_flip_left_right(image)
# 我们首先将原始图像调整为更大的尺寸,然后从中随机裁剪。
image = tf.image.resize(image, [RESIZE_TO, RESIZE_TO])
image = tf.image.random_crop(image, [CROP_TO, CROP_TO, 3])
if noisy:
image = augmenter.distort(image)
return image, label
def preprocess_test(image, label):
image = tf.image.resize(image, [CROP_TO, CROP_TO])
return image, label
train_ds = tf.data.Dataset.from_tensor_slices((new_train_x, new_y_train))
validation_ds = tf.data.Dataset.from_tensor_slices((val_x, val_y))
test_ds = tf.data.Dataset.from_tensor_slices((x_test, y_test))
我们确保 train_clean_ds
和 train_noisy_ds
使用相同的种子进行打乱,以确保它们的顺序完全相同。这在训练学生模型时会有所帮助。
# 此数据集将用于训练第一个模型。
train_clean_ds = (
train_ds.shuffle(BATCH_SIZE * 10, seed=42)
.map(lambda x, y: (preprocess_train(x, y, noisy=False)), num_parallel_calls=AUTO)
.batch(BATCH_SIZE)
.prefetch(AUTO)
)
# 这准备了 `Dataset` 对象以使用 RandAugment。
train_noisy_ds = (
train_ds.shuffle(BATCH_SIZE * 10, seed=42)
.map(preprocess_train, num_parallel_calls=AUTO)
.batch(BATCH_SIZE)
.prefetch(AUTO)
)
validation_ds = (
validation_ds.map(preprocess_test, num_parallel_calls=AUTO)
.batch(BATCH_SIZE)
.prefetch(AUTO)
)
test_ds = (
test_ds.map(preprocess_test, num_parallel_calls=AUTO)
.batch(BATCH_SIZE)
.prefetch(AUTO)
)
# 此数据集将用于训练第二个模型。
consistency_training_ds = tf.data.Dataset.zip((train_clean_ds, train_noisy_ds))
sample_images, sample_labels = next(iter(train_clean_ds))
plt.figure(figsize=(10, 10))
for i, image in enumerate(sample_images[:9]):
ax = plt.subplot(3, 3, i + 1)
plt.imshow(image.numpy().astype("int"))
plt.axis("off")
sample_images, sample_labels = next(iter(train_noisy_ds))
plt.figure(figsize=(10, 10))
for i, image in enumerate(sample_images[:9]):
ax = plt.subplot(3, 3, i + 1)
plt.imshow(image.numpy().astype("int"))
plt.axis("off")
我们现在定义我们的模型构建工具。我们的模型基于 ResNet50V2 架构。
def get_training_model(num_classes=10):
resnet50_v2 = tf.keras.applications.ResNet50V2(
weights=None, include_top=False, input_shape=(CROP_TO, CROP_TO, 3),
)
model = tf.keras.Sequential(
[
layers.Input((CROP_TO, CROP_TO, 3)),
layers.Rescaling(scale=1.0 / 127.5, offset=-1),
resnet50_v2,
layers.GlobalAveragePooling2D(),
layers.Dense(num_classes),
]
)
return model
为了可重复性,我们序列化教师网络的初始随机权重。
initial_teacher_model = get_training_model()
initial_teacher_model.save_weights("initial_teacher_model.h5")
正如在嘈杂学生训练中所提到的,如果教师模型通过 几何集成 进行训练,并且当学生模型被迫模仿这一点时,会导致更好的性能。原始工作使用 随机深度 和 Dropout 来引入集成部分,但在这个例子中,我们将使用 随机权重平均 (SWA),这也类似于几何集成。
# 定义回调。
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(patience=3)
early_stopping = tf.keras.callbacks.EarlyStopping(
patience=10, restore_best_weights=True
)
# 从 tf-hub 初始化 SWA。
SWA = tfa.optimizers.SWA
# 编译并训练教师模型。
teacher_model = get_training_model()
teacher_model.load_weights("initial_teacher_model.h5")
teacher_model.compile(
# 请注意,我们将优化器包装在 SWA 中
optimizer=SWA(tf.keras.optimizers.Adam()),
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=["accuracy"],
)
history = teacher_model.fit(
train_clean_ds,
epochs=EPOCHS,
validation_data=validation_ds,
callbacks=[reduce_lr, early_stopping],
)
# 在测试集上评估教师模型。
_, acc = teacher_model.evaluate(test_ds, verbose=0)
print(f"测试准确率: {acc*100}%")
Epoch 1/5
387/387 [==============================] - 73s 78ms/step - loss: 1.7785 - accuracy: 0.3582 - val_loss: 2.0589 - val_accuracy: 0.3920
Epoch 2/5
387/387 [==============================] - 28s 71ms/step - loss: 1.2493 - accuracy: 0.5542 - val_loss: 1.4228 - val_accuracy: 0.5380
Epoch 3/5
387/387 [==============================] - 28s 73ms/step - loss: 1.0294 - accuracy: 0.6350 - val_loss: 1.4422 - val_accuracy: 0.5900
Epoch 4/5
387/387 [==============================] - 28s 73ms/step - loss: 0.8954 - accuracy: 0.6864 - val_loss: 1.2189 - val_accuracy: 0.6520
Epoch 5/5
387/387 [==============================] - 28s 73ms/step - loss: 0.7879 - accuracy: 0.7231 - val_loss: 0.9790 - val_accuracy: 0.6500
Test accuracy: 65.83999991416931%
在这一部分,我们将借用来自这个 Keras 示例的 Distiller
类。
# 代码大部分取自:
# https://keras.io/examples/vision/knowledge_distillation/
class SelfTrainer(tf.keras.Model):
def __init__(self, student, teacher):
super().__init__()
self.student = student
self.teacher = teacher
def compile(
self, optimizer, metrics, student_loss_fn, distillation_loss_fn, temperature=3,
):
super().compile(optimizer=optimizer, metrics=metrics)
self.student_loss_fn = student_loss_fn
self.distillation_loss_fn = distillation_loss_fn
self.temperature = temperature
def train_step(self, data):
# 由于我们的数据集是两个独立数据集的压缩包,
# 在初步解析它们后,我们接下来将各自的
# 图像和标签分开。
clean_ds, noisy_ds = data
clean_images, _ = clean_ds
noisy_images, y = noisy_ds
# 教师的前向传播
teacher_predictions = self.teacher(clean_images, training=False)
with tf.GradientTape() as tape:
# 学生的前向传播
student_predictions = self.student(noisy_images, training=True)
# 计算损失
student_loss = self.student_loss_fn(y, student_predictions)
distillation_loss = self.distillation_loss_fn(
tf.nn.softmax(teacher_predictions / self.temperature, axis=1),
tf.nn.softmax(student_predictions / self.temperature, axis=1),
)
total_loss = (student_loss + distillation_loss) / 2
# 计算梯度
trainable_vars = self.student.trainable_variables
gradients = tape.gradient(total_loss, trainable_vars)
# 更新权重
self.optimizer.apply_gradients(zip(gradients, trainable_vars))
# 更新在 `compile()` 中配置的指标
self.compiled_metrics.update_state(
y, tf.nn.softmax(student_predictions, axis=1)
)
# 返回性能的字典
results = {m.name: m.result() for m in self.metrics}
results.update({"total_loss": total_loss})
return results
def test_step(self, data):
# 在推理期间,我们仅传递一个包含图像和标签的数据集。
x, y = data
# 计算预测
y_prediction = self.student(x, training=False)
# 更新指标
self.compiled_metrics.update_state(y, tf.nn.softmax(y_prediction, axis=1))
# 返回性能的字典
results = {m.name: m.result() for m in self.metrics}
return results
# 定义回调函数。
# 我们使用更大的衰减因子来稳定训练。
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(
patience=3, factor=0.5, monitor="val_accuracy"
)
early_stopping = tf.keras.callbacks.EarlyStopping(
patience=10, restore_best_weights=True, monitor="val_accuracy"
)
# 编译并训练学生模型。
self_trainer = SelfTrainer(student=get_training_model(), teacher=teacher_model)
self_trainer.compile(
# 请注意,这里我们*没有*使用SWA。
optimizer="adam",
metrics=["accuracy"],
student_loss_fn=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
distillation_loss_fn=tf.keras.losses.KLDivergence(),
temperature=10,
)
history = self_trainer.fit(
consistency_training_ds,
epochs=EPOCHS,
validation_data=validation_ds,
callbacks=[reduce_lr, early_stopping],
)
# 评估学生模型。
acc = self_trainer.evaluate(test_ds, verbose=0)
print(f"学生模型的测试准确率: {acc*100}%")
Epoch 1/5
387/387 [==============================] - 39s 84ms/step - accuracy: 0.2112 - total_loss: 1.0629 - val_accuracy: 0.4180
Epoch 2/5
387/387 [==============================] - 32s 82ms/step - accuracy: 0.3341 - total_loss: 0.9554 - val_accuracy: 0.3900
Epoch 3/5
387/387 [==============================] - 31s 81ms/step - accuracy: 0.3873 - total_loss: 0.8852 - val_accuracy: 0.4580
Epoch 4/5
387/387 [==============================] - 31s 81ms/step - accuracy: 0.4294 - total_loss: 0.8423 - val_accuracy: 0.5660
Epoch 5/5
387/387 [==============================] - 31s 81ms/step - accuracy: 0.4547 - total_loss: 0.8093 - val_accuracy: 0.5880
学生模型的测试准确率: 58.490002155303955%
评估视觉模型鲁棒性的标准基准是记录它们在像ImageNet-C和CIFAR-10-C这样被破坏的数据集上的表现,这两个数据集在针对常见腐败和扰动的神经网络鲁棒性基准中提出。对于这个例子,我们将使用CIFAR-10-C数据集,该数据集有19种不同的破坏,分为5种不同的严重性级别。为了评估模型在该数据集上的鲁棒性,我们将执行以下操作:
为了本例的目的,我们不会进行这些步骤。这就是我们仅训练模型5个epoch的原因。您可以查看这个 仓库,该仓库展示了全规模训练实验以及上述评估。 下图展示了该评估的执行摘要:
平均Top-1结果代表CIFAR-10-C数据集,测试Top-1结果代表CIFAR-10测试集。显而易见,一致性训练不仅在增强模型鲁棒性方面具有优势,在提高标准测试性能方面也表现良好。