在 TensorFlow.org 上查看 | 在 Google Colab 中运行 | 在 GitHub 上查看源代码 | 下载笔记本 |
概述
在本教程中,我们将探索使用对抗学习(Goodfellow 等人,2014)进行图像分类,使用神经结构化学习 (NSL) 框架。
对抗学习的核心思想是使用对抗性扰动数据(称为对抗样本)来训练模型,除了有机训练数据之外。在人眼看来,这些对抗样本看起来与原始样本相同,但扰动会导致模型混淆并做出错误的预测或分类。对抗样本的构建是为了故意误导模型做出错误的预测或分类。通过使用此类样本进行训练,模型在做出预测时学会对对抗性扰动具有鲁棒性。
在本教程中,我们说明了使用神经结构化学习框架将对抗学习应用于获得鲁棒模型的以下过程
- 创建一个神经网络作为基础模型。在本教程中,基础模型是使用
tf.keras
函数式 API 创建的;此过程与使用tf.keras
顺序和子类化 API 创建的模型兼容。有关 TensorFlow 中 Keras 模型的更多信息,请参阅此 文档。 - 使用 NSL 框架提供的
AdversarialRegularization
包装类包装基础模型,以创建一个新的tf.keras.Model
实例。此新模型将包括对抗损失作为其训练目标中的正则化项。 - 将训练数据中的示例转换为特征字典。
- 训练和评估新模型。
初学者回顾
有一个相应的 视频解释 关于 TensorFlow 神经结构化学习 YouTube 系列中图像分类的对抗学习部分。下面,我们总结了该视频中解释的关键概念,扩展了上面概述部分中提供的解释。
NSL 框架共同优化图像特征和结构化信号,以帮助神经网络更好地学习。但是,如果在训练神经网络时没有可用的显式结构怎么办?本教程解释了一种方法,该方法涉及创建对抗邻居(从原始样本修改而来)以动态构建结构。
首先,对抗邻居被定义为样本图像的修改版本,应用了小的扰动,这些扰动会误导神经网络输出不准确的分类。这些精心设计的扰动通常基于反向梯度方向,旨在混淆神经网络在训练过程中的学习。人类可能无法区分样本图像及其生成的对抗邻居。但是,对于神经网络来说,应用的扰动在导致不准确的结论方面是有效的。
然后将生成的对抗邻居连接到样本,从而动态地逐边构建结构。使用这种连接,神经网络学会保持样本与其对抗邻居之间的相似性,同时避免因错误分类而导致的混淆,从而提高整个神经网络的质量和准确性。
下面的代码段是对所涉及步骤的高级解释,而本教程的其余部分将深入探讨技术细节。
- 读取和准备数据。加载 MNIST 数据集并将特征值归一化为范围 [0,1] 内。
import neural_structured_learning as nsl
(x_train, y_train), (x_train, y_train) = tf.keras.datasets.mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
- 构建神经网络。本示例使用顺序 Keras 基础模型。
model = tf.keras.Sequential(...)
- 配置对抗模型。包括超参数:应用于对抗正则化的乘数,经验选择步长/学习率的不同值。使用包装类围绕构建的神经网络调用对抗正则化。
adv_config = nsl.configs.make_adv_reg_config(multiplier=0.2, adv_step_size=0.05)
adv_model = nsl.keras.AdversarialRegularization(model, adv_config)
- 以标准 Keras 工作流程结束:编译、拟合、评估。
adv_model.compile(optimizer='adam', loss='sparse_categorizal_crossentropy', metrics=['accuracy'])
adv_model.fit({'feature': x_train, 'label': y_train}, epochs=5)
adv_model.evaluate({'feature': x_test, 'label': y_test})
您在这里看到的是在 2 个步骤和 3 行简单的代码中启用的对抗学习。这就是神经结构化学习框架的简洁性。在接下来的部分中,我们将扩展此过程。
设置
安装神经结构化学习包。
pip install --quiet neural-structured-learning
导入库。我们将 neural_structured_learning
缩写为 nsl
。
import matplotlib.pyplot as plt
import neural_structured_learning as nsl
import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
2023-10-03 11:17:25.316470: E tensorflow/compiler/xla/stream_executor/cuda/cuda_dnn.cc:9342] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered 2023-10-03 11:17:25.316516: E tensorflow/compiler/xla/stream_executor/cuda/cuda_fft.cc:609] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered 2023-10-03 11:17:25.316552: E tensorflow/compiler/xla/stream_executor/cuda/cuda_blas.cc:1518] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
超参数
我们收集并解释模型训练和评估的超参数(在 HParams
对象中)。
输入/输出
input_shape
:输入张量的形状。每个图像都是 28x28 像素,具有 1 个通道。num_classes
:共有 10 个类别,对应于 10 个数字 [0-9]。
模型架构
conv_filters
:一个数字列表,每个数字指定卷积层中的滤波器数量。kernel_size
:所有卷积层的 2D 卷积窗口的大小。pool_size
:每个最大池化层中对图像进行缩小的因子。num_fc_units
:每个全连接层的单元数(即宽度)。
训练和评估
batch_size
:用于训练和评估的批次大小。epochs
:训练轮数。
对抗学习
adv_multiplier
:对抗损失在训练目标中的权重,相对于标记损失。adv_step_size
:对抗性扰动的幅度。adv_grad_norm
:用于衡量对抗性扰动幅度的范数。
class HParams(object):
def __init__(self):
self.input_shape = [28, 28, 1]
self.num_classes = 10
self.conv_filters = [32, 64, 64]
self.kernel_size = (3, 3)
self.pool_size = (2, 2)
self.num_fc_units = [64]
self.batch_size = 32
self.epochs = 5
self.adv_multiplier = 0.2
self.adv_step_size = 0.2
self.adv_grad_norm = 'infinity'
HPARAMS = HParams()
MNIST 数据集
MNIST 数据集包含手写数字(从 '0' 到 '9')的灰度图像。每个图像以低分辨率(28x28 像素)显示一个数字。任务是将图像分类为 10 个类别,每个类别对应一个数字。
在这里,我们从 TensorFlow Datasets 加载 MNIST 数据集。它处理数据的下载和构建一个 tf.data.Dataset
。加载的数据集有两个子集
train
包含 60,000 个示例,以及test
包含 10,000 个示例。
两个子集中的示例都存储在特征字典中,包含以下两个键
image
:像素值数组,范围从 0 到 255。label
:真实标签,范围从 0 到 9。
datasets = tfds.load('mnist')
train_dataset = datasets['train']
test_dataset = datasets['test']
IMAGE_INPUT_NAME = 'image'
LABEL_INPUT_NAME = 'label'
2023-10-03 11:17:28.523912: E tensorflow/compiler/xla/stream_executor/cuda/cuda_driver.cc:268] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected
为了使模型在数值上稳定,我们将像素值归一化到 [0, 1],方法是将数据集映射到 normalize
函数上。在对训练集进行混洗和批处理后,我们将示例转换为特征元组 (image, label)
,用于训练基础模型。我们还提供了一个函数,用于将元组转换为字典,以便在以后使用。
def normalize(features):
features[IMAGE_INPUT_NAME] = tf.cast(
features[IMAGE_INPUT_NAME], dtype=tf.float32) / 255.0
return features
def convert_to_tuples(features):
return features[IMAGE_INPUT_NAME], features[LABEL_INPUT_NAME]
def convert_to_dictionaries(image, label):
return {IMAGE_INPUT_NAME: image, LABEL_INPUT_NAME: label}
train_dataset = train_dataset.map(normalize).shuffle(10000).batch(HPARAMS.batch_size).map(convert_to_tuples)
test_dataset = test_dataset.map(normalize).batch(HPARAMS.batch_size).map(convert_to_tuples)
基础模型
我们的基础模型将是一个神经网络,它包含 3 个卷积层,后面跟着 2 个全连接层(如 HPARAMS
中定义)。在这里,我们使用 Keras 函数式 API 定义它。您可以随意尝试其他 API 或模型架构(例如子类化)。请注意,NSL 框架支持所有三种类型的 Keras API。
def build_base_model(hparams):
"""Builds a model according to the architecture defined in `hparams`."""
inputs = tf.keras.Input(
shape=hparams.input_shape, dtype=tf.float32, name=IMAGE_INPUT_NAME)
x = inputs
for i, num_filters in enumerate(hparams.conv_filters):
x = tf.keras.layers.Conv2D(
num_filters, hparams.kernel_size, activation='relu')(
x)
if i < len(hparams.conv_filters) - 1:
# max pooling between convolutional layers
x = tf.keras.layers.MaxPooling2D(hparams.pool_size)(x)
x = tf.keras.layers.Flatten()(x)
for num_units in hparams.num_fc_units:
x = tf.keras.layers.Dense(num_units, activation='relu')(x)
pred = tf.keras.layers.Dense(hparams.num_classes)(x)
model = tf.keras.Model(inputs=inputs, outputs=pred)
return model
base_model = build_base_model(HPARAMS)
base_model.summary()
Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= image (InputLayer) [(None, 28, 28, 1)] 0 conv2d (Conv2D) (None, 26, 26, 32) 320 max_pooling2d (MaxPooling2 (None, 13, 13, 32) 0 D) conv2d_1 (Conv2D) (None, 11, 11, 64) 18496 max_pooling2d_1 (MaxPoolin (None, 5, 5, 64) 0 g2D) conv2d_2 (Conv2D) (None, 3, 3, 64) 36928 flatten (Flatten) (None, 576) 0 dense (Dense) (None, 64) 36928 dense_1 (Dense) (None, 10) 650 ================================================================= Total params: 93322 (364.54 KB) Trainable params: 93322 (364.54 KB) Non-trainable params: 0 (0.00 Byte) _________________________________________________________________
接下来,我们训练和评估基础模型。
base_model.compile(
optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['acc'])
base_model.fit(train_dataset, epochs=HPARAMS.epochs)
Epoch 1/5 1875/1875 [==============================] - 15s 7ms/step - loss: 0.1421 - acc: 0.9570 Epoch 2/5 1875/1875 [==============================] - 13s 7ms/step - loss: 0.0459 - acc: 0.9862 Epoch 3/5 1875/1875 [==============================] - 13s 7ms/step - loss: 0.0327 - acc: 0.9897 Epoch 4/5 1875/1875 [==============================] - 13s 7ms/step - loss: 0.0240 - acc: 0.9923 Epoch 5/5 1875/1875 [==============================] - 13s 7ms/step - loss: 0.0204 - acc: 0.9934 <keras.src.callbacks.History at 0x7f822f65a2e0>
results = base_model.evaluate(test_dataset)
named_results = dict(zip(base_model.metrics_names, results))
print('\naccuracy:', named_results['acc'])
313/313 [==============================] - 1s 3ms/step - loss: 0.0261 - acc: 0.9918 accuracy: 0.9918000102043152
我们可以看到,基础模型在测试集上达到了 99% 的准确率。我们将在下面的 对抗性扰动下的鲁棒性 中看到它的鲁棒性。
对抗性正则化模型
在这里,我们展示了如何使用 NSL 框架将对抗性训练合并到 Keras 模型中,只需几行代码。基础模型被包装起来,以创建一个新的 tf.Keras.Model
,其训练目标包括对抗性正则化。
首先,我们使用辅助函数 nsl.configs.make_adv_reg_config
创建一个包含所有相关超参数的配置对象。
adv_config = nsl.configs.make_adv_reg_config(
multiplier=HPARAMS.adv_multiplier,
adv_step_size=HPARAMS.adv_step_size,
adv_grad_norm=HPARAMS.adv_grad_norm
)
现在,我们可以使用 AdversarialRegularization
包装基础模型。在这里,我们创建了一个新的基础模型 (base_adv_model
),以便现有的模型 (base_model
) 可以用于以后的比较。
返回的 adv_model
是一个 tf.keras.Model
对象,其训练目标包括对抗性损失的正则化项。为了计算该损失,模型必须除了常规输入(特征 image
)之外,还要访问标签信息(特征 label
)。出于这个原因,我们将数据集中的示例从元组转换回字典。我们通过 label_keys
参数告诉模型哪个特征包含标签信息。
base_adv_model = build_base_model(HPARAMS)
adv_model = nsl.keras.AdversarialRegularization(
base_adv_model,
label_keys=[LABEL_INPUT_NAME],
adv_config=adv_config
)
train_set_for_adv_model = train_dataset.map(convert_to_dictionaries)
test_set_for_adv_model = test_dataset.map(convert_to_dictionaries)
接下来,我们编译、训练和评估对抗性正则化模型。可能会出现类似“损失字典中缺少输出”的警告,这是正常的,因为 adv_model
不依赖于基础实现来计算总损失。
adv_model.compile(
optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['acc'])
adv_model.fit(train_set_for_adv_model, epochs=HPARAMS.epochs)
Epoch 1/5 WARNING:absl:Cannot perturb non-Tensor input: dict_keys(['label']) 1875/1875 [==============================] - 24s 11ms/step - loss: 0.2940 - sparse_categorical_crossentropy: 0.1341 - sparse_categorical_accuracy: 0.9598 - scaled_adversarial_loss: 0.1599 Epoch 2/5 1875/1875 [==============================] - 21s 11ms/step - loss: 0.1231 - sparse_categorical_crossentropy: 0.0403 - sparse_categorical_accuracy: 0.9872 - scaled_adversarial_loss: 0.0827 Epoch 3/5 1875/1875 [==============================] - 21s 11ms/step - loss: 0.0875 - sparse_categorical_crossentropy: 0.0266 - sparse_categorical_accuracy: 0.9917 - scaled_adversarial_loss: 0.0609 Epoch 4/5 1875/1875 [==============================] - 21s 11ms/step - loss: 0.0711 - sparse_categorical_crossentropy: 0.0222 - sparse_categorical_accuracy: 0.9930 - scaled_adversarial_loss: 0.0489 Epoch 5/5 1875/1875 [==============================] - 21s 11ms/step - loss: 0.0545 - sparse_categorical_crossentropy: 0.0163 - sparse_categorical_accuracy: 0.9947 - scaled_adversarial_loss: 0.0382 <keras.src.callbacks.History at 0x7f814416dbe0>
results = adv_model.evaluate(test_set_for_adv_model)
named_results = dict(zip(adv_model.metrics_names, results))
print('\naccuracy:', named_results['sparse_categorical_accuracy'])
313/313 [==============================] - 2s 6ms/step - loss: 0.0704 - sparse_categorical_crossentropy: 0.0312 - sparse_categorical_accuracy: 0.9898 - scaled_adversarial_loss: 0.0392 accuracy: 0.989799976348877
我们可以看到,对抗性正则化模型在测试集上也表现得很好(99% 的准确率)。
对抗性扰动下的鲁棒性
现在,我们比较基础模型和对抗性正则化模型在对抗性扰动下的鲁棒性。
我们将使用 AdversarialRegularization.perturb_on_batch
函数来生成对抗性扰动示例。我们希望生成基于基础模型的示例。为此,我们使用 AdversarialRegularization
包装基础模型。请注意,只要我们不调用训练 (Model.fit
),模型中的学习变量就不会改变,模型仍然与 基础模型 部分中的模型相同。
reference_model = nsl.keras.AdversarialRegularization(
base_model, label_keys=[LABEL_INPUT_NAME], adv_config=adv_config)
reference_model.compile(
optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['acc'])
我们在字典中收集要评估的模型,并为每个模型创建一个度量对象。
请注意,我们使用 adv_model.base_model
,以便具有与基础模型相同的输入格式(不需要标签信息)。adv_model.base_model
中的学习变量与 adv_model
中的学习变量相同。
models_to_eval = {
'base': base_model,
'adv-regularized': adv_model.base_model
}
metrics = {
name: tf.keras.metrics.SparseCategoricalAccuracy()
for name in models_to_eval.keys()
}
以下是生成扰动示例并使用它们评估模型的循环。我们将扰动图像、标签和预测保存起来,以便在下一节中进行可视化。
perturbed_images, labels, predictions = [], [], []
for batch in test_set_for_adv_model:
perturbed_batch = reference_model.perturb_on_batch(batch)
# Clipping makes perturbed examples have the same range as regular ones.
perturbed_batch[IMAGE_INPUT_NAME] = tf.clip_by_value(
perturbed_batch[IMAGE_INPUT_NAME], 0.0, 1.0)
y_true = perturbed_batch.pop(LABEL_INPUT_NAME)
perturbed_images.append(perturbed_batch[IMAGE_INPUT_NAME].numpy())
labels.append(y_true.numpy())
predictions.append({})
for name, model in models_to_eval.items():
y_pred = model(perturbed_batch)
metrics[name](y_true, y_pred)
predictions[-1][name] = tf.argmax(y_pred, axis=-1).numpy()
for name, metric in metrics.items():
print('%s model accuracy: %f' % (name, metric.result().numpy()))
WARNING:absl:Cannot perturb non-Tensor input: dict_keys(['label']) base model accuracy: 0.514900 adv-regularized model accuracy: 0.951000
我们可以看到,当输入受到对抗性扰动时,基础模型的准确率急剧下降(从 99% 降至约 50%)。另一方面,对抗性正则化模型的准确率只下降了一点点(从 99% 降至 95%)。这证明了对抗性学习在提高模型鲁棒性方面的有效性。
对抗性扰动图像示例
在这里,我们看一下对抗性扰动图像。我们可以看到,扰动图像仍然显示出人类可以识别的数字,但可以成功地欺骗基础模型。
batch_index = 0
batch_image = perturbed_images[batch_index]
batch_label = labels[batch_index]
batch_pred = predictions[batch_index]
batch_size = HPARAMS.batch_size
n_col = 4
n_row = (batch_size + n_col - 1) // n_col
print('accuracy in batch %d:' % batch_index)
for name, pred in batch_pred.items():
print('%s model: %d / %d' % (name, np.sum(batch_label == pred), batch_size))
plt.figure(figsize=(15, 15))
for i, (image, y) in enumerate(zip(batch_image, batch_label)):
y_base = batch_pred['base'][i]
y_adv = batch_pred['adv-regularized'][i]
plt.subplot(n_row, n_col, i+1)
plt.title('true: %d, base: %d, adv: %d' % (y, y_base, y_adv))
plt.imshow(tf.keras.utils.array_to_img(image), cmap='gray')
plt.axis('off')
plt.show()
accuracy in batch 0: base model: 16 / 32 adv-regularized model: 31 / 32
结论
我们已经演示了如何使用神经结构化学习 (NSL) 框架进行图像分类的对抗性学习。我们鼓励用户尝试不同的对抗性设置(在超参数中),并查看它们如何影响模型的鲁棒性。