Feature Sets

By: Sören Dobberschütz

Re-posted from: https://tensorflowjulia.blogspot.com/2018/08/feature-sets.html

The fourth part of the Machine Learning Crash Course deals with finding a minimal set of features that still gives a reasonable model.

The code makes use of two useful functions when dealing with DataFrames:

  • names() returns the names of the different columns. This allows for the creation of a DataFrame that contains the correlation matrix with the correct column names – see the line
    DataFrame([cor(df[:, a], df[:, b]) for a=1:size(df, 2), b=1:size(df, 2)], names(df))
  • On the other hand, if you programatically need to create new names for a DataFrame, you can use Symbol() to convert from a string. We used this when splitting the latitude data up into several buckets:
    Symbol(string(“latitude_”, range[1],”_”, range[2]))


The Jupyter notebook can be downloaded here. For the version displayed below, I needed to remove some scatter plots, which are contained in the original file.









This notebook is based on the file Feature sets programming exercise, which is part of Google’s Machine Learning Crash Course.
In [0]:
# 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.

Feature Sets

Learning Objective: Create a minimal set of features that performs just as well as a more complex feature set
So far, we’ve thrown all of our features into the model. Models with fewer features use fewer resources and are easier to maintain. Let’s see if we can build a model on a minimal set of housing features that will perform equally as well as one that uses all the features in the data set.

Setup

As before, let’s load and prepare the California housing data.
In [1]:
using Plots
gr()
using DataFrames
using TensorFlow
import CSV
import StatsBase

sess=Session()
california_housing_dataframe = CSV.read("california_housing_train.csv", delim=",");
california_housing_dataframe = california_housing_dataframe[shuffle(1:size(california_housing_dataframe, 1)),:];
In [2]:
function preprocess_features(california_housing_dataframe)
"""Prepares input features from California housing data set.

Args:
california_housing_dataframe: A DataFrame expected to contain data
from the California housing data set.
Returns:
A DataFrame that contains the features to be used for the model, including
synthetic features.
"""
selected_features = california_housing_dataframe[
[:latitude,
:longitude,
:housing_median_age,
:total_rooms,
:total_bedrooms,
:population,
:households,
:median_income]]
processed_features = selected_features
# Create a synthetic feature.
processed_features[:rooms_per_person] = (
california_housing_dataframe[:total_rooms] ./
california_housing_dataframe[:population])
return processed_features
end

function preprocess_targets(california_housing_dataframe)
"""Prepares target features (i.e., labels) from California housing data set.

Args:
california_housing_dataframe: A DataFrame expected to contain data
from the California housing data set.
Returns:
A DataFrame that contains the target feature.
"""
output_targets = DataFrame()
# Scale the target to be in units of thousands of dollars.
output_targets[:median_house_value] = (
california_housing_dataframe[:median_house_value] ./ 1000.0)
return output_targets
end
Out[2]:
preprocess_targets (generic function with 1 method)
2018-08-17 21:37:38.202481: I tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.2 AVX AVX2 FMA
In [24]:
# Choose the first 12000 (out of 17000) examples for training.
training_examples = preprocess_features(head(california_housing_dataframe,12000))
training_targets = preprocess_targets(head(california_housing_dataframe,12000))

# Choose the last 5000 (out of 17000) examples for validation.
validation_examples = preprocess_features(tail(california_housing_dataframe,5000))
validation_targets = preprocess_targets(tail(california_housing_dataframe,5000))

# Double-check that we've done the right thing.
println("Training examples summary:")
describe(training_examples)
println("Validation examples summary:")
describe(validation_examples)

println("Training targets summary:")
describe(training_targets)
println("Validation targets summary:")
describe(validation_targets)
Out[24]:
variable mean min median max nunique nmissing eltype
1 median_house_value 207.083 14.999 180.65 500.001 Float64
Training examples summary:
Validation examples summary:
Training targets summary:
Validation targets summary:

Task 1: Develop a Good Feature Set

What’s the best performance you can get with just 2 or 3 features?
correlation matrix shows pairwise correlations, both for each feature compared to the target and for each feature compared to other features.
Here, correlation is defined as the Pearson correlation coefficient. You don’t have to understand the mathematical details for this exercise.
Correlation values have the following meanings:
  • -1.0: perfect negative correlation
  • 0.0: no correlation
  • 1.0: perfect positive correlation
The following function will create a correlation matrix from a DataFrame.
In [5]:
function cordf(df::DataFrame)
out=DataFrame([cor(df[:, a], df[:, b]) for a=1:size(df, 2), b=1:size(df, 2)], names(df))
return(out)
end
Out[5]:
cordf (generic function with 1 method)
For our data, we obtain:
In [6]:
correlation_dataframe = copy(training_examples)
correlation_dataframe[:target] = training_targets[:median_house_value]
out=cordf(correlation_dataframe)
Out[6]:
latitude longitude housing_median_age total_rooms total_bedrooms population households median_income rooms_per_person target
1 1.0 -0.92442 0.0134331 -0.0330437 -0.0639037 -0.109424 -0.0696911 -0.0817275 0.14167 -0.143389
2 -0.92442 1.0 -0.111856 0.0423046 0.067479 0.101509 0.055539 -0.0147274 -0.0780178 -0.0485976
3 0.0134331 -0.111856 1.0 -0.359789 -0.318997 -0.304781 -0.301969 -0.113801 -0.105698 0.113157
4 -0.0330437 0.0423046 -0.359789 1.0 0.925286 0.86786 0.917274 0.2006 0.128665 0.134338
5 -0.0639037 0.067479 -0.318997 0.925286 1.0 0.889786 0.981526 -0.013094 0.0518407 0.0470016
6 -0.109424 0.101509 -0.304781 0.86786 0.889786 1.0 0.916776 0.0028029 -0.14189 -0.0279506
7 -0.0696911 0.055539 -0.301969 0.917274 0.981526 0.916776 1.0 0.0102467 -0.0289163 0.0627944
8 -0.0817275 -0.0147274 -0.113801 0.2006 -0.013094 0.0028029 0.0102467 1.0 0.241114 0.69338
9 0.14167 -0.0780178 -0.105698 0.128665 0.0518407 -0.14189 -0.0289163 0.241114 1.0 0.209683
10 -0.143389 -0.0485976 0.113157 0.134338 0.0470016 -0.0279506 0.0627944 0.69338 0.209683 1.0
Ideally, we’d like to have features that are strongly correlated with the target.
We’d also like to have features that aren’t so strongly correlated with each other, so that they add independent information.
Use this information to try removing features. You can also try developing additional synthetic features, such as ratios of two raw features.
For convenience, we’ve included the training code from the previous exercise.
In [7]:
function construct_columns(input_features):
"""Construct the Feature Columns.

Args:
input_features: Numerical input features to use.
Returns:
A set of converted feature columns
"""
out=convert(Array, input_features[:,:])
return convert.(Float64,out)

end
Out[7]:
construct_columns (generic function with 1 method)
In [8]:
function create_batches(features, targets, steps, batch_size=5, num_epochs=0)

if(num_epochs==0)
num_epochs=ceil(batch_size*steps/size(features,1))
end
names_features=names(features);
names_targets=names(targets);

features_batches=copy(features)
target_batches=copy(targets)

for i=1:num_epochs
select=shuffle(1:size(features,1))
if i==1
features_batches=(features[select,:])
target_batches=(targets[select,:])
else
append!(features_batches, features[select,:])
append!(target_batches, targets[select,:])
end
end
return features_batches, target_batches
end
Out[8]:
create_batches (generic function with 3 methods)
In [9]:
function next_batch(features_batches, targets_batches, batch_size, iter)
select=mod((iter-1)*batch_size+1, size(features_batches,1)):mod(iter*batch_size, size(features_batches,1));

ds=features_batches[select,:];
target=targets_batches[select,:];
return ds, target
end
Out[9]:
next_batch (generic function with 1 method)
In [10]:
function my_input_fn(features_batches, targets_batches, iter, batch_size=5, shuffle_flag=1):
"""Trains a linear regression model of one feature.

Args:
features: DataFrame of features
targets: DataFrame of targets
batch_size: Size of batches to be passed to the model
shuffle: True or False. Whether to shuffle the data.
num_epochs: Number of epochs for which data should be repeated. None = repeat indefinitely
Returns:
Tuple of (features, labels) for next data batch
"""

# Convert pandas data into a dict of np arrays.
#features = {key:np.array(value) for key,value in dict(features).items()}

# Construct a dataset, and configure batching/repeating.
#ds = Dataset.from_tensor_slices((features,targets)) # warning: 2GB limit
ds, target = next_batch(features_batches, targets_batches, batch_size, iter)

# Shuffle the data, if specified.
if shuffle_flag==1
select=shuffle(1:size(ds, 1));
ds = ds[select,:]
target = target[select, :]
end

# Return the next batch of data.
# features, labels = ds.make_one_shot_iterator().get_next()
return ds, target
end
Out[10]:
my_input_fn (generic function with 3 methods)
In [11]:
function train_model(learning_rate,
steps,
batch_size,
training_examples,
training_targets,
validation_examples,
validation_targets)
"""Trains a linear regression model of one feature.

Args:
learning_rate: A `float`, the learning rate.
steps: A non-zero `int`, the total number of training steps. A training step
consists of a forward and backward pass using a single batch.
batch_size: A non-zero `int`, the batch size.
input_feature: A column from `california_housing_dataframe`
to use as input feature.
"""

periods = 10
steps_per_period = steps / periods

# Create feature columns.
feature_columns = placeholder(Float32)
target_columns = placeholder(Float32)

# Create a linear regressor object.
# Configure the linear regression model with our feature columns and optimizer.
m=Variable(zeros(length(training_examples),1))
b=Variable(0.0)
y=(feature_columns*m) .+ b
loss=reduce_sum((target_columns - y).^2)
run(sess, global_variables_initializer())
features_batches, targets_batches = create_batches(training_examples, training_targets, steps, batch_size)

# Advanced gradient decent with gradient clipping
my_optimizer=(train.GradientDescentOptimizer(learning_rate))
gvs = train.compute_gradients(my_optimizer, loss)
capped_gvs = [(clip_by_norm(grad, 5.), var) for (grad, var) in gvs]
my_optimizer = train.apply_gradients(my_optimizer,capped_gvs)

# Train the model, but do so inside a loop so that we can periodically assess
# loss metrics.
println("Training model...")
println("RMSE (on training data):")
training_rmse = []
validation_rmse=[]
for period in 1:periods
# Train the model, starting from the prior state.
for i=1:steps_per_period
features, labels = my_input_fn(features_batches, targets_batches, convert(Int,(period-1)*steps_per_period+i), batch_size)
#println(construct_columns(features))
#println(construct_columns(labels))
run(sess, my_optimizer, Dict(feature_columns=>construct_columns(features), target_columns=>construct_columns(labels)))
end
# Take a break and compute predictions.
training_predictions = run(sess, y, Dict(feature_columns=> construct_columns(training_examples)));
validation_predictions = run(sess, y, Dict(feature_columns=> construct_columns(validation_examples)));

# Compute loss.
training_mean_squared_error = mean((training_predictions- construct_columns(training_targets)).^2)
training_root_mean_squared_error = sqrt(training_mean_squared_error)
validation_mean_squared_error = mean((validation_predictions- construct_columns(validation_targets)).^2)
validation_root_mean_squared_error = sqrt(validation_mean_squared_error)
# Occasionally print the current loss.
println(" period ", period, ": ", training_root_mean_squared_error)
# Add the loss metrics from this period to our list.
push!(training_rmse, training_root_mean_squared_error)
push!(validation_rmse, validation_root_mean_squared_error)
end

weight = run(sess,m)
bias = run(sess,b)

println("Model training finished.")

# Output a graph of loss metrics over periods.
p1=plot(training_rmse, label="training", title="Root Mean Squared Error vs. Periods", ylabel="RMSE", xlabel="Periods")
p1=plot!(validation_rmse, label="validation")

println("Final RMSE (on training data): ", training_rmse[end])
println("Final Weight (on training data): ", weight)
println("Final Bias (on training data): ", bias)

return weight, bias, p1 #, calibration_data
end
Out[11]:
train_model (generic function with 1 method)
Spend 5 minutes searching for a good set of features and training parameters. Then check the solution to see what we chose. Don’t forget that different features may require different learning parameters.
In [12]:
#
# Your code here: add your features of choice as a list of quoted strings.
#
minimal_features = [:latitude,
:median_income,
:rooms_per_person,
:total_bedrooms
]

minimal_training_examples = training_examples[minimal_features]
minimal_validation_examples = validation_examples[minimal_features]

#
# Don't forget to adjust these parameters.
#
weight, bias, p1 = train_model(
# TWEAK THESE VALUES TO SEE HOW MUCH YOU CAN IMPROVE THE RMSE
0.003, #learning rate
500, #steps
5, #batch_size
minimal_training_examples,
training_targets,
minimal_validation_examples,
validation_targets)
Training model...
RMSE (on training data):
period 1: 176.81135496278543
period 2: 197.19851279643493
period 3: 177.19929452338476
period 4: 163.2594309192727
period 5: 175.012054267792
period 6: 162.2266241582263
period 7: 160.72278841433283
period 8: 163.75381369589246
period 9: 160.8075414523598
period 10: 160.1573212530893
Model training finished.
Final RMSE (on training data): 160.1573212530893
Final Weight (on training data):
Out[12]:
([0.791808; 0.164787; 0.0513053; 0.272608], 3.4386208510277307, Plot{Plots.GRBackend() n=2})
[0.791808; 0.164787; 0.0513053; 0.272608]
Final Bias (on training data): 3.4386208510277307
In [13]:
plot(p1)
Out[13]:
246810160170180190200Root Mean Squared Error vs. PeriodsPeriodsRMSEtrainingvalidation

Solution

Click below for a solution.
In [14]:
minimal_features = [
:median_income,
:latitude,
]

minimal_training_examples = training_examples[minimal_features]
minimal_validation_examples = validation_examples[minimal_features]

weight, bias, p1 = train_model(
# TWEAK THESE VALUES TO SEE HOW MUCH YOU CAN IMPROVE THE RMSE
0.01, #learning rate
500, #steps
5, #batch_size
minimal_training_examples,
training_targets,
minimal_validation_examples,
validation_targets)
Training model...
RMSE (on training data):
period 1: 166.6381578795206
period 2: 123.40258711495447
period 3: 117.41654566769705
period 4: 116.47465501458149
period 5: 115.9032457429208
period 6: 115.22126809675004
period 7: 114.6400496471454
period 8: 114.50896882953226
period 9: 114.42983122512207
period 10: 113.22989798903644
Model training finished.
Final RMSE (on training data): 113.22989798903644
Final Weight (on training data):
Out[14]:
([3.90901; 5.17569], 6.1757715170917615, Plot{Plots.GRBackend() n=2})
[3.90901; 5.17569]
Final Bias (on training data): 6.1757715170917615
In [15]:
plot(p1)
Out[15]:
246810120130140150160Root Mean Squared Error vs. PeriodsPeriodsRMSEtrainingvalidation

Task 2: Make Better Use of Latitude

Plotting latitude vs. median_house_value shows that there really isn’t a linear relationship there.
Instead, there are a couple of peaks, which roughly correspond to Los Angeles and San Francisco.
In [25]:
#scatter(training_examples[:latitude], training_targets[:median_house_value])
Try creating some synthetic features that do a better job with latitude.
For example, you could have a feature that maps latitude to a value of |latitude - 38|, and call this distance_from_san_francisco.
Or you could break the space into 10 different buckets. latitude_32_to_33latitude_33_to_34, etc., each showing a value of 1.0 if latitude is within that bucket range and a value of 0.0 otherwise.
Use the correlation matrix to help guide development, and then add them to your model if you find something that looks good.
What’s the best validation performance you can get?
In [17]:
lat1=32:41
lat2=33:42
lat_range=zip(lat1,lat2) # zip creates a set of tuples from vectors

function create_index(value, r1, r2)
if value >=r1 && value <r2
out=1.0
else
out=0.0
end
return out
end

function select_and_transform_features(source_df, lat_range)
selected_examples=DataFrame()
selected_examples[:median_income]=source_df[:median_income]

# Symbol(string) allows to convert a string to a DataFrames name :string
for range in lat_range
selected_examples[Symbol(string("latitude_", range[1],"_", range[2]))]=create_index.(source_df[:latitude], range[1], range[2])
end

return selected_examples
end
Out[17]:
select_and_transform_features (generic function with 1 method)
In [19]:
selected_training_examples = select_and_transform_features(training_examples, lat_range)
selected_validation_examples = select_and_transform_features(validation_examples, lat_range);
In [20]:
correlation_dataframe = copy(selected_training_examples)
correlation_dataframe[:target] = training_targets[:median_house_value]
out=cordf(correlation_dataframe)
Out[20]:
median_income latitude_32_33 latitude_33_34 latitude_34_35 latitude_35_36 latitude_36_37 latitude_37_38 latitude_38_39 latitude_39_40 latitude_40_41 latitude_41_42 target
1 1.0 -0.0378197 0.0977279 0.00573422 -0.0782914 -0.111251 0.130371 -0.0591627 -0.104248 -0.0903707 -0.0532983 0.69338
2 -0.0378197 1.0 -0.145097 -0.151012 -0.0424998 -0.0658619 -0.137984 -0.0876875 -0.0444428 -0.0333901 -0.0165942 -0.0556183
3 0.0977279 -0.145097 1.0 -0.319565 -0.089936 -0.139374 -0.291996 -0.18556 -0.0940478 -0.0706586 -0.0351158 0.0862109
4 0.00573422 -0.151012 -0.319565 1.0 -0.0936025 -0.145056 -0.3039 -0.193125 -0.0978819 -0.0735392 -0.0365474 0.103302
5 -0.0782914 -0.0424998 -0.089936 -0.0936025 1.0 -0.0408235 -0.0855275 -0.0543517 -0.0275472 -0.0206963 -0.0102856 -0.126866
6 -0.111251 -0.0658619 -0.139374 -0.145056 -0.0408235 1.0 -0.132542 -0.0842289 -0.0426899 -0.0320731 -0.0159397 -0.176179
7 0.130371 -0.137984 -0.291996 -0.3039 -0.0855275 -0.132542 1.0 -0.176464 -0.0894377 -0.067195 -0.0333945 0.20737
8 -0.0591627 -0.0876875 -0.18556 -0.193125 -0.0543517 -0.0842289 -0.176464 1.0 -0.0568366 -0.0427016 -0.0212218 -0.154133
9 -0.104248 -0.0444428 -0.0940478 -0.0978819 -0.0275472 -0.0426899 -0.0894377 -0.0568366 1.0 -0.0216425 -0.0107559 -0.144115
10 -0.0903707 -0.0333901 -0.0706586 -0.0735392 -0.0206963 -0.0320731 -0.067195 -0.0427016 -0.0216425 1.0 -0.00808096 -0.132673
11 -0.0532983 -0.0165942 -0.0351158 -0.0365474 -0.0102856 -0.0159397 -0.0333945 -0.0212218 -0.0107559 -0.00808096 1.0 -0.0725599
12 0.69338 -0.0556183 0.0862109 0.103302 -0.126866 -0.176179 0.20737 -0.154133 -0.144115 -0.132673 -0.0725599 1.0
In [21]:
weight, bias, p1 = train_model(
# TWEAK THESE VALUES TO SEE HOW MUCH YOU CAN IMPROVE THE RMSE
0.01, #learning rate
1500, #steps
5, #batch_size
selected_training_examples,
training_targets,
selected_validation_examples,
validation_targets)
Training model...
RMSE (on training data):
period 1: 201.29734093314335
period 2: 166.27605201818633
period 3: 133.73623601522797
period 4: 106.60004957137373
period 5: 89.43877655956301
period 6: 84.57477454533738
period 7: 83.9017907032955
period 8: 83.39604187887103
period 9: 83.24050020132016
period 10: 83.04106977733369
Model training finished.
Final RMSE (on training data): 83.04106977733369
Final Weight (on training data):
Out[21]:
([41.177; 0.229034; … ; -0.487278; -0.0993445], 42.77082332272024, Plot{Plots.GRBackend() n=2})
[41.177; 0.229034; 2.88724; 4.8904; -0.485478; -0.826352; 4.51781; -0.819746; -0.583517; -0.487278; -0.0993445]
Final Bias (on training data): 42.77082332272024
In [22]:
plot(p1)
Out[22]:
246810100125150175200Root Mean Squared Error vs. PeriodsPeriodsRMSEtrainingvalidation

In [23]:
#EOF