#@title Copyright 2020 Google LLC. Double-click for license information.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Colabs¶
Machine Learning Crash Course uses Colaboratories (Colabs) for all programming exercises. Colab is Google's implementation of Jupyter Notebook. For more information about Colabs and how to use them, go to Welcome to Colaboratory.
Representation with a Feature Cross¶
In this exercise, you'll experiment with different ways to represent features.
Learning Objectives:¶
After doing this Colab, you'll know how to:
- Use TensorFlow preprocessing layers to represent features in different ways.
- Represent features as bins.
- Cross bins to create a feature cross.
The Dataset¶
Like several of the previous Colabs, this exercise uses the California Housing Dataset.
Call the import statements¶
The following code imports the necessary code to run the code in the rest of this Colaboratory.
#@title Load the imports
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras import layers
from matplotlib import pyplot as plt
# The following lines adjust the granularity of reporting.
pd.options.display.max_rows = 10
pd.options.display.float_format = "{:.1f}".format
tf.keras.backend.set_floatx('float32')
print("Imported the modules.")
Load, scale, and shuffle the examples¶
The following code cell loads the separate .csv files and creates the following two pandas DataFrames:
train_df, which contains the training settest_df, which contains the test set
The code cell then scales the median_house_value to a more human-friendly range and then shuffles the examples.
# Load the dataset
train_df = pd.read_csv("https://download.mlcc.google.com/mledu-datasets/california_housing_train.csv")
test_df = pd.read_csv("https://download.mlcc.google.com/mledu-datasets/california_housing_test.csv")
# Scale the labels
scale_factor = 1000.0
# Scale the training set's label.
train_df["median_house_value"] /= scale_factor
# Scale the test set's label
test_df["median_house_value"] /= scale_factor
# Shuffle the examples
train_df = train_df.reindex(np.random.permutation(train_df.index))
Represent latitude and longitude as floating-point values¶
Previous Colabs trained on only a single feature or a single synthetic feature. By contrast, this exercise trains on two features using Input layers.
A neighborhood's location is typically the most important feature in determining a house's value. The California Housing dataset provides two features, latitude and longitude that identify each neighborhood's location.
The following code cell defines two tf.keras.Input layers, one to represent latitude and another one to represent longitude, both as floating-point values.
This code cell specifies the features that you'll ultimately train the model on and how each of those features will be represented.
# Keras Input tensors of float values.
inputs = {
'latitude':
tf.keras.layers.Input(shape=(1,), dtype=tf.float32,
name='latitude'),
'longitude':
tf.keras.layers.Input(shape=(1,), dtype=tf.float32,
name='longitude')
}
Define functions that create and train a model, and a plotting function¶
The following code defines three functions:
create_model, which tells TensorFlow to build a linear regression model based on the inputs and outputs provided.train_model, which will ultimately train the model from training set examples.plot_the_loss_curve, which generates a loss curve.
#@title Define functions to create and train a model, and a plotting function
def create_model(my_inputs, my_outputs, my_learning_rate):
model = tf.keras.Model(inputs=my_inputs, outputs=my_outputs)
# Construct the layers into a model that TensorFlow can execute.
model.compile(optimizer=tf.keras.optimizers.RMSprop(
learning_rate=my_learning_rate),
loss="mean_squared_error",
metrics=[tf.keras.metrics.RootMeanSquaredError()])
return model
def train_model(model, dataset, epochs, batch_size, label_name):
"""Feed a dataset into the model in order to train it."""
features = {name:np.array(value) for name, value in dataset.items()}
label = np.array(features.pop(label_name))
history = model.fit(x=features, y=label, batch_size=batch_size,
epochs=epochs, shuffle=True)
# The list of epochs is stored separately from the rest of history.
epochs = history.epoch
# Isolate the mean absolute error for each epoch.
hist = pd.DataFrame(history.history)
rmse = hist["root_mean_squared_error"]
return epochs, rmse
def plot_the_loss_curve(epochs, rmse):
"""Plot a curve of loss vs. epoch."""
plt.figure()
plt.xlabel("Epoch")
plt.ylabel("Root Mean Squared Error")
plt.plot(epochs, rmse, label="Loss")
plt.legend()
plt.ylim([rmse.min()*0.94, rmse.max()* 1.05])
plt.show()
print("Defined the create_model, train_model, and plot_the_loss_curve functions.")
Train the model with floating-point representations¶
The following code cell calls the functions you just created to train, plot, and evaluate a model.
# The following variables are the hyperparameters.
learning_rate = 0.05
epochs = 30
batch_size = 100
label_name = 'median_house_value'
# The two Input layers are concatenated so they can be passed as a single
# tensor to a Dense layer.
preprocessing_layer = tf.keras.layers.Concatenate()(list(inputs.values()))
dense_output = layers.Dense(units=1, name='dense_layer')(preprocessing_layer)
# Create and compile the model's topography.
my_model = create_model(inputs, dense_output, learning_rate)
# To view a PNG of this model's layers, uncomment the call to
# `tf.keras.utils.plot_model` below. After running this code cell, click
# the file folder on the left, then the `my_model.png` file.
# tf.keras.utils.plot_model(my_model, "my_model.png", show_shapes=True)
# Train the model on the training set.
epochs, rmse = train_model(my_model, train_df, epochs, batch_size, label_name)
# Print out the model summary.
my_model.summary(expand_nested=True)
plot_the_loss_curve(epochs, rmse)
print("\n: Evaluate the new model against the test set:")
test_features = {name:np.array(value) for name, value in test_df.items()}
test_label = np.array(test_features.pop(label_name))
my_model.evaluate(x=test_features, y=test_label, batch_size=batch_size)
Task 1: Why aren't floating-point values a good way to represent latitude and longitude?¶
Are floating-point values a good way to represent latitude and longitude?
#@title Double-click to view an answer to Task 1.
# No. Representing latitude and longitude as
# floating-point values does not have much
# predictive power. For example, neighborhoods at
# latitude 35 are not 36/35 more valuable
# (or 35/36 less valuable) than houses at
# latitude 36.
# Representing `latitude` and `longitude` as
# floating-point values provides almost no
# predictive power. We're only using the raw values
# to establish a baseline for future experiments
# with better representations.
Represent latitude and longitude in buckets¶
The following code cell represents latitude and longitude in buckets (bins). Each bin represents all the neighborhoods within a single degree. For example, neighborhoods at latitude 35.4 and 35.8 are in the same bucket, but neighborhoods in latitude 35.4 and 36.2 are in different buckets.
The model will learn a separate weight for each bucket. For example, the model will learn one weight for all the neighborhoods in the "35" bin, a different weight for neighborhoods in the "36" bin, and so on. This representation will create approximately 20 buckets:
- 10 buckets for
latitude. - 10 buckets for
longitude.
resolution_in_degrees = 1.0
# Create a list of numbers representing the bucket boundaries for latitude.
latitude_boundaries = list(np.arange(int(min(train_df['latitude'])),
int(max(train_df['latitude'])),
resolution_in_degrees))
print("latitude boundaries: " + str(latitude_boundaries))
# Create a Discretization layer to separate the latitude data into buckets.
latitude = tf.keras.layers.Discretization(
bin_boundaries=latitude_boundaries,
name='discretization_latitude')(inputs.get('latitude'))
# Number of categories is the length of latitude_boundaries plus one.
latitude = tf.keras.layers.CategoryEncoding(
num_tokens=len(latitude_boundaries) + 1,
output_mode='one_hot',
name='category_encoding_latitude')(latitude)
# Create a list of numbers representing the bucket boundaries for longitude.
longitude_boundaries = list(np.arange(int(min(train_df['longitude'])),
int(max(train_df['longitude'])),
resolution_in_degrees))
print("longitude boundaries: " + str(longitude_boundaries))
# Create a Discretization layer to separate the longitude data into buckets.
longitude = tf.keras.layers.Discretization(
bin_boundaries=longitude_boundaries,
name='discretization_longitude')(inputs.get('longitude'))
# Number of categories is the length of longitude_boundaries plus one.
longitude = tf.keras.layers.CategoryEncoding(
num_tokens=len(longitude_boundaries) + 1,
output_mode='one_hot',
name='category_encoding_longitude')(longitude)
# Concatenate latitude and longitude into a single tensor as input for the Dense layer.
concatenate_layer = tf.keras.layers.Concatenate()([latitude, longitude])
dense_output = layers.Dense(units=1, name='dense_layer')(concatenate_layer)
Train the model with bucket representations¶
Run the following code cell to train the model with bucket representations rather than floating-point representations:
# The following variables are the hyperparameters.
learning_rate = 0.04
epochs = 35
# Build the model.
my_model = create_model(inputs, dense_output, learning_rate)
# Train the model on the training set.
epochs, rmse = train_model(my_model, train_df, epochs, batch_size, label_name)
# Print out the model summary.
my_model.summary(expand_nested=True)
plot_the_loss_curve(epochs, rmse)
print("\n: Evaluate the new model against the test set:")
my_model.evaluate(x=test_features, y=test_label, batch_size=batch_size)
Task 2: Did buckets outperform floating-point representations?¶
Compare the model's root_mean_squared_error values for the two representations (floating-point vs. buckets)? Which model produced lower losses?
#@title Double-click for an answer to Task 2.
# Bucket representation outperformed
# floating-point representations.
# However, you can still do far better.
Task 3: What is a better way to represent location?¶
Buckets are a big improvement over floating-point values. Can you identify an even better way to identify location with latitude and longitude?
#@title Double-click to view an answer to Task 3.
# Representing location as a feature cross should
# produce better results.
# In Task 2, you represented latitude in
# one-dimensional buckets and longitude in
# another series of one-dimensional buckets.
# Real-world locations, however, exist in
# two dimensions. Therefore, you should
# represent location as a two-dimensional feature
# cross. That is, you'll cross the 10 or so latitude
# buckets with the 10 or so longitude buckets to
# create a grid of 100 cells.
# The model will learn separate weights for each
# of the cells.
Represent location as a feature cross¶
The following code cell represents location as a feature cross. That is, the following code cell first creates buckets and then crosses the latitude and longitude features using a HashedCrossing layer.
resolution_in_degrees = 1.0
# Create a list of numbers representing the bucket boundaries for latitude.
latitude_boundaries = list(np.arange(int(min(train_df['latitude'])),
int(max(train_df['latitude'])),
resolution_in_degrees))
# Create a Discretization layer to separate the latitude data into buckets.
latitude = tf.keras.layers.Discretization(
bin_boundaries=latitude_boundaries,
name='discretization_latitude')(inputs.get('latitude'))
# Create a list of numbers representing the bucket boundaries for longitude.
longitude_boundaries = list(np.arange(int(min(train_df['longitude'])),
int(max(train_df['longitude'])),
resolution_in_degrees))
# Create a Discretization layer to separate the longitude data into buckets.
longitude = tf.keras.layers.Discretization(
bin_boundaries=longitude_boundaries,
name='discretization_longitude')(inputs.get('longitude'))
# Cross the latitude and longitude features into a single one-hot vector.
feature_cross = tf.keras.layers.HashedCrossing(
num_bins=len(latitude_boundaries) * len(longitude_boundaries),
output_mode='one_hot',
name='cross_latitude_longitude')([latitude, longitude])
dense_output = layers.Dense(units=1, name='dense_layer')(feature_cross)
Invoke the following code cell to test your solution for Task 3. Please ignore the warning messages.
# The following variables are the hyperparameters.
learning_rate = 0.04
epochs = 35
# Build the model, this time passing in the feature_cross_feature_layer:
my_model = create_model(inputs, dense_output, learning_rate)
# Train the model on the training set.
epochs, rmse = train_model(my_model, train_df, epochs, batch_size, label_name)
# Print out the model summary.
my_model.summary(expand_nested=True)
plot_the_loss_curve(epochs, rmse)
print("\n: Evaluate the new model against the test set:")
my_model.evaluate(x=test_features, y=test_label, batch_size=batch_size)
Task 4: Did the feature cross outperform buckets?¶
Compare the model's root_mean_squared_error values for the two representations (buckets vs. feature cross)? Which model produced
lower losses?
#@title Double-click for an answer to this question.
# Yes, representing these features as a feature
# cross produced much lower loss values than
# representing these features as buckets
Task 5: Adjust the resolution of the feature cross¶
Return to the code cell in the "Represent location as a feature cross" section. Notice that resolution_in_degrees is set to 1.0. Therefore, each cell represents an area of 1.0 degree of latitude by 1.0 degree of longitude, which corresponds to a cell of 110 km by 90 km. This resolution defines a rather large neighborhood.
Experiment with resolution_in_degrees to answer the following questions:
- What value of
resolution_in_degreesproduces the best results (lowest loss value)? - Why does loss increase when the value of
resolution_in_degreesdrops below a certain value?
Finally, answer the following question:
- What feature (that does not exist in the California Housing Dataset) would be a better proxy for location than latitude X longitude.
#@title Double-click for possible answers to Task 5.
#1. A resolution of ~0.4 degree provides the best
# results.
#2. Below ~0.4 degree, loss increases because the
# dataset does not contain enough examples in
# each cell to accurately predict prices for
# those cells.
#3. Postal code would be a far better feature
# than latitude X longitude, assuming that
# the dataset contained sufficient examples
# in each postal code.