The problem
Show a model a slice of tumour tissue and it will predict something useful. The obvious next question is which cells made it say that. There are popular tools for answering this, and people use them to make biological claims.
The catch is that they get tested on real tissue, where nobody knows the true answer. So “the method highlighted the important cells” is a statement no one can check. It is taken on faith.
So I built tissue where I know the answer
Not real tissue. Cells with types and marker profiles, scattered under rules I control, with a label defined as a function of the cells themselves: a sample counts as positive when enough tumour cells have a T cell sitting close by. Because I write the rule, I can record every cell that contributed. Fig 1 shows six generated samples.
Two extra things get recorded, and they turn out to matter more than the driver list. First, a stricter set: the smallest group of cells whose removal actually flips the label. Second, decoys, which are cells with identical marker profiles sitting in the wrong place. Anything worth calling an explanation should point at the first and ignore the second.
The test
Train a graph neural network on this tissue. It solves the task almost perfectly, around 0.999 AUC, which matters because you cannot ask what a broken model was thinking. Then hand each explanation tool a sample and ask which cells mattered, and score its answer against the recorded truth.
Most of them do not find the cells
GNNExplainer, probably the best known of these tools, lands at 0.044 against a random baseline of 0.058. It is worse than guessing. Attention rollout manages 0.090. The best graph method, integrated gradients, reaches 0.153.
The uncomfortable line is the highlighted one. That is a heuristic which asks “does this look like a tumour cell?” and stops there. It never touches the model. It scores 0.169, better than every graph explanation method tested.
The check that actually mattered
Here is a cheap test that is easy to skip. Scramble the network’s weights so it knows nothing, then run the explanation tools again. If a tool returns roughly the same answer on a randomised network as on a trained one, it was never reading the model in the first place.
GNNExplainer’s answers stayed 0.83 to 0.89 correlated on one architecture. Attention rollout reached 0.99. Both were, in effect, describing the shape of the graph rather than anything the model had learned. Only integrated gradients came through this cleanly.
One gotcha worth passing on
That randomisation number is not a fixed property of the method. It depends on how long you let GNNExplainer optimise. At 30 epochs it reads 0.85, which looks damning. At 400 it settles near 0.28, which looks acceptable. The common default is 100, partway down the slope, where the number has not converged and means very little.
Anyone reporting this test should say what budget they ran it at. I have not seen that done.
Things I got wrong
My first generator was broken in a way that took a while to notice. A model could hit 93 % accuracy on it without looking at a single tumour cell, because the local density of T cells gave the answer away on its own. Every faithfulness number I would have computed on it was measuring a shortcut.
Then I got the GNNExplainer verdict wrong twice in opposite directions. I called it void, found the number was an artefact of too little optimisation, retracted that, and then over-corrected by generalising from the single most favourable setting. The final answer sits between the two. Both corrections are in the repository rather than tidied away, which I think is the honest way to leave it.
What this does and does not show
This is synthetic tissue and one family of labels. It does not prove these tools fail on real data, and a method that misses my driver cells might still point somewhere biologically useful. What it does show is that when the answer is checkable, most of these tools do not recover it, a heuristic with no model access competes with the best of them, and the control that catches this costs about ten minutes to run.
Full write-up, code and the gated test suite are in the repository.