# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     name: python3
# ---

# + [markdown] id="Uo_p9OYg2mV7"
# # Fastai Application Architectures
# > In this article we will look at how to build custom applications in the fastai library, by looking at how current fastai image model applications are actually built.
# - toc: true 
# - comments: true
# - image: images/fastai.jpeg
# - categories: [fastai]

# + id="daRa_FSB2bVN"
#hide
# !pip install -Uqq fastbook
import fastbook
fastbook.setup_book()
from fastbook import *

# + [markdown] id="KwDeQlDR29VQ"
# ## Introduction
#
# The [fastai deep learning library (as of 2021)](https://www.fast.ai/2020/02/13/fastai-A-Layered-API-for-Deep-Learning/) is a layered API that has 4 levels of abstraction.
#
# - Application layer
# - High level API
# - Mid level API
# - Low level API
#
# ![](https://github.com/pranath/blog/raw/master/images/fastai-layered.png "The fastai layered API")
#
# In this article we will look at how to build custom applications in the fastai library, by looking at how current fastai image model applications are actually built.

# + [markdown] id="sTbL4lJ33C4M"
# ## Fastai Image Model Applications
#
# ### cnn_learner
#
# When using this application, the first parameter we need to give it is an architecture which will be used as the *body* of the network. Usually this will be a ResNet architecture we pre-trained weights that is automaticially downloaded for you.
#
# Next the final layer of the pre-trained model is cut, in fact all layers after the final pooling layer is also cut as well. Within each model we have a dictionary of information that allows us to identify these different points within the layers called *model_meta* here for example for ResNet50.

# + colab={"base_uri": "https://localhost:8080/"} id="eHfzam5DRWH9" outputId="5061529a-20dd-4392-caea-69df92aca75e"
model_meta[resnet50]

# + [markdown] id="_xqaZnudRpqY"
# Key parts of the network are:
#
# - **Head** - The part of the network specialised for a particular task i.e. with a CNN the part after the adaptive average pooling layer
# - **Body** - Everything else not the Head including the Stem
# - **Stem** - The first layers of the network
#
# We we take all the layers before the cut point of -2, we get the body of the model that fastai will keep to use for transfer learning. Then we can add a new head.

# + colab={"base_uri": "https://localhost:8080/"} id="ZNj_KmzFSwDN" outputId="95e529b4-6085-41fc-b458-f7bd8cbc75ce"
#hide_output
create_head(20,2)

# + [markdown] id="XqdLWloTS2mf"
# With this function we can choose how many extra layers should be added at the end as well as how much dropout and pooling. Fastai by default adds 2 linear layers rather than just one, as fastai have found this helps transfer learning work more quickly and easily than just one extra layer.
#
# ### unet_learner
#
# This architecture is most often used for image segmentation tasks. 
#
# We start of building this in the same way as the cnn_learner, chopping off the old head. For image segmentation, we are going to have to add a very different type of head to end up with a model that actually generates an image for segmentation. 
#
# One way we could do this is to add layers that can increase the grid size in a CNN, for example duplicating each of the pixels to make an image twice as big - this is known as *nearest neighbour interpolation*. Another approach uses strides, in this case a stride of half, which is known as *transposed convolution*. However neither of these approaches works well in practice.
#
# They key problem here is there is simply not enough information in these downsampled activations alone to be able to recreate something like the oroginal image quality needed for segmentation - its a big ask! And perhaps not realistic.
#
# The solution to this problem here is our friend again *skip connections* however using them not accross one layer - but reaching these connections far accross to the opposite side of the architecture.
#
# ![](https://github.com/pranath/blog/raw/master/images/unet.png "The Unet architecture")
#
# Here on the left half of the model is a CNN, and the transposed convolutional layers on the right, with the extra skip connections in gray. This helps the Unet do a much better job at generate the type of images we want for segmentation. One challenge with Unet's is the exact architecture does in this case depend on the image size, however fastai has a *DynamicUnet* object that automatically generates the correct architecture based on the data and image sizes given.
#
# ### A Siamese Network
#
# Let's now try to create a custom model. [In an earlier article we looked at creating a Siamese network model](https://livingdatalab.com/fastai/2021/05/30/fastai-midlevel-api.html). Let's recap the details of that model.

# + colab={"base_uri": "https://localhost:8080/", "height": 17} id="W3EpcHZeYiE9" outputId="1f49fc53-3be6-4eac-f987-5d710d2e88b8"
#hide
from fastai.vision.all import *
path = untar_data(URLs.PETS)
files = get_image_files(path/"images")

class SiameseImage(fastuple):
    def show(self, ctx=None, **kwargs): 
        img1,img2,same_breed = self
        if not isinstance(img1, Tensor):
            if img2.size != img1.size: img2 = img2.resize(img1.size)
            t1,t2 = tensor(img1),tensor(img2)
            t1,t2 = t1.permute(2,0,1),t2.permute(2,0,1)
        else: t1,t2 = img1,img2
        line = t1.new_zeros(t1.shape[0], t1.shape[1], 10)
        return show_image(torch.cat([t1,line,t2], dim=2), 
                          title=same_breed, ctx=ctx)
    
def label_func(fname):
    return re.match(r'^(.*)_\d+.jpg$', fname.name).groups()[0]

class SiameseTransform(Transform):
    def __init__(self, files, label_func, splits):
        self.labels = files.map(label_func).unique()
        self.lbl2files = {l: L(f for f in files if label_func(f) == l) for l in self.labels}
        self.label_func = label_func
        self.valid = {f: self._draw(f) for f in files[splits[1]]}
        
    def encodes(self, f):
        f2,t = self.valid.get(f, self._draw(f))
        img1,img2 = PILImage.create(f),PILImage.create(f2)
        return SiameseImage(img1, img2, t)
    
    def _draw(self, f):
        same = random.random() < 0.5
        cls = self.label_func(f)
        if not same: cls = random.choice(L(l for l in self.labels if l != cls)) 
        return random.choice(self.lbl2files[cls]),same
    
splits = RandomSplitter()(files)
tfm = SiameseTransform(files, label_func, splits)
tls = TfmdLists(files, tfm, splits=splits)
dls = tls.dataloaders(after_item=[Resize(224), ToTensor], 
    after_batch=[IntToFloatTensor, Normalize.from_stats(*imagenet_stats)])


# + [markdown] id="3GdlM_xtcbD5"
# Let's now build a custom model for the Siamese task. We will use a pre-trained model, pass 2 images through it, concatinate the results, then send them to a custom head that will return 2 predictions.
#
# In terms of overall architecture and models lets define it like this.

# + id="UxDNE0x6czgS"
class SiameseModel(Module):
    def __init__(self, encoder, head):
        self.encoder,self.head = encoder,head
    
    def forward(self, x1, x2):
        ftrs = torch.cat([self.encoder(x1), self.encoder(x2)], dim=1)
        return self.head(ftrs)


# + [markdown] id="tCinjML6c7f1"
# We can create a body/encoder by taking a pre-trained model and cutting it, we just need to specify where we want to cut. The cut position for a ResNet is -2.

# + colab={"base_uri": "https://localhost:8080/", "height": 83, "referenced_widgets": ["e58a8371e3244744906c7e25a16e99f3", "e3db04ef61d044d18a5b399d83216401", "02ddb25257b54c878eb599da96e65474", "ece02ba7171f49ee81a6457655f4c7e8", "235d6688b4f64b9ca845368146fa107a", "ad68f0b4ab2d4e1b844655aaa6887a0f", "c7582be27cf64f469ff2d3d2d839601a", "56d5191acc22490d86abfc609a796020"]} id="CVPO_4vpdPYR" outputId="db9a3e55-4296-4a6a-86e5-e06d88b2fc80"
encoder = create_body(resnet34, cut=-2)

# + [markdown] id="dKyRSb_AdUIg"
# We can then create a head. If we look at the encoder/body it will tell us the last layer has 512 features, so this head will take 2*512 - as we will have 2 images.

# + id="WKRNvOhJdpHy"
head = create_head(512*2, 2, ps=0.5)

# + [markdown] id="jReq7KxYdtZA"
# We can now build our model from our constructed head and body.

# + id="tkXFNDh5dx9z"
model = SiameseModel(encoder, head)


# + [markdown] id="fwJg0XW7d3F3"
# Before we can use a Learner to train the model we need to define 2 more things. Firstly, a loss function. We might use here cross-entropy, but as our targets are boolean we need to convert them to integers or Pytorch will throw and error.
#
# Secondly, we need to define a custom splitter that will tell the fastai library how to split the model into parameter groups, which will help train only the head of the model when we do transfer learning. Here we want 2 parameter groups one for the encoder/body and one for the head. So lets define a splitter as well.

# + id="fbMN52H2esgy"
def loss_func(out, targ):
    return nn.CrossEntropyLoss()(out, targ.long())

def siamese_splitter(model):
    return [params(model.encoder), params(model.head)]


# + [markdown] id="eyYoGI_7e1KG"
# We can now define a learner using our data, model, loss function, splitter and a metric. As we are defining a learner manually here, we also have to call freeze manually as well, to ensure only the last paramete group i.e. the head is trained.

# + id="NqCpEO-GfKvW"
learn = Learner(dls, model, loss_func=loss_func, 
                splitter=siamese_splitter, metrics=accuracy)
learn.freeze()

# + [markdown] id="Yid23FynfOxf"
# Let's now train our model.

# + colab={"base_uri": "https://localhost:8080/", "height": 173} id="9UjXCwlKfSn2" outputId="e66fdde1-692b-4345-ea22-1147fa8a594a"
learn.fit_one_cycle(4, 3e-3)

# + [markdown] id="bfEpk-TOfWEo"
# This has trained only our head. Lets now unfreeze the whole model to make it all trainable, and use discriminative learning rates. This will give a lower learning rate for the body and a higher one for the head.

# + colab={"base_uri": "https://localhost:8080/", "height": 173} id="JISyfyAyflXW" outputId="1d94dbac-e7e5-4813-f371-8cdbe5ee84bc"
learn.unfreeze()
learn.fit_one_cycle(4, slice(1e-6,1e-4))

# + [markdown] id="YZt5w4k7hVMr"
# ## Points to consider with architectures
#
# There are a few points to consider when training models in practice. if you are running out of memory or time - then training a smaller model could be a good approach. If you are not training long enough to actually overfit, then you are probably not taking advantage of the capacity of your model.
#
# So one should first try to get to the point where your model is overfitting.
#
# ![](https://github.com/pranath/blog/raw/master/images/practical_principles.png "Practical principles of applying deep learning in practice")
#
# Often many people when faced with a model that overfits, start with the wrong thing first i.e. to use a smaller model, or more regularization. Using a smaller model should be one of the last steps one tries, as this reduces the capaity of your model to actually learn what is needed.
#
# A better approach is to actually try to use **more data**, such as adding more labels to the data, or using data augmentation for example. Mixup can be useful for this. Only once you are using much more data and are still overfitting, one could consider more generalisable architectures - for example adding batch norm could help here.
#
# After this if its still not working, one could use regularisation, such as adding dropout to the last layers, but also throughout the model. Only after these have failed one should consider using a smaller model.

# + [markdown] id="5cJD1Y8Y3aLl"
# ## Conclusion
#
# In this article we have looked at how to build custom fastai application architectures, using image model examples.

StarcoderdataJupyterScriptsDedupFiltered