A Python library for creating threshold logic Neuron Networks in a declarative way. This library is based primarily on my MSc Dissertation work, and is an improved refactoring of the codebase. One is encouraged to read the dissertation, however a very abridged summary is given below, followed by documentation for the codebase and its example networks.
- Clone the repository
python -m venv venv
source ./venv/bin/activate
pip install -e .
Tests executable scripts are available in the test
folder which correspond to the networks in src/libThresholdLogic/ExampleNetworks
The primary component of threshold logic is the (*classical) Perceptron, whose scalar output
where
With clever choices for
nemo igitur vir magnus sine aliquo adflatu divino umquam fuit - Cicero
Threshold activation is exhibited by biological neurons in nature. Scientific experimentation observing how neurons and synapses react under electrical currents procured their mathematical modelling as dynamical systems of differential equations. One such model is the two Fitzhugh-Nagumo (FHN) equations (1961), simplifying the four differential equations of earlier work done by Hodgkin and Huxley (1952).
Bifurcation theory analysis of the FHN equations reveals a neuron reaching its threshold for activation to be equivalent to a limit-cycle dynamical system reaching its first bifurcation orbit. "The location of the singular point P and hence its stability depends on z" (Fitzhugh, 1961:450).
z = 0.0, subcricical; orbit-trapped to nullcline-intersection | z = -0.4, supercritical; first orbit |
---|---|
In a special choice of hyperparameters of
One exotic component of interest is the memristor. Similar to the transistor's journey of being hypothesised in the 1920s, and built many years later in the 1940s, the memristor was hypothesised in 1971 and first built in 2008. Fang et al (2022) fork the FHN model, modifying the Van der Pol circuit to use a memristor instead of a non-linear resistor. This uses much less power! They successfully demonstrate threshold logic ALUs in their paper.
A cooler, supercooled even, component is the Josephson Junction (JJ), which uses the Josephson Effect from quantum mechanics, predicted in 1962. Chalkiadakis and Hizanidis (2022) demonstrate that coupling two JJs together creates a 'neuron' which exhibits a threshold effect. These neurons operate one hundred million times faster than a neuron in a human brain! The only problem is we do not yet know how to create 'synapses' (connections between neurons) for JJ neurons, since the threshold effect is exhibited by the relative phase difference of the JJs rather than an output current.
Josephson Junction Neuron, with
To be clear, whilst JJs use a quantum mechanical effect, we could use them in a conventional-computing setting with threshold logic, as well as in quantum-computing which has been done by D-Wave Systems. If synapses were to be discovered, mass-manufacturing of JJs becomes popularised, and cloud computing companies safely house low-temperature supercooled JJ computers for ssh-ing into, we could be looking at threshold logic Josephson Junction computing overthrowing digital logic transistor computing for the next generational leap of computational power.
We focus on 'synthetic' threshold logic in this codebase, that is the lossless mathematical derivations and functions, independent of a particular technology such as Josephson Junctions or Memristors. This provides as much portability as possible. Each file within the codebase has good further documentation.
Every network / ALU inherits from the NeuronNetwork
class and overrides its abstract __init__
method, within it creating and connecting the network's neurons. This way of declaratively constructing a network, say my_network
, means that one can peacefully call my_network(inputs)
without having to think about the evaluation order of the neurons; the NeuronNetwork.__call__
method is coded to do that, a depth-first algorithm. All one has to provide to super().__init__
is an input layer of ProxyNeuron
s and an output layer of neurons, corresponding to the IO of the network. A ProxyNeuron
is a wrapper class for a BaseNeuron
component which one intends to provide at a later time. For example one may evaluate the network on its own, which connects ConstNeuron
s to the input layer, or one may connect networks together, chaining IO. Think of a ProxyNeuron
like a bare wire sticking out of a 555 timer chip.
I would definitely recommend Ben Eater's YouTube channel for learning about how computers work at the lowest level.
All of the following networks except HammingGate
(which came to me recently) are described in the thesis as well. We make use of the term little/big 'bittian' (inspired by endian-ness) when talking about bitwise binary enumeration of numbers.
A simple operation we could ask of a computer is to add numbers together. We start with bitwise addition.
Adds two binary bits together, yielding two binary outputs, a 'sum' (1s) bit and a 'carry' (2s) bit.
Observe how pleasant the way of connecting neurons together is.
class HalfAdder(NeuronNetwork):
def __init__(self) -> None:
neuron_sum = Perceptron(1.0)
neuron_carry = Perceptron(1.0)
neurons = [neuron_sum, neuron_carry]
# inhibitory connection from carry to sum
neuron_sum.add_input(-2.0, neuron_carry)
input_layer = [ProxyNeuron() for _ in range(2)]
# excitatory connections from inputs
for neuron_src in input_layer:
neuron_sum.add_input(1.0, neuron_src)
neuron_carry.add_input(0.5, neuron_src)
output_layer = neurons
super().__init__(input_layer, output_layer)
With a corresponding neuron diagram
It would be nice to be able to chain half-adders together for adding two k-bit numbers. The adders would experience a ripple-effect, carrying the carry bits forwards to the next adder each time we add some bits. For this we would need to connect another input to the adder's neurons, which is indeed what we do for a FullAdder
.
A full adder takes in three inputs, and provides two outputs.
We go further and develop a generalised FullAdder
, the GenericBitAdder
. Any network with GenericBitAdder
achieves this with its output corresponding to the binary representation of adding together its
Observe the carefully chosen pattern of powers of 2 for the weights in the FullAdder
above. A full justification and mathematical derivation is given in the thesis. A summary is that we extend to the GenericBitAdder
as follows:
- For each neuron
$N_i$ ,$i \in \lbrace 0, \dots, n - 1 \rbrace$ - Connect all
$2 ^ n - 1$ inputs to$N_i$ , each with excitatory weight$1 / 2 ^ i$ - For each neuron
$N_j$ ,$j \in \lbrace 0, \dots, i - 1 \rbrace$ - Connect the output of
$N_i$ to$N_j$ with inhibitory weight$2 ^ {i - j}$ .
- Connect the output of
- Connect all
The FullAdder
is a 2-neuron GenericBitAdder
.
To create a
When connecting GenericBitAdder
s together in a ripple carry fashion, one considers the upper
In other words we can simultaneously add together
Part III of the thesis establishes the compatibility of threshold logic and digital logic. Specifically we embed digital logic into threshold logic, and come up with optimisations-to / extensible-forms-of common logic gates.
Since we allow negative weighting of inputs in our networks, a NOT
gate is hardly ever required, but here it is anyways.
We consider these two families of gates next. One recognises that the formula AND
and OR
gates respectively. The complimentary side of the planes form NAND
and NOR
gates respectively, with the weights and biases recoverable by rearranging the formula
AND and NAND |
OR and NOR |
---|---|
AND |
NAND |
OR |
NOR |
---|---|---|---|
This is where things get powerful! The formulae for AND
and OR
(and their complements) can easily be generalised with threshold logic such that all, or any, of n_inputs
, which by default is 2.
We do the derivations in our thesis, considering shaving off the
Multi-input AND |
Multi-input OR |
---|---|
The most generalised versions of the mono-neuron gates achieved in the thesis (though now superseded by HammingGate
further on) are the GAND
and GNAND
gates. Short for Generalised-(N)AND, these provide arbitrary bit-selection / exclusion of the form
The gates take in a single argument seek_vector
/ flee_vector
, a tuple corresponding of bits that should be sought or not.
GAND |
GNAND |
---|---|
This gate came to me recently when going back over my work. The G(N)AND
gates work by checking whether or not their input target_vector
Indeed we can! An adjustment to equation (10.7) to the form
reduces the tolerance of the GAND
gate; the weights end up the same, but the bias has been reduced by
Furthermore if one really thinks hard, one notices that GAND
and GNAND
flavours of gate into one Hamming metric based gate which we call HammingGate
.
Hence the inheritance hierarchy in LogicGates.py
is the following:
HammingGate
GAND
AND
NOR
GNAND
NAND
NOT
OR
The amazing things we can achieve with just one neuron!
XXX TODO Finish this off