How to build a Siamese Network with fastai-style code

I am trying to build a Siamese Network for Humpback Whale Identification simulating Alex Fitts’s work.
I have rewritten his code for his fastai version is a little bit old ( v1.0.39 ) and not compatible with the latest version so far ( v1.0.51 ).
But when I have just defined my custom dataset, trying to load the data, an error occured:

class SiamImage(ItemBase):
    def __init__(self, img1, img2):
        self.img1, self.img2 = img1, img2
        self.obj, self.data = (img1, img2), [img1.data, img2.data]
    def apply_tfms(self, tfms, *args, **kwargs):
        self.img1 = self.img1.apply_tfms(tfms, *args, **kwargs)
        self.img2 = self.img2.apply_tfms(tfms, *args, **kwargs)
        self.data = [self.img1.data, self.img2.data]
        return self
    
    def __repr__(self):
        return f'{self.__class__.__name__} {self.img1.shape, self.img2.shape}'
    def to_one(self):
        return Image(torch.cat(self.data, dim=2))

class SiamImageList(ImageList):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def __len__(self):
        return len(self.items)

    def get(self, i):
        # 0 for match and 1 for dismatch
        target = (i >= len(self.items)//2)
        img1 = super().get(i)

        whaleImgs = self.inner_df.Image.values
        whaleIds = self.inner_df.Id.values
        img1Id = whaleIds[i]
        
        img2Name = ''
        if target == 0:
            img2Name = np.random.choice(whaleImgs[whaleIds == img1Id])
        else:
            img2Name = np.random.choice(whaleImgs[whaleIds != img1Id])
        
        img2 = super().open(self.path/img2Name)
        return SiamImage(img1, img2)

    def reconstruct(self, t):
        return SiamImage(t[0], t[1])

    def show_xys(self, xs, ys, figsize:Tuple[int,int]=(9,5), **kwargs):
        rows = int(math.sqrt(len(xs)))
        fig, axs = plt.subplots(rows,rows,figsize=figsize)
        for i, ax in enumerate(axs.flatten() if rows > 1 else [axs]):
            xs[i].to_one().show(ax=ax, y=ys[i], **kwargs)
        plt.tight_layout()
        
data = (SiamImageList.from_df(df=train, path='../input/train', cols='Image')
        .split_by_rand_pct(0.2)
        .label_from_df(cols='target')
        .transform(get_transforms(), size=224)
        .databunch(bs=8)
        .normalize(imagenet_stats))

data.show_batch()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-14-53e0711f0b1c> in <module>()
     41         .normalize(imagenet_stats))
     42 
---> 43 data.show_batch()

/opt/conda/lib/python3.6/site-packages/fastai/basic_data.py in show_batch(self, rows, ds_type, reverse, **kwargs)
    183     def show_batch(self, rows:int=5, ds_type:DatasetType=DatasetType.Train, reverse:bool=False, **kwargs)->None:
    184         "Show a batch of data in `ds_type` on a few `rows`."
--> 185         x,y = self.one_batch(ds_type, True, True)
    186         if reverse: x,y = x.flip(0),y.flip(0)
    187         n_items = rows **2 if self.train_ds.x._square_show else rows

/opt/conda/lib/python3.6/site-packages/fastai/basic_data.py in one_batch(self, ds_type, detach, denorm, cpu)
    166         w = self.num_workers
    167         self.num_workers = 0
--> 168         try:     x,y = next(iter(dl))
    169         finally: self.num_workers = w
    170         if detach: x,y = to_detach(x,cpu=cpu),to_detach(y,cpu=cpu)

/opt/conda/lib/python3.6/site-packages/fastai/basic_data.py in __iter__(self)
     73     def __iter__(self):
     74         "Process and returns items from `DataLoader`."
---> 75         for b in self.dl: yield self.proc_batch(b)
     76 
     77     @classmethod

/opt/conda/lib/python3.6/site-packages/fastai/basic_data.py in proc_batch(self, b)
     68         "Process batch `b` of `TensorImage`."
     69         b = to_device(b, self.device)
---> 70         for f in listify(self.tfms): b = f(b)
     71         return b
     72 

/opt/conda/lib/python3.6/site-packages/fastai/vision/data.py in _normalize_batch(b, mean, std, do_x, do_y)
     64     "`b` = `x`,`y` - normalize `x` array of imgs and `do_y` optionally `y`."
     65     x,y = b
---> 66     mean,std = mean.to(x.device),std.to(x.device)
     67     if do_x: x = normalize(x,mean,std)
     68     if do_y and len(y.shape) == 4: y = normalize(y,mean,std)

AttributeError: 'list' object has no attribute 'device'

What’s wrong with my code? My full code can be found on gist.

The reason is that the data property of your result in apply_tfms is not a torch.Tensor.

    def apply_tfms(self, tfms, *args, **kwargs):
        self.img1 = self.img1.apply_tfms(tfms, *args, **kwargs)
        self.img2 = self.img2.apply_tfms(tfms, *args, **kwargs)
        self.data = [self.img1.data, self.img2.data] # This should be a tensor...
        return self

Take a look at @baz great thread on that:

From his code: https://github.com/mogwai/fastai_audio/blob/voice-rec/Voice%20Identification.ipynb

...
    def apply_tfms(self, tfms):
        for tfm in tfms:
            self.data = torch.stack([tfm(x) for x in self.data])
        return self
...
2 Likes