Embodied AI как хобби. ЧАСТЬ 1
Как игровой робот учился видеть доску: подход, который поддавался, детектив с Captum, вечно деревянная доска — и почему самое дорогое знание в прикладном ML не гуглится.
Мы с приятелем делаем игрового робота-компаньона для настольных игр. Сейчас он умеет понимать позицию на игровой доске (гомоку, оно же «пять в ряд»), обдумывать ход (самописный аналог AlphaZero) и понимать базовые фразы для сопровождения игры — моделью, которая вместе с эмбеддингами весит меньше 80 мегабайт.

Исходные данные задачи: доска, фигурки, кадр с вебки
Забегая вперёд, расскажу сразу главный тезис статьи:
Самое дорогое знание в прикладном ML — не «как устроены методы»: это расскажет любой курс и любая нейронка. Самое дорогое — где у каждого метода границы применимости и в каких местах он ломается. Дальнейший текст — просто иллюстрация этого тезиса: длинная история неверного подхода, взятого из в целом хорошего курса на Coursera, и короткая успешная после смены подхода.
Авторы:
Юрий Зибрин — автор статьи и соавтор проекта. AI-инженер (RL, CV, LLM Agents, Embodied AI), ведущий системный аналитик, сооснователь solved-by.ml
Антон Петров — соавтор проекта. AI-инженер (CV, LLM Agents), разработчик, тимлид, сооснователь solved-by.ml
Fable 5.0 — редактор текста и советчик, раскопщик миллиона блокнотов. Если вам покажется, что слишком много запятых и длинных тире — все претензии к ней)
Задача и план: Робот играет в гомоку на физической доске так, как многие из нас играли в детстве в клетчатой тетрадке — ставя крестики и нолики в центры клеток (не как в классическом гомоку). И правила взяли попроще: без ограничений на ходы и выигрышные комбинации для первого игрока.
Чтобы сделать ход, робот должен по кадру с камеры понять игровую позицию — и передать её движку, который принимает решение о ходе.
План, как это реализовать, мы придумали почти сразу, и он до сих пор живёт в рабочей версии:
найти углы доски;
найти фигурки;
зная размерность доски, сопоставить одно с другим и получить матрицу с позицией.

Так как готового рецепта найти не удалось, подход пришлось выбирать самому. Выбор для шага с углами подсказал курс Эндрю Ына: там свёрточную сеть-классификатор лёгким движением превращали в детектор координат — «просто замените классификационную голову на регрессионную, и всё заработает. Лосс? Да берите любой» (цитирую по памяти, но небрежную интонацию передаю точно). Забегая вперёд: и «всё заработает», и особенно «любой лосс» оказалось преувеличенно оптимистично для нашего класса задач.
Подход, который поддавался
Итак, регрессия координат.
В Blender мы сделали датасет 224×224 с таргетами — координатами углов доски (точнее, углов игрового поля, без паддинга). Первая версия — около 12 тысяч рендеров, финальная — 66 тысяч сцен.
За год регрессия углов прошла путь: полный рандом на реальных фото → три угла встают на место, четвёртый улетает → почти попадание → ошибка в полклетки или в две… Каждая ступень выглядела прогрессом. В этом и ловушка: неверный подход не отказал ни разу — он поддавался. А курсы воспитали во мне главное правило прилежного студента: заработало — дожимай.

Внутри этого пути нас ждало много ошибок — простых и неочевидных — и много боковых веток: выбор разных сетей, ручные фильтры и многое другое.
статичный фон — все рендеры сняты в одной и той же виртуальной комнате: стол, доска, штора;
сплит синтетики на train и test — тест из того же генератора не проверял ничего;
вечно деревянная доска — фон рандомизировали, а материал самой доски забыли;
процедурные фоны — искусственный «шум» вместо фона так и не заработал, и мы до сих пор не знаем почему;
ручные фильтры — попытка скормить сети контуры вместо цвета;
баг с порядком углов — разметка выдавала углы в случайном порядке;
перебор лоссов — от классического MSE до самодельного линейного с «мёртвой зоной»;
перевёрнутая ось Y Блендера — след на весь проект.
Но даже после фикса всех ошибок максимум, что удалось выжать из регрессионного подхода, выглядит так:

Кому интересно — под спойлерами подробности попыток выжать из этого подхода максимум. Попытки эти, кстати, длились около года, и каждый раз результат становился лучше и лучше, но мы всё-таки упёрлись в потолок, изображённый на картинке выше.
Спойлер
Сплит синтетики: разбиение на трейн и тест и почему это бесполезнобсзполезно на синетике
Фон у рендеров был статичный: стол, на котором лежит доска (и, кажется, всегда одного цвета), позади - штора, как в моей комнате. Это была самая грубая ошибка - после самого выбора CNN с регрессией. Дальше мы сделали ровно то, чему учат на курсах (Слёрм: привет): поделили данные на train и test. Синтетику - на синтетический train и синтетический test. Метрику взяли жесткую: кадр считается ошибочным, если хотя бы один из четырех углов ушел от таргета больше чем на два пикселя. Получили на тесте 99+ процентов и обрадовались. Почему это не работает. Синтетика генерировалась с аугментацией по ракурсам и параметрам сцены, но все 12 тысяч картинок - из одного генератора с одним фоном. Случайно поделив их на train и test, получаешь две почти идентичные выборки: тест не проверяет ничего, кроме способности сети запомнить этот конкретный игрушечный мир. От реальных фотографий обе выборки отличаются одинаково сильно. Правильный тест для продукта, который будет работать на реальных фото, - небольшой набор реальных размеченных фотографий. Если бы train и test хотя бы аугментировались по-разному, смысла в сплите было бы больше - но и это не заменило бы реальный тест. Сфотографировал реальную доску - и, конечно, получил полную ерунду: предсказания были ближе к случайным, чем к углам. Контраст виден на первых двух ступенях лестницы выше: на своей синтетике - гроссмейстер, на физике - коллапс.
Captum: как понять на какие точки на самом деле смотрит сеть и как понять в чем ошибка в данных
В какой-то момент я - тоже по диагонали - прослушал кусок CV-курса, где разбиралась интерпретируемость моделей. Для тех, кто слышит про это впервые: интерпретируемость - это методы, позволяющие посмотреть, на какие пиксели входа сеть реально опиралась, принимая решение. Один из стандартных инструментов - Captum, библиотека для PyTorch от Meta: несколько строк кода - и поверх картинки рисуется “карта значимости” (мы использовали метод Integrated Gradients).
Применили к нашей провалившейся регрессии - и картина оказалась красноречивой: “важные” пиксели рассыпаны по всей сцене - пятна на однотонном столе, у шторы, вокруг фигурок - где угодно, только не собраны там, где сеть якобы нашла углы. В тогдашних прогонах попадались кадры и выразительнее - с точками ровно вдоль границы доски и стола, на перепаде цвета; те кадры не сохранились. Вывод читался один: сеть не искала углы - она выучила геометрию конкретной сцены и привязалась к перепадам цвета между доской, столом и шторой.
Картинок не сохранилось, а восстанавливать я не стал; В литературе этот эффект называется shortcut learning: сеть находит самый дешевый признак, коррелирующий с таргетом, а не тот, который вы имели в виду. Систематический разбор - у Geirhos et al., “Shortcut Learning in Deep Neural Networks” (Nature Machine Intelligence, 2020). После этой работы доверять красивой метрике на тесте становится сложно - и это полезно.
Фоны: линии с точками против чужих залов - почему важен разнообразный фон
Рандомизацию фона мы начали с процедурного генератора - линии, точки, градиенты, цветные пятна: дадим сети “шумный” фон, она перестанет на него опираться.

На реальных фото - снова ерунда. Помогло другое: мы взяли LSUN - классический датасет сцен - и стали подставлять фоном реальные фотографии конференц-залов (категория conference_room). Настоящие текстуры и настоящий свет - стало заметно лучше. Почему процедурные фоны не сработали, а фотографии чужих залов сработали - честно говоря, не понимаю до сих пор. Позже мы перепроверяли процедурный вариант уже на зрелом датасете - картина та же: работают только реальные фоны. Если у вас есть убедительная версия - расскажите в комментариях, вопрос живой. Параллельно датасет оброс полноценной доменной рандомизацией - по рецепту Tobin et al., “Domain Randomization” (OpenAI, 2017): не делай симулятор реалистичным - делай его настолько разнообразным, чтобы реальность оказалась частным случаем. Рандомизировались цвета всех объектов и линий, толщина линий, размеры и высота фигурок, расстояние и угол камеры, материалы доски (эти - позже всех, о чем следующая ветка), освещение.
Вечно деревянная доска: важность аугментации цвета и текстуры ключевого объекта
Фон, как видите, в итоге рандомизировали полностью - а сети все равно разваливались на реальных фото. Разгадка нашлась только при подготовке этой статьи, когда я раскопал старые версии генератора: рандомизировали фон, а саму доску - нет. Вплоть до поздней осени 2024-го в генераторе жестко стояло material = ‘Wood’: цвета фигурок, толщина и цвет линий разметки, ракурсы - рандомилось все, а доска в каждом кадре была одним и тем же рендерным деревом. У сети оставался идеальный якорь: константная текстура доски и переход “любой фон → вот это конкретное дерево” на ее границе. Тот же shortcut, что и со шторой, - якорь просто переехал с фона на саму доску. Реальные фото ломали его первым же кадром: настоящее дерево под настоящей лампой на рендерное не похоже. Список из двадцати материалов доски - восемнадцать сортов дерева, медь и алюминий - появился в генераторе только следующей весной, уже в сегментационную эпоху. И заметьте: разницу между процедурным фоном и LSUN из прошлой ветки это не объясняет - доска была одинаково деревянной в обе эпохи. Тот вопрос остается открытым.
Ручные фильтры: Собель, Лаплас, Canny - попытка упростить картинку убившая обучение Отдельная ветка - ручные фильтры. Мы подмешивали на вход карты границ Собеля и Лапласа, а затем и вовсе выкинули цвет: перевели вход в grayscale и скормили одноканальной сети бинарную карту границ Canny. Логика: доска - это же линии; оставим сети чистую геометрию, уберем все лишнее.

Для тех, кто слышит про это впервые: Собель, Лаплас, Canny - классические “ручные” фильтры из до-нейросетевого компьютерного зрения: фиксированные операторы, выделяющие перепады яркости - границы, вертикальные и горизонтальные линии. Первые системы зрения собирались именно из таких фильтров, подобранных вручную. Вопрос на понимание: почему подсовывать их сверточной сети обычно не нужно? Потому что обучение сверточной сети - это и есть выращивание нужных фильтров: обратное распространение ошибки настраивает ядра сверток под задачу - и обычно удачнее универсальной классики.
Стало только хуже. Сеть не училась вообще, мы не получили нормальной точности даже на синтетике на которой же и учились. Убрав цвет, мы отобрали у сети признаки, за которые она реально держалась, и оставили ей мир, где полторы сотни одинаковых пересечений линий отличать стало еще сложнее.
Лоссы и порядок углов, или как запутать сеть
Таргетом была коллекция координат четырех углов. Проблема: первым элементом в разметке мог оказаться любой угол - как повезло генератору. Сеть, выдавая координаты, не могла угадать, с какого угла “начинать ответ”, и получала штраф за правильные углы в неправильном порядке. С точки зрения обучения это выглядело как необъяснимо плохая сходимость. Чинили двумя способами сразу: канонизировали порядок в разметке (всегда с одного угла, по часовой) и матчили при подсчете лосса каждый предсказанный угол с ближайшим таргетом. Помогло и то, и другое, но канонизация - заметно сильнее. Вывод переиспользуемый: с неоднозначностью лучше бороться не усложнением лосса, а устранением неоднозначности из данных.
И отдельный привет тому самому “лосс берите любой”: именно лосс - самая “любая” часть системы - съел у нас больше всего недель. (Лосс - функция ошибки, по которой сеть на обучении понимает, насколько промахнулась.) Мы перепробовали целое семейство штрафов по удаленности от таргета: начали, как все, с квадратичного MSE - чем дальше промахнулся, тем страшнее наказание, - потом пробовали SmoothL1. А победил, вопреки интуиции, чисто линейный - да еще с “мертвой зоной”: отклонения меньше порога не штрафуются вовсе, остальное штрафуется по модулю, линейно. Задним числом все объяснимо: квадрат раздувает вклад самых спорных примеров, а у нас каждый неоднозначный угол давал именно большие промахи - агрессивный штраф превращал их в шум градиентов; линейный этот шум глушит, а мертвая зона не дает сети полировать субпиксельные мелочи вместо сути. Она же рифмуется с метрикой: успешным считался кадр, где все восемь координат легли в два пикселя. Остальные подробности той эпопеи психика вытеснила - и, полистав блокноты, я ее понимаю.
Выводы
Оглядываясь, я вижу в этой эпопее два слоя ошибок. Стратегический — сам выбор регрессии координат для точек, которые локально неотличимы друг от друга: эта дорога была тупиком независимо от качества исполнения. И тактический — все те ветки, что выше: обычные ошибки периода обучения, мы ведь учились параллельно с проектом.
Коварство в том, как слои взаимодействуют: каждый исправленный тактический косяк давал реальное улучшение и создавал ощущение «ещё чуть-чуть — и дожмём». Неверный подход не отказывает сразу — он поддаётся. Именно поэтому он съедает не неделю, а месяцы: прогресс в мелочах маскирует потолок в главном.
Сам же потолок объясняется просто. Регрессия координат плохо переносит задачи, где целевая точка локально не уникальна. Угол доски — это пересечение линий; таких же линий на доске ещё две дюжины, а похожих друг на друга пересечений — полторы сотни. Чтобы выдать координату, сеть должна глобально рассудить, какое из одинаковых пересечений крайнее, и свернуть этот вывод в два числа через регрессионную голову; при неоднозначности между кандидатами регрессия к тому же склонна усреднять — «мазать» между похожими точками. С глазом на кропе лица из примера Ына всё наоборот: точка одна, локально узнаваема, всегда примерно в одном месте кадра — потому тот пример и работал.
Дни, которые всё решили
Дальше вы уже догадались. Мы попробовали сегментационную сетку с предсказанием маски доски — точнее, игрового поля. Это был U-Net с претрейненным на ImageNet энкодером (у нас это EfficientNet-B7 — самый жирный из линейки, другие давали результат хуже). Уже через час обучения на этом датасете стало ясно, что метод выбран верно: маска доски получалась уверенно. Дальше решение только улучшалось — но это были дни доводки, а не месяцы борьбы.
Дальше чистая геометрия. По маске строится выпуклая оболочка, и с неё жадно срезаются вершины: на каждом шаге две соседние заменяются пересечением их рёбер — выбирается срез с минимальной потерей площади, — и так, пока не останется ровно четыре угла. Алгоритм раскопали в интернете: очень похожая реализация, вплоть до той же sympy-версии, описана в заметке «Efficiently Drawing Bounding Boxes Around Segmented Masks». Он был переписан на numpy — и всё полетело: 4 миллисекунды, и готов наш четырёхугольник с игровым полем.

Причина успеха тоже прозаична: попиксельная разметка не оставляет сети места для шорткатов, а точность углов обеспечивает уже не нейросеть, а геометрия.
Пайплайн целиком: фиксация успеха
Фигурки ищутся похожей связкой: сегментация плюс тепловая карта центров. Карта даёт кандидатов на фигурки, ложные пики подавляются, а уверенные подтверждаются сегментацией; эта связка дала около 99% успеха — пайплайн работал почти всегда. Чтобы «почти всегда» превратилось в «всегда, под почти любым углом и при почти любом освещении», пришлось сделать ещё ряд мелких костылей, которыми не буду перегружать статью.

Что накопилось с тех пор
Дожать можно почти любой подход — вопрос, какой ценой. Главный навык — понимать цену заранее, и с тех пор именно он стал для меня основным предметом охоты. Накопилось за последние годы:
работающий самописный аналог AlphaZero (MCTS + ResNet, self-play) — про его находки, от диагностики движка до трюков с политикой, будет отдельная статья: подпишитесь, чтобы не пропустить;
уроки DQN: он не осилил даже крестики-нолики 3×3, пока один из противников не стал рукописным — два шумных ученика не могут научить друг друга; подробности в той же статье про движок;
немало раскопанного про RL в целом;
заказные проекты, которые мы делаем с приятелем: LLM-пайплайны для сложных документов, ИИ-наблюдение за критичными событиями, семантический поиск (часть — на solved-by.ml);
сейчас копаем задачи перемещения объектов — впереди манипулятор.

Зачем этим заниматься и почему сейчас
Сейчас ровно то время, когда такие штуки надо делать.
В дата-сайенс есть уже огромное количество подходов, которыми можно реализовать тучу вещей. Много замечательных архитектур сетей. Умные нейронки, которые многое подскажут, и куча материалов в сети. И железо, на котором можно учить довольно нетривиальные вещи, вполне доступно.
Есть огромный спрос на AI — и на производстве, и в науке, и для быта.
Но почему-то 99,99% того, что можно было бы реализовать, даже не придумано.
Вот два простых забавных примера.
В «Перекрёстке» недавно появились ИИ-весы: положили яблоки — на экране только яблоки. Сети такого класса доступны уже больше десяти лет, и совершенно непонятно, почему этого не было раньше.
В ютубе был ролик с занавеской, которая сдвигается за прохожим, чтобы он не заглядывал в квартиру: сегментация или детектор плюс мотор — проект на неделю.
Таких незанятых идей — море, и делать их можем мы все.
Ситуация напоминает раннюю эпоху персональных компьютеров — времена Homebrew Computer Club, когда будущие основатели индустрии паяли машины в гаражах и сами писали к ним софт.
Препятствия тут только время, интерес и некоторое понимание того, в какую сторону думать, решая такие задачи, и, главное, какие подходы выбирать в каждом случае.
Чтобы сделать простого домашнего робота — не то что занавеску или весы, — в принципе достаточно для начала осознать подходы к решению следующих задач:
понять сцену — наличие объектов, их контуры и взаимное расположение (CV);
установить состоявшиеся факты по визуальной информации (VLM/VM);
принимать решения (RL или LLM);
совершать движения — перемещаться в пространстве или работать манипулятором (от простейшего Behavior Cloning до Decision Transformer и честного RL в варианте актор-критик, или, например, VLA);
отдельно стоят вопросы равновесия, но они уже решены;
и отдельно — среды, в которых всё это обкатывают: от чистой концепции без физики до полноценной симуляции.
Существенную часть из этого мы со Слёрмом уже готовы рассказать нашим читателям здесь — или участникам воркшопов и курса, если к таким активностям будет интерес. О чём написать дальше
Напишите в комментариях, о чём было бы интереснее почитать в следующей статье:
мучения с самописным движком AlphaZero и как он начал обыгрывать серьёзных игроков;
простой приём, как научить лёгкий трансформер (читай — LLM) весом 50 мегабайт понимать ограниченный набор пожеланий пользователя, сказанных в совершенно любой форме;
про раннюю стадию активности с переносом фигурки манипулятором — выбор концепции и причины выбора.
А ещё было бы интересно послушать, какие области исследуете вы — по работе или в качестве хобби. А если готовы рассказать подробнее — тем более пишите: может, удастся уговорить вас поучаствовать в теме с воркшопами или запилить тут пару статей.
netricks
А вы сразу обучали на сложных рендерах? ИМХО, можно было бы начать на генерируемой геометрии попроще. Более разнообразный датасет был бы. Было бы интересно узнать, есть ли там проблема с переносом результатов на более физичные изображения?