Этим летом я взялся разгребать свою изрядно захламленную папку с сохранёнными отовсюду картинками, и естественно это сорочье гнездо хотелось как-то упорядочить, потому что я привык общаться в чатах и на форумах, прилагая аттач с соответствующей реакцией. Когда картинок накопилось достаточно много, копаться в архиве стало уже довольно сложно, так что я начал описывать свою реакцию уже выбирая случайную картинку. Поэтому я решил исследовать, какие вообще сейчас представлены способы индексации картинок. Самый простой способ - обращаться к внешнему банку данных типа text2image, например к Google Images. Конечно, из минусов - это удаление приглянувшегося файла с сайта, где он хранился. Отсортировать картинки по папкам. Это привязывает к иерархической структуре хранения и заставляет сортировать все картинки вручную, что при большой коллекции сделать переход довольно болезненным. Кроме того, некоторые картинки можно отнести к нескольким категориям. Маркировать картинки тегами. Теги можно хранить непосредственно внутри файла в EXIF-секции, и для них даже будет доступен поиск Проводником. Некоторое время я маркировал коллекцию таким образом, затем упёрся в предел её эффективности, т.к. не все виды файлов поддерживают EXIF. Теги можно найти во внешних хранилищах (если картинка там есть). Арт-галереи, как правило, дают авторам прикреплять к своим рисункам некоторое количество тегов; движки обратного поиска изображений типа IQDB позволяют добраться до страницы рисунка при имеющемся превью. Минус, конечно, в пропускной способности - разметка всей коллекции при миграции на новую систему займёт некоторое время (у меня заняло три дня с учётом того, что я выставил кулдауны обращения к этим сервисам, чтобы не нагружать их слишком сильно и не нарушать их policy). При массивных коллекциях на десятки тысяч изображений маркировка тегами может растянуться на недели и месяцы. Взять теги из расшаренной коллекции, которая блуждает по сети пользователей HydrusNetwork. Возможно, для меня это было бы правильным решением, т.к. сейчас это наиболее массивная любительская экосистема для любителей каталогизации картинок, которую я нашёл. Можно найти нейросеть-классификатор, которая проставит теги без обращения к другим источникам. Это решение ограничено только вычислительными мощностями, которыми располагает хозяин коллекции. Искать по автоматически создаваемым индексам типа perceptive hash. Они хороши для построение поисковика картинок, но внутри немаркированной коллекции с их помощью можно разве что найти дубликаты для последующего удаления. Распознавать текст на картинке и искать по нему. Актуально для демотиваторов и всего в этом духе. Взять для индекса текст от описательные нейросети, строящие фразу на естественном языке, описывающую происходящее на картинке. Пример - BLIP. Пока это довольно любопытно, но пока я не придумал, как это полноценно использовать для поиска. Итак, наиболее любопытным оказался способ 3.4 - разметка коллекции нейросетью. При поиске я руководствовался простыми критериями: ленью и любопытством. Ленивая предпосылка означала, что я не стану собирать терабайты картинок (или хотя бы грузить их с Kaggle) и тренировать сетку неделями, чтобы потом размечать коллекцию на несколько гигабайтов. Кроме того, избыточное погружение в не очень хорошо знакомую мне тему нейронных сетей могло погасить мой интерес к пет-проекту. Любопытная предпосылка диктовала подыскать наиболее зрелищное решение из существующих. Поэтому я искал варианты, где кто-то щедрый уже не только разметил датасет, но и выложил pretrained модель. Итак, я нашёл как минимум два крупных проекта разметки изображений: более академический ImageNet и более хаотический Danbooru (который заблокирован в РФ по известным причинам). В одном больше фотографий, в другом больше рисованных картинок - в зависимости от характера коллекции мог быть ближе тот или иной датасет. На их основе существуют нейросети-классификаторы, у которых можно без проблем найти файлы весов... Вот только не все они выложены в формате, который подходит для проекта. Изначально я писал быстрый прототип на Electron. Внутрь постепенно утаптывались различные модули типа распознавания текста, ui-фреймворка, так что добавление нейросетки в бандл десктопного приложения было в уже ограниченных условиях, и варианты вырисовывались примерно такие: Можно эксплуатировать оригинальное решение на популярном фреймворке PyTorch, завернуть его в docker-модуль или в веб-сервер, чтобы оно функционировало в своей естественной среде обитания и время от времени глубокомысленно сообщало наружу, что обнаружило на картинке с лисой кошку, инфа 67%! Это не требовало переписывать на Javascript ничего, но добавляло лишние сущности, которые мне было лень обслуживать. Можно взять решение на более универсальном фреймворке Tensorflow, построить вокруг него обвязку на TensorflowJS (адаптации под Javascript), и этот вариант даже будет способен читать точку сохранения модели из protobuf-файла, куда она сгружена. Немного поискав, я обнаружил отличный на первый взгляд вариант:  взять *.pth файл от модели на PyTorch прогреть его тестовой картинкой сконвертировать в файл ONNX - специальный промежуточный формат конвертации между разными фреймворками нейросетей. сконвертировать в protobuf формат, который есть у Tensorflow. написать обвязку, которая примерно воспроизводит обработку картинки до и после скармливания оригинальной нейросети. Самым сложным пунктом оказался последний. Итак, если опустить детали, есть код под PyTorch и FastAI в духе Из него можно сделать выводы, что нейросеть имеет архитектуру ResNet152, что картинка и что значимая информация - это порядок значений выходных узлов, которые соответствуют тегам. Классификатор берёт самые значимые узлы выходного слоя нейронной сети, сортирует их по значениям и берёт первые 50. Не идеально, но и не совсем плохо. Поскольку в исходном датасете было меньше 1000 изображений с этим персонажем - её тег не попал в выборку; а некоторые теги включены в выдачу по ошибке. Но в остальном качество меня устраивало до некоторой степени, поэтому после некоторого размышления я решил включить нейросеть внутрь основного проекта как ассистента: не доверять ей расстановку всех тегов (чтобы не замусорить разметку), а подсказывать по востребованию список тегов, чтобы выбрать из них наиболее подходящие. К сожалению, для этого подхода надо уже знать, что и кто изображены на картинке. Сконвертировав файл весов нейросети и немного потыкав веточкой в него с разных сторон, опытным путём можно выяснить, что он находится в режиме serve, что на вход input.1 он принимает тензор с размерностью , выход у него называется ret.11, а внутри у него чёрный ящик. Возможно, при экспорте можно обозвать входы и выходы как-нибудь покрасивее взамен сгенерированных автоматически, но в данном случае это особой роли не играет. Проделаем аналогичный препроцессинг картинки внутри Tensorflow. PyTorch традиционно принимает на вход цветовые каналы в другом порядке, чем TensorFlow, поэтому мне понадобилось преобразовать тензор, иначе код в принципе не запускался без ошибки. Почему так? Я слышал, PyTorch больше ориентирован на вычисления на видеокарте по сравнению с Tensorflow, а такая конфигурация входного тензора экономит копеечку скорости. Также исходная нейросеть была натренирована на пакетный приём изображений, поэтому понадобится увеличить размерность тензора, положив таким образом картинку в пакет, где она будет одна. В TensorflowJS для этого есть, например, метод expandDims. Итого: И получим пшик. Вместо вероятностей тегов от нуля до единицы на выходе плавают трёхзначные числа. Порядок был каким угодно, но определённо не тем, который был в оригинале. Немного поискав и поспрашивав, я выяснил что нужно поделить значения цветовых каналов на 255 (чтобы они укладывались в промежуток [0, 1] и вычисления несильно раздували выходные значения). Для сравнения результатов с образцовым результатом от PyTorch я написал простой скрипт, который брал два выходных json-файла после эксперимента и считал количество тегов, которые совпадали у файлов, а затем считал итоговый процент совпадений на всей тестовой мини-выборке картинок. Функция расстояния, таким образом, выглядела примерно таким образом: Итого: Таким образом, есть метрика, благодаря которой исследование будет не окончательно слепым. Первый подход хуже второго, но и второй даёт искажение результатов. Переворошив гугл-выдачу, я построил следующие гипотезы: 1) Нужно не только нормализовать значения цветовых каналов как в PyTorch, нужно сместить цветовое пространство, как иногда оно используется в PyTorch. Это предлагается не во всех моделях обучения. Результат: оценка отклонений ухудшилась до 69.1%. 2) Надо добавить для выравнивания на вход softmax, уж это точно не испортит дело! Результат: оценка отклонений ухудшилась до 44.7%. 3) Я перепутал местами цветовые каналы и они лежат как-то иначе:  Оценка отклонений ухудшена до 30.5% 4) Искажений вносит ресайз картинки до квадрата 224х224, который по-разному устроен в PyTorch и в Tensorflow. Я подготовил вспомогательный датасет из картинок, заранее сжатых до 224х224, после чего опробовал на них лучшие найденные алгоритмы из обоих фреймворков нейросетей. И это очень хороший результат! К сожалению, он так же хорош, как и бесполезен. Я визуализировал процесс подготовки картинки в обоих фреймворках. У Tensorflow обнаружились некоторые проблемы с антиалиасингом, поэтому я попробовал использовать библиотеку sharp. Как ни странно, в исходном коде на PyTorch используется по умолчанию билинейная интерполяция (по крайней мере я так предположил из чтения исходников), однако она дала лучшее сглаживание краёв, чем Tensorflow. Библиотека Sharp действительно решала проблемы с зубчатыми краями, но добавляла другую проблему: по какой-то причине она иначе интерпретировала альфа-канал прозрачности, и это вносило собственные искажения. Эта библиотека предлагает 5 способов интерполяции при изменении размеров, и перебором мне удалось снизить процент ошибок с 16% до 13% на первоначальной тестовой выборке (kernel: sharp.kernel.mitchell). Впрочем, на более крупном датасете один из самых первых алгоритмов препроцессинга картинки для TensorflowJS оказался и самым близким по точности к тому, что выдаёт нейросеть на PyTorch. Поскольку я хотел сохранить прежнее впечатление от результата выполнения "Это charactername, инфа сотка!", то результирующие значения приглаживаются: нормализуются внутрь диапазона  и затем при рендеринге результата переводятся в проценты. Это не влияет на порядок выходных узлов (т. к. преобразование линейно и его множитель положителен), поэтому выдача тегов не искажается этим действием. При использовании решения на PyTorch можно было бы обойтись без этого шага. Несмотря на различия между фреймворками нейросетей, вы действительно можете конвертировать файлы весов между ними благодаря проекту-посреднику 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 - листинги экспериментов, которые были показаны в статье.