Neural networks have a reputation as black boxes. Decision trees have the opposite reputation: every prediction is a chain of if-else questions you can read out loud. It turns out these two things are not as different as they look. For a network built from linear layers and ReLU activations, there is an exact decision tree that produces identical outputs. Not an approximation, not a distillation. The same function, rewritten.
That is the claim proved by Aytekin (2022), "Neural Networks are Decision Trees", and nn2tree is a small Python library that does the rewrite for you. Give it a PyTorch, TensorFlow, or plain-numpy network and it hands back a tree you can print, explain, or compile to a dependency-free function in Python, Java, Rust, or Go.
The one idea behind it
A ReLU neuron does one of two things: it either passes its input through (when the input is
positive) or it outputs zero (when the input is negative). That single yes/no switch is a
decision. A network with k hidden neurons has k such switches, and
the specific on/off combination for a given input is called its activation pattern.
Here is the key: once you fix the activation pattern, every ReLU is either "multiply by 1" or "multiply by 0", so the whole network collapses into a single linear map from the raw input straight to the output. Each distinct activation pattern carves out a region of input space where the network is exactly linear. Those regions are the leaves. The switches that get you there are the branches. That is the tree.
- Tree depth = the number of hidden neurons.
- Each leaf = a linear formula, exact for every input that lands in that region.
- The number of leaves = the number of activation patterns the network actually uses.
Poke it yourself
Below is a tiny network with 2 inputs, 4 ReLU neurons, and 1 output, running live in your browser. Each ReLU neuron draws one line across the input plane. Together those 4 lines slice the plane into linear regions, and each region is a leaf. Move the point (drag on the canvas or use the sliders) and watch the decision path light up. The tree's output and the raw network's output are computed independently on the right. They always match.
Coloured regions are leaves · white lines are the 4 ReLU switches · drag the point to move it
Notice what the demo makes concrete. The four white lines are the four ReLU switches. Every coloured region is a leaf, and inside it the "output = ..." formula is a plain weighted sum, exact for the whole region. Cross a line and one switch flips, you drop into a neighbouring leaf, and the formula changes. The network is not a black box in that region at all. It is a line.
Doing it for real
The demo hardcodes one small network, but nn2tree does the same construction for any supported network automatically. The quick start looks like this:
import nn2tree
import torch.nn as nn
model = nn.Sequential(
nn.Linear(3, 8), nn.ReLU(),
nn.Linear(8, 4), nn.ReLU(),
nn.Linear(4, 1),
)
# Convert (lazy mode -> works for any size network)
tree = nn2tree.convert(model, input_names=["age", "income", "score"])
# Numerically identical to the original model
tree.predict(x)
# Verify equivalence over random inputs
print(nn2tree.verify(tree, model, n_samples=1000))
# VerificationResult(passed=True, max_diff=4.2e-07, ...)
That max_diff of about 4e-7 is just floating-point noise. The tree really is the
same function.
Explanations that are exact, not approximate
Because each leaf is genuinely linear, the usual explainability tools stop being estimates. The per-feature contributions at a leaf are exact (they are SHAP values for a linear model), and the decision path is the literal computation, not a summary of it.
=== Prediction explanation for x = [2.1, -0.3, 1.7] ===
Decision path:
L0.N0: + 0.3*age - 0.7*income + 0.1*score > 0 => +0.9 [FIRES (>0)]
L0.N1: + 0.15*age + 0.9*income - 0.2*score > 0 => -0.01 [silent (<=0)]
Leaf formula:
output = - 0.5*age + 0.8*score + 0.12
Feature importance (global):
age : 0.720 ####################
income : 0.210 ######
score : 0.070 ## Compile the tree to a dependency-free function
Once you have the tree, you can emit it as self-contained source with no runtime dependency on PyTorch, numpy, or nn2tree itself. Handy for shipping a model into an environment that cannot take a deep-learning stack.
print(tree.to_code("python")) # if/else Python
print(tree.to_code("rust")) # Rust fn
print(tree.to_code("go")) # Go func
print(tree.to_html()) # interactive collapsible tree Where the equivalence holds, and where it stops
The rewrite is exact precisely when the network is a stack of affine maps followed by piecewise-linear activations. That covers more than a plain MLP:
- ReLU and Leaky ReLU - exact, 2 branches per neuron.
- Linear / Dense / Conv2d - convolutions are unfolded to an equivalent linear map via im2col.
- BatchNorm - affine at inference, so it folds into the neighbouring layer with no extra tree depth.
- Residual / skip connections and a single-layer vanilla RNN unrolled over a fixed sequence length.
Smooth activations (sigmoid, tanh, GELU) are not piecewise-linear, so in hidden layers they become a piecewise-linear approximation with a tunable number of segments. As an output-layer activation on a classifier head they are applied exactly. And some things genuinely cannot be a tree at all: LSTM and GRU gates multiply two learned signals, and attention mixes values with input-dependent weights. Both are multiplicative rather than piecewise-linear, so no exact tree exists. nn2tree detects those up front and raises a clear error rather than returning something wrong.
The catch: exactness is not smallness
The honest limitation is size. Tree depth equals the number of hidden neurons, so the tree of a
large network is astronomically deep in the worst case. nn2tree handles this with a
"lazy" mode that computes the single path for a given input on demand (always exact,
any size), alongside an "eager" mode that materialises the whole tree for small
networks. The saving grace is that most activation patterns of a ReLU net are geometrically
impossible, so prune_during_build=True skips those dead branches while building and
lifts the size ceiling considerably.
So the value here is less "replace your network with a tree" and more "you now have a rigorous lens on what the network is doing". Every prediction sits in a linear region you can write down, inspect, and explain exactly. That is a nicer place to stand than the black box.
The code, cookbook, and the runnable end-to-end demo are on GitHub (hoofay/nn2tree).
Credit and citation
nn2tree is a direct implementation of the equivalence proved by Çağlar Aytekin. The idea, the proof, and the framing all belong to that paper. If you build on this, cite the paper rather than the library:
Çağlar Aytekin (2022). Neural Networks are Decision Trees. arXiv:2210.05189. https://arxiv.org/abs/2210.05189
@article{aytekin2022neural,
title = {Neural Networks are Decision Trees},
author = {Aytekin, {\c{C}}a{\u{g}}lar},
journal = {arXiv preprint arXiv:2210.05189},
year = {2022},
url = {https://arxiv.org/abs/2210.05189}
}