Научить автомобиль ездить самостоятельно при помощи машинного обучения Статьи редакции

Перевод выступления технического директора 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. Вот пример результатов обучения для одной из сетей:

Layer (type) Output Shape Param # Connected to ==================================================================================================== convolution2d_1 (Convolution2D) (None, 17, 37, 64) 16448 convolution2d_input_1[0][0] ____________________________________________________________________________________________________ flatten_1 (Flatten) (None, 40256) 0 convolution2d_1[0][0] ____________________________________________________________________________________________________ dense_1 (Dense) (None, 32) 1288224 flatten_1[0][0] ____________________________________________________________________________________________________ dense_2 (Dense) (None, 1) 33 dense_1[0][0] ==================================================================================================== Total params: 1304705 ____________________________________________________________________________________________________ Epoch 1/30 27144/27144 [==============================] - 160s - loss: 79.0047 - val_loss: 0.0780 Epoch 2/30 27144/27144 [==============================] - 147s - loss: 25.4130 - val_loss: 0.0692 Epoch 3/30 27144/27144 [==============================] - 148s - loss: 8.4912 - val_loss: 0.0670 Epoch 4/30 27144/27144 [==============================] - 148s - loss: 2.9383 - val_loss: 0.0638 … Epoch 12/30 27144/27144 [==============================] - 148s - loss: 0.0851 - val_loss: 0.0572 Epoch 13/30 27144/27144 [==============================] - 148s - loss: 0.0785 - val_loss: 0.0568 Epoch 14/30 27144/27144 [==============================] - 148s - loss: 0.0802 - val_loss: 0.0546 Epoch 15/30 27144/27144 [==============================] - 147s - loss: 0.0769 - val_loss: 0.0569 Epoch 16/30 27144/27144 [==============================] - 147s - loss: 0.0793 - val_loss: 0.0560 Epoch 17/30 27144/27144 [==============================] - 148s - loss: 0.0832 - val_loss: 0.0574

Тут много всего интересного, но я прокомментирую следующее. В начале — краткое описание обучаемой модели, включающее несколько параметров, которым нужно соответствовать. Помните: слои Inception остаются неизменными, мы занимаемся обучением лишь блока на конце объединённой сети.

Во второй секции — процесс обучения, который Keras описывает по мере развития. Я разбил собранные данные для обучения на две части: обучающую (80%) и контрольную (20%). Каждую эпоху Keras записывает значение средней абсолютной ошибки в прогнозах сети по обучающей (loss) и контрольной (val_loss) группам.

Для каждого из трёх слоёв обучение начинается со случайно определённых весов. Начальные потери при случайных весах очень велики и начинаются с 79 пунктов. Keras, однако, использует потери для уточнения весов: вновь обрабатывая обучающую группу, система снижает потери с каждой успешной эпохой.

Спустя 17 эпох потери в десятки раз ниже, около 0,08 пункта. Обучение заканчивается тогда, когда начинают расти потери в контрольной выборке (val_loss), — большее число эпох весьма вероятно приведёт к переобучению.

Запускаем модель с самыми низкими потерями в контрольной группе

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

Переводим симулятор на автономный режим, и машина начинает движение. Я не упоминал о регулировке газа, но в этом случае автомобиль разгоняется до 15 километров в час.

Можно заметить, как машина виляет и возвращается на курс. В конечном счёте она переусердствует и съезжает с дороги. Что ж, старт неплохой: видно, как машина пыталась удержаться на пути.

Доработки

Ищем баги. Множество произвольных решений не были указаны в начальной сетке гиперпараметров, поэтому я начал с того, что добавил несколько. Например:

  • Различные функции потерь: как нам измерить ошибку? Применив формулы среднего квадрата ошибки и средней абсолютной ошибки, я остановился на втором варианте.
  • Различные функции активации: я попробовал сигмоиду, гиперболический тангенс и выпрямитель; тангенс показал себя лучше.
  • Различная регуляризация: отбраковка весов, чтобы они не становились слишком большими, — распространённая практика избежания переобучения; я перебрал несколько весов L2-регуляризации.
  • Различные распределения начальных весов решили пару проблем со сходимостью во время обучения.
  • Различные размеры слоёв: сколько нейронов в каждом скрытом слое?
  • Различные архитектуры сети — добавить ли слоёв? Или убрать?

Тем не менее ничего из перечисленного не помогло. После нескольких дней биений я добавил оператор печати в петлю регулирования. Это быстро вскрыло проблему:

Задержка контроллера — 0,35 секунды

Контроллер тратил слишком много времени на обработку каждого кадра, поэтому подруливать он мог лишь три раза в секунду.

Кроме того, дальнейшее расследование показало: система проводила большую часть времени на слоях Inception. С одной стороны, проблему можно было решить покупкой более быстрого ноутбука. С другой — количество слоёв Inception можно сократить с 44-х до 12-ти (на картинке семь: остальные невидимы).

Уменьшаем задержку

Посмотрим, как система ведёт себя при низкой задержке (около 0,1 секунды вместо 0,35 секунды):

Намного лучше. Я ускорил видео, ведь автомобиль теперь едет гораздо дальше. Виляние уменьшилось, машина держится курса, правда, едва ли выдерживает апекс, но, возможно, потому, что я не особо за ним следил. В конце концов машина добирается до поворота с деревьями за отбойником и уезжает в сторону.

Решение — добавить данных. Я несколько раз проехал этот поворот сам, и система освоилась. Примерно в то же время на Udacity появились данные, где автомобилем управляли с помощью руля, их я тоже добавил. Я также нарастил количество данных, отзеркалив каждый кадр в обучающей группе и заменив угол поворота на отрицательный.

Результат

Со всеми дополнениями сеть смогла проехать полный круг:

Я поменял настройки газа, чтобы машина разгонялась до 50 километров в час — максимальной скорости в этой версии симулятора. Кое-где она по-прежнему виляет, но уже не разбивается. Успех.

Расширение

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

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

Заключение

В этом выступлении мы:

  • взяли нейронную сеть, обученную распознаванию пород собак;
  • переделали её под управление автомобилем;
  • научили систему водить, располагая 35 минутами образцов;
  • посмотрели, как она едет по незнакомому пути.

Более того:

  • в 2015 году мало кто считал, что и это будет возможно;
  • в 2016 году тысячи людей вроде меня занимались обучением беспилотников в свободное время на онлайн-курсах;
  • в 2018 году я руководил (совсем немного) работой школьника Джоша, который проделал всё это сам на радиоуправляемой машинке с Raspberry Pi.

Код проекта находится в открытом доступе.

0
2 комментария
Владимир

Не до конца понятно, но интересно :) Не знал, что можно резать готовые нейронки и переиспользовать для других задач. ЗЫ, вспомнился мем:

Ответить
Развернуть ветку
Александр Капцов

Погружение в сон от статьи - 45 секунд. Спасибо!

Ответить
Развернуть ветку
-1 комментариев
Раскрывать всегда