Posts文章
Modelling模型建構5 June 2026 · 18 min read2026年6月5日 · 閱讀約 29 分鐘

Demand Forecasting with Deep Learning: Does an LSTM Earn Its Complexity?用深度學習做需求預測:LSTM 值得它的複雜度嗎?

A practical comparison of a global LSTM with XGBoost across 120 store–SKU series, asking whether learned temporal representations justify the added complexity.一項針對 120 條門市-SKU 序列的全域 LSTM 與 XGBoost 實務比較,探討學習到的時間表徵,是否值得額外的複雜度。

Demand Forecasting with Deep Learning: Does an LSTM Earn Its Complexity?

This article continues the case study introduced in:

Statistical models provided a strong baseline for one store–SKU, while a global XGBoost scaled forecasting to 120 series but gained little from additional business context. We now test whether a global LSTM can learn directly from ordered demand sequences and improve enough to justify its complexity.

This article is structured as follows:

  1. Why move beyond engineered features?
  2. From engineered rows to ordered sequences
  3. How an LSTM creates memory
  4. Architecture Design
  5. Training choices
  6. Does the improvement justify the complexity?
  7. Operational recommendation
  8. Conclusion: complexity must earn its cost

Why move beyond engineered features?

Tabular supervised-learning models such as XGBoost do not receive demand history as an ordered sequence. We must first convert that history into features such as lag_7, lag_28, rolling_mean_7, and rolling_std_28. These features give the model a form of memory, but they also determine what information that memory contains.

This creates several limitations:

  • The temporal representation is predefined. A seven-day lag tells the model to examine the same weekday last week because we decided that relationship was important.
  • Historical detail is compressed. A 28-day mean summarises an entire month with one number, potentially hiding the order and shape of changes within that period.
  • Longer relationships require more engineering. Extending memory means creating, validating, and maintaining additional lag, rolling, trend, and interaction features.
  • A fixed feature set may not capture every series equally well. One product may respond to the previous weekend, while another may depend on a gradual multi-week change that the selected summaries do not preserve.
  • More temporal features increase maintenance. Every additional lag, rolling statistic, and interaction must be defined, validated, and reproduced consistently during training and forecasting.

These limitations motivate a different question:

Can a global sequence model learn a shared, nonlinear representation of demand history without relying on predefined statistical structures or handcrafted temporal summaries?

From engineered rows to ordered sequences

A Long Short-Term Memory network, or LSTM, reads the previous demand observations in order. As it moves through the sequence, it updates an internal memory that can retain useful patterns and discard less relevant information. The resulting hidden state becomes a learned representation of recent demand.

Like Holt–Winters and ARIMA, an LSTM preserves the order of historical observations. However, statistical models estimate parameters within predefined structures—such as level, trend, seasonality, and linear lag relationships—whereas an LSTM learns a nonlinear hidden representation from data.

An LSTM is still a supervised-learning model: historical inputs are paired with known targets during training. The change is not from supervised to unsupervised learning. It is from tabular supervised learning with engineered memory to sequence-based supervised learning with learned memory.

XGBoost and the LSTM see the same forecasting problem through different data representations.

XGBoostLSTM
Receives selected lags and rolling statisticsReceives the previous demand observations in order
History is represented through selected lag and rolling featuresThe representation of history is learned from an ordered sequence
Each example is one tabular rowEach example contains an ordered sequence
Store and SKU are one-hot encodedStore and SKU use learned embeddings in the global model
Day-ahead context is supplied as columnsDay-ahead context is joined to the learned sequence representation

For a target date tt, one LSTM training example looks back 28 days:

(yt28,yt27,,yt1),ut,s,kyt(y_{t-28},y_{t-27},\ldots,y_{t-1}),\quad u_t,\quad s,\quad k \longrightarrow y_t

The first input is the ordered 28-day demand sequence. The vector utu_t contains information known for the target date: calendar position, store status, promotion, discount, price ratio, competitor activity, marketing, and weather. The identifiers ss and kk represent the store and SKU; the model converts them into learned embeddings.

Applying this transformation to every eligible target date across all 120 store–SKU series creates the global sequence dataset. Each series contributes examples to the same model, while the chronological training, validation, and test boundaries remain unchanged.

WindowTarget days per seriesGlobal sequences
Training59070,800
Validation566,720
Test566,720

The shared LSTM therefore learns from 70,800 training sequences rather than the history of only one store–SKU. Its recurrent parameters can capture temporal behaviour shared across the portfolio without maintaining a separate network for every series.

The LSTM therefore does not eliminate feature engineering entirely. Tomorrow's planned promotion or weather forecast cannot be inferred reliably from past demand, so forecast-time context must remain explicit. What changes is how the historical demand signal is represented.

How an LSTM creates memory

How an LSTM uses forget, input, and output gates to update its cell and hidden states

A standard recurrent neural network repeatedly updates a hidden state as it reads a sequence. That gives it memory, but gradients can shrink or grow as they pass through many time steps. Learning longer relationships then becomes difficult.

An LSTM introduces a separate cell state and gates that regulate information flow. At day tt, the model receives the current input xtx_t, the previous hidden state ht1h_{t-1}, and the previous cell state ct1c_{t-1}.

The cell state ctc_t is the LSTM's longer-running internal memory. The hidden state hth_t is the exposed representation passed to the next sequence step and, eventually, to the forecasting layers. In the equations below, [ht1,xt][h_{t-1},x_t] joins the previous hidden state and current input into one vector. The matrices WW and vectors bb are parameters learned during training.

The forget gate decides how much existing memory to retain:

ft=σ(Wf[ht1,xt]+bf)f_t=\sigma\left(W_f[h_{t-1},x_t]+b_f\right)

The input gate decides how much new information should be written to memory:

it=σ(Wi[ht1,xt]+bi)i_t=\sigma\left(W_i[h_{t-1},x_t]+b_i\right)

The model separately creates the candidate memory c~t\widetilde c_t. This is a proposal for what the new information could be, based on the current input xtx_t and the previous hidden state ht1h_{t-1}:

c~t=tanh(Wc[ht1,xt]+bc)\widetilde c_t=\tanh\left(W_c[h_{t-1},x_t]+b_c\right)

The candidate is not yet the updated cell state. The input gate iti_t controls how much of it is added, while the forget gate ftf_t controls how much of the previous memory remains:

ct=ftct1+itc~tc_t=f_t\odot c_{t-1}+i_t\odot\widetilde c_t

In other words, c~t\widetilde c_t asks what could be added, iti_t decides how much to add, and ctc_t is the resulting updated memory.

Finally, the output gate controls what memory contributes to the hidden state:

ot=σ(Wo[ht1,xt]+bo)o_t=\sigma\left(W_o[h_{t-1},x_t]+b_o\right) ht=ottanh(ct)h_t=o_t\odot\tanh(c_t)

Why sigmoid for gates and tanh for memory?

Sigmoid and tanh perform different jobs inside an LSTM. The sigmoid function produces values between zero and one, making it suitable for controlling how much information passes through a gate. A value near zero blocks information, while a value near one allows most of it through. The forget, input, and output gates therefore use sigmoid as a soft control mechanism rather than an all-or-nothing switch.

The tanh function produces values between 1-1 and 11. This allows candidate memory to contain positive or negative internal signals—for example, evidence that the recent demand level is increasing or decreasing. Tanh bounds both the candidate memory and the transformed cell state exposed through hth_t; the internal cell state ctc_t itself is not restricted to this range.

In short:

Sigmoid controls how much information passes; tanh represents and transforms the information itself.

Why element-wise multiplication?

The symbol \odot represents element-wise multiplication. An LSTM's cell state contains multiple memory components, and each gate produces one control value for every corresponding component. Element-wise multiplication therefore lets the model retain, remove, or expose different parts of its memory independently.

For example, suppose the previous cell state is:

ct1=[0.8,0.3,0.5]c_{t-1}=[0.8,-0.3,0.5]

and the forget gate produces:

ft=[0.99,0.10,0.01]f_t=[0.99,0.10,0.01]

Then:

ftct1=[0.792,0.03,0.005]f_t\odot c_{t-1}=[0.792,-0.03,0.005]

Almost all of the first component is retained, 10% of the second remains, and only 1% of the third survives. A dot product would instead collapse these components into one number and prevent component-wise gating.

Element-wise multiplication allows each gate to act like an independent volume control for every component of the LSTM's memory.

In demand forecasting, the cell state might retain a sustained weekly pattern, reduce the influence of an isolated spike, or update when the recent level changes for several consecutive days. We do not tell the LSTM to calculate lag_7 or rolling_mean_28; it learns a hidden representation that is useful for reducing the forecasting loss.

That flexibility is also a limitation. A hidden state is harder to explain than a named lag or rolling mean, and there is no guarantee that the representation learned from training will remain useful in the next demand regime.

Architecture Design

Four global-LSTM input paths joined into one forecasting representation

The architecture concatenates four representations rather than adding them numerically:

rt=[htsequence    ut    esstore    ekSKU]r_t= \left[ h_t^{\mathrm{sequence}} \;\Vert\; u_t \;\Vert\; e_s^{\mathrm{store}} \;\Vert\; e_k^{\mathrm{SKU}} \right] y^t=Dense(rt)\widehat y_t=\operatorname{Dense}(r_t)

Here, \Vert denotes vector concatenation. The deliberately compact model contains:

  • A 28-day demand sequence with one value at each step.
  • One LSTM layer with 32 hidden units.
  • Seventeen explicit target-day context features.
  • Four-dimensional store embeddings and three-dimensional SKU embeddings.
  • A dense layer with 32 units, ReLU activation, and 10% dropout.
  • One non-negative demand forecast after reversing the scaling transformation.

Together, these components give the global LSTM 6,435 trainable parameters. The objective is not to build the largest neural network, but to test whether a compact learned sequence representation can outperform a strong tabular benchmark.

Building leakage-safe sequences

The experiment retains the same chronological boundaries used in the previous article. The final 112 target dates form consecutive 56-day validation and test windows.

Chronological training, validation, and test windows

The figure shows one representative series, but the global model applies the same target-date boundaries to all 120 series. Every sample contains the 28 demand observations preceding its target date. The target itself never appears inside the sequence.

LSTM layer setup

The recurrent layer receives only the standardised demand sequence. For a batch containing BB examples, its input shape is:

(B,28,1)(B,28,1)

The 28 positions represent consecutive historical days, while the final dimension contains one demand value per day. Target-day context and identity embeddings do not enter the recurrent layer; they are added after the sequence has been encoded.

The LSTM layer is configured as:

self.lstm = nn.LSTM(
    input_size=1,
    hidden_size=32,
    batch_first=True,
)
SettingValueMeaning
Lookback28 daysFour weeks of ordered demand history
Input size1One standardised demand value at each step
Hidden size32The hidden and cell states each contain 32 components
Batch firstTrueInputs use the shape batch × time × feature

As the LSTM reads the 28 observations, it updates its hidden and cell states at every step. The implementation keeps only the final hidden state:

_, (hidden_state, cell_state) = self.lstm(sequence)
sequence_representation = hidden_state[-1]

For a batch of BB sequences, sequence_representation has shape:

(B,32)(B,32)

This 32-dimensional vector summarises the complete 28-day sequence. The cell state helps maintain memory while the sequence is processed, but it is not passed directly to the forecasting head.

The sequence representation is then concatenated with the 17 target-day context features, four-dimensional store embedding, and three-dimensional SKU embedding:

32+17+4+3=5632+17+4+3=56

The resulting 56-dimensional vector enters the dense forecasting network. A single layer with 32 hidden units keeps the architecture deliberately compact: it provides capacity for nonlinear temporal patterns while limiting training cost and overfitting risk.

Learned identity embeddings

Store and SKU identities selecting learned embedding vectors before joining the global forecasting model

A shared recurrent model can learn temporal behaviour across the portfolio, but it must still distinguish one store or product from another. The same recent demand pattern may imply a different forecast for a high-volume flagship store than for a smaller regional store. The global LSTM handles such differences through learned identity embeddings.

Unlike the XGBoost pipeline, which explicitly one-hot encodes store and SKU identities, the LSTM receives each identity as an integer index.

Store_A → 0
Store_B → 1
Store_C → 2
...

That index directly selects one row from a trainable embedding. An embedding is a learned lookup table: selecting a store or SKU retrieves a small vector of trainable values. In this model, every store receives a four-dimensional vector and every SKU receives a three-dimensional vector

store_embedding = nn.Embedding(
    num_embeddings=n_stores,
    embedding_dim=4,
)

For NN stores, the model creates a matrix:

EstoreRN×4E_{\text{store}}\in\mathbb{R}^{N\times4}

The store and SKU vectors are concatenated with the learned sequence representation and target-day business context before entering the dense forecasting layer. The recurrent parameters remain shared across all series, while the embeddings give the downstream network information it can use to make identity-specific adjustments.

Embeddings are updated through backpropagation whenever their store or SKU appears in a training sequence. Identities that require similar forecasting adjustments may develop similar vectors. This does not prove that two stores or products are operationally or economically similar: the vectors encode only what helps reduce the forecasting loss.

Embeddings also create a cold-start limitation. A completely new store or SKU has no learned vector. A production system therefore needs a strategy such as a shared unknown-identity embedding, metadata-based features, or retraining after sufficient history becomes available. Accuracy must still be monitored by series because shared training and embeddings do not guarantee equal performance across the portfolio.

Scaling without future information

Demand levels differ substantially across stores and products. Training one global model on raw units could cause high-volume series to dominate the loss. Therefore, we standardise demand separately for each series:

zs,k,t=ys,k,tμs,kσs,kz_{s,k,t} =\frac{y_{s,k,t}-\mu_{s,k}}{\sigma_{s,k}}

The mean μs,k\mu_{s,k} and standard deviation σs,k\sigma_{s,k} are calculated using training dates only. Context features are also standardised with a scaler fitted only on the training window. Validation and test data are transformed with those locked statistics.

After prediction, the transformation is reversed to return forecasts to demand units. Negative predictions are clipped to zero because unit demand cannot be negative.

This is an easy place to create leakage. Computing a series mean or standard deviation from all 730 days would allow the future validation and test distributions to influence training. Sequence models do not make point-in-time data discipline less important; they make another preprocessing pipeline that must be protected.

Training choices

After fixing the architecture and input pipeline, three training choices remain especially important: the loss function, gradient control, and the number of training epochs. Together, they determine how the model responds to unusually large errors, how stable each parameter update remains, and how long learning continues before the model begins to overfit the training period.

Huber loss

Daily demand contains occasional large spikes. Squared-error loss gives those observations rapidly increasing influence, which can encourage the network to chase unusual events. Huber loss—implemented as SmoothL1Loss—behaves approximately like squared error near zero and more like absolute error for large residuals.

The notebook configures and applies it as follows:

LOSS_FUNCTION = nn.SmoothL1Loss(beta=0.5)

prediction = model(sequence, context, store_index, sku_index)
loss = LOSS_FUNCTION(prediction, target)

The beta=0.5 threshold marks the transition from the quadratic to the linear part of the loss. Because demand is standardised before training, this threshold is expressed in scaled-demand units.

This does not remove spikes or declare them unimportant. It limits how strongly one extreme residual can dominate an optimisation step.

Gradient Clipping

Training an LSTM requires backpropagation through all 28 sequence steps. Repeated recurrent operations can occasionally produce very large gradients, leading to unstable parameter updates—a problem known as exploding gradients.

After calculating the gradients, the notebook limits their combined norm:

loss.backward()
nn.utils.clip_grad_norm_(
    model.parameters(),
    max_norm=1.0,
)
optimizer.step()

The function calculates the combined Euclidean norm of all model gradients:

g2\lVert g\rVert_2

If the norm is at most 1, the gradients remain unchanged. If it exceeds 1, all gradients are scaled proportionally:

gclipped=g1g2g_{\mathrm{clipped}} =g\frac{1}{\lVert g\rVert_2}

For example, a total norm of 5 causes every gradient to be multiplied by 1/51/5. This preserves the overall update direction while reducing its magnitude.

Gradient clipping does not alter the model weights, predictions, or loss directly. It limits only the update passed to the optimiser, and it addresses exploding gradients rather than vanishing gradients.

Huber loss and gradient clipping therefore perform complementary roles. Huber loss limits the influence of individual large residuals, while gradient clipping protects the complete optimisation step when the combined gradient becomes unusually large.

The threshold of 1.0 is a training hyperparameter rather than a universal rule. A threshold that is too high may provide little protection, while one that is too low can slow learning. It should therefore be checked through validation and, ideally, gradient-norm monitoring.

Select training duration through validation

Validation loss is checked after every epoch, and training stops after six epochs without improvement. The epoch with the lowest validation loss is locked before the test window is opened.

Global LSTM training and validation Huber loss across seven epochs, with epoch one selected because it has the lowest validation loss.

Validation selects only one training epoch for the global LSTM. After that point, training loss continues to fall while validation loss gradually rises. More optimisation improves the fit to the training period but makes the model less useful for the next temporal regime. That is a textbook form of temporal overfitting.

The result is a useful reminder:

Lower training loss does not mean better forecasting of the future.

Early stopping acts as regularisation by limiting how long the model can adapt to training-specific structure. The selected epoch count is not a universal property of LSTMs; it belongs to this architecture, dataset, optimiser, and validation period.

After model selection, a fresh global model is initialised and trained on 77,520 development sequences—the combined training and validation windows—for the single locked epoch. This gives the final model access to every target available before the test boundary. The untouched test window still contributes no information to training duration or model fitting.

Does the improvement justify the complexity?

Deep learning earns its place only if learned temporal representations improve held-out forecasting enough to justify its cost.

Model Comparison

The primary comparison is global LSTM against the global supervised-learning models from the previous article. All results aggregate the same 6,720 test rows across 120 store–SKU series.

This is a comparison of deployable forecasting systems rather than a controlled experiment that changes only the temporal representation. The LSTM also uses a different loss, learned identity embeddings, and a sequence-specific preprocessing pipeline. The result tells us which complete system forecasted better in this holdout; it does not isolate the causal effect of learned memory alone.

Global modelMAERMSEWAPEBias
Global LSTM5.016.5126.44%−2.58%
XGBoost: history, calendar, and identity5.046.6426.60%−4.91%
XGBoost: full business context5.046.4826.62%2.30%
Linear regression: full context5.156.6427.17%−1.51%

Global LSTM achieves the lowest WAPE at 26.44%, improving on the strongest XGBoost result by approximately 0.17 percentage points. It also records the lowest MAE, while full-context XGBoost retains the lowest RMSE.

The result supports a narrow conclusion: learned sequence representations are competitive and slightly better on the article's primary metric. It does not support the stronger conclusion that deep learning decisively replaces tabular forecasting. A 0.17-point difference from one 56-day window is too small to separate durable improvement from regime-specific variation without additional rolling evaluation.

Model selection

Model selection is not only an accuracy ranking.

ConsiderationXGBoostGlobal LSTM
Temporal representationEngineered lags and rolling statisticsLearned from the ordered sequence
TrainingFast and comparatively stableSlower and more sensitive to optimisation choices
Identity handlingOne-hot encoded fieldsLearned embeddings
InterpretationSplit importance and tree-based explanationsHidden states and embeddings require attribution tooling
Production dataFeature tableSequence window, context vector, identities, and locked scalers
Overfitting controlRegularisation and early stoppingDropout, robust loss, and early stopping
Prediction intervalsAdditional method requiredAdditional method required

In this CPU experiment, global-LSTM model selection takes roughly 14 seconds, compared with about one second for XGBoost. The absolute times are small, but the relative difference signals additional cost when tuning expands across architectures, lookback windows, random seeds, and rolling validation periods.

The LSTM removes handcrafted lag and rolling features, but it does not remove the forecasting pipeline. Production must still maintain:

  • a correctly ordered 28-day window for every series;
  • training-only demand and context scalers;
  • store and SKU identity mappings;
  • point-in-time business context;
  • model weights, embedding tables, and architecture versions;
  • monitoring for drift, unstable training, and forecast bias.

Neither model automatically produces calibrated uncertainty. Quantile loss, conformal calibration, or a probabilistic forecasting model would be required before prediction intervals could support service levels or safety stock.

Operational recommendation

Keep history, calendar, and identity XGBoost as the production benchmark and run the global LSTM as a challenger. The LSTM's 0.17-percentage-point WAPE advantage is promising, but it is not enough evidence for an immediate replacement.

Before promoting the LSTM:

  • repeat the comparison across additional rolling validation and test windows;
  • test multiple random seeds to measure training variability;
  • translate forecast differences into stockouts, excess inventory, and service levels;
  • monitor bias at panel, store, SKU, and store–SKU levels;
  • verify that the sequence and context pipeline can be reproduced reliably;
  • add calibrated uncertainty if forecasts will determine safety stock.

In a daily workflow, the trained weights can remain frozen while each series' 28-day demand window and target-day context are refreshed. Weight retraining can happen on a slower schedule and should be triggered by a defined calendar or drift rule rather than by every new observation.

The global model should produce one demand forecast for every store–SKU. Those forecasts are not order quantities. Replenishment still needs stock on hand, confirmed inbound inventory, lead time, pack sizes, service targets, and a chosen uncertainty buffer.

Conclusion: complexity must earn its cost

The progression from XGBoost to LSTM is not simply a move from an old algorithm to a newer one. It changes how the model represents time.

  • Tabular supervised learning receives engineered temporal summaries.
  • The LSTM receives an ordered sequence and learns a hidden representation.
  • Global training supplies enough related sequences for representation learning to become plausible.
  • Embeddings let one model share parameters while adjusting for store and SKU.
  • Early stopping remains essential because additional optimisation can reduce future accuracy.

The global LSTM wins the panel WAPE table by a narrow margin. Whether it wins the forecasting system depends on what that margin is worth.

For this case study, the honest answer is that deep learning has become competitive, not indispensable. XGBoost remains the benchmark to beat, while the LSTM must prove that its learned memory creates stable operational value across more than one held-out window.

這篇文章延續了以下文章中介紹的案例研究:

統計模型為單一門市-SKU 提供了強健的基準表現;全域 XGBoost 則把預測規模擴展到 120 條序列,但額外的商業情境資訊,帶來的效益相當有限。 現在,我們要測試全域 LSTM,能否直接從有序的需求序列中學習,並改善到足以證成它額外複雜度的程度。

本文架構如下:

  1. 為什麼要超越工程設計出來的特徵?
  2. 從工程設計出來的資料列,到有序的序列
  3. LSTM 如何創造記憶
  4. 架構設計
  5. 訓練上的選擇
  6. 這樣的改善,值得增加的複雜度嗎?
  7. 營運建議
  8. 結論:複雜度必須值回它的成本

1. 為什麼要超越工程設計出來的特徵?

像 XGBoost 這樣的表格型監督式學習模型,並不會以有序序列的形式,接收需求歷史。我們必須先把這段歷史,轉換成像 lag_7lag_28rolling_mean_7rolling_std_28 這樣的特徵。這些特徵,賦予了模型某種形式的記憶,但也決定了這份記憶包含哪些資訊。

這會帶來幾項限制:

  • 時間表徵是預先定義好的。 七天的落後特徵,之所以會要求模型檢視上週同一個星期幾,是因為我們自己判斷這層關係很重要。
  • 歷史細節被壓縮了。 一個 28 天的平均數,用單一數字概括了整整一個月,可能因此隱藏了這段期間內變化的順序與樣貌。
  • 更長期的關係,需要更多工程設計。 延伸記憶,意味著必須建立、驗證並維護更多的落後、滾動、趨勢與交互作用特徵。
  • 固定的特徵集合,未必能同等有效地捕捉每一條序列。 某項產品,可能對上一個週末有反應;另一項產品,則可能取決於一個逐漸發生的多週變化,而所選的摘要統計量,並沒有保留這種變化。
  • 更多的時間特徵,會增加維護負擔。 每多一個落後特徵、滾動統計量或交互作用,都必須在訓練與預測時,被定義、驗證,並以一致的方式重現。

這些限制,帶出了一個不同的問題:

一個全域序列模型,能不能在不依賴預先定義的統計結構、也不依賴手工打造的時間摘要統計量的情況下,學到一個共享的、非線性的需求歷史表徵?

2. 從工程設計出來的資料列,到有序的序列

長短期記憶網路(Long Short-Term Memory network,簡稱 LSTM),會依照順序讀取過去的需求觀測值。當它逐步走過整個序列時,會更新一個內部記憶,保留有用的模式,並捨棄較不相關的資訊。最終得到的隱藏狀態,就成為近期需求的一種學習表徵。

和 Holt-Winters 與 ARIMA 一樣,LSTM 保留了歷史觀測值的順序。不過,統計模型是在預先定義好的結構——例如水準、趨勢、季節性與線性落後關係——之中估計參數,而 LSTM 則是從資料中,學習一個非線性的隱藏表徵。

LSTM 仍然是一種監督式學習模型:在訓練過程中,歷史輸入會與已知的目標值配對。這裡的轉變,並不是從監督式學習變成非監督式學習,而是從 具有工程設計記憶的表格型監督式學習,轉變為 具有學習記憶的序列型監督式學習

XGBoost 與 LSTM,是透過不同的資料表徵,來看待同一個預測問題。

XGBoostLSTM
接收選定的落後特徵與滾動統計量依照順序接收過去的需求觀測值
透過選定的落後與滾動特徵來表示歷史歷史的表徵,是從有序序列中學習出來的
每一筆樣本是一列表格資料每一筆樣本包含一段有序序列
門市與 SKU 經過 one-hot 編碼門市與 SKU 在全域模型中,使用學習到的嵌入向量
隔日情境資訊以欄位形式提供隔日情境資訊,會與學習到的序列表徵結合

對於目標日期 tt,一筆 LSTM 訓練樣本,會回顧過去 28 天:

(yt28,yt27,,yt1),ut,s,kyt(y_{t-28},y_{t-27},\ldots,y_{t-1}),\quad u_t,\quad s,\quad k \longrightarrow y_t

第一個輸入,是有序的 28 天需求序列。向量 utu_t,則包含目標日期已知的資訊:日曆位置、門市營業狀態、促銷、折扣、價格比率、競爭對手動態、行銷與天氣。識別碼 sskk,分別代表門市與 SKU;模型會把它們轉換成學習到的嵌入向量。

把這個轉換,套用到全部 120 條門市-SKU 序列中,每一個符合條件的目標日期上,就建立出了全域序列資料集。每條序列,都為同一個模型貢獻樣本,而依時間順序劃分的訓練、驗證與測試邊界,則維持不變。

區間每條序列的目標天數全域序列數
訓練集59070,800
驗證集566,720
測試集566,720

因此,這個共享的 LSTM,是從 70,800 筆訓練序列中學習,而不只是單一門市-SKU 的歷史。它的循環參數,能夠捕捉整個商品組合中共享的時間行為,而不需要為每一條序列各自維護一個網路。

因此,LSTM 並不會完全消除特徵工程。明天計畫中的促銷,或天氣預報,都無法可靠地從過去的需求推論出來,因此預測時點的情境資訊,仍然必須明確提供。真正改變的,是歷史需求訊號的表示方式。

LSTM 如何創造記憶

LSTM 如何運用遺忘閘、輸入閘與輸出閘,更新其細胞狀態與隱藏狀態

標準的循環神經網路,在讀取序列的過程中,會反覆更新一個隱藏狀態。這讓它具備記憶,但梯度在經過許多時間步之後,可能會縮小或放大,使得學習較長期的關係變得困難。

LSTM 引入了一個獨立的細胞狀態,以及一組用來調節資訊流動的閘門。在第 tt 天,模型接收目前的輸入 xtx_t、前一個隱藏狀態 ht1h_{t-1},以及前一個細胞狀態 ct1c_{t-1}

細胞狀態 ctc_t,是 LSTM 較長期運作的內部記憶;隱藏狀態 hth_t,則是傳遞給下一個序列步驟、最終再傳遞給預測層的外顯表徵。在以下的方程式中,[ht1,xt][h_{t-1},x_t] 把前一個隱藏狀態與目前輸入,結合成一個向量。矩陣 WW 與向量 bb,則是訓練過程中學到的參數。

遺忘閘 決定要保留多少既有的記憶:

ft=σ(Wf[ht1,xt]+bf)f_t=\sigma\left(W_f[h_{t-1},x_t]+b_f\right)

輸入閘 決定應該把多少新資訊寫入記憶:

it=σ(Wi[ht1,xt]+bi)i_t=\sigma\left(W_i[h_{t-1},x_t]+b_i\right)

模型會另外建立 候選記憶 c~t\widetilde c_t。這是根據目前輸入 xtx_t 與前一個隱藏狀態 ht1h_{t-1},對新資訊可能是什麼所提出的一個提案:

c~t=tanh(Wc[ht1,xt]+bc)\widetilde c_t=\tanh\left(W_c[h_{t-1},x_t]+b_c\right)

這個候選值,還不是更新後的細胞狀態。輸入閘 iti_t,控制要加入多少候選值;遺忘閘 ftf_t,則控制要保留多少先前的記憶:

ct=ftct1+itc~tc_t=f_t\odot c_{t-1}+i_t\odot\widetilde c_t

換句話說,c~t\widetilde c_t 問的是 可以加入什麼iti_t 決定 要加入多少,而 ctc_t 則是最終更新後的記憶。

最後,輸出閘 控制哪些記憶,會貢獻到隱藏狀態之中:

ot=σ(Wo[ht1,xt]+bo)o_t=\sigma\left(W_o[h_{t-1},x_t]+b_o\right) ht=ottanh(ct)h_t=o_t\odot\tanh(c_t)

為什麼閘門用 sigmoid,記憶用 tanh?

Sigmoid 與 tanh,在 LSTM 內部扮演不同的角色。Sigmoid 函數,會產生介於 0 到 1 之間的數值,適合用來控制有多少資訊能通過一道閘門:數值接近 0 會阻擋資訊,接近 1 則會讓大部分資訊通過。因此,遺忘閘、輸入閘與輸出閘,都使用 sigmoid 作為一種柔性的控制機制,而不是全有或全無的開關。

Tanh 函數,會產生介於 1-111 之間的數值。這讓候選記憶,可以包含正向或負向的內部訊號——例如,近期需求水準正在上升或下降的證據。Tanh 會限制候選記憶,以及透過 hth_t 外顯出來的轉換後細胞狀態;但內部的細胞狀態 ctc_t 本身,並不受限於這個範圍。

簡而言之:

Sigmoid 控制有多少資訊能通過;tanh 則表示並轉換資訊本身。

為什麼使用逐元素相乘?

符號 \odot 代表逐元素相乘。LSTM 的細胞狀態,包含多個記憶成分,而每一道閘門,都會為每一個對應的成分,產生一個控制值。因此,逐元素相乘,讓模型能夠獨立地保留、移除,或顯露記憶中的不同部分。

舉例來說,假設前一個細胞狀態是:

ct1=[0.8,0.3,0.5]c_{t-1}=[0.8,-0.3,0.5]

而遺忘閘產生的結果是:

ft=[0.99,0.10,0.01]f_t=[0.99,0.10,0.01]

那麼:

ftct1=[0.792,0.03,0.005]f_t\odot c_{t-1}=[0.792,-0.03,0.005]

第一個成分幾乎完全保留、第二個成分保留了 10%、第三個成分則只剩下 1%。如果改用內積,這些成分就會被壓縮成單一一個數字,導致無法逐成分進行閘控。

逐元素相乘,讓每一道閘門,都能像是 LSTM 記憶中每一個成分各自獨立的音量旋鈕。

在需求預測中,細胞狀態可能會保留一個持續存在的每週模式、降低某次孤立尖峰的影響力,或是在近期水準連續好幾天發生變化時進行更新。我們並不會告訴 LSTM 去計算 lag_7rolling_mean_28;它學到的,是一個有助於降低預測損失的隱藏表徵。

這種彈性,同時也是一種限制。隱藏狀態,比一個有名稱的落後特徵或滾動平均,更難以解釋;而且,也無法保證訓練中學到的表徵,在下一個需求狀態中,依然有用。

架構設計

全域 LSTM 的四條輸入路徑,結合成單一預測表徵

這個架構,把四種表徵串接在一起,而不是以數值方式相加:

rt=[htsequence    ut    esstore    ekSKU]r_t= \left[ h_t^{\mathrm{sequence}} \;\Vert\; u_t \;\Vert\; e_s^{\mathrm{store}} \;\Vert\; e_k^{\mathrm{SKU}} \right] y^t=Dense(rt)\widehat y_t=\operatorname{Dense}(r_t)

其中,\Vert 代表向量串接。這個刻意保持精簡的模型包含:

  • 一段 28 天的需求序列,每一步一個數值。
  • 一層具有 32 個隱藏單元的 LSTM 層。
  • 17 個明確的目標日情境特徵。
  • 四維的門市嵌入向量,以及三維的 SKU 嵌入向量。
  • 一層具有 32 個單元、ReLU 啟動函數與 10% dropout 的密集層。
  • 還原縮放轉換後,得到一個非負的需求預測值。

把這些元件加總起來,全域 LSTM 共有 6,435 個可訓練參數。這裡的目標,並不是打造最龐大的神經網路,而是要測試一個精簡的、學習出來的序列表徵,能否勝過一個強健的表格型基準模型。

建立防止洩漏的序列

這項實驗,沿用了前一篇文章所使用的時間邊界。最後 112 個目標日期,構成連續的 56 天驗證區間與 56 天測試區間。

依時間順序劃分的訓練、驗證與測試區間

這張圖顯示的是一條具代表性的序列,但全域模型,會把相同的目標日期邊界,套用到全部 120 條序列上。每一筆樣本,都包含目標日期之前的 28 筆需求觀測值;目標值本身,絕不會出現在序列之中。

LSTM 層的設定

循環層,只會接收標準化後的需求序列。對於包含 BB 筆樣本的一個批次,它的輸入形狀為:

(B,28,1)(B,28,1)

這 28 個位置,代表連續的歷史天數;最後一個維度,則包含每天的一個需求值。目標日情境資訊與身分嵌入向量,並不會進入循環層,而是在序列被編碼之後才加入。

LSTM 層的設定如下:

self.lstm = nn.LSTM(
    input_size=1,
    hidden_size=32,
    batch_first=True,
)
設定數值意義
回顧期間28 天四週的有序需求歷史
輸入維度1每一步一個標準化後的需求值
隱藏維度32隱藏狀態與細胞狀態,各包含 32 個成分
Batch firstTrue輸入的形狀為 batch × time × feature

當 LSTM 讀取這 28 筆觀測值時,會在每一步更新它的隱藏狀態與細胞狀態。這個實作,只保留最後一個隱藏狀態:

_, (hidden_state, cell_state) = self.lstm(sequence)
sequence_representation = hidden_state[-1]

對於一個包含 BB 段序列的批次,sequence_representation 的形狀為:

(B,32)(B,32)

這個 32 維向量,概括了整段 28 天的序列。細胞狀態,有助於在處理序列的過程中維持記憶,但並不會直接傳遞給預測輸出層。

接著,這個序列表徵,會與 17 個目標日情境特徵、四維的門市嵌入向量,以及三維的 SKU 嵌入向量串接在一起:

32+17+4+3=5632+17+4+3=56

得到的這個 56 維向量,會進入密集的預測網路。單一一層、32 個隱藏單元,讓這個架構刻意維持精簡:它為非線性的時間模式,提供了足夠的容量,同時也限制了訓練成本與過度配適的風險。

學習到的身分嵌入

門市與 SKU 身分選出學習到的嵌入向量,再併入全域預測模型

一個共享的循環模型,能夠學習整個商品組合中的時間行為,但仍然必須區分不同的門市或產品。同樣的近期需求模式,對一家高銷量的旗艦門市,與一家較小的區域門市而言,可能意味著不同的預測結果。全域 LSTM,透過學習到的身分嵌入向量,來處理這類差異。

與明確使用 one-hot 編碼門市與 SKU 身分的 XGBoost 流程不同,LSTM 接收到的每一個身分,都是一個整數索引。

Store_A → 0
Store_B → 1
Store_C → 2
...

這個索引,會直接從一個可訓練的嵌入表中,選出對應的一列。 嵌入向量,是一種學習到的查找表:選取一個門市或 SKU,就會取出一小組可訓練數值所組成的向量。 在這個模型中,每一家門市,會得到一個四維向量;每一個 SKU,則會得到一個三維向量

store_embedding = nn.Embedding(
    num_embeddings=n_stores,
    embedding_dim=4,
)

對於 NN 家門市,模型會建立一個矩陣:

E門市RN×4E_{\text{門市}}\in\mathbb{R}^{N\times4}

門市與 SKU 向量,會在進入密集預測層之前,與學習到的序列表徵及目標日商業情境串接在一起。循環參數,在所有序列之間維持共享;而嵌入向量,則為下游網路提供了可用來做出身分專屬調整的資訊。

每當某個門市或 SKU 出現在訓練序列中,它對應的嵌入向量,就會透過反向傳播進行更新。需要類似預測調整的身分,可能會演變出相似的向量。但這並不能證明兩家門市或兩項產品,在營運或經濟意義上是相似的——這些向量,編碼的只是有助於降低預測損失的資訊。

嵌入向量,也帶來了一項冷啟動的限制。一個全新的門市或 SKU,並沒有學習到的向量。因此,正式的生產系統,需要一套策略,例如共用一個「未知身分」的嵌入向量、以中繼資料為基礎的特徵,或是在累積足夠歷史資料後再重新訓練。準確度,仍然必須逐條序列監控,因為共享訓練與嵌入向量,並不能保證整個商品組合的表現一致。

不使用未來資訊的縮放

不同門市與產品之間的需求水準,差異相當大。如果直接用原始台數,訓練單一全域模型,可能會讓高銷量的序列主導整體損失。因此,我們針對每一條序列,各自進行需求標準化:

zs,k,t=ys,k,tμs,kσs,kz_{s,k,t} =\frac{y_{s,k,t}-\mu_{s,k}}{\sigma_{s,k}}

平均數 μs,k\mu_{s,k} 與標準差 σs,k\sigma_{s,k},只用訓練集的日期來計算。情境特徵,也是用只在訓練區間上配適的縮放器來標準化。驗證與測試資料,則使用這些已鎖定的統計量來轉換。

預測完成後,這個轉換會被還原,把預測值換回需求台數。負的預測值會被截斷為零,因為需求台數不可能為負。

這是一個很容易產生洩漏的地方。如果用全部 730 天的資料,來計算一條序列的平均數或標準差,就會讓未來驗證集與測試集的分布,影響到訓練過程。序列模型,並不會讓即時時點的資料紀律變得比較不重要;它們只是多了一條,同樣需要被妥善保護的前處理管線。

訓練上的選擇

在固定架構與輸入管線之後,還有三項訓練上的選擇特別重要:損失函數、梯度控制,以及訓練週期(epoch)的數量。這三者一起決定了:模型如何回應異常大的誤差、每次參數更新維持多穩定,以及模型在開始過度配適訓練期間之前,能持續學習多久。

Huber 損失

每日需求中,偶爾會出現大幅的尖峰。平方誤差損失,會讓這些觀測值的影響力快速增加,可能因此促使網路去追逐異常事件。Huber 損失——在實作中對應到 SmoothL1Loss——在誤差接近零時,行為近似平方誤差;而在殘差較大時,則更接近絕對誤差。

筆記本中,是這樣設定並套用它的:

LOSS_FUNCTION = nn.SmoothL1Loss(beta=0.5)

prediction = model(sequence, context, store_index, sku_index)
loss = LOSS_FUNCTION(prediction, target)

beta=0.5 這個門檻值,標記出損失函數從二次項過渡到線性項的位置。由於需求在訓練前已經過標準化,這個門檻值,是以縮放後的需求單位來表示的。

這並不會移除尖峰,也不代表尖峰不重要。它限制的,是單一一個極端殘差,能在一次最佳化步驟中,主導到什麼程度。

梯度裁剪

訓練 LSTM,需要透過全部 28 個序列步驟進行反向傳播。反覆的循環運算,偶爾會產生非常大的梯度,導致參數更新變得不穩定——這個問題稱為 梯度爆炸

在計算出梯度之後,筆記本會限制它們的合併範數:

loss.backward()
nn.utils.clip_grad_norm_(
    model.parameters(),
    max_norm=1.0,
)
optimizer.step()

這個函式,會計算模型所有梯度合併後的歐幾里得範數:

g2\lVert g\rVert_2

如果這個範數不超過 1,梯度就會維持不變。如果超過 1,所有梯度都會按比例縮放:

gclipped=g1g2g_{\mathrm{clipped}} =g\frac{1}{\lVert g\rVert_2}

舉例來說,如果總範數是 5,每一個梯度就會被乘上 1/51/5。這樣可以保留整體更新的方向,同時降低其幅度。

梯度裁剪,並不會直接改變模型權重、預測值或損失。它只限制傳遞給最佳化器的更新量,而且處理的是梯度爆炸,而不是梯度消失。

因此,Huber 損失與梯度裁剪,扮演的是互補的角色。Huber 損失,限制個別大型殘差的影響力;梯度裁剪,則在合併後的梯度異常龐大時,保護整個最佳化步驟。

1.0 這個門檻值,是一個訓練用的超參數,而不是一條放諸四海皆準的規則。門檻值太高,可能起不了什麼保護作用;太低,則可能拖慢學習速度。因此,理想上,應該透過驗證,以及梯度範數的監控,來檢視這個門檻值是否合適。

透過驗證來選擇訓練時長

每一個訓練週期結束後,都會檢查驗證損失;如果連續六個週期都沒有改善,訓練就會停止。驗證損失最低的那個週期,會在打開測試區間之前先行鎖定。

全域 LSTM 在七個訓練週期中的訓練與驗證 Huber 損失,第一個週期因驗證損失最低而被選定。

驗證集,為全域 LSTM 只選出了一個訓練週期。在那之後,訓練損失持續下降,但驗證損失卻逐漸上升。更多的最佳化,改善的是模型對訓練期間的配適程度,卻讓模型對下一個時間狀態變得較不實用。這正是時間性過度配適的典型範例。

這個結果,是一個有用的提醒:

更低的訓練損失,不代表對未來有更好的預測。

提前停止,透過限制模型能適應訓練特定結構的時間長度,發揮了一種正則化的作用。選定的週期數,並不是 LSTM 的普遍性質,而是專屬於這個架構、這份資料集、這個最佳化器與這段驗證期間的結果。

完成模型選擇之後,會重新初始化一個全新的全域模型,並在 77,520 筆開發用序列——也就是訓練集與驗證集合併後的區間——上,訓練那個已鎖定的單一週期數。這讓最終的模型,能夠取得測試邊界之前的每一個目標值。而完全未被觸碰的測試區間,仍然沒有為訓練時長或模型配適,提供任何資訊。

這樣的改善,值得增加的複雜度嗎?

唯有當學習到的時間表徵,能把保留樣本的預測改善到足以證成其成本的程度,深度學習才配得上自己的一席之地。

模型比較

主要的比較,是全域 LSTM 對比前一篇文章中的全域監督式學習模型。所有結果,都是彙總自全部 120 條門市-SKU 序列中,相同的 6,720 列測試資料。

這是可部署預測系統之間的比較,而不是一個只改變時間表徵的控制實驗。LSTM 同時也使用了不同的損失函數、學習到的身分嵌入向量,以及一套序列專屬的前處理管線。這個結果告訴我們的是,在這次保留測試中,哪一套完整的系統預測得更好;它並沒有單獨分離出學習記憶本身的因果效應。

全域模型MAERMSEWAPE偏差
全域 LSTM5.016.5126.44%−2.58%
XGBoost:歷史、日曆與身分5.046.6426.60%−4.91%
XGBoost:完整商業情境5.046.4826.62%2.30%
線性迴歸:完整情境5.156.6427.17%−1.51%

全域 LSTM 取得了最低的 WAPE,為 26.44%,比表現最好的 XGBoost 結果,改善了大約 0.17 個百分點。它也錄得最低的 MAE,而完整情境 XGBoost,則仍保有最低的 RMSE。

這個結果,支持的是一個有限的結論:學習到的序列表徵,具有競爭力,而且在本文的主要指標上稍微更好。它並不支持一個更強烈的結論——深度學習能決定性地取代表格型預測。單一一個 56 天區間、0.17 個百分點的差異,若沒有額外的滾動評估,太小了,不足以區分究竟是持久性的改善,還是特定狀態下的變動。

模型選擇

模型選擇,不只是替準確度排名而已。

考量因素XGBoost全域 LSTM
時間表徵工程設計出來的落後特徵與滾動統計量從有序序列中學習而來
訓練較快,也相對穩定較慢,且對最佳化選擇較敏感
身分處理One-hot 編碼欄位學習到的嵌入向量
解讀方式切分重要性與樹狀解釋隱藏狀態與嵌入向量,需要額外的歸因工具
生產環境所需資料特徵表格序列視窗、情境向量、身分識別碼,以及已鎖定的縮放器
過度配適控制正則化與提前停止Dropout、穩健損失函數與提前停止
預測區間需要額外的方法需要額外的方法

在這個以 CPU 執行的實驗中,全域 LSTM 的模型選擇,大約需要 14 秒,相較之下,XGBoost 只需要大約 1 秒。絕對時間雖然不長,但這個相對差異,預示著當調校範圍擴大到不同架構、回顧視窗、隨機種子與滾動驗證期間時,會帶來額外的成本。

LSTM 移除了手工打造的落後與滾動特徵,但並沒有移除整個預測管線。正式的生產環境,仍然必須維護:

  • 每一條序列,都要有正確排序的 28 天視窗;
  • 只在訓練集上配適的需求與情境縮放器;
  • 門市與 SKU 的身分對照表;
  • 即時時點的商業情境;
  • 模型權重、嵌入表,以及架構版本;
  • 對飄移、不穩定訓練與預測偏差的監控。

這兩種模型,都不會自動產生校準良好的不確定性估計。在預測區間,能夠支援服務水準或安全庫存之前,都需要額外的分位數損失函數、共形校準,或機率預測模型。

營運建議

應該把「歷史、日曆與身分」XGBoost,保留作為正式的生產基準模型,並讓全域 LSTM,以挑戰模型的身分運作。LSTM 在 WAPE 上 0.17 個百分點的優勢,令人期待,但這還不足以構成立即替換的證據。

在將 LSTM 升級為正式模型之前:

  • 在更多滾動驗證與測試區間中,重複這項比較;
  • 測試多個隨機種子,衡量訓練過程的變異程度;
  • 把預測結果的差異,換算成缺貨、庫存過剩與服務水準的實際影響;
  • 在全體序列、門市、SKU 與門市-SKU 各個層級,監控偏差;
  • 確認序列與情境管線,能夠被可靠地重現;
  • 如果預測結果,將用來決定安全庫存,就需要加入校準良好的不確定性估計。

在每日的作業流程中,已訓練好的權重可以維持凍結,只更新每條序列的 28 天需求視窗,以及目標日的情境資訊。權重的重新訓練,可以按照較慢的節奏進行,並且應該由明確定義的行事曆規則,或飄移規則來觸發,而不是每來一筆新的觀測值就重新訓練一次。

全域模型,應該為每一個門市-SKU,產生一個需求預測。這些預測,並不是訂購數量。補貨決策,仍然需要現有庫存、已確認到貨量、前置時間、包裝規格、服務目標,以及所選定的不確定性緩衝。

結論:複雜度必須值回它的成本

從 XGBoost 演進到 LSTM,並不只是從一個較舊的演算法,換成一個較新的演算法而已。它改變的,是模型表示時間的方式。

  • 表格型監督式學習,接收的是工程設計出來的時間摘要統計量。
  • LSTM,接收的是一段有序序列,並學習一個隱藏表徵。
  • 全域訓練,提供了足夠多的相關序列,讓表徵學習變得可行。
  • 嵌入向量,讓單一模型能共享參數,同時針對門市與 SKU 做出調整。
  • 提前停止依然不可或缺,因為更多的最佳化,可能會降低對未來的準確度。

全域 LSTM,以些微的差距,在全體序列彙總的 WAPE 表格中勝出。但它是否能贏得整個預測系統的採用,取決於這個差距究竟值多少。

就這個案例研究而言,誠實的答案是:深度學習已經變得具有競爭力,但還稱不上不可或缺。XGBoost,仍然是有待超越的基準模型;而 LSTM,則必須證明它學習到的記憶,能在不只一個保留區間中,創造出穩定的營運價值。