Time Series Forecasting
TF-Keras
Joblib
Keras
fumd_migration_forecaster
vehicular-networks
cellular-handover
mobility-prediction
5g
v2x
lstm
bilstm
attention
bahdanau-attention
tensorflow
sumo
omnetpp
simu5g
simulation
Eval Results (legacy)
Instructions to use fumd-ai/handover-forecaster with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- TF-Keras
How to use fumd-ai/handover-forecaster with TF-Keras:
# Note: 'keras<3.x' or 'tf_keras' must be installed (legacy), and from_pretrained_keras was removed in huggingface_hub 1.0. # See https://github.com/keras-team/tf-keras for more details. # !pip install "huggingface_hub<1.0" tf_keras from huggingface_hub import from_pretrained_keras model = from_pretrained_keras("fumd-ai/handover-forecaster") - Keras
How to use fumd-ai/handover-forecaster with Keras:
# !pip install -U keras tensorflow huggingface_hub # Keras needs TensorFlow installed to read "hf://" paths, so the tensorflow backend is selected here; # "jax" and "torch" also work for computation once TensorFlow is installed. import os os.environ["KERAS_BACKEND"] = "tensorflow" import keras model = keras.saving.load_model("hf://fumd-ai/handover-forecaster") - Notebooks
- Google Colab
- Kaggle
File size: 7,889 Bytes
b020cf2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | """
=============================================================================
FUMD-AI Handover Forecaster -- self-contained preprocessing
=============================================================================
Everything needed to turn a `dataset_labeled_w<W>.csv` (the AI-ready output of
the FUMD-AI preprocessing workflow's Step 6) into the exact `(n, 6, 23)` input
this model was trained on -- with no dependency on the training package, on
`joblib`, or on a specific scikit-learn version.
All fitted parameters live in `preprocessing.json`, a plain-JSON export of the
`scaler.joblib` / `label_encoders.joblib` shipped alongside it.
Author(s):
- Cristina Bernad (ORCID: 0000-0001-9537-415X)
- Sonja Filiposka <sonja.filiposka@finki.ukim.mk> (ORCID: 0000-0003-0034-2855)
- Katja Gilly (ORCID: 0000-0002-8985-0639)
Funding: This work has been funded by the FUMD-AI project, an EOSC GRAVITY -
Inter Project with Grant Number 25-EOSC-GRV-INTER-013.
SPDX-License-Identifier: MIT
"""
from __future__ import annotations
import json
import os
import numpy as np
import pandas as pd
HERE = os.path.dirname(os.path.abspath(__file__))
TIME_COLUMN = "t"
GROUP_COLUMN = "veh_id"
TARGET_COLUMN = "servingCell"
CATEGORICAL_COLUMNS = ["lane"]
LABEL_ONLY_COLUMNS = ["migration", "destination"]
SEQUENCE_LENGTH = 6
FUTURE_STEPS = [1, 2, 3, 4, 5, 6, 7]
NUM_BASE_STATIONS = 10
#: The 23 input features, in the order the scaler and the encoder LSTM expect.
FEATURE_COLUMNS = [
"angle", "speed", "pos", "lane", "signals",
"averageCqiDl", "distance", "measuredSinrDl", "measuredSinrUl", "rcvdSinrDl",
"rlcDelayDl", "rlcPduDelayDl", "rlcThroughputDl",
"servingCell",
"servingCell-1", "servingCell-2", "servingCell-3", "servingCell-4",
"servingCell-5", "servingCell-6", "servingCell-7",
"x", "y",
]
# ---------------------------------------------------------------------------
# Fitted parameters
# ---------------------------------------------------------------------------
class Preprocessor:
"""
Applies the model's fitted lane encoding and standardisation.
Transform-only by design: there is deliberately no `fit`. Re-fitting either
the encoder or the scaler on new data would make its outputs incomparable
with what the model was trained on (different lane->int codes, different
z-score statistics) while still appearing to work.
"""
def __init__(self, params: dict):
scaler = params["scaler"]
self.mean = np.asarray(scaler["mean"], dtype="float64")
self.scale = np.asarray(scaler["scale"], dtype="float64")
self.n_features = int(scaler["n_features_in"])
if len(self.mean) != self.n_features or len(self.scale) != self.n_features:
raise ValueError("preprocessing.json: scaler mean/scale length disagrees with n_features_in")
self.encoders = {}
for col, enc in params["label_encoders"].items():
classes = [str(c) for c in enc["classes"]]
self.encoders[col] = {
"index": {c: i for i, c in enumerate(classes)},
"unknown_code": int(enc["unknown_code"]),
"n_classes": len(classes),
}
@classmethod
def load(cls, path: str | None = None) -> "Preprocessor":
path = path or os.path.join(HERE, "preprocessing.json")
with open(path) as fh:
return cls(json.load(fh))
# -- categorical ------------------------------------------------------
def encode_categoricals(self, df: pd.DataFrame) -> pd.DataFrame:
"""Map each categorical column's string values to their fitted codes.
Values never seen at fit time map to that column's reserved unknown
code instead of raising -- a road segment the training traffic simply
never drove down is a legitimate input, not an error.
"""
out = df.copy()
for col, enc in self.encoders.items():
if col not in out.columns:
raise KeyError(f"expected categorical column {col!r} in the dataframe")
codes = out[col].astype(str).map(enc["index"])
n_unknown = int(codes.isna().sum())
if n_unknown:
print(f"encode_categoricals: {col}: {n_unknown} value(s) unseen in training "
f"-> unknown code {enc['unknown_code']}")
out[col] = codes.fillna(enc["unknown_code"]).astype(int)
return out
# -- scaling ----------------------------------------------------------
def scale_windows(self, X: np.ndarray) -> np.ndarray:
"""Z-score an (n, seq_len, n_features) array with the fitted statistics."""
X = np.asarray(X, dtype="float64")
if X.ndim != 3 or X.shape[-1] != self.n_features:
raise ValueError(f"expected (n, seq_len, {self.n_features}), got {X.shape}")
flat = X.reshape(-1, self.n_features)
flat = (flat - self.mean) / self.scale
return flat.reshape(X.shape).astype("float32")
# ---------------------------------------------------------------------------
# Windowing
# ---------------------------------------------------------------------------
def build_sequences(
df: pd.DataFrame,
feature_columns: list[str] = FEATURE_COLUMNS,
target_column: str = TARGET_COLUMN,
sequence_length: int = SEQUENCE_LENGTH,
future_steps: list[int] = FUTURE_STEPS,
group_col: str = GROUP_COLUMN,
time_col: str = TIME_COLUMN,
label_columns: list[str] = LABEL_ONLY_COLUMNS,
):
"""
Build per-vehicle sliding windows, identical to how the model was trained.
Returns
-------
X : (n_windows, sequence_length, n_features) -- unscaled
y : (n_windows, len(future_steps)) -- true serving cell per horizon
extra : {label_col: (n_windows,)} -- labels at the first forecast
step, for evaluation only; never model input.
Windows never cross a vehicle boundary, and a vehicle with fewer than
`sequence_length + max(future_steps)` rows contributes none.
"""
future_steps = list(future_steps)
needed = sequence_length + max(future_steps)
X_list, y_list = [], []
extra_lists = {c: [] for c in label_columns if c in df.columns}
n_skipped = 0
has_target = target_column in df.columns
for _, group in df.groupby(group_col, sort=False):
group = group.sort_values(time_col).reset_index(drop=True)
n = len(group)
if n < needed:
n_skipped += 1
continue
feats = group[feature_columns].to_numpy(dtype=float)
target = group[target_column].to_numpy() if has_target else None
label_arrays = {c: group[c].to_numpy() for c in extra_lists}
for i in range(n - needed):
X_list.append(feats[i:i + sequence_length])
if has_target:
y_list.append([target[i + sequence_length + s] for s in future_steps])
for c, arr in label_arrays.items():
extra_lists[c].append(arr[i + sequence_length + future_steps[0]])
if n_skipped:
print(f"build_sequences: skipped {n_skipped} vehicle(s) with < {needed} rows")
X = np.asarray(X_list, dtype=float)
y = np.asarray(y_list, dtype=float) if has_target else None
extra = {c: np.asarray(v) for c, v in extra_lists.items()}
print(f"build_sequences: {X.shape[0]} windows, X {X.shape}")
return X, y, extra
def prepare(
df: pd.DataFrame,
preprocessor: Preprocessor | None = None,
**kwargs,
):
"""Encode -> window -> scale, in one call. Returns `(X_scaled, y, extra)`."""
preprocessor = preprocessor or Preprocessor.load()
df = df.sort_values([GROUP_COLUMN, TIME_COLUMN]).reset_index(drop=True)
df = preprocessor.encode_categoricals(df)
X, y, extra = build_sequences(df, **kwargs)
return preprocessor.scale_windows(X), y, extra
|