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

1173 lines
44 KiB
Python

# Copyright (c) 2023 Predibase, Inc., 2019 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 contextlib
import logging
import os
import shutil
import uuid
from collections import namedtuple
import pandas as pd
import pytest
import torchvision
import yaml
from ludwig.api import LudwigModel
from ludwig.backend import LOCAL_BACKEND
from ludwig.callbacks import Callback
from ludwig.constants import BATCH_SIZE, COLUMN, ENCODER, H3, NAME, PREPROCESSING, TRAINER, TYPE
from ludwig.data.concatenate_datasets import concatenate_df
from ludwig.data.dataset_synthesizer import build_synthetic_dataset_df
from ludwig.encoders.registry import get_encoder_classes
from ludwig.error import ConfigValidationError
from ludwig.experiment import experiment_cli
from ludwig.globals import MODEL_FILE_NAME
from ludwig.predict import predict_cli
from ludwig.utils.data_utils import read_csv
from ludwig.utils.defaults import default_random_seed
from tests.integration_tests.utils import (
audio_feature,
bag_feature,
binary_feature,
category_distribution_feature,
category_feature,
create_data_set_to_use,
date_feature,
ENCODERS,
generate_data,
generate_output_features_with_dependencies,
generate_output_features_with_dependencies_complex,
h3_feature,
image_feature,
LocalTestBackend,
number_feature,
run_experiment,
sequence_feature,
set_feature,
text_feature,
timeseries_feature,
vector_feature,
)
pytestmark = pytest.mark.integration_tests_h
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logging.getLogger("ludwig").setLevel(logging.INFO)
@pytest.mark.parametrize("encoder", ["embed", "rnn", "transformer", "tf_idf"])
def test_experiment_text_feature_non_pretrained(encoder, csv_filename):
input_features = [
text_feature(encoder={"vocab_size": 30, "min_len": 1, "type": encoder}, preprocessing={"tokenizer": "space"})
]
output_features = [category_feature(decoder={"vocab_size": 2})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
def run_experiment_with_encoder(encoder, csv_filename):
# Run in a subprocess to clear TF and prevent OOM
# This also allows us to use GPU resources
input_features = [text_feature(encoder={"vocab_size": 30, "min_len": 1, "type": encoder})]
output_features = [category_feature(decoder={"vocab_size": 2})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize("encoder", ["embed", "rnn", "transformer"])
def test_experiment_seq_seq_generator(csv_filename, encoder):
input_features = [text_feature(encoder={"type": encoder, "reduce_output": None})]
output_features = [text_feature(decoder={"type": "generator"}, output_feature=True)]
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize("encoder", ["embed", "rnn", "transformer"])
def test_experiment_seq_seq_tagger(csv_filename, encoder):
input_features = [text_feature(encoder={"type": encoder, "reduce_output": None})]
output_features = [text_feature(decoder={"type": "tagger"}, reduce_input=None)]
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize("encoder", ["cnnrnn", "stacked_cnn"])
def test_experiment_seq_seq_tagger_fails_for_non_length_preserving_encoders(csv_filename, encoder):
input_features = [text_feature(encoder={"type": encoder, "reduce_output": None})]
output_features = [text_feature(decoder={"type": "tagger"}, reduce_input=None)]
rel_path = generate_data(input_features, output_features, csv_filename)
with pytest.raises(ValueError):
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_seq_seq_model_def_file(csv_filename, yaml_filename):
# seq-to-seq test to use config file instead of dictionary
input_features = [text_feature(encoder={"reduce_output": None, "type": "embed"})]
output_features = [text_feature(decoder={"vocab_size": 3, "type": "tagger"}, reduce_input=None)]
# Save the config to a yaml file
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 14},
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
}
with open(yaml_filename, "w") as yaml_out:
yaml.safe_dump(config, yaml_out)
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(None, None, dataset=rel_path, config=yaml_filename)
def test_experiment_seq_seq_train_test_valid(tmpdir):
# seq-to-seq test to use train, test, validation files
input_features = [text_feature(encoder={"reduce_output": None, "type": "rnn"})]
output_features = [text_feature(decoder={"vocab_size": 3, "type": "tagger"}, reduce_input=None)]
train_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "train.csv"))
test_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "test.csv"), 20)
valdation_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "val.csv"), 20)
run_experiment(
input_features, output_features, training_set=train_csv, test_set=test_csv, validation_set=valdation_csv
)
# Save intermediate output
run_experiment(
input_features, output_features, training_set=train_csv, test_set=test_csv, validation_set=valdation_csv
)
@pytest.mark.parametrize("encoder", ["embed", "rnn", "transformer"])
def test_experiment_multi_input_intent_classification(csv_filename, encoder):
# Multiple inputs, Single category output
input_features = [
text_feature(encoder={"vocab_size": 10, "min_len": 1, "representation": "sparse"}),
category_feature(encoder={"vocab_size": 10}),
]
output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
input_features[0][ENCODER][TYPE] = encoder
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_with_torch_module_dict_feature_name(csv_filename):
input_features = [category_feature(name="type")]
output_features = [category_feature(name="to", output_feature=True)]
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_multiclass_with_class_weights(csv_filename):
# Multiple inputs, Single category output
input_features = [category_feature(encoder={"vocab_size": 10})]
output_features = [category_feature(decoder={"vocab_size": 3}, loss={"class_weights": [0, 1, 2]})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_multilabel_with_class_weights(csv_filename):
# Multiple inputs, Single category output
input_features = [category_feature(encoder={"vocab_size": 10})]
output_features = [set_feature(decoder={"vocab_size": 3}, loss={"class_weights": [0, 1, 2, 3]})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize(
"output_features",
[
# baseline test case
[
category_feature(decoder={"reduce_input": "sum", "vocab_size": 2}),
sequence_feature(decoder={"vocab_size": 10, "max_len": 5}),
number_feature(),
],
# use generator as decoder
[
category_feature(decoder={"vocab_size": 2, "reduce_input": "sum"}),
sequence_feature(decoder={"vocab_size": 10, "max_len": 5, "type": "generator"}),
number_feature(),
],
# Generator decoder and reduce_input = None
[
category_feature(decoder={"vocab_size": 2, "reduce_input": "sum"}),
sequence_feature(decoder={"max_len": 5, "type": "generator"}, reduce_input=None),
number_feature(normalization="minmax"),
],
# output features with dependencies single dependency
generate_output_features_with_dependencies("number_feature", ["category_feature"]),
# output features with dependencies multiple dependencies
generate_output_features_with_dependencies("number_feature", ["category_feature", "sequence_feature"]),
# output features with dependencies multiple dependencies
generate_output_features_with_dependencies("sequence_feature", ["category_feature", "number_feature"]),
# output features with dependencies
generate_output_features_with_dependencies("category_feature", ["sequence_feature"]),
generate_output_features_with_dependencies_complex(),
],
)
def test_experiment_multiple_seq_seq(csv_filename, output_features):
input_features = [
text_feature(encoder={"vocab_size": 100, "min_len": 1, "type": "stacked_cnn"}),
number_feature(normalization="zscore"),
category_feature(encoder={"vocab_size": 10, "embedding_size": 5}),
set_feature(),
sequence_feature(encoder={"vocab_size": 10, "max_len": 10, "type": "embed"}),
]
output_features = output_features
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize(
"num_channels,image_source,in_memory,skip_save_processed_input",
[
(3, "file", True, True),
(1, "file", False, False),
(3, "tensor", True, False),
],
ids=["file_in_memory_3ch", "file_on_disk_1ch", "tensor_in_memory_3ch"],
)
def test_basic_image_feature(num_channels, image_source, in_memory, skip_save_processed_input, tmpdir):
# Image Inputs
image_dest_folder = os.path.join(tmpdir, "generated_images")
input_features = [
image_feature(
folder=image_dest_folder,
preprocessing={
"in_memory": in_memory,
"height": 12,
"width": 12,
"num_channels": num_channels,
"num_processes": 5,
},
encoder={
"type": "stacked_cnn",
"output_size": 16,
"num_filters": 8,
},
)
]
output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})]
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
if image_source == "file":
# use images from file
run_experiment(
input_features, output_features, dataset=rel_path, skip_save_processed_input=skip_save_processed_input
)
else:
# import image from file and store in dataframe as tensors.
df = pd.read_csv(rel_path)
image_feature_name = input_features[0]["name"]
df[image_feature_name] = df[image_feature_name].apply(lambda x: torchvision.io.read_image(x))
run_experiment(input_features, output_features, dataset=df, skip_save_processed_input=skip_save_processed_input)
def test_experiment_infer_image_metadata(tmpdir):
# Image Inputs
image_dest_folder = os.path.join(tmpdir, "generated_images")
# Resnet encoder
input_features = [
image_feature(folder=image_dest_folder, encoder={"type": "stacked_cnn", "output_size": 16, "num_filters": 8}),
text_feature(encoder={"type": "embed", "min_len": 1}),
number_feature(normalization="zscore"),
]
output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2}), number_feature()]
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
# remove image preprocessing section to force inferring image meta data
input_features[0].pop("preprocessing")
run_experiment(input_features, output_features, dataset=rel_path)
ImageParams = namedtuple("ImageTestParams", "image_encoder in_memory_flag skip_save_processed_input")
@pytest.mark.parametrize(
"image_params",
[
ImageParams("stacked_cnn", True, True),
ImageParams("stacked_cnn", False, False),
],
)
def test_experiment_image_inputs(image_params: ImageParams, tmpdir):
# Image Inputs
image_dest_folder = os.path.join(tmpdir, "generated_images")
# Resnet encoder
input_features = [
image_feature(
folder=image_dest_folder,
preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5},
encoder={"type": "resnet", "output_size": 16, "num_filters": 8},
),
text_feature(encoder={"type": "embed", "min_len": 1}),
number_feature(normalization="zscore"),
]
output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2}), number_feature()]
input_features[0]["encoder"]["type"] = image_params.image_encoder
input_features[0]["preprocessing"]["in_memory"] = image_params.in_memory_flag
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
run_experiment(
input_features,
output_features,
dataset=rel_path,
skip_save_processed_input=image_params.skip_save_processed_input,
)
# Primary focus of this test is to determine if exceptions are raised for different data set formats and in_memory
# setting.
@pytest.mark.parametrize(
"train_format,train_in_memory,test_format,test_in_memory",
[
("csv", True, "csv", True),
("df", False, "df", False),
("csv", False, "df", True),
],
ids=["csv_inmem", "df_ondisk", "csv_to_df_mixed"],
)
def test_experiment_image_dataset(train_format, train_in_memory, test_format, test_in_memory, tmpdir):
# Image Inputs
image_dest_folder = os.path.join(tmpdir, "generated_images")
input_features = [
image_feature(
folder=image_dest_folder,
preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5},
encoder={"type": "stacked_cnn", "output_size": 16, "num_filters": 8},
),
]
output_features = [
category_feature(decoder={"reduce_input": "sum", "vocab_size": 2}),
]
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 14},
"preprocessing": {},
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
}
# create temporary name for train and test data sets
train_csv_filename = os.path.join(tmpdir, "train_" + uuid.uuid4().hex[:10].upper() + ".csv")
test_csv_filename = os.path.join(tmpdir, "test_" + uuid.uuid4().hex[:10].upper() + ".csv")
# setup training data format to test
train_data = generate_data(input_features, output_features, train_csv_filename)
config["input_features"][0]["preprocessing"]["in_memory"] = train_in_memory
training_set_metadata = None
# define Ludwig model
backend = LocalTestBackend()
model = LudwigModel(
config=config,
backend=backend,
)
train_dataset_to_use = create_data_set_to_use(train_format, train_data)
model.train(dataset=train_dataset_to_use, training_set_metadata=training_set_metadata)
model.config_obj.input_features.to_list()[0]["preprocessing"]["in_memory"] = test_in_memory
# setup test data format to test
test_data = generate_data(input_features, output_features, test_csv_filename)
test_dataset_to_use = create_data_set_to_use(test_format, test_data)
# run functions with the specified data format
model.evaluate(dataset=test_dataset_to_use)
model.predict(dataset=test_dataset_to_use)
DATA_FORMATS_TO_TEST = [
"csv",
"df",
"dict",
"excel",
"feather",
"fwf",
"html",
"json",
"jsonl",
"parquet",
"pickle",
"stata",
"tsv",
]
@pytest.mark.parametrize("data_format", DATA_FORMATS_TO_TEST)
def test_experiment_dataset_formats(data_format, csv_filename):
# primary focus of this test is to determine if exceptions are
# raised for different data set formats and in_memory setting
input_features = [number_feature(), category_feature()]
output_features = [category_feature(output_feature=True), number_feature()]
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 14},
"preprocessing": {},
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
}
# setup training data format to test
raw_data = generate_data(input_features, output_features, csv_filename)
# define Ludwig model
model = LudwigModel(config=config)
dataset_to_use = create_data_set_to_use(data_format, raw_data)
# pickle auto-dispatch by extension is disabled (CWE-502); must opt in explicitly.
explicit_format = data_format if data_format == "pickle" else None
model.train(dataset=dataset_to_use, data_format=explicit_format, random_seed=default_random_seed)
# # run functions with the specified data format
model.evaluate(dataset=dataset_to_use, data_format=explicit_format)
model.predict(dataset=dataset_to_use, data_format=explicit_format)
def test_experiment_audio_inputs(tmpdir):
# Audio Inputs
audio_dest_folder = os.path.join(tmpdir, "generated_audio")
input_features = [audio_feature(folder=audio_dest_folder)]
output_features = [binary_feature()]
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_tied_weights(csv_filename):
# Single sequence input, single category output
input_features = [
text_feature(name="text_feature1", encoder={"min_len": 1, "type": "cnnrnn", "reduce_output": "sum"}),
text_feature(
name="text_feature2", encoder={"min_len": 1, "type": "cnnrnn", "reduce_output": "sum"}, tied="text_feature1"
),
]
output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
for encoder in ENCODERS:
input_features[0][ENCODER][TYPE] = encoder
input_features[1][ENCODER][TYPE] = encoder
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_tied_weights_sequence_combiner(csv_filename):
"""Tests that tied weights work with sequence combiners if `sequence_length` is provided.
Addresses https://github.com/ludwig-ai/ludwig/issues/3220
"""
input_features = [
text_feature(
name="feature1",
encoder={
"max_len": 5,
"reduce_output": None,
},
preprocessing={"sequence_length": 10},
),
text_feature(
name="feature2",
encoder={
"max_len": 3,
"reduce_output": None,
},
preprocessing={"sequence_length": 10},
tied="feature1",
),
]
output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})]
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "sequence"},
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
}
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(config=config, dataset=rel_path)
@pytest.mark.parametrize(
"enc_cell_type,attention",
[("lstm", True), ("rnn", False), ("gru", True)],
ids=["lstm_attn", "rnn_no_attn", "gru_attn"],
)
def test_sequence_tagger(enc_cell_type, attention, csv_filename):
# Define input and output features
input_features = [
sequence_feature(encoder={"max_len": 10, "type": "rnn", "cell_type": enc_cell_type, "reduce_output": None})
]
output_features = [
sequence_feature(decoder={"max_len": 10, "type": "tagger", "attention": attention}, reduce_input=None)
]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
# run the experiment
run_experiment(input_features, output_features, dataset=rel_path)
def test_sequence_tagger_text(csv_filename):
# Define input and output features
input_features = [text_feature(encoder={"max_len": 10, "type": "rnn", "reduce_output": None})]
output_features = [
sequence_feature(
decoder={"max_len": 10, "type": "tagger"},
reduce_input=None,
)
]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
# run the experiment
run_experiment(input_features, output_features, dataset=rel_path)
"""
@pytest.mark.distributed
@pytest.mark.distributed_d
def test_sequence_tagger_text_ray(csv_filename, ray_cluster_2cpu):
# Define input and output features
input_features = [text_feature(encoder={"max_len": 10, "type": "rnn", "reduce_output": None})]
output_features = [
sequence_feature(
decoder={"max_len": 10, "type": "tagger"},
reduce_input=None,
)
]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
# run the experiment
run_experiment(input_features, output_features, dataset=rel_path, backend="ray")
"""
def test_experiment_sequence_combiner_with_reduction_fails(csv_filename):
config = {
"input_features": [
sequence_feature(
name="seq1",
encoder={
"min_len": 5,
"max_len": 5,
"type": "embed",
"cell_type": "lstm",
"reduce_output": "sum",
},
),
sequence_feature(
name="seq2",
encoder={
"min_len": 5,
"max_len": 5,
"type": "embed",
"cell_type": "lstm",
"reduce_output": "sum",
},
),
category_feature(encoder={"vocab_size": 5}),
],
"output_features": [category_feature(decoder={"reduce_input": "sum", "vocab_size": 5})],
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
"combiner": {
"type": "sequence",
"encoder": {"type": "rnn"},
"main_sequence_feature": "seq1",
"reduce_output": None,
},
}
# Generate test data
rel_path = generate_data(config["input_features"], config["output_features"], csv_filename)
# Encoding sequence features with 'embed' should fail with SequenceConcatCombiner, since at least one sequence
# feature should be rank 3.
with pytest.raises(TypeError):
run_experiment(config=config, dataset=rel_path)
@pytest.mark.parametrize("sequence_encoder", ["rnn", "transformer"])
def test_experiment_sequence_combiner(sequence_encoder, csv_filename):
config = {
"input_features": [
sequence_feature(
name="seq1",
encoder={
"min_len": 5,
"max_len": 5,
"type": sequence_encoder,
"cell_type": "lstm",
"reduce_output": None,
},
),
sequence_feature(
name="seq2",
encoder={
"min_len": 5,
"max_len": 5,
"type": sequence_encoder,
"cell_type": "lstm",
"reduce_output": None,
},
),
category_feature(vocab_size=5),
],
"output_features": [category_feature(decoder={"reduce_input": "sum", "vocab_size": 5})],
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
"combiner": {
"type": "sequence",
"encoder": {"type": "rnn"},
"main_sequence_feature": "seq1",
"reduce_output": None,
},
}
# Generate test data
rel_path = generate_data(config["input_features"], config["output_features"], csv_filename)
run_experiment(config=config, dataset=rel_path)
def test_experiment_model_resume(tmpdir):
# Single sequence input, single category output
# Tests saving a model file, loading it to rerun training and predict
input_features = [sequence_feature(encoder={"type": "rnn", "reduce_output": "sum"})]
output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})]
# Generate test data
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 14},
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
}
_, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=tmpdir)
experiment_cli(config, dataset=rel_path, model_resume_path=output_dir)
predict_cli(os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path)
shutil.rmtree(output_dir, ignore_errors=True)
@pytest.mark.slow
@pytest.mark.parametrize(
"dist_strategy",
[
pytest.param("accelerate", id="accelerate", marks=pytest.mark.distributed),
],
)
def test_experiment_model_resume_distributed(tmpdir, dist_strategy, ray_cluster_4cpu):
_run_experiment_model_resume_distributed(tmpdir, dist_strategy)
def _run_experiment_model_resume_distributed(tmpdir, dist_strategy):
# Single sequence input, single category output
# Tests saving a model file, loading it to rerun training and predict
input_features = [number_feature()]
output_features = [category_feature(output_feature=True)]
# Generate test data
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 8},
TRAINER: {"epochs": 1, BATCH_SIZE: 128},
"backend": {"type": "ray", "trainer": {"strategy": dist_strategy, "num_workers": 2}},
}
_, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=os.path.join(tmpdir, "results1"))
experiment_cli(
config, dataset=rel_path, model_resume_path=output_dir, output_directory=os.path.join(tmpdir, "results2")
)
predict_cli(
os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path, output_directory=os.path.join(tmpdir, "results3")
)
@pytest.mark.parametrize(
"missing_file",
["training_progress.json", "training_checkpoints"],
ids=["training_progress", "training_checkpoints"],
)
def test_experiment_model_resume_missing_file(tmpdir, missing_file):
# Single sequence input, single category output
# Tests saving a model file, loading it to rerun training and predict
input_features = [sequence_feature(encoder={"type": "rnn", "reduce_output": "sum"})]
output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})]
# Generate test data
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 14},
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
}
_, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=tmpdir)
try:
# Remove file to simulate failure during first epoch of training which prevents
# training_checkpoints to be empty and training_progress.json to not be created
missing_file_path = os.path.join(output_dir, MODEL_FILE_NAME, missing_file)
if missing_file == "training_progress.json":
os.remove(missing_file_path)
else:
shutil.rmtree(missing_file_path)
finally:
# Training should start a fresh model training run without any errors
experiment_cli(config, dataset=rel_path, model_resume_path=output_dir)
predict_cli(os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path)
shutil.rmtree(output_dir, ignore_errors=True)
@pytest.mark.slow
@pytest.mark.distributed
@pytest.mark.distributed_d
def test_experiment_model_resume_before_1st_epoch_distributed(tmpdir, ray_cluster_4cpu):
# Single sequence input, single category output
# Tests saving a model file, loading it to rerun training and predict
input_features = [number_feature()]
output_features = [category_feature(output_feature=True)]
# Generate test data
training_set = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 8},
TRAINER: {"train_steps": 1, BATCH_SIZE: 128},
"backend": {"type": "ray", "trainer": {"strategy": "accelerate", "num_workers": 2}},
}
class InducedFailureCallback(Callback):
"""Class that defines the methods necessary to hook into process."""
def on_resume_training(self, is_coordinator):
if is_coordinator:
raise RuntimeError("Induced failure")
class NoFailureCallback(Callback):
"""Class that defines the methods necessary to hook into process."""
def on_resume_training(self, is_coordinator):
pass
try:
# Define Ludwig model object that drive model training
model = LudwigModel(config=config, logging_level=logging.INFO, callbacks=[InducedFailureCallback()])
model.train(
dataset=training_set,
experiment_name="simple_experiment",
model_name="simple_model_incomplete",
skip_save_processed_input=True,
output_directory=os.path.join(tmpdir, "results1"),
)
except Exception:
model = LudwigModel(config=config, logging_level=logging.INFO, callbacks=[NoFailureCallback()])
model.train(
dataset=training_set,
skip_save_processed_input=True,
model_resume_path=os.path.join(tmpdir, "results1"),
)
@pytest.mark.slow
@pytest.mark.distributed
@pytest.mark.distributed_d
def test_tabnet_with_batch_size_1(tmpdir, ray_cluster_4cpu):
input_features = [number_feature()]
output_features = [category_feature(output_feature=True)]
training_set = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "tabnet"},
TRAINER: {"train_steps": 1, BATCH_SIZE: 1},
"backend": {"type": "ray", "trainer": {"strategy": "accelerate", "num_workers": 2}},
}
model = LudwigModel(config=config, logging_level=logging.INFO)
model.train(
dataset=training_set,
skip_save_training_description=True,
skip_save_training_statistics=True,
skip_save_model=True,
skip_save_progress=True,
skip_save_log=True,
skip_save_processed_input=True,
)
def test_experiment_various_feature_types(csv_filename):
input_features = [binary_feature(), bag_feature()]
output_features = [set_feature(decoder={"max_len": 3, "vocab_size": 5})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_timeseries(csv_filename):
input_features = [timeseries_feature()]
output_features = [binary_feature()]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
input_features[0][ENCODER][TYPE] = "transformer"
run_experiment(input_features, output_features, dataset=rel_path)
def test_visual_question_answering(tmpdir):
image_dest_folder = os.path.join(tmpdir, "generated_images")
input_features = [
image_feature(
folder=image_dest_folder,
preprocessing={"in_memory": True, "height": 32, "width": 32, "num_channels": 3, "num_processes": 5},
encoder={
"type": "stacked_cnn",
},
),
text_feature(encoder={"type": "embed", "min_len": 1}),
]
output_features = [sequence_feature(decoder={"type": "generator", "cell_type": "lstm"})]
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"))
run_experiment(input_features, output_features, dataset=rel_path)
def test_image_resizing_num_channel_handling(tmpdir):
"""This test creates two image datasets with 3 channels and 1 channel. The combination of this data is used to
train a model. This checks the cases where the user may or may not specify a number of channels in the config.
:param csv_filename:
:return:
"""
# Image Inputs
image_dest_folder = os.path.join(tmpdir, "generated_images")
# Resnet encoder
input_features = [
image_feature(
folder=image_dest_folder,
preprocessing={"in_memory": True, "height": 32, "width": 32, "num_channels": 3, "num_processes": 5},
encoder={
"type": "stacked_cnn",
},
),
text_feature(encoder={"type": "embed", "min_len": 1}),
number_feature(normalization="minmax"),
]
output_features = [binary_feature(), number_feature()]
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset1.csv"), num_examples=20)
df1 = read_csv(rel_path)
input_features[0]["preprocessing"]["num_channels"] = 1
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset2.csv"), num_examples=20)
df2 = read_csv(rel_path)
df = concatenate_df(df1, df2, None, LOCAL_BACKEND)
df.to_csv(rel_path, index=False)
# Here the user specifies number of channels. Exception shouldn't be thrown
run_experiment(input_features, output_features, dataset=rel_path)
del input_features[0]["preprocessing"]["num_channels"]
# User doesn't specify num channels, but num channels is inferred. Exception shouldn't be thrown
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize("encoder", ["wave", "embed"])
def test_experiment_date(encoder, csv_filename):
input_features = [date_feature()]
output_features = [category_feature(decoder={"vocab_size": 2})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
input_features[0][ENCODER] = {TYPE: encoder}
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize("encoder", get_encoder_classes(H3).keys())
def test_experiment_h3(encoder, csv_filename):
input_features = [h3_feature()]
output_features = [binary_feature()]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
input_features[0][ENCODER] = {TYPE: encoder}
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_vector_feature(csv_filename):
input_features = [vector_feature()]
output_features = [binary_feature()]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_vector_feature_infer_size(csv_filename):
input_features = [vector_feature()]
output_features = [vector_feature()]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
# Unset vector_size so it needs to be inferred
del input_features[0][PREPROCESSING]
del output_features[0][PREPROCESSING]
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize("encoder", ["parallel_cnn", "dense", "passthrough"])
def test_forecasting_row_major(csv_filename, encoder):
input_features = [timeseries_feature(encoder={"type": encoder})]
output_features = [timeseries_feature(decoder={"type": "projector"})]
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 14, "flatten_inputs": True},
TRAINER: {"epochs": 2, BATCH_SIZE: 128},
}
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, config=config, dataset=rel_path)
def test_forecasting_column_major(csv_filename):
input_feature = timeseries_feature(preprocessing={"window_size": 3})
input_features = [input_feature]
# Ensure output feature has the same column and the input feature
output_feature = timeseries_feature(
name=input_feature[COLUMN], preprocessing={"horizon": 2}, decoder={"type": "projector"}
)
output_feature[NAME] = f"{input_feature[NAME]}_out"
output_features = [output_feature]
# Generate test data in column-major format. This is just a dataframe of numbers with the same column name
# as expected by the timeseries input feature
column_major_feature = number_feature(name=input_feature[COLUMN])
csv_filename = generate_data([column_major_feature], [], csv_filename)
input_df = pd.read_csv(csv_filename)
model, eval_stats, train_stats, preprocessed_data, output_directory = run_experiment(
input_features, output_features, dataset=csv_filename
)
train_set, val_set, test_set, _ = preprocessed_data
print(input_df)
# print(train_set.to_df())
horizon_df = model.forecast(input_df, horizon=5)
print(horizon_df)
@pytest.mark.parametrize("reduce_output", [("sum"), (None)], ids=["sum", "none"])
def test_experiment_text_output_feature_with_tagger_decoder(csv_filename, reduce_output):
"""Test that the tagger decoder works with text output features when reduce_output is set to None."""
input_features = [text_feature(encoder={"type": "parallel_cnn", "reduce_output": reduce_output})]
output_features = [text_feature(output_feature=True, decoder={"type": "tagger"})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
with pytest.raises(ConfigValidationError) if reduce_output == "sum" else contextlib.nullcontext():
run_experiment(input_features, output_features, dataset=rel_path)
@pytest.mark.parametrize("reduce_output", [("sum"), (None)], ids=["sum", "none"])
def test_experiment_sequence_output_feature_with_tagger_decoder(csv_filename, reduce_output):
"""Test that the tagger decoder works with sequence output features when reduce_output is set to None."""
input_features = [text_feature(encoder={"type": "parallel_cnn", "reduce_output": reduce_output})]
output_features = [sequence_feature(output_feature=True, decoder={"type": "tagger"})]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
with pytest.raises(ConfigValidationError) if reduce_output == "sum" else contextlib.nullcontext():
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_category_input_feature_with_tagger_decoder(csv_filename):
"""Test that the tagger decoder doesn't work with category input features."""
input_features = [category_feature()]
output_features = [sequence_feature(output_feature=True, decoder={"type": "tagger"})]
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 14, "reduce_output": None},
}
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
with pytest.raises(ConfigValidationError):
run_experiment(config=config, dataset=rel_path)
def test_experiment_category_distribution_feature(csv_filename):
vocab = ["a", "b", "c"]
input_features = [vector_feature()]
output_features = [
category_distribution_feature(
preprocessing={
"vocab": vocab,
}
)
]
# Generate test data
rel_path = generate_data(input_features, output_features, csv_filename)
input_df = pd.read_csv(rel_path)
# set batch_size=auto to ensure we produce the correct shaped synthetic data
config = {
"input_features": input_features,
"output_features": output_features,
"combiner": {"type": "concat", "output_size": 14},
TRAINER: {"epochs": 2, BATCH_SIZE: "auto"},
}
model, _, _, _, _ = run_experiment(input_features, output_features, dataset=rel_path, config=config)
preds, _ = model.predict(input_df)
# Check that predictions are category values drawn from the vocab, not distributions
assert all(v in vocab for v in preds[f"{output_features[0][NAME]}_predictions"].values)
def test_experiment_ordinal_category(csv_filename):
input_features = [category_feature(num_classes=5), number_feature()]
output_features = [category_feature(output_feature=True, loss={"type": "corn"})]
rel_path = generate_data(input_features, output_features, csv_filename)
run_experiment(input_features, output_features, dataset=rel_path)
def test_experiment_feature_names_with_non_word_chars(tmpdir):
config = yaml.safe_load("""
input_features:
- name: Pclass (new)
type: category
- name: review.text
type: category
- name: other_feature
type: category
tied: review.text
output_features:
- name: Survived (new)
type: binary
- name: Thrived
type: binary
dependencies:
- Survived (new)
combiner:
type: comparator
entity_1:
- Pclass (new)
- other_feature
entity_2:
- review.text
""")
df = build_synthetic_dataset_df(120, config)
model = LudwigModel(config, logging_level=logging.INFO)
model.train(dataset=df, output_directory=tmpdir)
def test_text_output_feature_cols(tmpdir, csv_filename):
"""Test ensures that there are 4 output columns when model.predict() is called for text output features."""
input_features = [text_feature(encoder={"type": "parallel_cnn"})]
output_features = [text_feature(output_feature=True)]
# Generate test data
rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename))
config = {
"input_features": input_features,
"output_features": output_features,
"trainer": {"train_steps": 2, "batch_size": 5},
}
model = LudwigModel(config, logging_level=logging.INFO)
model.train(dataset=rel_path, output_directory=tmpdir)
predict_output = model.predict(dataset=rel_path)[0]
assert len(predict_output.columns) == 4
predict_df_headers = {col_name.split("_")[2] for col_name in list(predict_output.columns)}
assert predict_df_headers == {"predictions", "probability", "probabilities", "response"}