Attaque d'un pixel. Ou comment tromper un réseau de neurones

Familiarisons-nous avec l'une des attaques contre le réseau neuronal, qui conduit à des erreurs de classification avec un minimum d'influences externes. Imaginez un instant que le réseau neuronal c'est vous. Et pour le moment, en buvant une tasse de café aromatique, vous classez les images de chats avec une précision de plus de 90% sans même soupçonner que «l'attaque d'un pixel» a transformé tous vos «chats» en camions.

Et maintenant, nous allons faire une pause, déplacer le café de côté, importer toutes les bibliothèques dont nous avons besoin et analyser le fonctionnement de ces attaques à un pixel.

Le but de cette attaque est de faire que l'algorithme (réseau de neurones) donne une réponse incorrecte. Ci-dessous, nous le verrons avec plusieurs modèles différents de réseaux de neurones convolutionnels. En utilisant l'une des méthodes d'optimisation mathématique multidimensionnelle - évolution différentielle, nous trouvons un pixel spécial qui peut changer l'image de sorte que le réseau de neurones commence à classer incorrectement cette image (malgré le fait qu'auparavant l'algorithme «reconnaissait» la même image correctement et avec une grande précision).

Importez les bibliothèques:

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

Pour notre expérience, nous chargerons l'ensemble de données CIFAR-10 contenant des images du monde réel divisées en 10 classes.

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

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

Regardons n'importe quelle image par son index. Par exemple, ici sur ce cheval.

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



Nous devrons rechercher le pixel très puissant qui peut changer la réponse du réseau neuronal, ce qui signifie qu'il est temps d'écrire une fonction pour changer un ou plusieurs pixels de l'image.

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

Vérifiez-le ?! Changez un pixel de notre cheval avec les coordonnées (16, 16) en jaune.

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)



Pour démontrer l'attaque, vous devez télécharger des modèles pré-formés de réseaux de neurones sur notre jeu de données CIFAR-10. Nous utiliserons deux modèles lenet et resnet, mais vous pouvez en utiliser d'autres pour vos expériences en décommentant les lignes de code correspondantes.

lenet = LeNet()
resnet = ResNet()

models = [lenet, resnet]

Après avoir chargé les modèles, il est nécessaire d'évaluer les images de test de chaque modèle pour s'assurer que nous n'attaquons que les images correctement classées. Le code ci-dessous affiche la précision et le nombre de paramètres pour chaque modèle.

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


Toutes ces attaques peuvent être divisées en deux classes: WhiteBox et BlackBox. La différence entre eux est que dans le premier cas, nous connaissons tous de manière fiable l'algorithme, le modèle avec lequel nous avons affaire. Dans le cas de la BlackBox, tout ce dont nous avons besoin est une entrée (image) et une sortie (probabilités d'être affectées à l'une des classes). Une attaque pixel fait référence à la BlackBox.

Dans cet article, nous considérons deux options pour attaquer un seul pixel: non ciblé et ciblé. Dans le premier cas, peu importe à quelle classe le réseau neuronal de notre chat appartiendra, mais surtout pas à la classe des chats. L'attaque ciblée est applicable lorsque nous voulons que notre chat devienne un camion et seulement un camion.

Mais comment trouver les pixels mêmes dont le changement entraînera un changement de classe de l'image? Comment trouver un pixel en changeant laquelle une attaque de pixel devient possible et réussie? Essayons de formuler ce problème comme un problème d'optimisation, mais seulement en termes très simples: avec une attaque non ciblée, nous devons minimiser la confiance dans la classe souhaitée, et avec ciblé, maximiser la confiance dans la classe cible.

Lors de l'exécution de telles attaques, il est difficile d'optimiser la fonction à l'aide d'un gradient. Un algorithme d'optimisation doit être utilisé qui ne repose pas sur la fluidité de la fonction.

Rappelons que pour notre expérience, nous utilisons l'ensemble de données CIFAR-10, qui contient des images du monde réel, de 32 x 32 pixels, divisées en 10 classes. Cela signifie que nous avons des valeurs discrètes entières de 0 à 31 et des intensités de couleur de 0 à 255, et la fonction ne devrait pas être lisse, mais plutôt irrégulière, comme indiqué ci-dessous:



C'est pourquoi nous utilisons l'algorithme d'évolution différentielle.

Mais revenons au code et écrivez une fonction qui renvoie la probabilité de fiabilité du modèle. Si la classe cible est correcte, nous voulons minimiser cette fonction afin que le modèle soit sûr d'une autre classe (ce qui n'est pas vrai).

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



Nous aurons besoin de la fonction suivante pour confirmer le critère de réussite de l'attaque, elle retournera True lorsque le changement aura suffi à tromper le modèle.

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

Regardons le travail de la fonction critère de succès. Afin de démontrer, nous supposons une attaque non ciblée.

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



Il est temps de rassembler tous les puzzles en une seule image. Nous utiliserons une petite modification de l'implémentation de l'évolution différentielle dans 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]

Il est temps de partager les résultats de l'étude (l'attaque) et de voir comment le changement d'un seul pixel transformera une grenouille en chien, un chat en grenouille et une voiture en avion. Mais plus les points d'image sont autorisés à changer, plus la probabilité d'une attaque réussie sur une image est élevée.



Démontrez une attaque réussie sur une image de grenouille en utilisant le modèle Resnet. Nous devrions voir la confiance dans le vrai déclin de la classe après plusieurs itérations.

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



Ce sont des exemples d'une attaque non ciblée, et maintenant nous allons mener une attaque ciblée et choisir dans quelle classe nous aimerions que le modèle classe l'image. La tâche est beaucoup plus compliquée que la précédente, car nous allons faire classer le réseau de neurones l'image d'un navire comme voiture et d'un cheval comme chat.



Ci-dessous, nous essaierons d'obtenir lenet pour classer l'image du navire en tant que voiture.

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



Après avoir traité des cas uniques d'attaques, nous collecterons des statistiques en utilisant l'architecture des réseaux de neurones convolutionnels ResNet, en passant par chaque modèle, en changeant 1, 3 ou 5 pixels de chaque image. Dans cet article, nous montrons les conclusions finales sans déranger le lecteur à se familiariser avec chaque itération, car cela prend beaucoup de temps et de ressources de calcul.

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)

Pour tester la possibilité de discrédit du réseau, un algorithme a été développé et son effet sur la qualité des prévisions de la solution de reconnaissance de formes a été mesuré.

Voyons les résultats finaux.

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)

Le tableau ci-dessous montre qu'en utilisant le réseau neuronal ResNet avec une précision de 0,9231, en changeant plusieurs pixels de l'image, nous avons obtenu un très bon pourcentage d'images attaquées avec succès (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

Dans vos expériences, vous êtes libre d'utiliser d'autres architectures de réseaux de neurones artificiels, car il y en a actuellement un grand nombre.



Les réseaux de neurones ont enveloppé le monde moderne de fils invisibles. Depuis longtemps, des services ont été inventés où, grâce à l'IA (intelligence artificielle), les utilisateurs reçoivent des photos traitées stylistiquement similaires au travail de grands artistes, et aujourd'hui les algorithmes eux-mêmes peuvent dessiner des images, créer des chefs-d'œuvre musicaux, écrire des livres et même des scripts pour des films.

Des domaines tels que la vision par ordinateur, la reconnaissance faciale, les véhicules sans pilote, le diagnostic des maladies - prennent des décisions importantes et n'ont pas le droit de faire des erreurs, et l'interférence avec le fonctionnement des algorithmes entraînera des conséquences désastreuses.

Une attaque à un pixel est un moyen d'usurper les attaques. Pour tester la possibilité de discrédit du réseau, un algorithme a été développé et son effet sur la qualité des prévisions de la solution de reconnaissance de formes a été mesuré. Le résultat a montré que les architectures de réseaux de neurones convolutifs utilisées sont vulnérables à l'algorithme d'attaque à un pixel spécialement formé, qui remplace un pixel, afin de discréditer l'algorithme de reconnaissance.

L'article a été préparé par Alexander Andronic et Adrey Cherny-Tkach dans le cadre d'un stage chez Data4 .

All Articles