Files
ludwig-ai--ludwig/tests/integration_tests/test_postprocessing.py
T
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

256 lines
8.9 KiB
Python

# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
import os
from functools import partial
from unittest import mock
import numpy as np
import pandas as pd
import pytest
import torch
from ludwig.api import LudwigModel
from ludwig.constants import BATCH_SIZE, DECODER, NAME, TRAINER
from ludwig.globals import MODEL_FILE_NAME
from tests.integration_tests.utils import (
binary_feature,
category_feature,
generate_data,
RAY_BACKEND_CONFIG,
set_feature,
text_feature,
)
def random_binary_logits(*args, num_predict_samples, **kwargs):
# Produce an even mix of True and False predictions, as the model may be biased
# towards one direction without training
return torch.tensor(np.random.uniform(low=-1.0, high=1.0, size=(num_predict_samples,)), dtype=torch.float32)
def random_set_logits(*args, num_predict_samples, vocab_size, pct_positive, **kwargs):
# Produce a desired mix of predictions based on the pct_positive, as the model may be biased
# towards one direction without training
num_positive = int(num_predict_samples * pct_positive)
num_negative = num_predict_samples - num_positive
negative_logits = np.random.uniform(low=-1.0, high=-0.1, size=(num_negative, vocab_size))
positive_logits = np.random.uniform(low=0.1, high=1.0, size=(num_positive, vocab_size))
logits = np.concatenate([negative_logits, positive_logits], axis=0)
return torch.tensor(logits, dtype=torch.float32) # simulate torch model output
def _run_binary_predictions(tmpdir, backend, distinct_values, ray_cluster_2cpu):
input_features = [
category_feature(encoder={"vocab_size": 3}),
]
feature = binary_feature()
output_features = [
feature,
]
data_csv_path = generate_data(
input_features,
output_features,
os.path.join(tmpdir, "dataset.csv"),
num_examples=20,
)
data_df = pd.read_csv(data_csv_path)
# Optionally convert bool values to strings, e.g., {'Yes', 'No'}
false_value, true_value = distinct_values
data_df[feature[NAME]] = data_df[feature[NAME]].map(lambda x: true_value if x else false_value)
data_df.to_csv(data_csv_path, index=False)
config = {
"input_features": input_features,
"output_features": output_features,
TRAINER: {"epochs": 1, BATCH_SIZE: 128},
}
patch_args = (
"ludwig.features.binary_feature.BinaryOutputFeature.logits",
partial(random_binary_logits, num_predict_samples=len(data_df)),
)
preds_df, _ = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args)
cols = set(preds_df.columns)
assert f"{feature[NAME]}_predictions" in cols
assert f"{feature[NAME]}_probabilities_{false_value!s}" in cols
assert f"{feature[NAME]}_probabilities_{true_value!s}" in cols
assert f"{feature[NAME]}_probability" in cols
for pred, prob_0, prob_1, prob in zip(
preds_df[f"{feature[NAME]}_predictions"],
preds_df[f"{feature[NAME]}_probabilities_{false_value!s}"],
preds_df[f"{feature[NAME]}_probabilities_{true_value!s}"],
preds_df[f"{feature[NAME]}_probability"],
):
assert pred == false_value or pred == true_value
if pred == true_value:
assert prob_1 == prob
else:
assert prob_0 == prob
assert np.allclose(prob_0, 1 - prob_1)
@pytest.mark.parametrize("distinct_values", [(False, True), ("No", "Yes")])
def test_binary_predictions(tmpdir, distinct_values, ray_cluster_2cpu):
_run_binary_predictions(tmpdir, "local", distinct_values, ray_cluster_2cpu)
@pytest.mark.slow
@pytest.mark.distributed
@pytest.mark.distributed_f
@pytest.mark.parametrize("distinct_values", [(False, True), ("No", "Yes")])
def test_binary_predictions_ray(tmpdir, distinct_values, ray_cluster_2cpu):
_run_binary_predictions(tmpdir, "ray", distinct_values, ray_cluster_2cpu)
def _run_binary_predictions_with_number_dtype(tmpdir, backend, distinct_values, ray_cluster_2cpu):
input_features = [
category_feature(encoder={"vocab_size": 3}),
]
feature = binary_feature()
output_features = [
feature,
]
data_csv_path = generate_data(
input_features,
output_features,
os.path.join(tmpdir, "dataset.csv"),
num_examples=20,
)
data_df = pd.read_csv(data_csv_path)
# Optionally convert bool values to strings, e.g., {'Yes', 'No'}
false_value, true_value = distinct_values
data_df[feature[NAME]] = data_df[feature[NAME]].map(lambda x: true_value if x else false_value)
data_df.to_csv(data_csv_path, index=False)
config = {
"input_features": input_features,
"output_features": output_features,
TRAINER: {"epochs": 1, BATCH_SIZE: 128},
}
patch_args = (
"ludwig.features.binary_feature.BinaryOutputFeature.logits",
partial(random_binary_logits, num_predict_samples=len(data_df)),
)
preds_df, _ = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args)
cols = set(preds_df.columns)
assert f"{feature[NAME]}_predictions" in cols
assert f"{feature[NAME]}_probabilities_False" in cols
assert f"{feature[NAME]}_probabilities_True" in cols
assert f"{feature[NAME]}_probability" in cols
for pred, prob_0, prob_1, prob in zip(
preds_df[f"{feature[NAME]}_predictions"],
preds_df[f"{feature[NAME]}_probabilities_False"],
preds_df[f"{feature[NAME]}_probabilities_True"],
preds_df[f"{feature[NAME]}_probability"],
):
assert isinstance(pred, bool)
if pred:
assert prob_1 == prob
else:
assert prob_0 == prob
assert np.allclose(prob_0, 1 - prob_1)
@pytest.mark.parametrize("distinct_values", [(0.0, 1.0), (0, 1)])
def test_binary_predictions_with_number_dtype(tmpdir, distinct_values, ray_cluster_2cpu):
_run_binary_predictions_with_number_dtype(tmpdir, "local", distinct_values, ray_cluster_2cpu)
@pytest.mark.slow
@pytest.mark.distributed
@pytest.mark.distributed_f
@pytest.mark.parametrize("distinct_values", [(0.0, 1.0), (0, 1)])
def test_binary_predictions_with_number_dtype_ray(tmpdir, distinct_values, ray_cluster_2cpu):
_run_binary_predictions_with_number_dtype(tmpdir, "ray", distinct_values, ray_cluster_2cpu)
@pytest.mark.parametrize("pct_positive", [1.0, 0.5, 0.0])
def test_set_feature_saving(tmpdir, pct_positive):
backend = "local"
input_features = [
text_feature(encoder={"vocab_size": 3}),
]
feature = set_feature(output_feature=True)
output_features = [
feature,
]
data_csv_path = generate_data(
input_features,
output_features,
os.path.join(tmpdir, "dataset.csv"),
num_examples=20,
)
data_df = pd.read_csv(data_csv_path)
config = {
"input_features": input_features,
"output_features": output_features,
TRAINER: {"epochs": 1, BATCH_SIZE: 128},
}
patch_args = (
"ludwig.features.set_feature.SetOutputFeature.logits",
partial(
random_set_logits,
num_predict_samples=len(data_df),
vocab_size=feature[DECODER]["vocab_size"] + 1, # +1 for UNK
pct_positive=pct_positive,
),
)
preds_df, ludwig_model = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args)
cols = set(preds_df.columns)
assert f"{feature[NAME]}_predictions" in cols
assert f"{feature[NAME]}_probabilities" in cols
backend = ludwig_model.backend
backend.df_engine.to_parquet(preds_df, os.path.join(tmpdir, "preds.parquet")) # test saving
def predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=None):
if backend == "ray":
backend = RAY_BACKEND_CONFIG
backend["processor"]["type"] = "dask"
ludwig_model = LudwigModel(config, backend=backend)
_, _, output_directory = ludwig_model.train(
dataset=data_csv_path,
output_directory=os.path.join(tmpdir, "output"),
)
# Check that metadata JSON saves and loads correctly
ludwig_model = LudwigModel.load(os.path.join(output_directory, MODEL_FILE_NAME))
if patch_args is not None:
with mock.patch(*patch_args):
preds_df, _ = ludwig_model.predict(dataset=data_csv_path)
else:
preds_df, _ = ludwig_model.predict(dataset=data_csv_path)
return preds_df, ludwig_model