Обман нейронной сети для начинающих

    image


    В рамках ежегодного контеста ZeroNights HackQuest 2018 участникам предлагалось попробовать силы в целом ряде нетривиальных заданий и конкурсов. Часть одного из них была связана с генерированием adversarial-примера для нейронной сети. В наших статьях мы уже уделяли внимание методам атаки и защиты алгоритмов машинного обучения. В рамках же этой публикации мы разберем пример того, как можно было решить задание с ZeroNights Hackquest при помощи библиотеки foolbox.


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


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

    В файле 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) ^_^)

    Для получения заветного тикета атакующему предлагалось преобразовать ZeroSource.bmp:


    image


    таким образом, чтобы после отправки на сервер нейронная сеть интерпретировала это изображение как 1. Если попытаться отправить данное изображение без обработки, то результат работы нейронной сети будет равен 0.


    И, конечно же, главная подсказка к этому заданию — файл модели KerasModel.h5 (именно этот файл помогает атакующему перевести атаку в плоскость WhiteBox, так как ему доступна нейронная сеть и все данные, связанные с ней). В названии файла сразу же содержится подсказка — название фреймворка, на котором реализована нейронная сеть.


    Именно с такими вводными участник и приступал к решению задания:


    • Модель нейронной сети написанная на Keras.
    • Возможность отправлять изображение на сервер с помощью curl.
    • Исходное изображение, которое необходимо было изменить.

    На стороне сервера проверка была максимально простая:


    1. Изображение должно быть нужного размера – 28x28 пикселей.
    2. На данном изображении модель должна вернуть 1.
    3. Разница между исходным изобаржением ZeroSource.bmp и отправленным на сервер должна быть меньше порога k по метрике MSE (среднеквадратичная ошибка).

    Итак, начнем.


    Сперва участнику было необходимо найти информацию о том, как же обмануть нейронную сеть. После недолгих действий в гугле он получал ключевые слова "Adversarial пример" и "Adversarial атака". Далее ему надо было поискать инструменты, позволяющие применять adversarial-атаки. Если вбить в Google запрос "Adversarial attacks on Keras Neural Net", первая же ссылка будет на GitHub проекта FoolBox – библиотеки на python для генерации adversarial-примеров. Конечно же, есть и другие библиотеки (о некоторых из них мы говорили в предыдущих статьях). Более того, атаки можно было написать, что называется, from scratch. Но мы все таки остановимся на самой популярной библиотеке, которую человек, ранее не сталкивавшийся с темой adversarial-атак, может найти по первой же ссылке в Google.


    Теперь необходимо написать Python-cкрипт, который сгенерирует adversarial-пример.
    Начнем мы, конечно же, с импорта.


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

    Что мы здесь видим?


    1. Keras — фреймворк, на котором написана Нейронная сеть, которую мы будем обманывать.
    2. NumPy — библиотека, которая позволит нам эффективно работать с векторами.
    3. PIL — инструмент для работы с изображениями.
    4. FoolBox — библиотека для генерации adversarial-примеров.

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


    model = keras.models.load_model("KerasModel.h5") # Загружаем модель
    model.summary() # Выводим информацию о модели
    model.input # Выводим информацию о том, какие данные должны поступать на вход НС

    На выходе мы получим следующее:


    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>

    Какую информацию отсюда можно вынести?


    1. Модель на вход(conv2d_1 слой) принимает объект размерностью ?x28x28x1, где "?" – количество объектов; если изображение одно, тогда размерность будет 1x28x28x1. А изображение представляет с собой трехмерный массив, где одна размерность равна 1. То есть изображение подается в виде таблицы значений от 0 до 255.
    2. На выходе у модели (dense_2 слой) получается вектор размерностью 10.

    Загрузим изображение и не забудем привести его к типу float (далее нейронная сеть будет работать именно с вещественными числами) и нормализовать его (поделим все значения на 255). Здесь стоит уточнить, что нормализация – один из "обязательных" приемов при работе с нейронными сетями, но атакующий ведь мог этого и не знать, поэтому мы специально добавили небольшую подсказку в описании к задаче):


    img = Image.open("ZeroSource.bmp") # Загружаем изображение
    img = np.array(img) # Приводим его к типу numpy.array
    img = img.astype('float32') # приводим значения массива к типу float
    img /= 255 # нормализуем

    Теперь мы можем отправить изображение в загруженную модель и посмотреть, какой результат она выдает:


    model.predict(img.reshape(1,28,28,1)) # вызываем метод predict у найше модели и посылаем туда массив предварительно откорректровав его размерность

    На выходе получаем следующую информацию


    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)

    Здесь стоит объяснить, что из себя представляет данный вектор: на самом деле, это распределение вероятностей, то есть каждое число представляет собой вероятность класса 0,1,2...,9. Сумма всех чисел в векторе равна 1. В данном случае видно, что модель уверена в том, что на входном изображении представлен класс 0 с вероятностью в 100%.


    Если изобразить это на гистограмме, мы получим следующее:


    image


    Абсолютная уверенность.


    Если бы модель не смогла определить класс, вектор вероятности стремился бы к равномерному распределению, что, в свою очередь, означало бы, что модель относит объект ко всем классам одновременно с одинаковой вероятностью. А гистограмма выглядела бы вот так:


    image


    Принято считать, что класс модели определяется по индексу максимального числа в данном векторе. То есть модель теоретически могла выбрать класс, у которого вероятность больше 10%. Но эта логика может изменяться в зависимости от логики принятия решения, описанной разработчиками.


    Теперь перейдем к самому интересному — adversarial-атакам.


    Во-первых, для работы с моделью в библиотеке FoolBox необходимо перевести модель в нотацию Foolbox. Сделать это можно так:


    fmodel = foolbox.models.KerasModel(model,bounds=(0,1)) # в bounds показывается, в каком диапазоне входных значений работает модель, так как наши данные были поделены на 255, то они как раз приведены в диапазон 0-1.

    После этого можно тестировать разные атаки. Начнем с самой популярной – 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)) # Индекс максимального числа в векторе

    Выход нейронной сети будет выглядеть следующим образом


    [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

    А полученное изображение:


    image


    Итак, теперь с вероятностью более 50% 0 был распознан как 6. Уже хорошо. Однако нам ведь все-таки хочется получить 1, да и уровень шума не сильно впечатляет. Изображение, действительно, выглядит неправдоподобно. Об этом чуть позже. А пока давайте попробуем просто поперебирать атаки. Вдруг мы все таки получим 1.


    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))

    Вывод:


    [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

    Изображение:


    image


    Опять мимо. Теперь у нас 0 распознается как 9 с вероятностью ~49%. Впрочем, шума уже гораздо меньше.


    Давайте на этом закончим бить рандомом. Пример был выбран таким образом, что рандомно получить результат будет весьма сложно. Сейчас мы нигде не указывали, что мы хотим получить 1. Соответственно мы проводили non-targeted атаку и верили, что все таки получим класс 1, но этого не случилось. Поэтому стоит перейти к targeted атакам. Давайте воспользуемся документацией к foolbox и найдем там модуль criteria


    В данном модуле можно выбрать критерий для атаки, если она их поддерживает. Конкретно нас интересует два критерия:


    1. TargetClass – делает так, чтобы в векторе распределений вероятности, элемент под номером k имел максимальную вероятность.
    2. TargetClassProbability – делает так, чтобы в векторе распределений вероятности, элемент под номером k имел вероятность не ниже p.

    Давайте попробуем оба:


    L-BFGS + TargetClass

    Главное в TargetClass критерии — получить вероятность класса k, выше чем вероятность любого другого класса. Тогда сеть, которая принимает решение просто смотря на максимальную вероятность — ошибется.


    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))

    Вывод:


    [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

    Изображение:


    image


    Как видно из вывода, теперь наша нейронная сеть утверждает, что это 1 с вероятностью 32,8%, при этом вероятность 0 максимально близка к этому значению и равна 32,6%. У нас получилось! В принципе, этого уже достаточно, чтобы выполнить задание. Но мы пойдем дальше и попробуем получить вероятность 1 выше 0,5.


    L-BFGS + TargetClassProbability

    Теперь воспользуемся критерием TargetClassProbability, который позволяет получить вероятность класса в объекте не ниже p. У него есть всего два параметра:
    1) Номер класса объекта.
    2) Вероятность этого класса в adversarial-примере.
    При этом, если достичь такой вероятности невозможно или время для нахождения такого объекта занимает слишком много времени, то объект adversarial будет равен none. Вы можете самостоятельно это проверить, попробовав сделать вероятность, скажем, 0,99. Тогда метод вполне может не сойтись.


    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))

    Вывод:


    [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]

    Ура! У нас вышло получить adversarial-пример, на котором вероятность 1 для нашей нейронной сети выше 50%! Замечательно! Теперь давайте сделаем денормализацию (вернем изображение в формат 0-255) и сохраним его.


    Итоговый скрипт получился следующим:


    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')

    А итоговое изображение выглядит следующим образом:


    image.


    Выводы

    Итак, как мы увидели из приведенных выше примеров, обмануть нейронную сеть было достаточно просто. Методов, способных это сделать, также большое количество. Достаточно просто открыть список доступных атак в foolbox и попробовать их применить. Мы предлагаем вам самим попробовать провернуть то же самое, взяв за основу ту же нейронную сеть и то же самое изображение, доступные по ссылке. Ваши вопросы вы можете оставлять в комментариях. Мы обязательно на них ответим!


    Всегда помните о том, что, как бы полезны ни были алгоритмы и модели, они могут быть крайне неустойчивы к небольшим сдвигам, которые могут приводить к серьезным ошибкам. Поэтому рекомендуем вам тестировать свои модели, в чем вам могут помочь python и инструменты по типу foolbox.


    Спасибо за внимание!

    • +23
    • 7,1k
    • 8
    Digital Security
    180,00
    Безопасность как искусство
    Поделиться публикацией

    Комментарии 8

      0
      Спасибо! а достигнуть подобного эффекта для обучения можно просто применив шум к изображению?
        –2
        проще единичку нарисовать
          0
          единичку для чего нарисовать? я спрашиваю чтобы обучить именно нулю даже если будет подобная атака
            +2
            Добрый день! На стороне сервера это было учтено, мы проверяли разницу между исходным изображением 0 и сгенерированным атакующим по среднеквадратичной ошибке. Если она была выше порога, который был подобран специально, чтобы нельзя было отправить изображение 1,2,3 и тд. и даже другое изображение 0, тогда сервер отклонял данный пример. Необходимо было как раз таки работать с генерацией Adversarial примера на основе исходного изображения.
            +1

            Добрый день! Не совсем понимаю ваш вопрос, о каком именно эффекте для обучения идет речь? Если вы имеете ввиду как достигнуть эффекта, чтобы сеть можно было обмануть — тогда этот эффект достигается абсолютно естественным образом, ничего особенного делать не надо, просто обучить сеть как обычно, об этом шла речь в первой статье.
            Если же ваш вопрос связан с тем, как сделать так, чтобы данный эффект не наблюдался при использовании, то тогда необходимо применять различные техники повышения устойчивости модели, о них рассказывалось во второй статье. Один из способов, под названием Adversarial Training как раз про то, чтобы добавить сгенерированные атаками изображения в обучение, тогда сеть будет более устойчива. Либо метод Gaussian Data Augmentation, в котором в обучающую выборку добавляются изображения с добавлением Гауссовского шума. Засчет чего достигается схожий с Adversarial Training эффект. Это те методы, что связанны с обучением.

              0
              да, я про Adversarial Training спрашивал
            0
            В качестве ликбеза: а если сэмплы, которые использовались для атаки, включить в обучающий набор с правильными индексами, насколько это в дальнейшем затруднит расчет значений для атаки? насколько снизится точность сети? как будут меняться веса? или обучающий набор с подобными сэмплами не будет эффективным для обучения и сетка просто ни чему не научится?
            [добавлено] и что если для входного слоя делать дропаут?
              +2

              Добрый день, начнем по порядку.
              1: а если сэмплы, которые использовались для атаки, включить в обучающий набор с правильными индексами, насколько это в дальнейшем затруднит расчет значений для атаки?


              • Данный метод называется Adversarial Training, мы атакуем нашу сеть, добавляем полученные Adversarial примеры в обучающую выборку, обучаем, атакуем опять и тд. Вообще AT в некотором роде, представляет собой подход Minimax из теории игр, на эту тему даже есть интересная статья. Данный метод действительно работает, устойчивость сети к атакам повышается — а соответственно повышается и уровень шума, необходимый для обмана сети. Но при этом есть одна серьезная проблема, атак работающих по разным принципам достаточно большое количество — соответственно необходимо покрывать большое распределение adversarial примеров. С всязи с чем мы должны генерировать все новые и новые примеры обучаться на них и тд. Что уже очень затратно по времени и ресурсам, но и это еще не все. Чем больше мы добавляем примеров с разным распределением, со временем целевая метрика(например точность) так же начнет падать — поэтому данный метод не пользуется большой популярностью на практике. Его альтернативой может быть Gaussian Data Augmentation суть которого в добавлении нормального шума к изображениям и обучении на новых семплах. Как показывают результаты работы он работает не хуже, а чаще даже лучше чем AT (Но тут надо смотреть на методику эксперимента, в работе скорее показали что качество моделей полученные методами Virtual Adversarial Training и AT на основе атаки FGSM, можно получить более простым и "дешевым" способом.)

              2: насколько снизится точность сети? как будут меняться веса? или обучающий набор с подобными сэмплами не будет эффективным для обучения и сетка просто ни чему не научится?


              • Здесь вопрос более практический, приведу пример из своей практики: Решалась задача обработки МРТ снимков и выявление опухолей и их классификация на основе этих снимков. В качестве одного из экспериментов мы решили применить описанные выше методы AT и GDA. И оказалось, что качество работы сети на реальных данных улучшилось! Это связано с тем, что в МРТ снимках есть сложные Гамма шумы, а используя методы AT и GDA мы повышаем сопротивляемость нейронной сети к этим шумам, за счет чего повышается и качество работы сети. Поэтому ответить однозначно на этот вопрос нельзя, так же в моей практике были примеры, где качество падало. Здесь все скорее зависит от решаемой задачи. Что точно можно сказать, так это то, что сеть будет обучаться(AT и GDA не внесут никакой фатальщины), но качество может меняться в ту или иную сторону.

              3: и что если для входного слоя делать дропаут?


              • Очень хороший вопрос! В прошлой статье, где говорилось о методах защиты, мы приводили в пример ансамблирование, с подтекстом, что обмануть 1 модель проще, чем обмануть N моделей на одном примере. Если вспомнить интерпретацию Дропаута, то окажется, что по своей сути дропаут так же является способом ансамблирования нейронных сетей, только виртуальным. А уже в работе исследовалось влияние Dropoutа как средство повышения устойчивости моделей к атакам. И действительно, повышается сопротивляемость атакам, но нужно искать золотую середину между качеством решения целевой задачи и уровнем сопротивляемости, тут тоже не все так однозначно.
              • В целом можно сказать следующее, что методы повышения устойчивости(защиты моделей) уж точно не будут бесплатными(время, ресурсы, качество работы на целевой задаче). Стоит это понимать и помнить об этом.

              Надеюсь я ответил на Ваши вопросы.

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