Fastai v2 daily code walk-thrus

I’ve added a playlist in my YouTube account to keep things simpler - although as mentioned the list is in the top post of this thread.

4 Likes

Read the Python Data Model docs about metaclasses, and see what it says about this. Let us know what you learn! :slight_smile:

2 Likes

Thank Jeremy! When I have time I plan on hitting “autoplay” to catch up :wink:

1 Like

Thanks Jeremy! Have made the correction in my post.

I have to head to NY tomorrow and get ready this afternoon, so there won’t be any code walk-thrus until Monday. Good chance for everyone to catch up! :slight_smile:

20 Likes

I’m still trying to wrap my head around this and how to use it properly. I’m designing an AudioSpectrogram class that subclasses TensorImageBase (is this right or should I be subclassing TensorImage)? Either way they both inherit from, and are of type BypassNewMeta. My questions are…

  1. Why exactly is the inheritance hierarchy structured this way, what are the benefits of BypassNewMeta?
  2. How do I override __init__() in my AudioSpectrogram class that extends TensorImageBase given that that inherits from BypassNewMeta?. I’ve tried button clicking to try to get things to do what I want, the closest I’ve come is to add the line _new_meta = __init__ which does call my init method, but ends with the error TypeError: __class__ assignment only supported for heap types or ModuleType subclasses.

With my limited understanding (please correct me if I say something off), I think that is where we want to use create method.
Say you have something like:
a = AudioSpectrogram(...)

The behavior gets altered by BypassNewMeta.__call__(). So it doesn’t go to AudioSpectrogram.__init__() in a way it usually does without the metaclass.

Hence if you want to create an instance of AudioSpectrogram, you want to do:
a = AudioSpectrogram.create(...)

2 Likes

I am not really sure what the answers to these questions are. I only started to learn the library and it seems you are referring to how things fit on a higher level, what the architecture is.

It seems it allows us to pass in something, say a tensor, perform some initialization to it and swap its class without allocating a new object. Just looking at the code right now. So if we have some Tensor, we can add additional behavior to it without having to recreate a new object, that’s what it seems like from looking at the code, but I have not experimented with it so might be completely wrong.

I think it’s best to not think about classes with the BypassNewMeta type as regular classes… meaning the whole notion of __new__ and __init__ can be foregone. We are not creating a new instance of cls (which is the point of __new__) and we are not configuring the newly created instance using __init__, since there is no newly created instance :slight_smile:.

It’s probably best to think about classes of these types as taking some object, running some configuration steps on it (whatever is defined in _new_meta) and than ‘casting’ that object to the new class by swapping out the __class__ reference.

It is probably important that TensorBase inherits from Tensor. So if your AudioSpectogram is a Tensor it probably could inherit from one of the Tensor classes (not sure which one would work best for your specific use case) and you can endow it with additional behavior via defining methods on the AudioSpectogram class…

Well, sorry, just trying to be helfpul but not sure if this is accurate. Hopefully in a couple of days, possibly weeks, I will be able to say more :slight_smile:

1 Like

At the end of the #1 walkthrough Jeremy says something very similar to what @hiromi said above :slight_smile:

There are a couple of classes in 07_vision_core.ipynb that implement create of this sort, might be a good notebook to look at for inspiration.

1 Like

Very helpful, thanks for pointing me in the right direction. I’ll be sure to follow up if and when I figure it out.

1 Like

I’m pretty sure this must be obvious by now to folks that have watched further walkthroughs (I have not yet), but just looking at PILBase right now

class PILBase(Image.Image, metaclass=BypassNewMeta):
    default_dl_tfms = ByteToFloatTensor
    _show_args = {'cmap':'viridis'}
    _open_args = {'mode': 'RGB'}
    @classmethod
    def create(cls, fn, **kwargs)->None:
        "Open an `Image` from path `fn`"
        return cls(load_image(fn, **merge(cls._open_args, kwargs)))

    def show(self, ctx=None, **kwargs):
        "Show image using `merge(self._show_args, kwargs)`"
        return show_image(self, ctx=ctx, **merge(self._show_args, kwargs))

and it seems to exemplify how BypassNewMeta is being used. PILBase inherits from PIL.Image, the create class method loads the image and calls the cls on it (triggering the BypassNewMeta mechanism of swapping out the class (1) and calling _new_meta that is absent in PILBase).

And in the end we get an instance of PILBase with all the added functionality (some attributes (4) and the show method (3)) while we still retain all the functionality of PIL.Image (2). And on top of that we can now use the type in type annotations for things like TypeDispatch()

(It retains the core of being a `PIL.Image.Image and all of its functionality! (2))

I have been thinking and experimenting a bit more on why we need the BypassNewMeta and I think the answer lies somewhere around the fact that we cannot swap out the self of an instance but we can swap out the __class__ reference.

Also, PIL.Image.Image (the class we might want to inherit from if we wanted to endow our new class with all of its functionality) does not have PIL.Image.Image.open. PIL.Image.open is a module function that returns a PIL.Image.Image so as a starting point we already have an existing instance. Not sure what we could do else? Somehow modify PIL.Image.open to return PILImageBase, PILImageMask and so on? But that would be messy and I am not sure if it is at all possible as I am guessing both PIL.Image.Image and PIL.Image.open probably wrap something written in C (maybe?)

Where the miracle happens is in a class inheriting from BypassNewMeta, in the __call__ the cls is a class created to our new class recipe! But it is fully instantiated (whatever that means in the context of a class) and we can just swap out the __class__ attribute on an object (whereas we cannot swap out the self). And in the end, it all works :smile:

class BypassNewMeta(type):
    "Metaclass: casts `x` to this class, initializing with `_new_meta` if available"
    def __call__(cls, x, *args, **kwargs):
        if hasattr(cls, '_new_meta'): x = cls._new_meta(x, *args, **kwargs)
        if cls!=x.__class__: x.__class__ = cls
        return x

Magic or technology? The words of Arthur C. Clarke may have never rang truer

Any sufficiently advanced technology is indistinguishable from magic.

3 Likes

Observation: because the notebooks do from local.something import something you can change code in the .py files in local and restart the kernel and you will get the new functionality!

Very useful for trying to better understand what is going on by removing / adding code here and there (or even adding a print call!)

BypassNewMeta changes init so that if you pass in something of the right class, and no other args, then it simply returns that, instead of creating a new object. Which is what you want in many situations, including getting the ability to cast types for your class.

Note that the existing init is unchanged other than that, so you can still use it.

2 Likes

thanks @muellerzr :slight_smile:

I feel that I have quite badly underestimated how cool literate programming + jupyter nbs + inline docs + self contained test framework are… Or in other ways, to what extent Jeremy and Sylvain have gone out of their way to make learning easy for us. I have been jumping between 08_pets and code in vim trying to understand what is going on… but there are literally tens of notebooks with prose explaining things… And you can touch and mess around with things on the spot… If only someone told me about this say in multiple hours of code walkthroughs :man_facepalming:

Or more precisely, if only I were more name_of_a_useful_attribute_to_have to pick up on those very slight hints in multiple hours of recordings and also plastered all over the forums :smile:

As you can imagine, I wouldn’t say I am particularly happy with my approach thus far. But guess we’re all only human after all, and if to be alive is to err, I guess I shall accept this state of affairs happily.

Anyhow, lesson learned - watch the walkthroughs, read the NBs, postpone worrying about not understanding how things work in detail till a later time.

omw to getting lost in the amazing notebooks

11 Likes

I couldn’t agree more - my very sentiments perfectly! We all owe Jeremy a debt of gratitude.

2 Likes

Understanding TypeDispatch

So I’ve spent a bit of time trying to understand TypeDispatch, and it’s really powerful! Basically, its a dictionary between types and functions.

You can refer to type hierarchy here

Let’s dig deeper and you’ll see how powerful it is!

def __init__(self, *funcs):
    self.funcs,self.cache = {},{}
    for f in funcs: self.add(f)
    self.inst = None

The __init__ takes in a list of functions, and adds the list of functions to the dictionary with type:func mapping. Inside TypeDispatch the type is determined by the annotation of the first parameter of a function f.

Too confusing? Let’s put it together.

#export
class TypeDispatch:
    "Dictionary-like object; `__getitem__` matches keys of types using `issubclass`"
    def __init__(self, *funcs):
        self.funcs,self.cache = {},{}
        for f in funcs: self.add(f)
        self.inst = None

    def _reset(self):
        self.funcs = {k:self.funcs[k] for k in sorted(self.funcs, key=cmp_instance, reverse=True)}
        self.cache = {**self.funcs}

    def add(self, f):
        "Add type `t` and function `f`"
        self.funcs[_p1_anno(f) or object] = f
        self._reset()

    def __repr__(self): return str({getattr(k,'__name__',str(k)):v.__name__ for k,v in self.funcs.items()})

Let’s look at a simpler version of TypeDispatch

Now, let’s create a function:

def some_func(a:numbers.Integral, b:bool)->TensorImage: pass

and pass it to TypeDispatch

t = TypeDispatch(some_func); t

>>>{'Integral': 'some_func'}

Viola! TypeDispatch works…! BUT how?

Step-1: __init__ takes a bunch of functions or a single function. To start with, self.funcs and self.cache are empty as defined by self.funcs,self.cache = {},{}

Step-2: for f in funcs: self.add(f) loop through each function passed and add them to dictionary self.funcs using add.
Inside, add, check for the annotation of the first parameter of function f, if None then use type object and add it to self.funcs.
Thus inside self.funcs creating a mapping between type of first param of f and f itself.

Step-3: Reorder self.funcs dictionary basd on key cmp_instance which sets the order using Python’s type hierarchy in reverse order. Thus if you pass int and bool, the first item inside this dict will be bool.
Finally, make self.cache same as self.funcs. We use cache to loop up mapping later. Since lookup keys inside dict is order f(1) it’s much faster.

And finally we have __repr__ which just returns the mapping self.funcs but return f's name and type's name.
Reason why there is a getattr inside getattr(k,'__name__',str(k) is I think because it’s possible that a type doesn’t have __name__ attribute when we use MetaClasses.

Hopefully, this helps everyone! Please feel free to correct me if I understood something wrong.

We do reorder as Jeremy said in walk-thru 5, because we try to find the closest match from Transforms. Thus, for integer the closest match would first be int and not Numbers.Integral.

Also, inside docstring of __getitem__: "Find first matching type that is a super-class of k"

1 Like

Now here’s an open question:

def some_func2(a, b:bool)->TensorImage: pass
t = TypeDispatch(some_func2); t

>>>{'bool': 'some_func2'}

At this point, we are using the second param mapping inside TypeDispatch but,

def _p1_anno(f):
    "Get the annotation of first param of `f`"
    hints = type_hints(f)
    ann = [o for n,o in hints.items() if n!='return']
    return ann[0] if ann else object

According to docstring of _p1_anno we should be using the annotation of first param of f

Thoughts??

1 Like

Here’s an insight!

So now that we know TypeDispatch is nothing but a pretty cool dict that looks something like:

{
bool: some_func1,
int: some_func2,
Numbers.Integral: some_func3 
}

ie., it is a mapping between type and the function that needs to be called on that specific type.

This is done through __call__ inside TypeDispatch ofcourse!

    def __call__(self, x, *args, **kwargs):
        f = self[type(x)]
        if not f: return x
        if self.inst: f = types.MethodType(f, self.inst)
        return f(x, *args, **kwargs)

f = self[type(x)] Check type of param being called ie., and look it up in TypeDispatch dict and call that function.
ie., foo(2) will return type(2) as int and then we lookup int which is coming from __getitem__ which simply returns the first matching type that is a super-class of type.

So we lookup inside self.cache which is also a mapping like

{
bool: some_func1,
int: some_func2,
Numbers.Integral: some_func3 
}

and we will find a function some_func2 for int. Thus, __getitem__ will return some_func2 as f.

So, f = self[type(x)] sets f as some_func2.

This is the magic! We will call the specific function using __call__ for the specific type based on the parameter being passed!!

Thus when we pass a TensorImage, it will find the function that corresponds to TensorImage from inside dict and call it which is just as simple as return f(x, *args, **kwargs)!

1 Like

In my quest to find more about the behaviour of the metaclass, I went through the Python Data Model docs but could not understand much. But I stumbled upon this article which I believe best explains the behaviour and is very close to the way we have implemented things in fastaiv2.

Let’s start with the basics. Let’s take a look at the relation between Instance, Class and Metaclass.

If we define a class and then create an instance of the class, the python interpreter will start working like this.

If we define a metaclass and then create a class that uses this metaclass, then the python interpreter will start working like this.

So this explains as to why the first call of Transform(_norm) went to the Metaclass call method. The metaclass of Transform is _TfmMeta. From there on it would call the new and the init method of the class which is Transform here. Since the init is defined here in Transform, this call is made. If the init and/or the new was not there in Transform then they would be delegated above to the ancestors of Transform.

Is the understanding correct here?

2 Likes