Skip to content

Allow choosing the early-stopping / best.pt metric (e.g. F1) for detection and classification training #852

Description

@velu-97

Goal

Let the user pick which validation metric drives early stopping and best.pt selection (for example F1 on an imbalanced detection dataset) instead of the trainer's fixed choice plus patience.

Current behavior (dev @ 5d1d7f6)

  • Early stopping and best.pt share one comparison in BaseTrainer (libreyolo/training/trainer.py:1968-1977, 2307-2323, 3812-3820). The only user-facing knob is patience (libreyolo/training/config.py:268, CLI --patience).
  • Detection (YOLO9, RF-DETR): the monitored key is the class attribute best_metric_key = "metrics/mAP50-95" (trainer.py:155). The detect validation branch reads that attribute (trainer.py:2929-2934), but nothing user-facing can set it.
  • Classification: _run_classify_validation hard-codes top-1 (trainer.py:3007-3016) and ignores best_metric_key. The semantic, depth and restore branches hard-code mIoU, delta1 and PSNR the same way.
  • The detection validator's metrics/precision and metrics/recall are deprecated aliases of mAP50-95 and AR100 (libreyolo/validation/coco_evaluator.py:205-225), not a precision/recall pair. The only real F1 is metrics/best_conf_f1, the maximum F1 over the confidence sweep at IoU 0.50 (detection_validator.py:1105-1145), which is already computed every epoch and can be NaN.
  • The classification validator keeps only running top-1/top-5 counts (libreyolo/validation/classify_validator.py:153-204). No precision, recall or F1.
  • Passing an unknown kwarg such as best_metric= is warned about and silently dropped by TrainConfig.from_kwargs (config.py:316-326).

Proposal

  • New train kwarg best_metric on Python and CLI (both key=value and --key value), sitting next to patience. Default None keeps each trainer's current key, so existing runs are unchanged.
  • Short aliases only, resolved per task. Detection: map50-95 (default), map50, map75, f1. Classification: top1 (default), top5, f1, precision, recall. Other tasks accept only their current default name.
  • Detection f1 maps to the existing metrics/best_conf_f1. NaN counts as no improvement.
  • Classification: accumulate a confusion matrix in the validator and emit macro metrics/precision, metrics/recall, metrics/f1.
  • Every validation branch (detect, classify, semantic, depth, restore) resolves best_metric through the configured key instead of hard-coding it.
  • An unknown alias, or one that is not valid for the model's task, raises at trainer construction with the valid list. No accept-and-ignore.
  • best_metric_key keeps going into checkpoint metadata as today, so resume keeps refusing to inherit a best value across a key change.
  • Coverage: YOLO9 and RF-DETR plus the classify families. Unit tests for the config field, alias resolution, both CLI grammars, the trainer's best-epoch selection, and the new classify keys.
  • Out of scope: mode=min and min_delta (loss-driven stopping). All selectable metrics are higher-is-better.

Environment

Code-level finding on dev at 5d1d7f6 (1.5.0.dev0). No runtime needed to reproduce; the paths above are the evidence.

Activity

  1. velu-97 commented on Sep 12, 2026

    @velu-97
    ContributorAuthor

    Already made a pull request following the instructions present in the repo. PR here: #853
    Also addressed greptile comment which has since been auto resolved.

    Unsure how to assign myself to the Kanban entry though, can't see any options to pick up the open ticket linked to this issue.

    Side note: Thanks for the awesome repo, appreciate the effort 👍

  2. EHxuban11 commented on Sep 12, 2026

    @EHxuban11
    Contributor

    Hello @velu-97 ,

    Thank you for the issue and the PR.

    I agree custom early stopping is useful, but I don't think it's the best approach, for two reasons:

    1. The flagship use case doesn't hold. mAP is already macro-averaged over classes, so it isn't skewed by imbalance. The only F1 the library has is micro-averaged at IoU 0.50, which handles imbalance worse than the default.
    2. A per-task alias table is a lot of permanent surface. It has to be maintained for every task and metric, and it's inert for three of the five tasks.

    Where you're right: the classification validator only has top-1/top-5, and that's a real gap. If you want to open a separate PR adding macro precision, recall and F1 from a confusion matrix in the classify validator, I'd be happy to review and merge that.

    For custom stopping and best.pt selection, I have something else in mind...

    Thanks again and very open to the separate PR adding macro precision, recall and F1 from a confusion matrix in the classify validator

  3. velu-97 commented on Sep 19, 2026

    @velu-97
    ContributorAuthor

    Hi @EHxuban11 !

    For custom stopping and best.pt selection, I have something else in mind...

    Noted 👍

    PR for classifier changes here

  4. EHxuban11 commented on Sep 24, 2026

    @EHxuban11
    Contributor

    The callback alternative is implemented and available in draft PR #900: #900

    • Branch: feat/custom-fitness, latest commit 7b0f0295. Another agent can clone/fetch from GitHub and continue directly; no local transcript is needed.
    • Optional fitness(metrics) callback controls best.pt, patience and checkpoint averaging. See the PR description and docs/training_loggers.md for usage and the contract.
    • The PR includes the verification summary, baseline-reproduced failures, remaining limitations, prior review fixes and continuation commands. Implementation session label: implement_fitness_clean (Codex).
    • Status: draft, not approved or merge-ready. GitHub CI and the latest pre-push Greptile review (ad89a4bc-9b8b-4d98-b484-b32edcf1e5f6) are pending. Next agent should assess current review/check results before recommending merge.
  5. EHxuban11 commented on Sep 25, 2026

    @EHxuban11
    Contributor

    Hello @velu-97, this is what I had in mind! Custom fitness is merged into dev in #900 and will ship in the next release (v1.6.0).

    You can pass a callback with a fitness(metrics) method to train(). Its return value (higher is better) selects best.pt and drives patience. With your classifier metrics from #883, you can use F1 directly:

    class F1Fitness:
        def fitness(self, metrics):
            return metrics["metrics/f1"]
    
    model.train(data="data.yaml", patience=10, callbacks=F1Fitness())

    Thanks again for the idea and for the classifier PR! Let us know how it goes 🙏

  6. velu-97 commented on Sep 26, 2026

    @velu-97
    ContributorAuthor

    Hi @EHxuban11 ! Thanks for the support on my PR and merging it in and also for adding in the feature! Looking forward to using it on release 👍

  7. EHxuban11 commented on Sep 26, 2026

    @EHxuban11
    Contributor

    @velu-97 Perfect!, thanks for suggesting. v1.6.0 should be released this weekend if everything goes well!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions