Un ataque de píxel. O cómo engañar a una red neuronal

Conozcamos uno de los ataques a la red neuronal, que conduce a errores de clasificación con mínimas influencias externas. Imagine por un momento que la red neuronal es usted. Y en este momento, mientras bebe una taza de café aromático, clasifica las imágenes de los gatos con una precisión de más del 90 por ciento sin siquiera sospechar que el "ataque de un píxel" convirtió a todos sus "gatos" en camiones.

Y ahora haremos una pausa, apartaremos el café, importaremos todas las bibliotecas que necesitamos y analizaremos cómo funcionan esos ataques de píxeles.

El propósito de este ataque es hacer que el algoritmo (red neuronal) dé una respuesta incorrecta. A continuación veremos esto con varios modelos diferentes de redes neuronales convolucionales. Usando uno de los métodos de optimización matemática multidimensional: evolución diferencial, encontramos un píxel especial que puede cambiar la imagen para que la red neuronal comience a clasificar incorrectamente esta imagen (a pesar de que anteriormente el algoritmo "reconoció" la misma imagen correctamente y con alta precisión).

Importar las bibliotecas:

# 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')

Para nuestro experimento, cargaremos el conjunto de datos CIFAR-10 que contiene imágenes del mundo real divididas en 10 clases.

(x_train, y_train), (x_test, y_test) = cifar10.load_data()

class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

Veamos cualquier imagen por su índice. Por ejemplo, aquí en este caballo.

image_id = 99 # Image index in the test set
helper.plot_image(x_test[image_id])



Tendremos que buscar el píxel muy poderoso que puede cambiar la respuesta de la red neuronal, lo que significa que es hora de escribir una función para cambiar uno o más píxeles de la imagen.

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

Echale un vistazo ?! Cambia un píxel de nuestro caballo con coordenadas (16, 16) a amarillo.

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)



Para demostrar el ataque, debe descargar modelos pre entrenados de redes neuronales en nuestro conjunto de datos CIFAR-10. Usaremos dos modelos lenet y resnet, pero puede usar otros para sus experimentos al descomentar las líneas de código correspondientes.

lenet = LeNet()
resnet = ResNet()

models = [lenet, resnet]

Después de cargar los modelos, es necesario evaluar las imágenes de prueba de cada modelo para asegurarse de que atacamos solo las imágenes que están clasificadas correctamente. El siguiente código muestra la precisión y el número de parámetros para cada modelo.

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


Todos estos ataques se pueden dividir en dos clases: WhiteBox y BlackBox. La diferencia entre ellos es que, en el primer caso, todos conocemos de manera confiable el algoritmo, el modelo con el que estamos tratando. En el caso de BlackBox, todo lo que necesitamos es entrada (imagen) y salida (probabilidades de ser asignado a una de las clases). Un ataque de píxel se refiere al BlackBox.

En este artículo, consideramos dos opciones para atacar un solo píxel: sin objetivo y dirigido. En el primer caso, no importará en absoluto a qué clase pertenecerá la red neuronal de nuestro gato, lo más importante, no a la clase de gatos. El ataque dirigido es aplicable cuando queremos que nuestro gato se convierta en un camión y solo en un camión.

¿Pero cómo encontrar los mismos píxeles cuyo cambio conducirá a un cambio en la clase de la imagen? ¿Cómo encontrar un píxel cambiando cuál ataque de píxel se vuelve posible y exitoso? Intentemos formular este problema como un problema de optimización, pero solo en palabras muy simples: con un ataque no dirigido, debemos minimizar la confianza en la clase deseada y, con el objetivo, maximizar la confianza en la clase objetivo.

Al llevar a cabo tales ataques, es difícil optimizar la función utilizando un gradiente. Se debe utilizar un algoritmo de optimización que no dependa de la suavidad de la función.

Recuerde que para nuestro experimento usamos el conjunto de datos CIFAR-10, que contiene imágenes del mundo real, de 32 x 32 píxeles de tamaño, divididas en 10 clases. Y esto significa que tenemos valores discretos enteros de 0 a 31 e intensidades de color de 0 a 255, y no se espera que la función sea suave, sino irregular, como se muestra a continuación:



Es por eso que usamos el algoritmo de evolución diferencial.

Pero regrese al código y escriba una función que devuelva la probabilidad de la confiabilidad del modelo. Si la clase de destino es correcta, entonces queremos minimizar esta función para que el modelo esté seguro de otra clase (lo cual no es cierto).

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



Necesitaremos la siguiente función para confirmar el criterio para el éxito del ataque, devolverá True cuando el cambio fue suficiente para engañar al modelo.

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

Veamos el trabajo de la función de criterio de éxito. Para demostrar, asumimos un ataque no objetivo.

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



Es hora de reunir todos los rompecabezas en una sola imagen. Utilizaremos una pequeña modificación de la implementación de la evolución diferencial en Scipy.

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]

Es hora de compartir los resultados del estudio (el ataque) y ver cómo cambiar solo un píxel convertirá una rana en un perro, un gato en una rana y un automóvil en un avión. Pero mientras más puntos de imagen se puedan cambiar, mayor será la probabilidad de un ataque exitoso sobre cualquier imagen.



Demuestre un ataque exitoso a una imagen de rana usando el modelo de rediseño. Deberíamos ver confianza en la verdadera disminución de la clase después de varias iteraciones.

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



Estos fueron ejemplos de un ataque no dirigido, y ahora realizaremos un ataque dirigido y elegiremos a qué clase nos gustaría que el modelo clasifique la imagen. La tarea es mucho más complicada que la anterior, porque haremos que la red neuronal clasifique la imagen de un barco como un automóvil y un caballo como un gato.



A continuación trataremos de obtener Lenet para clasificar la imagen del barco como un automóvil.

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



Después de tratar casos únicos de ataques, recopilaremos estadísticas utilizando la arquitectura de redes neuronales convolucionales ResNet, revisando cada modelo, cambiando 1, 3 o 5 píxeles de cada imagen. En este artículo, mostramos las conclusiones finales sin molestar al lector a familiarizarse con cada iteración, ya que requiere mucho tiempo y recursos computacionales.

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('\n', 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)

Para probar la posibilidad de desacreditar la red, se desarrolló un algoritmo y se midió su efecto sobre la calidad del pronóstico de la solución de reconocimiento de patrones.

Veamos los resultados finales.

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)

La siguiente tabla muestra que al usar la red neuronal ResNet con una precisión de 0.9231, cambiando varios píxeles de la imagen, obtuvimos un muy buen porcentaje de imágenes atacadas con éxito (tasa de éxito de ataque).

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

En sus experimentos, es libre de usar otras arquitecturas de redes neuronales artificiales, ya que actualmente hay muchas de ellas.



Las redes neuronales han envuelto el mundo moderno con hilos invisibles. Los servicios se han inventado durante mucho tiempo donde, usando inteligencia artificial (IA), los usuarios obtienen fotos procesadas estilísticamente similares al trabajo de grandes artistas, y hoy los algoritmos ya pueden dibujar imágenes ellos mismos, crear obras maestras musicales, escribir libros e incluso guiones para películas.

Áreas como la visión por computadora, el reconocimiento facial, los vehículos no tripulados, el diagnóstico de enfermedades: toman decisiones importantes y no tienen derecho a cometer errores, y la interferencia con el funcionamiento de los algoritmos tendrá consecuencias desastrosas.

Un ataque de píxeles es una forma de falsificar ataques. Para probar la posibilidad de desacreditar la red, se desarrolló un algoritmo y se midió su efecto sobre la calidad del pronóstico de la solución de reconocimiento de patrones. El resultado mostró que las arquitecturas de redes neuronales convolucionales utilizadas son vulnerables al algoritmo de ataque de un píxel especialmente entrenado, que reemplaza a un píxel, para desacreditar el algoritmo de reconocimiento.

El artículo fue preparado por Alexander Andronic y Adrey Cherny-Tkach como parte de una pasantía en Data4 .

All Articles