Поднимаем CRF, пока поиск не сломается

От Brenner Cruvinel • 5 минут чтения

Вопрос прошлой ночи задать было просто, а я всё тянул. Приложение хранит JPEG каждой карты в кэше для показа. .nest не обязан быть красивым, он должен отвечать, какая это карта. Значит, CRF, настройку качества кодека, можно поднимать, пока вектор не сломается, а вектор ломается намного позже глаза. Если так, то стоило бы подумать даже о двух уровнях: миниатюра внутри Nest для поиска и полная карта по запросу, когда приложению нужно её показать.

У меня был способ это измерить, или мне так казалось. У фильтра качества в Nest две стороны: визуальная метрика, сравнивающая декодированный кадр с источником, и дрейф вектора, который показывает, насколько вектор CLIP сдвигается между исходной картинкой и декодированной. Идея дрейфа в том, что картинка может выглядеть нормально и при этом сместиться в пространстве поиска. Я этой метрике доверял. Она была моей, я настоял на ней в плане, и причина была хорошей: на дерматоскопии она поймала случай, когда картинка проходила на глаз, а поиск по клиническому тексту падал вдвое.

Фильтр отбраковал всю лестницу

Я убрал из фильтра визуальную сторону и оставил только дрейф, с порогом, который Nest использует со времён дерматоскопии. Пустил CRF по лестнице от 40 до 60. Фильтр отбраковал всю лестницу, включая первую ступень, и откатился на самую консервативную, потому что идти было некуда. Дрейф уже на CRF 40 был ниже порога и дальше только хуже. А визуальная метрика, которую я убрал из фильтра, но продолжал считать, рассказывала ту же историю с другой стороны: начиная с CRF пятьдесят она на некоторых картах давала отрицательные значения, о чём я даже не знал.

Файл, который выдал фильтр, на CRF 40, самой нижней ступени, получился около гигабайта, в четыре раза меньше источника. Он уже был меньше рекомендованного билда, и фильтр его тоже отбраковал, просто ниже падать было некуда.

По логике фильтра ни один из этих файлов не годился. Я всё равно собрал один на CRF пятьдесят, без фильтра, просто посмотреть на размер. Полгигабайта. В семь с половиной раз меньше источника, а карты превратились в размазню, текст правил не читается, арт узнаётся только издалека. Файл, который я никому бы не показал.

И тогда я запустил метрику, которая на самом деле важна и которую я раньше не запускал, потому что лень было её собирать: поиск по названию. Сто запросов вида "арт такой-то карты" по пяти файлам полного корпуса, от варианта без потерь на 3,6 ГБ до полугигабайтной размазни. Первый прогон дал ноль попаданий на двух файлах из пяти, без потерь и рекомендованном, и на минуту я решил, что сломал поиск. Виноват был бенч: он сопоставлял результат по порядковому номеру карты в потоке, а у двух файлов с сортировкой по похожести другой порядок. В Nest как раз для этого у каждого элемента есть контентный id, и когда бенч стал сопоставлять по нему, все пять файлов снова ответили.

И точность у всех осталась в пределах шума. Полгигабайтный файл находит нужную карту так же хорошо, как файл на 1,3 ГБ, и оба остаются в пределах шума варианта без потерь.

Поиск переживает гораздо больше сжатия, чем глаз

Я какое-то время смотрел на это на экране. Косинусный дрейф меряет, сдвинулся ли вектор. Поиск меряет, находит ли вектор по-прежнему. Я использовал первое так, будто это второе, и оно отбраковало бы работающий файл. Вектор действительно сдвигается и при этом остаётся ближе к нужной карте, чем к любой другой. Расстояние, которое он проходит, не важно, если его соседи не меняются. Фильтру, который годится для такого применения, нужен порог по hit@k, то есть доле запросов, где нужная карта попадает в первые k результатов, а не по косинусу.

Так в Nest появился профиль retrieval-only, который записывает в manifest, что файл предназначен для поиска и никогда для показа. А AVIF, который в бенчмарке вышел примерно на 12 % меньше AV1 при том же качестве, подтвердился на полном корпусе: на 13 % меньше рекомендованного билда. Он кандидат на роль бэкенда по умолчанию для статичных картинок, но я ещё не решил, потому что сборка и встраивание AVIF занимают в четыре-десять раз больше времени.

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

Метрика слабая, и я это знаю. Это сто запросов с моделью, которая на этом корпусе угадывает плохо, CLIP, который смотрит только на арт. "Не просел" значит "я не смог измерить просадку", а не "одинаково". А WeMM, который читает напечатанное на карте название и угадывает девять из десяти, может пострадать от размазни куда сильнее, чем CLIP, потому что он читает ровно то, что CRF уничтожает первым. Я этого не мерил. Это следующий шаг, вместе с полным корпусом перепечаток, где inter есть что предсказывать.

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