How to train a demand forecast model in Keras

Demand forecasting models turn recent sales, orders, traffic, or usage history into short-term quantity estimates. In Keras, that usually means building time-ordered feature windows, training on earlier periods, validating on later periods, and saving a model artifact that can be reused by a forecasting job.

A small LSTM regression model is enough to prove the end-to-end pattern before real inventory or demand data is connected. The sample uses daily demand, promotion flags, and weekly seasonality features so the model sees both recent demand levels and calendar rhythm.

Keep the split chronological because demand rows are not independent shuffled examples. Normalization uses only the training period, validation uses the next block of days, and the holdout forecast table compares predicted demand against later rows that the model did not train on.

Steps to train a Keras demand forecast model:

  1. Install Keras, TensorFlow, and NumPy in the project environment.
    $ python -m pip install keras tensorflow numpy

    Use an isolated virtual environment for project dependencies. Set KERAS_BACKEND before import keras when using standalone Keras.
    Related: How to install Keras with pip

  2. Create train_demand_forecast.py with a synthetic demand series, a chronological split, and a small forecasting model.
    train_demand_forecast.py
    import os
     
    os.environ["KERAS_BACKEND"] = "tensorflow"
    os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
     
    from pathlib import Path
     
    import keras
    import numpy as np
    from keras import layers
     
    LOOKBACK = 28
    TRAIN_END = 190
    VALIDATION_END = 230
    MODEL_PATH = Path("demand_forecast.keras")
     
    keras.utils.set_random_seed(7)
     
    rng = np.random.default_rng(42)
    days = np.arange(260, dtype="float32")
    weekly = 12.0 * np.sin(2.0 * np.pi * days / 7.0)
    annual = 8.0 * np.sin(2.0 * np.pi * days / 365.0)
    trend = 0.08 * days
    promotion = ((days % 31) < 4).astype("float32")
    noise = rng.normal(0.0, 2.0, size=days.shape[0]).astype("float32")
    demand = 120.0 + trend + weekly + annual + (18.0 * promotion) + noise
     
    train_mean = demand[:TRAIN_END].mean()
    train_std = demand[:TRAIN_END].std()
    demand_scaled = (demand - train_mean) / train_std
     
    features = np.column_stack(
        [
            demand_scaled,
            promotion,
            np.sin(2.0 * np.pi * days / 7.0),
            np.cos(2.0 * np.pi * days / 7.0),
        ]
    ).astype("float32")
     
     
    def make_windows(feature_rows, target_values, lookback):
        windows = []
        targets = []
        target_days = []
        for start in range(len(feature_rows) - lookback):
            target_day = start + lookback
            windows.append(feature_rows[start:target_day])
            targets.append(target_values[target_day])
            target_days.append(target_day)
        return (
            np.asarray(windows, dtype="float32"),
            np.asarray(targets, dtype="float32"),
            np.asarray(target_days, dtype="int32"),
        )
     
     
    x_all, y_all, target_days = make_windows(features, demand_scaled, LOOKBACK)
    train_mask = target_days < TRAIN_END
    validation_mask = (target_days >= TRAIN_END) & (target_days < VALIDATION_END)
    holdout_mask = target_days >= VALIDATION_END
     
    x_train, y_train = x_all[train_mask], y_all[train_mask]
    x_val, y_val = x_all[validation_mask], y_all[validation_mask]
    x_holdout, y_holdout = x_all[holdout_mask], y_all[holdout_mask]
    holdout_days = target_days[holdout_mask]
     
    model = keras.Sequential(
        [
            layers.Input(shape=(LOOKBACK, features.shape[1])),
            layers.LSTM(24),
            layers.Dense(12, activation="relu"),
            layers.Dense(1),
        ]
    )
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate=0.01),
        loss="mse",
        metrics=[keras.metrics.MeanAbsoluteError(name="mae")],
    )
     
    history = model.fit(
        x_train,
        y_train,
        validation_data=(x_val, y_val),
        epochs=8,
        batch_size=32,
        shuffle=False,
        verbose=0,
    )
    metrics = model.evaluate(x_val, y_val, verbose=0, return_dict=True)
    forecast_scaled = model.predict(x_holdout[:7], verbose=0).reshape(-1)
    forecast = (forecast_scaled * train_std) + train_mean
    actual = (y_holdout[:7] * train_std) + train_mean
     
    model.save(MODEL_PATH)
    reloaded = keras.saving.load_model(MODEL_PATH)
    reloaded_forecast = reloaded.predict(x_holdout[:1], verbose=0).reshape(-1)[0]
    reload_delta = abs(reloaded_forecast - forecast_scaled[0])
     
    print(f"backend: {keras.backend.backend()}")
    print(f"train windows: {x_train.shape[0]}")
    print(f"validation windows: {x_val.shape[0]}")
    print(f"history keys: {', '.join(sorted(history.history.keys()))}")
    print(f"validation mae: {metrics['mae'] * train_std:.2f} units")
    print("holdout forecast:")
    print("day  predicted  actual")
    for day, predicted, observed in zip(holdout_days[:7], forecast, actual):
        print(f"{int(day):3d}  {predicted:9.1f}  {observed:6.1f}")
    print(f"saved model: {MODEL_PATH}")
    print(f"reload delta: {reload_delta:.6f}")

    Replace the synthetic demand, promotion, and calendar rows with real demand features after the sample run works. Keep the target day after each lookback window and compute scaling values from the training period only.

  3. Run the training script.
    $ python train_demand_forecast.py
    backend: tensorflow
    train windows: 162
    validation windows: 40
    history keys: loss, mae, val_loss, val_mae
    validation mae: 3.04 units
    holdout forecast:
    day  predicted  actual
    230      122.0   120.3
    231      129.5   131.4
    232      141.4   142.6
    233      145.9   146.6
    234      139.1   136.3
    235      126.8   126.0
    236      120.2   116.5
    saved model: demand_forecast.keras
    reload delta: 0.000000

    The validation and holdout rows come after the training rows. If validation error looks much better than the holdout comparison, check for feature leakage or a split that lets future demand influence training.

  4. Confirm that the saved model artifact exists.
    $ python -c 'from pathlib import Path; path = Path("demand_forecast.keras"); print(path.name, "exists:", path.is_file())'
    demand_forecast.keras exists: True

    The .keras file stores the model configuration, weights, and optimizer state for later loading.
    Related: How to save and load a Keras model