Обзор

Это третий проект наностепени Udacity's Self Driving Car, в рамках которого машина собирается почти ездить по трассе. Я использовал модифицированную архитектуру NVIDIA и различные методы увеличения данных для обучения модели. Наконец, требуется всего 147 строк кода для обучения модели автономному движению транспортного средства по трассе в симуляторе.

Ресурс

Посетите репозиторий github, чтобы увидеть полный код.

Если вы хотите увидеть реальное применение проекта клонирования поведения, прочтите эту отличную статью.

Если вы хотите увидеть, куда мы движемся с этой технологией, посмотрите это видео.

Модель Архитектура

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

Основная структура модели представлена ​​ниже:

# Creating the model
def get_model():
    model = Sequential()
    model.add(Lambda(lambda x: x/255.-0.5,input_shape=INPUT_SHAPE))
    model.add(Cropping2D(cropping=((70, 25), (0, 0))))
    model.add(Convolution2D(24, 5, 5, border_mode="same", subsample=(2,2), activation="elu"))
    model.add(Convolution2D(36, 5, 5, border_mode="same", subsample=(2,2), activation="elu"))
    model.add(Convolution2D(48, 5, 5, border_mode="valid", subsample=(2,2), activation="elu"))
    model.add(Convolution2D(64, 3, 3, border_mode="valid", activation="elu"))
    model.add(Convolution2D(64, 3, 3, border_mode="valid", activation="elu"))
    model.add(Flatten())
    model.add(Dropout(0.5))
    model.add(Dense(100, activation="elu"))
    model.add(Dense(50, activation="elu"))
    model.add(Dense(10, activation="elu"))
    model.add(Dense(1))
    adam = Adam(lr=LEARNING_PARAMETER)
    model.compile(optimizer=adam,loss='mse')
    return model

Основные отличия от реальной архитектуры:

  • Размер входного изображения модели составляет (160,320,3) по сравнению с входным размером модели Nvidia.
  • Удален один полностью связанный слой

Дополнение данных

Я использовал 5 техник увеличения данных.

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

# Randomly selecting the let, right, and center images
def random_select_image(data, i):
     
    random = np.random.randint(3)
    if random == 0:
        path = PATH_TO_IMG+data['left'][i].split('/')[-1]
        difference = CORRECTION
    elif random == 1:
        path = PATH_TO_IMG+data['center'][i].split('/')[-1]
        difference = 0 
    elif random == 2:
        path = PATH_TO_IMG+data['right'][i].split('/')[-1]
        difference = -CORRECTION
        
    image = cv2.imread(path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    angle = float(data['steer'][i])+difference
  
    return image, angle

Второй способ - преобразовать формат BGR в RGB.

image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

Третий прием - перевернуть изображение и изменить угол. то есть, если угол положительный, переворот изменит угол на отрицательный и наоборот.

# Flipping the images
def flip_img_angle(image, angle):
    image = cv2.flip(image, 1)
    angle *= -1.0

Фактический угол: -0,3012811

Угол поворота: 0,3012811

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

# Getting trans images
def trans_image(image, steer):
    trans_range = 100
    tr_x = trans_range * np.random.uniform() - trans_range / 2
    steer_ang = steer + tr_x / trans_range * 2 * .2
    tr_y = 0
    M = np.float32([[1, 0, tr_x], [0, 1, tr_y]])
    image_tr = cv2.warpAffine(image, M, (INPUT_SHAPE[1], INPUT_SHAPE[0]))
    
    return image_tr, steer_ang

В соответствии с кодом транс-диапазон изображения установлен на 100, и угол поворота изменяется соответствующим образом.

Фактический угол: -0,3012811

Измененный угол: -0,6706902124449865

Пятая и последняя техника увеличения - это метод увеличения яркости. При этом изображения преобразуются в случайную яркость. Для этого преобразовал изображение RGB в HSV, масштабировал канал V (яркость) с помощью случайного числа от 0,25 до 1,25 и преобразовал изображение обратно в RGB.

# Getting brightnessed image
def brightnessed_img(image):
    image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
    random_bright = .25 + np.random.uniform()
    image[:,:,2] = image[:,:,2] * random_bright
    image = cv2.cvtColor(image, cv2.COLOR_HSV2RGB)

Разные изображения со случайной яркостью:

Обучение модели и сохранение в файл

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

Я использовал pandas для чтения csv, чтобы получить местоположение изображения и угол поворота.

# Getting data from CSV
samples = get_csv()
# Training and Validation data
training_count = int(0.8 * len(samples))
training_data = samples[:training_count].reset_index()
validation_data = samples[training_count:].reset_index()
# Get data from csv
def get_csv():
    df = pd.read_csv(PATH_TO_CSV, index_col=False)
    df.columns = ['center', 'left', 'right', 'steer', 'throttle', 'brake', 'speed']
    df = df.sample(n=len(df))
    return df

Для получения данных для обучения и проверки я использовал ImageDataGenerator, предоставленный Керасом. Объект может генерировать пакеты данных тензорного изображения с увеличением данных в реальном времени. Несмотря на то, что я использую ImageDataGenerator, я не использовал какой-либо встроенный метод увеличения данных функции.

# Instantiating ImageDataGenerator other than yield function
gen_train = ImageDataGenerator(height_shift_range=0.2)
gen_valid = ImageDataGenerator()

ImageDataGenerator ожидает функций и меток. Я выполнил предварительную обработку изображения с помощью пользовательской функции get_data, и вывод этой функции - это функции и метки для отслеживания или проверки.

# Getting fetatures and lables from training and validation data
def get_data(data):
    images = []
    angles = []
    for i in data.index:
        image, angle = random_select_image(data, i)
        # Data augumentation
        if np.random.uniform() < 0.5:
            image, angle = flip_img_angle(image, angle)
        image = brightnessed_img(image)
        image, angle = trans_image(image, angle)
        images.append(image)
        angles.append(angle)
    # Creating as numpy array
    X = np.array(images)
    y = np.array(angles)
    return X, y

Затем загрузите их в объект ImageDataGenerator метода «потока».

# Getting features and labels for training and validation.
X_train, y_train = get_data(training_data)
X_valid, y_valid = get_data(validation_data)

Инициализируйте измененную модель Nvidia с помощью функции get_model.

# Model using Keras
model = get_model()
# Creating the model
def get_model():
    model = Sequential()
    model.add(Lambda(lambda x: x/255.-0.5,input_shape=INPUT_SHAPE))
    model.add(Cropping2D(cropping=((70, 25), (0, 0))))
    model.add(Convolution2D(24, 5, 5, border_mode="same", subsample=(2,2), activation="elu"))
    model.add(Convolution2D(36, 5, 5, border_mode="same", subsample=(2,2), activation="elu"))
    model.add(Convolution2D(48, 5, 5, border_mode="valid", subsample=(2,2), activation="elu"))
    model.add(Convolution2D(64, 3, 3, border_mode="valid", activation="elu"))
    model.add(Convolution2D(64, 3, 3, border_mode="valid", activation="elu"))
    model.add(Flatten())
    model.add(Dropout(0.5))
    model.add(Dense(100, activation="elu"))
    model.add(Dense(50, activation="elu"))
    model.add(Dense(10, activation="elu"))
    model.add(Dense(1))
    adam = Adam(lr=LEARNING_PARAMETER)
    model.compile(optimizer=adam,loss='mse')
    return model

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

# Training the model
model.fit_generator(gen_train.flow(X_train, y_train, batch_size=BATCH_SIZE), samples_per_epoch= samples_per_epoch_train, validation_data=gen_valid.flow(X_valid, y_valid, batch_size=BATCH_SIZE), nb_val_samples=samples_per_epoch_valid, nb_epoch=EPOCH)
model.save('model.h5')

Гиперпараметры

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

# Hyperparameteres
BATCH_SIZE = 32
EPOCH = 15
LEARNING_PARAMETER = .0001

Результат обучения и проверки

Epoch 2/15
12992/12992 [==============================] - 208s - loss: 0.0517 - val_loss: 0.0558
Epoch 3/15
12992/12992 [==============================] - 211s - loss: 0.0488 - val_loss: 0.0548
Epoch 4/15
12992/12992 [==============================] - 208s - loss: 0.0468 - val_loss: 0.0544
Epoch 5/15
12992/12992 [==============================] - 208s - loss: 0.0448 - val_loss: 0.0527
Epoch 6/15
12992/12992 [==============================] - 208s - loss: 0.0442 - val_loss: 0.0533
Epoch 7/15
12992/12992 [==============================] - 209s - loss: 0.0434 - val_loss: 0.0510
Epoch 8/15
12992/12992 [==============================] - 208s - loss: 0.0425 - val_loss: 0.0490
Epoch 9/15
12992/12992 [==============================] - 208s - loss: 0.0424 - val_loss: 0.0492
Epoch 10/15
12992/12992 [==============================] - 207s - loss: 0.0409 - val_loss: 0.0485
Epoch 11/15
12992/12992 [==============================] - 209s - loss: 0.0406 - val_loss: 0.0483
Epoch 12/15
12992/12992 [==============================] - 208s - loss: 0.0403 - val_loss: 0.0480
Epoch 13/15
12992/12992 [==============================] - 208s - loss: 0.0402 - val_loss: 0.0467
Epoch 14/15
12992/12992 [==============================] - 208s - loss: 0.0391 - val_loss: 0.0475
Epoch 15/15
12992/12992 [==============================] - 207s - loss: 0.0401 - val_loss: 0.0465

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

python drive.py model.h5 video

Что еще можно сделать?

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