Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Image data and convolutional neural networks

GPU is available, using GPU NVIDIA A40
OriginalTranslatedCoarsenedSuper-resolved
originaltranslatedcoarsefine

Compared to structured tabular datasets like Iris, images pose some particular challenges. For example, the four images above all contain the same object. But they would be viewed very differently by an MLP.

VersionVector
Original
Shifted
Coarsened
Super-resolved

If we trained a MLP only on centered MNIST images, the shifted image might cause a completely different set of activations in the hidden layer, leading to misclassification. And the coarsened/super-resolved images would have sizes that are incompatible with the hidden layer weight matrix.

Animal vision tends to overcome these problems, and CNNs are loosely inspired by experimental results on the visual cortex. These experiments revealed a hierarchical structure with simple and complex cells. The simple cells identify features such as edges within narrow visual regions, or receptive fields. The complex cells probe a larger region and their activation is less sensitive on the exact position of an object within their view.

Visual perception, therefore, roughly follows this process

Retinal stimulation→⏟Simple cellsLow-level features→⏟Complex cellsHigh-level features→⏟…Recognition\begin{align} \text{Retinal stimulation} \underbrace{\rightarrow}_{\text{Simple cells}} \text{Low-level features} \underbrace{\rightarrow}_{\text{Complex cells}} \text{High-level features} \underbrace{\rightarrow}_{\dots} \text{Recognition} \end{align}

Convolutional neural networks (CNN) aim to learn feature maps that incorporate several key aspects:

  • Sparse connectivity - like simple cells, each output of a CNN layer accesses only a small region of the input

  • Weight sharing - like complex cells, CNNs produce outputs that are insensitive to position by applying the same weights at different places, and pooling the outputs

Combined, these properties give CNNs an important property called translation equivariance

f(g(x))=g(f(x))f is equivariant to g\begin{align} f\big(g(x)\big) = g\big(f(x)\big) & & \mbox{f is equivariant to g} \\ \end{align}

Suppose we have a translation operator TT that acts on an image by shifting every pixel to the right by one: T(Ix,y)=Ix−1,yT(I_{x,y}) = I_{x-1, y}. A CNN layer produces a feature map fx,y(I)f_{x,y}(I) that is equivariant to TT

fx,y(T(Ix,y))=fx,y(Ix−1,y)=fx−1,y(Ix,y)=T(fx,y(Ix,y))\begin{align} f_{x,y}\big( T (I_{x,y})\big) = f_{x,y}( I_{x-1,y} ) = f_{x-1,y} (I_{x,y}) = T\big( f_{x,y} ( I_{x,y}) \big) \end{align}

The feature map of a shifted image is identical to the shifted feature map of the original image.

How do we possibly make this work in practice? That’s the focus of the lecture today.

What is a convolution?

The convolution of two functions f(x),w(x)f(x), w(x) is

(f∗w)(x)=∫−∞∞f(t) w(x−t) dt\begin{align} (f * w)(x) = \int_{-\infty}^{\infty} f(t) \, w(x-t) \, dt \end{align}

or equivalently

(f∗w)(x)=∫−∞∞f(x−t) w(x) dt\begin{align} (f * w)(x) = \int_{-\infty}^{\infty} f(x - t) \, w(x) \, dt \end{align}

Loosely, it’s a way of “mixing up” two functions. We sometimes refer to ww as a weighting function, or alternatively a kernel function.

Convolution has several very nice algebraic properties which we don’t need to get into. Read more here.

Computers are finite objects. So in general, we cannot evaluate integrals with infinitesimally small differentials dtdt. Instead, we compute the discrete convolution using a sum

z[i]=∑j=−∞∞x[i−j] w[j]\begin{align} z[i] = \sum_{j=-\infty}^{\infty} x[i-j] \, w[j] \end{align}

Note that we also cannot evaluate sums over infinite domains. So we often work with finite kernels such that

z[i]=∑j=−KKx[i−j] w[j]\begin{align} z[i] = \sum_{j=-K}^{K} x[i-j] \, w[j] \end{align}

Implementing discrete convolutions

[-2. -2.  0.  1. -3.  0.  4.  2.]
[-2. -2.  0.  1. -3.  0.  4.  2.]
For array x
 [[9. 9. 7. 7. 2.]
 [6. 4. 8. 8. 9.]
 [1. 2. 0. 3. 7.]
 [3. 0. 4. 6. 9.]
 [0. 7. 9. 6. 3.]]
Our implementation:
 [[-22.   2.   0.   9.  22.]
 [-19.  -1.  -7.  -4.  26.]
 [ -8.  -1. -12. -20.  20.]
 [ -9. -10. -12. -11.  21.]
 [-14. -19.  -4.   7.  18.]]
Scipy implementation:
 [[-22.   2.   0.   9.  22.]
 [-19.  -1.  -7.  -4.  26.]
 [ -8.  -1. -12. -20.  20.]
 [ -9. -10. -12. -11.  21.]
 [-14. -19.  -4.   7.  18.]]

Speeding up discrete convolutions

x is a float array of size (5, 5)
A stride along dim 0 is 40 bytes: x[0] = [9. 9. 7. 7. 2.]
A stride along dim 1 is 8 bytes: x[0,0] = 9.0
Input x = 
 [[9. 9. 7. 7. 2.]
 [6. 4. 8. 8. 9.]
 [1. 2. 0. 3. 7.]
 [3. 0. 4. 6. 9.]
 [0. 7. 9. 6. 3.]]
X has shape (5, 5, 3, 3) and strides (56, 8, 56, 8)
X[0, 0] = 
[[0. 0. 0.]
 [0. 9. 9.]
 [0. 6. 4.]]
X[1, 1] = 
[[9. 9. 7.]
 [6. 4. 8.]
 [1. 2. 0.]]

Our implementation:
 [[-22.   2.   0.   9.  22.]
 [-19.  -1.  -7.  -4.  26.]
 [ -8.  -1. -12. -20.  20.]
 [ -9. -10. -12. -11.  21.]
 [-14. -19.  -4.   7.  18.]]
Scipy implementation:
 [[-22.   2.   0.   9.  22.]
 [-19.  -1.  -7.  -4.  26.]
 [ -8.  -1. -12. -20.  20.]
 [ -9. -10. -12. -11.  21.]
 [-14. -19.  -4.   7.  18.]]
Basic convolution took 0.038687 s
Fancy convolution took 0.000414 s
Scipy convolution took 0.000303 s

Example: Gradient filter

<Figure size 1200x450 with 3 Axes>

Example: edge detection (Sobel)

<Figure size 1200x900 with 6 Axes>
<Figure size 960x720 with 1 Axes>

Image data often has multiple channels

<Figure size 960x720 with 4 Axes>
(1674, 1256, 6)
<Figure size 960x720 with 9 Axes>
(-0.5, 1255.5, 1673.5, -0.5)
<Figure size 960x720 with 1 Axes>
<Figure size 960x720 with 9 Axes>
(-0.5, 1255.5, 1673.5, -0.5)
<Figure size 960x720 with 1 Axes>
Docstring: conv2d(input, weight, bias=None, stride=1, padding=0, dilation=1, groups=1) -> Tensor Applies a 2D convolution over an input image composed of several input planes. This operator supports :ref:`TensorFloat32<tf32_on_ampere>`. See :class:`~torch.nn.Conv2d` for details and output shape. Note: In some circumstances when given tensors on a CUDA device and using CuDNN, this operator may select a nondeterministic algorithm to increase performance. If this is undesirable, you can try to make the operation deterministic (potentially at a performance cost) by setting ``torch.backends.cudnn.deterministic = True``. See :doc:`/notes/randomness` for more information. Note: This operator supports complex data types i.e. ``complex32, complex64, complex128``. Args: input: input tensor of shape :math:`(\text{minibatch} , \text{in\_channels} , iH , iW)` weight: filters of shape :math:`(\text{out\_channels} , \frac{\text{in\_channels}}{\text{groups}} , kH , kW)` bias: optional bias tensor of shape :math:`(\text{out\_channels})`. Default: ``None`` stride: the stride of the convolving kernel. Can be a single number or a tuple `(sH, sW)`. Default: 1 padding: implicit paddings on both sides of the input. Can be a string {'valid', 'same'}, single number or a tuple `(padH, padW)`. Default: 0 ``padding='valid'`` is the same as no padding. ``padding='same'`` pads the input so the output has the same shape as the input. However, this mode doesn't support any stride values other than 1. .. warning:: For ``padding='same'``, if the ``weight`` is even-length and ``dilation`` is odd in any dimension, a full :func:`pad` operation may be needed internally. Lowering performance. dilation: the spacing between kernel elements. Can be a single number or a tuple `(dH, dW)`. Default: 1 groups: split input into groups, both :math:`\text{in\_channels}` and :math:`\text{out\_channels}` should be divisible by the number of groups. Default: 1 Examples:: >>> # With square kernels and equal stride >>> filters = torch.randn(8, 4, 3, 3) >>> inputs = torch.randn(1, 4, 5, 5) >>> F.conv2d(inputs, filters, padding=1) Type: builtin_function_or_method