This tutorial walks you through the complete active learning cycle — from curated detections to an improved YOLO model fine-tuned for your specific field site. You'll learn not just how to run each tool, but when and why each step matters.
Prerequisites: You should be comfortable running batch detection and using Review mode to confirm/reject events. See First Analysis Tutorial for basics. For a complete CLI reference of every script mentioned here, see the Advanced Features Reference.
Your model was trained on a specific dataset. Your field site has different noise profiles, different recording equipment, different background species. Active learning adapts the model to YOUR data.
The cycle looks like this:
flowchart LR
A["🎙️ Raw\nRecordings"] --> B["🔍 Batch\nDetection"]
B --> C["👤 Human\nCuration"]
C --> D["📦 Dataset\nExtraction"]
D --> E["🧠 Model\nFine-tuning"]
E --> F["✅ Evaluation"]
F -->|"Re-run with\nimproved model"| B
style A fill:#4a9eff,color:#fff
style B fill:#6c5ce7,color:#fff
style C fill:#e17055,color:#fff
style D fill:#00b894,color:#fff
style E fill:#fdcb6e,color:#333
style F fill:#0984e3,color:#fff
Each loop through this cycle teaches the model about your site's specific challenges — the local noise floor, co-occurring species, equipment artifacts, and the particular acoustic variants of Hume's Leaf Warbler calls present in your recordings.
Important
You don't need to do the full cycle in one sitting. The database (batch.db) persists all your curation decisions, so you can curate events over several days and extract the dataset when you're ready.
Before investing time in the cycle, ask yourself whether it will actually help. Use this decision framework:
| Situation | Should You Fine-Tune? | Why |
|---|---|---|
| False positive rate > 20% | ✅ Yes | The model is triggering on local noise/species it wasn't trained against |
| Model misses calls you can clearly see in spectrograms | ✅ Yes | Your site's calls may differ from the training distribution |
| Deploying to a new geographic region | ✅ Essential | Different subspecies, equipment, and noise profiles |
| Model works great as-is (>90% precision and recall) | ❌ Don't bother | You'll just overfit to a small sample and may degrade performance |
Tip
A quick way to estimate your false positive rate: run batch detection on a representative recording, then review all events. If you're rejecting more than 1 in 5, active learning will help.
The quality of your fine-tuning depends entirely on the quality of your curation. Garbage in, garbage out.
As a rule of thumb, aim for:
- 100+ confirmed events (
retained = 1) — real buzz calls you're confident about - 50+ rejected events (
retained = 0) — false positives the model got wrong
More is always better, but diminishing returns kick in around 300 total events for a single-site deployment.
Use clear, unambiguous cases:
- Calls that are clearly visible in the spectrogram
- Rejections that are obviously not calls (noise, other species, equipment artifacts)
Avoid borderline cases:
- Faint calls that even an expert would debate
- Overlapping calls that are hard to separate
- Partially clipped events at file boundaries
Warning
Including ambiguous cases in your training set teaches the model to be uncertain. That uncertainty compounds — the next iteration will produce more borderline detections, not fewer.
- Run batch detection on your target recordings
- Open the results in Review mode
- Work through events systematically — don't cherry-pick
- For each event, ask: "Am I confident this is / is not a real call?"
- If yes → confirm or reject it
- If unsure → skip it (leave
retainedasNULL)
Only confirmed (retained = 1) and rejected (retained = 0) events are used for dataset extraction. Skipped events are ignored, so it's safe to leave uncertain cases unreviewed.
Per-Channel Energy Normalization (PCEN) is a modern alternative to standard dB spectrograms that handles noisy field conditions better. Before building your dataset, it's worth checking whether PCEN would improve things.
- Noisy sites — streams, roads, constant insect chorus
- Variable background — recordings spanning dawn to midday
- Rain or wind — broadband noise that masks calls
- Constant-frequency interferers — electronic hums, cicadas
Pick a representative audio file and generate a side-by-side comparison:
uv run python scripts/pcen_preprocessor.py \
--input data/example.WAV \
--offset 10.0 \
--duration 10.0This saves a comparison plot to output/pcen_comparison.png.
Open the comparison image and compare the two spectrograms:
- Top panel: Standard dB spectrogram
- Bottom panel: PCEN spectrogram (with bioacoustic-tuned parameters)
Tip
If the PCEN spectrogram shows clearer separation between buzz calls and the background noise floor, consider using PCEN-processed spectrograms for your fine-tuning dataset. If both look similar, standard dB is fine — PCEN adds complexity without benefit when noise levels are already low.
The script uses parameters optimized for transient bird calls: gain=0.98, bias=2.0, power=0.5, b=0.035. You can adjust these via CLI flags — see the Advanced Features Reference for all options.
This is where your curation decisions become training data. The active_learning.py script reads your database, extracts spectrogram images and YOLO-format labels, and produces a ready-to-train dataset.
The script queries batch.db and selects two categories of events:
| Category | Database Criteria | What It Teaches the Model |
|---|---|---|
| Positive samples | retained = 1 |
What real buzz calls look like |
| Negative samples | retained = 0 AND stage_a_conf >= threshold |
What false alarms look like — these are the most valuable training examples because they're the cases where the model was most confident but most wrong |
For each selected event, the script:
- Centers a 2.75-second window around the event (matching the YOLO model's input format)
- Extracts the audio clip and computes the cropped dB spectrogram
- For positives: generates YOLO bounding-box annotations (normalized
class x_center y_center width height) - For negatives: writes an empty label file — this teaches the model "this looks like a detection, but it isn't"
uv run python scripts/active_learning.py \
--db data/batch.db \
--dataset-dir output/dataset_active_learning \
--min-stage-a-conf 0.5The --min-stage-a-conf parameter controls which rejected events become negative training samples. Setting it to 0.5 means: "Only include false positives where the model was ≥50% confident." These high-confidence false positives are the cases where the model was most wrong — and therefore the most informative corrections.
Tip
If you have very few rejected events, lower --min-stage-a-conf to 0.3 to include more negative examples. If you have thousands, raise it to 0.7 to focus on the worst offenders.
output/dataset_active_learning/
├── dataset.yaml ← YOLO dataset config (points to images/ and labels/)
├── audio/
│ ├── event_00001.wav ← Raw audio clips (for reference/debugging)
│ └── ...
├── images/
│ ├── event_00001.png ← Spectrogram images (model input)
│ └── ...
└── labels/
├── event_00001.txt ← YOLO annotation (bounding boxes or empty)
└── ...
Before training, always spot-check your dataset:
- Open a few images from
images/— do they look like reasonable spectrograms? - For confirmed events, open the corresponding
.txtfile inlabels/— it should contain one or more lines like0 0.523 0.450 0.082 0.310 - For rejected events, the
.txtfile should be empty (zero bytes) - Check
dataset.yaml— it should reference the correct absolute paths
Warning
If label files contain bounding boxes for events you rejected (or are empty for events you confirmed), something went wrong with the database query. Check that your batch.db reflects your most recent curation session.
If you've run multiple batch sessions, you can extract data from just one:
uv run python scripts/active_learning.py \
--db data/batch.db \
--session-id 3 \
--dataset-dir output/dataset_session_3Before training, you can use Query-by-Example (QBE) to discover potential false negatives — real calls the model missed entirely. These are invisible to the active learning extraction (which only works with detected events), but QBE can surface them.
You pick a confirmed buzz call that you know is real. QBE then searches all other events in the session for ones with similar acoustic features — similar spectrogram shape, similar spectral timbre, similar duration. If it finds unreviewed events with high similarity, those are likely real calls the model missed.
Find the database ID of a good confirmed event (shown in Review mode), then:
uv run python scripts/query_by_example.py \
--query-id 42 \
--k 10This searches the same session and returns the 10 most similar events, with output like:
Top-10 matches for Query Event 42:
Rank | Event ID | Sim | Retained | File | Time Range | Freq Range
-----------------------------------------------------------------------------------------------------
1 | 87 | 0.9512 | 1 | 20250611_080000.WAV | 34.20-34.45s | 4200-7800Hz
2 | 156 | 0.9103 | None | 20250611_083000.WAV | 12.10-12.38s | 4100-7600Hz
3 | 203 | 0.8744 | 0 | 20250611_090000.WAV | 45.60-45.82s | 4300-7900Hz
...
| Score Range | Interpretation |
|---|---|
| > 0.90 | Very similar — almost certainly the same type of event |
| 0.70 – 0.90 | Somewhat similar — worth reviewing |
| < 0.70 | Probably different — likely a different call type or noise |
The --feature-type flag controls what acoustic properties QBE compares:
| Feature Type | Best For |
|---|---|
combined (default) |
General use — balances shape and timbre. Start here. |
spectrogram |
Calls with distinctive visual shapes (e.g., frequency sweeps, harmonics) |
mfcc |
Calls with distinctive timbral qualities, even if their spectrogram shapes vary |
- Pick 3–5 of your best confirmed events — clear, strong, unambiguous calls
- Run QBE on each with
--k 10or--k 20 - Look at results with
Retained = None— these are unreviewed events - If they have high similarity (>0.85), go review them — they're likely real calls the model missed
- If you confirm new events, re-run the dataset extraction (Step 3) to include them
Tip
QBE caches computed feature embeddings in output/ as .npz files. The first run on a session may take a minute; subsequent searches are near-instant. Use --recache if you've added new events to the session.
With your dataset extracted and verified, you're ready to fine-tune the YOLO localizer.
Caution
Back up your original model before overwriting it. Copy models/buzz_localizer.pt to models/buzz_localizer_original.pt (or similar). If fine-tuning goes wrong, you'll need the original to roll back.
cp models/buzz_localizer.pt models/buzz_localizer_backup_$(date +%Y%m%d).ptNever train on everything. You need a held-out validation set to detect overfitting.
A simple approach: split your dataset 80/20 before training. You can do this by manually moving ~20% of the image/label pairs into a separate val/ directory, or by modifying dataset.yaml to point train: and val: at separate directories.
Using the Ultralytics YOLO CLI:
yolo detect train \
data=output/dataset_active_learning/dataset.yaml \
model=models/buzz_localizer.pt \
epochs=50 \
imgsz=288 \
batch=16 \
lr0=0.001 \
freeze=10Key parameters:
| Parameter | Value | Rationale |
|---|---|---|
model |
Your current model | Start from existing weights, don't train from scratch |
epochs |
50 | Enough for convergence on a small dataset; monitor val loss |
imgsz |
288 | Must match the model's expected input size |
lr0 |
0.001 | Lower learning rate for fine-tuning (default 0.01 is too aggressive) |
freeze |
10 | Freeze early layers to preserve general features; only adapt later layers |
Important
Watch the validation loss during training. If it starts rising while training loss keeps falling, you're overfitting. Stop early and use the best checkpoint.
The fine-tuned model will be saved under runs/detect/train/weights/best.pt. Copy it to your models directory:
cp runs/detect/train/weights/best.pt models/buzz_localizer.ptA new model is only better if the numbers prove it. Don't skip evaluation.
Run the same batch on the same recordings using the new model:
- Select the same input files/directory you originally processed
- Run batch detection with default settings
- Note the summary statistics: total events detected, confidence distribution
Look for these signals:
| Metric | Good Sign | Bad Sign |
|---|---|---|
| Events detected | Roughly the same or slightly more | Dramatically more (false positives increased) |
| Average confidence | Higher for real calls | Lower overall (model became uncertain) |
| False positive rate | Lower — fewer rejections needed in Review | Higher — more noise getting through |
| Missed calls | Fewer — events you previously missed now appear | More — model forgot what it knew |
Open the new results in Review mode and check:
- Do the new detections (ones the old model missed) look like real calls?
- Are the old false positives gone, or are they still triggering?
- Did any previously detected real calls disappear?
| Problem | Likely Cause | Fix |
|---|---|---|
| More false positives than before | Too few negative examples in training | Go back to Step 3 — curate more rejections, lower --min-stage-a-conf |
| Model "forgot" previously good detections | Overtrained on a small dataset | Use more epochs with a lower lr0, or increase freeze to preserve more layers |
| Performance is identical | Dataset too small or too similar to original training data | Curate more diverse examples from different recordings/conditions |
If you fine-tune only on recordings from Site A, the model may degrade on Site B. Whenever possible, include data from multiple sites and conditions in your fine-tuning set.
A model trained mostly on positives learns to detect everything. Aim for at least a 2:1 ratio of positives to negatives — but 1:1 is even better. The high-confidence false positives (where stage_a_conf was high but retained = 0) are your most informative training examples.
If you wouldn't bet money that an event is a real call (or isn't), skip it. Training on uncertain labels injects noise into the model's objective function. It's better to have 100 clean examples than 300 noisy ones.
Fine-tuning overwrites the model's weights. If the new model is worse, you need the original to recover. Always copy the model before training.
Training loss going down doesn't mean the model is getting better — it may be memorizing your training set. Always hold out a validation split and monitor validation metrics.
Active learning is iterative by design. The first fine-tuning rarely achieves optimal results. Each cycle surfaces new failure modes that the next round of curation can address. Plan for 2–3 cycles minimum.
# 1. PCEN comparison (optional — check if PCEN helps your recordings)
uv run python scripts/pcen_preprocessor.py \
--input data/example.WAV --offset 10.0 --duration 10.0
# 2. Extract fine-tuning dataset from curated results
uv run python scripts/active_learning.py \
--db data/batch.db \
--dataset-dir output/dataset_active_learning \
--min-stage-a-conf 0.5
# 3. Query-by-example to find missed calls
uv run python scripts/query_by_example.py \
--query-id 42 --k 10
# 4. Back up the current model
cp models/buzz_localizer.pt models/buzz_localizer_backup_$(date +%Y%m%d).pt
# 5. Fine-tune YOLO
yolo detect train \
data=output/dataset_active_learning/dataset.yaml \
model=models/buzz_localizer.pt \
epochs=50 imgsz=288 batch=16 lr0=0.001 freeze=10
# 6. Deploy the fine-tuned model
cp runs/detect/train/weights/best.pt models/buzz_localizer.pt- Advanced Features Reference — Complete CLI arguments for all scripts
- First Analysis Tutorial — Batch detection and Review mode basics
- Ultralytics YOLO Training Docs — Full YOLO training configuration
- PCEN Paper (Wang et al. 2017) — The theory behind Per-Channel Energy Normalization