針對圖片分類的對抗式正規化

在 TensorFlow.org 上查看 在 Google Colab 中執行 在 GitHub 上查看原始碼 下載筆記本

總覽

在本教學課程中,我們將探討如何使用對抗式學習 (Goodfellow 等人,2014 年 論文) 搭配神經結構化學習 (NSL) 框架進行圖片分類。

對抗式學習的核心概念是使用對抗式干擾資料 (稱為對抗範例) 和自然訓練資料來訓練模型。以人眼來看,這些對抗範例看起來與原始範例相同,但干擾會使模型感到困惑並做出不正確的預測或分類。對抗範例的建構目的是為了刻意誤導模型,使其做出錯誤的預測或分類。透過使用這類範例進行訓練,模型學會在進行預測時,能夠有效抵抗對抗式干擾。

在本教學課程中,我們將說明如何運用神經結構化學習框架,套用對抗式學習來取得強健模型的程序

  1. 建立做為基礎模型的類神經網路。在本教學課程中,基礎模型是使用 tf.keras Functional API 建立的;此程序也與透過 tf.keras Sequential 和 Subclassing API 建立的模型相容。如需 TensorFlow 中 Keras 模型的詳細資訊,請參閱這份文件
  2. 使用 NSL 框架提供的 AdversarialRegularization 包裝函式類別包裝基礎模型,以建立新的 tf.keras.Model 執行個體。這個新模型會在訓練目標中加入做為正規化項目的對抗式損失。
  3. 將訓練資料中的範例轉換為特徵字典。
  4. 訓練及評估新模型。

給初學者的重點回顧

TensorFlow 神經結構化學習 YouTube 系列中有一部關於圖片分類對抗式學習的相關影片說明。以下我們整理了這部影片中說明的重點概念,並擴充了上方「總覽」章節中提供的說明。

NSL 框架共同最佳化圖片特徵和結構化訊號,以協助類神經網路更妥善地學習。然而,如果沒有可用的明確結構來訓練類神經網路,該怎麼辦?本教學課程說明一種方法,其中涉及建立對抗鄰居 (從原始範例修改而來) 以動態建構結構。

首先,對抗鄰居的定義是經過修改的範例圖片版本,其中套用了細微的干擾,會誤導類神經網路輸出不精確的分類。這些經過仔細設計的干擾通常以反向梯度方向為基礎,目的是在訓練期間混淆類神經網路。人類可能無法分辨範例圖片及其產生的對抗鄰居之間的差異。然而,對於類神經網路而言,套用的干擾可以有效地導致不精確的結論。

產生的對抗鄰居接著會連接到範例,因此以逐邊方式動態建構結構。透過這種連接方式,類神經網路學會維持範例與對抗鄰居之間的相似性,同時避免因錯誤分類而造成的混淆,進而提升整體類神經網路的品質和準確度。

以下程式碼片段高度說明了所涉及的步驟,而本教學課程的其餘部分則會深入探討技術細節。

  1. 讀取並準備資料。載入 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
  1. 建構類神經網路。本範例使用循序 Keras 基礎模型。
model = tf.keras.Sequential(...)
  1. 設定對抗式模型。包括超參數:套用在對抗式正規化上的乘數,以及憑經驗選擇的步長/學習率差異值。使用圍繞建構的類神經網路的包裝函式類別,叫用對抗式正規化。
adv_config = nsl.configs.make_adv_reg_config(multiplier=0.2, adv_step_size=0.05)
adv_model = nsl.keras.AdversarialRegularization(model, adv_config)
  1. 以標準 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

為了使模型在數值上穩定,我們透過將資料集對應到 normalize 函式,將像素值正規化為 [0, 1]。在隨機排序訓練集和批次處理後,我們會將範例轉換為特徵元組 (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 Functional 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 物件,其訓練目標包含對抗式損失的正規化項目。為了計算該損失,模型必須能夠存取標籤資訊 (特徵 label) 以及一般輸入 (特徵 image)。因此,我們會將資料集中的範例從元組轉換回字典。我們也會透過 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

png

結論

我們已示範如何使用神經結構化學習 (NSL) 框架,將對抗式學習用於圖片分類。我們鼓勵使用者嘗試不同的對抗式設定 (在超參數中),並查看這些設定如何影響模型強健性。