<jupyter_start><jupyter_text>Welcome to BrancherBrancher is a user-centered probabilistic differentiable programming library. Brancher is designed to be as beginner friendly as possible and to minimize boilerplate code while retaining all the power and flexibility of general-purpose differentiable probabilistic programming and deep learning. Brancher is built around the deep learning framework [Chainer](https://chainer.org/). In this notebook I will explain how to use Brancher for writing your own differentiable probabilistic program and perform variational inference in a/only few lines of code. Let's start by importing some modules!<jupyter_code>import sys
sys.path.append('/home/luca/GitRepositories/Brancher')

import chainer
import matplotlib.pyplot as plt

from brancher.variables import DeterministicVariable, ProbabilisticModel
from brancher.standard_variables import NormalVariable, LogNormalVariable
from brancher import inference
import brancher.functions as BF<jupyter_output><empty_output><jupyter_text>Brancher is an object oriented library. This means that all objects in brancher are instantiation of classes that define the abstractions required for probabilistic programming. The foundamental building block of all Brancher programs is the RandomVariable class. probabilistic models are constructed by linking random variables through differentiable functions. RandomVariables are characterized by a probability distribution, a set of parent variables and a link that specifies how the values of the parents determine the parameters of its distribution.For example, let's define a model where the mean of a normal random variable (mu) is determined by the sine of another normal random variable. Brancher allows you to define his model symbolically like you would do in a scientific paper:<jupyter_code># Normal model
nu = LogNormalVariable(mu=0., sigma=1., name="nu")
mu = NormalVariable(mu=0., sigma=10., name="mu")
x = NormalVariable(mu=BF.sin(mu), sigma=nu, name="x")<jupyter_output><empty_output><jupyter_text>You have to admit that this is a pretty clean sintax! The classes LogNormalVariable and  NormalVariable are subclasses of RandomVariables. The [log normal](https://en.wikipedia.org/wiki/Log-normal_distribution) variable is obtained by taking the exponential of a normal random variable. This is a known special case of the differentiable probabilistic programming approach of obtaining new probability distributions by applying differentiable functions on a small set of basic distributions such as the normal.Let's now define a probabilistic model with all our variables:<jupyter_code>model = ProbabilisticModel([x, mu, nu])<jupyter_output><empty_output><jupyter_text>Since mu and nu are parents of x, ProbabilisticModel([x])This expression is slightly reduntant because ProbabilisticModel([x]) already contains all the variables since mu and nu are parents of x. However, I recommend explicitly inputing all the variables for making your code more readable. We can now print a summary of our model:<jupyter_code>model.model_summary<jupyter_output><empty_output><jupyter_text>As we would expect, the variable x has mu and nu as parents. However, you might be surprised by the fact that there are new variables such as mu_mu and mu_sigma whioch we did not explicitly defined. These are deterministic variables that represent the fixed values of the parameters of our distribution. DeterministicVariables are a special case of BrancherVariables, they behave pretty much like RandomVariables expect their value being deterministic. As you can see, often you will not need to define these variables explicitly since Brancher will constract them for you when you input numbers or np.array as parameters for your RandomVariables.All the variables are currently marked as not observed. We will see how to "observe" some variables by inputing data later on.Let's now have a look to sampling: the most important operation you can do on probabilisticModels!<jupyter_code># Print samples
sample = model.get_sample(10)
sample<jupyter_output><empty_output><jupyter_text>As you can see, if we sample from the ProbabilisticModel we get samples for all the variables in our model. We can also sample from the individual variables:<jupyter_code># Print samples from single variable
x_sample = x.get_sample(10)
x_sample<jupyter_output><empty_output><jupyter_text>Note that Brancher is still sampling all the parents of x recursively (parents of the parents and so on) since it needs all these values for sampling x.Another thing we can do is to sample after inputing the values of some variables. This converts the ProbabilisticModel into a random function that takes some input and output some samples with the probability distribution defined by the model:<jupyter_code># Print samples conditional on an input
in_sample = model.get_sample(10, input_values={mu: 100.})
in_sample<jupyter_output><empty_output><jupyter_text>Note that this is equivalent to conditioning the model on the value of some variables and sampling everything downstream (children, grandchildren and so on). In order to sample upstream of some known values we need to define observation and use variational inference for getting an approximate posterior distribution. Let's begin by defining some ground truth values for the mu and nu variables and generate some synthetic observations:<jupyter_code># # Generate data
nu_real = 0.5
mu_real = -1.
data = x.get_sample(number_samples=100, input_values={mu: mu_real, nu: nu_real})<jupyter_output><empty_output><jupyter_text>We can now tell Brancher that the x variable is observed with the values specified by our data. In this case data is a Pandas dataframe since it is the output of the get_sample method. However you can also input a single number or an np.array of any shape (the first dimension of the array is going to be the batch dimension while the other define tensor values observations).<jupyter_code># Observe data
x.observe(data)
model.model_summary<jupyter_output><empty_output><jupyter_text>As you can see the x variable is now observed. If you want to sample the variables mu and nu that are downstream x you need to perform approximate Bayesian inference. In Brancher, you can do this by defining a variational distribution for all the variables you want to sample from. The variational model is a probabilistic model itself and it is constructed in the very same way of our original probabilistic model.if you want to go deeper into the theory behind the variational approximation you can start with the [wikipedia page](https://en.wikipedia.org/wiki/Variational_Bayesian_methods) or excellent textbooks such as [Pattern recognition and machine learning](https://cds.cern.ch/record/998831/files/9780387310732_TOC.pdf).The simplest way of specifying this distribution is the use the same distributions you had in your original model:<jupyter_code># Variational model
Qnu = LogNormalVariable(0., 1., "nu", learnable=True)
Qmu = NormalVariable(0., 1., "mu", learnable=True)
model.set_posterior_model(ProbabilisticModel([Qmu, Qnu]))<jupyter_output><empty_output><jupyter_text>We now need to use some stochastic optimization (usual deep leaning stuff...) for learning the parameters of our variational approximation. This technique is called stochastic variational inference and it is extremely powerful since it neatly fit Bayesian inference into the deep learning framework (indeed brancher is designed to work with deep neural networks as building blocks for complex probabilistic models).We told Brancher that the parameters of the variational distributions are learnable using the "learnable" flag. Let's now learn those parameters:<jupyter_code># Inference
inference.stochastic_variational_inference(model,
                                           number_iterations=500,
                                           number_samples=50,
                                           optimizer=chainer.optimizers.Adam(0.005))
loss_list = model.diagnostics["loss curve"]<jupyter_output>0%|          | 0/500 [00:00<?, ?it/s]<jupyter_text>Et voilà, the approximate posterior distribution is trained. The number_samples parameter specify the minibatch size of the parameters sampled from the variational distribution. You need to know something about stochastic variational inference for fully getting this point and I will cover it more properly in more advanced tutorials.Let's now plot the loss to be sure that everything went fine.<jupyter_code>plt.plot(loss_list)<jupyter_output><empty_output><jupyter_text>It looks ok! note however that this optimization can potentially get stuck in local minima like any other gradient based optimization so be careful. Stochastic variational inference is a powerful general purpose inference method but it does not do magic.Let's now get some samples from the posterior:<jupyter_code># print posterior sample
post_sample = model.get_posterior_sample(1000)
post_sample.describe()<jupyter_output><empty_output><jupyter_text>The method describe gets some important descriptive statistics from the samples. It is just a native Pandas method and we can use it because the output of Brancher are Pandas dataframes. It is nice to work on the shoulders of other people isn't it? [Pandas](https://pandas.pydata.org/pandas-docs/stable/) has plenty of methods for working with data and I recommend you to use them in your Brancher pipelines! I suggest you to get started with Pandas with [this tutorial] (https://pandas.pydata.org/pandas-docs/stable/10min.html).Let's now plot the posterior distributions together with the ground truth values:<jupyter_code>g = plt.hist(post_sample["mu"], 50)
plt.axvline(x=mu_real, color="k", lw=2)
g = plt.hist(post_sample["nu"], 50)
plt.axvline(x=nu_real, color="k", lw=2)<jupyter_output><empty_output><jupyter_text>We can visualize the posterior distribution using a Brancher plotting function. This function relies on [Seaborn](https://seaborn.pydata.org/), a very handy visualization library that works very well in combination with Pandas.<jupyter_code>from brancher.visualizations import plot_posterior

plot_posterior(model, variables=["mu", "nu", "x"])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.054755607797041826, 'fraction_numerical': 0.0071505534332451754, 'mean_word_length': 5.608414239482201, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 60, 'https://': 7, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '408985', 'license': 'MIT', 'n_tokens_mistral': 2647, 'n_tokens_neox': 2488, 'n_words': 1234, 'path': 'examples/notebooks/Introduction_to_Brancher.ipynb', 'repo_name': 'LucaAmbrogioni/Brancher_chainer_backend', 'size': '10209'}