awslabs / gluonts

Probabilistic time series modeling in Python
https://ts.gluon.ai
Apache License 2.0
4.46k stars 742 forks source link

[Draft] Simple Prediction Interface #1998

Open jaheba opened 2 years ago

jaheba commented 2 years ago

Creating a predictor in GluonTS is relatively complicated. Let's make it simpler!

Proposal

Each predictor should implement the following interface:

def predict(past_data: Any, n: int) -> [Distribution; n]:
    ...

The function takes some model-specific past_data attribute and a prediction-length n and is expected to return n Distribution values.

Distribution is an array-like object, on which we can call invoke numpy functions such as np.median(...).

Let's take a look at a trivial example:

def zero_forecast(past_data, n):
    return np.zeros(n)

forecast = zero_forecast(None, 7)
median_forecast = np.array([np.median(sample) for sample in forecast])

And a slightly more complex example returning samples:

@dataclass
class MeanPredictor:
    num_samples: int = 100
    context_length: Optional[int] = None

    def __call__(self, past_data, n):
        if self.context_length is not None:
            past_data = past_data[-self.context_length: ]

        samples = np.random.standard_normal((n, self.num_samples))
        return samples * np.nanstd(past_data) + np.nanmean(past_data)

forecast = MeanPredictor()([1, 2, 3, 4, 5, 6, 7], 7)
median_forecast = np.array([np.median(sample) for sample in forecast])

TODO:

lostella commented 2 years ago

Should predictors not allow being called without n? (That is, should n not have a default value?)

lostella commented 2 years ago

This line seems too complicated:

median_forecast = np.array([np.median(sample) for sample in forecast])

If forecast is an array-like object, should one not be able to do something like

median_forecast = np.median(forecast, axis=1)

?

jaheba commented 2 years ago

This line seems too complicated:

median_forecast = np.array([np.median(sample) for sample in forecast])

If forecast is an array-like object, should one not be able to do something like

median_forecast = np.median(forecast, axis=1)

?

I wanted to use the same code for both examples. The axis-parameter doesn't work where you only have a single value per time-stamp, I think.

lostella commented 2 years ago

Another note: predict_many may be the preferred way of implementing some of the predictors (e.g. NN-based ones that prefer to process multiple entries in one batch).

What is the idea here? Are you thinking of having a fall-back definition for predict and predict_many, each relying on the other one? So that if a predictor implements predict_many it gets predict for free, and vice versa.

jaheba commented 2 years ago

Should predictors not allow being called without n? (That is, should n not have a default value?)

Like 1, or do you mean one defined by the predictor? I would find the latter unintuitive since different predictors would then yield different length by default.

jaheba commented 2 years ago

Another note: predict_many may be the preferred way of implementing some of the predictors (e.g. NN-based ones that prefer to process multiple entries in one batch).

What is the idea here? Are you thinking of having a fall-back definition for predict and predict_many, each relying on the other one? So that if a predictor implements predict_many it gets predict for free, and vice versa.

Something like that. predict and predict_many would be low-level primitives that users don't interact with directly.

But something I find really annoying at the moment is when I want to predict a single time-series, I have to do something like:

forecast = list(predictor.predict([past_data]))[0]

instead of

forecast = predictor.predict(past_data)

With predict_one and predict_many (or however we would like to call them) we could define interfaces for both.

How they work internally is another question of course :)

lostella commented 2 years ago

I wanted to use the same code for both examples. The axis-parameter doesn't work where you only have a single value per time-stamp, I think.

Sure. One problem I see there, is that something like for sample in forecast will iterate along the first dimension of the forecast array, in case it's an actual np.array. So that axis should be the "sample" axis; if there is no sample axis (because the forecast is not sample-based, for example if it’s a parametric distribution or a point-forecast), iterating over it will give misleading results. In conclusion, whoever gets the forecast object back should in any case be aware of its shape and the meaning of the axes, in order to correctly operate on it.