cswork / libraryOpen practice workspace

Articles

CNN Coding Interview Questions: Output Shapes, Grouped Convolutions, and Test Cases

Practice CNN coding interviews with Conv2D output shapes, grouped channels, parameter counts, a NumPy reference, and tests that catch silent bugs.

A CNN coding interview can go wrong even when your convolution returns the expected shape. The filter might be flipped, the bias added once per input channel, or every output channel accidentally connected to every input group. Shape tests alone will miss all three.

A stronger answer separates dimensions, channel connectivity, and numerical values. Write down the contract, calculate a small example, implement a readable reference, and choose tests that would fail if one assumption changed.

This guide uses official PyTorch documentation for operator behavior. The worked examples below are original practice exercises. Here, CNN means convolutional neural network, not the media company.

What should you clarify before writing Conv2D?

Ask whether the input uses NCHW or NHWC, whether the kernel is applied directly or flipped, and which padding modes are required. State whether you are implementing forward inference only or an operation that must support gradients. These choices define different tasks, even when the interviewer calls all of them “convolution.”

Official fact: PyTorch Conv2d applies cross-correlation and represents a batched input as (N, C_in, H, W). Its documented weight shape is (C_out, C_in / groups, K_h, K_w). Both channel counts must be divisible by groups. PyTorch Conv2d reference.

For our original implementation, accept real NumPy arrays, integer symmetric zero-padding, positive integer stride and dilation, and a positive group count. Reject unsupported layouts rather than silently transposing them. Preserve the input arrays; allocate a new output. Excluding arbitrary padding modes makes the exercise small enough to inspect without weakening its useful edge cases.

An interview answer should also explain invalid inputs. An oversized effective kernel, incompatible channel dimensions, or a bias with the wrong length should produce a clear error. A negative output dimension is not a useful empty result under this contract.

Calculate output dimensions before counting parameters

The effective kernel span along one axis is dilation * (kernel_size - 1) + 1. For symmetric padding, calculate the output extent as:

out = (input + 2 * padding - effective_kernel) // stride + 1

This is the documented Conv2d shape rule expressed with integer division. Original worked example: take input (2, 4, 7, 9), six output channels, a 3 × 3 kernel, stride two, padding one, dilation one, and two groups.

The effective span is three. Height becomes (7 + 2 - 3) // 2 + 1 = 4; width becomes (9 + 2 - 3) // 2 + 1 = 5. The output is therefore (2, 6, 4, 5).

Now change only dilation to two. The effective span becomes five, giving height three and width four. The output becomes (2, 6, 3, 4). This changes which locations are sampled, but the kernel still contains nine weights per connected input channel. Dilation does not insert additional learnable weights.

For the original two-group case, each filter sees two input channels. The weight tensor is (6, 2, 3, 3): 108 weights, plus six optional biases, giving 114 parameters with bias. A standard one-group layer would have 216 weights plus six biases, or 222. Neither the batch size nor the number of output pixels enters this parameter count.

A useful follow-up is why doubling output height increases computation without increasing the layer's parameter count. Point to weight reuse across spatial locations, then distinguish parameter storage from multiply–accumulate work. Avoid answering a latency question with a parameter-count calculation.

Explain grouped convolution as a connectivity rule

With four input channels and six output channels split into two groups, input channels zero and one feed output channels zero through two. Input channels two and three feed output channels three through five. There are no connections across those partitions.

For an output channel co, compute its group as co // (C_out // groups). Its first connected input channel is that group multiplied by C_in // groups. The second dimension of the weight tensor indexes channels within that group, not the original global channel number.

This distinction produces a strong counterexample. Set the second input group to zero, disable bias, and put nonzero values only in the first group. The second output group must remain zero. A function that produces the right shape but reads all four input channels fails immediately.

Official fact: depthwise convolution is the special case groups == C_in, with output channels an integer multiple of input channels. Each input channel can have multiple filters; depthwise does not require exactly one output per input. PyTorch Conv2d reference.

A pointwise 1 × 1 layer with one group can subsequently mix channels. If asked about depthwise separable convolution, explain the two operations separately before calculating their combined parameters. Do not treat a grouped spatial layer as an automatic substitute for a complete separable block.

Implement a small reference before vectorizing

This original reference favors visible indexing over speed. It supports scalar stride, padding, and dilation; extending each to a height/width pair is a separate follow-up. The NumPy implementation was executed locally. A PyTorch comparison is described afterward, but was not executed in this environment.

import numpy as np

def conv2d_ref(x, w, b=None, stride=1, pad=0,
               dilation=1, groups=1):
    if x.ndim != 4 or w.ndim != 4:
        raise ValueError("expected NCHW and OIHW arrays")
    vals = (stride, pad, dilation, groups)
    if any(not isinstance(v, int) for v in vals):
        raise ValueError("integer settings required")
    if min(stride, dilation, groups) < 1 or pad < 0:
        raise ValueError("invalid settings")
    n, ci, h, width = x.shape
    co, local_ci, kh, kw = w.shape
    if min(x.shape + w.shape) < 1:
        raise ValueError("positive dimensions required")
    if ci % groups or co % groups or local_ci != ci // groups:
        raise ValueError("incompatible groups")
    if b is not None and np.shape(b) != (co,):
        raise ValueError("invalid bias shape")
    oh = (h + 2*pad - dilation*(kh-1) - 1) // stride + 1
    ow = (width + 2*pad - dilation*(kw-1) - 1) // stride + 1
    if min(oh, ow) < 1:
        raise ValueError("kernel exceeds padded input")
    xp = np.pad(x, ((0, 0), (0, 0), (pad, pad), (pad, pad)))
    y = np.zeros((n, co, oh, ow), dtype=np.float64)
    for batch in range(n):
        for out_c in range(co):
            start = (out_c // (co // groups)) * local_ci
            for i in range(oh):
                for j in range(ow):
                    patch = xp[batch, start:start+local_ci,
                               i*stride:i*stride+dilation*(kh-1)+1:dilation,
                               j*stride:j*stride+dilation*(kw-1)+1:dilation]
                    y[batch, out_c, i, j] = np.sum(patch * w[out_c])
                    if b is not None:
                        y[batch, out_c, i, j] += b[out_c]
    return y

Notice where the bias is added: once after the connected-channel sum for each output element. Keeping it outside an input-channel loop prevents the common repeated-bias bug. The explicit output dtype makes this an educational real-valued reference, not a promise to reproduce every library dtype or device behavior.

If the prompt bans loops, use this version as a reasoning oracle before proposing window extraction and tensor contraction. Explain the temporary-memory cost of materializing windows. A vectorized answer still needs the same channel partitions and boundary rules; shorter code does not establish correctness.

Choose tests that expose different mistakes

Our original test matrix uses five independent checks:

TestExpected resultMistake exposed
Non-square input, stride twoShape (2, 6, 4, 5)Wrong axis or floor calculation
Dilation two on the same caseShape (2, 6, 3, 4)Confusing kernel size with effective span
Input [[1,2],[3,4]], kernel [[1,2],[3,4]]Single value 30Accidental kernel flip, which gives 20
Only the first group is nonzero; no biasSecond output group stays zeroCross-group channel leakage
Zero input, bias [1,2,3,4,5,6]Each channel equals its bias everywhereRepeated or misaligned bias

Add invalid-group and oversized-kernel cases that assert the intended exception. Test batch size greater than one so a hard-coded batch index cannot pass. Preserve copies of the input and weight arrays and verify they remain unchanged after the call.

A second numerical control uses all-one inputs and weights with padding one. In our original grouped example, the top-left output contains four valid spatial positions across two connected channels, so its value is eight. An interior location contains nine positions across two channels, giving 18. This catches padding mistakes that a shape check cannot see.

When a test fails, report the first differing (batch, channel, row, column) and inspect its contributing patch. Logging an entire tensor makes a small indexing error harder to isolate.

Compare with a library without hiding the contract

Official fact: torch.nn.functional.conv2d accepts weights, optional bias, stride, padding, dilation, and groups. Build the library comparator with the same arrays and settings; do not compare against a randomly initialized module. Functional Conv2d reference.

For a future local comparator, create matching float64 CPU tensors and compare with an explicit tolerance, such as rtol=1e-9, atol=1e-9 for these small examples. That tolerance is an original test choice, not a universal PyTorch guarantee. Check shapes first so broadcasting cannot turn incompatible outputs into a misleading numerical comparison.

PyTorch documents floating-point differences between devices and equivalent computations; it also documents reproducibility limitations. A matching CPU forward test does not establish GPU determinism, gradient correctness, or production speed. Numerical accuracy, Reproducibility.

For a backward-pass extension, specify the gradient being checked and use tiny finite-difference controls. Keep that task separate from a forward-pass claim. Likewise, benchmark only after correctness, with warm-up, device synchronization where appropriate, fixed shapes, and a stated measurement scope.

Finish with an observable correctness argument

A convincing explanation names the assumptions, derives one shape and parameter count, shows a nontrivial numerical oracle, and demonstrates group isolation. If time runs short, state which supported cases pass and which extensions remain, rather than implying the reference implements every Conv2d feature.

Explain the expected values before running the code, then deliberately break one group index or bias placement and show which test catches it.

Sources and Further Reading

Research checked October 4, 2026. Library behavior is version-specific; the original NumPy controls are correctness exercises, not hiring-frequency or performance evidence.