Знаете это чувство, когда обучаешь классификатор изображений в десятый раз и ловишь себя на мысли, что делаешь ровно то же самое, что и в прошлый раз? Поменять архитектуру, подкрутить learning rate, добавить аугментацию, подождать, посмотреть на кривые, вздохнуть, поменять ещё раз. Рутина, которую вроде бы знаешь наизусть и именно поэтому она бесит больше всего.
В какой-то момент (прошлой осенью) я подумал: а почему этим до сих пор занимаюсь я, а не модель, которая в этом разбирается не хуже (ну наверное)? Так началась KiSinWi – платформа, где команда из LLM-агентов сама проходит весь путь от сырого датасета до обученной модели. Анализирует данные, спорит об архитектуре, изучает лучшие практики, собирает конфиг обучения, запускает его и потом сама же разбирает, что получилось.
С тех пор прошло уже больше полугода работы. Да, это не проект выходного дня, собранный на коленке между двумя чашками кофе. Настал момент рассказать о нём и заодно честно взглянуть на цифры. Не на красивые обещания на листе со шрифтом Times New Roman, а на реальные итоги. Работает ли оно по-настоящему?
Для этого я взял пять публичных датасетов с известными эталонными результатами и прогнал через платформу. Ниже представлено что вышло со всеми конфигами лучших моделей, цитатами агентов и с двумя датасетами, где платформа недотянула по метрикам. Их я тоже покажу. Надеюсь, к концу вы согласитесь, что это лечится не переписыванием платформы, а парой функций или ещё одним агентом в команде. А сел писать я не потому, что всё готово, а потому что “вот допилить нужно ещё чуть-чуть” говорил я и откладывал уже четыре раза – пора вылезать из этой ловушки перфекциониста.
Сначала – зачем всё это
Обучить классификатор картинок – это не “вызвать model.fit() и пойти спать”. Это цепочка решений, каждое из которых требует осмысленных действий:
-
какую архитектуру взять и обучать ли её с нуля или дообучать её;
-
какие аугментации помогут, а какие, наоборот, сотрут полезные признаки;
-
какой оптимизатор, какой scheduler, сколько эпох;
-
где вовремя остановиться и не переобучиться.
Каждое из этих решений человек принимает головой и опытом, а перебор стоит времени и GPU-часов. Идея KiSinWi проста, отдать весь этот цикл агентам. Пользователь приносит датасет и формулирует требование по-человечески – “хочу точность не ниже 0.93, и чтобы инференс был дешёвым”. А дальше платформа сама идёт от анализа данных до готовых весов и отчёта о качестве. Причём управлять агентами можно не только целевой метрикой. Им можно задать ограничения по гипотезам – например, направить в сторону конкретных архитектур или, наоборот, запретить лишние эксперименты, если вы уже знаете, что хотите. А можно описать железо, на котором модель будет жить в проде: сколько памяти, есть ли GPU, какой бюджет на latency. Агенты учитывают это при выборе архитектуры – нет смысла подбирать тяжёлую сеть под сервер, который её не потянет. По сути вы очерчиваете рамки, а внутри них платформа ищет лучшее решение сама. И в итоге мы имеем не просто AutoML с перебором гиперпараметров, а агентную систему, где каждый агент рассуждает вслух.
Как это устроено под капотом
Под капотом платформа – это конвейер из микросервисов. Данные проходят через них последовательно, как по конвейерной ленте:
datasets -> agents -> tasker -> trainer -> metrics + ml_models -> agent_history
Я разбил систему на сервисы не ради моды, а для возможности беспрерывного масштабирования. Сейчас платформа заточена под классификацию изображений, но это не тупик. Чтобы добавить новый тип задачи, к примеру детекцию объектов или NLP, достаточно добавить в сервис datasets возможность работать с данными и поднять отдельный trainer с соответствующей логикой. Все остальные сервисы остаются неизменными. Платформа расширяется не переписыванием старого, а подключением нового.
Важный момент о котором вы уже могли подумать – конфиг обучения динамический. Агенты не заполняют жёсткую форму с фиксированными полями – они собирают конфигурацию из доступных кирпичиков (архитектуры из timm, оптимизаторы, scheduler’ы, аугментации) и прогоняют её через формальную валидацию перед запуском. Никакого хардкода – это позволило в процессе разработки добавлять новые фичи в сервис тренировок без изменения агентов и позволит в будущем с лёгкостью внедрять новые сервисы обучения с другими задачами.
Знакомьтесь, команда
В платформе я реализовал два воркфлоу: полный и быстрый. Давайте сейчас рассмотрим полный воркфлоу и начнём с того, какие агенты у нас есть и что они делают.
|
Роль |
Чем занимается |
|---|---|
|
Computer Vision Dataset Analyst |
Анализирует датасет: классы, баланс, размеры, возможные утечки между выборками. Запускается первым и имеет право вето – если данные не готовы, воркфлоу останавливается. |
|
ML Researcher |
Выдвигает гипотезы по архитектуре, аугментациям и регуляризации. Дирижёр обсуждения гипотез. В его подчинении есть агент Internet Best-Practices Scout. |
|
Internet Best-Practices Scout |
По запросу Researcher’а ищет свежие практики – arXiv, парсинг страниц. |
|
ML Engineer |
Оценивает гипотезы Researcher’а: принимает, отклоняет или отправляет на доработку. Собирает финальный конфиг и валидирует его перед запуском. |
|
ML Debugging Engineer |
Включается только если обучение упало с ошибкой. Локализует проблемный параметр в конфиге и чинит его точечно не переписывая всё с нуля. |
|
ML Model Metrics Analyst |
Оценивает полученные результаты модели и запускает новую итерацию обучения в случае, если модель не достигла бизнес требований. |
|
ML Model Production Readiness Expert |
В конце подводит итог всех попыток и выдаёт вердикт: ДА / НЕТ / УСЛОВНО готова модель к проду. |
Схема взаимодействия показана на рисунке ниже. (Агенты подписаны полными именами)
Как вы видите на рисунке выше это не конвейер, где каждый говорит по разу. Dataset Analyst отрабатывает один раз в начале, а дальше крутится главный цикл – Researcher <-> ML Engineer. Researcher предлагает гипотезы, ML Engineer их судит: согласен – собирает конфиг и запускает обучение; не согласен – отправляет Researcher’а думать заново, и так до трёх подходов. Если обучение падает с ошибкой, запускается отдельный цикл с ML Debugging Engineer. Он имеет три попытки починить поломку, а так же может отказаться чинить остановив обучение, если решение ошибки не зависит от его действий. А поверх всего – внешний цикл итераций: платформа обучает модель, агент ML Model Metrics Analyst определяет, достигла модель требований, и если не достигла, запускает новую итерацию обучения(максимум прогонов 5, т.к. вероятность, что вам придётся брать кредит на токены из-за галлюцинаций никуда не пропадает).
ML Engineer не сочиняет конфиг по памяти – у него есть инструменты: список реально доступных архитектур (timm), опрос железа (есть ли GPU и сколько памяти), перечни поддерживаемых оптимизаторов, scheduler’ов и аугментаций, а финальный конфиг прогоняется через валидатор до запуска. Поэтому в обучение уходит не “галлюцинация”, а проверенная конфигурация – что во многом и объясняет, почему дефолты у платформы получаются вменяемыми.
Быстрый режим – это урезанный вариант обучения с одной итерацией обучения. Добавлен он чтобы гонять платформу на тестах, не упираясь в отказы от рассуждающего блока и агента аналитика данных. В нём ML Engineer обязан проанализировать данные и запустить обучение на имеющихся данных (права сказать “нет, эта задача безнадёжна” у него нет). После обучения подключается агент ML Model Metrics Analyst, который разбирает метрики готовой модели.
В бенчмарке я гонял именно полный пайплайн и самое приятное, что спор и действия агентов не спрятаны в логах сервера. Он живёт прямо в интерфейсе. Открываешь дискуссию – и видишь вертикальную ленту-таймлайн: сообщение за сообщением, каждое подписано ролью агента и помечено статусом. Кликаешь по карточке – она разворачивается, и под ней полное рассуждение агента, отрендеренное как Markdown: с заголовками, списками, кусками конфига. А рядом – кнопка “Инструменты”: жмёшь и видишь, чем именно агент пользовался – какие модели искал, что прогонял через валидацию, с какими аргументами на входе и что получил на выходе. То есть видно не только что агент решил, но и на основании чего.
Скриншоты интерфейса
И всё это в реальном времени: пока идёт прогон, лента сама подтягивает новые сообщения раз в пару секунд, статусы переключаются с “в процессе” на “готово”.
Для AutoML такая прозрачность редкость. Возьмите тот же AutoGluon или Auto-sklearn: после fit() вы зовёте .leaderboard() и получаете аккуратную таблицу – модель, score, время обучения. У H2O AutoML – то же самое через .leaderboard. Полезно, спору нет, но это ответ на вопрос “что победило”, а не “почему”. Цепочку рассуждений – какие гипотезы рассматривались, что отмели и на каком основании, какими инструментами это проверялось – оттуда не вытащишь, её просто нет. А здесь она есть, прямо в виде живого, читаемого диалога.
Подготовка: какой моделью “думают” агенты
Прежде чем гонять бенчмарк, надо было решить вопрос, от которого зависит вообще всё: какую LLM поставить мозгом агентов. И это оказалось совсем не формальностью – пара кандидатов отвалилась прямо на старте.
Платформу я с самого начала затачивал под модели OpenAI, и не из вкусовщины. Вся мультиагентная механика держится на двух вещах: агенты должны возвращать строгие структуры (я описываю их как Pydantic-схемы – это лучшая практика при использовании crewAi) и уметь дёргать инструменты. У OpenAI зрелые Structured Outputs со strict JSON – модель не просто “старается” попасть в формат, а гарантированно отдаёт JSON, который ложится в мою схему. Провайдер валидирует это на своей стороне. Плюс надёжный вызов инструментов во всей актуальной линейке. У меня же агенты вызывают инструменты буквально на каждом шаге.
И это не пустые слова – я пробовал поставить за штурвал других, и упёрся ровно в эти две вещи:
-
DeepSeek V4 – спотыкался на инструментах. Он раз за разом не мог корректно вызвать tools, а без инструментов агенты слепы: им нечем ни датасет посмотреть, ни конфиг провалидировать. Так что дальше первых шагов на нём не уедешь. Зато цена и скорость явно превосходят OpenAI модели.
-
Anthropic: Claude Opus 4.6 – наоборот, с инструментами дружил, но не держал строгую структуру ответа: возвращаемый JSON регулярно не сходился с моей Pydantic-схемой, и шаг падал на валидации.
Чтобы не вводить в заблуждение: это не приговор самим моделям. И DeepSeek, и Claude в принципе умеют и tool’ы использовать, и структурированный вывод – просто у каждого провайдера свой способ это отдавать, и litellm на тот момент дружил с ними по-разному. У меня всё было заточено под strict-режим OpenAI, поэтому именно с ним связка вела себя предсказуемо, а остальных пришлось бы отдельно настраивать. Возможно, под них платформу ещё допилю – но для бенчмарка я взял то, что работает как часы: openai/gpt-5.1. [1]
Как я вообще это мерил
Важная оговорка: я проверял не техническую работу платформы, а её качество как ML-инструмента. Бенчмарк под это написан сознательно максимально просто. Он не оценивает, не считает метрики и не ставит вердиктов: его задача – прогнать датасеты через платформу, запустить обучения и принести ссылки на готовые модели. А дальше начинается ручная работа: смотреть метрики, сравнивать с эталоном, разбираться, почему вышло так, а не иначе, – это мы делаем уже сами, глазами и головой.
Почему так? Потому что источник правды по метрикам – это сами сервисы платформы, а не локальная копия в JSON. Дублировать их в скрипте – значит плодить второй “оракл”, который рано или поздно разойдётся с реальностью. Поэтому скрипт намеренно держит у себя только ссылки. Также подчеркну, что в интерфейсе реализован просмотр моделей для сравнения.
Скриншоты интерфейса при сравнении
Сравнивать дальше я буду с эталоном типичной точностью на тесте для ResNet50 с предобучением на ImageNet [2]. Это честный ориентир именно для инструмента: не “побей лучшую модель в мире”, а “выйди на уровень, который грамотный инженер получает стандартным дообучением”.
Baseline у каждого датасета – не рекорд SOTA и не цифра с потолка, а уровень “ResNet50, дообученный с ImageNet”: крепкий, общеизвестный transfer-результат, который на этих датасетах берут годами. Сами проценты (98/96/93/96/80) я взял как типичные значения такого transfer-дообучения по литературе и публичным репортам – это не строгие официальные числа из одной таблицы, а ориентир “куда дотягивается грамотный инженер стандартным подходом”. И я не требую попасть в него тютелька-в-тютельку – мы с вами держим в уме разумную погрешность для метрик. Плюс отдельно посмотрим на разрыв train−val как индикатор переобучения.
Источники каждого датасета: CIFAR-10, Oxford Flowers-102, Oxford-IIIT Pets, Food-101, Beans. Шестым кейсом идёт deepfake-классификация: там я мерил платформу уже не против baseline, а против решения живого Kaggle-мастера на мета-датасете, собранном из 6 датасетов Kaggle (подробный разбор – в отдельной главе ниже).
Все прогоны – от 2026-06-15, GPU NVIDIA RTX 5080 Laptop, с несколькими итерациями рассуждений, в роли мозга агентов – openai/gpt-5.1 (почему именно она – в главе “Подготовка” выше).
А сколько это стоит по токенам. Резонный вопрос для AutoML: семь агентов с инструментами и несколькими итерациями – это не бесплатно. Цифры из сервиса метрик (он считает токены по каждому агенту): один датасет обходился в среднем примерно в 740k токенов – от ~640k на самых быстрых прогонах (Flowers, Pets) до ~945k на CIFAR-10, который крутил больше всего итераций. Подавляющая часть – это prompt-токены (~90%): агенты на каждом шаге таскают за собой контекст и результаты инструментов. По биллингу OpenRouter это выходило порядка 0,9$ за датасет. Для ясности так же обозначу, что цена могла быть ниже, если бы я использовал API OpenAI напрямую, а не через OpenRouter.
Результат сравнений
Вот сводная таблица, а дальше разберём каждый случай по-отдельности – что увидели агенты, что выбрали и чем всё кончилось.
|
Датасет |
Классы |
Архитектура |
Baseline |
Test acc |
Разрыв |
F1 |
Test AUROC |
Train−val |
Итераций |
Время |
Вердикт |
|---|---|---|---|---|---|---|---|---|---|---|---|
|
Beans |
3 |
mobilenetv3_large_100 |
98.0% |
98.4% |
+0.4 п.п. |
0.985 |
0.99 |
0 п.п. |
4 |
15.5 мин |
✅ ОК |
|
Oxford Flowers-102 |
102 |
efficientnet_b0 |
96.0% |
98.3% |
+2.3 п.п. |
0.984 |
0.99 |
+0.9 п.п. |
3 |
24 мин |
✅ ОК |
|
Food-101 (урезан до 100 картинок на класс) |
101 |
mobilenetv3_large_100 |
80.0% |
71.6% |
−8.4 п.п. |
0.711 |
0.98 |
+33.8 п.п. |
3 |
57.8 мин |
◐ ориентир (subset) |
|
CIFAR-10 |
10 |
resnet18 (с нуля) |
96.0% |
88.7% |
−7.3 п.п. |
0.887 |
0.99 |
+3.6 п.п. |
4 |
82.7 мин |
⚠️ accuracy недотянул |
|
Oxford-IIIT Pets |
37 |
efficientnet_b0 |
93.0% |
88.5% |
−4.5 п.п. |
0.882 |
0.99 |
+6.5 п.п. |
3 |
22 мин |
⚠️ accuracy недотянул |
|
Deepfake |
2 |
resnet50 |
98.0% |
98.3% |
+0.3 п.п. |
0.983 |
0.99 |
+0.2 п.п. |
3 |
8 ч 34 мин |
✅ ОК |
Пара слов про колонки, чтобы не запутаться. Архитектура – это то, что агенты выбрали сами. Разрыв – это отставание/опережение test accuracy от baseline, а Train−val – это разрыв между точностью на обучении и на валидации, то есть индикатор переобучения (чем больше, тем сильнее модель “зазубрила” трейн). F1 добавил как устойчивую к перекосам альтернативу accuracy.
Пара слов про методологию. Платформа крутит внешний цикл итераций и в конце сама выносит вердикт, какая из обученных версий лучшая – это делает агент-эксперт по метрикам. Именно эту, отмеченную платформой лучшую модель я и ставлю в таблицу: то самое итоговое решение, которое забрал бы и живой инженер. Любопытная деталь: на CIFAR-10 и Pets лучшей оказалась не последняя итерация, а более ранняя – то есть платформа не улучшает любой ценой и не выдаёт случайный регресс за прогресс.
Поехали по порядку. Но сразу обратите внимание на колонку AUROC – к ней мы вернёмся, когда дойдём до двух датасетов, где accuracy “недотянул”. Спойлер: всё не так грустно, как кажется по одной только accuracy.
Если AUROC вам ни о чём не говорит – это метрика того, насколько хорошо модель ранжирует объекты по уверенности, независимо от выбранного порога. Accuracy отвечает на вопрос “сколько угадал”, AUROC – “понимает ли модель, где какой класс, в принципе”. 1.0 – идеал, 0.5 – монетка. Важная оговорка наперёд: в многоклассовой задаче AUROC считается по схеме one-vs-rest (каждый класс против всех остальных), а такой бинарный вопрос решается легко, поэтому 0.99 здесь – частое и не особо геройское значение. Высокий AUROC не равно “почти взял baseline по accuracy” – это разные вопросы, и дальше я на этом подробно остановлюсь.
Beans – 98.4% на больных листьях фасоли
Это датасет состоящий из 3 классов, около 1300 фотографий листьев фасоли (здоровые и две болезни). Такой датасет взят, чтобы проверить – а базовый-то сценарий вообще работает? Если платформа спотыкается тут, дальше можно не смотреть. Агенты считали ситуацию мгновенно: мало данных, мало классов – значит, лёгкая предобученная архитектура плюс сильные аугментации против переобучения. ML Engineer выбрал mobilenetv3_large_100 (предобученный) и прямо обосновал выбор компромиссом “качество против дешёвого инференса в проде”. Не “давайте самую жирную сеть”, а именно по делу.
Полный конфиг обучения (Beans) – для тех, кому интересны внутренности
{
"model_params": { "type": "mobilenetv3_large_100", "pretrained": true },
"data_loader_params": {
"batch_size": 32,
"num_workers": 2,
"train_transforms_config": [
{ "name": "RandomResizedCrop", "params": { "scale": [0.8, 1.0], "size": [224, 224] } },
{ "name": "RandomHorizontalFlip", "params": { "p": 0.5 } },
{ "name": "RandomVerticalFlip", "params": { "p": 0.25 } },
{ "name": "RandomRotation", "params": { "degrees": 22 } },
{ "name": "ColorJitter", "params": { "brightness": 0.25, "contrast": 0.25, "saturation": 0.25, "hue": 0.05 } },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.485, 0.456, 0.406], "std": [0.229, 0.224, 0.225] } }
]
},
"trainer_params": {
"epochs": 40,
"loss_fn": { "name": "CrossEntropyLoss", "params": { "label_smoothing": 0.1 } },
"optimizer": { "name": "AdamW", "params": { "lr": 0.001, "weight_decay": 0.0003 } },
"scheduler": { "name": "CosineAnnealingLR", "params": { "T_max": 40, "eta_min": 1e-06 } },
"early_stop": { "metric_name": "loss", "patience": 8, "min_delta": 0.001, "mode": "min" },
"grad_clip_norm": 1.0,
"use_amp": true
},
"device": "cuda"
}
Итог – 98.4%, на 0.4 п.п. выше baseline, переобучения ноль, всё уложилось в 15 с половиной минут. Эталонный прогон, придраться не к чему. Приятно, когда базовый кейс просто берёт и работает.
Oxford Flowers-102 – 98.3% на сотне с лишним классов
А вот тут я слегка напрягся перед прогоном. 102 класса цветов, примеров на класс мало. Казалось бы, идеальный повод для платформы споткнуться. Но нет. ML Researcher и ML Engineer быстро сошлись на efficientnet_b0 (предобученном) – лёгкий, дешёвый. Из данных, собранных ML Researcher, ML Engineer вывел вот такое заключение:
«EfficientNet-B0 исторически показывает высокое качество на датасете Oxford Flowers-102, и при использовании предобученных весов, адекватных аугментаций и настройки обучения достижение accuracy выше 0.96 на сбалансированном тестовом сплите – реалистичная цель.» — ML Engineer
То есть они не нашли какую-то секретную архитектуру, а подтвердили общеизвестный best practice и решили не изобретать велосипед. И это, кстати, правильное поведение ML-инженера.
Полный конфиг обучения (Flowers-102)
{
"model_params": { "type": "efficientnet_b0", "pretrained": true },
"data_loader_params": {
"batch_size": 32,
"num_workers": 4,
"train_transforms_config": [
{ "name": "RandomResizedCrop", "params": { "scale": [0.7, 1.0], "size": [224, 224] } },
{ "name": "RandomHorizontalFlip", "params": { "p": 0.5 } },
{ "name": "RandomRotation", "params": { "degrees": 15 } },
{ "name": "ColorJitter", "params": { "brightness": 0.2, "contrast": 0.2, "saturation": 0.2, "hue": 0.1 } },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.485, 0.456, 0.406], "std": [0.229, 0.224, 0.225] } }
]
},
"trainer_params": {
"epochs": 40,
"loss_fn": { "name": "CrossEntropyLoss", "params": { "label_smoothing": 0.1 } },
"optimizer": { "name": "AdamW", "params": { "lr": 0.001, "weight_decay": 0.01 } },
"scheduler": { "name": "CosineAnnealingLR", "params": { "T_max": 30, "eta_min": 1e-06 } },
"early_stop": { "metric_name": "accuracy", "patience": 6, "min_delta": 0.0005, "mode": "max" },
"grad_clip_norm": 1.0,
"use_amp": true
},
"device": "cuda"
}
98.3%, на 2.3 п.п. выше baseline, переобучения почти нет – и это на 102 классах за 24 минуты. Платформа держит планку не только на “игрушечных” трёх классах, но и на нормальной многоклассовой fine-grained задаче.
Food-101 (subset) – сознательный стресс-тест
Здесь я специально подложил платформе свинью: 101 класс еды, но датасет урезан до 100 картинок на класс. Это классический рецепт переобучения – много классов, мало данных. Baseline в 80% дан для полного датасета, поэтому результат на subset я считаю грубым ориентиром, а не строгой планкой. И вот тут агенты показали то, ради чего всё затевалось. На второй итерации обучения ML Researcher увидел, что предыдущая попытка на EfficientNet-B1 даёт ~70% и сильно переобучается, и сам сменил стратегию – выбрал mobilenetv3_large_100 с усиленной регуляризацией:
«Текущий лучший бенчмарк на EfficientNet-B1 даёт ≈0.70 accuracy и сильно переобучается. С учётом продакшн-ограничения “минимизировать затраты инференса” целесообразно перейти на лёгкую архитектуру MobileNetV3 Large… усиленную регуляризацию для борьбы с переобучением.» — ML Researcher
Заметьте: это не я вмешался и подсказал. Агент сам прочитал результат предыдущей итерации, поставил диагноз “переобучение” и сменил подход. Именно то поведение, которого ждёшь от живого ML-инженера.
Полный конфиг обучения (Food-101 subset)
{
"model_params": { "type": "mobilenetv3_large_100", "pretrained": true },
"data_loader_params": {
"batch_size": 64,
"num_workers": 4,
"train_transforms_config": [
{ "name": "RandomResizedCrop", "params": { "scale": [0.6, 1.0], "size": [224, 224] } },
{ "name": "RandAugment", "params": { "num_ops": 2, "magnitude": 9 } },
{ "name": "RandomHorizontalFlip", "params": { "p": 0.5 } },
{ "name": "ColorJitter", "params": { "brightness": 0.2, "contrast": 0.2, "saturation": 0.2, "hue": 0.1 } },
{ "name": "RandomRotation", "params": { "degrees": 15 } },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.485, 0.456, 0.406], "std": [0.229, 0.224, 0.225] } }
]
},
"trainer_params": {
"epochs": 40,
"loss_fn": { "name": "CrossEntropyLoss", "params": { "label_smoothing": 0.1 } },
"optimizer": { "name": "AdamW", "params": { "lr": 0.0008, "weight_decay": 0.02 } },
"scheduler": { "name": "CosineAnnealingLR", "params": { "T_max": 40, "eta_min": 1e-06 } },
"early_stop": { "metric_name": "accuracy", "patience": 6, "min_delta": 0.001, "mode": "max" },
"grad_clip_norm": 1.0,
"use_amp": true
},
"device": "cuda"
}
Получилось 71.6% при ориентире 80% – на урезанном датасете я считаю это приемлемым. Но разрыв train−val настораживает: +34 п.п. Модель буквально упёрлась в нехватку данных, и тут уже никакая регуляризация полностью не спасёт – 100 картинок на класс для задачи с 101 классом это просто мало. Платформа взяла планку-ориентир, но кейс показал её реальный предел. И это нормально: важно, что предел виден в метриках, а не замаскирован.
Задачи, где accuracy недотянул
Два датасета из пяти baseline по accuracy не взяли. И это, пожалуй, самая интересная часть статьи и интересна она не тем, что у недобора есть понятная, измеримая причина, и я её не угадываю, а проверяю руками.
Сразу остужу один соблазн, в который легко свалиться. Помните колонку AUROC? На обоих “провальных” датасетах он получился 0.99 – практически как у датасетов-отличников. Заманчиво сказать: “ну вот, видите, модель всё понимает”. Так вот, это было бы передёргиванием, и я не хочу вам его продавать: как я уже оговорил во врезке выше, one-vs-rest AUROC в многоклассе структурно высок почти всегда и сам по себе не доказывает, что до baseline рукой подать. Высокий AUROC и проваленная accuracy спокойно живут вместе – и это нормально, а не парадокс.
Что AUROC говорит – так это что признаки разделимы: фундамент под капотом рабочий. Это полезный индикатор, но именно индикатор, а не пруф потенциала. Настоящее доказательство, что недобор – это вопрос полировки, а не потолка, лежит ниже: в обоих кейсах я взял ровно те же конфиги, поменял по сути одну вещь (предобучение на CIFAR, learning rate на Pets) и числом вернул большую часть отставания. Вот это – аргумент. AUROC лишь подсказал, где искать. Так же есть догадка, что агенты могли дойти до этого, если бы мы дали количество итераций побольше, но по моей изначальной задумке они должны за 3 прогона уже приходить к результату который мы хотим получить…
Разберём оба кейса по этой логике: что выбрали агенты, где именно недотянули, что показывает AUROC, и как это можно было сделать лучше. (Я не поленился и проверил экспериментально)
CIFAR-10 – accuracy 88.7% при baseline 96%, но AUROC 0.99
CIFAR-10 – это 60 тысяч крошечных картинок 32×32, 10 классов, классика, на которой baseline 96% знает каждый. Платформа сделала четыре попытки, потратила 82 минуты – и её лучший прогон дал accuracy 88.7%. Test AUROC при этом – 0.99, а F1 и precision/recall сошлись на 0.88. Но на эти 0.990 не ведёмся (см. врезку выше): недостающие 7.3 п.п. accuracy сидят не в AUROC, а в конкуренции похожих классов на argmax.
А вот что забавно: агенты всё поняли правильно. ML Researcher разложил канонический рецепт для CIFAR-10 – RandomCrop с padding, RandAugment, RandomErasing, label smoothing, длинное обучение с cosine annealing. ML Engineer собрал технически грамотный конфиг: resnet18, SGD с momentum, 200 эпох, сильные аугментации. На бумаге – всё как по учебнику. Первую попытку, к слову, агенты сделали на чуть более тяжёлой resnet32ts – та дала всего 75.6%, после чего они сознательно упростились до resnet18 как более дешёвой в инференсе и при этом более удачной.
Полный конфиг обучения (CIFAR-10)
{
"model_params": { "type": "resnet18", "pretrained": false },
"data_loader_params": {
"batch_size": 128, "num_workers": 2,
"train_transforms_config": [
{ "name": "RandomCrop", "params": { "size": [32, 32], "padding": 4 } },
{ "name": "RandomHorizontalFlip", "params": { "p": 0.5 } },
{ "name": "RandAugment", "params": {} },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.4914, 0.4822, 0.4465], "std": [0.247, 0.243, 0.261] } }
],
"val_and_test_transforms_config": [
{ "name": "Resize", "params": { "size": [32, 32] } },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.4914, 0.4822, 0.4465], "std": [0.247, 0.243, 0.261] } }
]
},
"trainer_params": {
"epochs": 200,
"loss_fn": { "name": "CrossEntropyLoss", "params": { "label_smoothing": 0.1 } },
"optimizer": { "name": "SGD", "params": { "lr": 0.1, "momentum": 0.9, "weight_decay": 0.0005 } },
"scheduler": { "name": "CosineAnnealingLR", "params": { "T_max": 200, "eta_min": 1e-06 } },
"early_stop": { "metric_name": "accuracy", "patience": 60, "min_delta": 0.0005, "mode": "max" },
"grad_clip_norm": 1.0,
"use_amp": true
},
"device": "cuda"
}
Кстати, на CIFAR заодно пригодился тот самый отдельный цикл с ML Debugging Engineer, о котором я писал выше – на одной из итераций ML Engineer сунул в аугментации RandomApply с вложенным RandomErasing, и прогон на этом упал. Debug-агент не стал гадать, а аккуратно вскрыл трейсбэк и починил ровно одну строку, не трогая остальное:
«Тип ошибки: ошибка на этапе … трансформации данных …
TypeError: 'dict' object is not callable… единственный проблемный параметр … использование вложенной трансформации вRandomApply… достаточно … использоватьRandomErasingнапрямую.» — ML Debugging Engineer
Ровно то, ради чего этот агент и задуман: не переписать весь конфиг с перепугу, а локализовать поломку до конкретного параметра и заменить точечно. После правки обучение поехало дальше.
Видите ключевую строчку? "pretrained": false. Агенты решили обучать resnet18 с нуля. А baseline бенчмарка – это ResNet50 с предобучением на ImageNet. То есть платформа соревновалась с предобученной моделью, а пошла путём обучения с нуля на мелких 32×32 – и закономерно отстала на 7.3 п.п. по accuracy. Конфиг технически прекрасен, но стратегически на этом датасете агенты предпочли “лёгкость и дешевизну инференса” гонке за метрикой.
И вот тут высокий AUROC(0.99) расставляет всё по местам. Признаки модель развела (кошка/собака, олень/лошадь на 32×32), просто без предобучения ей не хватило тех самых последних процентов. Лечится это, по сути, одной эвристикой: “для маленьких разрешений всё равно тяни предобученную архитектуру с upscale”. Маленькая правка в логике агентов – и этот датасет с большой вероятностью переходит в зелёную зону. Потенциал не гипотетический, он измерен. И характерная деталь: сам агент списал недобор на нехватку ёмкости архитектуры (“нужна сетка потяжелее ResNet-18”), а recovery на той же resnet18 показал, что дело было не в размере модели, а в отсутствии предобучения. То есть диагноз агента был мимо, и это ещё один аргумент именно за доработку его эвристик.
И что важно – платформа не стала врать и выдавать недотянувшую модель за успех. Финальный агент честно вынес вердикт “не готова”, заодно сам проговорив ту самую ловушку с AUROC, о которой я предупреждал выше:
«Ключевое бизнес-требование accuracy ≥ 0.96 на тесте не выполнено ни одной моделью … Несмотря на … высокий AUROC (~0.99) и отсутствие серьёзного оверфита, уровень ошибки … всё ещё далёк от ожидаемого топового качества.» — ML Model Production Readiness Expert
Проверка догадки. Чтобы не быть голословным, я взял ту же
resnet18и изменил в конфиге следующее:"pretrained": false->"pretrained": trueплюс upscale 32->224 на входе. Вместе с предобучением логично сменился и режим обучения: вместо SGD с нуля на 200 эпох – мягкий AdamW (lr=5e-4) на 30 эпох и аугментации под 224 (RandomResizedCrop вместо RandomCrop по 32). То есть это не “правка одной строки”, а честная смена стратегии с “учим с нуля” на “дообучаем предобученное” – но рычаг тут именно в предобучении, остальное лишь обслуживает его. Прогнал напрямую через платформу – и test accuracy прыгнула с 88.7% до 94.5% (+5.8 п.п.), а разрыв с baseline схлопнулся с −7.3 до −1.5 п.п. – то есть датасет переезжает в зелёную зону, в пределах допуска. Перевод в transfer-режим добрал недостающее – ровно как я и предполагал по высокому AUROC. Догадка подтверждена не на словах, а числом.
Oxford-IIIT Pets – accuracy 88.5% при baseline 93%
37 пород кошек и собак – fine-grained классификация, где классы визуально похожи (попробуйте сами на глаз отличить две породы короткошёрстных кошек). И вот что показательно: лучшей моделью здесь у платформы оказалась её самая первая и самая простая попытка – предобученный efficientnet_b0, 88.5%. Test AUROC при этом – 0.993 (даже выше, чем у CIFAR), при F1 0.882 и kappa 0.882 – и снова это лишь индикатор разделимости, а не пруф близости к baseline. Недостающие проценты тут теряются на недокрученном режиме fine-tuning: предобученные признаки есть, но их подстройку модель провела слишком грубо.
ML Engineer выбрал лёгкую предобученную архитектуру сразу и по делу:
«Выбранная архитектура efficientnet_b0 является сильным и при этом лёгким baseline для fine‑grained классификации.» — ML Engineer
Полный конфиг обучения (Oxford-IIIT Pets)
{
"model_params": { "type": "efficientnet_b0", "pretrained": true },
"data_loader_params": {
"batch_size": 64, "num_workers": 4,
"train_transforms_config": [
{ "name": "RandomResizedCrop", "params": { "scale": [0.8, 1.0], "size": [224, 224] } },
{ "name": "RandomHorizontalFlip", "params": { "p": 0.5 } },
{ "name": "RandomRotation", "params": { "degrees": 10 } },
{ "name": "ColorJitter", "params": { "brightness": 0.1, "contrast": 0.1, "saturation": 0.1, "hue": 0.02 } },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.485, 0.456, 0.406], "std": [0.229, 0.224, 0.225] } }
],
"val_and_test_transforms_config": [
{ "name": "Resize", "params": { "size": [224, 224] } },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.485, 0.456, 0.406], "std": [0.229, 0.224, 0.225] } }
]
},
"trainer_params": {
"epochs": 80,
"loss_fn": { "name": "CrossEntropyLoss", "params": { "label_smoothing": 0.05 } },
"optimizer": { "name": "AdamW", "params": { "lr": 0.001, "weight_decay": 0.01 } },
"scheduler": { "name": "CosineAnnealingLR", "params": { "T_max": 80, "eta_min": 1e-06 } },
"early_stop": { "metric_name": "accuracy", "patience": 12, "min_delta": 0.001, "mode": "max" },
"grad_clip_norm": 1.0,
"use_amp": true
},
"device": "cuda"
}
Accuracy вышла 88.5% – на 4.5 п.п. ниже baseline, с переобучением +6.5 п.п. train−val. Здесь подвёл learning rate: на AdamW великоват – он слишком грубо “расталкивает” предобученные признаки, вместо того чтобы аккуратно их подстроить. И AUROC 0.993 с этим согласуется (не доказывает, а согласуется): архитектура выбрана правильно, признаки модель ухватила – недокручен режим fine-tuning, а не сама модель. Более консервативная подстройка (меньший lr, дольше обучение) почти наверняка добрала бы недостающие проценты. Это та зона, где итерациям ещё есть куда копать глубже, и где видно, что упирается всё в настройку платформы, а не в её потолок.
А дальше – самое интересное про итерации, потому что путь у агентов был не по прямой. Получив первую efficientnet_b0, они попробовали два разных хода, и оба поучительны. Сначала – лобовой “добавить регуляризации и аугментаций посильнее”, и сами же зафиксировали, что это сделало только хуже:
«усиленная регуляризация и агрессивные аугментации привели к сильному снижению качества … такая комбинация RandAugment+сильный ColorJitter для fine-grained пород избыточна и разрушает полезные признаки.» — ML Model Production Readiness Expert
Потом сменили курс на более тяжёлую и современную tf_efficientnetv2_s, рассчитывая дотянуться до 93%:
«Выбран backbone tf_efficientnetv2_s как разумный компромисс между качеством и вычислительной стоимостью: он существенно компактнее тяжёлых ConvNeXt/ViT, но заметно сильнее классического EfficientNet-B0.» — ML Engineer
Но и она дала лишь 88.1% – то есть более тяжёлая сеть так и не превзошла простую b0. Трезвый сигнал: дело тут не в размере модели, а в том самом режиме дообучения. И снова – никакого приукрашивания со стороны платформы. Финальный агент прямо назвал и недобор по целевой метрике, и переобучение, не пряча их за высоким AUROC:
«Ни одна из трёх обученных версий … не достигает целевого порога accuracy≥0.93 на тестовом сплите … Наблюдается выраженное переобучение: train≈1.0 против test≈0.88.» — ML Model Production Readiness Expert
Проверка догадки. Гипотезу про lr я тоже проверил руками, а не оставил на честном слове. Взял ту самую более тяжёлую
tf_efficientnetv2_s, что пробовали агенты, те же аугментации – но learning rate спустил с1e-3до2e-4, а обучение чуть длиннее (80 эпох вместо 60). Прогнал через платформу – accuracy выросла с 88.1% до 91.0% (+2.9 п.п.). Разрыв с baseline сократился до −2.0 п.п. (в пределах допуска), а переобучение просело почти вдвое (с +9.5 до ~+4 п.п. train−val). Один лишь бережный lr добрал почти три процента и заодно усмирил переобучение.
Deepfake – accuracy 98.3% при baseline 98.0%
Для дополнительной проверки я сравнил платформу с решением задачи классификации deepfake действующим Notebooks Master`ом из Kaggle(далее буду упоминать автора под его ником “MuqaddasEjaz”). Предобработка датасетов (только деление на train/val/test) выполнялась точно так же, как у MuqaddasEjaz.
Датасеты используемые для решения этой задачи:
Для начала я написал скрипт для скачивания требуемых датасетов из Kaggle и создания из датасетов единого мета-датасета в точности как в ноутбуке. И при запуске Computer Vision Dataset Analyst выявил в мета-датасете несколько проблем: больше полумиллиона картинок, но с дубликатами, утечкой между train/val/test (одни и те же изображения в разных сплитах) и шумными метками от шести разных источников. Вердикт:
«🟥 Не готов к обучению…» — Computer Vision Dataset Analyst
Скриншоты ответа аналитика данных
Но мне же интересно сравнить платформу с работой человека и посмотреть, что будет дальше. Поэтому я сознательно отключил вето аналитика и продавил обучение на свой риск – платформа это позволяет, отметив в системном логе: «Аналитик данных забраковал датасет, но проверка отключена при запуске. Продолжаем обучение на свой риск». И вот тут начинается интересная часть, которую я и хочу разобрать, что предлагали агенты и как аргументировали.
Итерация 1. ML Engineer не стал делать вид, что данные чистые – он прямо заложил риски в конфиг: взял предобученный resnet50, добавил label_smoothing=0.1 против шумных меток и набор аугментаций (включая GaussianBlur) против дубликатов и разнобоя источников. Обучение на 500k+ картинок шло примерно пять с половиной часов и дало вполне приличную модель:
«Test accuracy: 0.9833 (≈98.33%) … Test AUROC ~0.9984 … kappa ~0.967 … Модель хорошо обобщает: разрыв между train (98.59%) и val/test (≈98.3%) небольшой.» — ML Model Metrics Analyst
Полный конфиг обучения
{
"model_params": { "type": "resnet50", "pretrained": true },
"data_loader_params": {
"batch_size": 64,
"num_workers": 4,
"dataset_id": "97e60848-c175-4409-9dd1-5882fb2ffaf4",
"img_h_size": null,
"img_w_size": null,
"version_id": "ccc63255-8257-4eea-b524-81cdffa97461",
"train_transforms_config": [
{ "name": "RandomResizedCrop", "params": { "scale": [0.6, 1], "size": [224, 224] } },
{ "name": "RandomHorizontalFlip", "params": { "p": 0.5 } },
{ "name": "ColorJitter", "params": { "brightness": 0.2, "contrast": 0.2, "saturation": 0.2, "hue": 0.05 } },
{ "name": "GaussianBlur", "params": { "kernel_size": 3, "sigma": [0.1, 1] } },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.485, 0.456, 0.406], "std": [0.229, 0.224, 0.225] } }
],
"val_and_test_transforms_config": [
{ "name": "Resize", "params": { "size": [224, 224] } },
{ "name": "ToTensor", "params": {} },
{ "name": "Normalize", "params": { "mean": [0.485, 0.456, 0.406], "std": [0.229, 0.224, 0.225] } }
]
},
"trainer_params": {
"epochs": 60,
"loss_fn": { "name": "CrossEntropyLoss", "params": { "reduction": "mean", "label_smoothing": 0.1 } },
"optimizer": { "name": "AdamW", "params": { "lr": 0.001, "weight_decay": 0.01 } },
"scheduler": { "name": "CosineAnnealingLR", "params": { "T_max": 60, "eta_min": 1e-06 } },
"early_stop": { "metric_name": "accuracy", "patience": 6, "min_delta": 0.0005, "mode": "max" },
"grad_clip_norm": 1.0,
"use_amp": true
},
"device": "cuda"
}
Казалось бы – почти как у MuqaddasEjaz (~99%), бери и радуйся. Но бизнес-цель я сформулировал жёстко – «максимальный accuracy, как у конкурентов, 99%» и агент-аналитик метрик не стал округлять 98.33% в 99%:
«… по бизнес-критерию “как у конкурентов: 99% accuracy” требование не выполнено … Также нет информации, что данные/сплит/метод подсчёта метрики точно совпадают с конкурентами; без этой уверенности честная позиция – считать, что требование не достигнуто.» — ML Model Metrics Analyst
Тонкий момент, который мне понравился: аналитик не просто увидел недобор в 0.7 п.п., он сам заметил, что сравнивать вслепую некорректно (он не знает, что у “конкурентов” данные точь-в-точь).
А дальше началась погоня за метрикой. Раз 99% не взято, внешний цикл пошёл на новую итерацию, и агенты начали наращивать мощность, чтобы добрать недостающие доли процента:
-
итерация 2 –
tf_efficientnet_b4с входом 380×380; -
итерация 3 –
tf_efficientnet_b5с входом 456×456.
Логика агентов вполне понятна: ограничений по железу я не задал, значит можно брать тяжёлые модели и разрешение повыше. Вот только на датасете в полмиллиона картинок это означает совсем другое время обучения – одна эпоха стала занимать больше восьми часов. И обе последние попытки я остановил руками: ждать 8+ часов на эпоху ради гипотетических +0.5 п.п. на датасете, который аналитик с самого начала пометил как грязный, занятие на любителя. Платформа это зафиксировала так: «Обучение остановлено пользователем … Разбираем частичные метрики, чтобы понять причину», а затем – «Работа агентов остановлена пользователем». Вы скажете, что тут нужно добавить взаимосвязь, чтоб агенты понимали причину остановки и вы совершенно правы. Если бы такая возможность была то скорее всего наши агенты начали бы искать конфигурацию для обучения с более простой моделью и подбирать под неё конфигурации, но увы этого ещё нет в платформе.
И главный вывод тут вовсе не “надо было слушаться вето”. Наоборот – я считаю 98.33% отличным результатом: платформа автономно, на сыром объединённом датасете, фактически вышла на уровень конкурента. Откуда тогда те самые 0.7 п.п. недобора? Достаточно посмотреть, чем эти 99% брали у MuqaddasEjaz – там не CNN, а ViT с кастомной головой (LayerNorm -> Dropout -> Linear(512) -> GELU -> ещё один LayerNorm/Dropout -> классификатор), то есть принципиально другой класс архитектуры и более тонко настроенная регуляризация:
# код из ноутбука MuqaddasEjaz
class ViTDeepFakeDetector(nn.Module):
def __init__(self, num_classes=2, dropout=0.4):
...
self.backbone = timm.create_model(
CFG['model_name'], pretrained=True, num_classes=0, global_pool='token')
self.head = nn.Sequential(
nn.LayerNorm(feat_dim), nn.Dropout(p=dropout),
nn.Linear(feat_dim, 512), nn.GELU(),
nn.LayerNorm(512), nn.Dropout(p=dropout * 0.67),
nn.Linear(512, num_classes),
)
А наши агенты пошли проверенным путём CNN c предобучением – resnet50 и далее семейство EfficientNet. Это не хуже и не лучше, это другой инструмент под ту же задачу, и разница между ними в районе одного процента и это то, что отделяет крепкое стандартное решение от вылизанного под конкретный датасет.
Что кейс действительно подсветил – так это дырку в обратной связи, о которой я писал абзацем выше: агенты наращивали мощность (а с ней и время эпохи до 8+ часов), не понимая, что я остановил обучение не из-за плохих метрик, а из-за стоимости. Дай я им этот сигнал – они бы наверняка свернули в сторону модели полегче, а не тяжелее.
Итог:
-
На двух из пяти бенчмарк-датасетов человек не нужен вообще (с натяжкой можно сказать на трёх, если считать Food-101 с урезанным количеством данных): принёс данные – забрал модель, которая бьёт или превосходит публичный baseline. Ещё на трёх платформа отработала так же автономно – просто там есть что разобрать по метрикам. И в шестом тесте, deepfake-кейсе, где на мета-датасете из 6 датасетов платформа автономно вышла на уровень живого Kaggle-мастера, где ориентиром был уже не baseline, а человек.
-
Дефолты не стыдные. Предобученные модели, аугментации под задачу, AdamW/SGD + CosineAnnealingLR, label smoothing, early stopping, AMP – агенты собирают конфиги, которые не стыдно показать живому ML-инженеру.
-
Recovery действительно спасает. До четырёх итераций подряд: первая модель слабая – платформа не отдаёт мусор, а думает заново.
-
Адаптация стратегии на лету. На Food-101 агенты сами диагностировали переобучение прошлой попытки и сменили архитектуру.
-
Полная прозрачность. Каждое решение задокументировано и читается в UI – видно не только что выбрано, но и почему.
-
Недобор по accuracy оказался полировкой, а не потолком – и это проверено числом. На обоих “слабых” датасетах система не стала прятаться за метрику.
2 из 5 датасетов – выше публичный baseline для ResNet50 transfer learning по accuracy, причём оба уверенно. Третий “успешный” кейс – Food-101 – я сознательно гонял на урезанном датасете (100 картинок на класс). И всё это полностью автономно: от датасета до обученной модели и отчёта о качестве, с подробным логом рассуждений, который не нужно принимать на веру – его можно открыть и проверить.
А два оставшихся датасета – это не “провалы платформы”, а её зоны роста с измеренным потенциалом. И ключевое слово здесь – измеренным. Я взял каждый из двух недотянувших кейсов, поправил по одному осмысленному рычагу и точечный повторный прогон вернул большую часть отставания, загнав разрыв с baseline в разумный допуск. Это принципиально другая ситуация, чем “не работает”.
Таким образом инструмент уже сейчас закрывает типовые задачи классификации изображений “под ключ”. А прозрачность рассуждений значит, что каждый его шаг можно не только использовать, но и перепроверить – что для AutoML, на мой взгляд, важнее любой одной красивой цифры, будь то accuracy или эффектный AUROC.
Если дочитали досюда – спасибо. Платформа ещё в пути, и я буду рад вопросам и критике в комментариях.
Источники:
-
Kornblith et al., Do Better ImageNet Models Transfer Better?, arXiv:1805.08974, 2018.
Автор: sSindiKk


