# Linear Regression with ExBurn # Run with: mix run examples/linear_regression.exs # # Demonstrates the simplest possible ML workflow: # 1. Generate synthetic data: y = 2x + 1 + noise # 2. Define a single-neuron model (linear regression) # 3. Train using SGD # 4. Verify the learned weights approximate w=2.0, b=1.0 Mix.install([ {:nx, "~> 0.7"}, {:axon, "~> 0.7"}, {:ex_burn, path: Path.expand("..", __DIR__)} ]) defmodule LinearRegression do @moduledoc """ Simple linear regression: y = wx + b Learns w ≈ 2.0, b ≈ 1.0 from noisy data. """ def run do IO.puts("=== Linear Regression with ExBurn ===\n") # ── 1. Generate synthetic data ────────────────────────────── num_samples = 200 key = Nx.Random.key(42) # x ~ Uniform(-3, 3) {x, key} = Nx.Random.uniform(key, -3.0, 3.0, shape: {num_samples, 1}) # y = 2x + 1 + noise(σ=0.1) {noise, _key} = Nx.Random.normal(key, 0.0, 0.1, shape: {num_samples, 1}) y = Nx.add(Nx.multiply(x, 2.0), Nx.add(1.0, noise)) IO.puts("Data: #{num_samples} samples, y = 2x + 1 + ε") IO.puts(" x range: [#{Nx.reduce_min(x) |> Nx.to_number() |> Float.round(2)}, #{Nx.reduce_max(x) |> Nx.to_number() |> Float.round(2)}]") IO.puts(" y range: [#{Nx.reduce_min(y) |> Nx.to_number() |> Float.round(2)}, #{Nx.reduce_max(y) |> Nx.to_number() |> Float.round(2)}]") # ── 2. Define model ───────────────────────────────────────── # Single dense layer with no activation = linear regression model = Axon.input("input", shape: {nil, 1}) |> Axon.dense(1, use_bias: true, name: "linear") IO.puts("\nModel:") IO.puts(Axon.Display.display(model, [])) # ── 3. Compile ────────────────────────────────────────────── compiled = ExBurn.Model.compile(model, loss: :mse, optimizer: :sgd, learning_rate: 0.05 ) IO.puts("Initial prediction (x=1.0):") pred0 = predict(compiled, Nx.tensor([[1.0]])) IO.puts(" f(1.0) = #{Float.round(Nx.to_number(pred0), 4)} (expected ≈ 3.0)\n") # ── 4. Train ──────────────────────────────────────────────── IO.puts("Training...") trained = ExBurn.Training.fit(compiled, {x, y}, epochs: 100, batch_size: 32, verbose: false, callbacks: [ fn %{epoch: epoch, loss: loss} when rem(epoch, 20) == 0 -> IO.puts(" Epoch #{String.pad_leading("#{epoch}", 3)}: loss = #{Float.round(loss, 6)}") %{epoch: epoch, loss: loss} metrics -> metrics end ] ) # ── 5. Evaluate ───────────────────────────────────────────── IO.puts("\nResults:") pred_final = predict(trained, Nx.tensor([[1.0]])) IO.puts(" f(1.0) = #{Float.round(Nx.to_number(pred_final), 4)} (expected ≈ 3.0)") pred_zero = predict(trained, Nx.tensor([[0.0]])) IO.puts(" f(0.0) = #{Float.round(Nx.to_number(pred_zero), 4)} (expected ≈ 1.0)") pred_neg = predict(trained, Nx.tensor([[-2.0]])) IO.puts(" f(-2.0) = #{Float.round(Nx.to_number(pred_neg), 4)} (expected ≈ -3.0)") # Compute final MSE y_pred = predict_batch(trained, x) mse = Nx.mean(Nx.power(Nx.subtract(y_pred, y), 2.0)) |> Nx.to_number() IO.puts("\n Final MSE: #{Float.round(mse, 6)}") # Extract learned weights params = ExBurn.Model.parameters(trained) {w, b} = extract_linear_params(params) IO.puts(" Learned: w = #{Float.round(w, 4)}, b = #{Float.round(b, 4)}") IO.puts(" Target: w = 2.0000, b = 1.0000") IO.puts("\n=== Done ===") end defp predict(%ExBurn.Model{params: params}, input) do # Forward pass: y = x @ w + b w = params["linear"]["weight"] b = params["linear"]["bias"] Nx.add(Nx.dot(input, w), b) end defp predict_batch(%ExBurn.Model{params: params}, input) do w = params["linear"]["weight"] b = params["linear"]["bias"] Nx.add(Nx.dot(input, Nx.transpose(w)), b) end defp extract_linear_params(params) do w = params["linear"]["weight"] |> Nx.to_number() b = params["linear"]["bias"] |> Nx.to_number() {w, b} end end LinearRegression.run()