Повышение устойчивости классификационной модели с помощью состязательной регуляризации в TensorFlow

Введение

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

От современного оборудования с охлаждением жидким азотом в виде блоков обработки тензорных блоков (TPU) до все более сложных многомиллионных параметрических глубинно-сверточных сетей, таких как GoogLeNet, AlexNet - возможности такой технологии продолжает разрушать ранее неприступные преграды.

Состязательная уязвимость

Несмотря на эти невероятные достижения, было доказано, что даже самые искусные модели не безупречны. Многочисленные исследования показали, насколько чувствительны эти модели даже к незаметно небольшим изменениям в структуре входных данных. Изначально в выводах совместной исследовательской работы Google и Нью-Йоркского университета: Интригующие свойства нейронных сетей, 2014 тема уязвимости модели к примерам состязательности теперь признана настолько важной, что теперь существуют соревнования. чтобы взяться за это.

Существование этих ошибок поднимает множество вопросов об обобщении вне выборки и о том, как использование таких примеров может быть использовано для злоупотребления развернутыми системами.

Нейронно-структурированное обучение

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

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

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

Neural Structured Learning (NSL) - это относительно новый фреймворк с открытым исходным кодом, разработанный хорошими ребятами из TensorFlow для обучения глубоких нейронных сетей со структурированными сигналами (в отличие от обычного одиночного образца). NSL реализует обучение нейронных графов, при котором нейронная сеть обучается с использованием графов (см. Изображение ниже), которые несут информацию как о цели (узле), так и о соседних узлах в других узлах, связанных через ребра узлов.

При этом это позволяет обученной модели одновременно использовать как помеченные, так и немаркированные данные посредством:

  1. Обучение модели на размеченных данных (стандартная процедура в любой задаче обучения с учителем);
  2. Смещение сети для изучения похожих скрытых представлений для соседних узлов на графе (относительно меток входных данных)

В соответствии с пунктом два мы можем наблюдать в приведенном выше выражении как минимизацию эмпирических потерь, то есть контролируемых потерь, так и потерю соседей. В приведенном выше примере это вычисляется как скалярное произведение вычисленного вектора веса в целевом скрытом слое и меры расстояния (т.е. расстояния L1, L2) между входными данными, X, и тот же вход с добавлением некоторой степени шума:

Состязательные примеры обычно создаются путем вычисления градиента выходных данных по отношению к входным, x_i, а затем максимизации потерь. Например, если у вас есть модель, которая классифицирует чихуахуа и кексы, и вы хотите создать состязательные примеры, вы должны ввести изображение чихуахуа размером 128 x 128 пикселей в свою сеть, вычислить градиент потерь относительно. входных данных (тензор 128 x 128), затем добавьте отрицательный градиент (возмущение) к вашему изображению, пока сеть не классифицирует изображение как маффин. При повторном обучении на этих сгенерированных изображениях с правильной меткой сеть становится более устойчивой к шуму / возмущениям.

Зачем использовать NSL?

  • Более высокая точность: структурированные сигналы среди выборок могут предоставить информацию, которая не всегда доступна во входных параметрах.
  • Повышенная надежность: модели, обученные на примерах противоборства, явно более устойчивы к противодействию возмущениям, созданным для введения в заблуждение прогнозов или классификации модели.
  • Требуются менее маркированные данные: NSL позволяет нейронным сетям использовать как маркированные, так и немаркированные данные, заставляя сеть изучать похожие скрытые представления для «соседних выборок», которые могут иметь или не иметь метки.

Состязательная регуляризация

Что мы можем сделать, если у нас нет таких явных структур в качестве входных данных?

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

Состязательная регуляризация на практике

В следующем примере мы собираемся сравнить производительность базовой модели классификации изображений (в частности, сверточной нейронной сети) с вариантом, в котором используется состязательная регуляризация. К сожалению, мы не можем продемонстрировать использование AR на каких-либо из вышеупомянутых данных майнинга, поскольку они являются собственностью.

Вместо этого мы проведем этот анализ для двух моделей, обученных на известном наборе данных классификации изображений - Beans. Мы сравним результаты базовой модели и модели, обученной на примерах состязательности, чтобы полностью понять влияние состязательной регуляризации на возможности и производительность каждой модели.

Записную книжку Colab с кодом, использованным в этой статье, можно найти здесь. Отличный учебник, в котором послужил источник вдохновения для этой статьи и откуда взялась часть кода, можно найти на странице TensorFlow NSL.

Прежде чем мы начнем, мы должны сначала установить пакет нейронного структурированного обучения TensorFlow:

!pip install neural_structured_learning

Импорт

import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import numpy as np
import keras_preprocessing
import neural_structured_learning as nsl
import tensorflow as tf
import tensorflow_datasets.public_api as tfds
from tensorflow.keras import models
from keras_preprocessing import image
from keras_preprocessing.image import ImageDataGenerator

Загрузить и проверить данные изображения

TensorFlow содержит ряд известных наборов данных в своей коллекции TensorFlow Datasets.

Мы можем загрузить набор данных Beans, которому мы хотим обучить нашу модель, используя метод tfds.load(), который выполняет две операции:

  1. Скачивает набор данных и сохраняет его как tfrecord файлы.
  2. Загружает tfrecord файлы и возвращает экземпляр tf.data.Dataset
# load dataset
dataset = 'beans' #@param
dataset = tfds.load(dataset, shuffle_files=True)
train, test = dataset['train'], dataset['test']
IMAGE_INPUT_NAME = 'image'
LABEL_INPUT_NAME = 'label'

Перед выполнением любого масштабирования изображения или увеличения / изменения изображения мы можем проверить образец изображений в наборе данных, чтобы получить представление о различных структурах и композициях, которые сверточный слой может воспринимать как функцию (и), и понять различия между различными классами в наборе данных:

# Get random batch
raw_images = train.take(10)
# Tensor to np format
raw_images = [item['image'] for item in
raw_images.as_numpy_iterator()]
# Plot batch
fig = plt.gcf()
fig.set_size_inches(10, 10)
for i, img in enumerate(raw_images):
  sp = plt.subplot(2, 5, i+1)
  sp.axis('Off')
  plt.imshow(img)
plt.show()

По умолчанию объект tf.data.Dataset содержит dict из tf.Tensors. Мы можем перебирать пакет изображений (значения ключа tf.data.Dataset), вызывая .as_numpy_iterator() на нашем raw_images в рамках нашего понимания списка. Этот метод возвращает генератор, который преобразует пакетные элементы набора данных из формата tf.Tensor в np.array. Затем мы можем построить получившийся пакет изображений:

Предварительная обработка

Мы выполняем простую операцию масштабирования данных изображения, чтобы сопоставить входные данные с тензором с плавающей запятой между 0 и 1 (набор данных Beans представляет собой набор изображений размером 500 x 500 x 3). Полезно, что наборы данных TDFS хранят атрибуты объектов в виде словарей:

FeaturesDict({
    'image': Image(shape=(500, 500, 3), dtype=tf.uint8),
    'label': ClassLabel(shape=(), dtype=tf.int64, num_classes=3),
})

В результате мы можем получить доступ к отдельным изображениям и их меткам и выполнить эти операции предварительной обработки на месте с атрибутом .map() нашего обучающего и tf.Dataset экземпляров:

def normalize(features):
  """Scale images to within 0-1 bound based on max image size."""
  features[IMAGE_INPUT_NAME] = tf.cast(
    features[IMAGE_INPUT_NAME], 
    dtype=tf.float32) / 500.0)
  return features
def examples_to_tuples(features):
  return features[IMAGE_INPUT_NAME], features[LABEL_INPUT_NAME]
def examples_to_dict(image, label):
  return {IMAGE_INPUT_NAME: image, LABEL_INPUT_NAME: label}
# Define train set, preprocess. (Note: inputs shuffled on load)
train_dataset = train.map(normalize)
                     .batch(28)
                     .map(examples_to_tuples)
test_dataset = test.map(normalize)
                   .batch(28)
                   .map(examples_to_tuples)

Функция examples_to_dict будет вскоре объяснена.

Базовая модель

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

def conv_nn_model(img_input_shape: tuple) -> tf.keras.Model():
  """Simple Conv2D Neural Network.
    Args:
      img_input_shape: An (mxnxo) tuple defining the input image   
      shape.
    Returns:
      model: An instance of tf.keras.Model.
  """
  model = tf.keras.models.Sequential([
      tf.keras.layers.Conv2D(16, (3,3), activation='relu',   
          input_shape=input_shape),
      tf.keras.layers.MaxPooling2D(2, 2),
      tf.keras.layers.Conv2D(32, (3,3), activation='relu'),
      tf.keras.layers.MaxPooling2D(2,2),
      tf.keras.layers.Conv2D(32, (3,3), activation='relu'),
      tf.keras.layers.MaxPooling2D(2,2),
      tf.keras.layers.Flatten(),
      tf.keras.layers.Dense(64, activation='relu'),
      # Note to adjust output layer for number of classes
      tf.keras.layers.Dense(3, activation='softmax')])
  return model
# Beans dataset img dims (pixel x pixel x bytes)
input_shape = (500, 500, 3)
# Establish baseline
baseline_model = conv_nn_model(input_shape)
baseline_model.summary()
baseline_model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['acc'])
baseline_history = baseline_model.fit(
    train_dataset,
    epochs=5)

results = baseline_model.evaluate(test_dataset)
print(f'Baseline Accuracy: {results[1]}')
3/3 [==============================] - 0s 72ms/step - loss: 0.1047 - acc: 0.8934 
Baseline Accuracy: 0.8934375

Мы видим, что наша базовая модель хорошо себя зарекомендовала на тестовом наборе данных, достигнув точности 89%.

Модель состязательной регуляризации

Теперь мы рассмотрим, как эта модель работает с тестовым набором, который включает в себя состязательно возмущенные примеры, и сопоставим ее с моделью, обученной на наборе данных, который включает указанные примеры. Сначала мы создадим еще одну сверточную модель NN, только на этот раз мы включим состязательное обучение в ее цель обучения.

Затем, используя структуру NSL TensorFlow, мы определяем объект конфигурации с вспомогательной функцией NSL nsl.configs.make_adv_reg_config:

#@title ADV Regularization Config
# Create new CNN model instance
base_adv_model = conv_nn_model(input_shape)
# Create AR config object 
adv_reg_config = nsl.configs.make_adv_reg_config(
    multiplier=0.2,
    adv_step_size=0.2,
    adv_grad_norm='infinity')
# Model wrapper 
adv_reg_model = nsl.keras.AdversarialRegularization(
    base_adv_model,
    label_keys=[LABEL_INPUT_NAME],
    adv_config=adv_config)

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

  • multiplier: Вес состязательного проигрыша относительно отмеченного проигрыша во время обучения относительно целевой функции нашей модели AR. Мы применяем 0,2 в качестве веса регуляризации.
  • adv_step_size: Степень / величина враждебного возмущения, применяемого во время обучения.
  • adv_grad_norm: Тензорная норма (L1 или L2) для нормализации градиента, т.е. мера величины враждебного возмущения. По умолчанию L2.

Затем мы можем обернуть нашу недавно созданную модель с помощью функции nsl.keras.AdversarialRegularization, которая добавит состязательную регуляризацию, которую мы настроили ранее с нашим adv_reg_configobject, к цели обучения (функция потерь, которая должна быть минимизирована) нашей базовой модели.

На этом этапе следует отметить важный момент: наша модель ожидает, что входные данные будут отображением словаря между именами и значениями функций. Можно видеть, что при создании экземпляра нашей модели противоборства мы должны передать label_keys в качестве параметра. Это позволяет нашей модели различать входные данные и целевые данные. Здесь мы можем использовать нашу функцию examples_to_dict и сопоставить ее с нашими наборами данных для обучения и тестирования:

train_set_for_adv_model = train_dataset.map(convert_to_dictionaries)
test_set_for_adv_model = test_dataset.map(convert_to_dictionaries)

Затем мы компилируем, подбираем и оцениваем нашу регуляризованную модель как обычно:

4/4 [==============================] - 0s 76ms/step - loss: 0.1015 - sparse_categorical_crossentropy: 0.1858 - sparse_categorical_accuracy: 0.8656 - scaled_adversarial_loss: 0.1057  accuracy: 0.911625

Точно так же наша агрессивно регуляризованная модель хорошо обобщается на наш тестовый набор данных, достигая точности (0,91%), аналогичной нашей baseline_model.

Оценка на основе данных, подвергшихся серьезным возмущениям

Теперь самое интересное.

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

Чтобы сгенерировать вышеупомянутые примеры, мы должны сначала создать эталонную модель, конфигурация которой (потери, метрики и откалиброванные / изученные веса) будут использоваться для генерации возмущенных примеров. Для этого мы еще раз оборачиваем нашу базовую модель производительности функцией nsl.keras.AdversarialRegularization и компилируем ее. Обратите внимание, что мы не подгоняем эту модель к нашему набору данных - мы хотим сохранить те же полученные веса, что и наша базовая модель):

# Wrap baseline model
reference_model = nsl.keras.AdversarialRegularization(
    baseline_model,
    label_keys=[LABEL_INPUT_NAME],
    adv_config=adv_reg_config)
reference_model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['acc']
models_to_eval = {
    'base': baseline_model,
    'adv-regularized': adv_reg_model.base_model}
metrics = {
    name: tf.keras.metrics.SparseCategoricalAccuracy()
    for name in models_to_eval.keys()}

Если на этом этапе вы похожи на меня и хотите понять логику этих вещей, вы можете найти исходный код, содержащий класс состязательной регуляризации, здесь.

Затем мы сохраняем две наши модели; базовый план и вариант со состязательной регуляризацией в словаре, а затем цикл по каждому пакету нашего тестового набора данных (пакетная оценка является требованием модели AdversarialRegularization).

С помощью .perturb_on_batch() метода нашего недавно обернутого reference_model мы можем генерировать зависящие от врага пакеты, согласованные с нашим adv_reg_config объектом, и оценивать производительность наших двух моделей на них:

labels, y_preds = [], []
# Generate perturbed batches, 
for batch in test_set_for_adv_model:
  perturbed_batch = reference_model.perturb_on_batch(batch)
  perturbed_batch[IMAGE_INPUT_NAME] = tf.clip_by_value(
      perturbed_batch[IMAGE_INPUT_NAME], 0.0, 1.0)
  # drop label from batch
  y = perturbed_batch.pop(LABEL_INPUT_NAME)
  y_preds.append({})
  for name, model in models_to_eval.items():
    y_pred = model(perturbed_batch)
    metrics[name](y, y_pred)
    predictions[-1][name] = tf.argmax(y_pred, axis=-1).numpy()
for name, metric in metrics.items():
  print(f'{name} model accuracy: {metric.result().numpy()}')
>> base model accuracy: 0.2201466 adv-regularized model accuracy: 0.8203125

Результаты

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

Производительность нашей базовой модели упала на 69% по сравнению с нашей регулируемой моделью, которая снизила производительность всего на 14%.

С помощью Kera Layers API мы можем изучить влияние данных, подвергнутых враждебному вмешательству, на нашу базовую модель, визуализировав сверточные слои, чтобы понять, какие функции были извлечены как до, так и после возмущения:

До возмущения

# Random img & conv layer idxs
IDX_IMAGE_1=2
IDX_IMAGE_2=5
IDX_IMAGE_3=10
CONVOLUTION_NUMBER = 10
# Get baseline_model layers 
layer_outputs = [layer.output for layer in baseline_model.layers]
activation_model = tf.keras.models.Model(
    inputs =baseline_model.input, 
    outputs = layer_outputs)
# Plot img at specified conv
f, axarr = plt.subplots(3,2, figsize=(8, 8))
for x in range(0, 2):
  f1 = activation_model.predict(test_images[IDX_IMAGE_1].reshape(
      1, 500, 500, 3))[x]
  axarr[0,x].imshow(f1[0, : , :, CONVOLUTION_NUMBER],cmap='inferno')
  axarr[0,x].grid(False)
  f2 = activation_model.predict(test_images[IDX_IMAGE_2].reshape(
      1,500, 500, 3))[x]
  axarr[1,x].imshow(f2[0, : , :, CONVOLUTION_NUMBER],cmap='inferno')
  axarr[1,x].grid(False)
  f3 = activation_model.predict(test_images[IDX_IMAGE_3].reshape(
      1, 500, 500, 3))[x]
  axarr[2,x].imshow(f3[0, : , :, CONVOLUTION_NUMBER],cmap='inferno')
  axarr[2,x].grid(False)

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

После возмущения

Теперь мы можем изучить, какие особенности базовая модель определяет в возмущенных данных:

# Pertubed test data
perturbed_images = []
for batch in test_set_for_adv_model:
  perturbed_batch = reference_model.perturb_on_batch(batch)
  perturbed_batch[IMAGE_INPUT_NAME] = tf.clip_by_value(
  perturbed_batch[IMAGE_INPUT_NAME], 0.0, 1.0)
  perturbed_images.append(perturbed_batch)
# Get images
pt_img = [item['image'] for item in perturbed_images]
IDX_IMAGE_1=0
IDX_IMAGE_2=1
IDX_IMAGE_3=2
CONVOLUTION_NUMBER = 11
base_mod_layer_out = [layer.output for layer in baseline_model.layers]
base_mod_activ = tf.keras.models.Model(
  inputs = baseline_model.input,
  outputs = base_mod_layer_out)
f1 = base_mod_activ.predict(pt_img[IDX_IMAGE_1].numpy())[x]
f2 = base_mod_activ.predict(pt_img[IDX_IMAGE_2].numpy())[x]
f3 = base_mod_activ.predict(pt_img[IDX_IMAGE_3].numpy())[x]

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

Вывод

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

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

Пожалуйста, прокомментируйте, если вы обнаружите ошибки или у вас есть конструктивная критика / сборки.

Спасибо за чтение.

использованная литература