The real convolution layer: many input channels in, many output channels out.
Input x has shape (C_in, H, W). The kernel bank has shape
(C_out, C_in, kH, kW) and the bias is (C_out,).
For each output channel o and each spatial position, sum the elementwise
product over every input channel and the whole window, then add the bias:
out[o, i, j] = bias[o] + sum over c, u, v of x[c, i*s+u, j*s+v] * kernel[o, c, u, v]
Zero pad the spatial dimensions by padding on all four sides, never the channel
axis. Output shape is
(C_out, (H + 2p - kH) // s + 1, (W + 2p - kW) // s + 1)
Two things to get right:
C_in maps. Each of the C_out filters sees the whole
depth of the input at once, which is how a layer combines edges from different
colour channels into one feature.This is still cross correlation, so the kernel is not flipped. If the padded
input is smaller than the kernel, return an array of shape (C_out, 0, 0).
def conv2d_multichannel(x, kernels, bias, stride=1, padding=0):
Input
x = np.array([[[1, 2], [3, 4]],
[[10, 20], [30, 40]]]) # (C_in=2, 2, 2)
kernels = np.ones((1, 2, 2, 2)) # (C_out=1, C_in=2, 2, 2)
bias = np.array([0.5])
conv2d_multichannel(x, kernels, bias)
Output
array([[[110.5]]])
Reasoning
There is one window position. The kernel of ones sums both channels: 1 + 2 + 3 + 4 = 10 and 10 + 20 + 30 + 40 = 100. Add the bias once to get 110.5.
(C_out, 0, 0).Check runs the visible cases against a fresh kernel. Submit runs the hidden ones too.
⇧↵run cell