There is more than one way to do this in principle, but most CNNs, and most CNN libraries will do the following:
Each layer has a target number of feature maps, which is what you have labelled nb_filters
.
Each feature map is derived from all previous layer's feature maps.
The weights of the locally connected kernels do follow a width x height x nb_input_features x nb_output_features
structure similar to your first example (with width and height being the convolutional kernel size), because each output feature in layer b is derived as the sum of convolutions over every feature in layer a.
So if your output feature map with (channel) index $j$ in layer $k+1$ is $I^{(j, k+1)}$, and you are looking at connection to all input feature maps $I^{(i, k)}_i$ in layer $k$ with filters connecting each map $F^{(i,j,k)}$, then the maths looks a bit like this:
$$I^{(j,k+1)} = f(b^{(j,k)} + \sum_i I^{(i,k)} \ast F^{(i,j,k)})$$
. . .where $f(x)$ is the transfer or activation function e.g. ReLU. The bias is $b^{(j,k)}$ and the $\ast$ represents the convolution function. Summing over $i$ means iterating over all the input channels. NB I have indexed the items all with a superscript like this to more clearly show that all the variables (apart from $b$) are matrices after indexing - i.e. this is not the equation for a specific pixel, but for a whole feature map.
You can see that the kernel $F$ is indexed by $(i,j,k)$ i.e. there are nb_input_features
times nb_output_features
filters connecting each layer.
In comparison, your expected scheme would keep the derived features separate layer-by-layer, and create an exponential growth in number of feature maps as the layers increased, with each feature "type" being handled separately. Whereas what happens in practice is each feature map is a "re-mix" of all the feature maps in the previous layer, convolved each with its own filter then summed up.
This cross-referencing of features between layers helps create richer complex feature maps - e.g. it can combine edge and corner detectors to detect corners with specific angles. Whilst a scheme that refined each feature into multiple separate sub-features would not be able to do this mixing between features as freely (until the later fully-connected layers) - although it might do well at other tasks - e.g. detecting variations of extended structures of the same type longer lines, curves etc (NB this is just a guess on my part, I don't know for certain).
If it's not working that way, how do I know what filters are applied to what activation maps?
You cannot say something like "this feature map in layer N is derived only from this other feature map in layer N-1, using this single filter", because each feature map in layer N is derived from all feature maps in layer N-1 and results of the filters are combined in a way that you cannot easily reverse.
However, the weights array will be arranged so that you can find which filters are connecting each input feature map to a specific output feature map. Which dimension you would use to isolate those depends on the library. If your library stores weights in a [kernel_width, kernel_height, input_feature_map_id, output_feature_map_id]
form, then your selection (using Numpy-style syntax might be) [:,:,:,n]
where n
is the id of the output feature map that you want to fetch the filters for.