diff --git a/ivy/functional/frontends/sklearn/metrics/_classification.py b/ivy/functional/frontends/sklearn/metrics/_classification.py index 4404653e97c0d..fab324a8b09ff 100644 --- a/ivy/functional/frontends/sklearn/metrics/_classification.py +++ b/ivy/functional/frontends/sklearn/metrics/_classification.py @@ -133,3 +133,33 @@ def recall_score(y_true, y_pred, *, sample_weight=None): ret = ret.astype("float64") return ret + + +@to_ivy_arrays_and_back +def log_loss(y_true, y_pred, *, eps=1e-15, sample_weight=None): + # Ensure that y_true and y_pred have the same shape + if y_true.shape != y_pred.shape: + raise IvyValueError("y_true and y_pred must have the same shape") + + # Clip y_pred to avoid log(0) issues + y_pred = ivy.clip(y_pred, eps, 1 - eps) + + # Calculate the log loss component-wise + log_loss = -(y_true * ivy.log(y_pred) + (1 - y_true) * ivy.log(1 - y_pred)) + + # Check if sample_weight is provided and normalize it + if sample_weight is not None: + sample_weight = ivy.array(sample_weight) + if sample_weight.shape[0] != y_true.shape[0]: + raise IvyValueError( + "sample_weight must have the same length as y_true and y_pred" + ) + + sample_weight = sample_weight / ivy.sum(sample_weight) + log_loss = log_loss * sample_weight + + # Compute the mean log loss + loss = ivy.mean(log_loss) + + loss = loss.astype("float64") + return loss diff --git a/ivy_tests/test_ivy/test_frontends/test_sklearn/test_metrics/test_classification.py b/ivy_tests/test_ivy/test_frontends/test_sklearn/test_metrics/test_classification.py index ee51db86a56aa..f5351a54f75fa 100644 --- a/ivy_tests/test_ivy/test_frontends/test_sklearn/test_metrics/test_classification.py +++ b/ivy_tests/test_ivy/test_frontends/test_sklearn/test_metrics/test_classification.py @@ -236,3 +236,67 @@ def test_sklearn_recall_score( y_pred=values[1], sample_weight=sample_weight, ) + +@handle_frontend_test( + fn_tree="sklearn.metrics.log_loss", + arrays_and_dtypes=helpers.dtype_and_values( + available_dtypes=helpers.get_dtypes("float"), + num_arrays=2, + min_value=0, + max_value=1, # log_loss expects probability values between 0 and 1 + shared_dtype=True, + shape=(helpers.ints(min_value=2, max_value=5)), + ), + sample_weight=st.lists( + st.floats(min_value=0.1, max_value=1), min_size=2, max_size=5 + ), +) +def test_sklearn_log_loss( + arrays_and_dtypes, + on_device, + fn_tree, + frontend, + test_flags, + backend_fw, + sample_weight, +): + dtypes, values = arrays_and_dtypes + # Ensure y_true is binary (0 or 1) and y_pred is within [0, 1] + values[0] = np.round(values[0]).astype(int) + values[1] = np.clip(values[1], 0, 1) + + # Adjust sample_weight to have the correct length + sample_weight = np.array(sample_weight).astype(float) + if len(sample_weight) != len(values[0]): + # If sample_weight is shorter, extend it with ones + sample_weight = np.pad( + sample_weight, + (0, max(0, len(values[0]) - len(sample_weight))), + "constant", + constant_values=1.0, + ) + # If sample_weight is longer, truncate it + sample_weight = sample_weight[: len(values[0])] + + # Detach tensors if they require grad before converting to NumPy arrays + if backend_fw == "torch": + values = [ + ( + value.detach().numpy() + if isinstance(value, torch.Tensor) and value.requires_grad + else value + ) + for value in values + ] + + helpers.test_frontend_function( + input_dtypes=dtypes, + backend_to_test=backend_fw, + test_flags=test_flags, + fn_tree=fn_tree, + frontend=frontend, + on_device=on_device, + y_true=values[0], + y_pred=values[1], + sample_weight=sample_weight, + )