From 08e4b9abdd7d347fa1648d70000990b8f4727c7d Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 1 Apr 2026 09:17:36 -0500 Subject: [PATCH] =?UTF-8?q?Bigger=20network=20(128=E2=86=9264=E2=86=9232)?= =?UTF-8?q?=20and=20percentile-based=20training=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3 hidden layers instead of 2 for better feature interaction learning - Target is within-band distance percentile (0-1) instead of raw normalized distance — reduces noise from operator/equipment variation --- lib/microwaveprop/propagation/model.ex | 8 +++-- lib/mix/tasks/propagation_train.ex | 35 ++++++++++++++----- test/microwaveprop/propagation/model_test.exs | 1 + 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/microwaveprop/propagation/model.ex b/lib/microwaveprop/propagation/model.ex index 66e9c2a3..4f78a047 100644 --- a/lib/microwaveprop/propagation/model.ex +++ b/lib/microwaveprop/propagation/model.ex @@ -52,10 +52,12 @@ defmodule Microwaveprop.Propagation.Model do def build do "features" |> Axon.input(shape: {nil, @feature_count}) - |> Axon.dense(64, activation: :relu, name: "hidden_1") + |> Axon.dense(128, activation: :relu, name: "hidden_1") |> Axon.dropout(rate: 0.2, name: "dropout_1") - |> Axon.dense(32, activation: :relu, name: "hidden_2") - |> Axon.dropout(rate: 0.1, name: "dropout_2") + |> Axon.dense(64, activation: :relu, name: "hidden_2") + |> Axon.dropout(rate: 0.15, name: "dropout_2") + |> Axon.dense(32, activation: :relu, name: "hidden_3") + |> Axon.dropout(rate: 0.1, name: "dropout_3") |> Axon.dense(1, activation: :sigmoid, name: "output") end diff --git a/lib/mix/tasks/propagation_train.ex b/lib/mix/tasks/propagation_train.ex index 4b398970..892838ce 100644 --- a/lib/mix/tasks/propagation_train.ex +++ b/lib/mix/tasks/propagation_train.ex @@ -116,13 +116,13 @@ defmodule Mix.Tasks.PropagationTrain do # Evaluate on validation set val_metrics = Model.evaluate(trained_state, val_features, val_targets) IO.puts("Validation metrics:") - IO.puts(" RMSE: #{Float.round(val_metrics.rmse, 4)}") + IO.puts(" RMSE: #{Float.round(val_metrics.rmse * 100, 2)} points (on 0-100 scale)") IO.puts(" R-squared: #{Float.round(val_metrics.r_squared, 4)}\n") # Evaluate on test set test_metrics = Model.evaluate(trained_state, test_features, test_targets) IO.puts("Test metrics:") - IO.puts(" RMSE: #{Float.round(test_metrics.rmse, 4)}") + IO.puts(" RMSE: #{Float.round(test_metrics.rmse * 100, 2)} points (on 0-100 scale)") IO.puts(" R-squared: #{Float.round(test_metrics.r_squared, 4)}\n") # Save model @@ -178,10 +178,31 @@ defmodule Mix.Tasks.PropagationTrain do band_counts = Enum.frequencies_by(rows, fn row -> Enum.at(row, 0) end) - {feature_rows, target_rows} = + # Group by band and compute within-band distance percentile as target. + # This converts raw distance to "how good were conditions relative to + # what's possible on this band" — reduces noise from operator/equipment. + rows_by_band = rows |> Enum.filter(fn [band_mhz | _] -> Map.has_key?(@band_max_km, band_mhz) end) - |> Enum.map(fn [band_mhz, distance_km, utc_hour, month, lon, temp, dewpoint, pressure, grad, hpbl, pwat] -> + |> Enum.group_by(fn [band_mhz | _] -> band_mhz end) + + percentile_lookup = + Enum.flat_map(rows_by_band, fn {_band, band_rows} -> + distances = band_rows |> Enum.map(fn [_, d | _] -> d end) |> Enum.sort() + n = length(distances) + + Enum.map(band_rows, fn [_, distance_km | _] = row -> + # Rank this distance within its band (0.0 to 1.0) + rank = Enum.count(distances, &(&1 <= distance_km)) + pct = rank / n + {row, pct} + end) + end) + + {feature_rows, target_rows} = + percentile_lookup + |> Enum.map(fn {[band_mhz, _distance_km, utc_hour, month, lon, temp, dewpoint, pressure, grad, hpbl, pwat], + percentile} -> features = Model.encode_features(%{ surface_temp_c: to_float(temp), @@ -196,11 +217,7 @@ defmodule Mix.Tasks.PropagationTrain do freq_mhz: band_mhz }) - # Normalize distance by band's p99 range, cap at 1.0 - max_km = Map.fetch!(@band_max_km, band_mhz) - target = [min(distance_km / max_km, 1.0)] - - {features, target} + {features, [percentile]} end) |> Enum.unzip() diff --git a/test/microwaveprop/propagation/model_test.exs b/test/microwaveprop/propagation/model_test.exs index afbbb99c..67698f8a 100644 --- a/test/microwaveprop/propagation/model_test.exs +++ b/test/microwaveprop/propagation/model_test.exs @@ -16,6 +16,7 @@ defmodule Microwaveprop.Propagation.ModelTest do assert %Axon.ModelState{} = params assert Map.has_key?(params.data, "hidden_1") assert Map.has_key?(params.data, "hidden_2") + assert Map.has_key?(params.data, "hidden_3") assert Map.has_key?(params.data, "output") end end