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.

In [3]:
import bcolz

Create paths to data directories


In [4]:
%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 [5]:
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 [6]:
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 [7]:
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 [8]:
def flip4DArray(array):
    """ Produces the mirror images of a 4D image array """
    return array[..., ::-1,:] #[:,:,::-1] also works but is 50% slower

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

In [10]:
def save_array(fname, arr):
    c=bcolz.carray(arr, rootdir=fname, mode='w')
    c.flush()

In [11]:
def load_array(fname):
    return bcolz.open(fname)[:]

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 [12]:
%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 [13]:
X_train.shape, Y_train.shape


Out[13]:
((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 [14]:
%cd /tmp
img = Image.fromarray(X_train[0], 'RGB')
img.save("temp.jpg")
image.load_img("temp.jpg")


/tmp
Out[14]:

Validation Data

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


In [15]:
%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 [16]:
X_valid.shape, Y_valid.shape


Out[16]:
((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 [17]:
img_rows, img_cols = (64, 64)

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


64
64

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

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


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

Visualize newly resized image.


In [21]:
%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[21]:

Batches

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


In [22]:
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 [23]:
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 [24]:
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[24]:

Convnet

Constants


In [25]:
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 [26]:
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 [27]:
model = get_model()

In [28]:
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 [29]:
# 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)

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

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


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

In [33]:
len(model.layers)


Out[33]:
11

In [34]:
model.pop()

In [35]:
len(model.layers)


Out[35]:
10

In [36]:
model.compile(loss='mean_absolute_error', optimizer='adam')

In [37]:
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]                    
====================================================================================================
Total params: 19,495,488
Trainable params: 19,495,488
Non-trainable params: 0
____________________________________________________________________________________________________

In [38]:
X_train_features = model.predict(X_train)
X_valid_features = model.predict(X_valid)

In [40]:
%cd $train_path
save_array("X_train_features.b", X_train_features)


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

In [41]:
%cd $valid_path
save_array("X_train_features.b", X_valid_features)


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

In [ ]:
X_train_features[9]

In [ ]:
def get_model_lstm():
    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

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 [ ]:
X_preds.shape

In [ ]:
X_train_preds.shape

In [ ]:
np.savetxt("X_train_valid.csv", X_preds, fmt='%.18e', delimiter=',', newline='\n')
np.savetxt("X_train_preds.csv", X_train_preds, fmt='%.18e', delimiter=',', newline='\n')

In [ ]: