
Como parte del concurso anual ZeroNights HackQuest 2018, los participantes fueron invitados a probar suerte en una serie de tareas y competencias no triviales. Parte de uno de ellos estaba relacionado con la generación de un ejemplo de confrontación para una red neuronal. En nuestros artículos, ya hemos prestado atención a los métodos de ataque y la defensa de los algoritmos de aprendizaje automático. En el marco de esta publicación, analizaremos un ejemplo de cómo fue posible resolver la tarea con ZeroNights Hackquest utilizando la biblioteca foolbox.
En esta tarea, se suponía que el atacante debía obtener acceso al servidor. Después de tener éxito, vio la siguiente estructura de archivos en su directorio de inicio:
| Home --| KerasModel.h5 --| Task.txt --| ZeroSource.bmp
La siguiente información estaba en el archivo 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 obtener el codiciado boleto, se le pidió al atacante que convirtiera ZeroSource.bmp:

para que después de enviar al servidor la red neuronal interprete esta imagen como 1. Si intenta enviar esta imagen sin procesar, el resultado de la red neuronal será 0.
Y, por supuesto, la pista principal para esta tarea es el archivo modelo KerasModel.h5 (este archivo ayuda al atacante a transferir el ataque al plano WhiteBox, ya que la red neuronal y todos los datos asociados con él son accesibles para él). El nombre del archivo contiene inmediatamente una pista: el nombre del marco en el que se implementa la red neuronal.
Fue con estas notas introductorias que el participante se propuso resolver la tarea:
- Modelo de red neuronal escrito en Keras.
- La capacidad de enviar una imagen al servidor usando curl.
- La imagen original que necesitaba ser cambiada.
En el lado del servidor, la verificación fue lo más simple posible:
- La imagen debe tener el tamaño correcto: 28x28 píxeles.
- En esta imagen, el modelo debería devolver 1.
- La diferencia entre la imagen inicial de ZeroSource.bmp y la enviada al servidor debe ser inferior al umbral k por la métrica MSE (error estándar).
Entonces comencemos.
Primero, el participante necesitaba encontrar información sobre cómo engañar a la red neuronal. Después de un corto tiempo en Google, obtuvo las palabras clave "Ejemplo adversario" y "Ataque adversario". Luego, necesitaba buscar herramientas para aplicar ataques adversos. Si conduce a Google la consulta "Ataques adversarios en la red neuronal de Keras", el primer enlace será al proyecto GitHub del FoolBox, una biblioteca de Python para generar ejemplos adversos. Por supuesto, hay otras bibliotecas (hablamos de algunas de ellas en artículos anteriores ). Además, los ataques podrían escribirse, como dicen, desde cero. Pero aún nos enfocamos en la biblioteca más popular, que una persona que no ha encontrado previamente el tema de los ataques adversos puede encontrar en el primer enlace en Google.
Ahora necesita escribir un script de Python que generará un ejemplo de confrontación.
Comenzaremos, por supuesto, con las importaciones.
import keras import numpy as np from PIL import Image import foolbox
¿Qué vemos aquí?
- Keras es el marco en el que está escrita la Red Neural, que engañaremos.
- NumPy es una biblioteca que nos permitirá trabajar con vectores de manera eficiente.
- PIL es una herramienta para trabajar con imágenes.
- FoolBox es una biblioteca para generar ejemplos adversos.
Lo primero que debe hacer es, por supuesto, cargar el modelo de red neuronal en la memoria de nuestro programa y ver la información del modelo.
model = keras.models.load_model("KerasModel.h5")
En la salida, obtenemos lo siguiente:
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>
¿Qué información puedo obtener de aquí?
- El modelo de entrada (capa conv2d_1) acepta un objeto de dimensión? X28x28x1, donde "?" - número de objetos; si la imagen es una, entonces la dimensión será 1x28x28x1. Y la imagen es una matriz tridimensional, donde una dimensión es 1. Es decir, la imagen se sirve como una tabla de valores de 0 a 255.
- En la salida del modelo (capa densa_2), se obtiene un vector de dimensión 10.
Cargamos la imagen y no nos olvidamos de convertirla a tipo flotante (además, la red neuronal funcionará con números reales) y normalizarla (dividir todos los valores por 255). Aquí vale la pena aclarar que la normalización es uno de los trucos "obligatorios" cuando se trabaja con redes neuronales, pero el atacante podría no haberlo sabido, por lo tanto, agregamos especialmente una pequeña pista en la descripción de la tarea):
img = Image.open("ZeroSource.bmp")
Ahora podemos enviar la imagen al modelo cargado y ver qué resultado produce:
model.predict(img.reshape(1,28,28,1))
La salida es la siguiente información
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 la pena explicar qué es este vector: de hecho, esta es una distribución de probabilidad, es decir, cada número representa una probabilidad de la clase 0,1,2 ..., 9. La suma de todos los números en el vector es 1. En este caso, se puede ver que el modelo confía en que la imagen de entrada representa la clase 0 con una probabilidad del 100%.
Si representamos esto en un histograma, obtenemos lo siguiente:

Absoluta confianza.
Si el modelo no pudiera determinar la clase, el vector de probabilidad tendería a una distribución uniforme, lo que, a su vez, significaría que el modelo asigna el objeto a todas las clases simultáneamente con la misma probabilidad. Y el histograma se vería así:

En general, se acepta que la clase de un modelo está determinada por el índice del número máximo en un vector dado. Es decir, el modelo teóricamente podría elegir una clase con una probabilidad de más del 10%. Pero esta lógica puede variar dependiendo de la lógica de decisión descrita por los desarrolladores.
Ahora pasemos a la parte más interesante: los ataques adversos.
Primero, para trabajar con un modelo en la biblioteca de FoolBox, debe traducir el modelo a la notación de Foolbox. Puedes hacerlo de esta manera:
fmodel = foolbox.models.KerasModel(model,bounds=(0,1))
Después de eso, puedes probar diferentes ataques. Comencemos con el más popular: FGSM:
Fgsm
attack = foolbox.attacks.FGSM(fmodel)
La salida de la red neuronal será la siguiente
[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
Y la imagen resultante:

Entonces, ahora con una probabilidad de más del 50% 0 se reconoció como 6. Ya es bueno. Sin embargo, todavía queremos obtener 1, y el nivel de ruido no es muy impresionante. La imagen realmente se ve inverosímil. Más sobre esto más tarde. Mientras tanto, tratemos de ignorar los ataques. De repente, todavía tenemos 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))
Conclusión
[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
Imagen:

De nuevo por. Ahora tenemos 0 reconocido como 9 con una probabilidad de ~ 49%. Sin embargo, el ruido es mucho menor.
Terminemos con un ritmo aleatorio. Se eligió un ejemplo de tal manera que sería muy difícil obtener el resultado al azar. Ahora no hemos indicado en ninguna parte que queremos obtener 1. En consecuencia, realizamos un ataque no dirigido y creíamos que seguiríamos obteniendo la clase 1, pero esto no sucedió. Por lo tanto, vale la pena pasar a los ataques dirigidos. Usemos la documentación de foolbox y busquemos el módulo de criterios allí
En este módulo, puede seleccionar un criterio para un ataque, si lo admite. Específicamente, estamos interesados en dos criterios:
- TargetClass: hace que, en el vector de distribuciones de probabilidad, el elemento con el número k tenga la probabilidad máxima.
- TargetClassProbability: hace que, en el vector de distribuciones de probabilidad, un elemento con número k tenga una probabilidad de al menos p.
Probemos ambos:
L-BFGS + TargetClass
Lo principal en los criterios de TargetClass es obtener la probabilidad de la clase k, mayor que la probabilidad de cualquier otra clase. Entonces la red que toma la decisión simplemente mirando la probabilidad máxima se equivocará.
attack = foolbox.attacks.LBFGSAttack(fmodel,foolbox.criteria.TargetClass(1))
Conclusión
[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
Imagen:

Como se puede ver en la conclusión, ahora nuestra red neuronal afirma que es 1 con una probabilidad de 32.8%, mientras que la probabilidad de 0 es lo más cercana posible a este valor y es 32.6%. Lo hicimos! En principio, esto ya es suficiente para completar la tarea. Pero iremos más allá e intentaremos obtener una probabilidad de 1 por encima de 0.5.
L-BFGS + TargetClassProbability
Ahora usamos el criterio TargetClassProbability, que le permite obtener la probabilidad de una clase en un objeto no inferior a p. Tiene solo dos parámetros:
1) El número de clase del objeto.
2) La probabilidad de esta clase en el ejemplo de confrontación.
Además, si es imposible lograr tal probabilidad, o el tiempo para encontrar dicho objeto lleva demasiado tiempo, entonces el objeto de confrontación será igual a ninguno. Puede verificar esto usted mismo tratando de calcular la probabilidad de, por ejemplo, 0.99. Entonces el método puede no converger.
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))
Conclusión
[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]
¡Hurra! ¡Logramos obtener un ejemplo de confrontación, en el que la probabilidad 1 para nuestra red neuronal es superior al 50%! Genial Ahora hagamos la desnormalización (regrese la imagen al formato 0-255) y guárdela.
El guión final es el siguiente:
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')
Y la imagen final es la siguiente:
.
Conclusiones
Entonces, como vimos en los ejemplos anteriores, engañar a una red neuronal era bastante simple. También hay una gran cantidad de métodos capaces de hacer esto. Simplemente abra la lista de ataques disponibles en foolbox e intente aplicarlos. Sugerimos que intente hacer lo mismo usted mismo, tomando como base la misma red neuronal y la misma imagen, disponible por referencia . Puedes dejar tus preguntas en los comentarios. ¡Les responderemos!
Recuerde siempre que, no importa cuán útiles sean los algoritmos y modelos, pueden ser extremadamente inestables a pequeños cambios que pueden conducir a errores graves. Por lo tanto, le recomendamos que pruebe sus modelos, en los que python y herramientas como foolbox pueden ayudar.
Gracias por su atencion!