Ataque de um pixel. Ou como enganar uma rede neural

Vamos nos familiarizar com um dos ataques à rede neural, o que leva a erros de classificação com o mínimo de influências externas. Imagine por um momento que a rede neural é você. E, no momento, enquanto bebe uma xícara de café aromático, você classifica as imagens de gatos com uma precisão de mais de 90%, sem sequer suspeitar que o "ataque de um pixel" transformou todos os seus "gatos" em caminhões.

E agora vamos fazer uma pausa, afastar o café, importar todas as bibliotecas necessárias e analisar como esses ataques de um pixel funcionam.

O objetivo deste ataque é fazer com que o algoritmo (rede neural) dê uma resposta incorreta. Abaixo, veremos isso com vários modelos diferentes de redes neurais convolucionais. Usando um dos métodos de otimização matemática multidimensional - evolução diferencial, encontramos um pixel especial que pode alterar a imagem para que a rede neural comece a classificá-la incorretamente (apesar do fato de que anteriormente o algoritmo "reconheceu" a mesma imagem corretamente e com alta precisão).

Importe as 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 nosso experimento, carregaremos o conjunto de dados CIFAR-10 contendo imagens do mundo real divididas em 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']

Vamos olhar para qualquer imagem pelo seu índice. Por exemplo, aqui neste cavalo.

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



Teremos que procurar o pixel muito poderoso que pode alterar a resposta da rede neural, o que significa que é hora de escrever uma função para alterar um ou mais pixels da imagem.

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

Confira ?! Mude um pixel do nosso cavalo com coordenadas (16, 16) para amarelo.

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 demonstrar o ataque, você precisa fazer o download de modelos pré-treinados de redes neurais em nosso conjunto de dados CIFAR-10. Usaremos dois modelos lenet e resnet, mas você pode usar outros para seus experimentos descomentando as linhas de código correspondentes.

lenet = LeNet()
resnet = ResNet()

models = [lenet, resnet]

Depois de carregar os modelos, é necessário avaliar as imagens de teste de cada modelo para garantir que atacemos apenas imagens classificadas corretamente. O código abaixo exibe a precisão e o 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 esses ataques podem ser divididos em duas classes: WhiteBox e BlackBox. A diferença entre eles é que, no primeiro caso, todos sabemos com segurança sobre o algoritmo, o modelo com o qual estamos lidando. No caso do BlackBox, tudo o que precisamos é de entrada (imagem) e saída (probabilidade de ser atribuída a uma das classes). Um ataque de pixel refere-se ao BlackBox.

Neste artigo, consideramos duas opções para atacar um único pixel: não segmentado e direcionado. No primeiro caso, não importará absolutamente a que classe a rede neural de nosso gato pertencerá, mais importante, não à classe de gatos. O ataque direcionado é aplicável quando queremos que nosso gato se torne um caminhão e apenas um caminhão.

Mas como encontrar os próprios pixels cuja mudança levará a uma mudança na classe da imagem? Como encontrar um pixel alterando qual ataque de um pixel se torna possível e bem-sucedido? Vamos tentar formular esse problema como um problema de otimização, mas apenas com palavras muito simples: com um ataque não direcionado, devemos minimizar a confiança na classe desejada e, com o objetivo, maximizar a confiança na classe de destino.

Ao realizar esses ataques, é difícil otimizar a função usando um gradiente. Deve-se usar um algoritmo de otimização que não dependa da suavidade da função.

Lembre-se de que, para nosso experimento, usamos o conjunto de dados CIFAR-10, que contém imagens do mundo real, tamanho 32 x 32 pixels, dividido em 10 classes. E isso significa que temos valores inteiros discretos de 0 a 31 e intensidades de cores de 0 a 255, e não se espera que a função seja suave, mas sim irregular, como mostrado abaixo:



É por isso que usamos o algoritmo de evolução diferencial.

Mas voltando ao código e escreva uma função que retorne a probabilidade da confiabilidade do modelo. Se a classe de destino estiver correta, queremos minimizar essa função para que o modelo tenha certeza de outra classe (o que não é verdade).

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



Nós precisaremos da próxima função para confirmar o critério para o sucesso do ataque, ele retornará True quando a alteração foi suficiente para enganar o 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

Vejamos o trabalho da função de critério de sucesso. Para demonstrar, assumimos um ataque não-alvo.

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



É hora de coletar todos os quebra-cabeças em uma imagem. Usaremos uma pequena modificação na implementação da evolução diferencial no 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]

É hora de compartilhar os resultados do estudo (o ataque) e ver como mudar apenas um pixel transformará um sapo em cachorro, um gato em sapo e um carro em avião. Porém, quanto mais pontos da imagem puderem mudar, maior a probabilidade de um ataque bem-sucedido a qualquer imagem.



Demonstre um ataque bem-sucedido a uma imagem de sapo usando o modelo de re-rede. Deveríamos ver confiança no verdadeiro declínio da classe após várias iterações.

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



Estes foram exemplos de um ataque não direcionado e agora vamos realizar um ataque direcionado e escolher para qual classe gostaríamos que o modelo classificasse a imagem. A tarefa é muito mais complicada do que a anterior, porque faremos com que a rede neural classifique a imagem de um navio como um carro e um cavalo como um gato.



Abaixo tentaremos obter lenet para classificar a imagem do navio como um carro.

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



Tendo lidado com casos únicos de ataques, coletaremos estatísticas usando a arquitetura das redes neurais convolucionais ResNet, passando por cada modelo, alterando 1, 3 ou 5 pixels de cada imagem. Neste artigo, mostramos as conclusões finais sem incomodar o leitor a se familiarizar com cada iteração, uma vez que leva muito tempo e recursos computacionais.

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 testar a possibilidade de desacreditar a rede, um algoritmo foi desenvolvido e seu efeito na qualidade prevista da solução de reconhecimento de padrões foi medido.

Vamos ver os resultados finais.

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)

A tabela abaixo mostra que, usando a rede neural ResNet com precisão de 0,9231, alterando vários pixels da imagem, obtivemos uma porcentagem muito boa de imagens atacadas com sucesso (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

Em seus experimentos, você é livre para usar outras arquiteturas de redes neurais artificiais, pois atualmente existem muitas delas.



As redes neurais envolveram o mundo moderno com fios invisíveis. Por um longo tempo, serviços foram inventados onde, usando IA (inteligência artificial), os usuários recebem fotos processadas estilisticamente semelhantes às de grandes artistas, e hoje os algoritmos podem desenhar, criar obras de arte musicais, escrever livros e até roteiros para filmes.

Áreas como visão computacional, reconhecimento facial, veículos não tripulados, diagnóstico de doenças - tomam decisões importantes e não têm o direito de cometer erros, e a interferência na operação dos algoritmos levará a conseqüências desastrosas.

Um ataque de pixel é uma maneira de falsificar ataques. Para testar a possibilidade de desacreditar a rede, um algoritmo foi desenvolvido e seu efeito na qualidade prevista da solução de reconhecimento de padrões foi medido. O resultado mostrou que as arquiteturas de redes neurais convolucionais usadas são vulneráveis ​​ao algoritmo de ataque de Um pixel especialmente treinado, que substitui um pixel, para desacreditar o algoritmo de reconhecimento.

O artigo foi preparado por Alexander Andronic e Adrey Cherny-Tkach como parte de um estágio no Data4 .

All Articles