Adversarial examples para reconocimiento facial

En los últimos años se ha descubierto que varios modelos de aprendizaje automático son vulnerables a los llamados adversarial examples - entradas que hacen que el modelo devuelva una salida errónea, normalmente construidas aplicando pequeñas perturbaciones a entradas legítimas. Por ejemplo, en Explaining and Harnessing Adversarial Examples, Goodfellow et al. construyen imagenes, añadiéndoles un ruido imperceptible, que son clasificadas erróneamente por un modelo con arquitectura GoogLeNet entrenado para clasificar imágenes en diferentes clases.

adversarial example
Un adversarial example en el que un panda es clasificado como un gibón, del artículo Explaining and Harnessing Adversarial Examples

En este artículo explicaré como construir adversarial examples para engañar sistemas de reconocimiento facial.

El problema

Los modelos de reconocimiento facial reciben normalmente como entrada dos caras alineadas y devuelven como salida la distancia entre esas dos caras (si la distancia es mayor a un umbral, entonces las caras pertenecen a diferentes personas). Para hacer esto usan dos CNNs (Redes Neuronales Convolucionales) que calculan un face embedding (un vector numérico que representa los rasgos de una cara) a partir de una imágen de una cara. Alimentan la primera CNN con la primera cara y la segunda CNN con la segunda cara, y después miden la distancia entre los dos embeddings. Esto se llama Siamese Network.

Una siamese network de reconocimiento facial

En este artículo he probado dos ataques diferentes:

  • Impersonation: El atacante intenta que su cara sea reconocida como otro usuario concreto. Por ejemplo, si un atacante quiere hacer login en la cuenta bancaria del usuario Bob, intentará que su cara sea reconocida como la cara de Bob. La arquitectura siamesa hace que este ataque sea imposible a menos que el atacante tenga acceso al face embedding de la víctima, ya que no hay manera de modificar el embedding de salida de la víctima cambiando la imagen de la cara del atacante.
  • Dodging: El atacante intenta ser identificado erróneamente como cualquier otra persona, o como una persona desconocida. Este ataque puede ser utilizado por motivos de privacidad, por ejemplo, cuando subimos una foto a una red social.

Construyendo adversarial examples en Facenet

Para construir adversarial examples, he usado el FGSM (Fast Gradient Sign Method) sobre una Inception Resnet v1 CNN pre-entrenada del proyecto Facenet que logra un 99.2% de precisión sobre la base de datos LFW. El modelo fue pre-entrenado usando la base de datos de caras MS-Celeb-1M. He usado Tensorflow y la librería Cleverhans, que contiene implementaciones de varios ataques para construir adversarial examples.

En la librería Cleverhans, el FGSM es usado contra problemas de clasificación, en los que la salida es un vector softmax en el que cada número representa el porcentaje de confianza en que la entrada sea de una clase. Aunque hay otras formas de usar el FGSM, se puede convertir un problema de reconocimiento facial en uno de clasificación, en el que las clases sean “Misma persona” y “Diferente persona”, siendo una la inversa de la otra. Para hacer eso solamente tuve que transformar la distancia entre face embeddings, de manera que a mayor distancia más confianza en la clase “diferente persona”.

Tras la conversión el modelo sería algo así:

Modelo de clasificación de reconocimiento facial

Pero ¿Como funciona el FGSM? El FGSM calcula el gradiente de la función de coste de la CNN respecto a los pixeles de la imagen de entrada, y después actualiza la imagen añadiendo el signo del gradiente calculado multiplicado por un pequeño valor ε. El coste se saca de la comparación entre el vector de salida softmax del modelo y las etiquetas (Una etiqueta será un vector [1, 0] si estás comparando dos caras de la misma persona, y [0, 1] si estás comparando dos caras de personas diferentes, ya que el primer número corresponde al score de “Misma persona”). Las etiquetas son calculadas automáticamente por la implementación del FGSM que hemos usado, con lo cual no necesitamos insertarlas explícitamente.

Echemos ahora un vistazo al código. Primero, he creado una clase Model de Cleverhans que carga la CNN de Facenet:

class InceptionResnetV1Model(Model):
    model_path = "models/facenet/20170512-110547/20170512-110547.pb"

    def __init__(self):
        super(InceptionResnetV1Model, self).__init__()

        # Load Facenet CNN
        facenet.load_model(self.model_path)
        # Save input and output tensors references
        graph = tf.get_default_graph()
        self.face_input = graph.get_tensor_by_name("input:0")
        self.embedding_output = graph.get_tensor_by_name("embeddings:0")

También he creado un método de esa clase que crea los tensores de Tensorflow necesarios para convertir el modelo en uno de clasificación, como he explicado previamente:

    def convert_to_classifier(self):
        # Create victim_embedding placeholder
        self.victim_embedding_input = tf.placeholder(
            tf.float32,
            shape=(None, 128))

        # Squared Euclidean Distance between embeddings
        distance = tf.reduce_sum(
            tf.square(self.embedding_output - self.victim_embedding_input),
            axis=1)

        # Convert distance to a softmax vector
        # 0.99 out of 4 is the distance threshold for the Facenet CNN
        threshold = 0.99
        score = tf.where(
            distance > threshold,
            0.5 + ((distance - threshold) * 0.5) / (4.0 - threshold),
            0.5 * distance / threshold)
        reverse_score = 1.0 - score
        self.softmax_output = tf.transpose(tf.stack([reverse_score, score]))

Después he programado la función “main”, en la que instanciamos y creamos el modelo:

with tf.Graph().as_default():
    with tf.Session() as sess:
        # Load model
        model = InceptionResnetV1Model()
        # Convert to classifier
        model.convert_to_classifier()

Antes de usar el FGSM he cargado 1000 pares de caras atacante-víctima de la base de datos LFW y he creado los face embeddings de las víctimas, pasando las caras de las víctimas a la CNN de Facenet (hay que tener en cuenta que estos embeddings podrían haberse obtenido de muchas otras maneras, por ejemplo, robándolos de una base de datos):

        # Load pairs of faces and their labels in one-hot encoding
        faces1, faces2, labels = set_loader.load_testset(1000)

        # Create victims' embeddings using Facenet itself
        graph = tf.get_default_graph()
        phase_train_placeholder = graph.get_tensor_by_name("phase_train:0")
        feed_dict = {model.face_input: faces2,
                     phase_train_placeholder: False}
        victims_embeddings = sess.run(
            model.embedding_output, feed_dict=feed_dict)

Después he programado una versión iterativa del FGSM usando la librería Cleverhans. En esta versión, en lugar de multiplicar los gradientes por la epsilon, los he multiplicado por epsilon dividido por el número total de iteraciones:

        # Define FGSM for the model
        steps = 1
        eps = 0.01
        alpha = eps / steps
        fgsm = FastGradientMethod(model)
        fgsm_params = {'eps': alpha,
                       'clip_min': 0.,
                       'clip_max': 1.}
        adv_x = fgsm.generate(model.face_input, **fgsm_params)

        # Run FGSM
        adv = faces1
        for i in range(steps):
            print("FGSM step " + str(i + 1))
            feed_dict = {model.face_input: adv,
                         model.victim_embedding_input: victims_embeddings,
                         phase_train_placeholder: False}
            adv = sess.run(adv_x, feed_dict=feed_dict)

Lo bueno de este método es que para pares de caras de la misma persona el FGSM tratará de hacer el ataque dodging, mientras que para pares de caras de personas diferentes tratará de hacer el ataque impersonation, ya que siempre tratará de hacer adversarial examples que confundan el modelo.

Este ataque ha sido incluido en los ejemplos de la librería Cleverhans. Puedes ver el código completo aquí.

Resultados

Para dodging he logrado una precisión del 0.4% (lo que significa que casi todos los adversarial examples han sido capaces de esquivar la clasificación del sistema de Reconocimiento Facial, que tiene un 99.2% de precisión sobre la base de datos LFW) usando un ε de 0.3 y una iteración del FGSM, aunque las caras generadas apenas se parecen a una cara real.

Conjunto de adversarial examples generadas usando un ε de 0.3 y 1 iteración para dodging e impersonation

Por otra parte, usando un ε de 0.01 y una iteración del FGSM he sido capaz de generar imágenes de caras casi idénticas a las originales, y lograr una precisión del 19.81% para dodging.

Ejemplo de adversarial example generado para dodging usando un ε de 0.01 y una iteración

Para impersonation he logrado una precisión del 48.29%, lo que significa que la mitad de los adversarial examples generados han sido capaces de ser clasificados erróneamente como una persona en concreto. Los mejores resultados se han obtenido usando un ε de 0.01 y una iteración del FGSM.

Ejemplo de adversarial example generado para impersonation usando un ε de 0.01 y una iteración.

Las imágenes generadas son muy similares a las imágenes originales también. Además, si miramos el ruido detenidamente podemos ver que tiene la forma de las facciones de la cara de la imagen original tanto para dodging como para impersonation.

Conjunto de ruido generado usando un ε de 0.01 y 1 iteración para dodging e impersonation.
Conjunto de adversarial examples generados usando un ε de 0.01 y 1 iteración para dodging e impersonation.

Al usar más iteraciones del FGSM el algoritmo acabó creando estas caras:

Conjunto de ruido generado usando un ε de 0.1 y 10 iteraciones para dodging e impersonation.
Conjunto de adversarial examples generados usando un ε de 0.1 y 10 iteraciones para dodging e impersonation.

Es extraño porque parece que el algoritmo está intentando recrear la cara de la víctima, pero algunos rasgos de la cara están en posiciones no habituales.

Ejemplo de adversarial example generado para impersonation usando un ε de 0.1 y 10 iteraciones.

Los resultados conseguidos con estos parámetros no fueron mejores (sobre un 44% para dodging y un 77% para impersonation), pero decidí incluirlos ya que las caras generadas son interesantes para entender lo que el FGSM está haciendo realmente.

Transferibilidad

Como habrás observado, para ejecutar el FGSM u otros ataques de caja blanca es necesario tener acceso a la red neuronal. Pero, ¿Que pasa si necesitamos romper un sistema de reconocimiento facial para el cual no tenemos acceso al modelo? En ese caso podríamos probar a usar un ataque de caja blanca sobre otro modelo y usar los adversarial examples generados para atacar el sistema. Esto funciona a veces por la propiedad de transferabilidad de los sistemas de aprendizaje automático.

Para probar la transferibilidad hemos usado las caras generadas a lo largo de este artículo sobre la Microsoft Azure Face API. Solamente he sido capaz de obtener un 69.77% de precisión para dodging, y nada de degradación en la precisión para impersonation.

Conclusiones

Hay algunas conclusiones y preguntas relativas a la seguridad que creo que pueden ser interesantes de discutir:

  • La arquitectura de redes siamesas parece una opción razonable en términos de seguridad cuando una de las entradas del sistema actúa como una “contraseña” (en este caso, el face embedding de un usuario), ya que evita que los atacantes puedan construir adversarial examples que intenten modificar los cómputos hechos por el modelo sobre la “contraseña”.
  • A pesar de que podemos crear adversarial examples a partir del face embedding de una víctima, guardar las imágenes de las caras de los usuarios en una base de datos parece menos seguro que guardar los face embeddings, ya que estas pueden ser reusadas en otros sistemas si estas son robadas.
  • El face embedding de un usuario puede usarse para crear adversarial examples para hacernos pasar por el mismo incluso en otros sistemas. ¿Qué pasaría si guardásemos estos embeddings en una base de datos y esta fuese robada? ¿Deberíamos de proteger los embeddings al igual que hacemos con las contraseñas (por ejemplo, normalmente hasheamos las contraseñas antes de guardarlas en una base de datos)?
  • Los ataques de caja blanca son más fáciles de realizar que los de caja negra. Para evitar los ataques de caja blanca podríamos asegurar el modelo en sí para evitar la construcción de adversarial examples, pero… ¿Confiar solamente en la ocultación del modelo es una buena manera de resolver el problema?

Updated: