Files
pytorch-ts/test/modules/test_implicit_quantile_distr_output.py
T
Adele Gouttes e3b8de5da9 ASF-3099 Implement IQN in pytorch-ts (#21)
* ASF-3099 Bootstrap: write backbones of test, distribution and distribution output

* ASF-3099 First proposition for IQN distribution output
Mostly for backbones, the functions themselves are far from final

* ASF-3099 Add sample() to ImplicitQuantile and its test

* ASF-3099 Test prediction, sampling and convergence logic for IQN

* ASF-3099 IQN module takes the data input and taus in

* ASF-3099 Add torch.no_grad in the sampling

* ASF-3099 Add tests on quantiles (10%, 90%), for both normal and uniform

* ASF-3099 Improve feeting of quantile function:
Increase depth of quantile network, change activation to PReLU

* ASF-3099 Fix distribution attributes and module imports

* ASF-3099 Fix implicit quantile init

* ASF-3099 Add integration test with deepAR

* ASF-3099 Add a transformed distribution

* ASF-3099 Fix init of ImplicitQuantile

* ASF-3099 Make iqn distribution compatible with forecast-length>1

* ASF-3099 Fix device for new tensors

* ASF-3099 Fix device for new tensors: device is not a function...

* ASF-3099 Fix output size of the network

* ASF-3099 Define torch network in the DistributionOutput only
Distribution takes only the predicted quantiles, or the parameters
of the trained model to define a new quantile function

* ASF-3099 Test: create quantile function on compatible device
Attempt: class method might force the module to be created only once,
not necessarily with the right device

* ASF-3099 Second attempt: create quantile function on compatible device

* ASF-3099 Sampling returns a tensor of the correct shape
Shape is (num_sample, batch_size, forecast_length)

* ASF-3099 Handle empty sample shapes

* ASF-3099 Fix tau device at inference time

* ASF-3099 Fix device of the layer

* ASF-3099 Handle empty sample shapes (fix output)

* ASF-3099 Last activation of quantile layer should be removed
That way, embedded quantiles are symetrically distributed around the 0.5
quantile. Otherwise we distort part of their distribution before applying
it to the forecasted quantities

* ASF-3099 (test) Add a bunch of layers

* ASF-3099 [test] reduce the embedding size of tau

* ASF-3099 [test] Use same IQN version as in sales forecaster

* ASF-3099 Put original parameters back

* use @torch.no_grad() decorator

* ASF-3099 [test, to be reverted] Remove - in front of the loss

* ASF-3099 Revert former commit: put - back in the loss

* ASF-3099 [fix] Add log_prob method in piecewise linear

* ASF-3099 [test] ImplicitQuantileModule should be instanciated only once
Current problem: it's instanciated once at training and once at prediction,
as if the model was never trained. Thus it is now defined as a
global variable. However, this can only be a temporary hack: it means that
only one model can be trained during a session.

* ASF-3099 [test] ImplicitQuantileModule should be instanciated only once
Current problem: it's instanciated once at training and once at prediction,
as if the model was never trained.
However here, if the same model is retrained in the same session,
the module is not reset.

* ASF-3099 Add notebooks for experiments

* ASF-3099 [test] Move module to the distribution

* ASF-3099 Class method for args_proj

* ASF-3099 Clean up

* ASF-3099 More clean up

* ASF-3099 Define options for domain of preditected quantiles
Predicted quantiles can be either positive, either real

* ASF-3099 Set quantile_arg_proj in the init of the distribution output
Before it was instanciated once per python session, thus when retraining
the same models, the previously trained module was used, and not a fresh
one

* ASF-3099 Add test on number of instantiation of the quantile_arg_proj

* ASF-3099 Add an example notebook

* ASF-3099 Remove some notebooks

* ASF-3099 Remove diff vs master

Co-authored-by: Kashif Rasul <kashif.rasul@zalando.de>
Co-authored-by: Mateusz Koren <mateusz.koren@zalando.de>
Co-authored-by: Adele Gouttes <agouttes@bm1-lxslurmctl01.corp.ad.zalando.net>
2020-10-01 15:52:04 +02:00

244 lines
8.5 KiB
Python

from typing import List
import numpy as np
import torch
import torch.nn as nn
from torch.distributions import (
Normal,
Uniform,
Bernoulli)
from torch.nn.utils import clip_grad_norm_
from torch.optim import SGD
from torch.utils.data import TensorDataset, DataLoader
from pts import Trainer
from pts.dataset.repository import get_dataset
from pts.evaluation import make_evaluation_predictions, Evaluator
from pts.model.deepar import DeepAREstimator
from pts.model.simple_feedforward import SimpleFeedForwardEstimator
from pts.modules import (
DistributionOutput,
ImplicitQuantileOutput
)
NUM_SAMPLES = 2000
BATCH_SIZE = 32
TOL = 0.3
START_TOL_MULTIPLE = 1
def inv_softplus(y: np.ndarray) -> np.ndarray:
return np.log(np.exp(y) - 1)
def learn_distribution(
distr_output: DistributionOutput,
samples: torch.Tensor,
init_biases: List[np.ndarray] = None,
num_epochs: int = 5,
learning_rate: float = 1e-2,
):
arg_proj = distr_output.get_args_proj(in_features=1)
if init_biases is not None:
for param, bias in zip(arg_proj.proj, init_biases):
nn.init.constant_(param.bias, bias)
dummy_data = torch.ones((len(samples), 1, 1))
dataset = TensorDataset(dummy_data, samples)
train_data = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
optimizer = SGD(arg_proj.parameters(), lr=learning_rate)
for e in range(num_epochs):
cumulative_loss = 0
num_batches = 0
for i, (data, sample_label) in enumerate(train_data):
optimizer.zero_grad()
distr_args = arg_proj(data)
distr = distr_output.distribution(distr_args)
loss = -distr.log_prob(sample_label).mean()
loss.backward()
clip_grad_norm_(arg_proj.parameters(), 10.0)
optimizer.step()
num_batches += 1
cumulative_loss += loss.item()
print("Epoch %s, loss: %s" % (e, cumulative_loss / num_batches))
sampling_dataloader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
i, (data, sample_label) = next(enumerate(sampling_dataloader))
distr_args = arg_proj(data)
distr = distr_output.distribution(distr_args)
samples = distr.sample((NUM_SAMPLES, ))
with torch.no_grad():
percentile_90 = distr.quantile_function(torch.ones((1, 1, 1)), torch.ones((1, 1)) * 0.9)
percentile_10 = distr.quantile_function(torch.ones((1, 1, 1)), torch.ones((1, 1)) * 0.1)
return samples.mean(), samples.std(), percentile_10, percentile_90
def test_independent_implicit_quantile() -> None:
num_samples = NUM_SAMPLES
# # Normal distrib
distr_mean = torch.Tensor([10.])
distr_std = torch.Tensor([4.])
distr_pp10 = distr_mean - 1.282 * distr_std
distr_pp90 = distr_mean + 1.282 * distr_std
distr = Normal(loc=distr_mean, scale=distr_std)
samples = distr.sample((num_samples,))
learned_mean, learned_std, learned_pp10, learned_pp90 = learn_distribution(
ImplicitQuantileOutput(output_domain="Real"),
samples=samples,
num_epochs=50,
learning_rate=1e-2
)
torch.testing.assert_allclose(learned_mean, distr_mean.squeeze(), rtol=0.1, atol=0.1*10)
torch.testing.assert_allclose(learned_std, distr_std.squeeze(), rtol=0.1, atol=.1*4)
torch.testing.assert_allclose(learned_pp90, distr_pp90.squeeze(), rtol=0.1, atol=.1 * 4)
torch.testing.assert_allclose(learned_pp10, distr_pp10.squeeze(), rtol=0.1, atol=.1 * 4)
# Uniform distrib
a = torch.Tensor([0.])
b = torch.Tensor([20.])
distr_mean = 0.5*(a+b)
distr_std = (1./12.*(b-a)**2)**0.5
distr_pp10 = 0.1 * (a+b)
distr_pp90 = 0.9 * (a+b)
distr = Uniform(low=a, high=b)
samples = distr.sample((num_samples,))
learned_mean, learned_std, learned_pp10, learned_pp90 = learn_distribution(
ImplicitQuantileOutput(output_domain="Positive"),
samples=samples,
num_epochs=50,
learning_rate=1e-2
)
torch.testing.assert_allclose(learned_mean, distr_mean.squeeze(), atol=1., rtol=0.1)
torch.testing.assert_allclose(learned_std, distr_std.squeeze(), atol=0.5, rtol=0.1)
torch.testing.assert_allclose(learned_pp90, distr_pp90.squeeze(), rtol=0.1, atol=.1 * 18)
torch.testing.assert_allclose(learned_pp10, distr_pp10.squeeze(), rtol=0.2, atol=.2 * 2)
# Bernoulli distrib
distr_mean = torch.Tensor([0.2])
distr_std = distr_mean * (1 - distr_mean)
distr_pp10 = torch.Tensor([0.])
distr_pp90 = torch.Tensor([1.])
distr = Bernoulli(probs=distr_mean)
samples = distr.sample((num_samples,))
learned_mean, learned_std, learned_pp10, learned_pp90 = learn_distribution(
ImplicitQuantileOutput(output_domain="Positive"),
samples=samples,
num_epochs=50,
learning_rate=1e-2
)
torch.testing.assert_allclose(learned_mean, distr_mean.squeeze(), atol=1., rtol=0.1)
torch.testing.assert_allclose(learned_std, distr_std.squeeze(), atol=0.5, rtol=0.1)
torch.testing.assert_allclose(learned_pp90, distr_pp90.squeeze(), rtol=0.1, atol=.1 * 18)
torch.testing.assert_allclose(learned_pp10, distr_pp10.squeeze(), rtol=0.1, atol=.1 * 2)
def test_training_with_implicit_quantile_output():
dataset = get_dataset("constant")
metadata = dataset.metadata
deepar_estimator = DeepAREstimator(
distr_output=ImplicitQuantileOutput(output_domain="Real"),
freq=metadata.freq,
prediction_length=metadata.prediction_length,
trainer=Trainer(device="cpu",
epochs=5,
learning_rate=1e-3,
num_batches_per_epoch=3,
batch_size=256,
num_workers=1,
),
input_size=48,
)
deepar_predictor = deepar_estimator.train(dataset.train)
forecast_it, ts_it = make_evaluation_predictions(
dataset=dataset.test, # test dataset
predictor=deepar_predictor, # predictor
num_samples=100, # number of sample paths we want for evaluation
)
forecasts = list(forecast_it)
tss = list(ts_it)
evaluator = Evaluator()
agg_metrics, item_metrics = evaluator(iter(tss), iter(forecasts), num_series=len(dataset.test))
assert agg_metrics["MSE"] > 0
def test_instanciation_of_args_proj():
class MockedImplicitQuantileOutput(ImplicitQuantileOutput):
method_calls = 0
@classmethod
def set_args_proj(cls):
super().set_args_proj()
cls.method_calls += 1
dataset = get_dataset("constant")
metadata = dataset.metadata
distr_output = MockedImplicitQuantileOutput(output_domain="Real")
deepar_estimator = DeepAREstimator(
distr_output=distr_output,
freq=metadata.freq,
prediction_length=metadata.prediction_length,
trainer=Trainer(device="cpu",
epochs=1,
learning_rate=1e-3,
num_batches_per_epoch=1,
batch_size=256,
num_workers=1,
),
input_size=48,
)
assert distr_output.method_calls == 1
deepar_predictor = deepar_estimator.train(dataset.train)
# Method should be called when the MockedImplicitQuantileOutput is instanciated,
# and one more time because in_features is different from 1
assert distr_output.method_calls == 2
forecast_it, ts_it = make_evaluation_predictions(
dataset=dataset.test, # test dataset
predictor=deepar_predictor, # predictor
num_samples=100, # number of sample paths we want for evaluation
)
forecasts = list(forecast_it)
tss = list(ts_it)
evaluator = Evaluator()
agg_metrics, item_metrics = evaluator(iter(tss), iter(forecasts), num_series=len(dataset.test))
assert distr_output.method_calls == 2
# Test that the implicit output module is proper reset
new_estimator = DeepAREstimator(
distr_output=MockedImplicitQuantileOutput(output_domain="Real"),
freq=metadata.freq,
prediction_length=metadata.prediction_length,
trainer=Trainer(device="cpu",
epochs=1,
learning_rate=1e-3,
num_batches_per_epoch=1,
batch_size=256,
num_workers=1,
),
input_size=48,
)
assert distr_output.method_calls == 3
new_estimator.train(dataset.train)
assert distr_output.method_calls == 3 # Since in_feature is the same as before, there should be no additional call