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

1195 lines
41 KiB
Python

import contextlib
import copy
import importlib.util
import logging
import os
import random
import string
from unittest import mock
import numpy as np
import pandas as pd
import pytest
from PIL import Image
from transformers import AutoTokenizer
import ludwig
from ludwig.api import LudwigModel
from ludwig.backend import initialize_backend
from ludwig.callbacks import Callback
from ludwig.constants import (
BASE_MODEL,
BATCH_SIZE,
COLUMN,
DECODER,
EPOCHS,
FULL,
INPUT_FEATURES,
MODEL_ECD,
MODEL_LLM,
MODEL_TYPE,
NAME,
OUTPUT_FEATURES,
PREPROCESSING,
PROC_COLUMN,
PROMPT,
SPLIT,
TRAINER,
TYPE,
)
from ludwig.data.concatenate_datasets import concatenate_df
from ludwig.data.preprocessing import handle_features_with_prompt_config, preprocess_for_prediction
from ludwig.schema.llms.prompt import PromptConfig
from ludwig.schema.model_types.base import ModelConfig
from tests.integration_tests.utils import (
assert_preprocessed_dataset_shape_and_dtype_for_feature,
audio_feature,
binary_feature,
category_feature,
generate_data,
generate_data_as_dataframe,
image_feature,
LocalTestBackend,
number_feature,
sequence_feature,
text_feature,
)
NUM_EXAMPLES = 20
@pytest.mark.parametrize(
"backend",
[
pytest.param("local", id="local"),
pytest.param("ray", id="ray", marks=pytest.mark.distributed),
],
)
def test_sample_ratio(backend, tmpdir, ray_cluster_2cpu):
num_examples = 50
sample_ratio = 0.5
input_features = [sequence_feature(encoder={"reduce_output": "sum"}), audio_feature(folder=tmpdir)]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=num_examples
)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
EPOCHS: 2,
},
PREPROCESSING: {"sample_ratio": sample_ratio},
}
model = LudwigModel(config, backend=backend)
train_set, val_set, test_set, training_set_metadata = model.preprocess(
data_csv,
skip_save_processed_input=True,
)
sample_size = num_examples * sample_ratio
count = len(train_set) + len(val_set) + len(test_set)
assert sample_size == count
# Check that sample ratio is disabled when doing preprocessing for prediction
dataset, _ = preprocess_for_prediction(
model.config_obj.to_dict(),
dataset=data_csv,
training_set_metadata=training_set_metadata,
split=FULL,
include_outputs=True,
backend=model.backend,
)
assert "sample_ratio" in model.config_obj.preprocessing.to_dict()
assert len(dataset) == num_examples
@pytest.mark.parametrize(
"backend",
[
pytest.param("local", id="local"),
pytest.param("ray", id="ray", marks=pytest.mark.distributed),
],
)
def test_sample_ratio_deterministic(backend, tmpdir, ray_cluster_2cpu):
"""Ensures that the sampled dataset is the same when using a random seed.
model.preprocess returns a PandasPandasDataset object when using local backend, and returns a RayDataset object when
using the Ray backend.
"""
num_examples = 50
sample_ratio = 0.5
input_features = [binary_feature()]
output_features = [category_feature()]
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=num_examples
)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
PREPROCESSING: {"sample_ratio": sample_ratio},
}
model1 = LudwigModel(config, backend=backend)
train_set_1, val_set_1, test_set_1, _ = model1.preprocess(
data_csv,
skip_save_processed_input=True,
)
model2 = LudwigModel(config, backend=backend)
train_set_2, val_set_2, test_set_2, _ = model2.preprocess(
data_csv,
skip_save_processed_input=True,
)
sample_size = num_examples * sample_ratio
# Ensure sizes are the same
assert sample_size == len(train_set_1) + len(val_set_1) + len(test_set_1)
assert sample_size == len(train_set_2) + len(val_set_2) + len(test_set_2)
# Ensure actual rows are the same
if backend == "local":
assert train_set_1.to_df().equals(train_set_2.to_df())
assert val_set_1.to_df().equals(val_set_2.to_df())
assert test_set_1.to_df().equals(test_set_2.to_df())
else:
assert train_set_1.to_df().compute().equals(train_set_2.to_df().compute())
assert val_set_1.to_df().compute().equals(val_set_2.to_df().compute())
assert test_set_1.to_df().compute().equals(test_set_2.to_df().compute())
@pytest.mark.parametrize(
"backend",
[
pytest.param("local", id="local"),
pytest.param("ray", id="ray", marks=pytest.mark.distributed),
],
)
def test_sample_size(backend, tmpdir, ray_cluster_2cpu):
num_examples = 50
sample_size = 25
input_features = [sequence_feature(encoder={"reduce_output": "sum"}), audio_feature(folder=tmpdir)]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=num_examples
)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
EPOCHS: 2,
},
PREPROCESSING: {"sample_size": sample_size},
}
model = LudwigModel(config, backend=backend)
train_set, val_set, test_set, training_set_metadata = model.preprocess(
data_csv,
skip_save_processed_input=True,
)
count = len(train_set) + len(val_set) + len(test_set)
assert sample_size == count
# Check that sample size is disabled when doing preprocessing for prediction
dataset, _ = preprocess_for_prediction(
model.config_obj.to_dict(),
dataset=data_csv,
training_set_metadata=training_set_metadata,
split=FULL,
include_outputs=True,
backend=model.backend,
)
assert "sample_size" in model.config_obj.preprocessing.to_dict()
assert len(dataset) == num_examples
@pytest.mark.parametrize(
"backend",
[
pytest.param("local", id="local"),
pytest.param("ray", id="ray", marks=pytest.mark.distributed),
],
)
def test_sample_size_deterministic(backend, tmpdir, ray_cluster_2cpu):
"""Ensures that the sampled dataset is the same when using a random seed.
model.preprocess returns a PandasPandasDataset object when using local backend, and returns a RayDataset object when
using the Ray backend.
"""
num_examples = 50
sample_size = 25
input_features = [binary_feature()]
output_features = [category_feature()]
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=num_examples
)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
PREPROCESSING: {"sample_size": sample_size},
}
model1 = LudwigModel(config, backend=backend)
train_set_1, val_set_1, test_set_1, _ = model1.preprocess(
data_csv,
skip_save_processed_input=True,
)
model2 = LudwigModel(config, backend=backend)
train_set_2, val_set_2, test_set_2, _ = model2.preprocess(
data_csv,
skip_save_processed_input=True,
)
# Ensure sizes are the same
assert sample_size == len(train_set_1) + len(val_set_1) + len(test_set_1)
assert sample_size == len(train_set_2) + len(val_set_2) + len(test_set_2)
# Ensure actual rows are the same
if backend == "local":
assert train_set_1.to_df().equals(train_set_2.to_df())
assert val_set_1.to_df().equals(val_set_2.to_df())
assert test_set_1.to_df().equals(test_set_2.to_df())
else:
assert train_set_1.to_df().compute().equals(train_set_2.to_df().compute())
assert val_set_1.to_df().compute().equals(val_set_2.to_df().compute())
assert test_set_1.to_df().compute().equals(test_set_2.to_df().compute())
def test_strip_whitespace_category(csv_filename, tmpdir):
data_csv_path = os.path.join(tmpdir, csv_filename)
input_features = [binary_feature()]
cat_feat = category_feature(decoder={"vocab_size": 3})
output_features = [cat_feat]
backend = LocalTestBackend()
config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}
training_data_csv_path = generate_data(input_features, output_features, data_csv_path)
df = pd.read_csv(training_data_csv_path)
# prefix with whitespace
df[cat_feat[COLUMN]] = df[cat_feat[COLUMN]].apply(lambda s: " " + s)
# run preprocessing
ludwig_model = LudwigModel(config, backend=backend)
train_ds, _, _, metadata = ludwig_model.preprocess(dataset=df)
# expect values containing whitespaces to be properly mapped to vocab_size unique values
assert len(np.unique(train_ds.dataset[cat_feat[PROC_COLUMN]])) == cat_feat[DECODER]["vocab_size"]
@pytest.mark.parametrize(
"backend",
[
pytest.param("local", id="local"),
pytest.param("ray", id="ray", marks=pytest.mark.distributed),
],
)
def test_with_split(backend, csv_filename, tmpdir, ray_cluster_2cpu):
num_examples = NUM_EXAMPLES
train_set_size = int(num_examples * 0.8)
val_set_size = int(num_examples * 0.1)
test_set_size = int(num_examples * 0.1)
input_features = [sequence_feature(encoder={"reduce_output": "sum"})]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples
)
data_df = pd.read_csv(data_csv)
data_df[SPLIT] = [0] * train_set_size + [1] * val_set_size + [2] * test_set_size
data_df.to_csv(data_csv, index=False)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
EPOCHS: 2,
},
PREPROCESSING: {SPLIT: {TYPE: "fixed", COLUMN: SPLIT}},
}
model = LudwigModel(config, backend=backend)
train_set, val_set, test_set, _ = model.preprocess(
data_csv,
skip_save_processed_input=False,
)
assert len(train_set) == train_set_size
assert len(val_set) == val_set_size
assert len(test_set) == test_set_size
@pytest.mark.distributed
@pytest.mark.distributed_f
@pytest.mark.parametrize("feature_fn", [image_feature, audio_feature])
def test_dask_known_divisions(feature_fn, csv_filename, tmpdir, ray_cluster_2cpu):
import dask.dataframe as dd
input_features = [feature_fn(os.path.join(tmpdir, "generated_output"))]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=20)
data_df = dd.from_pandas(pd.read_csv(data_csv), npartitions=2)
assert data_df.known_divisions
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
EPOCHS: 2,
},
}
backend = "ray"
model = LudwigModel(config, backend=backend)
train_set, val_set, test_set, _ = model.preprocess(
data_df,
skip_save_processed_input=False,
)
@pytest.mark.distributed
@pytest.mark.distributed_f
def test_drop_empty_partitions(csv_filename, tmpdir, ray_cluster_2cpu):
import dask.dataframe as dd
input_features = [image_feature(os.path.join(tmpdir, "generated_output"))]
output_features = [category_feature(vocab_size=5, reduce_input="sum", output_feature=True)]
# num_examples and npartitions set such that each post-split DataFrame has >1 samples, but empty partitions.
data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=25)
data_df = dd.from_pandas(pd.read_csv(data_csv), npartitions=10)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
EPOCHS: 2,
},
}
backend = "ray"
model = LudwigModel(config, backend=backend)
train_set, val_set, test_set, _ = model.preprocess(
data_df,
skip_save_processed_input=True,
)
for dataset in [train_set, val_set, test_set]:
df = dataset.ds.to_dask()
for partition in df.partitions:
assert len(partition) > 0, "empty partitions found in dataset"
@pytest.mark.parametrize("generate_images_as_numpy", [False, True])
def test_read_image_from_path(tmpdir, csv_filename, generate_images_as_numpy):
input_features = [image_feature(os.path.join(tmpdir, "generated_output"), save_as_numpy=generate_images_as_numpy)]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES
)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {EPOCHS: 2},
}
model = LudwigModel(config)
model.preprocess(
data_csv,
skip_save_processed_input=False,
)
def test_read_image_from_numpy_array(tmpdir, csv_filename):
input_features = [image_feature(os.path.join(tmpdir, "generated_output"))]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},
}
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES
)
df = pd.read_csv(data_csv)
processed_df_rows = []
for _, row in df.iterrows():
processed_df_rows.append(
{
input_features[0][NAME]: np.array(Image.open(row[input_features[0][NAME]])),
output_features[0][NAME]: row[output_features[0][NAME]],
}
)
df_with_images_as_numpy_arrays = pd.DataFrame(processed_df_rows)
model = LudwigModel(config)
model.preprocess(
df_with_images_as_numpy_arrays,
skip_save_processed_input=False,
)
def test_read_image_failure_default_image(monkeypatch, tmpdir, csv_filename):
"""Tests that the default image used when an image cannot be read has the correct properties."""
def mock_read_binary_files(self, column, map_fn, file_size):
"""Mock read_binary_files to return None (failed image read) to test error handling."""
return column.map(lambda x: None)
monkeypatch.setattr(ludwig.backend.base.LocalDataProcessingMixin, "read_binary_files", mock_read_binary_files)
# mode="eager" forces the eager path so that the monkeypatched read_binary_files is exercised.
# With mode="lazy" (the default), read_binary_files is never called and this test has no meaning.
image_feature_config = image_feature(os.path.join(tmpdir, "generated_output"), preprocessing={"mode": "eager"})
input_features = [image_feature_config]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},
}
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES, nan_percent=0.2
)
model = LudwigModel(config)
preprocessed_dataset = model.preprocess(data_csv)
training_set_metadata = preprocessed_dataset.training_set_metadata
preprocessing = training_set_metadata[input_features[0][NAME]][PREPROCESSING]
expected_shape = (preprocessing["num_channels"], preprocessing["height"], preprocessing["width"])
expected_dtype = np.float32
assert_preprocessed_dataset_shape_and_dtype_for_feature(
image_feature_config[NAME], preprocessed_dataset, model.config_obj, expected_dtype, expected_shape
)
def test_number_feature_wrong_dtype(csv_filename, tmpdir):
"""Tests that a number feature with all string values is treated as having missing values by default."""
data_csv_path = os.path.join(tmpdir, csv_filename)
num_feat = number_feature()
input_features = [num_feat]
output_features = [binary_feature()]
config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}
training_data_csv_path = generate_data(input_features, output_features, data_csv_path)
df = pd.read_csv(training_data_csv_path)
# convert numbers to random strings
def random_string():
letters = string.ascii_lowercase
return "".join(random.choice(letters) for _ in range(10))
df[num_feat[COLUMN]] = df[num_feat[COLUMN]].apply(lambda _: random_string())
# run preprocessing
backend = LocalTestBackend()
ludwig_model = LudwigModel(config, backend=backend)
train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df)
concatenated_df = concatenate_df(train_ds.to_df(), val_ds.to_df(), test_ds.to_df(), backend)
# check that train_ds had invalid values replaced with the missing value
assert len(concatenated_df) == len(df)
assert np.all(concatenated_df[num_feat[PROC_COLUMN]] == 0.0)
@pytest.mark.parametrize(
"max_len, sequence_length, max_sequence_length, sequence_length_expected",
[
# Case 1: infer from the dataset, max_sequence_length is larger than the largest sequence length.
# Expected: max_sequence_length is ignored, and the sequence length is dataset+2 (include start/stop tokens).
(10, None, 15, 12),
# Case 2: infer from the dataset, max_sequence_length is smaller than the largest sequence length.
# Expected: max_sequence_length is used, and the sequence length is max_sequence_length.
(10, None, 8, 8),
# Case 3: infer from the dataset, max_sequence_length is not set.
# Expected: max_sequence_length is ignored, and the sequence length is dataset+2 (include start/stop tokens).
(10, None, None, 12),
# Case 4: set sequence_length explicitly and it is larger than the dataset.
# Expected: sequence_length is used, and the sequence length is sequence_length.
(10, 15, 20, 15),
# Case 5: set sequence_length explicitly and it is smaller than the dataset.
# Expected: sequence_length is used, and the sequence length is sequence_length.
(10, 8, 20, 8),
],
)
@pytest.mark.parametrize(
"feature_type",
[
sequence_feature,
],
)
def test_seq_features_max_sequence_length(
csv_filename, tmpdir, feature_type, max_len, sequence_length, max_sequence_length, sequence_length_expected
):
"""Tests that a sequence feature has the correct max_sequence_length in metadata and prepocessed data."""
feat = feature_type(
encoder={"max_len": max_len},
preprocessing={"sequence_length": sequence_length, "max_sequence_length": max_sequence_length},
)
input_features = [feat]
output_features = [binary_feature()]
config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}
data_csv_path = os.path.join(tmpdir, csv_filename)
training_data_csv_path = generate_data(input_features, output_features, data_csv_path)
df = pd.read_csv(training_data_csv_path)
class CheckTrainingSetMetadataCallback(Callback):
def on_preprocess_end(self, proc_training_set, proc_validation_set, proc_test_set, training_set_metadata):
assert training_set_metadata[feat[NAME]]["max_sequence_length"] == sequence_length_expected
backend = LocalTestBackend()
ludwig_model = LudwigModel(config, backend=backend, callbacks=[CheckTrainingSetMetadataCallback()])
train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df)
all_df = concatenate_df(train_ds.to_df(), val_ds.to_df(), test_ds.to_df(), backend)
proc_column_name = feat[PROC_COLUMN]
assert all(len(x) == sequence_length_expected for x in all_df[proc_column_name])
def test_column_feature_type_mismatch_fill():
"""Tests that we are able to fill missing values even in columns where the column dtype and desired feature
dtype do not match."""
cat_feat = category_feature()
bin_feat = binary_feature()
input_features = [cat_feat]
output_features = [bin_feat]
config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}
# Construct dataframe with int-like column representing a categorical feature
df = pd.DataFrame(
{
cat_feat[NAME]: pd.Series(pd.array([None] + [1] * 24, dtype=pd.Int64Dtype())),
bin_feat[NAME]: pd.Series([True] * 25),
}
)
# run preprocessing
backend = LocalTestBackend()
ludwig_model = LudwigModel(config, backend=backend)
train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df)
@pytest.mark.parametrize("format", ["file", "df"])
def test_presplit_override(format, tmpdir):
"""Tests that provising a pre-split file or dataframe overrides the user's split config."""
num_feat = number_feature(normalization=None)
input_features = [num_feat, sequence_feature(encoder={"reduce_output": "sum"})]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=25)
data_df = pd.read_csv(data_csv)
# Set the feature value equal to an ordinal index so we can ensure the splits are identical before and after
# preprocessing.
data_df[num_feat[COLUMN]] = data_df.index
train_df = data_df[:15]
val_df = data_df[15:20]
test_df = data_df[20:]
train_data = train_df
val_data = val_df
test_data = test_df
if format == "file":
train_data = os.path.join(tmpdir, "train.csv")
val_data = os.path.join(tmpdir, "val.csv")
test_data = os.path.join(tmpdir, "test.csv")
train_df.to_csv(train_data)
val_df.to_csv(val_data)
test_df.to_csv(test_data)
data_df.to_csv(data_csv, index=False)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {
EPOCHS: 2,
},
PREPROCESSING: {SPLIT: {TYPE: "random"}},
}
model = LudwigModel(config, backend=LocalTestBackend())
train_set, val_set, test_set, _ = model.preprocess(
training_set=train_data, validation_set=val_data, test_set=test_data
)
assert len(train_set) == len(train_df)
assert len(val_set) == len(val_df)
assert len(test_set) == len(test_df)
assert np.all(train_set.to_df()[num_feat[PROC_COLUMN]].values == train_df[num_feat[COLUMN]].values)
assert np.all(val_set.to_df()[num_feat[PROC_COLUMN]].values == val_df[num_feat[COLUMN]].values)
assert np.all(test_set.to_df()[num_feat[PROC_COLUMN]].values == test_df[num_feat[COLUMN]].values)
@pytest.mark.parametrize(
"backend",
[
pytest.param("local", id="local"),
pytest.param("ray", id="ray", marks=pytest.mark.distributed),
],
)
def test_empty_training_set_error(backend, tmpdir, ray_cluster_2cpu):
"""Tests that an error is raised if one or more of the splits is empty after preprocessing."""
data_csv_path = os.path.join(tmpdir, "data.csv")
out_feat = binary_feature()
input_features = [number_feature()]
output_features = [out_feat]
config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}
training_data_csv_path = generate_data(input_features, output_features, data_csv_path)
df = pd.read_csv(training_data_csv_path)
# Convert all the output features rows to null. Because the default missing value strategy is to drop empty output
# rows, this will result in the dataset being empty after preprocessing.
df[out_feat[COLUMN]] = None
ludwig_model = LudwigModel(config, backend=backend)
with pytest.raises(ValueError, match="Training data is empty following preprocessing"):
ludwig_model.preprocess(dataset=df)
@pytest.mark.distributed
@pytest.mark.distributed_f
@pytest.mark.parametrize(
"backend",
[
pytest.param("local", id="local"),
pytest.param("ray", id="ray", marks=pytest.mark.distributed),
],
)
def test_in_memory_dataset_size(backend, tmpdir, ray_cluster_2cpu):
data_csv_path = os.path.join(tmpdir, "data.csv")
out_feat = binary_feature()
input_features = [number_feature()]
output_features = [out_feat]
config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}
training_data_csv_path = generate_data(input_features, output_features, data_csv_path)
df = pd.read_csv(training_data_csv_path)
ludwig_model = LudwigModel(config, backend=backend)
training_dataset, validation_dataset, test_dataset, _ = ludwig_model.preprocess(dataset=df)
assert training_dataset.in_memory_size_bytes > 0
assert validation_dataset.in_memory_size_bytes > 0
assert test_dataset.in_memory_size_bytes > 0
@pytest.mark.parametrize(
"binary_as_input, expected_preprocessing, missing_value_strategy",
[
pytest.param(
True,
{
"missing_value_strategy": "fill_with_true",
"fill_value": None,
"computed_fill_value": ">50K",
"fallback_true_label": ">50K",
},
"fill_with_true",
id="binary_as_input_1",
),
pytest.param(
True,
{
"missing_value_strategy": "fill_with_false",
"fill_value": None,
"computed_fill_value": "<=50K",
"fallback_true_label": ">50K",
},
"fill_with_false",
id="binary_as_input_2",
),
pytest.param(
False,
{
"missing_value_strategy": "drop_row",
"fill_value": None,
"computed_fill_value": None,
"fallback_true_label": ">50K",
},
"drop_row",
id="binary_as_output",
),
],
)
def test_non_conventional_bool_with_fallback(binary_as_input, expected_preprocessing, missing_value_strategy, tmpdir):
# Specify a non-conventional boolean feature with a fallback true label.
bin_feature = binary_feature(
bool2str=["<=50K", ">50K"],
preprocessing={"fallback_true_label": ">50K", "missing_value_strategy": missing_value_strategy},
)
# Generate data with the non-conventional boolean feature.
if binary_as_input:
input_features = [bin_feature]
output_features = [number_feature()]
else:
input_features = [number_feature()]
output_features = [bin_feature]
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},
}
data_csv_path = os.path.join(tmpdir, "data.csv")
training_data_csv_path = generate_data(input_features, output_features, data_csv_path)
df = pd.read_csv(training_data_csv_path)
# Preprocess the data.
ludwig_model = LudwigModel(config)
_, _, _, training_set_metadata = ludwig_model.preprocess(dataset=df)
# Check that true/false labels are set correctly.
assert training_set_metadata[bin_feature[NAME]] == {
"str2bool": {"<=50K": False, ">50K": True},
"bool2str": ["<=50K", ">50K"],
"fallback_true_label": ">50K",
PREPROCESSING: expected_preprocessing,
}
@pytest.mark.parametrize(
"binary_as_input", [pytest.param(True, id="binary_as_input"), pytest.param(False, id="binary_as_output")]
)
def test_non_conventional_bool_without_fallback_logs_warning(binary_as_input, caplog, tmpdir):
# Specify a non-conventional boolean feature without a fallback true label.
bin_feature = binary_feature(bool2str=["<=50K", ">50K"], preprocessing={"fallback_true_label": None})
# Generate data with the non-conventional boolean feature.
if binary_as_input:
input_features = [bin_feature]
output_features = [number_feature()]
else:
input_features = [number_feature()]
output_features = [bin_feature]
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},
}
data_csv_path = os.path.join(tmpdir, "data.csv")
training_data_csv_path = generate_data(input_features, output_features, data_csv_path)
df = pd.read_csv(training_data_csv_path)
# Preprocess the data.
with caplog.at_level(logging.WARN, logger="ludwig.features.binary_feature"):
ludwig_model = LudwigModel(config)
ludwig_model.preprocess(dataset=df)
# Check that a warning is logged.
assert "unconventional boolean value" in caplog.text
@pytest.mark.parametrize("feature_type", ["input_feature", "output_feature"], ids=["input_feature", "output_feature"])
def test_category_feature_vocab_size_1(feature_type, tmpdir) -> None:
data_csv_path = os.path.join(tmpdir, "data.csv")
input_feature = [category_feature(encoder={"vocab_size": 1})]
output_feature = [binary_feature()]
if feature_type == "output_feature":
input_feature = output_feature
output_feature = [category_feature(decoder={"vocab_size": 1})]
config = {INPUT_FEATURES: input_feature, OUTPUT_FEATURES: output_feature, "training": {EPOCHS: 1}}
training_data_csv_path = generate_data(config[INPUT_FEATURES], config[OUTPUT_FEATURES], data_csv_path)
ludwig_model = LudwigModel(config)
with pytest.raises(Exception) if feature_type == "output_feature" else contextlib.nullcontext():
ludwig_model.train(dataset=training_data_csv_path)
@pytest.mark.parametrize("use_pretrained", [False, True], ids=["false", "true"])
def test_vit_encoder_different_dimension_image(tmpdir, csv_filename, use_pretrained: bool):
input_features = [
image_feature(
os.path.join(tmpdir, "generated_output"),
preprocessing={"in_memory": True, "height": 224, "width": 206, "num_channels": 3},
encoder={TYPE: "vit", "model_variant": "b_16", "use_pretrained": use_pretrained},
)
]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES
)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {"train_steps": 1},
}
model = LudwigModel(config)
# Failure happens post preprocessing but before training during the ECD model creation phase
# so make sure the model can be created properly and training can proceed
model.train(dataset=data_csv)
@pytest.mark.skip(
reason=(
"Broken against torch nightly: "
"https://github.com/ludwig-ai/ludwig/actions/runs/4918126111/jobs/8784071603?pr=3388."
)
)
def test_image_encoder_torchvision_different_num_channels(tmpdir, csv_filename):
input_features = [
image_feature(
os.path.join(tmpdir, "generated_output"),
preprocessing={"in_memory": True, "height": 224, "width": 206, "num_channels": 1},
encoder={TYPE: "efficientnet"},
)
]
output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")]
data_csv = generate_data(
input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES
)
config = {
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
TRAINER: {"train_steps": 1},
}
model = LudwigModel(config)
# Failure happens post preprocessing but before training during the ECD model creation phase
# so make sure the model can be created properly and training can proceed
model.train(dataset=data_csv)
@pytest.mark.parametrize(
"df_engine",
[
pytest.param("pandas", id="pandas"),
pytest.param("dask", id="dask", marks=pytest.mark.distributed),
],
)
def test_fill_with_mode_different_df_engine(tmpdir, csv_filename, df_engine, ray_cluster_2cpu):
config = {
INPUT_FEATURES: [category_feature(preprocessing={"missing_value_strategy": "fill_with_mode"})],
OUTPUT_FEATURES: [binary_feature()],
}
training_data_csv_path = generate_data(
config[INPUT_FEATURES], config[OUTPUT_FEATURES], os.path.join(tmpdir, csv_filename)
)
df = pd.read_csv(training_data_csv_path)
if df_engine == "dask":
import dask.dataframe as dd
df = dd.from_pandas(df, npartitions=1)
# Only support Dask on Ray backend
config["backend"] = {TYPE: "ray"}
ludwig_model = LudwigModel(config)
ludwig_model.preprocess(dataset=df)
template_task_sample = """
Instruction: {__task__}
###
Examples:
###
Input: foo bar
Output: true
###
Input: baz quc
Output: false
###
Input: {__sample__}
Output:
"""
task = "predict the output feature. Return only values in {true, false}"
template_multi_col = """
You are a helpful chatbot. USER: {__sample__}: {country}, {year:.2f} ASSISTANT:
"""
expected_task_sample = """Instruction: predict the output feature. Return only values in {true, false}
###
Examples:
###
Input: foo bar
Output: true
###
Input: baz quc
Output: false
###
Input:"""
@pytest.mark.llm
@pytest.mark.parametrize("backend", ["local", "ray"])
@pytest.mark.parametrize("model_type", [MODEL_ECD, MODEL_LLM])
@pytest.mark.parametrize(
"input_features,expected",
[
(
[
text_feature(
preprocessing={
PROMPT: {"task": task, "template": template_task_sample},
"max_sequence_length": 512,
}
)
],
expected_task_sample,
),
(
[
text_feature(preprocessing={PROMPT: {"template": template_multi_col}}),
category_feature(name="country"),
number_feature(name="year"),
],
("You are a helpful chatbot. USER: "),
),
],
ids=["task_sample", "multi_col"],
)
def test_prompt_template(input_features, expected, model_type, backend, tmpdir, ray_cluster_2cpu):
"""Tests that prompt template is correctly applied to inputs."""
input_features = copy.deepcopy(input_features)
output_features = [category_feature()]
data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=25)
data_df = pd.read_csv(data_csv)
raw_values = [data_df[input_features[i][COLUMN]].values.tolist() for i in range(len(input_features))]
# Only use the first input feature (text) and discard the others, which are only used for data gen
input_features = input_features[:1]
config = {
MODEL_TYPE: model_type,
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
}
model_name = "hf-internal-testing/tiny-random-OPTModel"
if model_type == MODEL_LLM:
# For LLMs, specify the prompt at the top level
config[BASE_MODEL] = model_name
config[PROMPT] = input_features[0][PREPROCESSING][PROMPT]
del config[INPUT_FEATURES][0][PREPROCESSING][PROMPT]
config[INPUT_FEATURES][0]["encoder"] = {TYPE: "passthrough"}
else:
config[INPUT_FEATURES][0]["encoder"] = {
TYPE: "auto_transformer",
"pretrained_model_name_or_path": model_name,
}
model = LudwigModel(config, backend=backend)
train_set, _, _, _ = model.preprocess(
training_set=data_csv,
skip_save_processed_input=True,
output_directory=os.path.join(tmpdir, "processed"),
)
train_df = model.backend.df_engine.compute(train_set.to_df())
encoded_values = train_df[input_features[0][PROC_COLUMN]].values.tolist()
assert all(len(v) == len(encoded_values) for v in raw_values)
for i, encoded in enumerate(encoded_values):
tokenizer = AutoTokenizer.from_pretrained(model_name)
decoded = tokenizer.decode(encoded)
assert expected in decoded, f"decoded: '{decoded}' does not contain expected: {expected}"
for raw_col_values in raw_values:
v = raw_col_values[i]
if isinstance(v, float):
# Test formatting in parametrize uses 2 decimal places of precision
raw_text = format(v, ".2f")
else:
raw_text = str(v)
assert raw_text in decoded, f"'{raw_text}' not in '{decoded}'"
@pytest.mark.llm
@pytest.mark.parametrize("backend", ["local", "ray"])
@pytest.mark.parametrize(
"retrieval_kwargs",
[
pytest.param({"type": "random", "k": 2}, id="random_retrieval"),
pytest.param(
{"type": "semantic", "model_name": "paraphrase-MiniLM-L3-v2", "k": 2},
id="semantic_retrieval",
marks=pytest.mark.skipif(
not importlib.util.find_spec("sentence_transformers"),
reason="sentence_transformers not installed",
),
),
],
)
def test_handle_features_with_few_shot_prompt_config(backend, retrieval_kwargs, ray_cluster_2cpu):
prompt_config = PromptConfig.from_dict(
{
"task": (
"Given the sample input, complete this sentence by replacing XXXX: "
"The label is XXXX. Choose one value in this list: [1, 2, 3]."
),
"retrieval": retrieval_kwargs,
}
).to_dict() # convert back-and-forth to validate and add defaults
input_features = [
text_feature(
encoder={TYPE: "passthrough"},
)
]
output_features = [
category_feature(
output_feature=True,
decoder={TYPE: "category_extractor"},
)
]
input_feature_name = input_features[0][NAME]
output_feature_name = output_features[0][NAME]
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: "gpt2",
INPUT_FEATURES: input_features,
OUTPUT_FEATURES: output_features,
PROMPT: prompt_config,
}
config = ModelConfig.from_dict(config).to_dict()
df = generate_data_as_dataframe(input_features, output_features, 10, with_split=True) # retrieval needs fixed split
if backend == "ray":
import dask.dataframe as dd
df = dd.from_pandas(df, npartitions=2)
split_col = df[SPLIT]
feature_configs = config[INPUT_FEATURES] + config[OUTPUT_FEATURES]
if backend == "local":
context = mock.patch(
"ludwig.models.retrieval.SemanticRetrieval._encode",
side_effect=lambda row_strs, _: np.random.rand(len(row_strs), 16).astype(np.float32),
)
else:
# TODO: figure out how to get mocks to work with Ray backend
context = contextlib.nullcontext()
with context:
backend = initialize_backend(backend)
dataset_cols = handle_features_with_prompt_config(
config,
df,
feature_configs,
backend=backend,
split_col=split_col,
)
assert len(dataset_cols) == 1
assert input_feature_name in dataset_cols
# Inspect the generated prompts
col = backend.df_engine.compute(dataset_cols[input_feature_name])
for prompt in col:
# input_feature_name and output_feature_name should be in the prompt because
# labeled samples are provided by the context
assert input_feature_name in prompt
assert output_feature_name in prompt
@pytest.mark.llm
@pytest.mark.parametrize("backend", ["local", "ray"])
def test_handle_features_with_prompt_config_multi_col(backend, ray_cluster_2cpu):
df = pd.DataFrame(
[
{
"instruction": "Name this province",
"country": "Canada",
"year": 1871,
"answer": "British Columbia",
},
{
"instruction": "Name this city",
"country": "France",
"year": 1789,
"answer": "Paris",
},
{
"instruction": "Name this country",
"country": "UK",
"year": 1057,
"answer": "Wales",
},
]
)
config = {
MODEL_TYPE: MODEL_LLM,
BASE_MODEL: "gpt2",
INPUT_FEATURES: [text_feature(name="question", encoder={TYPE: "passthrough"})],
OUTPUT_FEATURES: [text_feature(name="answer")],
PROMPT: {
"template": "You are a helpful chatbot. USER: {instruction}: {country}, {year:.2f} ASSISTANT:",
},
}
config = ModelConfig.from_dict(config).to_dict()
if backend == "ray":
import dask.dataframe as dd
df = dd.from_pandas(df, npartitions=2)
feature_configs = config[INPUT_FEATURES] + config[OUTPUT_FEATURES]
backend = initialize_backend(backend)
dataset_cols = handle_features_with_prompt_config(
config,
df,
feature_configs,
backend=backend,
split_col=None,
)
assert len(dataset_cols) == 1
assert "question" in dataset_cols
col = backend.df_engine.compute(dataset_cols["question"])
assert len(col) == 3
assert col[0].startswith("You are a helpful chatbot. USER: Name this province: Canada, 1871.00 ASSISTANT:")
assert col[1].startswith("You are a helpful chatbot. USER: Name this city: France, 1789.00 ASSISTANT:")
assert col[2].startswith("You are a helpful chatbot. USER: Name this country: UK, 1057.00 ASSISTANT:")