Асинхронная загрузка больших датасетов в Tensorflow
В Сети много тюториалов и видеолекций, и других материалов обсуждающих основные принципы построения нейронных сетей, их архитектуру, стратегии обучения и т.д. Традиционно, обучение нейронных сетей производится путем предявления нейронной сети пакетов изображений из обучающей выборки и коррекции коэффициентов этой сети методом обратного распространения ошибки. Одним из наиболее популярных инструментов для работы с нейронными сетями является библиотека Tensorflow от Google.
Нейронная сеть в Tensorflow представляется последовательностю операций-слоев
(таких как перемножение матриц, свертка, пулинг и т.д.). Слои нейронной сети совместно с операциями корректировки коэффициентов образуют граф вычислений.
Процесс обучения нейронной сети при этом заключается в «предъявлении» нейронной
сети пакетов объектов, сравненнии предсказанных классов с истинными, вычисления
ошибки и модификации коэффициентов нейронной сети.
При этом Tensoflow скрывает технические подробности обучения и реализацию алгоритма корректировки коэффициентов, и с точки зрения программиста можно говорить в основном только о графе вычислений, производящем «предсказания». Сравните граф вычислений, о котором думает программист
с графом который в том числе выполняет подстройку коэффициенотов
.
Но что Tensorflow не может сделать за программиста, так это преобразовать входной датасет в датасет удобный для тренировки нейронной сети. Хотя библиотека имеет довольно много «базовых блоков».
Как с их использованием построить эффективный конвеер для «питания» (англ feed) нейронной сети входными данными я и хочу расскажу в этой статье.
В качестве примера задачи будет использован датасет ImageNet, опубликованный недавно в качестве соревнования по детектированию объектов на Kaggle Мы будем обучать сеть детектированию одного объекта, того, у которого самый большой ограничивающий прямоугольник.
Если вы еще не пробовали работать с этой библиотекой, возможно стоит изучить основные концепции, например в статье Библиотека глубокого обучения Tensorflow, или на официальном сайте
Подготовительные шаги
Ниже предполагается, что у вас
- установлен [Python][python_org], в примерах используется Python 2.7,
но не должно быть сложностей с их портированием на Python 3.* - становлена библитека [Tensorflow и Python-интерфейс к ней][install_tensorflow]
- скачан и распакован [набор данных][download_dataset] из соревнования на Kaggle
Традиционные алиасы для библиотек:
import tensorflow as tf import numpy as np
Препроцессинг данных
Для загрузки данных, мы будем использовать механизмы, предоставляемые модулем для работы с датасетами в Tensorflow.
Для тренировки и валидации нам потребуется датасет, в котором одновременно и изображения и их описания. Но в скачанном датасете файлы с изображениями и аннотациями аккуратно разложены по разным папочкам.
Поэтому мы сделаем итератор который итерируется по соответствующим парам.
ANNOTATION_DIR = os.path.join("Annotations", "DET") IMAGES_DIR = os.path.join("Data", "DET") IMAGES_EXT = "JPEG" def image_annotation_iterator(dataset_path, subset="train"): """ Yields tuples of image filename and corresponding annotation. :param dataset_path: Path to the root of uncompressed ImageNet dataset :param subset: one of 'train', 'val', 'test' :return: iterator """ annotations_root = os.path.join(dataset_path, ANNOTATION_DIR, subset) print annotations_root images_root = os.path.join(dataset_path, IMAGES_DIR, subset) print images_root for dir_path, _, file_names in os.walk(annotations_root): for annotation_file in file_names: path = os.path.join(dir_path, annotation_file) relpath = os.path.relpath(path, annotations_root) img_path = os.path.join( images_root, os.path.splitext(relpath)[0] + '.' + IMAGES_EXT ) assert os.path.isfile(img_path), \ RuntimeError("File <> doesn't exist".format(img_path)) yield img_path, path
Из этого можно уже сделать датасет и запустить «процессинг на графе»,
например извлекать имена файлов из датасета.
Создаем датасет:
files_dataset = tf.data.Dataset.from_generator( functools.partial(image_annotation_iterator, "./ILSVRC"), output_types=(tf.string, tf.string), output_shapes=(tf.TensorShape([]), tf.TensorShape([])) )
Для извлечения данных из датасета нам нужен итератор
make_one_shot_iterator создаст итератор, который проходит по
данным один раз. Iterator.get_next() создает тензор в вкоторый загружаются
данне из итератора.
iterator = files_dataset.make_one_shot_iterator() next_elem = iterator.get_next()
Теперь можно создать сессию и ее «вычислить значения» тензора:
with tf.Session() as sess: for i in range(10): element = sess.run(next_elem) print i, element
Но нам для использования в нейронных сетях нужны не имена файлов, а изображения в виде «трехслойных» матриц одинаковой формы и категории этих изображений в виде «one hot»-вектора
Кодируем категории изображений
Разбор файлов аннотаций, не очень инетересен сам по себе. Я использовал для этого пакет BeautifulSoup. Вспомогательный класс Annotation умеет инициализироваться из пути к файлу и хранить список объектов. Для начала нам надо собрать список категорий, чтобы знать размер вектора для кодирования cat_max . А так же сделать отображение строковых категорий в номер из [0..cat_max] . Создание таких отображений тоже не очень интересно, дальше будем считать что словари cat2id и id2cat содержат прямое и обратное отображение описанное выше.
Функция преобразования имени файла в закодированный векто категорий.
Видно что добавляется еще одна категория, для фона: на некоторых изображений не отмечен ни один объект.
def ann_file2one_hot(ann_file): annotation = reader.Annotation("unused", ann_file) category = annotation.main_object().cls result = np.zeros(len(cat2id) + 1) result[cat2id.get(category, len(cat2id))] = 1 return result
Применим преобразование к датасету:
dataset = file_dataset.map( lambda img_file_tensor, ann_file_tensor: (img_file_tensor, tf.py_func(ann_file2one_hot, [ann_file_tensor], tf.float64)) )
Метод map возвращает новый датасет, в котором к каждомй строчке изначального датасета применена функция. Функция на самом деле не применяется, пока мы не начали итерироваться по результирующему датасету.
Так же можно заметить, что мы завернули нашу функцию в tf.py_func нужно это т.к. в качестве параметров в функцию преобразования попадают тензоры, а не те значения, которые в них лежат.
И чтобы работать со строками нужна эта обёртка.
Загружаем изображение
В Tensorflow есть богатая библиотека для работы с изображениями. Воспользуемся ею для их загрузки. Нам нужно: прочитать файл, декодировать его в матрицу, привести матрицу к стандартному размеру (например среднему), нормализовать значения в этой матрице.
def image_parser(file_name): image_data = tf.read_file(file_name) image_parsed = tf.image.decode_jpeg(image_data, channels=3) image_parsed = tf.image.resize_image_with_crop_or_pad(image_parsed, 482, 415) image_parsed = tf.cast(image_parsed, dtype=tf.float16) image_parsed = tf.image.per_image_standardization(image_parsed) return image_parsed
В отличие от предыдущей функции, здесь file_name это тензор, а значит нам не надо эту функцию заворачивать, добавим ее в предыдущий сниппет:
dataset = file_dataset.map( lambda img_file_tensor, ann_file_tensor: ( image_parser(img_file_tensor), tf.py_func(ann_file2one_hot, [ann_file_tensor], tf.float64) ) )
Проверим что наш граф вычеслений призводит что-то осмысленное:
iterator = dataset.make_one_shot_iterator() next_elem = iterator.get_next() print type(next_elem[0]) with tf.Session() as sess: for i in range(3): element = sess.run(next_elem) print i, element[0].shape, element[1].shape
0 (482, 415, 3) (201,) 1 (482, 415, 3) (201,) 2 (482, 415, 3) (201,)
Как правило в самом начале следовало бы разделить датасет на 2 или 3 части для тренировки/валидации/тестирования. Мы же воспользуемся разделением на тренировочный и валидационный датасет из скачанного архива.
Конструирование графа вычислений
Мы будем треннировать сверточную нейронную сеть (англ. convolutional neural netwrok, CNN) методом похожим на стохастический градиентный спуск, но будем использовать улучшенную его версию Adam. Для этого нам надо объединить наши экземпляры в «пакеты» (англ. batch). Кроме того чтобы утилизировать многопроцессорность (а в лучшем случае наличие GPU для обучения) можно включить фоновую подкачку данных
BATCH_SIZE = 16 dataset = dataset.batch(BATCH_SIZE) dataset = dataset.prefetch(2)
Будем объдинять в пакеты по BATCH_SIZE экземпляров и подкачивать 2 таких пакета.
В ходе обучения мы хотим переодически прогонять валидацию, на выборке, которая не участвует в обучении. А значит нам надо повторить все манипуляции выше для еще одного датасета.
К счастью всех их можно объединить в функцию например dataset_from_file_iterator и создать два датасета:
train_dataset = dataset_from_file_iterator( functools.partial(image_annotation_iterator, "./ILSVRC", subset="train"), cat2id, BATCH_SIZE ) valid_dataset = . # то же самое только subset="val"
Но так как мы хотим дальше использовать один и тот же граф вычислений для тренировки и валидации, мы создадим более гибкий итератор. Такой который позволяет его переинициализировать.
iterator = tf.data.Iterator.from_structure( train_dataset.output_types, train_dataset.output_shapes ) train_initializer_op = iterator.make_initializer(train_dataset) valid_initializer_op = iterator.make_initializer(valid_dataset)
Позже «выполнив» ту или иную операцию мы сможем переключать итератор с одного датасета на
другой.
with tf.Session(config=config, graph=graph) as sess: sess.run(train_initialize_op) # Треннируем # . sess.run(valid_initialize_op) # валидируем # .
Для тепреь нам нужно описать нашу нейронную сеть, но не будем углубляться в этот вопрос.
Будем считать что функция semi_alex_net_v1(mages_batch, num_labels) строит нужную архитекутуру и возвращает тензор с выходными значениями, предсказанными нейронной сетью.
Зададим функцию ошибки, и тончности, операцию оптимизации:
img_batch, label_batch = iterator.get_next() logits = semi_alexnet_v1.semi_alexnet_v1(img_batch, len(cat2id)) loss = tf.losses.softmax_cross_entropy( logits=logits, onehot_labels=label_batch) labels = tf.argmax(label_batch, axis=1) predictions = tf.argmax(logits, axis=1) correct_predictions = tf.reduce_sum(tf.to_float(tf.equal(labels, predictions))) optimizer = tf.train.AdamOptimizer().minimize(loss)
Цикл тренировки и валидации
Теперь можно приступить к обучению:
with tf.Session() as sess: sess.run(tf.local_variables_initializer()) sess.run(tf.global_variables_initializer()) sess.run(train_initializer_op) counter = tqdm() total = 0. correct = 0. try: while True: opt, l, correct_batch = sess.run([optimizer, loss, correct_predictions]) total += BATCH_SIZE correct += correct_batch counter.set_postfix(< "loss": "".format(l), "accuracy": correct/total >) counter.update(BATCH_SIZE) except tf.errors.OutOfRangeError: print "Finished training"
Выше мы создаем сессию, инициализируем глобальные и локальные переменные в графе, инициализируем итератор тренировочными данными. [tqdm][tgdm] не относится к процессу обучения, это просто удобный инструмент визуализации прогресса.
В контексте той же сессии запускаем и валидацию: цикл валидации выглядит очень похоже. Основная разница: не запускается операция оптимизации.
with tf.Session() as sess: # Train # . # Validate counter = tqdm() sess.run(valid_initializer_op) total = 0. correct = 0. try: while True: l, correct_batch = sess.run([loss, correct_predictions]) total += BATCH_SIZE correct += correct_batch counter.set_postfix(< "loss": "".format(l), "valid accuracy": correct/total >) counter.update(BATCH_SIZE) except tf.errors.OutOfRangeError: print "Finished validation"
Эпохи и чекпойнты
Одного простого прохода по всем изображениям конечно же не достаточно для тренировки. И нужно код тренировки и валидации выше выполнять в цикле (внутри одной сессии).
Выполнять либо фиксированное число итераций, либо пока обучение помогает. Один проход по всему набору данных традиционно называется эпохой (англ. epoch).
На случай непредвиденных остановок обучения и для дальнейшего использования модели, нужно ее сохранять. Для этого при создании графа выполнения нужно создать объект класса Saver . А в ходе тренировки сохранять состояние модели.
# создаем граф # . saver = tf.train.Saver() # Создаем сессию with tf.Session() as sess: for i in range(EPOCHS): # Train # . # Validate # . saver.save(sess, "checkpoint/name")
Что дальше
Мы научились создавать датасеты, преобразовывать их с использованием функций работы с
тензорами, а так же обычными функциями написанными на питоне. Научились загружать изображения в фоновом цикле не пытаясь загрузить их в память или сохранить в разжатом виде. Так же научились сохранять обученную модель.
Применив часть шагов из описанных выше и загрузив ее можно сделать программу, которая будет распознавать изображения.
В статье совершенно не раскрывается тем нейронных сетей как таковых, их архитектуре и методов обучение. Для тех кто хочет в этом разобраться могу порекомендовать курс Deep Learning by Google на Udacity, он подойдет в том числе и совсем новичкам, без серьёзного бэкграунда. Про применение сверточных нейронных сетей для распознавания есть отличный курс лекций Convolutional Neural Networks for Visual Recognition от Стэнфордского университета. Так же стоит посмотреть на курсы специализации courseraning coursera на Сoursera. Так же есть довольно много материалов на Хабрахабр, например, неплохой обзор библиотеки Tensorflow от Open Data Science.
UPD: Скрипт и вспомогательные библиотеки доступны на Github
Конвертация нейросети из PyTorch в Tensorflow
Этим летом я взялся разгребать свою изрядно захламленную папку с сохранёнными отовсюду картинками, и естественно это сорочье гнездо хотелось как-то упорядочить, потому что я привык общаться в чатах и на форумах, прилагая аттач с соответствующей реакцией. Когда картинок накопилось достаточно много, копаться в архиве стало уже довольно сложно, так что я начал описывать свою реакцию уже выбирая случайную картинку. Поэтому я решил исследовать, какие вообще сейчас представлены способы индексации картинок.
- Самый простой способ — обращаться к внешнему банку данных типа text2image, например к Google Images. Конечно, из минусов — это удаление приглянувшегося файла с сайта, где он хранился.
- Отсортировать картинки по папкам. Это привязывает к иерархической структуре хранения и заставляет сортировать все картинки вручную, что при большой коллекции сделать переход довольно болезненным. Кроме того, некоторые картинки можно отнести к нескольким категориям.
- Маркировать картинки тегами.
- Теги можно хранить непосредственно внутри файла в EXIF-секции, и для них даже будет доступен поиск Проводником. Некоторое время я маркировал коллекцию таким образом, затем упёрся в предел её эффективности, т.к. не все виды файлов поддерживают EXIF.
- Теги можно найти во внешних хранилищах (если картинка там есть). Арт-галереи, как правило, дают авторам прикреплять к своим рисункам некоторое количество тегов; движки обратного поиска изображений типа IQDB позволяют добраться до страницы рисунка при имеющемся превью. Минус, конечно, в пропускной способности — разметка всей коллекции при миграции на новую систему займёт некоторое время (у меня заняло три дня с учётом того, что я выставил кулдауны обращения к этим сервисам, чтобы не нагружать их слишком сильно и не нарушать их policy). При массивных коллекциях на десятки тысяч изображений маркировка тегами может растянуться на недели и месяцы.
- Взять теги из расшаренной коллекции, которая блуждает по сети пользователей HydrusNetwork. Возможно, для меня это было бы правильным решением, т.к. сейчас это наиболее массивная любительская экосистема для любителей каталогизации картинок, которую я нашёл.
- Можно найти нейросеть-классификатор, которая проставит теги без обращения к другим источникам. Это решение ограничено только вычислительными мощностями, которыми располагает хозяин коллекции.
Итак, наиболее любопытным оказался способ 3.4 — разметка коллекции нейросетью.
Выбор нейросети
При поиске я руководствовался простыми критериями: ленью и любопытством. Ленивая предпосылка означала, что я не стану собирать терабайты картинок (или хотя бы грузить их с Kaggle) и тренировать сетку неделями, чтобы потом размечать коллекцию на несколько гигабайтов. Кроме того, избыточное погружение в не очень хорошо знакомую мне тему нейронных сетей могло погасить мой интерес к пет-проекту. Любопытная предпосылка диктовала подыскать наиболее зрелищное решение из существующих. Поэтому я искал варианты, где кто-то щедрый уже не только разметил датасет, но и выложил pretrained модель.
Итак, я нашёл как минимум два крупных проекта разметки изображений: более академический ImageNet и более хаотический Danbooru (который заблокирован в РФ по известным причинам). В одном больше фотографий, в другом больше рисованных картинок — в зависимости от характера коллекции мог быть ближе тот или иной датасет. На их основе существуют нейросети-классификаторы, у которых можно без проблем найти файлы весов. Вот только не все они выложены в формате, который подходит для проекта.
Изначально я писал быстрый прототип на Electron. Внутрь постепенно утаптывались различные модули типа распознавания текста, ui-фреймворка, так что добавление нейросетки в бандл десктопного приложения было в уже ограниченных условиях, и варианты вырисовывались примерно такие:
- Можно эксплуатировать оригинальное решение на популярном фреймворке PyTorch, завернуть его в docker-модуль или в веб-сервер, чтобы оно функционировало в своей естественной среде обитания и время от времени глубокомысленно сообщало наружу, что обнаружило на картинке с лисой кошку, инфа 67%! Это не требовало переписывать на Javascript ничего, но добавляло лишние сущности, которые мне было лень обслуживать.
- Можно взять решение на более универсальном фреймворке Tensorflow, построить вокруг него обвязку на TensorflowJS (адаптации под Javascript), и этот вариант даже будет способен читать точку сохранения модели из protobuf-файла, куда она сгружена.
Немного поискав, я обнаружил отличный на первый взгляд вариант:
- взять *.pth файл от модели на PyTorch
- прогреть его тестовой картинкой
- сконвертировать в файл ONNX — специальный промежуточный формат конвертации между разными фреймворками нейросетей.
- сконвертировать в protobuf формат, который есть у Tensorflow.
- написать обвязку, которая примерно воспроизводит обработку картинки до и после скармливания оригинальной нейросети.
Самым сложным пунктом оказался последний.
Препроцессинг изображений в PyTorch
Итак, если опустить детали, есть код под PyTorch и FastAI в духе
from fastbook import * from pandas import DataFrame, read_csv from fastai.callback.progress import ProgressCallback from os import listdir from os.path import isfile, join model_path = "models/model.pth" # тут лежит интересующий файл весов data_path = "test/tags.csv.gz" tags_path = "data/tags.json" df = read_csv(data_path) vocab = json.load(open(tags_path)) threshold = 0.01 limit = 50 bs = 64 dirpath = '../dataset-for-tagging/original2/' dblock = DataBlock( blocks=(ImageBlock, MultiCategoryBlock(vocab=vocab)), get_x=lambda df: Path("test") / df["filename"], get_y=lambda df: df["tags"].split(" "), item_tfms=Resize(224, method=ResizeMethod.Squish), batch_tfms=[RandomErasing()] ) dls = dblock.dataloaders(df) learn = vision_learner(dls, "resnet152", pretrained=False) model_file = open(model_path, "rb") learn.load(model_file, with_opt=False) learn.remove_cb(ProgressCallback) filepaths = [dirpath+f for f in listdir(dirpath) if isfile(join(dirpath, f))] tags = <> for filepath in filepaths: dl = learn.dls.test_dl([PILImage.create(filepath)], bs=bs) batch, _ = learn.get_preds(dl=dl) # тут нейросеть делает магию предсказаний for scores in batch: df = DataFrame() df = df[df.score >= threshold].sort_values( "score", ascending=False).head(limit) tags[filepath] = dict(zip(df.tag, df.score)) print(tags)
Из него можно сделать выводы, что нейросеть имеет архитектуру ResNet152, что картинка и что значимая информация — это порядок значений выходных узлов, которые соответствуют тегам. Классификатор берёт самые значимые узлы выходного слоя нейронной сети, сортирует их по значениям и берёт первые 50.
Не идеально, но и не совсем плохо. Поскольку в исходном датасете было меньше 1000 изображений с этим персонажем — её тег не попал в выборку; а некоторые теги включены в выдачу по ошибке. Но в остальном качество меня устраивало до некоторой степени, поэтому после некоторого размышления я решил включить нейросеть внутрь основного проекта как ассистента: не доверять ей расстановку всех тегов (чтобы не замусорить разметку), а подсказывать по востребованию список тегов, чтобы выбрать из них наиболее подходящие. К сожалению, для этого подхода надо уже знать, что и кто изображены на картинке.
Сконвертировав файл весов нейросети и немного потыкав веточкой в него с разных сторон, опытным путём можно выяснить, что он находится в режиме serve , что на вход input.1 он принимает тензор с размерностью [1, 3, 224, 224] , выход у него называется ret.11 , а внутри у него чёрный ящик. Возможно, при экспорте можно обозвать входы и выходы как-нибудь покрасивее взамен сгенерированных автоматически, но в данном случае это особой роли не играет.
Проделаем аналогичный препроцессинг картинки внутри Tensorflow.
PyTorch традиционно принимает на вход цветовые каналы в другом порядке, чем TensorFlow, поэтому мне понадобилось преобразовать тензор, иначе код в принципе не запускался без ошибки. Почему так? Я слышал, PyTorch больше ориентирован на вычисления на видеокарте по сравнению с Tensorflow, а такая конфигурация входного тензора экономит копеечку скорости.
Также исходная нейросеть была натренирована на пакетный приём изображений, поэтому понадобится увеличить размерность тензора, положив таким образом картинку в пакет, где она будет одна. В TensorflowJS для этого есть, например, метод expandDims .
tensor = tensor.expandDims().transpose([0, 3, 1, 2]);
const tf = require('@tensorflow/tfjs-node'); const fs = require('fs'); const path = require('path'); const tags = require('../pytorch-autotagger/data/tags.json'); const LIMIT = 50; const PREPARED_IMAGE_SIZE = 224; const MAX_COLOR_VALUE = 255; async function getSortedTags(filepath) < let model = await tf.node.loadSavedModel( './model/danbooru/', ['serve'], 'serving_default' ); return tf.tidy(() =>< let tensor = tf.node .decodeImage(fs.readFileSync(filepath), 3) .resizeBilinear([PREPARED_IMAGE_SIZE, PREPARED_IMAGE_SIZE]); tensor = tensor.expandDims().transpose([0, 3, 1, 2]); // move color channel to 2nd place let scores = model.predict(< 'input.1': tensor >)['ret.11']; let scoredTags = []; scores = scores.dataSync(); for (let i in scores) < scoredTags.push(< score: scores[i], tag: tags[i] >); > let sortedScoredTags = scoredTags.sort((a, b) => a.score - b.score); return sortedScoredTags .slice(sortedScoredTags.length - LIMIT, sortedScoredTags.length) .reverse(); >); > /** * @param dirPath * @returns
*/ function flattyReadDir(dirPath) < return fs .readdirSync(dirPath, < withFileTypes: true >) .filter((f) => f.isFile()) .map((f) => path.join(dirPath, f.name)); > (async () => < let result = <>; let filepaths = flattyReadDir('../dataset-for-tagging/original/'); for (let i = 0; i < filepaths.length; i++) < let filepath = filepaths[i]; result[filepath] = (await getSortedTags(filepath)).reduce( (accum, current) =>< accum[current['tag']] = current['score']; return accum; >, <> ); > console.log(result); >)(); И получим пшик. Вместо вероятностей тегов от нуля до единицы на выходе плавают трёхзначные числа. Порядок был каким угодно, но определённо не тем, который был в оригинале.
Немного поискав и поспрашивав, я выяснил что нужно поделить значения цветовых каналов на 255 (чтобы они укладывались в промежуток [0, 1] и вычисления несильно раздували выходные значения).
const MAX_COLOR_VALUE = 255; // . tensor = tensor.div(MAX_COLOR_VALUE);
Для сравнения результатов с образцовым результатом от PyTorch я написал простой скрипт, который брал два выходных json-файла после эксперимента и считал количество тегов, которые совпадали у файлов, а затем считал итоговый процент совпадений на всей тестовой мини-выборке картинок. Функция расстояния, таким образом, выглядела примерно таким образом:
// . function hammingDistanceBetweenSets(list1, list2) < let count = 0; for (let i = 0; i < list1.length; i++) < if (!list2.includes(list1[i])) < count = count + 1; >> return count; > // .
$ node compare-json-files.js tensorflow-1 pytorch-autotagger 82% 800px-Mural_in_Heidelberg.jpg 72% 800px-Neko_Wikipe-tan.png 66% 800px-Wikipe-tan_dressed_in_a_Halloween_costume.png 72% 800px-Wikipe-tan_mopping.png 86% 800px-Пасущаяся_лошадь_и_грозовое_небо.jpg 80% Dierentegel,_vliegende_vogel,_hoekmotief_ossenkop,_objectnr_17872.jpg 84% Tiere_an_einem_Hang_des_Fahrentalsgrabens_2.jpg 92% Utzenaich_(Gemeindeamt).jpg 64% Wiki-sisters.png 72% Wikipe-tan_donations.png 76% Wikipe-tan_the_Library_of_Babel.png ---- 76.9090909090909% total $ node compare-json-files.js tensorflow-2 pytorch-autotagger 18% 800px-Mural_in_Heidelberg.jpg 8% 800px-Neko_Wikipe-tan.png 18% 800px-Wikipe-tan_dressed_in_a_Halloween_costume.png 24% 800px-Wikipe-tan_mopping.png 6% 800px-Пасущаяся_лошадь_и_грозовое_небо.jpg 14% Dierentegel,_vliegende_vogel,_hoekmotief_ossenkop,_objectnr_17872.jpg 22% Tiere_an_einem_Hang_des_Fahrentalsgrabens_2.jpg 10% Utzenaich_(Gemeindeamt).jpg 8% Wiki-sisters.png 26% Wikipe-tan_donations.png 22% Wikipe-tan_the_Library_of_Babel.png ---- 16% total
Таким образом, есть метрика, благодаря которой исследование будет не окончательно слепым.
Первый подход хуже второго, но и второй даёт искажение результатов.
Переворошив гугл-выдачу, я построил следующие гипотезы:
1) Нужно не только нормализовать значения цветовых каналов как в PyTorch, нужно сместить цветовое пространство, как иногда оно используется в PyTorch. Это предлагается не во всех моделях обучения.
tensor = tensor .div(MAX_COLOR_VALUE) .sub([0.485, 0.456, 0.406]) .div([0.229, 0.224, 0.225]);
Результат: оценка отклонений ухудшилась до 69.1%.
2) Надо добавить для выравнивания на вход softmax , уж это точно не испортит дело!
tensor = tensor .div(MAX_COLOR_VALUE) .softmax();
Результат: оценка отклонений ухудшилась до 44.7%.
3) Я перепутал местами цветовые каналы и они лежат как-то иначе:
tensor = tensor.expandDims().transpose([0, 3, 2, 1]);
Оценка отклонений ухудшена до 30.5%
4) Искажений вносит ресайз картинки до квадрата 224х224, который по-разному устроен в PyTorch и в Tensorflow. Я подготовил вспомогательный датасет из картинок, заранее сжатых до 224х224, после чего опробовал на них лучшие найденные алгоритмы из обоих фреймворков нейросетей.
$ node compare-json-files.js tensorflow-2-squared-224 pytorch-autotagger-squared224 2% 800px-Mural_in_Heidelberg.jpg . 0% Wikipe-tan_the_Library_of_Babel.png ---- 0.54% total
И это очень хороший результат!
К сожалению, он так же хорош, как и бесполезен.
Я визуализировал процесс подготовки картинки в обоих фреймворках.
for filename in filenames: image = PILImage.create( dirpath_for_original + filename) rsz = Resize(224, method=ResizeMethod.Squish) rsz(image, split_idx=0).save(dirpath_for_squared + filename)
let tensor = tf.node .decodeImage(fs.readFileSync(filepath), 3) .resizeBilinear([PREPARED_IMAGE_SIZE, PREPARED_IMAGE_SIZE]); tf.node.encodeJpeg(tensor, 'rgb').then((image) => < fs.writeFileSync( newFilepath, Buffer.from(image) );
У Tensorflow обнаружились некоторые проблемы с антиалиасингом, поэтому я попробовал использовать библиотеку sharp. Как ни странно, в исходном коде на PyTorch используется по умолчанию билинейная интерполяция (по крайней мере я так предположил из чтения исходников), однако она дала лучшее сглаживание краёв, чем Tensorflow.
const PREPARED_IMAGE_SIZE = 224 // . let sharpedImage = sharp(filepath); sharpedImage = sharpedImage.resize(PREPARED_IMAGE_SIZE, PREPARED_IMAGE_SIZE, < fit: sharp.fit.cover, kernel: sharp.kernel.mitchell, >); let buffer = await sharpedImage.toBuffer(); let tensor = tf.node.decodeImage(buffer, 3); tf.node.encodeJpeg(tensor, 'rgb').then((image) => < fs.writeFileSync( newFilepath, Buffer.from(image) ); >);
Библиотека Sharp действительно решала проблемы с зубчатыми краями, но добавляла другую проблему: по какой-то причине она иначе интерпретировала альфа-канал прозрачности, и это вносило собственные искажения. Эта библиотека предлагает 5 способов интерполяции при изменении размеров, и перебором мне удалось снизить процент ошибок с 16% до 13% на первоначальной тестовой выборке ( kernel: sharp.kernel.mitchell ).
Впрочем, на более крупном датасете один из самых первых алгоритмов препроцессинга картинки для TensorflowJS оказался и самым близким по точности к тому, что выдаёт нейросеть на PyTorch.
Постпроцессинг
Поскольку я хотел сохранить прежнее впечатление от результата выполнения "Это charactername, инфа сотка!", то результирующие значения приглаживаются: нормализуются внутрь диапазона [0, 1] и затем при рендеринге результата переводятся в проценты. Это не влияет на порядок выходных узлов (т. к. преобразование линейно и его множитель положителен), поэтому выдача тегов не искажается этим действием.
При использовании решения на PyTorch можно было бы обойтись без этого шага.
let scores = model.predict(< 'input.1': tensor >)['ret.11']; scores = scores .sub(tf.min(scores).dataSync()[0]) .div(tf.max(scores).dataSync()[0] - tf.min(scores).dataSync()[0]);
Выводы
Несмотря на различия между фреймворками нейросетей, вы действительно можете конвертировать файлы весов между ними благодаря проекту-посреднику ONNX, чтобы подобрать архитектуру на одном фреймворке, а выложить в продакшн на другом. Вы даже сможете сменить язык программирования для работы с этой нейросетью. Но к сожалению, различие инструментов исказит точность первоначального результата, если вы не сможете точно воспроизвести алгоритм препроцессинга входных данных.
Ссылки
- https://www.tensorflow.org
- https://pytorch.org/
- https://www.fast.ai
- https://github.com/danbooru/autotagger - найденный репозиторий с исходной моделью под PyTorch.
- https://github.com/tauraloke/tegownitsa - менеджер тегов с адаптированной моделью под TensorflowJS.
- https://github.com/tauraloke/autotagger-paper-listings - листинги экспериментов, которые были показаны в статье.
Загрузка и преобразование изображений в PyTorch
После импорта всех необходимых библиотек и добавления VGG-19 на наше устройство, мы должны загрузить в память изображения, которые мы хотим преобразовать для переноса стиля в PyTorch.
У нас есть изображение контента, изображение стиля и целевое изображение, которые будут комбинацией этих изображений. Не все изображения должны иметь одинаковый размер или пиксели. Чтобы сделать изображения одинаковыми, мы применим процесс их преобразования.
Загрузка изображения
Мы должны загрузить изображение содержимого и изображение стиля в память, чтобы мы могли выполнить над ним операцию. Процесс загрузки играет важную роль в процессе передачи стиля. Нам нужны изображения в памяти, и процесс передачи стиля будет невозможен до процесса загрузки.
#defining a method with three parameters i.e. image location, maximum size and shape def load_image(img_path,max_size=400,shape=None): # Open the image, convert it into RGB and store in a variable image=Image.open(img_path).convert('RGB') # comparing image size with the maximum size if max(image.size)>max_size: size=max_size else: size=max(image.size) # checking for the image shape if shape is not None: size=shape #Applying appropriate transformation to our image such as Resize, ToTensor and Normalization in_transform=transforms.Compose([ transforms.Resize(size), transforms.ToTensor(), transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))]) #Calling in_transform with our image image=in_transform(image).unsqueeze(0) #unsqueeze(0) is used to add extra layer of dimensionality to the image #Returning image return image #Calling load_image() with our image and add it to our device content=load_image('ab.jpg').to(device) style=load_image('abc.jpg',shape=content.shape[-2:]).to(device)
Преобразование изображения
Перед импортом изображений нам нужно их преобразовать из тензора в изображения numpy, чтобы обеспечить совместимость с пакетом plot. Мы делали это раньше с помощью уже знакомой вспомогательной функции image_converts, которую мы использовали в преобразованиях изображений для распознавания.
def im_convert(tensor): image=tensor.cpu().clone().detach().numpy() image=image.transpose(1,2,0) image=image*np.array((0.5,0.5,0.5))+np.array((0.5,0.5,0.5)) image=image.clip(0,1) return image
Если мы запустим этот вспомогательный метод, он сгенерирует ошибку. Мы должны удалить одномерные записи из формы изображения и из формы массива. Таким образом, мы сожмем наше изображение перед методом транспонирования.
image=image.squeeze()
Построение изображений
fig,(ax1,ax2)=plt.subplots(1,2,figsize=(20,10)) ax1.imshow(im_convert(content)) ax1.axis('off') ax2.imshow(im_convert(style)) ax2.axis('off')
Когда мы запустим его в блокноте Google Colab, он даст нам ожидаемый результат:
Как импортировать (загрузить) изображение в python?
Примеры того, как импортировать (загрузить) изображение в python:
Импорт изображения с помощью matplotlib
Чтобы импортировать изображение в python, одним из решений является использование matplotlib:
Code language: JavaScript (javascript)from matplotlib import image from matplotlib import pyplot as plt img = image.imread("fav.jpg")
Code language: PHP (php)print(type(img)) print(img.shape)
Code language: HTML, XML (xml)class 'numpy.ndarray'>
(300, 450, 3)
3 соответствует режиму RGB.
После этого можно вывести изображение с помощью imshow из matplotlib в виде графика.
Code language: CSS (css)plt.imshow(img) plt.show()
Импорт изображения с помощью Pillow
Другое решение – использовать Pillow
Code language: JavaScript (javascript)from PIL import Image from matplotlib import pyplot as plt img= Image.open("fav.jpg")
type(img)
не является массивом numpy:
Code language: CSS (css)PIL.JpegImagePlugin.JpegImageFile
Тем не менее, все еще можно вывести изображение на экран с помощью imshow
Code language: CSS (css)plt.imshow(img) plt.show()
Для преобразования img в матрицу numpy
Code language: JavaScript (javascript)import numpy as np img = np.asarray(img)