Upload LSTM — test RMSE 2.9386 m R² 0.5505
Browse files- .gitattributes +4 -0
- README.md +99 -0
- fig_lstm_baseline_forecast.png +3 -0
- fig_lstm_comparison.png +3 -0
- fig_lstm_errors.png +0 -0
- fig_lstm_forecast.png +3 -0
- fig_lstm_training.png +0 -0
- fig_lstm_tuned_training.png +0 -0
- inference.py +89 -0
- lstm_model.keras +3 -0
- model_config.json +51 -0
- scaler_X.pkl +3 -0
- scaler_y.pkl +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
fig_lstm_baseline_forecast.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
fig_lstm_comparison.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
fig_lstm_forecast.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
lstm_model.keras filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
tags:
|
| 4 |
+
- time-series
|
| 5 |
+
- forecasting
|
| 6 |
+
- lstm
|
| 7 |
+
- hydrology
|
| 8 |
+
- groundwater
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# LSTM Groundwater Level Forecasting — UK
|
| 12 |
+
|
| 13 |
+
A tuned LSTM model for single-step monthly groundwater level forecasting
|
| 14 |
+
using meteorological variables as exogenous inputs.
|
| 15 |
+
|
| 16 |
+
## Model Details
|
| 17 |
+
|
| 18 |
+
| Parameter | Value |
|
| 19 |
+
|---|---|
|
| 20 |
+
| Architecture | LSTM(128) → Dropout(0.1) → Dense(32) → Dense(1) |
|
| 21 |
+
| Framework | TensorFlow / Keras |
|
| 22 |
+
| Task | Single-step monthly forecasting |
|
| 23 |
+
| Lookback window | 24 months |
|
| 24 |
+
| Input features | water_level, temperature, precipitation, wind_speed |
|
| 25 |
+
| Tuning method | Bayesian Optimisation (Keras Tuner, 20 trials) |
|
| 26 |
+
|
| 27 |
+
## Data Splits
|
| 28 |
+
|
| 29 |
+
| Split | Period | Months |
|
| 30 |
+
|---|---|---|
|
| 31 |
+
| Training | 1944-01-01 → 2007-10-01 | 766 |
|
| 32 |
+
| Validation | 2007-11-01 → 2015-10-01 | 96 |
|
| 33 |
+
| Test | 2015-11-01 → 2023-10-01 | 96 |
|
| 34 |
+
|
| 35 |
+
## Best Hyperparameters
|
| 36 |
+
|
| 37 |
+
| Parameter | Value |
|
| 38 |
+
|---|---|
|
| 39 |
+
| LSTM layers | 2 |
|
| 40 |
+
| Units | 64 |
|
| 41 |
+
| Dropout | 0.1 |
|
| 42 |
+
| Learning rate | 0.000773 |
|
| 43 |
+
| Batch size | 32 |
|
| 44 |
+
|
| 45 |
+
## Test Set Performance
|
| 46 |
+
|
| 47 |
+
| Metric | Value |
|
| 48 |
+
|---|---|
|
| 49 |
+
| RMSE | 2.9386 m |
|
| 50 |
+
| MAE | 2.397 m |
|
| 51 |
+
| MAPE | 3.671% |
|
| 52 |
+
| R² | 0.5505 |
|
| 53 |
+
| NSE | 0.5505 |
|
| 54 |
+
|
| 55 |
+
> This model is part of a benchmark study comparing SARIMAX, LSTM, and TCN
|
| 56 |
+
> for UK groundwater level forecasting.
|
| 57 |
+
|
| 58 |
+
## Important Note
|
| 59 |
+
|
| 60 |
+
Contemporaneous meteorological variables are used as inputs at forecast time
|
| 61 |
+
(oracle assumption). Future met values are treated as known — consistent with
|
| 62 |
+
the experimental setup used across all models in this study.
|
| 63 |
+
|
| 64 |
+
## Repository Contents
|
| 65 |
+
|
| 66 |
+
```
|
| 67 |
+
├── lstm_model.keras # Trained Keras model
|
| 68 |
+
├── scaler_X.pkl # Feature scaler (MinMaxScaler)
|
| 69 |
+
├── scaler_y.pkl # Target scaler (MinMaxScaler)
|
| 70 |
+
├── model_config.json # Config, hyperparameters & metrics
|
| 71 |
+
├── inference.py # Load model & generate forecasts
|
| 72 |
+
└── README.md # This file
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## Quick Start
|
| 76 |
+
|
| 77 |
+
```python
|
| 78 |
+
from huggingface_hub import hf_hub_download
|
| 79 |
+
from tensorflow.keras.models import load_model
|
| 80 |
+
import joblib, pandas as pd, numpy as np
|
| 81 |
+
|
| 82 |
+
model = load_model(hf_hub_download('kozy9/GWLSTM', 'lstm_model.keras'))
|
| 83 |
+
scaler_X = joblib.load(hf_hub_download('kozy9/GWLSTM', 'scaler_X.pkl'))
|
| 84 |
+
scaler_y = joblib.load(hf_hub_download('kozy9/GWLSTM', 'scaler_y.pkl'))
|
| 85 |
+
|
| 86 |
+
# Provide a 24-month window of features
|
| 87 |
+
X_window = pd.DataFrame({
|
| 88 |
+
'water_level' : [...], # 24 values
|
| 89 |
+
'temperature' : [...],
|
| 90 |
+
'precipitation': [...],
|
| 91 |
+
'wind_speed' : [...],
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
X_scaled = scaler_X.transform(X_window)
|
| 95 |
+
X_input = X_scaled.reshape(1, 24, 4)
|
| 96 |
+
y_scaled = model.predict(X_input)
|
| 97 |
+
pred = scaler_y.inverse_transform(y_scaled)[0][0]
|
| 98 |
+
print(f'Next month forecast: {pred:.2f} m')
|
| 99 |
+
```
|
fig_lstm_baseline_forecast.png
ADDED
|
Git LFS Details
|
fig_lstm_comparison.png
ADDED
|
Git LFS Details
|
fig_lstm_errors.png
ADDED
|
fig_lstm_forecast.png
ADDED
|
Git LFS Details
|
fig_lstm_training.png
ADDED
|
fig_lstm_tuned_training.png
ADDED
|
inference.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
inference.py — LSTM Groundwater Level Forecasting
|
| 3 |
+
==================================================
|
| 4 |
+
Usage
|
| 5 |
+
-----
|
| 6 |
+
from inference import load_model, forecast
|
| 7 |
+
model, scaler_X, scaler_y = load_model()
|
| 8 |
+
prediction = forecast(model, scaler_X, scaler_y, X_window)
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import joblib
|
| 13 |
+
import numpy as np
|
| 14 |
+
import pandas as pd
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from tensorflow.keras.models import load_model as keras_load
|
| 17 |
+
|
| 18 |
+
MODEL_PATH = Path(__file__).parent / "lstm_model.keras"
|
| 19 |
+
SCALER_X_PATH = Path(__file__).parent / "scaler_X.pkl"
|
| 20 |
+
SCALER_Y_PATH = Path(__file__).parent / "scaler_y.pkl"
|
| 21 |
+
CONFIG_PATH = Path(__file__).parent / "model_config.json"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_model():
|
| 25 |
+
"""Load the LSTM model and both scalers."""
|
| 26 |
+
model = keras_load(MODEL_PATH)
|
| 27 |
+
scaler_X = joblib.load(SCALER_X_PATH)
|
| 28 |
+
scaler_y = joblib.load(SCALER_Y_PATH)
|
| 29 |
+
print("LSTM model and scalers loaded.")
|
| 30 |
+
return model, scaler_X, scaler_y
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def load_config():
|
| 34 |
+
with open(CONFIG_PATH) as f:
|
| 35 |
+
return json.load(f)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def forecast(model, scaler_X, scaler_y, X_window: pd.DataFrame):
|
| 39 |
+
"""
|
| 40 |
+
Predict the next month's groundwater level.
|
| 41 |
+
|
| 42 |
+
Parameters
|
| 43 |
+
----------
|
| 44 |
+
model : loaded Keras model
|
| 45 |
+
scaler_X : fitted MinMaxScaler for features
|
| 46 |
+
scaler_y : fitted MinMaxScaler for target
|
| 47 |
+
X_window : DataFrame with columns [water_level, temperature,
|
| 48 |
+
precipitation, wind_speed] and exactly 24 rows
|
| 49 |
+
(the lookback window)
|
| 50 |
+
|
| 51 |
+
Returns
|
| 52 |
+
-------
|
| 53 |
+
prediction : float — forecasted water level in original units (m)
|
| 54 |
+
"""
|
| 55 |
+
cfg = load_config()
|
| 56 |
+
required = cfg['features']
|
| 57 |
+
lookback = cfg['lookback_months']
|
| 58 |
+
|
| 59 |
+
missing = [c for c in required if c not in X_window.columns]
|
| 60 |
+
if missing:
|
| 61 |
+
raise ValueError(f"X_window is missing columns: {missing}")
|
| 62 |
+
if len(X_window) != lookback:
|
| 63 |
+
raise ValueError(f"X_window must have {lookback} rows, got {len(X_window)}")
|
| 64 |
+
|
| 65 |
+
X_scaled = scaler_X.transform(X_window[required])
|
| 66 |
+
X_input = X_scaled.reshape(1, lookback, len(required))
|
| 67 |
+
|
| 68 |
+
y_scaled = model.predict(X_input, verbose=0).flatten()
|
| 69 |
+
prediction = scaler_y.inverse_transform(y_scaled.reshape(-1, 1)).flatten()[0]
|
| 70 |
+
|
| 71 |
+
return float(prediction)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
model, scaler_X, scaler_y = load_model()
|
| 76 |
+
cfg = load_config()
|
| 77 |
+
print(f"Model: {cfg['architecture']}")
|
| 78 |
+
print(f"Test RMSE: {cfg['test_metrics']['RMSE']} m")
|
| 79 |
+
|
| 80 |
+
# Dummy window — replace with real data
|
| 81 |
+
import numpy as np
|
| 82 |
+
dummy = pd.DataFrame({
|
| 83 |
+
'water_level' : np.random.uniform(60, 75, 24),
|
| 84 |
+
'temperature' : np.random.uniform(3, 15, 24),
|
| 85 |
+
'precipitation': np.random.uniform(20, 120, 24),
|
| 86 |
+
'wind_speed' : np.random.uniform(10, 25, 24),
|
| 87 |
+
})
|
| 88 |
+
pred = forecast(model, scaler_X, scaler_y, dummy)
|
| 89 |
+
print(f"\nForecast (next month): {pred:.4f} m")
|
lstm_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c19fbb8e942ce8360eb95361163ed979b76f071792ab1d726c05de62a35c25d0
|
| 3 |
+
size 675521
|
model_config.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "LSTM",
|
| 3 |
+
"architecture": "LSTM(128) \u2192 Dropout(0.1) \u2192 Dense(32) \u2192 Dense(1)",
|
| 4 |
+
"framework": "TensorFlow/Keras",
|
| 5 |
+
"task": "Single-step monthly groundwater level forecasting",
|
| 6 |
+
"features": [
|
| 7 |
+
"water_level",
|
| 8 |
+
"temperature",
|
| 9 |
+
"precipitation",
|
| 10 |
+
"wind_speed"
|
| 11 |
+
],
|
| 12 |
+
"target": "water_level",
|
| 13 |
+
"lookback_months": 24,
|
| 14 |
+
"horizon_months": 1,
|
| 15 |
+
"tuning": {
|
| 16 |
+
"method": "Bayesian Optimisation (Keras Tuner)",
|
| 17 |
+
"n_trials": 20,
|
| 18 |
+
"best_config": {
|
| 19 |
+
"n_layers": 2,
|
| 20 |
+
"units_1": 64,
|
| 21 |
+
"dropout": 0.1,
|
| 22 |
+
"lr": 0.0007731576839806804,
|
| 23 |
+
"batch_size": 32
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
"data_splits": {
|
| 27 |
+
"train": {
|
| 28 |
+
"start": "1944-01-01",
|
| 29 |
+
"end": "2007-10-01",
|
| 30 |
+
"n_months": 766
|
| 31 |
+
},
|
| 32 |
+
"validation": {
|
| 33 |
+
"start": "2007-11-01",
|
| 34 |
+
"end": "2015-10-01",
|
| 35 |
+
"n_months": 96
|
| 36 |
+
},
|
| 37 |
+
"test": {
|
| 38 |
+
"start": "2015-11-01",
|
| 39 |
+
"end": "2023-10-01",
|
| 40 |
+
"n_months": 96
|
| 41 |
+
}
|
| 42 |
+
},
|
| 43 |
+
"test_metrics": {
|
| 44 |
+
"RMSE": 2.9386,
|
| 45 |
+
"MAE": 2.397,
|
| 46 |
+
"MAPE_pct": 3.671,
|
| 47 |
+
"R2": 0.5505,
|
| 48 |
+
"NSE": 0.5505
|
| 49 |
+
},
|
| 50 |
+
"notes": "Scaler fitted on train only. Oracle exog assumption \u2014 contemporaneous met vars used at forecast time."
|
| 51 |
+
}
|
scaler_X.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3c0d11dcc6e97c52246a4bc0a08b926e75edaa58a268400446e4eafdd81c199a
|
| 3 |
+
size 1127
|
scaler_y.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:865d8113514357d4b75338166ebba5771c914f7cd10b5892fc2187265ae4515c
|
| 3 |
+
size 975
|