Repository: ultralytics/ultralytics
Issue category: Bug Report

Issue:
wandb callback reporting fails if no positive examples in validator
### Search before asking

- [X] I have searched the Ultralytics YOLO [issues](https://github.com/ultralytics/ultralytics/issues) and found no similar bug report.


### Ultralytics YOLO Component

Predict

### Bug

When using the `wandb` callback, the following error occurs if there are no positive examples:
```
Traceback (most recent call last):
  File "pipeline.py", line 97, in <module>
    main()
  File "pipeline.py", line 84, in main
    output_path = train_stage(stage_config)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "train_new.py", line 57, in train_stage
    results = model.train(**train_args)
              ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/ultralytics/engine/model.py", line 802, in train
    self.trainer.train()
  File "site-packages/ultralytics/engine/trainer.py", line 207, in train
    self._do_train(world_size)
  File "site-packages/ultralytics/engine/trainer.py", line 477, in _do_train
    self.run_callbacks("on_train_end")
  File "site-packages/ultralytics/engine/trainer.py", line 168, in run_callbacks
    callback(self)
  File "site-packages/ultralytics/utils/callbacks/wb.py", line 141, in on_train_end
    if trainer.args.plots and trainer.validator.metrics.curves and trainer.validator.metrics.curves_results:
                                                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/ultralytics/utils/__init__.py", line 221, in __getattr__
    raise AttributeError(f"'{name}' object has no attribute '{attr}'. See valid attributes below.\n{self.__doc__}")
AttributeError: 'DetMetrics' object has no attribute 'curves_results'. See valid attributes below.
    Utility class for computing detection metrics such as precision, recall, and mean average precision (mAP) of an
    object detection model.
    Args:
        save_dir (Path): A path to the directory where the output plots will be saved. Defaults to current directory.
        plot (bool): A flag that indicates whether to plot precision-recall curves for each class. Defaults to False.
        on_plot (func): An optional callback to pass plots path and data when they are rendered. Defaults to None.
        names (dict of str): A dict of strings that represents the names of the classes. Defaults to an empty tuple.
    Attributes:
        save_dir (Path): A path to the directory where the output plots will be saved.
        plot (bool): A flag that indicates whether to plot the precision-recall curves for each class.
        on_plot (func): An optional callback to pass plots path and data when they are rendered.
        names (dict of str): A dict of strings that represents the names of the classes.
        box (Metric): An instance of the Metric class for storing the results of the detection metrics.
        speed (dict): A dictionary for storing the execution time of different parts of the detection process.
    Methods:
        process(tp, conf, pred_cls, target_cls): Updates the metric results with the latest batch of predictions.
        keys: Returns a list of keys for accessing the computed detection metrics.
        mean_results: Returns a list of mean values for the computed detection metrics.
        class_result(i): Returns a list of values for the computed detection metrics for a specific class.
        maps: Returns a dictionary of mean average precision (mAP) values for different IoU thresholds.
        fitness: Computes the fitness score based on the computed detection metrics.
        ap_class_index: Returns a list of class indices sorted by their average precision (AP) values.
        results_dict: Returns a dictionary that maps detection metric keys to their computed values.
        curves: TODO
        curves_results: TODO
```

### Environment

```
Ultralytics 8.3.30 🚀 Python-3.11.5 torch-2.1.2+cu121 CUDA:0 (NVIDIA GeForce RTX 4090, 24111MiB)
Setup complete ✅ (32 CPUs, 251.5 GB RAM, 6725.0/7096.0 GB disk)

OS                  Linux-6.5.0-45-generic-x86_64-with-glibc2.35
Environment         Linux
Python              3.11.5
Install             pip
RAM                 251.52 GB
Disk                6725.0/7096.0 GB
CPU                 AMD Ryzen Threadripper PRO 5955WX 16-Cores
CPU count           32
GPU                 NVIDIA GeForce RTX 4090, 24111MiB
GPU count           3
CUDA                12.1

numpy               ✅ 1.24.3>=1.23.0
matplotlib          ✅ 3.7.2>=3.3.0
opencv-python       ✅ 4.8.1.78>=4.6.0
pillow              ✅ 10.1.0>=7.1.2
pyyaml              ✅ 6.0>=5.3.1
requests            ✅ 2.31.0>=2.23.0
scipy               ✅ 1.11.1>=1.4.1
torch               ✅ 2.1.2>=1.8.0
torchvision         ✅ 0.16.2>=0.9.0
tqdm                ✅ 4.65.0>=4.64.0
psutil              ✅ 5.9.0
py-cpuinfo          ✅ 8.0.0
pandas              ✅ 2.0.3>=1.1.4
seaborn             ✅ 0.12.2>=0.11.0
ultralytics-thop    ✅ 2.0.0>=2.0.0
numpy               ✅ 1.24.3<2.0.0; sys_platform == "darwin"
torch               ✅ 2.1.2!=2.4.0,>=1.8.0; sys_platform == "win32"
```

### Minimal Reproducible Example

```
import wandb
from ultralytics import YOLO
from wandb.integration.ultralytics import add_wandb_callback

def train_yolo():
    # Initialize wandb
    wandb.init(
        project="yolo-example",
        name="training-run",
        job_type="training"
    )
    
    # Initialize YOLO model
    model = YOLO('yolov8n.yaml')  # or 'yolov8n.pt' for pretrained
    
    # Add wandb callback to log metrics
    add_wandb_callback(model)
    
    # Train the model
    results = model.train(
        data='coco128_negative.yaml',  # path to data config file with negatives
        epochs=3,
        batch=16,
        imgsz=640
    )
    
    # Close wandb run
    wandb.finish()

if __name__ == "__main__":
    train_yolo()
```

### Additional

_No response_

### Are you willing to submit a PR?

- [X] Yes I'd like to help by submitting a PR!


Hints:

Patch:
diff --git a/ultralytics/utils/callbacks/wb.py b/ultralytics/utils/callbacks/wb.py
index b82b8d85ec3..22bbc347566 100644
--- a/ultralytics/utils/callbacks/wb.py
+++ b/ultralytics/utils/callbacks/wb.py
@@ -138,7 +138,7 @@ def on_train_end(trainer):
         art.add_file(trainer.best)
         wb.run.log_artifact(art, aliases=["best"])
     # Check if we actually have plots to save
-    if trainer.args.plots:
+    if trainer.args.plots and hasattr(trainer.validator.metrics, "curves_results"):
         for curve_name, curve_values in zip(trainer.validator.metrics.curves, trainer.validator.metrics.curves_results):
             x, y, x_title, y_title = curve_values
             _plot_curve(


Test patch: