Behavioral Cloning


In [1]:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import collections
import cv2 #this needs to be imported before TF!!
import tensorflow as tf
from tensorflow.contrib.layers import flatten
import random
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from six.moves import map
import re
import os
from datetime import datetime

%matplotlib inline

In [16]:
"""Embed a YouTube video via its embed url into a notebook."""
from functools import partial

from IPython.display import display, IFrame

width, height = (560, 315, )

def _iframe_attrs(embed_url):
    """Get IFrame args."""
    return (
        ('src', 'width', 'height'), 
        (embed_url, width, height, ),
    )

def _get_args(embed_url):
    """Get args for type to create a class."""
    iframe = dict(zip(*_iframe_attrs(embed_url)))
    attrs = {
        'display': partial(display, IFrame(**iframe)),
    }
    return ('YouTubeVideo', (object, ), attrs, )

def youtube_video(embed_url):
    """Embed YouTube video into a notebook.
    Place this module into the same directory as the notebook.
    >>> from embed import youtube_video
    >>> youtube_video(url).display()
    """
    YouTubeVideo = type(*_get_args(embed_url)) # make a class
    return YouTubeVideo() # return an object

Preprocessing


In [2]:
# Summary Distribution before preprocessing
driving_log = pd.read_csv("data/driving_log.csv")
driving_log.columns = ['center_image','left_image','right_image','steering_angle','throttle','break','speed']
driving_log.steering_angle.plot.hist(alpha=0.7,bins=100)
_summary = {'mean':driving_log.steering_angle.mean(), 
'median':driving_log.steering_angle.median(),
'max':driving_log.steering_angle.max(),
'min':driving_log.steering_angle.min(),
'std': driving_log.steering_angle.std()}
_summary


Out[2]:
{'max': 1.0,
 'mean': -0.0049362082434534632,
 'median': 0.0,
 'min': -1.0,
 'std': 0.17198275703927299}

Ground Truth Smoothing


In [3]:
# Output smoothing - smooth the ground truth steering angle by applying a rolling mean
# First compute the time difference between window start and end to make sure the frames
# belong to the same sequence
frame_lookback = 5
def compute_timestamp(df):
    conv = "%Y%m%d%H%M%S%f"
    r = r'(.*)(center_)(.*)(.jpg)'
    _res = df.center_image.str.match(r)
    df['record_time'] = [datetime.strptime(r[2].replace("_",""),conv) for r in _res]
    df['record_diff'] = df['record_time'].diff(frame_lookback) / np.timedelta64(1, 's')
    return df

In [4]:
# The  module retrieves the time the image is captured and the delta between consecutive images 
import functools
driving_log = compute_timestamp(driving_log)
_median_lookback = driving_log.record_diff.median()
print("Median time lapse between {} frames: {}".format(frame_lookback, _median_lookback))

def apply_smoothing(row, lookback):
    if row['record_diff'] > lookback:
        # Do not smooth otherwise
        return row['steering_angle']
    else:
        return row['smooth_steering_angle']
# Add some leeway to include initial dataset in smoothing
smoothing_fn = functools.partial(apply_smoothing, lookback= _median_lookback + 1 )
driving_log['smooth_steering_angle'] = driving_log['steering_angle'].rolling(frame_lookback).mean()
driving_log['smooth_steering_angle'] = driving_log.apply(smoothing_fn,axis=1).fillna(0)


/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/ipykernel/__main__.py:8: FutureWarning: In future versions of pandas, match will change to always return a bool indexer.
Median time lapse between 5 frames: 0.432

In [5]:
driving_log[['steering_angle','smooth_steering_angle']].plot()


Out[5]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f86ce829f98>

In [6]:
# Removing straight angles now
keep_straight_p = 0.25
keep = lambda x: True if np.random.random() < keep_straight_p or x != 0 else False
driving_log['is_kept'] = driving_log.steering_angle.apply(keep)
print("Before filter: {}".format(driving_log.shape[0]))
filtered_driving_log = driving_log[driving_log['is_kept']]
print("After filter: {}".format(filtered_driving_log.shape[0]))


Before filter: 39873
After filter: 35492

In [7]:
# Ground truth processing after smoothing & filtering
filtered_driving_log.smooth_steering_angle.plot.hist(alpha=0.7,bins=100)
_summary = {'mean':filtered_driving_log.steering_angle.mean(), 
'median':filtered_driving_log.smooth_steering_angle.median(),
'max':filtered_driving_log.smooth_steering_angle.max(),
'min':filtered_driving_log.smooth_steering_angle.min(),
'std': filtered_driving_log.smooth_steering_angle.std()}
_summary


Out[7]:
{'max': 1.0000000000000011,
 'mean': -0.0055455153637783148,
 'median': -0.013388119099999388,
 'min': -1.0000000000000007,
 'std': 0.17591982406876785}

In [8]:
# adjust the steering angle heuristicly
# filter instances of straight driving
# create a mapping from imagenames to angles
# Take the smoothed angle
f = lambda x: "".join(x.split("/")[-1])
correction =  s = np.random.normal(0.25, 0.05, filtered_driving_log.shape[0])
center = pd.DataFrame({'image': filtered_driving_log['center_image'].apply(f), 'steering_angle': filtered_driving_log['smooth_steering_angle']})
left = pd.DataFrame({'image': filtered_driving_log['left_image'].apply(f), 'steering_angle': filtered_driving_log['smooth_steering_angle'] + correction })
right = pd.DataFrame({'image': filtered_driving_log['right_image'].apply(f), 'steering_angle': filtered_driving_log['smooth_steering_angle'] - correction})

steering_angles = pd.concat([center, left, right])
target_map = pd.Series(steering_angles.steering_angle.values,index=steering_angles.image)
# # Split training / validation
_target_map_train, _target_map_validation = train_test_split(target_map, test_size= 0.1)
target_map_train, target_map_validation = _target_map_train.to_dict(), _target_map_validation.to_dict()
print("image data size: {}".format(len(target_map_train)))

_dir = "data/IMG/"
# Assert # images == # of annotated images
file_names = os.listdir(_dir)
#assert len(target_map) == len(file_names)
# No NaNs in the target data
assert sum(np.isnan(np.fromiter(iter(target_map_train.values()), dtype=float))) == 0


image data size: 95828

In [9]:
from data_generator.image import ImageDataGenerator
input_shape, output_shape = (160, 320, 3) , (32, 128, 3)
batch_size = 256

Visualizations


In [10]:
# investigate target distribution
gen = ImageDataGenerator(target_map = target_map_train, 
                         root_path = _dir, 
                         input_shape = input_shape, 
                         output_shape = output_shape, 
                         batch_size = batch_size)
# Generate some data
i = 0
X, y = [], []
while i < 5:
    i += 1
    _x, _y = next(gen)
    X.append(_x)
    y.append(_y)
print("done")

# Visualize the distribution of simulated training targets for couple of batches of training data
df = pd.DataFrame(y).T
f = plt.figure(figsize=(10, 10))
df.plot.hist(alpha=0.5, bins=20, legend=False, ax=f.gca())


done
Out[10]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f86c9967080>

In [11]:
nb_images = 10
i = np.random.randint(1,128,nb_images)
#plt.imshow(X[0][5])
plt.figure(figsize=(75,50))
for ix, _i in enumerate(i):
    plt.subplot(nb_images,1,ix+1)
    plt.imshow(X[0][_i,...]) 
    plt.axis('off')
    plt.title("Steering Angle: " + str(np.round(y[0][_i],4)),fontsize=20);


Training


In [12]:
# Set up the training and validation generator
train_gen = ImageDataGenerator(target_map = target_map_train, 
                         root_path = _dir, 
                         input_shape = input_shape, 
                         output_shape = output_shape, 
                         batch_size = batch_size)

val_gen = ImageDataGenerator(target_map = target_map_validation, 
                         root_path = _dir, 
                         input_shape = input_shape, 
                         output_shape = output_shape, 
                         batch_size = batch_size,
                         training = False)

In [13]:
# Set up the model
from model.model import CNNModel
model = CNNModel(input_shape = output_shape, 
                 nb_fully_connected = 128, 
                 filter_sizes = [8,5,3], 
                 pool_size = (2,2), 
                 dropout_p = 0.5, 
                 l2_reg = 0.005 )
print("Model Summary:")
model.model.summary()


Using TensorFlow backend.
Building model...
Compiled the model...
Model Summary:
____________________________________________________________________________________________________
Layer (type)                     Output Shape          Param #     Connected to                     
====================================================================================================
lambda_1 (Lambda)                (None, 32, 128, 3)    0           lambda_input_1[0][0]             
____________________________________________________________________________________________________
convolution2d_1 (Convolution2D)  (None, 32, 128, 16)   3088        lambda_1[0][0]                   
____________________________________________________________________________________________________
maxpooling2d_1 (MaxPooling2D)    (None, 16, 64, 16)    0           convolution2d_1[0][0]            
____________________________________________________________________________________________________
convolution2d_2 (Convolution2D)  (None, 16, 64, 32)    12832       maxpooling2d_1[0][0]             
____________________________________________________________________________________________________
maxpooling2d_2 (MaxPooling2D)    (None, 8, 32, 32)     0           convolution2d_2[0][0]            
____________________________________________________________________________________________________
convolution2d_3 (Convolution2D)  (None, 8, 32, 64)     18496       maxpooling2d_2[0][0]             
____________________________________________________________________________________________________
maxpooling2d_3 (MaxPooling2D)    (None, 4, 16, 64)     0           convolution2d_3[0][0]            
____________________________________________________________________________________________________
convolution2d_4 (Convolution2D)  (None, 4, 16, 64)     36928       maxpooling2d_3[0][0]             
____________________________________________________________________________________________________
maxpooling2d_4 (MaxPooling2D)    (None, 2, 8, 64)      0           convolution2d_4[0][0]            
____________________________________________________________________________________________________
flatten_1 (Flatten)              (None, 1024)          0           maxpooling2d_4[0][0]             
____________________________________________________________________________________________________
dropout_1 (Dropout)              (None, 1024)          0           flatten_1[0][0]                  
____________________________________________________________________________________________________
dense_1 (Dense)                  (None, 128)           131200      dropout_1[0][0]                  
____________________________________________________________________________________________________
dense_2 (Dense)                  (None, 64)            8256        dense_1[0][0]                    
____________________________________________________________________________________________________
dense_3 (Dense)                  (None, 1)             65          dense_2[0][0]                    
====================================================================================================
Total params: 210,865
Trainable params: 210,865
Non-trainable params: 0
____________________________________________________________________________________________________

In [14]:
# Call *fit_generator*
hist = model.train(train_gen,
                   batch_size = 100,
                   nb_epochs= 100,
                   val_gen = val_gen
                  )


Epoch 1/100
12800/12800 [==============================] - 302s - loss: 1.5119 - val_loss: 1.3937
Epoch 2/100
12800/12800 [==============================] - 280s - loss: 1.2874 - val_loss: 1.1763
Epoch 3/100
12800/12800 [==============================] - 277s - loss: 1.0946 - val_loss: 1.0024
Epoch 4/100
12800/12800 [==============================] - 307s - loss: 0.9332 - val_loss: 0.8624
Epoch 5/100
12800/12800 [==============================] - 303s - loss: 0.7975 - val_loss: 0.7307
Epoch 6/100
12800/12800 [==============================] - 376s - loss: 0.6858 - val_loss: 0.6361
Epoch 7/100
12800/12800 [==============================] - 274s - loss: 0.5910 - val_loss: 0.5465
Epoch 8/100
12628/12800 [============================>.] - ETA: 3s - loss: 0.5097  
/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/keras/engine/training.py:1569: UserWarning: Epoch comprised more than `samples_per_epoch` samples, which might affect learning results. Set `samples_per_epoch` correctly to avoid this warning.
  warnings.warn('Epoch comprised more than '
12884/12800 [==============================] - 251s - loss: 0.5089 - val_loss: 0.4661
Epoch 9/100
12800/12800 [==============================] - 265s - loss: 0.4408 - val_loss: 0.3998
Epoch 10/100
12800/12800 [==============================] - 250s - loss: 0.3839 - val_loss: 0.3511
Epoch 11/100
12800/12800 [==============================] - 240s - loss: 0.3348 - val_loss: 0.3204
Epoch 12/100
12800/12800 [==============================] - 232s - loss: 0.2951 - val_loss: 0.2732
Epoch 13/100
12800/12800 [==============================] - 252s - loss: 0.2622 - val_loss: 0.2443
Epoch 14/100
12800/12800 [==============================] - 267s - loss: 0.2324 - val_loss: 0.2146
Epoch 15/100
12884/12800 [==============================] - 251s - loss: 0.2073 - val_loss: 0.1955
Epoch 16/100
12800/12800 [==============================] - 246s - loss: 0.1863 - val_loss: 0.1722
Epoch 17/100
12800/12800 [==============================] - 260s - loss: 0.1685 - val_loss: 0.1557
Epoch 18/100
12800/12800 [==============================] - 241s - loss: 0.1528 - val_loss: 0.1443
Epoch 19/100
12800/12800 [==============================] - 263s - loss: 0.1399 - val_loss: 0.1293
Epoch 20/100
12800/12800 [==============================] - 252s - loss: 0.1280 - val_loss: 0.1143
Epoch 21/100
12800/12800 [==============================] - 246s - loss: 0.1183 - val_loss: 0.1106
Epoch 22/100
12800/12800 [==============================] - 285s - loss: 0.1072 - val_loss: 0.0949
Epoch 23/100
12884/12800 [==============================] - 262s - loss: 0.1003 - val_loss: 0.0967
Epoch 24/100
12800/12800 [==============================] - 272s - loss: 0.0936 - val_loss: 0.0833
Epoch 25/100
12800/12800 [==============================] - 258s - loss: 0.0870 - val_loss: 0.0788
Epoch 26/100
12800/12800 [==============================] - 253s - loss: 0.0818 - val_loss: 0.0750
Epoch 27/100
12800/12800 [==============================] - 250s - loss: 0.0769 - val_loss: 0.0723
Epoch 28/100
12800/12800 [==============================] - 267s - loss: 0.0719 - val_loss: 0.0675
Epoch 29/100
12800/12800 [==============================] - 295s - loss: 0.0693 - val_loss: 0.0612
Epoch 30/100
12884/12800 [==============================] - 264s - loss: 0.0659 - val_loss: 0.0613
Epoch 31/100
12800/12800 [==============================] - 233s - loss: 0.0613 - val_loss: 0.0523
Epoch 32/100
12800/12800 [==============================] - 232s - loss: 0.0581 - val_loss: 0.0534
Epoch 33/100
12800/12800 [==============================] - 234s - loss: 0.0562 - val_loss: 0.0488
Epoch 34/100
12800/12800 [==============================] - 234s - loss: 0.0528 - val_loss: 0.0465
Epoch 35/100
12800/12800 [==============================] - 233s - loss: 0.0536 - val_loss: 0.0416
Epoch 36/100
12800/12800 [==============================] - 233s - loss: 0.0492 - val_loss: 0.0406
Epoch 37/100
12800/12800 [==============================] - 233s - loss: 0.0468 - val_loss: 0.0402
Epoch 38/100
12884/12800 [==============================] - 235s - loss: 0.0459 - val_loss: 0.0428
Epoch 39/100
12800/12800 [==============================] - 234s - loss: 0.0450 - val_loss: 0.0300
Epoch 40/100
12800/12800 [==============================] - 234s - loss: 0.0428 - val_loss: 0.0326
Epoch 41/100
12800/12800 [==============================] - 238s - loss: 0.0419 - val_loss: 0.0351
Epoch 42/100
12800/12800 [==============================] - 237s - loss: 0.0405 - val_loss: 0.0322
Epoch 43/100
12800/12800 [==============================] - 236s - loss: 0.0407 - val_loss: 0.0378
Epoch 44/100
12800/12800 [==============================] - 236s - loss: 0.0402 - val_loss: 0.0341
Epoch 45/100
12884/12800 [==============================] - 238s - loss: 0.0384 - val_loss: 0.0355
Epoch 46/100
12800/12800 [==============================] - 239s - loss: 0.0386 - val_loss: 0.0326
Epoch 47/100
12800/12800 [==============================] - 240s - loss: 0.0385 - val_loss: 0.0333
Epoch 48/100
12800/12800 [==============================] - 236s - loss: 0.0371 - val_loss: 0.0303
Epoch 49/100
12800/12800 [==============================] - 237s - loss: 0.0361 - val_loss: 0.0315
Epoch 50/100
12800/12800 [==============================] - 291s - loss: 0.0356 - val_loss: 0.0323
Epoch 51/100
12800/12800 [==============================] - 296s - loss: 0.0362 - val_loss: 0.0303
Epoch 52/100
12800/12800 [==============================] - 240s - loss: 0.0356 - val_loss: 0.0279
Epoch 53/100
12884/12800 [==============================] - 248s - loss: 0.0359 - val_loss: 0.0203
Epoch 54/100
12800/12800 [==============================] - 259s - loss: 0.0349 - val_loss: 0.0300
Epoch 55/100
12800/12800 [==============================] - 282s - loss: 0.0348 - val_loss: 0.0295
Epoch 56/100
12800/12800 [==============================] - 248s - loss: 0.0343 - val_loss: 0.0339
Epoch 57/100
12800/12800 [==============================] - 251s - loss: 0.0340 - val_loss: 0.0303
Epoch 58/100
12800/12800 [==============================] - 244s - loss: 0.0351 - val_loss: 0.0314
Epoch 59/100
12800/12800 [==============================] - 279s - loss: 0.0341 - val_loss: 0.0281
Epoch 60/100
12884/12800 [==============================] - 274s - loss: 0.0347 - val_loss: 0.0251
Epoch 61/100
12800/12800 [==============================] - 290s - loss: 0.0335 - val_loss: 0.0321
Epoch 62/100
12800/12800 [==============================] - 254s - loss: 0.0334 - val_loss: 0.0290
Epoch 63/100
12800/12800 [==============================] - 269s - loss: 0.0340 - val_loss: 0.0267
Epoch 64/100
12800/12800 [==============================] - 274s - loss: 0.0321 - val_loss: 0.0343
Epoch 65/100
12800/12800 [==============================] - 273s - loss: 0.0329 - val_loss: 0.0273
Epoch 66/100
12800/12800 [==============================] - 261s - loss: 0.0341 - val_loss: 0.0226
Epoch 67/100
12800/12800 [==============================] - 270s - loss: 0.0334 - val_loss: 0.0294
Epoch 68/100
12884/12800 [==============================] - 297s - loss: 0.0320 - val_loss: 0.0254
Epoch 69/100
12800/12800 [==============================] - 268s - loss: 0.0324 - val_loss: 0.0282
Epoch 70/100
12800/12800 [==============================] - 280s - loss: 0.0325 - val_loss: 0.0288
Epoch 71/100
12800/12800 [==============================] - 318s - loss: 0.0317 - val_loss: 0.0232
Epoch 72/100
12800/12800 [==============================] - 301s - loss: 0.0322 - val_loss: 0.0245
Epoch 73/100
12800/12800 [==============================] - 345s - loss: 0.0320 - val_loss: 0.0193
Epoch 74/100
12800/12800 [==============================] - 343s - loss: 0.0324 - val_loss: 0.0233
Epoch 75/100
12884/12800 [==============================] - 296s - loss: 0.0325 - val_loss: 0.0233
Epoch 76/100
10752/12800 [========================>.....] - ETA: 40s - loss: 0.0311 
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-14-52566aba79a2> in <module>()
      3                    batch_size = 100,
      4                    nb_epochs= 100,
----> 5                    val_gen = val_gen
      6                   )

/src/model/model.py in train(self, gen, batch_size, nb_epochs, val_gen)
     62             nb_epoch = nb_epochs,
     63             validation_data = val_gen,
---> 64             nb_val_samples = batch_size * 5)
     65 
     66     def save(self, file_json, file_weights):

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/keras/models.py in fit_generator(self, generator, samples_per_epoch, nb_epoch, verbose, callbacks, validation_data, nb_val_samples, class_weight, max_q_size, nb_worker, pickle_safe, initial_epoch, **kwargs)
    933                                         nb_worker=nb_worker,
    934                                         pickle_safe=pickle_safe,
--> 935                                         initial_epoch=initial_epoch)
    936 
    937     def evaluate_generator(self, generator, val_samples,

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/keras/engine/training.py in fit_generator(self, generator, samples_per_epoch, nb_epoch, verbose, callbacks, validation_data, nb_val_samples, class_weight, max_q_size, nb_worker, pickle_safe, initial_epoch)
   1551                     outs = self.train_on_batch(x, y,
   1552                                                sample_weight=sample_weight,
-> 1553                                                class_weight=class_weight)
   1554 
   1555                     if not isinstance(outs, list):

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/keras/engine/training.py in train_on_batch(self, x, y, sample_weight, class_weight)
   1314             ins = x + y + sample_weights
   1315         self._make_train_function()
-> 1316         outputs = self.train_function(ins)
   1317         if len(outputs) == 1:
   1318             return outputs[0]

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/keras/backend/tensorflow_backend.py in __call__(self, inputs)
   1898         session = get_session()
   1899         updated = session.run(self.outputs + [self.updates_op],
-> 1900                               feed_dict=feed_dict)
   1901         return updated[:len(self.outputs)]
   1902 

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/tensorflow/python/client/session.py in run(self, fetches, feed_dict, options, run_metadata)
    764     try:
    765       result = self._run(None, fetches, feed_dict, options_ptr,
--> 766                          run_metadata_ptr)
    767       if run_metadata:
    768         proto_data = tf_session.TF_GetBuffer(run_metadata_ptr)

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/tensorflow/python/client/session.py in _run(self, handle, fetches, feed_dict, options, run_metadata)
    962     if final_fetches or final_targets:
    963       results = self._do_run(handle, final_targets, final_fetches,
--> 964                              feed_dict_string, options, run_metadata)
    965     else:
    966       results = []

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/tensorflow/python/client/session.py in _do_run(self, handle, target_list, fetch_list, feed_dict, options, run_metadata)
   1012     if handle is None:
   1013       return self._do_call(_run_fn, self._session, feed_dict, fetch_list,
-> 1014                            target_list, options, run_metadata)
   1015     else:
   1016       return self._do_call(_prun_fn, self._session, handle, feed_dict,

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/tensorflow/python/client/session.py in _do_call(self, fn, *args)
   1019   def _do_call(self, fn, *args):
   1020     try:
-> 1021       return fn(*args)
   1022     except errors.OpError as e:
   1023       message = compat.as_text(e.message)

/root/miniconda3/envs/carnd-term1/lib/python3.5/site-packages/tensorflow/python/client/session.py in _run_fn(session, feed_dict, fetch_list, target_list, options, run_metadata)
   1001         return tf_session.TF_Run(session, options,
   1002                                  feed_dict, fetch_list, target_list,
-> 1003                                  status, run_metadata)
   1004 
   1005     def _prun_fn(session, handle, feed_dict, fetch_list):

KeyboardInterrupt: 

In [15]:
#save the model
model.save("model.json", "model.h5" )