Data augmentation and .pkl file

I successfully exported my model to a .pkl file. I am deploying my model on a Jetson nano GPU. The problem is that I created a custom transform (RandFlip) that I included in the item_tfms parameter of DataBlock. I want this transform to be applied only in training and not in inference. My input is a camera image. On the nano I am feeding the camera image directly to the predict function without going through a data loader. I do not want the transform RandFlip to be applied in deploying the model.

The problem I have is that the export operation fails complaining that it does not recognize my RandFlip class. This should not be a problem because RandFlip should not be applied in deployment. If I remove the RandFlip class in training, the export to .pkl works fine and I can do prediction on the nano.

So my question, is there a way to do augmentation with my custom RandFlip class, export my model to a .pkl file, and do inference bypassing augmentation?

Is this the right place to ask my question?

Create a new dummy class RandFlip that just does nothing but return the inputs as it is. That being said I’m also looking forward to better answers on how to handle these cases efficiently.

Set it’s split_idx to 1 on the transform so it only applies to validation?

Otherwise check out my test_dl article, I showed an example of new pipelines to replace the old, this can apply for after_batch and after_item as well: https://muellerzr.github.io/fastblog/2020/08/10/testdl.html

Setting split_idx allowed the export to work successfully. But in loading “export.pkl” on the nano I now get this error.
AttributeError: Can’t get attribute ‘RandFlip’ on <module ‘main’ from ‘testdrive.py’>

I can probably solve this by adding the RandFlip class to my testdrive.py but this should be unnecessary.

No that is actually completely necessary. Pickles reference source code, they are not source code in of itself.

But export.pkl is 120M vs the .pth which is 5M. Loading the .pkl it seems it is including all the fastai stuff but not my custom class.

That is correct. Because all the fastai stuff is inside the namespace available when you load it in. Your custom class is not. It exports references to it, but not the actual class itself. The fastai bits work the exact same way as well, you have fastai installed it can grab those references (and the steps to recreate your Learner from it)

The same thing happens with the acc_camvid function for anyone trying to export and bring in a segmentation model from the course (it’s a very very often asked question on the forums)

1 Like

Thanks for the explanation. This stuff makes my head spin. Jeremy swears you don’t need a PhD but…

I can concur :slight_smile: I’m still in Undergrad :slight_smile: Just come with a general idea of the course will not prepare you for every scenario, and over time you will learn much more Python (which is why he recommends coding for at least 1 year before taking the course).

1 Like

To followup, this is the transform I want to happen in training but not inference. I thought that since this is called in the DataLoader it would not be called in inference because I am using learn.predict(image) which presumably bypasses the DataLoader. So, setting both split_idx=1 and p=0 do not prevent the transform being applied in inference. The transform happens about half the time like it was in training with p=.5. This makes no sense to me.

class RandFlip(RandTransform):
    def __init__(self, p=0, **kwargs):
        super().__init__(p=p, split_idx=1, **kwargs)
    def encodes(self, o: Tensor):
        return Tensor([-o[0], o[1]])   # this reverses the target steering value
    def encodes(self, o: PILImage):
        print("This should not happen")  # with p=0 this should not print
        return ImageOps.mirror(o)     # this mirrors the input image

I added the print statement to debug inference.

Does calling an instance of it and checking what it’s split idx show 1?

mytfm = RandFlip()
mytfm.split_idx

Otherwise I would set it up similar to the other transforms like so:

class RandFlip(RandTransform):
    split_idx=1
    def __init__(self, p=0, **kwargs):
        super().__init__(p=p, **kwargs)
    def encodes(self, o: Tensor):
        return Tensor([-o[0], o[1]])   # this reverses the target steering value
    def encodes(self, o: PILImage):
        print("This should not happen")  # with p=0 this should not print
        return ImageOps.mirror(o)     # this mirrors the input image

Not sure on the p issue OTTOMH

Update. I solved my problem by not exporting to a .pkl file. Instead I saved the state dict on the training side which turns out saving a lot of space (pickling: 120M vs state_dict: 1.6M).

torch.save(model.state_dict(), new_model_path)

Then on the inference side I just included the model definition and loaded an image like this. Who knew it could be so easy.

from torchvision import transforms
from torch.autograd import Variable

model.load_state_dict(torch.load(new_model_path)

def image_loader(image_path):
    """load image, returns cuda tensor"""
    image = Image.open(image_path)
    image = transforms.ToTensor()(image).float()
    image = Variable(image, requires_grad=True)
    image = image.unsqueeze(0)  
    #return image.cuda()  #assumes that you're using GPU
    return image  #assumes no GPU

image = image_loader(image_path)

model(image)  # this does the prediction
2 Likes

Make sure though when you’re doing this to normalize your image the same way you trained. Otherwise it won’t work as you expect

1 Like

You don’t need the image = Variable(image, requires_grad=True) line in inference.