Bigger network (128→64→32) and percentile-based training target

- 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
This commit is contained in:
Graham McIntire 2026-04-01 09:17:36 -05:00
parent 10cbbf47b3
commit 08e4b9abdd
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 32 additions and 12 deletions

View file

@ -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

View file

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

View file

@ -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