Repository: awslabs/gluonts

Issue:
Temporal Fusion Transformer fails on multiple datasets
## Description

Although the temporal fusion transformer (TFT) works on most datasets bundled with GluonTS, it fails on the following:

- M3 Yearly (training fails on the first batch)
- M4 Yearly (training fails on the first batch)
- M4 Daily (training fails after ~70 epochs), separate issue #1513

The issue seems to be caused by TFT's data loading logic, see error message(s) below.

## To Reproduce

```python
from gluonts.dataset.repository.datasets import get_dataset
from gluonts.model.tft import TemporalFusionTransformerEstimator
from gluonts.mx.trainer import Trainer
from gluonts.mx.trainer.learning_rate_scheduler import LearningRateReduction

dataset = get_dataset("m3_yearly")
callbacks = [
    LearningRateReduction(
        objective="min", patience=9, base_lr=1e-3, decay_factor=0.5
    )
]
estimator = TemporalFusionTransformerEstimator(
    freq=dataset.metadata.freq,
    prediction_length=dataset.metadata.prediction_length,
    trainer=Trainer(epochs=81, num_batches_per_epoch=50, callbacks=callbacks),
    context_length=4 * dataset.metadata.prediction_length,
)
estimator.train(dataset.train)
```

## Error message or code output

Traceback of GluonTS-specific code (M3 Yearly and M4 Yearly):

```
  File "/usr/local/lib/python3.8/site-packages/gluonts/mx/model/estimator.py", line 201, in train
    return self.train_model(
  File "/usr/local/lib/python3.8/site-packages/gluonts/mx/model/estimator.py", line 176, in train_model
    self.trainer(
  File "/usr/local/lib/python3.8/site-packages/gluonts/mx/trainer/_base.py", line 436, in __call__
    epoch_loss = loop(
  File "/usr/local/lib/python3.8/site-packages/gluonts/mx/trainer/_base.py", line 325, in loop
    for batch_no, batch in enumerate(it, start=1):
  File "/usr/local/lib/python3.8/site-packages/tqdm/std.py", line 1178, in __iter__
    for obj in iterable:
  File "/usr/local/lib/python3.8/site-packages/gluonts/itertools.py", line 51, in get_batch
    return list(itertools.islice(it, batch_size))
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/dataset.py", line 53, in __iter__
    yield from self.transformation(
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/_base.py", line 81, in __call__
    for data_entry in data_it:
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/_base.py", line 130, in __call__
    for data_entry in data_it:
  File "/usr/local/lib/python3.8/site-packages/gluonts/itertools.py", line 32, in __iter__
    for el in self.iterable:
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/dataset.py", line 53, in __iter__
    yield from self.transformation(
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/_base.py", line 81, in __call__
    for data_entry in data_it:
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/_base.py", line 81, in __call__
    for data_entry in data_it:
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/_base.py", line 81, in __call__
    for data_entry in data_it:
  [Previous line repeated 3 more times]
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/_base.py", line 85, in __call__
    raise e
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/_base.py", line 83, in __call__
    yield self.map_transform(data_entry.copy(), is_train)
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/_base.py", line 98, in map_transform
    return self.transform(data)
  File "/usr/local/lib/python3.8/site-packages/gluonts/transform/convert.py", line 134, in transform
    output = np.vstack(r) if not self.h_stack else np.hstack(r)
  File "<__array_function__ internals>", line 5, in vstack
  File "/usr/local/lib/python3.8/site-packages/numpy/core/shape_base.py", line 283, in vstack
    return _nx.concatenate(arrs, 0)
  File "<__array_function__ internals>", line 5, in concatenate
ValueError: need at least one array to concatenate
```

## Environment

- Operating system: Debian Buster
- Python version: `3.8.9`
- GluonTS version: Master, Post 0.7.0 (commit f6948bacb7a038df3374e768ad4939455c74b49d)
- MXNet version: `1.8.0.post0`

Full list of dependencies:

```
PyYAML = "^5.4.1"
click = "^7.1.2"
fastparquet = "^0.6.1"
fbprophet = "^0.7.1"
gluonts = {git = "https://github.com/awslabs/gluon-ts.git", rev = "f6948bacb7a038df3374e768ad4939455c74b49d"}
holidays = "^0.11.1"
mxnet = "^1.8.0"
numpy = "^1.20.3"
pandas = "^1.2.4"
pyarrow = "^4.0.0"
pydantic = "^1.8.2"
pystan = "^2.0.0"
python = ">=3.8,<3.10"
sagemaker = "^2.40.0"
sagemaker-training = "^3.9.2"
scikit-learn = "^0.24.2"
scipy = "^1.6.3"
toolz = "^0.11.1"
tqdm = "^4.60.0"
ujson = "^4.0.2"
xgboost = "^1.4.1"
```



Discussion:

Resolving patch:
diff --git a/src/gluonts/model/tft/_estimator.py b/src/gluonts/model/tft/_estimator.py
--- a/src/gluonts/model/tft/_estimator.py
+++ b/src/gluonts/model/tft/_estimator.py
@@ -33,7 +33,11 @@
 from gluonts.mx.model.estimator import GluonEstimator
 from gluonts.mx.trainer import Trainer
 from gluonts.mx.util import copy_parameters, get_hybrid_forward_input_names
-from gluonts.time_feature import TimeFeature, time_features_from_frequency_str
+from gluonts.time_feature import (
+    TimeFeature,
+    time_features_from_frequency_str,
+    Constant,
+)
 from gluonts.transform import (
     AddObservedValuesIndicator,
     AddTimeFeatures,
@@ -109,6 +113,10 @@ def __init__(
 
         if not time_features:
             self.time_features = time_features_from_frequency_str(self.freq)
+            if not self.time_features:
+                # If time features are empty (as for yearly data), we add a
+                # constant feature of 0
+                self.time_features = [Constant()]
         else:
             self.time_features = time_features
         self.static_cardinalities = static_cardinalities
diff --git a/src/gluonts/time_feature/__init__.py b/src/gluonts/time_feature/__init__.py
--- a/src/gluonts/time_feature/__init__.py
+++ b/src/gluonts/time_feature/__init__.py
@@ -28,6 +28,7 @@
     MonthOfYearIndex,
     WeekOfYearIndex,
     time_features_from_frequency_str,
+    Constant,
     norm_freq_str,
 )
 from .holiday import SPECIAL_DATE_FEATURES, SpecialDateFeatureSet
@@ -50,6 +51,7 @@
     "MinuteOfHourIndex",
     "MonthOfYearIndex",
     "WeekOfYearIndex",
+    "Constant",
     "SPECIAL_DATE_FEATURES",
     "SpecialDateFeatureSet",
     "get_lags_for_frequency",
diff --git a/src/gluonts/time_feature/_base.py b/src/gluonts/time_feature/_base.py
--- a/src/gluonts/time_feature/_base.py
+++ b/src/gluonts/time_feature/_base.py
@@ -151,6 +151,18 @@ def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
         return (week - 1).map(float)
 
 
+class Constant(TimeFeature):
+    """Constant time feature using a predefined value."""
+
+    @validated()
+    def __init__(self, value: float = 0.0):
+        super().__init__()
+        self.value = value
+
+    def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
+        return np.full(index.shape, self.value)
+
+
 def norm_freq_str(freq_str: str) -> str:
     return freq_str.split("-")[0]
 


Test patch:
diff --git a/test/time_feature/test_features.py b/test/time_feature/test_features.py
--- a/test/time_feature/test_features.py
+++ b/test/time_feature/test_features.py
@@ -24,6 +24,7 @@
     DayOfYear,
     WeekOfYear,
     MonthOfYear,
+    Constant,
     MinuteOfHourIndex,
     HourOfDayIndex,
     DayOfWeekIndex,
@@ -52,6 +53,7 @@
         (DayOfYear(), pd.date_range("01-01-2015", periods=365 * 5, freq="D")),
         (WeekOfYear(), pd.date_range("01-01-2015", periods=53 * 5, freq="W")),
         (MonthOfYear(), pd.date_range("01-01-2015", periods=12 * 5, freq="M")),
+        (Constant(), pd.date_range("01-01-2015", periods=5, freq="A")),
     ],
 )
 def test_feature_normalized_bounds(