continious Active Inference on the RxInfer platform #474
Replies: 22 comments 2 replies
-
|
@ThijsvdLaar @ofSingularMind do you want to collaborate on this? |
Beta Was this translation helpful? Give feedback.
-
|
@Charelvanhoof Thanks for opening this discussion and for sharing your notebooks, they are a very nice resource. I like the idea of an education-oriented narrative for explaining continuous-time active inference (CT AIF), with open-source notebooks that exemplify crucial design decisions in the framework. @Charelvanhoof @ofSingularMind If you're interested, we can start to take CT AIF apart so to speak, and then explain the ingredients that make it tick. I think that alternative design choices for CT AIF can be made (along the way), and RxInfer can play a role here (even without much modification). For example, I've been trying to implement Friston and Ao's 2012 paper on "Free Energy, Value and Attractors" with RxInfer, and see the following connections:
I've been meaning to collect examples of reactive AIF implementations into a public repository, https://github.com/biaslab/RxAIF, so others may learn and take inspiration (but I haven't gotten very far yet). I can already share some materials on CT AIF in this repo (throughout the coming weeks, because I'm currently on leave), and we can collaborate if you like. |
Beta Was this translation helpful? Give feedback.
-
|
Hi all-- Thanks @Charelvanhoof for opening this discussion. Perhaps I can add my view on this. I think there are two different threads in this issue. One is the simulation of continuous-time dynamical systems, and the other is the simulation of active inference (AIF) processes. I recommend that we consider these issues separately. In principle, all dynamical systems are simulated using discrete time steps, regardless of whether they are described on paper by differential equations (i.e., continuous-time systems) or by difference equations (i.e., discrete-time systems). The default in RxInfer is to define and simulate systems by difference equations. @Ismail Senoz has worked on the simulation of CT dynamical systems in RxInfer, which essentially comes down to simulating by adaptive time step durations. We can ask @ismailsenoz for more input here. The simulation of an AIF system (agent + environment) is nothing but the simulation of a dynamical system. By itself, transitioning from discrete-time to continuous-time simulation (or vice versa) is conceptually unrelated to AIF—it's merely a change in the simulation paradigm. So, going forward, I recommend using difference equations for both model specification and for simulation of the inference process, as RxInfer far better supports this. Once that works, we can consider whether the dynamical AIF system can also be specified by differential equations and if the continuous-time inference process can be simulated in RxInfer using the techniques that Ismail has worked on. As for the second thread, how to simulate (discrete-time) AIF processes in RxInfer, I would consider the following. The expected free energy (EFE) is not a variational free energy (VFE), but rather a cost function for policies. In almost all of the AIF literature, it is suggested to compute EFE for a "smartly selected" set of policy candidates (represented by: u) and then assign a prior "softmax(-G(u))" to those candidates. This is a method that (1) is not consistent with the FEP idea that FE minimization is the only driving process, and (2) does not scale well (since the "smartly selected"-issue leads to tree-search algorithms that explode for large-dimensional state spaces and/or deep horizons). I recommend following Theorem 1 in https://arxiv.org/abs/2504.14898, which reframes EFE-based planning as an emergent property of regular variational free energy (VFE) minimization in a generative model-rollout into the future, terminated by both target priors and epistemic priors. This approach is, in principle, directly realizable in RxInfer. As an example, consider the slide below, which shows some key code fragments of multiple agents that need to move to known end points without collisions. The code already includes a generative model and target priors. Inference in this model leads to the agents being moved by a "KL control" driving force. In order to turn this agent into an AIF agent, the code only needs to be extended by adding epistemic priors (eqs 9 in https://arxiv.org/abs/2504.14898). This is not entirely straightforward, but we are working on a nice user interface for RxInfer to support this. @Wouter Nuijten is the most informed person about this work. Going forward, I recommend syncing with @wouterwln on when and how epistemic priors are supported in RxInfer. In summary, I recommend proceeding as follows:
Generally speaking, BIASlab and Lazy Dynamics members can help with tasks 3 and 4 if you encounter problems. In particular, @wouterwln is the expert in helping with task 3. @bvdmitri and other Lazy Dynamics members are experts if task 4 (the inference process) fails. Everybody else in BIASlab can also help, either by themselves or in finding appropriate help. If the above works, then the same recipe can be used for all other agents. As discussed, the automatic simulation of continuous-time inference processes in RxInfer is a separate issue that needs the involvement of @ismailsenoz. I recommend not considering it until we have many AIF agents working smoothly by discrete-time simulations. |
Beta Was this translation helpful? Give feedback.
-
|
First of all, my apologies, I missed these replies. All notifications went to my old Volvocars account, where I worked in the past. And I did not have in my routine to check the github discussion pages. I have repaired the settings, so I can react next time. |
Beta Was this translation helpful? Give feedback.
-
|
Summary of our pair programming session:
It should now be possible to calculate action with the prediction error formula. That action can then be fed to the environment at each timestep, which would generate the next observation (thus completing the loop). Next steps are:
If there's anything I missed or needs to be corrected, let me know. |
Beta Was this translation helpful? Give feedback.
-
|
My thanks and good meeting! I have been testing/reviewing/playing with the code and see if I can reproduce the typical active inference graphs. I have build the same typical example in RxInfer as I presented in IWAI If I run the code I get this result: The hidden state estimate will move between the prior and sensory observations, closer to the prior if the prior has a higher precision, and closer to the sensory observations if these have a higher precision. (and is this example both precisions given as input). With RxInfer code it looks like the prior is 'ignored' and the sensory observation drives the hidden state estimate. Note that in both examples there is no action (yet), it is pure hidden state inference. I suspect the issue it is in the model definition, probably because x_init is a Gaussian and mu_v a real? end |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
|
Hi Charel, I promised to post a bug report of the error you encountered. However, I can't seem to reproduce it on my end. The code below runs without errors. using RxInfer
using Distributions
g(x) = 25 - 16 / (1 + exp(5 - x / 5))
@model function mwe(y,u,prior, s2_x, s2_y)
x_prev ~ prior
for i in 1:length(y)
x[i] ~ Normal(mean=x_prev-u, var=s2_x)
gk[i] ~ g(x[i]) where { meta = DeltaMeta(method = Linearization()) }
y[i] ~ Normal(mean=gk[i], var=s2_y)
x_prev = x[i]
end
end
x0 = Normal()
u = 1.0
y = randn(3)
cs = @constraints begin
q(x_prev,x,gk) = q(x_prev)q(x,gk)
end
is = @initialization begin
q(x_prev) = vague(NormalMeanVariance)
q(x) = vague(NormalMeanVariance)
q(gk) = vague(NormalMeanVariance)
μ(x) = vague(NormalMeanVariance)
end
res = infer(
model = mwe(prior=x0, u=u),
data = (y=y,),
free_energy = true,
constraints = cs,
initialization = is,
iterations = 10,
returnvars = (x = KeepLast(),),
)I tried a few variants (e.g., other constraints or initialization) but it always runs for In the meantime, I will compare Friston's solution with our message passing solution and see if I find any discrepancies that explain the difference in prior variances and estimated state means. |
Beta Was this translation helpful? Give feedback.
-
|
I have an hypothesis: the difference is due to filtering vs smoothing. Can you post the generative process where you simulate data? Then I can test this hypothesis. |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
|
Basic active inference code in a diagram (in generalised coordinates of motion) |
Beta Was this translation helpful? Give feedback.
-
|
Okay, so that didn't work. As discussed today, the difference between RxInfer and Friston's code may have to do with the time discretization of the generative model. For the state transition, we have where, for now, I double-checked the steps today and realized a mistake. For an approximate (first-order) discretization of the state SDE, the discretization should be: where Anyway, if I change the state transition in the batch inference (aka smoothing) model, I get
It's odd that it jumps up so quickly instead of the graceful exponential decay in Friston's code. But it does reach the $$\pm$$24 value that Friston's code levels off to (the tail at the end has to do with the batch inference process). There must be another discrepancy between the two codebases. I will investigate further. Code: """Generative process"""
dt = 0.005
T = 10+dt
tsteps = range(start=dt,step=dt,stop=T)
len_time = length(tsteps)
# f(mx::Real, mv::Real) = -mx + mv
# f_step(mx::Real, mv::Real) = mx + dt*f(mx, mv)
# g(x) = 25 - 16 / (1 + exp(5 - x / 5))
gp_x_init = 25.0 # actual depth of hydar
gp_x_p = 1.0 # actual precision of function of motion
gp_y_p = 1.0 # actual precision of function of sensory observations
gm_x_init = 15.0 # Hyadr's belief of depth
gm_x_target = 15.0 # Prior belief of Hydar
gm_x_p = 1.0 # Hydars'belief of precision of function of motion
gm_y_p = 1.0 # Hydars belief of precision of function of sensory observations
v_t = 0.0
# The generative process " the world"
function initializeWorld(; x_p, y_p, x_init, dt)
# input parameters
# x_p = precision of the generative process noise on the function of motion
# y_p = precision of the generative process noise on the function of sensory mapping
#x_init
# generative process
f_motion(x,v,u) = u
g_temp(x,v) = 25 - 16 / (1 + exp(5 - x / 5))
x_t_last = x_init
y_t_temp = g_temp(x_init,0)
function execute(a_t::Float64)
# Original python code
# x_dot= self.hydar.f(self.x[i],self.v, action) + np.random.randn(1) * np.sqrt(self.Sigma2_x) where f= 0*x + 1*u
# self.x[i+1]= self.x[i] + self.dt*x_dot
# self.y_temp[i+1] = self.hydar.g_temp(self.x[i+1],self.v) + np.random.randn(1) * np.sqrt(self.Sigma2_y_temp) where g=25 -16 / (1 + np.exp(5-x/5))
x_t_w = rand(Normal(0, sqrt(1/x_p)))
y_t_w = rand(Normal(0, sqrt(1/y_p)))
x_dot = f_motion(0,0,a_t) + x_t_w
x_t = x_t_last + dt*x_dot
x_t_last = x_t # Memory for next step
y_t_temp = g_temp(x_t,0) + y_t_w
return y_t_temp
end
observe() = y_t_temp # Temperature is observed
return (execute, observe, f_motion, g_temp)
end;
execute_world, observe_world,f,g = initializeWorld(x_p=gp_x_p, y_p=gp_y_p, x_init=gp_x_init, dt=dt);
y = [g(gp_x_init, 0) for _ in 1:len_time]
# y = [g(rand(Normal(gp_x_init,1/gp_y_p)), 0) for _ in 1:len_time] #variant with added sensory noise
"""Model specification"""
@model function Hydar_model(y, x_init, mu_v, dt, x_s2, y_s2)
x_prev ~ x_init
for i in 1:length(y)
x[i] ~ Normal(mean= (1-dt)*x_prev + dt*mu_v , var=dt*x_s2) # x_t+1=x_t + dt*f(x_t, v_t) = xt+1 = xt -dt*x_t + dt*v_t
# tmp ~ mu_v - x_prev
# x[i] ~ Normal(mean= tmp , var=x_s2)
#xk[i] ~ f_step(x_prev,mu_v) where { meta = DeltaMeta(method = Linearization()) }
#x[i] ~ Normal(mean = xk[i], var=x_s2)
gk[i] ~ g(x[i],0) where { meta = DeltaMeta(method = Linearization()) }
y[i] ~ Normal(mean = gk[i], var=y_s2)
x_prev = x[i]
end
end
"""Inference"""
x_init = NormalMeanVariance(gm_x_init, 1. / gm_x_p)
mu_v = 1.0
constraints = @constraints begin
q(x,gk,x_prev) = q(x,gk,x_prev)
#q(mu_x,gk,xk,x_prev) = q(mu_x)q(x_prev)q(gk)q(xk)
end
inits = @initialization begin
q(x) = vague(NormalMeanVariance)
q(x_prev) = vague(NormalMeanVariance)
q(gk) = vague(NormalMeanVariance)
#q(xk) = vague(NormalMeanVariance)
μ(x) = vague(NormalMeanVariance)
end
result = infer(
model = Hydar_model(x_init=x_init, mu_v=mu_v, dt=dt, x_s2=1/gm_x_p, y_s2=1/gm_y_p),
data = (y = y,),
free_energy = false,
constraints=constraints,
initialization=inits,
iterations=10,
showprogress=true,
returnvars=(x = KeepLast(),),
options = (limit_stack_depth = 100,),
)
estimates = mean.(result.posteriors[:x])
var_state = var.(result.posteriors[:x])
plot(xlabel="time", ylabel="depth")
hline!([gp_x_init], label="Generative process", color="orange")
# scatter!(tsteps, y, label="Observations")
plot!([0.; tsteps], [gm_x_init; estimates], label="State belief", color="blue")
hline!([gm_x_target], label="Prior belief", color="black") |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
|
Ok, so I compared the two estimators (as currently specified) for the linear model of generalized coordinate depth 1 by hand. Gradient descent on Free Energy (GDFE)The generative model for a single generalized coordinate is specified to be: where The free energy under this model, with The gradient of the free energy with respect to The change in the state estimate is given as: If we then discretize this continuous-time change
Message passing (MP)In this case, the continuous-time state transition has to be discretized at the start. With Euler-Marayuma, this is: which implies that the discrete-time stochastic state transition is Thus, we have the generative model: with the accompanying recursive factor graph: There are 4 messages:
Message 0 is the state prior itself: Message 1 is the integral of the state transition over the incoming message 0: Message 2 is the integral of the likelihood node over the observed data point: Message 3 is the product of messages 1 and 2: where ComparisonThe two approaches lead to different results. But we can perhaps find a corresponding case. If we assume Let's use the same notation on the GPFE estimate: It seems to me that the time step sizes are in entirely different places, and that the GDFE method is missing the previous state uncertainty |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
|
Today I have been testing a non-linear model, and the short conclusion is that it works. I did the same experiment as experiment 1.01 in notebook: https://www.kaggle.com/code/charel/active-inference-code-by-example-1 The RxInfer generative model Highly simular graphs. the Python code converges to μₓ last 5 values: [26.776059 26.776059 26.776059 26.776059 26.776059] while the RxIbnfer code to [26.776391, 26.776391, 26.776391, 26.776391, 26.776391, 26.776391]. Which can be easily explained because in the Python code the exact derivative of the non-linear function g has to be given as input to the model (return -16/5* np.exp(5-x/5) / (np.exp(5-x/5)+1)**2) while in RxInfer is is approximated (gk ~ g(x) where { meta = DeltaMeta(method = Linearization()) }) and it does not have be calculated/given upfront. RxInfer has done a remarkable good job to approximate the non-linear function. Next I did some experiments with different precisions. Also here the results are highly comparable
Giving me the confidence that this continuous active setup on RxInfer is working correctly (non-generalized coordinates, excluding action, etc) |
Beta Was this translation helpful? Give feedback.
-
|
Nice! That's excellent! Great to see. So, the major pain point still is the generalized coordinates setup? In hindsight, the major issue was that RxInfer defaults to a structured factorization of the variational model (for Gaussians) while Friston assumes mean-field over states. It could be that there is still a mismatch between the factorization of each coordinate in the generalized coordinates vector between RxInfer and Friston. Do you happen to know what Friston assumes there? Because if you construct the generalized coordinates as a state vector in RxInfer, then I think it defaults to a structured factorization as well (I have to double-check). |
Beta Was this translation helpful? Give feedback.
-
|
Great progress @Charelvanhoof ! A small tip: it is possible to provide the inverse function to the g = (x::Real) -> 25 - 16 / (1 + exp(5 - x / 5))
# has singularities at y=9 and at y=25
inverse_g = (y::Real) -> 25 - 5 * log((y - 9) / (25 - y))
DeltaMeta(method = Linearization(), inverse = inverse_g)In this particular case the inverse function has singularities, but still would be interesting to test. It should also in theory improve the performance. |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.






















Uh oh!
There was an error while loading. Please reload this page.
-
Hi Team,
Thank you for our excellent meeting/discussion yesterday. Looking forward to see how we can make (continuous) active inference work on the RxInfer platform. It looks promising. I guess we might need to add some features to the platform to capture the specifics of continuous active inference.
As requested I opened a topic over here to get things going.
As discussed, as a first step, lets recreate the IWAI 2024 tutorial (Basic active inference capsule) on RxInfer. Here is the link: https://www.kaggle.com/code/charel/active-inference-example-iwai2024 (including working python code, so we can double check we get the same results).
The example is set in a simulation environment where an early evolutionary imaginary aquatic ancestor (Hydra vulgaris) must preserve its physical integrity to survive. For example, it needs to keep in a certain temperature range. It is a basic case of a single active inference capsule with one sensor, one hidden state and one causal state. And Hydar can take action to move up or down.
I propose to build the example step by step:
Step 1.1 Inference without generalized coordinates
Inference of the hidden state, causal state (without generalized coordinates). In the example the function of sensory mapping g is non-linear. If this is an extra challenge we can take a first step with a linear g function
Step 1.2 Add the action
Realtime/online inference including the inference of the reflex action u, which is input for the next timestep (it is not discrete/EFE active inference)
Step 1.3 Add the generalized coordinates
Need to express the states in generalized coordinates e.g [position, speed, acceleration,..] and inference all generalized coordinates. (see the generalized coordinates as a kind of short-term-prediction). Need to be able to generate colored noise and the precision matrix of the colored noise.
Thank for the help because I am new to RxInfer/Julia.
Beta Was this translation helpful? Give feedback.
All reactions