Skip to content

Training

Basic usage

python -m birdnet_stm32 train \
  --data_path_train data/train \
  --checkpoint_path checkpoints/my_model.keras

The defaults are the 2.1 release recipe: raw frontend at 448 frames with the two-exposure filterbank (gain 16), pwl magnitude scaling, 24 kHz, 2.5 s chunks, width 1.5 with stage widths 48 / 128 / 512 / 1024 (the last stage is the 1024-d embedding) and 5 × 5 depthwise kernels, 50 epochs of one pass each under a per-class cap of 3,000 files, at learning rate 2e-4 after a half-epoch warm-up. With --teacher_cache the teacher's scores make up half of each target and choose the training chunk. On a GPU, float training runs XLA-compiled in mixed precision. The 2.1 release then runs the head fine-tune.

For a leakage-safe precomputed validation split and stable output order, pass a separate validation root and a one-label-per-line classes file:

python -m birdnet_stm32 train \
  --data_path_train data/train \
  --data_path_val data/validation \
  --classes_file data/labels.txt

The class file controls model-output order. Folders named noise, silence, background, or other are still loaded as all-zero examples and must not be listed as outputs. When --data_path_val is present, --val_split is ignored.

The script saves these files alongside the checkpoint:

  • my_model.keras — trained Keras model
  • my_model_model_config.json — conversion metadata (frontend, shapes, etc.)
  • my_model_labels.txt — ordered class names
  • my_model_history.csv — per-epoch training metrics (loss, ROC-AUC)
  • my_model_curves.png — loss and exact validation cMAP curves plot
  • my_model_train_state.json — epoch counter for --resume

Audio frontends

Frontend Input to model Description
raw (default) Peak-normalized waveform Model applies a mel-seeded, trainable Gabor quadrature filterbank. The release frontend: the whole pipeline runs on the NPU.
hybrid Linear magnitude STFT Model applies a learned mel mixer and magnitude scaling. The STFT runs outside the model (host, or the Cortex-M55). With --input_compression sqrt the best INT8 accuracy measured, at 148 ms per file against 75 ms for raw (1.4 models).
librosa Mel spectrogram Mel spectrogram computed outside the model. Smallest quantization loss, weakest float model.

hybrid and librosa inputs are defined by birdnet_stm32.audio.stft and specified in Spectrogram Input.

These three are the only frontends. mfcc and log_mel were removed in 1.2.0: both were host-precomputed variants of the same librosa path, and no release ever used them.

Raw frontend memory limit

The raw input must contain fewer than 65,536 samples. At 24 kHz, use a chunk no longer than about 2.7 seconds; 2.5 seconds is the tested setting.

Magnitude scaling

Mode Description Quantization friendliness
pwl (default) Piecewise-linear learned compression Excellent — recommended for deployment
none No compression Baseline only, for ablations

pcen and db were removed in 1.2.0. dB's log op produces exactly the wide dynamic range INT8 cannot hold — the failure this frontend exists to avoid — and PCEN was never used by a release. cpwl, a compressive variant of pwl, was removed in 1.3.0: best float of any raw model, worst INT8.

Model architecture

--backbone picks the network behind the frontend. The default from 2.3 is efficientnet_b0_n6w, N6-B0 with wider bottlenecks: expansion 4 in stages 2–4 and the residual stream doubled in stages 4–6 (12.1 M parameters, 1.18 G MACs, 62 ms per inference on the STM32N6570-DK). N6-B0 (efficientnet_b0_n6, 2.2) is EfficientNet-B0's inverted-residual blocks with the three changes that let it survive INT8 on the N6 — ReLU6 instead of SiLU, no squeeze-and-excite, and a bounded residual stream (ReLU6 after each projection and residual add). B0 as published lost 14% of its WABAD AP to post-training quantization. N6-B0 runs at 128 bands (--num_mels) into a 1,280-channel top convolution (--embeddings_size), 4.9 M parameters and 523 M MACs, 34 ms per inference on the STM32N6570-DK.

dscnn is the backbone of every release up to 2.1. It is scaled with two knobs:

  • --alpha (width multiplier): scales channel counts across all stages. Default 1.5 (2.0 release). Values like 0.75 or 1.0 produce smaller models.
  • --depth_multiplier: repeats each depthwise-separable block. Default 1. Increase to 2 for deeper models.

Two options change the backbone shape:

  • --dw_kernel_size: depthwise kernel size in stages 2–4, 5 (default) or 3. Stage 1 stays 3 × 3.
  • --stage_widths: base output channels of the four stages before --alpha, default 32 86 342 683 (2.1: 48 / 128 / 512 / 1024 at width 1.5). 2.0 used 32 64 128 256, which configs saved before the option existed are still read as. The 2.1 widths go where the 2.0 model used its width: the late stages, where the feature map is small and extra channels are cheap in activation memory. When the last stage is as wide as --embeddings_size, the separate 1 × 1 embedding convolution is left out.

No convolution kernel in the DS-CNN blocks carries weight decay. On the depthwise kernels it killed 54–62% of the depthwise channels during 2.0 training; on the pointwise kernels (1e-4) it killed 35–49% of them in a model widened in stages 3–4, which then trailed the narrower model. Without either, none died, and the widened model learned several times faster.

Channel alignment

Keep channel counts as multiples of 8 for optimal NPU vectorization. The model builder enforces this automatically via _make_divisible.

Training options

Data augmentation

  • Mixup: controlled by --mixup_alpha (default 0.2, 0 disables) and --mixup_probability (default 0.25). Uses Dirichlet multi-source mixing (2–3 sources per sample) to realistically emulate overlapping bird vocalizations. Labels are combined via element-wise max.
  • SpecAugment: enabled by default. Applies random frequency and time masking to spectrograms during training. Disable with --no_spec_augment. Control mask widths with --freq_mask_max (default 8 bins) and --time_mask_max (default 25 frames).
  • Teacher targets: --teacher_cache points at per-window scores from a larger model, computed once offline over every training recording, and --teacher_weight (default 0.5 with a cache, otherwise off) blends them into each chunk's label as (1 - w) * label + w * teacher on the classes the teacher covers. Training labels are weak: a recording carries one species label, so every chunk drawn from it trains as that species, whether it holds the call, silence, or a different bird. The teacher says which. Classes the teacher has no output for, and recordings missing from the cache, keep their hard label. Validation and checkpoint selection always use hard labels. The cache format is documented in birdnet_stm32/data/teacher.py.
  • Teacher embeddings: when the cache also holds the teacher's embedding of each window (emb.npy), training adds a cosine loss between the student's pooled embedding, mapped through a training-only linear projector, and that embedding. It is on by default at --teacher_embedding_weight 0.2 whenever the cache has embeddings; 0 turns it off. The embedding carries what the teacher hears beyond our classes, and a signal for the non-bird classes and noise recordings, which get no teacher scores. Mixed chunks and chunks without a teacher window are left out of it. The projector never reaches the checkpoint, so the deployed model is unchanged. A run that starts from trained weights (--init_checkpoint, --resume) fits the projector to them by ridge regression first. Trained from scratch at 448 frames, it raised the float window AUPRC on WABAD from 0.442 to 0.475 and the INT8 catalog cMAP from 0.712 to 0.728.
  • Smart crop: long recordings (> 2 chunks) are automatically cropped to salient regions using short-time energy (STE) analysis, reducing label noise from silent or irrelevant segments. Energy ranking selects the loudest part of a recording, which in field audio is as often rain, wind or an insect chorus as the target bird. --crop_policy teacher (the default with --teacher_cache) ranks half-overlapping candidates by the teacher's score for the recording's labelled species and keeps the best, so the chunk is chosen for holding the species rather than for being loud. It falls back to energy for noise recordings, classes the teacher has no output for, and recordings missing from the cache, and works with --teacher_weight 0 to change only which chunk is drawn. Evaluation is unaffected by every policy: it always scores whole files with overlapping windows.
  • Multi-chunk I/O reuse: long files (e.g. 60 s recordings) yield up to --max_chunks_per_file (default 1) salient chunks per file open, stored in a memory-bounded shuffled reservoir. This avoids redundant FLAC decode + resample for the same file across epochs.

Loss function

The classifier head is always sigmoid + binary crossentropy. Soundscape recordings are inherently multi-label, so we always optimise per-class probabilities even when the source label is single-class.

Optimizer

Select with --optimizer (default adam):

Optimizer Description
adam Adaptive moment estimation (default)
sgd SGD with momentum 0.9
adamw AdamW with decoupled weight decay

Set weight decay with --weight_decay (default 0, only used by adamw).

Deterministic mode

Training is always deterministic — all random seeds (Python, NumPy, TensorFlow) are set and TF_DETERMINISTIC_OPS is enabled automatically. Use --seed (default 42) to change the RNG seed.

Gradient clipping

Gradient clipping by global norm is enabled by default (--grad_clip 1.0). Set to 0 to disable. Prevents exploding gradients, especially useful with large models or unstable training.

Mixed precision and XLA

On a GPU, float training runs in mixed precision (FP16 compute, FP32 accumulation) with the training step XLA-compiled: the raw 448 model trains about 3x faster, since the depthwise backbone is most of the step and XLA fuses it far better than graph execution does. QAT and linear probing stay in float32 without XLA, and so does training on a CPU. Checkpoints are loaded in float32 for conversion and evaluation.

Resumable training

Use --resume to continue training from a previously saved checkpoint. The optimizer state is recompiled and training resumes from the last saved epoch. Example:

# Initial training (interrupted or completed at epoch 30)
python -m birdnet_stm32 train --epochs 30 --checkpoint_path ckpt/model.keras ...

# Resume and extend to 50 epochs
python -m birdnet_stm32 train --epochs 50 --resume --checkpoint_path ckpt/model.keras ...

Quantization-Aware Training (QAT)

Use --qat to fine-tune a pretrained model with simulated INT8 quantization noise, teaching the weights to survive quantization.

Part of the recipe again from 2.2

Up to 2.1 QAT never beat its starting point (2.0 Raw: INT8 cMAP 0.545 at epoch 0, 0.520–0.541 after each of 8 epochs). It had two bugs: it drew from the raw, unbalanced training list instead of the class-capped draw with field clips the float run uses, and it simulated and selected a probability output on the INT8 grid, while the releases ship logits. With both fixed, two epochs at learning rate 1e-5 took 2.2's N6-B0 from INT8 validation cMAP 0.7665 to 0.7745 (float 0.781) and from 5.2% to 4.5% INT8 loss on WABAD. 2.3 runs four (two of warm-up, then two of decay, which gave the best epochs): INT8 validation cMAP 0.8081 to 0.8109.

QAT requires a pretrained model

Always train normally first, then fine-tune with --qat. Do not use --qat from scratch — the quantization noise destabilizes randomly initialized weights and the model will not converge. The dataset must have the same classes as the pretrained model; use --linear_probe to adapt to a different class set first.

QAT calibrates activation ranges on the converter's exact deterministic, class-stratified training manifest (1,024 samples by default), then trains with per-channel INT8 kernel grids and per-tensor INT8 activation grids. The simulation includes the quantized waveform input and the kernels and elementwise boundaries inside the custom raw frontend. BatchNorm layers are frozen. Standard variables are shared with a clean deployment graph; cloned frontend variables are synchronized before each checkpoint. Only the clean graph is saved, so no FakeQuant ops remain in the model.

A frozen copy of the untouched checkpoint acts as the teacher. QAT minimizes the normal label loss, per-output Bernoulli KL divergence, and both mean and worst-sample per-sample cosine distance from that teacher. The defaults apply the tail loss to the worst 10% of each batch with 0.75 weight; both values are configurable. This constrains background and low-confidence probabilities while directly optimizing the lower tail that the release parity gate measures.

With --qat, the defaults switch to the fine-tuning schedule of 2.3: 4 epochs at learning rate 1e-5, two of warm-up and two of cosine decay (with --steps_per_epoch 2850), with activation ranges recalibrated on the current weights after every epoch (--qat_range_refresh, on by default). QAT draws its training audio as the float run does (--class_cap_per_epoch, --field_table), uses the converter's activation-range percentiles and simulates the logit output the releases ship. The simulation quantizes every tensor the converter quantizes, including each partial filterbank convolution, so its validation cMAP (val_sim_int8_cmap) tracks the converted model's (val_int8_cmap) to within a few thousandths; selection converts each epoch as the release does (logit output, two-exposure PWL) and keeps every epoch's INT8 candidate.

The full raw pipeline (the 2.2 recipe):

# Step 1: Normal training (with a teacher cache, as the releases are trained)
python -m birdnet_stm32 train --data_path_train data/train \
  --data_path_val data/validation --classes_file data/labels.txt \
  --teacher_cache data/teacher_cache --checkpoint_path checkpoints/model.keras

# Step 2: Refit the classifier head on every training file (see below)
python -m birdnet_stm32 finetune-head --checkpoint_path checkpoints/model.keras \
  --data_path_train data/train --data_path_val data/validation \
  --teacher_cache data/teacher_cache --output_path checkpoints/model_ft.keras

# Step 3: Equalize the frontend's per-band ranges (raw only; exact in float)
python -m birdnet_stm32 equalize --checkpoint_path checkpoints/model_ft.keras \
  --data_path_train data/train --output_path checkpoints/model_eq.keras

# Step 4: Four epochs of quantization-aware training on the equalized checkpoint
python -m birdnet_stm32 train --qat --checkpoint_path checkpoints/qat/model.keras \
  --data_path_train data/train --data_path_val data/validation --classes_file data/labels.txt \
  --teacher_cache data/teacher_cache --field_table data/field_table.csv \
  --steps_per_epoch 2850
# (checkpoints/qat/model.keras is a copy of model_eq.keras; QAT writes model_qat.keras)

# Step 5: Convert
python -m birdnet_stm32 convert \
  --checkpoint_path checkpoints/qat/model_qat.keras \
  --model_config checkpoints/qat/model_qat_model_config.json \
  --data_path_train data/train

A QAT run is saved as {name}_qat.keras alongside the original. equalize rescales each band of the raw filterbank and PWL so that every band gets the same share of the tensors' INT8 grids, and refuses to save if the float output changes. See INT8 quality for the measurements.

For hybrid and librosa, skip step 3 (QAT on hybrid models scored below plain post-training quantization at every epoch up to 2.0).

Head fine-tuning

finetune-head is a post-training step that refits only the classifier (pred) on the frozen backbone, over every training file. It embeds each training file once: one chunk, cut and targeted as the training loader cuts it (crop policy, teacher targets, label sidecar). The embeddings go to a disk cache (--cache_dir; 2 kB per file for the embedding plus 2 bytes per output, so about 12 GB for 3 million files and 1,013 outputs; resumable, and reused when the files and settings match). The head then trains from its trained weights: each epoch draws the cached files class-capped as training does (--class_cap_per_epoch, with the field table's share). The epoch with the best validation class-mean AP plus micro AP is kept (--epochs 30, --patience 5, --learning_rate 3e-4). Only pred changes; the config is copied unchanged.

Optionally, --cooccurrence_csv makes the head weigh its negatives by geography. The CSV lists label,group rows (for example each species' regions); a negative from a species that shares no group with an output counts --far_weight (0.3). Labels not in the CSV, such as non-bird outputs, co-occur with everything. With a species range filter at inference, the head then spends its capacity on the confusions that remain; without one, it costs nothing.

Measured on 2.1 Raw (1,013 outputs, INT8, all 61 WABAD sites, no species mask), micro window AP:

  • +3.3% for the plain refit;
  • +3.9% with co-occurrence weighting.

The released 2.1 Raw INT8 model, built this way, reaches 0.356 against 0.342 for the same checkpoint without the refit (+4.1%).

With each region's species mask, the gain over the unrefitted head is +10% in North America and Europe and +16% to +19% in the Neotropics. Starting from the trained rows matters: heads fitted from scratch, or on a few hundred files per class, lose to the trained head.

Two-exposure raw frontend

The raw frontend carries linear amplitude through its INT8 grids, so quiet and dense field audio lands in their bottom few levels. The raw model therefore runs a second exposure of its filterbank: the same filters at gain 16 (--raw_exposure_gain, default 16; 1 turns it off), clamped to ±1 at the convolution's own requantization, and mixed with the first per band into a knee compressor before the band stage. Every grid after the mix then carries a compressed envelope, much as hybrid compresses its spectrogram before its first INT8 tensor, and all of it stays on the NPU. On the 1.6 raw model it cut the field INT8 loss by more than half (WABAD INT8 recall 0.255 → 0.309).

From 2.0 the model trains with both exposures from the start; 1.7 to 1.9 trained on the linear envelope first and switched afterwards. equalize leaves the two-exposure filterbank alone (it is already normalized per band).

Linear probing

Use --linear_probe to freeze a pretrained backbone and train only a new classification head on your custom species dataset. This is useful when you have a pretrained model (e.g. a large BirdNET checkpoint) and want to adapt it to a different set of species with limited data.

python -m birdnet_stm32 train --data_path_train data/my_species \
  --linear_probe --checkpoint_path checkpoints/pretrained.keras \
  --data_path_val data/my_species_val \
  --classes_file data/my_species/labels.txt \
  --epochs 20 --learning_rate 0.001

The probe model is saved as {name}_probe.keras with a new labels file and {name}_probe_model_config.json.

Pass --classes_file whenever the head will be shipped. The head's output order is its labels file, and without an explicit schema that order comes from a directory listing, which is not a contract. --data_path_val gives the probe a fixed validation root instead of a random slice of the training set, so repeated runs are comparable.

Probing a checkpoint from an earlier compression step needs --model_config: a QAT checkpoint writes no config of its own, since its architecture is the base model's.

python -m birdnet_stm32 train --data_path_train data/my_species \
  --linear_probe --checkpoint_path checkpoints/pretrained_qat.keras \
  --model_config checkpoints/pretrained_model_config.json \
  --classes_file data/my_species/labels.txt --epochs 20

To ship the resulting head on its own, convert it against the backbone already on the device — see Updating the head against a flashed backbone.

Learning rate

A linear warm-up of --warmup_epochs (default 0.5) reaches --learning_rate (default 2e-4), followed by cosine decay to near-zero over --epochs (default 50). Best-checkpoint selection and early stopping maximize exact validation cMAP. Early stopping (patience 10) only starts after 80% of --epochs, once the learning rate has come down: in the middle of a long schedule validation cMAP plateaus noisily, and stopping there ends the run before most of its gain. Standard CLI training evaluates files with the configured overlap and pooling. QAT selects on converted INT8 file cMAP, including epoch zero, and saves the exact TFLite artifact that was scored alongside its matching Keras checkpoint. The chunk PR-AUC metric is logged as pr_auc and does not select checkpoints.

Full argument reference

Argument Default Description
--data_path_train (required) Path to training data
--data_path_val None Separate validation root; disables random validation splitting
--classes_file None Ordered one-label-per-line output schema
--max_classes None Use only the N most populated classes
--max_samples None Max files per class
--sample_rate 24000 Audio sample rate (Hz)
--num_mels 128 Number of mel frequency bins (up to 2.1: 64)
--spec_width per frontend Frames per chunk: 448 raw, 384 hybrid, 256 librosa
--fft_length 512 FFT window length
--chunk_duration 2.5 Chunk duration (seconds)
--max_duration 60 Max seconds to load per file
--audio_frontend raw raw, hybrid, or librosa — raw models trained before the NPU fixes must be retrained
--mag_scale pwl pwl or none
--input_compression none none, sqrt or log: compress a librosa/hybrid spectrogram before its first INT8 quantization; the firmware applies the same compression
--embeddings_size 1280 Embedding channels before head (N6-B0's top conv; up to 2.1: 1024)
--alpha 1.5 Model width scaling
--depth_multiplier 1 Block repeats per stage
--dw_kernel_size 5 Depthwise kernel in stages 2–4: 5 or 3
--stage_widths 32 86 342 683 Base channels of the four stages, before --alpha (2.0: 32 64 128 256)
--backbone efficientnet_b0_n6w efficientnet_b0_n6w (2.3: N6-B0 with wider bottlenecks, expansion 4 in stages 2–4 and the residual stream ×2 in stages 4–6), efficientnet_b0_n6 (2.2, N6-B0: EfficientNet-B0 with ReLU6, no squeeze-and-excite and a bounded residual stream) or dscnn (releases up to 2.1); --alpha, --depth_multiplier, --dw_kernel_size and --stage_widths apply to dscnn only
--raw_exposure_gain 16 (raw) Gain of the raw filterbank's second, clamped exposure, mixed per band into a knee compressor; 1 = one exposure (raw only)
--mixup_alpha 0.2 Mixup alpha (0 disables)
--mixup_probability 0.25 Fraction of batch to mix
--no_spec_augment False Disable SpecAugment masking (on by default)
--freq_mask_max 8 Max frequency mask width (bins)
--time_mask_max 25 Max time mask width (frames)
--teacher_cache None Directory of cached per-window teacher scores, and optionally embeddings
--teacher_weight 0.5 with --teacher_cache, else 0 Teacher share of the training target in [0, 1] (0 = hard labels only). Background birds: a species the file does not name takes the teacher's own score from 0.3 and 1 from 0.8 instead of the blend (always on with teacher targets from 2.3, as in finetune-head)
--label_sidecar None CSV of per-file label additions (sample_id, positives, negatives; classes separated by ;): positives join the folder's class in the hard label (recordings with several species), negatives are confirmed absent, held at 0 and never raised by the teacher's soft target
--teacher_embedding_weight 0.2 with embeddings Weight of a cosine loss to the teacher's embedding of each chunk; on when --teacher_cache holds emb.npy, 0 disables
--crop_policy teacher with --teacher_cache, else energy How training chunks are chosen: energy or teacher (needs --teacher_cache)
--dropout 0.5 Dropout rate before classifier head
--optimizer adam adam, sgd, or adamw
--weight_decay 0.0 Weight decay (adamw only)
--grad_clip 1.0 Max gradient norm for clipping (0 = disabled)
--field_table None CSV sample_id,site,recording of field clips; with --class_cap_per_epoch they are drawn for diversity: round-robin over sites and recordings, at most --field_share of a class's cap
--field_share 0.5 Max share of a class's per-epoch cap filled by field clips (more only when focal files cannot fill the rest)
--field_per_recording 2 Max segments of one field recording per class and epoch
--resume False Resume training from checkpoint
--init_checkpoint — Start from this model's weights at epoch 0; its frontend must match the architecture flags. Checkpoint selection and early stopping start after the learning-rate warm-up
--seed 42 Random seed
--batch_size 32 Batch size
--num_workers 8 Parallel data loading workers (0 = sequential)
--max_chunks_per_file 1 Max salient chunks per file open (reduces redundant I/O)
--prefetch_batches 2 Loader prefetch depth in batches
--epochs 50 (4 with --qat) Number of epochs
--steps_per_epoch 0 Training steps per epoch; 0 = one pass over the training files. Fix it to keep a schedule unchanged when the dataset grows
--warmup_epochs 0.5 Linear learning-rate warm-up before the cosine decay, in epochs; may be fractional
--class_cap_per_epoch 3000 At most this many files per class in each epoch (0 = off): a larger class contributes a different subset each epoch, rotating through all its files, so the draw is more balanced without repeating or discarding files. Noise folders are not capped
--learning_rate 2e-4 (1e-5 with --qat, 1e-3 with --linear_probe) Initial learning rate
--val_split 0.2 Validation split fraction when --data_path_val is not supplied
--checkpoint_path checkpoints/best_model.keras Output path (.keras)
--qat False Quantization-aware fine-tuning
--qat_calibration_samples 1024 Exact stratified samples used for QAT and conversion calibration
--qat_distillation_weight 1.0 Frozen-teacher Bernoulli-KL weight
--qat_cosine_weight 0.10 Mean teacher/student cosine-loss weight
--qat_cosine_tail_weight 0.75 Worst-sample cosine-loss weight
--qat_cosine_tail_fraction 0.10 Fraction of each batch included in the worst-sample loss
--qat_range_refresh on Recalibrate activation ranges on the current weights after every epoch, so QAT trains against the grid conversion will use; --no-qat_range_refresh disables it
--linear_probe False Freeze backbone and train only classifier head
--model_config (inferred) Architecture config for --qat, --linear_probe; required when the checkpoint has no sibling config
--validation_overlap half the chunk duration File validation overlap in seconds
--validation_pooling max File validation pooling
--validation_subset 0 Score checkpoint selection (training and QAT) on a fixed class-balanced draw of N validation files, seed 1234 (0 = all). Biased upward; not comparable to full-manifest cMAP

Data pipeline

The training pipeline uses a multiprocessing pool for parallel data loading, bypassing the GIL so FLAC decode, resampling, smart-crop, and spectrogram computation run across separate CPU cores.

When --max_chunks_per_file is greater than 1 (default 3), each file open extracts multiple salient chunks which are buffered in a shuffled in-memory reservoir sized from the sample representation and the loader's memory budget. This dramatically reduces I/O for long recordings: a 60 s file decoded once yields 3 usable chunks instead of re-opening the same file 3 times across epochs.

The reservoir maintains batch diversity by shuffling samples from many different files before yielding them.

Training also checks host-available memory every 25 batches. It aborts before available RAM falls below an adaptive reserve (20% of host RAM, bounded between 2 and 12 GiB), leaving the last completed-epoch checkpoint available for a safer restart. This protects the host from multiprocessing and TensorFlow memory spikes; it is not a substitute for choosing a conservative worker count and batch size.

Tune with:

  • --num_workers N — number of worker processes (default 8, 0 = sequential)
  • --max_chunks_per_file N — chunks per file open (default 3, 1 = original behavior)
  • --prefetch_batches N — queued batches (default 2; higher uses more RAM)

Noise classes

Place audio in folders named noise, silence, background, or other under data/train/. These receive all-zero label vectors and help the model learn to reject non-bird sounds.