← back

Every Neural Network Is a Decision Tree

nn2tree rewrites a trained ReLU network as an exact, numerically identical decision tree, so you can read off why it made every prediction. With a live demo you can poke.

13 July 2026 · hoofay
pythonmachine-learningexplainabilitydecision-trees

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.

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.

— decision path —
— leaf (this region is exactly linear) —
— outputs agree —

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:

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}
}