Line Follower - CompRobo17

This notebook will show the general procedure to use our project data directories and how to do a regression task using convnets

Imports and Directories


In [1]:
#Create references to important directories we will use over and over
import os, sys
DATA_HOME_DIR = '/home/nathan/olin/spring2017/line-follower/line-follower/data'

In [2]:
#import modules
import numpy as np
from glob import glob
from PIL import Image
from tqdm import tqdm
from scipy.ndimage import zoom

from keras.models import Sequential
from keras.metrics import categorical_crossentropy, categorical_accuracy
from keras.layers.convolutional import *
from keras.preprocessing import image
from keras.layers.core import Flatten, Dense
from keras.optimizers import Adam
from keras.layers.normalization import BatchNormalization

from matplotlib import pyplot as plt
import seaborn as sns
%matplotlib inline


Using TensorFlow backend.

Create paths to data directories


In [3]:
%cd $DATA_HOME_DIR

path = DATA_HOME_DIR
train_path=path + '/sun_apr_16_office_full_line_1'
valid_path=path + '/sun_apr_16_office_full_line_2'


/home/nathan/olin/spring2017/line-follower/line-follower/data

Helper Functions

Throughout the notebook, we will take advantage of helper functions to cleanly process our data.


In [4]:
def resize_vectorized4D(data, new_size=(64, 64)):
    """
    A vectorized implementation of 4d image resizing
    
    Args:
        data (4D array): The images you want to resize
        new_size (tuple): The desired image size
        
    Returns: (4D array): The resized images
    """
    fy, fx = np.asarray(new_size, np.float32) / data.shape[1:3]
    return zoom(data, (1, fy, fx, 1), order=1) # order is the order of spline interpolation

In [5]:
def lowerHalfImage(array):
    """ 
    Returns the lower half rows of an image
    
    Args: array (array): the array you want to extract the lower half from
    
    Returns: The lower half of the array
    """
    return array[round(array.shape[0]/2):,:,:]

In [6]:
def folder_to_numpy(image_directory_full):
    """
    Read sorted pictures (by filename) in a folder to a numpy array. 
    We have hardcoded the extraction of the lower half of the images as
    that is the relevant data
    
    USAGE:
        data_folder = '/train/test1'
        X_train = folder_to_numpy(data_folder)
    
    Args:
        data_folder (str): The relative folder from DATA_HOME_DIR
        
    Returns:
        picture_array (np array): The numpy array in tensorflow format
    """
    # change directory
    print ("Moving to directory: " + image_directory_full)
    os.chdir(image_directory_full)
    
    # read in filenames from directory
    g = glob('*.png')
    if len(g) == 0:
        g = glob('*.jpg')
    print ("Found {} pictures".format(len(g)))
    
    # sort filenames
    g.sort()
    
    # open and convert images to numpy array - then extract the lower half of each image
    print("Starting pictures to numpy conversion")
    picture_arrays = np.array([lowerHalfImage(np.array(Image.open(image_path))) for image_path in g])
    
#     reshape to tensorflow format
#     picture_arrays = picture_arrays.reshape(*picture_arrays.shape, 1)
    print ("Shape of output: {}".format(picture_arrays.shape))
    
    # return array
    return picture_arrays
    return picture_arrays.astype('float32')

In [7]:
def flip4DArray(array):
    """ Produces the mirror images of a 4D image array """
    return array[..., ::-1,:] #[:,:,::-1] also works but is 50% slower

In [8]:
def concatCmdVelFlip(array):
    """ Concatentaes and returns Cmd Vel array """
    return np.concatenate((array, array*-1)) # multiply by negative 1 for opposite turn

Data

Because we are using a CNN and unordered pictures, we can flip our data and concatenate it on the end of all training and validation data to make sure we don't bias left or right turns.

Training Data

Extract and store the training data in X_train and Y_train


In [9]:
%cd $train_path
Y_train = np.genfromtxt('cmd_vel.csv', delimiter=',')[:,1] # only use turning angle
Y_train = np.concatenate((Y_train, Y_train*-1))
X_train = folder_to_numpy(train_path + '/raw')
X_train = np.concatenate((X_train, flip4DArray(X_train)))


/home/nathan/olin/spring2017/line-follower/line-follower/data/sun_apr_16_office_full_line_1
Moving to directory: /home/nathan/olin/spring2017/line-follower/line-follower/data/sun_apr_16_office_full_line_1/raw
Found 286 pictures
Starting pictures to numpy conversion
Shape of output: (286, 240, 640, 3)

Test the shape of the arrays:
X_train: (N, 240, 640, 3)
Y_train: (N,)


In [10]:
X_train.shape, Y_train.shape


Out[10]:
((572, 240, 640, 3), (572,))

Visualize the training data, currently using a hacky method to display the numpy matrix as this is being run over a remote server and I can't view new windows


In [11]:
%cd /tmp
img = Image.fromarray(X_train[0], 'RGB')
img.save("temp.jpg")
image.load_img("temp.jpg")


/tmp
Out[11]:

Validation Data

Follow the same steps for as the training data for the validation data.


In [12]:
%cd $valid_path
Y_valid = np.genfromtxt('cmd_vel.csv', delimiter=',')[:,1]
Y_valid = np.concatenate((Y_valid, Y_valid*-1))
X_valid = folder_to_numpy(valid_path + '/raw')
X_valid = np.concatenate((X_valid, flip4DArray(X_valid)))


/home/nathan/olin/spring2017/line-follower/line-follower/data/sun_apr_16_office_full_line_2
Moving to directory: /home/nathan/olin/spring2017/line-follower/line-follower/data/sun_apr_16_office_full_line_2/raw
Found 130 pictures
Starting pictures to numpy conversion
Shape of output: (130, 240, 640, 3)

Test the shape of the arrays:
X_valid: (N, 240, 640, 3)
Y_valid: (N,)


In [13]:
X_valid.shape, Y_valid.shape


Out[13]:
((260, 240, 640, 3), (260,))

Resize Data

When we train the network, we don't want to be dealing with (240, 640, 3) images as they are way too big. Instead, we will resize the images to something more managable, like (64, 64, 3) or (128, 128, 3). In terms of network predictive performance, we are not concerned with the change in aspect ratio, but might want to test a (24, 64, 3) images for faster training


In [7]:
img_rows, img_cols = (64, 64)

In [15]:
print(img_rows)
print(img_cols)


64
64

In [16]:
X_train = resize_vectorized4D(X_train, (img_rows, img_cols))
X_valid = resize_vectorized4D(X_valid, (img_rows, img_cols))

In [17]:
print(X_train.shape)
print(X_valid.shape)


(572, 64, 64, 3)
(260, 64, 64, 3)

Visualize newly resized image.


In [18]:
%cd /tmp
img = Image.fromarray(X_train[np.random.randint(0, X_train.shape[0])], 'RGB')
img.save("temp.jpg")
image.load_img("temp.jpg")


/tmp
Out[18]:

Batches

gen allows us to normalize and augment our images. We will just use it to rescale the images.


In [19]:
gen = image.ImageDataGenerator(
#                                 rescale=1. / 255 # normalize data between 0 and 1
                              )

Next, create the train and valid generators, these are shuffle and have a batch size of 32 by default


In [20]:
train_generator = gen.flow(X_train, Y_train)#, batch_size=batch_size, shuffle=True)
valid_generator = gen.flow(X_valid, Y_valid)#, batch_size=batch_size, shuffle=True)

# get_batches(train_path, batch_size=batch_size, 
#                             target_size=in_shape, 
#                             gen=gen)
# val_batches   = get_batches(valid_path, batch_size=batch_size, 
#                             target_size=in_shape, 
#                             gen=gen)

In [21]:
data, category = next(train_generator)
print ("Shape of data: {}".format(data[0].shape))
%cd /tmp
img = Image.fromarray(data[np.random.randint(0, data.shape[0])].astype('uint8'), 'RGB')
img.save("temp.jpg")
image.load_img("temp.jpg")


Shape of data: (64, 64, 3)
/tmp
Out[21]:

Convnet

Constants


In [8]:
in_shape = (img_rows, img_cols, 3)

Model

Our test model will use a VGG like structure with a few changes. We are removing the final activation function. We will also use either mean_absolute_error or mean_squared_error as our loss function for regression purposes.


In [9]:
def get_model():
    model = Sequential([
        Convolution2D(32,3,3, border_mode='same', activation='relu', input_shape=in_shape),
        MaxPooling2D(),
        Convolution2D(64,3,3, border_mode='same', activation='relu'),
        MaxPooling2D(),
        Convolution2D(128,3,3, border_mode='same', activation='relu'),
        MaxPooling2D(),
        Flatten(),
        Dense(2048, activation='relu'),
        Dense(1024, activation='relu'),
        Dense(512, activation='relu'),
        Dense(1)
        ])
    model.compile(loss='mean_absolute_error', optimizer='adam')
    return model

In [10]:
model = get_model()

In [11]:
model.summary()


____________________________________________________________________________________________________
Layer (type)                     Output Shape          Param #     Connected to                     
====================================================================================================
convolution2d_1 (Convolution2D)  (None, 64, 64, 32)    896         convolution2d_input_1[0][0]      
____________________________________________________________________________________________________
maxpooling2d_1 (MaxPooling2D)    (None, 32, 32, 32)    0           convolution2d_1[0][0]            
____________________________________________________________________________________________________
convolution2d_2 (Convolution2D)  (None, 32, 32, 64)    18496       maxpooling2d_1[0][0]             
____________________________________________________________________________________________________
maxpooling2d_2 (MaxPooling2D)    (None, 16, 16, 64)    0           convolution2d_2[0][0]            
____________________________________________________________________________________________________
convolution2d_3 (Convolution2D)  (None, 16, 16, 128)   73856       maxpooling2d_2[0][0]             
____________________________________________________________________________________________________
maxpooling2d_3 (MaxPooling2D)    (None, 8, 8, 128)     0           convolution2d_3[0][0]            
____________________________________________________________________________________________________
flatten_1 (Flatten)              (None, 8192)          0           maxpooling2d_3[0][0]             
____________________________________________________________________________________________________
dense_1 (Dense)                  (None, 2048)          16779264    flatten_1[0][0]                  
____________________________________________________________________________________________________
dense_2 (Dense)                  (None, 1024)          2098176     dense_1[0][0]                    
____________________________________________________________________________________________________
dense_3 (Dense)                  (None, 512)           524800      dense_2[0][0]                    
____________________________________________________________________________________________________
dense_4 (Dense)                  (None, 1)             513         dense_3[0][0]                    
====================================================================================================
Total params: 19,496,001
Trainable params: 19,496,001
Non-trainable params: 0
____________________________________________________________________________________________________

Train


In [ ]:
history = model.fit_generator(train_generator, 
                    samples_per_epoch=train_generator.n,
                    nb_epoch=2500,
                    validation_data=valid_generator,
                    nb_val_samples=valid_generator.n,
                    verbose=True)


Epoch 1/2500
572/572 [==============================] - 1s - loss: 48.2472 - val_loss: 0.0874
Epoch 2/2500
572/572 [==============================] - 0s - loss: 0.0466 - val_loss: 0.0365
Epoch 3/2500
572/572 [==============================] - 0s - loss: 0.0354 - val_loss: 0.0351
Epoch 4/2500
572/572 [==============================] - 0s - loss: 0.0342 - val_loss: 0.0362
Epoch 5/2500
572/572 [==============================] - 0s - loss: 0.0329 - val_loss: 0.0312
Epoch 6/2500
572/572 [==============================] - 0s - loss: 0.0322 - val_loss: 0.0333
Epoch 7/2500
572/572 [==============================] - 0s - loss: 0.0320 - val_loss: 0.0340
Epoch 8/2500
572/572 [==============================] - 0s - loss: 0.0317 - val_loss: 0.0332
Epoch 9/2500
572/572 [==============================] - 0s - loss: 0.0323 - val_loss: 0.0322
Epoch 10/2500
572/572 [==============================] - 0s - loss: 0.0311 - val_loss: 0.0332
Epoch 11/2500
572/572 [==============================] - 0s - loss: 0.0307 - val_loss: 0.0335
Epoch 12/2500
572/572 [==============================] - 0s - loss: 0.0324 - val_loss: 0.0321
Epoch 13/2500
572/572 [==============================] - 0s - loss: 0.0314 - val_loss: 0.0316
Epoch 14/2500
572/572 [==============================] - 0s - loss: 0.0309 - val_loss: 0.0322
Epoch 15/2500
572/572 [==============================] - 0s - loss: 0.0301 - val_loss: 0.0303
Epoch 16/2500
572/572 [==============================] - 0s - loss: 0.0284 - val_loss: 0.0328
Epoch 17/2500
572/572 [==============================] - 0s - loss: 0.0306 - val_loss: 0.0317
Epoch 18/2500
572/572 [==============================] - 0s - loss: 0.0292 - val_loss: 0.0302
Epoch 19/2500
572/572 [==============================] - 0s - loss: 0.0305 - val_loss: 0.0323
Epoch 20/2500
572/572 [==============================] - 0s - loss: 0.0301 - val_loss: 0.0310
Epoch 21/2500
572/572 [==============================] - 0s - loss: 0.0288 - val_loss: 0.0327
Epoch 22/2500
572/572 [==============================] - 0s - loss: 0.0295 - val_loss: 0.0289
Epoch 23/2500
572/572 [==============================] - 0s - loss: 0.0283 - val_loss: 0.0304
Epoch 24/2500
572/572 [==============================] - 0s - loss: 0.0295 - val_loss: 0.0310
Epoch 25/2500
572/572 [==============================] - 0s - loss: 0.0285 - val_loss: 0.0309
Epoch 26/2500
572/572 [==============================] - 0s - loss: 0.0283 - val_loss: 0.0319
Epoch 27/2500
 32/572 [>.............................] - ETA: 0s - loss: 0.0306

In [ ]:
# %cd $DATA_HOME_DIR
# model.save_weights('epoche_2500.h5')

In [12]:
%cd $DATA_HOME_DIR
model.load_weights('epoche_2500.h5')


/home/nathan/olin/spring2017/line-follower/line-follower/data

Visualize Training


In [ ]:
val_plot = np.convolve(history.history['val_loss'], np.repeat(1/10, 10), mode='valid')
train_plot = np.convolve(history.history['loss'], np.repeat(1/10, 10), mode='valid')

In [ ]:
sns.tsplot(val_plot)

In [ ]:
X_preds = model.predict(X_valid).reshape(X_valid.shape[0],)
for i in range(len(X_valid)):
    print("{:07f} | {:07f}".format(Y_valid[i], X_preds[i]))

In [ ]:
X_train_preds = model.predict(X_train).reshape(X_train.shape[0],)
for i in range(len(X_train_preds)):
    print("{:07f} | {:07f}".format(Y_train[i], X_train_preds[i]))

Notes

  • 32 by 32 images are too small resolution for regression
  • 64 by 64 seemed to work really well
  • Moving average plot to see val_loss over time is really nice
  • Can take up to 2000 epochs to reach a nice minimum

In [ ]: