Deterministic Without Predictability: Using Chaotic Neural Dynamics to Model the Conditions for Agency
Advisor: Anmol Bhardwaj
Deterministic differential equation based recurrent neural networks (RNNs) can be used to replicate, on a small scale, the processes of the human brain. RNNs are typically assumed to be fully predictable given complete knowledge of initial conditions. This project investigates how an RNN, when coupled with biologically accurate noise and amplification mechanisms, can nonetheless exhibit practical unpredictability on behaviorally realistic timescales while remaining responsive to structured inputs (“reasons”). The objective of this project is to suggest that human thought, when modeled by recurrent neural networks (RNNs), can be both unpredictable and still exhibit normative agency, creating implications for fields of computational neuroscience and quantitative philosophy.
Project Blog
Post 1: Blog Post 0 - Introduction
As this is my first research blog post, I'll split this into two sections: an introduction to my research, and what I've been doing lately related to my project and my on-site placement. Starting with my research...
In the modern day, philosophical arguments regarding free will have changed significantly compared to the past. Instead of arguing about the existence of will as a metaphysical abstraction, determinism (the belief that all events are caused by preceding factors) is now the basis of most discussions, and from this stems two major groups of philosophers: compatibilists and hard determinists. Hard determinists believe that free will is fundamentally incompatible with determinism, which makes sense -- if all events, including human actions, are determined by prior states, then "the ability to do otherwise" doesn't effectively exist with every outcome being predetermined. Compatibilists, on the other hand, believe that the two can co-exist. They define "freedom" not as the ability to do otherwise but as the ability to act according to one's desires and character, regardless of whether said actions were predetermined or not. Thus, determinism matters less in the arguments over free will and more of how "free will" is actually defined.
My project takes this idea of accepting determinism and applies it to a computational approach. Imagine the weather: a fully deterministic physical system governed by known equations, and yet scientists still struggle to forecast its precise state beyond a limited time window. This difficulty rises from sensitivity to initial conditions, where tiny differences grow over time, rendering long-term prediction practically impossible. Human cognition, I argue, may function similarly.
I will use recurrent neural networks (RNNs) as simplified models of cognitive systems. Although RNNs defined by differential equations are, in principle, fully determined by their initial conditions, I show that when biologically realistic noise and transient gain amplification are introduced, small perturbations can be magnified into divergent outcomes, just like the weather. Crucially, this unpredictability does not collapse into randomness. In certain parameter regimes, structured inputs continue to reliably bias decision statistics, meaning the system’s behavior remains sensitive to reasons even as individual outcomes vary.
Philosophically, this project weakens the notion that determinism = predictability. Many hard determinists (including me) have made this assumption, but the goal of this project is to show that while theoretically this may be true, in practice such a feat could never happen. In a way, this project is sort of a quest to disprove my own preconceptions.
As of late, I have started to do in depth reading of my sources, starting way back with Doyon's 1992 paper Destabilization and route to chaos in neural networks with random connectivity. I have also been in contact with professors at Georgetown University and hopefully will be able to get an internship there.
Week 1: Blog Post 1 - Week 1 and Deeper Introduction
I spent the majority of this week refamiliarizing myself with my programs. It has been a while since I have looked at first my coding draft, but I thankfully remember most of it. Rereading my code also reminded me that commenting is extremely necessary, so I took some time to do so while reviewing.
I also looked at some of my preliminary results, their flaws, and how to fix them.
Here were the results that I got for my flip probability experiment. This experiment is supposed to compare the probability a neuron "flips" its decision given a microscopic perturbation at a given time. Originally, I decided to test whether or not a perturbation induces flip probability in a network. I later changed this, however, because agency depends more on when the system is vulnerable to change. If a network is vulnerable forever, it's unstable, while if it never is, it's rigid. Agency requires a "decision window" where the system is open to change.
Here were the results for my predictability horizon experiment. This is something I have considered changing. The thing is, while measuring trajectory divergence between one perturbed and one unperturbed network is mathematically sound, it doesn't necessarily translate to differentiating behavior, as they may diverge in high-dimensional space and yet still land in the same "decision basin." To prove unpredictability, I need to prove instead that the past no longer influences the future (or influences it less by a significant degree). I am still researching how to do this, but I know that if the network "forgets" its initial state due to noise amplification, it can be said that the network is practically unpredictable.
I was unfortunately unable to recover working results for experiment 3, or my reasons-responsiveness experiment where I compared how a biasing vector could allow two networks to behave differently. What I dislike about this experiment is that a bias vector indicates more of a preference than a "reason." To claim reasons-responsiveness, I have to allow the network to effectively process given information. To do this, I have discovered a method called Two-Alternative Forced Choice task paradigm which is commonly utilized in neuroscience experiments. This however will require much more research.
I will be working on these things and hope to create a final and concrete plan in the week to come.
Week 2: Blog Post 2 - Week 2
This week, I finalized my improved experiments.
Experiment 1: Originally, what I did was compute the probability that a given neuron "changes its mind" between a perturbed and non-perturbed system. The issue with that is that it doesn't exactly pin-point the reason behind this change. Here's my improved procedure:
The Finite-Time Lyapunov Exponent (FTLE) is a numerical constant that quantifies how chaotic a system behaves. If the FTLE > 0, small changes amplify into large ones. If the FTLE is approximately 0, the system remains static. Likewise, when the FTLE < 0, it acts as a chaotic attractor, but that's out of the scope of this project. The point is, this is a much better approach at quantification than I used before. Secondly, I am also turning off noise for experiment 1. I can add it back later for biological realism, but I have to prove the system is chaotic first. Next, I will measure the flip probability as before, but this time, I'll be comparing it and looking for correlation with the FTLE. The critical question behind this experiment is whether or not divergence and chaos (the FTLE) correlate to crossing a decision boundary.
Experiment 2: Before, this experiment quantified how the trajectories of two different systems diverged. The issue with this is that predictability and unpredictability do not equate to agency, which requires control. For example, a system can be predictable and uncontrollable (a huge boulder rolling down a hill) or unpredictable and uncontrollable (static noise). Neither exhibit agency. So, the question for my neural network becomes not "can I control the system" but rather "how long can I control the system for before it devolves into chaos"? In other words, is the control horizon (the time window where inputs matter) longer than the decision time (how long it takes to act)? If not, the system can never be reasons-responsive, or not entirely at least. So, how will I approach this?
I plan to do the following: similarly to before, I will run the system once with no external input (only background noise) and get the decision readout from the network. I will then run a separate identical simulation except with a structured input pulse at time t, recording the decision readout from this network as well. I'll then systematically delay t to see how how it effects the decision readout. I'm not trying to figure out if control decays over time, as that mathematically has to be the case. Rather, I'm trying to figure out how much unpredictability we can have in a system given a certain decision window.
Experiment 3: Previously, this was my biased input reasons-responsiveness test. This logic, however, was fundamentally flawed. Just like how a coin biased to always land on heads or a dice biased to always roll six doesn't exhibit reasons-responsiveness, neither can my system. In other words, my system may be responsive to input, but it cannot rationally process given information. Agency requires a differentiation between noise and functional signals. Thus, here is my revised method:
As said before, we need to be able to prove my network can take information and actually process it. So, instead of putting a biased input at time t, I'll feed the system a long string of evidence that is both partially coherent and partially noise. I'll vary the ratio between the two across trials, ranging from a string of noise to a pure signal. I'll also be sure to keep noise active. After that, I'll plot the probability of choosing a certain decision against the coherence level of my input, which is also called a psychometric curve. If the slope of this curve >> 0, then the network exhibits rational behavior: stronger evidence means stronger commitment. Additionally, to mathematically show correlation and disprove false positives, I'll be using a Mutual Information Score. This quantified by how much information you can get about the input from the result.
Finally, for experiment 4, I'm increasing the scope of the heat diagram. Like before, I'm changing both gain and noise values in the network. However, this time, I'll be measuring the dependent variables of the 3 previous experiments. This includes quantifying the Lyapunov exponent, responsiveness window, Mutual Information Score, psychometric curve slope, and flexibility in experiment 3. I'm still deciding on how to weigh each of these results.
In conclusion, I've made a ton of progress beyond my old code this week. While I haven't fully implemented this, I'll spend this week working and may have some new results next week.
Post 4: Blog Post 3 - Finding the Ideal Spectral Radius
Coming into this week's blog post, I really wanted to show of some fancy new results that looked promising. I spent all of last week coding a new and revised experiment 1, and here is what I got:
Yeah... it's not looking great. Here's the issue I have with my results. Graph A's goal was to simply compare my FTLE against gain. Last week, I briefly mentioned what a negative FTLE meant: a chaotic attractor, where trajectories converged instead of diverged. This is what is happening here. I'm not getting chaotic behavior even though all mathematical signs point towards higher chaos with more gain. So, why is this?
This explains it:
Spectral radius: 7.065
Saturation fraction: 96.8%
Imagine my neural network as a large table with hundreds of cups. These cups can be filled or emptied by an outside source, and can even share water between them based off of my network weights. However, the two values I have shown above basically mean that I have taken a giant bucket and poured it all over the table with all the cups overflowing with water and getting everywhere. Spectral radius, the highest eigenvalue of my weight matrix, acts similarly to an FTLE: when it is < 1, signals and gradients diminish over time, when it = 1, signals are stable and memory is preserved, and when it is > 1, states grow exponentially and things become unstable. I generally want a spectral radius that is slightly greater than 1 for this project, but have it at 7 means that everything is blown way out of proportion.
This continues until each neuron gets saturated (think of this as all the cups are now full). No matter how chaotic I want my network to behave, there is no place for all the water to go: and thus, it converges (no movement or change). This is why I'm getting a negative Lyapunov exponent. Now, everyone's favorite part: fixing my mistakes.
The first change I'm implementing is at the very root: the initialization of my matrix. In my initialization method, I implemented code that finds the maximum eigenvalue (spectral radius) and multiplies the matrix by the ratio between the current spectral radius and the target one (1.2). This should effectively help with my issues. Keyword should. I still need to test it. So, I created a small test function that varies the spectral radius. Here goes!
Now, this is EXACTLY what I've been waiting for. It's honestly so satisfying looking at this. Graph A shows that my suspicions were right. There is an ideal spectral radius that results in the greatest Lyapunov exponent. Both too low and too high results in attraction. Graph B adds to this, showing that saturation certainly does occur at higher spectral radius values. Graph C shows the diminishing slope of divergence per spectral radii which makes sense as well. This is really really exciting for me, as this is the behavior that I have seen in past papers. For example, "Lyapunov spectra of chaotic recurrent neural networks" by Engelken et al posits that while coupling strength between neurons increases, mass saturation occurs, bending the Lyapunov spectrum downwards.
Graph D, however, really surprised me. It shows that there is a different ideal spectral radius for each value of gain. This sort of makes sense intuitively, however. For each greater spectral radii, saturation occurs faster, which means a lower gain is needed to balance it out. Thus, I have a difficult decision to make. When I vary gain in the later experiments, should I change the network itself just to ensure chaos? Or should I accept that neuron saturation is an element that comes with higher gain? I'll have to consider this in biological aspects as well. That's it for this week, however. It may seem like I didn't make much progress, but I certainly have done a whole ton of work. Thanks for reading!
Post 5: Blog Post 4 - Experiment 1 Complete
I ended last week's post with my findings regarding the inverse relationship between the chaos maximizing values of gain and spectral radius. Let's see how this new information effects my experiment 1 results:
We finally have a positive Lyapunov exponent! However, the caveat is that the maximum Lyapunov exponent achieved is 0.025, which is barely chaotic. This might not necessarily be an issue, however. Recall my design of the network. I deliberately stated that I wanted my network to have biological constraints, such as Dale's Law and an 80:20 E/I ratio. These rules effectively limit how chaotically my network behaves, so as long as my network has a positive Lyapunov exponent, I'm happy to continue. Furthermore, noise seems to have an upward effect on my Lyapunov exponent, which is understandable. Finally, for graph D, we can see that we really are sitting on the "edge of chaos," where some trials diverge from one another while some stagnate and converge. I feel like I've found an alternate sweet spot for network behavior: I originally intended to have my network behave exclusively chaotically, but this might make results for my experiments even more interesting. I'll be working on Experiment 2 next where the effects of the factors I discussed here will be put into play and shown.
Post 6: Blog Post 5 - Refresher
It's been a while since I've posted. During my hiatus of sorts, I've been figuring out what college to go to, which includes lots, and lots, and lots of tours. In total, I think I've been on UMD's campus for the purpose of touring over 9 times, and over 4 times for UVA.
As far as my research is concerned, I have been using this time to draft the code for experiment 2, though it isn't in a very functional state right now. It occurred to me that I never explained the structure and different parts of my code, so I figured I could explain this while I'm still debugging my code.
From an overarching view, my code functions through several programs working in conjunction. For my parameters and presets, I have two "YAML" files, which essentially are human readable files that store data assigned to variable names (something like this:
n_neurons: 100
excitatory_ratio: 0.83).
My program can then read this and turn the text into actual workable variables. The first YAML file I have designed is for biological parameters, which include variables such as gain, noise magnitude and firing rate of the neurons. The second is for the overall configuration of the network, including variables such as then number of neurons, number of trials per experiment, and random number generator seed.
The next big part of my program is "network.py." Instead of putting all the experimental procedure and complicated analytics within this program, I decided to only include the network and its functionalities, and have the other programs (which I will explain next) interact with the network and draw out any data necessary. After importing and converting the YAML file variables from earlier, I have an initialization method called "___init___()" which takes these variables and creates a matrix W, ensuring Dale's Law and zeroing out self connections. I then tweak it to have a certain spectral radius (I introduced this when I was messing with exp1). This is it for the network itself.
Next, I have a method that "reads" the current network state and neuron values and returns them for interpretation. This is pretty simple: I just return two arrays, one called Pool and the other B, of the neurons in the network.
The last important method in the network program is the simulate() method. This handles the core of the update function of the network, and is called thousands of times for each experiment. After counting the number of steps it must take per trial, it initializes itself, accounts for any external input, and then uses an Euler-Maruyama integration to calculate the change in each neuron. Most of the time that each experiment takes happens here. I then apply saturation bounds, and record changes.
Finally, we have analysis.py, which works together with each of the experiment programs to carry out the procedure. In this program, I have a section of code that is responsible for each of the experiments. For example, for experiment 1, analysis.py was responsible for calculating the divergence between two trials, fitting sections of this divergence to a linear growth phase, and attempting linear regression. On the other hand, exp1.py was responsible for calling analysis.py and then formatting results into graphical and numerical forms.
As you can see, a majority of the heavy lifting is done by analysis.py, and the reason for this is that I wanted to keep exp1.py as clean and readable as possible so anyone parsing my code can understand my procedure relatively easily. If they then want to dig deeper, they can through analysis.py and network.py.
This about sums up how my entire project functions. There are other programs, such as utils.py which helps generate sequences of random and biased noise for experiment 2, but these are mostly irrelevant to the greater architecture. I hope this helps you somewhat understand the structure of my code, and I'll report back with results about experiment 2 and what I've done to make it work in the next post. Thank you for reading!