Truque de rede neural para iniciantes

imagem


Como parte do concurso anual ZeroNights HackQuest 2018, os participantes foram convidados a experimentar várias tarefas e competições não triviais. Parte de um deles estava relacionada à geração de um exemplo contraditório para uma rede neural. Em nossos artigos, já prestamos atenção aos métodos de ataque e à defesa de algoritmos de aprendizado de máquina. Na estrutura desta publicação, analisaremos um exemplo de como foi possível resolver a tarefa com o ZeroNights Hackquest usando a biblioteca foolbox.


Nesta tarefa, o invasor deveria obter acesso ao servidor. Depois que ele conseguiu, ele viu a seguinte estrutura de arquivos em seu diretório pessoal:


| Home --| KerasModel.h5 --| Task.txt --| ZeroSource.bmp 

As seguintes informações estavam no arquivo Task.txt:


 Now it is time for a final boss! http://51.15.100.188:36491/predict You have a mode and an image. To get a ticket, you need to change an image so that it is identified as "1". curl -X POST -F image=@ZeroSource.bmp 'http://51.15.100.188:36491/predict'. (don't forget about normalization (/255) ^_^) 

Para obter o ticket desejado, o invasor foi solicitado a converter o ZeroSource.bmp:


imagem


para que, após o envio ao servidor, a rede neural interprete essa imagem como 1. Se você tentar enviar essa imagem sem processamento, o resultado da rede neural será 0.


E, é claro, a principal dica para essa tarefa é o arquivo de modelo KerasModel.h5 (esse arquivo ajuda o invasor a transferir o ataque para o plano WhiteBox, já que a rede neural e todos os dados associados a ele estão acessíveis a ele). O nome do arquivo contém imediatamente uma dica - o nome da estrutura na qual a rede neural é implementada.


Foi com estas notas introdutórias que o participante começou a resolver a tarefa:


  • Modelo de rede neural escrito em Keras.
  • A capacidade de enviar uma imagem para o servidor usando curl.
  • A imagem original que precisava ser alterada.

No lado do servidor, a verificação foi a mais simples possível:


  1. A imagem deve ter o tamanho certo - 28 x 28 pixels.
  2. Nesta imagem, o modelo deve retornar 1.
  3. A diferença entre a imagem inicial do ZeroSource.bmp e a enviada ao servidor deve ser menor que o limite k pela métrica MSE (erro padrão).

Então, vamos começar.


Primeiro, o participante precisava encontrar informações sobre como enganar a rede neural. Depois de um breve período no Google, ele recebeu as palavras-chave "Exemplo adversário" e "Ataque adversário". Em seguida, ele precisou procurar ferramentas para aplicar ataques adversários. Se você acessar o Google na consulta "Ataques adversos ao Keras Neural Net", o primeiro link será para o GitHub do projeto FoolBox - uma biblioteca python para gerar exemplos adversários. Obviamente, existem outras bibliotecas (falamos sobre algumas delas em artigos anteriores ). Além disso, os ataques podem ser escritos, como se costuma dizer, do zero. Mas ainda nos concentramos na biblioteca mais popular, que uma pessoa que ainda não encontrou o tópico de ataques adversários pode encontrar no primeiro link no Google.


Agora você precisa escrever um script Python que gere um exemplo contraditório.
Vamos começar, é claro, com importações.


 import keras import numpy as np from PIL import Image import foolbox 

O que vemos aqui?


  1. Keras é a estrutura sobre a qual a Rede Neural está escrita, que iremos enganar.
  2. O NumPy é uma biblioteca que nos permitirá trabalhar com vetores com eficiência.
  3. PIL é uma ferramenta para trabalhar com imagens.
  4. O FoolBox é uma biblioteca para gerar exemplos contraditórios.

A primeira coisa a fazer é, é claro, carregar o modelo de rede neural na memória do nosso programa e ver as informações do modelo.


 model = keras.models.load_model("KerasModel.h5") #   model.summary() #     model.input #    ,        

Na saída, obtemos o seguinte:


 Layer (type) Output Shape Param # ================================================================= conv2d_1 (Conv2D) (None, 26, 26, 32) 320 _________________________________________________________________ conv2d_2 (Conv2D) (None, 26, 26, 64) 18496 _________________________________________________________________ max_pooling2d_1 (MaxPooling2 (None, 13, 13, 64) 0 _________________________________________________________________ dropout_1 (Dropout) (None, 13, 13, 64) 0 _________________________________________________________________ conv2d_3 (Conv2D) (None, 13, 13, 64) 36928 _________________________________________________________________ conv2d_4 (Conv2D) (None, 13, 13, 128) 73856 _________________________________________________________________ max_pooling2d_2 (MaxPooling2 (None, 6, 6, 128) 0 _________________________________________________________________ flatten_1 (Flatten) (None, 4608) 0 _________________________________________________________________ dense_1 (Dense) (None, 256) 1179904 _________________________________________________________________ dense_2 (Dense) (None, 10) 2570 ================================================================= Total params: 1,312,074 Trainable params: 1,312,074 Non-trainable params: 0 _________________________________________________________________ <tf.Tensor 'conv2d_1_input_1:0' shape=(?, 28, 28, 1) dtype=float32> 

Que informações posso obter daqui?


  1. O modelo de entrada (camada conv2d_1) aceita um objeto de dimensão? X28x28x1, onde "?" - número de objetos; se a imagem for uma, a dimensão será 1x28x28x1. E a imagem é uma matriz tridimensional, onde uma dimensão é 1. Ou seja, a imagem é servida como uma tabela de valores de 0 a 255.
  2. Na saída do modelo (camada densa_2), um vetor da dimensão 10 é obtido.

Carregamos a imagem e não esquecemos de convertê-la para o tipo float (além disso, a rede neural funcionará com números reais) e normalizá-la (divida todos os valores por 255). Aqui, vale esclarecer que a normalização é um dos truques "obrigatórios" ao trabalhar com redes neurais, mas o invasor pode não saber disso; portanto, adicionamos especialmente uma pequena dica na descrição da tarefa):


 img = Image.open("ZeroSource.bmp") #   img = np.array(img) #     numpy.array img = img.astype('float32') #      float img /= 255 #  

Agora podemos enviar a imagem para o modelo carregado e ver qual resultado ela produz:


 model.predict(img.reshape(1,28,28,1)) #   predict            

Na saída, obtemos as seguintes informações


 array([[1.0000000e+00, 4.2309660e-19, 3.1170484e-15, 6.2545637e-18, 1.4199094e-16, 6.3990816e-13, 6.9493417e-10, 2.8936278e-12, 8.9440377e-14, 1.6340098e-12]], dtype=float32) 

Vale a pena explicar o que é esse vetor: de fato, é uma distribuição de probabilidade, ou seja, cada número representa uma probabilidade da classe 0,1,2 ..., 9. A soma de todos os números no vetor é 1. Nesse caso, pode-se ver que o modelo está confiante de que a imagem de entrada representa a classe 0 com uma probabilidade de 100%.


Se representamos isso em um histograma, obtemos o seguinte:


imagem


Confiança absoluta.


Se o modelo não pudesse determinar a classe, o vetor de probabilidade tenderia a uma distribuição uniforme, o que, por sua vez, significaria que o modelo atribui o objeto a todas as classes simultaneamente com a mesma probabilidade. E o histograma ficaria assim:


imagem


É geralmente aceito que a classe de um modelo é determinada pelo índice do número máximo nesse vetor. Ou seja, o modelo poderia, teoricamente, escolher uma classe com uma probabilidade superior a 10%. Mas essa lógica pode variar dependendo da lógica de decisão descrita pelos desenvolvedores.


Agora vamos para a parte mais interessante - ataques adversários.


Primeiro, para trabalhar com um modelo na biblioteca do FoolBox, você deve converter o modelo na notação do Foolbox. Você pode fazer assim:


 fmodel = foolbox.models.KerasModel(model,bounds=(0,1)) #  bounds ,       ,        255,        0-1. 

Depois disso, você pode testar diferentes ataques. Vamos começar com o mais popular - FGSM:


Fgsm

 attack = foolbox.attacks.FGSM(fmodel) #    FGSM     adversarial = attack(img.reshape(28,28,1),0) #  ,  adversarial  probs = model.predict(adversarial.reshape(1,28,28,1)) #     print(probs) #    print(np.argmax(probs)) #      

A saída da rede neural será a seguinte


 [4.8592144e-01 2.5432981e-14 5.7048566e-13 1.6787202e-14 1.6875961e-11 1.2974949e-07 5.1407838e-01 3.9819957e-12 1.9827724e-09 5.7383300e-12] 6 

E a imagem resultante:


imagem


Portanto, agora com uma probabilidade de mais de 50%, 0 foi reconhecido como 6. Já é bom. No entanto, ainda queremos obter 1, e o nível de ruído não é muito impressionante. A imagem realmente parece implausível. Mais sobre isso mais tarde. Enquanto isso, vamos tentar zombar dos ataques. De repente, ainda temos 1.


Ataque L-BFGS

 attack = foolbox.attacks.LBFGSAttack(fmodel) adversarial = attack(img.reshape(28,28,1),0) probs = model.predict(adversarial.reshape(1,28,28,1)) print(probs) print(np.argmax(probs)) 

Conclusão:


 [4.7782943e-01, 1.9682934e-10, 1.0285517e-06, 3.2558936e-10, 6.5797998e-05, 4.0495447e-06, 2.5545436e-04, 3.4730587e-02, 5.5223148e-07, 4.8711312e-01] 9 

Imagem:


imagem


Mais uma vez por. Agora, temos 0 reconhecido como 9 com uma probabilidade de ~ 49%. No entanto, o barulho é muito menor.


Vamos terminar com uma batida aleatória. Um exemplo foi escolhido de tal maneira que seria muito difícil obter o resultado aleatoriamente. Agora, não indicamos em nenhum lugar que queremos obter 1. Consequentemente, realizamos um ataque não direcionado e acreditamos que ainda receberíamos a classe 1, mas isso não aconteceu. Portanto, vale a pena seguir para ataques direcionados. Vamos usar a documentação do foolbox e encontrar o módulo de critérios


Neste módulo, você pode selecionar um critério para um ataque, se ele os suportar. Especificamente, estamos interessados ​​em dois critérios:


  1. TargetClass - faz com que, no vetor de distribuições de probabilidade, o elemento com o número k tenha a probabilidade máxima.
  2. TargetClassProbability - faz com que, no vetor de distribuições de probabilidade, um elemento com o número k tenha uma probabilidade de pelo menos p.

Vamos tentar os dois:


L-BFGS + TargetClass

O principal nos critérios do TargetClass é obter a probabilidade da classe k, maior que a probabilidade de qualquer outra classe. Então, a rede que toma a decisão simplesmente olhando para a probabilidade máxima estará enganada.


 attack = foolbox.attacks.LBFGSAttack(fmodel,foolbox.criteria.TargetClass(1))#    ,     ,    TargetClass,  ,      ,      adversarial = attack(img.reshape(28,28,1),0) probs = model.predict(adversarial.reshape(1,28,28,1)) print(probs) print(np.argmax(probs)) 

Conclusão:


 [3.2620126e-01 3.2813528e-01 8.5446298e-02 8.1292394e-04 1.1273423e-03 2.4886258e-02 3.3904776e-02 1.9947644e-01 8.2347924e-07 8.5878673e-06] 1 

Imagem:


imagem


Como pode ser visto na conclusão, agora nossa rede neural afirma que é 1 com uma probabilidade de 32,8%, enquanto a probabilidade de 0 é o mais próximo possível desse valor e é de 32,6%. Nós conseguimos! Em princípio, isso já é suficiente para concluir a tarefa. Mas iremos além e tentaremos obter uma probabilidade de 1 acima de 0,5.


L-BFGS + TargetClassProbability

Agora usamos o critério TargetClassProbability, que permite obter a probabilidade de uma classe em um objeto não inferior a p. Possui apenas dois parâmetros:
1) O número da classe do objeto.
2) A probabilidade desta classe no exemplo adversário.
Ao mesmo tempo, se for impossível atingir essa probabilidade ou se o tempo para encontrar esse objeto levar muito tempo, o objeto adversário será igual a nenhum. Você pode verificar isso sozinho, tentando fazer a probabilidade de, por exemplo, 0,99. Então o método pode não convergir.


 attack = foolbox.attacks.LBFGSAttack(fmodel,foolbox.criteria.TargetClassProbability(1,0.5)) adversarial = attack(img.reshape(28,28,1),0) probs = model.predict(adversarial.reshape(1,28,28,1)) print(probs) print(np.argmax(probs)) 

Conclusão:


 [4.2620126e-01 5.0013528e-01 9.5413298e-02 8.1292394e-04 1.1273423e-03 2.4886258e-02 3.3904776e-02 1.9947644e-01 8.2347924e-07 8.5878673e-06] 

Viva! Conseguimos obter um exemplo contraditório, no qual a probabilidade 1 para nossa rede neural está acima de 50%! Ótimo! Agora vamos fazer a desnormalização (retornar a imagem para o formato 0-255) e salvá-la.


O script final é o seguinte:


 import keras from PIL import Image import numpy as np import foolbox from foolbox.criteria import TargetClassProbability import scipy.misc model = keras.models.load_model("KerasModel.h5") img = Image.open("ZeroSource.bmp") img = np.array(img.getdata()) img = img.astype('float32') img = img /255. img = img.reshape(28,28,1) fmodel = foolbox.models.KerasModel(model,bounds=(0,1)) attack = foolbox.attacks.LBFGSAttack(fmodel,criterion=TargetClassProbability(1 ,p=.5)) adversarial = attack(img[:,:,::-1], 0) adversarial = adversarial * 255 adversarial = adversarial.astype('int') scipy.misc.toimage(adversarial.reshape(28,28)).save('AdversarialExampleZero.bmp') 

E a imagem final é a seguinte:


imagem .


Conclusões

Então, como vimos nos exemplos acima, enganar uma rede neural era bastante simples. Há também um grande número de métodos capazes de fazer isso. Simplesmente abra a lista de ataques disponíveis no foolbox e tente aplicá-los. Sugerimos que você tente fazer o mesmo, baseando-se na mesma rede neural e na mesma imagem, disponível por referência . Você pode deixar suas perguntas nos comentários. Nós responderemos a eles!


Lembre-se sempre de que, por mais úteis que sejam os algoritmos e os modelos, eles podem ser extremamente instáveis ​​para pequenas mudanças que podem levar a erros graves. Portanto, recomendamos que você teste seus modelos, nos quais python e ferramentas como o foolbox podem ajudar.


Obrigado pela atenção!

Source: https://habr.com/ru/post/pt443164/


All Articles