Препарирование нейронок или TSNE и кластеризация на терабайтах данных

image-loader.svg

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

Методов для интроспекции нейронных сетей придумано достаточно. Первое, что приходит в голову:

(Еще можно посмотреть здесь).

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

image-loader.svg

В ходе решения задачи распознавания лиц у нас возникла такая гипотеза, что в данных содержится гораздо больше информации, чем остается после сжатия в вектор признаков (Feature Vector). К примеру, у лиц, несомненно, есть атрибуты. Принято полагать, что на каждом следующем слое нейронной сети выделяются все более и более высокоуровневые признаки: начиная с уголков, черточек и других примитивов, заканчивая прической, полом, возрастом (применительно к распознаванию лиц). Мы не знаем заранее, какие высокоуровневые признаки на самом деле выделяются, из-за отсутствия разметки. Например, это могут быть: ношение очков, пол, ракурс съемки, прочие визуальные препятствия на фото. Если получить разметку таких скрытых атрибутов удастся, то можно фильтровать данные по ним, собирать малопредставленные в данных признаки (очки, маски) и так далее. В общем, вещь полезная.

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

Идея

Как достать больше информации из нейронной сети? Хочется взглянуть на ее внутреннее представление до сжатия в вектор признаков. Однако размерность пространства достаточно большая, и анализировать его сложно. На помощь могли бы прийти PCA или TSNE, которые отлично справляются со сжатием в ограниченное число размерностей. Рассмотрим PCA:

image-loader.svg

Рассмотрим компоненты PCA и визуализируем первые 10 из них картинками из датасета. Выясняется, что:

  1. Чтобы объяснить 80% вариативности, нужно достаточно много (200) компонент.

  2. Анализ главных компонент PCA не оказался информативным, преобладание компоненты не означало наличия интерпретируемого признака (зато там есть Гарри Поттер).

    Анализ главных компонент на признаках нейронной сетиАнализ главных компонент на признаках нейронной сети

Теперь TSNE. Его проблемы состоят в том, что:

  1. Он не масштабируется на размер датасета, который предполагается исследовать — мы не дождемся результатов.

  2. Если мы сделаем сжатие на меньшей выборке, экстраполировать на остальную выборку не представляется возможным: есть fit, нет predict.

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

Масштабируем TSNE + Кластеризацию

Пайплайн обучения модели TSNE+КластеризацииПайплайн обучения модели TSNE+Кластеризации

Решение кроется в том, что в действительности нам не так важен результат работы TSNE — куда важнее получить метки кластеров. Будем использовать внутреннее представление нейронной сети — эмбеддинги. Если нам надо получить только метки, мы можем, например, сделать следующий трюк (нумерация соответствует картинке выше):

  1. Прогоняем нейронную сеть и делаем подвыборку из эмбеддингов датасета.

  2. Делаем TSNE на подвыборке (2% — в нашем случае).

  3. Делаем кластеризацию на результате п.2.

  4. Обучаем классификатор «эмбеддинг — номер кластера». Сохраняем модель в ONNX.

  5. Объединяем ONNX-граф нейронной сети с ONNX-графом классификатора.

  6. Прогоняем классификатор на полной выборке или новых данных.

При таком подходе мы сможем аппроксимировать результат понижения размерности + кластеризации на неограниченный объем данных. Приступим.

Готовимся

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

Такой конфигурации будет более чем достаточно, чтобы и извлечь эмбеддинги, и запустить TSNE и провести кластеризацию.

Весь используемый код находится в этом репозитории: https://github.com/NTech-Lab/dl-tsne

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

Подготовка нейронной сети

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

Для открытого эксперимента мы взяли нейронную сеть распознавания лиц из репозитория InsightFace: webface_r50_pfc.onnx. Одну из самых лучших из доступных для свободного скачивания в академических целях.

Извлечение признаков

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

Модификация ONNX

model = onnx.load_model(model_file)
intermediate_tensor_name = model.graph.node[-4].output[0]
intermediate_layer_value_info = onnx.helper.ValueInfoProto()
intermediate_layer_value_info.name = intermediate_tensor_name
model.graph.output.extend([intermediate_layer_value_info])
onnx.save(model, "interim+"+model_file)

О выборе промежуточного слоя. Посмотреть список промежуточных слоев в нейронной сети можно через model.graph.node — это лист из нод ONNX. Для желаемого слоя нам надо узнать имя тензора, где сохраняется результат выхода. Интуиция такая, что:

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

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

Конечно, хочется использовать сам вектор признаков лица, однако это будет не оптимально. Если посмотреть на устройство нейронной сети, увидим, что вектор признаков размерности 512 получается из тензора размерности 512×7х7 и сжимает информацию. В нашем случае — у сетки webface_r50_pfc.onnx сжатие информации происходит для создания эмбеддинга лица. Последний слой перед сжатием это:

model.graph.node[-4]

input: "679"
input: "bn2.weight"
input: "bn2.bias"
input: "bn2.running_mean"
input: "bn2.running_var"
output: "680"
name: "BatchNormalization_126"
op_type: "BatchNormalization"
attribute {
  name: "epsilon"
  f: 9.999999747378752e-06
  type: FLOAT
}
attribute {
  name: "momentum"
  f: 0.8999999761581421
  type: FLOAT
}

Подготовка данных

Для эксперимента мы скачали уже подготовленные данные glint360k, ссылку на скачивание можно найти в репозитории InsightFace (распакован в data/glint360). Для использования своих датасетов можно обратить внимание на скрипт, который мы подготовили (он задействует пайплайн инсайта для детекции и нормализации). Мы сложили все в папку data/, чтобы можно было, в случае чего, подменить данные.

Для однообразного доступа к каждой картинке можно использовать простые списки файлов. Например, файл glint360.txt был создан, как

cd data
find ./glint_orig/ -name '*.jpg' > ../lists/glint360.txt

Готовим признаки

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

def prepare_batch(imgs):
    if not isinstance(imgs, list):
        imgs = [imgs]
    blob = cv2.dnn.blobFromImages(
				imgs, 1.0 / input_std, input_size,
        (input_mean, input_mean, input_mean), 
				swapRB=True
		)
    return blob

Для хранения признаков настоятельно рекомендуем использовать HDF5 формат, он удобен, переносим и выдерживает огромные размеры датасетов. Из недостатков самый неприятный — случайный доступ к конкретным элементам, что понадобится в дальнейшем. Имеет смысл сразу сделать как полный дамп эмбеддингов, так и подвыборку, чтобы потом сэкономить время.

Как мы организовали дамп в HDF5

Важно: для скрытого слоя берем центральный пиксель. Эмпирически

© Habrahabr.ru