Tag Archives: Julia

Introduction to TensorFlow.jl

By: Sören Dobberschütz

Re-posted from: https://tensorflowjulia.blogspot.com/2018/08/introduction-to-tensorflowjl.html

The first exercise is about getting a general idea of TensorFlow. We run through the following number of steps:
  1. Load the necessary packages and start a new session.
  2. Load the data.
  3. Define the features and targets as placeholders in TensorFlow. Define the variables of the model and
    put them together to give a linear regressor model.
  4. Create some functions that help with feeding the input features to the model.
  5. Train the model and have a look at the result.

One interesting difference between the Python version and Julia is how to implement gradient clipping.

Tensorflow.jl does not expose tf.contrib.estimator.clip_gradients_by_norm (to my knowledge),
so instead of tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0) we use the construction

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)


The Jupyter Notebook can be downloaded here. It is also displayed below. 

And if anyone knows how to get a decent formatting for it, please leave a comment 🙂

This file is based on the file First Steps with TensorFlow, which is part of Google’s Machine Learning Crash Course.
In [1]:
# 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.

First Steps with TensorFlow.jl

Learning Objectives:
  • Learn fundamental TensorFlow concepts
  • Building a linear regressor in TensorFlow to predict median housing price, at the granularity of city blocks, based on one input feature
  • Evaluate the accuracy of a model’s predictions using Root Mean Squared Error (RMSE)
  • Improve the accuracy of a model by tuning its hyperparameters
The data is based on 1990 census data from California. The training data can be downloaded here.

Setup

In this first cell, we’ll load the necessary libraries.
In [1]:
using Plots
gr()
using DataFrames
using TensorFlow
import CSV
Start a new TensorFlow session.
In [2]:
sess=Session()
Out[2]:
Session(Ptr{Void} @0x000000011ba9dd70)
2018-08-05 21:12:46.959154: 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
Next, we’ll load our data set.
In [4]:
california_housing_dataframe = CSV.read("california_housing_train.csv", delim=",");
We’ll randomize the data, just to be sure not to get any pathological ordering effects that might harm the performance of Stochastic Gradient Descent. Additionally, we’ll scale median_house_value to be in units of thousands, so it can be learned a little more easily with learning rates in a range that we usually use.
In [5]:
california_housing_dataframe = california_housing_dataframe[shuffle(1:size(california_housing_dataframe, 1)),:];
california_housing_dataframe[:median_house_value] /= 1000.0
california_housing_dataframe
Out[5]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income median_house_value
1 -121.44 38.56 52.0 906.0 165.0 257.0 166.0 2.8542 139.4
2 -122.13 37.77 24.0 2459.0 317.0 916.0 324.0 7.0712 293.0
3 -117.96 34.14 33.0 1994.0 405.0 993.0 403.0 3.766 163.9
4 -117.89 34.1 35.0 3185.0 544.0 1858.0 564.0 3.8304 175.9
5 -118.21 34.09 37.0 1822.0 498.0 1961.0 506.0 1.9881 159.2
6 -122.3 37.53 37.0 1338.0 215.0 535.0 221.0 5.4351 376.6
7 -118.64 34.19 33.0 3017.0 494.0 1423.0 470.0 5.6163 248.4
8 -121.23 37.8 11.0 2451.0 665.0 1155.0 533.0 2.2254 130.8
9 -117.09 34.07 24.0 6260.0 1271.0 3132.0 1189.0 2.5156 103.0
10 -121.71 37.99 27.0 3861.0 718.0 2085.0 707.0 3.3558 129.7
11 -122.0 37.54 26.0 1910.0 371.0 852.0 357.0 5.8325 298.9
12 -122.48 37.75 52.0 2515.0 494.0 1583.0 477.0 4.3393 317.6
13 -118.97 37.64 14.0 1847.0 439.0 238.0 98.0 3.6042 137.5
14 -120.85 37.51 5.0 2899.0 745.0 1593.0 633.0 2.2292 127.5
15 -116.2 33.63 23.0 1152.0 273.0 1077.0 235.0 2.5 96.3
16 -122.39 38.37 33.0 1066.0 191.0 403.0 163.0 6.8 240.8
17 -118.3 33.89 37.0 2132.0 565.0 1369.0 565.0 3.285 218.1
18 -117.01 32.73 22.0 2526.0 530.0 1556.0 529.0 2.8646 120.8
19 -122.42 37.79 52.0 3457.0 1021.0 2286.0 994.0 2.565 225.0
20 -118.02 34.15 44.0 2267.0 426.0 980.0 372.0 3.6 307.4
21 -119.63 36.58 22.0 1794.0 435.0 1127.0 359.0 1.2647 55.3
22 -118.12 33.87 21.0 3764.0 1081.0 1919.0 977.0 2.5057 156.3
23 -118.15 34.71 36.0 1338.0 250.0 709.0 250.0 3.5625 101.4
24 -118.06 33.72 22.0 4311.0 531.0 1426.0 533.0 9.8177 500.001
25 -121.93 37.28 34.0 2422.0 370.0 1010.0 395.0 5.6494 376.2
26 -118.54 34.23 35.0 3422.0 601.0 1690.0 574.0 4.375 232.9
27 -118.4 33.88 35.0 1753.0 296.0 615.0 275.0 7.5 500.001
28 -121.64 36.72 17.0 4203.0 816.0 2900.0 827.0 4.1742 159.9
29 -118.31 33.73 52.0 1665.0 280.0 656.0 282.0 5.249 351.9
30 -117.68 34.11 16.0 3190.0 471.0 1414.0 464.0 5.5292 208.6

Examine the Data

It’s a good idea to get to know your data a little bit before you work with it.
We’ll print out a quick summary of a few useful statistics on each column: count of examples, mean, standard deviation, max, min, and various quantiles.
In [6]:
describe(california_housing_dataframe)
Out[6]:
variable mean min median max nunique nmissing eltype
1 longitude -119.562 -124.35 -118.49 -114.31 0 Float64
2 latitude 35.6252 32.54 34.25 41.95 0 Float64
3 housing_median_age 28.5894 1.0 29.0 52.0 0 Float64
4 total_rooms 2643.66 2.0 2127.0 37937.0 0 Float64
5 total_bedrooms 539.411 1.0 434.0 6445.0 0 Float64
6 population 1429.57 3.0 1167.0 35682.0 0 Float64
7 households 501.222 1.0 409.0 6082.0 0 Float64
8 median_income 3.88358 0.4999 3.5446 15.0001 0 Float64
9 median_house_value 207.301 14.999 180.4 500.001 Float64

Build the First Model

In this exercise, we’ll try to predict median_house_value, which will be our label (sometimes also called a target). We’ll use total_rooms as our input feature.
NOTE: Our data is at the city block level, so this feature represents the total number of rooms in that block.
To train our model, we’ll set up a linear regressor model.

Step 1: Define Features and Configure Feature Columns

In order to import our training data into TensorFlow, we need to specify what type of data each feature contains. There are two main types of data we’ll use in this and future exercises:
  • Categorical Data: Data that is textual. In this exercise, our housing data set does not contain any categorical features, but examples you might see would be the home style, the words in a real-estate ad.
  • Numerical Data: Data that is a number (integer or float) and that you want to treat as a number. As we will discuss more later sometimes you might want to treat numerical data (e.g., a postal code) as if it were categorical.
To start, we’re going to use just one numeric input feature, total_rooms. The following code pulls the total_rooms data from our california_housing_dataframe and defines a feature and targer column:
In [7]:
# Define the input feature: total_rooms.
my_feature = california_housing_dataframe[:total_rooms]

# Configure a numeric feature column for total_rooms.
feature_columns = placeholder(Float32)
target_columns = placeholder(Float32)
Out[7]:
<Tensor placeholder_2:1 shape=unknown dtype=Float32>

Step 2: Define the Target

Next, we’ll define our target, which is median_house_value. Again, we can pull it from our california_housing_dataframe:
In [8]:
# Define the label.
targets = california_housing_dataframe[:median_house_value];

Step 3: Configure the LinearRegressor

Next, we’ll configure a linear regression model using LinearRegressor. We’ll train this model using the GradientDescentOptimizer, which implements Mini-Batch Stochastic Gradient Descent (SGD). The learning_rate argument controls the size of the gradient step.
NOTE: To be safe, we also apply gradient clipping to our optimizer via clip_gradients_by_norm. Gradient clipping ensures the magnitude of the gradients do not become too large during training, which can cause gradient descent to fail.
In [9]:
# Configure the linear regression model with our feature columns and optimizer.
m=Variable(0.05)
b=Variable(0.0)
y=m.*feature_columns+b
loss=reduce_sum((target_columns - y).^2)

# Use gradient descent as the optimizer for training the model.
# Set a learning rate of 0.0000001 for Gradient Descent.
learning_rate=0.0000001;
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)
Out[9]:
<Tensor Group:1 shape=unknown dtype=Any>

Step 4: Define the Input Function

To import our California housing data into our linear regressor model, we need to define an input function, which instructs TensorFlow how to preprocess the data, as well as how to batch, shuffle, and repeat it during model training.
First, we’ll convert our DataFrame feature data into an array. We can then construct a dataset object from our data, and then break our data into batches of batch_size, to be repeated for the specified number of epochs (num_epochs).
NOTE: When the default value of num_epochs=None is passed to repeat(), the input data will be repeated indefinitely.
Next, if shuffle is set to True, we’ll shuffle the data so that it’s passed to the model randomly during training. The buffer_size argument specifies the size of the dataset from which shuffle will randomly sample.
Finally, our input function constructs an iterator for the dataset and returns the next batch of data to the linear regressor.
In [11]:
function create_batches(features, targets, steps, batch_size=5, num_epochs=0)

if(num_epochs==0)
num_epochs=ceil(batch_size*steps/length(features))
end

features_batches=Union{Float64, Missings.Missing}[]
target_batches=Union{Float64, Missings.Missing}[]

for i=1:num_epochs
select=shuffle(1:length(features))
append!(features_batches, features[select])
append!(target_batches, targets[select])
end

return features_batches, target_batches
end
Out[11]:
create_batches (generic function with 3 methods)
In [10]:
function next_batch(features_batches, targets_batches, batch_size, iter)

select=mod((iter-1)*batch_size+1, length(features_batches)):mod(iter*batch_size, length(features_batches));

ds=features_batches[select];
target=targets_batches[select];

return ds, target
end
Out[10]:
next_batch (generic function with 1 method)
In [12]:
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: pandas DataFrame of features
targets: pandas 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
"""


# Construct a dataset, and configure batching/repeating.
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.
return convert.(Float64,ds), convert.(Float64,target)
end
Out[12]:
my_input_fn (generic function with 3 methods)
NOTE: We’ll continue to use this same input function in later exercises.

Step 5: Train the Model

We can now call train() on our my_optimizer to train the model. To start, we’ll train for 100 steps.
In [13]:
steps=100;
batch_size=5;
run(sess, global_variables_initializer())
features_batches, targets_batches = create_batches(my_feature, targets, steps, batch_size)

for i=1:steps
features, labels = my_input_fn(features_batches, targets_batches, i, batch_size)
run(sess, my_optimizer, Dict(feature_columns=>features, target_columns=>labels))
end
We can assess the values for the weight and bias variables:
In [14]:
weight = run(sess,m)
Out[14]:
0.05002900000000003
In [15]:
bias = run(sess,b)
Out[15]:
3.899999999999994e-5

Step 6: Evaluate the Model

Let’s make predictions on that training data, to see how well our model fit it during training.
NOTE: Training error measures how well your model fits the training data, but it does not measure how well your model generalizes to new data. In later exercises, you’ll explore how to split your data to evaluate your model’s ability to generalize.
In [16]:
# Run the TF session on the data to make predictions.
predictions = run(sess, y, Dict(feature_columns=>convert.(Float64, my_feature)));

# Print Mean Squared Error and Root Mean Squared Error.
mean_squared_error = mean((predictions- targets).^2);
root_mean_squared_error = sqrt(mean_squared_error);
println("Mean Squared Error (on training data): ", mean_squared_error)
println("Root Mean Squared Error (on training data): ", root_mean_squared_error)
Mean Squared Error (on training data): 27662.40649651179
Root Mean Squared Error (on training data): 166.32019269021964
Is this a good model? How would you judge how large this error is?
Mean Squared Error (MSE) can be hard to interpret, so we often look at Root Mean Squared Error (RMSE) instead. A nice property of RMSE is that it can be interpreted on the same scale as the original targets.
Let’s compare the RMSE to the difference of the min and max of our targets:
In [17]:
min_house_value = minimum(california_housing_dataframe[:median_house_value])
max_house_value = maximum(california_housing_dataframe[:median_house_value])
min_max_difference = max_house_value - min_house_value

println("Min. Median House Value: " , min_house_value)
println("Max. Median House Value: " , max_house_value)
println("Difference between Min. and Max.: " , min_max_difference)
println("Root Mean Squared Error: " , root_mean_squared_error)
Min. Median House Value: 14.999
Max. Median House Value: 500.001
Difference between Min. and Max.: 485.00199999999995
Root Mean Squared Error: 166.32019269021964
Our error spans nearly half the range of the target values. Can we do better?
This is the question that nags at every model developer. Let’s develop some basic strategies to reduce model error.
The first thing we can do is take a look at how well our predictions match our targets, in terms of overall summary statistics.
In [18]:
calibration_data = DataFrame();
calibration_data[:predictions] = predictions;
calibration_data[:targets] = targets;
describe(calibration_data)
Out[18]:
variable mean min median max nunique nmissing eltype
1 predictions 132.26 0.100097 106.412 1897.95 Float64
2 targets 207.301 14.999 180.4 500.001 Float64
Okay, maybe this information is helpful. How does the mean value compare to the model’s RMSE? How about the various quantiles?
We can also visualize the data and the line we’ve learned. Recall that linear regression on a single feature can be drawn as a line mapping input x to output y.
First, we’ll get a uniform random sample of the data so we can make a readable scatter plot.
In [19]:
sample = california_housing_dataframe[rand(1:size(california_housing_dataframe,1), 300),:];
Next, we’ll plot the line we’ve learned, drawing from the model’s bias term and feature weight, together with the scatter plot. The line will show up red.
In [20]:
# Get the min and max total_rooms values.
x_0 = minimum(sample[:total_rooms])
x_1 = maximum(sample[:total_rooms])

# Retrieve the final weight and bias generated during training.
weight = run(sess,m)
bias = run(sess,b)

# Get the predicted median_house_values for the min and max total_rooms values.
y_0 = weight * x_0 + bias
y_1 = weight * x_1 + bias

# Plot our regression line from (x_0, y_0) to (x_1, y_1).
plot([x_0, x_1], [y_0, y_1], c=:red, ylabel="median_house_value", xlabel="total_rooms", label="Fit line")

# Plot a scatter plot from our data sample.
scatter!(sample[:total_rooms], sample[:median_house_value], c=:blue, label="Data")
Out[20]:
05.0×1031.0×1041.5×1040200400600800total_roomsmedian_house_valueFit lineData
In [24]:
input_feature=:total_rooms
Out[24]:
:total_rooms
This initial line looks way off. See if you can look back at the summary stats and see the same information encoded there.
Together, these initial sanity checks suggest we may be able to find a much better line.

Tweak the Model Hyperparameters

For this exercise, we’ve put all the above code in a single function for convenience. You can call the function with different parameters to see the effect.
In this function, we’ll proceed in 10 evenly divided periods so that we can observe the model improvement at each period.
For each period, we’ll compute and graph training loss. This may help you judge when a model is converged, or if it needs more iterations.
We’ll also plot the feature weight and bias term values learned by the model over time. This is another way to see how things converge.
In [27]:
function train_model(learning_rate, steps, batch_size, input_feature=:total_rooms)
"""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 `string` specifying a column from `california_housing_dataframe`
to use as input feature.
"""

periods = 10
steps_per_period = steps / periods

my_feature = input_feature
my_feature_data = convert.(Float32,california_housing_dataframe[my_feature])
my_label = :median_house_value
targets = convert.(Float32,california_housing_dataframe[my_label])

# 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(0.0)
b=Variable(0.0)
y=m.*feature_columns .+ b
loss=reduce_sum((target_columns - y).^2)
features_batches, targets_batches = create_batches(my_feature_data, targets, steps, batch_size)

# Use gradient descent as the optimizer for training the model.
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)

run(sess, global_variables_initializer())

# Set up to plot the state of our model's line each period.
sample = california_housing_dataframe[rand(1:size(california_housing_dataframe,1), 300),:];
p1=scatter(sample[my_feature], sample[my_label], title="Learned Line by Period", ylabel=my_label, xlabel=my_feature,color=:coolwarm)
colors= [ColorGradient(:coolwarm)[i] for i in linspace(0,1, periods+1)]

# 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):")
root_mean_squared_errors = []
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)
run(sess, my_optimizer, Dict(feature_columns=>features, target_columns=>labels))
end
# Take a break and compute predictions.
predictions = run(sess, y, Dict(feature_columns=>convert.(Float64, my_feature_data)));

# Compute loss.
mean_squared_error = mean((predictions- targets).^2)
root_mean_squared_error = sqrt(mean_squared_error)
# Occasionally print the current loss.
println(" period ", period, ": ", root_mean_squared_error)
# Add the loss metrics from this period to our list.
push!(root_mean_squared_errors, root_mean_squared_error)
# Finally, track the weights and biases over time.

# Apply some math to ensure that the data and line are plotted neatly.
y_extents = [0 maximum(sample[my_label])]

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

x_extents = (y_extents - bias) / weight
x_extents = max.(min.(x_extents, maximum(sample[my_feature])),
minimum(sample[my_feature]))
y_extents = weight .* x_extents .+ bias

p1=plot!(x_extents', y_extents', color=colors[period], linewidth=2)
end

println("Model training finished.")

# Output a graph of loss metrics over periods.
p2=plot(root_mean_squared_errors, title="Root Mean Squared Error vs. Periods", ylabel="RMSE", xlabel="Periods")

# Output a table with calibration data.
calibration_data = DataFrame()
calibration_data[:predictions] = predictions
calibration_data[:targets] = targets
describe(calibration_data)

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

return p1, p2
end
Out[27]:
train_model (generic function with 2 methods)

Task 1: Achieve an RMSE of 180 or Below

Tweak the model hyperparameters to improve loss and better match the target distribution. If, after 5 minutes or so, you’re having trouble beating a RMSE of 180, check the solution for a possible combination.
In [34]:
sess=Session()
p1, p2= train_model(
0.0001, # learning rate
20, # steps
5 # batch size
)
Training model...
RMSE (on training data):
period 1: 235.10452754834785
period 2: 232.69434701416756
period 3: 230.30994696291228
period 4: 227.95213639459652
period 5: 225.62174891342735
period 6: 223.31964301811482
period 7: 221.04670233191547
period 8: 218.80383576386936
period 9: 216.59197759206555
period 10: 214.4120874591565
Model training finished.
Out[34]:
(Plot{Plots.GRBackend() n=11}, Plot{Plots.GRBackend() n=1})
Final RMSE (on training data): 214.4120874591565
Final Weight (on training data): 0.05002900000000003
Final Bias (on training data): 3.899999999999994e-5
In [35]:
plot(p1, p2, layout=(1,2), legend=false)
Out[35]:
0250050007500100000100200300400500Learned Line by Periodtotal_roomsmedian_house_value246810215220225230235Root Mean Squared Error vs. PeriodsPeriodsRMSE

Solution

Click below for one possible solution.
In [36]:
p1, p2= train_model(
0.001, # learning rate
20, # steps
5 # batch size
)
Training model...
RMSE (on training data):
period 1: 214.4120874591565
period 2: 194.600111798902
period 3: 179.20684050800634
period 4: 169.44086805355903
period 5: 166.29657770919368
period 6: 170.14148129718657
period 7: 170.13862388370202
period 8: 170.13576700999337
period 9: 180.5271084663441
period 10: 170.13291067608785
Model training finished.
Out[36]:
(Plot{Plots.GRBackend() n=11}, Plot{Plots.GRBackend() n=1})
Final RMSE (on training data): 170.13291067608785
Final Weight (on training data): 0.05002900000000003
Final Bias (on training data): 3.899999999999994e-5
In [37]:
plot(p1, p2, layout=(1,2), legend=false)
Out[37]:
05.0×1031.0×1041.5×1040100200300400500Learned Line by Periodtotal_roomsmedian_house_value246810170180190200210Root Mean Squared Error vs. PeriodsPeriodsRMSE
This is just one possible configuration; there may be other combinations of settings that also give good results. Note that in general, this exercise isn’t about finding the one best setting, but to help build your intutions about how tweaking the model configuration affects prediction quality.

Is There a Standard Heuristic for Model Tuning?

This is a commonly asked question. The short answer is that the effects of different hyperparameters are data dependent. So there are no hard-and-fast rules; you’ll need to test on your data.
That said, here are a few rules of thumb that may help guide you:
  • Training error should steadily decrease, steeply at first, and should eventually plateau as training converges.
  • If the training has not converged, try running it for longer.
  • If the training error decreases too slowly, increasing the learning rate may help it decrease faster.
    • But sometimes the exact opposite may happen if the learning rate is too high.
  • If the training error varies wildly, try decreasing the learning rate.
    • Lower learning rate plus larger number of steps or larger batch size is often a good combination.
  • Very small batch sizes can also cause instability. First try larger values like 100 or 1000, and decrease until you see degradation.
Again, never go strictly by these rules of thumb, because the effects are data dependent. Always experiment and verify.

Task 2: Try a Different Feature

See if you can do any better by replacing the total_rooms feature with the population feature.
In [44]:
# YOUR CODE HERE
p1, p2= train_model(
0.0001, # learning rate
300, # steps
5, # batch size
:population #feature
)
Training model...
RMSE (on training data):
period 1: 219.99196584705817
period 2: 204.6500256564945
period 3: 192.7864903464405
period 4: 183.76726822988732
period 5: 178.63082652548096
period 6: 177.0318900993104
period 7: 176.3389445217556
period 8: 175.9467609063822
period 9: 176.1995685595212
period 10: 176.62301419567666
Model training finished.
Out[44]:
(Plot{Plots.GRBackend() n=11}, Plot{Plots.GRBackend() n=1})
Final RMSE (on training data): 176.62301419567666
Final Weight (on training data): 0.05002900000000003
Final Bias (on training data): 3.899999999999994e-5
In [45]:
plot(p1, p2, layout=(1,2), legend=false)
Out[45]:
0250050007500100000100200300400500Learned Line by Periodpopulationmedian_house_value246810180190200210220Root Mean Squared Error vs. PeriodsPeriodsRMSE

Welcome!

By: Sören Dobberschütz

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

Welcome! With this blog, we are going to explore the Tensorflow.jl-Package for the Julia Programming Language

I am new to both Tensorflow and Julia – so please don’t expect any in-depth programming tricks or advanced API explanations. I will use Google’s newly released Machine Learning Crash Course and implement most of the exercises in a Jupyter notebook.

Why Julia?


Julia is a rather new programming language, initially released in 2012. The things that attracted me personally are

  • Most of my professional life, I have developed code in Matlab. Julia’s syntax is very close to that of Matlab, which makes the transition very easy. 
  • Julia is open-source. So no hassles with obtaining an expensive license.
  • Julia is fast (as compared to Matlab or Python). Its just-in-time compiler compiles the code to native machine code before execution. One of the first bits of code I wrote with Julia contained a part that enlarged and appended a big array for thousands of iterations. Of course this is very bad programming practise – but still it performed surprisingly well.

Why TensorFlow?


  • TensorFlow is open-source and used by Google and many other big companies for production machine learning tasks. 








Disclaimer

I am neither affiliated with Google (who runs the Machine Learning Crash Course), Julia or the creators of Tensorflow.jl. This is a pure just-for-fun hobby project.

Why Numba and Cython are not substitutes for Julia

By: Christopher Rackauckas

Re-posted from: http://www.stochasticlifestyle.com/why-numba-and-cython-are-not-substitutes-for-julia/

Sometimes people ask: why does Julia need to be a new language? What about Julia is truly different from tools like Cython and Numba? The purpose of this blog post is to describe how Julia’s design gives a very different package development experience than something like Cython, and how that can lead to many more optimizations. What I really want to show is:

  1. Julia’s compilation setup is built for specialization of labor which is required for scientific progress
  2. Composition of Julia codes can utilize the compilation process to build new programs which are greater than the sum of the parts

I will also explain some of the engineering tradeoffs which were made to make this happen. There are many state-of-the-art scientific computing and data science packages in Julia and what I want to describe is how these are using the more “hardcore language-level features” to get there.

Point 1: Any sufficiently competent IR generator will hit the optimization limit on small examples, but that doesn’t necessarily generalize to full package solutions

You will find benchmarks on small scripts (microbenchmarks) where Numba and Cython do as well as Julia. Can these tools optimize as well as Julia? I will give you even more than that. I will go as far as saying that any sufficient competent IR generating mechanism will be efficient on a single function (or in Julia terminology, method). It’s actually not that difficult of a problem in 2018. If you build an LLVM IR with concrete types then LLVM will compile it well, and pretty much any decent representation of that IR will get you to around the same spot. Cython, Numba, etc. all will work just as well as Julia if you have a single function with known input types and all of that because there’s just a limit to how optimal you can make your computations and most of the optimizations are just standard LLVM passes on the IR. It’s not hard to get a loop written in terms of simple basics (well okay, it is quite hard but I mean “not hard” as in “give enough programmers a bunch of time and they’ll get it right”). Cool, we’re all the same. And microbenchmark comparisons between Cython/Numba/Julia will point out 5% gains here and 2% losses here and try to extrapolate to how that means entire package ecosystems will collapse into chaos, when in reality most of this is likely due to different compiler versions and go away as the compilers themselves update. So let’s not waste time on these single functions.

Let’s talk about large code bases. Scaling code to real applications is what matters. Here’s the issue: LLVM cannot optimize a Python interpreter which sits in the middle between two of your optimized function calls, and this can HURT. Take a look at this example which introduces the DifferentialEquations.jl bindings in Python and R. In it we show how JIT compiling a function with Numba only moderately helps the ODE solver (i.e. 50% performance gain).

import numpy as np
from scipy.integrate import odeint
import timeit
import numba
 
def f(u, t, sigma, rho, beta):
    x, y, z = u
    return [sigma * (y - x), x * (rho - z) - y, x * y - beta * z]
 
u0 = [1.0,0.0,0.0]
tspan = (0., 100.)
t = np.linspace(0, 100, 1001)
sol = odeint(f, u0, t, args=(10.0,28.0,8/3))
 
def time_func():
    odeint(f, u0, t, args=(10.0,28.0,8/3),rtol = 1e-8, atol=1e-8)
 
timeit.Timer(time_func).timeit(number=100) # 6.637224912643433 seconds
 
numba_f = numba.jit(f)
def time_func():
    odeint(numba_f, u0, t, args=(10.0,28.0,8/3),rtol = 1e-8, atol=1e-8)
 
timeit.Timer(time_func).timeit(number=100) # 4.471225690009305 seconds

But if you allow the whole thing to be a Julia code it can optimize it a lot further:

from diffeqpy import de
jul_f = de.eval("""
function f(du,u,p,t)
  x, y, z = u
  sigma, rho, beta = p
  du[1] = sigma * (y - x)
  du[2] = x * (rho - z) - y
  du[3] = x * y - beta * z
end""")
prob = de.ODEProblem(jul_f, u0, tspan, p)
sol = de.solve(prob,saveat=t,abstol=1e-8,reltol=1e-8)
 
def time_func():
    sol = de.solve(prob,saveat=t,abstol=1e-8,reltol=1e-8)
 
timeit.Timer(time_func).timeit(number=100) # 0.8532032310031354 seconds

and then Julia’s excellent package building pieces allows us to easily implement all sorts of more optimized algorithms and so we get:

def time_func():
    sol = de.solve(prob,de.Vern9(),saveat=t,abstol=1e-8,reltol=1e-8)
 
timeit.Timer(time_func).timeit(number=100) # 0.5534579039958771 seconds

with the “Julia called from Python” solution which is about 10x faster than the SciPy+Numba code, which was really just Fortran+Numba vs a full Julia solution. The main issue is that Fortran+Numba still has Python context switches in there because the two pieces were independently compiled and it’s this which becomes the remaining bottleneck that cannot be erased. And it’s clear why this fact will not be of influence in simple one-function microbenchmarks but it is an extremely important difference when trying to build an optimized full ecosystem of scientific computing tools. I don’t care about these little 5% compiler version differences when there’s a 10x difference the moment I throw a real problem at it, and this is the kind of technical issue which you see requires a completely different architectural structure to fully solve.

Can you avoid these performance issues in Cython?

You can work past this with Cython. You can design the entire package yourself as one monolithic code base. You write the whole thing in Cython and don’t use person X’s C++ nonlinear solver library or person Y’s Numba nonlinear optimization tool and don’t use person Z’s CUDA kernel because you cannot optimize them together, oh and you don’t use person W’s Cython code without modification because you needed your Cython compilation to be aware of the existence of their Cython-able object before you do the compilation.

The problem is that monolithic architectures become maintenance nightmares which decrease programming productivity since you’re having to repeat the coding of many algorithms which have already been done. But there’s an even larger issue in the context of scientific computing: it is the intersection of high performance computational utilities with complex mathematical algorithms that gives you strong performance. Write a modern EPIRK ODE integrator with Krylov exponential approximation (one of the state-of-the-art stiff ODE solvers with few implementations) in pure Python using objects to describe your scientific model and your problem will be bogged down due to the computational structures that are used. Write a fast and simple Runge-Kutta order 4 integrator in Cython and your simulation will be bogged down since the choice of mathematical algorithm is unoptimized and will require many more function calls than necessary. It’s the intersection: good algorithm plus efficient data structures and compiled code, that produces efficient large-scale scientific software.

Monolithic programming structures are antithetical to the knowledge specialization that’s required in higher level mathematics. It’s simply impossible for someone to be an expert in all areas of numerical mathematics, let alone have the know-how and time to create optimized implementations of the newest algorithms from every discipline. In fact, there are very few people in each mathematical discipline that can even know the state-of-the-art in any detail! I still haven’t seen a fixed-leading coefficient Nordsieck BDF formulation in Python at all so native Python is behind ecosystems like SUNDIALS algorithmically, and that’s not even Cython, and that’s not using GPUs are with a single scientific model. Thinking you can quickly/productively write all of this yourself in one giant codebase is hubris.

Rewriting every single difficult algorithm from scratch in order to utilize the most modern methods with the most efficient structures is not a style of programming that scales well. This is 2018: we’re past this. We have packages and package managers now, and we want the ability to have separate packages work fully together. This solution already exists: it’s Julia.

Point 2: Julia uses fully-dependent compilation

Julia avoids this through its generic algorithms and dependent compilation: write the integrator once and recompile it to new data structures via multiple dispatch. Let’s explain this approach in some detail. If you’re new to the Julia-sphere and you haven’t read this explanation of the Julia compilation process, you may want to do a little pre-reading.

The idea is that if you know all of the code in its original un-compiled form then you can see you have literals (constants) on the top and propagate them throughout this code as true constants at compile time. You’d have to have no dynamicness in the middle, no interpreter, etc. since otherwise you wouldn’t be able to guarantee const-ness. You’d have to compile the entire package together. In order to propagate these constants and inline function calls into another package, you’d even need to compile different package calls together at the same. This is what Julia does and it’s why it’s able to do full interprodcedural optimization in a way that combines functions from different packages.

Take for example ForwardDiff.jl which can take arbitrary pure Julia code and do forward-mode automatic differentiation to it (even Julia’s Base library). When using ForwardDiff.jl inside of a large package like DifferentialEquations.jl, it doesn’t use a compiled version of ForwardDiff.jl operations inside of DifferentialEquations.jl, but instead it generates on-demand the full functions it needs to compile so that way the function’s value and its derivative are computed simultaneously. While doing this it inlines the ForwardDiff.jl arithmetic operations (along with any small first class functions which were passed into the routines). Julia then compiles the whole call together. And it’s not just packages: since Julia’s Base library and its standard library are written in Julia as well, it takes all of the Base library code as well to build a single typed script and can compile all of this together to a fully optimized form. The result is you get a full LLVM IR where ideas like “dual numbers” are abstractions which can be completely eliminated, and from there LLVM passes like common subexpression eliminated (CSE) can reduce the repeated arithmetic.

I say “can” because Julia’s compiler uses a cost model to decide what to keep separate as purely a function call, and then inserts the function call if it determines that the function call wouldn’t significantly change the runtime. By using a function call, it can use a separately compiled version of the function in order to reduce the amount of compilation time. This separately compiled version though is still a type-dependent compiled form. Note though that by using `@inline` you can bump the weight in the inlining heuristic to almost force it to happen (i.e. copy/paste the code in and compile in full instead of compiling the separate function).

This highlights a key difference between the Cython approach and the Julia approach, and it highlights the tradeoff. In Cython you have separately compiled functions and packages, much like static compilation to shared libraries in C++, and then you put function calls between them. In a few cases where you compile parts together it can inline, but generally you have separate packages/modules/etc. compile separately. This cuts down on compile time and makes it easier to generate a static binary but adds runtime costs.

In contrast, with Julia you have fully dependent compilation. Packages which call other packages can take control of the full code before compilation and then choices have to be made at how to separate it in a meaningful way. Yes, this means that managing compilation times is much more difficult in Julia as seen by the use of inlining cost models and “no-specialization” catches and tricks. If you go through the latency tag you can see that core developers are finding ways to automatically reduce specializations without cutting runtimes. Also, this means that static compilation is much more difficult than in other languages though Julia developers have already made large headway into making it a reality. There are package-level examples of this as well. In this SO post I describe how an ODE solver call can take a 2-3 second compilation to compile a version of the entire integrator program specifically to your ODE model which can reduce the runtime cost by about 4x-5x (which really matters in parameter estimation!), and how we have developed high level ways to turn this off to give users a choice to remove this extra specialization.

You can see that once we have fully dependent compilation, we want language level features in order to fully utilize and optimize this process. This is my next point.

Point 3: Compile-time control, code generation, and optimization require language-level compilation control features

Once you have the dependent compilation process as a large feature, you need/want language level features to be able to control this process. This begs the question: other than behind-the-scenes optimizations that this can add, are there any extra optimizations that can be had by allowing programmers control during this dependent compilation process?

Being able to fully optimize dependently JIT compiled code depends on having strong language level tools and utilizes to control the compilation process. You cannot even discuss these optimization opportunities from a Cython/Numba-perspective because you have a “Sapir-Whorf hypothesis problem”: Python does not have this dependent compilation setup so it doesn’t have the language or language-level tools for handling behavior at this phase. So let’s look at what you gain from dependent compilation and how Julia’s language level features interact with it.

This is where things like macros come in handy. Macros and metaprogramming apply at compile-time, so before functions get compiled you can modify what function you will be compilation based on an expression. But another huge fact is that “function” doesn’t even mean the same thing as in Python/Cython. In Julia, a function is a collection of methods and the method chosen to be used in the final compiled code is dependent on the input types. Since input types matter, you may want to specialize an algorithm for arrays of 64-bit floating point numbers separately from how you’d treat arrays of 32-bit floating point numbers. This is where Julia’s parametric typing comes in: you can specify Vector{Float64} vs Vector{Float32} and dispatch to separate algorithms which are optimal in each of the cases. You can even put these ideas together with generated functions which are “functions” where you can programmatically build a function expression during the compilation stages depending on the input types that you see. In fact, since the entire function compilation is at your control, you can build tools like Cassette.jl which allows you to take control of anyone else’s function and “overdub” it in the compilation process to change what it’s doing.

So what optimizations can you do with these tools? First of all, since you can see the entire code of a function, you can use the dependent compilation to build alternative output functions at compile-time. A type-based dispatch approach utilizes a wrapper type and have it re-write the internal function calls using Julia’s multiple dispatch. Once again, ForwardDiff.jl is a great example to point out. It uses a wrapper into Dual numbers and then on-demand compiles new versions of functions in a way that does automatic differentiation. For example, you can make an entire ODE solver be reconfigured at compile-time to be essentially two parallel ODE solvers which then computes the derivative simultaneous to the equation, with the parallel derivative part generated automatically by the compile-time controls. It’s not even that you can, it’s that it takes no more than 2 lines for a user to set it up. Actually, nobody had to write this functionality for it to exist in Julia since it’s a result of the compilation process! Let me repeat that: nobody had to implement this ability to efficiently autodifferentiate through the ODE solvers: this all happens due to Julia’s compilation process and ForwardDiff’s function overloads. The result is that packages compose nicely and efficiently.

This is that interaction of efficient data structures and difficult algorithms. The ODE solvers in DifferentialEquations.jl are generically-typed algorithms written independently by differential equation solving experts and include many optimized versions of the newest methods. ForwardDiff.jl was created by autodiff experts. The combination, fast autodifferentiation through Runge-Kutta-Chebyshev, EPIRK, Nordsieck BDF, etc. integrators is something that no one could create and optimize on their own, but it’s a combination that Julia can create and optimize! Autodifferentiation is much less work than numerical differentiation and is more accurate, so this is a good combination algorithm to have! Yes, in some cases like fully continuous ODEs you can improve upon this via forward/adjoint sensitivity analysis, but the code of this type-based compilation approach applies to ODEs/SDEs/DAEs/DDEs/SDAEs/mixed Gillespie + ODE/SDE, etc. and many of these cases the sensitivity analysis derivation has never been done and would be a bear to implement. And again, ForwardDiff.jl utilizes value-types (i.e. it’s pointer-free unlike objects) for its Dual numbers and inlines the small function calls, so it builds a very efficient AD runtime in other libraries. Separation of labor without overhead leads to huge productivity and efficiency gains!

So that’s a nice optimization which you may have missed because there’s no code to look at and see how this combination works in full. It works as the free result of compilation controls: the ODE solvers written generically and the autodifferentiation library reconfiguring algorithms using their number type overloads.

Composition of Julia codes gives new free and efficiently implemented features!

This is far from the only example. While Python has an uncertainties package with a type that calculates uncertainties, you cannot make a Cython kernel with NumPy linear algebra and throw this into SciPy ODE solvers and get an a fully optimized function call with uncertainty propagation. You would need to (A) recompile your Cython code to take into account this object (possible, but not automatic and it won’t automatically do this through all dependent packages even if they were Cython), (B) recompile the NumPy linear algebra kernels to use this object in their Fortran code (good luck), and (C) recompile the SciPy ODE solvers to utilize all of this type information internally to propagate it through all of the internal linear combinations. I have never seen cross-package type-dependent optimized auto-recompilation in Python, let alone one that crosses language barriers into the C/C++/Fortran code. But what about Julia? How much programming was needed to be done to solve this enormous problem? A user notified me in a Discourse post that it worked without having to do anything. Cool, that’s literally infinitely less work! Oh, and once again Julia’s dependent compilation process optimizes it.

I can keep going. Just see Cassette’s recent video for a bunch of compile-time optimizations and context-dependent compilation to take control of other people’s packages/code and recompile it to be distributed/gpu/etc.

Another interesting case of this is Julia’s broadcast system. You probably think of “broadcast” as synonymous with “vectorization”, but let me describe it in a very different way to highlight how it can be used to a much greater effect. Broadcast defines full expressions for “element-wise” generically using a lazy type-building system. It does this through a type promotion system plus function overloading. If a package uses broadcast for its element-wise kernels, this means that broadcast overloads allow you to essentially overdub these kernels. I describe in a fair bit of detail how this is used within the DifferentialEquations.jl architecture to allow for heterogeneous scientific models being solved on heterogeneous architecture (GPUs/multithreaded/distributed) to get specialized compilation for the DiffEq solvers. Notice though that this composition is greater than adding GPU functions to ODE solver calls. Instead, by overloading broadcast, the array type’s implementation takes control of the internal loops of the ODE solver and reconfigures/recompiles them to be OpenCL/CUDA kernels on the GPU. Every internal operation is now GPUitized, not just ones from the user passed in functions. And nothing in the DiffEq code was written for GPUs to make this work: this is all context and compilation controls.

Again, in Cython you could write an ODE solver which directly utilizes the structure of your mathematical model and puts pieces on the GPU as necessary, but this is far different than having an efficient combination built automatically from generic algorithms by the dependent compilation of different interacting parts of existing packages!

If you never thought about doing this, if you always believed you had to write code to build a software to solve your problem, then this is very Sapir-Whorf. While I will agree with you that these tools may not be what the vast majority of Julia users are using, this is the Julia that core developers of the base language and its package ecosystem are using to build the tools which are unrivaled in Python/Cython/Numba.

So what is the identity of Julia?

But Julia showcases itself as a simple language for R/Python/MATLAB users, and broadcast is described as a tool for vectorization?! Yes, this is because using any of these compilation control features is optional. I would probably say that most Julia programmers are not using it in full, and that’s fine. Even if the end user of your package doesn’t make use of all of these tools, someone else’s package can still optimize over your code if you have written Julia code. Remember, since we can dependent compile entire function calls, if all of the code is in Julia then we can do all of our powerful stuff like AD through your code even if you don’t know how these compilation controls work. This accumulation effect is in some sense like how as Google generates more data it gets more accurate predictions. In Julia, as the package ecosystem replaces ccall/PyCall packages with native Julia packages, the amount of compilation control and the available optimizations on larger scientific projects grows. So Julia does well by presenting a simple form of the language to users who want a “scripting language but faster”, i.e. it looks like it’s a Cython thing, to get everyone writing pure Julia code.

Julia is really not a simple language if you take the time to utilize all of the language’s features together. I would instead think about it like this. The FFTW package is well-known as the fastest open-source fast Fourier Transform library out there. It does this not by writing an FFT code in C, but with OCaml code which generates C code based on aspects of the problem you are trying to FFT. Because of the overwhelming success of code generation for the purpose of optimizing code, you can ask the question: what if we built a scripting language that is designed so we can do these kinds of things on a user’s scientific computing code? I don’t think it’s a surprise that the author of FFTW is now one of the core contributors of Julia. Regardless of first intentions or how Julia has been marketed, Julia has become a language of generic programming and compile-time optimization to its hardcore users and there is an entire rabbit hole of dynamic compilation control features to explore.

Conclusion

What Julia offers is different because of a full language level solution, which has its own tradeoffs that the Julia developers are working on. Julia as a language has parametric typing to make it multiple dispatch mechanism more powerful and easier to use because controlling compilation through different type structures is a way to hijack downstream/upstream code/packages in order to make the code more optimized as a whole. The language-level features like macros, generated functions, and the newer tools like Cassette.jl (which needed compiler changes to work in full) allow you to utilize the entire code and do modifications dynamically in order to take CPU code and optimize it or throw parts out to GPUs/TPUs/distributed, even if you don’t “own” the code. Dependent compilation fully eliminates the overhead that exists when different libraries are separately compiled. Broadcast overrides allow you to dictate how internal structures of scientific computing codes should be implemented and optimized on your specific model. These are all unnecessary if you could write your algorithm as a simple script, but this is necessary if you want to have packages play nicely together and automatically build optimized combinations of features. This is a whole different world than “write fast code”. Instead, you may never need to write the best features of your package, and they will still come out optimized. This is not something in the realm of Cython/Numba.

There is a tradeoff that I alluded to here and it’s compile-time. For this system to be used interactively, you have to take a step back and find out where you want to stop specializing and where to put up artificial walls. The core Julia developers have made a lot of headway just in the latest part of the Julia v0.7 release candidate in terms of latency, and this is what’s seen in the pretty new latency label on the Julialang/julia Github page. Parts like the Julia REPL can be compiled separately and more controls can be added. There’s a ton to do here. Also, getting full static compilation of libraries is just beginning to show up. Makie.jl is Julia’s next generation plotting library and it statically compiles. Again, there’s no such thing as “fully statically compiling” in Julia because everything is so extendable: you’d have to compile new functions dependent on the input types if these are “new types from packages” (example: think back to the numbers with uncertainties). This is not something that a Python plotting package would deal with (if you create a new primitive type in Cython and throw it to matplotlib it’ll give you a weird look and say “this Float64 doesn’t look right!”). Since Makie.jl is a Julia library, it has extension tie-ins via recipes which allow taking control of the internal function dispatching to change the datatype conversions, but this requires recompilation of specific internals for the new types and so mixing cached native precompilation and static compilation with this dependent compilation is an engineering challenge. But again, Julia and its packages are already making great headway into solving these issues, so I see a very bright future ahead of us.

This might not be the Julia most users are seeing, but if you want a programming language that gives you a massive rabbit hole to explore then this is just a peek at what’s available.

The post Why Numba and Cython are not substitutes for Julia appeared first on Stochastic Lifestyle.