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

Chapter 2: Your First Learning Program

02_first/02_first.livemd

Chapter 2: Your First Learning Program

Mix.install([
  {:nx, "~> 0.5.3"},
  {:kino_vega_lite, "~> 0.1.7"}
])

Import whitespace-separated data from text

The file contains 30 lines of data. Each is an example, composed of an input variable (the reservations) and a numerical label (the pizzas).

path = __DIR__ |> Path.join("files/pizza.txt") |> Path.expand()

{x, y} =
  path
  |> File.stream!()
  |> Stream.map(&String.split/1)
  |> Stream.map(&List.to_tuple/1)
  |> Enum.into([])
  # Drop header
  |> List.delete_at(0)
  |> Enum.unzip()

x = Enum.map(x, &String.to_integer/1)
y = Enum.map(y, &String.to_integer/1)

Plotting Reservations vs Pizzas

reservations = %{reservations: x, pizzas: y}
VegaLite.new(width: 400, height: 400)
|> VegaLite.data_from_values(reservations, only: ["reservations", "pizzas"])
|> VegaLite.mark(:point)
|> VegaLite.encode_field(:x, "reservations", type: :quantitative)
|> VegaLite.encode_field(:y, "pizzas", type: :quantitative)

Defining the model

Here is the mathematical equation of a line that passes by the origin of the axes:
y_hat = x * w

where:

  • y_hat is the forecast
  • x is the reservation
  • w is the slope or weight

Linear Regression

defmodule C2.LinearRegression do
  import Nx.Defn

  defn predict(x, w) do
    x * w
  end

  @doc """
  Average the squared errors (losses) of all the examples
  """
  defn loss(x, y, w) do
    x
    |> predict(w)
    |> Nx.subtract(y)
    |> Nx.pow(2)
    |> Nx.mean()
  end

  @doc """
  This function estimates the weight through a loop that adds/subtract lr (learning rate)
  It iterates until it gets the smallest error possible.
  """
  def train(x, y, iterations, lr) do
    w = 0

    Enum.reduce_while(0..iterations, w, fn i, w ->
      current_loss = loss(x, y, w) |> Nx.to_number()
      IO.puts("Iteration #{i} => Loss: #{current_loss}")

      cond do
        loss(x, y, w + lr) |> Nx.to_number() < current_loss -> {:cont, w + lr}
        loss(x, y, w - lr) |> Nx.to_number() < current_loss -> {:cont, w - lr}
        true -> {:halt, w}
      end
    end)
  end
end

Train the system

x = Nx.tensor(x)
y = Nx.tensor(y)
w = C2.LinearRegression.train(x, y, 10_000, 0.01)

Predict the number of pizzas

y_hat = C2.LinearRegression.predict(20, w)
predictions = %{reservations: [0, 20], pizzas: [0, Nx.to_number(y_hat)]}
IO.puts("Prediction: x=#{20} => y=#{Nx.to_number(y_hat)}")
VegaLite.new()
|> VegaLite.layers([
  VegaLite.new()
  |> VegaLite.data_from_values(reservations, only: ["reservations", "pizzas"])
  |> VegaLite.mark(:point)
  |> VegaLite.encode_field(:x, "reservations", type: :quantitative)
  |> VegaLite.encode_field(:y, "pizzas", type: :quantitative),
  VegaLite.new()
  |> VegaLite.data_from_values(predictions, only: ["reservations", "pizzas"])
  |> VegaLite.mark(:line)
  |> VegaLite.encode_field(:x, "reservations", type: :quantitative)
  |> VegaLite.encode_field(:y, "pizzas", type: :quantitative)
])

Adding a bias

y = mx + b
bias is the parameter that shifts the line up or down the chart

defmodule C2.LinearRegressionWithBias do
  import Nx.Defn

  defn predict(x, w, b) do
    x * w + b
  end

  def loss(x, y, w, b) do
    x
    |> predict(w, b)
    |> Nx.subtract(y)
    |> Nx.pow(2)
    |> Nx.mean()
  end

  def train(x, y, iterations, lr) do
    w = b = 0

    Enum.reduce_while(0..iterations, {w, b}, fn i, {w, b} ->
      current_loss = loss(x, y, w, b) |> Nx.to_number()
      IO.puts("Iteration #{i} => Loss: #{current_loss}")

      cond do
        loss(x, y, w + lr, b) |> Nx.to_number() < current_loss -> {:cont, {w + lr, b}}
        loss(x, y, w - lr, b) |> Nx.to_number() < current_loss -> {:cont, {w - lr, b}}
        loss(x, y, w, b + lr) |> Nx.to_number() < current_loss -> {:cont, {w, b + lr}}
        loss(x, y, w, b - lr) |> Nx.to_number() < current_loss -> {:cont, {w, b - lr}}
        true -> {:halt, {w, b}}
      end
    end)
  end
end

Training the system with bias

{w, b} = C2.LinearRegressionWithBias.train(x, y, 10_000, 0.01)
IO.puts("w=#{w}, b=#{b}")

Predict the number of pizzas with bias

y_hat = C2.LinearRegressionWithBias.predict(20, w, b)
predictions = %{reservations: [0, 20], pizzas: [b, Nx.to_number(y_hat)]}
IO.puts("Prediction: x=#{20} => y=#{Nx.to_number(y_hat)}")
VegaLite.new()
|> VegaLite.layers([
  VegaLite.new()
  |> VegaLite.data_from_values(reservations, only: ["reservations", "pizzas"])
  |> VegaLite.mark(:point)
  |> VegaLite.encode_field(:x, "reservations", type: :quantitative)
  |> VegaLite.encode_field(:y, "pizzas", type: :quantitative),
  VegaLite.new()
  |> VegaLite.data_from_values(predictions, only: ["reservations", "pizzas"])
  |> VegaLite.mark(:line)
  |> VegaLite.encode_field(:x, "reservations", type: :quantitative)
  |> VegaLite.encode_field(:y, "pizzas", type: :quantitative)
])

Hyperparameters

Hyperparameters means high level parameters. These are parameters used to train a model. While parameters are set of values returned by the training function that we can use in the model, i.e. in this discussion w and b are parameters while iterations and lr are hyperparameters