Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Hierarchical] Allow computing projection based on a non-Euclidean norm #2961

Merged
merged 7 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions src/gluonts/mx/model/deepvar_hierarchical/_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ def constraint_mat(S: np.ndarray) -> np.ndarray:
return A


def null_space_projection_mat(A: np.ndarray) -> np.ndarray:
def null_space_projection_mat(
A: np.ndarray,
D: Optional[np.ndarray] = None,
) -> np.ndarray:
"""
Computes the projection matrix for projecting onto the null space of A.

Expand All @@ -82,14 +85,32 @@ def null_space_projection_mat(A: np.ndarray) -> np.ndarray:
A
The constraint matrix A in the equation: Ay = 0 (y being the
values/forecasts of all time series in the hierarchy).
D
Symmetric positive definite matrix (typically a diagonal matrix).
Optional.
If provided then the distance between the reconciled and unreconciled
forecasts is calculated based on the norm induced by D. Useful for
weighing the distances differently for each level of the hierarchy.
By default Euclidean distance is used.

Returns
-------
Numpy ND array
Projection matrix, shape (total_num_time_series, total_num_time_series)
"""
num_ts = A.shape[1]
return np.eye(num_ts) - A.T @ np.linalg.pinv(A @ A.T) @ A
if D is None:
return np.eye(num_ts) - A.T @ np.linalg.pinv(A @ A.T) @ A
else:
assert np.all(D == D.T), "`D` must be symmetric."
assert np.all(
np.linalg.eigvals(D) > 0
), "`D` must be positive definite."

D_inv = np.linalg.inv(D)
melopeo marked this conversation as resolved.
Show resolved Hide resolved
return (
np.eye(num_ts) - D_inv @ A.T @ np.linalg.pinv(A @ D_inv @ A.T) @ A
)


class DeepVARHierarchicalEstimator(DeepVAREstimator):
Expand All @@ -109,6 +130,12 @@ class DeepVARHierarchicalEstimator(DeepVAREstimator):
Length of the prediction horizon
S
Summation or aggregation matrix.
D
Positive definite matrix (typically a diagonal matrix). Optional.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we specify "symmetric" positive definite matrix?

If provided then the distance between the reconciled and unreconciled
forecasts is calculated based on the norm induced by D. Useful for
weighing the distances differently for each level of the hierarchy.
By default Euclidean distance is used.
num_samples_for_loss
Number of samples to draw from the predicted distribution to compute
the training loss.
Expand Down Expand Up @@ -195,6 +222,7 @@ def __init__(
freq: str,
prediction_length: int,
S: np.ndarray,
D: Optional[np.ndarray] = None,
num_samples_for_loss: int = 200,
likelihood_weight: float = 0.0,
CRPS_weight: float = 1.0,
Expand Down Expand Up @@ -273,7 +301,7 @@ def __init__(
), "Cannot project only during training (and not during prediction)"

A = constraint_mat(S.astype(self.dtype))
M = null_space_projection_mat(A)
M = null_space_projection_mat(A=A, D=D)
ctx = self.trainer.ctx
self.M = mx.nd.array(M, ctx=ctx)
self.A = mx.nd.array(A, ctx=ctx)
Expand Down
22 changes: 19 additions & 3 deletions test/mx/model/deepvar_hierarchical/test_reconcile_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@

num_bottom_ts = S.shape[1]
A = constraint_mat(S)
reconciliation_mat = null_space_projection_mat(A)


@pytest.mark.parametrize(
Expand All @@ -53,6 +52,23 @@
100.0 + 2.0 * np.random.standard_normal(size=(10, 32, S.shape[0])),
],
)
@pytest.mark.parametrize(
"D",
[
None,
# Root gets the maximum weight and the two aggregated levels get
# more weight than the leaf level.
np.diag([4, 2, 2, 1, 1, 1, 1]),
# Random diagonal matrix
np.diag(np.random.rand(S.shape[0])),
# Random positive definite matrix
np.diag(np.random.rand(S.shape[0]))
+ np.dot(
np.array([[4, 2, 2, 1, 1, 1, 1]]).T,
np.array([[4, 2, 2, 1, 1, 1, 1]]),
),
],
)
@pytest.mark.parametrize(
"seq_axis",
[
Expand All @@ -63,9 +79,9 @@
[1, 0],
],
)
def test_reconciliation_error(samples, seq_axis):
def test_reconciliation_error(samples, D, seq_axis):
coherent_samples = reconcile_samples(
reconciliation_mat=mx.nd.array(reconciliation_mat),
reconciliation_mat=mx.nd.array(null_space_projection_mat(A=A, D=D)),
samples=mx.nd.array(samples),
seq_axis=seq_axis,
)
Expand Down
Loading