219 lines
7.0 KiB
Python
219 lines
7.0 KiB
Python
import math
|
|
from unittest import mock
|
|
|
|
import lightgbm as lgbm
|
|
import pandas as pd
|
|
import pytest
|
|
from sklearn.datasets import load_breast_cancer
|
|
from sklearn.model_selection import train_test_split
|
|
|
|
import ray
|
|
from ray import tune
|
|
from ray.train import ScalingConfig
|
|
from ray.train.constants import TRAIN_DATASET_KEY
|
|
from ray.train.lightgbm import LightGBMTrainer, RayTrainReportCallback
|
|
|
|
|
|
@pytest.fixture
|
|
def ray_start_6_cpus():
|
|
address_info = ray.init(num_cpus=6)
|
|
yield address_info
|
|
# The code after the yield will run as teardown code.
|
|
ray.shutdown()
|
|
|
|
|
|
@pytest.fixture
|
|
def ray_start_8_cpus():
|
|
address_info = ray.init(num_cpus=8)
|
|
yield address_info
|
|
# The code after the yield will run as teardown code.
|
|
ray.shutdown()
|
|
|
|
|
|
scale_config = ScalingConfig(num_workers=2)
|
|
|
|
data_raw = load_breast_cancer()
|
|
dataset_df = pd.DataFrame(data_raw["data"], columns=data_raw["feature_names"])
|
|
dataset_df["target"] = data_raw["target"]
|
|
train_df, test_df = train_test_split(dataset_df, test_size=0.3)
|
|
|
|
params = {
|
|
"objective": "binary",
|
|
"metric": ["binary_logloss", "binary_error"],
|
|
}
|
|
|
|
|
|
def get_num_trees(booster: lgbm.Booster) -> int:
|
|
return booster.current_iteration()
|
|
|
|
|
|
def test_fit_with_categoricals(ray_start_6_cpus):
|
|
train_df_with_cat = train_df.copy()
|
|
test_df_with_cat = test_df.copy()
|
|
train_df_with_cat["categorical_column"] = pd.Series(
|
|
(["A", "B"] * math.ceil(len(train_df_with_cat) / 2))[: len(train_df_with_cat)]
|
|
).astype("category")
|
|
test_df_with_cat["categorical_column"] = pd.Series(
|
|
(["A", "B"] * math.ceil(len(test_df_with_cat) / 2))[: len(test_df_with_cat)]
|
|
).astype("category")
|
|
|
|
train_dataset = ray.data.from_pandas(train_df_with_cat)
|
|
valid_dataset = ray.data.from_pandas(test_df_with_cat)
|
|
trainer = LightGBMTrainer(
|
|
scaling_config=scale_config,
|
|
label_column="target",
|
|
params=params,
|
|
datasets={TRAIN_DATASET_KEY: train_dataset, "valid": valid_dataset},
|
|
)
|
|
result = trainer.fit()
|
|
checkpoint = result.checkpoint
|
|
model = LightGBMTrainer.get_model(checkpoint)
|
|
assert model.pandas_categorical == [["A", "B"]]
|
|
|
|
|
|
def test_resume_from_checkpoint(ray_start_6_cpus, tmpdir):
|
|
train_dataset = ray.data.from_pandas(train_df)
|
|
valid_dataset = ray.data.from_pandas(test_df)
|
|
trainer = LightGBMTrainer(
|
|
scaling_config=scale_config,
|
|
label_column="target",
|
|
params=params,
|
|
num_boost_round=5,
|
|
datasets={TRAIN_DATASET_KEY: train_dataset, "valid": valid_dataset},
|
|
)
|
|
result = trainer.fit()
|
|
model = LightGBMTrainer.get_model(result.checkpoint)
|
|
assert get_num_trees(model) == 5
|
|
|
|
trainer = LightGBMTrainer(
|
|
scaling_config=scale_config,
|
|
label_column="target",
|
|
params=params,
|
|
num_boost_round=10,
|
|
datasets={TRAIN_DATASET_KEY: train_dataset, "valid": valid_dataset},
|
|
resume_from_checkpoint=result.checkpoint,
|
|
)
|
|
result = trainer.fit()
|
|
checkpoint = result.checkpoint
|
|
model = LightGBMTrainer.get_model(checkpoint)
|
|
assert get_num_trees(model) == 10
|
|
|
|
|
|
def test_fit_with_arrow_backed_pandas_dtypes(ray_start_6_cpus):
|
|
# `from_items` produces Arrow-backed blocks, so `to_pandas()` inside the
|
|
# trainer returns Arrow-backed dtypes — the regression path this test guards.
|
|
train_dataset = ray.data.from_items(train_df.to_dict("records"))
|
|
valid_dataset = ray.data.from_items(test_df.to_dict("records"))
|
|
|
|
trainer = LightGBMTrainer(
|
|
scaling_config=scale_config,
|
|
label_column="target",
|
|
params=params,
|
|
datasets={TRAIN_DATASET_KEY: train_dataset, "valid": valid_dataset},
|
|
)
|
|
result = trainer.fit()
|
|
model = LightGBMTrainer.get_model(result.checkpoint)
|
|
assert get_num_trees(model) == 10
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"freq_end_expected",
|
|
[
|
|
# With num_boost_round=25 with 0 indexing, the checkpoints will be at:
|
|
(4, True, 7), # 3, 7, 11, 15, 19, 23, 24 (end)
|
|
(4, False, 6), # 3, 7, 11, 15, 19, 23
|
|
(5, True, 5), # 4, 9, 14, 19, 24
|
|
(0, True, 1), # 24 (end)
|
|
(0, False, 0),
|
|
],
|
|
)
|
|
def test_checkpoint_freq(ray_start_6_cpus, freq_end_expected):
|
|
freq, end, expected = freq_end_expected
|
|
|
|
train_dataset = ray.data.from_pandas(train_df)
|
|
valid_dataset = ray.data.from_pandas(test_df)
|
|
trainer = LightGBMTrainer(
|
|
run_config=ray.train.RunConfig(
|
|
checkpoint_config=ray.train.CheckpointConfig(
|
|
checkpoint_frequency=freq, checkpoint_at_end=end
|
|
)
|
|
),
|
|
scaling_config=scale_config,
|
|
label_column="target",
|
|
params=params,
|
|
num_boost_round=25,
|
|
datasets={TRAIN_DATASET_KEY: train_dataset, "valid": valid_dataset},
|
|
)
|
|
result = trainer.fit()
|
|
|
|
# Assert number of checkpoints
|
|
assert len(result.best_checkpoints) == expected, str(
|
|
[(metrics["training_iteration"], cp) for cp, metrics in result.best_checkpoints]
|
|
)
|
|
|
|
# Assert checkpoint numbers are increasing
|
|
cp_paths = [cp.path for cp, _ in result.best_checkpoints]
|
|
assert cp_paths == sorted(cp_paths), str(cp_paths)
|
|
|
|
|
|
def test_tune(ray_start_8_cpus):
|
|
train_dataset = ray.data.from_pandas(train_df)
|
|
valid_dataset = ray.data.from_pandas(test_df)
|
|
trainer = LightGBMTrainer(
|
|
scaling_config=ScalingConfig(num_workers=2, resources_per_worker={"CPU": 1}),
|
|
label_column="target",
|
|
params={**params, "max_depth": 1},
|
|
datasets={TRAIN_DATASET_KEY: train_dataset, "valid": valid_dataset},
|
|
)
|
|
|
|
tuner = tune.Tuner(
|
|
trainer,
|
|
param_space={"params": {"max_depth": tune.grid_search([2, 4])}},
|
|
)
|
|
results = tuner.fit()
|
|
assert sorted([r.config["params"]["max_depth"] for r in results]) == [2, 4]
|
|
|
|
|
|
def test_validation(ray_start_6_cpus):
|
|
valid_dataset = ray.data.from_pandas(test_df)
|
|
with pytest.raises(ValueError, match=TRAIN_DATASET_KEY):
|
|
LightGBMTrainer(
|
|
scaling_config=ScalingConfig(num_workers=2),
|
|
label_column="target",
|
|
params=params,
|
|
datasets={"valid": valid_dataset},
|
|
)
|
|
with pytest.raises(ValueError, match="label_column"):
|
|
LightGBMTrainer(
|
|
scaling_config=ScalingConfig(num_workers=2),
|
|
datasets={"train": valid_dataset},
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("rank", [None, 0, 1])
|
|
def test_checkpoint_only_on_rank0(rank):
|
|
"""Tests that the callback only reports checkpoints on rank 0,
|
|
or if the rank is not available (Tune usage)."""
|
|
callback = RayTrainReportCallback(frequency=2, checkpoint_at_end=True)
|
|
|
|
booster = mock.MagicMock()
|
|
|
|
with mock.patch("ray.train.get_context") as mock_get_context:
|
|
mock_context = mock.MagicMock()
|
|
mock_context.get_world_rank.return_value = rank
|
|
mock_get_context.return_value = mock_context
|
|
|
|
with callback._get_checkpoint(booster) as checkpoint:
|
|
if rank in (0, None):
|
|
assert checkpoint
|
|
else:
|
|
assert not checkpoint
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.exit(pytest.main(["-v", "-x", __file__]))
|