An unmodeled Keras layer class in the middle of a chain drops the output shape for everything downstream of it. Two such classes are reachable in ordinary models: MaxPool2D and LSTM.
Witness
The fixture tf2_test_layer_call_result_rank.py carries four chains behind four sinks, all ending in the same Dense(10) and all called with the same batch of 32.
| chain |
consumer types as |
Dense only |
(32, 10) float32 |
Conv2D then Flatten then Dense |
(32, 10) float32 |
Conv2D then MaxPool2D then Flatten then Dense |
{? of float32} |
LSTM then Dense |
{? of float32} |
The first two rows are the controls and they pass, so the isolation is pinned by passing tests beside the failing ones rather than by removing code and re-running. The dtype survives in every row. What is lost is the shape, and it is lost as ⊤ rather than as a wildcard axis, so the result declares nothing about rank at all.
Inserting a single pooling layer into a chain that otherwise resolves is what turns the third row from the second.
Cause
The model file describes the layer classes Dense, Conv2D, Flatten, Embedding, Dropout and GlobalAveragePooling1D, each paired with a generator that computes the call's output shape. Neither MaxPool2D nor LSTM appears. An instance of an unmodeled class is called rather than applied, so the call produces no output shape and every value after it in the chain inherits ⊤.
This is a gap in an existing family rather than an absent capability: GlobalAveragePooling1DCall already models a rank-reducing pooling call, so the shape of the fix is established.
Why It Is Worth Prioritising
A ⊤ shape is not a coarse answer, it is the absence of one. A declaration derived from it constrains nothing, and downstream consumers that read a static axis cannot be served by it at all. Pooling after a convolution and a recurrent layer before a projection are both ordinary shapes for a model to take, so the two missing classes sit on common paths rather than exotic ones.
MaxPool2D is the more mechanical of the two: the output shape is a function of the input shape, the pool size, the strides and the padding, in the same way the existing pooling generator computes its own. LSTM needs the units argument stored on the instance and read back at the call, in the same way Dense stores units and Embedding stores output_dim, with the wrinkle that return_sequences decides whether the temporal axis survives.
An unmodeled Keras layer class in the middle of a chain drops the output shape for everything downstream of it. Two such classes are reachable in ordinary models:
MaxPool2DandLSTM.Witness
The fixture
tf2_test_layer_call_result_rank.pycarries four chains behind four sinks, all ending in the sameDense(10)and all called with the same batch of 32.Denseonly(32, 10) float32Conv2DthenFlattenthenDense(32, 10) float32Conv2DthenMaxPool2DthenFlattenthenDense{? of float32}LSTMthenDense{? of float32}The first two rows are the controls and they pass, so the isolation is pinned by passing tests beside the failing ones rather than by removing code and re-running. The dtype survives in every row. What is lost is the shape, and it is lost as
⊤rather than as a wildcard axis, so the result declares nothing about rank at all.Inserting a single pooling layer into a chain that otherwise resolves is what turns the third row from the second.
Cause
The model file describes the layer classes
Dense,Conv2D,Flatten,Embedding,DropoutandGlobalAveragePooling1D, each paired with a generator that computes the call's output shape. NeitherMaxPool2DnorLSTMappears. An instance of an unmodeled class is called rather than applied, so the call produces no output shape and every value after it in the chain inherits⊤.This is a gap in an existing family rather than an absent capability:
GlobalAveragePooling1DCallalready models a rank-reducing pooling call, so the shape of the fix is established.Why It Is Worth Prioritising
A
⊤shape is not a coarse answer, it is the absence of one. A declaration derived from it constrains nothing, and downstream consumers that read a static axis cannot be served by it at all. Pooling after a convolution and a recurrent layer before a projection are both ordinary shapes for a model to take, so the two missing classes sit on common paths rather than exotic ones.MaxPool2Dis the more mechanical of the two: the output shape is a function of the input shape, the pool size, the strides and the padding, in the same way the existing pooling generator computes its own.LSTMneeds theunitsargument stored on the instance and read back at the call, in the same wayDensestoresunitsandEmbeddingstoresoutput_dim, with the wrinkle thatreturn_sequencesdecides whether the temporal axis survives.