Modified the one is the book so that it would be easier to read

from fastai.vision.all import *
def get_data(url, presize, resize):
    path = untar_data(url)
    dls = DataBlock(
        blocks=(ImageBlock, CategoryBlock),
        get_items=get_image_files,
        splitter=GrandparentSplitter(valid_name='val'),
        get_y=parent_label,
        item_tfms=Resize(presize),
        batch_tfms=[
            *aug_transforms(min_scale=0.5, size=resize),
            Normalize.from_stats(*imagenet_stats),
        ],
    ).dataloaders(path, bs=128)
    return dls

dls = get_data(URLs.IMAGENETTE_160, 160, 128)
def _conv_block(ni,nf,stride):
    return nn.Sequential(
        ConvLayer(ni, nf, stride=stride),
        ConvLayer(nf, nf, act_cls=None, norm_type=NormType.BatchZero))
class ResBlock(Module):
    def __init__(self, ni, nf, stride=1):
        self.convs = _conv_block(ni,nf,stride)
        self.idconv = noop if ni==nf else ConvLayer(ni, nf, 1, act_cls=None)
        self.pool = noop if stride==1 else nn.AvgPool2d(2, ceil_mode=True)

    def forward(self, x):
        return F.relu(self.convs(x) + self.idconv(self.pool(x)))

Original '_resnet_stem()'

def _resnet_stem(*sizes):
    return [
        ConvLayer(sizes[i], sizes[i+1], 3, stride = 2 if i==0 else 1)
            for i in range(len(sizes)-1)
    ] + [nn.MaxPool2d(kernel_size=3, stride=2, padding=1)]

Modified '_resnet_stem()'

def _resnet_stem(*sizes):
    
    # Conv Macro only within this function
    def __C(i, s):
        return ConvLayer(sizes[i], sizes[i+1], 3, stride=s)
    
    l =  [__C(i=0, s=2)]
    l += [__C(i=i, s=1) for i in range(1, len(sizes)-1)]
    l += [nn.MaxPool2d(kernel_size=3, stride=2, padding=1)]
    return l 

#_resnet_stem(3,32,32,64)

Modified the above '_resnet_stem' to read a little bit easier, where we deal with the 1st iteration and the rest of the others separately since only the 1st time has differenet params. The 1st iter is done out side of the loop and then the loop will do the rest.

Original 'ResNet()'

class ResNet(nn.Sequential):
    def __init__(self, n_out, layers, expansion=1):
        stem = _resnet_stem(3,32,32,64)
        self.block_szs = [64, 64, 128, 256, 512]
        for i in range(1,5): self.block_szs[i] *= expansion
        blocks = [self._make_layer(*o) for o in enumerate(layers)]
        super().__init__(*stem, *blocks,
                         nn.AdaptiveAvgPool2d(1), Flatten(),
                         nn.Linear(self.block_szs[-1], n_out))
    
    def _make_layer(self, idx, n_layers):
        stride = 1 if idx==0 else 2
        ch_in,ch_out = self.block_szs[idx:idx+2]
        return nn.Sequential(*[
            ResBlock(ch_in if i==0 else ch_out, ch_out, stride if i==0 else 1)
            for i in range(n_layers)
        ])

Modified 'ResNet()'

class ResNet(nn.Sequential):
    def __init__(self, n_out, layers, expansion=1):
        self.block_szs = [64, 64, 128, 256, 512]
        for i in range(1,5): self.block_szs[i] *= expansion
        
        l = _resnet_stem(3,32,32,64)
        
        l += [self._make_layer(layers[0], *self.block_szs[:2],    1)] # Do 0th
        l += [self._make_layer(layers[i], *self.block_szs[i:i+2], 2)  # Do the rest
              for i in range(1, len(layers))]
        
        # Appending the Head part
        l += [
            nn.AdaptiveAvgPool2d(1),
            Flatten(),
            nn.Linear(self.block_szs[-1], n_out)
        ]
        
        super().__init__(*l)
    
    def _make_layer(self, n_layers, ch_in, ch_out, stride):
        l =  [ResBlock(ch_in,  ch_out, stride)] # Do 0th
        l += [ResBlock(ch_out, ch_out, 1)       # Do the rest
              for i in range(1, n_layers)]
        return nn.Sequential(*l)
learn = Learner(dls, ResNet(dls.c, [2,2,2,2]), loss_func=nn.CrossEntropyLoss(),
                metrics=accuracy).to_fp16()
learn.fit_one_cycle(5, 3e-3)
epoch train_loss valid_loss accuracy time
0 1.669369 3.107110 0.332484 00:16
1 1.346194 1.786952 0.458599 00:16
2 1.105611 1.140638 0.642293 00:16
3 0.897768 0.917169 0.710573 00:16
4 0.769690 0.756307 0.761274 00:16

Bottleneck

dls = get_data(URLs.IMAGENETTE_320, presize=320, resize=224)
def _conv_block(ni,nf,stride):
    return nn.Sequential(
        ConvLayer(ni, nf//4, 1),
        ConvLayer(nf//4, nf//4, stride=stride), 
        ConvLayer(nf//4, nf, 1, act_cls=None, norm_type=NormType.BatchZero))

learn = Learner(dls, ResNet(dls.c, [3,4,6,3], 4), loss_func=nn.CrossEntropyLoss(),
                metrics=accuracy).to_fp16()
learn.fit_one_cycle(20, 3e-3)
epoch train_loss valid_loss accuracy time
0 1.609382 1.529753 0.510318 01:30
1 1.383831 1.579404 0.533758 01:32
2 1.265062 1.538380 0.535541 01:34
3 1.160110 1.392963 0.538344 01:34
4 1.063608 2.391305 0.418853 01:34
5 0.950941 1.787218 0.471338 01:34
6 0.848306 1.497377 0.636433 01:34
7 0.783319 0.930480 0.701401 01:34
8 0.713762 1.030928 0.681783 01:34
9 0.639707 1.577286 0.574013 01:34
10 0.604874 0.995040 0.695541 01:34
11 0.539998 0.803806 0.754650 01:34
12 0.484867 0.688639 0.779618 01:34
13 0.459248 0.712160 0.785733 01:34
14 0.392319 0.468867 0.857580 01:34
15 0.353768 0.558457 0.837197 01:34
16 0.314284 0.418582 0.871338 01:34
17 0.287317 0.439048 0.866752 01:34
18 0.260283 0.403132 0.877452 01:34
19 0.242763 0.425505 0.871847 01:34