Implementation of PilotNet

Implement PilotNet using Keras (with tensorflow backend), with some modifications


In [1]:
import os
import pandas as pd
import numpy as np
from sklearn.utils import shuffle

In [2]:
import keras

from keras.models import Sequential
from keras.models import load_model

from keras.layers import Flatten, Dense
from keras.layers import BatchNormalization
from keras.layers import Conv2D

from keras.optimizers import SGD
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
from keras.callbacks import CSVLogger, ModelCheckpoint


Using TensorFlow backend.

In [3]:
# limit GPU memory usage
import tensorflow as tf
from keras.backend.tensorflow_backend import set_session
config = tf.ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.75
set_session(tf.Session(config=config))

Check data format


In [4]:
%ls ../data/csv/final


v1.csv  v2.csv  v3.csv  v3_train.csv  v3_valid.csv

In [5]:
# define path variables
parent_path = os.path.dirname(os.getcwd())

data_path = os.path.join(parent_path, 'data')
img_front_dir_path = os.path.join(data_path, 'img', 'front')
model_path = os.path.join(parent_path, 'model')
log_path = os.path.join(model_path, 'log')


csv_dir_path = os.path.join(data_path, 'csv', 'final')
cur_file = 'v3'
train_file = os.path.join(csv_dir_path, cur_file + '_train.csv')
valid_file = os.path.join(csv_dir_path, cur_file + '_valid.csv')

# divide by a constant to bound output to [0,100]
OUTPUT_NORMALIZATION = 655.35

In [6]:
df_train = pd.read_csv(os.path.join(data_path, train_file))
print("%d rows" % df_train.shape[0])
df_train.head(3)


129996 rows
Out[6]:
img wheel-axis clutch brake gas paddle-left paddle-right wheel-button-left-1 wheel-button-left-2 wheel-button-left-3 ... gear-1 gear-2 gear-3 gear-4 gear-5 gear-6 gear-R front side_left side_right
0 9d0c3c2b_2017_07_27_14_56_31_97.jpg -321 0 0 65535 0 0 0 0 0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 9d0c3c2b_2017_07_27_14_56_31_97_front.jpg 9d0c3c2b_2017_07_27_14_56_31_97_left.jpg 9d0c3c2b_2017_07_27_14_56_31_97_right.jpg
1 7d590ce8_2017_08_07_14_49_16_12.jpg -741 27091 0 65535 0 0 0 0 0 ... 0.0 0.0 1.0 0.0 0.0 0.0 0.0 7d590ce8_2017_08_07_14_49_16_12_front.jpg 7d590ce8_2017_08_07_14_49_16_12_left.jpg 7d590ce8_2017_08_07_14_49_16_12_right.jpg
2 3f80cca8_2017_08_08_14_39_00_36.jpg 152 15222 0 65535 0 0 0 0 0 ... 0.0 0.0 1.0 0.0 0.0 0.0 0.0 3f80cca8_2017_08_08_14_39_00_36_front.jpg 3f80cca8_2017_08_08_14_39_00_36_left.jpg 3f80cca8_2017_08_08_14_39_00_36_right.jpg

3 rows × 33 columns


In [7]:
df_val = pd.read_csv(os.path.join(data_path, valid_file))
print("%d rows" % df_val.shape[0])
df_val.head(3)


32499 rows
Out[7]:
img wheel-axis clutch brake gas paddle-left paddle-right wheel-button-left-1 wheel-button-left-2 wheel-button-left-3 ... gear-1 gear-2 gear-3 gear-4 gear-5 gear-6 gear-R front side_left side_right
0 7a13935b_2017_07_30_15_38_28_68.jpg -41 0 0 45153 0 0 0 0 0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 7a13935b_2017_07_30_15_38_28_68_front.jpg 7a13935b_2017_07_30_15_38_28_68_left.jpg 7a13935b_2017_07_30_15_38_28_68_right.jpg
1 9d0c3c2b_2017_07_28_18_09_10_29.jpg 2000 0 0 65535 0 0 0 0 0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 9d0c3c2b_2017_07_28_18_09_10_29_front.jpg 9d0c3c2b_2017_07_28_18_09_10_29_left.jpg 9d0c3c2b_2017_07_28_18_09_10_29_right.jpg
2 15be80cb_2017_07_28_22_48_24_52.jpg -1593 0 0 63730 0 0 0 0 0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 15be80cb_2017_07_28_22_48_24_52_front.jpg 15be80cb_2017_07_28_22_48_24_52_left.jpg 15be80cb_2017_07_28_22_48_24_52_right.jpg

3 rows × 33 columns

Predict steering angle using only the front image


In [8]:
def img_to_arr(p):
    with image.load_img(p) as img:
        img = image.img_to_array(img)
    return img

# values computed from dataset sample.
def normalize(img):
    img[:,:,0] -= 94.9449
    img[:,:,0] /= 58.6121

    img[:,:,1] -= 103.599
    img[:,:,1] /= 61.6239

    img[:,:,2] -= 92.9077
    img[:,:,2] /= 68.66
    
    return img

In [9]:
# define generator that loops through the data
def generator(df, batch_size, img_shape, should_shuffle):
    # shuffle dataframe for each epoch
    if should_shuffle:
        df = shuffle(df)
        
    img_list = df['front']
    wheel_axis = df['wheel-axis']
    
    # create empty batch
    batch_img = np.zeros((batch_size,) + img_shape)
    batch_label = np.zeros((batch_size, 1))
    
    index = 0
    while True:
        for i in range(batch_size):
            img_name = img_list[index]
            arr = img_to_arr(os.path.join(img_front_dir_path, img_name))
            
            batch_img[i] = normalize(arr)
            batch_label[i] = wheel_axis[index] / OUTPUT_NORMALIZATION
            
            index += 1
            if index == len(img_list):
                index = 0
            
        yield batch_img, batch_label

In [10]:
input_shape = img_to_arr(os.path.join(img_front_dir_path, df_train['front'][0])).shape
batch_size = 160
train_steps = (df_train.shape[0] / batch_size) + 1
val_steps = (df_val.shape[0] / batch_size) + 1

print("input_shape: %s, batch_size: %d, train_steps: %d, val_steps: %d" % 
      (input_shape, batch_size, train_steps, val_steps))


input_shape: (341, 562, 3), batch_size: 160, train_steps: 813, val_steps: 204

In [11]:
train_batch = generator(df_train, batch_size, input_shape, True)
val_batch = generator(df_val, batch_size, input_shape, False)

Define model


In [12]:
# define PilotNet model, with batch normalization included.
def get_model(input_shape):
    model = Sequential([
        Conv2D(24, kernel_size=(5,5), strides=(2,2), activation='relu', input_shape=input_shape),
        BatchNormalization(axis=1),
        Conv2D(36, kernel_size=(5,5), strides=(2,2), activation='relu'),
        BatchNormalization(axis=1),
        Conv2D(48, kernel_size=(5,5), strides=(2,2), activation='relu'),
        BatchNormalization(axis=1),
        Conv2D(64, kernel_size=(3,3), strides=(1,1), activation='relu'),
        BatchNormalization(axis=1),
        Conv2D(64, kernel_size=(3,3), strides=(1,1), activation='relu'),
        BatchNormalization(axis=1),
        Flatten(),
        Dense(100, activation='relu'),
        BatchNormalization(),
        Dense(50, activation='relu'),
        BatchNormalization(),
        Dense(10, activation='relu'),
        BatchNormalization(),
        Dense(1)
    ])
    
    return model

model = get_model(input_shape)
sgd = SGD(lr=1e-3, decay=1e-4, momentum=0.9, nesterov=True)
model.compile(optimizer=sgd, loss="mse") 
model.summary()


_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 169, 279, 24)      1824      
_________________________________________________________________
batch_normalization_1 (Batch (None, 169, 279, 24)      676       
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 83, 138, 36)       21636     
_________________________________________________________________
batch_normalization_2 (Batch (None, 83, 138, 36)       332       
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 40, 67, 48)        43248     
_________________________________________________________________
batch_normalization_3 (Batch (None, 40, 67, 48)        160       
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 38, 65, 64)        27712     
_________________________________________________________________
batch_normalization_4 (Batch (None, 38, 65, 64)        152       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 36, 63, 64)        36928     
_________________________________________________________________
batch_normalization_5 (Batch (None, 36, 63, 64)        144       
_________________________________________________________________
flatten_1 (Flatten)          (None, 145152)            0         
_________________________________________________________________
dense_1 (Dense)              (None, 100)               14515300  
_________________________________________________________________
batch_normalization_6 (Batch (None, 100)               400       
_________________________________________________________________
dense_2 (Dense)              (None, 50)                5050      
_________________________________________________________________
batch_normalization_7 (Batch (None, 50)                200       
_________________________________________________________________
dense_3 (Dense)              (None, 10)                510       
_________________________________________________________________
batch_normalization_8 (Batch (None, 10)                40        
_________________________________________________________________
dense_4 (Dense)              (None, 1)                 11        
=================================================================
Total params: 14,654,323
Trainable params: 14,653,271
Non-trainable params: 1,052
_________________________________________________________________

In [13]:
# or load from saved model
# model = load_model(os.path.join(model_path, 'v3-PilotNet_v1-029-0.0783.h5'))

Define callback for training


In [14]:
# define callbacks
cur_model = cur_file + '-PilotNet_v2'
csv_logger = CSVLogger(os.path.join(log_path, cur_model + '.log'))

model_file_name= os.path.join(model_path, cur_model + '-{epoch:03d}-{val_loss:.5f}.h5')
checkpoint = ModelCheckpoint(model_file_name, verbose=0, save_best_only=True)

Train


In [15]:
model.fit_generator(train_batch, 
                    train_steps, 
                    epochs=20, 
                    verbose=1, 
                    callbacks=[csv_logger, checkpoint], 
                    validation_data=val_batch, 
                    validation_steps=val_steps, 
                    initial_epoch=0)


Epoch 1/20
813/813 [==============================] - 2063s - loss: 0.8427 - val_loss: 0.6818
Epoch 2/20
813/813 [==============================] - 2053s - loss: 0.5841 - val_loss: 0.5631
Epoch 3/20
813/813 [==============================] - 2052s - loss: 0.5171 - val_loss: 0.5847
Epoch 4/20
813/813 [==============================] - 2053s - loss: 0.4730 - val_loss: 0.5128
Epoch 5/20
813/813 [==============================] - 2053s - loss: 0.4532 - val_loss: 0.5554
Epoch 6/20
813/813 [==============================] - 2054s - loss: 0.4362 - val_loss: 0.5531
Epoch 7/20
813/813 [==============================] - 2054s - loss: 0.4387 - val_loss: 0.5861
Epoch 8/20
813/813 [==============================] - 2053s - loss: 0.4186 - val_loss: 0.5477
Epoch 9/20
813/813 [==============================] - 2054s - loss: 0.4254 - val_loss: 0.5060
Epoch 10/20
813/813 [==============================] - 2055s - loss: 0.4135 - val_loss: 0.5132
Epoch 11/20
813/813 [==============================] - 2054s - loss: 0.4235 - val_loss: 0.5307
Epoch 12/20
813/813 [==============================] - 2053s - loss: 0.4162 - val_loss: 0.5250
Epoch 13/20
813/813 [==============================] - 2054s - loss: 0.4003 - val_loss: 0.4799
Epoch 14/20
813/813 [==============================] - 2053s - loss: 0.4108 - val_loss: 0.5082
Epoch 15/20
813/813 [==============================] - 2054s - loss: 0.3992 - val_loss: 0.4705
Epoch 16/20
813/813 [==============================] - 2053s - loss: 0.4046 - val_loss: 0.5610
Epoch 17/20
813/813 [==============================] - 2054s - loss: 0.3826 - val_loss: 0.4966
Epoch 18/20
813/813 [==============================] - 2054s - loss: 0.4230 - val_loss: 0.4479
Epoch 19/20
813/813 [==============================] - 2054s - loss: 0.3928 - val_loss: 0.4900
Epoch 20/20
813/813 [==============================] - 2054s - loss: 0.4042 - val_loss: 0.4522
Out[15]:
<keras.callbacks.History at 0x7f50dfde0f90>

In [ ]: