Files
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

497 lines
18 KiB
Python

import json
import logging
import os.path
import re
import numpy as np
import pandas as pd
import pytest
import torch
from ludwig import globals as global_vars
from ludwig.api import LudwigModel
from ludwig.backend import LOCAL_BACKEND
from ludwig.constants import (
BATCH_SIZE,
CATEGORY,
DEFAULTS,
EPOCHS,
INPUT_FEATURES,
OUTPUT_FEATURES,
PREPROCESSING,
TRAINER,
TRAINING,
)
from ludwig.contribs.mlflow import MlflowCallback
from ludwig.experiment import experiment_cli
from ludwig.features.number_feature import numeric_transformation_registry
from ludwig.globals import DESCRIPTION_FILE_NAME, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME, TRAINING_PREPROC_FILE_NAME
from ludwig.utils.data_utils import load_json, replace_file_extension
from ludwig.utils.misc_utils import get_from_registry
from ludwig.utils.package_utils import LazyLoader
from tests.integration_tests import synthetic_test_data
from tests.integration_tests.utils import category_feature, generate_data, LocalTestBackend
mlflow = LazyLoader("mlflow", globals(), "mlflow")
RANDOM_SEED = 42
@pytest.mark.parametrize("early_stop", [3, 5])
def test_early_stopping(early_stop, tmp_path):
input_features, output_features = synthetic_test_data.get_feature_configs()
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat"},
TRAINER: {"epochs": 20, "early_stop": early_stop, "batch_size": 16, "learning_rate": 0.01},
}
# create sub-directory to store results
results_dir = tmp_path / "results"
results_dir.mkdir()
# run experiment
generated_data = synthetic_test_data.get_generated_data()
_, _, _, _, output_dir = experiment_cli(
training_set=generated_data.train_df,
validation_set=generated_data.validation_df,
test_set=generated_data.test_df,
output_directory=str(results_dir),
config=config,
skip_save_processed_input=True,
skip_save_progress=True,
skip_save_unprocessed_output=True,
skip_save_model=True,
skip_save_log=True,
)
# test existence of required files
train_stats_fp = os.path.join(output_dir, "training_statistics.json")
metadata_fp = os.path.join(output_dir, DESCRIPTION_FILE_NAME)
assert os.path.isfile(train_stats_fp)
assert os.path.isfile(metadata_fp)
# retrieve results so we can validate early stopping
with open(train_stats_fp) as f:
train_stats = json.load(f)
with open(metadata_fp) as f:
metadata = json.load(f)
# get early stopping value
early_stop_value = metadata["config"][TRAINER]["early_stop"]
# retrieve validation losses
vald_losses_data = train_stats["validation"]["combined"]["loss"]
last_evaluation = len(vald_losses_data) - 1
best_evaluation = np.argmin(vald_losses_data)
assert last_evaluation - best_evaluation == early_stop_value
@pytest.mark.parametrize("skip_save_progress", [False])
@pytest.mark.parametrize("skip_save_model", [False, True])
def test_model_progress_save(skip_save_progress, skip_save_model, tmp_path):
input_features, output_features = synthetic_test_data.get_feature_configs()
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat"},
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
}
# create sub-directory to store results
results_dir = tmp_path / "results"
results_dir.mkdir()
# run experiment
generated_data = synthetic_test_data.get_generated_data()
_, _, _, _, output_dir = experiment_cli(
training_set=generated_data.train_df,
validation_set=generated_data.validation_df,
test_set=generated_data.test_df,
output_directory=str(results_dir),
config=config,
skip_save_processed_input=True,
skip_save_progress=skip_save_progress,
skip_save_unprocessed_output=True,
skip_save_model=skip_save_model,
skip_save_log=True,
)
# ========== Check for required result data sets =============
model_dir = os.path.join(output_dir, MODEL_FILE_NAME)
files = [f for f in os.listdir(model_dir) if re.match(MODEL_WEIGHTS_FILE_NAME, f)]
if skip_save_model:
assert len(files) == 0
else:
assert len(files) == 1
training_checkpoints_dir = os.path.join(output_dir, MODEL_FILE_NAME, "training_checkpoints")
training_checkpoints = os.listdir(training_checkpoints_dir)
if skip_save_progress:
assert len(training_checkpoints) == 0
else:
assert len(training_checkpoints) > 0
@pytest.mark.parametrize("optimizer", ["sgd", "adam"])
def test_resume_training(optimizer, tmp_path):
input_features, output_features = synthetic_test_data.get_feature_configs()
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat"},
TRAINER: {"epochs": 2, "batch_size": 16, "optimizer": {"type": optimizer}},
}
# create sub-directory to store results
results_dir = tmp_path / "results"
results_dir.mkdir()
generated_data = synthetic_test_data.get_generated_data()
_, _, _, _, output_dir1 = experiment_cli(
config,
training_set=generated_data.train_df,
validation_set=generated_data.validation_df,
test_set=generated_data.test_df,
)
config[TRAINER]["epochs"] = 5
experiment_cli(
config,
training_set=generated_data.train_df,
validation_set=generated_data.validation_df,
test_set=generated_data.test_df,
model_resume_path=output_dir1,
)
_, _, _, _, output_dir2 = experiment_cli(
config,
training_set=generated_data.train_df,
validation_set=generated_data.validation_df,
test_set=generated_data.test_df,
)
# compare learning curves with and without resuming
ts1 = load_json(os.path.join(output_dir1, "training_statistics.json"))
ts2 = load_json(os.path.join(output_dir2, "training_statistics.json"))
print("ts1", ts1)
print("ts2", ts2)
assert ts1[TRAINING]["combined"]["loss"] == ts2[TRAINING]["combined"]["loss"]
# compare predictions with and without resuming
y_pred1 = np.load(os.path.join(output_dir1, "y_predictions.npy"))
y_pred2 = np.load(os.path.join(output_dir2, "y_predictions.npy"))
print("y_pred1", y_pred1)
print("y_pred2", y_pred2)
assert np.all(np.isclose(y_pred1, y_pred2))
@pytest.mark.parametrize("optimizer", ["sgd", "adam"])
def test_resume_training_mlflow(optimizer, tmp_path):
input_features, output_features = synthetic_test_data.get_feature_configs()
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat"},
TRAINER: {"epochs": 2, "batch_size": 16, "eval_batch_size": 2, "optimizer": {"type": optimizer}},
}
# create sub-directory to store results
results_dir = tmp_path / "results"
results_dir.mkdir()
mlflow_uri = f"file://{tmp_path}/mlruns"
experiment_name = optimizer + "_experiment"
generated_data = synthetic_test_data.get_generated_data()
_, _, _, _, output_dir1 = experiment_cli(
config,
training_set=generated_data.train_df,
validation_set=generated_data.validation_df,
test_set=generated_data.test_df,
callbacks=[MlflowCallback(mlflow_uri)],
experiment_name=experiment_name,
)
# Can't change any artifact spec on a run once it has been logged to mlflow, so skipping changing epochs
_, _, _, _, output_dir2 = experiment_cli(
config,
training_set=generated_data.train_df,
validation_set=generated_data.validation_df,
test_set=generated_data.test_df,
model_resume_path=output_dir1,
callbacks=[MlflowCallback(mlflow_uri)],
experiment_name=experiment_name,
)
# make sure there is only one mlflow run id
experiment = mlflow.get_experiment_by_name(experiment_name)
previous_runs = mlflow.search_runs([experiment.experiment_id])
assert len(previous_runs) == 1
@pytest.mark.parametrize("optimizer_type", ["sgd", "adam", "adamw", "adagrad", "rmsprop"])
def test_optimizers(optimizer_type, tmp_path):
if (optimizer_type in {"lars", "lamb", "lion"}) and (
not torch.cuda.is_available() or torch.cuda.device_count() == 0
):
pytest.skip("Skip: lars, lamb, and lion optimizers require GPU and none are available.")
if ("paged" in optimizer_type or "8bit" in optimizer_type) and (
not torch.cuda.is_available() or torch.cuda.device_count() == 0
):
pytest.skip("Skip: paged and 8-bit optimizers require GPU and none are available.")
input_features, output_features = synthetic_test_data.get_feature_configs()
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat"},
TRAINER: {"epochs": 2, "batch_size": 16, "evaluate_training_set": True, "optimizer": {"type": optimizer_type}},
}
# special handling for adadelta and lbfgs, break out of local minima
if optimizer_type == "adadelta":
config[TRAINER]["learning_rate"] = 0.1
if optimizer_type == "lbfgs":
config[TRAINER]["learning_rate"] = 0.05
model = LudwigModel(config)
# create sub-directory to store results
results_dir = tmp_path / "results"
results_dir.mkdir()
# run experiment
generated_data = synthetic_test_data.get_generated_data_for_optimizer()
train_stats, preprocessed_data, output_directory = model.train(
training_set=generated_data.train_df,
output_directory=str(results_dir),
config=config,
skip_save_processed_input=True,
skip_save_progress=True,
skip_save_unprocessed_output=True,
skip_save_model=True,
skip_save_log=True,
)
# retrieve training losses for first and last entries.
train_losses = train_stats[TRAINING]["combined"]["loss"]
last_entry = len(train_losses)
# ensure train loss for last entry is less than to the first entry.
np.testing.assert_array_less(train_losses[last_entry - 1], train_losses[0])
def test_regularization(tmp_path):
input_features, output_features = synthetic_test_data.get_feature_configs()
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat"},
TRAINER: {
"epochs": 1,
"batch_size": 16,
"regularization_lambda": 1,
},
}
# create sub-directory to store results
results_dir = tmp_path / "results"
results_dir.mkdir()
regularization_losses = []
generated_data = synthetic_test_data.get_generated_data()
for regularizer in [None, "l1", "l2", "l1_l2"]:
np.random.seed(RANDOM_SEED)
torch.manual_seed(RANDOM_SEED)
# setup regularization parameters
config[TRAINER]["regularization_type"] = regularizer
# run experiment
_, _, _, _, output_dir = experiment_cli(
training_set=generated_data.train_df,
validation_set=generated_data.validation_df,
test_set=generated_data.test_df,
output_directory=str(results_dir),
config=config,
experiment_name="regularization",
model_name=str(regularizer),
skip_save_processed_input=True,
skip_save_progress=True,
skip_save_unprocessed_output=True,
skip_save_model=True,
skip_save_log=True,
)
# test existence of required files
train_stats_fp = os.path.join(output_dir, "training_statistics.json")
metadata_fp = os.path.join(output_dir, DESCRIPTION_FILE_NAME)
assert os.path.isfile(train_stats_fp)
assert os.path.isfile(metadata_fp)
# retrieve results so we can compare training loss with regularization
with open(train_stats_fp) as f:
train_stats = json.load(f)
# retrieve training losses for all epochs
train_losses = train_stats[TRAINING]["combined"]["loss"]
regularization_losses.append(train_losses[0])
# create a set of losses
regularization_losses_set = set(regularization_losses)
# ensure all losses obtained with the different methods are different
assert len(regularization_losses) == len(regularization_losses_set)
# test cache checksum function
def test_cache_checksum(csv_filename, tmp_path):
# setup for training
input_features = [category_feature(encoder={"vocab_size": 5})]
output_features = [category_feature(decoder={"vocab_size": 2}, top_k=2)]
source_dataset = os.path.join(tmp_path, csv_filename)
source_dataset = generate_data(input_features, output_features, source_dataset)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
DEFAULTS: {CATEGORY: {PREPROCESSING: {"fill_value": "<UNKNOWN>"}}},
TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},
}
backend = LocalTestBackend()
cache_fname = replace_file_extension(source_dataset, TRAINING_PREPROC_FILE_NAME)
# conduct initial training
output_directory = os.path.join(tmp_path, "results")
model = LudwigModel(config, backend=backend)
model.train(dataset=source_dataset, output_directory=output_directory)
first_training_timestamp = os.path.getmtime(cache_fname)
# conduct second training, should not force recreating hdf5
model = LudwigModel(config, backend=backend)
model.train(dataset=source_dataset, output_directory=output_directory)
current_training_timestamp = os.path.getmtime(cache_fname)
# time stamps should be the same
assert first_training_timestamp == current_training_timestamp
# force recreating cache file by changing checksum by updating defaults
prior_training_timestamp = current_training_timestamp
config[DEFAULTS][CATEGORY][PREPROCESSING]["fill_value"] = "<EMPTY>"
model = LudwigModel(config, backend=backend)
model.train(dataset=source_dataset, output_directory=output_directory)
current_training_timestamp = os.path.getmtime(cache_fname)
# timestamp should differ
assert prior_training_timestamp < current_training_timestamp
# force recreating cache by updating modification time of source dataset
prior_training_timestamp = current_training_timestamp
os.utime(source_dataset)
model = LudwigModel(config, backend=backend)
model.train(dataset=source_dataset, output_directory=output_directory)
current_training_timestamp = os.path.getmtime(cache_fname)
# timestamps should be different
assert prior_training_timestamp < current_training_timestamp
# force change in feature preprocessing
prior_training_timestamp = current_training_timestamp
input_features = config[INPUT_FEATURES].copy()
input_features[0][PREPROCESSING] = {"lowercase": True}
config[INPUT_FEATURES] = input_features
model = LudwigModel(config, backend=backend)
model.train(dataset=source_dataset, output_directory=output_directory)
current_training_timestamp = os.path.getmtime(cache_fname)
# timestamps should be different
assert prior_training_timestamp < current_training_timestamp
# force change in features names (and properties)
prior_training_timestamp = current_training_timestamp
input_features = [category_feature(encoder={"vocab_size": 5}), category_feature()]
source_dataset = generate_data(input_features, output_features, source_dataset)
config[INPUT_FEATURES] = input_features
model = LudwigModel(config, backend=backend)
model.train(dataset=source_dataset, output_directory=output_directory)
current_training_timestamp = os.path.getmtime(cache_fname)
# timestamps should be different
assert prior_training_timestamp < current_training_timestamp
# force change in Ludwig version
prior_training_timestamp = current_training_timestamp
global_vars.LUDWIG_VERSION = "new_version"
model = LudwigModel(config, backend=backend)
model.train(dataset=source_dataset, output_directory=output_directory)
current_training_timestamp = os.path.getmtime(cache_fname)
# timestamps should be different
assert prior_training_timestamp < current_training_timestamp
@pytest.mark.parametrize("transformer_key", list(numeric_transformation_registry.keys()))
def test_numeric_transformer(transformer_key, tmpdir):
Transformer = get_from_registry(transformer_key, numeric_transformation_registry)
transformer_name = Transformer().__class__.__name__
if transformer_name == "Log1pTransformer":
raw_values = np.random.lognormal(5, 2, size=100)
else:
raw_values = np.random.normal(5, 2, size=100)
backend = LOCAL_BACKEND
parameters = Transformer.fit_transform_params(raw_values, backend)
if transformer_name in {"Log1pTransformer", "IdentityTransformer"}:
# should be empty
assert not bool(parameters)
else:
# should not be empty
assert bool(parameters)
# instantiate numeric transformer
numeric_transfomer = Transformer(**parameters)
# transform values
transformed_values = numeric_transfomer.transform(raw_values)
# inverse transform the prior transformed values
reconstructed_values = numeric_transfomer.inverse_transform(transformed_values)
# should now match
assert np.allclose(raw_values, reconstructed_values)
# now test numeric transformer with output feature
df = pd.DataFrame(np.array([raw_values, raw_values]).T, columns=["x", "y"])
config = {
"input_features": [{"name": "x", "type": "number"}],
"output_features": [{"name": "y", "type": "number", "preprocessing": {"normalization": transformer_key}}],
"combiner": {
"type": "concat",
},
TRAINER: {
"epochs": 2,
"batch_size": 16,
},
}
args = {
"config": config,
"skip_save_processed_input": True,
"output_directory": os.path.join(tmpdir, "results"),
"logging_level": logging.WARN,
}
# ensure no exceptions are raised
experiment_cli(dataset=df, **args)