Example #1
0
    def model(self, zero_data, covariates):
        with pyro.plate("batch", len(zero_data), dim=-2):
            zero_data = pyro.subsample(zero_data, event_dim=1)
            covariates = pyro.subsample(covariates, event_dim=1)

            loc = zero_data[..., :1, :]
            scale = pyro.sample("scale", dist.LogNormal(loc, 1).to_event(1))

            with self.time_plate:
                jumps = pyro.sample("jumps", dist.Normal(0, scale).to_event(1))
            prediction = jumps.cumsum(-2)

            duration, obs_dim = zero_data.shape[-2:]
            noise_dist = dist.LinearHMM(
                dist.Stable(1.9, 0).expand([obs_dim]).to_event(1),
                torch.eye(obs_dim),
                dist.Stable(1.9, 0).expand([obs_dim]).to_event(1),
                torch.eye(obs_dim),
                dist.Stable(1.9, 0).expand([obs_dim]).to_event(1),
                duration=duration,
            )
            rep = StableReparam()
            with poutine.reparam(
                    config={"residual": LinearHMMReparam(rep, rep, rep)}):
                self.predict(noise_dist, prediction)
Example #2
0
    def model(self, zero_data, covariates):
        assert zero_data.size(-1) == 1  # univariate
        num_stores, num_products, duration, one = zero_data.shape
        time_index = covariates.squeeze(-1)

        store_plate = pyro.plate("store", num_stores, dim=-3)
        product_plate = pyro.plate("product", num_products, dim=-2)
        day_of_week_plate = pyro.plate("day_of_week", 7, dim=-1)

        snap = self.snap[..., time_index, :]
        # subsample the data
        with product_plate:
            dept = pyro.subsample(self.dept, event_dim=1)
            saled = pyro.subsample(self.saled, event_dim=1)[..., time_index, :]
            log_ma = pyro.subsample(self.log_ma, event_dim=1)[...,
                                                              time_index, :]

        # we construct latent variables for each store and each department;
        # here, we declare department dimension as event dimension for simplicity,
        # (nb: the numbers of products in each department are different)
        # the last event dimension is used to model mean/scale separately.
        with store_plate:
            ma_weight = pyro.sample(
                "ma_weight",
                dist.Normal(0, 1).expand([2, log_ma.size(-1), 7]).to_event(3))
            ma_weight = ma_weight.matmul(
                dept.unsqueeze(-2).unsqueeze(-1)).squeeze(-1)
            moving_average = ma_weight.matmul(log_ma.unsqueeze(-1)).squeeze(-1)

            snap_weight = pyro.sample(
                "snap_weight",
                dist.Normal(0, 1).expand([2, 7]).to_event(2))
            snap_weight = snap_weight.matmul(dept.unsqueeze(-1)).squeeze(-1)
            snap_effect = snap_weight * snap

            with day_of_week_plate:
                seasonal = pyro.sample(
                    "seasonal",
                    dist.Normal(0, 1).expand([2, 7]).to_event(2))
            seasonal = seasonal.matmul(dept.unsqueeze(-1)).squeeze(-1)
            seasonal = periodic_repeat(seasonal, duration, dim=-2)

        prediction = moving_average + snap_effect + seasonal
        log_mean, log_scale = prediction[..., :1], prediction[..., 1:]
        # we add a pretty small bias 1e-3 to avoid the case mean=scale=0
        # either when saled == 0 or saled == 1
        mean = bounded_exp(log_mean) * saled + 1e-3
        scale = bounded_exp(log_scale) * saled + 1e-3

        rate = scale.reciprocal()
        concentration = mean * rate
        # alternative: GammaPoisson (or NegativeBinomial, ZeroInflatedNegativeBinomial)
        noise_dist = dist.Gamma(concentration, rate)

        with store_plate, product_plate:
            self.predict(noise_dist, mean.new_zeros(mean.shape))
Example #3
0
 def model(x, y=None, batch_size=None):
     loc = pyro.param("loc", lambda: torch.tensor(0.))
     scale = pyro.param("scale", lambda: torch.tensor(1.),
                        constraint=constraints.positive)
     with pyro.plate("batch", len(x), subsample_size=batch_size):
         batch_x = pyro.subsample(x, event_dim=0)
         batch_y = pyro.subsample(y, event_dim=0) if y is not None else None
         mean = loc + scale * batch_x
         sigma = pyro.sample("sigma", dist.LogNormal(0., 1.))
         return pyro.sample("obs", dist.Normal(mean, sigma), obs=batch_y)
Example #4
0
 def model(data):
     size, size = data.shape
     origin_plate = pyro.plate("origin", size, dim=-2)
     destin_plate = pyro.plate("destin", size, dim=-1)
     with origin_plate, destin_plate:
         batch = pyro.subsample(data, event_dim=0)
         assert batch.size(0) == batch.size(1), batch.shape
         pyro.sample("obs", dist.Normal(0, 1), obs=batch)
Example #5
0
    def model(self, zero_data, covariates):
        with pyro.plate("batch", len(zero_data), dim=-2):
            zero_data = pyro.subsample(zero_data, event_dim=1)
            covariates = pyro.subsample(covariates, event_dim=1)

            loc = zero_data[..., :1, :]
            scale = pyro.sample("scale", dist.LogNormal(loc, 1).to_event(1))

            with self.time_plate:
                jumps = pyro.sample("jumps", dist.Normal(0, scale).to_event(1))
            prediction = jumps.cumsum(-2)

            duration, obs_dim = zero_data.shape[-2:]
            noise_dist = dist.GaussianHMM(
                dist.Normal(0, 1).expand([obs_dim]).to_event(1),
                torch.eye(obs_dim),
                dist.Normal(0, 1).expand([obs_dim]).to_event(1),
                torch.eye(obs_dim),
                dist.Normal(0, 1).expand([obs_dim]).to_event(1),
                duration=duration,
            )
            self.predict(noise_dist, prediction)
Example #6
0
    def predict(self, noise_dist, prediction):
        """
        Prediction function, to be called by :meth:`model` implementations.

        This should be called outside of the  :meth:`time_plate`.

        This is similar to an observe statement in Pyro::

            pyro.sample("residual", noise_dist,
                        obs=(data - prediction))

        but with (1) additional reshaping logic to allow time-dependent
        ``noise_dist`` (most often a :class:`~pyro.distributions.GaussianHMM`
        or variant); and (2) additional logic to allow only a partial
        observation and forecast the remaining data.

        :param noise_dist: A noise distribution with ``.event_dim in {0,1,2}``.
            ``noise_dist`` is typically zero-mean or zero-median or zero-mode
            or somehow centered.
        :type noise_dist: ~pyro.distributions.Distribution
        :param prediction: A prediction for the data. This should have the same
            shape as ``data``, but broadcastable to full duration of the
            ``covariates``.
        :type prediction: ~torch.Tensor
        """
        assert self._data is not None, ".predict() called outside .model()"
        assert self._forecast is None, ".predict() called twice"
        assert isinstance(noise_dist, dist.Distribution)
        assert isinstance(prediction, torch.Tensor)
        if noise_dist.event_dim == 0:
            if noise_dist.batch_shape[-2:] != prediction.shape[-2:]:
                noise_dist = noise_dist.expand(noise_dist.batch_shape[:-2] +
                                               prediction.shape[-2:])
            noise_dist = noise_dist.to_event(2)
        elif noise_dist.event_dim == 1:
            if noise_dist.batch_shape[-1:] != prediction.shape[-2:-1]:
                noise_dist = noise_dist.expand(noise_dist.batch_shape[:-1] +
                                               prediction.shape[-2:-1])
            noise_dist = noise_dist.to_event(1)
        assert noise_dist.event_dim == 2
        assert noise_dist.event_shape == prediction.shape[-2:]

        # The following reshaping logic is required to reconcile batch and
        # event shapes. This would be unnecessary if Pyro used name dimensions
        # internally, e.g. using Funsor.
        #
        #     batch_shape                    | event_shape
        #     -------------------------------+----------------
        #  1. sample_shape + shape + (time,) | (obs_dim,)
        #  2.           sample_shape + shape | (time, obs_dim)
        #  3.    sample_shape + shape + (1,) | (time, obs_dim)
        #
        # Parameters like noise_dist.loc typically have shape as in 1.  However
        # calling .to_event(1) will shift the shapes resulting in 2., where
        # sample_shape+shape will be misaligned with other batch shapes in the
        # trace. To fix this the following logic "unsqueezes" the distribution,
        # resulting in correctly aligned shapes 3. Note the "time" dimension is
        # effectively moved from a batch dimension to an event dimension.
        noise_dist = reshape_batch(noise_dist, noise_dist.batch_shape + (1, ))
        data = pyro.subsample(self._data.unsqueeze(-3), event_dim=2)
        prediction = prediction.unsqueeze(-3)

        # Create a sample site.
        t_obs = data.size(-2)
        t_cov = prediction.size(-2)
        if t_obs == t_cov:  # training
            pyro.sample("residual", noise_dist, obs=data - prediction)
            self._forecast = data.new_zeros(data.shape[:-2] + (0, ) +
                                            data.shape[-1:])
        else:  # forecasting
            left_pred = prediction[..., :t_obs, :]
            right_pred = prediction[..., t_obs:, :]

            # This prefix_condition indirection is needed to ensure that
            # PrefixConditionMessenger is handled outside of the .model() call.
            self._prefix_condition_data["residual"] = data - left_pred
            noise = pyro.sample("residual", noise_dist)
            del self._prefix_condition_data["residual"]

            assert noise.shape[-data.dim():] == right_pred.shape[-data.dim():]
            self._forecast = right_pred + noise

        # Move the "time" batch dim back to its original place.
        assert self._forecast.size(-3) == 1
        self._forecast = self._forecast.squeeze(-3)