«Противники» буквально означают оппонента в соревновании, в случае моделей машинного обучения зашумленные данные часто могут действовать как этот противник. Давайте попробуем понять злоумышленников, используя простой пример модели логистической регрессии.

Мы будем использовать sklearn.datasets для создания фиктивных данных и PyTorch для построения нашей модели логистической регрессии.

# Importing nessecary libraries
import torch
import random, os
from torch import nn
import numpy as np
import torch.nn.functional as F
import seaborn as sns
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt

Закладка фиксированных семян для размножения.

torch.manual_seed(42)
np.random.seed(42)
random.seed(42)

Теперь мы генерируем данные, используя функциональность make_blobs(), присутствующую в модуле набора данных sklearn. Мы генерируем 500 с 2 функциями. Мы также преобразуем массив Numpy, сгенерированный из make_blobs, в тензор факела.

def generate_data(n_samples = 500):
  X, y = make_blobs(n_samples = 500, 
                    n_features = 2, 
                    centers = 2, 
                    cluster_std = 2, 
                    random_state = 42)
  X = torch.tensor(X, dtype = torch.float32)
  y = torch.tensor(y, dtype = torch.float32)
  return X, y
X, y = generate_data()

Сгенерированные данные должны выглядеть примерно так:

Чтобы определить нашу модель сейчас, нам в основном нужен (2, 1)-мерный тензор случайных чисел, который будет обучаться во время оптимизации в нашей модели.

# lr is too high but the model converged at that.
def train_model(X, y, 
                loss_fn = None, 
                activation = None, 
                epochs = 2000, lr = 12):
    w = torch.randn((2, 1), dtype = torch.float32, requires_grad = True)
    b = torch.randn(1, dtype = torch.float32, requires_grad = True)
    for _ in range(epochs):
        output = activation((X @ w) + b)
        loss = loss_fn(output, y.unsqueeze(dim = 1))
        w.grad = None
        b.grad = None
        loss.backward()
        w.data = w.data - lr * w.grad.data
        b.data = b.data - lr * b.grad.data
    print(f"The model achieved a loss of {loss}")
    return w, b

act = nn.Sigmoid()
loss_fn = nn.BCELoss()
w, b = train_model(X, y, loss_fn = loss_fn, activation = act)

Модель должна достичь низких потерь в этой точке, сходящихся примерно к 0,018, и граница решения должна выглядеть следующим образом.

Если мы внимательно посмотрим на границу решения, то увидим, что некоторые точки классифицируются моделью неправильно, некоторые лежат относительно близко к границе решения, и, наконец, некоторые точки довольно хорошо классифицируются.

Наше внимание сосредоточено на точках, которые лежат относительно близко к границе решения, и если бы эти точки немного встряхнули по направлению к границе решения, добавив к ним некоторый случайный шум, они были бы неправильно классифицированы нашей моделью. Оказывается, линейные классификаторы, такие как логистическая регрессия, и нелинейные классификаторы, такие как нейронные сети, плохо реагируют на эти встряски. Нейронные сети в больших размерностях иногда могут стать слишком локализованными для определенного класса примеров, как будто они построили потемкинскую деревню, которая хорошо реагирует на естественные данные, но выставляется как подделка при посещении точек данных с низкой вероятностью происходит.

Теперь возникает вопрос, как мы генерируем такие состязательные примеры, чтобы разоблачать такие потемкинские деревни?

Метод быстрого знака градиента (FSGM)

Метод FSGM, предложенный Гудфеллоу и др. (статья), решает именно эту проблему подготовки нашей модели против таких противников. Он решает проблему создания состязательных примеров для нашей модели, которые можно использовать, чтобы сделать ее более надежной.

При обучении модели машинного обучения мы стремимся оптимизировать веса модели. Эти веса в случае нейронных сетей оптимизируются с использованием обратного распространения, которое включает в себя определение градиента функции потерь по отношению к весам. Другими словами, этот градиент сообщает нам значения, которые нам нужно вычесть из наших весов, чтобы уменьшить потери, что, в свою очередь, делает нашу модель лучше.

Если мы можем вычислить градиент потерь по отношению к весам, то мы можем вычислить этот градиент и по отношению к входным данным. Это даст нам значения, которые при добавлении к нашим исходным входам увеличат общие потери модели. Именно такую ​​идею предложили авторы.

Изображение выше взято из статьи FSGM, модель предсказывает, что входные данные — это панда с достоверностью 57,7%, что довольно мало. Эта низкая достоверность прогноза означает, что это изображение находится довольно близко к границе решения в пространстве более высокого измерения, поэтому, если бы мы подтолкнули его немного ближе к границе решения, нарушив его, модель ошибочно классифицировала бы это как что-то другое. который в данном случае оказывается гиббоном с достоверностью 99,3%.

Авторы вычисляют знак градиента входного изображения по отношению к функции потерь. Это дает нам значения, которые при добавлении к исходным входным данным увеличивают функцию потерь и в дальнейшем дают нам состязательный пример. Примечание. В примере с пандой возмущение уменьшено в 0,007 раза.

Цель взятия знака градиента состоит в том, что функция знака имеет производную, равную нулю или неопределенную везде, что делает невозможным для таких моделей, как нейронные сети, предвидеть, как могут возникнуть возмущения во входных данных. Эпсилон здесь контролирует интенсивность атаки FSGM, масштабируя знак градиентов вверх или вниз. Чем выше значение эпсилон, тем выше интенсивность атаки и тем выше увеличение потерь при оценке модели на этих враждебных примерах.

ФСГМ в действии

Давайте посмотрим, как мы можем генерировать состязательные примеры для нашей модели логистической регрессии из предыдущих и атаковать нашу модель, используя эти данные. Код для создания враждебных примеров так же прост, как установка для атрибута require_grad входного тензора значения True. Это позволит вычислить градиенты для входных примеров во время обратного распространения.

def generate_adversarial_examples(X, y, w, b, loss_fn, activation, epsilon):
    # Set requires_grad for input to True
    X.requires_grad = True
    outputs = activation((X @ w) + b)
    loss = loss_fn(outputs, y.unsqueeze(dim = 1))
    loss.backward()
    X_adv = X + epsilon * (torch.sign(X.grad.data))
    return X_adv
X_adv = generate_adversarial_examples(X, y, w, b,
                                      loss_fn,
                                      activation = act, 
                                      epsilon = 0.7)

Мы можем посмотреть, как вновь сгенерированные состязательные примеры распределяются по границе решения старой модели, используя диаграмму рассеивания.

Мы видим, как атака FSGM на исходные данные привела к тому, что некоторые точки оторвались от своих первоначальных мест. Потери модели увеличиваются примерно с 0,018 до 0,07. Примечание. Эти числа могут отличаться из-за случайной инициализации веса и смещения при обучении модели.

Ключевые моменты и выводы

  1. При обучении моделей машинного обучения мы сталкиваемся с зашумленными данными, и метод быстрого знака градиента дает нам способ оценить модели на этих возмущениях.
  2. FSGM используется для генерации состязательных примеров путем вычисления градиента потерь по отношению к входным данным.
  3. Знак этого градиента дает нам значения, которые при добавлении к нашим исходным данным увеличат потери и в дальнейшем будут давать нам состязательные примеры.
  4. Эти примеры можно использовать, чтобы сделать модель устойчивой к таким возмущениям. Это называется состязательной подготовкой, о которой я планирую рассказать в следующих блогах.