Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Chapter 4: Hyperspace!

04_hyperspace/multiple_regression.livemd

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()