Skip to content

Metrics

A metric accumulates data over an epoch and produces a scalar report at the end. Metrics are attached to a model from config and run during validation and test. Losses are handled by the model itself, not by metrics.

The design separates two roles:

  • A suite (MetricSuite, a torchmetrics.Metric) is a task state engine. It owns the accumulated state, its cross-GPU reduction, and the dispatch across the range and filter axes. It does not decide which metrics run.
  • A metric (Metric) is a small, self-contained, injectable object. It computes its own numbers from the state the suite builds, and declares which stages it runs in and which filter it reads.

Which metrics run, in which stages, and over which slices is pure configuration. The suite is just the engine that feeds them.

What runs in each split

Split Losses Metrics
train logged not run
val logged run, metrics whose stages include val
test logged run, metrics whose stages include test
predict not run not run

Each metric declares its stages. The convention is that cheap headline metrics run in both val and test, while the heavier reporting runs only in test, so validation epochs stay fast. A suite is cloned only for the stages where at least one of its components reports, so a heavy test-only suite does not exist at validation time at all.

Lifecycle

A suite runs the standard torchmetrics contract across an epoch.

  • update(eval_out) runs once per batch on each GPU. It folds the batch into the suite's state and never talks to other GPUs. Everything that needs per-frame context that is not a tensor, such as the ego pose or the scene's lanelet map, happens here.
  • sync runs once at epoch end, inside compute. torchmetrics combines every GPU's state using the reduction declared for each state. This is the only cross-GPU step.
  • compute() runs after sync. Components are grouped by their filter, the suite builds one state per filter and range window, and every stage-applicable metric evaluates the state its filter selects.

result(stage) sets the reporting stage and calls compute. The mixin clones a suite per stage it reports at and resets it at epoch start, so each instance reports for exactly one stage. A suite declares the eval-output keys it needs through required_keys(), which folds in the keys its active components and their filters require, and the first batch fails loud when one is missing.

sequenceDiagram
    participant L as Lightning
    participant M as Model
    participant S as Suite
    participant Me as Metric
    loop each val batch
        L->>M: on_validation_batch_end(outputs, batch)
        M->>M: build_eval_output(batch, outputs)
        M->>S: update(eval_out)
    end
    L->>M: on_validation_epoch_end()
    M->>S: result(stage)
    S->>S: compute() syncs state across GPUs
    S->>S: state_for(range, filter) builds the state
    S->>Me: evaluate(state, stage) for each metric in this stage
    Me-->>S: MetricReport with bare names
    S-->>M: MetricReport placed in filter slices and range windows
    M->>L: log under val/prefix/key

Built-in suites

Each piece of state is registered with add_state(name, default, dist_reduce_fx), the reduce function torchmetrics uses to combine that state across GPUs.

Suite prefix Required keys State (dist_reduce_fx)
Detection3DMetricSuite det3d predictions, gt_boxes, gt_labels per-frame box tensors as list states (None)
Segmentation3DConfusionMatrixMetricSuite seg3d seg3d_frames one confusion tensor over (filter, range) buckets (sum)
Segmentation3DPointCloudMetricSuite seg3d_pt seg3d_frames per-frame point tensors (None)

A confusion matrix is a bounded sufficient statistic, so its counts sum across ranks. Detection matching is score ordered inside each frame and the point-level metrics read raw points, so those states stay per-frame list elements gathered with no reduction. Evaluation filters need the ego pose and the scene's lanelet map, which are not tensors, so their keep-masks are computed per frame at update and stored as boolean states, and DDP gathers only tensors.

seg3d_frames is a list with one entry per frame carrying that frame's point coordinates, predicted and target labels, per-class scores, and the per-frame metadata configured filters need (ego2global, lanelet_map_path). required_keys() extends the base tuple with what the configuration demands: a detection suite with a collision provider also requires ego2global and lanelet_map_path, and a ground-truth point filter requires gt_num_points.

A metric records its values under bare names such as mAP_car in a MetricReport. Every value travels with a typed MetricKey that names the suite prefix, the taxonomy view, the filter_name, the metric name and the range_suffix, and renders them as {prefix}/{view}/{filter_name}/{name}_{range_suffix} with the empty levels left out. The suite places each metric's keys in the filter slice and range window it evaluated, the model places each suite's keys under its prefix, and the key is logged as {split}/{rendered key}, for example test/det3d/region_road/mAP_0m_50m. A report rejects a duplicate key at every level and the log payload rejects two keys that render to one name, so two metrics, two filters or two suites can never silently overwrite each other. A value is read back by its key, report.metric_value(MetricKey(...)), and report.metric_names() lists the keys. Two suites of the same class run side by side under distinct configured prefixes, and the mixin rejects duplicate prefixes.

Class structure

classDiagram
    class TorchMetric["torchmetrics.Metric"] {
        +add_state(name, default, dist_reduce_fx)
        +update(eval_out)
        +compute()
        +reset()
    }
    class MetricSuite {
        +prefix : str
        +components : list[Metric]
        +required_keys()
        +update(eval_out)*
        +state_for(range, filter)*
        +compute()
        +result(stage)
    }
    class Metric {
        +stages : frozenset[EvalStage]
        +filter : MetricFilter
        +evaluate(state, stage)*
    }
    class TaskSuite["Detection3DMetricSuite / Segmentation3D...Suite"]
    class TaskMetric["MeanAP / IoU / ..."]
    TorchMetric <|-- MetricSuite
    MetricSuite <|-- TaskSuite
    Metric <|-- TaskMetric
    MetricSuite o-- "0..*" Metric : runs injected

Method marks:

Mark Meaning
* abstract, the subclass implements it
none concrete, provided by the base

A suite implements update and state_for and declares its prefix and required keys. A metric implements evaluate and declares stages and an optional filter. The suite holds a list of metrics it was given and runs each one against the state its filter selects. Adding a metric means adding a Metric subclass and listing it in config, never editing the suite.

Data requirements

The baseline metrics need nothing beyond the model's predictions and the ground truth. Each optional capability adds a concrete requirement, checked loud at the first batch.

Capability Eval-output keys External data
detection metrics predictions, gt_boxes, gt_labels none
segmentation metrics seg3d_frames none
ground-truth point filter, occlusion split gt_num_points (per-box point count) none
corridor filter (straight strip) none none
region filters and collision filter ego2global, lanelet_map_path per frame one lanelet2 map per scene
collision metrics (critical FP/FN, weighted mAP) ego2global, lanelet_map_path per frame one lanelet2 map per scene
velocity error (TP errors, NDS) boxes with velocity components none
partial-detection score ground-truth boxes inside seg3d_frames none

The map-based features never read a map path directly. They pass the frame's scene_token to a map provider, and an injected resolver turns that identifier into the scene's lanelet2_map.osm. Any data source that can name a map file per scene and attach a per-frame ego pose in the map frame can use them.

A missing eval-output key fails loud at the first batch, naming the suite and the key. A missing map is the one tolerated absence: the scene is excluded from the map-based slices and the collision metrics, kept in the whole-scene and corridor metrics, and the per-filter coverage is logged every epoch (see Coverage below). Boxes without velocity components score the worst-case velocity error instead of failing.

Evaluation axes

Every metric can be reported over orthogonal, purely configurational axes.

Ranges. Radial distance windows. Every key a metric emits is also emitted per range with a distance suffix. Detection clips boxes per range, segmentation buckets points.

Concentric radial distance bins around ego. Every object or point falls into the bin of its distance from ego, and every metric key is emitted once per bin in addition to the unbinned value. Radial range bins around ego every reported key is emitted once more per bin, with a distance suffix ego 0-50 m 50-90 m 90-120 m

Region filter. RegionFilter keeps only the elements whose position, transformed to the map frame by the frame's ego pose, falls inside a chosen set of lanelet regions. Detection tests the whole box footprint, segmentation tests each point. Any metric combined with a region filter becomes its on-road or on-walkway variant. The bundled configs define the road slice as the union of the lanelet primitives road, road_shoulder, crosswalk, drivable_area, intersection_area and crosswalk_polygon, and the walkway slice as the walkway primitive.

Bird's-eye view of the lanelet regions: a road band (including a crosswalk stripe, where the car drives) and a separate walkway band; objects are assigned to road, walkway, or neither, and metrics run per region. A car whose center is off the road but whose footprint overhangs it is still included, because membership is any footprint overlap (no threshold), not the center. road (drivable lanelet primitives) crosswalk walkway (pedestrian-only) ego road slice walkway slice off-map -> whole-scene only overhangs road -> included ● center off-road

An optional margin erodes the outer border of the union of the mapped regions, so noisy points at the edge of the mapped surface stop counting in the filtered slice.

The border margin erodes only the outer border of the union of all mapped regions: strips along the top of the walkway and the bottom of the road are excluded, while the internal road-to-walkway border keeps its points on both sides - no dead gap between adjacent regions. walkway road outer border ⊖ margin (negative margin) -> excluded internal border (road <-> walkway) - not eroded, no gap margin kept - both sides dropped dropped kept erosion applies to the union of ALL regions - never per region

Corridor filter. CorridorFilter keeps only the elements inside a straight corridor ahead of ego: a fixed-width forward strip in the ego frame. The strip carries no length bound of its own, because every key is range-binned anyway, so the range axis already slices the corridor by distance. It needs no map and no pose, so the slice covers every scene. A detection box counts when any part of its footprint overlaps the strip.

Bird's-eye view of the straight corridor: a fixed-width forward strip ahead of ego with no length bound. An object inside or overlapping the strip is kept, an object beside or behind it is dropped. A fixed-width strip ahead of ego, no map involved ego no length bound, distance is sliced by the range bins width inside -> kept overlaps -> kept beside -> dropped

Collision filter. CollisionFilter keeps only the elements inside the ego collision area, everything ego could collide with within the horizon under bounded steering, clipped to the drivable lanelets so it follows the road on bends. It is a reporting slice like the region filter, computed per frame from the same collision model the collision metrics use. A detection box counts when any part of its footprint meets the area, a segmentation point when it lies inside. Only ego is propagated here and every element is tested where it stands; the objects' own reachable sets enter through the collision metrics, not through the filter.

Bird's-eye view: the collision area is everything ego could collide with within the horizon at the lane speed limit under bounded steering, hard-clipped to the road; an object inside is kept, one off the road or beyond the horizon is dropped. sidewalk road (drivable) ego in path -> kept off road -> dropped beyond the horizon -> dropped lane speed limit + tightest feasible turn, clipped at the road edge

Class catalog. Every dataset family keeps a class catalog per task in configs/metrics/<dataset>/<task>/class_catalog/, one row per class name with its behaviour group, the distance it is evaluated up to (max_distance), its collision motion_model and max_speed, and whether the partial detection score checks it. A model reads the rows of the classes it trains, and a trained class the catalog does not list stops the run.

Behaviour groups. class_groups folds the trained classes onto behaviour-equivalent groups. Confusing a bus for a truck does not change how the vehicle drives, so inside a group it counts as a hit. Grouping always replaces the class axis, never extends it: the same suite is configured twice, once per class and once grouped under its own prefix, and both views share one accumulation contract.

Coverage. A scene without a lanelet map cannot be map-filtered, so region and collision slices exclude its frames while the whole-scene and corridor metrics keep them. The suites count per-filter frame coverage and log it every epoch, so a slice computed over fewer scenes is never silent.

The collision model

The collision model is not a filter but mechanics. It extends the collision filter: the same region construction (one shared ego agent, the same clipped area) with a time rule applied on top. Configured on the suite as the CollisionTTC provider, it computes a time to collision (TTC) per detection box once per frame at update time, from the ego pose, the class-specific collision sets, and the scene's lanelet map. The metrics that declare needs_ttc read the stored TTC at compute time.

The model answers one question: if ego never sees this object, what is the worst that can follow? Every agent moves at its class maximum speed under its tightest turn, forward or in reverse, so the collision set is a bound, not a prediction. A lead at matched speed is not exempt, it may brake. A wheeled body is swept as a rectangle placed at every time step along the arc, clipped to the drivable area and reduced to the part still connected to the driven path. The rectangle sits around the pose's reference point: a detection box is centred, ego is measured at its rear axle.

Bird's-eye worst-case collision model: ego expands to everything its body can sweep at maximum speed under its tightest feasible left/right turn - constant-curvature arcs that keep their curvature right up to the road edge, where the drivable area hard-clips them with a sharp corner. Only the forward half is drawn, reverse travel mirrors it behind ego. A crossing pedestrian expands an isotropic disc; a stationary barrier stays put. TTC is the first time a set meets ego's region. sidewalk road (drivable) 1 2 3 s ego worst case: max speed + tightest turn, hard-clipped at the road edge (reverse is the mirror image) pedestrian * disc set ∩ ego region -> finite TTC barrier

The time to collision (TTC) of an object is the first time step at which its collision set and ego's overlap. An object that cannot reach ego within the horizon has an infinite TTC.

A lead vehicle at the same speed: ego keeps advancing to the right while the lead may brake or reverse, so the collision sets grow towards each other and meet after about one second - matched speed gives no protection. ego (t) lead (t) ego keeps going lead brakes or reverses sets meet: TTC ~ 1 s lead at matched speed (t+1) matched speed is no protection -> the gap closes at the sum of both speeds

The model runs on the following constants. Values marked config are set in the bundled dataset configs and tunable there, the rest are code defaults.

Constant Value
horizon 4 s (config)
propagation time step 0.1 s (config)
max lateral acceleration (turn bound) 3.0 m/s^2 (config)
minimum turn radius floor ego: wheel base over tan max steer angle (config), objects: 3.0 m friction floor (config)
arc samples per reachable set at least 21, raised until neighbouring sweeps overlap at their far ends
ego body vehicle description dimensions from the dataset config, measured from the rear axle
object body the object's own box length and width
wheeled speed on the map the lanelet speed limit at the agent's position
off-map fallback speed 16.7 m/s (config)
omnidirectional max speeds pedestrian 3.0, animal 4.0, bicycle 6.0 m/s
wheeled classes car, truck, bus, train, motorcycle
omnidirectional classes pedestrian, animal, bicycle
static classes (footprint only) barrier, traffic_cone, debris, bicycle_rack, vehicle_extension
corridor width 3.0 m (config)

Default slices

The bundled dataset configs attach every metric to every filter slice (whole scene, road, walkway, corridor, collision area). Every key is also emitted per radial range bin and once more per behaviour group through the grouped twin suite. The partial-detection score is wired in the joint detection plus segmentation configs only (the detection ground truth rides in seg3d_frames) and is never grouped, its box to class mapping is bound to the trained label space.

Detection metrics

The detection state matches predictions to ground truth greedily in score order, center distance by default with a corner-distance cost selectable per suite. Matching is memoized per class and threshold, so every component reads the same match curves.

Mean AP

MeanAP is the center-distance average precision over its match thresholds, reported per class and as the class mean. It is the headline metric that also runs at validation. At test it adds per-class ground-truth counts, match counts, the maximum F1, and the optimal-confidence operating point of each curve.

Bird's-eye matching scene. A prediction whose center lies inside the ground-truth distance threshold is a true positive, a lone prediction is a false positive, and an unmatched ground-truth box is a false negative. Greedy center-distance matching in score order distance threshold score 0.92 inside threshold -> TP score 0.71 no ground truth near -> FP no prediction -> FN Predictions claim the nearest unclaimed ground truth, AP integrates the resulting precision-recall curve.

Heading AP

HeadingAP weights every true positive by its heading score, so a detector that finds objects but points them the wrong way scores lower than plain AP.

Two matched pairs with the same footprint overlap. The left prediction points with the ground truth and keeps full credit, the right prediction is rotated ninety degrees and keeps half credit. yaw error 0 -> weight 1.0 yaw error 90 deg -> weight 0.5 prediction heading matches ground truth prediction heading off by 90 deg

NDS

Nds folds the mean AP and the true-positive errors, taken at a single configured operating threshold, into one composite detection score.

Block diagram of the composite score. The mean AP contributes half of the score and the complement of the mean true-positive errors contributes the other half. Half detection quality, half true-positive quality mean AP weight 5 + 1 - mean TP error, weight 5 ATE AOE ASE AVE NDS The errors are read at one configured operating threshold, then the sum is scaled to the 0 to 1 range.

TP errors

TpErrors reports the translation, orientation, scale, and velocity errors of true positives at configured recall operating points, plus the optimal-confidence operating point.

One matched pair annotated with the four true-positive errors: the center offset, the yaw gap, the size mismatch, and the velocity gap between the two heading arrows. ATE AVE AOE ASE reported per class ATE center offset [m] AOE yaw gap [rad] ASE 1 - size IoU AVE velocity gap [m/s] Averaged over true positives selected at configured recall operating points.

Corner displacement error

CornerError couples position, size, and yaw into one distance in meters, measured where planning collides with things: the box outline. It also exposes footprint inflation that center distance is blind to.

A ground-truth truck box and a predicted box rotated by a few degrees share a center, but the corners are displaced by half a meter. δ ~ 0.5 m ground truth (dashed) prediction, 3 deg yaw error - angle exaggerated for visibility

Heading-flip rate

HeadingFlipRate counts true positives whose heading is reversed by about 180 degrees. Corner displacement forgives a flip because the outline barely moves, yet a flipped heading inverts the object's velocity direction and breaks tracking, so it is penalized on its own.

A ground-truth box and a predicted box that share the same outline but point in opposite directions: corner displacement is near zero, yet the heading is reversed, so it counts as one flip. GT heading -> prediction <- reversed same outline -> corner error ~ 0 heading reversed -> flip +1

Signed nearest-surface error

NearestSurfaceError measures the error of the object face nearest to ego, which is what stopping distance is computed against. The sign is the safety signal: positive means the predicted near face sits farther than the truth and ego brakes late, negative means over-caution.

Ego on the left; the predicted box places the near face farther than ground truth, producing a positive error that means braking later. ego d_gt (dashed = ground truth near face) d_pred (predicted near face) err > 0 = brakes late

Critical FP / FN

CriticalFPFN reports two numbers that are never averaged: false positives in ego's path cause phantom braking (usability), false negatives in ego's path mean driving toward something unseen (safety). An object is critical when its collision TTC is within the horizon. Requires the suite's collision provider.

Two bird's-eye scenes. Left: a predicted box sits in the ego path with no real object there, so ego brakes for a ghost - a false positive, a usability failure. Right: a real object sits in the ego path but the model missed it, so ego drives into it - a false negative, a safety failure. Phantom brake - false positive ego path -> ego brakes predicted box no real object FP in path -> phantom brake (usability) Collision - false negative ego path -> ego (no brake) real object (GT) model saw nothing FN in path -> collision (safety)

Both are reported as curves over the confidence threshold, so the operating point trade-off is visible.

Two curves over the confidence threshold: false positives in the collision set fall as the threshold rises while false negatives rise; both are read at the release operating point. release operating point FP in collision set - phantom brake (usability) FN in collision set - missed object (safety) confidence threshold τ -> errors / frame

Collision-risk-weighted mAP

CollisionWeightedMeanAP weights every object's contribution to precision and recall by an exponentially decaying function of its TTC, so mistakes on objects about to matter dominate the score. Requires the suite's collision provider.

Exponential decay of the collision-risk weight over time-to-collision across the four-second horizon, with labeled points at 1, 2 and 4 seconds. 1.0 0.5 0.0 0 1 2 3 4 TTC (seconds) - horizon T = 4 s weight w 0.61 0.37 0.14
Bird's-eye view: ego on a two-lane road. An oncoming vehicle closes fast (low time-to-collision, high weight); a parked car beyond the horizon reach can never be met (infinite time-to-collision, weight zero). weight follows collision TTC - not distance ego -> oncoming TTC~1 s * w 0.61 horizon reach parked, 70 m ahead TTC inf * w 0

Calibration error

CalibrationError bins predictions by score and measures the gap between the mean score and the empirical precision in each bin. Reported pooled and macro-averaged per class, so a dominant class cannot hide a badly calibrated rare one.

Reliability diagram: the diagonal is perfect calibration; bars below it show an overconfident model, and the gap between bar and diagonal is the calibration error. perfect calibration confidence (detection score) -> precision gap = |acc - conf|

Confident-error rate

ConfidentErrorRate reports, over the false positives, the fraction the model was confident about. A high-score phantom is what triggers a hard brake, so this is the usability release blocker. The missed-object axis has no score to read and belongs to the critical FN curve.

A two-by-two of correctness versus confidence; the confident-and-wrong quadrant is the dangerous one this metric measures. uncertain & correct confident & correct uncertain & wrong confident & wrong confidence -> wrong <- -> correct

Confusion matrix

ConfusionMatrix pairs predictions and ground truth with class-agnostic matching and counts the label of each matched pair, so a car predicted where a truck stands lands off the diagonal. It is the label-confusion view among detections that did match, not a recall metric.

A predicted car box matched onto a ground-truth truck votes one count into the off-diagonal truck-as-car cell of the class confusion matrix. Matched pairs vote a confusion cell ground truth: truck predicted: car predicted class car truck bus +1 car truck bus unmatched boxes are dropped

Frame and box counts

FrameCount reports how many frames the other metrics scored, as num_frames. A region-filtered component counts only the frames its filter covered, so a slice reported over fewer scenes than the whole-scene metrics shows it next to its scores. Range clipping removes boxes, never frames, so the component declares itself range_invariant and the suite reports it once per slice instead of once per range window. The segmentation suites ship the same component under autoware_ml.metrics.segmentation3d.frame_count, reading the frame count every suite keeps in its base class: one whole-scene counter, and the coverage counter for each filter slice.

BoxCounts reports how many ground-truth and predicted boxes the other metrics scored, in total and per class. The ground truth is already filtered by the minimum point count and the class range of the suite, and both sides are clipped to the evaluated range, so a change of the eval filters shows up in the counts next to the scores.

Occlusion-aware recall split

Not a component but a suite pattern: with per-box point counts (gt_num_points) in the eval output, a box with zero points is occluded. Two extra suites under distinct prefixes, one keeping visible boxes and one keeping everything, differ exactly by the occlusion effect on recall.

Bird's-eye scene: a ground-truth box behind an occluder receives no sensor returns and is occluded, a box outside the shadow collects hits and is visible. A box behind an occluder returns no points visible suite keeps hit boxes, the overall suite keeps all, the recall gap between them is the occlusion effect ego occluder 0 points -> occluded hits -> visible

Segmentation metrics

IoU, accuracy, precision / recall / F1

IoU, Accuracy, and PrecisionRecallF1 read the accumulated confusion matrix and report per class and macro-averaged values. IoU is the headline metric that also runs at validation.

The point sets of one class as two overlapping regions: points in both are true positives, predicted-only points are false positives, ground-truth-only points are false negatives. Point sets of one class ground truth region prediction missed (FN) correct (TP) spurious (FP) IoU = TP / (TP + FP + FN) precision = TP / (TP + FP) recall = TP / (TP + FN)

Confusion matrix

ConfusionMatrix emits the accumulated point confusion counts, rows true class, columns predicted class. In a grouped suite the matrix is already folded onto the behaviour groups.

Walkway points predicted as road vote into the off-diagonal walkway-as-road cell of the point confusion matrix. Every point votes a confusion cell road walkway walkway points predicted as road predicted class road walkway terrain +5 road walkway terrain grouped suites fold both axes onto behaviour groups

Error clusters

ErrorClusters reports the misclassification rate together with the connected error clusters: how many contiguous wrong regions exist and how large they are. A hundred scattered wrong points are noise, the same hundred points in one blob are a phantom obstacle.

Wrongly-classified points on the drivable surface: every connected group is ringed as one cluster - a lone point and a dense blob each count as a single phantom obstacle, regardless of size. drivable surface ego single-point clusters - 1 phantom each dense cluster - still 1 phantom

Neighbourhood-tolerant error rate

NeighbourhoodTolerantErrorRate counts a misclassified point as an error only when the model predicted its true class on no point within a small radius. Point-wise labels are not perfect, and for driving a handful of flipped points do not matter as long as the right class is present right there.

Two misclassified points: one sits beside a point where the model predicted its true class and is forgiven; the other has no correct-class neighbour within the radius and counts as an error. forgiven radius r pred A * GT B neighbour predicted B -> tolerated counts as error pred A * GT B nearest B is beyond r -> error

Small-object partial-detection score

PartialDetection3DScore groups segmentation points inside each small-object ground-truth box and rewards partial hits with a saturating credit: for a pedestrian or a cone, classifying even a few points correctly is far better than none, which point-averaged mIoU cannot see. A diagnostic metric of the joint detection plus segmentation suite (datasets/<dataset>/det3d_seg3d), whose seg3d_frames carry the detection ground-truth boxes. The detection class catalog marks the classes it reports with partial_detection.

Left: the saturating credit curve - zero correct points score zero, the first point earns about half, and credit saturates toward one at all points correct. Right: two instances, one with no correct point scoring zero and one with a single correct point scoring about half. saturating credit - h = 1 1.0 0.5 0 -> 0 1 pt -> 0.5 20/20 -> 1.0 correct points k inside the GT box (n = 20) existence is the big step 0 / 9 -> credit 0.00 1 / 9 -> credit ~ 0.56 point classified correctly point misclassified dashed = small-object GT box

Calibration error

CalibrationError is the expected calibration error over the per-point confidence: a model that says 90 percent road should be right about 90 percent of the time. Reported overall and macro-averaged per predicted class.

Reliability diagram: the diagonal is perfect calibration; bars below it show an overconfident model, and the gap between bar and diagonal is the calibration error. perfect calibration confidence (max softmax) -> accuracy gap = |acc - conf|

Uncertainty usefulness

UncertaintyUsefulness measures how well the per-point predictive entropy separates correct from misclassified points, as the AUROC of the binary "is this point wrong" task. 0.5 means the uncertainty is meaningless, higher means it can flag the model's own mistakes.

Two entropy distributions: correct points cluster at low entropy and misclassified points at high entropy; the more they separate, the higher the AUROC of entropy as an error detector. correct misclassified overlap = confusable normalized entropy H -> 0 1 point density

Confident-error rate

ConfidentErrorRate reports the fraction of misclassified points predicted with high confidence, measured as low predictive entropy. An error the model is unsure about is recoverable, a confident error is what hides an obstacle.

A two-by-two of correctness versus confidence; the confident-and-wrong quadrant is the dangerous one this metric measures. uncertain & correct confident & correct uncertain & wrong confident & wrong confidence -> wrong <- -> correct

Attaching metrics

model.metrics is a list of suites, so a joint segmentation and detection model lists several. Each suite is given its components, and each component its stages and optional filter. The suites live in the metrics config groups of each database and read the tunable pieces (class names, ranges, filters, groups) from interpolation variables, because Hydra replaces a list wholesale rather than merging it, so overriding a suite directly would mean restating every field.

model:
  metrics:
    - _target_: autoware_ml.metrics.detection3d.suite.Detection3DMetricSuite
      class_names: ${taxonomy.detection3d.class_names}
      max_distances: ${metric_params.detection3d.max_distances}
      ranges: ${metric_ranges}
      components:
        - { _target_: autoware_ml.metrics.detection3d.mean_ap.MeanAP, stages: [val, test] }
        - { _target_: autoware_ml.metrics.detection3d.nds.Nds, stages: [test] }
        - { _target_: autoware_ml.metrics.detection3d.mean_ap.MeanAP, stages: [test],
            filter: '${metric_params.detection3d.filters.region_filters.road}' }

The same component listed once more with a filter is one more reported slice.

What a model provides

One method. It maps the typed forward outputs and model inputs to the flat dict the suites read. Model-specific work like box decoding happens here, and per-frame metadata the dataset supplies (ego pose, scene token) is passed through.

class ModelA(ModuleBaseModel):
    def build_eval_output(self, batch, outputs):
        return detection3d_eval_output(self.decode_outputs(batch, outputs), batch)

The mixin feeds this dict into every attached suite. The model never calls update, compute, or result.

Writing a custom metric

A metric is the unit of extension. Subclass Metric, declare the stages it runs in (or accept the default), and read the suite's state. The example adds a per-class accuracy view to segmentation without touching the suite.

class PerClassAccuracy(Metric):
    def evaluate(self, state, stage):
        report = MetricReport()
        for i in range(state.num_classes):
            if bool(state.has_support[i]):
                report.record(f"acc_class_{i}", float(state.recall[i].item()))
        return report

Add it to the suite's components list in config and its keys appear under the suite prefix. A new metric family that needs new state is a new suite, which implements update and state_for.

Distributed runs

Quantity How it combines across GPUs
Loss Lightning reduces the scalar with sync_dist=True
Metric torchmetrics reduces each state by its dist_reduce_fx, then compute runs once

Losses are means, so a mean across GPUs is correct. Metrics are not always linear, so each state declares how it combines and torchmetrics applies it before computing. After sync the state is identical on every rank, so the result is logged without sync_dist.

Distributed eval padding

autoware-ml test runs on a single device by default, so there is no padding and the metrics are exact. Pass --use-config-devices to evaluate on the config's devices. The caveat below only applies when evaluation runs on more than one device, for example validation during multi-GPU training or test with --use-config-devices on several GPUs.

Under DDP the validation sampler pads the last batch with repeated frames so the dataset divides evenly across ranks, which double counts at most world_size - 1 frames. On a normal validation set this is well under a tenth of a percent and is left uncorrected. A detection suite could drop the duplicates by frame id, but a segmentation suite cannot, because its confusion matrix has already pooled the points and a single frame can no longer be removed. Bit exact multi-device eval would instead use a non padding sampler at the datamodule level, which is out of scope for the metrics.