Chapter 4: Hyperspace!
Mix.install(
[
{:exla, "~> 0.5"},
{:nx, "~> 0.5"},
{:vega_lite, "~> 0.1.6"},
{:kino, "~> 0.8.1"},
{:kino_vega_lite, "~> 0.1.7"}
],
config: [nx: [default_backend: EXLA.Backend]]
)
Upgrading the Learner
Preparing Data
file =
__DIR__
|> Path.join("pizza_3_vars.txt")
|> Path.expand()
# Read the data from the file, remove the header and return
# `[%{reservations: integer(), temperature: integer(), tourists: integer(), pizzas: integer()}]`
data =
File.read!(file)
|> String.split("\n", trim: true)
|> Enum.slice(1..-1)
|> Enum.map(&String.split(&1, ~r{\s+}, trim: true))
|> Enum.map(fn [r, temp, tour, p] ->
%{
reservations: String.to_integer(r),
temperature: String.to_integer(temp),
tourists: String.to_integer(tour),
pizzas: String.to_integer(p)
}
end)
Kino.DataTable.new(data, keys: [:reservations, :temperature, :tourists, :pizzas])
# Transform the data to unpack the 4 columns `reservations`,
# `temperature`, `tourists` and `pizzas` into separate arrays
# called x1, x2, x3 and y
%{x1: x1, x2: x2, x3: x3, y: y} =
Enum.reduce(data, %{x1: [], x2: [], x3: [], y: []}, fn item, %{x1: x1, x2: x2, x3: x3, y: y} ->
%{
x1: x1 ++ [item.reservations],
x2: x2 ++ [item.temperature],
x3: x3 ++ [item.tourists],
y: y ++ [item.pizzas]
}
end)
Let’s build the matrix x for input variables
# Same of `numpy.column_stack((x1, x2, x3))` used in the book
x =
[x1, x2, x3]
|> Nx.tensor()
|> Nx.transpose()
# Inspect x shape
x.shape()
# Get the first 2 rows of x
x[0..1]
And reshape y into a matrix for labels
# Same of `y.reshape(-1, 1)` used in the book
y = Nx.tensor([y]) |> Nx.transpose()
# Inspect y shape
y.shape()
Multiple Linear Regression
defmodule C4.MultipleLinearRegression do
import Nx.Defn
@doc """
Return the prediction tensor given the inputs and weight.
"""
defn(predict(x, weight), do: Nx.dot(x, weight))
@doc """
Returns the mean squared error.
"""
defn loss(x, y, weight) do
predictions = predict(x, weight)
errors = Nx.subtract(predictions, y)
squared_error = Nx.pow(errors, 2)
Nx.mean(squared_error)
end
@doc """
Returns the derivative of the loss curve.
"""
defn gradient(x, y, weight) do
# in python:
# 2 * np.matmul(X.T, (predict(X, w) - Y)) / X.shape[0]
predictions = predict(x, weight)
errors = Nx.subtract(predictions, y)
n_examples = elem(Nx.shape(x), 0)
Nx.transpose(x)
|> Nx.dot(errors)
|> Nx.multiply(2)
|> Nx.divide(n_examples)
end
@doc """
Computes the weight by training the system
with the given inputs and labels, by iterating
over the examples the specified number of times.
"""
def train(x, y, iterations, lr) do
Enum.reduce(0..(iterations - 1), init_weight(x), fn i, weight ->
current_loss = loss(x, y, weight) |> Nx.to_number()
IO.puts("Iteration #{i} => Loss: #{current_loss}")
Nx.subtract(weight, Nx.multiply(gradient(x, y, weight), lr))
end)
end
# Given n elements it returns a tensor
# with this shape {n, 1}, each element
# initialized to 0
defnp init_weight(x) do
Nx.broadcast(Nx.tensor([0]), {elem(Nx.shape(x), 1), 1})
end
end
Train the system
iterations = Kino.Input.number("iterations", default: 10_000)
lr = Kino.Input.number("lr (learning rate)", default: 0.001)
iterations = Kino.Input.read(iterations)
lr = Kino.Input.read(lr)
weight = C4.MultipleLinearRegression.train(x, y, iterations, lr)
loss = C4.MultipleLinearRegression.loss(x, y, weight) |> Nx.to_number()
Bye bye, bias 👋
Quoting the book:
> The bias is just the weight of an input variable that happens to have the constant value 1.
Basically, this expression:
ŷ = x1 * w1 + x2 * w2 + x3 * w3 + b
can be rewritten as:
ŷ = x1 * w1 + x2 * w2 + x3 * w3 + x0 * b
where x0
is a constant matrix of value 1 with {30, 1}
shape.
x0 = List.duplicate(1, length(x1))
And now let’s add the new input x0
to the x
tensor.
x =
[x0, x1, x2, x3]
|> Nx.tensor()
|> Nx.transpose()
weight = C4.MultipleLinearRegression.train(x, y, iterations, lr)
loss = C4.MultipleLinearRegression.loss(x, y, weight)
A few predictions
Enum.map(0..4, fn i ->
prediction =
x[i]
|> C4.MultipleLinearRegression.predict(weight)
|> Nx.squeeze()
|> Nx.to_number()
|> Float.round(4)
label =
y[i]
|> Nx.squeeze()
|> Nx.to_number()
IO.inspect("x[#{i}] -> #{prediction} (label: #{label})")
end)
Kino.nothing()