Давайте познакомимся с одной из атак на нейросети, которая приводит к ошибкам классификации при минимальных внешних воздействиях. Представьте на минуту, что нейросеть это вы. И в данный момент, попивая чашечку ароматного кофе, вы классифицируете изображения котиков с точностью более 90 процентов даже не подозревая, что “атака одного пикселя” превратила всех ваших “котеек” в грузовики.
А теперь поставим на паузу, отодвинем кофе в сторону, импортируем все необходимые нам библиотеки и разберем как работают подобные атаки one pixel attack.
Цель данной атаки заставить алгоритм (нейросеть) выдать некорректный ответ. Ниже увидим это с несколькими различными моделями сверточных нейронных сетей. Используя один из методов многомерной математической оптимизации — дифференциальную эволюцию, найдем особенный пиксель, способный изменить изображение так, чтобы нейросеть стала неправильно классифицировать это изображение (несмотря на то, что ранее алгоритм “узнавал” это же изображение корректно и с высокой точностью).
Импортируем библиотеки:
# Python Libraries %matplotlib inline import pickle import numpy as np import pandas as pd import matplotlib from keras.datasets import cifar10 from keras import backend as K # Custom Networks from networks.lenet import LeNet from networks.pure_cnn import PureCnn from networks.network_in_network import NetworkInNetwork from networks.resnet import ResNet from networks.densenet import DenseNet from networks.wide_resnet import WideResNet from networks.capsnet import CapsNet # Helper functions from differential_evolution import differential_evolution import helper matplotlib.style.use('ggplot')
Для нашего эксперимента загрузим датасет CIFAR-10, содержащий изображения реального мира, разбитых на 10 классов.
(x_train, y_train), (x_test, y_test) = cifar10.load_data() class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
Посмотрим на любое изображение по его индексу. Например, вот на эту лошадь.
image_id = 99 # Image index in the test set helper.plot_image(x_test[image_id])

def perturb_image(xs, img): # If this function is passed just one perturbation vector, # pack it in a list to keep the computation the same if xs.ndim < 2: xs = np.array([xs]) # Copy the image n == len(xs) times so that we can # create n new perturbed images tile = [len(xs)] + [1]*(xs.ndim+1) imgs = np.tile(img, tile) # Make sure to floor the members of xs as int types xs = xs.astype(int) for x,img in zip(xs, imgs): # Split x into an array of 5-tuples (perturbation pixels) # i.e., [[x,y,r,g,b], ...] pixels = np.split(x, len(x) // 5) for pixel in pixels: # At each pixel's x,y position, assign its rgb value x_pos, y_pos, *rgb = pixel img[x_pos, y_pos] = rgb return imgs
Проверим?! Изменим один пиксель нашей лошади с координатами (16, 16) на желтый.
image_id = 99 # Image index in the test set pixel = np.array([16, 16, 255, 255, 0]) # pixel = x,y,r,g,b image_perturbed = perturb_image(pixel, x_test[image_id])[0] helper.plot_image(image_perturbed)

lenet = LeNet() resnet = ResNet() models = [lenet, resnet]
После загрузки моделей необходимо оценить тестовые изображения каждой модели, чтобы убедиться что мы атакуем только изображения, которые правильно классифицированы. Код ниже отображает точность и количество параметров каждой модели.
network_stats, correct_imgs = helper.evaluate_models(models, x_test, y_test) correct_imgs = pd.DataFrame(correct_imgs, columns=['name', 'img', 'label', 'confidence', 'pred']) network_stats = pd.DataFrame(network_stats, columns=['name', 'accuracy', 'param_count']) network_stats Evaluating lenet Evaluating resnet Out[11]: name accuracy param_count 0 lenet 0.748 62006 1 resnet 0.9231 470218
Все подобные атаки можно разделить на два класса: WhiteBox и BlackBox. Разница между ними в том, что в первом случае нам все достоверно известно об алгоритме, модели с которой имеем дело. В случае с BlackBox все что нам нужно это входные данные (изображение) и выходные данные (вероятности отнесения к одному из классов). Атака одного пикселя (one pixel attack) относится к BlackBox.
В этой статье рассмотрим два варианта атаки одного пикселя: untargeted и targeted. В первом случае нам будет абсолютно все равно к какому классу отнесет нейронная сеть нашего котика, главное, чтобы не к классу котиков. Targeted атака применима когда мы хотим, чтобы наш котик непременно стал грузовиком и только грузовиком.
Но как же найти те самые пиксели, изменение которых приведет к изменению класса изображения? Как найти пиксель, поменяв который one pixel attack станет возможна и успешна? Давайте попробуем сформулировать эту проблему как задачу оптимизации, но только очень простыми словами: при untargeted attack мы должны минимизировать доверие к нужному классу, а при targeted — максимизировать доверие к целевому классу.
При проведении подобного рода атак трудно оптимизировать функцию с помощью градиента. Необходимо использовать алгоритм оптимизации, который не полагается на гладкость функции.
Напомним, что для нашего эксперимента мы используем датасет CIFAR-10, содержащий изображения реального мира, размером 32 х 32 пикселя, разбитых на 10 классов. А это означает, что у нас есть целочисленные дискретные значения от 0 до 31 и интенсивности цвета от 0 до 255, и функция ожидается не гладкая, а скорее зазубренная, как показано ниже:

def predict_classes(xs, img, target_class, model, minimize=True): # Perturb the image with the given pixel(s) x and get the prediction of the model imgs_perturbed = perturb_image(xs, img) predictions = model.predict(imgs_perturbed)[:,target_class] # This function should always be minimized, so return its complement if needed return predictions if minimize else 1 - predictions image_id = 384 pixel = np.array([16, 13, 25, 48, 156]) model = resnet true_class = y_test[image_id, 0] prior_confidence = model.predict_one(x_test[image_id])[true_class] confidence = predict_classes(pixel, x_test[image_id], true_class, model)[0] print('Confidence in true class', class_names[true_class], 'is', confidence) print('Prior confidence was', prior_confidence) helper.plot_image(perturb_image(pixel, x_test[image_id])[0]) Confidence in true class bird is 0.00018887444 Prior confidence was 0.70661753

def attack_success(x, img, target_class, model, targeted_attack=False, verbose=False): # Perturb the image with the given pixel(s) and get the prediction of the model attack_image = perturb_image(x, img) confidence = model.predict(attack_image)[0] predicted_class = np.argmax(confidence) # If the prediction is what we want (misclassification or # targeted classification), return True if verbose: print('Confidence:', confidence[target_class]) if ((targeted_attack and predicted_class == target_class) or (not targeted_attack and predicted_class != target_class)): return True # NOTE: return None otherwise (not False), due to how Scipy handles its callback function
Посмотрим на работу функции критерия успеха. В целях демонстрации предполагаем нецелевую атаку.
image_id = 541 pixel = np.array([17, 18, 185, 36, 215]) model = resnet true_class = y_test[image_id, 0] prior_confidence = model.predict_one(x_test[image_id])[true_class] success = attack_success(pixel, x_test[image_id], true_class, model, verbose=True) print('Prior confidence', prior_confidence) print('Attack success:', success == True) helper.plot_image(perturb_image(pixel, x_test[image_id])[0]) Confidence: 0.07460087 Prior confidence 0.50054216 Attack success: True

def attack(img_id, model, target=None, pixel_count=1, maxiter=75, popsize=400, verbose=False): # Change the target class based on whether this is a targeted attack or not targeted_attack = target is not None target_class = target if targeted_attack else y_test[img_id, 0] # Define bounds for a flat vector of x,y,r,g,b values # For more pixels, repeat this layout bounds = [(0,32), (0,32), (0,256), (0,256), (0,256)] * pixel_count # Population multiplier, in terms of the size of the perturbation vector x popmul = max(1, popsize // len(bounds)) # Format the predict/callback functions for the differential evolution algorithm def predict_fn(xs): return predict_classes(xs, x_test[img_id], target_class, model, target is None) def callback_fn(x, convergence): return attack_success(x, x_test[img_id], target_class, model, targeted_attack, verbose) # Call Scipy's Implementation of Differential Evolution attack_result = differential_evolution( predict_fn, bounds, maxiter=maxiter, popsize=popmul, recombination=1, atol=-1, callback=callback_fn, polish=False) # Calculate some useful statistics to return from this function attack_image = perturb_image(attack_result.x, x_test[img_id])[0] prior_probs = model.predict_one(x_test[img_id]) predicted_probs = model.predict_one(attack_image) predicted_class = np.argmax(predicted_probs) actual_class = y_test[img_id, 0] success = predicted_class != actual_class cdiff = prior_probs[actual_class] - predicted_probs[actual_class] # Show the best attempt at a solution (successful or not) helper.plot_image(attack_image, actual_class, class_names, predicted_class) return [model.name, pixel_count, img_id, actual_class, predicted_class, success, cdiff, prior_probs, predicted_probs, attack_result.x]
Пришло время поделиться результатами исследования (проведенной атаки) и посмотреть как изменение лишь одного пикселя превратит лягушку в собаку, кота в лягушку, а автомобиль в самолет. А ведь чем больше точек изображения позволено изменять, тем выше вероятность успешной атаки на любое изображение.



image_id = 102 pixels = 1 # Number of pixels to attack model = resnet _ = attack(image_id, model, pixel_count=pixels, verbose=True) Confidence: 0.9938618 Confidence: 0.77454716 Confidence: 0.77454716 Confidence: 0.77454716 Confidence: 0.77454716 Confidence: 0.77454716 Confidence: 0.53226393 Confidence: 0.53226393 Confidence: 0.53226393 Confidence: 0.53226393 Confidence: 0.4211318



image_id = 108 target_class = 1 # Integer in range 0-9 pixels = 3 model = lenet print('Attacking with target', class_names[target_class]) _ = attack(image_id, model, target_class, pixel_count=pixels, verbose=True) Attacking with target automobile Confidence: 0.044409167 Confidence: 0.044409167 Confidence: 0.044409167 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.054611664 Confidence: 0.081972085 Confidence: 0.081972085 Confidence: 0.081972085 Confidence: 0.081972085 Confidence: 0.1537778 Confidence: 0.1537778 Confidence: 0.1537778 Confidence: 0.22246778 Confidence: 0.23916133 Confidence: 0.25238588 Confidence: 0.25238588 Confidence: 0.25238588 Confidence: 0.44560355 Confidence: 0.44560355 Confidence: 0.44560355 Confidence: 0.5711696

def attack_all(models, samples=500, pixels=(1,3,5), targeted=False, maxiter=75, popsize=400, verbose=False): results = [] for model in models: model_results = [] valid_imgs = correct_imgs[correct_imgs.name == model.name].img img_samples = np.random.choice(valid_imgs, samples, replace=False) for pixel_count in pixels: for i, img_id in enumerate(img_samples): print(' ', model.name, '- image', img_id, '-', i+1, '/', len(img_samples)) targets = [None] if not targeted else range(10) for target in targets: if targeted: print('Attacking with target', class_names[target]) if target == y_test[img, 0]: continue result = attack(img_id, model, target, pixel_count, maxiter=maxiter, popsize=popsize, verbose=verbose) model_results.append(result) results += model_results helper.checkpoint(results, targeted) return results untargeted = attack_all(models, samples=100, targeted=False) targeted = attack_all(models, samples=10, targeted=False)
Для проверки возможности дискредитации сети был разработан алгоритм и измерено его влияние на качество прогноза решения по распознаванию образов.
Посмотрим окончательные результаты.
untargeted, targeted = helper.load_results() columns = ['model', 'pixels', 'image', 'true', 'predicted', 'success', 'cdiff', 'prior_probs', 'predicted_probs', 'perturbation'] untargeted_results = pd.DataFrame(untargeted, columns=columns) targeted_results = pd.DataFrame(targeted, columns=columns)
В приведенной таблице видно, что используя нейронную сеть ResNet с точностью 0.9231, меняя несколько пикселей изображения, мы получили очень неплохой процент успешно атакованных изображений (attack_success_rate).
helper.attack_stats(targeted_results, models, network_stats) Out[26]: model accuracy pixels attack_success_rate 0 resnet 0.9231 1 0.144444 1 resnet 0.9231 3 0.211111 2 resnet 0.9231 5 0.222222 helper.attack_stats(untargeted_results, models, network_stats) Out[27]: model accuracy pixels attack_success_rate 0 resnet 0.9231 1 0.34 1 resnet 0.9231 3 0.79 2 resnet 0.9231 5 0.79
В своих экспериментах вы вольны использовать и другие архитектуры искусственных нейронных сетей, благо их в настоящее время великое множество.

Нейросети окутали современный мир незримыми нитями. Уже давно придуманы сервисы, где используя ИИ (искусственный интеллект), пользователи получают обработанные фото, стилистически похожие на работы кисти великих художников, а сегодня алгоритмы уже умеют сами рисовать картины, создавать музыкальные шедевры, писать книги и даже сценарии к фильмам. Такие сферы, как компьютерное зрение, распознавание лиц, беспилотные автомобили, диагностика заболеваний — принимают важные решения и не имеют права на ошибку, а вмешательство в работу алгоритмов приведет к катастрофическим последствиям. One pixel attack – один из способов спуфинг атак. Для проверки возможности дискредитации сети был разработан алгоритм и измерено его влияние на качество прогноза решения по распознаванию образов. Результат показал, что применяющиеся сверточные архитектуры нейросетей уязвимы перед специально обученным алгоритмом One pixel attack, который подменяет один пиксель, с целью дискредитации алгоритма распознавания.
Статью подготовили Александр Андроник и Адрей Черный-Ткач в рамках стажировки в компании Data4.