skip to content

In Keras, how do you extract intermediate layer activations from a model?

level: middleimportance: should knowfreq 50%

answer

  1. the graph is still there to slice
  2. address a layer by its name
  3. wrap it as a second model
  4. same layer objects, same weights
  5. no recorded graph, nothing to cut

basics

~20 s

For a Functional model, build a second model over the same graph: keras.Model(inputs=model.inputs, outputs=model.get_layer("hidden").output). It reuses the same layer objects, so weights are shared and nothing is retrained. Subclassed models have no recorded graph, so this does not work on them.

solid answer

~50 s

A Functional model keeps the symbolic graph it was built from, so any node in it is addressable. `model.get_layer("hidden")` returns the layer object and `.output` gives the symbolic tensor that layer produced; wrapping it as `keras.Model(inputs=model.inputs, outputs=model.get_layer("hidden").output)` gives you a feature extractor. You can pass a **list** of such tensors to capture several layers in one forward pass, which is what style-transfer and debugging setups do. The new model does not copy anything — it references the same layer instances, so the weights are shared and no training is involved. On a subclassed model the same call fails: the layer was only ever invoked inside Python, so it has no recorded output tensor and `.output` raises. There the options are to add the activations to what `call()` returns, or to restructure the model so the reusable part is a custom layer wired into a Functional graph.

code

python · 14 lines
python
import keras
import numpy as np

inputs = keras.Input(shape=(32,))
h = keras.layers.Dense(64, activation="relu", name="hidden")(inputs)
outputs = keras.layers.Dense(10, name="logits")(h)
model = keras.Model(inputs, outputs)

extractor = keras.Model(
    inputs=model.inputs,
    outputs=[model.get_layer("hidden").output, model.get_layer("logits").output],
)
hidden, logits = extractor.predict(np.zeros((4, 32)))
print(hidden.shape, logits.shape)

go deeper

for a junior

Remember the one-liner: keras.Model(inputs=model.inputs, outputs=model.get_layer(name).output), and that you find the layer name from model.summary().

for a middle

Explain that this works because the Functional API records a symbolic graph, that the layers and weights are shared rather than copied, and that outputs accepts a list so one pass can return several activations.

for a senior

Bring in the operational details: naming layers deliberately so surgery survives refactors, the ambiguity when a layer has several recorded nodes, and the structural fix when a subclassed model gives you nothing to slice.

for a principal

Treat introspectability as an architectural property. If debugging, feature reuse and transfer learning all depend on being able to cut a model at a named point, that is an argument for a declarative model layer as a codebase standard.

## Why this works at all When you build a model with the Functional API, every layer call on a symbolic tensor records a node: which layer ran, on which tensor, producing which tensor. `keras.Model(inputs, outputs)` then stores that graph as the model's topology. That stored graph is what makes *model surgery* possible after the fact — you can point at any recorded tensor and declare a new model that ends there. The canonical form is: `extractor = keras.Model(inputs=model.inputs, outputs=model.get_layer("hidden").output)` `model.inputs` is the list of the original `keras.Input` tensors. `get_layer` finds a layer by `name=` or by `index=`. `.output` is the symbolic tensor that layer produced in the graph. ## Weights are shared, not copied The new model contains the *same layer objects* as the original. There is no copy, no re-initialisation and no training step. Two practical consequences follow: - If you keep training the original model, the extractor's outputs change too, because both read the same weight variables. - Setting `layer.trainable = False` on a shared layer affects it wherever it appears, and takes effect for training after the model is compiled again. This is exactly the mechanism behind transfer learning on a pretrained backbone: take a Functional backbone, cut at a named layer, attach a new head, and train the head. ## Capturing several layers at once `outputs=` accepts a list, so one extractor can return many activations in a single forward pass: `names = ["block1_conv1", "block2_conv1"]` then `outputs=[model.get_layer(n).output for n in names]`. The result is a multi-output model whose prediction is a list of arrays. Doing it in one model rather than one model per layer matters for cost: a single pass computes all of them. ## Finding the layer - `model.summary()` lists every layer name. - `model.layers` is the list of layer objects; `[l.name for l in model.layers]` is the quick way to see the names. - `get_layer` accepts either `name=` or `index=`, not both. Names are far more robust than indices, since indices shift the moment the architecture changes. - Layer names are unique within a model. If you did not name a layer, Keras auto-generated one like `dense_3`, and those auto-names are not stable across sessions or refactors — which is why naming layers you intend to address later is a real habit, not a style preference. ## Layers with more than one node If the same layer instance was called more than once — a shared tower, or a backbone applied to two inputs — that layer has several recorded output tensors, one per call. Asking for `.output` is then ambiguous and Keras will tell you so; `layer.get_output_at(node_index)` selects a specific call. This edge case is exactly the one people hit with siamese models and it is a good sign of hands-on experience if you mention it. ## Why subclassed models cannot do this A subclassed model's `call()` is Python. Layers are applied to real tensors at runtime and nothing about that is recorded as a graph. So even after the model is built, `model.get_layer("hidden").output` raises an error stating the layer has never been called in a way that defines an output. There is no topology to slice. The workarounds are all structural rather than clever: 1. **Return the extra tensors.** Add a flag or an extra return value to `call()` so the forward pass hands back the intermediate activations you need. Blunt, but explicit and always works. 2. **Keep a reference during the call.** Store the activation on the instance as a side effect. This is fragile — it breaks under compiled execution and under any form of parallelism — and is best used only for interactive debugging. 3. **Restructure.** Put the novel computation in a custom `keras.layers.Layer` and assemble the model itself with the Functional API. You get the graph back, and with it summary, plotting and surgery. ## When you would actually reach for this Debugging a training run by watching whether a block's activations are saturating or dying; embedding extraction for retrieval or clustering; feature-map visualisation for a convolutional network; perceptual and style losses that compare activations at chosen depths; and transfer learning where you cut a pretrained network at a chosen layer. All of them lean on the same one-liner.

  • If you keep training the original model, what happens to the extractor you built from it?
    Its outputs change, because the extractor references the same layer objects and therefore the same weight variables — nothing was copied. If you need a frozen snapshot of the features, either stop training the shared layers or save and reload a separate copy of the model to break the aliasing.
  • How do you address a layer that was called twice in the same graph?
    Each call records its own node, so `.output` is ambiguous and Keras raises rather than guessing. Use `layer.get_output_at(node_index)` to pick the call you mean. This comes up with shared towers and with a pretrained backbone applied to two different inputs.
  • What is the cleanest way to get intermediate activations out of a subclassed model?
    Make them part of the forward pass contract: have `call()` return them, optionally behind a flag, so the values come out through the normal path. Storing them on the instance as a side effect looks tidier but breaks under compiled execution and concurrent calls. The structural fix is to move the reusable block into a custom layer and assemble the model functionally.

saying these in an interview costs you the question

  • Believing the extractor gets its own copy of the weights
  • Using layer indices instead of names to address layers
  • Expecting get_layer(...).output to work on a subclassed model
  • Building one extractor per layer instead of one multi-output model
  • Thinking the extracted features need retraining before use

context