A Recipe for Training Neural Networks
Андрей Карпати делится практическим рецептом обучения нейронных сетей, основанным на двух ключевых наблюдениях: обучение нейросетей — это «дырявая абстракция», а ошибки в нём проявляются молча, не вызывая исключений. Поэтому подход «быстро и яростно» не работает — нужны терпение, внимательность к деталям и одержимость визуализациями. Автор описывает шестиэтапный процесс: глубокое изучение данных, построение end-to-end скелета обучения с глупыми бейзлайнами, переобучение на большой модели, регуляризация, тонкая настройка и финальное «выжимание сока». На каждом шаге даются конкретные советы: фиксировать random seed, проверять loss при инициализации, переобучать на одном батче, использовать Adam с lr 3e-4, не быть героем и копировать архитектуру из ближайшей статьи, предпочитать random search перед grid search, применять ансамбли и не останавливать обучение слишком рано. Главный посыл — строить от простого к сложному, формулируя гипотезы и проверяя их экспериментами.
Несколько недель назад я опубликовал твит о «самых распространённых ошибках при работе с нейросетями», перечислив несколько типичных подводных камней, связанных с обучением нейронных сетей. Твит получил гораздо больше отклика, чем я ожидал (включая вебинар :)). Очевидно, многие лично сталкивались с огромной пропастью между «вот так работает свёрточный слой» и «наша свёрточная сеть достигает state of the art».
Поэтому я решил, что будет интересно стряхнуть пыль со своего блога и развернуть твит в полноценную статью, которой эта тема заслуживает. Однако вместо того чтобы перечислять ещё больше распространённых ошибок или расписывать их детальнее, я хотел копнуть глубже и поговорить о том, как избежать этих ошибок в принципе (или исправлять их очень быстро). Хитрость в том, чтобы следовать определённому процессу, который, насколько я могу судить, нечасто документируется. Начнём с двух важных наблюдений, которые его мотивируют.
1) Обучение нейросетей — это дырявая абстракция
Якобы легко начать обучать нейросети. Многочисленные библиотеки и фреймворки гордо демонстрируют чудо-сниппеты в 30 строк, решающие ваши задачи с данными, создавая (ложное) впечатление, что это технология plug-and-play. Часто можно увидеть нечто вроде:
>>> your_data = # подключите сюда свой потрясающий датасет >>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer) # покоряем мир здесь
Эти библиотеки и примеры активируют ту часть нашего мозга, которая привыкла к обычному софту — где чистые API и абстракции часто достижимы. Покажем на примере библиотеки Requests:
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass')) >>> r.status_code 200
Здорово! Смелый разработчик взял на себя бремя понимания query-строк, URL, GET/POST запросов, HTTP-соединений и так далее, и в значительной мере спрятал сложность за несколькими строками кода. Это то, к чему мы привыкли и чего ожидаем. К сожалению, нейросети совсем не такие. Они не являются «коробочной» технологией в тот момент, когда вы хоть немного отходите от обучения классификатора на ImageNet. Я пытался донести эту мысль в посте «Yes you should understand backprop», выбрав для критики backpropagation и назвав его «дырявой абстракцией», но ситуация, к сожалению, гораздо плачевнее. Backprop + SGD не превращает магически вашу сеть в работающую. Batch norm магически не заставляет её сходиться быстрее. RNN магически не позволяют вам «подключить» текст. И то, что вы можете сформулировать задачу как RL, не означает, что нужно это делать. Если вы упорно используете технологию, не понимая, как она работает, вы, скорее всего, потерпите неудачу. Что подводит меня к…
2) Обучение нейросетей падает молча
Когда вы ломаете или неверно настраиваете код, вы обычно получаете какое-то исключение. Подсунули integer туда, где ожидалась строка. Функция принимала только 3 аргумента. Этот импорт не прошёл. Этого ключа не существует. Количество элементов в двух списках не совпадает. Кроме того, часто можно написать unit-тесты для определённой функциональности.
Это лишь начало, когда речь идёт об обучении нейросетей. Всё может быть синтаксически правильно, но при этом в целом устроено неверно — и это очень сложно заметить. «Поверхность возможных ошибок» большая, логическая (а не синтаксическая) и очень трудно покрывается unit-тестами. Например, возможно, вы забыли отразить метки, когда отразили изображение по горизонтали при аугментации данных. Ваша сеть всё равно (поразительно) может работать вполне неплохо, потому что внутри она научится распознавать отражённые изображения и затем зеркалить свои предсказания. Или, может быть, ваша авторегрессионная модель случайно принимает на вход то, что пытается предсказать, из-за off-by-one бага. Или вы пытались клипнуть градиенты, но вместо этого клипнули loss, из-за чего выбросы игнорировались при обучении. Или вы инициализировали веса из предобученного чекпоинта, но не использовали оригинальное среднее. Или вы просто накосячили с настройками силы регуляризации, learning rate, его decay rate, размером модели и т.д. Поэтому ваша неправильно настроенная нейросеть выбросит исключения только если вам повезло; в большинстве случаев она обучится, но молча будет работать чуть хуже.
В результате (и это нееевероятно сложно переоценить) подход «быстро и яростно» к обучению нейросетей не работает и приводит только к страданиям. Страдания — совершенно естественная часть процесса получения хорошо работающей нейросети, но их можно смягчить, будучи скрупулёзным, оборонительным, параноидальным и одержимым визуализациями буквально всего возможного. Качества, которые по моему опыту сильнее всего коррелируют с успехом в deep learning — это терпение и внимание к деталям.
Рецепт
В свете двух фактов выше я выработал для себя определённый процесс, которому следую, применяя нейросеть к новой задаче, и который попытаюсь описать. Вы увидите, что он очень серьёзно относится к двум вышеназванным принципам. В частности, он строится от простого к сложному, и на каждом шаге мы формулируем конкретные гипотезы о том, что произойдёт, и затем либо подтверждаем их экспериментом, либо разбираемся до тех пор, пока не найдём какую-то проблему. Чего мы очень стараемся избегать — это введения большого количества «непроверенной» сложности сразу, что неминуемо приведёт к багам/неправильным настройкам, которые потом будут искать вечно (если вообще найдут). Если бы написание кода нейросети было похоже на её обучение, вы бы использовали очень маленький learning rate и угадывали бы, а затем оценивали полный тестовый набор после каждой итерации.
1. Слейся с данными
Первый шаг к обучению нейросети — вообще не трогать код нейросети, а начать с тщательного изучения ваших данных. Этот шаг критичен. Я люблю тратить уйму времени (измеряемого в часах), просматривая тысячи примеров, понимая их распределение и ища закономерности. К счастью, ваш мозг неплохо в этом разбирается. Однажды я обнаружил, что данные содержат дубликаты примеров. В другой раз я нашёл повреждённые изображения / метки. Я ищу дисбалансы и предвзятости в данных. Я также обычно обращаю внимание на собственный процесс классификации данных — это намекает на то, какие архитектуры мы в итоге будем исследовать. Например, достаточно ли локальных признаков или нужен глобальный контекст? Насколько велика вариативность и какую форму она принимает? Какая вариативность является шумовой и может быть устранена препроцессингом? Имеет ли значение пространственное положение или мы хотим усреднить его через pooling? Насколько важны детали и насколько мы можем позволить себе уменьшить разрешение изображений? Насколько шумные метки?
Кроме того, поскольку нейросеть фактически является сжатой/скомпилированной версией вашего датасета, вы сможете посмотреть на (не)верные предсказания вашей сети и понять, откуда они могут идти. И если ваша сеть выдаёт предсказание, которое не согласуется с тем, что вы видели в данных, что-то не так.
Получив качественное понимание, также неплохо написать простой код для поиска/фильтрации/сортировки по чему угодно (например, по типу метки, размеру аннотаций, количеству аннотаций и т.д.), визуализировать их распределения и выбросы по любым осям. Особенно выбросы почти всегда вскрывают какие-то баги в качестве данных или препроцессинге.
2. Настрой end-to-end скелет обучения/оценки + получи глупые бейзлайны
Теперь, когда мы понимаем данные, можем ли мы дотянуться до нашего супер-крутого Multi-scale ASPP FPN ResNet и начать обучать потрясающие модели? Однозначно нет. Это путь к страданиям. Наш следующий шаг — настроить полный скелет обучения + оценки и обрести уверенность в его корректности через серию экспериментов. На этом этапе лучше всего выбрать какую-то простую модель, которую вы никак не могли бы сломать — например, линейный классификатор или совсем крошечную ConvNet. Мы захотим её обучить, визуализировать loss, любые другие метрики (например, accuracy), предсказания модели и провести серию ablation-экспериментов с явными гипотезами по ходу дела.
Советы и трюки для этого этапа:
зафиксируй random seed. Всегда используйте фиксированный random seed, чтобы при двукратном запуске кода вы получали один и тот же результат. Это убирает фактор вариативности и поможет сохранить рассудок. упрощай. Обязательно отключите всё лишнее. Например, точно отключите любую аугментацию данных на этом этапе. Аугментация данных — это стратегия регуляризации, которую мы можем подключить позже, но сейчас это просто ещё одна возможность ввести какой-то глупый баг. добавь значащие цифры в evaluation. При построении графика test loss запускайте оценку на всём (большом) тестовом наборе. Не просто стройте test loss по батчам и полагайтесь на сглаживание в Tensorboard. Мы стремимся к корректности и охотно жертвуем временем ради сохранения рассудка. проверь loss @ init. Убедитесь, что ваш loss стартует с правильного значения. Например, если вы правильно инициализировали финальный слой, на softmax при инициализации вы должны измерить -log(1/n_classes). Те же дефолтные значения можно вывести для L2-регрессии, Huber losses и т.д. хорошо инициализируй. Корректно инициализируйте веса финального слоя. Например, если вы делаете регрессию значений со средним 50, инициализируйте финальный bias значением 50. Если у вас несбалансированный датасет с соотношением 1:10 positives:negatives, установите bias на логитах так, чтобы при инициализации сеть предсказывала вероятность 0.1. Правильная установка этого ускорит сходимость и устранит «хоккейные» кривые loss, где в первые несколько итераций сеть, по сути, просто учит bias. человеческий бейзлайн. Отслеживайте метрики, помимо loss, которые интерпретируемы человеком и проверяемы (например, accuracy). При возможности оценивайте свою собственную (человеческую) accuracy и сравнивайте с ней. Альтернативно, аннотируйте тестовые данные дважды и для каждого примера трактуйте одну аннотацию как предсказание, а вторую как ground truth. input-independent бейзлайн. Обучите input-independent бейзлайн (например, проще всего — обнулить все входы). Это должно работать хуже, чем когда вы реально подключаете данные без обнуления. Работает? То есть учится ли ваша модель извлекать какую-либо информацию из входа вообще? переобучи один батч. Переобучитесь на одном батче из всего нескольких примеров (например, всего двух). Для этого мы увеличиваем ёмкость модели (например, добавляем слои или фильтры) и проверяем, что можем достичь наименьшего возможного loss (например, нуля). Также мне нравится визуализировать на одном графике и метку, и предсказание и убеждаться, что они в итоге идеально совпадают, когда мы достигаем минимума loss. Если нет — где-то баг, и мы не можем двигаться к следующему этапу. проверь убывание training loss. На этом этапе вы, надеюсь, будете недообучаться на датасете, потому что работаете с игрушечной моделью. Попробуйте слегка увеличить её ёмкость. Уменьшился ли training loss, как должен был? визуализируй прямо перед сетью. Однозначно правильное место для визуализации данных — это непосредственно перед y_hat = model(x) (или sess.run в tf). То есть вы хотите визуализировать именно то, что попадает в сеть, декодируя сырой тензор данных и меток в визуализации. Это единственный «источник истины». Я не могу сосчитать, сколько раз это меня спасало и вскрывало проблемы в препроцессинге данных и аугментации. визуализируй динамику предсказаний. Мне нравится визуализировать предсказания модели на фиксированном тестовом батче в ходе обучения. «Динамика» того, как двигаются эти предсказания, даст вам невероятно хорошую интуицию о ходе обучения. Часто можно почувствовать, как сеть «борется» с подгонкой к данным, если она слишком сильно колеблется в каком-то виде, что выдаёт нестабильности. Очень низкие или очень высокие learning rate также легко заметны по амплитуде дрожания. используй backprop для построения зависимостей. Ваш код deep learning часто содержит сложные, векторизованные и broadcast-операции. Относительно распространённый баг, который я встречал несколько раз — это когда люди делают это неверно (например, используют view вместо transpose/permute где-то) и непреднамеренно смешивают информацию по размерности батча. Удручающий факт состоит в том, что ваша сеть обычно всё равно неплохо обучится, потому что научится игнорировать данные из других примеров. Один из способов отладить это (и другие связанные проблемы) — задать loss как что-то тривиальное, например, сумму всех выходов примера i, прогнать backward pass до самого входа и убедиться, что ненулевой градиент вы получаете только на i-том входе. Та же стратегия может использоваться, например, для проверки того, что ваша авторегрессионная модель в момент t зависит только от 1..t-1. В целом, градиенты дают вам информацию о том, что от чего зависит в вашей сети, что может быть полезно для отладки. обобщай частный случай. Это уже более общий совет по программированию, но я часто видел, как люди создают баги, когда откусывают больше, чем могут прожевать, написав сразу с нуля довольно общую функциональность. Мне нравится писать очень специфичную функцию для того, что я делаю прямо сейчас, заставлять её работать, а затем обобщать её позже, убеждаясь, что получаю тот же результат. Часто это применимо к векторизации кода, где я почти всегда сначала пишу полностью циклическую версию и только потом превращаю её в векторизованный код, один цикл за раз.
3. Переобучи
К этому моменту мы должны хорошо понимать датасет, и у нас есть рабочий полный пайплайн обучения + оценки. Для любой данной модели мы можем (воспроизводимо) посчитать метрику, которой доверяем. Также мы вооружены результатом input-independent бейзлайна, результатами нескольких глупых бейзлайнов (которые нам лучше превзойти), и у нас есть грубое представление о человеческом результате (которого мы надеемся достичь). Теперь сцена готова для итераций по поиску хорошей модели.
Подход, который я предпочитаю для поиска хорошей модели, состоит из двух стадий: сначала получить модель, достаточно большую, чтобы переобучаться (т.е. фокус на training loss), а затем регуляризовать её надлежащим образом (пожертвовать частью training loss ради улучшения validation loss). Причина, по которой мне нравятся эти две стадии, в том, что если мы не способны достичь низкого error rate ни с какой моделью вообще, это снова может указывать на какие-то проблемы, баги или неправильную настройку.
Несколько советов и трюков для этого этапа:
выбор модели. Чтобы достичь хорошего training loss, вам нужно подобрать подходящую архитектуру под данные. Когда дело доходит до этого выбора, мой совет №1: Не будь героем. Я видел много людей, которым не терпится начать креативить и сходить с ума, складывая лего-блоки нейросетевого инструментария в различные экзотические архитектуры, кажущиеся им осмысленными. Решительно сопротивляйтесь этому искушению на ранних стадиях проекта. Я всегда советую людям просто найти наиболее релевантную статью и copy-paste-нуть оттуда простейшую архитектуру, дающую хороший результат. Например, если вы классифицируете изображения, не будьте героем и просто copy-paste-ните ResNet-50 для первого запуска. Что-то более кастомное вам разрешается сделать позже и превзойти это. Adam безопасен. На ранних стадиях установки бейзлайнов мне нравится использовать Adam с learning rate 3e-4. По моему опыту Adam гораздо более прощающий по отношению к гиперпараметрам, включая плохой learning rate. Для ConvNet хорошо настроенный SGD почти всегда слегка обходит Adam, но область оптимальных learning rate гораздо уже и специфична для задачи. (Замечание: если вы используете RNN и связанные sequence-модели, более распространено использовать Adam. На начальной стадии проекта, опять же, не будьте героем и следуйте тому, что делают самые близкие по теме статьи.) усложняй только по одному за раз. Если у вас есть несколько сигналов для подачи в классификатор, я бы посоветовал подключать их по одному и каждый раз убеждаться, что вы получаете ожидаемый прирост качества. Не швыряйте в модель всё подряд с самого начала. Есть и другие способы наращивать сложность — например, можно сначала подавать меньшие изображения, а потом увеличивать их и т.д. не доверяй дефолтам learning rate decay. Если вы переиспользуете код из другой области, всегда будьте очень осторожны с learning rate decay. Мало того, что для разных задач вы захотите разные schedule'ы decay, так ещё и — что хуже — в типичной реализации schedule основан на текущем номере epoch, который может сильно варьироваться просто в зависимости от размера вашего датасета. Например, ImageNet делает decay в 10 раз на 30-й epoch. Если вы не обучаете ImageNet, вам почти наверняка такое не нужно. Если вы невнимательны, ваш код может тайно сбрасывать learning rate в ноль слишком рано, не давая модели сойтись. В своей работе я всегда полностью отключаю learning rate decay (использую константный LR) и подбираю это в самом конце.
4. Регуляризуй
В идеале мы теперь в точке, где у нас есть большая модель, по крайней мере подгоняющая training set. Теперь время её регуляризовать и получить какую-то validation accuracy, пожертвовав частью training accuracy. Несколько советов и трюков:
добывай больше данных. Во-первых, безусловно лучший и предпочитаемый способ регуляризации модели в любых практических условиях — добавить больше реальных тренировочных данных. Очень распространённая ошибка — тратить кучу инженерных циклов, пытаясь выжать сок из маленького датасета, когда вместо этого можно было бы собирать больше данных. Насколько мне известно, добавление большего количества данных — это практически единственный гарантированный способ монотонно улучшать качество хорошо настроенной нейросети практически бесконечно. Другим был бы ensemble (если можете себе позволить), но он перестаёт давать прирост примерно после 5 моделей. аугментируй данные. Следующее лучшее после реальных данных — полуподдельные данные: попробуйте более агрессивную аугментацию данных. креативная аугментация. Если полуподдельные данные не помогают, поддельные данные тоже могут что-то дать. Люди находят креативные способы расширения датасетов: например, domain randomization, использование симуляций, хитрые гибриды вроде вставки (возможно, симулированных) данных в сцены, или даже GAN. предобучение. Редко когда вредно использовать предобученную сеть, если можете, даже если у вас достаточно данных. придерживайся supervised learning. Не слишком восторгайтесь unsupervised pretraining. В отличие от того, что говорит вам тот блог-пост из 2008 года, насколько мне известно, ни одна его версия не показала сильных результатов в современном computer vision (хотя NLP, похоже, неплохо себя чувствует с BERT и компанией в последнее время, что весьма вероятно связано с более осмысленной природой текста и более высоким соотношением сигнал/шум). меньшая размерность входа. Удалите признаки, которые могут содержать ложный сигнал. Любой добавленный ложный вход — это просто ещё одна возможность переобучиться, если ваш датасет небольшой. Аналогично, если низкоуровневые детали не очень важны, попробуйте подать меньшее изображение. меньший размер модели. Во многих случаях вы можете использовать доменные знания как ограничения на сеть, чтобы уменьшить её размер. Например, раньше было модно использовать Fully Connected слои поверх backbone для ImageNet, но с тех пор их заменили на простой average pooling, устранив тонну параметров в процессе. уменьши размер батча. Из-за нормализации внутри batch norm меньшие размеры батча в каком-то смысле соответствуют более сильной регуляризации. Это связано с тем, что эмпирические mean/std батча — это более приблизительные версии полных mean/std, поэтому scale & offset «болтают» ваш батч сильнее. drop. Добавьте dropout. Используйте dropout2d (spatial dropout) для ConvNet. Используйте это умеренно/осторожно, потому что dropout не очень хорошо ладит с batch normalization. weight decay. Увеличьте штраф weight decay. early stopping. Останавливайте обучение на основе измеренного validation loss, чтобы поймать модель прямо в тот момент, когда она вот-вот переобучится. попробуй модель побольше. Упоминаю это последним и только после early stopping, но в прошлом несколько раз я обнаруживал, что большие модели, конечно, переобучаются гораздо сильнее в итоге, но их «early stopped» качество часто может быть гораздо лучше, чем у меньших моделей.
Наконец, чтобы получить дополнительную уверенность, что ваша сеть — разумный классификатор, мне нравится визуализировать веса первого слоя сети и убеждаться, что вы получаете красивые осмысленные грани. Если фильтры вашего первого слоя выглядят как шум, что-то может быть не так. Аналогично, активации внутри сети иногда могут показывать странные артефакты и намекать на проблемы.
5. Настраивай
Теперь вы должны быть «в теме» с вашим датасетом, исследуя широкое пространство моделей в поисках архитектур, дающих низкий validation loss. Несколько советов и трюков для этого шага:
random search вместо grid search. Для одновременной настройки нескольких гиперпараметров может показаться заманчивым использовать grid search, чтобы обеспечить покрытие всех настроек, но имейте в виду, что лучше использовать random search. Интуитивно это потому, что нейросети часто гораздо чувствительнее к одним параметрам, чем к другим. В пределе, если параметр a важен, а изменение b не имеет эффекта, вы предпочли бы сэмплировать a более тщательно, чем в нескольких фиксированных точках по многу раз. оптимизация гиперпараметров. Существует большое количество модных байесовских инструментов для оптимизации гиперпараметров, и несколько моих друзей сообщали об успехе с ними, но мой личный опыт говорит, что state-of-the-art подход к исследованию хорошего и широкого пространства моделей и гиперпараметров — это использовать стажёра :). Шучу.
6. Выжми сок
Как только вы нашли лучшие типы архитектур и гиперпараметров, вы можете использовать ещё несколько трюков, чтобы выжать последние капли сока из системы:
ансамбли. Ансамбли моделей — это практически гарантированный способ получить 2% accuracy на чём угодно. Если не можете позволить себе вычисления на тесте, посмотрите в сторону дистилляции вашего ансамбля в одну сеть с помощью dark knowledge. оставь её обучаться. Я часто видел, как у людей возникает соблазн остановить обучение модели, когда validation loss, кажется, выходит на плато. По моему опыту сети продолжают обучаться неинтуитивно долгое время. Один раз я случайно оставил модель обучаться на зимние каникулы, и когда я вернулся в январе, это был SOTA («state of the art»).
Заключение
Как только вы дойдёте сюда, у вас будут все ингредиенты для успеха: вы глубоко понимаете технологию, датасет и задачу, вы настроили всю инфраструктуру обучения/оценки и достигли высокой уверенности в её корректности, и вы исследовали всё более сложные модели, получая прирост качества способами, которые предсказывали на каждом шагу. Теперь вы готовы прочитать кучу статей, провести большое количество экспериментов и получить свои SOTA-результаты. Удачи!