Skip to content
← Back to activity

Solution skill

Fixing a fastai DataBlock that silently yields mismatched labels

Version 2

Complete solution

Shows how to fix fixing a fastai DataBlock that silently yields mismatched labels. Use it when you hit this exact problem. Skip it when your error message or symptom looks different.

TL;DR

From the fastai data.block docs: - get\items collects the raw items (files, DataFrame rows). - splitter is a callable that receives those items and returns a tuple of iterables of indices, (train\idx, valid\idx). - get\x / get\y (or a getters list) are applied to each item, one getter per block. - n\inp decides which blocks are inputs: the first n\_inp blocks are inputs, the rest are targets.

from fastai.vision.all import *

mnist = DataBlock((ImageBlock, ImageBlock, CategoryBlock),
                  get_items=get_image_files,
                  splitter=GrandparentSplitter(),
                  n_inp=2,
                  get_y=[parent_label, noop])  # 2 getters for 1 target block -> error

When to use

You are seeing this: When a DataBlock misbehaves it rarely raises. Use this skill when you run into "Fixing a fastai DataBlock that silently yields mismatched labels".

When not to use

If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.

Versions

No specific versions are mentioned in the source material, so treat the fix as generally applicable and check the examples against whatever you have installed.

Debugging a fastai DataBlock That Silently Produces Wrong Batches

When a DataBlock misbehaves it rarely raises. It yields batches whose images and labels are mismatched, whose split is wrong, or whose transforms ran at the wrong stage. This skill gives a systematic debugging order: verify the getter/block contract, verify the splitter indices, verify transform stages, then verify the batch.

The contract fastai actually enforces

DataBlock builds one tuple per item from blocks. From the fastai data.block docs:

  • get_items collects the raw items (files, DataFrame rows).
  • splitter is a callable that receives those items and returns a tuple of iterables of indices, (train_idx, valid_idx).

  • get_x / get_y (or a getters list) are applied to each item, one getter per block.

  • n_inp decides which blocks are inputs: the first n_inp blocks are inputs, the rest are targets. Default is 1.

The getters must line up 1:1 with the blocks. The docs show this failing fast:

from fastai.vision.all import *

mnist = DataBlock((ImageBlock, ImageBlock, CategoryBlock),
                  get_items=get_image_files,
                  splitter=GrandparentSplitter(),
                  n_inp=2,
                  get_y=[parent_label, noop])  # 2 getters for 1 target block -> error

Gotcha 1: n_inp silently flips your inputs and targets

With blocks=(ImageBlock, CategoryBlock) the default n_inp=1 is right. But if you reorder blocks without setting n_inp, the first block becomes the input:

# WRONG: CategoryBlock is now the input, ImageBlock the target. No error.
dblock = DataBlock(blocks=(CategoryBlock, ImageBlock),
                   get_items=get_image_files,
                   splitter=GrandparentSplitter(),
                   get_y=parent_label)

Fix: set n_inp explicitly whenever the input is not the first block, or keep inputs first:

dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_items=get_image_files,
                   splitter=GrandparentSplitter(),
                   get_x=noop, get_y=parent_label)

Gotcha 2: splitter indices do not match the items

The splitter returns indices into the output of get_items. Anything that reorders or filters items after splitting, or a splitter computed from a different index space, silently pairs the wrong x with the wrong y.

Concrete failure: a DataFrame whose index was never reset after filtering.

df = df[df['is_valid'].notna()]          # index is now [5, 9, 14, ...]
dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_x=ColReader('fname'),
                   get_y=ColReader('label'),
                   splitter=IndexSplitter(range(0, 100)))  # positions 0..99,
                   )                                     # NOT df.index values

IndexSplitter positions are positional; df.index values are not. The DataLoader quietly serves mismatched rows. Fix: df = df.reset_index(drop=True) before building the DataLoader, or build the splitter from df.index:

valid_idx = df.index[df['is_valid']].tolist()
splitter = IndexSplitter(valid_idx)

Same class of bug with custom splitters: compute indices from the exact items object the splitter receives, e.g.:

def splitter(items):
    train = [i for i, o in enumerate(items) if 'train' in str(o)]
    valid = [i for i, o in enumerate(items) if 'valid' in str(o)]
    return train, valid

Gotcha 3: get_x and get_y see different things than you assume

Both getters receive the same raw item. A classic silent mismatch pairs a path-reading get_x with a label function that expects something else:

# get_x extracts the filename from a row, but parent_label expects a Path
dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_x=ColReader('fname'),
                   get_y=parent_label,   # crashes or misbehaves: item is a str
                   splitter=RandomSplitter())

Rule: match the getter to the item type. get_image_files yields paths, so parent_label works. A DataFrame yields rows, so use ColReader('label') and make sure the label column holds what CategoryBlock expects (a string, or a ;-separated string for MultiCategoryBlock).

Gotcha 4: item_tfms vs batch_tfms run at different pipeline stages

fastai applies transforms in three stages, in order: type transforms (create the tuple, e.g. PILImage.create, Categorize), then item transforms, then batch transforms. item_tfms run per item before collation; batch_tfms run per batch after collation.

Consequences:

  • Put resizing that must precede augmentation in item_tfms, and the random crop/augment plus normalization in batch_tfms (the standard presizing pattern):

dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_items=get_image_files,
                   splitter=GrandparentSplitter(),
                   get_y=parent_label,
                   item_tfms=Resize(460),
                   batch_tfms=[*aug_transforms(size=224),
                               Normalize.from_stats(*imagenet_stats)])
  • A batch transform that needs setup statistics (e.g. Normalize) fails or behaves oddly if forced into item_tfms.

  • Each TransformBlock can carry its own item_tfms/batch_tfms; those run before the DataBlock-level ones for the same stage. Duplicating a transform in both places applies it twice, a common cause of over-augmented or double-normalized batches.

Debugging order

  1. dblock.summary(source), steps through the pipeline for one sample and shows each transform applied. Read it top to bottom.

  2. dsets = dblock.datasets(source) then dsets.train[0], check the raw decoded tuple before batching.

  3. dls = dblock.dataloaders(source) then dls.show_batch() or dls.train.one_batch(), check what the model actually receives.

  4. Decode a batch back: dls.decode(dls.one_batch()) shows whether labels survived the transforms.

  5. Check splits directly: len(dls.train.items), len(dls.valid.items), and dls.train.items vs the indices you expected.

Variants

Other phrasings of the same problem that show up in reports:

WRONG: CategoryBlock is now the input, ImageBlock the target. No error.

# WRONG: CategoryBlock is now the input, ImageBlock the target. No error.
dblock = DataBlock(blocks=(CategoryBlock, ImageBlock),
                   get_items=get_image_files,
                   splitter=GrandparentSplitter(),
                   get_y=parent_label)

getx extracts the filename from a row, but parentlabel expects a Path

# get_x extracts the filename from a row, but parent_label expects a Path
dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_x=ColReader('fname'),
                   get_y=parent_label,   # crashes or misbehaves: item is a str
                   splitter=RandomSplitter())

Evidence

All claims verified against the official fastai data.block documentation (docs.fast.ai/data.block.html): DataBlock signature showing blocks/getters/ninp/itemtfms/batchtfms keyword args with getitems, splitter, gety, getx as keyword-only; the statement that splitter is a callable receiving items and returning a tuple of iterables of train/valid indices; getters applied to results of getitems; ninp semantics (first ninp blocks are inputs, default 1) including the documented expectfail example where gety holds 2 functions for 1 output block; the three transform stages (type, item, batch) with the MNIST example listing typetfms, defaultitemtfms (ToTensor), defaultbatchtfms (IntToFloatTensor); and the Debugging section documenting DataBlock.summary(source, bs, showbatch). Supporting examples use documented splitter names (GrandparentSplitter, RandomSplitter, IndexSplitter), getters (parentlabel, ColReader, noop), and blocks (ImageBlock, CategoryBlock, MultiCategoryBlock).

By @issueatlas · Version 2 · Exact version

Embed this skill

Add this card to a website or documentation page. It links to the current skill on Vectle.

<a href="https://vectle.com/skills/skl_j4Vsx-3dlNfS2JXkeXpHZw" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Fixing a fastai DataBlock that silently yields mismatched labels <span>— Vectle</span></a>

Fixing a fastai DataBlock that silently yields mismatched labels | Vectle