Going Back to Imagenette

It’s going to be tough to judge any improvements we make to our models when we are already at an accuracy that is as high as we saw on MNIST in the previous chapter, so we will tackle a tougher image classification problem by going back to Imagenette. We’ll stick with small images to keep things reasonably fast.

Let’s grab the data—we’ll use the already-resized 160 px version to make things faster still, and will random crop to 128 px:

In [ ]:

  1. def get_data(url, presize, resize):
  2. path = untar_data(url)
  3. return DataBlock(
  4. blocks=(ImageBlock, CategoryBlock), get_items=get_image_files,
  5. splitter=GrandparentSplitter(valid_name='val'),
  6. get_y=parent_label, item_tfms=Resize(presize),
  7. batch_tfms=[*aug_transforms(min_scale=0.5, size=resize),
  8. Normalize.from_stats(*imagenet_stats)],
  9. ).dataloaders(path, bs=128)

In [ ]:

  1. dls = get_data(URLs.IMAGENETTE_160, 160, 128)

In [ ]:

  1. dls.show_batch(max_n=4)

Going Back to Imagenette - 图1

When we looked at MNIST we were dealing with 28×28-pixel images. For Imagenette we are going to be training with 128×128-pixel images. Later, we would like to be able to use larger images as well—at least as big as 224×224 pixels, the ImageNet standard. Do you recall how we managed to get a single vector of activations for each image out of the MNIST convolutional neural network?

The approach we used was to ensure that there were enough stride-2 convolutions such that the final layer would have a grid size of 1. Then we just flattened out the unit axes that we ended up with, to get a vector for each image (so, a matrix of activations for a mini-batch). We could do the same thing for Imagenette, but that would cause two problems:

  • We’d need lots of stride-2 layers to make our grid 1×1 at the end—perhaps more than we would otherwise choose.
  • The model would not work on images of any size other than the size we originally trained on.

One approach to dealing with the first of these issues would be to flatten the final convolutional layer in a way that handles a grid size other than 1×1. That is, we could simply flatten a matrix into a vector as we have done before, by laying out each row after the previous row. In fact, this is the approach that convolutional neural networks up until 2013 nearly always took. The most famous example is the 2013 ImageNet winner VGG, still sometimes used today. But there was another problem with this architecture: not only did it not work with images other than those of the same size used in the training set, but it required a lot of memory, because flattening out the convolutional layer resulted in many activations being fed into the final layers. Therefore, the weight matrices of the final layers were enormous.

This problem was solved through the creation of fully convolutional networks. The trick in fully convolutional networks is to take the average of activations across a convolutional grid. In other words, we can simply use this function:

In [ ]:

  1. def avg_pool(x): return x.mean((2,3))

As you see, it is taking the mean over the x- and y-axes. This function will always convert a grid of activations into a single activation per image. PyTorch provides a slightly more versatile module called nn.AdaptiveAvgPool2d, which averages a grid of activations into whatever sized destination you require (although we nearly always use a size of 1).

A fully convolutional network, therefore, has a number of convolutional layers, some of which will be stride 2, at the end of which is an adaptive average pooling layer, a flatten layer to remove the unit axes, and finally a linear layer. Here is our first fully convolutional network:

In [ ]:

  1. def block(ni, nf): return ConvLayer(ni, nf, stride=2)
  2. def get_model():
  3. return nn.Sequential(
  4. block(3, 16),
  5. block(16, 32),
  6. block(32, 64),
  7. block(64, 128),
  8. block(128, 256),
  9. nn.AdaptiveAvgPool2d(1),
  10. Flatten(),
  11. nn.Linear(256, dls.c))

We’re going to be replacing the implementation of block in the network with other variants in a moment, which is why we’re not calling it conv any more. We’re also saving some time by taking advantage of fastai’s ConvLayer, which that already provides the functionality of conv from the last chapter (plus a lot more!).

stop: Consider this question: would this approach makes sense for an optical character recognition (OCR) problem such as MNIST? The vast majority of practitioners tackling OCR and similar problems tend to use fully convolutional networks, because that’s what nearly everybody learns nowadays. But it really doesn’t make any sense! You can’t decide, for instance, whether a number is a 3 or an 8 by slicing it into small pieces, jumbling them up, and deciding whether on average each piece looks like a 3 or an 8. But that’s what adaptive average pooling effectively does! Fully convolutional networks are only really a good choice for objects that don’t have a single correct orientation or size (e.g., like most natural photos).

Once we are done with our convolutional layers, we will get activations of size bs x ch x h x w (batch size, a certain number of channels, height, and width). We want to convert this to a tensor of size bs x ch, so we take the average over the last two dimensions and flatten the trailing 1×1 dimension like we did in our previous model.

This is different from regular pooling in the sense that those layers will generally take the average (for average pooling) or the maximum (for max pooling) of a window of a given size. For instance, max pooling layers of size 2, which were very popular in older CNNs, reduce the size of our image by half on each dimension by taking the maximum of each 2×2 window (with a stride of 2).

As before, we can define a Learner with our custom model and then train it on the data we grabbed earlier:

In [ ]:

  1. def get_learner(m):
  2. return Learner(dls, m, loss_func=nn.CrossEntropyLoss(), metrics=accuracy
  3. ).to_fp16()
  4. learn = get_learner(get_model())

In [ ]:

  1. learn.lr_find()

Out[ ]:

  1. (0.47863011360168456, 3.981071710586548)

Going Back to Imagenette - 图2

3e-3 is often a good learning rate for CNNs, and that appears to be the case here too, so let’s try that:

In [ ]:

  1. learn.fit_one_cycle(5, 3e-3)
epochtrain_lossvalid_lossaccuracytime
01.9015822.1550900.32535000:07
11.5598551.5867950.50777100:07
21.2963501.2954990.57172000:07
31.1441391.1392570.63923600:07
41.0497701.0926190.65910800:07

That’s a pretty good start, considering we have to pick the correct one of 10 categories, and we’re training from scratch for just 5 epochs! We can do way better than this using a deeper mode, but just stacking new layers won’t really improve our results (you can try and see for yourself!). To work around this problem, ResNets introduce the idea of skip connections. We’ll explore those and other aspects of ResNets in the next section.