Predict Vehicle Fuel Economy Using a Deep Neural Network

In this tutorial, we will use Tensorflow 2.0 with Keras to build a deep neural network that will enable us to predict a vehicle’s fuel economy (in miles per gallon) from eight different attributes: 

  1. Cylinders
  2. Displacement
  3. Horsepower
  4. Weight
  5. Acceleration 
  6. Model year 
  7. Origin
  8. Car name

We will use the Auto MPG Data Set at the UCI Machine Learning Repository.

Prerequisites

  • You have TensorFlow 2 Installed.
    • Windows 10 Users, see this post.
    • If you want to use GPU support for your TensorFlow installation, you will need to follow these steps. If you have trouble following those steps, you can follow these steps (note that the steps change quite frequently, but the overall process remains relatively the same).

Directions

Open up a new Python program (in your favorite text editor or Python IDE) and write the following code. I’m going to name the program vehicle_fuel_economy.py. I’ll explain the code later in this tutorial.

# Project: Predict Vehicle Fuel Economy Using a Deep Neural Network
# Author: Addison Sears-Collins
# Date created: November 3, 2020

import pandas as pd # Used for data analysis
import pathlib # An object-oriented interface to the filesystem
import matplotlib.pyplot as plt # Handles the creation of plots
import seaborn as sns # Data visualization library
import tensorflow as tf # Machine learning library
from tensorflow import keras # Library for neural networks
from tensorflow.keras import layers # Handles the layers of the neural network

def main():

  # Set the data path for the Auto-Mpg data set from the UCI Machine Learning Repository
  datasetPath = keras.utils.get_file("auto-mpg.data", "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")

  # Set the column names for the data set
  columnNames = ['MPG', 'Cylinders','Displacement','Horsepower','Weight',
               'Acceleration','Model Year','Origin']

  # Import the data set
  originalData = pd.read_csv(datasetPath, names=columnNames, na_values = "?", 
                           comment='\t', sep=" ", skipinitialspace=True)
						 
  # Check the data set
  # print("Original Data Set Excerpt")
  # print(originalData.head())
  # print()

  # Generate a copy of the data set
  data = originalData.copy()

  # Count how many NAs each data attribute has
  # print("Number of NAs in the data set")
  # print(data.isna().sum())
  # print()

  # Now, let's remove the NAs from the data set
  data = data.dropna()

  # Perform one-hot encoding on the Origin attribute 
  # since it is a categorical variable
  origin = data.pop('Origin') # Return item and drop from frame
  data['USA'] = (origin == 1) * 1.0
  data['Europe'] = (origin == 2) * 1.0
  data['Japan'] = (origin == 3) * 1.0

  # Generate a training data set (80% of the data) and a testing set (20% of the data)
  trainingData = data.sample(frac = 0.8, random_state = 0)

  # Generate a testing data set
  testingData = data.drop(trainingData.index)

  # Separate the attributes from the label in both the testing
  # and training data. The label is the thing we are trying
  # to predit (i.e. miles per gallon 'MPG')
  trainingLabelData = trainingData.pop('MPG')
  testingLabelData = testingData.pop('MPG')
  
  # Normalize the data
  normalizedTrainingData = normalize(trainingData)
  normalizedTestingData = normalize(testingData)
  #print(normalizedTrainingData.head()) 
  
  # Generate the neural network
  neuralNet = generateNeuralNetwork(trainingData)
  
  # See a summary of the neural network
  # The first layer has 640 parameters 
    #(9 input values * 64 neurons) + 64 bias values
  # The second layer has 4160 parameters 
    #(64 input values * 64 neurons) + 64 bias values
  # The output layer has 65 parameters 
    #(64 input values * 1 neuron) + 1 bias value
  #print(neuralNet.summary())
  
  EPOCHS = 1000
  
  # Train the model for a fixed number of epochs
  # history.history attribute is returned from the fit() function.
  # history.history is a record of training loss values and 
  # metrics values at successive epochs, as well as validation 
  # loss values and validation metrics values.
  history = neuralNet.fit(
    x = normalizedTrainingData, 
    y = trainingLabelData,
    epochs = EPOCHS, 
    validation_split = 0.2, 
    verbose = 0,
    callbacks = [PrintDot()]
  )   
  
  # Plot the neural network metrics (Training error and validation error)
  # Training error is the error when the trained neural network is 
  #   run on the training data.
  # Validation error is used to minimize overfitting. It indicates how
  #   well the data fits on data it hasn't been trained on.
  #plotNeuralNetMetrics(history)
  
  # Generate another neural network so that we can use early stopping
  neuralNet2 = generateNeuralNetwork(trainingData)
  
  # We want to stop training the model when the 
  # validation error stops improving.
  # monitor indicates the quantity we want to monitor.
  # patience indicates the number of epochs with no improvement after which
  # training will terminate.
  earlyStopping = keras.callbacks.EarlyStopping(monitor = 'val_loss', patience = 10)

  history2 = neuralNet2.fit(
    x = normalizedTrainingData, 
    y = trainingLabelData,
    epochs = EPOCHS, 
    validation_split = 0.2, 
    verbose = 0,
    callbacks = [earlyStopping, PrintDot()]
  )    

  # Plot metrics
  #plotNeuralNetMetrics(history2) 
  
  # Return the loss value and metrics values for the model in test mode
  # The mean absolute error for the predictions should 
  # stabilize around 2 miles per gallon  
  loss, meanAbsoluteError, meanSquaredError = neuralNet2.evaluate(
    x = normalizedTestingData,
	y = testingLabelData,
    verbose = 0
  )
  
  #print(f'\nMean Absolute Error on Test Data Set = {meanAbsoluteError} miles per gallon')
  
  # Make fuel economy predictions by deploying the trained neural network on the 
  # test data set (data that is brand new for the trained neural network).
  testingDataPredictions = neuralNet2.predict(normalizedTestingData).flatten()
  
  # Plot the predicted MPG vs. the true MPG
  # testingLabelData are the true MPG values
  # testingDataPredictions are the predicted MPG values
  #plotTestingDataPredictions(testingLabelData, testingDataPredictions)
  
  # Plot the prediction error distribution
  #plotPredictionError(testingLabelData, testingDataPredictions)
  
  # Save the neural network in Hierarchical Data Format version 5 (HDF5) format
  neuralNet2.save('fuel_economy_prediction_nnet.h5')
  
  # Import the saved model
  neuralNet3 = keras.models.load_model('fuel_economy_prediction_nnet.h5')
  print("\n\nNeural network has loaded successfully...\n")
  
  # Show neural network parameters
  print(neuralNet3.summary())
  
  # Make a prediction using the saved model we just imported
  print("\nMaking predictions...")
  testingDataPredictionsNN3 = neuralNet3.predict(normalizedTestingData).flatten()
  
  # Show Predicted MPG vs. Actual MPG
  plotTestingDataPredictions(testingLabelData, testingDataPredictionsNN3) 
  
# Generate the neural network
def generateNeuralNetwork(trainingData):
  # A Sequential model is a stack of layers where each layer is
  # single-input, single-output
  # This network below has 3 layers.
  neuralNet = keras.Sequential([
  
    # Each neuron in a layer recieves input from all the 
    # neurons in the previous layer (Densely connected)
    # Use the ReLU activation function. This function transforms the input
	# into a node (i.e. summed weighted input) into output	
    # The first layer needs to know the number of attributes (keys) in the data set.
	# This first and second layers have 64 nodes.
    layers.Dense(64, activation=tf.nn.relu, input_shape=[len(trainingData.keys())]),
    layers.Dense(64, activation=tf.nn.relu),
    layers.Dense(1) # This output layer is a single, continuous value (i.e. Miles per gallon)
  ])

  # Penalize the update of the neural network parameters that are causing
  # the cost function to have large oscillations by using a moving average
  # of the square of the gradients and dibiding the gradient by the root of this
  # average. Reduces the step size for large gradients and increases 
  # the step size for small gradients.
  # The input into this function is the learning rate.
  optimizer = keras.optimizers.RMSprop(0.001)
 
  # Set the configurations for the model to get it ready for training
  neuralNet.compile(loss = 'mean_squared_error',
                optimizer = optimizer,
                metrics = ['mean_absolute_error', 'mean_squared_error'])
  return neuralNet
    
# Normalize the data set using the mean and standard deviation 
def normalize(data):
  statistics = data.describe()
  statistics = statistics.transpose()
  return(data - statistics['mean']) / statistics['std']

# Plot metrics for the neural network  
def plotNeuralNetMetrics(history):
  neuralNetMetrics = pd.DataFrame(history.history)
  neuralNetMetrics['epoch'] = history.epoch
  
  plt.figure()
  plt.xlabel('Epoch')
  plt.ylabel('Mean Abs Error [MPG]')
  plt.plot(neuralNetMetrics['epoch'], 
           neuralNetMetrics['mean_absolute_error'],
           label='Train Error')
  plt.plot(neuralNetMetrics['epoch'], 
           neuralNetMetrics['val_mean_absolute_error'],
           label = 'Val Error')
  plt.ylim([0,5])
  plt.legend()
  
  plt.figure()
  plt.xlabel('Epoch')
  plt.ylabel('Mean Square Error [$MPG^2$]')
  plt.plot(neuralNetMetrics['epoch'], 
           neuralNetMetrics['mean_squared_error'],
           label='Train Error')
  plt.plot(neuralNetMetrics['epoch'], 
           neuralNetMetrics['val_mean_squared_error'],
           label = 'Val Error')
  plt.ylim([0,20])
  plt.legend()
  plt.show()
  
# Plot prediction error
def plotPredictionError(testingLabelData, testingDataPredictions):

  # Error = Predicted - Actual
  error = testingDataPredictions - testingLabelData
  
  plt.hist(error, bins = 50)
  plt.xlim([-10,10])
  plt.xlabel("Predicted MPG - Actual MPG")
  _ = plt.ylabel("Count")
  plt.show()

# Plot predictions vs. true values
def plotTestingDataPredictions(testingLabelData, testingDataPredictions):

  # Plot the data points (x, y)
  plt.scatter(testingLabelData, testingDataPredictions)
  
  # Label the axes
  plt.xlabel('True Values (Miles per gallon)')
  plt.ylabel('Predicted Values (Miles per gallon)')

  # Plot a line between (0,0) and (50,50) 
  point1 = [0, 0]
  point2 = [50, 50]
  xValues = [point1[0], point2[0]] 
  yValues = [point1[1], point2[1]]
  plt.plot(xValues, yValues) 
  
  # Set the x and y axes limits
  plt.xlim(0, 50)
  plt.ylim(0, 50)

  # x and y axes are equal in displayed dimensions
  plt.gca().set_aspect('equal', adjustable='box')
  
  # Show the plot
  plt.show()
  
  
# Show the training process by printing a period for each epoch that completes
class PrintDot(keras.callbacks.Callback):
  def on_epoch_end(self, epoch, logs):
    if epoch % 100 == 0: print('')
    print('.', end='')
	
main()

Save the Python program.

If you run your Python programs using Anaconda, open the Anaconda prompt.

If you like to run your programs in a virtual environment, activate the virtual environment. I have a virtual environment named tf_2.

conda activate tf_2

Navigate to the folder where you saved the Python program.

cd [path to folder]

For example,

cd C:\MyFiles

Install any libraries that you need. I didn’t have some of the libraries in the “import” section of my code installed, so I’ll install them now.

pip install pandas
pip install seaborn

To run the code, type:

python vehicle_fuel_economy.py

If you’re using a GPU with Tensorflow, and you’re getting error messages about libraries missing, go to this folder C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.1\bin, and you can search on the Internet for the missing dll files. Download them, and then put them in that bin folder.

Code Output

In this section, I will pull out snippets of the code and show you the resulting output when you uncomment those lines.

  # Check the data set
  print("Original Data Set Excerpt")
  print(originalData.head())
  print()
1_original_datasetJPG
  # Count how many NAs each data attribute has
  print("Number of NAs in the data set")
  print(data.isna().sum())
  print()
2-number-of-nasJPG
  # See a summary of the neural network
  # The first layer has 640 parameters 
    #(9 input values * 64 neurons) + 64 bias values
  # The second layer has 4160 parameters 
    #(64 input values * 64 neurons) + 64 bias values
  # The output layer has 65 parameters 
    #(64 input values * 1 neuron) + 1 bias value
  print(neuralNet.summary())

3-output-of-neural-net-summaryJPG
  # Plot the neural network metrics (Training error and validation error)
  # Training error is the error when the trained neural network is 
  #   run on the training data.
  # Validation error is used to minimize overfitting. It indicates how
  #   well the data fits on data it hasn't been trained on.
  plotNeuralNetMetrics(history)

4-mean-absolute-errorJPG
5-mean-squared-errorJPG
  # Plot metrics
  plotNeuralNetMetrics(history2) 
6-error-with-early-stoppingJPG
print(f'\nMean Absolute Error on Test Data Set = {meanAbsoluteError} miles per gallon') 
7-mae-test-data-setJPG
  # Plot the predicted MPG vs. the true MPG
  # testingLabelData are the true MPG values
  # testingDataPredictions are the predicted MPG values
  plotTestingDataPredictions(testingLabelData, testingDataPredictions)
8-predicted-vs-trueJPG
  # Plot the prediction error distribution
  plotPredictionError(testingLabelData, testingDataPredictions)
9-prediction-error-frequencyJPG
  # Save the neural network in Hierarchical Data Format version 5 (HDF5) format
  neuralNet2.save('fuel_economy_prediction_nnet.h5')
  
  # Import the saved model
  neuralNet3 = keras.models.load_model('fuel_economy_prediction_nnet.h5')
10-loading-and-saving-a-neural-networkJPG

References

Quinlan,R. (1993). Combining Instance-Based and Model-Based Learning. In Proceedings on the Tenth International Conference of Machine Learning, 236-243, University of Massachusetts, Amherst. Morgan Kaufmann.

How to Make a Mobile Robot in Gazebo (ROS2 Foxy)

In this tutorial, we will learn how to make a model of a mobile robot in Gazebo from scratch. Our simulated robot will be a wheeled mobile robot. It will have two big wheels on each side and a caster wheel in the middle. Here is what you will build. In this case, I have the robot going in reverse (i.e. the big wheels are on the front of the vehicle):

wheeled-robot-gazebo

Prerequisites

Setup the Model Directory

Here are the official instructions, but we’ll walk through all the steps below. It is important to go slow and build your robotic models in small steps. No need to hurry. 

Create a folder for the model.

mkdir -p ~/.gazebo/models/my_robot

Create a model config file. This file will contain a description of the model.

gedit ~/.gazebo/models/my_robot/model.config

Add the following lines to the file, and then Save. You can see this file contains fields for the name of the robot, the version, the author (that’s you), your e-mail address, and a description of the robot.

<?xml version="1.0"?>
<model>
  <name>My Robot</name>
  <version>1.0</version>
  <sdf version='1.4'>model.sdf</sdf>

  <author>
   <name>My Name</name>
   <email>me@my.email</email>
  </author>

  <description>
    My awesome robot.
  </description>
</model>

Close the config file.

Now, let’s create an SDF (Simulation Description Format) file. This file will contain the tags that are needed to create an instance of the my_robot model. 

gedit ~/.gazebo/models/my_robot/model.sdf

Copy and paste the following lines inside the sdf file.

<?xml version='1.0'?>
<sdf version='1.4'>
  <model name="my_robot">
  </model>
</sdf>

Save the file, but don’t close it yet.

Create the Structure of the Model

Now we need to create the structure of the robot. We will start out by adding basic shapes. While we are creating the robot, we want Gazebo’s physics engine to ignore the robot. Otherwise the robot will move around the environment as we add more stuff on it.

To get the physics engine to ignore the robot, add this line underneath the <model name=”my_robot”> tag.

<static>true</static>

This is what you’re sdf file should look like at this stage:

1-add-static-tagJPG

Now underneath the <static>true</static> line, add these lines:

          <link name='chassis'>
            <pose>0 0 .1 0 0 0</pose>

            <collision name='collision'>
              <geometry>
                <box>
                  <size>.4 .2 .1</size>
                </box>
              </geometry>
            </collision>

            <visual name='visual'>
              <geometry>
                <box>
                  <size>.4 .2 .1</size>
                </box>
              </geometry>
            </visual>
          </link>

Here is how your sdf file should look now. 

2-here-is-how-should-look-nowJPG

Click Save and close the file.

What you have done is create a box. The name of this structure (i.e. link) is ‘chassis’. A chassis for a mobile robot is the frame (i.e. skeleton) of the vehicle.

The pose (i.e. position and orientation) of the geometric center of this box will be a position of (x = 0 meters, y = 0 meters, z = 0.1 meters) and an orientation of (roll = 0 radians, pitch = 0 radians, yaw = 0 radians). The pose of the chassis (as well as the post of any link in an sdf file) is defined relative to the model coordinate frame (which is ‘my_robot’).

Remember:

  • Roll (rotation about the x-axis)
  • Pitch (rotation about the y-axis)
  • Yaw (rotation about the z-axis)  

Inside the collision element of the sdf file, you specify the shape (i.e. geometry) that Gazebo’s collision detection engine will use. In this case, we want the collision detection engine to represent our vehicle as a box that is 0.4 meters in length, 0.2 meters in width, and 0.1 meters in height. 

Inside the visual tag, we place the shape that Gazebo’s rendering engine will use to display the robot. 

In most cases, the visual tag will be the same as the collision tag, but in some cases it is not.

For example, if your robot car has some complex geometry that looks like the toy Ferrari car below, you might model the collision physics of that body as a box but would use a custom mesh to make the robot model look more realistic, like the figure below.

ferrari_red_auto_sports_0

Now let’s run Gazebo so that we can see our model. Type the following command:

gazebo
3-launch-gazeboJPG

On the left-hand side, click the “Insert” tab.

On the left panel, click “My Robot”. You should see a white box. You can place it wherever you want.

4-white-boxJPG

Go back to the terminal window, and type CTRL + C to close Gazebo.

Let’s add a caster wheel to the robot. The caster wheel will be modeled as a sphere with a radius of 0.05 meters and no friction.

Relative to the geometric center of the white box (i.e. the robot chassis), the center of the spherical caster wheel will be located at x = -0.15 meters, y = 0 meters, and z = -0.05 meters.

Note that when we create a box shape, the positive x-axis points towards the front of the vehicle (i.e. the direction of travel). The x-axis is that red line below.

The positive y-axis points out the left side of the chassis. It is the green line below. The positive z-axis points straight upwards towards the sky.

gedit ~/.gazebo/models/my_robot/model.sdf

Add these lines after the first </visual> tag but before the </link> tag.

          <collision name='caster_collision'>
            <pose>-0.15 0 -0.05 0 0 0</pose>
            <geometry>
                <sphere>
                <radius>.05</radius>
              </sphere>
            </geometry>

            <surface>
              <friction>
                <ode>
                  <mu>0</mu>
                  <mu2>0</mu2>
                  <slip1>1.0</slip1>
                  <slip2>1.0</slip2>
                </ode>
              </friction>
            </surface>
          </collision>

          <visual name='caster_visual'>
            <pose>-0.15 0 -0.05 0 0 0</pose>
            <geometry>
              <sphere>
                <radius>.05</radius>
              </sphere>
            </geometry>
          </visual>

Click Save and close the file.

Now relaunch gazebo, and insert the model.

gazebo

Insert -> My Robot

Go back to the terminal window, and close gazebo by typing CTRL + C.

You can now see our robot has a brand new caster wheel located towards the rear of the vehicle.

5-robot-with-the-wheelJPG

Now let’s add the left wheel. We will model this as a cylinder with a radius of 0.1 meters and a length of 0.05 meters.

gedit ~/.gazebo/models/my_robot/model.sdf

Right after the </link> tag (but before the </model> tag, add the following lines. You can see that the wheel is placed at the front of the vehicle (x=0.1m), the left side of the vehicle (y=0.13m), and 0.1m above the surface (z=0.1m). It is a cylinder that is rotated 90 degrees (i.e. 1.5707 radians) around the model’s y axis and 90 degrees around the model’s z axis.

     <link name="left_wheel">
        <pose>0.1 0.13 0.1 0 1.5707 1.5707</pose>
        <collision name="collision">
          <geometry>
            <cylinder>
              <radius>.1</radius>
              <length>.05</length>
            </cylinder>
          </geometry>
        </collision>
        <visual name="visual">
          <geometry>
            <cylinder>
              <radius>.1</radius>
              <length>.05</length>
            </cylinder>
          </geometry>
        </visual>
      </link>

Launch Gazebo, and see how it looks. You can click the box at the top of the panel to see the robot from different perspectives.

7-different-perspectiveJPG
6-see-how-it-looksJPG
8-top-viewJPG
9-back-viewJPG

Now, let’s add a right wheel. Insert these lines into your sdf file right before the </model> tag. 

    <link name="right_wheel">
        <pose>0.1 -0.13 0.1 0 1.5707 1.5707</pose>
        <collision name="collision">
          <geometry>
            <cylinder>
              <radius>.1</radius>
              <length>.05</length>
            </cylinder>
          </geometry>
        </collision>
        <visual name="visual">
          <geometry>
            <cylinder>
              <radius>.1</radius>
              <length>.05</length>
            </cylinder>
          </geometry>
        </visual>
      </link>

Save it, close it, and open Gazebo to see your robot. Your robot has a body, a spherical caster wheel, and two big wheels on either side. 

10-robot-with-right-wheelJPG

Now let’s change the robot from static to dynamic. We need to add two revolute joints, one for the left wheel and one for the right wheel. Revolute joints (also known as “hinge joints”) are your wheels motors. These motors generate rotational motion to make the wheels move.

In the sdf file, first change this:

<static>true</static>

To this

<static>false</static>

Then add these lines right before the closing </model> tag.

      <joint type="revolute" name="left_wheel_hinge">
        <pose>0 0 -0.03 0 0 0</pose>
        <child>left_wheel</child>
        <parent>chassis</parent>
        <axis>
          <xyz>0 1 0</xyz>
        </axis>
      </joint>

      <joint type="revolute" name="right_wheel_hinge">
        <pose>0 0 0.03 0 0 0</pose>
        <child>right_wheel</child>
        <parent>chassis</parent>
        <axis>
          <xyz>0 1 0</xyz>
        </axis>
      </joint>

<child> defines the name of this joint’s child link.

<parent> defines the parent link.

<pose> describes the offset from the the origin of the child link in the frame of the child link.

<axis> defines the joint’s axis specified in the parent model frame. This axis is the axis of rotation. In this case, the two joints rotate about the y axis.

<xyz> defines the x, y, and z components of the normalized axis vector. 

You can learn more about sdf tags at the official website.

Save the sdf file, open Gazebo, and insert your model.

Click on your model to select it.

You will see six tiny dots on the right side of the screen. Click on those and drag your mouse to the left. It is a bit tricky to click on these as Gazebo can be quite sensitive. Just keep trying to drag those dots to the left using your mouse.

Under the “World -> Models” tab on the left, select the my_robot model. You should see two joints appear on the Joints tab on the right.

12-two-joints-appearJPG
11-two-joints-appearJPG

Under the Force tab, increase the force applied to each joint to 0.1 N-m. You should see your robot move around in the environment.

Congratulations! You have built a mobile robot in Gazebo.

What Are Parallel Manipulators?

Up until now, the robotic arms that we have been building are serial manipulators. Robotic arms like this one, for example, are called serial manipulators because each joint (i.e. servo motor) is attached to either the joint before it (via a link) or to the base.

However, with a parallel manipulator, the joints are not connected together. Instead, each joint is connected to both the base of the robot and to the end effector (e.g. vacuum suction cup).

The two main types of parallel manipulators are the Gough-Stewart Platform (pictured below), which is sometimes used in flight simulators and the delta robot (often used in factories for pick and place tasks along conveyor belts).

In the Gough-Stewart Platform type robots, all of the joints are prismatic (i.e. linear actuators…motors that generate motion only in a linear fashion). 

Hexapod0a
Image Source: Wikipedia

In delta robots, all of the joints are revolute (i.e. motors that generate rotational motion). Here is a video of a delta robot in action. It is performing a pick and place task.