Auteurs: G. W. Ding

Brève introduction au phénomène antagoniste

On sait que les modèles d’apprentissage automatique sont vulnérables aux perturbations antagonistes. Ces perturbations consistent en des modifications mineures des données d’entrée, imperceptibles pour l’humain, qui peuvent entraîner des prédictions radicalement différentes pour la machine.

Voici un exemple (réalisé avec Jupyter) : une perturbation minimale (image du milieu) ajoutée à l’image du panda (à gauche) amène le modèle du réseau de neurones à classer l’image perturbée (à droite) comme un seau. Et pourtant, pour un observateur humain, l’image perturbée semble parfaitement identique à l’image originale.

Pourquoi ce phénomène est-il important ?

La perturbation antagoniste présente, pour les applications d’apprentissage automatique, certains risques qui peuvent avoir un effet significatif dans des situations d’utilisation réelle. Ainsi, les chercheurs ont démontré que la superposition de bandes noires et blanches sur un signal d’arrêt empêche les systèmes ultramodernes de détection de reconnaître correctement l’objet.

Eykholt et al. (2018)

Ce problème ne touche pas que les images : les systèmes de reconnaissance de la parole et de détection de logiciels malveillants présentent les mêmes vulnérabilités face à ce type d’attaques. En fait, n’importe quel système d’apprentissage automatique est susceptible de subir une attaque antagoniste si une telle attaque peut s’avérer payante : systèmes de détection des fraudes, d’identification, de prise de décisions, etc.

AdverTorch entre en scène

Borealis AI a créé, dans son laboratoire de recherche, l’outil AdverTorch (voir référentiel et rapport), qui permet d’exécuter une série de stratégies de type « attaque-défense ». L’idée a germé en 2017, quand mon équipe a commencé à travailler plus intensivement sur la robustesse antagoniste. Nous disposions alors de deux seuls outils : CleverHans et Foolbox.

Ce sont deux bons outils, mais qui comportent chacun des limites. À l’époque, CleverHans était configuré pour TensorFlow, donc nous ne pouvions pas l’utiliser dans d’autres cadres d’apprentissage profond (PyTorch en l’occurrence). De plus, l’approche en graphe computationnel statique de TensorFlow rendait la création d’attaques plus complexes. Pour une personne qui ne connaît pas ce champ de recherche, il est difficile de comprendre ce qui se passe quand l’attaque est écrite dans un langage de graphe statique.

Foolbox, pour sa part, contient une variété de méthodes d’attaque, mais n’admet que les attaques en image par image, et non en lot par lot. Ces paramètres ralentissent les processus et confinent donc l’outil au domaine des évaluations. Une autre lacune de Foolbox réside dans le nombre insuffisant d’attaques possibles ; il manque par exemple l’attaque par descente de gradient projeté (PGD) et l’attaque Carlini-Wagner sous contrainte ℓ2-norm.

Notre solution

En l’absence d’une boîte à outils adaptée à nos besoins, nous avons décidé de créer notre propre solution – un outil original qui nous permettrait notamment d’utiliser notre langage préféré (PyTorch), une capacité inexistante sur les deux autres outils.

Nous cherchions avant tout à fournir aux chercheurs des outils pouvant servir à explorer les divers aspects de la robustesse antagoniste. Pour le moment, notre outil AdverTorch s’adresse surtout aux chercheurs et aux praticiens qui peuvent avoir une compréhension algorithmique des méthodes employées.

Voici quels étaient nos critères :

  • Interfaces de programmation simples et cohérentes pour les attaques et les moyens de défense ; 
  • Implantations de référence condensées, basées sur les graphes computationnels dynamiques de PyTorch ; 
  • Exécution rapide par des implantations PyTorch sur processeurs graphiques (GPU), un critère important pour les algorithmes d’« attaques de boucle », dans le cas d’un entraînement antagoniste, par exemple.

Pour l’avenir, nous rendrons l’outil plus convivial, selon les ressources disponibles.
 

Stratégies « attaques-défenses »

Pour les attaques par descente du gradient, nous pouvons recourir notamment aux méthodes du signe du gradient rapide (Goodfellow et al., 2014), aux méthodes de descente du gradient projeté (Madry et al., 2017), à l’attaque de type Carlini-Wagner (Carlini et Wagner, 2017) et à la transformation spatiale (Xiao et al., 2018). Nous avons aussi pris en compte quelques attaques sans gradient, dont l’attaque par pixel unique, l’attaque par recherche locale (Narodytska et Kasiviswanathan, 2016) et l’attaque de carte de saillance avec jacobien (Papernot et al., 2016). 

Au-delà des attaques, nous avons aussi créé un conteneur pratique pour l’approximation différentiable rétroactive (Athalye et al., 2018) ; il s’agit d’une technique qui perfectionne les attaques par gradient face aux modèles privés de composantes non différentiables ou de masquage du gradient.

Du côté de la défense, nous avons retenu deux stratégies : i) la défense par prétraitement et ii) l’entraînement robuste. Dans le cas de la défense par prétraitement, nous appliquons un filtre JPEG, la compression des bits et une variété de filtres de lissage spatial.

Quant aux méthodes d’entraînement robuste, elles prennent la forme d’exemples dans notre référentiel. À ce jour, nous possédons un script pour l’entraînement antagoniste dans MNIST (auquel vous pouvez accéder ici) et nous prévoyons ajouter de nouveaux exemples exploitant des méthodes différentes sur de multiples ensembles de données.
 

Comment créer une attaque

Nous utilisons la méthode du signe du gradient rapide pour montrer comment créer une attaque dans AdverTorch. Le module GradientSignAttack est accessible ici : advertorch.attacks.one_step_gradient.

Pour viser un classificateur, nous devons récupérer Attack et LabelMixin de la baseadvertorch.attacks.base.

from advertorch.attacks.base import Attack
from advertorch.attacks.base import LabelMixin

Attack est la classe de base de toutes les attaques dans AdverTorch. Elle définit l’API d’une Attack. En voici l’essentiel :

    def __init__(self, predict, loss_fn, clip_min, clip_max):
        self.predict = predict
        self.loss_fn = loss_fn
        self.clip_min = clip_min
        self.clip_max = clip_max

    def perturb(self, x, **kwargs):
        error = "Sub-classes must implement perturb."
        raise NotImplementedError(error)

    def __call__(self, *args, **kwargs):
        return self.perturb(*args, **kwargs)

Une attaque comprend trois éléments centraux :

predict : la fonction cible de l’attaque ;
loss_fn : la fonction de perte que nous maximisons pour maintenir l’attaque ; 
perturb : la méthode qui implémente l’algorithme d’attaque.

Voici un exemple utilisant GradientSignAttack.

class GradientSignAttack(Attack, LabelMixin):
    """
    Méthode du signe du gradient rapide en une étape (Goodfellow et al, 2014).
    Paper:
https://arxiv.org/abs/1412.6572

    :param predict: forward pass function.
    :param loss_fn: loss function.
    :param eps: attack step size.
    :param clip_min: minimum value per input dimension.
    :param clip_max: maximum value per input dimension.
    :param targeted: indicate if this is a targeted attack.
    """

    def __init__(self, predict, loss_fn=None, eps=0.3, clip_min=0.,
                 clip_max=1., targeted=False):

            """
        Créez une instance de GradientSignAttack.
        """

        super(GradientSignAttack, self).__init__(
            predict, loss_fn, clip_min, clip_max)

        self.eps = eps
        self.targeted = targeted
        if self.loss_fn is None:
            self.loss_fn = nn.CrossEntropyLoss(reduction="sum")

    def perturb(self, x, y=None):
        """
        L’instance tient compte des exemples (x, y) puis retourne leurs équivalents antagonistes
        avec une longueur d’attaque de eps.

        :param x : input tensor.
        :param y: label tensor.
                  - if None and self.targeted=False, compute y as predicted
                    labels.
                  - if self.targeted=True, then y must be the targeted labels.
        :return: tensor containing perturbed inputs.

        """

        x, y = self._verify_and_process_inputs(x, y)
        xadv = x.requires_grad_()
        
        ###############################
        # start: the attack algorithm #

        outputs = self.predict(xadv)
        loss = self.loss_fn(outputs, y)
        if self.targeted:
            loss = -loss
        loss.backward()
        grad_sign = xadv.grad.detach().sign()
        xadv = xadv + self.eps * grad_sign
        xadv = clamp(xadv, self.clip_min, self.clip_max)
        # end:   the attack algorithm # 
        ###############################

               return xadv

predict est le classificateur comme tel, et loss_fn est la fonction de perte pour le calcul du gradient. La méthode perturb retient x et y comme arguments, où x est l’entrée à attaquer, et y, la véritable étiquette de x. predict(x) ; elle contient les « logits » du travail neuronal. La fonction de perte loss_fn peut consister en une fonction de perte par entropie croisée, ou en une autre fonction de perte qui prend predict(x) et y comme arguments.

Grâce à son graphe computationnel dynamique, l’algorithme d’attaque de PyTorch peut être mis en œuvre facilement, en quelques lignes de code. Pour lancer d’autres types d’attaques, il suffit de remplacer le code de l’algorithme dans perturb, puis de modifier les paramètres qu’on veut passer à __init__.

La dissociation de ces trois composantes centrales permet de les adapter à un éventail d’attaques encore plus vaste. Normalement, il faut que predict et loss_fn soient conçus pour que loss_fn prenne toujours predict(x) et y comme entrées. C’est pourquoi la méthode perturb n’a pas besoin de connaître le contenu de predict et de loss_fn. Par exemple, FastFeatureAttack et PGDAttack partagent la même fonction itérative (perturb_iterative), mais diffèrent par rapport à predict et à loss_fn. Dans FastFeatureAttack, predict(x) retourne la représentation d’une caractéristique à partir d’une couche spécifique, y est la représentation-guide qui doit être appariée par predict(x), et loss_fn devient l’erreur moyenne au carré.

En fait, y peut être n’importe quelle cible de la perturbation antagoniste, tandis que predict(x) peut produire (en sortie) des structures de données plus complexes, pour autant que loss_fn puisse les accepter en entrée. Il devient alors possible de générer une perturbation capable de fausser, simultanément, les résultats du classificateur du modèle A et la représentation d’une caractéristique du modèle B. Il suffirait de transformer y et predict(x) en n-uplets d’étiquettes et de caractéristiques, et de modifier loss_fn en conséquence. Tout cela peut se faire sans modifier la perturbation initiale.
 

Établir une nouvelle défense

Tel que mentionné plus haut, AdverTorch comporte des modules de défense par prétraitement de même que des exemples d’entraînement robuste.

Nous utilisons MedianSmoothing2D pour illustrer comment définir une défense par prétraitement.

class MedianSmoothing2D(Processor):
    """
    Median Smoothing 2D.
    :param kernel_size: aperture linear size; must be odd and greater than 1.
    :param stride: stride of the convolution.
    """

    def __init__(self, kernel_size=3, stride=1):
        super(MedianSmoothing2D, self).__init__()
        self.kernel_size = kernel_size
        self.stride = stride
        padding = int(kernel_size) // 2
        if _is_even(kernel_size):
            # both ways of padding should be fine here
            # self.padding = (padding, 0, padding, 0)

            self.padding = (0, padding, 0, padding)
        else:
            self.padding = _quadruple(padding)

     def forward(self, x):
        x = F.pad(x, pad=self.padding, mode="reflect")
        x = x.unfold(2, self.kernel_size, self.stride)
        x = x.unfold(3, self.kernel_size, self.stride)
        x = x.contiguous().view(x.shape[:4] + (-1, )).median(dim=-1)[0]
        return x

Le préprocesseur se résume simplement à un torch.nn.Module. La fonction __init__ extrait les paramètres nécessaires, tandis que la fonction vers l’avant implémente l’algorithme de prétraitement comme tel. Avec MedianSmoothing2D, on peut créer un nouveau modèle en modifiant le modèle initial :

median_filter = MedianSmoothing2D()
new_model = torch.nn.Sequential(median_filter, model)
y = new_model(x)

Ou on peut l’invoquer en série :

processed_x = median_filter(x)
y = model(processed_x)

Vous trouverez dans tutorial_train_mnist.py un exemple montrant comment AdverTorch peut être utilisé pour un entraînement antagoniste (Madry et al. 2018). À la différence d’un entraînement normal, nous n’avons besoin que de deux modifications. Premièrement, il faut initialiser un antagoniste, avant le début de l’entraînement.

   if flag_advtrain:
        from advertorch.attacks import LinfPGDAttack
        adversary = LinfPGDAttack(
            model, loss_fn=nn.CrossEntropyLoss(reduction="sum"), eps=0.3,
            nb_iter=40, eps_iter=0.01, rand_init=True, clip_min=0.0,
            clip_max=1.0, targeted=False)

Deuxièmement, il s’agit de générer un « mini-lot antagoniste » pendant l’entraînement, et de l’utiliser pour entraîner le modèle plutôt que le mini-lot original.         

     if flag_advtrain:
                advdata = adversary.perturb(clndata, target)
                with torch.no_grad():
                    output = model(advdata)
                test_advloss += F.cross_entropy(
                    output, target, reduction='sum').item()
                pred = output.max(1, keepdim=True)[1]
                advcorrect += pred.eq(target.view_as(pred)).sum().item()

Depuis sa production, nous avons utilisé cette boîte à outils pour deux articles : « On the Sensitivity of Adversarial Robustness to Input Data Distributions » et « MMA Training: Direct Input Space Margin Maximization through Adversarial Training ». Nous espérons sincèrement que vous utiliserez AdverTorch dans vos recherches et que vous trouverez ses différents modules utiles. Nous accueillons évidemment tous les ajouts ou commentaires qui pourraient bénéficier à la communauté. Vous pouvez ouvrir une « Issue » ou une « Pull request » pour obtenir AdverTorch, ou écrivez-moi à l’adresse gavin.ding@borealisai.com

Auteurs