Files
2026-07-13 13:27:18 +08:00

4911 lines
194 KiB
Python

# coding: utf-8
import copy
import itertools
import json
import math
import pickle
import platform
import random
import re
from pathlib import Path
from shutil import copyfile
import numpy as np
import psutil
import pytest
from scipy.sparse import csr_matrix, isspmatrix_csc, isspmatrix_csr
from sklearn.datasets import load_svmlight_file, make_blobs, make_classification, make_multilabel_classification
from sklearn.metrics import (
average_precision_score,
log_loss,
mean_absolute_error,
mean_squared_error,
r2_score,
roc_auc_score,
)
from sklearn.model_selection import GroupKFold, TimeSeriesSplit, train_test_split
import lightgbm as lgb
from .utils import (
SERIALIZERS,
BuildInfo,
assert_all_trees_valid,
assert_silent,
dummy_obj,
load_breast_cancer,
load_digits,
load_iris,
logistic_sigmoid,
make_synthetic_regression,
mse_obj,
np_assert_array_equal,
pickle_and_unpickle_object,
sklearn_multiclass_custom_objective,
softmax,
)
decreasing_generator = itertools.count(0, -1)
def logloss_obj(preds, train_data):
y_true = train_data.get_label()
y_pred = logistic_sigmoid(preds)
grad = y_pred - y_true
hess = y_pred * (1.0 - y_pred)
return grad, hess
def multi_logloss(y_true, y_pred):
return np.mean([-math.log(y_pred[i][y]) for i, y in enumerate(y_true)])
def top_k_error(y_true, y_pred, k):
if k == y_pred.shape[1]:
return 0
max_rest = np.max(-np.partition(-y_pred, k)[:, k:], axis=1)
return 1 - np.mean((y_pred[np.arange(len(y_true)), y_true] > max_rest))
def constant_metric(preds, train_data):
return ("error", 0.0, False)
def constant_metric_multi(preds, train_data):
return [
("important_metric", 1.5, False),
("irrelevant_metric", 7.8, False),
]
def decreasing_metric(preds, train_data):
return ("decreasing_metric", next(decreasing_generator), False)
def categorize(continuous_x):
return np.digitize(continuous_x, bins=np.arange(0, 1, 0.01))
def test_binary():
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"objective": "binary",
"metric": "binary_logloss",
"verbose": -1,
"num_iteration": 50, # test num_iteration in dict here
}
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=20, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
ret = log_loss(y_test, gbm.predict(X_test))
assert ret < 0.14
assert len(evals_result["valid_0"]["binary_logloss"]) == 50
assert evals_result["valid_0"]["binary_logloss"][-1] == pytest.approx(ret)
def test_rf():
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"boosting_type": "rf",
"objective": "binary",
"bagging_freq": 1,
"bagging_fraction": 0.5,
"feature_fraction": 0.5,
"num_leaves": 50,
"metric": "binary_logloss",
"verbose": -1,
}
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=50, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
ret = log_loss(y_test, gbm.predict(X_test))
assert ret < 0.19
assert evals_result["valid_0"]["binary_logloss"][-1] == pytest.approx(ret)
@pytest.mark.parametrize("objective", ["regression", "regression_l1", "huber", "fair", "poisson", "quantile"])
def test_regression(objective):
X, y = make_synthetic_regression()
y = np.abs(y)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {"objective": objective, "metric": "l2", "verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=50, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
ret = mean_squared_error(y_test, gbm.predict(X_test))
if objective == "huber":
assert ret < 430
elif objective == "fair":
assert ret < 296
elif objective == "poisson":
assert ret < 193
elif objective == "quantile":
assert ret < 1311
else:
assert ret < 343
assert evals_result["valid_0"]["l2"][-1] == pytest.approx(ret)
@pytest.mark.skipif(
BuildInfo.has_cuda, reason="CUDA version has a different implementation of WeightedPercentileFun, not tested here"
)
@pytest.mark.parametrize("objective", ["regression_l1", "quantile", "mape"])
def test_weighted_percentile_inside_label_range(objective):
# Regression test for https://github.com/lightgbm-org/LightGBM/issues/7151
# WeightedPercentileFun used the wrong
# CDF segment for linear interpolation and could return values outside
# [min(y), max(y)]. The pre-fix implementation produced a "weighted
# median" of 1.0 for y=[2,3,4,5], w=[4,3,2,1], far below min(y)=2.
#
# The correct weighted median for that example is 2 + 1/3 = 2.333..., and
# any weighted percentile with non-negative weights must lie in the label
# range. We train a model that cannot learn any structure from X (a single
# constant feature) so BoostFromScore dominates the prediction.
X = np.zeros((4, 1))
y = np.array([2.0, 3.0, 4.0, 5.0])
w = np.array([4.0, 3.0, 2.0, 1.0])
params = {
"objective": objective,
"verbose": -1,
"num_leaves": 2,
"min_data_in_leaf": 1,
"min_sum_hessian_in_leaf": 0.0,
"learning_rate": 1.0,
"feature_fraction": 1.0,
"bagging_fraction": 1.0,
}
if objective == "quantile":
params["alpha"] = 0.5
train = lgb.Dataset(X, label=y, weight=w)
gbm = lgb.train(params, train, num_boost_round=1)
preds = gbm.predict(X)
assert np.all(preds >= y.min() - 1e-6), f"{objective}: prediction {preds.min()} fell below min(y)={y.min()}"
assert np.all(preds <= y.max() + 1e-6), f"{objective}: prediction {preds.max()} rose above max(y)={y.max()}"
# For regression_l1 the single-leaf prediction is exactly the weighted
# median of y; verify it matches the expected 2 + 1/3.
if objective == "regression_l1":
np.testing.assert_allclose(preds, 2.0 + 1.0 / 3.0, rtol=1e-6)
def test_missing_value_handle():
X_train = np.zeros((100, 1))
y_train = np.zeros(100)
trues = random.sample(range(100), 20)
for idx in trues:
X_train[idx, 0] = np.nan
y_train[idx] = 1
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_train, y_train)
params = {"metric": "l2", "verbose": -1, "boost_from_average": False}
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=20, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
ret = mean_squared_error(y_train, gbm.predict(X_train))
assert ret < 0.005
assert evals_result["valid_0"]["l2"][-1] == pytest.approx(ret)
def test_missing_value_handle_more_na():
X_train = np.ones((100, 1))
y_train = np.ones(100)
trues = random.sample(range(100), 80)
for idx in trues:
X_train[idx, 0] = np.nan
y_train[idx] = 0
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_train, y_train)
params = {"metric": "l2", "verbose": -1, "boost_from_average": False}
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=20, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
ret = mean_squared_error(y_train, gbm.predict(X_train))
assert ret < 0.005
assert evals_result["valid_0"]["l2"][-1] == pytest.approx(ret)
def test_missing_value_handle_na():
x = [0, 1, 2, 3, 4, 5, 6, 7, np.nan]
y = [1, 1, 1, 1, 0, 0, 0, 0, 1]
X_train = np.array(x).reshape(len(x), 1)
y_train = np.array(y)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_train, y_train)
params = {
"objective": "regression",
"metric": "auc",
"verbose": -1,
"boost_from_average": False,
"min_data": 1,
"num_leaves": 2,
"learning_rate": 1,
"min_data_in_bin": 1,
"zero_as_missing": False,
}
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=1, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
pred = gbm.predict(X_train)
np.testing.assert_allclose(pred, y)
ret = roc_auc_score(y_train, pred)
assert ret > 0.999
assert evals_result["valid_0"]["auc"][-1] == pytest.approx(ret)
def test_missing_value_handle_zero():
x = [0, 1, 2, 3, 4, 5, 6, 7, np.nan]
y = [0, 1, 1, 1, 0, 0, 0, 0, 0]
X_train = np.array(x).reshape(len(x), 1)
y_train = np.array(y)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_train, y_train)
params = {
"objective": "regression",
"metric": "auc",
"verbose": -1,
"boost_from_average": False,
"min_data": 1,
"num_leaves": 2,
"learning_rate": 1,
"min_data_in_bin": 1,
"zero_as_missing": True,
}
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=1, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
pred = gbm.predict(X_train)
np.testing.assert_allclose(pred, y)
ret = roc_auc_score(y_train, pred)
assert ret > 0.999
assert evals_result["valid_0"]["auc"][-1] == pytest.approx(ret)
def test_missing_value_handle_none():
x = [0, 1, 2, 3, 4, 5, 6, 7, np.nan]
y = [0, 1, 1, 1, 0, 0, 0, 0, 0]
X_train = np.array(x).reshape(len(x), 1)
y_train = np.array(y)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_train, y_train)
params = {
"objective": "regression",
"metric": "auc",
"verbose": -1,
"boost_from_average": False,
"min_data": 1,
"num_leaves": 2,
"learning_rate": 1,
"min_data_in_bin": 1,
"use_missing": False,
}
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=1, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
pred = gbm.predict(X_train)
assert pred[0] == pytest.approx(pred[1])
assert pred[-1] == pytest.approx(pred[0])
ret = roc_auc_score(y_train, pred)
assert ret > 0.83
assert evals_result["valid_0"]["auc"][-1] == pytest.approx(ret)
@pytest.mark.parametrize(
"use_quantized_grad",
[
pytest.param(
True,
marks=pytest.mark.skipif(
BuildInfo.has_cuda,
reason="Skip because quantized training with categorical features is not supported for cuda version",
),
),
False,
],
)
def test_categorical_handle(use_quantized_grad):
x = [0, 1, 2, 3, 4, 5, 6, 7]
y = [0, 1, 0, 1, 0, 1, 0, 1]
X_train = np.array(x).reshape(len(x), 1)
y_train = np.array(y)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_train, y_train)
params = {
"objective": "regression",
"metric": "auc",
"verbose": -1,
"boost_from_average": False,
"min_data": 1,
"num_leaves": 2,
"learning_rate": 1,
"min_data_in_bin": 1,
"min_data_per_group": 1,
"cat_smooth": 1,
"cat_l2": 0,
"max_cat_to_onehot": 1,
"zero_as_missing": True,
"categorical_column": 0,
"use_quantized_grad": use_quantized_grad,
}
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=1, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
pred = gbm.predict(X_train)
np.testing.assert_allclose(pred, y)
ret = roc_auc_score(y_train, pred)
assert ret > 0.999
assert evals_result["valid_0"]["auc"][-1] == pytest.approx(ret)
@pytest.mark.parametrize(
"use_quantized_grad",
[
pytest.param(
True,
marks=pytest.mark.skipif(
BuildInfo.has_cuda,
reason="Skip because quantized training with categorical features is not supported for cuda version",
),
),
False,
],
)
def test_categorical_handle_na(use_quantized_grad):
x = [0, np.nan, 0, np.nan, 0, np.nan]
y = [0, 1, 0, 1, 0, 1]
X_train = np.array(x).reshape(len(x), 1)
y_train = np.array(y)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_train, y_train)
params = {
"objective": "regression",
"metric": "auc",
"verbose": -1,
"boost_from_average": False,
"min_data": 1,
"num_leaves": 2,
"learning_rate": 1,
"min_data_in_bin": 1,
"min_data_per_group": 1,
"cat_smooth": 1,
"cat_l2": 0,
"max_cat_to_onehot": 1,
"zero_as_missing": False,
"categorical_column": 0,
"use_quantized_grad": use_quantized_grad,
}
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=1, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
pred = gbm.predict(X_train)
np.testing.assert_allclose(pred, y)
ret = roc_auc_score(y_train, pred)
assert ret > 0.999
assert evals_result["valid_0"]["auc"][-1] == pytest.approx(ret)
@pytest.mark.parametrize(
"use_quantized_grad",
[
pytest.param(
True,
marks=pytest.mark.skipif(
BuildInfo.has_cuda,
reason="Skip because quantized training with categorical features is not supported for cuda version",
),
),
False,
],
)
def test_categorical_non_zero_inputs(use_quantized_grad):
x = [1, 1, 1, 1, 1, 1, 2, 2]
y = [1, 1, 1, 1, 1, 1, 0, 0]
X_train = np.array(x).reshape(len(x), 1)
y_train = np.array(y)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_train, y_train)
params = {
"objective": "regression",
"metric": "auc",
"verbose": -1,
"boost_from_average": False,
"min_data": 1,
"num_leaves": 2,
"learning_rate": 1,
"min_data_in_bin": 1,
"min_data_per_group": 1,
"cat_smooth": 1,
"cat_l2": 0,
"max_cat_to_onehot": 1,
"zero_as_missing": False,
"categorical_column": 0,
"use_quantized_grad": use_quantized_grad,
}
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=1, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
pred = gbm.predict(X_train)
np.testing.assert_allclose(pred, y)
ret = roc_auc_score(y_train, pred)
assert ret > 0.999
assert evals_result["valid_0"]["auc"][-1] == pytest.approx(ret)
def test_multiclass():
X, y = load_digits(n_class=10, return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {"objective": "multiclass", "metric": "multi_logloss", "num_class": 10, "verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train, params=params)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train, params=params)
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=50, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
ret = multi_logloss(y_test, gbm.predict(X_test))
assert ret < 0.16
assert evals_result["valid_0"]["multi_logloss"][-1] == pytest.approx(ret)
def test_multiclass_rf():
X, y = load_digits(n_class=10, return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"boosting_type": "rf",
"objective": "multiclass",
"metric": "multi_logloss",
"bagging_freq": 1,
"bagging_fraction": 0.6,
"feature_fraction": 0.6,
"num_class": 10,
"num_leaves": 50,
"min_data": 1,
"verbose": -1,
"gpu_use_dp": True,
}
lgb_train = lgb.Dataset(X_train, y_train, params=params)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train, params=params)
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=50, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
ret = multi_logloss(y_test, gbm.predict(X_test))
assert ret < 0.23
assert evals_result["valid_0"]["multi_logloss"][-1] == pytest.approx(ret)
def test_multiclass_prediction_early_stopping():
X, y = load_digits(n_class=10, return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {"objective": "multiclass", "metric": "multi_logloss", "num_class": 10, "verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train, params=params)
gbm = lgb.train(params, lgb_train, num_boost_round=50)
pred_parameter = {"pred_early_stop": True, "pred_early_stop_freq": 5, "pred_early_stop_margin": 1.5}
ret = multi_logloss(y_test, gbm.predict(X_test, **pred_parameter))
assert ret < 0.8
assert ret > 0.6 # loss will be higher than when evaluating the full model
pred_parameter["pred_early_stop_margin"] = 5.5
ret = multi_logloss(y_test, gbm.predict(X_test, **pred_parameter))
assert ret < 0.2
def test_multi_class_error():
X, y = load_digits(n_class=10, return_X_y=True)
params = {"objective": "multiclass", "num_classes": 10, "metric": "multi_error", "num_leaves": 4, "verbose": -1}
lgb_data = lgb.Dataset(X, label=y)
est = lgb.train(params, lgb_data, num_boost_round=10)
predict_default = est.predict(X)
results = {}
est = lgb.train(
dict(params, multi_error_top_k=1),
lgb_data,
num_boost_round=10,
valid_sets=[lgb_data],
callbacks=[lgb.record_evaluation(results)],
)
predict_1 = est.predict(X)
# check that default gives same result as k = 1
np.testing.assert_allclose(predict_1, predict_default)
# check against independent calculation for k = 1
err = top_k_error(y, predict_1, 1)
assert results["training"]["multi_error"][-1] == pytest.approx(err)
# check against independent calculation for k = 2
results = {}
est = lgb.train(
dict(params, multi_error_top_k=2),
lgb_data,
num_boost_round=10,
valid_sets=[lgb_data],
callbacks=[lgb.record_evaluation(results)],
)
predict_2 = est.predict(X)
err = top_k_error(y, predict_2, 2)
assert results["training"]["multi_error@2"][-1] == pytest.approx(err)
# check against independent calculation for k = 10
results = {}
est = lgb.train(
dict(params, multi_error_top_k=10),
lgb_data,
num_boost_round=10,
valid_sets=[lgb_data],
callbacks=[lgb.record_evaluation(results)],
)
predict_3 = est.predict(X)
err = top_k_error(y, predict_3, 10)
assert results["training"]["multi_error@10"][-1] == pytest.approx(err)
# check cases where predictions are equal
X = np.array([[0, 0], [0, 0]])
y = np.array([0, 1])
lgb_data = lgb.Dataset(X, label=y)
params["num_classes"] = 2
results = {}
lgb.train(params, lgb_data, num_boost_round=10, valid_sets=[lgb_data], callbacks=[lgb.record_evaluation(results)])
assert results["training"]["multi_error"][-1] == pytest.approx(1)
results = {}
lgb.train(
dict(params, multi_error_top_k=2),
lgb_data,
num_boost_round=10,
valid_sets=[lgb_data],
callbacks=[lgb.record_evaluation(results)],
)
assert results["training"]["multi_error@2"][-1] == pytest.approx(0)
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Skip due to differences in implementation details of CUDA version")
def test_auc_mu(rng):
# should give same result as binary auc for 2 classes
X, y = load_digits(n_class=10, return_X_y=True)
y_new = np.zeros((len(y)))
y_new[y != 0] = 1
lgb_X = lgb.Dataset(X, label=y_new)
params = {"objective": "multiclass", "metric": "auc_mu", "verbose": -1, "num_classes": 2, "seed": 0}
results_auc_mu = {}
lgb.train(params, lgb_X, num_boost_round=10, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(results_auc_mu)])
params = {"objective": "binary", "metric": "auc", "verbose": -1, "seed": 0}
results_auc = {}
lgb.train(params, lgb_X, num_boost_round=10, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(results_auc)])
np.testing.assert_allclose(results_auc_mu["training"]["auc_mu"], results_auc["training"]["auc"])
# test the case where all predictions are equal
lgb_X = lgb.Dataset(X[:10], label=y_new[:10])
params = {
"objective": "multiclass",
"metric": "auc_mu",
"verbose": -1,
"num_classes": 2,
"min_data_in_leaf": 20,
"seed": 0,
}
results_auc_mu = {}
lgb.train(params, lgb_X, num_boost_round=10, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(results_auc_mu)])
assert results_auc_mu["training"]["auc_mu"][-1] == pytest.approx(0.5)
# test that weighted data gives different auc_mu
lgb_X = lgb.Dataset(X, label=y)
lgb_X_weighted = lgb.Dataset(X, label=y, weight=np.abs(rng.standard_normal(size=y.shape)))
results_unweighted = {}
results_weighted = {}
params = dict(params, num_classes=10, num_leaves=5)
lgb.train(
params, lgb_X, num_boost_round=10, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(results_unweighted)]
)
lgb.train(
params,
lgb_X_weighted,
num_boost_round=10,
valid_sets=[lgb_X_weighted],
callbacks=[lgb.record_evaluation(results_weighted)],
)
assert results_weighted["training"]["auc_mu"][-1] < 1
assert results_unweighted["training"]["auc_mu"][-1] != results_weighted["training"]["auc_mu"][-1]
# test that equal data weights give same auc_mu as unweighted data
lgb_X_weighted = lgb.Dataset(X, label=y, weight=np.ones(y.shape) * 0.5)
lgb.train(
params,
lgb_X_weighted,
num_boost_round=10,
valid_sets=[lgb_X_weighted],
callbacks=[lgb.record_evaluation(results_weighted)],
)
assert results_unweighted["training"]["auc_mu"][-1] == pytest.approx(
results_weighted["training"]["auc_mu"][-1], abs=1e-5
)
# should give 1 when accuracy = 1
X = X[:10, :]
y = y[:10]
lgb_X = lgb.Dataset(X, label=y)
params = {"objective": "multiclass", "metric": "auc_mu", "num_classes": 10, "min_data_in_leaf": 1, "verbose": -1}
results = {}
lgb.train(params, lgb_X, num_boost_round=100, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(results)])
assert results["training"]["auc_mu"][-1] == pytest.approx(1)
# test loading class weights
Xy = np.loadtxt(
str(Path(__file__).absolute().parents[2] / "examples" / "multiclass_classification" / "multiclass.train")
)
y = Xy[:, 0]
X = Xy[:, 1:]
lgb_X = lgb.Dataset(X, label=y)
params = {
"objective": "multiclass",
"metric": "auc_mu",
"auc_mu_weights": [0, 2, 2, 2, 2, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0],
"num_classes": 5,
"verbose": -1,
"seed": 0,
}
results_weight = {}
lgb.train(params, lgb_X, num_boost_round=5, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(results_weight)])
params["auc_mu_weights"] = []
results_no_weight = {}
lgb.train(
params, lgb_X, num_boost_round=5, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(results_no_weight)]
)
assert results_weight["training"]["auc_mu"][-1] != results_no_weight["training"]["auc_mu"][-1]
def test_ranking_prediction_early_stopping():
rank_example_dir = Path(__file__).absolute().parents[2] / "examples" / "lambdarank"
X_train, y_train = load_svmlight_file(str(rank_example_dir / "rank.train"))
q_train = np.loadtxt(str(rank_example_dir / "rank.train.query"))
X_test, _ = load_svmlight_file(str(rank_example_dir / "rank.test"))
params = {"objective": "rank_xendcg", "verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train, group=q_train, params=params)
gbm = lgb.train(params, lgb_train, num_boost_round=50)
pred_parameter = {"pred_early_stop": True, "pred_early_stop_freq": 5, "pred_early_stop_margin": 1.5}
ret_early = gbm.predict(X_test, **pred_parameter)
pred_parameter["pred_early_stop_margin"] = 5.5
ret_early_more_strict = gbm.predict(X_test, **pred_parameter)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(ret_early, ret_early_more_strict)
# Simulates position bias for a given ranking dataset.
# The output dataset is identical to the input one with the exception for the relevance labels.
# The new labels are generated according to an instance of a cascade user model:
# for each query, the user is simulated to be traversing the list of documents ranked by a baseline ranker
# (in our example it is simply the ordering by some feature correlated with relevance, e.g., 34)
# and clicks on that document (new_label=1) with some probability 'pclick' depending on its true relevance;
# at each position the user may stop the traversal with some probability pstop. For the non-clicked documents,
# new_label=0. Thus the generated new labels are biased towards the baseline ranker.
# The positions of the documents in the ranked lists produced by the baseline, are returned.
def simulate_position_bias(file_dataset_in, file_query_in, file_dataset_out, baseline_feature):
# a mapping of a document's true relevance (defined on a 5-grade scale) into the probability of clicking it
def get_pclick(label):
if label == 0:
return 0.4
elif label == 1:
return 0.6
elif label == 2:
return 0.7
elif label == 3:
return 0.8
else:
return 0.9
# an instantiation of a cascade model where the user stops with probability 0.2 after observing each document
pstop = 0.2
f_dataset_in = open(file_dataset_in, "r")
f_dataset_out = open(file_dataset_out, "w")
random.seed(10)
positions_all = []
for line in open(file_query_in):
docs_num = int(line)
lines = []
index_values = []
positions = [0] * docs_num
for index in range(docs_num):
features = f_dataset_in.readline().split()
lines.append(features)
val = 0.0
for feature_val in features:
feature_val_split = feature_val.split(":")
if int(feature_val_split[0]) == baseline_feature:
val = float(feature_val_split[1])
index_values.append([index, val])
index_values.sort(key=lambda x: -x[1])
stop = False
for pos in range(docs_num):
index = index_values[pos][0]
new_label = 0
if not stop:
label = int(lines[index][0])
pclick = get_pclick(label)
if random.random() < pclick:
new_label = 1
stop = random.random() < pstop
lines[index][0] = str(new_label)
positions[index] = pos
for features in lines:
f_dataset_out.write(" ".join(features) + "\n")
positions_all.extend(positions)
f_dataset_out.close()
return positions_all
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Positions in learning to rank is not supported in CUDA version yet")
def test_ranking_with_position_information_with_file(tmp_path):
rank_example_dir = Path(__file__).absolute().parents[2] / "examples" / "lambdarank"
params = {
"objective": "lambdarank",
"verbose": -1,
"eval_at": [3],
"metric": "ndcg",
"bagging_freq": 1,
"bagging_fraction": 0.9,
"min_data_in_leaf": 50,
"min_sum_hessian_in_leaf": 5.0,
}
# simulate position bias for the train dataset and put the train dataset with biased labels to temp directory
positions = simulate_position_bias(
str(rank_example_dir / "rank.train"),
str(rank_example_dir / "rank.train.query"),
str(tmp_path / "rank.train"),
baseline_feature=34,
)
copyfile(str(rank_example_dir / "rank.train.query"), str(tmp_path / "rank.train.query"))
copyfile(str(rank_example_dir / "rank.test"), str(tmp_path / "rank.test"))
copyfile(str(rank_example_dir / "rank.test.query"), str(tmp_path / "rank.test.query"))
lgb_train = lgb.Dataset(str(tmp_path / "rank.train"), params=params)
lgb_valid = [lgb_train.create_valid(str(tmp_path / "rank.test"))]
gbm_baseline = lgb.train(params, lgb_train, valid_sets=lgb_valid, num_boost_round=50)
f_positions_out = open(str(tmp_path / "rank.train.position"), "w")
for pos in positions:
f_positions_out.write(str(pos) + "\n")
f_positions_out.close()
lgb_train = lgb.Dataset(str(tmp_path / "rank.train"), params=params)
lgb_valid = [lgb_train.create_valid(str(tmp_path / "rank.test"))]
gbm_unbiased_with_file = lgb.train(params, lgb_train, valid_sets=lgb_valid, num_boost_round=50)
# the performance of the unbiased LambdaMART should outperform the plain LambdaMART on the dataset with position bias
assert gbm_baseline.best_score["valid_0"]["ndcg@3"] + 0.03 <= gbm_unbiased_with_file.best_score["valid_0"]["ndcg@3"]
# add extra row to position file
with open(str(tmp_path / "rank.train.position"), "a") as file:
file.write("pos_1000\n")
file.close()
lgb_train = lgb.Dataset(str(tmp_path / "rank.train"), params=params)
lgb_valid = [lgb_train.create_valid(str(tmp_path / "rank.test"))]
with pytest.raises(lgb.basic.LightGBMError, match=r"Positions size \(3006\) doesn't match data size"):
lgb.train(params, lgb_train, valid_sets=lgb_valid, num_boost_round=50)
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Positions in learning to rank is not supported in CUDA version yet")
def test_ranking_with_position_information_with_dataset_constructor(tmp_path):
rank_example_dir = Path(__file__).absolute().parents[2] / "examples" / "lambdarank"
params = {
"objective": "lambdarank",
"verbose": -1,
"eval_at": [3],
"metric": "ndcg",
"bagging_freq": 1,
"bagging_fraction": 0.9,
"min_data_in_leaf": 50,
"min_sum_hessian_in_leaf": 5.0,
"num_threads": 1,
"deterministic": True,
"seed": 0,
}
# simulate position bias for the train dataset and put the train dataset with biased labels to temp directory
positions = simulate_position_bias(
str(rank_example_dir / "rank.train"),
str(rank_example_dir / "rank.train.query"),
str(tmp_path / "rank.train"),
baseline_feature=34,
)
copyfile(str(rank_example_dir / "rank.train.query"), str(tmp_path / "rank.train.query"))
copyfile(str(rank_example_dir / "rank.test"), str(tmp_path / "rank.test"))
copyfile(str(rank_example_dir / "rank.test.query"), str(tmp_path / "rank.test.query"))
lgb_train = lgb.Dataset(str(tmp_path / "rank.train"), params=params)
lgb_valid = [lgb_train.create_valid(str(tmp_path / "rank.test"))]
gbm_baseline = lgb.train(params, lgb_train, valid_sets=lgb_valid, num_boost_round=50)
positions = np.array(positions)
# test setting positions through Dataset constructor with numpy array
lgb_train = lgb.Dataset(str(tmp_path / "rank.train"), params=params, position=positions)
lgb_valid = [lgb_train.create_valid(str(tmp_path / "rank.test"))]
gbm_unbiased = lgb.train(params, lgb_train, valid_sets=lgb_valid, num_boost_round=50)
# the performance of the unbiased LambdaMART should outperform the plain LambdaMART on the dataset with position bias
assert gbm_baseline.best_score["valid_0"]["ndcg@3"] + 0.03 <= gbm_unbiased.best_score["valid_0"]["ndcg@3"]
try:
import pandas as pd # noqa: PLC0415
# test setting positions through Dataset constructor with pandas Series
lgb_train = lgb.Dataset(str(tmp_path / "rank.train"), params=params, position=pd.Series(positions))
lgb_valid = [lgb_train.create_valid(str(tmp_path / "rank.test"))]
gbm_unbiased_pandas_series = lgb.train(params, lgb_train, valid_sets=lgb_valid, num_boost_round=50)
assert (
gbm_unbiased.best_score["valid_0"]["ndcg@3"] == gbm_unbiased_pandas_series.best_score["valid_0"]["ndcg@3"]
)
except ImportError:
pass
# test setting positions through set_position
lgb_train = lgb.Dataset(str(tmp_path / "rank.train"), params=params)
lgb_valid = [lgb_train.create_valid(str(tmp_path / "rank.test"))]
lgb_train.set_position(positions)
gbm_unbiased_set_position = lgb.train(params, lgb_train, valid_sets=lgb_valid, num_boost_round=50)
assert gbm_unbiased.best_score["valid_0"]["ndcg@3"] == gbm_unbiased_set_position.best_score["valid_0"]["ndcg@3"]
# test get_position works (positions are remapped to dense int32 indices on the C++
# side, so compare against get_field("position") rather than the original input)
positions_from_get = lgb_train.get_position()
np_assert_array_equal(positions_from_get, lgb_train.get_field("position"), strict=True)
assert positions_from_get.dtype == np.int32
assert positions_from_get.shape == positions.shape
def test_early_stopping():
X, y = load_breast_cancer(return_X_y=True)
params = {"objective": "binary", "metric": "binary_logloss", "verbose": -1}
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
valid_set_name = "valid_set"
# no early stopping
gbm = lgb.train(
params,
lgb_train,
num_boost_round=10,
valid_sets=lgb_eval,
valid_names=valid_set_name,
callbacks=[lgb.early_stopping(stopping_rounds=5)],
)
assert gbm.best_iteration == 10
assert valid_set_name in gbm.best_score
assert "binary_logloss" in gbm.best_score[valid_set_name]
# early stopping occurs
gbm = lgb.train(
params,
lgb_train,
num_boost_round=40,
valid_sets=lgb_eval,
valid_names=valid_set_name,
callbacks=[lgb.early_stopping(stopping_rounds=5)],
)
assert gbm.best_iteration <= 39
assert valid_set_name in gbm.best_score
assert "binary_logloss" in gbm.best_score[valid_set_name]
@pytest.mark.parametrize("use_valid", [True, False])
def test_early_stopping_ignores_training_set(use_valid):
x = np.linspace(-1, 1, 100)
X = x.reshape(-1, 1)
y = x**2
X_train, X_valid = X[:80], X[80:]
y_train, y_valid = y[:80], y[80:]
train_ds = lgb.Dataset(X_train, y_train)
valid_ds = lgb.Dataset(X_valid, y_valid)
valid_sets = [train_ds]
valid_names = ["train"]
if use_valid:
valid_sets.append(valid_ds)
valid_names.append("valid")
eval_result = {}
def train_fn():
return lgb.train(
{"num_leaves": 5},
train_ds,
num_boost_round=2,
valid_sets=valid_sets,
valid_names=valid_names,
callbacks=[lgb.early_stopping(1), lgb.record_evaluation(eval_result)],
)
if use_valid:
bst = train_fn()
assert bst.best_iteration == 1
assert eval_result["train"]["l2"][1] < eval_result["train"]["l2"][0] # train improved
assert eval_result["valid"]["l2"][1] > eval_result["valid"]["l2"][0] # valid didn't
else:
with pytest.warns(UserWarning, match="Only training set found, disabling early stopping."):
bst = train_fn()
assert bst.current_iteration() == 2
assert bst.best_iteration == 0
@pytest.mark.parametrize("first_metric_only", [True, False])
def test_early_stopping_via_global_params(first_metric_only):
X, y = load_breast_cancer(return_X_y=True)
num_trees = 5
params = {
"num_trees": num_trees,
"objective": "binary",
"metric": "None",
"verbose": -1,
"early_stopping_round": 2,
"first_metric_only": first_metric_only,
}
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
valid_set_name = "valid_set"
gbm = lgb.train(
params, lgb_train, feval=[decreasing_metric, constant_metric], valid_sets=lgb_eval, valid_names=valid_set_name
)
if first_metric_only:
assert gbm.best_iteration == num_trees
else:
assert gbm.best_iteration == 1
assert valid_set_name in gbm.best_score
assert "decreasing_metric" in gbm.best_score[valid_set_name]
assert "error" in gbm.best_score[valid_set_name]
@pytest.mark.parametrize("early_stopping_round", [-10, -1, 0, None, "None"])
def test_early_stopping_is_not_enabled_for_non_positive_stopping_rounds(early_stopping_round):
X, y = load_breast_cancer(return_X_y=True)
num_trees = 5
params = {
"num_trees": num_trees,
"objective": "binary",
"metric": "None",
"verbose": -1,
"early_stopping_round": early_stopping_round,
"first_metric_only": True,
}
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
valid_set_name = "valid_set"
if early_stopping_round is None:
gbm = lgb.train(
params,
lgb_train,
feval=[constant_metric],
valid_sets=lgb_eval,
valid_names=valid_set_name,
)
assert "early_stopping_round" not in gbm.params
assert gbm.num_trees() == num_trees
elif early_stopping_round == "None":
with pytest.raises(TypeError, match="early_stopping_round should be an integer. Got 'str'"):
gbm = lgb.train(
params,
lgb_train,
feval=[constant_metric],
valid_sets=lgb_eval,
valid_names=valid_set_name,
)
elif early_stopping_round <= 0:
gbm = lgb.train(
params,
lgb_train,
feval=[constant_metric],
valid_sets=lgb_eval,
valid_names=valid_set_name,
)
assert gbm.params["early_stopping_round"] == early_stopping_round
assert gbm.num_trees() == num_trees
@pytest.mark.parametrize("first_only", [True, False])
@pytest.mark.parametrize("single_metric", [True, False])
@pytest.mark.parametrize("greater_is_better", [True, False])
def test_early_stopping_min_delta(first_only, single_metric, greater_is_better):
if single_metric and not first_only:
pytest.skip("first_metric_only doesn't affect single metric.")
metric2min_delta = {
"auc": 0.001,
"binary_logloss": 0.01,
"average_precision": 0.001,
"mape": 0.01,
}
if single_metric:
if greater_is_better:
metric = "auc"
else:
metric = "binary_logloss"
else:
if first_only:
if greater_is_better:
metric = ["auc", "binary_logloss"]
else:
metric = ["binary_logloss", "auc"]
else:
if greater_is_better:
metric = ["auc", "average_precision"]
else:
metric = ["binary_logloss", "mape"]
X, y = load_breast_cancer(return_X_y=True)
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2, random_state=0)
train_ds = lgb.Dataset(X_train, y_train)
valid_ds = lgb.Dataset(X_valid, y_valid, reference=train_ds)
params = {"objective": "binary", "metric": metric, "verbose": -1}
if isinstance(metric, str):
min_delta = metric2min_delta[metric]
elif first_only:
min_delta = metric2min_delta[metric[0]]
else:
min_delta = [metric2min_delta[m] for m in metric]
train_kwargs = {
"params": params,
"train_set": train_ds,
"num_boost_round": 50,
"valid_sets": [train_ds, valid_ds],
"valid_names": ["training", "valid"],
}
# regular early stopping
evals_result = {}
train_kwargs["callbacks"] = [
lgb.callback.early_stopping(10, first_only, verbose=False),
lgb.record_evaluation(evals_result),
]
bst = lgb.train(**train_kwargs)
scores = np.vstack(list(evals_result["valid"].values())).T
# positive min_delta
delta_result = {}
train_kwargs["callbacks"] = [
lgb.callback.early_stopping(10, first_only, verbose=False, min_delta=min_delta),
lgb.record_evaluation(delta_result),
]
delta_bst = lgb.train(**train_kwargs)
delta_scores = np.vstack(list(delta_result["valid"].values())).T
if first_only:
scores = scores[:, 0]
delta_scores = delta_scores[:, 0]
assert delta_bst.num_trees() < bst.num_trees()
np.testing.assert_allclose(scores[: len(delta_scores)], delta_scores)
last_score = delta_scores[-1]
best_score = delta_scores[delta_bst.num_trees() - 1]
if greater_is_better:
assert np.less_equal(last_score, best_score + min_delta).any()
else:
assert np.greater_equal(last_score, best_score - min_delta).any()
@pytest.mark.parametrize("early_stopping_min_delta", [1e3, 0.0])
def test_early_stopping_min_delta_via_global_params(early_stopping_min_delta):
X, y = load_breast_cancer(return_X_y=True)
num_trees = 5
params = {
"num_trees": num_trees,
"num_leaves": 5,
"objective": "binary",
"metric": "None",
"verbose": -1,
"early_stopping_round": 2,
"early_stopping_min_delta": early_stopping_min_delta,
}
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
gbm = lgb.train(params, lgb_train, feval=decreasing_metric, valid_sets=lgb_eval)
if early_stopping_min_delta == 0:
assert gbm.best_iteration == num_trees
else:
assert gbm.best_iteration == 1
def test_early_stopping_can_be_triggered_via_custom_callback():
X, y = make_synthetic_regression()
def _early_stop_after_seventh_iteration(env):
if env.iteration == 6:
exc = lgb.EarlyStopException(
best_iteration=6, best_score=[("some_validation_set", "some_metric", 0.708, True)]
)
raise exc
bst = lgb.train(
params={"objective": "regression", "verbose": -1, "num_leaves": 2},
train_set=lgb.Dataset(X, label=y),
num_boost_round=23,
callbacks=[_early_stop_after_seventh_iteration],
)
assert bst.num_trees() == 7
assert bst.best_score["some_validation_set"]["some_metric"] == 0.708
assert bst.best_iteration == 7
assert bst.current_iteration() == 7
def test_continue_train(tmp_path):
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {"objective": "regression", "metric": "l1", "verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train, free_raw_data=False)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train, free_raw_data=False)
init_gbm = lgb.train(params, lgb_train, num_boost_round=20)
model_path = tmp_path / "model.txt"
init_gbm.save_model(model_path)
evals_result = {}
gbm = lgb.train(
params,
lgb_train,
num_boost_round=30,
valid_sets=lgb_eval,
# test custom eval metrics
feval=(lambda p, d: ("custom_mae", mean_absolute_error(p, d.get_label()), False)),
callbacks=[lgb.record_evaluation(evals_result)],
init_model=model_path,
)
ret = mean_absolute_error(y_test, gbm.predict(X_test))
assert ret < 13.6
assert evals_result["valid_0"]["l1"][-1] == pytest.approx(ret)
np.testing.assert_allclose(evals_result["valid_0"]["l1"], evals_result["valid_0"]["custom_mae"])
def test_continue_train_reused_dataset():
X, y = make_synthetic_regression()
params = {"objective": "regression", "verbose": -1}
lgb_train = lgb.Dataset(X, y, free_raw_data=False)
init_gbm = lgb.train(params, lgb_train, num_boost_round=5)
init_gbm_2 = lgb.train(params, lgb_train, num_boost_round=5, init_model=init_gbm)
init_gbm_3 = lgb.train(params, lgb_train, num_boost_round=5, init_model=init_gbm_2)
gbm = lgb.train(params, lgb_train, num_boost_round=5, init_model=init_gbm_3)
assert gbm.current_iteration() == 20
def test_continue_train_dart():
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {"boosting_type": "dart", "objective": "regression", "metric": "l1", "verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train, free_raw_data=False)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train, free_raw_data=False)
init_gbm = lgb.train(params, lgb_train, num_boost_round=50)
evals_result = {}
gbm = lgb.train(
params,
lgb_train,
num_boost_round=50,
valid_sets=lgb_eval,
callbacks=[lgb.record_evaluation(evals_result)],
init_model=init_gbm,
)
ret = mean_absolute_error(y_test, gbm.predict(X_test))
assert ret < 13.6
assert evals_result["valid_0"]["l1"][-1] == pytest.approx(ret)
def test_continue_train_multiclass():
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {"objective": "multiclass", "metric": "multi_logloss", "num_class": 3, "verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train, params=params, free_raw_data=False)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train, params=params, free_raw_data=False)
init_gbm = lgb.train(params, lgb_train, num_boost_round=20)
evals_result = {}
gbm = lgb.train(
params,
lgb_train,
num_boost_round=30,
valid_sets=lgb_eval,
callbacks=[lgb.record_evaluation(evals_result)],
init_model=init_gbm,
)
ret = multi_logloss(y_test, gbm.predict(X_test))
assert ret < 0.1
assert evals_result["valid_0"]["multi_logloss"][-1] == pytest.approx(ret)
def test_cv():
X_train, y_train = make_synthetic_regression()
params = {"verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train)
# shuffle = False, override metric in params
params_with_metric = {"metric": "l2", "verbose": -1}
cv_res = lgb.cv(
params_with_metric, lgb_train, num_boost_round=10, nfold=3, stratified=False, shuffle=False, metrics="l1"
)
assert "valid l1-mean" in cv_res
assert "valid l2-mean" not in cv_res
assert len(cv_res["valid l1-mean"]) == 10
# shuffle = True, callbacks
cv_res = lgb.cv(
params,
lgb_train,
num_boost_round=10,
nfold=3,
stratified=False,
shuffle=True,
metrics="l1",
callbacks=[lgb.reset_parameter(learning_rate=lambda i: 0.1 - 0.001 * i)],
)
assert "valid l1-mean" in cv_res
assert len(cv_res["valid l1-mean"]) == 10
# enable display training loss
cv_res = lgb.cv(
params_with_metric,
lgb_train,
num_boost_round=10,
nfold=3,
stratified=False,
shuffle=False,
metrics="l1",
eval_train_metric=True,
)
assert "train l1-mean" in cv_res
assert "valid l1-mean" in cv_res
assert "train l2-mean" not in cv_res
assert "valid l2-mean" not in cv_res
assert len(cv_res["train l1-mean"]) == 10
assert len(cv_res["valid l1-mean"]) == 10
# self defined folds
tss = TimeSeriesSplit(3)
folds = tss.split(X_train)
cv_res_gen = lgb.cv(params_with_metric, lgb_train, num_boost_round=10, folds=folds)
cv_res_obj = lgb.cv(params_with_metric, lgb_train, num_boost_round=10, folds=tss)
np.testing.assert_allclose(cv_res_gen["valid l2-mean"], cv_res_obj["valid l2-mean"])
# LambdaRank
rank_example_dir = Path(__file__).absolute().parents[2] / "examples" / "lambdarank"
X_train, y_train = load_svmlight_file(str(rank_example_dir / "rank.train"))
q_train = np.loadtxt(str(rank_example_dir / "rank.train.query"))
params_lambdarank = {"objective": "lambdarank", "verbose": -1, "eval_at": 3}
lgb_train = lgb.Dataset(X_train, y_train, group=q_train)
# ... with l2 metric
cv_res_lambda = lgb.cv(params_lambdarank, lgb_train, num_boost_round=10, nfold=3, metrics="l2")
assert len(cv_res_lambda) == 2
assert not np.isnan(cv_res_lambda["valid l2-mean"]).any()
# ... with NDCG (default) metric
cv_res_lambda = lgb.cv(params_lambdarank, lgb_train, num_boost_round=10, nfold=3)
assert len(cv_res_lambda) == 2
assert not np.isnan(cv_res_lambda["valid ndcg@3-mean"]).any()
# self defined folds with lambdarank
cv_res_lambda_obj = lgb.cv(params_lambdarank, lgb_train, num_boost_round=10, folds=GroupKFold(n_splits=3))
np.testing.assert_allclose(cv_res_lambda["valid ndcg@3-mean"], cv_res_lambda_obj["valid ndcg@3-mean"])
def test_cv_works_with_init_model(tmp_path):
X, y = make_synthetic_regression()
params = {"objective": "regression", "verbose": -1}
num_train_rounds = 2
lgb_train = lgb.Dataset(X, y, free_raw_data=False)
bst = lgb.train(params=params, train_set=lgb_train, num_boost_round=num_train_rounds)
preds_raw = bst.predict(X, raw_score=True)
model_path_txt = str(tmp_path / "lgb.model")
bst.save_model(model_path_txt)
num_cv_rounds = 5
cv_kwargs = {
"num_boost_round": num_cv_rounds,
"nfold": 3,
"stratified": False,
"shuffle": False,
"seed": 708,
"return_cvbooster": True,
"params": params,
}
# init_model from an in-memory Booster
cv_res = lgb.cv(train_set=lgb_train, init_model=bst, **cv_kwargs)
cv_bst_w_in_mem_init_model = cv_res["cvbooster"]
assert cv_bst_w_in_mem_init_model.current_iteration() == [num_train_rounds + num_cv_rounds] * 3
for booster in cv_bst_w_in_mem_init_model.boosters:
np.testing.assert_allclose(preds_raw, booster.predict(X, raw_score=True, num_iteration=num_train_rounds))
# init_model from a text file
cv_res = lgb.cv(train_set=lgb_train, init_model=model_path_txt, **cv_kwargs)
cv_bst_w_file_init_model = cv_res["cvbooster"]
assert cv_bst_w_file_init_model.current_iteration() == [num_train_rounds + num_cv_rounds] * 3
for booster in cv_bst_w_file_init_model.boosters:
np.testing.assert_allclose(preds_raw, booster.predict(X, raw_score=True, num_iteration=num_train_rounds))
# predictions should be identical
for i in range(3):
np.testing.assert_allclose(
cv_bst_w_in_mem_init_model.boosters[i].predict(X), cv_bst_w_file_init_model.boosters[i].predict(X)
)
def test_cvbooster():
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"objective": "binary",
"metric": "binary_logloss",
"verbose": -1,
}
nfold = 3
lgb_train = lgb.Dataset(X_train, y_train)
# with early stopping
cv_res = lgb.cv(
params,
lgb_train,
num_boost_round=25,
nfold=nfold,
callbacks=[lgb.early_stopping(stopping_rounds=5)],
return_cvbooster=True,
)
assert "cvbooster" in cv_res
cvb = cv_res["cvbooster"]
assert isinstance(cvb, lgb.CVBooster)
assert isinstance(cvb.boosters, list)
assert len(cvb.boosters) == nfold
assert all(isinstance(bst, lgb.Booster) for bst in cvb.boosters)
assert cvb.best_iteration > 0
# predict by each fold booster
preds = cvb.predict(X_test)
assert isinstance(preds, list)
assert len(preds) == nfold
# check that each booster predicted using the best iteration
for fold_preds, bst in zip(preds, cvb.boosters, strict=True):
assert bst.best_iteration == cvb.best_iteration
expected = bst.predict(X_test, num_iteration=cvb.best_iteration)
np.testing.assert_allclose(fold_preds, expected)
# fold averaging
avg_pred = np.mean(preds, axis=0)
ret = log_loss(y_test, avg_pred)
assert ret < 0.13
# without early stopping
cv_res = lgb.cv(params, lgb_train, num_boost_round=20, nfold=3, return_cvbooster=True)
cvb = cv_res["cvbooster"]
assert cvb.best_iteration == -1
preds = cvb.predict(X_test)
avg_pred = np.mean(preds, axis=0)
ret = log_loss(y_test, avg_pred)
assert ret < 0.15
def test_cvbooster_save_load(tmp_path):
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, _ = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"objective": "binary",
"metric": "binary_logloss",
"verbose": -1,
}
nfold = 3
lgb_train = lgb.Dataset(X_train, y_train)
cv_res = lgb.cv(
params,
lgb_train,
num_boost_round=10,
nfold=nfold,
callbacks=[lgb.early_stopping(stopping_rounds=5)],
return_cvbooster=True,
)
cvbooster = cv_res["cvbooster"]
preds = cvbooster.predict(X_test)
best_iteration = cvbooster.best_iteration
model_path_txt = str(tmp_path / "lgb.model")
cvbooster.save_model(model_path_txt)
model_string = cvbooster.model_to_string()
del cvbooster
cvbooster_from_txt_file = lgb.CVBooster(model_file=model_path_txt)
cvbooster_from_string = lgb.CVBooster().model_from_string(model_string)
for cvbooster_loaded in [cvbooster_from_txt_file, cvbooster_from_string]:
assert best_iteration == cvbooster_loaded.best_iteration
np_assert_array_equal(preds, cvbooster_loaded.predict(X_test), strict=True)
@pytest.mark.parametrize("serializer", SERIALIZERS)
def test_cvbooster_picklable(serializer):
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, _ = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"objective": "binary",
"metric": "binary_logloss",
"verbose": -1,
}
nfold = 3
lgb_train = lgb.Dataset(X_train, y_train)
cv_res = lgb.cv(
params,
lgb_train,
num_boost_round=10,
nfold=nfold,
callbacks=[lgb.early_stopping(stopping_rounds=5)],
return_cvbooster=True,
)
cvbooster = cv_res["cvbooster"]
preds = cvbooster.predict(X_test)
best_iteration = cvbooster.best_iteration
cvbooster_from_disk = pickle_and_unpickle_object(obj=cvbooster, serializer=serializer)
del cvbooster
assert best_iteration == cvbooster_from_disk.best_iteration
preds_from_disk = cvbooster_from_disk.predict(X_test)
np_assert_array_equal(preds, preds_from_disk, strict=True)
def test_feature_name():
X_train, y_train = make_synthetic_regression()
params = {"verbose": -1}
feature_names = [f"f_{i}" for i in range(X_train.shape[-1])]
lgb_train = lgb.Dataset(X_train, y_train, feature_name=feature_names)
gbm = lgb.train(params, lgb_train, num_boost_round=5)
assert feature_names == gbm.feature_name()
# test feature_names with whitespaces
feature_names_with_space = [f"f {i}" for i in range(X_train.shape[-1])]
lgb_train.set_feature_name(feature_names_with_space)
gbm = lgb.train(params, lgb_train, num_boost_round=5)
assert feature_names == gbm.feature_name()
def test_feature_name_with_non_ascii(rng, tmp_path):
X_train = rng.normal(size=(100, 4))
y_train = rng.normal(size=(100,))
# This has non-ascii strings.
feature_names = ["F_零", "F_一", "F_二", "F_三"]
params = {"verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train, feature_name=feature_names)
gbm = lgb.train(params, lgb_train, num_boost_round=5)
assert feature_names == gbm.feature_name()
model_path_txt = str(tmp_path / "lgb.model")
gbm.save_model(model_path_txt)
gbm2 = lgb.Booster(model_file=model_path_txt)
assert feature_names == gbm2.feature_name()
def test_parameters_are_loaded_from_model_file(tmp_path, capsys, rng):
X = np.hstack(
[
rng.uniform(size=(100, 1)),
rng.integers(low=0, high=5, size=(100, 2)),
]
)
y = rng.uniform(size=(100,))
ds = lgb.Dataset(X, y, categorical_feature=[1, 2])
params = {
"bagging_fraction": 0.8,
"bagging_freq": 2,
"boosting": "rf",
"feature_contri": [0.5, 0.5, 0.5],
"feature_fraction": 0.7,
"boost_from_average": False,
"interaction_constraints": [[0, 1], [0]],
"metric": ["l2", "rmse"],
"num_leaves": 5,
"num_threads": 1,
"verbosity": 0,
}
model_file = tmp_path / "model.txt"
orig_bst = lgb.train(params, ds, num_boost_round=1)
orig_bst.save_model(model_file)
with model_file.open("rt") as f:
model_contents = f.readlines()
params_start = model_contents.index("parameters:\n")
model_contents.insert(params_start + 1, "[max_conflict_rate: 0]\n")
with model_file.open("wt") as f:
f.writelines(model_contents)
bst = lgb.Booster(model_file=model_file)
expected_msg = "[LightGBM] [Warning] Ignoring unrecognized parameter 'max_conflict_rate' found in model string."
stdout = capsys.readouterr().out
assert expected_msg in stdout
set_params = {k: bst.params[k] for k in params.keys()}
assert set_params == params
assert bst.params["categorical_feature"] == [1, 2]
# check that passing parameters to the constructor raises warning and ignores them
with pytest.warns(UserWarning, match="Ignoring params argument, using parameters from model file."):
bst2 = lgb.Booster(params={"num_leaves": 7}, model_file=model_file)
assert bst.params == bst2.params
# check inference isn't affected by unknown parameter
orig_preds = orig_bst.predict(X)
preds = bst.predict(X)
np.testing.assert_allclose(preds, orig_preds)
def test_string_serialized_params_retrieval(rng):
# Random train data
train_x = rng.random((500, 3))
train_y = rng.integers(0, 1, 500)
train_data = lgb.Dataset(train_x, train_y)
# Parameters
params = {
"boosting": "gbdt",
"deterministic": True,
"feature_contri": [0.5] * train_x.shape[1],
"interaction_constraints": [[0, 1], [0]],
"objective": "binary",
"metric": ["auc"],
"num_leaves": 7,
"learning_rate": 0.05,
"feature_fraction": 0.9,
"bagging_fraction": 0.8,
"bagging_freq": 5,
"verbosity": -100,
}
# train a model and serialize it to a string in memory
model = lgb.train(params, train_data, num_boost_round=2)
model_serialized = model.model_to_string()
# load a new model with the string
with pytest.warns(UserWarning, match="Ignoring params argument, using parameters from model string."):
new_model = lgb.Booster(params={"num_leaves": 32}, model_str=model_serialized)
assert new_model.params["boosting"] == "gbdt"
assert new_model.params["deterministic"] is True
assert new_model.params["feature_contri"] == [0.5] * train_x.shape[1]
assert new_model.params["interaction_constraints"] == [[0, 1], [0]]
assert new_model.params["objective"] == "binary"
assert new_model.params["metric"] == ["auc"]
assert new_model.params["num_leaves"] == 7
assert new_model.params["learning_rate"] == 0.05
assert new_model.params["feature_fraction"] == 0.9
assert new_model.params["bagging_fraction"] == 0.8
assert new_model.params["bagging_freq"] == 5
assert new_model.params["verbosity"] == -100
def test_save_load_copy_pickle(tmp_path):
def train_and_predict(init_model=None, return_model=False):
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {"objective": "regression", "metric": "l2", "verbose": -1}
lgb_train = lgb.Dataset(X_train, y_train)
gbm_template = lgb.train(params, lgb_train, num_boost_round=10, init_model=init_model)
return gbm_template if return_model else mean_squared_error(y_test, gbm_template.predict(X_test))
gbm = train_and_predict(return_model=True)
ret_origin = train_and_predict(init_model=gbm)
other_ret = []
model_path_txt = str(tmp_path / "lgb.model")
gbm.save_model(model_path_txt)
with open(model_path_txt) as f: # check all params are logged into model file correctly
assert f.read().find("[num_iterations: 10]") != -1
other_ret.append(train_and_predict(init_model=model_path_txt))
gbm_load = lgb.Booster(model_file=model_path_txt)
other_ret.append(train_and_predict(init_model=gbm_load))
other_ret.append(train_and_predict(init_model=copy.copy(gbm)))
other_ret.append(train_and_predict(init_model=copy.deepcopy(gbm)))
model_path_pkl = str(tmp_path / "lgb.pkl")
with open(model_path_pkl, "wb") as f:
pickle.dump(gbm, f)
with open(model_path_pkl, "rb") as f:
gbm_pickle = pickle.load(f)
other_ret.append(train_and_predict(init_model=gbm_pickle))
gbm_pickles = pickle.loads(pickle.dumps(gbm))
other_ret.append(train_and_predict(init_model=gbm_pickles))
for ret in other_ret:
assert ret_origin == pytest.approx(ret)
def test_all_expected_params_are_written_out_to_model_text(tmp_path):
X, y = make_synthetic_regression()
params = {
"objective": "mape",
"metric": ["l2", "mae"],
"seed": 708,
"data_sample_strategy": "bagging",
"sub_row": 0.8234,
"verbose": -1,
}
dtrain = lgb.Dataset(data=X, label=y)
gbm = lgb.train(params=params, train_set=dtrain, num_boost_round=3)
model_txt_from_memory = gbm.model_to_string()
model_file = tmp_path / "out.model"
gbm.save_model(filename=model_file)
with open(model_file, "r") as f:
model_txt_from_file = f.read()
assert model_txt_from_memory == model_txt_from_file
# entries whose values should reflect params passed to lgb.train()
non_default_param_entries = [
"[objective: mape]",
# 'l1' was passed in with alias 'mae'
"[metric: l2,l1]",
"[data_sample_strategy: bagging]",
"[seed: 708]",
# NOTE: this was passed in with alias 'sub_row'
"[bagging_fraction: 0.8234]",
"[num_iterations: 3]",
]
# entries with default values of params
default_param_entries = [
"[boosting: gbdt]",
"[tree_learner: serial]",
"[data: ]",
"[valid: ]",
"[learning_rate: 0.1]",
"[num_leaves: 31]",
"[num_threads: 0]",
"[deterministic: 0]",
"[histogram_pool_size: -1]",
"[max_depth: -1]",
"[min_data_in_leaf: 20]",
"[min_sum_hessian_in_leaf: 0.001]",
"[pos_bagging_fraction: 1]",
"[neg_bagging_fraction: 1]",
"[bagging_freq: 0]",
"[bagging_seed: 15415]",
"[feature_fraction: 1]",
"[feature_fraction_bynode: 1]",
"[feature_fraction_seed: 32671]",
"[extra_trees: 0]",
"[extra_seed: 6642]",
"[early_stopping_round: 0]",
"[early_stopping_min_delta: 0]",
"[first_metric_only: 0]",
"[max_delta_step: 0]",
"[lambda_l1: 0]",
"[lambda_l2: 0]",
"[linear_lambda: 0]",
"[min_gain_to_split: 0]",
"[drop_rate: 0.1]",
"[max_drop: 50]",
"[skip_drop: 0.5]",
"[xgboost_dart_mode: 0]",
"[uniform_drop: 0]",
"[drop_seed: 20623]",
"[top_rate: 0.2]",
"[other_rate: 0.1]",
"[min_data_per_group: 100]",
"[max_cat_threshold: 32]",
"[cat_l2: 10]",
"[cat_smooth: 10]",
"[max_cat_to_onehot: 4]",
"[top_k: 20]",
"[monotone_constraints: ]",
"[monotone_constraints_method: basic]",
"[monotone_penalty: 0]",
"[feature_contri: ]",
"[forcedsplits_filename: ]",
"[refit_decay_rate: 0.9]",
"[cegb_tradeoff: 1]",
"[cegb_penalty_split: 0]",
"[cegb_penalty_feature_lazy: ]",
"[cegb_penalty_feature_coupled: ]",
"[path_smooth: 0]",
"[interaction_constraints: ]",
"[verbosity: -1]",
"[saved_feature_importance_type: 0]",
"[use_quantized_grad: 0]",
"[num_grad_quant_bins: 4]",
"[quant_train_renew_leaf: 0]",
"[stochastic_rounding: 1]",
"[linear_tree: 0]",
"[max_bin: 255]",
"[max_bin_by_feature: ]",
"[min_data_in_bin: 3]",
"[bin_construct_sample_cnt: 200000]",
"[data_random_seed: 2350]",
"[is_enable_sparse: 1]",
"[enable_bundle: 1]",
"[use_missing: 1]",
"[zero_as_missing: 0]",
"[feature_pre_filter: 1]",
"[pre_partition: 0]",
"[two_round: 0]",
"[header: 0]",
"[label_column: ]",
"[weight_column: ]",
"[group_column: ]",
"[ignore_column: ]",
"[categorical_feature: ]",
"[forcedbins_filename: ]",
"[precise_float_parser: 0]",
"[parser_config_file: ]",
"[objective_seed: 4309]",
"[num_class: 1]",
"[is_unbalance: 0]",
"[scale_pos_weight: 1]",
"[sigmoid: 1]",
"[boost_from_average: 1]",
"[reg_sqrt: 0]",
"[alpha: 0.9]",
"[fair_c: 1]",
"[poisson_max_delta_step: 0.7]",
"[tweedie_variance_power: 1.5]",
"[lambdarank_truncation_level: 30]",
"[lambdarank_norm: 1]",
"[label_gain: ]",
"[lambdarank_position_bias_regularization: 0]",
"[eval_at: ]",
"[multi_error_top_k: 1]",
"[auc_mu_weights: ]",
"[num_machines: 1]",
"[local_listen_port: 12400]",
"[time_out: 120]",
"[machine_list_filename: ]",
"[machines: ]",
"[gpu_platform_id: -1]",
"[gpu_device_id: -1]",
"[num_gpu: 1]",
]
all_param_entries = non_default_param_entries + default_param_entries
# add device-specific entries
#
# passed-in force_col_wise / force_row_wise parameters are ignored on CUDA and GPU builds...
# https://github.com/lightgbm-org/LightGBM/blob/1d7ee63686272bceffd522284127573b511df6be/src/io/config.cpp#L375-L377
if BuildInfo.has_cuda:
device_entries = ["[force_col_wise: 0]", "[force_row_wise: 1]", "[device_type: cuda]", "[gpu_use_dp: 1]"]
elif BuildInfo.has_gpu:
device_entries = ["[force_col_wise: 1]", "[force_row_wise: 0]", "[device_type: gpu]", "[gpu_use_dp: 0]"]
else:
device_entries = ["[force_col_wise: 0]", "[force_row_wise: 0]", "[device_type: cpu]", "[gpu_use_dp: 0]"]
all_param_entries += device_entries
# check that model text has all expected param entries
for param_str in all_param_entries:
assert param_str in model_txt_from_file
assert param_str in model_txt_from_memory
# since Booster.model_to_string() is used when pickling, check that parameters all
# roundtrip pickling successfully too
gbm_pkl = pickle_and_unpickle_object(gbm, serializer="joblib")
model_txt_from_memory = gbm_pkl.model_to_string()
model_file = tmp_path / "out-pkl.model"
gbm_pkl.save_model(filename=model_file)
with open(model_file, "r") as f:
model_txt_from_file = f.read()
for param_str in all_param_entries:
assert param_str in model_txt_from_file
assert param_str in model_txt_from_memory
# why fixed seed?
# sometimes there is no difference how cols are treated (cat or not cat)
def test_pandas_categorical(rng_fixed_seed, tmp_path):
pd = pytest.importorskip("pandas")
X = pd.DataFrame(
{
"A": rng_fixed_seed.permutation(["a", "b", "c", "d"] * 75), # str
"B": rng_fixed_seed.permutation([1, 2, 3] * 100), # int
"C": rng_fixed_seed.permutation([0.1, 0.2, -0.1, -0.1, 0.2] * 60), # float
"D": rng_fixed_seed.permutation([True, False] * 150), # bool
"E": pd.Categorical(rng_fixed_seed.permutation(["z", "y", "x", "w", "v"] * 60), ordered=True),
}
) # str and ordered categorical
y = rng_fixed_seed.permutation([0, 1] * 150)
X_test = pd.DataFrame(
{
"A": rng_fixed_seed.permutation(["a", "b", "e"] * 20), # unseen category
"B": rng_fixed_seed.permutation([1, 3] * 30),
"C": rng_fixed_seed.permutation([0.1, -0.1, 0.2, 0.2] * 15),
"D": rng_fixed_seed.permutation([True, False] * 30),
"E": pd.Categorical(rng_fixed_seed.permutation(["z", "y"] * 30), ordered=True),
}
)
cat_cols_actual = ["A", "B", "C", "D"]
cat_cols_to_store = cat_cols_actual + ["E"]
X[cat_cols_actual] = X[cat_cols_actual].astype("category")
X_test[cat_cols_actual] = X_test[cat_cols_actual].astype("category")
cat_values = [X[col].cat.categories.tolist() for col in cat_cols_to_store]
params = {"objective": "binary", "metric": "binary_logloss", "verbose": -1}
lgb_train = lgb.Dataset(X, y)
gbm0 = lgb.train(params, lgb_train, num_boost_round=10)
pred0 = gbm0.predict(X_test)
assert lgb_train.categorical_feature == "auto"
lgb_train = lgb.Dataset(
X, pd.DataFrame(y), categorical_feature=[0]
) # also test that label can be one-column pd.DataFrame
gbm1 = lgb.train(params, lgb_train, num_boost_round=10)
pred1 = gbm1.predict(X_test)
assert lgb_train.categorical_feature == [0]
lgb_train = lgb.Dataset(X, pd.Series(y), categorical_feature=["A"]) # also test that label can be pd.Series
gbm2 = lgb.train(params, lgb_train, num_boost_round=10)
pred2 = gbm2.predict(X_test)
assert lgb_train.categorical_feature == ["A"]
lgb_train = lgb.Dataset(X, y, categorical_feature=["A", "B", "C", "D"])
gbm3 = lgb.train(params, lgb_train, num_boost_round=10)
pred3 = gbm3.predict(X_test)
assert lgb_train.categorical_feature == ["A", "B", "C", "D"]
categorical_model_path = tmp_path / "categorical.model"
gbm3.save_model(categorical_model_path)
gbm4 = lgb.Booster(model_file=categorical_model_path)
pred4 = gbm4.predict(X_test)
model_str = gbm4.model_to_string()
gbm4.model_from_string(model_str)
pred5 = gbm4.predict(X_test)
gbm5 = lgb.Booster(model_str=model_str)
pred6 = gbm5.predict(X_test)
lgb_train = lgb.Dataset(X, y, categorical_feature=["A", "B", "C", "D", "E"])
gbm6 = lgb.train(params, lgb_train, num_boost_round=10)
pred7 = gbm6.predict(X_test)
assert lgb_train.categorical_feature == ["A", "B", "C", "D", "E"]
lgb_train = lgb.Dataset(X, y, categorical_feature=[])
gbm7 = lgb.train(params, lgb_train, num_boost_round=10)
pred8 = gbm7.predict(X_test)
assert lgb_train.categorical_feature == []
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(pred0, pred1)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(pred0, pred2)
np.testing.assert_allclose(pred1, pred2)
np.testing.assert_allclose(pred0, pred3)
np.testing.assert_allclose(pred0, pred4)
np.testing.assert_allclose(pred0, pred5)
np.testing.assert_allclose(pred0, pred6)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(pred0, pred7) # ordered cat features aren't treated as cat features by default
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(pred0, pred8)
assert gbm0.pandas_categorical == cat_values
assert gbm1.pandas_categorical == cat_values
assert gbm2.pandas_categorical == cat_values
assert gbm3.pandas_categorical == cat_values
assert gbm4.pandas_categorical == cat_values
assert gbm5.pandas_categorical == cat_values
assert gbm6.pandas_categorical == cat_values
assert gbm7.pandas_categorical == cat_values
def test_pandas_sparse(rng):
pd = pytest.importorskip("pandas")
X = pd.DataFrame(
{
"A": pd.arrays.SparseArray(rng.permutation([0, 1, 2] * 100)),
"B": pd.arrays.SparseArray(rng.permutation([0.0, 0.1, 0.2, -0.1, 0.2] * 60)),
"C": pd.arrays.SparseArray(rng.permutation([True, False] * 150)),
}
)
y = pd.Series(pd.arrays.SparseArray(rng.permutation([0, 1] * 150)))
X_test = pd.DataFrame(
{
"A": pd.arrays.SparseArray(rng.permutation([0, 2] * 30)),
"B": pd.arrays.SparseArray(rng.permutation([0.0, 0.1, 0.2, -0.1] * 15)),
"C": pd.arrays.SparseArray(rng.permutation([True, False] * 30)),
}
)
for dtype in pd.concat([X.dtypes, X_test.dtypes, pd.Series(y.dtypes)]):
assert isinstance(dtype, pd.SparseDtype)
params = {"objective": "binary", "verbose": -1}
lgb_train = lgb.Dataset(X, y)
gbm = lgb.train(params, lgb_train, num_boost_round=10)
pred_sparse = gbm.predict(X_test, raw_score=True)
if hasattr(X_test, "sparse"):
pred_dense = gbm.predict(X_test.sparse.to_dense(), raw_score=True)
else:
pred_dense = gbm.predict(X_test.to_dense(), raw_score=True)
np.testing.assert_allclose(pred_sparse, pred_dense)
def test_reference_chain(rng):
X = rng.normal(size=(100, 2))
y = rng.normal(size=(100,))
tmp_dat = lgb.Dataset(X, y)
# take subsets and train
tmp_dat_train = tmp_dat.subset(np.arange(80))
tmp_dat_val = tmp_dat.subset(np.arange(80, 100)).subset(np.arange(18))
params = {"objective": "regression_l2", "metric": "rmse"}
evals_result = {}
lgb.train(
params,
tmp_dat_train,
num_boost_round=20,
valid_sets=[tmp_dat_train, tmp_dat_val],
callbacks=[lgb.record_evaluation(evals_result)],
)
assert len(evals_result["training"]["rmse"]) == 20
assert len(evals_result["valid_1"]["rmse"]) == 20
def test_contribs():
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"objective": "binary",
"metric": "binary_logloss",
"verbose": -1,
}
lgb_train = lgb.Dataset(X_train, y_train)
gbm = lgb.train(params, lgb_train, num_boost_round=20)
assert (
np.linalg.norm(gbm.predict(X_test, raw_score=True) - np.sum(gbm.predict(X_test, pred_contrib=True), axis=1))
< 1e-4
)
def test_contribs_sparse():
n_features = 20
n_samples = 100
# generate CSR sparse dataset
X, y = make_multilabel_classification(
n_samples=n_samples, sparse=True, n_features=n_features, n_classes=1, n_labels=2
)
y = y.flatten()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"objective": "binary",
"verbose": -1,
}
lgb_train = lgb.Dataset(X_train, y_train)
gbm = lgb.train(params, lgb_train, num_boost_round=20)
contribs_csr = gbm.predict(X_test, pred_contrib=True)
assert isspmatrix_csr(contribs_csr)
# convert data to dense and get back same contribs
contribs_dense = gbm.predict(X_test.toarray(), pred_contrib=True)
# validate the values are the same
if platform.machine() == "aarch64":
np.testing.assert_allclose(contribs_csr.toarray(), contribs_dense, rtol=1, atol=1e-12)
else:
np.testing.assert_allclose(contribs_csr.toarray(), contribs_dense)
assert np.linalg.norm(gbm.predict(X_test, raw_score=True) - np.sum(contribs_dense, axis=1)) < 1e-4
# validate using CSC matrix
X_test_csc = X_test.tocsc()
contribs_csc = gbm.predict(X_test_csc, pred_contrib=True)
assert isspmatrix_csc(contribs_csc)
# validate the values are the same
if platform.machine() == "aarch64":
np.testing.assert_allclose(contribs_csc.toarray(), contribs_dense, rtol=1, atol=1e-12)
else:
np.testing.assert_allclose(contribs_csc.toarray(), contribs_dense)
def test_contribs_sparse_multiclass():
n_features = 20
n_samples = 100
n_labels = 4
# generate CSR sparse dataset
X, y = make_multilabel_classification(
n_samples=n_samples, sparse=True, n_features=n_features, n_classes=1, n_labels=n_labels
)
y = y.flatten()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"objective": "multiclass",
"num_class": n_labels,
"verbose": -1,
}
lgb_train = lgb.Dataset(X_train, y_train)
gbm = lgb.train(params, lgb_train, num_boost_round=20)
contribs_csr = gbm.predict(X_test, pred_contrib=True)
assert isinstance(contribs_csr, list)
for perclass_contribs_csr in contribs_csr:
assert isspmatrix_csr(perclass_contribs_csr)
# convert data to dense and get back same contribs
contribs_dense = gbm.predict(X_test.toarray(), pred_contrib=True)
# validate the values are the same
contribs_csr_array = np.swapaxes(np.array([sparse_array.toarray() for sparse_array in contribs_csr]), 0, 1)
contribs_csr_arr_re = contribs_csr_array.reshape(
(contribs_csr_array.shape[0], contribs_csr_array.shape[1] * contribs_csr_array.shape[2])
)
if platform.machine() == "aarch64":
np.testing.assert_allclose(contribs_csr_arr_re, contribs_dense, rtol=1, atol=1e-12)
else:
np.testing.assert_allclose(contribs_csr_arr_re, contribs_dense)
contribs_dense_re = contribs_dense.reshape(contribs_csr_array.shape)
assert np.linalg.norm(gbm.predict(X_test, raw_score=True) - np.sum(contribs_dense_re, axis=2)) < 1e-4
# validate using CSC matrix
X_test_csc = X_test.tocsc()
contribs_csc = gbm.predict(X_test_csc, pred_contrib=True)
assert isinstance(contribs_csc, list)
for perclass_contribs_csc in contribs_csc:
assert isspmatrix_csc(perclass_contribs_csc)
# validate the values are the same
contribs_csc_array = np.swapaxes(np.array([sparse_array.toarray() for sparse_array in contribs_csc]), 0, 1)
contribs_csc_array = contribs_csc_array.reshape(
(contribs_csc_array.shape[0], contribs_csc_array.shape[1] * contribs_csc_array.shape[2])
)
if platform.machine() == "aarch64":
np.testing.assert_allclose(contribs_csc_array, contribs_dense, rtol=1, atol=1e-12)
else:
np.testing.assert_allclose(contribs_csc_array, contribs_dense)
@pytest.mark.skipif(
BuildInfo.has_cuda, reason="Skip because int64 sparse matrix indices are not supported for CUDA version"
)
def test_predict_contrib_int64():
X, y = make_multilabel_classification(n_samples=100, sparse=True, n_features=5, n_classes=1, n_labels=2)
y = y.flatten()
X_train, X_test, y_train, _ = train_test_split(X, y, test_size=0.1, random_state=42)
X_test.indptr = X_test.indptr.astype(np.int64)
train_data = lgb.Dataset(X_train, label=y_train)
params = {
"objective": "binary",
"num_leaves": 7,
"min_data_in_bin": 1,
"min_data_in_leaf": 1,
"seed": 708,
"verbose": -1,
}
booster = lgb.train(params, train_set=train_data, num_boost_round=5)
preds = booster.predict(X_test, pred_contrib=True)
assert preds is not None
assert preds.shape[0] == X_test.shape[0]
assert preds.shape[1] == X_test.shape[1] + 1
@pytest.mark.skipif(psutil.virtual_memory().available / 1024 / 1024 / 1024 < 3, reason="not enough RAM")
def test_int32_max_sparse_contribs(rng):
params = {"objective": "binary"}
train_features = rng.uniform(size=(100, 1000))
train_targets = [0] * 50 + [1] * 50
lgb_train = lgb.Dataset(train_features, train_targets)
gbm = lgb.train(params, lgb_train, num_boost_round=2)
csr_input_shape = (3000000, 1000)
test_features = csr_matrix(csr_input_shape)
for i in range(0, csr_input_shape[0], csr_input_shape[0] // 6):
for j in range(0, 1000, 100):
test_features[i, j] = random.random()
y_pred_csr = gbm.predict(test_features, pred_contrib=True)
# Note there is an extra column added to the output for the expected value
csr_output_shape = (csr_input_shape[0], csr_input_shape[1] + 1)
assert y_pred_csr.shape == csr_output_shape
y_pred_csc = gbm.predict(test_features.tocsc(), pred_contrib=True)
# Note output CSC shape should be same as CSR output shape
assert y_pred_csc.shape == csr_output_shape
def test_sliced_data(rng):
def train_and_get_predictions(features, labels):
dataset = lgb.Dataset(features, label=labels)
lgb_params = {
"application": "binary",
"verbose": -1,
"min_data": 5,
}
gbm = lgb.train(
params=lgb_params,
train_set=dataset,
num_boost_round=10,
)
return gbm.predict(features)
num_samples = 100
features = rng.uniform(size=(num_samples, 5))
positive_samples = int(num_samples * 0.25)
labels = np.append(
np.ones(positive_samples, dtype=np.float32), np.zeros(num_samples - positive_samples, dtype=np.float32)
)
# test sliced labels
origin_pred = train_and_get_predictions(features, labels)
stacked_labels = np.column_stack((labels, np.ones(num_samples, dtype=np.float32)))
sliced_labels = stacked_labels[:, 0]
sliced_pred = train_and_get_predictions(features, sliced_labels)
np.testing.assert_allclose(origin_pred, sliced_pred)
# append some columns
stacked_features = np.column_stack((np.ones(num_samples, dtype=np.float32), features))
stacked_features = np.column_stack((np.ones(num_samples, dtype=np.float32), stacked_features))
stacked_features = np.column_stack((stacked_features, np.ones(num_samples, dtype=np.float32)))
stacked_features = np.column_stack((stacked_features, np.ones(num_samples, dtype=np.float32)))
# append some rows
stacked_features = np.concatenate((np.ones(9, dtype=np.float32).reshape((1, 9)), stacked_features), axis=0)
stacked_features = np.concatenate((np.ones(9, dtype=np.float32).reshape((1, 9)), stacked_features), axis=0)
stacked_features = np.concatenate((stacked_features, np.ones(9, dtype=np.float32).reshape((1, 9))), axis=0)
stacked_features = np.concatenate((stacked_features, np.ones(9, dtype=np.float32).reshape((1, 9))), axis=0)
# test sliced 2d matrix
sliced_features = stacked_features[2:102, 2:7]
assert np.all(sliced_features == features)
sliced_pred = train_and_get_predictions(sliced_features, sliced_labels)
np.testing.assert_allclose(origin_pred, sliced_pred)
# test sliced CSR
stacked_csr = csr_matrix(stacked_features)
sliced_csr = stacked_csr[2:102, 2:7]
assert np.all(sliced_csr == features)
sliced_pred = train_and_get_predictions(sliced_csr, sliced_labels)
np.testing.assert_allclose(origin_pred, sliced_pred)
def test_init_with_subset(tmp_path, rng):
data = rng.uniform(size=(50, 2))
y = [1] * 25 + [0] * 25
lgb_train = lgb.Dataset(data, y, free_raw_data=False)
subset_index_1 = rng.choice(a=np.arange(50), size=30, replace=False)
subset_data_1 = lgb_train.subset(subset_index_1)
subset_index_2 = rng.choice(a=np.arange(50), size=20, replace=False)
subset_data_2 = lgb_train.subset(subset_index_2)
params = {"objective": "binary", "verbose": -1}
init_gbm = lgb.train(params=params, train_set=subset_data_1, num_boost_round=10, keep_training_booster=True)
lgb.train(params=params, train_set=subset_data_2, num_boost_round=10, init_model=init_gbm)
assert lgb_train.get_data().shape[0] == 50
assert subset_data_1.get_data().shape[0] == 30
assert subset_data_2.get_data().shape[0] == 20
lgb_train_data = str(tmp_path / "lgb_train_data.bin")
lgb_train.save_binary(lgb_train_data)
lgb_train_from_file = lgb.Dataset(lgb_train_data, free_raw_data=False)
subset_data_3 = lgb_train_from_file.subset(subset_index_1)
subset_data_4 = lgb_train_from_file.subset(subset_index_2)
init_gbm_2 = lgb.train(params=params, train_set=subset_data_3, num_boost_round=10, keep_training_booster=True)
with np.testing.assert_raises_regex(lgb.basic.LightGBMError, "Unknown format of training data"):
lgb.train(params=params, train_set=subset_data_4, num_boost_round=10, init_model=init_gbm_2)
assert lgb_train_from_file.get_data() == lgb_train_data
assert subset_data_3.get_data() == lgb_train_data
assert subset_data_4.get_data() == lgb_train_data
def test_training_on_constructed_subset_without_params(rng):
X = rng.uniform(size=(100, 10))
y = rng.uniform(size=(100,))
lgb_data = lgb.Dataset(X, y)
subset_indices = [1, 2, 3, 4]
subset = lgb_data.subset(subset_indices).construct()
bst = lgb.train({}, subset, num_boost_round=1)
assert subset.get_params() == {}
assert subset.num_data() == len(subset_indices)
assert bst.current_iteration() == 1
def generate_trainset_for_monotone_constraints_tests(x3_to_category=True):
number_of_dpoints = 3000
rng = np.random.default_rng()
x1_positively_correlated_with_y = rng.uniform(size=number_of_dpoints)
x2_negatively_correlated_with_y = rng.uniform(size=number_of_dpoints)
x3_negatively_correlated_with_y = rng.uniform(size=number_of_dpoints)
x = np.column_stack(
(
x1_positively_correlated_with_y,
x2_negatively_correlated_with_y,
categorize(x3_negatively_correlated_with_y) if x3_to_category else x3_negatively_correlated_with_y,
)
)
zs = rng.normal(loc=0.0, scale=0.01, size=number_of_dpoints)
scales = 10.0 * (rng.uniform(size=6) + 0.5)
y = (
scales[0] * x1_positively_correlated_with_y
+ np.sin(scales[1] * np.pi * x1_positively_correlated_with_y)
- scales[2] * x2_negatively_correlated_with_y
- np.cos(scales[3] * np.pi * x2_negatively_correlated_with_y)
- scales[4] * x3_negatively_correlated_with_y
- np.cos(scales[5] * np.pi * x3_negatively_correlated_with_y)
+ zs
)
categorical_features = []
if x3_to_category:
categorical_features = [2]
return lgb.Dataset(x, label=y, categorical_feature=categorical_features, free_raw_data=False)
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Monotone constraints are not yet supported by CUDA version")
@pytest.mark.parametrize("test_with_categorical_variable", [True, False])
def test_monotone_constraints(test_with_categorical_variable):
def is_increasing(y):
return (np.diff(y) >= 0.0).all()
def is_decreasing(y):
return (np.diff(y) <= 0.0).all()
def is_non_monotone(y):
return (np.diff(y) < 0.0).any() and (np.diff(y) > 0.0).any()
def is_correctly_constrained(learner, x3_to_category=True):
iterations = 10
n = 1000
variable_x = np.linspace(0, 1, n).reshape((n, 1))
fixed_xs_values = np.linspace(0, 1, n)
for i in range(iterations):
fixed_x = fixed_xs_values[i] * np.ones((n, 1))
monotonically_increasing_x = np.column_stack((variable_x, fixed_x, fixed_x))
monotonically_increasing_y = learner.predict(monotonically_increasing_x)
monotonically_decreasing_x = np.column_stack((fixed_x, variable_x, fixed_x))
monotonically_decreasing_y = learner.predict(monotonically_decreasing_x)
non_monotone_x = np.column_stack(
(
fixed_x,
fixed_x,
categorize(variable_x) if x3_to_category else variable_x,
)
)
non_monotone_y = learner.predict(non_monotone_x)
if not (
is_increasing(monotonically_increasing_y)
and is_decreasing(monotonically_decreasing_y)
and is_non_monotone(non_monotone_y)
):
return False
return True
def are_interactions_enforced(gbm, feature_sets):
def parse_tree_features(gbm):
# trees start at position 1.
tree_str = gbm.model_to_string().split("Tree")[1:]
feature_sets = []
for tree in tree_str:
# split_features are in 4th line.
features = tree.splitlines()[3].split("=")[1].split(" ")
features = {f"Column_{f}" for f in features}
feature_sets.append(features)
return np.array(feature_sets)
def has_interaction(treef):
n = 0
for fs in feature_sets:
if len(treef.intersection(fs)) > 0:
n += 1
return n > 1
tree_features = parse_tree_features(gbm)
has_interaction_flag = np.array([has_interaction(treef) for treef in tree_features])
return not has_interaction_flag.any()
trainset = generate_trainset_for_monotone_constraints_tests(test_with_categorical_variable)
for test_with_interaction_constraints in [True, False]:
error_msg = (
f"Model not correctly constrained (test_with_interaction_constraints={test_with_interaction_constraints})"
)
for monotone_constraints_method in ["basic", "intermediate", "advanced"]:
params = {
"min_data": 20,
"num_leaves": 20,
"monotone_constraints": [1, -1, 0],
"monotone_constraints_method": monotone_constraints_method,
"use_missing": False,
}
if test_with_interaction_constraints:
params["interaction_constraints"] = [[0], [1], [2]]
constrained_model = lgb.train(params, trainset)
assert is_correctly_constrained(constrained_model, test_with_categorical_variable), error_msg
if test_with_interaction_constraints:
feature_sets = [["Column_0"], ["Column_1"], "Column_2"]
assert are_interactions_enforced(constrained_model, feature_sets)
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Monotone constraints are not yet supported by CUDA version")
def test_monotone_penalty():
def are_first_splits_non_monotone(tree, n, monotone_constraints):
if n <= 0:
return True
if "leaf_value" in tree:
return True
if monotone_constraints[tree["split_feature"]] != 0:
return False
return are_first_splits_non_monotone(
tree["left_child"], n - 1, monotone_constraints
) and are_first_splits_non_monotone(tree["right_child"], n - 1, monotone_constraints)
def are_there_monotone_splits(tree, monotone_constraints):
if "leaf_value" in tree:
return False
if monotone_constraints[tree["split_feature"]] != 0:
return True
return are_there_monotone_splits(tree["left_child"], monotone_constraints) or are_there_monotone_splits(
tree["right_child"], monotone_constraints
)
max_depth = 5
monotone_constraints = [1, -1, 0]
penalization_parameter = 2.0
trainset = generate_trainset_for_monotone_constraints_tests(x3_to_category=False)
for monotone_constraints_method in ["basic", "intermediate", "advanced"]:
params = {
"max_depth": max_depth,
"monotone_constraints": monotone_constraints,
"monotone_penalty": penalization_parameter,
"monotone_constraints_method": monotone_constraints_method,
}
constrained_model = lgb.train(params, trainset, 10)
dumped_model = constrained_model.dump_model()["tree_info"]
for tree in dumped_model:
assert are_first_splits_non_monotone(
tree["tree_structure"], int(penalization_parameter), monotone_constraints
)
assert are_there_monotone_splits(tree["tree_structure"], monotone_constraints)
# test if a penalty as high as the depth indeed prohibits all monotone splits
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Monotone constraints are not yet supported by CUDA version")
def test_monotone_penalty_max():
max_depth = 5
monotone_constraints = [1, -1, 0]
penalization_parameter = max_depth
trainset_constrained_model = generate_trainset_for_monotone_constraints_tests(x3_to_category=False)
x = trainset_constrained_model.data
y = trainset_constrained_model.label
x3_negatively_correlated_with_y = x[:, 2]
trainset_unconstrained_model = lgb.Dataset(x3_negatively_correlated_with_y.reshape(-1, 1), label=y)
params_constrained_model = {
"monotone_constraints": monotone_constraints,
"monotone_penalty": penalization_parameter,
"max_depth": max_depth,
"gpu_use_dp": True,
}
params_unconstrained_model = {
"max_depth": max_depth,
"gpu_use_dp": True,
}
unconstrained_model = lgb.train(params_unconstrained_model, trainset_unconstrained_model, 10)
unconstrained_model_predictions = unconstrained_model.predict(x3_negatively_correlated_with_y.reshape(-1, 1))
for monotone_constraints_method in ["basic", "intermediate", "advanced"]:
params_constrained_model["monotone_constraints_method"] = monotone_constraints_method
# The penalization is so high that the first 2 features should not be used here
constrained_model = lgb.train(params_constrained_model, trainset_constrained_model, 10)
# Check that a very high penalization is the same as not using the features at all
np_assert_array_equal(constrained_model.predict(x), unconstrained_model_predictions, strict=True)
def test_max_bin_by_feature():
col1 = np.arange(0, 100)[:, np.newaxis]
col2 = np.zeros((100, 1))
col2[20:] = 1
X = np.concatenate([col1, col2], axis=1)
y = np.arange(0, 100)
params = {
"objective": "regression_l2",
"verbose": -1,
"num_leaves": 100,
"min_data_in_leaf": 1,
"min_sum_hessian_in_leaf": 0,
"min_data_in_bin": 1,
"max_bin_by_feature": [100, 2],
}
lgb_data = lgb.Dataset(X, label=y)
est = lgb.train(params, lgb_data, num_boost_round=1)
assert len(np.unique(est.predict(X))) == 100
params["max_bin_by_feature"] = [2, 100]
lgb_data = lgb.Dataset(X, label=y)
est = lgb.train(params, lgb_data, num_boost_round=1)
assert len(np.unique(est.predict(X))) == 3
def test_small_max_bin(rng_fixed_seed):
y = rng_fixed_seed.choice([0, 1], 100)
x = np.ones((100, 1))
x[:30, 0] = -1
x[60:, 0] = 2
params = {"objective": "binary", "seed": 0, "min_data_in_leaf": 1, "verbose": -1, "max_bin": 2}
lgb_x = lgb.Dataset(x, label=y)
lgb.train(params, lgb_x, num_boost_round=5)
x[0, 0] = np.nan
params["max_bin"] = 3
lgb_x = lgb.Dataset(x, label=y)
lgb.train(params, lgb_x, num_boost_round=5)
def test_refit():
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {"objective": "binary", "metric": "binary_logloss", "verbose": -1, "min_data": 10}
lgb_train = lgb.Dataset(X_train, y_train)
gbm = lgb.train(params, lgb_train, num_boost_round=20)
err_pred = log_loss(y_test, gbm.predict(X_test))
new_gbm = gbm.refit(X_test, y_test)
new_err_pred = log_loss(y_test, new_gbm.predict(X_test))
assert err_pred > new_err_pred
def test_refit_with_one_tree_regression():
X, y = make_synthetic_regression(n_samples=1_000, n_features=2)
lgb_train = lgb.Dataset(X, label=y)
params = {"objective": "regression", "verbosity": -1}
model = lgb.train(params, lgb_train, num_boost_round=1)
model_refit = model.refit(X, y)
assert isinstance(model_refit, lgb.Booster)
def test_refit_with_one_tree_binary_classification():
X, y = load_breast_cancer(return_X_y=True)
lgb_train = lgb.Dataset(X, label=y)
params = {"objective": "binary", "verbosity": -1}
model = lgb.train(params, lgb_train, num_boost_round=1)
model_refit = model.refit(X, y)
assert isinstance(model_refit, lgb.Booster)
def test_refit_with_one_tree_multiclass_classification():
X, y = load_iris(return_X_y=True)
lgb_train = lgb.Dataset(X, y)
params = {"objective": "multiclass", "num_class": 3, "verbose": -1}
model = lgb.train(params, lgb_train, num_boost_round=1)
model_refit = model.refit(X, y)
assert isinstance(model_refit, lgb.Booster)
def test_refit_dataset_params(rng):
# check refit accepts dataset_params
X, y = load_breast_cancer(return_X_y=True)
lgb_train = lgb.Dataset(X, y, init_score=np.zeros(y.size))
train_params = {"objective": "binary", "verbose": -1, "seed": 123}
gbm = lgb.train(train_params, lgb_train, num_boost_round=10)
non_weight_err_pred = log_loss(y, gbm.predict(X))
refit_weight = rng.uniform(size=(y.shape[0],))
dataset_params = {
"max_bin": 260,
"min_data_in_bin": 5,
"data_random_seed": 123,
}
new_gbm = gbm.refit(
data=X,
label=y,
weight=refit_weight,
dataset_params=dataset_params,
decay_rate=0.0,
)
weight_err_pred = log_loss(y, new_gbm.predict(X))
train_set_params = new_gbm.train_set.get_params()
stored_weights = new_gbm.train_set.get_weight()
assert weight_err_pred != non_weight_err_pred
assert train_set_params["max_bin"] == 260
assert train_set_params["min_data_in_bin"] == 5
assert train_set_params["data_random_seed"] == 123
np.testing.assert_allclose(stored_weights, refit_weight)
@pytest.mark.parametrize("boosting_type", ["rf", "dart"])
def test_mape_for_specific_boosting_types(boosting_type):
X, y = make_synthetic_regression()
y = abs(y)
params = {
"boosting_type": boosting_type,
"objective": "mape",
"verbose": -1,
"bagging_freq": 1,
"bagging_fraction": 0.8,
"feature_fraction": 0.8,
"boost_from_average": True,
}
lgb_train = lgb.Dataset(X, y)
gbm = lgb.train(params, lgb_train, num_boost_round=20)
pred = gbm.predict(X)
pred_mean = pred.mean()
# the following checks that dart and rf with mape can predict outside the 0-1 range
# https://github.com/lightgbm-org/LightGBM/issues/1579
# Threshold is intentionally loose (>5) because fixing the
# WeightedPercentileFun segment bug (#7151) shifted the output of MAPE
# training. The intent of this assertion is to guard against predictions
# being stuck inside [0, 1]; a mean around 6-8 satisfies that.
assert pred_mean > 5
def check_constant_features(y_true, expected_pred, more_params):
X_train = np.ones((len(y_true), 1))
y_train = np.array(y_true)
params = {
"objective": "regression",
"num_class": 1,
"verbose": -1,
"min_data": 1,
"num_leaves": 2,
"learning_rate": 1,
"min_data_in_bin": 1,
"boost_from_average": True,
}
params.update(more_params)
lgb_train = lgb.Dataset(X_train, y_train, params=params)
gbm = lgb.train(params, lgb_train, num_boost_round=2)
pred = gbm.predict(X_train)
assert np.allclose(pred, expected_pred)
def test_constant_features_regression():
params = {"objective": "regression"}
check_constant_features([0.0, 10.0, 0.0, 10.0], 5.0, params)
check_constant_features([0.0, 1.0, 2.0, 3.0], 1.5, params)
check_constant_features([-1.0, 1.0, -2.0, 2.0], 0.0, params)
def test_constant_features_binary():
params = {"objective": "binary"}
check_constant_features([0.0, 10.0, 0.0, 10.0], 0.5, params)
check_constant_features([0.0, 1.0, 2.0, 3.0], 0.75, params)
def test_constant_features_multiclass():
params = {"objective": "multiclass", "num_class": 3}
check_constant_features([0.0, 1.0, 2.0, 0.0], [0.5, 0.25, 0.25], params)
check_constant_features([0.0, 1.0, 2.0, 1.0], [0.25, 0.5, 0.25], params)
def test_constant_features_multiclassova():
params = {"objective": "multiclassova", "num_class": 3}
check_constant_features([0.0, 1.0, 2.0, 0.0], [0.5, 0.25, 0.25], params)
check_constant_features([0.0, 1.0, 2.0, 1.0], [0.25, 0.5, 0.25], params)
def test_fpreproc():
def preprocess_data(dtrain, dtest, params):
train_data = dtrain.construct().get_data()
test_data = dtest.construct().get_data()
train_data[:, 0] += 1
test_data[:, 0] += 1
dtrain.label[-5:] = 3
dtest.label[-5:] = 3
dtrain = lgb.Dataset(train_data, dtrain.label)
dtest = lgb.Dataset(test_data, dtest.label, reference=dtrain)
params["num_class"] = 4
return dtrain, dtest, params
X, y = load_iris(return_X_y=True)
dataset = lgb.Dataset(X, y, free_raw_data=False)
params = {"objective": "multiclass", "num_class": 3, "verbose": -1}
results = lgb.cv(params, dataset, num_boost_round=10, fpreproc=preprocess_data)
assert "valid multi_logloss-mean" in results
assert len(results["valid multi_logloss-mean"]) == 10
def test_metrics():
X, y = load_digits(n_class=2, return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_valid = lgb.Dataset(X_test, y_test, reference=lgb_train)
evals_result = {}
params_dummy_obj_verbose = {"verbose": -1, "objective": dummy_obj}
params_obj_verbose = {"objective": "binary", "verbose": -1}
params_obj_metric_log_verbose = {"objective": "binary", "metric": "binary_logloss", "verbose": -1}
params_obj_metric_err_verbose = {"objective": "binary", "metric": "binary_error", "verbose": -1}
params_obj_metric_inv_verbose = {"objective": "binary", "metric": "invalid_metric", "verbose": -1}
params_obj_metric_quant_verbose = {"objective": "regression", "metric": "quantile", "verbose": 2}
params_obj_metric_multi_verbose = {
"objective": "binary",
"metric": ["binary_logloss", "binary_error"],
"verbose": -1,
}
params_obj_metric_none_verbose = {"objective": "binary", "metric": "None", "verbose": -1}
params_dummy_obj_metric_log_verbose = {"objective": dummy_obj, "metric": "binary_logloss", "verbose": -1}
params_dummy_obj_metric_err_verbose = {"objective": dummy_obj, "metric": "binary_error", "verbose": -1}
params_dummy_obj_metric_inv_verbose = {"objective": dummy_obj, "metric_types": "invalid_metric", "verbose": -1}
params_dummy_obj_metric_multi_verbose = {
"objective": dummy_obj,
"metric": ["binary_logloss", "binary_error"],
"verbose": -1,
}
params_dummy_obj_metric_none_verbose = {"objective": dummy_obj, "metric": "None", "verbose": -1}
def get_cv_result(params=params_obj_verbose, **kwargs):
return lgb.cv(params, lgb_train, num_boost_round=2, **kwargs)
def train_booster(params=params_obj_verbose, **kwargs):
lgb.train(
params,
lgb_train,
num_boost_round=2,
valid_sets=[lgb_valid],
callbacks=[lgb.record_evaluation(evals_result)],
**kwargs,
)
# no custom objective, no feval
# default metric
res = get_cv_result()
assert len(res) == 2
assert "valid binary_logloss-mean" in res
# non-default metric in params
res = get_cv_result(params=params_obj_metric_err_verbose)
assert len(res) == 2
assert "valid binary_error-mean" in res
# default metric in args
res = get_cv_result(metrics="binary_logloss")
assert len(res) == 2
assert "valid binary_logloss-mean" in res
# non-default metric in args
res = get_cv_result(metrics="binary_error")
assert len(res) == 2
assert "valid binary_error-mean" in res
# metric in args overwrites one in params
res = get_cv_result(params=params_obj_metric_inv_verbose, metrics="binary_error")
assert len(res) == 2
assert "valid binary_error-mean" in res
# metric in args overwrites one in params
res = get_cv_result(params=params_obj_metric_quant_verbose)
assert len(res) == 2
assert "valid quantile-mean" in res
# multiple metrics in params
res = get_cv_result(params=params_obj_metric_multi_verbose)
assert len(res) == 4
assert "valid binary_logloss-mean" in res
assert "valid binary_error-mean" in res
# multiple metrics in args
res = get_cv_result(metrics=["binary_logloss", "binary_error"])
assert len(res) == 4
assert "valid binary_logloss-mean" in res
assert "valid binary_error-mean" in res
# remove default metric by 'None' in list
res = get_cv_result(metrics=["None"])
assert len(res) == 0
# remove default metric by 'None' aliases
for na_alias in ("None", "na", "null", "custom"):
res = get_cv_result(metrics=na_alias)
assert len(res) == 0
# custom objective, no feval
# no default metric
res = get_cv_result(params=params_dummy_obj_verbose)
assert len(res) == 0
# metric in params
res = get_cv_result(params=params_dummy_obj_metric_err_verbose)
assert len(res) == 2
assert "valid binary_error-mean" in res
# metric in args
res = get_cv_result(params=params_dummy_obj_verbose, metrics="binary_error")
assert len(res) == 2
assert "valid binary_error-mean" in res
# metric in args overwrites its' alias in params
res = get_cv_result(params=params_dummy_obj_metric_inv_verbose, metrics="binary_error")
assert len(res) == 2
assert "valid binary_error-mean" in res
# multiple metrics in params
res = get_cv_result(params=params_dummy_obj_metric_multi_verbose)
assert len(res) == 4
assert "valid binary_logloss-mean" in res
assert "valid binary_error-mean" in res
# multiple metrics in args
res = get_cv_result(params=params_dummy_obj_verbose, metrics=["binary_logloss", "binary_error"])
assert len(res) == 4
assert "valid binary_logloss-mean" in res
assert "valid binary_error-mean" in res
# no custom objective, feval
# default metric with custom one
res = get_cv_result(feval=constant_metric)
assert len(res) == 4
assert "valid binary_logloss-mean" in res
assert "valid error-mean" in res
# non-default metric in params with custom one
res = get_cv_result(params=params_obj_metric_err_verbose, feval=constant_metric)
assert len(res) == 4
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# default metric in args with custom one
res = get_cv_result(metrics="binary_logloss", feval=constant_metric)
assert len(res) == 4
assert "valid binary_logloss-mean" in res
assert "valid error-mean" in res
# default metric in args with 1 custom function returning a list of 2 metrics
res = get_cv_result(metrics="binary_logloss", feval=constant_metric_multi)
assert len(res) == 6
assert "valid binary_logloss-mean" in res
assert res["valid important_metric-mean"] == [1.5, 1.5]
assert res["valid irrelevant_metric-mean"] == [7.8, 7.8]
# non-default metric in args with custom one
res = get_cv_result(metrics="binary_error", feval=constant_metric)
assert len(res) == 4
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# metric in args overwrites one in params, custom one is evaluated too
res = get_cv_result(params=params_obj_metric_inv_verbose, metrics="binary_error", feval=constant_metric)
assert len(res) == 4
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# multiple metrics in params with custom one
res = get_cv_result(params=params_obj_metric_multi_verbose, feval=constant_metric)
assert len(res) == 6
assert "valid binary_logloss-mean" in res
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# multiple metrics in args with custom one
res = get_cv_result(metrics=["binary_logloss", "binary_error"], feval=constant_metric)
assert len(res) == 6
assert "valid binary_logloss-mean" in res
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# custom metric is evaluated despite 'None' is passed
res = get_cv_result(metrics=["None"], feval=constant_metric)
assert len(res) == 2
assert "valid error-mean" in res
# custom objective, feval
# no default metric, only custom one
res = get_cv_result(params=params_dummy_obj_verbose, feval=constant_metric)
assert len(res) == 2
assert "valid error-mean" in res
# metric in params with custom one
res = get_cv_result(params=params_dummy_obj_metric_err_verbose, feval=constant_metric)
assert len(res) == 4
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# metric in args with custom one
res = get_cv_result(params=params_dummy_obj_verbose, feval=constant_metric, metrics="binary_error")
assert len(res) == 4
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# metric in args overwrites one in params, custom one is evaluated too
res = get_cv_result(params=params_dummy_obj_metric_inv_verbose, feval=constant_metric, metrics="binary_error")
assert len(res) == 4
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# multiple metrics in params with custom one
res = get_cv_result(params=params_dummy_obj_metric_multi_verbose, feval=constant_metric)
assert len(res) == 6
assert "valid binary_logloss-mean" in res
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# multiple metrics in args with custom one
res = get_cv_result(
params=params_dummy_obj_verbose, feval=constant_metric, metrics=["binary_logloss", "binary_error"]
)
assert len(res) == 6
assert "valid binary_logloss-mean" in res
assert "valid binary_error-mean" in res
assert "valid error-mean" in res
# custom metric is evaluated despite 'None' is passed
res = get_cv_result(params=params_dummy_obj_metric_none_verbose, feval=constant_metric)
assert len(res) == 2
assert "valid error-mean" in res
# no custom objective, no feval
# default metric
train_booster()
assert len(evals_result["valid_0"]) == 1
assert "binary_logloss" in evals_result["valid_0"]
# default metric in params
train_booster(params=params_obj_metric_log_verbose)
assert len(evals_result["valid_0"]) == 1
assert "binary_logloss" in evals_result["valid_0"]
# non-default metric in params
train_booster(params=params_obj_metric_err_verbose)
assert len(evals_result["valid_0"]) == 1
assert "binary_error" in evals_result["valid_0"]
# multiple metrics in params
train_booster(params=params_obj_metric_multi_verbose)
assert len(evals_result["valid_0"]) == 2
assert "binary_logloss" in evals_result["valid_0"]
assert "binary_error" in evals_result["valid_0"]
# remove default metric by 'None' aliases
for na_alias in ("None", "na", "null", "custom"):
params = {"objective": "binary", "metric": na_alias, "verbose": -1}
train_booster(params=params)
assert len(evals_result) == 0
# custom objective, no feval
# no default metric
train_booster(params=params_dummy_obj_verbose)
assert len(evals_result) == 0
# metric in params
train_booster(params=params_dummy_obj_metric_log_verbose)
assert len(evals_result["valid_0"]) == 1
assert "binary_logloss" in evals_result["valid_0"]
# multiple metrics in params
train_booster(params=params_dummy_obj_metric_multi_verbose)
assert len(evals_result["valid_0"]) == 2
assert "binary_logloss" in evals_result["valid_0"]
assert "binary_error" in evals_result["valid_0"]
# no custom objective, feval
# default metric with custom one
train_booster(feval=constant_metric)
assert len(evals_result["valid_0"]) == 2
assert "binary_logloss" in evals_result["valid_0"]
assert "error" in evals_result["valid_0"]
# default metric in params with custom one
train_booster(params=params_obj_metric_log_verbose, feval=constant_metric)
assert len(evals_result["valid_0"]) == 2
assert "binary_logloss" in evals_result["valid_0"]
assert "error" in evals_result["valid_0"]
# default metric in params with custom function returning a list of 2 metrics
train_booster(params=params_obj_metric_log_verbose, feval=constant_metric_multi)
assert len(evals_result["valid_0"]) == 3
assert "binary_logloss" in evals_result["valid_0"]
assert evals_result["valid_0"]["important_metric"] == [1.5, 1.5]
assert evals_result["valid_0"]["irrelevant_metric"] == [7.8, 7.8]
# non-default metric in params with custom one
train_booster(params=params_obj_metric_err_verbose, feval=constant_metric)
assert len(evals_result["valid_0"]) == 2
assert "binary_error" in evals_result["valid_0"]
assert "error" in evals_result["valid_0"]
# multiple metrics in params with custom one
train_booster(params=params_obj_metric_multi_verbose, feval=constant_metric)
assert len(evals_result["valid_0"]) == 3
assert "binary_logloss" in evals_result["valid_0"]
assert "binary_error" in evals_result["valid_0"]
assert "error" in evals_result["valid_0"]
# custom metric is evaluated despite 'None' is passed
train_booster(params=params_obj_metric_none_verbose, feval=constant_metric)
assert len(evals_result) == 1
assert "error" in evals_result["valid_0"]
# custom objective, feval
# no default metric, only custom one
train_booster(params=params_dummy_obj_verbose, feval=constant_metric)
assert len(evals_result["valid_0"]) == 1
assert "error" in evals_result["valid_0"]
# metric in params with custom one
train_booster(params=params_dummy_obj_metric_log_verbose, feval=constant_metric)
assert len(evals_result["valid_0"]) == 2
assert "binary_logloss" in evals_result["valid_0"]
assert "error" in evals_result["valid_0"]
# multiple metrics in params with custom one
train_booster(params=params_dummy_obj_metric_multi_verbose, feval=constant_metric)
assert len(evals_result["valid_0"]) == 3
assert "binary_logloss" in evals_result["valid_0"]
assert "binary_error" in evals_result["valid_0"]
assert "error" in evals_result["valid_0"]
# custom metric is evaluated despite 'None' is passed
train_booster(params=params_dummy_obj_metric_none_verbose, feval=constant_metric)
assert len(evals_result) == 1
assert "error" in evals_result["valid_0"]
X, y = load_digits(n_class=3, return_X_y=True)
lgb_train = lgb.Dataset(X, y)
obj_multi_aliases = ["multiclass", "softmax", "multiclassova", "multiclass_ova", "ova", "ovr"]
for obj_multi_alias in obj_multi_aliases:
# Custom objective replaces multiclass
params_obj_class_3_verbose = {"objective": obj_multi_alias, "num_class": 3, "verbose": -1}
params_dummy_obj_class_3_verbose = {"objective": dummy_obj, "num_class": 3, "verbose": -1}
params_dummy_obj_class_1_verbose = {"objective": dummy_obj, "num_class": 1, "verbose": -1}
params_obj_verbose = {"objective": obj_multi_alias, "verbose": -1}
params_dummy_obj_verbose = {"objective": dummy_obj, "verbose": -1}
# multiclass default metric
res = get_cv_result(params_obj_class_3_verbose)
assert len(res) == 2
assert "valid multi_logloss-mean" in res
# multiclass default metric with custom one
res = get_cv_result(params_obj_class_3_verbose, feval=constant_metric)
assert len(res) == 4
assert "valid multi_logloss-mean" in res
assert "valid error-mean" in res
# multiclass metric alias with custom one for custom objective
res = get_cv_result(params_dummy_obj_class_3_verbose, feval=constant_metric)
assert len(res) == 2
assert "valid error-mean" in res
# no metric for invalid class_num
res = get_cv_result(params_dummy_obj_class_1_verbose)
assert len(res) == 0
# custom metric for invalid class_num
res = get_cv_result(params_dummy_obj_class_1_verbose, feval=constant_metric)
assert len(res) == 2
assert "valid error-mean" in res
# multiclass metric alias with custom one with invalid class_num
with pytest.raises(lgb.basic.LightGBMError, match="Multiclass objective and metrics don't match"):
get_cv_result(params_dummy_obj_class_1_verbose, metrics=obj_multi_alias, feval=constant_metric)
# multiclass default metric without num_class
with pytest.raises(
lgb.basic.LightGBMError,
match="Number of classes should be specified and greater than 1 for multiclass training",
):
get_cv_result(params_obj_verbose)
for metric_multi_alias in obj_multi_aliases + ["multi_logloss"]:
# multiclass metric alias
res = get_cv_result(params_obj_class_3_verbose, metrics=metric_multi_alias)
assert len(res) == 2
assert "valid multi_logloss-mean" in res
# multiclass metric
res = get_cv_result(params_obj_class_3_verbose, metrics="multi_error")
assert len(res) == 2
assert "valid multi_error-mean" in res
# non-valid metric for multiclass objective
with pytest.raises(lgb.basic.LightGBMError, match="Multiclass objective and metrics don't match"):
get_cv_result(params_obj_class_3_verbose, metrics="binary_logloss")
params_class_3_verbose = {"num_class": 3, "verbose": -1}
# non-default num_class for default objective
with pytest.raises(lgb.basic.LightGBMError, match="Number of classes must be 1 for non-multiclass training"):
get_cv_result(params_class_3_verbose)
# no metric with non-default num_class for custom objective
res = get_cv_result(params_dummy_obj_class_3_verbose)
assert len(res) == 0
for metric_multi_alias in obj_multi_aliases + ["multi_logloss"]:
# multiclass metric alias for custom objective
res = get_cv_result(params_dummy_obj_class_3_verbose, metrics=metric_multi_alias)
assert len(res) == 2
assert "valid multi_logloss-mean" in res
# multiclass metric for custom objective
res = get_cv_result(params_dummy_obj_class_3_verbose, metrics="multi_error")
assert len(res) == 2
assert "valid multi_error-mean" in res
# binary metric with non-default num_class for custom objective
with pytest.raises(lgb.basic.LightGBMError, match="Multiclass objective and metrics don't match"):
get_cv_result(params_dummy_obj_class_3_verbose, metrics="binary_error")
def test_multiple_feval_train():
X, y = load_breast_cancer(return_X_y=True)
params = {"verbose": -1, "objective": "binary", "metric": "binary_logloss"}
X_train, X_validation, y_train, y_validation = train_test_split(X, y, test_size=0.2)
train_dataset = lgb.Dataset(data=X_train, label=y_train)
validation_dataset = lgb.Dataset(data=X_validation, label=y_validation, reference=train_dataset)
evals_result = {}
lgb.train(
params=params,
train_set=train_dataset,
valid_sets=validation_dataset,
num_boost_round=5,
feval=[constant_metric, decreasing_metric],
callbacks=[lgb.record_evaluation(evals_result)],
)
assert len(evals_result["valid_0"]) == 3
assert "binary_logloss" in evals_result["valid_0"]
assert "error" in evals_result["valid_0"]
assert "decreasing_metric" in evals_result["valid_0"]
def test_objective_callable_train_binary_classification():
X, y = load_breast_cancer(return_X_y=True)
params = {"verbose": -1, "objective": logloss_obj, "learning_rate": 0.01}
train_dataset = lgb.Dataset(X, y)
booster = lgb.train(params=params, train_set=train_dataset, num_boost_round=20)
y_pred = logistic_sigmoid(booster.predict(X))
logloss_error = log_loss(y, y_pred)
rocauc_error = roc_auc_score(y, y_pred)
assert booster.params["objective"] == "none"
assert logloss_error == pytest.approx(0.547907)
assert rocauc_error == pytest.approx(0.995944)
def test_objective_callable_train_regression():
X, y = make_synthetic_regression()
params = {"verbose": -1, "objective": mse_obj}
lgb_train = lgb.Dataset(X, y)
booster = lgb.train(params, lgb_train, num_boost_round=20)
y_pred = booster.predict(X)
mse_error = mean_squared_error(y, y_pred)
assert booster.params["objective"] == "none"
assert mse_error == pytest.approx(286.724194)
def test_objective_callable_cv_binary_classification():
X, y = load_breast_cancer(return_X_y=True)
params = {"verbose": -1, "objective": logloss_obj, "learning_rate": 0.01}
train_dataset = lgb.Dataset(X, y)
cv_res = lgb.cv(params, train_dataset, num_boost_round=20, nfold=3, return_cvbooster=True)
cv_booster = cv_res["cvbooster"].boosters
cv_logloss_errors = [log_loss(y, logistic_sigmoid(cb.predict(X))) < 0.56 for cb in cv_booster]
cv_objs = [cb.params["objective"] == "none" for cb in cv_booster]
assert all(cv_objs)
assert all(cv_logloss_errors)
def test_objective_callable_cv_regression():
X, y = make_synthetic_regression()
lgb_train = lgb.Dataset(X, y)
params = {"verbose": -1, "objective": mse_obj}
cv_res = lgb.cv(params, lgb_train, num_boost_round=20, nfold=3, stratified=False, return_cvbooster=True)
cv_booster = cv_res["cvbooster"].boosters
cv_mse_errors = [mean_squared_error(y, cb.predict(X)) < 504 for cb in cv_booster]
cv_objs = [cb.params["objective"] == "none" for cb in cv_booster]
assert all(cv_objs)
assert all(cv_mse_errors)
def test_multiple_feval_cv():
X, y = load_breast_cancer(return_X_y=True)
params = {"verbose": -1, "objective": "binary", "metric": "binary_logloss"}
train_dataset = lgb.Dataset(data=X, label=y)
cv_results = lgb.cv(
params=params, train_set=train_dataset, num_boost_round=5, feval=[constant_metric, decreasing_metric]
)
# Expect three metrics but mean and stdv for each metric
assert len(cv_results) == 6
assert "valid binary_logloss-mean" in cv_results
assert "valid error-mean" in cv_results
assert "valid decreasing_metric-mean" in cv_results
assert "valid binary_logloss-stdv" in cv_results
assert "valid error-stdv" in cv_results
assert "valid decreasing_metric-stdv" in cv_results
def test_default_objective_and_metric():
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
train_dataset = lgb.Dataset(data=X_train, label=y_train)
validation_dataset = lgb.Dataset(data=X_test, label=y_test, reference=train_dataset)
evals_result = {}
params = {"verbose": -1}
lgb.train(
params=params,
train_set=train_dataset,
valid_sets=validation_dataset,
num_boost_round=5,
callbacks=[lgb.record_evaluation(evals_result)],
)
assert "valid_0" in evals_result
assert len(evals_result["valid_0"]) == 1
assert "l2" in evals_result["valid_0"]
assert len(evals_result["valid_0"]["l2"]) == 5
@pytest.mark.parametrize("use_weight", [True, False])
def test_multiclass_custom_objective(use_weight):
def custom_obj(y_pred, ds):
y_true = ds.get_label()
weight = ds.get_weight()
grad, hess = sklearn_multiclass_custom_objective(y_true, y_pred, weight)
return grad, hess
centers = [[-4, -4], [4, 4], [-4, 4]]
X, y = make_blobs(n_samples=1_000, centers=centers, random_state=42)
weight = np.full_like(y, 2)
ds = lgb.Dataset(X, y)
if use_weight:
ds.set_weight(weight)
params = {"objective": "multiclass", "num_class": 3, "num_leaves": 7}
builtin_obj_bst = lgb.train(params, ds, num_boost_round=10)
builtin_obj_preds = builtin_obj_bst.predict(X)
params["objective"] = custom_obj
custom_obj_bst = lgb.train(params, ds, num_boost_round=10)
custom_obj_preds = softmax(custom_obj_bst.predict(X))
np.testing.assert_allclose(builtin_obj_preds, custom_obj_preds, rtol=0.01)
@pytest.mark.parametrize("use_weight", [True, False])
def test_multiclass_custom_eval(use_weight):
def custom_eval(y_pred, ds):
y_true = ds.get_label()
weight = ds.get_weight() # weight is None when not set
loss = log_loss(y_true, y_pred, sample_weight=weight)
return "custom_logloss", loss, False
centers = [[-4, -4], [4, 4], [-4, 4]]
X, y = make_blobs(n_samples=1_000, centers=centers, random_state=42)
weight = np.full_like(y, 2)
X_train, X_valid, y_train, y_valid, weight_train, weight_valid = train_test_split(
X, y, weight, test_size=0.2, random_state=0
)
train_ds = lgb.Dataset(X_train, y_train)
valid_ds = lgb.Dataset(X_valid, y_valid, reference=train_ds)
if use_weight:
train_ds.set_weight(weight_train)
valid_ds.set_weight(weight_valid)
params = {"objective": "multiclass", "num_class": 3, "num_leaves": 7}
eval_result = {}
bst = lgb.train(
params,
train_ds,
num_boost_round=10,
valid_sets=[train_ds, valid_ds],
valid_names=["train", "valid"],
feval=custom_eval,
callbacks=[lgb.record_evaluation(eval_result)],
keep_training_booster=True,
)
for key, ds in zip(["train", "valid"], [train_ds, valid_ds], strict=True):
np.testing.assert_allclose(eval_result[key]["multi_logloss"], eval_result[key]["custom_logloss"])
_, metric, value, _ = bst.eval(ds, key, feval=custom_eval)[1] # first element is multi_logloss
assert metric == "custom_logloss"
np.testing.assert_allclose(value, eval_result[key][metric][-1])
@pytest.mark.skipif(psutil.virtual_memory().available / 1024 / 1024 / 1024 < 3, reason="not enough RAM")
def test_model_size():
X, y = make_synthetic_regression()
data = lgb.Dataset(X, y)
bst = lgb.train({"verbose": -1}, data, num_boost_round=2)
y_pred = bst.predict(X)
model_str = bst.model_to_string()
one_tree = model_str[model_str.find("Tree=1") : model_str.find("end of trees")]
one_tree_size = len(one_tree)
one_tree = one_tree.replace("Tree=1", "Tree={}")
multiplier = 100
total_trees = multiplier + 2
try:
before_tree_sizes = model_str[: model_str.find("tree_sizes")]
trees = model_str[model_str.find("Tree=0") : model_str.find("end of trees")]
more_trees = (one_tree * multiplier).format(*range(2, total_trees))
after_trees = model_str[model_str.find("end of trees") :]
num_end_spaces = 2**31 - one_tree_size * total_trees
new_model_str = f"{before_tree_sizes}\n\n{trees}{more_trees}{after_trees}{'':{num_end_spaces}}"
assert len(new_model_str) > 2**31
bst.model_from_string(new_model_str)
assert bst.num_trees() == total_trees
y_pred_new = bst.predict(X, num_iteration=2)
np.testing.assert_allclose(y_pred, y_pred_new)
except MemoryError:
pytest.skipTest("not enough RAM")
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Skip due to differences in implementation details of CUDA version")
def test_get_split_value_histogram(rng_fixed_seed):
X, y = make_synthetic_regression()
X = np.repeat(X, 3, axis=0)
y = np.repeat(y, 3, axis=0)
X[:, 2] = np.random.default_rng(0).integers(0, 20, size=X.shape[0])
lgb_train = lgb.Dataset(X, y, categorical_feature=[2])
gbm = lgb.train({"verbose": -1}, lgb_train, num_boost_round=20)
# test XGBoost-style return value
params = {"feature": 0, "xgboost_style": True}
assert gbm.get_split_value_histogram(**params).shape == (12, 2)
assert gbm.get_split_value_histogram(bins=999, **params).shape == (12, 2)
assert gbm.get_split_value_histogram(bins=-1, **params).shape == (1, 2)
assert gbm.get_split_value_histogram(bins=0, **params).shape == (1, 2)
assert gbm.get_split_value_histogram(bins=1, **params).shape == (1, 2)
assert gbm.get_split_value_histogram(bins=2, **params).shape == (2, 2)
assert gbm.get_split_value_histogram(bins=6, **params).shape == (6, 2)
assert gbm.get_split_value_histogram(bins=7, **params).shape == (7, 2)
if lgb.compat.PANDAS_INSTALLED:
np.testing.assert_allclose(
gbm.get_split_value_histogram(0, xgboost_style=True).values,
gbm.get_split_value_histogram(gbm.feature_name()[0], xgboost_style=True).values,
)
np.testing.assert_allclose(
gbm.get_split_value_histogram(X.shape[-1] - 1, xgboost_style=True).values,
gbm.get_split_value_histogram(gbm.feature_name()[X.shape[-1] - 1], xgboost_style=True).values,
)
else:
np.testing.assert_allclose(
gbm.get_split_value_histogram(0, xgboost_style=True),
gbm.get_split_value_histogram(gbm.feature_name()[0], xgboost_style=True),
)
np.testing.assert_allclose(
gbm.get_split_value_histogram(X.shape[-1] - 1, xgboost_style=True),
gbm.get_split_value_histogram(gbm.feature_name()[X.shape[-1] - 1], xgboost_style=True),
)
# test numpy-style return value
hist, bins = gbm.get_split_value_histogram(0)
assert len(hist) == 20
assert len(bins) == 21
hist, bins = gbm.get_split_value_histogram(0, bins=999)
assert len(hist) == 999
assert len(bins) == 1000
with pytest.raises(ValueError, match="`bins` must be positive, when an integer"):
gbm.get_split_value_histogram(0, bins=-1)
with pytest.raises(ValueError, match="`bins` must be positive, when an integer"):
gbm.get_split_value_histogram(0, bins=0)
hist, bins = gbm.get_split_value_histogram(0, bins=1)
assert len(hist) == 1
assert len(bins) == 2
hist, bins = gbm.get_split_value_histogram(0, bins=2)
assert len(hist) == 2
assert len(bins) == 3
hist, bins = gbm.get_split_value_histogram(0, bins=6)
assert len(hist) == 6
assert len(bins) == 7
hist, bins = gbm.get_split_value_histogram(0, bins=7)
assert len(hist) == 7
assert len(bins) == 8
hist_idx, bins_idx = gbm.get_split_value_histogram(0)
hist_name, bins_name = gbm.get_split_value_histogram(gbm.feature_name()[0])
np_assert_array_equal(hist_idx, hist_name, strict=True)
np.testing.assert_allclose(bins_idx, bins_name)
hist_idx, bins_idx = gbm.get_split_value_histogram(X.shape[-1] - 1)
hist_name, bins_name = gbm.get_split_value_histogram(gbm.feature_name()[X.shape[-1] - 1])
np_assert_array_equal(hist_idx, hist_name, strict=True)
np.testing.assert_allclose(bins_idx, bins_name)
# test bins string type
hist_vals, bin_edges = gbm.get_split_value_histogram(0, bins="auto")
hist = gbm.get_split_value_histogram(0, bins="auto", xgboost_style=True)
if lgb.compat.PANDAS_INSTALLED:
mask = hist_vals > 0
# strict=False due to dtype mismatch: 'int64' and 'float64'
np_assert_array_equal(hist_vals[mask], hist["Count"].values, strict=False)
np.testing.assert_allclose(bin_edges[1:][mask], hist["SplitValue"].values)
else:
mask = hist_vals > 0
# strict=False due to dtype mismatch: 'int64' and 'float64'
np_assert_array_equal(hist_vals[mask], hist[:, 1], strict=False)
np.testing.assert_allclose(bin_edges[1:][mask], hist[:, 0])
# test histogram is disabled for categorical features
with pytest.raises(
lgb.basic.LightGBMError, match="Cannot compute split value histogram for the categorical feature"
):
gbm.get_split_value_histogram(2)
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Skip due to differences in implementation details of CUDA version")
def test_early_stopping_for_only_first_metric():
def metrics_combination_train_regression(valid_sets, metric_list, assumed_iteration, first_metric_only, feval=None):
params = {
"objective": "regression",
"learning_rate": 1.1,
"num_leaves": 10,
"metric": metric_list,
"verbose": -1,
"seed": 123,
}
gbm = lgb.train(
params,
lgb_train,
num_boost_round=25,
valid_sets=valid_sets,
feval=feval,
callbacks=[lgb.early_stopping(stopping_rounds=5, first_metric_only=first_metric_only)],
)
assert assumed_iteration == gbm.best_iteration
def metrics_combination_cv_regression(
metric_list, assumed_iteration, first_metric_only, eval_train_metric, feval=None
):
params = {
"objective": "regression",
"learning_rate": 0.9,
"num_leaves": 10,
"metric": metric_list,
"verbose": -1,
"seed": 123,
"gpu_use_dp": True,
}
ret = lgb.cv(
params,
train_set=lgb_train,
num_boost_round=25,
stratified=False,
feval=feval,
callbacks=[lgb.early_stopping(stopping_rounds=5, first_metric_only=first_metric_only)],
eval_train_metric=eval_train_metric,
)
assert assumed_iteration == len(ret[list(ret.keys())[0]])
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
X_test1, X_test2, y_test1, y_test2 = train_test_split(X_test, y_test, test_size=0.5, random_state=73)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_valid1 = lgb.Dataset(X_test1, y_test1, reference=lgb_train)
lgb_valid2 = lgb.Dataset(X_test2, y_test2, reference=lgb_train)
iter_valid1_l1 = 3
iter_valid1_l2 = 3
iter_valid2_l1 = 3
iter_valid2_l2 = 15
assert len({iter_valid1_l1, iter_valid1_l2, iter_valid2_l1, iter_valid2_l2}) == 2
iter_min_l1 = min([iter_valid1_l1, iter_valid2_l1])
iter_min_l2 = min([iter_valid1_l2, iter_valid2_l2])
iter_min_valid1 = min([iter_valid1_l1, iter_valid1_l2])
iter_cv_l1 = 15
iter_cv_l2 = 13
assert len({iter_cv_l1, iter_cv_l2}) == 2
iter_cv_min = min([iter_cv_l1, iter_cv_l2])
# test for lgb.train
metrics_combination_train_regression(lgb_valid1, [], iter_valid1_l2, False)
metrics_combination_train_regression(lgb_valid1, [], iter_valid1_l2, True)
metrics_combination_train_regression(lgb_valid1, None, iter_valid1_l2, False)
metrics_combination_train_regression(lgb_valid1, None, iter_valid1_l2, True)
metrics_combination_train_regression(lgb_valid1, "l2", iter_valid1_l2, True)
metrics_combination_train_regression(lgb_valid1, "l1", iter_valid1_l1, True)
metrics_combination_train_regression(lgb_valid1, ["l2", "l1"], iter_valid1_l2, True)
metrics_combination_train_regression(lgb_valid1, ["l1", "l2"], iter_valid1_l1, True)
metrics_combination_train_regression(lgb_valid1, ["l2", "l1"], iter_min_valid1, False)
metrics_combination_train_regression(lgb_valid1, ["l1", "l2"], iter_min_valid1, False)
# test feval for lgb.train
metrics_combination_train_regression(
lgb_valid1,
"None",
1,
False,
feval=lambda preds, train_data: [decreasing_metric(preds, train_data), constant_metric(preds, train_data)],
)
metrics_combination_train_regression(
lgb_valid1,
"None",
25,
True,
feval=lambda preds, train_data: [decreasing_metric(preds, train_data), constant_metric(preds, train_data)],
)
metrics_combination_train_regression(
lgb_valid1,
"None",
1,
True,
feval=lambda preds, train_data: [constant_metric(preds, train_data), decreasing_metric(preds, train_data)],
)
# test with two valid data for lgb.train
metrics_combination_train_regression([lgb_valid1, lgb_valid2], ["l2", "l1"], iter_min_l2, True)
metrics_combination_train_regression([lgb_valid2, lgb_valid1], ["l2", "l1"], iter_min_l2, True)
metrics_combination_train_regression([lgb_valid1, lgb_valid2], ["l1", "l2"], iter_min_l1, True)
metrics_combination_train_regression([lgb_valid2, lgb_valid1], ["l1", "l2"], iter_min_l1, True)
# test for lgb.cv
metrics_combination_cv_regression(None, iter_cv_l2, True, False)
metrics_combination_cv_regression("l2", iter_cv_l2, True, False)
metrics_combination_cv_regression("l1", iter_cv_l1, True, False)
metrics_combination_cv_regression(["l2", "l1"], iter_cv_l2, True, False)
metrics_combination_cv_regression(["l1", "l2"], iter_cv_l1, True, False)
metrics_combination_cv_regression(["l2", "l1"], iter_cv_min, False, False)
metrics_combination_cv_regression(["l1", "l2"], iter_cv_min, False, False)
metrics_combination_cv_regression(None, iter_cv_l2, True, True)
metrics_combination_cv_regression("l2", iter_cv_l2, True, True)
metrics_combination_cv_regression("l1", iter_cv_l1, True, True)
metrics_combination_cv_regression(["l2", "l1"], iter_cv_l2, True, True)
metrics_combination_cv_regression(["l1", "l2"], iter_cv_l1, True, True)
metrics_combination_cv_regression(["l2", "l1"], iter_cv_min, False, True)
metrics_combination_cv_regression(["l1", "l2"], iter_cv_min, False, True)
# test feval for lgb.cv
metrics_combination_cv_regression(
"None",
1,
False,
False,
feval=lambda preds, train_data: [decreasing_metric(preds, train_data), constant_metric(preds, train_data)],
)
metrics_combination_cv_regression(
"None",
25,
True,
False,
feval=lambda preds, train_data: [decreasing_metric(preds, train_data), constant_metric(preds, train_data)],
)
metrics_combination_cv_regression(
"None",
1,
True,
False,
feval=lambda preds, train_data: [constant_metric(preds, train_data), decreasing_metric(preds, train_data)],
)
def test_node_level_subcol():
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
params = {
"objective": "binary",
"metric": "binary_logloss",
"feature_fraction_bynode": 0.8,
"feature_fraction": 1.0,
"verbose": -1,
}
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
evals_result = {}
gbm = lgb.train(
params, lgb_train, num_boost_round=25, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
ret = log_loss(y_test, gbm.predict(X_test))
assert ret < 0.14
assert evals_result["valid_0"]["binary_logloss"][-1] == pytest.approx(ret)
params["feature_fraction"] = 0.5
gbm2 = lgb.train(params, lgb_train, num_boost_round=25)
ret2 = log_loss(y_test, gbm2.predict(X_test))
assert ret != ret2
def test_forced_split_feature_indices(tmp_path):
X, y = make_synthetic_regression()
forced_split = {
"feature": 0,
"threshold": 0.5,
"left": {"feature": X.shape[1], "threshold": 0.5},
}
tmp_split_file = tmp_path / "forced_split.json"
with open(tmp_split_file, "w") as f:
f.write(json.dumps(forced_split))
lgb_train = lgb.Dataset(X, y)
params = {"objective": "regression", "forcedsplits_filename": tmp_split_file}
with pytest.raises(lgb.basic.LightGBMError, match="Forced splits file includes feature index"):
lgb.train(params, lgb_train)
def test_forced_bins():
x = np.empty((100, 2))
x[:, 0] = np.arange(0, 1, 0.01)
x[:, 1] = -np.arange(0, 1, 0.01)
y = np.arange(0, 1, 0.01)
forcedbins_filename = Path(__file__).absolute().parents[2] / "examples" / "regression" / "forced_bins.json"
params = {
"objective": "regression_l1",
"max_bin": 5,
"forcedbins_filename": forcedbins_filename,
"num_leaves": 2,
"min_data_in_leaf": 1,
"verbose": -1,
}
lgb_x = lgb.Dataset(x, label=y)
est = lgb.train(params, lgb_x, num_boost_round=20)
new_x = np.zeros((3, x.shape[1]))
new_x[:, 0] = [0.31, 0.37, 0.41]
predicted = est.predict(new_x)
assert len(np.unique(predicted)) == 3
new_x[:, 0] = [0, 0, 0]
new_x[:, 1] = [-0.9, -0.6, -0.3]
predicted = est.predict(new_x)
assert len(np.unique(predicted)) == 1
params["forcedbins_filename"] = ""
lgb_x = lgb.Dataset(x, label=y)
est = lgb.train(params, lgb_x, num_boost_round=20)
predicted = est.predict(new_x)
assert len(np.unique(predicted)) == 3
params["forcedbins_filename"] = (
Path(__file__).absolute().parents[2] / "examples" / "regression" / "forced_bins2.json"
)
params["max_bin"] = 11
lgb_x = lgb.Dataset(x[:, :1], label=y)
est = lgb.train(params, lgb_x, num_boost_round=50)
predicted = est.predict(x[1:, :1])
_, counts = np.unique(predicted, return_counts=True)
assert min(counts) >= 9
assert max(counts) <= 11
def test_binning_same_sign():
# test that binning works properly for features with only positive or only negative values
x = np.empty((99, 2))
x[:, 0] = np.arange(0.01, 1, 0.01)
x[:, 1] = -np.arange(0.01, 1, 0.01)
y = np.arange(0.01, 1, 0.01)
params = {
"objective": "regression_l1",
"max_bin": 5,
"num_leaves": 2,
"min_data_in_leaf": 1,
"verbose": -1,
"seed": 0,
}
lgb_x = lgb.Dataset(x, label=y)
est = lgb.train(params, lgb_x, num_boost_round=20)
new_x = np.zeros((3, 2))
new_x[:, 0] = [-1, 0, 1]
predicted = est.predict(new_x)
assert predicted[0] == pytest.approx(predicted[1])
assert predicted[1] != pytest.approx(predicted[2])
new_x = np.zeros((3, 2))
new_x[:, 1] = [-1, 0, 1]
predicted = est.predict(new_x)
assert predicted[0] != pytest.approx(predicted[1])
assert predicted[1] == pytest.approx(predicted[2])
def test_dataset_update_params(rng):
default_params = {
"max_bin": 100,
"max_bin_by_feature": [20, 10],
"bin_construct_sample_cnt": 10000,
"min_data_in_bin": 1,
"use_missing": False,
"zero_as_missing": False,
"categorical_feature": [0],
"feature_pre_filter": True,
"pre_partition": False,
"enable_bundle": True,
"data_random_seed": 0,
"is_enable_sparse": True,
"header": True,
"two_round": True,
"label_column": 0,
"weight_column": 0,
"group_column": 0,
"ignore_column": 0,
"min_data_in_leaf": 10,
"linear_tree": False,
"precise_float_parser": True,
"verbose": -1,
}
unchangeable_params = {
"max_bin": 150,
"max_bin_by_feature": [30, 5],
"bin_construct_sample_cnt": 5000,
"min_data_in_bin": 2,
"use_missing": True,
"zero_as_missing": True,
"categorical_feature": [0, 1],
"feature_pre_filter": False,
"pre_partition": True,
"enable_bundle": False,
"data_random_seed": 1,
"is_enable_sparse": False,
"header": False,
"two_round": False,
"label_column": 1,
"weight_column": 1,
"group_column": 1,
"ignore_column": 1,
"forcedbins_filename": "/some/path/forcedbins.json",
"min_data_in_leaf": 2,
"linear_tree": True,
"precise_float_parser": False,
}
X = rng.uniform(size=(100, 2))
y = rng.uniform(size=(100,))
# decreasing without freeing raw data is allowed
lgb_data = lgb.Dataset(X, y, params=default_params, free_raw_data=False).construct()
default_params["min_data_in_leaf"] -= 1
lgb.train(default_params, lgb_data, num_boost_round=3)
# decreasing before lazy init is allowed
lgb_data = lgb.Dataset(X, y, params=default_params)
default_params["min_data_in_leaf"] -= 1
lgb.train(default_params, lgb_data, num_boost_round=3)
# increasing is allowed
default_params["min_data_in_leaf"] += 2
lgb.train(default_params, lgb_data, num_boost_round=3)
# decreasing with disabled filter is allowed
default_params["feature_pre_filter"] = False
lgb_data = lgb.Dataset(X, y, params=default_params).construct()
default_params["min_data_in_leaf"] -= 4
lgb.train(default_params, lgb_data, num_boost_round=3)
# decreasing with enabled filter is disallowed;
# also changes of other params are disallowed
default_params["feature_pre_filter"] = True
lgb_data = lgb.Dataset(X, y, params=default_params).construct()
for key, value in unchangeable_params.items():
new_params = default_params.copy()
new_params[key] = value
if key != "forcedbins_filename":
param_name = key
else:
param_name = "forced bins"
err_msg = (
"Reducing `min_data_in_leaf` with `feature_pre_filter=true` may cause *"
if key == "min_data_in_leaf"
else f"Cannot change {param_name} *"
)
with np.testing.assert_raises_regex(lgb.basic.LightGBMError, err_msg):
lgb.train(new_params, lgb_data, num_boost_round=3)
def test_dataset_params_with_reference(rng):
default_params = {"max_bin": 100}
X = rng.uniform(size=(100, 2))
y = rng.uniform(size=(100,))
X_val = rng.uniform(size=(100, 2))
y_val = rng.uniform(size=(100,))
lgb_train = lgb.Dataset(X, y, params=default_params, free_raw_data=False).construct()
lgb_val = lgb.Dataset(X_val, y_val, reference=lgb_train, free_raw_data=False).construct()
assert lgb_train.get_params() == default_params
assert lgb_val.get_params() == default_params
lgb.train(default_params, lgb_train, valid_sets=[lgb_val])
def test_extra_trees():
# check extra trees increases regularization
X, y = make_synthetic_regression()
lgb_x = lgb.Dataset(X, label=y)
params = {"objective": "regression", "num_leaves": 32, "verbose": -1, "extra_trees": False, "seed": 0}
est = lgb.train(params, lgb_x, num_boost_round=10)
predicted = est.predict(X)
err = mean_squared_error(y, predicted)
params["extra_trees"] = True
est = lgb.train(params, lgb_x, num_boost_round=10)
predicted_new = est.predict(X)
err_new = mean_squared_error(y, predicted_new)
assert err < err_new
def test_path_smoothing():
# check path smoothing increases regularization
X, y = make_synthetic_regression()
lgb_x = lgb.Dataset(X, label=y)
params = {"objective": "regression", "num_leaves": 32, "verbose": -1, "seed": 0}
est = lgb.train(params, lgb_x, num_boost_round=10)
predicted = est.predict(X)
err = mean_squared_error(y, predicted)
params["path_smooth"] = 1
est = lgb.train(params, lgb_x, num_boost_round=10)
predicted_new = est.predict(X)
err_new = mean_squared_error(y, predicted_new)
assert err < err_new
def test_trees_to_dataframe(rng):
pytest.importorskip("pandas")
def _imptcs_to_numpy(X, impcts_dict):
cols = [f"Column_{i}" for i in range(X.shape[1])]
return [impcts_dict.get(col, 0.0) for col in cols]
X, y = load_breast_cancer(return_X_y=True)
data = lgb.Dataset(X, label=y)
num_trees = 10
bst = lgb.train({"objective": "binary", "verbose": -1}, data, num_trees)
tree_df = bst.trees_to_dataframe()
split_dict = tree_df[~tree_df["split_gain"].isnull()].groupby("split_feature").size().to_dict()
gains_dict = tree_df.groupby("split_feature")["split_gain"].sum().to_dict()
tree_split = _imptcs_to_numpy(X, split_dict)
tree_gains = _imptcs_to_numpy(X, gains_dict)
mod_split = bst.feature_importance("split")
mod_gains = bst.feature_importance("gain")
num_trees_from_df = tree_df["tree_index"].nunique()
obs_counts_from_df = tree_df.loc[tree_df["node_depth"] == 1, "count"].values
np.testing.assert_equal(tree_split, mod_split)
np.testing.assert_allclose(tree_gains, mod_gains)
assert num_trees_from_df == num_trees
np.testing.assert_equal(obs_counts_from_df, len(y))
# test edge case with one leaf
X = np.ones((10, 2))
y = rng.uniform(size=(10,))
data = lgb.Dataset(X, label=y)
bst = lgb.train({"objective": "binary", "verbose": -1}, data, num_trees)
tree_df = bst.trees_to_dataframe()
assert len(tree_df) == 1
assert tree_df.loc[0, "tree_index"] == 0
assert tree_df.loc[0, "node_depth"] == 1
assert tree_df.loc[0, "node_index"] == "0-L0"
assert tree_df.loc[0, "value"] is not None
for col in (
"left_child",
"right_child",
"parent_index",
"split_feature",
"split_gain",
"threshold",
"decision_type",
"missing_direction",
"missing_type",
"weight",
"count",
):
assert tree_df.loc[0, col] is None
def test_interaction_constraints():
X, y = make_synthetic_regression(n_samples=200)
num_features = X.shape[1]
train_data = lgb.Dataset(X, label=y)
# check that constraint containing all features is equivalent to no constraint
params = {"verbose": -1, "seed": 0}
est = lgb.train(params, train_data, num_boost_round=10)
pred1 = est.predict(X)
est = lgb.train(dict(params, interaction_constraints=[list(range(num_features))]), train_data, num_boost_round=10)
pred2 = est.predict(X)
np.testing.assert_allclose(pred1, pred2)
# check that constraint partitioning the features reduces train accuracy
est = lgb.train(dict(params, interaction_constraints=[[0, 2], [1, 3]]), train_data, num_boost_round=10)
pred3 = est.predict(X)
assert mean_squared_error(y, pred1) < mean_squared_error(y, pred3)
# check that constraints consisting of single features reduce accuracy further
est = lgb.train(
dict(params, interaction_constraints=[[i] for i in range(num_features)]), train_data, num_boost_round=10
)
pred4 = est.predict(X)
assert mean_squared_error(y, pred3) < mean_squared_error(y, pred4)
# test that interaction constraints work when not all features are used
X = np.concatenate([np.zeros((X.shape[0], 1)), X], axis=1)
num_features = X.shape[1]
train_data = lgb.Dataset(X, label=y)
est = lgb.train(
dict(params, interaction_constraints=[[0] + list(range(2, num_features)), [1] + list(range(2, num_features))]),
train_data,
num_boost_round=10,
)
def test_linear_trees_num_threads(rng_fixed_seed):
# check that number of threads does not affect result
x = np.arange(0, 1000, 0.1)
y = 2 * x + rng_fixed_seed.normal(loc=0, scale=0.1, size=(len(x),))
x = x[:, np.newaxis]
lgb_train = lgb.Dataset(x, label=y)
params = {"verbose": -1, "objective": "regression", "seed": 0, "linear_tree": True, "num_threads": 2}
est = lgb.train(params, lgb_train, num_boost_round=100)
pred1 = est.predict(x)
params["num_threads"] = 4
est = lgb.train(params, lgb_train, num_boost_round=100)
pred2 = est.predict(x)
np.testing.assert_allclose(pred1, pred2)
def test_linear_trees(tmp_path, rng_fixed_seed):
# check that setting linear_tree=True fits better than ordinary trees when data has linear relationship
x = np.arange(0, 100, 0.1)
y = 2 * x + rng_fixed_seed.normal(0, 0.1, len(x))
x = x[:, np.newaxis]
lgb_train = lgb.Dataset(x, label=y)
params = {"verbose": -1, "metric": "mse", "seed": 0, "num_leaves": 2}
est = lgb.train(params, lgb_train, num_boost_round=10)
pred1 = est.predict(x)
lgb_train = lgb.Dataset(x, label=y)
res = {}
est = lgb.train(
dict(params, linear_tree=True),
lgb_train,
num_boost_round=10,
valid_sets=[lgb_train],
valid_names=["train"],
callbacks=[lgb.record_evaluation(res)],
)
pred2 = est.predict(x)
assert res["train"]["l2"][-1] == pytest.approx(mean_squared_error(y, pred2), abs=1e-1)
assert mean_squared_error(y, pred2) < mean_squared_error(y, pred1)
# test again with nans in data
x[:10] = np.nan
lgb_train = lgb.Dataset(x, label=y)
est = lgb.train(params, lgb_train, num_boost_round=10)
pred1 = est.predict(x)
lgb_train = lgb.Dataset(x, label=y)
res = {}
est = lgb.train(
dict(params, linear_tree=True),
lgb_train,
num_boost_round=10,
valid_sets=[lgb_train],
valid_names=["train"],
callbacks=[lgb.record_evaluation(res)],
)
pred2 = est.predict(x)
assert res["train"]["l2"][-1] == pytest.approx(mean_squared_error(y, pred2), abs=1e-1)
assert mean_squared_error(y, pred2) < mean_squared_error(y, pred1)
# test again with bagging
res = {}
est = lgb.train(
dict(params, linear_tree=True, subsample=0.8, bagging_freq=1),
lgb_train,
num_boost_round=10,
valid_sets=[lgb_train],
valid_names=["train"],
callbacks=[lgb.record_evaluation(res)],
)
pred = est.predict(x)
assert res["train"]["l2"][-1] == pytest.approx(mean_squared_error(y, pred), abs=1e-1)
# test with a feature that has only one non-nan value
x = np.concatenate([np.ones([x.shape[0], 1]), x], 1)
x[500:, 1] = np.nan
y[500:] += 10
lgb_train = lgb.Dataset(x, label=y)
res = {}
est = lgb.train(
dict(params, linear_tree=True, subsample=0.8, bagging_freq=1),
lgb_train,
num_boost_round=10,
valid_sets=[lgb_train],
valid_names=["train"],
callbacks=[lgb.record_evaluation(res)],
)
pred = est.predict(x)
assert res["train"]["l2"][-1] == pytest.approx(mean_squared_error(y, pred), abs=1e-1)
# test with a categorical feature
x[:250, 0] = 0
y[:250] += 10
lgb_train = lgb.Dataset(x, label=y, categorical_feature=[0])
est = lgb.train(
dict(params, linear_tree=True, subsample=0.8, bagging_freq=1),
lgb_train,
num_boost_round=10,
)
# test refit: same results on same data
est2 = est.refit(x, label=y)
p1 = est.predict(x)
p2 = est2.predict(x)
assert np.mean(np.abs(p1 - p2)) < 2
# test refit with save and load
temp_model = str(tmp_path / "temp_model.txt")
est.save_model(temp_model)
est2 = lgb.Booster(model_file=temp_model)
est2 = est2.refit(x, label=y)
p1 = est.predict(x)
p2 = est2.predict(x)
assert np.mean(np.abs(p1 - p2)) < 2
# test refit: different results training on different data
est3 = est.refit(x[:100, :], label=y[:100])
p3 = est3.predict(x)
assert np.mean(np.abs(p2 - p1)) > np.abs(np.max(p3 - p1))
# test when num_leaves - 1 < num_features and when num_leaves - 1 > num_features
X_train, _, y_train, _ = train_test_split(*load_breast_cancer(return_X_y=True), test_size=0.1, random_state=2)
params = {"linear_tree": True, "verbose": -1, "metric": "mse", "seed": 0}
train_data = lgb.Dataset(
X_train,
label=y_train,
params=dict(params, num_leaves=2),
categorical_feature=[0],
)
est = lgb.train(params, train_data, num_boost_round=10)
train_data = lgb.Dataset(
X_train,
label=y_train,
params=dict(params, num_leaves=60),
categorical_feature=[0],
)
est = lgb.train(params, train_data, num_boost_round=10)
def test_save_and_load_linear(tmp_path):
X_train, X_test, y_train, y_test = train_test_split(
*load_breast_cancer(return_X_y=True), test_size=0.1, random_state=2
)
X_train = np.concatenate([np.ones((X_train.shape[0], 1)), X_train], 1)
X_train[: X_train.shape[0] // 2, 0] = 0
y_train[: X_train.shape[0] // 2] = 1
params = {"linear_tree": True}
train_data_1 = lgb.Dataset(X_train, label=y_train, params=params, categorical_feature=[0])
est_1 = lgb.train(params, train_data_1, num_boost_round=10)
pred_1 = est_1.predict(X_train)
tmp_dataset = str(tmp_path / "temp_dataset.bin")
train_data_1.save_binary(tmp_dataset)
train_data_2 = lgb.Dataset(tmp_dataset)
est_2 = lgb.train(params, train_data_2, num_boost_round=10)
pred_2 = est_2.predict(X_train)
np.testing.assert_allclose(pred_1, pred_2)
model_file = str(tmp_path / "model.txt")
est_2.save_model(model_file)
est_3 = lgb.Booster(model_file=model_file)
pred_3 = est_3.predict(X_train)
np.testing.assert_allclose(pred_2, pred_3)
def test_linear_single_leaf():
X_train, y_train = load_breast_cancer(return_X_y=True)
train_data = lgb.Dataset(X_train, label=y_train)
params = {"objective": "binary", "linear_tree": True, "min_sum_hessian": 5000}
bst = lgb.train(params, train_data, num_boost_round=5)
y_pred = bst.predict(X_train)
assert log_loss(y_train, y_pred) < 0.661
def test_linear_raises_informative_errors_on_unsupported_params():
X, y = make_synthetic_regression()
with pytest.raises(lgb.basic.LightGBMError, match="Cannot use regression_l1 objective when fitting linear trees"):
lgb.train(
train_set=lgb.Dataset(X, label=y),
params={"linear_tree": True, "objective": "regression_l1"},
num_boost_round=1,
)
with pytest.raises(lgb.basic.LightGBMError, match="zero_as_missing must be false when fitting linear trees"):
lgb.train(
train_set=lgb.Dataset(X, label=y),
params={"linear_tree": True, "zero_as_missing": True},
num_boost_round=1,
)
def test_predict_with_start_iteration():
def inner_test(X, y, params, early_stopping_rounds):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
train_data = lgb.Dataset(X_train, label=y_train)
valid_data = lgb.Dataset(X_test, label=y_test)
callbacks = [lgb.early_stopping(early_stopping_rounds)] if early_stopping_rounds is not None else []
booster = lgb.train(params, train_data, num_boost_round=50, valid_sets=[valid_data], callbacks=callbacks)
# test that the predict once with all iterations equals summed results with start_iteration and num_iteration
all_pred = booster.predict(X, raw_score=True)
all_pred_contrib = booster.predict(X, pred_contrib=True)
steps = [10, 12]
for step in steps:
pred = np.zeros_like(all_pred)
pred_contrib = np.zeros_like(all_pred_contrib)
for start_iter in range(0, 50, step):
pred += booster.predict(X, start_iteration=start_iter, num_iteration=step, raw_score=True)
pred_contrib += booster.predict(X, start_iteration=start_iter, num_iteration=step, pred_contrib=True)
np.testing.assert_allclose(all_pred, pred)
np.testing.assert_allclose(all_pred_contrib, pred_contrib)
# test the case where start_iteration <= 0, and num_iteration is None
pred1 = booster.predict(X, start_iteration=-1)
pred2 = booster.predict(X, num_iteration=booster.best_iteration)
np.testing.assert_allclose(pred1, pred2)
# test the case where start_iteration > 0, and num_iteration <= 0
pred4 = booster.predict(X, start_iteration=10, num_iteration=-1)
pred5 = booster.predict(X, start_iteration=10, num_iteration=90)
pred6 = booster.predict(X, start_iteration=10, num_iteration=0)
np.testing.assert_allclose(pred4, pred5)
np.testing.assert_allclose(pred4, pred6)
# test the case where start_iteration > 0, and num_iteration <= 0, with pred_leaf=True
pred4 = booster.predict(X, start_iteration=10, num_iteration=-1, pred_leaf=True)
pred5 = booster.predict(X, start_iteration=10, num_iteration=40, pred_leaf=True)
pred6 = booster.predict(X, start_iteration=10, num_iteration=0, pred_leaf=True)
np.testing.assert_allclose(pred4, pred5)
np.testing.assert_allclose(pred4, pred6)
# test the case where start_iteration > 0, and num_iteration <= 0, with pred_contrib=True
pred4 = booster.predict(X, start_iteration=10, num_iteration=-1, pred_contrib=True)
pred5 = booster.predict(X, start_iteration=10, num_iteration=40, pred_contrib=True)
pred6 = booster.predict(X, start_iteration=10, num_iteration=0, pred_contrib=True)
np.testing.assert_allclose(pred4, pred5)
np.testing.assert_allclose(pred4, pred6)
# test for regression
X, y = make_synthetic_regression()
params = {"objective": "regression", "verbose": -1, "metric": "l2", "learning_rate": 0.5}
# test both with and without early stopping
inner_test(X, y, params, early_stopping_rounds=1)
inner_test(X, y, params, early_stopping_rounds=5)
inner_test(X, y, params, early_stopping_rounds=None)
# test for multi-class
X, y = load_iris(return_X_y=True)
params = {"objective": "multiclass", "num_class": 3, "verbose": -1, "metric": "multi_error"}
# test both with and without early stopping
inner_test(X, y, params, early_stopping_rounds=1)
inner_test(X, y, params, early_stopping_rounds=5)
inner_test(X, y, params, early_stopping_rounds=None)
# test for binary
X, y = load_breast_cancer(return_X_y=True)
params = {"objective": "binary", "verbose": -1, "metric": "auc"}
# test both with and without early stopping
inner_test(X, y, params, early_stopping_rounds=1)
inner_test(X, y, params, early_stopping_rounds=5)
inner_test(X, y, params, early_stopping_rounds=None)
@pytest.mark.parametrize("use_init_score", [False, True])
def test_predict_stump(rng, use_init_score):
X, y = load_breast_cancer(return_X_y=True)
dataset_kwargs = {"data": X, "label": y}
if use_init_score:
dataset_kwargs.update({"init_score": rng.uniform(size=y.shape)})
bst = lgb.train(
train_set=lgb.Dataset(**dataset_kwargs),
params={"objective": "binary", "min_data_in_leaf": X.shape[0]},
num_boost_round=5,
)
# checking prediction from 1 iteration and the whole model, to prevent bugs
# of the form "a model of n stumps predicts n * initial_score"
preds_1 = bst.predict(X, raw_score=True, num_iteration=1)
preds_all = bst.predict(X, raw_score=True)
if use_init_score:
# if init_score was provided, a model of stumps should predict all 0s
all_zeroes = np.full_like(preds_1, fill_value=0.0)
np.testing.assert_allclose(preds_1, all_zeroes)
np.testing.assert_allclose(preds_all, all_zeroes)
else:
# if init_score was not provided, prediction for a model of stumps should be
# the "average" of the labels
y_avg = np.log(y.mean() / (1.0 - y.mean()))
np.testing.assert_allclose(preds_1, np.full_like(preds_1, fill_value=y_avg))
np.testing.assert_allclose(preds_all, np.full_like(preds_all, fill_value=y_avg))
def test_predict_regression_output_shape():
n_samples = 1_000
n_features = 4
X, y = make_synthetic_regression(n_samples=n_samples, n_features=n_features)
dtrain = lgb.Dataset(X, label=y)
params = {"objective": "regression", "verbosity": -1}
# 1-round model
bst = lgb.train(params, dtrain, num_boost_round=1)
assert bst.predict(X).shape == (n_samples,)
assert bst.predict(X, raw_score=True).shape == (n_samples,)
assert bst.predict(X, pred_contrib=True).shape == (n_samples, n_features + 1)
assert bst.predict(X, pred_leaf=True).shape == (n_samples, 1)
# 2-round model
bst = lgb.train(params, dtrain, num_boost_round=2)
assert bst.predict(X).shape == (n_samples,)
assert bst.predict(X, raw_score=True).shape == (n_samples,)
assert bst.predict(X, pred_contrib=True).shape == (n_samples, n_features + 1)
assert bst.predict(X, pred_leaf=True).shape == (n_samples, 2)
def test_predict_binary_classification_output_shape():
n_samples = 1_000
n_features = 4
X, y = make_classification(n_samples=n_samples, n_features=n_features, n_classes=2)
dtrain = lgb.Dataset(X, label=y)
params = {"objective": "binary", "verbosity": -1}
# 1-round model
bst = lgb.train(params, dtrain, num_boost_round=1)
assert bst.predict(X).shape == (n_samples,)
assert bst.predict(X, raw_score=True).shape == (n_samples,)
assert bst.predict(X, pred_contrib=True).shape == (n_samples, n_features + 1)
assert bst.predict(X, pred_leaf=True).shape == (n_samples, 1)
# 2-round model
bst = lgb.train(params, dtrain, num_boost_round=2)
assert bst.predict(X).shape == (n_samples,)
assert bst.predict(X, raw_score=True).shape == (n_samples,)
assert bst.predict(X, pred_contrib=True).shape == (n_samples, n_features + 1)
assert bst.predict(X, pred_leaf=True).shape == (n_samples, 2)
def test_predict_multiclass_classification_output_shape():
n_samples = 1_000
n_features = 10
n_classes = 3
X, y = make_classification(n_samples=n_samples, n_features=n_features, n_classes=n_classes, n_informative=6)
dtrain = lgb.Dataset(X, label=y)
params = {"objective": "multiclass", "verbosity": -1, "num_class": n_classes}
# 1-round model
bst = lgb.train(params, dtrain, num_boost_round=1)
assert bst.predict(X).shape == (n_samples, n_classes)
assert bst.predict(X, raw_score=True).shape == (n_samples, n_classes)
assert bst.predict(X, pred_contrib=True).shape == (n_samples, n_classes * (n_features + 1))
assert bst.predict(X, pred_leaf=True).shape == (n_samples, n_classes)
# 2-round model
bst = lgb.train(params, dtrain, num_boost_round=2)
assert bst.predict(X).shape == (n_samples, n_classes)
assert bst.predict(X, raw_score=True).shape == (n_samples, n_classes)
assert bst.predict(X, pred_contrib=True).shape == (n_samples, n_classes * (n_features + 1))
assert bst.predict(X, pred_leaf=True).shape == (n_samples, n_classes * 2)
def test_average_precision_metric():
# test against sklearn average precision metric
X, y = load_breast_cancer(return_X_y=True)
params = {"objective": "binary", "metric": "average_precision", "verbose": -1}
res = {}
lgb_X = lgb.Dataset(X, label=y)
est = lgb.train(params, lgb_X, num_boost_round=10, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(res)])
ap = res["training"]["average_precision"][-1]
pred = est.predict(X)
sklearn_ap = average_precision_score(y, pred)
assert ap == pytest.approx(sklearn_ap)
# test that average precision is 1 where model predicts perfectly
y = y.copy()
y[:] = 1
lgb_X = lgb.Dataset(X, label=y)
lgb.train(params, lgb_X, num_boost_round=1, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(res)])
assert res["training"]["average_precision"][-1] == pytest.approx(1)
def test_r2_metric():
# test against sklearn R2 metric
X, y = make_synthetic_regression()
params = {"objective": "regression", "metric": "r2", "verbose": -1}
res = {}
train_data = lgb.Dataset(X, label=y)
est = lgb.train(
params, train_data, num_boost_round=1, valid_sets=[train_data], callbacks=[lgb.record_evaluation(res)]
)
r2 = res["training"]["r2"][-1]
pred = est.predict(X)
sklearn_r2 = r2_score(y, pred)
assert r2 == pytest.approx(sklearn_r2)
assert r2 != 0
assert r2 != 1
# test that R2 is 1 when y has no variance and the model predicts perfectly
y = y.copy()
y[:] = 1
lgb_X = lgb.Dataset(X, label=y)
lgb.train(params, lgb_X, num_boost_round=1, valid_sets=[lgb_X], callbacks=[lgb.record_evaluation(res)])
assert res["training"]["r2"][-1] == pytest.approx(1)
def test_reset_params_works_with_metric_num_class_and_boosting():
X, y = load_breast_cancer(return_X_y=True)
dataset_params = {"max_bin": 150}
booster_params = {
"objective": "multiclass",
"max_depth": 4,
"bagging_fraction": 0.8,
"metric": ["multi_logloss", "multi_error"],
"boosting": "gbdt",
"num_class": 5,
}
dtrain = lgb.Dataset(X, y, params=dataset_params)
bst = lgb.Booster(params=booster_params, train_set=dtrain)
expected_params = dict(dataset_params, **booster_params)
assert bst.params == expected_params
booster_params["bagging_fraction"] += 0.1
new_bst = bst.reset_parameter(booster_params)
expected_params = dict(dataset_params, **booster_params)
assert bst.params == expected_params
assert new_bst.params == expected_params
@pytest.mark.parametrize("linear_tree", [False, True])
def test_dump_model_stump(linear_tree):
X, y = load_breast_cancer(return_X_y=True)
train_data = lgb.Dataset(X, label=y)
params = {"objective": "binary", "verbose": -1, "linear_tree": linear_tree, "min_data_in_leaf": len(y)}
bst = lgb.train(params, train_data, num_boost_round=5)
dumped_model = bst.dump_model(num_iteration=5, start_iteration=0)
tree_structure = dumped_model["tree_info"][0]["tree_structure"]
assert len(dumped_model["tree_info"]) == 1
assert "leaf_value" in tree_structure
assert tree_structure["leaf_count"] == len(y)
def test_dump_model():
initial_score_offset = 57.5
X, y = make_synthetic_regression()
train_data = lgb.Dataset(X, label=y + initial_score_offset)
params = {
"objective": "regression",
"verbose": -1,
"boost_from_average": True,
}
bst = lgb.train(params, train_data, num_boost_round=5)
dumped_model = bst.dump_model(num_iteration=5, start_iteration=0)
dumped_model_str = str(dumped_model)
assert "leaf_features" not in dumped_model_str
assert "leaf_coeff" not in dumped_model_str
assert "leaf_const" not in dumped_model_str
assert "leaf_value" in dumped_model_str
assert "leaf_count" in dumped_model_str
for tree in dumped_model["tree_info"]:
assert tree["tree_structure"]["internal_value"] != 0
assert dumped_model["tree_info"][0]["tree_structure"]["internal_value"] == pytest.approx(
initial_score_offset, abs=1
)
assert_all_trees_valid(dumped_model)
def test_dump_model_linear():
X, y = load_breast_cancer(return_X_y=True)
params = {
"objective": "binary",
"verbose": -1,
"linear_tree": True,
}
train_data = lgb.Dataset(X, label=y)
bst = lgb.train(params, train_data, num_boost_round=5)
dumped_model = bst.dump_model(num_iteration=5, start_iteration=0)
assert_all_trees_valid(dumped_model)
dumped_model_str = str(dumped_model)
assert "leaf_features" in dumped_model_str
assert "leaf_coeff" in dumped_model_str
assert "leaf_const" in dumped_model_str
assert "leaf_value" in dumped_model_str
assert "leaf_count" in dumped_model_str
def test_dump_model_hook():
def hook(obj):
if "leaf_value" in obj:
obj["LV"] = obj["leaf_value"]
del obj["leaf_value"]
return obj
X, y = load_breast_cancer(return_X_y=True)
train_data = lgb.Dataset(X, label=y)
params = {"objective": "binary", "verbose": -1}
bst = lgb.train(params, train_data, num_boost_round=5)
dumped_model_str = str(bst.dump_model(5, 0, object_hook=hook))
assert "leaf_value" not in dumped_model_str
assert "LV" in dumped_model_str
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Forced splits are not yet supported by CUDA version")
def test_force_split_with_feature_fraction(tmp_path):
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
lgb_train = lgb.Dataset(X_train, y_train)
forced_split = {"feature": 0, "threshold": 0.5, "right": {"feature": 2, "threshold": 10.0}}
tmp_split_file = tmp_path / "forced_split.json"
with open(tmp_split_file, "w") as f:
f.write(json.dumps(forced_split))
params = {
"objective": "regression",
"feature_fraction": 0.6,
"force_col_wise": True,
"feature_fraction_seed": 1,
"forcedsplits_filename": tmp_split_file,
}
gbm = lgb.train(params, lgb_train)
ret = mean_absolute_error(y_test, gbm.predict(X_test))
assert ret < 15.7
tree_info = gbm.dump_model()["tree_info"]
assert len(tree_info) > 1
for tree in tree_info:
tree_structure = tree["tree_structure"]
assert tree_structure["split_feature"] == 0
def test_goss_boosting_and_strategy_equivalent():
X, y = make_synthetic_regression(n_samples=10_000, n_features=10, n_informative=5, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
base_params = {
"metric": "l2",
"verbose": -1,
"bagging_seed": 0,
"learning_rate": 0.05,
"num_threads": 1,
"force_row_wise": True,
"gpu_use_dp": True,
}
params1 = {**base_params, "boosting": "goss"}
evals_result1 = {}
lgb.train(
params1, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result1)]
)
params2 = {**base_params, "data_sample_strategy": "goss"}
evals_result2 = {}
lgb.train(
params2, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result2)]
)
assert evals_result1["valid_0"]["l2"] == evals_result2["valid_0"]["l2"]
def test_sample_strategy_with_boosting():
X, y = make_synthetic_regression(n_samples=10_000, n_features=10, n_informative=5, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
base_params = {
"metric": "l2",
"verbose": -1,
"num_threads": 1,
"force_row_wise": True,
"gpu_use_dp": True,
}
params1 = {**base_params, "boosting": "dart", "data_sample_strategy": "goss"}
evals_result = {}
gbm = lgb.train(
params1, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
eval_res1 = evals_result["valid_0"]["l2"][-1]
test_res1 = mean_squared_error(y_test, gbm.predict(X_test))
assert test_res1 == pytest.approx(3149.393862, abs=1.0)
assert eval_res1 == pytest.approx(test_res1)
params2 = {**base_params, "boosting": "gbdt", "data_sample_strategy": "goss"}
evals_result = {}
gbm = lgb.train(
params2, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
eval_res2 = evals_result["valid_0"]["l2"][-1]
test_res2 = mean_squared_error(y_test, gbm.predict(X_test))
assert test_res2 == pytest.approx(2547.715968, abs=1.0)
assert eval_res2 == pytest.approx(test_res2)
params3 = {**base_params, "boosting": "goss", "data_sample_strategy": "goss"}
evals_result = {}
gbm = lgb.train(
params3, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
eval_res3 = evals_result["valid_0"]["l2"][-1]
test_res3 = mean_squared_error(y_test, gbm.predict(X_test))
assert test_res3 == pytest.approx(2547.715968, abs=1.0)
assert eval_res3 == pytest.approx(test_res3)
params4 = {**base_params, "boosting": "rf", "data_sample_strategy": "goss"}
evals_result = {}
gbm = lgb.train(
params4, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
eval_res4 = evals_result["valid_0"]["l2"][-1]
test_res4 = mean_squared_error(y_test, gbm.predict(X_test))
assert test_res4 == pytest.approx(2095.538735, abs=1.0)
assert eval_res4 == pytest.approx(test_res4)
assert test_res1 != test_res2
assert eval_res1 != eval_res2
assert test_res2 == test_res3
assert eval_res2 == eval_res3
assert eval_res1 != eval_res4
assert test_res1 != test_res4
assert eval_res2 != eval_res4
assert test_res2 != test_res4
params5 = {
**base_params,
"boosting": "dart",
"data_sample_strategy": "bagging",
"bagging_freq": 1,
"bagging_fraction": 0.5,
}
evals_result = {}
gbm = lgb.train(
params5, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
eval_res5 = evals_result["valid_0"]["l2"][-1]
test_res5 = mean_squared_error(y_test, gbm.predict(X_test))
assert test_res5 == pytest.approx(3134.866931, abs=1.0)
assert eval_res5 == pytest.approx(test_res5)
params6 = {
**base_params,
"boosting": "gbdt",
"data_sample_strategy": "bagging",
"bagging_freq": 1,
"bagging_fraction": 0.5,
}
evals_result = {}
gbm = lgb.train(
params6, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
eval_res6 = evals_result["valid_0"]["l2"][-1]
test_res6 = mean_squared_error(y_test, gbm.predict(X_test))
assert test_res6 == pytest.approx(2539.792378, abs=1.0)
assert eval_res6 == pytest.approx(test_res6)
assert test_res5 != test_res6
assert eval_res5 != eval_res6
params7 = {
**base_params,
"boosting": "rf",
"data_sample_strategy": "bagging",
"bagging_freq": 1,
"bagging_fraction": 0.5,
}
evals_result = {}
gbm = lgb.train(
params7, lgb_train, num_boost_round=10, valid_sets=lgb_eval, callbacks=[lgb.record_evaluation(evals_result)]
)
eval_res7 = evals_result["valid_0"]["l2"][-1]
test_res7 = mean_squared_error(y_test, gbm.predict(X_test))
assert test_res7 == pytest.approx(1518.704481, abs=1.0)
assert eval_res7 == pytest.approx(test_res7)
assert test_res5 != test_res7
assert eval_res5 != eval_res7
assert test_res6 != test_res7
assert eval_res6 != eval_res7
def test_record_evaluation_with_train():
X, y = make_synthetic_regression()
ds = lgb.Dataset(X, y)
eval_result = {}
callbacks = [lgb.record_evaluation(eval_result)]
params = {"objective": "l2", "num_leaves": 3}
num_boost_round = 5
bst = lgb.train(params, ds, num_boost_round=num_boost_round, valid_sets=[ds], callbacks=callbacks)
assert list(eval_result.keys()) == ["training"]
train_mses = []
for i in range(num_boost_round):
pred = bst.predict(X, num_iteration=i + 1)
mse = mean_squared_error(y, pred)
train_mses.append(mse)
np.testing.assert_allclose(eval_result["training"]["l2"], train_mses)
@pytest.mark.parametrize("train_metric", [False, True])
def test_record_evaluation_with_cv(train_metric):
X, y = make_synthetic_regression()
ds = lgb.Dataset(X, y)
eval_result = {}
callbacks = [lgb.record_evaluation(eval_result)]
metrics = ["l2", "rmse"]
params = {"objective": "l2", "num_leaves": 3, "metric": metrics}
cv_hist = lgb.cv(
params, ds, num_boost_round=5, stratified=False, callbacks=callbacks, eval_train_metric=train_metric
)
expected_datasets = {"valid"}
if train_metric:
expected_datasets.add("train")
assert set(eval_result.keys()) == expected_datasets
for dataset in expected_datasets:
for metric in metrics:
for agg in ("mean", "stdv"):
key = f"{dataset} {metric}-{agg}"
np.testing.assert_allclose(cv_hist[key], eval_result[dataset][f"{metric}-{agg}"])
def test_pandas_with_numpy_regular_dtypes(rng_fixed_seed):
pd = pytest.importorskip("pandas")
uints = ["uint8", "uint16", "uint32", "uint64"]
ints = ["int8", "int16", "int32", "int64"]
bool_and_floats = ["bool", "float16", "float32", "float64"]
n_samples = 100
# data as float64
df = pd.DataFrame(
{
"x1": rng_fixed_seed.integers(low=0, high=2, size=n_samples),
"x2": rng_fixed_seed.integers(low=1, high=3, size=n_samples),
"x3": 10 * rng_fixed_seed.integers(low=1, high=3, size=n_samples),
"x4": 100 * rng_fixed_seed.integers(low=1, high=3, size=n_samples),
}
)
df = df.astype(np.float64)
y = df["x1"] * (df["x2"] + df["x3"] + df["x4"])
ds = lgb.Dataset(df, y)
params = {"objective": "l2", "num_leaves": 31, "min_child_samples": 1}
bst = lgb.train(params, ds, num_boost_round=5)
preds = bst.predict(df)
# test all features were used
assert bst.trees_to_dataframe()["split_feature"].nunique() == df.shape[1]
# test the score is better than predicting the mean
baseline = np.full_like(y, y.mean())
assert mean_squared_error(y, preds) < mean_squared_error(y, baseline)
# test all predictions are equal using different input dtypes
for target_dtypes in [uints, ints, bool_and_floats]:
df2 = df.astype({f"x{i}": dtype for i, dtype in enumerate(target_dtypes, start=1)})
assert df2.dtypes.tolist() == target_dtypes
ds2 = lgb.Dataset(df2, y)
bst2 = lgb.train(params, ds2, num_boost_round=5)
preds2 = bst2.predict(df2)
np.testing.assert_allclose(preds, preds2)
def test_pandas_nullable_dtypes(rng_fixed_seed):
pd = pytest.importorskip("pandas")
df = pd.DataFrame(
{
"x1": rng_fixed_seed.integers(low=1, high=3, size=100),
"x2": np.linspace(-1, 1, 100),
"x3": pd.arrays.SparseArray(rng_fixed_seed.integers(low=0, high=11, size=100)),
"x4": rng_fixed_seed.uniform(size=(100,)) < 0.5,
}
)
# introduce some missing values
df.loc[1, "x1"] = np.nan
df.loc[2, "x2"] = np.nan
# in recent versions of pandas, type 'bool' is incompatible with nan values in x4
df["x4"] = df["x4"].astype(np.float64)
df.loc[3, "x4"] = np.nan
y = df["x1"] * df["x2"] + df["x3"] * (1 + df["x4"])
y = y.fillna(0)
# train with regular dtypes
params = {"objective": "l2", "num_leaves": 31, "min_child_samples": 1}
ds = lgb.Dataset(df, y)
bst = lgb.train(params, ds, num_boost_round=5)
preds = bst.predict(df)
# convert to nullable dtypes
df2 = df.copy()
df2["x1"] = df2["x1"].astype("Int32")
df2["x2"] = df2["x2"].astype("Float64")
df2["x4"] = df2["x4"].astype("boolean")
# test training succeeds
ds_nullable_dtypes = lgb.Dataset(df2, y)
bst_nullable_dtypes = lgb.train(params, ds_nullable_dtypes, num_boost_round=5)
preds_nullable_dtypes = bst_nullable_dtypes.predict(df2)
trees_df = bst_nullable_dtypes.trees_to_dataframe()
# test all features were used
assert trees_df["split_feature"].nunique() == df.shape[1]
# test the score is better than predicting the mean
baseline = np.full_like(y, y.mean())
assert mean_squared_error(y, preds) < mean_squared_error(y, baseline)
# test equal predictions
np.testing.assert_allclose(preds, preds_nullable_dtypes)
def test_boost_from_average_with_single_leaf_trees():
# test data are taken from bug report
# https://github.com/lightgbm-org/LightGBM/issues/4708
X = np.array(
[
[1021.0589, 1018.9578],
[1023.85754, 1018.7854],
[1024.5468, 1018.88513],
[1019.02954, 1018.88513],
[1016.79926, 1018.88513],
[1007.6, 1018.88513],
],
dtype=np.float32,
)
y = np.array([1023.8, 1024.6, 1024.4, 1023.8, 1022.0, 1014.4], dtype=np.float32)
params = {
"extra_trees": True,
"min_data_in_bin": 1,
"extra_seed": 7,
"objective": "regression",
"verbose": -1,
"boost_from_average": True,
"min_data_in_leaf": 1,
}
train_set = lgb.Dataset(X, y)
model = lgb.train(params=params, train_set=train_set, num_boost_round=10)
preds = model.predict(X)
mean_preds = np.mean(preds)
assert y.min() <= mean_preds <= y.max()
def test_cegb_split_buffer_clean(rng_fixed_seed):
# modified from https://github.com/lightgbm-org/LightGBM/issues/3679#issuecomment-938652811
# and https://github.com/lightgbm-org/LightGBM/pull/5087
# test that the ``splits_per_leaf_`` of CEGB is cleaned before training a new tree
# which is done in the fix #5164
# without the fix:
# Check failed: (best_split_info.left_count) > (0)
R, C = 1000, 100
data = rng_fixed_seed.standard_normal(size=(R, C))
for i in range(1, C):
data[i] += data[0] * rng_fixed_seed.standard_normal()
N = int(0.8 * len(data))
train_data = data[:N]
test_data = data[N:]
train_y = np.sum(train_data, axis=1)
test_y = np.sum(test_data, axis=1)
train = lgb.Dataset(train_data, train_y, free_raw_data=True)
params = {
"boosting_type": "gbdt",
"objective": "regression",
"max_bin": 255,
"num_leaves": 31,
"seed": 0,
"learning_rate": 0.1,
"min_data_in_leaf": 0,
"verbose": -1,
"min_split_gain": 1000.0,
"cegb_penalty_feature_coupled": 5 * np.arange(C),
"cegb_penalty_split": 0.0002,
"cegb_tradeoff": 10.0,
"force_col_wise": True,
}
model = lgb.train(params, train, num_boost_round=10)
predicts = model.predict(test_data)
rmse = np.sqrt(mean_squared_error(test_y, predicts))
assert rmse < 10.0
def test_verbosity_and_verbose(capsys):
X, y = make_synthetic_regression()
ds = lgb.Dataset(X, y)
params = {
"num_leaves": 3,
"verbose": 1,
"verbosity": 0,
}
lgb.train(params, ds, num_boost_round=1)
expected_msg = "[LightGBM] [Warning] verbosity is set=0, verbose=1 will be ignored. Current value: verbosity=0"
stdout = capsys.readouterr().out
assert expected_msg in stdout
def test_verbosity_is_respected_when_using_custom_objective(capsys):
X, y = make_synthetic_regression()
ds = lgb.Dataset(X, y)
params = {
"objective": mse_obj,
"nonsense": 123,
"num_leaves": 3,
}
lgb.train({**params, "verbosity": -1}, ds, num_boost_round=1)
assert_silent(capsys)
lgb.train({**params, "verbosity": 0}, ds, num_boost_round=1)
assert "[LightGBM] [Warning] Unknown parameter: nonsense" in capsys.readouterr().out
@pytest.mark.parametrize("verbosity_param", lgb.basic._ConfigAliases.get("verbosity"))
@pytest.mark.parametrize("verbosity", [-1, 0])
def test_verbosity_can_suppress_alias_warnings(capsys, verbosity_param, verbosity):
X, y = make_synthetic_regression()
ds = lgb.Dataset(X, y)
params = {
"num_leaves": 3,
"subsample": 0.75,
"bagging_fraction": 0.8,
"force_col_wise": True,
verbosity_param: verbosity,
}
lgb.train(params, ds, num_boost_round=1)
expected_msg = (
"[LightGBM] [Warning] bagging_fraction is set=0.8, subsample=0.75 will be ignored. "
"Current value: bagging_fraction=0.8"
)
stdout = capsys.readouterr().out
if verbosity >= 0:
assert expected_msg in stdout
else:
assert re.search(r"\[LightGBM\]", stdout) is None
def test_cv_only_raises_num_rounds_warning_when_expected(capsys):
X, y = make_synthetic_regression()
ds = lgb.Dataset(X, y)
base_params = {
"num_leaves": 5,
"objective": "regression",
"verbosity": -1,
}
additional_kwargs = {"return_cvbooster": True, "stratified": False}
# no warning: no aliases, all defaults
cv_bst = lgb.cv({**base_params}, ds, **additional_kwargs)
assert all(t == 100 for t in cv_bst["cvbooster"].num_trees())
assert_silent(capsys)
# no warning: no aliases, just num_boost_round
cv_bst = lgb.cv({**base_params}, ds, num_boost_round=2, **additional_kwargs)
assert all(t == 2 for t in cv_bst["cvbooster"].num_trees())
assert_silent(capsys)
# no warning: 1 alias + num_boost_round (both same value)
cv_bst = lgb.cv({**base_params, "n_iter": 3}, ds, num_boost_round=3, **additional_kwargs)
assert all(t == 3 for t in cv_bst["cvbooster"].num_trees())
assert_silent(capsys)
# no warning: 1 alias + num_boost_round (different values... value from params should win)
cv_bst = lgb.cv({**base_params, "n_iter": 4}, ds, num_boost_round=3, **additional_kwargs)
assert all(t == 4 for t in cv_bst["cvbooster"].num_trees())
assert_silent(capsys)
# no warning: 2 aliases (both same value)
cv_bst = lgb.cv({**base_params, "n_iter": 3, "num_iterations": 3}, ds, **additional_kwargs)
assert all(t == 3 for t in cv_bst["cvbooster"].num_trees())
assert_silent(capsys)
# no warning: 4 aliases (all same value)
cv_bst = lgb.cv({**base_params, "n_iter": 3, "num_trees": 3, "nrounds": 3, "max_iter": 3}, ds, **additional_kwargs)
assert all(t == 3 for t in cv_bst["cvbooster"].num_trees())
assert_silent(capsys)
# warning: 2 aliases (different values... "num_iterations" wins because it's the main param name)
with pytest.warns(UserWarning, match="LightGBM will perform up to 5 boosting rounds"):
cv_bst = lgb.cv({**base_params, "n_iter": 6, "num_iterations": 5}, ds, **additional_kwargs)
assert all(t == 5 for t in cv_bst["cvbooster"].num_trees())
# should not be any other logs (except the warning, intercepted by pytest)
assert_silent(capsys)
# warning: 2 aliases (different values... first one in the order from Config::parameter2aliases() wins)
with pytest.warns(UserWarning, match="LightGBM will perform up to 4 boosting rounds"):
cv_bst = lgb.cv({**base_params, "n_iter": 4, "max_iter": 5}, ds, **additional_kwargs)["cvbooster"]
assert all(t == 4 for t in cv_bst.num_trees())
# should not be any other logs (except the warning, intercepted by pytest)
assert_silent(capsys)
def test_train_only_raises_num_rounds_warning_when_expected(capsys):
X, y = make_synthetic_regression()
ds = lgb.Dataset(X, y)
base_params = {
"num_leaves": 5,
"objective": "regression",
"verbosity": -1,
}
# no warning: no aliases, all defaults
bst = lgb.train({**base_params}, ds)
assert bst.num_trees() == 100
assert_silent(capsys)
# no warning: no aliases, just num_boost_round
bst = lgb.train({**base_params}, ds, num_boost_round=2)
assert bst.num_trees() == 2
assert_silent(capsys)
# no warning: 1 alias + num_boost_round (both same value)
bst = lgb.train({**base_params, "n_iter": 3}, ds, num_boost_round=3)
assert bst.num_trees() == 3
assert_silent(capsys)
# no warning: 1 alias + num_boost_round (different values... value from params should win)
bst = lgb.train({**base_params, "n_iter": 4}, ds, num_boost_round=3)
assert bst.num_trees() == 4
assert_silent(capsys)
# no warning: 2 aliases (both same value)
bst = lgb.train({**base_params, "n_iter": 3, "num_iterations": 3}, ds)
assert bst.num_trees() == 3
assert_silent(capsys)
# no warning: 4 aliases (all same value)
bst = lgb.train({**base_params, "n_iter": 3, "num_trees": 3, "nrounds": 3, "max_iter": 3}, ds)
assert bst.num_trees() == 3
assert_silent(capsys)
# warning: 2 aliases (different values... "num_iterations" wins because it's the main param name)
with pytest.warns(UserWarning, match="LightGBM will perform up to 5 boosting rounds"):
bst = lgb.train({**base_params, "n_iter": 6, "num_iterations": 5}, ds)
assert bst.num_trees() == 5
# should not be any other logs (except the warning, intercepted by pytest)
assert_silent(capsys)
# warning: 2 aliases (different values... first one in the order from Config::parameter2aliases() wins)
with pytest.warns(UserWarning, match="LightGBM will perform up to 4 boosting rounds"):
bst = lgb.train({**base_params, "n_iter": 4, "max_iter": 5}, ds)
assert bst.num_trees() == 4
# should not be any other logs (except the warning, intercepted by pytest)
assert_silent(capsys)
def test_validate_features():
pd = pytest.importorskip("pandas")
X, y = make_synthetic_regression()
features = ["x1", "x2", "x3", "x4"]
df = pd.DataFrame(X, columns=features)
ds = lgb.Dataset(df, y)
bst = lgb.train({"num_leaves": 15, "verbose": -1}, ds, num_boost_round=10)
assert bst.feature_name() == features
# try to predict with a different feature
df2 = df.rename(columns={"x3": "z"})
with pytest.raises(lgb.basic.LightGBMError, match="Expected 'x3' at position 2 but found 'z'"):
bst.predict(df2, validate_features=True)
# check that disabling the check doesn't raise the error
bst.predict(df2, validate_features=False)
# try to refit with a different feature
with pytest.raises(lgb.basic.LightGBMError, match="Expected 'x3' at position 2 but found 'z'"):
bst.refit(df2, y, validate_features=True)
# check that disabling the check doesn't raise the error
bst.refit(df2, y, validate_features=False)
def test_train_and_cv_raise_informative_error_for_train_set_of_wrong_type():
with pytest.raises(TypeError, match=r"train\(\) only accepts Dataset object, train_set has type 'list'\."):
lgb.train({}, train_set=[])
with pytest.raises(TypeError, match=r"cv\(\) only accepts Dataset object, train_set has type 'list'\."):
lgb.cv({}, train_set=[])
@pytest.mark.parametrize("num_boost_round", [-7, -1, 0])
def test_train_and_cv_raise_informative_error_for_impossible_num_boost_round(num_boost_round):
X, y = make_synthetic_regression(n_samples=100)
error_msg = rf"Number of boosting rounds must be greater than 0\. Got {num_boost_round}\."
with pytest.raises(ValueError, match=error_msg):
lgb.train({}, train_set=lgb.Dataset(X, y), num_boost_round=num_boost_round)
with pytest.raises(ValueError, match=error_msg):
lgb.cv({}, train_set=lgb.Dataset(X, y), num_boost_round=num_boost_round)
def test_train_raises_informative_error_if_any_valid_sets_are_not_dataset_objects():
X, y = make_synthetic_regression(n_samples=100)
X_valid = X * 2.0
with pytest.raises(
TypeError, match=r"Every item in valid_sets must be a Dataset object\. Item 1 has type 'tuple'\."
):
lgb.train(
params={},
train_set=lgb.Dataset(X, y),
valid_sets=[lgb.Dataset(X_valid, y), ([1.0], [2.0]), [5.6, 5.7, 5.8]],
)
def test_train_raises_informative_error_for_params_of_wrong_type():
X, y = make_synthetic_regression()
params = {"num_leaves": "too-many"}
dtrain = lgb.Dataset(X, label=y)
with pytest.raises(lgb.basic.LightGBMError, match='Parameter num_leaves should be of type int, got "too-many"'):
lgb.train(params, dtrain)
def test_quantized_training():
X, y = make_synthetic_regression()
ds = lgb.Dataset(X, label=y)
bst_params = {"num_leaves": 15, "verbose": -1, "seed": 0}
bst = lgb.train(bst_params, ds, num_boost_round=10)
rmse = np.sqrt(np.mean((bst.predict(X) - y) ** 2))
bst_params.update(
{
"use_quantized_grad": True,
"num_grad_quant_bins": 30,
"quant_train_renew_leaf": True,
}
)
quant_bst = lgb.train(bst_params, ds, num_boost_round=10)
quant_rmse = np.sqrt(np.mean((quant_bst.predict(X) - y) ** 2))
assert quant_rmse < rmse + 6.0
def test_bagging_by_query_in_lambdarank():
rank_example_dir = Path(__file__).absolute().parents[2] / "examples" / "lambdarank"
X_train, y_train = load_svmlight_file(str(rank_example_dir / "rank.train"))
q_train = np.loadtxt(str(rank_example_dir / "rank.train.query"))
X_test, y_test = load_svmlight_file(str(rank_example_dir / "rank.test"))
q_test = np.loadtxt(str(rank_example_dir / "rank.test.query"))
params = {"objective": "lambdarank", "verbose": -1, "metric": "ndcg", "ndcg_eval_at": [5]}
lgb_train = lgb.Dataset(X_train, y_train, group=q_train, params=params)
lgb_test = lgb.Dataset(X_test, y_test, group=q_test, params=params)
gbm = lgb.train(params, lgb_train, num_boost_round=50, valid_sets=[lgb_test])
ndcg_score = gbm.best_score["valid_0"]["ndcg@5"]
params.update({"bagging_by_query": True, "bagging_fraction": 0.1, "bagging_freq": 1})
gbm_bagging_by_query = lgb.train(params, lgb_train, num_boost_round=50, valid_sets=[lgb_test])
ndcg_score_bagging_by_query = gbm_bagging_by_query.best_score["valid_0"]["ndcg@5"]
params.update({"bagging_by_query": False, "bagging_fraction": 0.1, "bagging_freq": 1})
gbm_no_bagging_by_query = lgb.train(params, lgb_train, num_boost_round=50, valid_sets=[lgb_test])
ndcg_score_no_bagging_by_query = gbm_no_bagging_by_query.best_score["valid_0"]["ndcg@5"]
assert ndcg_score_bagging_by_query >= ndcg_score - 0.1
assert ndcg_score_no_bagging_by_query >= ndcg_score - 0.1
def test_equal_predict_from_row_major_and_col_major_data():
X_row, y = make_synthetic_regression()
assert X_row.flags["C_CONTIGUOUS"]
assert not X_row.flags["F_CONTIGUOUS"]
ds = lgb.Dataset(X_row, y)
params = {"num_leaves": 8, "verbose": -1}
bst = lgb.train(params, ds, num_boost_round=5)
preds_row = bst.predict(X_row)
X_col = np.asfortranarray(X_row)
assert X_col.flags["F_CONTIGUOUS"]
assert not X_col.flags["C_CONTIGUOUS"]
preds_col = bst.predict(X_col)
np.testing.assert_allclose(preds_row, preds_col)