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

Chapter 1

ch-1.livemd

Chapter 1

Mix.install([
  {:axon, "~> 0.5"},
  {:nx, "~> 0.5"},
  {:explorer, "~> 0.5"},
  {:kino, "~> 0.8"},
  {:kino_explorer, "~> 0.1"}
])

Dataset Normalization and Slicing

require Explorer.DataFrame, as: DF
iris = Explorer.Datasets.iris()
# Normalize
columns = ~w(petal_length petal_width sepal_length sepal_width)

iris =
  iris
  # Ensure input features operate on a common scale, i.e. standardize
  # `across`, `mean`, and `variance` are not imported here. They are from the `mutate` macro
  |> DF.mutate(Enum.map(across(^columns), &{&1.name, (&1 - mean(&1)) / variance(&1)}))
  # `species` is from the `mutate` macro
  # `species` is a categorical feature with no notion of scale, nothing to standardize
  |> DF.mutate(species: Explorer.Series.cast(species, :category))
  # Shuffle so we don't train or test on the same data every time
  |> DF.shuffle()

# Split dataframe into training and testing sets
# Can we use `sample` here?
# https://hexdocs.pm/explorer/Explorer.DataFrame.html#sample/3
train_df = DF.slice(iris, 0..119)
test_df = DF.slice(iris, 120..149)

Formatting

# `stack` converts it to a tensor

# Why axis -1?
# Grab the last column and turn it into a row
x_train = Nx.stack(train_df[columns], axis: -1)

# Why `iota` with these values?
# 1x3 shaped tensor
y_train =
  train_df["species"]
  |> Nx.stack(axis: -1)
  # One-hot encode the field
  |> Nx.equal(Nx.iota({1, 3}, axis: -1))

x_test = Nx.stack(test_df[columns], axis: -1)

y_test =
  test_df["species"]
  |> Nx.stack(axis: -1)
  |> Nx.equal(Nx.iota({1, 3}, axis: -1))

Model Training

# Shape represents the values of each dimension
# Create an input layer named "iris_features"
model =
  "iris_features"
  # Why this particular shape?
  # Accept a tensor  of 4 values in each row
  # `nil` because it's 1 dimensional
  |> Axon.input(shape: {nil, 4})
  # Why these values?
  # 3 neurons to encode this data
  # Dense layer because it's fully connected. Also called a linear layer.
  # Each neuron is connected to all other neurons in the next layer
  # The output is probabilities
  # Softmax is used more for classification and less for regression
  |> Axon.dense(3, activation: :softmax)
Axon.Display.as_graph(model, Nx.template({1, 4}, :f32))
  1. Grab inputs from the input pipeline
  2. Make predictions from inputs
  3. Determine how good the predictions were
  4. Update the model based on prediction goodness
  5. Repeat
# Categorical cross entropy is a loss function
# SGD stands for stochastic-gradient descent
trained_model =
  model
  |> Axon.Loop.trainer(:categorical_cross_entropy, :sgd)
  |> Axon.Loop.metric(:accuracy)
  # Can pass in the trained model as the initial state to progressively train it.
  |> Axon.Loop.run(Stream.repeatedly(fn -> {x_train, y_train} end), %{},
    iterations: 500,
    epochs: 10
  )

Evaluation

model
|> Axon.Loop.evaluator()
|> Axon.Loop.metric(:accuracy)
|> Axon.Loop.run([{x_test, y_test}], trained_model)