Направленные состязательные (Targeted adversarial) атаки с использование Keras и TensorFlow

В прошлом руководстве была рассмотрена базовая состязательная атака на сверточную нейронную сеть (CNN), однако, она (атака) обладала существенным недостатком: замещающий (целевой) класс был заранее неизвестен и неконтролируем. В этой заметке будет рассмотрена направленная атака, то есть та, в которой целевой класс контролируется атакующим.

Чем направленная состязательная атака отличается от ненаправленной?

При создании простейшей ненаправленной состязательной атаки невозможно контролировать, какой будет окончательная метка выходного класса атакуемого изображения, а единственная цель — заставить модель неправильно классифицировать изображение.

С другой стороны, направленные состязательные атаки нацелены на то, чтобы предоставить больший контроль над окончательной прогнозируемой меткой.

Настройка среды разработки

Также, как обычно, для этой статьи специально подготовлен блокнот в экосистеме Google Colaboratory, так что Вам не придётся что-то специально настраивать.

Направленные состязательные (Targeted adversarial) атаки с использование Keras и TensorFlow (Google Colab)

Обычная классификация изображений без атак с использованием Keras и TensorFlow

Также как и для самой простой состязательной атаки понадобится реализация нормальной классификации, которая полностью идентична рассмотренной в статье Состязательные (Adversarial) атаки с помощью Keras и TensorFlow. По этой причине в основном тексте стати разбора приводится не будет (в блокноте Google Colab эти шаги по очевидным причинам присутствуют).

Также, как и в прошлый раз, будем использовать изображение кота.

Как можно видеть выше, используемая нейронная сеть классифицировала изображение, как Египетского кота, это не совсем корректно, но класс животного (кот) определен верно.

Запомним индекс оригинального класса изображения (Egyptian_cat => 285).

Реализация направленных состязательных атак с помощью Keras и TensorFlow

Теперь рассмотрим, как реализовать направленные состязательные атаки с помощью Keras и TensorFlow. Часть используемого кода будет совпадать с ненаправленной атакой.

Начнем, как обычно, с импорта зависимостей:

# import necessary packages
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.applications import ResNet50
from tensorflow.keras.losses import SparseCategoricalCrossentropy
from tensorflow.keras.applications.resnet50 import decode_predictions
from tensorflow.keras.applications.resnet50 import preprocess_input
import tensorflow as tf
import numpy as np
import argparse
import cv2

Вы можете заметить, что снова используется архитектуру ResNet50 с соответствующей функцией preprocess_input (для предварительной обработки и масштабирования входных изображений) и утилитой decode_predictions для декодирования выходных прогнозов и отображения классов ImageNet.

SparseCategoricalCrossentropy вычисляет функцию потери кросс-энтропии между реальными классами и их прогнозами.

Так же, как и в прошлый раз нужно создать функцию для предобработки изображения, однако она будет несколько отличаться:

def preprocess_image_attack(image):
	# swap color channels, resize the input image, and add a batch
	# dimension
	image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
	image = cv2.resize(image, (224, 224))
	image = np.expand_dims(image, axis=0)
	# return the preprocessed image
	return image

Эта реализация идентична написанной ранее, за исключением того, что не используется вызов функции preprocess_input — чуть позже станет понятно, почему эта функция не вызывается.

Далее, напишем простую вспомогательную функцию clip_eps:

def clip_eps(tensor, eps):
	# clip the values of the tensor to a given range and return it
	return tf.clip_by_value(tensor, clip_value_min=-eps,
		clip_value_max=eps)

Задача этой функции — принять тензор, а затем вырезать любые значения в диапазоне [-eps, eps].

Обрезанный тензор затем возвращается вызывающей функции.

Теперь рассмотрим функцию generate_adversaries, которая является основой состязательной атаки:

def generate_adversaries(model, baseImage, delta, classIdx, target, steps=50):
	# Цикл по количеству шагов
	for step in range(0, steps):
		# Сохраняем градиент
		with tf.GradientTape() as tape:
			# явно указать, что наш вектор искажения должен
			# отслеживаться для обновления градиента
			tape.watch(delta)
   			# добавляем наш вектор искажения к базовому изображению
			# и предварительно обрабатываем полученное изображение
			adversary = preprocess_input(baseImage + delta)
			# пропускаем этот недавно построенный тензор изображения
			# через нашу модель и вычисляем потери относительно
			# *исходного* индекса класса
			predictions = model(adversary, training=False)
			originalLoss = -sccLoss(tf.convert_to_tensor([classIdx]),
				predictions)
			# НОВОЕ. Добавляем функцию потерь относительно целевого класса
			targetLoss = sccLoss(tf.convert_to_tensor([target]),
				predictions)
			totalLoss = originalLoss + targetLoss
			# проверьте, выводим ли мы значение потерь, и если да,
			# отображаем его на терминале
			if step % 20 == 0:
				print("step: {}, loss: {}...".format(step,
					totalLoss.numpy()))
		# вычисляем градиенты потерь 
		# относительно вектора искажения
		gradients = tape.gradient(totalLoss, delta)
		# обновить веса, отсечь вектор искажения и
		# обновить его значение
		optimizer.apply_gradients([(gradients, delta)])
		delta.assign_add(clip_eps(delta, eps=EPS))
	# вернуть вектор искажения
	return delta

Метод generate_adversaries — это основная часть атакующего сценария. Эта функция принимает четыре обязательных параметра и необязательный пятый:

  • model: модель ResNet50 (ее можно заменить на другую предварительно обученную модель, такую как VGG16, MobileNet и т. д., если хотите попробовать атаку на другую модель).
  • baseImage: исходное входное изображение, для которого нужно создать состязательную атаку, в результате которой модель должна будет неверно классифицировать.
  • delta: вектор шума, который будет добавлен к baseImage, что в конечном итоге приведет к ошибочной классификации. Этот дельта-вектор обновится с помощью градиентного спуска.
  • classIdx: Целочисленный индекс метки класса, который был получен, при нормальной работе сети.
  • target: целевой класс атаки
  • steps: количество выполняемых шагов градиентного спуска (по умолчанию 50 шагов).

Функция имеет лишь одно принципиальное отличие от простейшей атаки: строки 18-25. В этих строках добавлено вычисление составной функции потерь, состоящей из суммы рассчитанной, как и в прошлый раз «близости» к истинному классу и новой составляющей, описываюшей «близость» к целевому классу.

Теперь мы можем выполнить пару инициализаций, загрузить и предварительно обработать изображение нашего кота:

# определить эпсилон и константы скорости обучения
EPS = 2 / 255.0
LR = 0.005
# загрузить входное изображение с диска и предварительно обработать его
print("[INFO] loading image...")
image = cv2.imread("cat_3.jpg")
image = preprocess_image_attack(image)

class_idx = 285
target_class_idx = 94

В строке 2 определяется значение epsilon (EPS), используемое для отсечения тензоров при построении состязательного изображения. Значение EPS, равное 2/255,0, является стандартным значением, используемым в публикациях и учебных пособиях, связанных с атаками.

Затем определяется скорость обучения в строке 3. Значение LR = 0.005 было получено путем эмпирической настройки — вам может потребоваться обновить это значение при построении ваших собственных состязательных изображений.

Строки 6 и 7 содержат команды загрузки входного изображение с диска и предварительной обрабоки его с помощью вспомогательной функции preprocess_image.

В строке 10 задается целевой класс атаки: 94 (hummingbird), то есть колибри.

Затем загружаем модель ResNet:

# загрузить предварительно обученную модель ResNet50 для выполнения вывода
print("[INFO] loading pre-trained ResNet50 model...")
model = ResNet50(weights="imagenet")
# инициализировать оптимизатор и функцию потерь
optimizer = Adam(learning_rate=LR)
sccLoss = SparseCategoricalCrossentropy()

В строке 3 загружается модель ResNet50, предварительно обученная на наборе данных ImageNet.

Оптимизатор Adam будет использоваться вместе с реализацией разреженных категориальных потерь при обновлении вектора искажения.

Теперь создадим атакующее изображение:

# создать тензор на основе входного изображения и инициализировать 
# вектор искажения (этот вектор будет обновляться в процессе обучения)
baseImage = tf.constant(image, dtype=tf.float32)
delta = tf.Variable(tf.zeros_like(baseImage), trainable=True)
# сгенерировать вектор искажения для создания состязательного примера
print("[INFO] generating perturbation...")
deltaUpdated = generate_adversaries(model, baseImage, delta, class_idx, target_class_idx, steps=200)

# создать состязательный пример, поменять местами цветовые каналы 
# и сохранить выходное изображение на диск
print("[INFO] creating adversarial example...")
adverImage = (baseImage + deltaUpdated).numpy().squeeze()
adverImage = np.clip(adverImage, 0, 255).astype("uint8")
adverImage = cv2.cvtColor(adverImage, cv2.COLOR_RGB2BGR)
cv2.imwrite("wrong_cat.jpeg", adverImage)

В строке 3 создается тензор из входного изображения, а в строке 4 инициализируется вектор искажения.

Чтобы фактически создать и обновить дельта-вектор, вызывается функция generate_adversaries, в которую передается модель ResNet50, входное изображение, вектор искажения и индекс исходного класса.

Функция generate_adversaries запускается, попутно обновляя вектор дельта-искажения, в результате чего получается deltaUpdated, окончательный вектор шума.

Создается окончательное состязательное изображение (adverImage) в строке 12, путем добавления вектора deltaUpdated к baseImage.

После этого выполняется постобработке полученного состязательного изображения:

  • Отсечение любых значений, выходящих за пределы диапазона [0, 255]
  • Преобразование изображения в 8-битное целое число без знака (чтобы OpenCV теперь мог работать с изображением)
  • Изменение порядка цветовых каналов с RGB на BGR

После вышеуказанных шагов постобработки выходное изображение записывается на диск.

Результаты атаки

Сможет ли созданное изображение обмануть модель ResNet? Следующий блок кода ответит на этот вопрос:

# выполнить вывод с этим состязательным примером, проанализировать результаты,
# и отобразить первый прогнозируемый результат
print("[INFO] running inference on the adversarial example...")
preprocessedImage = preprocess_input(baseImage + deltaUpdated)
predictions = model.predict(preprocessedImage)
predictions = decode_predictions(predictions, top=3)[0]
label = predictions[0][1]
confidence = predictions[0][2] * 100
print("[INFO] label: {} confidence: {:.2f}%".format(label,
	confidence))
# нарисуйте метку с наиболее вероятным прогнозом на атакующем изображении 
# вместе с показателем достоверности
text = "{}: {:.2f}%".format(label, confidence)
cv2.putText(adverImage, text, (3, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5,
	(0, 255, 0), 2)
# показать выходное изображение
cv2_imshow(adverImage)

В галерее выше, слева приведено исходное, а справа — атакующее изображение. Как видите, между изображениями нет заметной разницы —человеческие глаза не могут видеть разницу между этими двумя изображениями, но для ResNet они совершенно разные, и вместо кота нейронная сеть, как и было задумано, видит колибри.

Один комментарий к “Направленные состязательные (Targeted adversarial) атаки с использование Keras и TensorFlow

  1. Уведомление: Состязательные (Adversarial) атаки с помощью Keras и TensorFlow | Digiratory

Обсуждение закрыто.