def loss_batch(model, loss_func, xb, yb, opt=None, metric=None):
    """Calculates the loss and metric value for a batch of data,
    and optionally performs gradient descent if an optimizer is provided."""
    preds = model(xb)
    loss = loss_func(preds, yb)

    if opt is not None:
        loss.backward()  # Compute gradients
        opt.step()  # Update parameters
        opt.zero_grad()  # Reset gradients

    metric_result = None
    if metric is not None:
        metric_result = metric(preds, yb)  # Compute the metric

    return loss.item(), len(xb), metric_result