Научить автомобиль ездить самостоятельно при помощи машинного обучения
Перевод выступления технического директора Overleaf Джона Лиса-Миллера.
Введение
Этот проект был личным, но в некоторой степени он связан с моей работой. Я занимаю должность технического директора в Overleaf, онлайн-редакторе LaTeX. Сегодня платформа насчитывает 3 млн пользователей.
Я и мой коллега-сооснователь прежде занимались беспилотными автомобилями.
Overleaf мы запустили, одновременно проектируя Heathrow Pods, первую в мире систему беспилотных такси. Она открылась в 2011 году и до сих пор перевозит пассажиров.
Ещё в 2011 году нам удалось создать сеть беспилотников благодаря отдельной сети дорог. Heathrow Pods — закрытая система, поэтому мы полагались на традиционные методы разработки и проверки безопасности.
Вместе с тем большинство современных исследований автономных машин нацелены на то, чтобы вывести их на общественные дороги, где нужно иметь в виду водителей, пешеходов, велосипедистов, светофоры, знаки и много чего ещё.
Один из пионеров этой области — Себастьян Трун, профессор Стэнфорда и победитель гонки DARPA Grand Challenge в 2005 году, во многом ознаменовавшей современную эпоху беспилотников. Позже Трун основал платформу онлайн-обучения Udacity. В 2016 году на сайте появился курс по беспилотным машинам. Я записался.
В основе моего выступления — одна из лабораторных работ курса, которая, в свою очередь, опирается на доклад Nvidia «Сквозное обучение в беспилотных автомобилях».
Команда производителя показала: снимки, сделанные камерой на переднем бампере, можно поместить в свёрточную нейронную сеть (о ней мы ещё поговорим), а она выведет команды для управления машиной. Короче говоря, как вы смотрите на дорогу впереди, решая, дать ли рулём вправо или влево, так и нейросеть.
Подобный подход в корне отличается от привычного метода разработки автономных систем: прежде мы разбивали основную задачу на множество подзадач вроде распознавания объектов, классификации, картирования, планирования и так далее.
Каждую подзадачу обслуживала своя подсистема, затем их объединяли в одну. Здесь же мы обучаем монолитную нейронную сеть, которая каким-то образом управляет автомобилем. Слегка напоминает магию.
Наша задача на той лабораторной — воспроизвести эту магию. Пойдём по тому же пути:
- соберём обучающие данные в симуляторе, управляя машиной вручную;
- обучим нейронную сеть на изображениях с камеры и углах поворота;
- проверим сеть в симуляторе.
Поехали.
Собираем данные для обучения
Сперва соберём данные, самостоятельно управляя машиной в симуляторе. Нейросеть будет подражать моей манере езды: в основном я еду посередине дороги.
В правом верхнем углу видна иконка «Запись»: мы записываем изображение с камеры и углы поворота в каждом кадре десять раз в секунду.
Вы, наверное, заметили, что едет машина неровно. Причина: управлять ей можно лишь стрелками влево-вправо. Поэтому для начала возьмём необработанные рулёжные данные и сгладим их.
Следующее препятствие. Если ехать только по центру, система не поймёт, что делать в случае смещения машины к краю. Дело решается записью возврата на курс: я останавливаю запись, подъезжаю к краю, возобновляю запись и возвращаюсь на середину.
Таким образом машина учится возвращаться в центр трассы (надеюсь, это не научит её съезжать).
Проехавшись по дороге несколько раз и записав возврат на курс, я получил 11 тысяч кадров:
В каком-то смысле данных у нас много, но в мире машинного обучения этого мало. Думаю, с нуля обучить сеть на таком количестве данных будет сложно. Чтобы обойти проблему, задействуем технику переноса обучения.
Перенос обучения
Вкратце: мы возьмём сеть, обученную кем-то для другой задачи, и извлечём небольшую часть, приспособив её для себя.
В нашем проекте мы задействуем сеть Inception v3, обученную Google для состязания по распознаванию изображений. На входе — картинка, на выходе — класс изображения, вещь на нём. Если в систему загрузить фото слева, она скажет, что на нём сибирская хаски, если справа — эскимосская собака (не уверен, что сам бы их различил).
Сеть Inception огромна — более 25 млн параметров; на её обучение Google потратила немало сил и денег. Как же нам её адаптировать? Ответ прост: мы проведём «лоботомию» и вытащим лишь первые 44 слоя (отмечены красной рамкой).
Почему это работает? Дело в том, что вырезанные слои типичны для обработки изображений (например, детекторы границ). Разница между породами собак определяется позже, как и другие классы изображений нам не нужные. Мы дополним группу Inception тремя собственными слоями:
Нам придётся обучить лишь собственные доработки, не трогая секцию Inception. Будем думать, что в таком случае мы обойдёмся меньшим количеством данных, чем если бы начали с нуля.
Добавлю, что архитектура последних трёх слоёв выбрана методом проб и ошибок. Были варианты попроще, но этот оказался самым простым работающим.
Свёрточные нейронные сети
Как же работает такая сеть? Ненадолго погрузимся в теорию. Чтобы создать свёрточную нейронную сеть, нам понадобится три строительных блока:
- свёртка;
- изменение размера (особенно в меньшую сторону);
- функции активации.
Рассмотрим каждый по очереди. В качестве образца возьмём кадр ниже:
Посмотрим на него следующим образом: наша цель для этой сети — от кадра размером 320х160 пикселей прийти к одному числу, равному углу поворота, который машина должна применить, увидев изображение.
Первой идёт свёртка — простая, но универсальная операция. Для её выполнения нам потребуется ядро — небольшая числовая матрица. Мы задействуем ядро 3x3 пикселя.
Начнём с того, что совместим ядро такого же размера с частью изображения (левый верхний угол). Затем перемножим каждый пиксель этой области на соответствующее значение в матрице ядра и сложим произведения — первый пиксель выходного изображения готов. Так мы движемся от пикселя к пикселю, каждый раз прибавляя к изображению на выходе одну точку.
Операция свёртки очень проста, эффективна и полезна. В Photoshop, скажем, большая часть инструментов в меню фильтров так или иначе опирается на свёртку, только ядро у каждой своё. Ядро тождественности нас не особо интересует, поскольку оно просто копирует изображение, но есть, например, ядра для определения границ, размытия и резкости.
Чтобы задействовать свёртку в нейросети, нужно не высчитывать ядра самостоятельно, а позволить системе самой проанализировать данные и вывести множество ядер. Используемые слои Inception располагают около 700 тысячами параметров, немало из которых — параметры ядер, обученные Google на 1,2 млн изображений.
Запуская свёртку с таким большим количеством ядер, мы из одного изображения на входе получаем сразу несколько — по одному на каждое ядро. Но продолжай мы добавлять картинки, память бы закончилась. Поэтому-то нам нужен второй блок — изменение размера.
Тут всё очевидно — время от времени мы уменьшаем размер изображения. Есть много способов это сделать, например, с помощью максимальной подвыборки.
Меняя размер, мы теряем в пространственном разрешении, но приобретаем в глубине. Это позволяет взять плоское изображение на входе и, несколько раз повторив операции свёртки и изменения размера, получить вытянутое «изображение» низкого разрешения и большей глубины.
С глубиной в системе в каком-то смысле развивается понимание — фрагмент изображения из пикселей она превратила в совокупность черт, значимых для решения поставленной задачи.
Перейдём к функциям активации — в словосочетании «нейронная сеть» появляется слово «нейронный».
Биологический нейрон — штука невероятно сложная, с удивительными динамическими свойствами и занимательными типами поведения. Мы смоделируем его в общих чертах.
Так, у нейрона есть дендриты, «входы», соединённые с восходящими нейронами. Если «входы» в сумме преодолевают определённый порог, этот нейрон активируется и отсылает сигнал через аксон в нисходящие нейроны.
Математически это можно представить как простую функцию сдавливания. Если сумма входов отрицательная, на выходе мы получим значение, близкое к нулю, — нейрон не активирован. Если же сумма положительная, на выходе получается значение, близкое к единице, — нейрон активирован.
Вот и всё. Эти три операции мы повторяем снова и снова. Отмечу, однако, что свёртка (по крайней мере, тип используемый здесь) — линейная операция. Не будь изменение размера и функции активации нелинейными, композиция свёрток попросту схлопнулась бы в одну большую линейную функцию.
Но стоит нам добавить чуть-чуть нелинейности, мы от представления исключительно линейных функций переходим к аппроксимации любых.
Сборка
С теорией покончено. Теперь посмотрим, на что способна наша сеть, используя картинку-образец. Пропустив её через 44 слоя сети Inception, мы получим 256 изображений в серых тонах; их стороны примерно в десять раз меньше сторон изображения на входе, но они гораздо глубже.
Вот выборка из девяти случайных изображений, начиная с 42-го. Каждая представляет собой один из откликов, который нейронная сеть даёт на тестовое изображение. Светлый пиксель означает, что в этой точке изображения нейрон активирован, то есть он отвечает на какую-то черту соответствующей части входного изображения. Тёмный пиксель — нейрон не активирован.
Истолковать ответ нейронов будет проще, если наложить его на входное изображение:
На изображении 42 в верхнем левом углу, например, можно увидеть, что нейроны активно реагируют на границы дороги. Во время езды важно знать, где они, стало быть, это изображение будет полезным.
На 43-ем (сверху в середине), похоже, видна поверхность дороги — тоже может пригодиться. Кое-где, правда, проступает фон, но 48 изображение (справа в средней строке) почти полностью берёт его на себя. Комбинация этих изображений, судя по всему, даст нам полезную информацию.
Замечу: для этой части сети мы не указывали черты, необходимые в решении нашей задачи. На самом деле она обучена Google для классификации изображений.
Это переносит нас к фрагменту сети, который мы обучаем сами, — те три слоя, добавленные в конце.
Первый слой — очередная свёртка, только с ядром 1х1. Эта свёртка подбирает заданное число линейных комбинаций изображений, получившихся после обработки Inception; нередко её используют для понижения размерности.
Такой выбор архитектуры обусловлен выше — вместо изображения 42, 43 и 48 могут хорошо работать на распознавание дороги. Мы же хотим, чтобы сеть отбирала самые полезные комбинации, верно?
В этом примере мы выберем 64 линейные комбинации из 256 изображений. Ниже — изображения до и после свёртки с ядром 1х1. Они похожи, но вторая группа в целом ярче и сглаженнее.
И вновь на некоторых изображениях отчётливо проявляются края дороги, например на 45-м (на этот раз из 64 картинок).
Наконец, у нас есть два полносвязных слоя. С этого момента мы не сможем визуализировать результаты так же легко, потому что все значения после свёртки мы поместим в один большой список чисел. Один из слоёв затем вычислит заданное количество линейных комбинаций для всех этих чисел.
Схема справа описывает одну из таких комбинаций — один нейрон — в одном полносвязном слое. Такой слой состоит из множества нейронов, соединнёных со всеми выходами предыдущего слоя. Здесь мы обучаем веса wi и выходы xi. И снова мы применяем функцию активации f к каждому нейрону — вносим нелинейность.
Выходные данные первого полносвязного слоя переходят во второй, а выходные данные второго и есть угол поворота. Архитектура отлажена, мы готовы приступить к обучению.
Обучение
Ну, почти готовы. Даже для последних трёх слоёв требуется выставить немало гиперпараметров, перед тем как полностью определить сеть и сценарии обучения. Сколько ядер необходимо в свёртке 1х1? Насколько велик должен быть каждый полносвязный слой? Какое сглаживание применить к углу поворота?
Чтобы отыскать гиперпараметры, есть способы и получше, но в нашем случае я просто перепробовал все возможные комбинации по большой сетке параметров. На полную обработку сетки уходит много времени, однако это просто вычисление — запустив процесс перед сном, вы получите свежие данные вечером, по возвращению с работы.
Каждая точка в сетке представляет собой одну сеть для обучения и оценки. Мы можем измерить работу каждой и выбрать лучшие настройки гиперпараметров.
К счастью, реальное обучение происходит очень легко благодаря библиотекам вроде Keras. Вот пример результатов обучения для одной из сетей:
Тут много всего интересного, но я прокомментирую следующее. В начале — краткое описание обучаемой модели, включающее несколько параметров, которым нужно соответствовать. Помните: слои Inception остаются неизменными, мы занимаемся обучением лишь блока на конце объединённой сети.
Во второй секции — процесс обучения, который Keras описывает по мере развития. Я разбил собранные данные для обучения на две части: обучающую (80%) и контрольную (20%). Каждую эпоху Keras записывает значение средней абсолютной ошибки в прогнозах сети по обучающей (loss) и контрольной (val_loss) группам.
Для каждого из трёх слоёв обучение начинается со случайно определённых весов. Начальные потери при случайных весах очень велики и начинаются с 79 пунктов. Keras, однако, использует потери для уточнения весов: вновь обрабатывая обучающую группу, система снижает потери с каждой успешной эпохой.
Спустя 17 эпох потери в десятки раз ниже, около 0,08 пункта. Обучение заканчивается тогда, когда начинают расти потери в контрольной выборке (val_loss), — большее число эпох весьма вероятно приведёт к переобучению.
Запускаем модель с самыми низкими потерями в контрольной группе
Повторив процесс обучения на сотнях сетей для всех гиперпараметров, подключим модель с самыми низкими потерями к машине и посмотрим, как она едет.
Переводим симулятор на автономный режим, и машина начинает движение. Я не упоминал о регулировке газа, но в этом случае автомобиль разгоняется до 15 километров в час.
Можно заметить, как машина виляет и возвращается на курс. В конечном счёте она переусердствует и съезжает с дороги. Что ж, старт неплохой: видно, как машина пыталась удержаться на пути.
Доработки
Ищем баги. Множество произвольных решений не были указаны в начальной сетке гиперпараметров, поэтому я начал с того, что добавил несколько. Например:
- Различные функции потерь: как нам измерить ошибку? Применив формулы среднего квадрата ошибки и средней абсолютной ошибки, я остановился на втором варианте.
- Различные функции активации: я попробовал сигмоиду, гиперболический тангенс и выпрямитель; тангенс показал себя лучше.
- Различная регуляризация: отбраковка весов, чтобы они не становились слишком большими, — распространённая практика избежания переобучения; я перебрал несколько весов L2-регуляризации.
- Различные распределения начальных весов решили пару проблем со сходимостью во время обучения.
- Различные размеры слоёв: сколько нейронов в каждом скрытом слое?
- Различные архитектуры сети — добавить ли слоёв? Или убрать?
Тем не менее ничего из перечисленного не помогло. После нескольких дней биений я добавил оператор печати в петлю регулирования. Это быстро вскрыло проблему:
Контроллер тратил слишком много времени на обработку каждого кадра, поэтому подруливать он мог лишь три раза в секунду.
Кроме того, дальнейшее расследование показало: система проводила большую часть времени на слоях Inception. С одной стороны, проблему можно было решить покупкой более быстрого ноутбука. С другой — количество слоёв Inception можно сократить с 44-х до 12-ти (на картинке семь: остальные невидимы).
Уменьшаем задержку
Посмотрим, как система ведёт себя при низкой задержке (около 0,1 секунды вместо 0,35 секунды):
Намного лучше. Я ускорил видео, ведь автомобиль теперь едет гораздо дальше. Виляние уменьшилось, машина держится курса, правда, едва ли выдерживает апекс, но, возможно, потому, что я не особо за ним следил. В конце концов машина добирается до поворота с деревьями за отбойником и уезжает в сторону.
Решение — добавить данных. Я несколько раз проехал этот поворот сам, и система освоилась. Примерно в то же время на Udacity появились данные, где автомобилем управляли с помощью руля, их я тоже добавил. Я также нарастил количество данных, отзеркалив каждый кадр в обучающей группе и заменив угол поворота на отрицательный.
Результат
Со всеми дополнениями сеть смогла проехать полный круг:
Я поменял настройки газа, чтобы машина разгонялась до 50 километров в час — максимальной скорости в этой версии симулятора. Кое-где она по-прежнему виляет, но уже не разбивается. Успех.
Расширение
Итак, мы обучили систему езде на данной трассе. Но что если поместить её на совершенно другую дорогу? К счастью, недавно на Udacity появился второй трек, поэтому давайте проверим. Сеть остаётся прежней, в обучающих данных нет информации о новой дороге.
Едет. И вновь автомобиль сбивают с толку деревья — на 24 секунде он чуть не врезался в ограждение, но дальше всё равно поехал не туда. Учитывая простоту нашей сети по меркам нейросетей и короткое обучение, факт того, что машина так хорошо едет по неизвестному пути, пусть и в том же симуляторе, — довольно неплохой результат.
Заключение
В этом выступлении мы:
- взяли нейронную сеть, обученную распознаванию пород собак;
- переделали её под управление автомобилем;
- научили систему водить, располагая 35 минутами образцов;
- посмотрели, как она едет по незнакомому пути.
Более того:
- в 2015 году мало кто считал, что и это будет возможно;
- в 2016 году тысячи людей вроде меня занимались обучением беспилотников в свободное время на онлайн-курсах;
- в 2018 году я руководил (совсем немного) работой школьника Джоша, который проделал всё это сам на радиоуправляемой машинке с Raspberry Pi.
Код проекта находится в открытом доступе.
Не до конца понятно, но интересно :) Не знал, что можно резать готовые нейронки и переиспользовать для других задач. ЗЫ, вспомнился мем:
Погружение в сон от статьи - 45 секунд. Спасибо!
Она затупил не из за деревьев, а их за камеры и крутого поворота