Посмотреть на github, twitter, linkedin
В этом проекте мы пытаемся предсказать углы поворота для беспилотного автомобиля в симуляторе на основе единства. Чтобы получить наши данные, мы должны запустить симулятор и управлять автомобилем вручную (клавиатура + мышь или контроллер). Для сбора данных я использовал два разных подхода. Один набор данных состоял из того, что на машине проехали четыре круга и все время пытались оставаться посередине дороги. Другой набор известен как «данные для восстановления», когда автомобиль тронулся с места на краю дороги, и изображения были сняты, когда он «восстанавливался» или «возвращался» к середине дороги. Цель этого второго набора - добавить данные для исправления ошибок: в случае, если наша машина упадет с обочины дороги, она будет знать, что с ней делать. В прямом наборе есть 8К изображений, а в комплекте восстановления - 1,3К.
Сбор данных:
Наши данные сохраняются в таком формате:
── данные
│ ├── driving_log.csv
│ └── IMG
│ ├── center_2017_01_06_13_54_47_000.jpg
│ ├── center_2017_01_06_13_54_47_110.jpg
│ ├── left_2017_02_14_21_46_00_967.jpg
│ ├── left_2017_02_14_21_46_01_041.jpg
│ ├── right_2016_12_01_13_42_06_070.jpg
│_17_126_ right_
С одного круга видеосъемки таких изображений тысячи. Они записываются с трех разных камер в автомобиле: центральной, левой и правой камеры.
Чтобы упростить жизнь, я использовал DataFrames pandas для сортировки и организации путей к изображениям. Я не мог прочитать каждый файл изображения и сохранить его локально в своей программе, потому что у меня закончилась память, поэтому я работал с путями к изображениям. (подробнее об этом позже в разделе о генераторах).
def createDataFrame(data_path): """ input: data_path: path to data return: data frame """ data_frame = pd.read_csv(data_path) data_frame.columns = ['center', 'left', 'right', 'steering', 'throttle', 'brake', 'speed'] return data_frame
Трасса, которую я использовал для сбора тренировочных данных, идет против часовой стрелки, и поворотов влево больше, чем поворотов вправо. Чтобы обобщить любой трек, мне нужно создать дополнительные данные.
Большинство полученных данных имели углы поворота 0 градусов, потому что большую часть трассы мы едем прямо. Вначале наша гистограмма углов поворота выглядит так:
Проблема 1. Как видите, существует 25 000 изображений с нулевым углом наклона. Данных по прямому движению в 5 раз больше, чем по левому или правому повороту. Это означает, что наша модель, вероятно, предполагает, что большую часть времени мы едем прямо.
Проблема 2: Другая проблема заключается в том, что у меня было только около 8 КБ изображений из набора для прямого движения и 1,3 КБ изображений из набора данных для восстановления. В общей сложности это 9,3 тыс. Изображений, чего недостаточно для обучения моей сети.
Решение, часть 1: я разделил набор данных на три разные категории: center_turns (нулевой угол при прямом движении), left_turns и right_turns.
def createTrainingDataPathsCLR(df, prefix_path): """ creates training data and training labels/ measurements from a data frame inputs: df: pandas DataFrame object prefix_path: path to the dataset output: (center_turns, left_turns, right_turns) list tuple """ # Turn types center_turns = [] left_turns = [] right_turns = [] abs_path_to_IMG = os.path.abspath(prefix_path) for idx, row in df.iterrows(): center_image_cam = os.path.join(abs_path_to_IMG, row['center'].strip()) left_image_cam = os.path.join(abs_path_to_IMG, row['left'].strip()) right_image_cam = os.path.join(abs_path_to_IMG, row['right'].strip()) steering_angle = row['steering'] # Right image condition if steering_angle > 0.125: right_turns.append([center_image_cam, left_image_cam, right_image_cam, steering_angle]) # This is a left image elif steering_angle < -1 * 0.125: left_turns.append([center_image_cam, left_image_cam, right_image_cam, steering_angle]) # This is a center image else: # center images center_turns.append([center_image_cam, left_image_cam, right_image_cam, steering_angle]) return (center_turns, left_turns, right_turns)
Решение, часть 2: после разделения данных я создал дополнительные данные для всех трех категорий, но не одинаково. Данные о прямом движении были настолько многочисленны, чем данные о левом или правом повороте, что я дал очень большой прирост данных поворота налево и значительный прирост данных поворота направо. Цель состоит в том, чтобы получить нормализованную гистограмму.
def makeMore(dataArray, amount): """ This function creates additional data for center, left, and right camera angles given a dataArray of a specific turn type (center_turn, left_turn, or right_turn) input: dataArray: array of specific turn type amount: amount to increase the input array output: dataArray of same turn type with values * amount """ for i in range(len(dataArray)): for j in range(amount): dataArray.append([dataArray[i][0], dataArray[i][1], dataArray[i][2], dataArray[i][3]]) return dataArray center_turns = makeMore(center_turns, 5) left_turns = makeMore(left_turns, 18) right_turns = makeMore(right_turns, 12)
Теперь моя гистограмма угла поворота выглядит так:
Сделав это с обоими наборами данных, я создал гистограмму, которая выглядит следующим образом:
Предварительная обработка
Наши данные представлены в виде изображений RGB размером 160 x 320 x 3. Я использовал несколько методов предварительной обработки для увеличения, преобразования и создания большего количества данных, чтобы дать моей сети больше шансов на обобщение для различных функций трека.
Увеличение яркости: у этой трассы хорошее освещение, но мне нужно уметь ездить в плохих условиях освещения, в тенях, на полосах с более темными линиями и т. д. Для этого я создал эту функцию:
def change_brightness(image): """ Augments the brightness of the image by multiplying the saturation by a uniform random variable input: image (RGB) output: image with brightness augmentation (RGB) """ bright_factor = 0.2 + np.random.uniform() hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) # perform brightness augmentation only on the second channel hsv_image[:,:,2] = hsv_image[:,:,2] * bright_factor # change back to RGB image_rgb = cv2.cvtColor(hsv_image, cv2.COLOR_HSV2RGB) return image_rgb
Переворачивание. Наши данные были получены при движении против часовой стрелки. Чтобы научить нашу модель двигаться по часовой стрелке, я позволил с вероятностью 50% вертикально перевернуть каждое изображение и инвертировать угол поворота.
def flip_image(image, ang): image = np.fliplr(image) ang = -1 * ang return (image, ang)
Обрезка. Нам нужен только участок дороги с полосами движения, поэтому я вырезал небо и рулевое колесо. Для изображения размером 160 x 320 x 3 пикселя я обрезал высоту с 55 до 140 пикселей. Что-то вроде этого подойдет
image = image[image.shape[0] * 0.35 : image.shape[0] * 0.875, :, :]
Изменение размера: я использовал архитектуру сети сквозного обучения Nvidia, которая использует изображения RGB размером 66 x 220 x 3, поэтому я изменил размер своих изображений на это.
img = cv2.resize(image, (220, 66), interpolation=cv2.INTER_AREA)
Конвейер полной предварительной обработки:
Генераторы:
Итак, в этом проекте у нас есть более 80 тысяч изображений для работы. Я просто не могу сохранить все это в памяти. Я использовал генератор для создания пакетов изображений для моего обучающего конвейера. Выглядит это так:
def generate_training_data(data, batch_size = 32): """ We create a loop through out data and send out an individual row in the dataframe to preprocess_image_from_path, which is then sent to preprocess_image inputs: data: pandas DataFrame batch_size: batch sizes, size to make each batch returns a yield a batch of (image_batch, label_batch) """ image_batch = np.zeros((batch_size * 2, 66, 220, 3)) # nvidia input params label_batch = np.zeros((batch_size * 2)) while True: for i in range(batch_size): idx = np.random.randint(len(data)) row = data.iloc[[idx]].reset_index() x, y = preprocess_image_from_path(row['center'].values[0], row['steering'].values[0]) # preprocess another center image x2, y2 = preprocess_image_from_path(row['center'].values[0], row['steering'].values[0]) if np.random.randint(3) == 1: # 33% chance to overwrite center image (2) with left image + correction_factor x2, y2 = preprocess_image_from_path(row['left'].values[0], row['steering'] + 0.125) if np.random.randint(3) == 2: # 33% change to overwrite center image (2) give right image - correction_factor x2, y2 = preprocess_image_from_path(row['right'].values[0], row['steering'] - 0.125) image_batch[i] = x label_batch[i] = y image_batch[i + 1] = x2 label_batch[i + 1] = y2 yield shuffle(image_batch, label_batch)
В этом генераторе я создавал партии изображений. Я решил использовать партии, чтобы контролировать, что происходит с каждой партией. В этом случае я удвоил заданный размер пакета и выделил 33% для добавления изображений левой или правой камеры, а также центрального изображения. В нашем моделировании мы берем центральное изображение и прогнозируем угол поворота. Я добавил поправочный коэффициент 0,125 к изображениям левой камеры и вычел этот поправочный коэффициент из изображений правой камеры, чтобы они были в соответствии с углами поворота центральных изображений. Теперь я просто на лету создал в 2 раза больше тренировочных данных.
Я также создал генератор проверки, который каждый раз просто генерирует одно изображение. Эти изображения проходят только этапы предварительной обработки обрезки и изменения размера, такие же, как и для тестовых данных в моделировании.
Сетевая архитектура: я решил использовать для этого проекта модель непрерывного обучения сверточной нейронной сети от Nvidia.
Я решил использовать ELU (экспоненциальные линейные единицы) вместо ReLu. ELU избегает проблемы исчезающего градиента (как и ReLu и дырявый ReLu). Они также ускоряют обучение, избегая предвзятого сдвига, к которому склонны ReLu. ELU эффективны для очень глубоких моделей, в которых количество слоев превышает четыре. Эти функции активации хорошо справились с задачей ImageNet за меньшее количество эпох, чем сеть на основе ReLu с той же архитектурой. Подробнее см. Здесь.
Я реализовал слой Dropout, чтобы предотвратить чрезмерную подгонку. Я реализовал только один слой Dropout, в будущем я бы добавил больше слоев Dropout между первыми 3 свертками с вероятностями от 0,9 до 0,7, а затем включил бы вероятность 0,5 после четвертого или пятого сверточного слоя.
Я нормализовал входные значения пикселей RGB до диапазона [-1, 1]. Я уверен, что [0, 1] тоже сработало бы
Обучение:
model = nvidia_model() train_size = len(train_data.index) for i in range(3): train_generator = generate_training_data(train_data, BATCH) history = model.fit_generator( train_generator, samples_per_epoch = 20480, # try putting the whole thing in here in the future nb_epoch = 6, validation_data = valid_generator, nb_val_samples = val_size) print(history) model.save_weights('model-weights-F1.h5') model.save('model-F1.h5')
- Образцов на эпоху: 20480 Я использую около 80 тыс. изображений (я создал около 4/5 из них). Это масса данных. Поэтому я не отбираю все изображения для каждой эпохи. Я отбираю только 1/4 из этих 80k = ›20k изображений. Однако на каждом из этих 20k я создаю пакеты из 32 изображений, поэтому я фактически тренирую свою модель на
20480 * 32 = 655k
изображениях в каждую эпоху. Это потому, что я случайным образом беру изображение в своем наборе данных, чтобы разбрасывать его по партиям с заменой. Всего обработано20480 * 32 * 18 = 11.8M
изображений, что составляет11.8M * 66 * 220 * 3 = 513B pixels
- Количество эпох: 18 Я использую 18 эпох за 3 цикла. Я выполнил три цикла, потому что хочу иметь возможность последовательно тренироваться достаточно долго (nb_epochs = 6), а затем иметь возможность оценить эту модель, если она хорошая, я хочу ее сохранить. Затем я обучаю еще 2 модели и выбираю модель с наименьшими потерями при проверке. Когда я попытался установить диапазон на 4, у меня возникла ошибка памяти. Итак, 3 было самым высоким, на что я мог пойти.
- Размер пакета: 16. Затем я умножаю этот размер пакета * 2 в своем генераторе, чтобы он стал 32. Я попытался использовать большой размер пакета, но у меня очень быстро закончилась память, потому что я пытался сохранить все эти постобработанные изображения в памяти внутри функции генератора. Уменьшение размера партии до 32, похоже, сработало хорошо, и это заставило мое обучение ускориться.
- nb_val_samples: общая длина данных проверки. Мой генератор проверки просто выдает каждый раз одно изображение, поэтому я просто использую все данные проверки для размера моей проверочной выборки. Это приводит к тому, что все мои проверочные данные загружаются в генератор один за другим в последовательном порядке. Они загрузились бы по порядку, если бы я не перетасовал обучающие данные перед разделением их на наборы данных для обучения и проверки.
Использованная литература:
- ELU: https://arxiv.org/pdf/1511.07289v1.pdf
- Модель Nvidia: https://arxiv.org/pdf/1604.07316v1.pdf
- Https://www.youtube.com/watch?v=rpxZ87YFg0M
- Http://selfdrivingcars.mit.edu/
- Http://images.nvidia.com/content/tegra/automotive/images/2016/solutions/pdf/end-to-end-dl-using-px.pdf
- Http://jacobgil.github.io/deeplearning/vehicle-steering-angle-visualizations
- Http://medium.com/udacity/teaching-a-machine-to-steer-a-car-d73217f2492c
- Http://chatbotslife.com/using-augmentation-to-mimic-human-driving-496b569760a9
- Майкл А. Нильсен, «Нейронные сети и глубокое обучение», Determination Press, 2015 г.