|
Download docs/custom_data.md from smerkd/affmae: direct link, hf CLI and curl.
- Browser
- Download file 14.2 kB
-
https://huggingface.co/spaces/smerkd/affmae/resolve/main/docs/custom_data.md
- Command line
-
hf download hf://spaces/smerkd/affmae/docs/custom_data.md
-
curl -L -o custom_data.md https://huggingface.co/spaces/smerkd/affmae/resolve/main/docs/custom_data.md
14.2 kB
| # Training on your own data | |
| Pretraining reads **WebDataset `.tar` shards**; finetuning is supervised and reads **image/mask | |
| directories**. | |
| ``` | |
| data/ | |
| pretrain/ # stage 1: WebDataset shards | |
| customdata-000.tar | |
| customdata-001.tar | |
| ... | |
| finetune/ # stage 2: supervised splits | |
| train/ images/ masks/ | |
| val/ images/ masks/ | |
| test/ images/ masks/ | |
| ``` | |
| `data/` does not have to be local. Configs support env vars and default reads | |
| `${DATA_ROOT:-data}/...`, so you can either symlink it | |
| ```bash | |
| ln -s /mnt/big-disk/affmae-data data | |
| ``` | |
| or point the variable elsewhere and leave the repository alone: | |
| ```bash | |
| export DATA_ROOT=/mnt/big-disk/affmae-data | |
| ``` | |
| Relative paths resolve against the **repository root**, not your shell's working | |
| directory, so training scripts work from anywhere. The same applies to | |
| `CHECKPOINT_ROOT` (default `weights`) and `AFFMAE_OUTPUT_DIR` (default `output`). | |
| --- | |
| ## Stage 1 β pretraining data (WebDataset) | |
| ### Shard layout | |
| Pretraining is masked autoencoding, so just images. Each | |
| shard is an uncompressed `.tar` of images, and `webdataset` groups files by | |
| basename, so one image per basename is all that is required: | |
| ``` | |
| customdata-000.tar | |
| βββ 000000.png | |
| βββ 000001.png | |
| βββ 000002.png | |
| βββ ... | |
| ``` | |
| Three things to consider when building shards: | |
| 1. **The extension is the key.** The loader calls `.decode("pil")` and then reads | |
| `sample["png"]`, so files must be `.png`. Rename before packing, or change the | |
| `map_dict(png=...)` key in `affmae/data/pretrain_dataset.py` to match your | |
| extension. | |
| 2. **Shards should be roughly equal in size**, ideally 100β1000 images each. | |
| Under multi-GPU training each rank gets a disjoint set of shards, so one giant | |
| shard leaves other ranks idle. | |
| 3. **Have at least as many shards as `num_workers` Γ ranks.** Shards are split | |
| across DataLoader workers *and* across ranks, so a worker that gets none makes | |
| WebDataset raise *"No samples found in dataset; perhaps you have fewer shards | |
| than workers."* Three shards with `num_workers: 8` fails, and it fails at the | |
| first batch rather than at startup. | |
| ### Building shards | |
| ```python | |
| import webdataset as wds | |
| from pathlib import Path | |
| images = sorted(Path("/my/raw/images").glob("*.png")) | |
| # maxcount caps images per shard; %03d must match the brace range in the config | |
| with wds.ShardWriter("data/pretrain/customdata-%03d.tar", maxcount=1000) as sink: | |
| for i, path in enumerate(images): | |
| sink.write({"__key__": f"{i:08d}", "png": path.read_bytes()}) | |
| ``` | |
| `ShardWriter` prints the shard count when it finishes. If it wrote | |
| `customdata-000.tar` through `customdata-031.tar`, the config pattern is | |
| `customdata-{000..031}.tar` β the brace range is **inclusive** and the digit | |
| count must match the filenames exactly. | |
| ### Counting your samples | |
| A WebDataset is an `IterableDataset` with no length, so the epoch boundary comes | |
| from `data.total_samples` in the config rather than from the data. Getting it | |
| wrong does not crash: too high repeats images, too low silently truncates the | |
| epoch. Count once: | |
| ```bash | |
| python -c " | |
| import tarfile, glob | |
| n = sum(sum(1 for m in tarfile.open(f) if m.name.endswith('.png')) | |
| for f in glob.glob('data/pretrain/*.tar')) | |
| print(n) | |
| " | |
| ``` | |
| ### Running it | |
| ```bash | |
| cp configs/template_pretrain_512.yaml configs/my_pretrain.yaml | |
| # edit data.path, data.total_samples, data.in_channels | |
| python pretrain.py --config configs/my_pretrain.yaml | |
| ``` | |
| Multi-GPU is mostly untested (use at your own rick) with `torchrun`; `data.batch_size` is the **global** batch and is divided | |
| across ranks, so the effective batch does not change with GPU count: | |
| ```bash | |
| torchrun --nproc_per_node=4 pretrain.py --config configs/my_pretrain.yaml | |
| ``` | |
| --- | |
| ## Stage 2 β finetuning data (images and masks) | |
| ### Directory layout | |
| ``` | |
| data/finetune/ | |
| train/ | |
| images/ img_0001.tif img_0002.tif ... | |
| masks/ img_0001.tiff img_0002.tiff ... | |
| val/ images/ masks/ | |
| test/ images/ masks/ | |
| ``` | |
| Things to consider for finetuning on your data: | |
| - **Images and masks pair by filename stem.** `img_0001.tif` needs | |
| `img_0001.tiff`. The loader takes the *intersection* of the two directories, so | |
| an unpaired file is skipped **without warning**. | |
| - **Image extension is configurable** via `data.input_ext` (default `.tif`). | |
| **The mask extension is fixed at `.tiff`** in | |
| `affmae/data/finetune_dataset.py`. | |
| - `val/` is the split evaluated during training. `test/` is read only by | |
| `evaluate.py`. All three are required by the templates; point `val` and `test` | |
| at the same files if you only have two splits. | |
| ### Mask format | |
| Masks are multi-channel (ie multi-label) TIFFs, one binary channel per annotated structure β **not** a single-channel label map | |
| with integer class IDs. | |
| `data.indices` selects which channels to train on, and background is added | |
| implicitly: | |
| ```yaml | |
| data: | |
| indices: [0, 1] # train on mask channels 0 and 1 | |
| num_classes: 3 # len(indices) + 1 for background | |
| ``` | |
| So `num_classes` is typically `len(indices) + 1`, depending on loss selected, and `train.class_weighting` must | |
| have exactly `num_classes` entries, background first. A wrong length fails at | |
| loss construction with *"weight tensor should be defined either for all or no classes"*. | |
| The selected channels are flattened into a single label map in `__getitem__`: | |
| each channel is binarized at `THRESHOLD = 10`, and a pixel is assigned | |
| `position_in_indices + 1`. Two consequences: | |
| - **Class IDs follow the order of `indices`, not the channel numbers.** | |
| `indices: [2, 3]` makes channel 2 class 1 and channel 3 class 2. | |
| - **Channels are applied in order, so on overlapping annotations the later channel wins.** Put the class you care most about last. | |
| A single-channel integer label map is **not** supported β `__getitem__` unpacks | |
| `C, H, W` and a 2-D mask raises a `ValueError`. One-hot it into channels when preparing the data. | |
| Raise a class's weight if it is rare β the shipped `[0.2, 2.0, 3.0]` deliberately down-weights background. | |
| ### Running it | |
| ```bash | |
| cp configs/template_finetune_512.yaml configs/my_finetune.yaml | |
| # edit data.base_path, data.indices, data.num_classes, | |
| # train.class_weighting, model.pretrained_ckpt_path | |
| python finetune.py --config configs/my_finetune.yaml | |
| ``` | |
| You do not have to pretrain first. To start from our released backbone, fetch it | |
| once and point the config at the result: | |
| ```python | |
| from affmae.data.weights import EMWeights | |
| print(EMWeights.AFFMAE_BASE_PRETRAIN_512.fetch()) | |
| # -> weights/pretrain/ckpt_epoch_399_affmae_fpw.pth | |
| ``` | |
| ```yaml | |
| model: | |
| pretrained_ckpt_path: "${CHECKPOINT_ROOT:-weights}/pretrain/ckpt_epoch_399_affmae_fpw.pth" | |
| ``` | |
| The encoder architecture keys in your config must match that checkpoint, so keep | |
| the `aff_*` block from the template unchanged unless you are pretraining yourself. | |
| See the weights table in the README for every released checkpoint. | |
| --- | |
| ## Checklist before a long run | |
| Cheap to check, expensive to discover at hour six. | |
| | Check | Symptom if wrong | | |
| |---|---| | |
| | `data.total_samples` matches your shard count | epoch silently truncated or images repeated | | |
| | Brace range digits match filenames | `FileNotFoundError` on the first batch | | |
| | `num_classes == len(indices) + 1` | crash in the loss, or a dead output channel | | |
| | `len(class_weighting) == num_classes` | *"weight tensor should be defined either for all or no classes"* | | |
| | `in_channels` matches your images (1 grey, 3 RGB) | shape mismatch in the patch embedding | | |
| | Encoder architecture keys identical to pretraining | checkpoint loads with missing/unexpected keys | | |
| | Dataset size is what you expect | unpaired image/mask stems dropped silently | | |
| | Shards >= `num_workers` x ranks | *"No samples found in dataset"* at the first batch | | |
| | `stats.py` matches your data | washed-out or clipped figures; slower convergence | | |
| | CLAHE removed if not wanted | real intensity differences flattened | | |
| | Info-bar crop removed for non-EM images | bottom of bright images silently truncated | | |
| Sanity-check the pipeline before committing to a full run: | |
| ```bash | |
| python -c " | |
| from affmae.config import load_config | |
| from affmae.data.finetune_dataset import build_finetune_dataloader | |
| cfg = load_config('configs/my_finetune.yaml') | |
| loader = build_finetune_dataloader(cfg, is_train=True) # False reads val/ | |
| img, mask = next(iter(loader))[:2] | |
| print('image', img.shape, img.dtype) | |
| print('mask ', mask.shape, 'classes present', mask.unique().tolist()) | |
| " | |
| ``` | |
| `mask.unique()` should be class indices in `[0, num_classes)`. If you see values | |
| above that, `indices` and `num_classes` disagree. | |
| --- | |
| ## Adapting the preprocessing | |
| Everything in this section is tuned for greyscale electron micrographs. None of it | |
| is wrong for other data, but none of it is neutral either β these are the four | |
| places where "trained on EM" is baked in. | |
| ### 1. Images are single-channel | |
| We trained on greyscale. Two consequences: | |
| - `data.in_channels: 1` in the config. Set it to 3 for RGB; the patch embedding | |
| and the normalization tuple both follow it. | |
| - The **pretraining loader converts to greyscale regardless**: | |
| `apply_custom_processing` in `affmae/data/pretrain_dataset.py` calls | |
| `pil_image.convert("L")`. For RGB pretraining, remove that call β otherwise | |
| `in_channels: 3` silently receives a replicated grey channel. | |
| ### 2. Normalization statistics | |
| `affmae/data/stats.py` holds four numbers measured on our data: | |
| ```python | |
| IMAGE_MEAN = 0.6266 # labelled FPW split: finetuning and inference | |
| IMAGE_STD = 0.2259 | |
| PRETRAIN_IMAGE_MEAN = 0.5562 # unlabelled pretraining corpus | |
| PRETRAIN_IMAGE_STD = 0.2396 | |
| ``` | |
| **Change these for your own data.** They are used by the loaders *and* by the | |
| renderers, which denormalize with the same numbers β so a mismatch shows up as | |
| washed-out or clipped figures long before you suspect the statistics. | |
| Measure yours with: | |
| ```bash | |
| python scripts/calc_norm.py --config configs/my_pretrain.yaml | |
| ``` | |
| It reports mean/std over your shards and prints the currently configured values | |
| next to them. It measures the *raw* pipeline deliberately: an earlier version read | |
| through the normalized pipeline and reported the residual (~0, ~1), which looks | |
| like a passing result and is useless. | |
| For multi-channel data, note these are scalars broadcast across channels | |
| (`(mean,) * n_channels`). Per-channel statistics need `create_transforms` in | |
| `affmae/data/pretrain_dataset.py` changed to take tuples. | |
| ### 3. CLAHE | |
| Contrast-limited adaptive histogram equalization is applied in **four** places, | |
| because EM micrographs have low local contrast. If your images are already | |
| well-exposed, this will flatten real intensity differences and you should remove | |
| it: | |
| | Where | What | Flag? | | |
| |---|---|---| | |
| | `affmae/data/preprocess.py` | `apply_clahe`, `clipLimit=4.25`, `8x8` tiles | yes β `preprocess_image(..., use_clahe=False)` | | |
| | `affmae/data/finetune_dataset.py:116, 214` | same parameters, inline | **no flag, edit the call** | | |
| | `affmae/data/pretrain_dataset.py:80` | `clipLimit=4.0`, inside `apply_custom_processing` | **no flag, edit the call** | | |
| | `affmae/data/pretrain_dataset.py:58` | random CLAHE, an augmentation | part of `random_transform` | | |
| Only the inference path takes a flag. The dataset classes hardcode it, so removing | |
| CLAHE from training means editing those call sites. If you do, **re-measure the | |
| normalization statistics afterwards** β CLAHE changes the intensity distribution, | |
| so the old mean/std no longer describe your inputs. | |
| ### 4. The microscope info bar | |
| `apply_custom_processing` in `affmae/data/pretrain_dataset.py` crops the bottom of | |
| the image when it finds a row that is more than 95% pure white below the 60% | |
| height mark. That removes the scale-bar and metadata strip our microscope burns | |
| into every frame. | |
| This is the most EM-specific step in the pipeline and the most likely to damage | |
| other data: any image with a genuinely bright bottom edge β a light background, a | |
| white border β gets silently truncated, and because the crop is variable-height | |
| the resize then changes the aspect ratio per image. **Remove it unless your images | |
| have the same burned-in strip.** | |
| ### 5. Augmentation | |
| `ElasticTransform` in `affmae/data/transforms.py` plus affine and photometric | |
| transforms are tuned for tissue sections, which deform plausibly. For rigid | |
| subjects (documents, industrial parts) elastic warping invents geometry that | |
| cannot occur; see `scripts/visualize_aug.py` to look at what the current pipeline | |
| does to your images before training on it: | |
| ```bash | |
| python scripts/visualize_aug.py --base-path data/finetune --output output/aug.png | |
| ``` | |
| ## Adapting the architecture | |
| Defaults are AFF-Base at 512px with patch size 8. What is worth changing: | |
| | Goal | Change | | |
| |---|---| | |
| | Larger images | `data.img_size` 768 or 1024, and lower `batch_size` | | |
| | Smaller/faster model | shorten `aff_depths`, narrow `aff_embed_dims` | | |
| | More tokens kept per stage | raise `aff_ds_rates` (0.4 keeps 40% per stage) | | |
| | Different masking difficulty | `model.mask_ratio`, pretraining only | | |
| | Out of memory | lower `batch_size`, raise `train.num_accum` to compensate | | |
| Two big constraints: | |
| - **`decoder_depth` must equal the number of encoder stages** (4 for AFF-Base), | |
| one decoder block per stage. | |
| - **Encoder keys must match between pretraining and finetuning.** The finetune | |
| config re-declares `aff_embed_dims`, `aff_depths`, `aff_num_heads`, | |
| `aff_nbhd_sizes`, `aff_cluster_size`, `aff_ds_rates`, `aff_mlp_ratio` and | |
| `aff_merging_method`; any disagreement means the checkpoint does not load into | |
| the encoder. | |
| `img_size` is the exception β it may differ between stages. Cost is driven by the | |
| token count, which scales with `img_size / patch_size` rather than with pixel | |
| count, so moving from 512 to 1024 at patch size 8 doubles the grid on each axis. | |
| Leave `cluster_attention_backend` and `decoder_deform_backend` at `"auto"` unless | |
| you have profiled the alternative. `"auto"` selects the fused Triton kernels on | |
| CUDA and the PyTorch fallback elsewhere, so the same config runs on CPU, CUDA and | |
| Apple silicon; see *Hardware and Kernel Coverage* in the README. | |