100 строк кода: Обучаем нейронную сеть управлять автомобилем

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

Благодаря таким библиотекам, как TensorFlow и Keras, мы получаем готовые реализации алгоритмов, и нам остается лишь приятная часть работы: выбор архитектуры модели, настройка гиперпараметров и аугментация (augmentation) (создание дополнительных обучающих данных).

В этой статье, мы покажем, как обучить глубокую нейронную сеть для управления автомобилем, написав при этом всего 99 строк кода. (Для сравнения метод get_permalink() из WordPress, возвращающий URL-адрес записи, содержит 124 строки кода!) Для сбора обучающих данных и тестирования модели мы используем специальный симулятор, применяемый в рамках образовательного курса Udacity по программированию беспилотных автомобилей.

Следует отметить, что немало людей описали на Medium свои решения данной задачи. Рекомендуем вам прочитать статью Вивека Ядава (Vivek Yadav), в которой содержится много полезной информации, а также хорошо описаны подходы к аугментации данных.

Итак, без дальнейших церемоний, представляем полное решение: 99 строк, включая комментарии.

"""
import csv, random, numpy as np
from keras.models import load_model, Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers.convolutional import Convolution2D, MaxPooling2D
from keras.preprocessing.image import img_to_array, load_img, flip_axis, random_shift

def model(load, shape, checkpoint=None):
    """Return a model from file or to train on."""
    if load and checkpoint: return load_model(checkpoint)

    conv_layers, dense_layers = [32, 32, 64, 128], [1024, 512]
    
    model = Sequential()
    model.add(Convolution2D(32, 3, 3, activation='elu', input_shape=shape))
    model.add(MaxPooling2D())
    for cl in conv_layers:
        model.add(Convolution2D(cl, 3, 3, activation='elu'))
        model.add(MaxPooling2D())
    model.add(Flatten())
    for dl in dense_layers:
        model.add(Dense(dl, activation='elu'))
        model.add(Dropout(0.5))
    model.add(Dense(1, activation='linear'))
    model.compile(loss='mse', optimizer="adam")
    return model
    
def get_X_y(data_file):
    """Read the log file and turn it into X/y pairs. Add an offset to left images, remove from right images."""
    X, y = [], []
    steering_offset = 0.4
    with open(data_file) as fin:
        for _, left_img, right_img, steering_angle, _, _, speed in csv.reader(fin):
            if float(speed) < 20: continue  # throw away low-speed samples
            X += [left_img.strip(), right_img.strip()]
            y += [float(steering_angle) + steering_offset, float(steering_angle) - steering_offset]
    return X, y

def process_image(path, steering_angle, augment, shape=(100,100)):
    """Process and augment an image."""
    image = load_img(path, target_size=shape)
    
    if augment and random.random() < 0.5:
        image = random_darken(image)  # before numpy'd

    image = img_to_array(image)
        
    if augment:
        image = random_shift(image, 0, 0.2, 0, 1, 2)  # only vertical
        if random.random() < 0.5:
            image = flip_axis(image, 1)
            steering_angle = -steering_angle

    image = (image / 255. - .5).astype(np.float32)
    return image, steering_angle

def random_darken(image):
    """Given an image (from Image.open), randomly darken a part of it."""
    w, h = image.size

    # Make a random box.
    x1, y1 = random.randint(0, w), random.randint(0, h)
    x2, y2 = random.randint(x1, w), random.randint(y1, h)

    # Loop through every pixel of our box (*GASP*) and darken.
    for i in range(x1, x2):
        for j in range(y1, y2):
            new_value = tuple([int(x * 0.5) for x in image.getpixel((i, j))])
            image.putpixel((i, j), new_value)
    return image

def _generator(batch_size, X, y):
    """Generate batches of training data forever."""
    while 1:
        batch_X, batch_y = [], []
        for i in range(batch_size):
            sample_index = random.randint(0, len(X) - 1)
            sa = y[sample_index]
            image, sa = process_image(X[sample_index], sa, augment=True)
            batch_X.append(image)
            batch_y.append(sa)
        yield np.array(batch_X), np.array(batch_y)

def train():
    """Load our network and our data, fit the model, save it."""
    net = model(load=False, shape=(100, 100, 3))
    X, y = get_X_y('./data/t1_1/driving_log.csv')
    net.fit_generator(_generator(256, X, y), samples_per_epoch=20224, nb_epoch=2)
    net.save('checkpoints/short.h5')

if __name__ == '__main__':
    train()

Симулятор

Симулятор имеет два режима (обучающий и автономный) и две трассы. Задача заключается в том, чтобы разработать нейронную сеть, которая, обучившись на 1-й трассе, сможет самостоятельно пройти 2-ю трассу, имеющую существенно отличающиеся характеристики.

Для решения этой задачи, сперва в обучающем режиме мы должны проехать 5 кругов по 1-й трассе, собственноручно управляя автомобилем, чтобы сгенерировать необходимые для обучения данные. Следует постараться, чтобы автомобиль все время оставался в центре трассы. После этого запускаем процесс обучения, выполнив представленный выше код. Давайте рассмотрим каждый этап.

Модель

Модель представляет собой сверточную нейронную сеть (convolutional neural network). Идеи для создания модели были взяты из различных публикаций: NVIDIA, VGG16, статья Вивека Ядава, упомянутая выше, и др.

Мы используем 5 сверточных слоев возрастающей ширины, между которыми размещаются слои макс-пулинга (max-pooling) для сокращения пространственной размерности (и экономии памяти). Далее следует несколько полносвязных (dense) слоев, последний из которых имеет линейную функцию активации, дающую на выходе непрерывную величину угла поворота. Во всех слоях, кроме последнего, мы использовали функцию активации ELU, а для реализации обучения применили оптимизатор Adam. Это позволило обеспечить достаточно быструю сходимость.

Извлечение данных из CSV-файла

В процессе сбора обучающих данных для каждого момента времени симулятор фиксирует три изображения (левое, центральное и правое), угол поворота, скорость и др. Пути к изображениям и остальные параметры записываются в CSV-файл.

Мы не используем центральное изображение, потому что левое и правое изображения содержат достаточное количество информации. В рамках каждого наблюдения мы увеличиваем угол поворота на 0.4 для левого изображения и уменьшаем угол поворота на 0.4 для правого изображения. Логика заключается в том, что если центральное изображение (которое увидела бы модель, двигаясь прямо) похоже на правое изображение, вероятно, необходимо повернуть немного влево, и наоборот.

Значение 0.4 является достаточно агрессивным и было найдено экспериментальным путем. В других опубликованных решения используются значения 0.2 или 0.25. Хотя эти значения хорошо подходят для 1-й трассы, лучшую обобщающую способность для 2-й трассы обеспечивает именно значение 0.4.

Обработка изображений

Мы должны передать обучающие изображения в нейронную сеть в виде NumPy-массивов. Это преобразование выполняется в функции process_image, где мы также нормализуем значения, приводя их к интервалу от -0.5 до 0.5.

Кроме того, в данной функции мы выполняем аугментацию, чтобы улучшить обобщающую способность модели. Аугментация играет решающую роль в обеспечении эффективной работы модели на 2-й трассе. Каждое изображение мы случайным образом сдвигаем по вертикали на величину в интервале от -20% до 20%. Применение сдвига существенно улучшает результат модели на обеих трассах. Кроме того, на каждом изображении мы случайным образом формируем затемненную область. Дело в том, что на 2-й трассе присутствует большое количество резки теней, в то время как на 1-й трассе их почти нет. Соответственно, модель, обученная на 1-й трассе, испытывает большие трудности на 2-й трассе, встретив перед собой тень. Нанесение на обучающие изображения случайных темных областей позволяет решить эту проблему.

Наконец, мы удваиваем количество обучающих примеров, случайным образом выполняя отражение изображений по горизонтали (horizontal flip) с соответствующей инверсией угла поворота. Этот подход позволяет нейтрализовать смещение, вызванное несбалансированностью обучающих данных.

Обучение

Поскольку мы имеем дело с изображениями, мы не можем загрузить весь обучающий набор данных в память. Чтобы обучить модель, мы используем отличную функцию fit_generator из библиотеки Keras. В качестве аргумента мы передаем ей функцию _generator (Python-генератор), генерирующую пакеты (batch) обучающих данных.

Функция _generator случайным образом выбирает определенное количество (заданное с помощью параметра batch_size) обучающих примеров, пропускает изображения через функцию обработки и генерирует пакет обучающих данных. Мы использовали размер пакета, равный 64, что позволило добиться лучших результатов, по сравнению с меньшими значениями (например, 32) и существенно большими значениями (например, 256 или 512). Необходимо провести дополнительное исследование, чтобы разобраться в причинах этой зависимости.

Тестирование модели

Модель смогла безупречно управлять автомобилем на 1-й трассе после всего лишь одной эпохи обучения на 20224 примерах. Обучение заняло 64 секунды на GeForce 960M. Просто удивительно, как быстро модель смогла добиться успеха на 1-й трассе.


Модель управляет автомобилем на 1-й трассе.

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

Чтобы обучить модель проходить 2-ю трассу, потребовалось 8 эпох. Обучение заняло больше времени, поскольку 2-я трасса существенно отличается от 1-й трассы, в том числе в виду наличия большого количества теней.

Модель управляет автомобилем на 2-й трассе.

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

Итак, перед вами модель, способная эффективно управлять автомобилем на незнакомой трассе, создание и обучение которой потребовало менее 100 строк кода. Теперь сделаем несколько замечаний:

  • Мы не ставили перед собой задачу уложиться в определенное количество строк. В данной статье мы хотели продемонстрировать, насколько эффективными являются современные библиотеки, позволяющие нам сосредоточиться на прикладных аспектах машинного обучения, избавляя от необходимости разрабатывать сложные алгоритмы.
  • Несмотря на то, что для реализации проекта потребовалось менее 100 строк кода, большое количество времени было затрачено на эксперименты, связанные с поиском оптимальных гиперпараметров сети и методик аугментации. Но в этом и заключается прелесть: мы смогли посвятить все наше время этим вопросам, и были избавлены от необходимости погружаться в детали сложных алгоритмов.
  • Подобная легкость обучения глубоких сетей была бы невозможна без таких библиотек, как TensorFlow, TFLearn и Keras. Разработчики этих проектов заслуживают отдельной благодарности.

Чтобы применить обученную модель в симуляторе, вам также потребуется следующий код:

"""
Based on:
https://github.com/dolaameng/Udacity-SDC_Behavior-Cloning/tree/master/sdc
"""
import base64
from flask import Flask, render_template
from io import BytesIO
from train import process_image, model
import eventlet
import eventlet.wsgi
import numpy as np
import socketio

sio = socketio.Server()
app = Flask(__name__)
target_speed = 22
shape = (100, 100, 3)
model = model(True, shape)

@sio.on('telemetry')
def telemetry(sid, data):
    # The current image from the center camera of the car
    img_str = data["image"]
    speed = float(data["speed"])

    # Set the throttle.
    throttle = 1.2 - (speed / target_speed)

    # read and process image
    image_bytes = BytesIO(base64.b64decode(img_str))
    image, _ = process_image(image_bytes, None, False)

    # make prediction on steering
    sa = model.predict(np.array([image]))[0][0]

    print(sa, throttle)
    send_control(sa, throttle)

@sio.on('connect')
def connect(sid, environ):
    print("connect ", sid)
    send_control(0, 0)

def send_control(steering_angle, throttle):
    sio.emit("steer", data={
    'steering_angle': steering_angle.__str__(),
    'throttle': throttle.__str__()
    }, skip_sid=True)

if __name__ == '__main__':
    # wrap Flask application with engineio's middleware
    app = socketio.Middleware(sio, app)

    # deploy as an eventlet WSGI server
    eventlet.wsgi.server(eventlet.listen(('', 4567)), app)

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

Источник

Перевод Станислава Петренко

Добавить комментарий

Ваш адрес email не будет опубликован.

закрыть

Поделиться

Отправить на почту
закрыть

Вход

закрыть

Регистрация

+ =