Ejemplo n.º 1
0
 def single_draw(elems):
     values = dict(zip(trace_vars, elems))
     _, st = evaluate_model_posterior_predictive(model,
                                                 values=values,
                                                 observed=observed)
     return tuple([
         (st.untransformed_values[k] if k in st.untransformed_values else
          (st.deterministics[k]
           if k in st.deterministics else st.transformed_values[k]))
         for k in var_names
     ])
Ejemplo n.º 2
0
def sample_posterior_predictive(
    model: ModelType,
    trace: Dict[str, Any],
    var_names: Optional[List[str]] = None,
    observed: Optional[Dict[str, Any]] = None,
) -> Dict[str, np.ndarray]:
    """
    Draw ``sample_shape`` values from the model for the desired ``var_names``.

    Parameters
    ----------
    model : types.GeneratorType, pymc4.Model
        Model to draw samples from
    trace: Dict[str, Any]
        The samples drawn from the model's posterior distribution that should
        be used for sampling from the posterior predictive
    var_names: Optional[List[str]]
        The list of variable names that will be included in the returned
        samples. If ``None``, the samples drawn for all observed
        distributions will be returned in the ``Samples`` dictionary.
    observed : Optional[Dict[str, Any]]
        A dictionary that can be used to override the distribution observed
        values defined in the model.

    Returns
    -------
    Samples: Dict[str, np.ndarray]
        A dictionary of ``var_names`` keys and their corresponding drawn
        samples.

    Examples
    --------
    Lets define a simple model to sample from

    >>> import pymc4 as pm
    >>> @pm.model
    ... def model():
    ...     sd = yield pm.HalfNormal("sd", 5.)
    ...     norm = yield pm.Normal("n", 0, sd, observed=np.random.randn(100))

    Now, we may want to draw samples from the model's posterior to then sample
    from the posterior predictive.

    >>> trace, stats = pm.inference.sampling.sample(model())
    >>> ppc = pm.sample_posterior_predictive(model(), trace)

    The samples are returned as a dictionary with the variable names as keys

    >>> sorted(list(ppc))
    ['model/n']

    The drawn values are the dictionary's values, and their shape will depend
    on the supplied ``trace``

    >>> ppc["model/n"].shape
    (1000, 10, 100)

    """
    # Get a copy of trace because we may manipulate the dictionary later in this
    # function
    trace = trace.copy()
    if var_names is not None and len(var_names) == 0:
        raise ValueError("Supplied an empty var_names list to sample from")

    # We cannot assume that the model is vectorized, so we have batch the
    # pm.evaluate_model_posterior_predictive calls across the trace entries
    # This brings one big problem: we need to infer the batch dimensions from
    # the trace. To do this, we will do
    # 1) A regular single forward pass to determine the variable's shapes
    #    (we'll call these the core shapes)
    # 2) Go through the supplied trace to get each variable's batch shapes
    #    (the shapes to the left of the core shapes)
    # 3) Broadcast the encountered batch shapes between each other as a sanity
    #    check to get the global trace's batch_shape
    # 4) Broadcast the values in the trace to the global batch_shape to get
    #    each variable's broadcasted value.
    # 5) As tf.vectorized_map only iterates across the first dimension, we want
    #    to flatten the batch dimensions. To do this, we reshape the broadcasted
    #    values to (-1,) + core_shape. This way, tf.vectorized_map will be able
    #    to vectorize across the entire batch
    # 6) Collect the samples from, reshape them to batch_shape + core_shape and
    #    return them

    # Do a single forward pass to infer the distributions core shapes and
    # default observeds
    state = evaluate_model_posterior_predictive(model, observed=observed)[1]
    if var_names is None:
        var_names = list(state.posterior_predictives)
    else:
        defined_variables = set(state.all_values) | set(state.deterministics)
        if not set(var_names) <= defined_variables:
            raise KeyError(
                "The supplied var_names = {} are not defined in the model.\n"
                "Defined variables are = {}".format(
                    list(set(var_names) - defined_variables), list(defined_variables),
                )
            )

    # Get the global batch_shape
    batch_shape = tf.TensorShape([])
    trace_names = list(trace)
    for var_name in trace_names:
        values = tf.convert_to_tensor(trace[var_name])
        try:
            core_shape = state.all_values[var_name].shape
        except KeyError:
            if var_name in state.deterministics:
                # Remove the deterministics from the trace
                del trace[var_name]
                continue
            else:
                raise TypeError(
                    "Supplied the variable {} in the trace, yet this variable is "
                    "not defined in the model: {!r}".format(var_name, state)
                )
        assert_values_compatible_with_distribution_shape(var_name, values, core_shape)
        batch_shape = tf.TensorShape(
            tf.broadcast_static_shape(
                values.shape[: len(values.shape) - len(core_shape)], batch_shape,
            )
        )

    # Flatten the batch axis
    flattened_trace = []
    for k, v in trace.items():
        core_shape = tf.TensorShape(state.all_values[k].shape)
        batched_val = tf.broadcast_to(v, batch_shape + core_shape)
        flattened_trace.append(tf.reshape(batched_val, shape=[-1] + core_shape.as_list()))
    trace_vars = list(trace)

    # Setup the function that makes a single draw
    @tf.function(autograph=False)
    def single_draw(elems):
        values = dict(zip(trace_vars, elems))
        _, st = evaluate_model_posterior_predictive(model, values=values, observed=observed)
        return tuple(
            [
                (
                    st.untransformed_values[k]
                    if k in st.untransformed_values
                    else (
                        st.deterministics[k] if k in st.deterministics else st.transformed_values[k]
                    )
                )
                for k in var_names
            ]
        )

    # Make draws in parallel across the batch elements with tf.vectorized_map
    samples = tf.vectorized_map(single_draw, flattened_trace)

    # Convert the samples to ndarrays and make a dictionary with the correct
    # batch_shape + core_shape
    output = dict()
    for name, sample in zip(var_names, samples):
        sample = sample.numpy()
        output[name] = np.reshape(sample, batch_shape + sample.shape[1:])
    return output
Ejemplo n.º 3
0
def sample_posterior_predictive(
    model: ModelType,
    trace: InferenceData,
    var_names: Optional[Union[str, List[str]]] = None,
    observed: Optional[Dict[str, Any]] = None,
    use_auto_batching: bool = True,
    inplace: bool = True,
) -> InferenceData:
    """
    Draw ``sample_shape`` values from the model for the desired ``var_names``.

    Parameters
    ----------
    model : types.GeneratorType, pymc4.Model
        Model to draw samples from
    trace: ArviZ's InferenceData object
        The samples drawn from the model's posterior distribution that should be used for sampling
        from the posterior predictive
    var_names: Optional[Union[str, List[str]]]
        The list of variable names that will be included in the returned samples. Strings can be
        used to specify a single variable. If ``None``, the samples drawn for all observed
        distributions will be returned in the ``Samples`` dictionary.
    observed : Optional[Dict[str, Any]]
        A dictionary that can be used to override the distribution observed values defined in the
        model.
    use_auto_batching: bool
        A bool value that indicates whether ``sample_posterior_predictive`` should automatically
        batch the draws or not. If you are sure you have manually tuned your model to be fully
        vectorized, then you can set this to ``False``, and your sampling should be faster than the
        auto batched counterpart. If you are not sure if your model is vectorized, then auto
        batching will safely sample from it but with some additional overhead.
    inplace: If True (default) it will add a posterior_predictive group to the provided ``trace``,
        instead of returning a new InferenceData object. If a posterior_predictive group is already
        present in ``trace`` it will be overwritten.

    Returns
    -------
    Samples: InferenceDataType
        An ArviZ's InferenceData object with a posterior_predictive group

    Examples
    --------
    Lets define a simple model to sample from

    >>> import pymc4 as pm
    >>> @pm.model
    ... def model():
    ...     sd = yield pm.HalfNormal("sd", 5.)
    ...     norm = yield pm.Normal("n", 0, sd, observed=np.random.randn(100))

    Now, we may want to draw samples from the model's posterior to then sample from the posterior
    predictive.

    >>> trace = pm.inference.sampling.sample(model())
    >>> ppc = pm.sample_posterior_predictive(model(), trace).posterior_predictive

    The samples are returned as a dictionary with the variable names as keys

    >>> sorted(list(ppc))
    ['model/n']

    The drawn values are the dictionary's values, and their shape will depend
    on the supplied ``trace``

    >>> ppc["model/n"].shape
    (10, 1000, 100)

    """
    if var_names is not None and len(var_names) == 0:
        raise ValueError("Supplied an empty var_names list to sample from")
    if isinstance(var_names, str):
        var_names = [var_names]

    # If we don't have to deal with auto-batching we can simply evaluate_model
    # passing the trace as values
    if not use_auto_batching:
        values = {
            var_name: tf.convert_to_tensor(value)
            for var_name, value in trace.posterior.items()
        }
        # We need to pass the number of chains and draws as sample_shape for
        # observed conditionally independent variables
        sample_shape = (trace.posterior.sizes["chain"],
                        trace.posterior.sizes["draw"])
        _, state = evaluate_model_posterior_predictive(
            model, values=values, observed=observed, sample_shape=sample_shape)
        all_values = collections.ChainMap(state.all_values,
                                          state.deterministics_values)
        if var_names is None:
            var_names = list(state.posterior_predictives)
        output = {k: all_values[k] for k in var_names}
        return trace_to_arviz(trace=trace,
                              posterior_predictive=output,
                              inplace=inplace)

    # We cannot assume that the model is vectorized, so we have batch the
    # pm.evaluate_model_posterior_predictive calls across the trace entries
    # This brings one big problem: we need to infer the batch dimensions from
    # the trace. To do this, we will do
    # 1) A single forward pass with the meta executor to determine the
    #    variable's shapes (we'll call these the core shapes)
    # 2) Go through the supplied trace to get each variable's batch shapes
    #    (the shapes to the left of the core shapes)
    # 3) Broadcast the encountered batch shapes between each other as a sanity
    #    check to get the global trace's batch_shape
    # 4) Broadcast the values in the trace to the global batch_shape to get
    #    each variable's broadcasted value.
    # 5) As tf.vectorized_map only iterates across the first dimension, we want
    #    to flatten the batch dimensions. To do this, we reshape the broadcasted
    #    values to (-1,) + core_shape. This way, tf.vectorized_map will be able
    #    to vectorize across the entire batch
    # 6) Collect the samples from, reshape them to batch_shape + core_shape and
    #    return them

    # Do a single forward pass to infer the distributions core shapes and
    # default observeds
    _, state = evaluate_meta_posterior_predictive_model(model,
                                                        observed=observed)
    if var_names is None:
        var_names = list(state.posterior_predictives)
    else:
        defined_variables = set(state.all_values) | set(
            state.deterministics_values)
        if not set(var_names) <= defined_variables:
            raise KeyError(
                "The supplied var_names = {} are not defined in the model.\n"
                "Defined variables are = {}".format(
                    list(set(var_names) - defined_variables),
                    list(defined_variables)))

    # Get the global batch_shape
    batch_shape = tf.TensorShape([])
    # Get a copy of trace because we may manipulate the dictionary later in this
    # function
    posterior = trace.posterior.copy()  # type: ignore
    posterior_names = list(posterior)
    for var_name in posterior_names:
        values = tf.convert_to_tensor(posterior[var_name].values)
        try:
            core_shape = state.all_values[var_name].shape
        except KeyError:
            if var_name in state.deterministics_values:
                # Remove the deterministics from the trace
                del posterior[var_name]
                continue
            else:
                raise TypeError(
                    "Supplied the variable {} in the trace, yet this variable is "
                    "not defined in the model: {!r}".format(var_name, state))
        assert_values_compatible_with_distribution_shape(
            var_name,
            values,
            batch_shape=tf.TensorShape([]),
            event_shape=core_shape)
        batch_shape = tf.TensorShape(
            tf.broadcast_static_shape(
                values.shape[:len(values.shape) -
                             len(core_shape)],  # type: ignore
                batch_shape,
            ))

    # Flatten the batch axis
    flattened_posterior = []
    for k, v in posterior.items():
        core_shape = tf.TensorShape(state.all_values[k].shape)
        batched_val = tf.broadcast_to(v.values, batch_shape + core_shape)
        flattened_posterior.append(
            tf.reshape(batched_val, shape=[-1] + core_shape.as_list()))
    posterior_vars = list(posterior)
    # Setup the function that makes a single draw
    @tf.function(autograph=False)
    def single_draw(elems):
        values = dict(zip(posterior_vars, elems))
        _, st = evaluate_model_posterior_predictive(model,
                                                    values=values,
                                                    observed=observed)
        return tuple([
            (st.untransformed_values[k] if k in st.untransformed_values else
             (st.deterministics_values[k]
              if k in st.deterministics_values else st.transformed_values[k]))
            for k in var_names
        ])

    # Make draws in parallel across the batch elements with tf.vectorized_map
    samples = tf.vectorized_map(single_draw, flattened_posterior)
    # Convert the samples to ndarrays and make a dictionary with the correct
    # batch_shape + core_shape
    output = dict()
    for name, sample in zip(var_names, samples):
        sample = sample.numpy()
        output[name] = np.reshape(sample, batch_shape + sample.shape[1:])
    return trace_to_arviz(trace=trace,
                          posterior_predictive=output,
                          inplace=inplace)