Issue with onehot encoding

I’m trying to break captcha with fastai. The images contains 4 digits, so I’d like to encoding the target with onehot:

encoding_dict = {l:e for e,l in enumerate("0123456789")}
decoding_dict = {e:l for l,e in encoding_dict.items()}
code_dimension=10 #10=>0-9
captcha_dimension=4 # because we have 5 length captcha

def to_onehot(filepath):
    code = filepath.parts[-1].split("_")[0]
    onehot = np.zeros((code_dimension, captcha_dimension))
    for column, letter in enumerate(code):
        onehot[encoding_dict[letter], column] = 1
    return onehot.reshape(-1)

and construct datablock this way:

path = "/kaggle/working/sample/"

bearer = DataBlock(blocks=(ImageBlock, RegressionBlock), get_items=get_image_files,get_y=to_onehot)
dls = bearer.dataloaders(source=path)

the learner is:

learner = cnn_learner(dls, resnet18, metrics=error_rate)
learner.fine_tune(1)

However, this will cause the following error:

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in _with_events(self, f, event_type, ex, final)
    158 
    159     def _with_events(self, f, event_type, ex, final=noop):
--> 160         try: self(f'before_{event_type}');  f()
    161         except ex: self(f'after_cancel_{event_type}')
    162         self(f'after_{event_type}');  final()

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in _do_fit(self)
    201         for epoch in range(self.n_epoch):
    202             self.epoch=epoch
--> 203             self._with_events(self._do_epoch, 'epoch', CancelEpochException)
    204 
    205     def fit(self, n_epoch, lr=None, wd=None, cbs=None, reset_opt=False):

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in _with_events(self, f, event_type, ex, final)
    158 
    159     def _with_events(self, f, event_type, ex, final=noop):
--> 160         try: self(f'before_{event_type}');  f()
    161         except ex: self(f'after_cancel_{event_type}')
    162         self(f'after_{event_type}');  final()

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in _do_epoch(self)
    196     def _do_epoch(self):
    197         self._do_epoch_train()
--> 198         self._do_epoch_validate()
    199 
    200     def _do_fit(self):

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in _do_epoch_validate(self, ds_idx, dl)
    192         if dl is None: dl = self.dls[ds_idx]
    193         self.dl = dl
--> 194         with torch.no_grad(): self._with_events(self.all_batches, 'validate', CancelValidException)
    195 
    196     def _do_epoch(self):

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in _with_events(self, f, event_type, ex, final)
    158 
    159     def _with_events(self, f, event_type, ex, final=noop):
--> 160         try: self(f'before_{event_type}');  f()
    161         except ex: self(f'after_cancel_{event_type}')
    162         self(f'after_{event_type}');  final()

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in all_batches(self)
    164     def all_batches(self):
    165         self.n_iter = len(self.dl)
--> 166         for o in enumerate(self.dl): self.one_batch(*o)
    167 
    168     def _do_one_batch(self):

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in one_batch(self, i, b)
    183         b_on_device = tuple( e.to(device=self.dls.device) for e in b if hasattr(e, "to")) if self.dls.device is not None else b
    184         self._split(b_on_device)
--> 185         self._with_events(self._do_one_batch, 'batch', CancelBatchException)
    186 
    187     def _do_epoch_train(self):

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in _with_events(self, f, event_type, ex, final)
    160         try: self(f'before_{event_type}');  f()
    161         except ex: self(f'after_cancel_{event_type}')
--> 162         self(f'after_{event_type}');  final()
    163 
    164     def all_batches(self):

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in __call__(self, event_name)
    139 
    140     def ordered_cbs(self, event): return [cb for cb in self.cbs.sorted('order') if hasattr(cb, event)]
--> 141     def __call__(self, event_name): L(event_name).map(self._call_one)
    142 
    143     def _call_one(self, event_name):

/opt/conda/lib/python3.7/site-packages/fastcore/foundation.py in map(self, f, gen, *args, **kwargs)
    152     def range(cls, a, b=None, step=None): return cls(range_of(a, b=b, step=step))
    153 
--> 154     def map(self, f, *args, gen=False, **kwargs): return self._new(map_ex(self, f, *args, gen=gen, **kwargs))
    155     def argwhere(self, f, negate=False, **kwargs): return self._new(argwhere(self, f, negate, **kwargs))
    156     def filter(self, f=noop, negate=False, gen=False, **kwargs):

/opt/conda/lib/python3.7/site-packages/fastcore/basics.py in map_ex(iterable, f, gen, *args, **kwargs)
    664     res = map(g, iterable)
    665     if gen: return res
--> 666     return list(res)
    667 
    668 # Cell

/opt/conda/lib/python3.7/site-packages/fastcore/basics.py in __call__(self, *args, **kwargs)
    649             if isinstance(v,_Arg): kwargs[k] = args.pop(v.i)
    650         fargs = [args[x.i] if isinstance(x, _Arg) else x for x in self.pargs] + args[self.maxi+1:]
--> 651         return self.func(*fargs, **kwargs)
    652 
    653 # Cell

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in _call_one(self, event_name)
    143     def _call_one(self, event_name):
    144         if not hasattr(event, event_name): raise Exception(f'missing {event_name}')
--> 145         for cb in self.cbs.sorted('order'): cb(event_name)
    146 
    147     def _bn_bias_state(self, with_bias): return norm_bias_params(self.model, with_bias).map(self.opt.state)

/opt/conda/lib/python3.7/site-packages/fastai/callback/core.py in __call__(self, event_name)
     42                (self.run_valid and not getattr(self, 'training', False)))
     43         res = None
---> 44         if self.run and _run: res = getattr(self, event_name, noop)()
     45         if event_name=='after_fit': self.run=True #Reset self.run to True at each end of fit
     46         return res

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in after_batch(self)
    493         if len(self.yb) == 0: return
    494         mets = self._train_mets if self.training else self._valid_mets
--> 495         for met in mets: met.accumulate(self.learn)
    496         if not self.training: return
    497         self.lrs.append(self.opt.hypers[-1]['lr'])

/opt/conda/lib/python3.7/site-packages/fastai/learner.py in accumulate(self, learn)
    415     def accumulate(self, learn):
    416         bs = find_bs(learn.yb)
--> 417         self.total += learn.to_detach(self.func(learn.pred, *learn.yb))*bs
    418         self.count += bs
    419     @property

/opt/conda/lib/python3.7/site-packages/fastai/metrics.py in error_rate(inp, targ, axis)
    103 def error_rate(inp, targ, axis=-1):
    104     "1 - `accuracy`"
--> 105     return 1 - accuracy(inp, targ, axis=axis)
    106 
    107 # Cell

/opt/conda/lib/python3.7/site-packages/fastai/metrics.py in accuracy(inp, targ, axis)
     97 def accuracy(inp, targ, axis=-1):
     98     "Compute accuracy with `targ` when `pred` is bs * n_classes"
---> 99     pred,targ = flatten_check(inp.argmax(dim=axis), targ)
    100     return (pred == targ).float().mean()
    101 

/opt/conda/lib/python3.7/site-packages/fastai/torch_core.py in flatten_check(inp, targ)
    799     "Check that `out` and `targ` have the same number of elements and flatten them."
    800     inp,targ = TensorBase(inp.contiguous()).view(-1),TensorBase(targ.contiguous()).view(-1)
--> 801     test_eq(len(inp), len(targ))
    802     return inp,targ

/opt/conda/lib/python3.7/site-packages/fastcore/test.py in test_eq(a, b)
     33 def test_eq(a,b):
     34     "`test` that `a==b`"
---> 35     test(a,b,equals, '==')
     36 
     37 # Cell

/opt/conda/lib/python3.7/site-packages/fastcore/test.py in test(a, b, cmp, cname)
     23     "`assert` that `cmp(a,b)`; display inputs and `cname or cmp.__name__` if it fails"
     24     if cname is None: cname=cmp.__name__
---> 25     assert cmp(a,b),f"{cname}:\n{a}\n{b}"
     26 
     27 # Cell

AssertionError: ==:
64
2560

guess 64 is the size of minibatch, and it implies it want a scalar for target, when the actual target is a 64 * 40 array. But I don’t know how to correct it.

If CategoryBlock is used, then the error will be( raised when construct dataloaders):

/opt/conda/lib/python3.7/site-packages/fastcore/transform.py in setup(self, items, train_setup)
     77     def setup(self, items=None, train_setup=False):
     78         train_setup = train_setup if self.train_setup is None else self.train_setup
---> 79         return self.setups(getattr(items, 'train', items) if train_setup else items)
     80 
     81     def _call(self, fn, x, split_idx=None, **kwargs):

/opt/conda/lib/python3.7/site-packages/fastcore/dispatch.py in __call__(self, *args, **kwargs)
    116         elif self.inst is not None: f = MethodType(f, self.inst)
    117         elif self.owner is not None: f = MethodType(f, self.owner)
--> 118         return f(*args, **kwargs)
    119 
    120     def __get__(self, inst, owner):

/opt/conda/lib/python3.7/site-packages/fastai/data/transforms.py in setups(self, dsets)
    239 
    240     def setups(self, dsets):
--> 241         if self.vocab is None and dsets is not None: self.vocab = CategoryMap(dsets, sort=self.sort, add_na=self.add_na)
    242         self.c = len(self.vocab)
    243 

/opt/conda/lib/python3.7/site-packages/fastai/data/transforms.py in __init__(self, col, sort, add_na, strict)
    215             if not hasattr(col,'unique'): col = L(col, use_list=True)
    216             # `o==o` is the generalized definition of non-NaN used by Pandas
--> 217             items = L(o for o in col.unique() if o==o)
    218             if sort: items = items.sorted()
    219         self.items = '#na#' + items if add_na else items

/opt/conda/lib/python3.7/site-packages/fastcore/foundation.py in unique(self, sort, bidir, start)
    159     def enumerate(self): return L(enumerate(self))
    160     def renumerate(self): return L(renumerate(self))
--> 161     def unique(self, sort=False, bidir=False, start=None): return L(uniqueify(self, sort=sort, bidir=bidir, start=start))
    162     def val2idx(self): return val2idx(self)
    163     def cycle(self): return cycle(self)

/opt/conda/lib/python3.7/site-packages/fastcore/basics.py in uniqueify(x, sort, bidir, start)
    571 def uniqueify(x, sort=False, bidir=False, start=None):
    572     "Unique elements in `x`, optionally `sort`-ed, optionally return reverse correspondence, optionally prepend with elements."
--> 573     res = list(dict.fromkeys(x))
    574     if start is not None: res = listify(start)+res
    575     if sort: res.sort()

TypeError: unhashable type: 'numpy.ndarray'

Hi if your datablock is (ImageBlock, RegressionBlock) your target has to be 64 (the batch size) single values.

If you need 4 categories as target (1 for each digit) I think your datablock has to be something like that (ImageBlock, CategoryBlock, CategoryBlock, CategoryBlock, CategoryBlock) and set n_inp to 1 (so you have 1 input and the ramaining 4 category are output).
I never did it so let’s wait for some other users with some experience.

Because each single digit is only 1 category, I’m not sure you have to use one hot encoding for the digit.

Thank you Mugnaio!
If i choose (ImageBlock, CategoryBlock, CategoryBlock, CategoryBlock, CategoryBlock) , then how should I provide get_y? I have tried this but failed:

path = "/kaggle/working/sample/"

get_y = [to_onehot_0, to_onehot_1, to_onehot_2, to_onehot_3]
bearer = DataBlock(blocks=(ImageBlock, CategoryBlock,CategoryBlock,CategoryBlock,CategoryBlock),n_inp=1, get_items=get_image_files,get_y=get_y)
dls = bearer.dataloaders(source=path)

bearer.summary(path)

by the way, I have tried MultiCategoryBlock also, but still get input/target size not matched issue.

As I said I don’t think you want one hot encoded (it is good for MultiCategory but it is not your case), I think every CategoryBlock want a single number from 1 to 9 and this is your category for that digit so your get_y I think must return a list of 4 categories.
EDIT: in fact, I think you need 4 functions, see next post.

It is like a single image classification, your y is not a one_hot_encoded, but it is a single number from 1 to n_cat. Here you have 4 different categories as output so you need 4 of that numbers. For example if your label for that image is “1263” you could return [“1”,“2”,“6”,“3”].

Check here and lookup for " Bounding boxes" for an example of datablock with more than one output.

This is my understanding but some more experienced user could help you more.

Ok, it seems that you need 4 different functions for get_y, one for every digit.
Because this was a good datablock exsercise for me too I tried a fast exmple with this dataset Captcha images | Kaggle.
Here every image has the name like i.e. 2634.jpg if it represent a 2634.

This is my Datablock:

def label_func1(fname):
    return  fname.name[0]

def label_func2(fname):
     return  fname.name[1]

def label_func3(fname):
    return  fname.name[2]

def label_func4(fname):
    return fname.name[3]

dblock = DataBlock(blocks=(ImageBlock, CategoryBlock,CategoryBlock,CategoryBlock,CategoryBlock),n_inp=1, get_items=get_image_files,get_y=[label_func1,label_func2, label_func3, label_func4] ,splitter  = RandomSplitter())
1 Like

I trained a model using that dls above (I just also added batch_tfms=[*aug_transforms(do_flip=False), Normalize()]) and it seems to work.
I used this

loss_func=FourDigitCrossEntropy()
learn = cnn_learner(dls, resnet18, n_out=(40),loss_func=loss_func,metrics=multiaccuracy)

so the model returns an array of len 40.

Then my loss function just keep first group of 10 values of that array to get a class for first digit, second group of 10 for second digit and so on.
It is pretty raw, I also added the decoded method so learn.predict work.

I also added a metric to get accuracy, as I said it seems to work, but any suggestion to improve it would be great.

class FourDigitCrossEntropy(BaseLoss):
  def __init__(self, *args, axis=-1, **kwargs): 
    self.func = None

  def __call__(self,inp,y1,y2,y3,y4):
    preds = inp.split(10,dim=1)
    return nn.CrossEntropyLoss()(preds[0],y1) + nn.CrossEntropyLoss()(preds[1],y2) + nn.CrossEntropyLoss()(preds[2],y3) + nn.CrossEntropyLoss()(preds[3],y4)

  def decodes(self,x):
    preds = x.split(10,dim=1) return [preds[0].argmax(dim=1), preds[1].argmax(dim=1), preds[2].argmax(dim=1), preds[3].argmax(dim=1)]`

for metric I used

def multiaccuracy(preds, y1, y2, y3, y4):
  preds = preds.split(10,dim=1)

  return ((preds[0].argmax(dim=1) == y1).float().mean() + (preds[1].argmax(dim=1) == y2).float().mean() + (preds[2].argmax(dim=1) == y3).float().mean() + (preds[3].argmax(dim=1) == y4).float().mean())/4

Thanks! It does work.

Would you please suggest if it possible to implement it in this way:

  1. Use MultiCategoryBlock with encoded=True, and get_y is:
def get_y(filepath):
    label = get_label(filepath)
    
    onehot = np.zeros((captcha_dimension, code_dimension))
    for row, letter in enumerate(label):
        col = encoding_dict[letter]
        onehot[row][col] = 1
        
    return onehot.flatten()
  1. customize the loss function, in loss function, split inp as bs410 arrays, then compare to target.

However, I have tried several time, no luck.

If I understood, you want to use MultiCategory to obtain a target like [0,0,0,0,0,0,0,01,0,0,0,1,0,0,0,0,0,0,0… (length 40). This is possible, but the problem is that the Multicategory “thinks” you have 40 categories when it try to decode your target but actually you only have 10.

you could try a label_func like

def label_func(fname):
    label = fname.name.split(".")[0]
    digits = []
    for digit in label:
      digits.append(int(digit))
    digit_tensor = torch.tensor(digits)
    onehot = F.one_hot(digit_tensor, num_classes=10)
    return onehot.flatten()

then you could “fake” your vocab with
blocks=(ImageBlock, MultiCategoryBlock(encoded=True,vocab=list(range(40))))

that surely works, it will be weird when you do dls.show_batch() though. What you really want I think is a custom MultiCategoryBlock that can decode your target.

So I would go with the 4 CategoryBlock approach.

totally understand now. It’s better to use multi CategoryBlock, rather than use MultiCategoryBlock.

Thanks!

My final code is at here:

https://www.kaggle.com/sbaaron/hacktcha

One more question, I failed to export the modle by learner.export(), it raises “can’t pickle inner object RandomSplitter issue”. Any idea how would this happen?

learner.export works for me, with your code I had an issue exporting but it was related to label_func, not RandomSplitter.

I fixed this way:

 def get_label_with(filepath, pos):
    label = get_label(filepath)
    return label[pos]

and

label_funcs = [partial(get_label_with,pos=i) for i in range(4)]

the only other difference I have is that I use a colored images.

btw, how you can past here the exact output of the code or cell errors, with colors, arrows, ecc? I just use “preformatted text” but you must use a better way.

I don’t know why my code is colorized. I just use ‘```’ (fenced code mark) and paste code/error msg from notebook

1 Like

ok thank you, it works with ```

the culprit is RandomSplitter. If i just remove it, then it works with export.

The code is at here:

identify that this is an issue of RandomSplitter. It can be reproduced by the following code:

from fastai.data.transforms import RandomSplitter
import pickle
pickle.dumps(RandomSplitter())

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-4260ef7da67d> in <module>
----> 1 pickle.dumps(RandomSplitter())

AttributeError: Can't pickle local object 'RandomSplitter.<locals>._inner'

yes, this code generates an error to me too. Anyway I very often use dataloaders with RandomSplitter and I never had an error exporting the Learner (using Colab, installing fastai last version), so I’m not sure what could be the cause.