![]() |
![]() |
![]() |
![]() |
總覽
在本教學課程中,我們將探討如何使用對抗式學習 (Goodfellow 等人,2014 年 論文) 搭配神經結構化學習 (NSL) 框架進行圖片分類。
對抗式學習的核心概念是使用對抗式干擾資料 (稱為對抗範例) 和自然訓練資料來訓練模型。以人眼來看,這些對抗範例看起來與原始範例相同,但干擾會使模型感到困惑並做出不正確的預測或分類。對抗範例的建構目的是為了刻意誤導模型,使其做出錯誤的預測或分類。透過使用這類範例進行訓練,模型學會在進行預測時,能夠有效抵抗對抗式干擾。
在本教學課程中,我們將說明如何運用神經結構化學習框架,套用對抗式學習來取得強健模型的程序
- 建立做為基礎模型的類神經網路。在本教學課程中,基礎模型是使用
tf.keras
Functional API 建立的;此程序也與透過tf.keras
Sequential 和 Subclassing 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
為了使模型在數值上穩定,我們透過將資料集對應到 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
結論
我們已示範如何使用神經結構化學習 (NSL) 框架,將對抗式學習用於圖片分類。我們鼓勵使用者嘗試不同的對抗式設定 (在超參數中),並查看這些設定如何影響模型強健性。