From d72d1a58f0cd6f1e971c0b3eb27bf810f235d097 Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 13:27:18 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .appveyor.yml | 40 + .ci/README.md | 6 + .ci/append-comment.sh | 44 + .ci/build-docs.R | 19 + .ci/build-docs.sh | 13 + .ci/check-dynamic-dependencies.sh | 129 + .ci/check-omp-pragmas.sh | 35 + .ci/check-python-dists.sh | 58 + .ci/check-workflow-status.sh | 51 + .ci/conda-envs/README.md | 11 + .ci/conda-envs/ci-core.txt | 45 + .ci/create-nuget.py | 85 + .ci/download-artifacts.sh | 69 + .ci/install-opencl.ps1 | 40 + .ci/install-r-deps.R | 125 + .ci/lint-all.sh | 21 + .ci/lint-powershell.ps1 | 87 + .ci/lint-r-code.R | 154 + .ci/parameter-generator.py | 408 + .ci/pip-envs/requirements-latest.txt | 24 + .ci/pip-envs/requirements-oldest.txt | 15 + .ci/rerun-workflow.sh | 54 + .ci/run-r-cmd-check.sh | 46 + .ci/set-commit-status.sh | 54 + .ci/setup.sh | 192 + .ci/test-python-latest.sh | 18 + .ci/test-python-oldest.sh | 56 + .ci/test-r-package-valgrind.sh | 100 + .ci/test-r-package-windows.ps1 | 337 + .ci/test-r-package.sh | 260 + .ci/test-windows.ps1 | 190 + .ci/test.sh | 321 + .editorconfig | 19 + .git-blame-ignore-revs | 8 + .gitattributes | 34 + .github/CODEOWNERS | 10 + .github/ISSUE_TEMPLATE/BUG_REPORT.md | 26 + .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md | 25 + .github/dependabot.yml | 17 + .github/release-drafter.yml | 20 + .github/workflows/build.yml | 81 + .github/workflows/cpp.yml | 158 + .github/workflows/cuda.yml | 115 + .github/workflows/lock.yml | 58 + .github/workflows/lychee.yml | 54 + .github/workflows/no_response.yml | 40 + .github/workflows/optional_checks.yml | 44 + .github/workflows/python_package.yml | 534 + .github/workflows/r_configure.yml | 60 + .github/workflows/r_package.yml | 395 + .github/workflows/r_valgrind.yml | 106 + .github/workflows/release_drafter.yml | 29 + .github/workflows/static_analysis.yml | 122 + .github/workflows/swig.yml | 106 + .gitignore | 486 + .gitmodules | 15 + .pre-commit-config.yaml | 115 + .readthedocs.yaml | 16 + .typos.toml | 22 + .yamllint.yml | 10 + CMakeLists.txt | 866 + CODE_OF_CONDUCT.md | 41 + CONTRIBUTING.md | 28 + LICENSE | 22 + MAINTAINING.md | 130 + R-package/.Rbuildignore | 52 + R-package/AUTOCONF_UBUNTU_VERSION | 1 + R-package/DESCRIPTION | 66 + R-package/LICENSE | 2 + R-package/NAMESPACE | 66 + R-package/R/aliases.R | 104 + R-package/R/callback.R | 368 + R-package/R/lgb.Booster.R | 1575 ++ R-package/R/lgb.DataProcessor.R | 94 + R-package/R/lgb.Dataset.R | 1290 ++ R-package/R/lgb.Predictor.R | 529 + R-package/R/lgb.convert_with_rules.R | 207 + R-package/R/lgb.cv.R | 617 + R-package/R/lgb.drop_serialized.R | 21 + R-package/R/lgb.importance.R | 83 + R-package/R/lgb.interpret.R | 256 + R-package/R/lgb.make_serializable.R | 21 + R-package/R/lgb.model.dt.tree.R | 192 + R-package/R/lgb.plot.importance.R | 99 + R-package/R/lgb.plot.interpretation.R | 166 + R-package/R/lgb.restore_handle.R | 47 + R-package/R/lgb.train.R | 383 + R-package/R/lightgbm.R | 368 + R-package/R/metrics.R | 38 + R-package/R/multithreading.R | 51 + R-package/R/utils.R | 260 + R-package/README.md | 508 + R-package/cleanup | 6 + R-package/configure | 3072 ++++ R-package/configure.ac | 172 + R-package/configure.win | 102 + R-package/cran-comments.md | 962 ++ R-package/demo/00Index | 10 + R-package/demo/basic_walkthrough.R | 153 + R-package/demo/boost_from_prediction.R | 38 + R-package/demo/categorical_features_rules.R | 100 + R-package/demo/cross_validation.R | 72 + R-package/demo/early_stopping.R | 51 + R-package/demo/efficient_many_training.R | 36 + R-package/demo/leaf_stability.R | 256 + R-package/demo/multiclass.R | 70 + R-package/demo/multiclass_custom_objective.R | 117 + R-package/demo/weight_param.R | 104 + R-package/inst/Makevars | 1 + R-package/inst/make-r-def.R | 98 + R-package/man/agaricus.test.Rd | 31 + R-package/man/agaricus.train.Rd | 31 + R-package/man/bank.Rd | 26 + R-package/man/dim.Rd | 34 + R-package/man/dimnames.lgb.Dataset.Rd | 42 + R-package/man/figures/logo.svg | 41 + R-package/man/getLGBMThreads.Rd | 26 + R-package/man/get_field.Rd | 48 + R-package/man/lgb.Dataset.Rd | 82 + R-package/man/lgb.Dataset.construct.Rd | 27 + R-package/man/lgb.Dataset.create.valid.Rd | 95 + R-package/man/lgb.Dataset.save.Rd | 30 + R-package/man/lgb.Dataset.set.categorical.Rd | 35 + R-package/man/lgb.Dataset.set.reference.Rd | 35 + R-package/man/lgb.configure_fast_predict.Rd | 144 + R-package/man/lgb.convert_with_rules.Rd | 74 + R-package/man/lgb.cv.Rd | 164 + R-package/man/lgb.drop_serialized.Rd | 25 + R-package/man/lgb.dump.Rd | 54 + R-package/man/lgb.get.eval.result.Rd | 69 + R-package/man/lgb.importance.Rd | 51 + R-package/man/lgb.interpret.Rd | 63 + R-package/man/lgb.interprete.Rd | 14 + R-package/man/lgb.load.Rd | 52 + R-package/man/lgb.make_serializable.Rd | 25 + R-package/man/lgb.model.dt.tree.Rd | 67 + R-package/man/lgb.plot.importance.Rd | 64 + R-package/man/lgb.plot.interpretation.Rd | 81 + R-package/man/lgb.restore_handle.Rd | 54 + R-package/man/lgb.save.Rd | 56 + R-package/man/lgb.slice.Dataset.Rd | 35 + R-package/man/lgb.train.Rd | 153 + R-package/man/lgb_predict_shared_params.Rd | 57 + R-package/man/lgb_shared_dataset_params.Rd | 26 + R-package/man/lgb_shared_params.Rd | 115 + R-package/man/lightgbm.Rd | 150 + R-package/man/predict.lgb.Booster.Rd | 157 + R-package/man/print.lgb.Booster.Rd | 21 + R-package/man/setLGBMThreads.Rd | 32 + R-package/man/set_field.Rd | 50 + R-package/man/summary.lgb.Booster.Rd | 21 + R-package/pkgdown/_pkgdown.yml | 111 + .../favicon/apple-touch-icon-120x120.png | Bin 0 -> 9412 bytes .../favicon/apple-touch-icon-152x152.png | Bin 0 -> 12243 bytes .../favicon/apple-touch-icon-180x180.png | Bin 0 -> 14484 bytes .../favicon/apple-touch-icon-60x60.png | Bin 0 -> 3946 bytes .../favicon/apple-touch-icon-76x76.png | Bin 0 -> 5070 bytes .../pkgdown/favicon/apple-touch-icon.png | Bin 0 -> 14484 bytes R-package/pkgdown/favicon/favicon-16x16.png | Bin 0 -> 1227 bytes R-package/pkgdown/favicon/favicon-32x32.png | Bin 0 -> 2121 bytes R-package/pkgdown/favicon/favicon.ico | Bin 0 -> 15086 bytes R-package/recreate-configure.sh | 39 + R-package/src/Makevars.in | 59 + R-package/src/Makevars.win.in | 60 + R-package/src/install.libs.R | 242 + R-package/src/lightgbm_R.cpp | 1513 ++ R-package/src/lightgbm_R.h | 907 + R-package/tests/testthat.R | 9 + R-package/tests/testthat/helper.R | 54 + R-package/tests/testthat/test_Predictor.R | 706 + R-package/tests/testthat/test_basic.R | 3863 +++++ .../tests/testthat/test_custom_objective.R | 85 + R-package/tests/testthat/test_dataset.R | 665 + .../tests/testthat/test_learning_to_rank.R | 157 + R-package/tests/testthat/test_lgb.Booster.R | 1800 ++ .../testthat/test_lgb.convert_with_rules.R | 246 + .../tests/testthat/test_lgb.importance.R | 37 + R-package/tests/testthat/test_lgb.interpret.R | 115 + .../tests/testthat/test_lgb.model.dt.tree.R | 184 + .../tests/testthat/test_lgb.plot.importance.R | 47 + .../testthat/test_lgb.plot.interpretation.R | 99 + R-package/tests/testthat/test_metrics.R | 10 + .../tests/testthat/test_multithreading.R | 16 + R-package/tests/testthat/test_parameters.R | 169 + R-package/tests/testthat/test_utils.R | 167 + R-package/tests/testthat/test_weighted_loss.R | 65 + R-package/vignettes/basic_walkthrough.Rmd | 127 + README.md | 195 + README.wehub.md | 7 + SECURITY.md | 30 + VERSION.txt | 1 + biome.json | 41 + build-cran-package.sh | 214 + build-python.sh | 435 + build_r.R | 435 + cmake/IntegratedOpenCL.cmake | 217 + cmake/Sanitizer.cmake | 51 + cmake/Utils.cmake | 43 + cmake/modules/FindLibR.cmake | 267 + cmake/modules/FindNCCL.cmake | 84 + docker/README.md | 196 + docker/dockerfile-cli | 36 + docker/dockerfile-python | 34 + docker/dockerfile-r | 16 + docker/gpu/README.md | 58 + docker/gpu/dockerfile-cli-only-distroless.gpu | 75 + docker/gpu/dockerfile-cli-only.gpu | 67 + docker/gpu/dockerfile.gpu | 122 + docs/.lychee.toml | 32 + docs/Advanced-Topics.rst | 121 + docs/C-API.rst | 4 + docs/Development-Guide.rst | 95 + docs/Experiments.rst | 253 + docs/FAQ.rst | 420 + docs/Features.rst | 298 + docs/GPU-Performance.rst | 211 + docs/GPU-Targets.rst | 174 + docs/GPU-Tutorial.rst | 192 + docs/GPU-Windows.rst | 3 + docs/Installation-Guide.rst | 1143 ++ docs/Key-Events.md | 1 + docs/Makefile | 20 + docs/Parallel-Learning-Guide.rst | 549 + docs/Parameters-Tuning.rst | 221 + docs/Parameters.rst | 1483 ++ docs/Python-API.rst | 78 + docs/Python-Intro.rst | 267 + docs/Quick-Start.rst | 88 + docs/README.rst | 69 + docs/_static/images/dask-concat.svg | 1 + docs/_static/images/dask-initial-setup.svg | 1 + docs/_static/images/favicon.ico | Bin 0 -> 34494 bytes .../images/gpu-performance-comparison.png | Bin 0 -> 110367 bytes docs/_static/images/leaf-wise.png | Bin 0 -> 17926 bytes docs/_static/images/level-wise.png | Bin 0 -> 14091 bytes docs/_static/js/script.js | 52 + docs/build-docs.sh | 27 + docs/conf.py | 335 + docs/env.yml | 19 + docs/gcc-Tips.rst | 1 + docs/index.rst | 58 + docs/logo/LightGBM-logo-hex.cdr | Bin 0 -> 426020 bytes docs/logo/LightGBM-logo-hex.svg | 41 + docs/logo/LightGBM_logo-hex.png | Bin 0 -> 78403 bytes docs/logo/LightGBM_logo.cdr | Bin 0 -> 432409 bytes docs/logo/LightGBM_logo_black_text.svg | 39 + docs/logo/LightGBM_logo_black_text_huge.png | Bin 0 -> 139831 bytes docs/logo/LightGBM_logo_black_text_large.png | Bin 0 -> 63727 bytes docs/logo/LightGBM_logo_black_text_medium.png | Bin 0 -> 30491 bytes docs/logo/LightGBM_logo_black_text_small.png | Bin 0 -> 15197 bytes docs/logo/LightGBM_logo_black_text_tiny.png | Bin 0 -> 7565 bytes docs/logo/LightGBM_logo_grey_text.svg | 37 + docs/logo/LightGBM_logo_grey_text_huge.png | Bin 0 -> 141447 bytes docs/logo/LightGBM_logo_grey_text_large.png | Bin 0 -> 65233 bytes docs/logo/LightGBM_logo_grey_text_medium.png | Bin 0 -> 30826 bytes docs/logo/LightGBM_logo_grey_text_small.png | Bin 0 -> 15285 bytes docs/logo/LightGBM_logo_grey_text_tiny.png | Bin 0 -> 7759 bytes docs/logo/LightGBM_logo_no_text.svg | 27 + docs/logo/LightGBM_logo_no_text_huge.png | Bin 0 -> 89777 bytes docs/logo/LightGBM_logo_no_text_large.png | Bin 0 -> 42196 bytes docs/logo/LightGBM_logo_no_text_medium.png | Bin 0 -> 17988 bytes docs/logo/LightGBM_logo_no_text_small.png | Bin 0 -> 8176 bytes docs/logo/LightGBM_logo_no_text_tiny.png | Bin 0 -> 4040 bytes docs/make.bat | 37 + examples/README.md | 72 + examples/binary_classification/README.md | 27 + examples/binary_classification/binary.test | 500 + .../binary_classification/binary.test.weight | 500 + examples/binary_classification/binary.train | 7000 ++++++++ .../binary_classification/binary.train.weight | 7000 ++++++++ .../binary_classification/forced_splits.json | 12 + examples/binary_classification/predict.conf | 5 + examples/binary_classification/train.conf | 114 + .../binary_classification/train_linear.conf | 113 + examples/lambdarank/README.md | 33 + examples/lambdarank/predict.conf | 5 + examples/lambdarank/rank.test | 768 + examples/lambdarank/rank.test.query | 50 + examples/lambdarank/rank.train | 3005 ++++ examples/lambdarank/rank.train.query | 201 + examples/lambdarank/train.conf | 116 + examples/multiclass_classification/README.md | 27 + .../multiclass_classification/multiclass.test | 500 + .../multiclass.train | 7000 ++++++++ .../multiclass_classification/predict.conf | 5 + examples/multiclass_classification/train.conf | 68 + examples/parallel_learning/README.md | 22 + examples/parallel_learning/binary.test | 500 + examples/parallel_learning/binary.train | 7000 ++++++++ examples/parallel_learning/predict.conf | 5 + examples/parallel_learning/train.conf | 111 + examples/python-guide/README.md | 66 + examples/python-guide/advanced_example.py | 220 + examples/python-guide/dask/README.md | 25 + .../dask/binary-classification.py | 30 + .../dask/multiclass-classification.py | 30 + examples/python-guide/dask/prediction.py | 48 + examples/python-guide/dask/ranking.py | 50 + examples/python-guide/dask/regression.py | 30 + .../python-guide/dataset_from_multi_hdf5.py | 110 + examples/python-guide/logistic_regression.py | 101 + .../notebooks/interactive_plot_example.ipynb | 432 + examples/python-guide/plot_example.py | 62 + examples/python-guide/simple_example.py | 52 + examples/python-guide/sklearn_example.py | 80 + examples/regression/README.md | 27 + examples/regression/forced_bins.json | 18 + examples/regression/forced_bins2.json | 11 + examples/regression/predict.conf | 5 + examples/regression/regression.test | 500 + examples/regression/regression.test.init | 500 + examples/regression/regression.train | 7000 ++++++++ examples/regression/regression.train.init | 7000 ++++++++ examples/regression/train.conf | 114 + examples/xendcg/README.md | 33 + examples/xendcg/predict.conf | 5 + examples/xendcg/rank.test | 768 + examples/xendcg/rank.test.query | 50 + examples/xendcg/rank.train | 3005 ++++ examples/xendcg/rank.train.query | 201 + examples/xendcg/train.conf | 117 + include/LightGBM/application.h | 93 + include/LightGBM/arrow.h | 90 + include/LightGBM/bin.h | 655 + include/LightGBM/boosting.h | 334 + include/LightGBM/c_api.h | 1739 ++ include/LightGBM/config.h | 1334 ++ include/LightGBM/cuda/cuda_algorithms.hpp | 626 + include/LightGBM/cuda/cuda_column_data.hpp | 152 + include/LightGBM/cuda/cuda_metadata.hpp | 59 + include/LightGBM/cuda/cuda_metric.hpp | 45 + include/LightGBM/cuda/cuda_nccl_topology.hpp | 234 + .../LightGBM/cuda/cuda_objective_function.hpp | 98 + include/LightGBM/cuda/cuda_random.hpp | 77 + include/LightGBM/cuda/cuda_rocm_interop.h | 71 + include/LightGBM/cuda/cuda_row_data.hpp | 182 + include/LightGBM/cuda/cuda_split_info.hpp | 109 + include/LightGBM/cuda/cuda_tree.hpp | 174 + include/LightGBM/cuda/cuda_utils.hu | 325 + include/LightGBM/cuda/vector_cudahost.h | 99 + include/LightGBM/dataset.h | 1086 ++ include/LightGBM/dataset_loader.h | 111 + include/LightGBM/export.h | 26 + include/LightGBM/feature_group.h | 633 + include/LightGBM/meta.h | 92 + include/LightGBM/metric.h | 146 + include/LightGBM/network.h | 318 + include/LightGBM/objective_function.h | 142 + include/LightGBM/prediction_early_stop.h | 37 + include/LightGBM/sample_strategy.h | 88 + include/LightGBM/train_share_states.h | 367 + include/LightGBM/tree.h | 732 + include/LightGBM/tree_learner.h | 119 + include/LightGBM/utils/array_args.h | 200 + include/LightGBM/utils/binary_writer.h | 59 + include/LightGBM/utils/byte_buffer.h | 63 + include/LightGBM/utils/chunked_array.hpp | 261 + include/LightGBM/utils/common.h | 1271 ++ include/LightGBM/utils/file_io.h | 79 + include/LightGBM/utils/json11.h | 226 + include/LightGBM/utils/log.h | 186 + include/LightGBM/utils/openmp_wrapper.h | 136 + include/LightGBM/utils/pipeline_reader.h | 72 + include/LightGBM/utils/random.h | 118 + include/LightGBM/utils/text_reader.h | 360 + include/LightGBM/utils/threading.h | 201 + .../utils/yamc/alternate_shared_mutex.hpp | 212 + .../LightGBM/utils/yamc/yamc_rwlock_sched.hpp | 149 + .../LightGBM/utils/yamc/yamc_shared_lock.hpp | 199 + pixi.lock | 14222 ++++++++++++++++ pixi.toml | 44 + python-package/README.rst | 423 + python-package/lightgbm/__init__.py | 58 + python-package/lightgbm/basic.py | 5251 ++++++ python-package/lightgbm/callback.py | 511 + python-package/lightgbm/compat.py | 223 + python-package/lightgbm/dask.py | 1749 ++ python-package/lightgbm/engine.py | 872 + python-package/lightgbm/libpath.py | 49 + python-package/lightgbm/plotting.py | 849 + python-package/lightgbm/py.typed | 0 python-package/lightgbm/sklearn.py | 1954 +++ python-package/pyproject.toml | 229 + src/application/application.cpp | 296 + src/application/predictor.hpp | 303 + src/arrow/array.hpp | 413 + src/boosting/bagging.hpp | 297 + src/boosting/boosting.cpp | 102 + src/boosting/cuda/cuda_score_updater.cpp | 93 + src/boosting/cuda/cuda_score_updater.cu | 46 + src/boosting/cuda/cuda_score_updater.hpp | 66 + src/boosting/cuda/nccl_gbdt.cpp | 210 + src/boosting/cuda/nccl_gbdt.hpp | 146 + src/boosting/cuda/nccl_gbdt_component.hpp | 104 + src/boosting/dart.hpp | 212 + src/boosting/gbdt.cpp | 890 + src/boosting/gbdt.h | 625 + src/boosting/gbdt_model_text.cpp | 667 + src/boosting/gbdt_prediction.cpp | 100 + src/boosting/goss.hpp | 173 + src/boosting/prediction_early_stop.cpp | 94 + src/boosting/rf.hpp | 237 + src/boosting/sample_strategy.cpp | 28 + src/boosting/score_updater.hpp | 129 + src/c_api.cpp | 3080 ++++ src/cuda/cuda_algorithms.cu | 451 + src/cuda/cuda_utils.cpp | 61 + src/io/bin.cpp | 1077 ++ src/io/config.cpp | 519 + src/io/config_auto.cpp | 1096 ++ src/io/cuda/cuda_column_data.cpp | 227 + src/io/cuda/cuda_column_data.cu | 62 + src/io/cuda/cuda_metadata.cpp | 79 + src/io/cuda/cuda_row_data.cpp | 438 + src/io/cuda/cuda_tree.cpp | 214 + src/io/cuda/cuda_tree.cu | 460 + src/io/dataset.cpp | 1955 +++ src/io/dataset_loader.cpp | 1587 ++ src/io/dense_bin.hpp | 650 + src/io/file_io.cpp | 80 + src/io/json11.cpp | 785 + src/io/metadata.cpp | 946 + src/io/multi_val_dense_bin.hpp | 360 + src/io/multi_val_sparse_bin.hpp | 449 + src/io/parser.cpp | 319 + src/io/parser.hpp | 136 + src/io/sparse_bin.hpp | 858 + src/io/train_share_states.cpp | 540 + src/io/tree.cpp | 1060 ++ src/main.cpp | 46 + src/metric/binary_metric.hpp | 389 + src/metric/cuda/cuda_binary_metric.cpp | 35 + src/metric/cuda/cuda_binary_metric.hpp | 73 + src/metric/cuda/cuda_pointwise_metric.cpp | 52 + src/metric/cuda/cuda_pointwise_metric.cu | 85 + src/metric/cuda/cuda_pointwise_metric.hpp | 46 + src/metric/cuda/cuda_regression_metric.cpp | 53 + src/metric/cuda/cuda_regression_metric.hpp | 216 + src/metric/dcg_calculator.cpp | 174 + src/metric/map_metric.hpp | 169 + src/metric/metric.cpp | 142 + src/metric/multiclass_metric.hpp | 369 + src/metric/rank_metric.hpp | 170 + src/metric/regression_metric.hpp | 433 + src/metric/xentropy_metric.hpp | 359 + src/network/linker_topo.cpp | 182 + src/network/linkers.h | 329 + src/network/linkers_mpi.cpp | 64 + src/network/linkers_socket.cpp | 241 + src/network/network.cpp | 332 + src/network/socket_wrapper.hpp | 333 + src/objective/binary_objective.hpp | 217 + src/objective/cuda/cuda_binary_objective.cpp | 58 + src/objective/cuda/cuda_binary_objective.cu | 225 + src/objective/cuda/cuda_binary_objective.hpp | 64 + .../cuda/cuda_multiclass_objective.cpp | 63 + .../cuda/cuda_multiclass_objective.cu | 109 + .../cuda/cuda_multiclass_objective.hpp | 79 + src/objective/cuda/cuda_rank_objective.cpp | 68 + src/objective/cuda/cuda_rank_objective.cu | 666 + src/objective/cuda/cuda_rank_objective.hpp | 123 + .../cuda/cuda_regression_objective.cpp | 110 + .../cuda/cuda_regression_objective.cu | 482 + .../cuda/cuda_regression_objective.hpp | 168 + src/objective/multiclass_objective.hpp | 280 + src/objective/objective_function.cpp | 164 + src/objective/rank_objective.hpp | 466 + src/objective/regression_objective.hpp | 764 + src/objective/xentropy_objective.hpp | 317 + src/treelearner/col_sampler.hpp | 208 + .../cost_effective_gradient_boosting.hpp | 175 + .../cuda/cuda_best_split_finder.cpp | 380 + .../cuda/cuda_best_split_finder.cu | 2268 +++ .../cuda/cuda_best_split_finder.hpp | 249 + src/treelearner/cuda/cuda_data_partition.cpp | 352 + src/treelearner/cuda/cuda_data_partition.cu | 1169 ++ src/treelearner/cuda/cuda_data_partition.hpp | 406 + .../cuda/cuda_gradient_discretizer.cu | 185 + .../cuda/cuda_gradient_discretizer.hpp | 121 + .../cuda/cuda_histogram_constructor.cpp | 192 + .../cuda/cuda_histogram_constructor.cu | 963 ++ .../cuda/cuda_histogram_constructor.hpp | 199 + src/treelearner/cuda/cuda_leaf_splits.cpp | 79 + src/treelearner/cuda/cuda_leaf_splits.cu | 415 + src/treelearner/cuda/cuda_leaf_splits.hpp | 180 + .../cuda/cuda_single_gpu_tree_learner.cpp | 726 + .../cuda/cuda_single_gpu_tree_learner.cu | 296 + .../cuda/cuda_single_gpu_tree_learner.hpp | 183 + .../data_parallel_tree_learner.cpp | 470 + src/treelearner/data_partition.hpp | 171 + src/treelearner/feature_histogram.cpp | 746 + src/treelearner/feature_histogram.hpp | 1598 ++ .../feature_parallel_tree_learner.cpp | 84 + src/treelearner/gpu_tree_learner.cpp | 1132 ++ src/treelearner/gpu_tree_learner.h | 288 + src/treelearner/gradient_discretizer.cpp | 263 + src/treelearner/gradient_discretizer.hpp | 129 + src/treelearner/leaf_splits.hpp | 277 + src/treelearner/linear_tree_learner.cpp | 406 + src/treelearner/linear_tree_learner.h | 130 + src/treelearner/monotone_constraints.hpp | 1187 ++ src/treelearner/ocl/histogram16.cl | 767 + src/treelearner/ocl/histogram256.cl | 792 + src/treelearner/ocl/histogram64.cl | 743 + src/treelearner/parallel_tree_learner.h | 236 + src/treelearner/serial_tree_learner.cpp | 1118 ++ src/treelearner/serial_tree_learner.h | 251 + src/treelearner/split_info.hpp | 294 + src/treelearner/tree_learner.cpp | 57 + .../voting_parallel_tree_learner.cpp | 515 + src/utils/openmp_wrapper.cpp | 44 + swig/ChunkedArray_API_extensions.i | 28 + swig/StringArray.hpp | 141 + swig/StringArray.i | 98 + swig/StringArray_API_extensions.i | 148 + swig/lightgbmlib.i | 289 + swig/pointer_manipulation.i | 151 + tests/c_api_test/test_.py | 266 + tests/cpp_tests/predict.conf | 5 + tests/cpp_tests/test.py | 7 + tests/cpp_tests/test_array_args.cpp | 54 + tests/cpp_tests/test_arrow.cpp | 216 + tests/cpp_tests/test_arrow_deprecated.cpp | 220 + tests/cpp_tests/test_byte_buffer.cpp | 73 + tests/cpp_tests/test_chunked_array.cpp | 265 + tests/cpp_tests/test_common.cpp | 154 + tests/cpp_tests/test_main.cpp | 12 + tests/cpp_tests/test_serialize.cpp | 86 + tests/cpp_tests/test_single_row.cpp | 190 + tests/cpp_tests/test_stream.cpp | 356 + tests/cpp_tests/testutils.cpp | 441 + tests/cpp_tests/testutils.h | 125 + tests/cpp_tests/train.conf | 7 + tests/data/categorical.data | 7000 ++++++++ tests/distributed/_test_distributed.py | 196 + tests/distributed/conftest.py | 7 + tests/python_package_test/__init__.py | 0 tests/python_package_test/conftest.py | 12 + tests/python_package_test/test_arrow.py | 512 + tests/python_package_test/test_basic.py | 1277 ++ tests/python_package_test/test_callback.py | 65 + tests/python_package_test/test_consistency.py | 143 + tests/python_package_test/test_dask.py | 1701 ++ tests/python_package_test/test_dual.py | 37 + tests/python_package_test/test_engine.py | 4910 ++++++ tests/python_package_test/test_plotting.py | 631 + tests/python_package_test/test_polars.py | 413 + tests/python_package_test/test_sklearn.py | 2367 +++ tests/python_package_test/test_utilities.py | 160 + tests/python_package_test/utils.py | 279 + windows/LightGBM.sln | 33 + windows/LightGBM.vcxproj | 358 + windows/LightGBM.vcxproj.filters | 352 + 553 files changed, 214565 insertions(+) create mode 100644 .appveyor.yml create mode 100644 .ci/README.md create mode 100755 .ci/append-comment.sh create mode 100644 .ci/build-docs.R create mode 100755 .ci/build-docs.sh create mode 100755 .ci/check-dynamic-dependencies.sh create mode 100755 .ci/check-omp-pragmas.sh create mode 100644 .ci/check-python-dists.sh create mode 100755 .ci/check-workflow-status.sh create mode 100644 .ci/conda-envs/README.md create mode 100644 .ci/conda-envs/ci-core.txt create mode 100644 .ci/create-nuget.py create mode 100755 .ci/download-artifacts.sh create mode 100644 .ci/install-opencl.ps1 create mode 100644 .ci/install-r-deps.R create mode 100755 .ci/lint-all.sh create mode 100644 .ci/lint-powershell.ps1 create mode 100755 .ci/lint-r-code.R create mode 100644 .ci/parameter-generator.py create mode 100644 .ci/pip-envs/requirements-latest.txt create mode 100644 .ci/pip-envs/requirements-oldest.txt create mode 100755 .ci/rerun-workflow.sh create mode 100755 .ci/run-r-cmd-check.sh create mode 100755 .ci/set-commit-status.sh create mode 100755 .ci/setup.sh create mode 100755 .ci/test-python-latest.sh create mode 100644 .ci/test-python-oldest.sh create mode 100755 .ci/test-r-package-valgrind.sh create mode 100644 .ci/test-r-package-windows.ps1 create mode 100755 .ci/test-r-package.sh create mode 100644 .ci/test-windows.ps1 create mode 100755 .ci/test.sh create mode 100644 .editorconfig create mode 100644 .git-blame-ignore-revs create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.md create mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md create mode 100644 .github/dependabot.yml create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/cpp.yml create mode 100644 .github/workflows/cuda.yml create mode 100644 .github/workflows/lock.yml create mode 100644 .github/workflows/lychee.yml create mode 100644 .github/workflows/no_response.yml create mode 100644 .github/workflows/optional_checks.yml create mode 100644 .github/workflows/python_package.yml create mode 100644 .github/workflows/r_configure.yml create mode 100644 .github/workflows/r_package.yml create mode 100644 .github/workflows/r_valgrind.yml create mode 100644 .github/workflows/release_drafter.yml create mode 100644 .github/workflows/static_analysis.yml create mode 100644 .github/workflows/swig.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 .typos.toml create mode 100644 .yamllint.yml create mode 100644 CMakeLists.txt create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 MAINTAINING.md create mode 100644 R-package/.Rbuildignore create mode 100644 R-package/AUTOCONF_UBUNTU_VERSION create mode 100755 R-package/DESCRIPTION create mode 100644 R-package/LICENSE create mode 100644 R-package/NAMESPACE create mode 100644 R-package/R/aliases.R create mode 100644 R-package/R/callback.R create mode 100644 R-package/R/lgb.Booster.R create mode 100644 R-package/R/lgb.DataProcessor.R create mode 100644 R-package/R/lgb.Dataset.R create mode 100644 R-package/R/lgb.Predictor.R create mode 100644 R-package/R/lgb.convert_with_rules.R create mode 100644 R-package/R/lgb.cv.R create mode 100644 R-package/R/lgb.drop_serialized.R create mode 100644 R-package/R/lgb.importance.R create mode 100644 R-package/R/lgb.interpret.R create mode 100644 R-package/R/lgb.make_serializable.R create mode 100644 R-package/R/lgb.model.dt.tree.R create mode 100644 R-package/R/lgb.plot.importance.R create mode 100644 R-package/R/lgb.plot.interpretation.R create mode 100644 R-package/R/lgb.restore_handle.R create mode 100644 R-package/R/lgb.train.R create mode 100644 R-package/R/lightgbm.R create mode 100644 R-package/R/metrics.R create mode 100644 R-package/R/multithreading.R create mode 100644 R-package/R/utils.R create mode 100644 R-package/README.md create mode 100755 R-package/cleanup create mode 100755 R-package/configure create mode 100644 R-package/configure.ac create mode 100755 R-package/configure.win create mode 100644 R-package/cran-comments.md create mode 100644 R-package/demo/00Index create mode 100644 R-package/demo/basic_walkthrough.R create mode 100644 R-package/demo/boost_from_prediction.R create mode 100644 R-package/demo/categorical_features_rules.R create mode 100644 R-package/demo/cross_validation.R create mode 100644 R-package/demo/early_stopping.R create mode 100644 R-package/demo/efficient_many_training.R create mode 100644 R-package/demo/leaf_stability.R create mode 100644 R-package/demo/multiclass.R create mode 100644 R-package/demo/multiclass_custom_objective.R create mode 100644 R-package/demo/weight_param.R create mode 100644 R-package/inst/Makevars create mode 100644 R-package/inst/make-r-def.R create mode 100644 R-package/man/agaricus.test.Rd create mode 100644 R-package/man/agaricus.train.Rd create mode 100644 R-package/man/bank.Rd create mode 100644 R-package/man/dim.Rd create mode 100644 R-package/man/dimnames.lgb.Dataset.Rd create mode 100644 R-package/man/figures/logo.svg create mode 100644 R-package/man/getLGBMThreads.Rd create mode 100644 R-package/man/get_field.Rd create mode 100644 R-package/man/lgb.Dataset.Rd create mode 100644 R-package/man/lgb.Dataset.construct.Rd create mode 100644 R-package/man/lgb.Dataset.create.valid.Rd create mode 100644 R-package/man/lgb.Dataset.save.Rd create mode 100644 R-package/man/lgb.Dataset.set.categorical.Rd create mode 100644 R-package/man/lgb.Dataset.set.reference.Rd create mode 100644 R-package/man/lgb.configure_fast_predict.Rd create mode 100644 R-package/man/lgb.convert_with_rules.Rd create mode 100644 R-package/man/lgb.cv.Rd create mode 100644 R-package/man/lgb.drop_serialized.Rd create mode 100644 R-package/man/lgb.dump.Rd create mode 100644 R-package/man/lgb.get.eval.result.Rd create mode 100644 R-package/man/lgb.importance.Rd create mode 100644 R-package/man/lgb.interpret.Rd create mode 100644 R-package/man/lgb.interprete.Rd create mode 100644 R-package/man/lgb.load.Rd create mode 100644 R-package/man/lgb.make_serializable.Rd create mode 100644 R-package/man/lgb.model.dt.tree.Rd create mode 100644 R-package/man/lgb.plot.importance.Rd create mode 100644 R-package/man/lgb.plot.interpretation.Rd create mode 100644 R-package/man/lgb.restore_handle.Rd create mode 100644 R-package/man/lgb.save.Rd create mode 100644 R-package/man/lgb.slice.Dataset.Rd create mode 100644 R-package/man/lgb.train.Rd create mode 100644 R-package/man/lgb_predict_shared_params.Rd create mode 100644 R-package/man/lgb_shared_dataset_params.Rd create mode 100644 R-package/man/lgb_shared_params.Rd create mode 100644 R-package/man/lightgbm.Rd create mode 100644 R-package/man/predict.lgb.Booster.Rd create mode 100644 R-package/man/print.lgb.Booster.Rd create mode 100644 R-package/man/setLGBMThreads.Rd create mode 100644 R-package/man/set_field.Rd create mode 100644 R-package/man/summary.lgb.Booster.Rd create mode 100644 R-package/pkgdown/_pkgdown.yml create mode 100644 R-package/pkgdown/favicon/apple-touch-icon-120x120.png create mode 100644 R-package/pkgdown/favicon/apple-touch-icon-152x152.png create mode 100644 R-package/pkgdown/favicon/apple-touch-icon-180x180.png create mode 100644 R-package/pkgdown/favicon/apple-touch-icon-60x60.png create mode 100644 R-package/pkgdown/favicon/apple-touch-icon-76x76.png create mode 100644 R-package/pkgdown/favicon/apple-touch-icon.png create mode 100644 R-package/pkgdown/favicon/favicon-16x16.png create mode 100644 R-package/pkgdown/favicon/favicon-32x32.png create mode 100644 R-package/pkgdown/favicon/favicon.ico create mode 100755 R-package/recreate-configure.sh create mode 100644 R-package/src/Makevars.in create mode 100644 R-package/src/Makevars.win.in create mode 100644 R-package/src/install.libs.R create mode 100644 R-package/src/lightgbm_R.cpp create mode 100644 R-package/src/lightgbm_R.h create mode 100644 R-package/tests/testthat.R create mode 100644 R-package/tests/testthat/helper.R create mode 100644 R-package/tests/testthat/test_Predictor.R create mode 100644 R-package/tests/testthat/test_basic.R create mode 100644 R-package/tests/testthat/test_custom_objective.R create mode 100644 R-package/tests/testthat/test_dataset.R create mode 100644 R-package/tests/testthat/test_learning_to_rank.R create mode 100644 R-package/tests/testthat/test_lgb.Booster.R create mode 100644 R-package/tests/testthat/test_lgb.convert_with_rules.R create mode 100644 R-package/tests/testthat/test_lgb.importance.R create mode 100644 R-package/tests/testthat/test_lgb.interpret.R create mode 100644 R-package/tests/testthat/test_lgb.model.dt.tree.R create mode 100644 R-package/tests/testthat/test_lgb.plot.importance.R create mode 100644 R-package/tests/testthat/test_lgb.plot.interpretation.R create mode 100644 R-package/tests/testthat/test_metrics.R create mode 100644 R-package/tests/testthat/test_multithreading.R create mode 100644 R-package/tests/testthat/test_parameters.R create mode 100644 R-package/tests/testthat/test_utils.R create mode 100644 R-package/tests/testthat/test_weighted_loss.R create mode 100644 R-package/vignettes/basic_walkthrough.Rmd create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 SECURITY.md create mode 100644 VERSION.txt create mode 100644 biome.json create mode 100755 build-cran-package.sh create mode 100755 build-python.sh create mode 100644 build_r.R create mode 100644 cmake/IntegratedOpenCL.cmake create mode 100644 cmake/Sanitizer.cmake create mode 100644 cmake/Utils.cmake create mode 100644 cmake/modules/FindLibR.cmake create mode 100644 cmake/modules/FindNCCL.cmake create mode 100644 docker/README.md create mode 100644 docker/dockerfile-cli create mode 100644 docker/dockerfile-python create mode 100644 docker/dockerfile-r create mode 100644 docker/gpu/README.md create mode 100644 docker/gpu/dockerfile-cli-only-distroless.gpu create mode 100644 docker/gpu/dockerfile-cli-only.gpu create mode 100644 docker/gpu/dockerfile.gpu create mode 100644 docs/.lychee.toml create mode 100644 docs/Advanced-Topics.rst create mode 100644 docs/C-API.rst create mode 100644 docs/Development-Guide.rst create mode 100644 docs/Experiments.rst create mode 100644 docs/FAQ.rst create mode 100644 docs/Features.rst create mode 100644 docs/GPU-Performance.rst create mode 100644 docs/GPU-Targets.rst create mode 100644 docs/GPU-Tutorial.rst create mode 100644 docs/GPU-Windows.rst create mode 100644 docs/Installation-Guide.rst create mode 100644 docs/Key-Events.md create mode 100644 docs/Makefile create mode 100644 docs/Parallel-Learning-Guide.rst create mode 100644 docs/Parameters-Tuning.rst create mode 100644 docs/Parameters.rst create mode 100644 docs/Python-API.rst create mode 100644 docs/Python-Intro.rst create mode 100644 docs/Quick-Start.rst create mode 100644 docs/README.rst create mode 100644 docs/_static/images/dask-concat.svg create mode 100644 docs/_static/images/dask-initial-setup.svg create mode 100644 docs/_static/images/favicon.ico create mode 100644 docs/_static/images/gpu-performance-comparison.png create mode 100644 docs/_static/images/leaf-wise.png create mode 100644 docs/_static/images/level-wise.png create mode 100644 docs/_static/js/script.js create mode 100644 docs/build-docs.sh create mode 100644 docs/conf.py create mode 100644 docs/env.yml create mode 100644 docs/gcc-Tips.rst create mode 100644 docs/index.rst create mode 100644 docs/logo/LightGBM-logo-hex.cdr create mode 100644 docs/logo/LightGBM-logo-hex.svg create mode 100644 docs/logo/LightGBM_logo-hex.png create mode 100644 docs/logo/LightGBM_logo.cdr create mode 100644 docs/logo/LightGBM_logo_black_text.svg create mode 100644 docs/logo/LightGBM_logo_black_text_huge.png create mode 100644 docs/logo/LightGBM_logo_black_text_large.png create mode 100644 docs/logo/LightGBM_logo_black_text_medium.png create mode 100644 docs/logo/LightGBM_logo_black_text_small.png create mode 100644 docs/logo/LightGBM_logo_black_text_tiny.png create mode 100644 docs/logo/LightGBM_logo_grey_text.svg create mode 100644 docs/logo/LightGBM_logo_grey_text_huge.png create mode 100644 docs/logo/LightGBM_logo_grey_text_large.png create mode 100644 docs/logo/LightGBM_logo_grey_text_medium.png create mode 100644 docs/logo/LightGBM_logo_grey_text_small.png create mode 100644 docs/logo/LightGBM_logo_grey_text_tiny.png create mode 100644 docs/logo/LightGBM_logo_no_text.svg create mode 100644 docs/logo/LightGBM_logo_no_text_huge.png create mode 100644 docs/logo/LightGBM_logo_no_text_large.png create mode 100644 docs/logo/LightGBM_logo_no_text_medium.png create mode 100644 docs/logo/LightGBM_logo_no_text_small.png create mode 100644 docs/logo/LightGBM_logo_no_text_tiny.png create mode 100644 docs/make.bat create mode 100644 examples/README.md create mode 100644 examples/binary_classification/README.md create mode 100644 examples/binary_classification/binary.test create mode 100644 examples/binary_classification/binary.test.weight create mode 100644 examples/binary_classification/binary.train create mode 100644 examples/binary_classification/binary.train.weight create mode 100644 examples/binary_classification/forced_splits.json create mode 100644 examples/binary_classification/predict.conf create mode 100644 examples/binary_classification/train.conf create mode 100644 examples/binary_classification/train_linear.conf create mode 100644 examples/lambdarank/README.md create mode 100644 examples/lambdarank/predict.conf create mode 100644 examples/lambdarank/rank.test create mode 100644 examples/lambdarank/rank.test.query create mode 100644 examples/lambdarank/rank.train create mode 100644 examples/lambdarank/rank.train.query create mode 100644 examples/lambdarank/train.conf create mode 100644 examples/multiclass_classification/README.md create mode 100644 examples/multiclass_classification/multiclass.test create mode 100644 examples/multiclass_classification/multiclass.train create mode 100644 examples/multiclass_classification/predict.conf create mode 100644 examples/multiclass_classification/train.conf create mode 100644 examples/parallel_learning/README.md create mode 100644 examples/parallel_learning/binary.test create mode 100644 examples/parallel_learning/binary.train create mode 100644 examples/parallel_learning/predict.conf create mode 100644 examples/parallel_learning/train.conf create mode 100644 examples/python-guide/README.md create mode 100644 examples/python-guide/advanced_example.py create mode 100644 examples/python-guide/dask/README.md create mode 100644 examples/python-guide/dask/binary-classification.py create mode 100644 examples/python-guide/dask/multiclass-classification.py create mode 100644 examples/python-guide/dask/prediction.py create mode 100644 examples/python-guide/dask/ranking.py create mode 100644 examples/python-guide/dask/regression.py create mode 100644 examples/python-guide/dataset_from_multi_hdf5.py create mode 100644 examples/python-guide/logistic_regression.py create mode 100644 examples/python-guide/notebooks/interactive_plot_example.ipynb create mode 100644 examples/python-guide/plot_example.py create mode 100644 examples/python-guide/simple_example.py create mode 100644 examples/python-guide/sklearn_example.py create mode 100644 examples/regression/README.md create mode 100644 examples/regression/forced_bins.json create mode 100644 examples/regression/forced_bins2.json create mode 100644 examples/regression/predict.conf create mode 100644 examples/regression/regression.test create mode 100644 examples/regression/regression.test.init create mode 100644 examples/regression/regression.train create mode 100644 examples/regression/regression.train.init create mode 100644 examples/regression/train.conf create mode 100644 examples/xendcg/README.md create mode 100644 examples/xendcg/predict.conf create mode 100644 examples/xendcg/rank.test create mode 100644 examples/xendcg/rank.test.query create mode 100644 examples/xendcg/rank.train create mode 100644 examples/xendcg/rank.train.query create mode 100644 examples/xendcg/train.conf create mode 100644 include/LightGBM/application.h create mode 100644 include/LightGBM/arrow.h create mode 100644 include/LightGBM/bin.h create mode 100644 include/LightGBM/boosting.h create mode 100644 include/LightGBM/c_api.h create mode 100644 include/LightGBM/config.h create mode 100644 include/LightGBM/cuda/cuda_algorithms.hpp create mode 100644 include/LightGBM/cuda/cuda_column_data.hpp create mode 100644 include/LightGBM/cuda/cuda_metadata.hpp create mode 100644 include/LightGBM/cuda/cuda_metric.hpp create mode 100644 include/LightGBM/cuda/cuda_nccl_topology.hpp create mode 100644 include/LightGBM/cuda/cuda_objective_function.hpp create mode 100644 include/LightGBM/cuda/cuda_random.hpp create mode 100644 include/LightGBM/cuda/cuda_rocm_interop.h create mode 100644 include/LightGBM/cuda/cuda_row_data.hpp create mode 100644 include/LightGBM/cuda/cuda_split_info.hpp create mode 100644 include/LightGBM/cuda/cuda_tree.hpp create mode 100644 include/LightGBM/cuda/cuda_utils.hu create mode 100644 include/LightGBM/cuda/vector_cudahost.h create mode 100644 include/LightGBM/dataset.h create mode 100644 include/LightGBM/dataset_loader.h create mode 100644 include/LightGBM/export.h create mode 100644 include/LightGBM/feature_group.h create mode 100644 include/LightGBM/meta.h create mode 100644 include/LightGBM/metric.h create mode 100644 include/LightGBM/network.h create mode 100644 include/LightGBM/objective_function.h create mode 100644 include/LightGBM/prediction_early_stop.h create mode 100644 include/LightGBM/sample_strategy.h create mode 100644 include/LightGBM/train_share_states.h create mode 100644 include/LightGBM/tree.h create mode 100644 include/LightGBM/tree_learner.h create mode 100644 include/LightGBM/utils/array_args.h create mode 100644 include/LightGBM/utils/binary_writer.h create mode 100644 include/LightGBM/utils/byte_buffer.h create mode 100644 include/LightGBM/utils/chunked_array.hpp create mode 100644 include/LightGBM/utils/common.h create mode 100644 include/LightGBM/utils/file_io.h create mode 100644 include/LightGBM/utils/json11.h create mode 100644 include/LightGBM/utils/log.h create mode 100644 include/LightGBM/utils/openmp_wrapper.h create mode 100644 include/LightGBM/utils/pipeline_reader.h create mode 100644 include/LightGBM/utils/random.h create mode 100644 include/LightGBM/utils/text_reader.h create mode 100644 include/LightGBM/utils/threading.h create mode 100644 include/LightGBM/utils/yamc/alternate_shared_mutex.hpp create mode 100644 include/LightGBM/utils/yamc/yamc_rwlock_sched.hpp create mode 100644 include/LightGBM/utils/yamc/yamc_shared_lock.hpp create mode 100644 pixi.lock create mode 100644 pixi.toml create mode 100644 python-package/README.rst create mode 100644 python-package/lightgbm/__init__.py create mode 100644 python-package/lightgbm/basic.py create mode 100644 python-package/lightgbm/callback.py create mode 100644 python-package/lightgbm/compat.py create mode 100644 python-package/lightgbm/dask.py create mode 100644 python-package/lightgbm/engine.py create mode 100644 python-package/lightgbm/libpath.py create mode 100644 python-package/lightgbm/plotting.py create mode 100644 python-package/lightgbm/py.typed create mode 100644 python-package/lightgbm/sklearn.py create mode 100644 python-package/pyproject.toml create mode 100644 src/application/application.cpp create mode 100644 src/application/predictor.hpp create mode 100644 src/arrow/array.hpp create mode 100644 src/boosting/bagging.hpp create mode 100644 src/boosting/boosting.cpp create mode 100644 src/boosting/cuda/cuda_score_updater.cpp create mode 100644 src/boosting/cuda/cuda_score_updater.cu create mode 100644 src/boosting/cuda/cuda_score_updater.hpp create mode 100644 src/boosting/cuda/nccl_gbdt.cpp create mode 100644 src/boosting/cuda/nccl_gbdt.hpp create mode 100644 src/boosting/cuda/nccl_gbdt_component.hpp create mode 100644 src/boosting/dart.hpp create mode 100644 src/boosting/gbdt.cpp create mode 100644 src/boosting/gbdt.h create mode 100644 src/boosting/gbdt_model_text.cpp create mode 100644 src/boosting/gbdt_prediction.cpp create mode 100644 src/boosting/goss.hpp create mode 100644 src/boosting/prediction_early_stop.cpp create mode 100644 src/boosting/rf.hpp create mode 100644 src/boosting/sample_strategy.cpp create mode 100644 src/boosting/score_updater.hpp create mode 100644 src/c_api.cpp create mode 100644 src/cuda/cuda_algorithms.cu create mode 100644 src/cuda/cuda_utils.cpp create mode 100644 src/io/bin.cpp create mode 100644 src/io/config.cpp create mode 100644 src/io/config_auto.cpp create mode 100644 src/io/cuda/cuda_column_data.cpp create mode 100644 src/io/cuda/cuda_column_data.cu create mode 100644 src/io/cuda/cuda_metadata.cpp create mode 100644 src/io/cuda/cuda_row_data.cpp create mode 100644 src/io/cuda/cuda_tree.cpp create mode 100644 src/io/cuda/cuda_tree.cu create mode 100644 src/io/dataset.cpp create mode 100644 src/io/dataset_loader.cpp create mode 100644 src/io/dense_bin.hpp create mode 100644 src/io/file_io.cpp create mode 100644 src/io/json11.cpp create mode 100644 src/io/metadata.cpp create mode 100644 src/io/multi_val_dense_bin.hpp create mode 100644 src/io/multi_val_sparse_bin.hpp create mode 100644 src/io/parser.cpp create mode 100644 src/io/parser.hpp create mode 100644 src/io/sparse_bin.hpp create mode 100644 src/io/train_share_states.cpp create mode 100644 src/io/tree.cpp create mode 100644 src/main.cpp create mode 100644 src/metric/binary_metric.hpp create mode 100644 src/metric/cuda/cuda_binary_metric.cpp create mode 100644 src/metric/cuda/cuda_binary_metric.hpp create mode 100644 src/metric/cuda/cuda_pointwise_metric.cpp create mode 100644 src/metric/cuda/cuda_pointwise_metric.cu create mode 100644 src/metric/cuda/cuda_pointwise_metric.hpp create mode 100644 src/metric/cuda/cuda_regression_metric.cpp create mode 100644 src/metric/cuda/cuda_regression_metric.hpp create mode 100644 src/metric/dcg_calculator.cpp create mode 100644 src/metric/map_metric.hpp create mode 100644 src/metric/metric.cpp create mode 100644 src/metric/multiclass_metric.hpp create mode 100644 src/metric/rank_metric.hpp create mode 100644 src/metric/regression_metric.hpp create mode 100644 src/metric/xentropy_metric.hpp create mode 100644 src/network/linker_topo.cpp create mode 100644 src/network/linkers.h create mode 100644 src/network/linkers_mpi.cpp create mode 100644 src/network/linkers_socket.cpp create mode 100644 src/network/network.cpp create mode 100644 src/network/socket_wrapper.hpp create mode 100644 src/objective/binary_objective.hpp create mode 100644 src/objective/cuda/cuda_binary_objective.cpp create mode 100644 src/objective/cuda/cuda_binary_objective.cu create mode 100644 src/objective/cuda/cuda_binary_objective.hpp create mode 100644 src/objective/cuda/cuda_multiclass_objective.cpp create mode 100644 src/objective/cuda/cuda_multiclass_objective.cu create mode 100644 src/objective/cuda/cuda_multiclass_objective.hpp create mode 100644 src/objective/cuda/cuda_rank_objective.cpp create mode 100644 src/objective/cuda/cuda_rank_objective.cu create mode 100644 src/objective/cuda/cuda_rank_objective.hpp create mode 100644 src/objective/cuda/cuda_regression_objective.cpp create mode 100644 src/objective/cuda/cuda_regression_objective.cu create mode 100644 src/objective/cuda/cuda_regression_objective.hpp create mode 100644 src/objective/multiclass_objective.hpp create mode 100644 src/objective/objective_function.cpp create mode 100644 src/objective/rank_objective.hpp create mode 100644 src/objective/regression_objective.hpp create mode 100644 src/objective/xentropy_objective.hpp create mode 100644 src/treelearner/col_sampler.hpp create mode 100644 src/treelearner/cost_effective_gradient_boosting.hpp create mode 100644 src/treelearner/cuda/cuda_best_split_finder.cpp create mode 100644 src/treelearner/cuda/cuda_best_split_finder.cu create mode 100644 src/treelearner/cuda/cuda_best_split_finder.hpp create mode 100644 src/treelearner/cuda/cuda_data_partition.cpp create mode 100644 src/treelearner/cuda/cuda_data_partition.cu create mode 100644 src/treelearner/cuda/cuda_data_partition.hpp create mode 100644 src/treelearner/cuda/cuda_gradient_discretizer.cu create mode 100644 src/treelearner/cuda/cuda_gradient_discretizer.hpp create mode 100644 src/treelearner/cuda/cuda_histogram_constructor.cpp create mode 100644 src/treelearner/cuda/cuda_histogram_constructor.cu create mode 100644 src/treelearner/cuda/cuda_histogram_constructor.hpp create mode 100644 src/treelearner/cuda/cuda_leaf_splits.cpp create mode 100644 src/treelearner/cuda/cuda_leaf_splits.cu create mode 100644 src/treelearner/cuda/cuda_leaf_splits.hpp create mode 100644 src/treelearner/cuda/cuda_single_gpu_tree_learner.cpp create mode 100644 src/treelearner/cuda/cuda_single_gpu_tree_learner.cu create mode 100644 src/treelearner/cuda/cuda_single_gpu_tree_learner.hpp create mode 100644 src/treelearner/data_parallel_tree_learner.cpp create mode 100644 src/treelearner/data_partition.hpp create mode 100644 src/treelearner/feature_histogram.cpp create mode 100644 src/treelearner/feature_histogram.hpp create mode 100644 src/treelearner/feature_parallel_tree_learner.cpp create mode 100644 src/treelearner/gpu_tree_learner.cpp create mode 100644 src/treelearner/gpu_tree_learner.h create mode 100644 src/treelearner/gradient_discretizer.cpp create mode 100644 src/treelearner/gradient_discretizer.hpp create mode 100644 src/treelearner/leaf_splits.hpp create mode 100644 src/treelearner/linear_tree_learner.cpp create mode 100644 src/treelearner/linear_tree_learner.h create mode 100644 src/treelearner/monotone_constraints.hpp create mode 100644 src/treelearner/ocl/histogram16.cl create mode 100644 src/treelearner/ocl/histogram256.cl create mode 100644 src/treelearner/ocl/histogram64.cl create mode 100644 src/treelearner/parallel_tree_learner.h create mode 100644 src/treelearner/serial_tree_learner.cpp create mode 100644 src/treelearner/serial_tree_learner.h create mode 100644 src/treelearner/split_info.hpp create mode 100644 src/treelearner/tree_learner.cpp create mode 100644 src/treelearner/voting_parallel_tree_learner.cpp create mode 100644 src/utils/openmp_wrapper.cpp create mode 100644 swig/ChunkedArray_API_extensions.i create mode 100644 swig/StringArray.hpp create mode 100644 swig/StringArray.i create mode 100644 swig/StringArray_API_extensions.i create mode 100644 swig/lightgbmlib.i create mode 100644 swig/pointer_manipulation.i create mode 100644 tests/c_api_test/test_.py create mode 100644 tests/cpp_tests/predict.conf create mode 100644 tests/cpp_tests/test.py create mode 100644 tests/cpp_tests/test_array_args.cpp create mode 100644 tests/cpp_tests/test_arrow.cpp create mode 100644 tests/cpp_tests/test_arrow_deprecated.cpp create mode 100644 tests/cpp_tests/test_byte_buffer.cpp create mode 100644 tests/cpp_tests/test_chunked_array.cpp create mode 100755 tests/cpp_tests/test_common.cpp create mode 100644 tests/cpp_tests/test_main.cpp create mode 100644 tests/cpp_tests/test_serialize.cpp create mode 100644 tests/cpp_tests/test_single_row.cpp create mode 100644 tests/cpp_tests/test_stream.cpp create mode 100644 tests/cpp_tests/testutils.cpp create mode 100644 tests/cpp_tests/testutils.h create mode 100644 tests/cpp_tests/train.conf create mode 100644 tests/data/categorical.data create mode 100644 tests/distributed/_test_distributed.py create mode 100644 tests/distributed/conftest.py create mode 100644 tests/python_package_test/__init__.py create mode 100644 tests/python_package_test/conftest.py create mode 100644 tests/python_package_test/test_arrow.py create mode 100644 tests/python_package_test/test_basic.py create mode 100644 tests/python_package_test/test_callback.py create mode 100644 tests/python_package_test/test_consistency.py create mode 100644 tests/python_package_test/test_dask.py create mode 100644 tests/python_package_test/test_dual.py create mode 100644 tests/python_package_test/test_engine.py create mode 100644 tests/python_package_test/test_plotting.py create mode 100644 tests/python_package_test/test_polars.py create mode 100644 tests/python_package_test/test_sklearn.py create mode 100644 tests/python_package_test/test_utilities.py create mode 100644 tests/python_package_test/utils.py create mode 100644 windows/LightGBM.sln create mode 100644 windows/LightGBM.vcxproj create mode 100644 windows/LightGBM.vcxproj.filters diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..40e4597 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,40 @@ +version: 4.6.0.99.{build} + +image: Visual Studio 2017 +platform: x64 +configuration: + - '3.10' + +# only build on 'master' / 'main' and pull requests targeting it +branches: + only: + - master + - main + +environment: + matrix: + - COMPILER: MSVC + TASK: python + - COMPILER: MINGW + TASK: python + +clone_depth: 5 + +install: + - git submodule update --init --recursive # get `external_libs` folder + - set PATH=C:\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin;%PATH% + - set PYTHON_VERSION=%CONFIGURATION% + - powershell.exe -ExecutionPolicy ByPass -c "Invoke-RestMethod https://pixi.sh/install.ps1 | Invoke-Expression" + - set PATH=C:\Users\appveyor\.pixi\bin;%PATH% + - ps: | + $env:APPVEYOR = "true" + $env:CMAKE_BUILD_PARALLEL_LEVEL = 4 + $env:BUILD_SOURCESDIRECTORY = "$env:APPVEYOR_BUILD_FOLDER" + # tell scripts where to put artifacts + # (this variable name is left over from when jobs ran on Azure DevOps) + $env:BUILD_ARTIFACTSTAGINGDIRECTORY = "$env:APPVEYOR_BUILD_FOLDER/artifacts" + +build: false + +test_script: + - powershell.exe -ExecutionPolicy Bypass -File %APPVEYOR_BUILD_FOLDER%\.ci\test-windows.ps1 diff --git a/.ci/README.md b/.ci/README.md new file mode 100644 index 0000000..fbcb229 --- /dev/null +++ b/.ci/README.md @@ -0,0 +1,6 @@ +Helper Scripts for CI +===================== + +This folder contains scripts which are run on CI services. + +Dockerfile used on CI service is maintained in a separate [GitHub repository](https://github.com/guolinke/lightgbm-ci-docker) and can be pulled from [Docker Hub](https://hub.docker.com/r/lightgbm/vsts-agent). diff --git a/.ci/append-comment.sh b/.ci/append-comment.sh new file mode 100755 index 0000000..8738495 --- /dev/null +++ b/.ci/append-comment.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# +# [description] +# Post a comment to a pull request. +# +# [usage] +# append-comment.sh +# +# PULL_REQUEST_ID: ID of PR to post the comment on. +# +# BODY: Text of the comment to be posted. + +set -e -E -u -o pipefail + +if [ -z "$GITHUB_ACTIONS" ]; then + echo "Must be run inside GitHub Actions CI" + exit 1 +fi + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +pr_id=$1 +body=$2 + +body=${body/failure/failure ❌} +body=${body/error/failure ❌} +body=${body/cancelled/failure ❌} +body=${body/timed_out/failure ❌} +body=${body/success/success ✔️} +data=$( + jq -n \ + --argjson body "\"$body\"" \ + '{"body": $body}' +) +curl -sL \ + --fail \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -d "$data" \ + "${GITHUB_API_URL}/repos/lightgbm-org/LightGBM/issues/${pr_id}/comments" diff --git a/.ci/build-docs.R b/.ci/build-docs.R new file mode 100644 index 0000000..d2c807b --- /dev/null +++ b/.ci/build-docs.R @@ -0,0 +1,19 @@ +loadNamespace("pkgdown") +loadNamespace("roxygen2") + +roxygen2::roxygenize( + "R-package/" + , load = "installed" +) + +pkgdown::build_site( + "R-package/" + , lazy = FALSE + , install = FALSE + , devel = FALSE + , examples = TRUE + , run_dont_run = TRUE + , seed = 42L + , preview = FALSE + , new_process = TRUE +) diff --git a/.ci/build-docs.sh b/.ci/build-docs.sh new file mode 100755 index 0000000..799ddfe --- /dev/null +++ b/.ci/build-docs.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +conda env create \ + --name test-env \ + --file ./docs/env.yml \ +|| exit 1 + +# shellcheck disable=SC1091 +source activate test-env + +make -C docs html || exit 1 diff --git a/.ci/check-dynamic-dependencies.sh b/.ci/check-dynamic-dependencies.sh new file mode 100755 index 0000000..bb83638 --- /dev/null +++ b/.ci/check-dynamic-dependencies.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# +# [description] +# Helper script for checking versions in the dynamic symbol table. +# This script checks that LightGBM library is linked to the appropriate symbol versions. +# Linking to newer symbol versions at compile time is problematic because it could result +# in built artifacts being unusable on older platforms. +# +# Version history for these symbols can be found at the following: +# * GLIBC: https://sourceware.org/glibc/wiki/Glibc%20Timeline +# * GLIBCXX: https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html +# * OMP/GOMP: https://github.com/gcc-mirror/gcc/blob/master/libgomp/libgomp.map +# +# [usage] +# check-dynamic-dependencies.sh +# +# PATH: Path to the file. +# Path to the file with the dynamic symbol table entries of the file +# (result of `objdump -T` command). + +set -e -E -u -o pipefail + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +INPUT_FILE="$1" + +if [ ! -f "$INPUT_FILE" ]; then + echo "Error: File '$INPUT_FILE' not found." + exit 1 +fi + +awk ' +BEGIN { + glibc_count = 0 + glibcxx_count = 0 + gomp_count = 0 + + has_error = 0 +} + +# --- Check GLIBC --- +/0{16}[ \t\(]+GLIBC_[0-9]+\.[0-9]+/ { + match($0, /GLIBC_([0-9]+)\.([0-9]+)/, parts) + if (RSTART > 0) { + match($0, /GLIBC_[0-9]+\.[0-9]+/) + ver_str = substr($0, RSTART+6, RLENGTH-6) # skip "GLIBC_" + split(ver_str, v, ".") + major = v[1] + 0 + minor = v[2] + 0 + + glibc_count++ + + if (major > 2 || (major == 2 && minor > 28)) { + print "Error: found unexpected GLIBC version: \x27" major "." minor "\x27" + has_error = 1 + } + } +} + +# --- Check GLIBCXX --- +/0{16}[ \t\(]+GLIBCXX_[0-9]+\.[0-9]+/ { + match($0, /GLIBCXX_[0-9]+\.[0-9]+(\.[0-9]+)?/) + if (RSTART > 0) { + ver_str = substr($0, RSTART+8, RLENGTH-8) # skip "GLIBCXX_" + n = split(ver_str, v, ".") + major = v[1] + 0 + minor = v[2] + 0 + patch = (n >= 3) ? v[3] + 0 : 0 + patch_str = (n >= 3) ? v[3] : "" + + glibcxx_count++ + + msg_ver = major "." minor + if (n >= 3) msg_ver = msg_ver "." patch + + if (major != 3 || minor != 4) { + print "Error: found unexpected GLIBCXX version: \x27" msg_ver "\x27" + has_error = 1 + } + if (n >= 3 && patch > 22) { + print "Error: found unexpected GLIBCXX version: \x27" msg_ver "\x27" + has_error = 1 + } + } +} + +# --- Check OMP/GOMP --- +/0{16}[ \t\(]+G?OMP_[0-9]+\.[0-9]+/ { + match($0, /G?OMP_[0-9]+\.[0-9]+/) + if (RSTART > 0) { + full_match = substr($0, RSTART, RLENGTH) + us_idx = index(full_match, "_") + ver_str = substr(full_match, us_idx + 1) + + split(ver_str, v, ".") + major = v[1] + 0 + minor = v[2] + 0 + + gomp_count++ + + if (major > 4 || (major == 4 && minor > 5)) { + print "Error: found unexpected OMP/GOMP version: \x27" major "." minor "\x27" + has_error = 1 + } + } +} + +END { + if (glibc_count <= 1) { + print "Error: Not enough GLIBC symbols found (found " glibc_count ", expected > 1)" + has_error = 1 + } + if (glibcxx_count <= 1) { + print "Error: Not enough GLIBCXX symbols found (found " glibcxx_count ", expected > 1)" + has_error = 1 + } + if (gomp_count <= 1) { + print "Error: Not enough OMP/GOMP symbols found (found " gomp_count ", expected > 1)" + has_error = 1 + } + + if (has_error == 1) { + exit 1 + } +} +' "$INPUT_FILE" diff --git a/.ci/check-omp-pragmas.sh b/.ci/check-omp-pragmas.sh new file mode 100755 index 0000000..ac83b16 --- /dev/null +++ b/.ci/check-omp-pragmas.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e -u + +echo "checking that all OpenMP pragmas specify num_threads()" +get_omp_pragmas_without_num_threads() { + grep \ + -n \ + -R \ + --include='*.c' \ + --include='*.cc' \ + --include='*.cpp' \ + --include='*.h' \ + --include='*.hpp' \ + 'pragma omp parallel' \ + | grep -v ' num_threads' +} + +# 'grep' returns a non-0 exit code if 0 lines were found. +# Turning off '-e -o pipefail' options here so that bash doesn't +# consider this a failure and stop execution of the script. +# +# ref: https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html +set +e +PROBLEMATIC_LINES=$( + get_omp_pragmas_without_num_threads +) +set -e +if test "${PROBLEMATIC_LINES}" != ""; then + get_omp_pragmas_without_num_threads + echo "Found '#pragma omp parallel' not using explicit num_threads() configuration. Fix those." + echo "For details, see https://www.openmp.org/spec-html/5.0/openmpse14.html#x54-800002.6" + exit 1 +fi +echo "done checking OpenMP pragmas" diff --git a/.ci/check-python-dists.sh b/.ci/check-python-dists.sh new file mode 100644 index 0000000..2cfe2c6 --- /dev/null +++ b/.ci/check-python-dists.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +set -e -u + +DIST_DIR=${1} + +# defaults +METHOD=${METHOD:-""} +TASK=${TASK:-""} + +echo "checking Python-package distributions in '${DIST_DIR}'" + +pip install \ + -qq \ + check-wheel-contents \ + twine || exit 1 + +echo "twine check..." +twine check --strict "$(echo "${DIST_DIR}"/*)" || exit 1 + +if { test "${TASK}" = "bdist" || test "${METHOD}" = "wheel"; }; then + echo "check-wheel-contents..." + check-wheel-contents "$(echo "${DIST_DIR}"/*.whl)" || exit 1 +fi + +PY_MINOR_VER=$(python -c "import sys; print(sys.version_info.minor)") +if [ "$PY_MINOR_VER" -gt 7 ]; then + echo "pydistcheck..." + pip install 'pydistcheck>=0.9.1' + if { test "${TASK}" = "cuda" || test "${METHOD}" = "wheel"; }; then + pydistcheck \ + --inspect \ + --ignore 'compiled-objects-have-debug-symbols'\ + --ignore 'distro-too-large-compressed' \ + --max-allowed-size-uncompressed '550M' \ + --max-allowed-files 806 \ + "$(echo "${DIST_DIR}"/*)" || exit 1 + elif { test "$(uname -m)" = "aarch64"; }; then + pydistcheck \ + --inspect \ + --ignore 'compiled-objects-have-debug-symbols' \ + --max-allowed-size-compressed '5M' \ + --max-allowed-size-uncompressed '15M' \ + --max-allowed-files 806 \ + "$(echo "${DIST_DIR}"/*)" || exit 1 + else + pydistcheck \ + --inspect \ + --max-allowed-size-compressed '5M' \ + --max-allowed-size-uncompressed '15M' \ + --max-allowed-files 806 \ + "$(echo "${DIST_DIR}"/*)" || exit 1 + fi +else + echo "skipping pydistcheck (does not support Python 3.${PY_MINOR_VER})" +fi + +echo "done checking Python-package distributions" diff --git a/.ci/check-workflow-status.sh b/.ci/check-workflow-status.sh new file mode 100755 index 0000000..711a4a1 --- /dev/null +++ b/.ci/check-workflow-status.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# [description] +# +# Look for the last run of a given GitHub Actions workflow on a given branch. +# If there's never been one (as might be the case with optional workflows like valgrind), +# exit with 0. +# +# Otherwise, check the status of that latest run. +# If it wasn't successful, exit with a non-0 exit code. +# +# [usage] +# +# check-workflow-status.sh +# +# BRANCH: name of a branch involved in a pull request. +# +# WORKFLOW_FILE: filename (e.g. 'r_valgrind.yml') defining the GitHub Actions workflow. +# + +set -e -u -o pipefail + +BRANCH="${1}" +WORKFLOW_FILE="${2}" +PR_NUMBER="${3}" + +# Limit how much data is pulled from the API and needs to be parsed locally. +OLDEST_ALLOWED_RUN_DATE=$(date --date='7 days ago' '+%F') + +echo "Searching for latest run of '${WORKFLOW_FILE}' on branch '${BRANCH}' " + +LATEST_RUN_ID=$( + gh run list \ + --repo 'lightgbm-org/LightGBM' \ + --event 'workflow_dispatch' \ + --created ">= ${OLDEST_ALLOWED_RUN_DATE}" \ + --workflow "${WORKFLOW_FILE}" \ + --json 'createdAt,databaseId,name' \ + --jq "sort_by(.createdAt) | reverse | map(select(.name | contains (\"pr=${PR_NUMBER}\"))) | .[0] | .databaseId" +) + +if [[ "${LATEST_RUN_ID}" == "" ]]; then + echo "No runs of '${WORKFLOW_FILE}' found on branch from pull request ${PR_NUMBER} (on or after ${OLDEST_ALLOWED_RUN_DATE})." + exit 0 +fi + +echo "Checking status of workflow run '${LATEST_RUN_ID}'" +gh run view \ + --repo "lightgbm-org/LightGBM" \ + --exit-status \ + "${LATEST_RUN_ID}" diff --git a/.ci/conda-envs/README.md b/.ci/conda-envs/README.md new file mode 100644 index 0000000..4666f96 --- /dev/null +++ b/.ci/conda-envs/README.md @@ -0,0 +1,11 @@ +# conda-envs + +This directory contains files used to create `conda` environments for development +and testing of LightGBM. + +The `.txt` files here are intended to be used with `conda create --file`. + +For details on that, see the `conda` docs: + +* `conda create` docs ([link](https://conda.io/projects/conda/en/latest/commands/create.html)) +* "Managing Environments" ([link](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html)) diff --git a/.ci/conda-envs/ci-core.txt b/.ci/conda-envs/ci-core.txt new file mode 100644 index 0000000..6a83c5d --- /dev/null +++ b/.ci/conda-envs/ci-core.txt @@ -0,0 +1,45 @@ +# [description] +# +# Core dependencies used across most LightGBM continuous integration (CI) jobs. +# +# 'python' constraint is intentionally omitted, so this file can be reused across +# Python versions. +# +# These floors are not the oldest versions LightGBM supports... they're here just to make conda +# solves faster, and should generally be the latest versions that work for all CI jobs using this. +# +# [usage] +# +# conda create \ +# --name test-env \ +# --file ./.ci/conda-envs/ci-core.txt \ +# python=3.10 +# + +# direct imports +cffi>=1.16 +dask>=2025.4.0 +joblib>=1.3.2 +matplotlib-base>=3.7.3 +narwhals>=1.15 +numpy>=1.24.4 +pandas>2.0 +polars>=1.0.0 +pyarrow-core>=16.0 +python-graphviz>=0.20.3 +scikit-learn>=1.3.2 +scipy>=1.7 + +# starting some time around 2026, 'pip' is no longer guaranteed to be installed +pip>=24 + +# testing-only dependencies +cloudpickle>=3.0.0 +psutil>=5.9.8 +pytest>=8.1.1 + +# other recursive dependencies, just +# pinned here to help speed up solves +pluggy>=1.4.0 +setuptools>=69.2 +wheel>=0.43 diff --git a/.ci/create-nuget.py b/.ci/create-nuget.py new file mode 100644 index 0000000..ad48d87 --- /dev/null +++ b/.ci/create-nuget.py @@ -0,0 +1,85 @@ +# coding: utf-8 +"""Script for generating files with NuGet package metadata.""" + +import datetime +import sys +from pathlib import Path +from shutil import copyfile + +if __name__ == "__main__": + source = Path(sys.argv[1]) + nuget_dir = Path(__file__).absolute().parent / "nuget" + print(f"Creating nuget directory '{nuget_dir}'") + linux_folder_path = nuget_dir / "runtimes" / "linux-x64" / "native" + linux_folder_path.mkdir(parents=True, exist_ok=True) + osx_folder_path = nuget_dir / "runtimes" / "osx-x64" / "native" + osx_folder_path.mkdir(parents=True, exist_ok=True) + windows_folder_path = nuget_dir / "runtimes" / "win-x64" / "native" + windows_folder_path.mkdir(parents=True, exist_ok=True) + build_folder_path = nuget_dir / "build" + build_folder_path.mkdir(parents=True, exist_ok=True) + print(f"Looking for libraries in '{source}'") + copyfile(source / "lib_lightgbm.so", linux_folder_path / "lib_lightgbm.so") + copyfile(source / "lib_lightgbm.dylib", osx_folder_path / "lib_lightgbm.dylib") + copyfile(source / "lib_lightgbm.dll", windows_folder_path / "lib_lightgbm.dll") + copyfile(source / "lightgbm.exe", windows_folder_path / "lightgbm.exe") + version = (nuget_dir.parents[1] / "VERSION.txt").read_text(encoding="utf-8").strip().replace("rc", "-rc") + print(f"Setting version to '{version}'") + nuget_str = rf""" + + + LightGBM + {version} + Guolin Ke + Guolin Ke + MIT + https://github.com/lightgbm-org/LightGBM + false + A fast, distributed, high performance gradient boosting framework + Copyright {datetime.datetime.now().year} @ Microsoft + machine-learning data-mining distributed native boosting gbdt + + + + + + + + """ + prop_str = r""" + + + + PreserveNewest + false + + + PreserveNewest + false + + + + """ + target_str = r""" + + + true + + + + + + """ + (nuget_dir / "LightGBM.nuspec").write_text(nuget_str, encoding="utf-8") + (nuget_dir / "build" / "LightGBM.props").write_text(prop_str, encoding="utf-8") + (nuget_dir / "build" / "LightGBM.targets").write_text(target_str, encoding="utf-8") + print("Done creating NuGet package") diff --git a/.ci/download-artifacts.sh b/.ci/download-artifacts.sh new file mode 100755 index 0000000..11f4bb9 --- /dev/null +++ b/.ci/download-artifacts.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# [description] +# Collect and download artifacts from all workflow runs for a commit. +# +# [usage] +# ./download-artifacts.sh +# + +set -e -u -E -o pipefail + +COMMIT_ID="${1}" +OUTPUT_DIR="./release-artifacts" + +get-latest-run-id() { + gh run list \ + --repo "lightgbm-org/LightGBM" \ + --commit "${1}" \ + --workflow "${2}" \ + --json 'createdAt,databaseId' \ + --jq 'sort_by(.createdAt) | reverse | .[0] | .databaseId' +} + +# ensure directory for storing artifacts exists +echo "preparing to download artifacts for commit '${COMMIT_ID}' to '${OUTPUT_DIR}'" +mkdir -p "${OUTPUT_DIR}" + +# get core artifacts +echo "downloading core artifacts" +gh run download \ + --repo "lightgbm-org/LightGBM" \ + --dir "${OUTPUT_DIR}" \ + "$(get-latest-run-id "${COMMIT_ID}" 'build.yml')" +echo "done downloading core artifacts" + +# get python-package artifacts +echo "downloading python-package artifacts and NuGet package" +gh run download \ + --repo "lightgbm-org/LightGBM" \ + --dir "${OUTPUT_DIR}" \ + "$(get-latest-run-id "${COMMIT_ID}" 'python_package.yml')" +echo "done downloading python-package artifacts and NuGet package" + +# get R-package artifacts +echo "downloading R-package artifacts" +gh run download \ + --repo "lightgbm-org/LightGBM" \ + --dir "${OUTPUT_DIR}" \ + "$(get-latest-run-id "${COMMIT_ID}" 'r_package.yml')" +echo "done downloading R-package artifacts" + +# get SWIG artifacts +echo "downloading SWIG artifacts" +gh run download \ + --repo "lightgbm-org/LightGBM" \ + --dir "${OUTPUT_DIR}" \ + "$(get-latest-run-id "${COMMIT_ID}" 'swig.yml')" +echo "done downloading SWIG artifacts" + +# 'gh run download' unpackages into nested directories like {artifact-name}/{file}. +# +# This moves all files to the top level and then deletes those {artifact-name}/ directories, +# to make it easier to bulk upload all files to a release. +echo "flattening directory structure" +find "${OUTPUT_DIR}" -type f -mindepth 2 -exec mv -i '{}' "${OUTPUT_DIR}" \; +find "${OUTPUT_DIR}" -type d -mindepth 1 -exec rm -r '{}' \+ + +echo "downloaded artifacts:" +find "${OUTPUT_DIR}" -type f diff --git a/.ci/install-opencl.ps1 b/.ci/install-opencl.ps1 new file mode 100644 index 0000000..76bcf5c --- /dev/null +++ b/.ci/install-opencl.ps1 @@ -0,0 +1,40 @@ +Write-Output "Installing OpenCL CPU platform" + +$installer = "AMD-APP-SDKInstaller-v3.0.130.135-GA-windows-F-x64.exe" + +Write-Output "Downloading OpenCL platform installer" +$ProgressPreference = "SilentlyContinue" # progress bar bug extremely slows down download speed +$params = @{ + OutFile = "$installer" + Uri = "https://github.com/lightgbm-org/LightGBM/releases/download/v2.0.12/$installer" +} +Invoke-WebRequest @params + +if (Test-Path "$installer") { + Write-Output "Successfully downloaded OpenCL platform installer" +} else { + Write-Output "Unable to download OpenCL platform installer" + Write-Output "Setting EXIT" + $host.SetShouldExit(-1) + exit 1 +} + +# Install OpenCL platform from installer executable +Write-Output "Running OpenCL installer" +Invoke-Command -ScriptBlock { + Start-Process "$installer" -ArgumentList '/S /V"/quiet /norestart /passive /log opencl.log"' -Wait +} + +$property = Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\OpenCL\Vendors +if ($null -eq $property) { + Write-Output "Unable to install OpenCL CPU platform" + Write-Output "OpenCL installation log:" + Get-Content "opencl.log" + Write-Output "Setting EXIT" + $host.SetShouldExit(-1) + exit 1 +} else { + Write-Output "Successfully installed OpenCL CPU platform" + Write-Output "Current OpenCL drivers:" + Write-Output $property +} diff --git a/.ci/install-r-deps.R b/.ci/install-r-deps.R new file mode 100644 index 0000000..da13aa8 --- /dev/null +++ b/.ci/install-r-deps.R @@ -0,0 +1,125 @@ +# Install R dependencies, using only base R. +# +# Supported arguments: +# +# --all Install all the 'Depends', 'Imports', 'LinkingTo', and 'Suggests' dependencies +# (automatically implies --build --test). +# +# --build Install the packages needed to build. +# +# --exclude= Comma-delimited list of packages to NOT install. +# +# --include= Comma-delimited list of additional packages to install. +# These will always be installed, unless also used in "--exclude". +# +# --test Install packages needed to run tests. +# + + +# [description] Parse command line arguments into an R list. +# Returns a list where keys are arguments and values +# are either TRUE (for flags) or a vector of values passed via a +# comma-delimited list. +.parse_args <- function(args) { + out <- list( + "--all" = FALSE + , "--build" = FALSE + , "--exclude" = character(0L) + , "--include" = character(0L) + , "--test" = FALSE + ) + for (arg in args) { + parsed_arg <- unlist(strsplit(arg, "=", fixed = TRUE)) + arg_name <- parsed_arg[[1L]] + if (!(arg_name %in% names(out))) { + stop(sprintf("Unrecognized argument: '%s'", arg_name)) + } + if (length(parsed_arg) == 2L) { + # lists, like "--include=roxygen2,testthat" + values <- unlist(strsplit(parsed_arg[[2L]], ",", fixed = TRUE)) + out[[arg_name]] <- values + } else { + # flags, like "--build" + out[[arg]] <- TRUE + } + } + return(out) +} + +args <- .parse_args( + commandArgs(trailingOnly = TRUE) +) + +# which dependencies to install +ALL_DEPS <- isTRUE(args[["--all"]]) +BUILD_DEPS <- ALL_DEPS || isTRUE(args[["--build"]]) +TEST_DEPS <- ALL_DEPS || isTRUE(args[["--test"]]) + +# force downloading of binary packages on macOS +COMPILE_FROM_SOURCE <- "both" +PACKAGE_TYPE <- getOption("pkgType") + +# CRAN has precompiled binaries for macOS and Windows... prefer those, +# for faster installation. +if (Sys.info()[["sysname"]] == "Darwin" || .Platform$OS.type == "windows") { + COMPILE_FROM_SOURCE <- "never" + PACKAGE_TYPE <- "binary" +} +options( + install.packages.check.source = "no" + , install.packages.compile.from.source = COMPILE_FROM_SOURCE +) + +# always use the same CRAN mirror +CRAN_MIRROR <- Sys.getenv("CRAN_MIRROR", unset = "https://cran.r-project.org") + +# we always want these +deps_to_install <- c( + "data.table" + , "jsonlite" + , "Matrix" + , "R6" +) + +if (isTRUE(BUILD_DEPS)) { + deps_to_install <- c( + deps_to_install + , "knitr" + , "markdown" + ) +} + +if (isTRUE(TEST_DEPS)) { + deps_to_install <- c( + deps_to_install + , "RhpcBLASctl" + , "testthat" + ) +} + +# add packages passed through '--include' +deps_to_install <- unique(c( + deps_to_install + , args[["--include"]] +)) + +# remove packages passed through '--exclude' +deps_to_install <- setdiff( + x = deps_to_install + , args[["--exclude"]] +) + +msg <- sprintf( + "[install-r-deps] installing R packages: %s\n" + , toString(sort(deps_to_install)) +) +cat(msg) + +install.packages( # nolint: undesirable_function. + pkgs = deps_to_install + , dependencies = c("Depends", "Imports", "LinkingTo") + , lib = Sys.getenv("R_LIB_PATH", unset = .libPaths()[[1L]]) + , repos = CRAN_MIRROR + , type = PACKAGE_TYPE + , Ncpus = parallel::detectCores() +) diff --git a/.ci/lint-all.sh b/.ci/lint-all.sh new file mode 100755 index 0000000..3a3bfb2 --- /dev/null +++ b/.ci/lint-all.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +pwsh -command "Install-Module -Name PSScriptAnalyzer -Scope CurrentUser -SkipPublisherCheck" +echo "Linting PowerShell code" +pwsh -file ./.ci/lint-powershell.ps1 || exit 1 + +conda create -q -y -n test-env \ + "python=3.14[build=*_cp*]" \ + 'pre-commit>=3.8.0' \ + 'r-lintr>=3.3.0' + +# shellcheck disable=SC1091 +source activate test-env + +echo "Running pre-commit checks" +pre-commit run --all-files || exit 1 + +echo "Linting R code" +Rscript ./.ci/lint-r-code.R "$(pwd)" || exit 1 diff --git a/.ci/lint-powershell.ps1 b/.ci/lint-powershell.ps1 new file mode 100644 index 0000000..ff0bd0e --- /dev/null +++ b/.ci/lint-powershell.ps1 @@ -0,0 +1,87 @@ +$ErrorActionPreference = 'Stop' + +$settings = @{ + Severity = @( + 'Information', + 'Warning', + 'Error' + ) + IncludeDefaultRules = $true + ExcludeRules = @( + 'PSAvoidUsingInvokeExpression' + ) + # Additional rules that are disabled by default. + # + # Some of the skips could be replaced with inline comments if PSScriptAnalyzer + # supports that in the future (https://github.com/PowerShell/PSScriptAnalyzer/issues/849). + Rules = @{ + PSAvoidExclaimOperator = @{ + Enable = $true + } + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 120 + } + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } + PSPlaceCloseBrace = @{ + Enable = $true + NoEmptyLineBefore = $true + IgnoreOneLineBlock = $true + NewLineAfter = $false + } + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + PSUseConsistentIndentation = @{ + Enable = $true + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + Kind = 'space' + } + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckSeparator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $true + CheckParameter = $true + IgnoreAssignmentOperatorInsideHashTable = $false + } + PSUseCorrectCasing = @{ + Enable = $true + } + } +} + +# this pre-listing of files can be removed whenever PSScriptAnalyzer adds support for exclusions. +# +# see: +# +# * https://github.com/PowerShell/PSScriptAnalyzer/issues/561 +# * https://github.com/PowerShell/vscode-powershell/issues/3048 +# +# lint-powershell.ps1 itself is included here because linting this script itself +# sometimes fails (non-deterministically!) with an error like "Object reference not set to an instance of an object" +# +$files = @( + Get-ChildItem -Path ./ -Recurse -Force -Filter '*.ps1' | + Where-Object { $_.FullName -notmatch '[/\\]bin[/\\]' } | + Where-Object { $_.FullName -notmatch '[/\\]external_libs[/\\]' } | + Where-Object { $_.FullName -notmatch '[/\\]\.pixi[/\\]' } | + Where-Object { $_.FullName -notmatch '[/\\]venv[/\\]' } | + Where-Object { $_.Name -ne 'lint-powershell.ps1' } | + ForEach-Object { $_.FullName } +) + +foreach ($file in $files) { + Write-Output "linting '$file'" + Invoke-ScriptAnalyzer -Path $file -EnableExit -Settings $settings +} diff --git a/.ci/lint-r-code.R b/.ci/lint-r-code.R new file mode 100755 index 0000000..9eae00a --- /dev/null +++ b/.ci/lint-r-code.R @@ -0,0 +1,154 @@ +loadNamespace("lintr") + +args <- commandArgs( + trailingOnly = TRUE +) +SOURCE_DIR <- args[[1L]] + +FILES_TO_LINT <- list.files( + path = SOURCE_DIR + , pattern = "\\.r$|\\.rmd$" + , all.files = TRUE + , ignore.case = TRUE + , full.names = TRUE + , recursive = TRUE + , include.dirs = FALSE +) + +# text to use for pipe operators from packages like 'magrittr' +pipe_text <- paste0( + "For consistency and the sake of being explicit, this project's code " + , "does not use the pipe operator." +) + +# text to use for functions that should only be called interactively +interactive_text <- paste0( + "Functions like '?', 'help', and 'install.packages()' should only be used " + , "interactively, not in package code." +) + +LINTERS_TO_USE <- list( + "absolute_path" = lintr::absolute_path_linter() + , "any_duplicated" = lintr::any_duplicated_linter() + , "any_is_na" = lintr::any_is_na_linter() + , "assignment" = lintr::assignment_linter() + , "backport" = lintr::backport_linter() + , "boolean_arithmetic" = lintr::boolean_arithmetic_linter() + , "braces" = lintr::brace_linter() + , "class_equals" = lintr::class_equals_linter() + , "commas" = lintr::commas_linter() + , "conjunct_test" = lintr::conjunct_test_linter() + , "duplicate_argument" = lintr::duplicate_argument_linter() + , "empty_assignment" = lintr::empty_assignment_linter() + , "equals_na" = lintr::equals_na_linter() + , "fixed_regex" = lintr::fixed_regex_linter() + , "for_loop_index" = lintr::for_loop_index_linter() + , "function_left" = lintr::function_left_parentheses_linter() + , "function_return" = lintr::function_return_linter() + , "implicit_assignment" = lintr::implicit_assignment_linter() + , "implicit_integers" = lintr::implicit_integer_linter() + , "infix_spaces" = lintr::infix_spaces_linter() + , "inner_combine" = lintr::inner_combine_linter() + , "is_numeric" = lintr::is_numeric_linter() + , "lengths" = lintr::lengths_linter() + , "length_levels" = lintr::length_levels_linter() + , "length_test" = lintr::length_test_linter() + , "line_length" = lintr::line_length_linter(length = 120L) + , "literal_coercion" = lintr::literal_coercion_linter() + , "matrix" = lintr::matrix_apply_linter() + , "missing_argument" = lintr::missing_argument_linter() + , "non_portable_path" = lintr::nonportable_path_linter() + , "numeric_leading_zero" = lintr::numeric_leading_zero_linter() + , "outer_negation" = lintr::outer_negation_linter() + , "package_hooks" = lintr::package_hooks_linter() + , "paren_body" = lintr::paren_body_linter() + , "paste" = lintr::paste_linter() + , "quotes" = lintr::quotes_linter() + , "redundant_equals" = lintr::redundant_equals_linter() + , "regex_subset" = lintr::regex_subset_linter() + , "routine_registration" = lintr::routine_registration_linter() + , "scalar_in" = lintr::scalar_in_linter() + , "semicolon" = lintr::semicolon_linter() + , "seq" = lintr::seq_linter() + , "spaces_inside" = lintr::spaces_inside_linter() + , "spaces_left_parens" = lintr::spaces_left_parentheses_linter() + , "sprintf" = lintr::sprintf_linter() + , "string_boundary" = lintr::string_boundary_linter() + , "todo_comments" = lintr::todo_comment_linter(c("todo", "fixme", "to-do")) + , "trailing_blank" = lintr::trailing_blank_lines_linter() + , "trailing_white" = lintr::trailing_whitespace_linter() + , "true_false" = lintr::T_and_F_symbol_linter() + , "undesirable_function" = lintr::undesirable_function_linter( + fun = c( + "cbind" = paste0( + "cbind is an unsafe way to build up a data frame. merge() or direct " + , "column assignment is preferred." + ) + , "dyn.load" = "Directly loading or unloading .dll or .so files in package code should not be necessary." + , "dyn.unload" = "Directly loading or unloading .dll or .so files in package code should not be necessary." + , "help" = interactive_text + , "ifelse" = "The use of ifelse() is dangerous because it will silently allow mixing types." + , "install.packages" = interactive_text + , "is.list" = paste0( + "This project uses data.table, and is.list(x) is TRUE for a data.table. " + , "identical(class(x), 'list') is a safer way to check that something is an R list object." + ) + , "rbind" = "data.table::rbindlist() is faster and safer than rbind(), and is preferred in this project." + , "require" = paste0( + "library() is preferred to require() because it will raise an error immediately " + , "if a package is missing." + ) + ) + ) + , "undesirable_operator" = lintr::undesirable_operator_linter( + op = c( + "%>%" = pipe_text + , "%.%" = pipe_text + , "%..%" = pipe_text + , "|>" = pipe_text + , "?" = interactive_text + , "??" = interactive_text + ) + ) + , "unnecessary_concatenation" = lintr::unnecessary_concatenation_linter() + , "unnecessary_lambda" = lintr::unnecessary_lambda_linter() + , "unreachable_code" = lintr::unreachable_code_linter() + , "unused_import" = lintr::unused_import_linter() + , "vector_logic" = lintr::vector_logic_linter() + , "whitespace" = lintr::whitespace_linter() +) + +noquote(paste0(length(FILES_TO_LINT), " R files need linting")) + +results <- NULL + +for (r_file in FILES_TO_LINT) { + + this_result <- lintr::lint( + filename = r_file + , linters = LINTERS_TO_USE + , cache = FALSE + ) + + print( + sprintf( + "Found %i linting errors in %s" + , length(this_result) + , r_file + ) + , quote = FALSE + ) + + results <- c(results, this_result) + +} + +issues_found <- length(results) + +noquote(paste0("Total linting issues found: ", issues_found)) + +if (issues_found > 0L) { + print(results) +} + +quit(save = "no", status = issues_found) diff --git a/.ci/parameter-generator.py b/.ci/parameter-generator.py new file mode 100644 index 0000000..af020ec --- /dev/null +++ b/.ci/parameter-generator.py @@ -0,0 +1,408 @@ +# coding: utf-8 +"""Helper script for generating config file and parameters list. + +This script generates LightGBM/src/io/config_auto.cpp file +with list of all parameters, aliases table and other routines +along with parameters description in LightGBM/docs/Parameters.rst file +from the information in LightGBM/include/LightGBM/config.h file. +""" + +import re +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Tuple + + +def get_parameter_infos(config_hpp: Path) -> Tuple[List[Tuple[str, int]], List[List[Dict[str, List]]]]: + """Parse config header file. + + Parameters + ---------- + config_hpp : pathlib.Path + Path to the config header file. + + Returns + ------- + infos : tuple + Tuple with names and content of sections. + """ + is_inparameter = False + cur_key = None + key_lvl = 0 + cur_info: Dict[str, List] = {} + keys = [] + member_infos: List[List[Dict[str, List]]] = [] + with open(config_hpp) as config_hpp_file: + for line in config_hpp_file: + if line.strip() in {"#ifndef __NVCC__", "#endif // __NVCC__"}: + continue + if "#pragma region Parameters" in line: + is_inparameter = True + elif "#pragma region" in line and "Parameters" in line: + key_lvl += 1 + cur_key = line.split("region")[1].strip() + keys.append((cur_key, key_lvl)) + member_infos.append([]) + elif "#pragma endregion" in line: + key_lvl -= 1 + if cur_key is not None: + cur_key = None + elif is_inparameter: + is_inparameter = False + elif cur_key is not None: + line = line.strip() + if line.startswith("//"): + key, _, val = line[2:].partition("=") + key = key.strip() + val = val.strip() + if key not in cur_info: + if key == "descl2" and "desc" not in cur_info: + cur_info["desc"] = [] + elif key != "descl2": + cur_info[key] = [] + if key == "desc": + cur_info["desc"].append(("l1", val)) + elif key == "descl2": + cur_info["desc"].append(("l2", val)) + else: + cur_info[key].append(val) + elif line: + has_eqsgn = False + tokens = line.split("=") + if len(tokens) == 2: + if "default" not in cur_info: + cur_info["default"] = [tokens[1][:-1].strip()] + has_eqsgn = True + tokens = line.split() + cur_info["inner_type"] = [tokens[0].strip()] + if "name" not in cur_info: + if has_eqsgn: + cur_info["name"] = [tokens[1].strip()] + else: + cur_info["name"] = [tokens[1][:-1].strip()] + member_infos[-1].append(cur_info) + cur_info = {} + + return keys, member_infos + + +def get_names(infos: List[List[Dict[str, List]]]) -> List[str]: + """Get names of all parameters. + + Parameters + ---------- + infos : list + Content of the config header file. + + Returns + ------- + names : list + Names of all parameters. + """ + names = [] + for x in infos: + for y in x: + names.append(y["name"][0]) + return names + + +def get_alias(infos: List[List[Dict[str, List]]]) -> List[Tuple[str, str]]: + """Get aliases of all parameters. + + Parameters + ---------- + infos : list + Content of the config header file. + + Returns + ------- + pairs : list + List of tuples (param alias, param name). + """ + pairs = [] + for x in infos: + for y in x: + if "alias" in y: + name = y["name"][0] + alias = y["alias"][0].split(",") + for name2 in alias: + pairs.append((name2.strip(), name)) + return pairs + + +def parse_check(check: str, reverse: bool = False) -> Tuple[str, str]: + """Parse the constraint. + + Parameters + ---------- + check : str + String representation of the constraint. + reverse : bool, optional (default=False) + Whether to reverse the sign of the constraint. + + Returns + ------- + pair : tuple + Parsed constraint in the form of tuple (value, sign). + """ + try: + idx = 1 + float(check[idx:]) + except ValueError: + idx = 2 + float(check[idx:]) + if reverse: + reversed_sign = {"<": ">", ">": "<", "<=": ">=", ">=": "<="} + return check[idx:], reversed_sign[check[:idx]] + else: + return check[idx:], check[:idx] + + +def set_one_var_from_string(name: str, param_type: str, checks: List[str]) -> str: + """Construct code for auto config file for one param value. + + Parameters + ---------- + name : str + Name of the parameter. + param_type : str + Type of the parameter. + checks : list + Constraints of the parameter. + + Returns + ------- + ret : str + Lines of auto config file with getting and checks of one parameter value. + """ + ret = "" + univar_mapper = {"int": "GetInt", "double": "GetDouble", "bool": "GetBool", "std::string": "GetString"} + if "vector" not in param_type: + ret += f' {univar_mapper[param_type]}(params, "{name}", &{name});\n' + if len(checks) > 0: + check_mapper = {"<": "LT", ">": "GT", "<=": "LE", ">=": "GE"} + for check in checks: + value, sign = parse_check(check) + ret += f" CHECK_{check_mapper[sign]}({name}, {value});\n" + ret += "\n" + else: + ret += f' if (GetString(params, "{name}", &tmp_str)) {{\n' + type2 = param_type.split("<")[1][:-1] + if type2 == "std::string": + ret += f" {name} = Common::Split(tmp_str.c_str(), ',');\n" + else: + ret += f" {name} = Common::StringToArray<{type2}>(tmp_str, ',');\n" + ret += " }\n\n" + return ret + + +def gen_parameter_description( + sections: List[Tuple[str, int]], descriptions: List[List[Dict[str, List]]], params_rst: Path +) -> None: + """Write descriptions of parameters to the documentation file. + + Parameters + ---------- + sections : list + Names of parameters sections. + descriptions : list + Structured descriptions of parameters. + params_rst : pathlib.Path + Path to the file with parameters documentation. + """ + params_to_write = [] + lvl_mapper = {1: "-", 2: "~"} + for (section_name, section_lvl), section_params in zip(sections, descriptions, strict=True): + heading_sign = lvl_mapper[section_lvl] + params_to_write.append(f"{section_name}\n{heading_sign * len(section_name)}") + for param_desc in section_params: + name = param_desc["name"][0] + default_raw = param_desc["default"][0] + default = default_raw.strip('"') if len(default_raw.strip('"')) > 0 else default_raw + param_type = param_desc.get("type", param_desc["inner_type"])[0].split(":")[-1].split("<")[-1].strip(">") + options = param_desc.get("options", []) + if len(options) > 0: + opts = "``, ``".join([x.strip() for x in options[0].split(",")]) + options_str = f", options: ``{opts}``" + else: + options_str = "" + aliases = param_desc.get("alias", []) + if len(aliases) > 0: + aliases_joined = "``, ``".join([x.strip() for x in aliases[0].split(",")]) + aliases_str = f", aliases: ``{aliases_joined}``" + else: + aliases_str = "" + checks = sorted(param_desc.get("check", [])) + checks_len = len(checks) + if checks_len > 1: + number1, sign1 = parse_check(checks[0]) + number2, sign2 = parse_check(checks[1], reverse=True) + checks_str = f", constraints: ``{number2} {sign2} {name} {sign1} {number1}``" + elif checks_len == 1: + number, sign = parse_check(checks[0]) + checks_str = f", constraints: ``{name} {sign} {number}``" + else: + checks_str = "" + main_desc = f'- ``{name}`` :raw-html:`🔗︎`, default = ``{default}``, type = {param_type}{options_str}{aliases_str}{checks_str}' + params_to_write.append(main_desc) + params_to_write.extend([f"{' ' * 3 * int(desc[0][-1])}- {desc[1]}" for desc in param_desc["desc"]]) + + with open(params_rst) as original_params_file: + all_lines = original_params_file.read() + before, start_sep, _ = all_lines.partition(".. start params list\n\n") + _, end_sep, after = all_lines.partition("\n\n.. end params list") + + with open(params_rst, "w") as new_params_file: + new_params_file.write(before) + new_params_file.write(start_sep) + new_params_file.write("\n\n".join(params_to_write)) + new_params_file.write(end_sep) + new_params_file.write(after) + + +def gen_parameter_code( + config_hpp: Path, config_out_cpp: Path +) -> Tuple[List[Tuple[str, int]], List[List[Dict[str, List]]]]: + """Generate auto config file. + + Parameters + ---------- + config_hpp : pathlib.Path + Path to the config header file. + config_out_cpp : pathlib.Path + Path to the auto config file. + + Returns + ------- + infos : tuple + Tuple with names and content of sections. + """ + keys, infos = get_parameter_infos(config_hpp) + names = get_names(infos) + alias = get_alias(infos) + names_with_aliases = defaultdict(list) + str_to_write = r"""/*! + * Copyright (c) 2018-2026 Microsoft Corporation. All rights reserved. + * Copyright (c) 2018-2026 The LightGBM developers. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + * + * \note + * This file is auto generated by LightGBM\.ci\parameter-generator.py from LightGBM\include\LightGBM\config.h file. + */ +""" + str_to_write += "#include \n\n" + str_to_write += "#include \n" + str_to_write += "#include \n" + str_to_write += "#include \n" + str_to_write += "#include \n\n" + str_to_write += "namespace LightGBM {\n" + + # alias table + str_to_write += "const std::unordered_map& Config::alias_table() {\n" + str_to_write += " static std::unordered_map aliases({\n" + + for pair in alias: + str_to_write += f' {{"{pair[0]}", "{pair[1]}"}},\n' + names_with_aliases[pair[1]].append(pair[0]) + str_to_write += " });\n" + str_to_write += " return aliases;\n" + str_to_write += "}\n\n" + + # names + str_to_write += "const std::unordered_set& Config::parameter_set() {\n" + str_to_write += " static std::unordered_set params({\n" + + for name in names: + str_to_write += f' "{name}",\n' + str_to_write += " });\n" + str_to_write += " return params;\n" + str_to_write += "}\n\n" + # from strings + str_to_write += "void Config::GetMembersFromString(const std::unordered_map& params) {\n" + str_to_write += ' std::string tmp_str = "";\n' + for x in infos: + for y in x: + if "[no-automatically-extract]" in y: + continue + param_type = y["inner_type"][0] + name = y["name"][0] + checks = [] + if "check" in y: + checks = y["check"] + tmp = set_one_var_from_string(name, param_type, checks) + str_to_write += tmp + # tails + str_to_write = f"{str_to_write.strip()}\n}}\n\n" + str_to_write += "std::string Config::SaveMembersToString() const {\n" + str_to_write += " std::stringstream str_buf;\n" + for x in infos: + for y in x: + if "[no-save]" in y: + continue + param_type = y["inner_type"][0] + name = y["name"][0] + if "vector" in param_type: + if "int8" in param_type: + str_to_write += f' str_buf << "[{name}: " << Common::Join(Common::ArrayCast({name}), ",") << "]\\n";\n' + else: + str_to_write += f' str_buf << "[{name}: " << Common::Join({name}, ",") << "]\\n";\n' + else: + str_to_write += f' str_buf << "[{name}: " << {name} << "]\\n";\n' + # tails + str_to_write += " return str_buf.str();\n" + str_to_write += "}\n\n" + + str_to_write += """const std::unordered_map>& Config::parameter2aliases() { + static std::unordered_map> map({""" + for name in names: + str_to_write += '\n {"' + name + '", ' + if names_with_aliases[name]: + str_to_write += '{"' + '", "'.join(names_with_aliases[name]) + '"}},' + else: + str_to_write += "{}}," + str_to_write += """ + }); + return map; +} + +""" + str_to_write += """const std::unordered_map& Config::ParameterTypes() { + static std::unordered_map map({""" + int_t_pat = re.compile(r"int\d+_t") + # the following are stored as comma separated strings but are arrays in the wrappers + overrides = { + "categorical_feature": "vector", + "ignore_column": "vector", + "interaction_constraints": "vector>", + } + for x in infos: + for y in x: + name = y["name"][0] + if name == "task": + continue + if name in overrides: + param_type = overrides[name] + else: + param_type = int_t_pat.sub("int", y["inner_type"][0]).replace("std::", "") + str_to_write += '\n {"' + name + '", "' + param_type + '"},' + str_to_write += """ + }); + return map; +} + +""" + + str_to_write += "} // namespace LightGBM\n" + with open(config_out_cpp, "w") as config_out_cpp_file: + config_out_cpp_file.write(str_to_write) + + return keys, infos + + +if __name__ == "__main__": + current_dir = Path(__file__).absolute().parent + config_hpp = current_dir.parent / "include" / "LightGBM" / "config.h" + config_out_cpp = current_dir.parent / "src" / "io" / "config_auto.cpp" + params_rst = current_dir.parent / "docs" / "Parameters.rst" + sections, descriptions = gen_parameter_code(config_hpp, config_out_cpp) + gen_parameter_description(sections, descriptions, params_rst) diff --git a/.ci/pip-envs/requirements-latest.txt b/.ci/pip-envs/requirements-latest.txt new file mode 100644 index 0000000..c7be0aa --- /dev/null +++ b/.ci/pip-envs/requirements-latest.txt @@ -0,0 +1,24 @@ +# nightlies for: matplotlib, numpy, pandas, pyarrow, scipy, scikit-learn +--extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple + +# runtime dependencies (using `.dev0` suffix to allow nightlies) +# +# pinning rules: +# +# * latest versions of lightgbm's dependencies, +# * including pre-releases and nightlies +# +cffi>=2.0.0 +matplotlib>=3.11.0.dev0 +narwhals>=2.21.0.dev0 +numpy>=2.5.0.dev0 +pandas>=3.1.0.dev0 +polars>=1.40.1.dev0 +pyarrow>=24.0.0.dev0 +scikit-learn>=1.9.dev0 +scipy>=1.18.0.dev0 + +# testing-only dependencies +cloudpickle>=3.1.2 +psutil>=7.2.2 +pytest>=9.0.2 diff --git a/.ci/pip-envs/requirements-oldest.txt b/.ci/pip-envs/requirements-oldest.txt new file mode 100644 index 0000000..fb38b71 --- /dev/null +++ b/.ci/pip-envs/requirements-oldest.txt @@ -0,0 +1,15 @@ +# oldest versions of dependencies published after +# minimum supported Python version's first release, +# for which there are wheels compatible with the +# python:{version} image +# +# see https://devguide.python.org/versions/ +# +cffi==1.15.1 +narwhals==1.15.0 +numpy==1.22.4 +pandas==1.3.4 +polars==1.0.0 +pyarrow==16.0.0 +scikit-learn==1.0.2 +scipy==1.7.2 diff --git a/.ci/rerun-workflow.sh b/.ci/rerun-workflow.sh new file mode 100755 index 0000000..8f343df --- /dev/null +++ b/.ci/rerun-workflow.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# +# [description] +# Rerun specified workflow for given pull request. +# +# [usage] +# rerun-workflow.sh +# +# WORKFLOW_ID: Identifier (config name of ID) of a workflow to be rerun. +# +# PR_BRANCH: Name of pull request's branch. + +set -e -E -u -o pipefail + +if [ -z "$GITHUB_ACTIONS" ]; then + echo "Must be run inside GitHub Actions CI" + exit 1 +fi + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +workflow_id=$1 +pr_branch=$2 + +# --branch for some GitHub GLI commands does not respect the difference between forks and branches +# on the main repo. While some parts of the GitHub API refer to the branch of a workflow +# as '{org}:{branch}' for branches from forks, others expect only '{branch}'. +# +# This expansion trims a leading '{org}:' from 'pr_branch' if one is present. +pr_branch_no_fork_prefix="${pr_branch/*:/}" + +RUN_ID=$( + gh run list \ + --repo 'lightgbm-org/LightGBM' \ + --workflow "${workflow_id}" \ + --event "pull_request" \ + --branch "${pr_branch_no_fork_prefix}" \ + --json 'createdAt,databaseId' \ + --jq 'sort_by(.createdAt) | reverse | .[0] | .databaseId' +) + +if [[ -z "${RUN_ID}" ]]; then + echo "ERROR: failed to find a run of workflow '${workflow_id}' for branch '${pr_branch}'" + exit 1 +fi + +echo "Re-running workflow '${workflow_id}' (run ID ${RUN_ID})" + +gh run rerun \ + --repo 'lightgbm-org/LightGBM' \ + "${RUN_ID}" diff --git a/.ci/run-r-cmd-check.sh b/.ci/run-r-cmd-check.sh new file mode 100755 index 0000000..239425c --- /dev/null +++ b/.ci/run-r-cmd-check.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e -u -o pipefail + +PKG_TARBALL="${1}" +declare -i ALLOWED_CHECK_NOTES=${2} + +# 'R CMD check' redirects installation logs to a file, and returns +# a non-0 exit code if ERRORs are raised. +# +# The '||' here gives us an opportunity to echo out the installation +# logs prior to exiting the script. +check_succeeded="yes" +R CMD check "${PKG_TARBALL}" \ + --as-cran \ + --run-donttest \ +|| check_succeeded="no" + +CHECK_LOG_FILE=lightgbm.Rcheck/00check.log +BUILD_LOG_FILE=lightgbm.Rcheck/00install.out + +echo "R CMD check build logs:" +cat "${BUILD_LOG_FILE}" + +if [[ $check_succeeded == "no" ]]; then + echo "R CMD check failed" + exit 1 +fi + +# WARNINGs or ERRORs should be treated as a failure +if grep -q -E "WARNING|ERROR" "${CHECK_LOG_FILE}"; then + echo "WARNINGs or ERRORs have been found by R CMD check" + exit 1 +fi + +# Allow a configurable number of NOTEs. +# Sometimes NOTEs are raised in CI that wouldn't show up on an actual CRAN submission. +set +e +NUM_CHECK_NOTES=$( + grep -o -E '[0-9]+ NOTE' "${CHECK_LOG_FILE}" \ + | sed 's/[^0-9]*//g' +) +if [[ ${NUM_CHECK_NOTES} -gt ${ALLOWED_CHECK_NOTES} ]]; then + echo "Found ${NUM_CHECK_NOTES} NOTEs from R CMD check. Only ${ALLOWED_CHECK_NOTES} are allowed" + exit 1 +fi diff --git a/.ci/set-commit-status.sh b/.ci/set-commit-status.sh new file mode 100755 index 0000000..3169b0c --- /dev/null +++ b/.ci/set-commit-status.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# +# [description] +# Set a status with a given name to the specified commit. +# +# [usage] +# set-commit-status.sh +# +# NAME: Name of status. +# Status with existing name overwrites a previous one. +# +# STATUS: Status to be set. +# Can be "error", "failure", "pending" or "success". +# +# SHA: SHA of a commit to set a status on. + +set -e -E -u -o pipefail + +if [ -z "$GITHUB_ACTIONS" ]; then + echo "Must be run inside GitHub Actions CI" + exit 1 +fi + +if [ $# -ne 3 ]; then + echo "Usage: $0 " + exit 1 +fi + +name=$1 + +status=$2 +status=${status/error/failure} +status=${status/cancelled/failure} +status=${status/timed_out/failure} +status=${status/in_progress/pending} +status=${status/queued/pending} + +sha=$3 + +data=$( + jq -n \ + --arg state "${status}" \ + --arg url "${GITHUB_SERVER_URL}/lightgbm-org/LightGBM/actions/runs/${GITHUB_RUN_ID}" \ + --arg name "${name}" \ + '{"state":$state,"target_url":$url,"context":$name}' +) + +curl -sL \ + --fail \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -d "$data" \ + "${GITHUB_API_URL}/repos/lightgbm-org/LightGBM/statuses/$sha" diff --git a/.ci/setup.sh b/.ci/setup.sh new file mode 100755 index 0000000..f89636f --- /dev/null +++ b/.ci/setup.sh @@ -0,0 +1,192 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +# defaults +IN_UBUNTU_BASE_CONTAINER=${IN_UBUNTU_BASE_CONTAINER:-"false"} +SETUP_CONDA=${SETUP_CONDA:-"true"} + +ARCH=$(uname -m) + + +if [[ $OS_NAME == "macos" ]]; then + # Check https://github.com/actions/runner-images/tree/main/images/macos for available + # versions of Xcode + macos_ver=$(sw_vers --productVersion) + if [[ "${macos_ver}" =~ 15. ]]; then + xcode_path="/Applications/Xcode_16.0.app/Contents/Developer" + else + xcode_path="/Applications/Xcode_15.0.app/Contents/Developer" + fi + sudo xcode-select -s "${xcode_path}" || exit 1 + if [[ $COMPILER == "clang" ]]; then + brew install libomp + else # gcc + brew install 'gcc@14' + fi + if [[ $TASK == "mpi" ]]; then + brew install open-mpi + fi + if [[ $TASK == "swig" ]]; then + brew install swig + fi +else # Linux + if type -f apt > /dev/null 2>&1; then + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + ca-certificates \ + curl + else + sudo yum update -y + sudo yum install -y \ + ca-certificates \ + curl + fi + CMAKE_VERSION="3.30.0" + curl -O -L \ + "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" \ + || exit 1 + sudo mkdir /opt/cmake || exit 1 + sudo sh "cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" --skip-license --prefix=/opt/cmake || exit 1 + sudo ln -sf /opt/cmake/bin/cmake /usr/local/bin/cmake || exit 1 + + if [[ $IN_UBUNTU_BASE_CONTAINER == "true" ]]; then + # fixes error "unable to initialize frontend: Dialog" + # https://github.com/moby/moby/issues/27988#issuecomment-462809153 + echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections + + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + software-properties-common + + sudo apt-get install --no-install-recommends -y \ + build-essential \ + git \ + libcurl4 \ + libicu-dev \ + libssl-dev \ + locales \ + locales-all || exit 1 + if [[ $COMPILER == "clang" ]]; then + sudo apt-get install --no-install-recommends -y \ + clang \ + libomp-dev + elif [[ $COMPILER == "clang-17" ]]; then + sudo apt-get install --no-install-recommends -y \ + wget + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + sudo apt-add-repository deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main + sudo apt-add-repository deb-src http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main + sudo apt-get update + sudo apt-get install -y \ + clang-17 \ + libomp-17-dev + fi + + export LANG="en_US.UTF-8" + sudo update-locale LANG=${LANG} + export LC_ALL="${LANG}" + fi + if [[ $TASK == "r-package" ]] && [[ $COMPILER == "clang" ]]; then + sudo apt-get install --no-install-recommends -y \ + libomp-dev + fi + if [[ $TASK == "mpi" ]]; then + if [[ $IN_UBUNTU_BASE_CONTAINER == "true" ]]; then + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + libopenmpi-dev \ + openmpi-bin + else # in manylinux image + sudo yum update -y + sudo yum install -y \ + openmpi-devel \ + || exit 1 + fi + fi + if [[ $TASK == "gpu" ]]; then + if [[ $IN_UBUNTU_BASE_CONTAINER == "true" ]]; then + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + libboost1.74-dev \ + libboost-filesystem1.74-dev \ + ocl-icd-opencl-dev + else # in manylinux image + sudo yum update -y + sudo yum install -y \ + boost-devel \ + ocl-icd-devel \ + opencl-headers \ + || exit 1 + fi + fi + if [[ $TASK == "gpu" || $TASK == "bdist" ]]; then + if [[ $IN_UBUNTU_BASE_CONTAINER == "true" ]]; then + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + patchelf \ + pocl-opencl-icd + elif [[ $(uname -m) == "x86_64" ]]; then + sudo yum update -y + sudo yum install -y \ + ocl-icd-devel \ + opencl-headers \ + || exit 1 + fi + fi + if [[ $TASK == "cuda" ]]; then + echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections + apt-get update + APT_INSTALL_PACKAGES=() + + # only re-install NCCL if there wasn't one already pre-installed in the image + # (for example, nvidia/cuda 12.9.1 images excluded it) + if ! apt list --installed | grep -E 'libnccl\-dev' >/dev/null 2>&1; then + echo "libnccl-dev not found, manually installing it" + + # find a compatible libnccl-dev for this CUDA version + # + # (just 'apt-get install libnccl-dev' can result in mixing across CUDA versions) + IFS='.' read -r CUDA_MAJOR CUDA_MINOR _ <<< "${CI_CUDA_VERSION}" + NCCL_VERSION=$( + apt-cache madison libnccl-dev \ + | awk -F'|' '{print $2}' \ + | tr -d ' ' \ + | grep "+cuda${CUDA_MAJOR}.${CUDA_MINOR}$" \ + | sort -V \ + | tail -1 + ) + + APT_INSTALL_PACKAGES+=( + "libnccl-dev=${NCCL_VERSION}" + "libnccl2=${NCCL_VERSION}" + ) + else + echo "libnccl-dev already installed" + fi + if [[ $COMPILER == "clang" ]]; then + APT_INSTALL_PACKAGES+=( + clang + libomp-dev + ) + fi + apt-get install --no-install-recommends -y \ + "${APT_INSTALL_PACKAGES[@]}" + fi +fi + +if [[ "${TASK}" != "cpp-tests" ]] && [[ "${TASK}" != "r-package" ]] && [[ "${TASK}" != "swig" ]]; then + if [[ $SETUP_CONDA != "false" ]]; then + curl \ + -sL \ + -o miniforge.sh \ + "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-${ARCH}.sh" + sh miniforge.sh -b -p "${CONDA}" + fi + conda config --set always_yes yes --set changeps1 no + conda update -q -y conda + + # print output of 'conda info', to help in submitting bug reports + echo "conda info:" + conda info +fi diff --git a/.ci/test-python-latest.sh b/.ci/test-python-latest.sh new file mode 100755 index 0000000..0ff2bbd --- /dev/null +++ b/.ci/test-python-latest.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +echo "installing lightgbm and its dependencies" +pip install \ + --prefer-binary \ + --upgrade \ + -r ./.ci/pip-envs/requirements-latest.txt \ + dist/*.whl + +echo "installed package versions:" +pip freeze + +echo "" +echo "running tests" +pytest tests/c_api_test/ +pytest tests/python_package_test/ diff --git a/.ci/test-python-oldest.sh b/.ci/test-python-oldest.sh new file mode 100644 index 0000000..aa88355 --- /dev/null +++ b/.ci/test-python-oldest.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +echo "installing lightgbm and its dependencies" +pip install \ + --prefer-binary \ + --upgrade \ + --constraint ./.ci/pip-envs/requirements-oldest.txt \ + "$(echo dist/*.whl)[arrow,pandas,scikit-learn]" + +echo "installed package versions:" +pip freeze + +echo "" +echo "checking that examples run without error" + +# run a few examples to test that Python-package minimally works +echo "" +echo "--- advanced_example.py ---" +echo "" +python ./examples/python-guide/advanced_example.py || exit 1 + +echo "" +echo "--- logistic_regression.py ---" +echo "" +python ./examples/python-guide/logistic_regression.py || exit 1 + +echo "" +echo "--- simple_example.py ---" +echo "" +python ./examples/python-guide/simple_example.py || exit 1 + +echo "" +echo "--- sklearn_example.py ---" +echo "" +python ./examples/python-guide/sklearn_example.py || exit 1 +echo "" + +echo "checking that imports work without any optional dependencies" +pip uninstall --yes \ + cffi \ + dask \ + distributed \ + graphviz \ + joblib \ + matplotlib \ + pandas \ + psutil \ + pyarrow \ + scikit-learn + +python -c "import lightgbm" + +echo "" +echo "done testing on oldest supported Python version" diff --git a/.ci/test-r-package-valgrind.sh b/.ci/test-r-package-valgrind.sh new file mode 100755 index 0000000..11fb517 --- /dev/null +++ b/.ci/test-r-package-valgrind.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +RDscriptvalgrind ./.ci/install-r-deps.R --test || exit 1 +sh build-cran-package.sh \ + --r-executable=RDvalgrind \ + --no-build-vignettes \ + || exit 1 + +RDvalgrind CMD INSTALL --preclean --install-tests lightgbm_*.tar.gz || exit 1 + +cd R-package/tests + +ALL_LOGS_FILE="out.log" +VALGRIND_LOGS_FILE="valgrind-logs.log" + +RDvalgrind \ + --no-readline \ + --vanilla \ + -d "valgrind --tool=memcheck --leak-check=full --track-origins=yes --gen-suppressions=all" \ + -f testthat.R \ + > ${ALL_LOGS_FILE} 2>&1 || exit 1 + +cat ${ALL_LOGS_FILE} + +echo "writing valgrind output to ${VALGRIND_LOGS_FILE}" +cat ${ALL_LOGS_FILE} | grep -E "^\=" > ${VALGRIND_LOGS_FILE} + +bytes_definitely_lost=$( + cat ${VALGRIND_LOGS_FILE} \ + | grep -E "definitely lost\: .*" \ + | sed 's/^.*definitely lost\: \(.*\) bytes.*$/\1/' \ + | tr -d "," +) +echo "valgrind found ${bytes_definitely_lost} bytes definitely lost" +if [[ ${bytes_definitely_lost} -gt 0 ]]; then + exit 1 +fi + +bytes_indirectly_lost=$( + cat ${VALGRIND_LOGS_FILE} \ + | grep -E "indirectly lost\: .*" \ + | sed 's/^.*indirectly lost\: \(.*\) bytes.*$/\1/' \ + | tr -d "," +) +echo "valgrind found ${bytes_indirectly_lost} bytes indirectly lost" +if [[ ${bytes_indirectly_lost} -gt 0 ]]; then + exit 1 +fi + +# one error caused by a false positive between valgrind and openmp is allowed +# ==2063== 352 bytes in 1 blocks are possibly lost in loss record 153 of 2,709 +# ==2063== at 0x483DD99: calloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) +# ==2063== by 0x40149CA: allocate_dtv (dl-tls.c:286) +# ==2063== by 0x40149CA: _dl_allocate_tls (dl-tls.c:532) +# ==2063== by 0x5702322: allocate_stack (allocatestack.c:622) +# ==2063== by 0x5702322: pthread_create@@GLIBC_2.2.5 (pthread_create.c:660) +# ==2063== by 0x56D0DDA: ??? (in /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0) +# ==2063== by 0x56C88E0: GOMP_parallel (in /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0) +# ==2063== by 0x1544D29C: LGBM_DatasetCreateFromCSC (c_api.cpp:1286) +# ==2063== by 0x1546F980: LGBM_DatasetCreateFromCSC_R (lightgbm_R.cpp:91) +# ==2063== by 0x4941E2F: R_doDotCall (dotcode.c:634) +# ==2063== by 0x494CCC6: do_dotcall (dotcode.c:1281) +# ==2063== by 0x499FB01: bcEval (eval.c:7078) +# ==2063== by 0x498B67F: Rf_eval (eval.c:727) +# ==2063== by 0x498E414: R_execClosure (eval.c:1895) +bytes_possibly_lost=$( + cat ${VALGRIND_LOGS_FILE} \ + | grep -E "possibly lost\: .*" \ + | sed 's/^.*possibly lost\: \(.*\) bytes.*$/\1/' \ + | tr -d "," +) +echo "valgrind found ${bytes_possibly_lost} bytes possibly lost" +if [[ ${bytes_possibly_lost} -gt 1104 ]]; then + exit 1 +fi + +# ensure 'grep --count' doesn't cause failures +set +e + +echo "checking for invalid reads" +invalid_reads=$( + cat ${VALGRIND_LOGS_FILE} \ + | grep --count -i "Invalid read" +) +if [[ ${invalid_reads} -gt 0 ]]; then + echo "valgrind found invalid reads: ${invalid_reads}" + exit 1 +fi + +echo "checking for invalid writes" +invalid_writes=$( + cat ${VALGRIND_LOGS_FILE} \ + | grep --count -i "Invalid write" +) +if [[ ${invalid_writes} -gt 0 ]]; then + echo "valgrind found invalid writes: ${invalid_writes}" + exit 1 +fi diff --git a/.ci/test-r-package-windows.ps1 b/.ci/test-r-package-windows.ps1 new file mode 100644 index 0000000..2b37473 --- /dev/null +++ b/.ci/test-r-package-windows.ps1 @@ -0,0 +1,337 @@ +# Download a file and retry upon failure. This looks like +# an infinite loop but CI-level timeouts will kill it +function Get-File-With-Tenacity { + param( + [Parameter(Mandatory = $true)][string]$url, + [Parameter(Mandatory = $true)][string]$destfile + ) + $ProgressPreference = "SilentlyContinue" # progress bar bug extremely slows down download speed + do { + Write-Output "Downloading ${url}" + sleep 5 + Invoke-WebRequest -Uri $url -OutFile $destfile + } while (-not $?) +} + +# External utilities like R.exe / Rscript.exe writing to stderr (even for harmless +# status information) can cause failures in GitHub Actions PowerShell jobs. +# See https://github.community/t/powershell-steps-fail-nondeterministically/115496 +# +# Using standard PowerShell redirection does not work to avoid these errors. +# This function uses R's built-in redirection mechanism, sink(). Any place where +# this function is used is a command that writes harmless messages to stderr +function Invoke-R-Code-Redirect-Stderr { + param( + [Parameter(Mandatory = $true)][string]$rcode + ) + $decorated_code = "out_file <- file(tempfile(), open = 'wt'); sink(out_file, type = 'message'); $rcode; sink()" + Rscript --vanilla -e $decorated_code +} + +# Remove all items matching some pattern from PATH environment variable +function Remove-From-Path { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)][string]$pattern_to_remove + ) + if ($PSCmdlet.ShouldProcess($env:PATH, "Removing ${pattern_to_remove}")) { + $env:PATH = ($env:PATH.Split(';') | Where-Object { $_ -notmatch "$pattern_to_remove" }) -join ';' + } +} + +# remove some details that exist in the GitHub Actions images which might +# cause conflicts with R and other components installed by this script +$env:RTOOLS40_HOME = "" +Remove-From-Path ".*Amazon.*" +Remove-From-Path ".*Anaconda.*" +Remove-From-Path ".*android.*" +Remove-From-Path ".*Android.*" +Remove-From-Path ".*chocolatey.*" +Remove-From-Path ".*Chocolatey.*" +Remove-From-Path ".*cmake.*" +Remove-From-Path ".*CMake.*" +Remove-From-Path ".*\\Git\\.*" +Remove-From-Path "(?!.*pandoc.*).*hostedtoolcache.*" +Remove-From-Path ".*Microsoft SDKs.*" +Remove-From-Path ".*mingw.*" +Remove-From-Path ".*msys64.*" +Remove-From-Path ".*PostgreSQL.*" +Remove-From-Path ".*\\R\\.*" +Remove-From-Path ".*R Client.*" +Remove-From-Path ".*rtools40.*" +Remove-From-Path ".*rtools42.*" +Remove-From-Path ".*rtools43.*" +Remove-From-Path ".*rtools44.*" +Remove-From-Path ".*rtools45.*" +Remove-From-Path ".*shells.*" +Remove-From-Path ".*Strawberry.*" +Remove-From-Path ".*tools.*" + +Remove-Item C:\rtools40 -Force -Recurse -ErrorAction Ignore +Remove-Item C:\rtools42 -Force -Recurse -ErrorAction Ignore +Remove-Item C:\rtools43 -Force -Recurse -ErrorAction Ignore +Remove-Item C:\rtools44 -Force -Recurse -ErrorAction Ignore +Remove-Item C:\rtools45 -Force -Recurse -ErrorAction Ignore + +# Get details needed for installing R components +# +# NOTES: +# * some paths and file names are different on R4.0 +$env:R_MAJOR_VERSION = $env:R_VERSION.split('.')[0] +if ($env:R_MAJOR_VERSION -eq "4") { + $RTOOLS_INSTALL_PATH = "C:\rtools43" + $env:RTOOLS_BIN = "$RTOOLS_INSTALL_PATH\usr\bin" + $env:RTOOLS_MINGW_BIN = "$RTOOLS_INSTALL_PATH\x86_64-w64-mingw32.static.posix\bin" + $env:RTOOLS_EXE_FILE = "rtools43-5550-5548.exe" + $env:R_WINDOWS_VERSION = "4.3.1" +} else { + Write-Output "[ERROR] Unrecognized R version: $env:R_VERSION" + Assert-Output $false +} +$env:CMAKE_VERSION = "3.30.0" + +$env:R_LIB_PATH = "$env:BUILD_SOURCESDIRECTORY/RLibrary" -replace '[\\]', '/' +$env:R_LIBS = "$env:R_LIB_PATH" +$env:CMAKE_PATH = "$env:BUILD_SOURCESDIRECTORY/CMake_installation" +$env:PATH = @( + "$env:RTOOLS_BIN", + "$env:RTOOLS_MINGW_BIN", + "$env:R_LIB_PATH/R/bin/x64", + "$env:CMAKE_PATH/cmake-$env:CMAKE_VERSION-windows-x86_64/bin", + "$env:PATH" +) -join ";" +$env:CRAN_MIRROR = "https://cran.rstudio.com" +$env:MIKTEX_EXCEPTION_PATH = "$env:TEMP\miktex" + +# don't fail builds for long-running examples unless they're very long. +# See https://github.com/lightgbm-org/LightGBM/issues/4049#issuecomment-793412254. +if ($env:R_BUILD_TYPE -ne "cran") { + $env:_R_CHECK_EXAMPLE_TIMING_THRESHOLD_ = 30 +} + +if (($env:COMPILER -eq "MINGW") -and ($env:R_BUILD_TYPE -eq "cmake")) { + $env:CXX = "$env:RTOOLS_MINGW_BIN/g++.exe" + $env:CC = "$env:RTOOLS_MINGW_BIN/gcc.exe" +} + +Set-Location "$env:BUILD_SOURCESDIRECTORY" +tzutil /s "GMT Standard Time" +[Void][System.IO.Directory]::CreateDirectory("$env:R_LIB_PATH") +[Void][System.IO.Directory]::CreateDirectory("$env:CMAKE_PATH") + +# download R, RTools and CMake +Write-Output "Downloading R, Rtools and CMake" +$params = @{ + url = "$env:CRAN_MIRROR/bin/windows/base/old/$env:R_WINDOWS_VERSION/R-$env:R_WINDOWS_VERSION-win.exe" + destfile = "R-win.exe" +} +Get-File-With-Tenacity @params + +$params = @{ + url = "https://github.com/lightgbm-org/LightGBM/releases/download/v2.0.12/$env:RTOOLS_EXE_FILE" + destfile = "Rtools.exe" +} +Get-File-With-Tenacity @params + +$params = @{ + url = "https://github.com/Kitware/CMake/releases/download/v{0}/cmake-{0}-windows-x86_64.zip" -f $env:CMAKE_VERSION + destfile = "$env:CMAKE_PATH/cmake.zip" +} +Get-File-With-Tenacity @params + +# Install R +Write-Output "Installing R" +$params = @{ + FilePath = "R-win.exe" + NoNewWindow = $true + Wait = $true + ArgumentList = "/VERYSILENT /DIR=$env:R_LIB_PATH/R /COMPONENTS=main,x64,i386" +} +Start-Process @params ; Assert-Output $? +Write-Output "Done installing R" + +Write-Output "Installing Rtools" +$params = @{ + FilePath = "Rtools.exe" + NoNewWindow = $true + Wait = $true + ArgumentList = "/VERYSILENT /SUPPRESSMSGBOXES /DIR=$RTOOLS_INSTALL_PATH" +} +Start-Process @params; Assert-Output $? +Write-Output "Done installing Rtools" + +Write-Output "Installing CMake" +Add-Type -AssemblyName System.IO.Compression.FileSystem +[System.IO.Compression.ZipFile]::ExtractToDirectory("$env:CMAKE_PATH/cmake.zip", "$env:CMAKE_PATH") ; Assert-Output $? +# Remove old CMake shipped with RTools +Remove-Item "$env:RTOOLS_MINGW_BIN/cmake.exe" -Force -ErrorAction Ignore +Write-Output "Done installing CMake" + +Write-Output "Installing dependencies" +Rscript.exe --vanilla ".ci/install-r-deps.R" --build --include=processx --test ; Assert-Output $? + +Write-Output "Building R-package" + +# R CMD check is not used for MSVC builds +if ($env:COMPILER -ne "MSVC") { + + $PKG_FILE_NAME = "lightgbm_$env:LGB_VER.tar.gz" + $LOG_FILE_NAME = "lightgbm.Rcheck/00check.log" + + if ($env:R_BUILD_TYPE -eq "cmake") { + if ($env:TOOLCHAIN -eq "MINGW") { + Write-Output "Telling R to use MinGW" + $env:BUILD_R_FLAGS = "c('--skip-install', '--use-mingw', '-j4')" + } elseif ($env:TOOLCHAIN -eq "MSYS") { + Write-Output "Telling R to use MSYS" + $env:BUILD_R_FLAGS = "c('--skip-install', '--use-msys2', '-j4')" + } elseif ($env:TOOLCHAIN -eq "MSVC") { + $env:BUILD_R_FLAGS = "'--skip-install'" + } else { + Write-Output "[ERROR] Unrecognized toolchain: $env:TOOLCHAIN" + Assert-Output $false + } + Invoke-R-Code-Redirect-Stderr "commandArgs <- function(...){$env:BUILD_R_FLAGS}; source('build_r.R')" + Assert-Output $? + } elseif ($env:R_BUILD_TYPE -eq "cran") { + $params = -join @( + "result <- processx::run(command = 'sh', args = 'build-cran-package.sh', ", + "echo = TRUE, windows_verbatim_args = FALSE, error_on_status = TRUE)" + ) + Invoke-R-Code-Redirect-Stderr $params ; Assert-Output $? + Remove-From-Path ".*msys64.*" + # Test CRAN source .tar.gz in a directory that is not this repo or below it. + # When people install.packages('lightgbm'), they won't have the LightGBM + # git repo around. This is to protect against the use of relative paths + # like ../../CMakeLists.txt that would only work if you are in the repoo + $R_CMD_CHECK_DIR = "tmp-r-cmd-check" + New-Item -Path "C:\" -Name $R_CMD_CHECK_DIR -ItemType "directory" > $null + Move-Item -Path "$PKG_FILE_NAME" -Destination "C:\$R_CMD_CHECK_DIR\" > $null + Set-Location "C:\$R_CMD_CHECK_DIR\" + } + + Write-Output "Running R CMD check" + if ($env:R_BUILD_TYPE -eq "cran") { + # CRAN packages must pass without --no-multiarch (build on 64-bit and 32-bit) + $check_args = "c('CMD', 'check', '--as-cran', '--run-donttest', '$PKG_FILE_NAME')" + } else { + $check_args = "c('CMD', 'check', '--no-multiarch', '--as-cran', '--run-donttest', '$PKG_FILE_NAME')" + } + $params = -join ( + "result <- processx::run(command = 'R.exe', args = $check_args, ", + "echo = TRUE, windows_verbatim_args = FALSE, error_on_status = TRUE)" + ) + Invoke-R-Code-Redirect-Stderr $params ; $check_succeeded = $? + + Write-Output "R CMD check build logs:" + $INSTALL_LOG_FILE_NAME = "lightgbm.Rcheck\00install.out" + Get-Content -Path "$INSTALL_LOG_FILE_NAME" + + Assert-Output $check_succeeded + + Write-Output "Looking for issues with R CMD check results" + if (Get-Content "$LOG_FILE_NAME" | Select-String -Pattern "NOTE|WARNING|ERROR" -CaseSensitive -Quiet) { + Write-Output "NOTEs, WARNINGs, or ERRORs have been found by R CMD check" + Assert-Output $False + } +} else { + $INSTALL_LOG_FILE_NAME = "$env:BUILD_SOURCESDIRECTORY\00install_out.txt" + Invoke-R-Code-Redirect-Stderr "source('build_r.R')" 1> $INSTALL_LOG_FILE_NAME ; $install_succeeded = $? + Write-Output "----- build and install logs -----" + Get-Content -Path "$INSTALL_LOG_FILE_NAME" + Write-Output "----- end of build and install logs -----" + Assert-Output $install_succeeded + # some errors are not raised above, but can be found in the logs + if (Get-Content "$INSTALL_LOG_FILE_NAME" | Select-String -Pattern "ERROR" -CaseSensitive -Quiet) { + Write-Output "ERRORs have been found installing lightgbm" + Assert-Output $False + } +} + +# Checking that the correct R version was used +if ($env:TOOLCHAIN -ne "MSVC") { + $checks = Select-String -Path "${LOG_FILE_NAME}" -Pattern "using R version $env:R_WINDOWS_VERSION" + $checks_cnt = $checks.Matches.length +} else { + $checksParams = @{ + Path = "${INSTALL_LOG_FILE_NAME}" + Pattern = "R version passed into FindLibR.* $env:R_WINDOWS_VERSION" + } + $checks = Select-String @checksParams + $checks_cnt = $checks.Matches.length +} +if ($checks_cnt -eq 0) { + Write-Output "Wrong R version was found (expected '$env:R_WINDOWS_VERSION'). Check the build logs." + Assert-Output $False +} + +# Checking that we actually got the expected compiler. The R-package has some logic +# to fail back to MinGW if MSVC fails, but for CI builds we need to check that the correct +# compiler was used. +if ($env:R_BUILD_TYPE -eq "cmake") { + $checks = Select-String -Path "${INSTALL_LOG_FILE_NAME}" -Pattern "Check for working CXX compiler.*$env:COMPILER" + if ($checks.Matches.length -eq 0) { + Write-Output "The wrong compiler was used. Check the build logs." + Assert-Output $False + } +} + +# Checking that we got the right toolchain for MinGW. If using MinGW, both +# MinGW and MSYS toolchains are supported +if (($env:COMPILER -eq "MINGW") -and ($env:R_BUILD_TYPE -eq "cmake")) { + $checks = Select-String -Path "${INSTALL_LOG_FILE_NAME}" -Pattern "Trying to build with.*$env:TOOLCHAIN" + if ($checks.Matches.length -eq 0) { + Write-Output "The wrong toolchain was used. Check the build logs." + Assert-Output $False + } +} + +# Checking that MM_PREFETCH preprocessor definition is actually used in CI builds. +if ($env:R_BUILD_TYPE -eq "cran") { + $checks = Select-String -Path "${INSTALL_LOG_FILE_NAME}" -Pattern "checking whether MM_PREFETCH work.*yes" + $checks_cnt = $checks.Matches.length +} elseif ($env:TOOLCHAIN -ne "MSVC") { + $checks = Select-String -Path "${INSTALL_LOG_FILE_NAME}" -Pattern ".*Performing Test MM_PREFETCH - Success" + $checks_cnt = $checks.Matches.length +} else { + $checks_cnt = 1 +} +if ($checks_cnt -eq 0) { + Write-Output "MM_PREFETCH preprocessor definition wasn't used. Check the build logs." + Assert-Output $False +} + +# Checking that MM_MALLOC preprocessor definition is actually used in CI builds. +if ($env:R_BUILD_TYPE -eq "cran") { + $checks = Select-String -Path "${INSTALL_LOG_FILE_NAME}" -Pattern "checking whether MM_MALLOC work.*yes" + $checks_cnt = $checks.Matches.length +} elseif ($env:TOOLCHAIN -ne "MSVC") { + $checks = Select-String -Path "${INSTALL_LOG_FILE_NAME}" -Pattern ".*Performing Test MM_MALLOC - Success" + $checks_cnt = $checks.Matches.length +} else { + $checks_cnt = 1 +} +if ($checks_cnt -eq 0) { + Write-Output "MM_MALLOC preprocessor definition wasn't used. Check the build logs." + Assert-Output $False +} + +# Checking that OpenMP is actually used in CMake builds. +if ($env:R_BUILD_TYPE -eq "cmake") { + $checks = Select-String -Path "${INSTALL_LOG_FILE_NAME}" -Pattern ".*Found OpenMP: TRUE.*" + if ($checks.Matches.length -eq 0) { + Write-Output "OpenMP wasn't found. Check the build logs." + Assert-Output $False + } +} + +if ($env:COMPILER -eq "MSVC") { + Write-Output "Running tests with testthat.R" + Set-Location R-package/tests + # NOTE: using Rscript.exe intentionally here, instead of Invoke-R-Code-Redirect-Stderr, + # because something about the interaction between Invoke-R-Code-Redirect-Stderr + # and testthat results in failing tests not exiting with a non-0 exit code. + Rscript.exe --vanilla "testthat.R" ; Assert-Output $? +} + +Write-Output "No issues were found checking the R-package" diff --git a/.ci/test-r-package.sh b/.ci/test-r-package.sh new file mode 100755 index 0000000..24d3991 --- /dev/null +++ b/.ci/test-r-package.sh @@ -0,0 +1,260 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +# defaults +ARCH=$(uname -m) + +# set up R environment +export CRAN_MIRROR="https://cran.rstudio.com" +export R_LIB_PATH=~/Rlib +mkdir -p $R_LIB_PATH +export R_LIBS=$R_LIB_PATH +export PATH="$R_LIB_PATH/R/bin:$PATH" + +# don't fail builds for long-running examples unless they're very long. +# See https://github.com/lightgbm-org/LightGBM/issues/4049#issuecomment-793412254. +if [[ $R_BUILD_TYPE != "cran" ]]; then + export _R_CHECK_EXAMPLE_TIMING_THRESHOLD_=30 +fi + +# Get details needed for installing R components +R_MAJOR_VERSION="${R_VERSION%.*}" +if [[ "${R_MAJOR_VERSION}" == "4" ]]; then + export R_MAC_VERSION=4.3.1 + export R_MAC_PKG_URL=${CRAN_MIRROR}/bin/macosx/big-sur-${ARCH}/base/R-${R_MAC_VERSION}-${ARCH}.pkg + export R_LINUX_VERSION="4.3.1-1.2204.0" + export R_APT_REPO="jammy-cran40/" +else + echo "Unrecognized R version: ${R_VERSION}" + exit 1 +fi + +# installing precompiled R for Ubuntu +# https://cran.r-project.org/bin/linux/ubuntu/#installation +# adding steps from https://stackoverflow.com/a/56378217/3986677 to get latest version +# +# `devscripts` is required for 'checkbashisms' (https://github.com/r-lib/actions/issues/111) +if [[ $OS_NAME == "linux" ]]; then + mkdir -p ~/.gnupg + echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf + sudo apt-key adv \ + --homedir ~/.gnupg \ + --keyserver keyserver.ubuntu.com \ + --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9 || exit 1 + sudo add-apt-repository \ + "deb ${CRAN_MIRROR}/bin/linux/ubuntu ${R_APT_REPO}" || exit 1 + sudo apt-get update + sudo apt-get install \ + --no-install-recommends \ + -y \ + devscripts \ + libuv1-dev \ + r-base-core=${R_LINUX_VERSION} \ + r-base-dev=${R_LINUX_VERSION} \ + texinfo \ + texlive-latex-extra \ + texlive-latex-recommended \ + texlive-fonts-recommended \ + texlive-fonts-extra \ + tidy \ + qpdf \ + || exit 1 + + if [[ $R_BUILD_TYPE == "cran" ]]; then + sudo apt-get install \ + --no-install-recommends \ + -y \ + "autoconf=$(cat R-package/AUTOCONF_UBUNTU_VERSION)" \ + automake \ + || exit 1 + fi +fi + +# Installing R precompiled for Mac OS 10.11 or higher +if [[ $OS_NAME == "macos" ]]; then + brew update-reset --auto-update + brew update --auto-update + if [[ $R_BUILD_TYPE == "cran" ]]; then + brew install automake || exit 1 + fi + brew install \ + checkbashisms \ + qpdf || exit 1 + brew install basictex || exit 1 + export PATH="/Library/TeX/texbin:$PATH" + sudo tlmgr --verify-repo=none update --self || exit 1 + sudo tlmgr --verify-repo=none install inconsolata helvetic rsfs || exit 1 + + curl -sL "${R_MAC_PKG_URL}" -o R.pkg || exit 1 + sudo installer \ + -pkg "$(pwd)/R.pkg" \ + -target / || exit 1 + + # install tidy v5.8.0 + # ref: https://groups.google.com/g/r-sig-mac/c/7u_ivEj4zhM + TIDY_URL=https://github.com/htacg/tidy-html5/releases/download/5.8.0/tidy-5.8.0-macos-x86_64+arm64.pkg + curl -sL ${TIDY_URL} -o tidy.pkg + sudo installer \ + -pkg "$(pwd)/tidy.pkg" \ + -target / + + # ensure that this newer version of 'tidy' is used by 'R CMD check' + # ref: https://cran.r-project.org/doc/manuals/R-exts.html#Checking-packages + export R_TIDYCMD=/usr/local/bin/tidy +fi + +# {Matrix} needs {lattice}, so this needs to run before manually installing {Matrix}. +# This should be unnecessary on R >=4.4.0 +# ref: https://github.com/lightgbm-org/LightGBM/issues/6433 +Rscript --vanilla -e "install.packages('lattice', repos = '${CRAN_MIRROR}', lib = '${R_LIB_PATH}')" + +# manually install {Matrix}, as {Matrix}=1.7-0 raised its R floor all the way to R 4.4.0 +# ref: https://github.com/lightgbm-org/LightGBM/issues/6433 +Rscript --vanilla -e "install.packages('https://cran.r-project.org/src/contrib/Archive/Matrix/Matrix_1.6-5.tar.gz', repos = NULL, lib = '${R_LIB_PATH}')" + +# Manually install dependencies to avoid a CI-time dependency on devtools (for devtools::install_deps()) +Rscript --vanilla ./.ci/install-r-deps.R --build --test --exclude=Matrix || exit 1 + +cd "${BUILD_DIRECTORY}" +PKG_TARBALL="lightgbm_$(head -1 VERSION.txt).tar.gz" +BUILD_LOG_FILE="lightgbm.Rcheck/00install.out" +LOG_FILE_NAME="lightgbm.Rcheck/00check.log" +if [[ $R_BUILD_TYPE == "cmake" ]]; then + Rscript build_r.R -j4 --skip-install || exit 1 +elif [[ $R_BUILD_TYPE == "cran" ]]; then + + # on Linux, we recreate configure in CI to test if + # a change in a PR has changed configure.ac + if [[ $OS_NAME == "linux" ]]; then + ./R-package/recreate-configure.sh + + num_files_changed=$( + git diff --name-only | wc -l + ) + if [[ ${num_files_changed} -gt 0 ]]; then + echo "'configure' in the R-package has changed. Please recreate it and commit the changes." + echo "Changed files:" + git diff --compact-summary + echo "See R-package/README.md for details on how to recreate this script." + echo "" + exit 1 + fi + fi + + ./build-cran-package.sh || exit 1 + + # Test CRAN source .tar.gz in a directory that is not this repo or below it. + # When people install.packages('lightgbm'), they won't have the LightGBM + # git repo around. This is to protect against the use of relative paths + # like ../../CMakeLists.txt that would only work if you are in the repo + R_CMD_CHECK_DIR="${HOME}/tmp-r-cmd-check/" + mkdir -p "${R_CMD_CHECK_DIR}" + mv "${PKG_TARBALL}" "${R_CMD_CHECK_DIR}" + cd "${R_CMD_CHECK_DIR}" +fi + +if [[ $PRODUCES_ARTIFACTS == "true" ]]; then + cp "${PKG_TARBALL}" "${BUILD_ARTIFACTSTAGINGDIRECTORY}/lightgbm-${LGB_VER}-r-cran.tar.gz" +fi + +declare -i allowed_notes=0 +bash "${BUILD_DIRECTORY}/.ci/run-r-cmd-check.sh" \ + "${PKG_TARBALL}" \ + "${allowed_notes}" + +# ensure 'grep --count' doesn't cause failures +set +e + +used_correct_r_version=$( + cat $LOG_FILE_NAME \ + | grep --count "using R version ${R_VERSION}" +) +if [[ $used_correct_r_version -ne 1 ]]; then + echo "Unexpected R version was used. Expected '${R_VERSION}'." + exit 1 +fi + +if [[ $R_BUILD_TYPE == "cmake" ]]; then + passed_correct_r_version_to_cmake=$( + cat $BUILD_LOG_FILE \ + | grep --count "R version passed into FindLibR.cmake: ${R_VERSION}" + ) + if [[ $passed_correct_r_version_to_cmake -ne 1 ]]; then + echo "Unexpected R version was passed into cmake. Expected '${R_VERSION}'." + exit 1 + fi +fi + +# this check makes sure that CI builds of the package actually use OpenMP +if [[ $OS_NAME == "macos" ]] && [[ $R_BUILD_TYPE == "cran" ]]; then + omp_working=$( + cat $BUILD_LOG_FILE \ + | grep --count -E "checking whether OpenMP will work .*yes" + ) +elif [[ $R_BUILD_TYPE == "cmake" ]]; then + omp_working=$( + cat $BUILD_LOG_FILE \ + | grep --count -E ".*Found OpenMP: TRUE.*" + ) +else + omp_working=1 +fi +if [[ $omp_working -ne 1 ]]; then + echo "OpenMP was not found" + exit 1 +fi + +# this check makes sure that CI builds of the package +# actually use MM_PREFETCH preprocessor definition +# +# _mm_prefetch will not work on arm64 architecture +# ref: https://github.com/lightgbm-org/LightGBM/issues/4124 +if [[ $ARCH != "arm64" ]]; then + if [[ $R_BUILD_TYPE == "cran" ]]; then + mm_prefetch_working=$( + cat $BUILD_LOG_FILE \ + | grep --count -E "checking whether MM_PREFETCH work.*yes" + ) + else + mm_prefetch_working=$( + cat $BUILD_LOG_FILE \ + | grep --count -E ".*Performing Test MM_PREFETCH - Success" + ) + fi + if [[ $mm_prefetch_working -ne 1 ]]; then + echo "MM_PREFETCH test was not passed" + exit 1 + fi +fi + +# this check makes sure that CI builds of the package +# actually use MM_MALLOC preprocessor definition +if [[ $R_BUILD_TYPE == "cran" ]]; then + mm_malloc_working=$( + cat $BUILD_LOG_FILE \ + | grep --count -E "checking whether MM_MALLOC work.*yes" + ) +else + mm_malloc_working=$( + cat $BUILD_LOG_FILE \ + | grep --count -E ".*Performing Test MM_MALLOC - Success" + ) +fi +if [[ $mm_malloc_working -ne 1 ]]; then + echo "MM_MALLOC test was not passed" + exit 1 +fi + +# this check makes sure that no "warning: unknown pragma ignored" logs +# reach the user leading them to believe that something went wrong +if [[ $R_BUILD_TYPE == "cran" ]]; then + pragma_warning_present=$( + cat $BUILD_LOG_FILE \ + | grep --count -E "warning: unknown pragma ignored" + ) + if [[ $pragma_warning_present -ne 0 ]]; then + echo "Unknown pragma warning is present, pragmas should have been removed before build" + exit 1 + fi +fi diff --git a/.ci/test-windows.ps1 b/.ci/test-windows.ps1 new file mode 100644 index 0000000..33b3002 --- /dev/null +++ b/.ci/test-windows.ps1 @@ -0,0 +1,190 @@ +function Assert-Output { + param( [Parameter(Mandatory = $true)][bool]$success ) + if (-not $success) { + $host.SetShouldExit(-1) + exit 1 + } +} + +$env:LGB_VER = (Get-Content $env:BUILD_SOURCESDIRECTORY\VERSION.txt).trim() +# Use custom temp directory to avoid +# > warning MSB8029: The Intermediate directory or Output directory cannot reside under the Temporary directory +# > as it could lead to issues with incremental build. +# And make sure this directory is always clean +$env:TMPDIR = "$env:USERPROFILE\tmp" +Remove-Item $env:TMPDIR -Force -Recurse -ErrorAction Ignore +[Void][System.IO.Directory]::CreateDirectory($env:TMPDIR) + +# create the artifact upload directory if it doesn't exist yet +[Void][System.IO.Directory]::CreateDirectory($env:BUILD_ARTIFACTSTAGINGDIRECTORY) + +if ($env:TASK -eq "r-package") { + & .\.ci\test-r-package-windows.ps1 ; Assert-Output $? + exit 0 +} + +if ($env:TASK -eq "cpp-tests") { + cmake -B build -S . -DBUILD_CPP_TEST=ON -DUSE_DEBUG=ON -A x64 + cmake --build build --target testlightgbm --config Debug ; Assert-Output $? + .\Debug\testlightgbm.exe ; Assert-Output $? + exit 0 +} + +if ($env:TASK -eq "swig") { + $env:JAVA_HOME = $env:JAVA_HOME_8_X64 # there is pre-installed Eclipse Temurin 8 somewhere + $ProgressPreference = "SilentlyContinue" # progress bar bug extremely slows down download speed + $params = @{ + Uri = "https://sourceforge.net/projects/swig/files/latest/download" + OutFile = "$env:BUILD_SOURCESDIRECTORY/swig/swigwin.zip" + UserAgent = "curl" + } + Invoke-WebRequest @params + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory( + "$env:BUILD_SOURCESDIRECTORY/swig/swigwin.zip", + "$env:BUILD_SOURCESDIRECTORY/swig" + ) ; Assert-Output $? + $SwigFolder = Get-ChildItem -Name -Path "$env:BUILD_SOURCESDIRECTORY/swig" -Attributes Directory + $env:PATH = @("$env:BUILD_SOURCESDIRECTORY/swig/$SwigFolder", "$env:PATH") -join ";" + $BuildLogFileName = "$env:BUILD_SOURCESDIRECTORY\cmake_build.log" + cmake -B build -S . -A x64 -DUSE_SWIG=ON *> "$BuildLogFileName" ; $build_succeeded = $? + Write-Output "CMake build logs:" + Get-Content -Path "$BuildLogFileName" + Assert-Output $build_succeeded + $checks = Select-String -Path "${BuildLogFileName}" -Pattern "-- Found SWIG.*${SwigFolder}/swig.exe" + $checks_cnt = $checks.Matches.length + if ($checks_cnt -eq 0) { + Write-Output "Wrong SWIG version was found (expected '${SwigFolder}'). Check the build logs." + Assert-Output $False + } + cmake --build build --target ALL_BUILD --config Release ; Assert-Output $? + if ($env:PRODUCES_ARTIFACTS -eq "true") { + cp ./build/lightgbmlib.jar $env:BUILD_ARTIFACTSTAGINGDIRECTORY/lightgbmlib_win.jar ; Assert-Output $? + } + exit 0 +} + +# 'pixi' is used for end-of-life Python versions +if ($env:PYTHON_VERSION -eq "3.10") { + $activation = ((& pixi shell-hook --locked -e py310 --shell powershell) -join "`n") + Invoke-Expression $activation ; Assert-Output $? +} else { + # update conda env + $env:CONDA_ENV = "test-env" + conda activate ; Assert-Output $? + conda config --set always_yes yes --set changeps1 no ; Assert-Output $? + conda config --remove channels defaults ; Assert-Output $? + conda config --add channels nodefaults ; Assert-Output $? + conda config --add channels conda-forge ; Assert-Output $? + conda config --set channel_priority strict ; Assert-Output $? + conda install -q -y conda "python=$env:PYTHON_VERSION[build=*_cp*]" ; Assert-Output $? + + # print output of 'conda info', to help in submitting bug reports + Write-Output "conda info:" + conda info + + $env:CONDA_REQUIREMENT_FILE = "$env:BUILD_SOURCESDIRECTORY/.ci/conda-envs/ci-core.txt" + + $condaParams = @( + "-y", + "-n", "$env:CONDA_ENV", + "--file", "$env:CONDA_REQUIREMENT_FILE", + "python=$env:PYTHON_VERSION[build=*_cp*]" + ) + conda create @condaParams ; Assert-Output $? + + # print output of 'conda list', to help in submitting bug reports + Write-Output "conda list:" + conda list -n $env:CONDA_ENV + + # 'bdist' job invokes 'RefreshEnv' to update PATH from the registry (which may have been modified + # by building in OpenCL support), so defer activating the conda environment until later for those builds. + if ($env:TASK -ne "bdist") { + conda activate $env:CONDA_ENV + } +} + +Set-Location "$env:BUILD_SOURCESDIRECTORY" +if ($env:TASK -eq "regular") { + cmake -B build -S . -A x64 ; Assert-Output $? + cmake --build build --target ALL_BUILD --config Release ; Assert-Output $? + sh ./build-python.sh install --precompile ; Assert-Output $? + cp ./Release/lib_lightgbm.dll "$env:BUILD_ARTIFACTSTAGINGDIRECTORY" + cp ./Release/lightgbm.exe "$env:BUILD_ARTIFACTSTAGINGDIRECTORY" +} elseif ($env:TASK -eq "sdist") { + sh ./build-python.sh sdist ; Assert-Output $? + sh ./.ci/check-python-dists.sh ./dist ; Assert-Output $? + Set-Location dist; pip install --no-deps @(Get-ChildItem *.gz) -v ; Assert-Output $? +} elseif ($env:TASK -eq "bdist") { + # Import the Chocolatey profile module so that the RefreshEnv command + # invoked below properly updates the current PowerShell session environment. + $module = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" + Import-Module "$module" ; Assert-Output $? + RefreshEnv + + Write-Output "Current OpenCL drivers:" + Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\OpenCL\Vendors + + # (re-) activate conda environment, in case any activation logic was overridden by that 'RefreshEnv' call above + conda activate $env:CONDA_ENV + + # TODO: restore --integrated-opencl as part of https://github.com/lightgbm-org/LightGBM/issues/6968 + sh "build-python.sh" bdist_wheel ; Assert-Output $? + sh ./.ci/check-python-dists.sh ./dist ; Assert-Output $? + Set-Location dist; pip install --no-deps @(Get-ChildItem *py3-none-win_amd64.whl) ; Assert-Output $? + cp @(Get-ChildItem *py3-none-win_amd64.whl) "$env:BUILD_ARTIFACTSTAGINGDIRECTORY" +} elseif (($env:APPVEYOR -eq "true") -and ($env:TASK -eq "python")) { + if ($env:COMPILER -eq "MINGW") { + sh ./build-python.sh install --mingw ; Assert-Output $? + } else { + sh ./build-python.sh install; Assert-Output $? + } +} + +if (($env:TASK -eq "sdist") -or (($env:APPVEYOR -eq "true") -and ($env:TASK -eq "python"))) { + # cannot test C API with "sdist" task + $tests = "$env:BUILD_SOURCESDIRECTORY/tests/python_package_test" +} else { + $tests = "$env:BUILD_SOURCESDIRECTORY/tests" +} +if ($env:TASK -eq "bdist") { + # Make sure we can do both CPU and GPU; see tests/python_package_test/test_dual.py + # TODO: set LIGHTGBM_TEST_DUAL_CPU_GPU back to "1" as part of https://github.com/lightgbm-org/LightGBM/issues/6968 + $env:LIGHTGBM_TEST_DUAL_CPU_GPU = "0" +} + +pytest -ra $tests ; Assert-Output $? + +if (($env:TASK -eq "regular") -or (($env:APPVEYOR -eq "true") -and ($env:TASK -eq "python"))) { + Set-Location "$env:BUILD_SOURCESDIRECTORY/examples/python-guide" + @("import matplotlib", "matplotlib.use('Agg')") + (Get-Content "plot_example.py") | Set-Content "plot_example.py" + # Prevent interactive window mode + (Get-Content "plot_example.py").replace( + 'graph.render(view=True)', + 'graph.render(view=False)' + ) | Set-Content "plot_example.py" + + # install optional plotting libraries + # (not necessary for pixi-managed environments, where they're just installed by default) + if ($env:PYTHON_VERSION -ne "3.10") { + conda install -y -n $env:CONDA_ENV "h5py>=3.10" "ipywidgets>=8.1.2" "notebook>=7.1.2" + } + # Run all examples + foreach ($file in @(Get-ChildItem *.py)) { + @( + "import sys, warnings", + -join @( + "warnings.showwarning = lambda message, category, filename, lineno, file=None, line=None: ", + "sys.stdout.write(warnings.formatwarning(message, category, filename, lineno, line))" + ) + ) + (Get-Content $file) | Set-Content $file + python $file ; Assert-Output $? + } + # Run all notebooks + Set-Location "$env:BUILD_SOURCESDIRECTORY/examples/python-guide/notebooks" + (Get-Content "interactive_plot_example.ipynb").replace( + 'INTERACTIVE = False', + 'assert False, \"Interactive mode disabled\"' + ) | Set-Content "interactive_plot_example.ipynb" + jupyter nbconvert --ExecutePreprocessor.timeout=180 --to notebook --execute --inplace *.ipynb ; Assert-Output $? +} diff --git a/.ci/test.sh b/.ci/test.sh new file mode 100755 index 0000000..ffcd396 --- /dev/null +++ b/.ci/test.sh @@ -0,0 +1,321 @@ +#!/bin/bash + +set -e -E -o -u pipefail + +# defaults +CONDA_ENV="test-env" +IN_UBUNTU_BASE_CONTAINER=${IN_UBUNTU_BASE_CONTAINER:-"false"} +METHOD=${METHOD:-""} +PRODUCES_ARTIFACTS=${PRODUCES_ARTIFACTS:-"false"} +SANITIZERS=${SANITIZERS:-""} + +ARCH=$(uname -m) + +LGB_VER=$(head -n 1 "${BUILD_DIRECTORY}/VERSION.txt") + +# create the artifact upload directory if it doesn't exist yet +mkdir -p "${BUILD_ARTIFACTSTAGINGDIRECTORY}" + +if [[ $OS_NAME == "macos" ]] && [[ $COMPILER == "gcc" ]]; then + export CXX=g++-14 + export CC=gcc-14 +elif [[ $OS_NAME == "linux" ]] && [[ $COMPILER == "clang" ]]; then + export CXX=clang++ + export CC=clang +elif [[ $OS_NAME == "linux" ]] && [[ $COMPILER == "clang-17" ]]; then + export CXX=clang++-17 + export CC=clang-17 +fi + +if [[ $IN_UBUNTU_BASE_CONTAINER == "true" ]]; then + export LANG="en_US.UTF-8" + export LC_ALL="en_US.UTF-8" +fi + +# Setting MACOSX_DEPLOYMENT_TARGET prevents CMake from building against too-new +# macOS features, and helps tools like Python build tools determine the appropriate +# wheel compatibility tags. +# +# ref: +# * https://cmake.org/cmake/help/latest/envvar/MACOSX_DEPLOYMENT_TARGET.html +# * https://github.com/scikit-build/scikit-build-core/blob/acb7d0346e4a05bcb47a4ea3939c705ab71e3145/src/scikit_build_core/builder/macos.py#L36 +if [[ $ARCH == "x86_64" ]]; then + export MACOSX_DEPLOYMENT_TARGET=10.15 +else + export MACOSX_DEPLOYMENT_TARGET=12.0 +fi + +if [[ "${TASK}" == "r-package" ]]; then + bash "${BUILD_DIRECTORY}/.ci/test-r-package.sh" || exit 1 + exit 0 +fi + +cd "${BUILD_DIRECTORY}" + +if [[ $TASK == "swig" ]]; then + cmake -B build -S . -DUSE_SWIG=ON + cmake --build build -j4 || exit 1 + if [[ $OS_NAME == "linux" ]] && [[ $COMPILER == "gcc" ]]; then + objdump -T ./lib_lightgbm.so > ./objdump.log || exit 1 + objdump -T ./lib_lightgbm_swig.so >> ./objdump.log || exit 1 + ./.ci/check-dynamic-dependencies.sh ./objdump.log || exit 1 + fi + if [[ $PRODUCES_ARTIFACTS == "true" ]]; then + cp ./build/lightgbmlib.jar "${BUILD_ARTIFACTSTAGINGDIRECTORY}/lightgbmlib_${OS_NAME}.jar" + fi + exit 0 +fi + +if [[ "$TASK" == "cpp-tests" ]]; then + cmake_args=( + -DBUILD_CPP_TEST=ON + -DUSE_DEBUG=ON + ) + if [[ $METHOD == "with-sanitizers" ]]; then + cmake_args+=("-DUSE_SANITIZER=ON") + if [[ -n $SANITIZERS ]]; then + cmake_args+=("-DENABLED_SANITIZERS=$SANITIZERS") + fi + fi + cmake -B build -S . "${cmake_args[@]}" + cmake --build build --target testlightgbm -j4 || exit 1 + ./testlightgbm || exit 1 + exit 0 +fi + +# including python=version=[build=*_cp*] to ensure that conda prefers CPython and doesn't fall back to +# other implementations like pypy +CONDA_PYTHON_REQUIREMENT="python=${PYTHON_VERSION}[build=*_cp*]" + +if [[ $TASK == "if-else" ]]; then + conda create -q -y -n "${CONDA_ENV}" "${CONDA_PYTHON_REQUIREMENT}" numpy + # shellcheck disable=SC1091 + source activate "${CONDA_ENV}" + cmake -B build -S . || exit 1 + cmake --build build --target lightgbm -j4 || exit 1 + cd "$BUILD_DIRECTORY/tests/cpp_tests" + ../../lightgbm config=train.conf convert_model_language=cpp convert_model=../../src/boosting/gbdt_prediction.cpp + ../../lightgbm config=predict.conf output_result=origin.pred + ../../lightgbm config=predict.conf output_result=ifelse.pred + python test.py + exit 0 +fi + +PYTHON_ENV_MANAGER="conda" +if [[ "${PYTHON_VERSION}" == "3.10" ]]; then + PYTHON_ENV_MANAGER="pixi" + CI_PIXI_ENV="py310" +fi + +# 'pixi' is used for end-of-life Python versions +if [[ "${PYTHON_ENV_MANAGER}" == "pixi" ]]; then + eval "$(pixi shell-hook --locked -e "${CI_PIXI_ENV}")" +else + CONDA_REQUIREMENT_FILE="${BUILD_DIRECTORY}/.ci/conda-envs/ci-core.txt" + conda create \ + -y \ + -n "${CONDA_ENV}" \ + --file "${CONDA_REQUIREMENT_FILE}" \ + "${CONDA_PYTHON_REQUIREMENT}" \ + || exit 1 + + # print output of 'conda list', to help in submitting bug reports + echo "conda list:" + conda list -n ${CONDA_ENV} + + # shellcheck disable=SC1091 + source activate $CONDA_ENV +fi + +cd "${BUILD_DIRECTORY}" + +if [[ $TASK == "sdist" ]]; then + sh ./build-python.sh sdist || exit 1 + sh .ci/check-python-dists.sh ./dist || exit 1 + pip install -v --no-deps "./dist/lightgbm-${LGB_VER}.tar.gz" || exit 1 + if [[ $PRODUCES_ARTIFACTS == "true" ]]; then + cp "./dist/lightgbm-${LGB_VER}.tar.gz" "${BUILD_ARTIFACTSTAGINGDIRECTORY}" || exit 1 + fi + pytest -ra ./tests/python_package_test || exit 1 + exit 0 +elif [[ $TASK == "bdist" ]]; then + if [[ $OS_NAME == "macos" ]]; then + sh ./build-python.sh bdist_wheel || exit 1 + sh .ci/check-python-dists.sh ./dist || exit 1 + if [[ $PRODUCES_ARTIFACTS == "true" ]]; then + cp "$(echo "dist/lightgbm-${LGB_VER}-py3-none-macosx"*.whl)" "${BUILD_ARTIFACTSTAGINGDIRECTORY}" || exit 1 + fi + else + sh ./build-python.sh bdist_wheel --integrated-opencl || exit 1 + + # print some debugging logs about the wheel's GLIBC version and dependencies on shared libraries + pip install 'auditwheel>=6.5.1' + auditwheel show ./dist/lightgbm*.whl + + # pass through 'auditwheel repair' to set the appropriate wheel tags. + # + # intentionally avoid vendoring libgomp, to reduce the risk of multiple OpenMP libraries + # being loaded in the same process. + auditwheel repair \ + --exclude 'libgomp.so*' \ + --lib-sdir '' \ + --wheel-dir dist-fixed/ \ + ./dist/lightgbm*.whl + + # overwrite the original wheel with the new one + rm ./dist/lightgbm*.whl + mv ./dist-fixed/lightgbm*.whl ./dist + + # check wheel properties + sh .ci/check-python-dists.sh ./dist || exit 1 + + if [[ $PRODUCES_ARTIFACTS == "true" ]]; then + # hard-code expected tag so CI will fail if 'auditwheel repair' has a surprising result (e.g. newer + # manylinux tag than we intended) + if [[ $ARCH == "x86_64" ]]; then + PLATFORM="manylinux_2_27_x86_64.manylinux_2_28_x86_64" + else + PLATFORM="manylinux2014_aarch64.manylinux_2_17_aarch64" + fi + cp "dist/lightgbm-${LGB_VER}-py3-none-${PLATFORM}.whl" "${BUILD_ARTIFACTSTAGINGDIRECTORY}" || exit 1 + fi + # Make sure we can do both CPU and GPU; see tests/python_package_test/test_dual.py + export LIGHTGBM_TEST_DUAL_CPU_GPU=1 + fi + pip install -v --no-deps ./dist/*.whl || exit 1 + pytest -ra ./tests || exit 1 + exit 0 +fi + +if [[ $TASK == "gpu" ]]; then + sed -i'.bak' 's/std::string device_type = "cpu";/std::string device_type = "gpu";/' ./include/LightGBM/config.h + grep -q 'std::string device_type = "gpu"' ./include/LightGBM/config.h || exit 1 # make sure that changes were really done + if [[ $METHOD == "pip" ]]; then + sh ./build-python.sh sdist || exit 1 + sh .ci/check-python-dists.sh ./dist || exit 1 + pip install \ + -v \ + --no-deps \ + --config-settings=cmake.define.USE_GPU=ON \ + "./dist/lightgbm-${LGB_VER}.tar.gz" \ + || exit 1 + pytest -ra ./tests/python_package_test || exit 1 + exit 0 + elif [[ $METHOD == "wheel" ]]; then + sh ./build-python.sh bdist_wheel --gpu || exit 1 + sh ./.ci/check-python-dists.sh ./dist || exit 1 + pip install -v --no-deps "$(echo "./dist/lightgbm-${LGB_VER}"*.whl)" || exit 1 + pytest -ra ./tests || exit 1 + exit 0 + elif [[ $METHOD == "source" ]]; then + cmake -B build -S . -DUSE_GPU=ON + fi +elif [[ $TASK == "cuda" ]]; then + sed -i'.bak' 's/std::string device_type = "cpu";/std::string device_type = "cuda";/' ./include/LightGBM/config.h + grep -q 'std::string device_type = "cuda"' ./include/LightGBM/config.h || exit 1 # make sure that changes were really done + # by default ``gpu_use_dp=false`` for efficiency. change to ``true`` here for exact results in ci tests + sed -i'.bak' 's/gpu_use_dp = false;/gpu_use_dp = true;/' ./include/LightGBM/config.h + grep -q 'gpu_use_dp = true' ./include/LightGBM/config.h || exit 1 # make sure that changes were really done + if [[ $METHOD == "pip" ]]; then + sh ./build-python.sh sdist || exit 1 + sh ./.ci/check-python-dists.sh ./dist || exit 1 + pip install \ + -v \ + --no-deps \ + --config-settings=cmake.define.USE_CUDA=ON \ + "./dist/lightgbm-${LGB_VER}.tar.gz" \ + || exit 1 + pytest -ra ./tests/python_package_test || exit 1 + exit 0 + elif [[ $METHOD == "wheel" ]]; then + sh ./build-python.sh bdist_wheel --cuda || exit 1 + sh ./.ci/check-python-dists.sh ./dist || exit 1 + pip install -v --no-deps "$(echo "./dist/lightgbm-${LGB_VER}"*.whl)" || exit 1 + pytest -ra ./tests || exit 1 + exit 0 + elif [[ $METHOD == "source" ]]; then + # we want at least 1 CI job testing that manual override of CMAKE_CUDA_ARCHITECTURES works + cmake \ + -B build \ + -S . \ + -DUSE_CUDA=ON \ + -DCMAKE_CUDA_ARCHITECTURES="native" + fi +elif [[ $TASK == "mpi" ]]; then + if [[ $METHOD == "pip" ]]; then + sh ./build-python.sh sdist || exit 1 + sh ./.ci/check-python-dists.sh ./dist || exit 1 + pip install \ + -v \ + --no-deps \ + --config-settings=cmake.define.USE_MPI=ON \ + "./dist/lightgbm-${LGB_VER}.tar.gz" \ + || exit 1 + pytest -ra ./tests/python_package_test || exit 1 + exit 0 + elif [[ $METHOD == "wheel" ]]; then + sh ./build-python.sh bdist_wheel --mpi || exit 1 + sh ./.ci/check-python-dists.sh ./dist || exit 1 + pip install -v --no-deps "$(echo "./dist/lightgbm-${LGB_VER}"*.whl)" || exit 1 + pytest -ra ./tests || exit 1 + exit 0 + elif [[ $METHOD == "source" ]]; then + cmake -B build -S . -DUSE_MPI=ON -DUSE_DEBUG=ON + fi +else + cmake -B build -S . +fi + +cmake --build build --target _lightgbm -j4 || exit 1 + +sh ./build-python.sh install --precompile || exit 1 +pytest -ra ./tests || exit 1 + +if [[ $TASK == "regular" ]]; then + if [[ $PRODUCES_ARTIFACTS == "true" ]]; then + if [[ $OS_NAME == "macos" ]]; then + cp ./lib_lightgbm.dylib "${BUILD_ARTIFACTSTAGINGDIRECTORY}/lib_lightgbm.dylib" + else + if [[ $COMPILER == "gcc" ]]; then + objdump -T ./lib_lightgbm.so > ./objdump.log || exit 1 + ./.ci/check-dynamic-dependencies.sh ./objdump.log || exit 1 + fi + cp ./lib_lightgbm.so "${BUILD_ARTIFACTSTAGINGDIRECTORY}/lib_lightgbm.so" + fi + fi + cd "$BUILD_DIRECTORY/examples/python-guide" + sed -i'.bak' '/import lightgbm as lgb/a\ +import matplotlib\ +matplotlib.use\(\"Agg\"\)\ +' plot_example.py # prevent interactive window mode + sed -i'.bak' 's/graph.render(view=True)/graph.render(view=False)/' plot_example.py + # install optional plotting libraries + # (not necessary for pixi-managed environments, where they're just installed by default) + if [[ "${PYTHON_ENV_MANAGER}" != "pixi" ]]; then + conda install -y -n $CONDA_ENV \ + 'h5py>=3.10' \ + 'ipywidgets>=8.1.2' \ + 'notebook>=7.1.2' + fi + for f in *.py **/*.py; do python "${f}" || exit 1; done # run all examples + cd "$BUILD_DIRECTORY/examples/python-guide/notebooks" + sed -i'.bak' 's/INTERACTIVE = False/assert False, \\"Interactive mode disabled\\"/' interactive_plot_example.ipynb + jupyter nbconvert --ExecutePreprocessor.timeout=180 --to notebook --execute --inplace ./*.ipynb || exit 1 # run all notebooks + + # importing the library should succeed even if all optional dependencies are not present + if [[ "${PYTHON_ENV_MANAGER}" != "pixi" ]]; then + conda uninstall -n $CONDA_ENV --force --yes \ + cffi \ + dask \ + distributed \ + joblib \ + matplotlib-base \ + pandas \ + polars \ + psutil \ + pyarrow \ + python-graphviz \ + scikit-learn || exit 1 + fi + python -c "import lightgbm" || exit 1 +fi diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e7191b6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf +indent_style = space +indent_size = 2 + +[*.{py,sh,ps1,js,json}] +indent_size = 4 +max_line_length = 120 +skip = external_libs +known_first_party = lightgbm + +# Tabs matter for Makefile and .gitmodules +[{makefile*,Makefile*,*.mk,*.mak,*.makefile,*.Makefile,GNUmakefile,BSDmakefile,make.bat,Makevars*,*.gitmodules}] +indent_style = tab diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..6ade88f --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,8 @@ +# introduce ruff-format (#6308) +6330d6269c81dfd4c96e664b99239b8ff39ccf91 +# enable ruff format on tests and examples (#6317) +1b792e716682254c33ddb5eb845357e84018636d +# enable ruff-format on main library Python code (#6336) +dd31208ab7a7aea86762830697b00666f843ded9 +# enable whitespace/indent_namespace rule from cpplint (#7056) +50f11a9f3c066eadee475ea567f9e9d49e6bb827 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..351fd66 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,34 @@ +* text=auto eol=lf + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.c text +*.cc text +*.cmake text +*.conf text +*.cpp text +*.cu text +*.cuh text +*.h text +*.hpp text +*.i text +*.js text +*.md text +*.py text +*.R text +*.rst text +*.sh text +*.svg text +*.toml text +*.tpp text +*.txt text +*.yaml text +*.yml text + +# help git identify which files should be treated as binary +*.ico binary +*.png binary + +# SCM syntax highlighting & preventing 3-way merges +# (added automatically by 'pixi init') +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4284a97 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +# This file controls default reviewers for LightGBM code. +# See https://help.github.com/en/articles/about-code-owners +# for details +# +# Maintainers are encouraged to use their best discretion in +# setting reviewers on PRs manually, but this file should +# offer a reasonable automatic best-guess + +# catch-all rule (this only gets matched if no rules below match) +* @guolinke @jameslamb @shiyu1994 @jmoralez @borchero @StrikerRUS @mayer79 diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 0000000..0a90d33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,26 @@ +--- +name: Bug Report 🐞 +about: Something isn't working as expected? Here is the right place to report. +--- + +## Description + + +## Reproducible example + + +## Environment info + +LightGBM version or commit hash: + +Command(s) you used to install LightGBM + +```shell + +``` + + + + +## Additional Comments + diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 0000000..206b629 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,25 @@ +--- +name: Feature Request 💡 +about: Suggest a new idea for the project. +labels: enhancement, feature-request +--- + + + +## Summary + + + +## Motivation + + + +## Description + + + +## References + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..99e65af --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + groups: + ci-dependencies: + patterns: + - "*" + cooldown: + # only accept releases that have been out for at least 15 days + default-days: 15 + commit-message: + prefix: "[ci]" + labels: + - maintenance diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..4682781 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,20 @@ +name-template: 'v$NEXT_PATCH_VERSION' +tag-template: 'v$NEXT_PATCH_VERSION' +categories: + - title: '💡 New Features' + label: 'feature' + - title: '🔨 Breaking' + label: 'breaking' + - title: '🚀 Efficiency Improvement' + label: 'efficiency' + - title: '🐛 Bug Fixes' + label: 'fix' + - title: '📖 Documentation' + label: 'doc' + - title: '🧰 Maintenance' + label: 'maintenance' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4be5966 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,81 @@ +# builds core artifacts, intended to be attached to releases +# or used by other workflows +name: Build + +on: + push: + branches: + - main + pull_request: + branches: + - main + - master + +# automatically cancel in-progress builds if another commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +env: + # tell scripts where to put artifacts + BUILD_ARTIFACTSTAGINGDIRECTORY: '${{ github.workspace }}/artifacts' + +jobs: + archive: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: true + - name: Create source archive + run: | + mkdir -p "${BUILD_ARTIFACTSTAGINGDIRECTORY}" + tar \ + -czvf \ + /tmp/LightGBM-complete_source_code_tar_gz.tar.gz \ + . + mv \ + /tmp/LightGBM-complete_source_code_tar_gz.tar.gz \ + ${BUILD_ARTIFACTSTAGINGDIRECTORY}/ + - name: Create commit.txt + shell: bash + run: | + # for pull requests, github.sha refers to the merge commit from merging the PR and + # target branch... we want the actual commit that was pushed + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + COMMIT_SHA="${{ github.event.pull_request.head.sha }}" + else + COMMIT_SHA="${{ github.sha }}" + fi + echo "${COMMIT_SHA}" > "${BUILD_ARTIFACTSTAGINGDIRECTORY}/commit.txt" + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: source-archive + path: | + ${{ env.BUILD_ARTIFACTSTAGINGDIRECTORY }}/commit.txt + ${{ env.BUILD_ARTIFACTSTAGINGDIRECTORY }}/LightGBM-complete_source_code_tar_gz.tar.gz + if-no-files-found: error + all-build-jobs-successful: + if: always() + runs-on: ubuntu-latest + needs: + - archive + permissions: + statuses: read + steps: + - name: Note that all tests succeeded + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml new file mode 100644 index 0000000..b60a9ae --- /dev/null +++ b/.github/workflows/cpp.yml @@ -0,0 +1,158 @@ +name: C++ + +on: + push: + branches: + - main + pull_request: + branches: + - main + - master + +# automatically cancel in-progress builds if another commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +env: + # tell scripts where to put artifacts + # (this variable name is left over from when jobs ran on Azure DevOps) + BUILD_ARTIFACTSTAGINGDIRECTORY: '${{ github.workspace }}/artifacts' + # where repo sources are cloned to + BUILD_DIRECTORY: '${{ github.workspace }}' + # in CMake-driven builds, parallelize compilation + CMAKE_BUILD_PARALLEL_LEVEL: 4 + +jobs: + test: + name: ${{ matrix.task }} (${{ matrix.os-display-name || matrix.os }}, ${{ matrix.compiler }}) + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + timeout-minutes: 60 + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + ############# + # C++ tests # + ############# + - os: ubuntu-latest + task: cpp-tests + compiler: clang-17 + method: with-sanitizers + container: 'ubuntu:22.04' + os-display-name: 'ubuntu22.04' + - os: macos-15-intel + task: cpp-tests + compiler: clang + method: with-sanitizers + sanitizers: "address;undefined" + container: null + - os: windows-2022 + task: cpp-tests + compiler: msvc + container: null + ########### + # if-else # + ########### + # These test CLI-generated C++ inference code. + # + # They need Python because they're written with pytest, but they + # do not need the LightGBM Python package + - os: ubuntu-latest + task: if-else + compiler: clang-17 + container: 'ubuntu:22.04' + os-display-name: 'ubuntu22.04' + python_version: '3.14' + setup-conda: 'true' + - os: macos-15-intel + task: if-else + compiler: clang + python_version: '3.10' + container: null + - os: ubuntu-latest + task: if-else + compiler: gcc + python_version: '3.11' + container: 'ghcr.io/lightgbm-org/lightgbm-vsts-agent:manylinux_2_28_x86_64' + os-display-name: 'manylinux_2_28' + steps: + - name: Install packages used by third-party actions + if: matrix.container == 'ubuntu:22.04' + shell: bash + run: | + apt-get update -y + apt-get install --no-install-recommends -y \ + dirmngr \ + gpg \ + gpg-agent \ + software-properties-common \ + sudo + # install newest version of git + # ref: + # - https://unix.stackexchange.com/a/170831/550004 + # - https://git-scm.com/download/linux + add-apt-repository ppa:git-core/ppa -y + apt-get update -y + apt-get install --no-install-recommends -y \ + git + - name: Trust git cloning LightGBM + if: startsWith(matrix.os, 'ubuntu') + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: true + - name: Setup and run tests on Linux and macOS + if: startsWith(matrix.os, 'macos') || startsWith(matrix.os, 'ubuntu') + shell: bash + run: | + export COMPILER="${{ matrix.compiler }}" + export CONDA="${HOME}/miniforge" + export METHOD="${{ matrix.method }}" + export PATH=${CONDA}/bin:${PATH} + export PYTHON_VERSION="${{ matrix.python_version }}" + export SANITIZERS="${{ matrix.sanitizers }}" + export SETUP_CONDA="${{ matrix.setup-conda }}" + export TASK="${{ matrix.task }}" + if [[ "${{ matrix.os }}" =~ ^macos ]]; then + export OS_NAME="macos" + elif [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then + export OS_NAME="linux" + fi + if [[ "${{ matrix.container }}" == "ubuntu:22.04" ]]; then + export DEBIAN_FRONTEND=noninteractive + export IN_UBUNTU_BASE_CONTAINER="true" + fi + $GITHUB_WORKSPACE/.ci/setup.sh + $GITHUB_WORKSPACE/.ci/test.sh + - name: Setup and run tests on Windows + if: startsWith(matrix.os, 'windows') + shell: pwsh -command ". {0}" + run: | + $env:BUILD_SOURCESDIRECTORY = $env:BUILD_DIRECTORY + $env:TASK = "${{ matrix.task }}" + & "$env:GITHUB_WORKSPACE/.ci/test-windows.ps1" + all-cpp-jobs-successful: + if: always() + runs-on: ubuntu-latest + needs: + - test + permissions: + statuses: read + steps: + - name: Note that all tests succeeded + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/cuda.yml b/.github/workflows/cuda.yml new file mode 100644 index 0000000..5cdcba1 --- /dev/null +++ b/.github/workflows/cuda.yml @@ -0,0 +1,115 @@ +name: CUDA Version + +on: + push: + # run on pushes to LightGBM branches matching certain names + branches: + - cuda-* + # Run manually by clicking a button in the UI + workflow_dispatch: + +# automatically cancel in-progress builds if another commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +jobs: + test: + # yamllint disable-line rule:line-length + name: ${{ matrix.task }} ${{ matrix.cuda_version }} ${{ matrix.method }} (${{ matrix.linux_version }}, ${{ matrix.compiler }}, Python ${{ matrix.python_version }}) + # yamllint disable-line rule:line-length + # ref: https://docs.github.com/en/actions/concepts/runners/about-larger-runners#specifications-for-gpu-larger-runners + runs-on: github-hosted-linux-gpu-amd64 + container: + image: nvcr.io/nvidia/cuda:${{ matrix.cuda_version }}-devel-${{ matrix.linux_version }} + env: + CMAKE_BUILD_PARALLEL_LEVEL: 4 + COMPILER: ${{ matrix.compiler }} + CONDA: /tmp/miniforge + DEBIAN_FRONTEND: noninteractive + METHOD: ${{ matrix.method }} + OS_NAME: linux + PYTHON_VERSION: ${{ matrix.python_version }} + TASK: ${{ matrix.task }} + SKBUILD_STRICT_CONFIG: true + options: --gpus all + timeout-minutes: 30 + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - method: wheel + compiler: gcc + python_version: "3.11" + cuda_version: "13.2.1" + linux_version: "ubuntu24.04" + task: cuda + - method: wheel + compiler: gcc + python_version: "3.11" + cuda_version: "12.9.1" + linux_version: "ubuntu22.04" + task: cuda + - method: source + compiler: gcc + python_version: "3.14" + cuda_version: "12.2.2" + linux_version: "ubuntu22.04" + task: cuda + - method: pip + compiler: gcc + python_version: "3.12" + cuda_version: "11.8.0" + linux_version: "ubuntu20.04" + task: cuda + steps: + - name: Install latest git and sudo + run: | + apt-get update + apt-get install --no-install-recommends -y \ + ca-certificates \ + software-properties-common + add-apt-repository ppa:git-core/ppa -y + apt-get update + apt-get install --no-install-recommends -y \ + git \ + sudo + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: true + - name: Setup and run tests + env: + CI_CUDA_VERSION: ${{ matrix.cuda_version }} + run: | + export BUILD_ARTIFACTSTAGINGDIRECTORY="${{ github.workspace }}/artifacts" + export BUILD_DIRECTORY="$GITHUB_WORKSPACE" + export PATH=$CONDA/bin:$PATH + + # check GPU usage + nvidia-smi + + # build and test + $GITHUB_WORKSPACE/.ci/setup.sh + $GITHUB_WORKSPACE/.ci/test.sh + all-cuda-jobs-successful: + if: always() + runs-on: ubuntu-latest + needs: + - test + permissions: + statuses: read + steps: + - name: Note that all tests succeeded + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 0000000..3eab150 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,58 @@ +name: 'Lock Inactive Threads' + +on: + schedule: + # midnight UTC, every Wednesday, for Issues + - cron: '0 0 * * 3' + # midnight UTC, every Thursday, for PRs + - cron: '0 0 * * 4' + # allow manual triggering from GitHub UI + workflow_dispatch: + +# only 1 job running in the repo at any time +concurrency: + group: lock + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +jobs: + action: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2 + with: + github-token: ${{ github.token }} + # after how many days of inactivity should a closed issue/PR be locked? + issue-inactive-days: '365' + pr-inactive-days: '365' + # do not close feature request issues... + # we close those but track them in https://github.com/lightgbm-org/LightGBM/issues/2302 + exclude-any-issue-labels: 'feature request' + # what labels should be removed prior to locking? + remove-issue-labels: 'awaiting response,awaiting review,blocking,in progress' + remove-pr-labels: 'awaiting response,awaiting review,blocking,in progress' + # what message should be posted prior to locking? + issue-comment: > + This issue has been automatically locked + since there has not been any recent activity since it was closed. + + To start a new related discussion, + open a new issue at https://github.com/lightgbm-org/LightGBM/issues + including a reference to this. + pr-comment: > + This pull request has been automatically locked + since there has not been any recent activity since it was closed. + + To start a new related discussion, + open a new issue at https://github.com/lightgbm-org/LightGBM/issues + including a reference to this. + # what should the locking status be? + issue-lock-reason: 'resolved' + pr-lock-reason: 'resolved' + process-only: ${{ github.event.schedule == '0 0 * * 3' && 'issues' || 'prs' }} diff --git a/.github/workflows/lychee.yml b/.github/workflows/lychee.yml new file mode 100644 index 0000000..b0e7620 --- /dev/null +++ b/.github/workflows/lychee.yml @@ -0,0 +1,54 @@ +name: Link checks + +on: + # Run manually by clicking a button in the UI + workflow_dispatch: + # Run once a day at 8:00am UTC + schedule: + - cron: '0 8 * * *' + +# only 1 job running in the repo at any time +concurrency: + group: lock + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +env: + COMPILER: gcc + OS_NAME: 'linux' + TASK: 'check-links' + +jobs: + check-links: + timeout-minutes: 60 + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: false + - name: Build docs + run: | + export CONDA=${HOME}/miniforge + export PATH=${CONDA}/bin:${HOME}/.local/bin:${PATH} + $GITHUB_WORKSPACE/.ci/setup.sh || exit 1 + $GITHUB_WORKSPACE/.ci/build-docs.sh || exit 1 + - name: Check links + uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 + with: + args: >- + --config=./docs/.lychee.toml + -- + "**/*.rst" + "**/*.md" + "./R-package/**/*.Rd" + "./docs/_build/html/*.html" + fail: true + failIfEmpty: true diff --git a/.github/workflows/no_response.yml b/.github/workflows/no_response.yml new file mode 100644 index 0000000..ce48a52 --- /dev/null +++ b/.github/workflows/no_response.yml @@ -0,0 +1,40 @@ +name: No Response Bot + +on: + issue_comment: + types: [created] + schedule: + # "every day at 04:00 UTC" + - cron: '0 4 * * *' + +# only 1 job running in the repo at any time +concurrency: + group: lock + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +jobs: + no-response: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: lee-dohm/no-response@9bb0a4b5e6a45046f00353d5de7d90fb8bd773bb # v0.5.0 + with: + closeComment: > + This issue has been automatically closed + because it has been awaiting a response for too long. + + When you have time to to work with the maintainers to resolve this issue, + please post a new comment and it will be re-opened. + If the issue has been locked for editing by the time you return to it, + please open a new issue and reference this one. + + Thank you for taking the time to improve LightGBM! + daysUntilClose: 30 + responseRequiredLabel: awaiting response + token: ${{ github.token }} diff --git a/.github/workflows/optional_checks.yml b/.github/workflows/optional_checks.yml new file mode 100644 index 0000000..30f27d5 --- /dev/null +++ b/.github/workflows/optional_checks.yml @@ -0,0 +1,44 @@ +name: Optional checks + +on: + pull_request: + branches: + - main + - master + +# automatically cancel in-progress builds if another commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +jobs: + all-optional-checks-successful: + timeout-minutes: 30 + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ github.token }} + permissions: + contents: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: false + - name: Check valgrind workflow + shell: bash + run: | + # ref: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context + PR_BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" + echo "checking status for branch '${PR_BRANCH}'" + ./.ci/check-workflow-status.sh \ + "${PR_BRANCH}" \ + 'r_valgrind.yml' \ + ${{ github.event.number }} diff --git a/.github/workflows/python_package.yml b/.github/workflows/python_package.yml new file mode 100644 index 0000000..57b18fc --- /dev/null +++ b/.github/workflows/python_package.yml @@ -0,0 +1,534 @@ +name: Python-package + +on: + push: + branches: + - main + pull_request: + branches: + - main + - master + +# automatically cancel in-progress builds if another commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +env: + # tell scripts where to put artifacts + # (this variable name is left over from when jobs ran on Azure DevOps) + BUILD_ARTIFACTSTAGINGDIRECTORY: '${{ github.workspace }}/artifacts' + # where repo sources are cloned to + BUILD_SOURCESDIRECTORY: '${{ github.workspace }}' + CMAKE_BUILD_PARALLEL_LEVEL: 4 + # avoid interactive prompts in Debian-based distributions + DEBIAN_FRONTEND: noninteractive + # On runners using nvidia-container-runtime with Docker, ensure GPUs are available to running processes. + # + # ref: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html + NVIDIA_VISIBLE_DEVICES: 'all' + SKBUILD_STRICT_CONFIG: true + +jobs: + test-linux-aarch64: + name: bdist wheel (manylinux2014-arm, gcc, Python 3.14) + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: true + - name: Setup and run tests + shell: bash + # this uses 'docker run' instead of just setting 'container:' + # because actions/checkout requires GLIBC 2.28 and that is too + # new for manylinux2014 + env: + BUILD_DIRECTORY: /LightGBM + run: | + cat > ./docker-script.sh <> tests.log 2>&1 || exit_code=-1 + cat ./tests.log + exit ${exit_code} + test-r-extra-checks: + name: r-package (${{ matrix.image }}, R-devel) + timeout-minutes: 60 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + # references: + # * CRAN "additional checks": https://cran.r-project.org/web/checks/check_issue_kinds.html + # * images: https://r-hub.github.io/containers/containers.html + # + # Generally, only images that are still supported, listed at + # https://r-hub.github.io/containers/#available-containers, should be used. + image: + - clang21 + - clang22 + - gcc16 + - rchk + - ubuntu-clang + - ubuntu-gcc12 + runs-on: ubuntu-latest + container: ghcr.io/r-hub/containers/${{ matrix.image }}:latest # zizmor: ignore[unpinned-images] + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: true + - name: Install pandoc + uses: r-lib/actions/setup-pandoc@a51a8012b0aab7c32ef9d19bf54da93f3254335e # v2.12.0 + - name: Install system dependencies + shell: bash + run: | + # libuv is needed for {fs} (https://github.com/lightgbm-org/LightGBM/issues/7208) + if type -f apt 2>&1 > /dev/null; then + apt-get update + apt-get install --no-install-recommends -y \ + cmake \ + devscripts \ + libcurl4-openssl-dev \ + libuv1-dev \ + texinfo \ + texlive-latex-extra \ + texlive-latex-recommended \ + texlive-fonts-recommended \ + texlive-fonts-extra \ + tidy \ + qpdf + else + yum update -y + yum install -y \ + cmake \ + devscripts \ + libcurl-devel \ + libuv-devel \ + qpdf \ + texinfo \ + texinfo-tex \ + texlive-amsmath \ + texlive-amsfonts \ + texlive-collection-fontsrecommended \ + texlive-collection-latexrecommended \ + texlive-latex \ + tidy + fi + - name: Install packages and run tests + shell: bash + run: | + Rscript ./.ci/install-r-deps.R --build --test --exclude=testthat + sh build-cran-package.sh + + # 'rchk' isn't run through 'R CMD check', use the approach documented at + # https://r-hub.github.io/containers/local.html + if [[ "${{ matrix.image }}" =~ "rchk" ]]; then + r-check "$(pwd)" \ + | tee ./rchk-logs.txt 2>&1 + + # the '-v' exceptions below are from R/rchk itself and not LightGBM: + # https://github.com/kalibera/rchk/issues/22#issuecomment-656036156 + if grep -E '\[PB\]|ERROR' ./rchk-logs.txt \ + | grep -v 'too many states' \ + > /dev/null; \ + then + echo "rchk found issues" + exit 1 + else + echo "rchk did not find any issues" + exit 0 + fi + fi + + # why these packages? + # + # - 'curl' is not a dependency of {lightgbm}, but it's needed for 'R CMD check' URL checks + # - 'testthat' is not needed by 'rchk', so avoid installing it until here + # + Rscript -e " + install.packages(c('curl', 'testthat'), + repos = 'https://cran.rstudio.com', + Ncpus = parallel::detectCores()) + " + + if [[ "${{ matrix.image }}" =~ "clang" ]]; then + # allowing the following NOTEs (produced by default in the clang images): + # + # * checking compilation flags used ... NOTE + # Compilation used the following non-portable flag(s): + # ‘-Wp,-D_FORTIFY_SOURCE=3’ + # + # even though CRAN itself sets that: + # https://www.stats.ox.ac.uk/pub/bdr/Rconfig/r-devel-linux-x86_64-fedora-clang + # + declare -i allowed_notes=1 + else + declare -i allowed_notes=0 + fi + + bash .ci/run-r-cmd-check.sh \ + "$(echo lightgbm_$(head -1 VERSION.txt).tar.gz)" \ + "${allowed_notes}" + all-r-package-jobs-successful: + if: always() + runs-on: ubuntu-latest + needs: + - test + - test-r-sanitizers + - test-r-extra-checks + permissions: + statuses: read + steps: + - name: Note that all tests succeeded + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/r_valgrind.yml b/.github/workflows/r_valgrind.yml new file mode 100644 index 0000000..8c2e64e --- /dev/null +++ b/.github/workflows/r_valgrind.yml @@ -0,0 +1,106 @@ +name: R valgrind tests + +# 'run-name' is used here to distinguish in the API between different runs from forks. +# +# When this was added, it was the only way to feed workflow inputs into something that +# would show up in the output of 'gh run list'. +# +# See https://github.com/orgs/community/discussions/73223#discussioncomment-11862624 +run-name: R valgrind tests (pr=${{ inputs.pr-number }}) + +# automatically cancel in-progress builds if another commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +on: + workflow_dispatch: + inputs: + pr-branch: + type: string + description: | + Branch the PR was submitted from. + Branches from forks should be prefixed with the user/org they originate from, + like '{user}:{branch}'. + pr-number: + type: string + description: Pull request ID, found in the PR URL. + +jobs: + test-r-valgrind: + name: r-package (ubuntu-latest, R-devel, valgrind) + timeout-minutes: 360 + runs-on: ubuntu-latest + container: wch1/r-debug # zizmor: ignore[unpinned-images] + env: + GITHUB_TOKEN: ${{ github.token }} + permissions: + actions: write + checks: write + contents: read + id-token: write + pull-requests: write + statuses: write + steps: + - name: Install essential software before checkout + shell: bash + run: | + apt-get update + apt-get install --no-install-recommends -y \ + curl \ + jq + - name: Install GitHub CLI + run: | + GH_CLI_VERSION="2.83.0" + curl \ + --fail \ + -O \ + -L \ + https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}/gh_${GH_CLI_VERSION}_linux_amd64.tar.gz + tar -xvf ./gh_${GH_CLI_VERSION}_linux_amd64.tar.gz + mv ./gh_${GH_CLI_VERSION}_linux_amd64/bin/gh /usr/local/bin/ + - name: Trust git cloning LightGBM + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + submodules: true + persist-credentials: false + repository: lightgbm-org/LightGBM + ref: "refs/pull/${{ inputs.pr-number }}/merge" + - name: Run tests with valgrind + shell: bash + run: ./.ci/test-r-package-valgrind.sh + - name: Send final status + if: ${{ always() }} + env: + GITHUB__WORKFLOW: ${{ github.workflow }} + INPUTS__PR_NUMBER: ${{ inputs.pr-number }} + JOB__STATUS: ${{ job.status }} + run: | + $GITHUB_WORKSPACE/.ci/set-commit-status.sh \ + "${GITHUB_WORKFLOW}" \ + "${JOB__STATUS}" \ + "${{ github.sha }}" + comment="Workflow **${GITHUB__WORKFLOW}** has been triggered! 🚀\r\n" + comment="${comment}\r\n${GITHUB_SERVER_URL}/lightgbm-org/LightGBM/actions/runs/${GITHUB_RUN_ID} \r\n" + comment="${comment}\r\nStatus: ${JOB__STATUS}" + $GITHUB_WORKSPACE/.ci/append-comment.sh \ + "${INPUTS__PR_NUMBER}" \ + "${comment}" + - name: Rerun workflow-indicator + if: ${{ always() }} + env: + INPUTS__PR_BRANCH: ${{ inputs.pr-branch }} + run: | + bash $GITHUB_WORKSPACE/.ci/rerun-workflow.sh \ + "optional_checks.yml" \ + "${INPUTS__PR_BRANCH}" \ + || true diff --git a/.github/workflows/release_drafter.yml b/.github/workflows/release_drafter.yml new file mode 100644 index 0000000..fa40d17 --- /dev/null +++ b/.github/workflows/release_drafter.yml @@ -0,0 +1,29 @@ +name: Release Drafter + +on: + push: + branches: + - main + +# only 1 job running in the repo at any time +concurrency: + group: lock + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +jobs: + updateReleaseDraft: + permissions: + contents: write + pull-requests: read + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@ed4bc48ec97379be2258e7b7ac2624a3e26ab809 # v7.4.0 + with: + config-name: release-drafter.yml + disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml new file mode 100644 index 0000000..29ad5b8 --- /dev/null +++ b/.github/workflows/static_analysis.yml @@ -0,0 +1,122 @@ +# contains non-functional tests, like checks on docs +# and code style +name: Static Analysis + +on: + push: + branches: + - main + pull_request: + branches: + - main + - master + +# automatically cancel in-progress builds if another commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +env: + COMPILER: 'gcc' + MAKEFLAGS: '-j4' + OS_NAME: 'linux' + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: false + - name: Setup and run tests + shell: bash + run: | + export TASK=lint + export CONDA=${HOME}/miniforge + export PATH=${CONDA}/bin:$HOME/.local/bin:${PATH} + $GITHUB_WORKSPACE/.ci/setup.sh || exit 1 + $GITHUB_WORKSPACE/.ci/lint-all.sh || exit 1 + check-docs: + name: check-docs + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: true + - name: Setup and run tests + shell: bash + run: | + export TASK=check-docs + export CONDA=${HOME}/miniforge + export PATH=${CONDA}/bin:$HOME/.local/bin:${PATH} + $GITHUB_WORKSPACE/.ci/setup.sh || exit 1 + $GITHUB_WORKSPACE/.ci/build-docs.sh || exit 1 + r-check-docs: + name: r-package-check-docs + timeout-minutes: 60 + runs-on: ubuntu-latest + container: rocker/verse # zizmor: ignore[unpinned-images] + permissions: + contents: read + steps: + - name: Trust git cloning LightGBM + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: true + - name: Install packages + shell: bash + run: | + Rscript ./.ci/install-r-deps.R --build --include=roxygen2 + sh build-cran-package.sh || exit 1 + R CMD INSTALL --with-keep.source lightgbm_*.tar.gz || exit 1 + - name: Test documentation + shell: bash --noprofile --norc {0} + run: | + Rscript .ci/build-docs.R || exit 1 + num_doc_files_changed=$( + git diff --name-only | grep --count -E "\.Rd|NAMESPACE" + ) + if [[ ${num_doc_files_changed} -gt 0 ]]; then + echo "Some R documentation files have changed. Please re-generate them and commit those changes." + echo "" + echo " sh build-cran-package.sh" + echo " R CMD INSTALL --with-keep.source lightgbm_*.tar.gz" + echo " Rscript -e \"roxygen2::roxygenize('R-package/', load = 'installed')\"" + echo "" + exit 1 + fi + all-static-analysis-jobs-successful: + if: always() + runs-on: ubuntu-latest + needs: + - lint + - check-docs + - r-check-docs + permissions: + statuses: read + steps: + - name: Note that all tests succeeded + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/swig.yml b/.github/workflows/swig.yml new file mode 100644 index 0000000..0953d0f --- /dev/null +++ b/.github/workflows/swig.yml @@ -0,0 +1,106 @@ +name: SWIG + +on: + push: + branches: + - main + pull_request: + branches: + - main + - master + +# automatically cancel in-progress builds if another commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# default to 0 permissions +# (job-level overrides add the minimal permissions needed) +permissions: + contents: none + +env: + # tell scripts where to put artifacts + # (this variable name is left over from when jobs ran on Azure DevOps) + BUILD_ARTIFACTSTAGINGDIRECTORY: '${{ github.workspace }}/artifacts' + # in CMake-driven builds, parallelize compilation + CMAKE_BUILD_PARALLEL_LEVEL: 4 + # all SWIG jobs produce artifacts + PRODUCES_ARTIFACTS: 'true' + # all jobs here have the same 'TASK' + TASK: swig + +jobs: + test-swig: + name: swig (${{ matrix.os-display-name || matrix.os }}, ${{ matrix.compiler }}) + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + timeout-minutes: 60 + permissions: + contents: write + id-token: write + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + compiler: gcc + container: 'ghcr.io/lightgbm-org/lightgbm-vsts-agent:manylinux_2_28_x86_64' + artifact-name: swig-linux-x86_64-jar + os-display-name: 'manylinux_2_28' + - os: macos-15-intel + compiler: clang + container: null + artifact-name: swig-macos-x86_64-jar + # Visual Studio 2022 + - os: windows-2022 + compiler: msvc + container: null + artifact-name: swig-windows-x86_64-jar + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 5 + persist-credentials: false + submodules: true + - name: Setup and run tests on Linux and macOS + if: startsWith(matrix.os, 'macos') || startsWith(matrix.os, 'ubuntu') + shell: bash + run: | + export BUILD_DIRECTORY="${GITHUB_WORKSPACE}" + export COMPILER="${{ matrix.compiler }}" + export CONDA="${HOME}/miniforge" + export PATH=${CONDA}/bin:${PATH} + if [[ "${{ matrix.os }}" =~ ^macos ]]; then + export OS_NAME="macos" + elif [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then + export OS_NAME="linux" + fi + $GITHUB_WORKSPACE/.ci/setup.sh + $GITHUB_WORKSPACE/.ci/test.sh + - name: Setup and run tests on Windows + if: startsWith(matrix.os, 'windows') + shell: pwsh -command ". {0}" + run: | + $env:BUILD_DIRECTORY = $env:GITHUB_WORKSPACE + $env:BUILD_SOURCESDIRECTORY = $env:BUILD_DIRECTORY + & "$env:GITHUB_WORKSPACE/.ci/test-windows.ps1" + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.artifact-name }} + path: ${{ env.BUILD_ARTIFACTSTAGINGDIRECTORY }}/*.jar + if-no-files-found: error + all-swig-jobs-successful: + if: always() + runs-on: ubuntu-latest + needs: + - test-swig + permissions: + statuses: read + steps: + - name: Note that all tests succeeded + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..458ad4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,486 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Bb]uild/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +nuget/ +# The packages folder can be ignored because of Package Restore +**/packages/ +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +.*.swp +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ + + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app +/windows/LightGBM.VC.db +/lightgbm +/testlightgbm + +# Created by https://www.gitignore.io/api/python + +### Python ### +!/python-package/lightgbm/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +prof/ +*.prof +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +.ruff_cache/ +cover/ +**/coverage.html +**/coverage.html.zip +**/Rplots.pdf + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +docs/pythonapi/ +*.flag + +# Doxygen documentation +docs/doxyoutput/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints +Untitled*.ipynb + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# R testing artefact +lightgbm.model + +# saved or dumped model/data +*.model +*.pkl +*.bin +*.h5 + +# macOS +**/.DS_Store + +# VSCode +.vscode + +# IntelliJ/CLion +.idea +*.iml +/cmake-build-debug/ + +# Files from local Python install +lightgbm-python/ +python-package/LICENSE +python-package/build_cpp/ +python-package/compile/ +python-package/lightgbm/VERSION.txt + +# R build artefacts +**/autom4te.cache/ +R-package/conftest* +R-package/config.status +!R-package/data/agaricus.test.rda +!R-package/data/agaricus.train.rda +!R-package/data/bank.rda +R-package/docs +R-package/src/CMakeLists.txt +R-package/src/Makevars +R-package/src/lib_lightgbm.so.dSYM/ +R-package/src/src/ +R-package/src-x64 +R-package/src-i386 +R-package/**/VERSION.txt +**/Makevars.win +lightgbm_r/ +lightgbm*.tar.gz +lightgbm*.tgz +lightgbm.Rcheck/ +miktex*.zip +*.def + +# Files created by examples and tests +*.buffer +**/lgb-Dataset.data +**/lgb.Dataset.data +**/model.txt +**/lgb-model.txt +examples/**/*.txt +tests/distributed/mlist.txt +tests/distributed/train* +tests/distributed/model* +tests/distributed/predict* + + +# Files from interactive R sessions +.Rproj.user +**/.Rapp.history +**/.Rhistory +*.rda +*.RData +*.rds + +# Files generated by aspell +**/*.bak + +# GraphViz artifacts +*.gv +*.gv.* + +# Files from local Dask work +dask-worker-space/ + +# credentials and key material +*.env +*.pem +*.pub +*.rdp +*_rsa + +# hipify-perl -inplace leaves behind *.prehip files +*.prehip + +# pixi environments +.pixi +!.pixi/config.toml + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# files created by release tasks +/release-artifacts diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..63f0903 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,15 @@ +[submodule "include/boost/compute"] + path = external_libs/compute + url = https://github.com/boostorg/compute +[submodule "eigen"] + path = external_libs/eigen + url = https://gitlab.com/libeigen/eigen.git +[submodule "external_libs/fmt"] + path = external_libs/fmt + url = https://github.com/fmtlib/fmt.git +[submodule "external_libs/fast_double_parser"] + path = external_libs/fast_double_parser + url = https://github.com/lemire/fast_double_parser.git +[submodule "external_libs/nanoarrow"] + path = external_libs/nanoarrow + url = https://github.com/apache/arrow-nanoarrow.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6aca01c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,115 @@ +# exclude files which are auto-generated by build tools +exclude: | + (?x)^( + build| + external_libs| + lightgbm-python| + lightgbm_r| + \.pixi| + )$ + |R-package/configure$ + |R-package/inst/Makevars$ + |R-package/inst/Makevars.win$ + |R-package/man/.*Rd$ + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-toml + - id: check-xml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/cmake-lint/cmake-lint + rev: '1.4.3' + hooks: + - id: cmakelint + args: ["--linelength=120"] + - repo: https://github.com/cpplint/cpplint + rev: '2.0.2' + hooks: + - id: cpplint + args: + - --root=.. # workaround to get correct header guard pattern + - --recursive + - --filter=-build/include_subdir,-whitespace/line_length + - repo: local + hooks: + - id: check-omp-pragmas + name: check-omp-pragmas + entry: sh + args: + - .ci/check-omp-pragmas.sh + language: system + pass_filenames: false + - repo: https://github.com/adrienverge/yamllint + rev: v1.38.0 + hooks: + - id: yamllint + args: ["--strict"] + - repo: local + hooks: + - id: regenerate-parameters + name: regenerate-parameters + entry: python + args: + - ./.ci/parameter-generator.py + language: python + pass_filenames: false + - repo: https://github.com/rstcheck/rstcheck + rev: v6.2.5 + hooks: + - id: rstcheck + args: ["--config", "./python-package/pyproject.toml"] + additional_dependencies: + - breathe>=4.36.0 + - sphinx>=8.1.3 + - sphinx_rtd_theme>=3.0.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.20 + hooks: + - id: ruff-check + args: ["--config", "python-package/pyproject.toml"] + types_or: [python, jupyter] + - id: ruff-format + args: ["--config", "python-package/pyproject.toml"] + types_or: [python, jupyter] + - repo: https://github.com/biomejs/pre-commit + rev: v2.5.2 + hooks: + - id: biome-ci + args: + - --config-path=./biome.json + - --diagnostic-level=info + - --error-on-warnings + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + - id: shellcheck + - repo: https://github.com/crate-ci/typos + rev: v1.48.0 + hooks: + - id: typos + args: ["--force-exclude"] + exclude: (\.gitignore$)|(^\.editorconfig$) + - repo: https://github.com/henryiii/validate-pyproject-schema-store + rev: 2026.06.30 + hooks: + - id: validate-pyproject + files: python-package/pyproject.toml$ + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v2.1.0 + hooks: + - id: mypy + args: ["--config-file", "python-package/pyproject.toml", "python-package/"] + pass_filenames: false + verbose: true + additional_dependencies: + - matplotlib>=3.9.1 + - narwhals>=1.15 + - pandas>=2.0 + - scikit-learn>=1.5.2 + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.26.1 + hooks: + - id: zizmor diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..1915551 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 +build: + os: "ubuntu-24.04" + tools: + python: "miniforge3-latest" +conda: + environment: docs/env.yml +formats: + - pdf +sphinx: + builder: html + configuration: docs/conf.py + fail_on_warning: true +submodules: + include: all + recursive: true diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..426ad08 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,22 @@ +default.extend-ignore-re = [ + "/Ot", + "mis-alignment", + "mis-spelled", + "posix-seh-rt", +] + +[default.extend-words] +MAPE = "MAPE" +datas = "datas" +indx = "indx" +interprete = "interprete" +mape = "mape" +splitted = "splitted" + +[default.extend-identifiers] +ERRORs = "ERRORs" +GAM = "GAM" +ND24s = "ND24s" +WARNINGs = "WARNINGs" +fullset = "fullset" +thess = "thess" diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..7bb0ae9 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,10 @@ +# default config: https://yamllint.readthedocs.io/en/stable/configuration.html#default-configuration +extends: default + +rules: + document-start: disable + line-length: + max: 120 + truthy: + # prevent treating GitHub Workflow "on" key as boolean value + check-keys: false diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..109975c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,866 @@ +# Copyright (c) 2016-2026 Microsoft Corporation. All rights reserved. +# Copyright (c) 2016-2026 The LightGBM developers. All rights reserved. +# Licensed under the MIT License. See LICENSE file in the project root for license information. + +cmake_minimum_required(VERSION 3.28) + +option(USE_MPI "Enable MPI-based distributed learning" OFF) +option(USE_OPENMP "Enable OpenMP" ON) +option(USE_GPU "Enable GPU-accelerated training" OFF) +option(USE_SWIG "Enable SWIG to generate Java API" OFF) +option(USE_TIMETAG "Set to ON to output time costs" OFF) +option(USE_CUDA "Enable CUDA-accelerated training " OFF) +option(USE_ROCM "Enable ROCm-accelerated training " OFF) +option(USE_DEBUG "Set to ON for Debug mode" OFF) +option(USE_SANITIZER "Use sanitizer flags" OFF) +set( + ENABLED_SANITIZERS + "address" "leak" "undefined" + CACHE + STRING + "Semicolon separated list of sanitizer names, e.g., 'address;leak'. \ +Supported sanitizers are address, leak, undefined and thread." +) +option(USE_HOMEBREW_FALLBACK "(macOS-only) also look in 'brew --prefix' for libraries (e.g. OpenMP)" ON) +option(BUILD_CLI "Build the 'lightgbm' command-line interface in addition to lib_lightgbm" ON) +option(BUILD_CPP_TEST "Build C++ tests with Google Test" OFF) +option(BUILD_STATIC_LIB "Build static library" OFF) +option(INSTALL_HEADERS "Install headers to CMAKE_INSTALL_PREFIX (e.g. '/usr/local/include')" ON) +option(__BUILD_FOR_PYTHON "Set to ON if building lib_lightgbm for use with the Python-package" OFF) +option(__BUILD_FOR_R "Set to ON if building lib_lightgbm for use with the R-package" OFF) +option(__INTEGRATE_OPENCL "Set to ON if building LightGBM with the OpenCL ICD Loader and its dependencies included" OFF) + +# If using Visual Studio generators, always target v10.x of the Windows SDK. +# Doing this avoids lookups that could fall back to very old versions, e.g. by finding +# outdated registry entries. +# ref: https://cmake.org/cmake/help/latest/variable/CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION.html +if(CMAKE_GENERATOR MATCHES "Visual Studio") + set(CMAKE_SYSTEM_VERSION 10.0 CACHE INTERNAL "target Windows SDK version" FORCE) +endif() + +project(lightgbm LANGUAGES C CXX) + +include(cmake/Utils.cmake) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/modules") + +#-- Sanitizer +if(USE_SANITIZER) + if(MSVC) + message(FATAL_ERROR "Sanitizers are not supported with MSVC.") + endif() + include(cmake/Sanitizer.cmake) + enable_sanitizers("${ENABLED_SANITIZERS}") +endif() + +if(__INTEGRATE_OPENCL) + set(__INTEGRATE_OPENCL ON CACHE BOOL "" FORCE) + set(USE_GPU OFF CACHE BOOL "" FORCE) + message(STATUS "Building library with integrated OpenCL components") +endif() + +if(__BUILD_FOR_PYTHON OR __BUILD_FOR_R OR USE_SWIG) + # the SWIG wrapper, the Python and R packages don't require the CLI + set(BUILD_CLI OFF) + # installing the SWIG wrapper, the R and Python packages shouldn't place LightGBM's headers + # outside of where the package is installed + set(INSTALL_HEADERS OFF) +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "4.8.2") + message(FATAL_ERROR "Insufficient gcc version (${CMAKE_CXX_COMPILER_VERSION})") + endif() +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "3.8") + message(FATAL_ERROR "Insufficient Clang version (${CMAKE_CXX_COMPILER_VERSION})") + endif() +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "8.1.0") + message(FATAL_ERROR "Insufficient AppleClang version (${CMAKE_CXX_COMPILER_VERSION})") + endif() +elseif(MSVC) + if(MSVC_VERSION LESS 1900) + message(FATAL_ERROR "Insufficient MSVC version (${MSVC_VERSION})") + endif() +endif() + +if(USE_SWIG) + find_package(SWIG REQUIRED) + find_package(Java REQUIRED) + find_package(JNI REQUIRED) + include(UseJava) + include(UseSWIG) + set(SWIG_CXX_EXTENSION "cxx") + set(SWIG_EXTRA_LIBRARIES "") + set(SWIG_JAVA_EXTRA_FILE_EXTENSIONS ".java" "JNI.java") + set(SWIG_MODULE_JAVA_LANGUAGE "JAVA") + set(SWIG_MODULE_JAVA_SWIG_LANGUAGE_FLAG "java") + set(CMAKE_SWIG_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/java") + include_directories(Java_INCLUDE_DIRS) + include_directories(JNI_INCLUDE_DIRS) + include_directories($ENV{JAVA_HOME}/include) + if(WIN32) + set(LGBM_SWIG_DESTINATION_DIR "${CMAKE_CURRENT_BINARY_DIR}/com/microsoft/ml/lightgbm/windows/x86_64") + include_directories($ENV{JAVA_HOME}/include/win32) + elseif(APPLE) + set(LGBM_SWIG_DESTINATION_DIR "${CMAKE_CURRENT_BINARY_DIR}/com/microsoft/ml/lightgbm/osx/x86_64") + include_directories($ENV{JAVA_HOME}/include/darwin) + else() + set(LGBM_SWIG_DESTINATION_DIR "${CMAKE_CURRENT_BINARY_DIR}/com/microsoft/ml/lightgbm/linux/x86_64") + include_directories($ENV{JAVA_HOME}/include/linux) + endif() + file(MAKE_DIRECTORY "${LGBM_SWIG_DESTINATION_DIR}") +endif() + +set(EIGEN_DIR "${PROJECT_SOURCE_DIR}/external_libs/eigen") +include_directories(${EIGEN_DIR}) + +# See https://gitlab.com/libeigen/eigen/-/blob/master/COPYING.README +add_definitions(-DEIGEN_MPL2_ONLY) +add_definitions(-DEIGEN_DONT_PARALLELIZE) + +set(FAST_DOUBLE_PARSER_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/external_libs/fast_double_parser/include") +include_directories(${FAST_DOUBLE_PARSER_INCLUDE_DIR}) + +set(FMT_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/external_libs/fmt/include") +include_directories(${FMT_INCLUDE_DIR}) + +if(BUILD_CPP_TEST AND MSVC) + # Use /MT flag to statically link the C runtime. Must be set before + # add_subdirectory(nanoarrow) so the bundled library uses the same runtime + # as the test executable; otherwise linking fails with unresolved __imp_* + # symbols (e.g. __imp_realloc). + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() + +# nanoarrow is only needed by the Arrow-based C API entry points, which are +# disabled for the R package (the R API never calls them). Skipping the +# subdirectory keeps the package tarball free of nanoarrow sources, which +# would otherwise trigger CRAN warnings about diagnostic-suppressing pragmas. +if(NOT __BUILD_FOR_R) + add_subdirectory("${PROJECT_SOURCE_DIR}/external_libs/nanoarrow" EXCLUDE_FROM_ALL) + if(NOT BUILD_STATIC_LIB) + set_target_properties(nanoarrow_static PROPERTIES POSITION_INDEPENDENT_CODE ON) + endif() +endif() + +if(__BUILD_FOR_R) + find_package(LibR REQUIRED) + message(STATUS "LIBR_EXECUTABLE: ${LIBR_EXECUTABLE}") + message(STATUS "LIBR_INCLUDE_DIRS: ${LIBR_INCLUDE_DIRS}") + message(STATUS "LIBR_LIBS_DIR: ${LIBR_LIBS_DIR}") + message(STATUS "LIBR_CORE_LIBRARY: ${LIBR_CORE_LIBRARY}") + include_directories(${LIBR_INCLUDE_DIRS}) + add_definitions(-DLGB_R_BUILD) +endif() + +if(USE_TIMETAG) + add_definitions(-DTIMETAG) +endif() + +if(USE_DEBUG) + add_definitions(-DDEBUG) +endif() + +if(USE_MPI) + find_package(MPI REQUIRED) + add_definitions(-DUSE_MPI) +else() + add_definitions(-DUSE_SOCKET) +endif() + +if(USE_CUDA) + set(CMAKE_CUDA_HOST_COMPILER "${CMAKE_CXX_COMPILER}") + find_package(CUDAToolkit 11.8 REQUIRED) + if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) + compute_cmake_cuda_archs("${CUDAToolkit_VERSION}") + endif() + enable_language(CUDA) + set(USE_OPENMP ON CACHE BOOL "CUDA requires OpenMP" FORCE) +endif() + +if(USE_ROCM) + enable_language(HIP) + set(USE_OPENMP ON CACHE BOOL "ROCm requires OpenMP" FORCE) +endif() + +if(USE_OPENMP) + if(APPLE) + find_package(OpenMP) + if(NOT OpenMP_FOUND) + if(USE_HOMEBREW_FALLBACK) + # libomp 15.0+ from brew is keg-only, so have to search in other locations. + # See https://github.com/Homebrew/homebrew-core/issues/112107#issuecomment-1278042927. + execute_process(COMMAND brew --prefix libomp + OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE) + set(OpenMP_C_FLAGS "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") + set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") + set(OpenMP_C_LIB_NAMES omp) + set(OpenMP_CXX_LIB_NAMES omp) + set(OpenMP_omp_LIBRARY ${HOMEBREW_LIBOMP_PREFIX}/lib/libomp.dylib) + endif() + find_package(OpenMP REQUIRED) + endif() + else() + find_package(OpenMP REQUIRED) + endif() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") +endif() + +if(USE_GPU) + set(BOOST_COMPUTE_HEADER_DIR ${PROJECT_SOURCE_DIR}/external_libs/compute/include) + include_directories(${BOOST_COMPUTE_HEADER_DIR}) + find_package(OpenCL REQUIRED) + include_directories(${OpenCL_INCLUDE_DIRS}) + message(STATUS "OpenCL include directory: " ${OpenCL_INCLUDE_DIRS}) + if(WIN32) + set(Boost_USE_STATIC_LIBS ON) + endif() + find_package(Boost 1.56.0 COMPONENTS filesystem system REQUIRED) + if(WIN32) + # disable autolinking in boost + add_definitions(-DBOOST_ALL_NO_LIB) + endif() + include_directories(${Boost_INCLUDE_DIRS}) + add_definitions(-DUSE_GPU) +endif() + +if(__INTEGRATE_OPENCL) + if(APPLE) + message(FATAL_ERROR "Integrated OpenCL build is not available on macOS") + else() + include(cmake/IntegratedOpenCL.cmake) + add_definitions(-DUSE_GPU) + endif() +endif() + +if(USE_CUDA) + find_package(NCCL REQUIRED) + include_directories(${CUDAToolkit_INCLUDE_DIRS}) + set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Xcompiler=${OpenMP_CXX_FLAGS} -Xcompiler=-fPIC -Xcompiler=-Wall") + if(USE_DEBUG) + set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -g") + else() + set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -O3 -lineinfo") + endif() + message(STATUS "CMAKE_CUDA_FLAGS: ${CMAKE_CUDA_FLAGS}") + + add_definitions(-DUSE_CUDA) + + if(NOT DEFINED CMAKE_CUDA_STANDARD) + set(CMAKE_CUDA_STANDARD 17) + set(CMAKE_CUDA_STANDARD_REQUIRED ON) + endif() +endif() + +if(USE_ROCM) + find_package(HIP) + include_directories(${HIP_INCLUDE_DIRS}) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__HIP_PLATFORM_AMD__") + set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} ${OpenMP_CXX_FLAGS} -fPIC -Wall") + + # avoid warning: unused variable 'mask' due to __shfl_down_sync work-around + set(DISABLED_WARNINGS "${DISABLED_WARNINGS} -Wno-unused-variable") + # avoid warning: 'hipHostAlloc' is deprecated: use hipHostMalloc instead + set(DISABLED_WARNINGS "${DISABLED_WARNINGS} -Wno-deprecated-declarations") + # avoid many warnings about missing overrides + set(DISABLED_WARNINGS "${DISABLED_WARNINGS} -Wno-inconsistent-missing-override") + # avoid warning: shift count >= width of type in feature_histogram.hpp + set(DISABLED_WARNINGS "${DISABLED_WARNINGS} -Wno-shift-count-overflow") + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DISABLED_WARNINGS}") + set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} ${DISABLED_WARNINGS}") + + if(USE_DEBUG) + set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -g -O0") + else() + set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -O3") + endif() + message(STATUS "CMAKE_HIP_FLAGS: ${CMAKE_HIP_FLAGS}") + + # Building for ROCm almost always means USE_CUDA. + # Exceptions to this will be guarded by USE_ROCM. + add_definitions(-DUSE_CUDA) + add_definitions(-DUSE_ROCM) +endif() + +include(CheckCXXSourceCompiles) +check_cxx_source_compiles(" +#include +int main() { + int a = 0; + _mm_prefetch(&a, _MM_HINT_NTA); + return 0; +} +" MM_PREFETCH) + +if(${MM_PREFETCH}) + message(STATUS "Using _mm_prefetch") + add_definitions(-DMM_PREFETCH) +endif() + +include(CheckCXXSourceCompiles) +check_cxx_source_compiles(" +#include +int main() { + char *a = (char*)_mm_malloc(8, 16); + _mm_free(a); + return 0; +} +" MM_MALLOC) + +if(${MM_MALLOC}) + message(STATUS "Using _mm_malloc") + add_definitions(-DMM_MALLOC) +endif() + +if(UNIX OR MINGW OR CYGWIN) + set( + CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -pthread -Wextra -Wall -Wno-ignored-attributes -Wno-unknown-pragmas -Wno-return-type" + ) + if(MINGW) + # ignore this warning: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95353 + set( + CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -Wno-stringop-overflow" + ) + endif() + if(USE_DEBUG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0") + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3") + endif() + if(USE_SWIG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-strict-aliasing") + endif() + if(NOT USE_OPENMP) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-pragmas -Wno-unused-private-field") + endif() + if(__BUILD_FOR_R AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-cast-function-type") + endif() +endif() + +if(WIN32 AND MINGW) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libstdc++") +endif() + +# Check if inet_pton is already available, to avoid conflicts with the implementation in LightGBM. +# As of 2022, MinGW started including a definition of inet_pton. +if(WIN32) + include(CheckSymbolExists) + list(APPEND CMAKE_REQUIRED_LIBRARIES "ws2_32") + check_symbol_exists(inet_pton "ws2tcpip.h" WIN_INET_PTON_FOUND) + if(WIN_INET_PTON_FOUND) + add_definitions(-DWIN_HAS_INET_PTON) + endif() + list(REMOVE_ITEM CMAKE_REQUIRED_LIBRARIES "ws2_32") +endif() + +if(MSVC) + # compiling 'fmt' on MSVC: "Unicode support requires compiling with /utf-8" + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /MP /utf-8") + if(__BUILD_FOR_R) + # MSVC does not like this commit: + # https://github.com/wch/r-source/commit/fb52ac1a610571fcb8ac92d886b9fefcffaa7d48 + # + # and raises "error C3646: 'private_data_c': unknown override specifier" + add_definitions(-DR_LEGACY_RCOMPLEX) + endif() + if(USE_DEBUG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Od") + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2 /Ob2 /Oi /Ot /Oy") + endif() +else() + if(NOT BUILD_STATIC_LIB) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") + endif() + if(NOT USE_DEBUG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -funroll-loops") + endif() +endif() + +set(LightGBM_HEADER_DIR ${PROJECT_SOURCE_DIR}/include) + +set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}) +set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}) + +include_directories(${LightGBM_HEADER_DIR}) + +if(USE_MPI) + include_directories(${MPI_CXX_INCLUDE_PATH}) +endif() + +set( + LGBM_SOURCES + src/boosting/boosting.cpp + src/boosting/gbdt_model_text.cpp + src/boosting/gbdt_prediction.cpp + src/boosting/gbdt.cpp + src/boosting/prediction_early_stop.cpp + src/boosting/sample_strategy.cpp + src/io/bin.cpp + src/io/config_auto.cpp + src/io/config.cpp + src/io/dataset_loader.cpp + src/io/dataset.cpp + src/io/file_io.cpp + src/io/json11.cpp + src/io/metadata.cpp + src/io/parser.cpp + src/io/train_share_states.cpp + src/io/tree.cpp + src/metric/dcg_calculator.cpp + src/metric/metric.cpp + src/network/linker_topo.cpp + src/network/linkers_mpi.cpp + src/network/linkers_socket.cpp + src/network/network.cpp + src/objective/objective_function.cpp + src/treelearner/data_parallel_tree_learner.cpp + src/treelearner/feature_histogram.cpp + src/treelearner/feature_parallel_tree_learner.cpp + src/treelearner/gpu_tree_learner.cpp + src/treelearner/gradient_discretizer.cpp + src/treelearner/linear_tree_learner.cpp + src/treelearner/serial_tree_learner.cpp + src/treelearner/tree_learner.cpp + src/treelearner/voting_parallel_tree_learner.cpp + src/utils/openmp_wrapper.cpp +) +set( + LGBM_CUDA_SOURCES + src/boosting/cuda/cuda_score_updater.cpp + src/boosting/cuda/cuda_score_updater.cu + src/boosting/cuda/nccl_gbdt.cpp + src/metric/cuda/cuda_binary_metric.cpp + src/metric/cuda/cuda_pointwise_metric.cpp + src/metric/cuda/cuda_regression_metric.cpp + src/metric/cuda/cuda_pointwise_metric.cu + src/objective/cuda/cuda_binary_objective.cpp + src/objective/cuda/cuda_multiclass_objective.cpp + src/objective/cuda/cuda_rank_objective.cpp + src/objective/cuda/cuda_regression_objective.cpp + src/objective/cuda/cuda_binary_objective.cu + src/objective/cuda/cuda_multiclass_objective.cu + src/objective/cuda/cuda_rank_objective.cu + src/objective/cuda/cuda_regression_objective.cu + src/treelearner/cuda/cuda_best_split_finder.cpp + src/treelearner/cuda/cuda_data_partition.cpp + src/treelearner/cuda/cuda_histogram_constructor.cpp + src/treelearner/cuda/cuda_leaf_splits.cpp + src/treelearner/cuda/cuda_single_gpu_tree_learner.cpp + src/treelearner/cuda/cuda_best_split_finder.cu + src/treelearner/cuda/cuda_data_partition.cu + src/treelearner/cuda/cuda_gradient_discretizer.cu + src/treelearner/cuda/cuda_histogram_constructor.cu + src/treelearner/cuda/cuda_leaf_splits.cu + src/treelearner/cuda/cuda_single_gpu_tree_learner.cu + src/io/cuda/cuda_column_data.cu + src/io/cuda/cuda_tree.cu + src/io/cuda/cuda_column_data.cpp + src/io/cuda/cuda_metadata.cpp + src/io/cuda/cuda_row_data.cpp + src/io/cuda/cuda_tree.cpp + src/cuda/cuda_utils.cpp + src/cuda/cuda_algorithms.cu +) + +if(USE_CUDA OR USE_ROCM) + list(APPEND LGBM_SOURCES ${LGBM_CUDA_SOURCES}) +endif() + +if(USE_ROCM) + set(CU_FILES "") + foreach(file IN LISTS LGBM_CUDA_SOURCES) + string(REGEX MATCH "\\.cu$" is_cu_file "${file}") + if(is_cu_file) + list(APPEND CU_FILES "${file}") + endif() + endforeach() + set_source_files_properties(${CU_FILES} PROPERTIES LANGUAGE HIP) +endif() + +add_library(lightgbm_objs OBJECT ${LGBM_SOURCES}) +if(NOT __BUILD_FOR_R) + target_link_libraries(lightgbm_objs PRIVATE nanoarrow_static) +endif() + +if(BUILD_CLI) + add_executable(lightgbm src/main.cpp src/application/application.cpp) + target_link_libraries(lightgbm PRIVATE lightgbm_objs) +endif() + +set(API_SOURCES "src/c_api.cpp") +# Only build the R part of the library if building for +# use with the R-package +if(__BUILD_FOR_R) + list(APPEND API_SOURCES "src/lightgbm_R.cpp") +endif() + +add_library(lightgbm_capi_objs OBJECT ${API_SOURCES}) +if(NOT __BUILD_FOR_R) + target_link_libraries(lightgbm_capi_objs PRIVATE nanoarrow_static) +endif() + +if(BUILD_STATIC_LIB) + add_library(_lightgbm STATIC) +else() + add_library(_lightgbm SHARED) +endif() + +# R expects libraries of the form .{dll,dylib,so}, not lib_.{dll,dylib,so} +if(__BUILD_FOR_R) + set_target_properties( + _lightgbm + PROPERTIES + PREFIX "" + OUTPUT_NAME "lightgbm" + ) +endif() + +# LightGBM headers include openmp, cuda, R etc. headers, +# thus PUBLIC is required for building _lightgbm_swig target. +target_link_libraries(_lightgbm PUBLIC lightgbm_capi_objs lightgbm_objs) + +if(MSVC AND NOT __BUILD_FOR_R) + set_target_properties(_lightgbm PROPERTIES OUTPUT_NAME "lib_lightgbm") +endif() + +if(USE_SWIG) + set_property(SOURCE swig/lightgbmlib.i PROPERTY CPLUSPLUS ON) + list(APPEND swig_options -package com.microsoft.ml.lightgbm) + set_property(SOURCE swig/lightgbmlib.i PROPERTY SWIG_FLAGS "${swig_options}") + swig_add_library(_lightgbm_swig LANGUAGE java SOURCES swig/lightgbmlib.i) + swig_link_libraries(_lightgbm_swig _lightgbm) + set_target_properties( + _lightgbm_swig + PROPERTIES + # needed to ensure Linux build does not have lib prefix specified twice, e.g. liblib_lightgbm_swig + PREFIX "" + # needed in some versions of CMake for VS and MinGW builds to ensure output dll has lib prefix + OUTPUT_NAME "lib_lightgbm_swig" + ) + if(WIN32) + set(LGBM_SWIG_LIB_DESTINATION_PATH "${LGBM_SWIG_DESTINATION_DIR}/lib_lightgbm_swig.dll") + if(MINGW OR CYGWIN) + set(LGBM_LIB_SOURCE_PATH "${PROJECT_SOURCE_DIR}/lib_lightgbm.dll") + set(LGBM_SWIG_LIB_SOURCE_PATH "${PROJECT_SOURCE_DIR}/lib_lightgbm_swig.dll") + else() + set(LGBM_LIB_SOURCE_PATH "${PROJECT_SOURCE_DIR}/Release/lib_lightgbm.dll") + set(LGBM_SWIG_LIB_SOURCE_PATH "${PROJECT_SOURCE_DIR}/Release/lib_lightgbm_swig.dll") + endif() + elseif(APPLE) + set(LGBM_LIB_SOURCE_PATH "${PROJECT_SOURCE_DIR}/lib_lightgbm.dylib") + set(LGBM_SWIG_LIB_SOURCE_PATH "${PROJECT_SOURCE_DIR}/lib_lightgbm_swig.jnilib") + set(LGBM_SWIG_LIB_DESTINATION_PATH "${LGBM_SWIG_DESTINATION_DIR}/lib_lightgbm_swig.dylib") + else() + set(LGBM_LIB_SOURCE_PATH "${PROJECT_SOURCE_DIR}/lib_lightgbm.so") + set(LGBM_SWIG_LIB_SOURCE_PATH "${PROJECT_SOURCE_DIR}/lib_lightgbm_swig.so") + set(LGBM_SWIG_LIB_DESTINATION_PATH "${LGBM_SWIG_DESTINATION_DIR}/lib_lightgbm_swig.so") + endif() + add_custom_command( + TARGET _lightgbm_swig + POST_BUILD + COMMAND "${Java_JAVAC_EXECUTABLE}" -d . java/*.java + COMMAND + "${CMAKE_COMMAND}" + -E + copy_if_different + "${LGBM_LIB_SOURCE_PATH}" + "${LGBM_SWIG_DESTINATION_DIR}" + COMMAND + "${CMAKE_COMMAND}" + -E + copy_if_different + "${LGBM_SWIG_LIB_SOURCE_PATH}" + "${LGBM_SWIG_LIB_DESTINATION_PATH}" + COMMAND "${Java_JAR_EXECUTABLE}" -cf lightgbmlib.jar com + ) +endif() + +if(USE_MPI) + target_link_libraries(lightgbm_objs PUBLIC ${MPI_CXX_LIBRARIES}) +endif() + +if(USE_OPENMP) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_link_libraries(lightgbm_objs PUBLIC OpenMP::OpenMP_CXX) + # c_api headers also includes OpenMP headers, thus compiling + # lightgbm_capi_objs needs include directory for OpenMP. + # Specifying OpenMP in target_link_libraries will get include directory + # requirements for compilation. + # This uses CMake's Transitive Usage Requirements. Refer to CMake doc: + # https://cmake.org/cmake/help/v3.16/manual/cmake-buildsystem.7.html#transitive-usage-requirements + target_link_libraries(lightgbm_capi_objs PUBLIC OpenMP::OpenMP_CXX) + endif() +endif() + +if(USE_GPU) + target_link_libraries(lightgbm_objs PUBLIC ${OpenCL_LIBRARY} ${Boost_LIBRARIES}) +endif() + +if(USE_CUDA) + target_link_libraries(lightgbm_objs PUBLIC NCCL::NCCL) +endif() + +if(USE_ROCM) + find_package(rccl) + target_link_libraries(lightgbm_objs PUBLIC ${RCCL_LIBRARY}) +endif() + +if(__INTEGRATE_OPENCL) + # targets OpenCL and Boost are added in IntegratedOpenCL.cmake + add_dependencies(lightgbm_objs OpenCL Boost) + # variables INTEGRATED_OPENCL_* are set in IntegratedOpenCL.cmake + target_include_directories(lightgbm_objs PRIVATE ${INTEGRATED_OPENCL_INCLUDES}) + target_compile_definitions(lightgbm_objs PRIVATE ${INTEGRATED_OPENCL_DEFINITIONS}) + target_link_libraries(lightgbm_objs PUBLIC ${INTEGRATED_OPENCL_LIBRARIES} ${CMAKE_DL_LIBS}) +endif() + +if(USE_CUDA) + + set_target_properties( + lightgbm_objs + PROPERTIES + CUDA_SEPARABLE_COMPILATION ON + ) + + set_target_properties( + _lightgbm + PROPERTIES + CUDA_RESOLVE_DEVICE_SYMBOLS ON + ) + + if(BUILD_CLI) + set_target_properties( + lightgbm + PROPERTIES + CUDA_RESOLVE_DEVICE_SYMBOLS ON + ) + endif() +endif() + +if(USE_ROCM) + target_link_libraries(lightgbm_objs PUBLIC hip::host) +endif() + +if(WIN32) + if(MINGW OR CYGWIN) + target_link_libraries(lightgbm_objs PUBLIC ws2_32 iphlpapi) + endif() +endif() + +if(__BUILD_FOR_R) + # utils/log.h and capi uses R headers, thus both object libraries need to link + # with R lib. + if(MSVC) + set(R_LIB ${LIBR_MSVC_CORE_LIBRARY}) + else() + set(R_LIB ${LIBR_CORE_LIBRARY}) + endif() + target_link_libraries(lightgbm_objs PUBLIC ${R_LIB}) + target_link_libraries(lightgbm_capi_objs PUBLIC ${R_LIB}) +endif() + +#-- Google C++ tests +if(BUILD_CPP_TEST) + find_package(GTest CONFIG) + if(NOT GTEST_FOUND) + message(STATUS "Did not find Google Test in the system root. Fetching Google Test now...") + include(FetchContent) +# lint_cmake: -readability/wonkycase + FetchContent_Declare( +# lint_cmake: +readability/wonkycase + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 + ) +# lint_cmake: -readability/wonkycase + FetchContent_MakeAvailable(googletest) +# lint_cmake: +readability/wonkycase + add_library(GTest::GTest ALIAS gtest) + endif() + + set(LightGBM_TEST_HEADER_DIR ${PROJECT_SOURCE_DIR}/tests/cpp_tests) + include_directories(${LightGBM_TEST_HEADER_DIR}) + + set( + CPP_TEST_SOURCES + tests/cpp_tests/test_array_args.cpp + tests/cpp_tests/test_arrow.cpp + tests/cpp_tests/test_arrow_deprecated.cpp + tests/cpp_tests/test_byte_buffer.cpp + tests/cpp_tests/test_chunked_array.cpp + tests/cpp_tests/test_common.cpp + tests/cpp_tests/test_main.cpp + tests/cpp_tests/test_serialize.cpp + tests/cpp_tests/test_single_row.cpp + tests/cpp_tests/test_stream.cpp + tests/cpp_tests/testutils.cpp + ) + if(MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /permissive-") + endif() + add_executable(testlightgbm ${CPP_TEST_SOURCES}) + target_link_libraries(testlightgbm PRIVATE lightgbm_objs lightgbm_capi_objs nanoarrow_static GTest::GTest) +endif() + +if(BUILD_CLI) + install( + TARGETS lightgbm + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin + ) +endif() + +if(__BUILD_FOR_PYTHON) + set(CMAKE_INSTALL_PREFIX "lightgbm") +endif() + +# The macOS linker puts an absolute path to linked libraries in lib_lightgbm.dylib. +# This block overrides that information for LightGBM's OpenMP dependency, to allow +# finding that library in more places. +# +# This reduces the risk of runtime issues resulting from multiple {libgomp,libiomp,libomp}.dylib being loaded. +# +if(APPLE AND USE_OPENMP AND NOT BUILD_STATIC_LIB) + # store path to {libgomp,libiomp,libomp}.dylib found at build time in a variable + get_target_property( + OpenMP_LIBRARY_LOCATION + OpenMP::OpenMP_CXX + INTERFACE_LINK_LIBRARIES + ) + # get just the filename of that path + # (to deal with the possibility that it might be 'libomp.dylib' or 'libgomp.dylib' or 'libiomp.dylib') + get_filename_component( + OpenMP_LIBRARY_NAME + ${OpenMP_LIBRARY_LOCATION} + NAME + ) + # get directory of that path + get_filename_component( + OpenMP_LIBRARY_DIR + ${OpenMP_LIBRARY_LOCATION} + DIRECTORY + ) + # get exact name of the library in a variable + get_target_property( + __LIB_LIGHTGBM_OUTPUT_NAME + _lightgbm + OUTPUT_NAME + ) + if(NOT __LIB_LIGHTGBM_OUTPUT_NAME) + set(__LIB_LIGHTGBM_OUTPUT_NAME "lib_lightgbm") + endif() + + if(CMAKE_SHARED_LIBRARY_SUFFIX_CXX) + set( + __LIB_LIGHTGBM_FILENAME "${__LIB_LIGHTGBM_OUTPUT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX_CXX}" + CACHE INTERNAL "lightgbm shared library filename" + ) + else() + set( + __LIB_LIGHTGBM_FILENAME "${__LIB_LIGHTGBM_OUTPUT_NAME}.dylib" + CACHE INTERNAL "lightgbm shared library filename" + ) + endif() + + # Override the absolute path to OpenMP with a relative one using @rpath. + # + # This also ensures that if a {libgomp,libiomp,libomp}.dylib has already been loaded, it'll just use that. + add_custom_command( + TARGET _lightgbm + POST_BUILD + COMMAND + install_name_tool + -change + ${OpenMP_LIBRARY_LOCATION} + "@rpath/${OpenMP_LIBRARY_NAME}" + "${__LIB_LIGHTGBM_FILENAME}" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Replacing hard-coded OpenMP install_name with '@rpath/${OpenMP_LIBRARY_NAME}'..." + ) + # add RPATH entries to ensure the loader looks in the following, in the following order: + # + # - (R-only) ${LIBR_LIBS_DIR} (wherever R for macOS stores vendored third-party libraries) + # - ${OpenMP_LIBRARY_DIR} (wherever find_package(OpenMP) found OpenMP at build time) + # - /opt/homebrew/opt/libomp/lib (where 'brew install' / 'brew link' puts libomp.dylib) + # - /opt/local/lib/libomp (where 'port install' puts libomp.dylib) + # + + # with some compilers, OpenMP ships with the compiler (e.g. libgomp with gcc) + list(APPEND __omp_install_rpaths "${OpenMP_LIBRARY_DIR}") + + # with clang, libomp doesn't ship with the compiler and might be supplied separately + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + list( + APPEND __omp_install_rpaths + "/opt/homebrew/opt/libomp/lib" + "/opt/local/lib/libomp" + ) + # It appears that CRAN's macOS binaries compiled with -fopenmp have install names + # of the form: + # + # /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libomp.dylib + # + # That corresponds to the libomp.dylib that ships with the R framework for macOS, available + # from https://cran.r-project.org/bin/macosx/. + # + # That absolute-path install name leads to that library being loaded unconditionally. + # + # That can result in e.g. 'library(data.table)' loading R's libomp.dylib and 'library(lightgbm)' loading + # Homebrew's. Having 2 loaded in the same process can lead to segfaults and unpredictable behavior. + # + # This can't be easily avoided by forcing R-package builds in LightGBM to use R's libomp.dylib + # at build time... LightGBM's CMake uses find_package(OpenMP), and R for macOS only provides the + # library, not CMake config files for it. + # + # Best we can do, to allow CMake-based builds of the R-package here to continue to work + # alongside CRAN-prepared binaries of other packages with OpenMP dependencies, is to + # ensure that R's library directory is the first place the loader searches for + # libomp.dylib when clang is used. + # + # ref: https://github.com/lightgbm-org/LightGBM/issues/6628 + # + if(__BUILD_FOR_R) + list(PREPEND __omp_install_rpaths "${LIBR_LIBS_DIR}") + endif() + endif() + set_target_properties( + _lightgbm + PROPERTIES + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "${__omp_install_rpaths}" + INSTALL_RPATH_USE_LINK_PATH FALSE + ) +endif() + +install( + TARGETS _lightgbm + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin + LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib + ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib +) + +if(INSTALL_HEADERS) + install( + DIRECTORY ${LightGBM_HEADER_DIR}/LightGBM + DESTINATION ${CMAKE_INSTALL_PREFIX}/include + ) + install( + FILES ${FAST_DOUBLE_PARSER_INCLUDE_DIR}/fast_double_parser.h + DESTINATION ${CMAKE_INSTALL_PREFIX}/include/LightGBM/utils + ) + install( + DIRECTORY ${FMT_INCLUDE_DIR}/ + DESTINATION ${CMAKE_INSTALL_PREFIX}/include/LightGBM/utils + FILES_MATCHING PATTERN "*.h" + ) +endif() diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e066932 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,41 @@ +# Microsoft Open Source Code of Conduct + +This code of conduct outlines expectations for participation in Microsoft-managed open source communities, as well as steps for reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all. People violating this code of conduct may be banned from the community. + +## Our open source communities strive to: + +* Be friendly and patient: Remember you might not be communicating in someone else's primary spoken or programming language, and others may not have your level of understanding. +* Be welcoming: Our communities welcome and support people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. +* Be respectful: We are a world-wide community of professionals, and we conduct ourselves professionally. Disagreement is no excuse for poor behavior and poor manners. Disrespectful and unacceptable behavior includes, but is not limited to: + 1. Violent threats or language. + 2. Discriminatory or derogatory jokes and language. + 3. Posting sexually explicit or violent material. + 4. Posting, or threatening to post, people's personally identifying information ("doxing"). + 5. Insults, especially those using discriminatory terms or slurs. +Behavior that could be perceived as sexual attention. +Advocating for or encouraging any of the above behaviors. +* Understand disagreements: Disagreements, both social and technical, are useful learning opportunities. Seek to understand the other viewpoints and resolve differences constructively. +* This code is not exhaustive or complete. It serves to capture our common understanding of a productive, collaborative environment. We expect the code to be followed in spirit as much as in the letter. + +## Scope + +This code of conduct applies to all repos and communities for Microsoft-managed open source projects regardless of whether or not the repo explicitly calls out its use of this code. The code also applies in public spaces when an individual is representing a project or its community. Examples include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +Note: Some Microsoft-managed communities have codes of conduct that pre-date this document and issue resolution process. While communities are not required to change their code, they are expected to use the resolution process outlined here. The review team will coordinate with the communities involved to address your concerns. + +## Reporting Code of Conduct Issues + +We encourage all communities to resolve issues on their own whenever possible. This builds a broader and deeper understanding and ultimately a healthier interaction. In the event that an issue cannot be resolved locally, please feel free to report your concerns by contacting opencode@microsoft.com. Your report will be handled in accordance with the issue resolution process described in the Code of Conduct FAQ. + +In your report please include: + +* Your contact information. +* Names (real, usernames or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. +* Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public chat log), please include a link or attachment. +* Any additional information that may be helpful. + +All reports will be reviewed by a multi-person team and will result in a response that is deemed necessary and appropriate to the circumstances. Where additional perspectives are needed, the team may seek insight from others with relevant expertise or experience. The confidentiality of the person reporting the incident will be kept at all times. Involved parties are never part of the review team. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the review team may take any action they deem appropriate, including a permanent ban from the community. + +This code of conduct is based on the template established by the TODO Group and used by numerous other large communities (e.g., Facebook, Yahoo, Twitter, GitHub) and the Scope section from the Contributor Covenant version 1.4. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3a25dfb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# contributing + +LightGBM has been developed and used by many active community members. + +Your help is very valuable to make it better for everyone. + +## How to Contribute + +- Check the [Feature Requests Hub](https://github.com/lightgbm-org/LightGBM/issues/2302), and submit pull requests to address chosen issue. If you need development guideline, you can check the [Development Guide](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Development-Guide.rst) or directly ask us in Issues/Pull Requests. +- Contribute to the [tests](https://github.com/lightgbm-org/LightGBM/tree/main/tests) to make it more reliable. +- Contribute to the [documentation](https://github.com/lightgbm-org/LightGBM/tree/main/docs) to make it clearer for everyone. +- Contribute to the [examples](https://github.com/lightgbm-org/LightGBM/tree/main/examples) to share your experience with other users. +- Add your stories and experience to [Awesome LightGBM](https://github.com/lightgbm-org/LightGBM/blob/main/examples/README.md). If LightGBM helped you in a machine learning competition or some research application, we want to hear about it! +- [Open an issue](https://github.com/lightgbm-org/LightGBM/issues) to report problems or recommend new features. + +## Development Guide + +### Linting + +Every commit in the repository is tested with multiple static analyzers. + +When developing locally, run some of them using `pre-commit` ([pre-commit docs](https://pre-commit.com/)). + +```shell +pre-commit run --all-files +``` + +That command will check for some issues and automatically reformat the code. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..54a487f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) Microsoft Corporation +Copyright (c) The LightGBM developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MAINTAINING.md b/MAINTAINING.md new file mode 100644 index 0000000..7930cef --- /dev/null +++ b/MAINTAINING.md @@ -0,0 +1,130 @@ +# maintaining + +This document is for LightGBM maintainers. + +## Managing Dependencies + +### Locked Environments with `pixi` + +This project uses `pixi` for tasks and CI jobs that run with a locked set of dependencies. + +In general, updating these environments looks like: + +1. manually modify `pixi.toml` +2. run `pixi install` + +And running inside one looks like: + +```shell +# interactive shell +pixi shell -e py310 + +# run a task +pixi run -e py310 python -c "import pandas; print(pandas.__version__)" +``` + +See https://pixi.prefix.dev/latest/ for more details. + + +## Releasing + +### Step 1: Put up a Release PR + +Create a pull request into `main` which prepares the source code for release. + +Copy the description and checklist from the previous release PR (for example: https://github.com/lightgbm-org/LightGBM/pull/6796). + +This should usually also include a checklist of other issues and PRs that should be completed for the release, +and the PR should be used to discuss what makes it into the release. + +### Step 2: Merge the Release PR + +Once the PR is approved, merge it. + +Do not merge any other PRs into `main` until the rest of the release is complete. + +### Step 3: Wait for a New CI Run on `main` + +Wait for all CI runs triggered by the merge to `main` to complete successfully. + +These runs build and test the official artifacts that will be attached to the GitHub release and published to package managers. + +### Step 4: Create a Release + +Navigate to https://github.com/lightgbm-org/LightGBM/releases. + +Click "edit" on the draft release that `release-drafter` has created there. + +* update the tag and release title to match the version of LightGBM, in the format `v{major}.{minor}.{patch}` +* ensure that tag points at the commit on `main` created by merging the release PR + +When you're satisfied with the state of the release, click "Publish release". + +### Step 5: Upload Artifacts + +After creating a release, run the following from the root of the repo to populate it with artifacts. + +```shell +# download all artifacts to a local directory +./.ci/download-artifacts.sh ${COMMIT_ID} + +# attach them to the GitHub release +gh release upload \ + --repo lightgbm-org/LightGBM \ + "${TAG}" \ + ./release-artifacts/* +``` + +Where: + +* `COMMIT_ID` = full commit hash of the commit on `main` corresponding to the release +* `TAG` = the tag for the release (e.g. `v4.6.0`) + +### Step 6: Complete All Other Post-merge Release Steps + +These include things like publishing to package managers, updating build configs for repackagers like ``conda-forge``, and many other steps. + +See the release checklist on the PR for details. + +## Nightly Packages + +Nightly packages for the `lightgbm` Python package are uploaded to https://anaconda.org/lightgbm-packages on every merge to `main`. + +That's done using an upload token stored in a secret in CI. +Those tokens expire after 1 year. + +To generate a new one, run the following. + +```shell +# install Anaconda CLI +conda install -y -c conda-forge \ + anaconda-auth \ + anaconda-client + +# authenticate locally +anaconda auth login + +# create a token (this expires after 1 year) +TOKEN=$( + anaconda org auth \ + --create \ + --name nightly-uploads \ + --org lightgbm-packages \ + --scopes 'api:read api:write pypi:upload' +) +``` + +That token can be used by maintainers to manually upload packages as well. + +For example: + +```shell +./.ci/download-artifacts.sh $(git rev-parse HEAD) + +# NOTE: set upload token in environment variable 'ANACONDA_API_TOKEN' +anaconda upload \ + --package lightgbm \ + --force-metadata-update \ + -t pypi \ + ./release-artifacts/*.whl +``` diff --git a/R-package/.Rbuildignore b/R-package/.Rbuildignore new file mode 100644 index 0000000..54e8bad --- /dev/null +++ b/R-package/.Rbuildignore @@ -0,0 +1,52 @@ +\.appveyor\.yml +AUTOCONF_UBUNTU_VERSION +^autom4te.cache/.*$ +^.*\.bin +^build_r.R$ +\.clang-format +^.*\.clusterfuzzlite$ +^cran-comments\.md$ +^docs$ +^.*\.dll +\.drone\.yml +^.*\.dylib +\.git +\.gitkeep$ +^.*\.history +^Makefile$ +^.*\.o +^.*\.out +^pkgdown$ +^recreate-configure\.sh$ +^.*\.so +^src/build/.*$ +^src/CMakeLists.txt$ +^src/external_libs/compute/.appveyor.yml$ +^src/external_libs/compute/.coveralls.yml$ +^src/external_libs/compute/.travis.yml$ +^src/external_libs/compute/test/.*$ +^src/external_libs/compute/index.html$ +^src/external_libs/compute/.git$ +^src/external_libs/compute/.gitignore$ +^src/external_libs/compute/CONTRIBUTING.md$ +^src/external_libs/compute/README.md$ +src/external_libs/fast_double_parser/benchmarks +src/external_libs/fast_double_parser/Makefile +src/external_libs/fast_double_parser/.*\.md +src/external_libs/fast_double_parser/tests +src/external_libs/fast_double_parser/.*\.yaml +src/external_libs/fast_double_parser/.*\.yml +src/external_libs/fmt/.*\.md +src/external_libs/fmt/.travis.yml +src/external_libs/fmt/doc +src/external_libs/fmt/support/Android\.mk +src/external_libs/fmt/support/bazel/.bazel.* +src/external_libs/fmt/support/.*\.gradle +src/external_libs/fmt/support/.*\.pro +src/external_libs/fmt/support/.*\.py +src/external_libs/fmt/support/rtd +src/external_libs/fmt/support/.*sublime-syntax +src/external_libs/fmt/support/Vagrantfile +src/external_libs/fmt/support/.*\.xml +src/external_libs/fmt/support/.*\.yml +src/external_libs/fmt/test diff --git a/R-package/AUTOCONF_UBUNTU_VERSION b/R-package/AUTOCONF_UBUNTU_VERSION new file mode 100644 index 0000000..e522974 --- /dev/null +++ b/R-package/AUTOCONF_UBUNTU_VERSION @@ -0,0 +1 @@ +2.71-2 diff --git a/R-package/DESCRIPTION b/R-package/DESCRIPTION new file mode 100755 index 0000000..c3f7052 --- /dev/null +++ b/R-package/DESCRIPTION @@ -0,0 +1,66 @@ +Package: lightgbm +Type: Package +Title: Light Gradient Boosting Machine +Version: ~~VERSION~~ +Date: ~~DATE~~ +Authors@R: c( + person("Yu", "Shi", email = "yushi2@microsoft.com", role = c("aut")), + person("Guolin", "Ke", email = "guolin.ke@outlook.com", role = c("aut")), + person("Damien", "Soukhavong", email = "damien.soukhavong@skema.edu", role = c("aut")), + person("James", "Lamb", email="jaylamb20@gmail.com", role = c("aut", "cre")), + person("Qi", "Meng", role = c("aut")), + person("Thomas", "Finley", role = c("aut")), + person("Taifeng", "Wang", role = c("aut")), + person("Wei", "Chen", role = c("aut")), + person("Weidong", "Ma", role = c("aut")), + person("Qiwei", "Ye", role = c("aut")), + person("Tie-Yan", "Liu", role = c("aut")), + person("Nikita", "Titov", role = c("aut")), + person("Yachen", "Yan", role = c("ctb")), + person("Microsoft Corporation", role = c("cph")), + person("Dropbox, Inc.", role = c("cph")), + person("Alberto", "Ferreira", role = c("ctb")), + person("Daniel", "Lemire", role = c("ctb")), + person("Victor", "Zverovich", role = c("cph")), + person("IBM Corporation", role = c("ctb")), + person("David", "Cortes", role = c("aut")), + person("Michael", "Mayer", role = c("ctb")) + ) +Description: Tree based algorithms can be improved by introducing boosting frameworks. + 'LightGBM' is one such framework, based on Ke, Guolin et al. (2017) . + This package offers an R interface to work with it. + It is designed to be distributed and efficient with the following advantages: + 1. Faster training speed and higher efficiency. + 2. Lower memory usage. + 3. Better accuracy. + 4. Parallel learning supported. + 5. Capable of handling large-scale data. + In recognition of these advantages, 'LightGBM' has been widely-used in many winning solutions of machine learning competitions. + Comparison experiments on public datasets suggest that 'LightGBM' can outperform existing boosting frameworks on both efficiency and accuracy, with significantly lower memory consumption. In addition, parallel experiments suggest that in certain circumstances, 'LightGBM' can achieve a linear speed-up in training time by using multiple machines. +Encoding: UTF-8 +License: MIT + file LICENSE +URL: https://github.com/lightgbm-org/LightGBM +BugReports: https://github.com/lightgbm-org/LightGBM/issues +NeedsCompilation: yes +Biarch: true +VignetteBuilder: knitr +Suggests: + knitr, + markdown, + processx, + RhpcBLASctl, + testthat +Depends: + R (>= 4.0) +Imports: + R6 (>= 2.4.0), + data.table (>= 1.9.6), + graphics, + jsonlite (>= 1.0), + Matrix (>= 1.1-0), + methods, + parallel, + utils +SystemRequirements: + C++17 +Config/roxygen2/version: 8.0.0 diff --git a/R-package/LICENSE b/R-package/LICENSE new file mode 100644 index 0000000..3c99fcc --- /dev/null +++ b/R-package/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2016 +COPYRIGHT HOLDER: Microsoft Corporation diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE new file mode 100644 index 0000000..8c5e4e1 --- /dev/null +++ b/R-package/NAMESPACE @@ -0,0 +1,66 @@ +# Generated by roxygen2: do not edit by hand + +S3method("dimnames<-",lgb.Dataset) +S3method(dim,lgb.Dataset) +S3method(dimnames,lgb.Dataset) +S3method(get_field,lgb.Dataset) +S3method(predict,lgb.Booster) +S3method(print,lgb.Booster) +S3method(set_field,lgb.Dataset) +S3method(summary,lgb.Booster) +export(getLGBMthreads) +export(get_field) +export(lgb.Dataset) +export(lgb.Dataset.construct) +export(lgb.Dataset.create.valid) +export(lgb.Dataset.save) +export(lgb.Dataset.set.categorical) +export(lgb.Dataset.set.reference) +export(lgb.configure_fast_predict) +export(lgb.convert_with_rules) +export(lgb.cv) +export(lgb.drop_serialized) +export(lgb.dump) +export(lgb.get.eval.result) +export(lgb.importance) +export(lgb.interpret) +export(lgb.interprete) +export(lgb.load) +export(lgb.make_serializable) +export(lgb.model.dt.tree) +export(lgb.plot.importance) +export(lgb.plot.interpretation) +export(lgb.restore_handle) +export(lgb.save) +export(lgb.slice.Dataset) +export(lgb.train) +export(lightgbm) +export(setLGBMthreads) +export(set_field) +import(methods) +importClassesFrom(Matrix,CsparseMatrix) +importClassesFrom(Matrix,RsparseMatrix) +importClassesFrom(Matrix,dgCMatrix) +importClassesFrom(Matrix,dgRMatrix) +importClassesFrom(Matrix,dsparseMatrix) +importClassesFrom(Matrix,dsparseVector) +importFrom(Matrix,Matrix) +importFrom(R6,R6Class) +importFrom(data.table,":=") +importFrom(data.table,as.data.table) +importFrom(data.table,data.table) +importFrom(data.table,rbindlist) +importFrom(data.table,set) +importFrom(data.table,setnames) +importFrom(data.table,setorder) +importFrom(data.table,setorderv) +importFrom(graphics,barplot) +importFrom(graphics,par) +importFrom(jsonlite,fromJSON) +importFrom(methods,is) +importFrom(methods,new) +importFrom(parallel,detectCores) +importFrom(stats,quantile) +importFrom(utils,modifyList) +importFrom(utils,read.delim) +useDynLib(lightgbm , .registration = TRUE) diff --git a/R-package/R/aliases.R b/R-package/R/aliases.R new file mode 100644 index 0000000..5251cca --- /dev/null +++ b/R-package/R/aliases.R @@ -0,0 +1,104 @@ +# Central location for parameter aliases. +# See https://lightgbm.readthedocs.io/en/latest/Parameters.html#core-parameters + +# [description] List of respected parameter aliases specific to lgb.Dataset. Wrapped in a function to +# take advantage of lazy evaluation (so it doesn't matter what order +# R sources files during installation). +# [return] A named list, where each key is a parameter relevant to lgb.Dataset and each value is a character +# vector of corresponding aliases. +.DATASET_PARAMETERS <- function() { + all_aliases <- .PARAMETER_ALIASES() + return(all_aliases[c( + "bin_construct_sample_cnt" + , "categorical_feature" + , "data_random_seed" + , "enable_bundle" + , "feature_pre_filter" + , "forcedbins_filename" + , "group_column" + , "header" + , "ignore_column" + , "is_enable_sparse" + , "label_column" + , "linear_tree" + , "max_bin" + , "max_bin_by_feature" + , "min_data_in_bin" + , "pre_partition" + , "precise_float_parser" + , "two_round" + , "use_missing" + , "weight_column" + , "zero_as_missing" + )]) +} + +# [description] Non-exported environment, used for caching details that only need to be +# computed once per R session. +.lgb_session_cache_env <- new.env() + +# [description] List of respected parameter aliases. Wrapped in a function to take advantage of +# lazy evaluation (so it doesn't matter what order R sources files during installation). +# [return] A named list, where each key is a main LightGBM parameter and each value is a character +# vector of corresponding aliases. +.PARAMETER_ALIASES <- function() { + if (exists("PARAMETER_ALIASES", where = .lgb_session_cache_env)) { + return(get("PARAMETER_ALIASES", envir = .lgb_session_cache_env)) + } + params_to_aliases <- jsonlite::fromJSON( + .Call( + LGBM_DumpParamAliases_R + ) + ) + for (main_name in names(params_to_aliases)) { + aliases_with_main_name <- c(main_name, unlist(params_to_aliases[[main_name]])) + params_to_aliases[[main_name]] <- aliases_with_main_name + } + # store in cache so the next call to `.PARAMETER_ALIASES()` doesn't need to recompute this + assign( + x = "PARAMETER_ALIASES" + , value = params_to_aliases + , envir = .lgb_session_cache_env + ) + return(params_to_aliases) +} + +# [description] +# Per https://github.com/lightgbm-org/LightGBM/blob/main/docs/Parameters.rst#metric, +# a few different strings can be used to indicate "no metrics". +# [returns] +# A character vector +.NO_METRIC_STRINGS <- function() { + return( + c( + "na" + , "None" + , "null" + , "custom" + ) + ) +} + +.MULTICLASS_OBJECTIVES <- function() { + return( + c( + "multi_logloss" + , "multiclass" + , "softmax" + , "multiclassova" + , "multiclass_ova" + , "ova" + , "ovr" + ) + ) +} + +.BINARY_OBJECTIVES <- function() { + return( + c( + "binary_logloss" + , "binary" + , "binary_error" + ) + ) +} diff --git a/R-package/R/callback.R b/R-package/R/callback.R new file mode 100644 index 0000000..ba3f742 --- /dev/null +++ b/R-package/R/callback.R @@ -0,0 +1,368 @@ +# constants that control naming in lists +.EVAL_KEY <- function() { + return("eval") +} +.EVAL_ERR_KEY <- function() { + return("eval_err") +} + +#' @importFrom R6 R6Class +CB_ENV <- R6::R6Class( + "lgb.cb_env", + cloneable = FALSE, + public = list( + model = NULL, + iteration = NULL, + begin_iteration = NULL, + end_iteration = NULL, + eval_list = list(), + eval_err_list = list(), + best_iter = -1L, + best_score = NA, + met_early_stop = FALSE + ) +) + +# Format the evaluation metric string +.format_eval_string <- function(eval_res, eval_err) { + + # Check for empty evaluation string + if (is.null(eval_res) || length(eval_res) == 0L) { + stop("no evaluation results") + } + + # Check for empty evaluation error + if (!is.null(eval_err)) { + return(sprintf("%s\'s %s:%g+%g", eval_res$data_name, eval_res$name, eval_res$value, eval_err)) + } else { + return(sprintf("%s\'s %s:%g", eval_res$data_name, eval_res$name, eval_res$value)) + } + +} + +.merge_eval_string <- function(env) { + + # Check length of evaluation list + if (length(env$eval_list) <= 0L) { + return("") + } + + # Get evaluation + msg <- list(sprintf("[%d]:", env$iteration)) + + # Set if evaluation error + is_eval_err <- length(env$eval_err_list) > 0L + + # Loop through evaluation list + for (j in seq_along(env$eval_list)) { + + # Store evaluation error + eval_err <- NULL + if (isTRUE(is_eval_err)) { + eval_err <- env$eval_err_list[[j]] + } + + # Set error message + msg <- c(msg, .format_eval_string(eval_res = env$eval_list[[j]], eval_err = eval_err)) + + } + + return(paste(msg, collapse = " ")) + +} + +cb_print_evaluation <- function(period) { + + # Create callback + callback <- function(env) { + + # Check if period is at least 1 or more + if (period > 0L) { + + # Store iteration + i <- env$iteration + + # Check if iteration matches moduo + if ((i - 1L) %% period == 0L || is.element(i, c(env$begin_iteration, env$end_iteration))) { + + # Merge evaluation string + msg <- .merge_eval_string(env = env) + + # Check if message is existing + if (nchar(msg) > 0L) { + cat(.merge_eval_string(env = env), "\n") + } + + } + + } + + return(invisible(NULL)) + + } + + # Store attributes + attr(callback, "call") <- match.call() + attr(callback, "name") <- "cb_print_evaluation" + + return(callback) + +} + +cb_record_evaluation <- function() { + + # Create callback + callback <- function(env) { + + if (length(env$eval_list) <= 0L) { + return() + } + + # Set if evaluation error + is_eval_err <- length(env$eval_err_list) > 0L + + # Check length of recorded evaluation + if (length(env$model$record_evals) == 0L) { + + # Loop through each evaluation list element + for (j in seq_along(env$eval_list)) { + + # Store names + data_name <- env$eval_list[[j]]$data_name + name <- env$eval_list[[j]]$name + env$model$record_evals$start_iter <- env$begin_iteration + + # Check if evaluation record exists + if (is.null(env$model$record_evals[[data_name]])) { + env$model$record_evals[[data_name]] <- list() + } + + # Create dummy lists + env$model$record_evals[[data_name]][[name]] <- list() + env$model$record_evals[[data_name]][[name]][[.EVAL_KEY()]] <- list() + env$model$record_evals[[data_name]][[name]][[.EVAL_ERR_KEY()]] <- list() + + } + + } + + # Loop through each evaluation list element + for (j in seq_along(env$eval_list)) { + + # Get evaluation data + eval_res <- env$eval_list[[j]] + eval_err <- NULL + if (isTRUE(is_eval_err)) { + eval_err <- env$eval_err_list[[j]] + } + + # Store names + data_name <- eval_res$data_name + name <- eval_res$name + + # Store evaluation data + env$model$record_evals[[data_name]][[name]][[.EVAL_KEY()]] <- c( + env$model$record_evals[[data_name]][[name]][[.EVAL_KEY()]] + , eval_res$value + ) + env$model$record_evals[[data_name]][[name]][[.EVAL_ERR_KEY()]] <- c( + env$model$record_evals[[data_name]][[name]][[.EVAL_ERR_KEY()]] + , eval_err + ) + + } + + return(invisible(NULL)) + + } + + # Store attributes + attr(callback, "call") <- match.call() + attr(callback, "name") <- "cb_record_evaluation" + + return(callback) + +} + +cb_early_stop <- function(stopping_rounds, first_metric_only, verbose) { + + factor_to_bigger_better <- NULL + best_iter <- NULL + best_score <- NULL + best_msg <- NULL + eval_len <- NULL + + # Initialization function + init <- function(env) { + + # Early stopping cannot work without metrics + if (length(env$eval_list) == 0L) { + stop("For early stopping, valids must have at least one element") + } + + # Store evaluation length + eval_len <<- length(env$eval_list) + + # Check if verbose or not + if (isTRUE(verbose)) { + msg <- paste0( + "Will train until there is no improvement in " + , stopping_rounds + , " rounds.\n" + ) + cat(msg) + } + + # Internally treat everything as a maximization task + factor_to_bigger_better <<- rep.int(1.0, eval_len) + best_iter <<- rep.int(-1L, eval_len) + best_score <<- rep.int(-Inf, eval_len) + best_msg <<- list() + + # Loop through evaluation elements + for (i in seq_len(eval_len)) { + + # Prepend message + best_msg <<- c(best_msg, "") + + # Internally treat everything as a maximization task + if (!isTRUE(env$eval_list[[i]]$higher_better)) { + factor_to_bigger_better[i] <<- -1.0 + } + + } + + return(invisible(NULL)) + + } + + # Create callback + callback <- function(env) { + + # Check for empty evaluation + if (is.null(eval_len)) { + init(env = env) + } + + # Store iteration + cur_iter <- env$iteration + + # By default, any metric can trigger early stopping. This can be disabled + # with 'first_metric_only = TRUE' + if (isTRUE(first_metric_only)) { + evals_to_check <- 1L + } else { + evals_to_check <- seq_len(eval_len) + } + + # Loop through evaluation + for (i in evals_to_check) { + + # Store score + score <- env$eval_list[[i]]$value * factor_to_bigger_better[i] + + # Check if score is better + if (score > best_score[i]) { + + # Store new scores + best_score[i] <<- score + best_iter[i] <<- cur_iter + + # Prepare to print if verbose + if (verbose) { + best_msg[[i]] <<- as.character(.merge_eval_string(env = env)) + } + + } else { + + # Check if early stopping is required + if (cur_iter - best_iter[i] >= stopping_rounds) { + + if (!is.null(env$model)) { + env$model$best_score <- best_score[i] + env$model$best_iter <- best_iter[i] + } + + if (isTRUE(verbose)) { + cat(paste0("Early stopping, best iteration is: ", best_msg[[i]], "\n")) + } + + # Store best iteration and stop + env$best_iter <- best_iter[i] + env$met_early_stop <- TRUE + } + + } + + if (!isTRUE(env$met_early_stop) && cur_iter == env$end_iteration) { + + if (!is.null(env$model)) { + env$model$best_score <- best_score[i] + env$model$best_iter <- best_iter[i] + } + + if (isTRUE(verbose)) { + cat(paste0("Did not meet early stopping, best iteration is: ", best_msg[[i]], "\n")) + } + + # Store best iteration and stop + env$best_iter <- best_iter[i] + env$met_early_stop <- TRUE + } + } + + return(invisible(NULL)) + + } + + attr(callback, "call") <- match.call() + attr(callback, "name") <- "cb_early_stop" + + return(callback) + +} + +# Extract callback names from the list of callbacks +.callback_names <- function(cb_list) { + return(unlist(lapply(cb_list, attr, "name"))) +} + +.add_cb <- function(cb_list, cb) { + + # Combine two elements + cb_list <- c(cb_list, cb) + + # Set names of elements + names(cb_list) <- .callback_names(cb_list = cb_list) + + if ("cb_early_stop" %in% names(cb_list)) { + + # Concatenate existing elements + cb_list <- c(cb_list, cb_list["cb_early_stop"]) + + # Remove only the first one + cb_list["cb_early_stop"] <- NULL + + } + + return(cb_list) + +} + +.categorize_callbacks <- function(cb_list) { + + # Check for pre-iteration or post-iteration + return( + list( + pre_iter = Filter(function(x) { + pre <- attr(x, "is_pre_iteration") + !is.null(pre) && pre + }, cb_list), + post_iter = Filter(function(x) { + pre <- attr(x, "is_pre_iteration") + is.null(pre) || !pre + }, cb_list) + ) + ) + +} diff --git a/R-package/R/lgb.Booster.R b/R-package/R/lgb.Booster.R new file mode 100644 index 0000000..6252485 --- /dev/null +++ b/R-package/R/lgb.Booster.R @@ -0,0 +1,1575 @@ +#' @importFrom R6 R6Class +#' @importFrom utils modifyList +Booster <- R6::R6Class( + classname = "lgb.Booster", + cloneable = FALSE, + public = list( + + best_iter = -1L, + best_score = NA_real_, + params = list(), + record_evals = list(), + data_processor = NULL, + + # Initialize will create a starter booster + initialize = function(params = list(), + train_set = NULL, + modelfile = NULL, + model_str = NULL) { + + handle <- NULL + + if (!is.null(train_set)) { + + if (!.is_Dataset(train_set)) { + stop("lgb.Booster: Can only use lgb.Dataset as training data") + } + train_set_handle <- train_set$.__enclos_env__$private$get_handle() + params <- utils::modifyList(params, train_set$get_params()) + params_str <- .params2str(params = params) + # Store booster handle + handle <- .Call( + LGBM_BoosterCreate_R + , train_set_handle + , params_str + ) + + # Create private booster information + private$train_set <- train_set + private$train_set_version <- train_set$.__enclos_env__$private$version + private$num_dataset <- 1L + private$init_predictor <- train_set$.__enclos_env__$private$predictor + + if (!is.null(private$init_predictor)) { + + # Merge booster + .Call( + LGBM_BoosterMerge_R + , handle + , private$init_predictor$.__enclos_env__$private$handle + ) + + } + + # Check current iteration + private$is_predicted_cur_iter <- c(private$is_predicted_cur_iter, FALSE) + + } else if (!is.null(modelfile)) { + + # Do we have a model file as character? + if (!is.character(modelfile)) { + stop("lgb.Booster: Can only use a string as model file path") + } + + modelfile <- path.expand(modelfile) + + # Create booster from model + handle <- .Call( + LGBM_BoosterCreateFromModelfile_R + , modelfile + ) + params <- private$get_loaded_param(handle) + + } else if (!is.null(model_str)) { + + # Do we have a model_str as character/raw? + if (!is.raw(model_str) && !is.character(model_str)) { + stop("lgb.Booster: Can only use a character/raw vector as model_str") + } + + # Create booster from model + handle <- .Call( + LGBM_BoosterLoadModelFromString_R + , model_str + ) + + } else { + + # Booster non existent + stop( + "lgb.Booster: Need at least either training dataset, " + , "model file, or model_str to create booster instance" + ) + + } + + class(handle) <- "lgb.Booster.handle" + private$handle <- handle + private$num_class <- 1L + .Call( + LGBM_BoosterGetNumClasses_R + , private$handle + , private$num_class + ) + + self$params <- params + + return(invisible(NULL)) + + }, + + # Set training data name + set_train_data_name = function(name) { + + # Set name + private$name_train_set <- name + return(invisible(self)) + + }, + + # Add validation data + add_valid = function(data, name) { + + if (!.is_Dataset(data)) { + stop("lgb.Booster.add_valid: Can only use lgb.Dataset as validation data") + } + + if (!identical(data$.__enclos_env__$private$predictor, private$init_predictor)) { + stop( + "lgb.Booster.add_valid: Failed to add validation data; " + , "you should use the same predictor for these data" + ) + } + + if (!is.character(name)) { + stop("lgb.Booster.add_valid: Can only use characters as data name") + } + + # Add validation data to booster + .Call( + LGBM_BoosterAddValidData_R + , private$handle + , data$.__enclos_env__$private$get_handle() + ) + + private$valid_sets <- c(private$valid_sets, data) + private$name_valid_sets <- c(private$name_valid_sets, name) + private$num_dataset <- private$num_dataset + 1L + private$is_predicted_cur_iter <- c(private$is_predicted_cur_iter, FALSE) + + return(invisible(self)) + + }, + + reset_parameter = function(params) { + + if (methods::is(self$params, "list")) { + params <- utils::modifyList(self$params, params) + } + + params_str <- .params2str(params = params) + + self$restore_handle() + + .Call( + LGBM_BoosterResetParameter_R + , private$handle + , params_str + ) + self$params <- params + + return(invisible(self)) + + }, + + # Perform boosting update iteration + update = function(train_set = NULL, fobj = NULL) { + + if (is.null(train_set)) { + if (private$train_set$.__enclos_env__$private$version != private$train_set_version) { + train_set <- private$train_set + } + } + + if (!is.null(train_set)) { + + if (!.is_Dataset(train_set)) { + stop("lgb.Booster.update: Only can use lgb.Dataset as training data") + } + + if (!identical(train_set$predictor, private$init_predictor)) { + stop("lgb.Booster.update: Change train_set failed, you should use the same predictor for these data") + } + + .Call( + LGBM_BoosterResetTrainingData_R + , private$handle + , train_set$.__enclos_env__$private$get_handle() + ) + + private$train_set <- train_set + private$train_set_version <- train_set$.__enclos_env__$private$version + + } + + # Check if objective is empty + if (is.null(fobj)) { + if (private$set_objective_to_none) { + stop("lgb.Booster.update: cannot update due to null objective function") + } + # Boost iteration from known objective + .Call( + LGBM_BoosterUpdateOneIter_R + , private$handle + ) + + } else { + + if (!is.function(fobj)) { + stop("lgb.Booster.update: fobj should be a function") + } + if (!private$set_objective_to_none) { + self$reset_parameter(params = list(objective = "none")) + private$set_objective_to_none <- TRUE + } + # Perform objective calculation + preds <- private$inner_predict(1L) + gpair <- fobj(preds, private$train_set) + + # Check for gradient and hessian as list + if (is.null(gpair$grad) || is.null(gpair$hess)) { + stop("lgb.Booster.update: custom objective should + return a list with attributes (hess, grad)") + } + + # Check grad and hess have the right shape + n_grad <- length(gpair$grad) + n_hess <- length(gpair$hess) + n_preds <- length(preds) + if (n_grad != n_preds) { + stop(sprintf("Expected custom objective function to return grad with length %d, got %d.", n_preds, n_grad)) + } + if (n_hess != n_preds) { + stop(sprintf("Expected custom objective function to return hess with length %d, got %d.", n_preds, n_hess)) + } + + # Return custom boosting gradient/hessian + .Call( + LGBM_BoosterUpdateOneIterCustom_R + , private$handle + , gpair$grad + , gpair$hess + , n_preds + ) + + } + + # Loop through each iteration + for (i in seq_along(private$is_predicted_cur_iter)) { + private$is_predicted_cur_iter[[i]] <- FALSE + } + + return(invisible(self)) + + }, + + # Return one iteration behind + rollback_one_iter = function() { + + self$restore_handle() + + .Call( + LGBM_BoosterRollbackOneIter_R + , private$handle + ) + + # Loop through each iteration + for (i in seq_along(private$is_predicted_cur_iter)) { + private$is_predicted_cur_iter[[i]] <- FALSE + } + + return(invisible(self)) + + }, + + # Get current iteration + current_iter = function() { + + self$restore_handle() + + cur_iter <- 0L + .Call( + LGBM_BoosterGetCurrentIteration_R + , private$handle + , cur_iter + ) + return(cur_iter) + + }, + + # Number of trees per iteration + num_trees_per_iter = function() { + + self$restore_handle() + + trees_per_iter <- 1L + .Call( + LGBM_BoosterNumModelPerIteration_R + , private$handle + , trees_per_iter + ) + return(trees_per_iter) + + }, + + # Total number of trees + num_trees = function() { + + self$restore_handle() + + ntrees <- 0L + .Call( + LGBM_BoosterNumberOfTotalModel_R + , private$handle + , ntrees + ) + return(ntrees) + + }, + + # Number of iterations (= rounds) + num_iter = function() { + + ntrees <- self$num_trees() + trees_per_iter <- self$num_trees_per_iter() + + return(ntrees / trees_per_iter) + + }, + + # Get upper bound + upper_bound = function() { + + self$restore_handle() + + upper_bound <- 0.0 + .Call( + LGBM_BoosterGetUpperBoundValue_R + , private$handle + , upper_bound + ) + return(upper_bound) + + }, + + # Get lower bound + lower_bound = function() { + + self$restore_handle() + + lower_bound <- 0.0 + .Call( + LGBM_BoosterGetLowerBoundValue_R + , private$handle + , lower_bound + ) + return(lower_bound) + + }, + + # Evaluate data on metrics + eval = function(data, name, feval = NULL) { + + if (!.is_Dataset(data)) { + stop("lgb.Booster.eval: Can only use lgb.Dataset to eval") + } + + # Check for identical data + data_idx <- 0L + if (identical(data, private$train_set)) { + data_idx <- 1L + } else { + + # Check for validation data + if (length(private$valid_sets) > 0L) { + + for (i in seq_along(private$valid_sets)) { + + # Check for identical validation data with training data + if (identical(data, private$valid_sets[[i]])) { + + # Found identical data, skip + data_idx <- i + 1L + break + + } + + } + + } + + } + + # Check if evaluation was not done + if (data_idx == 0L) { + + # Add validation data by name + self$add_valid(data, name) + data_idx <- private$num_dataset + + } + + # Evaluate data + return( + private$inner_eval( + data_name = name + , data_idx = data_idx + , feval = feval + ) + ) + + }, + + # Evaluation training data + eval_train = function(feval = NULL) { + return(private$inner_eval(private$name_train_set, 1L, feval)) + }, + + # Evaluation validation data + eval_valid = function(feval = NULL) { + + ret <- list() + + if (length(private$valid_sets) <= 0L) { + return(ret) + } + + for (i in seq_along(private$valid_sets)) { + ret <- append( + x = ret + , values = private$inner_eval(private$name_valid_sets[[i]], i + 1L, feval) + ) + } + + return(ret) + + }, + + # Save model + save_model = function( + filename + , num_iteration = NULL + , feature_importance_type = 0L + , start_iteration = 1L + ) { + + self$restore_handle() + + if (is.null(num_iteration)) { + num_iteration <- self$best_iter + } + + filename <- path.expand(filename) + + .Call( + LGBM_BoosterSaveModel_R + , private$handle + , as.integer(num_iteration) + , as.integer(feature_importance_type) + , filename + , as.integer(start_iteration) - 1L # Turn to 0-based + ) + + return(invisible(self)) + }, + + save_model_to_string = function( + num_iteration = NULL + , feature_importance_type = 0L + , as_char = TRUE + , start_iteration = 1L + ) { + + self$restore_handle() + + if (is.null(num_iteration)) { + num_iteration <- self$best_iter + } + + model_str <- .Call( + LGBM_BoosterSaveModelToString_R + , private$handle + , as.integer(num_iteration) + , as.integer(feature_importance_type) + , as.integer(start_iteration) - 1L # Turn to 0-based + ) + + if (as_char) { + model_str <- rawToChar(model_str) + } + + return(model_str) + + }, + + # Dump model in memory + dump_model = function( + num_iteration = NULL, feature_importance_type = 0L, start_iteration = 1L + ) { + + self$restore_handle() + + if (is.null(num_iteration)) { + num_iteration <- self$best_iter + } + + model_str <- .Call( + LGBM_BoosterDumpModel_R + , private$handle + , as.integer(num_iteration) + , as.integer(feature_importance_type) + , as.integer(start_iteration) - 1L # Turn to 0-based + ) + + return(model_str) + + }, + + # Predict on new data + predict = function(data, + start_iteration = NULL, + num_iteration = NULL, + rawscore = FALSE, + predleaf = FALSE, + predcontrib = FALSE, + header = FALSE, + params = list()) { + + self$restore_handle() + + if (is.null(num_iteration)) { + num_iteration <- self$best_iter + } + + if (is.null(start_iteration)) { + start_iteration <- 0L + } + + # possibly override keyword arguments with parameters + # + # NOTE: this length() check minimizes the latency introduced by these checks, + # for the common case where params is empty + # + # NOTE: doing this here instead of in Predictor$predict() to keep + # Predictor$predict() as fast as possible + if (length(params) > 0L) { + params <- .check_wrapper_param( + main_param_name = "predict_raw_score" + , params = params + , alternative_kwarg_value = rawscore + ) + params <- .check_wrapper_param( + main_param_name = "predict_leaf_index" + , params = params + , alternative_kwarg_value = predleaf + ) + params <- .check_wrapper_param( + main_param_name = "predict_contrib" + , params = params + , alternative_kwarg_value = predcontrib + ) + rawscore <- params[["predict_raw_score"]] + predleaf <- params[["predict_leaf_index"]] + predcontrib <- params[["predict_contrib"]] + } + + # Predict on new data + predictor <- Predictor$new( + modelfile = private$handle + , params = params + , fast_predict_config = private$fast_predict_config + ) + return( + predictor$predict( + data = data + , start_iteration = start_iteration + , num_iteration = num_iteration + , rawscore = rawscore + , predleaf = predleaf + , predcontrib = predcontrib + , header = header + ) + ) + + }, + + # Transform into predictor + to_predictor = function() { + return(Predictor$new(modelfile = private$handle)) + }, + + configure_fast_predict = function(csr = FALSE, + start_iteration = NULL, + num_iteration = NULL, + rawscore = FALSE, + predleaf = FALSE, + predcontrib = FALSE, + params = list()) { + + self$restore_handle() + ncols <- .Call(LGBM_BoosterGetNumFeature_R, private$handle) + + if (is.null(num_iteration)) { + num_iteration <- -1L + } + if (is.null(start_iteration)) { + start_iteration <- 0L + } + + if (!csr) { + fun <- LGBM_BoosterPredictForMatSingleRowFastInit_R + } else { + fun <- LGBM_BoosterPredictForCSRSingleRowFastInit_R + } + + fast_handle <- .Call( + fun + , private$handle + , ncols + , rawscore + , predleaf + , predcontrib + , start_iteration + , num_iteration + , .params2str(params = params) + ) + + private$fast_predict_config <- list( + handle = fast_handle + , csr = as.logical(csr) + , ncols = ncols + , start_iteration = start_iteration + , num_iteration = num_iteration + , rawscore = as.logical(rawscore) + , predleaf = as.logical(predleaf) + , predcontrib = as.logical(predcontrib) + , params = params + ) + + return(invisible(NULL)) + }, + + # Used for serialization + raw = NULL, + + # Store serialized raw bytes in model object + save_raw = function() { + if (is.null(self$raw)) { + self$raw <- self$save_model_to_string(NULL, as_char = FALSE) + } + return(invisible(NULL)) + + }, + + drop_raw = function() { + self$raw <- NULL + return(invisible(NULL)) + }, + + check_null_handle = function() { + return(.is_null_handle(private$handle)) + }, + + restore_handle = function() { + if (self$check_null_handle()) { + if (is.null(self$raw)) { + .Call(LGBM_NullBoosterHandleError_R) + } + private$handle <- .Call(LGBM_BoosterLoadModelFromString_R, self$raw) + } + return(invisible(NULL)) + }, + + get_handle = function() { + return(private$handle) + } + + ), + private = list( + handle = NULL, + train_set = NULL, + name_train_set = "training", + valid_sets = list(), + name_valid_sets = list(), + predict_buffer = list(), + is_predicted_cur_iter = list(), + num_class = 1L, + num_dataset = 0L, + init_predictor = NULL, + eval_names = NULL, + higher_better_inner_eval = NULL, + set_objective_to_none = FALSE, + train_set_version = 0L, + fast_predict_config = list(), + + # finalize() will free up the handles + finalize = function() { + .Call( + LGBM_BoosterFree_R + , private$handle + ) + private$handle <- NULL + return(invisible(NULL)) + }, + + # Predict data + inner_predict = function(idx) { + + # Store data name + data_name <- private$name_train_set + + if (idx > 1L) { + data_name <- private$name_valid_sets[[idx - 1L]] + } + + # Check for unknown dataset (over the maximum provided range) + if (idx > private$num_dataset) { + stop("data_idx should not be greater than num_dataset") + } + + # Check for prediction buffer + if (is.null(private$predict_buffer[[data_name]])) { + + # Store predictions + npred <- 0L + .Call( + LGBM_BoosterGetNumPredict_R + , private$handle + , as.integer(idx - 1L) + , npred + ) + private$predict_buffer[[data_name]] <- numeric(npred) + + } + + # Check if current iteration was already predicted + if (!private$is_predicted_cur_iter[[idx]]) { + + # Use buffer + .Call( + LGBM_BoosterGetPredict_R + , private$handle + , as.integer(idx - 1L) + , private$predict_buffer[[data_name]] + ) + private$is_predicted_cur_iter[[idx]] <- TRUE + } + + return(private$predict_buffer[[data_name]]) + }, + + # Get evaluation information + get_eval_info = function() { + + if (is.null(private$eval_names)) { + eval_names <- .Call( + LGBM_BoosterGetEvalNames_R + , private$handle + ) + + if (length(eval_names) > 0L) { + + # Parse and store privately names + private$eval_names <- eval_names + + # some metrics don't map cleanly to metric names, for example "ndcg@1" is just the + # ndcg metric evaluated at the first "query result" in learning-to-rank + metric_names <- gsub("@.*", "", eval_names) + private$higher_better_inner_eval <- .METRICS_HIGHER_BETTER()[metric_names] + + } + + } + + return(private$eval_names) + + }, + + get_loaded_param = function(handle) { + params_str <- .Call( + LGBM_BoosterGetLoadedParam_R + , handle + ) + params <- jsonlite::fromJSON(params_str) + if ("interaction_constraints" %in% names(params)) { + params[["interaction_constraints"]] <- lapply(params[["interaction_constraints"]], function(x) x + 1L) + } + + return(params) + + }, + + inner_eval = function(data_name, data_idx, feval = NULL) { + + # Check for unknown dataset (over the maximum provided range) + if (data_idx > private$num_dataset) { + stop("data_idx should not be greater than num_dataset") + } + + self$restore_handle() + + private$get_eval_info() + + ret <- list() + + if (length(private$eval_names) > 0L) { + + # Create evaluation values + tmp_vals <- numeric(length(private$eval_names)) + .Call( + LGBM_BoosterGetEval_R + , private$handle + , as.integer(data_idx - 1L) + , tmp_vals + ) + + for (i in seq_along(private$eval_names)) { + + # Store evaluation and append to return + res <- list() + res$data_name <- data_name + res$name <- private$eval_names[i] + res$value <- tmp_vals[i] + res$higher_better <- private$higher_better_inner_eval[i] + ret <- append(ret, list(res)) + + } + + } + + # Check if there are evaluation metrics + if (!is.null(feval)) { + + # Check if evaluation metric is a function + if (!is.function(feval)) { + stop("lgb.Booster.eval: feval should be a function") + } + + data <- private$train_set + + # Check if data to assess is existing differently + if (data_idx > 1L) { + data <- private$valid_sets[[data_idx - 1L]] + } + + # Perform function evaluation + res <- feval(private$inner_predict(data_idx), data) + + if (is.null(res$name) || is.null(res$value) || is.null(res$higher_better)) { + stop( + "lgb.Booster.eval: custom eval function should return a list with attribute (name, value, higher_better)" + ) + } + + # Append names and evaluation + res$data_name <- data_name + ret <- append(ret, list(res)) + } + + return(ret) + + } + + ) +) + +#' @name lgb_predict_shared_params +#' @title Shared prediction parameter docs +#' @param type Type of prediction to output. Allowed types are:\itemize{ +#' \item \code{"response"}: will output the predicted score according to the objective function being +#' optimized (depending on the link function that the objective uses), after applying any necessary +#' transformations - for example, for \code{objective="binary"}, it will output class probabilities. +#' \item \code{"class"}: for classification objectives, will output the class with the highest predicted +#' probability. For other objectives, will output the same as "response". Note that \code{"class"} is +#' not a supported type for \link{lgb.configure_fast_predict} (see the documentation of that function +#' for more details). +#' \item \code{"raw"}: will output the non-transformed numbers (sum of predictions from boosting iterations' +#' results) from which the "response" number is produced for a given objective function - for example, +#' for \code{objective="binary"}, this corresponds to log-odds. For many objectives such as +#' "regression", since no transformation is applied, the output will be the same as for "response". +#' \item \code{"leaf"}: will output the index of the terminal node / leaf at which each observations falls +#' in each tree in the model, outputted as integers, with one column per tree. +#' \item \code{"contrib"}: will return the per-feature contributions for each prediction, including an +#' intercept (each feature will produce one column). +#' } +#' +#' Note that, if using custom objectives, types "class" and "response" will not be available and will +#' default towards using "raw" instead. +#' +#' If the model was fit through function \link{lightgbm} and it was passed a factor as labels, +#' passing the prediction type through \code{params} instead of through this argument might +#' result in factor levels for classification objectives not being applied correctly to the +#' resulting output. +#' +#' \emph{New in version 4.0.0} +#' +#' @param start_iteration int or None, optional (default=None) +#' Start index of the iteration to predict. +#' If None or <= 0, starts from the first iteration. +#' @param num_iteration int or None, optional (default=None) +#' Limit number of iterations in the prediction. +#' If None, if the best iteration exists and start_iteration is None or <= 0, the +#' best iteration is used; otherwise, all iterations from start_iteration are used. +#' If <= 0, all iterations from start_iteration are used (no limits). +#' @param params a list of additional named parameters. See +#' \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#predict-parameters}{ +#' the "Predict Parameters" section of the documentation} for a list of parameters and +#' valid values. Where these conflict with the values of keyword arguments to this function, +#' the values in \code{params} take precedence. +#' @details This page contains shared documentation for prediction-related parameters used throughout the package. +#' @keywords internal +NULL + +#' @name predict.lgb.Booster +#' @title Predict method for LightGBM model +#' @description Predicted values based on class \code{lgb.Booster} +#' +#' \emph{New in version 4.0.0} +#' +#' @details If the model object has been configured for fast single-row predictions through +#' \link{lgb.configure_fast_predict}, this function will use the prediction parameters +#' that were configured for it - as such, extra prediction parameters should not be passed +#' here, otherwise the configuration will be ignored and the slow route will be taken. +#' @inheritParams lgb_predict_shared_params +#' @param object Object of class \code{lgb.Booster} +#' @param newdata a \code{matrix} object, a \code{dgCMatrix}, a \code{dgRMatrix} object, a \code{dsparseVector} object, +#' or a character representing a path to a text file (CSV, TSV, or LibSVM). +#' +#' For sparse inputs, if predictions are only going to be made for a single row, it will be faster to +#' use CSR format, in which case the data may be passed as either a single-row CSR matrix (class +#' \code{dgRMatrix} from package \code{Matrix}) or as a sparse numeric vector (class +#' \code{dsparseVector} from package \code{Matrix}). +#' +#' If single-row predictions are going to be performed frequently, it is recommended to +#' pre-configure the model object for fast single-row sparse predictions through function +#' \link{lgb.configure_fast_predict}. +#' +#' \emph{Changed from 'data', in version 4.0.0} +#' +#' @param header only used for prediction for text file. True if text file has header +#' @param ... ignored +#' @return For prediction types that are meant to always return one output per observation (e.g. when predicting +#' \code{type="response"} or \code{type="raw"} on a binary classification or regression objective), will +#' return a vector with one element per row in \code{newdata}. +#' +#' For prediction types that are meant to return more than one output per observation (e.g. when predicting +#' \code{type="response"} or \code{type="raw"} on a multi-class objective, or when predicting +#' \code{type="leaf"}, regardless of objective), will return a matrix with one row per observation in +#' \code{newdata} and one column per output. +#' +#' For \code{type="leaf"} predictions, will return a matrix with one row per observation in \code{newdata} +#' and one column per tree. Note that for multiclass objectives, LightGBM trains one tree per class at each +#' boosting iteration. That means that, for example, for a multiclass model with 3 classes, the leaf +#' predictions for the first class can be found in columns 1, 4, 7, 10, etc. +#' +#' For \code{type="contrib"}, will return a matrix of SHAP values with one row per observation in +#' \code{newdata} and columns corresponding to features. For regression, ranking, cross-entropy, and binary +#' classification objectives, this matrix contains one column per feature plus a final column containing the +#' Shapley base value. For multiclass objectives, this matrix will represent \code{num_classes} such matrices, +#' in the order "feature contributions for first class, feature contributions for second class, feature +#' contributions for third class, etc.". +#' +#' If the model was fit through function \link{lightgbm} and it was passed a factor as labels, predictions +#' returned from this function will retain the factor levels (either as values for \code{type="class"}, or +#' as column names for \code{type="response"} and \code{type="raw"} for multi-class objectives). Note that +#' passing the requested prediction type under \code{params} instead of through \code{type} might result in +#' the factor levels not being present in the output. +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +#' params <- list( +#' objective = "regression" +#' , metric = "l2" +#' , min_data = 1L +#' , learning_rate = 1.0 +#' , num_threads = 2L +#' ) +#' valids <- list(test = dtest) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 5L +#' , valids = valids +#' ) +#' preds <- predict(model, test$data) +#' +#' # pass other prediction parameters +#' preds <- predict( +#' model, +#' test$data, +#' params = list( +#' predict_disable_shape_check = TRUE +#' ) +#' ) +#' } +#' @importFrom utils modifyList +#' @export +predict.lgb.Booster <- function(object, + newdata, + type = "response", + start_iteration = NULL, + num_iteration = NULL, + header = FALSE, + params = list(), + ...) { + + if (!.is_Booster(x = object)) { + stop("predict.lgb.Booster: object should be an ", sQuote("lgb.Booster", q = FALSE)) + } + + additional_params <- list(...) + if (length(additional_params) > 0L) { + additional_params_names <- names(additional_params) + if ("reshape" %in% additional_params_names) { + stop("'reshape' argument is no longer supported.") + } + + old_args_for_type <- list( + "rawscore" = "raw" + , "predleaf" = "leaf" + , "predcontrib" = "contrib" + ) + for (arg in names(old_args_for_type)) { + if (arg %in% additional_params_names) { + stop(sprintf("Argument '%s' is no longer supported. Use type='%s' instead." + , arg + , old_args_for_type[[arg]])) + } + } + + warning(paste0( + "predict.lgb.Booster: Found the following passed through '...': " + , toString(names(additional_params)) + , ". These are ignored. Use argument 'params' instead." + )) + } + + if (!is.null(object$params$objective) && object$params$objective == "none" && type %in% c("class", "response")) { + warning("Prediction types 'class' and 'response' are not supported for custom objectives.") + type <- "raw" + } + + rawscore <- FALSE + predleaf <- FALSE + predcontrib <- FALSE + if (type == "raw") { + rawscore <- TRUE + } else if (type == "leaf") { + predleaf <- TRUE + } else if (type == "contrib") { + predcontrib <- TRUE + } + + pred <- object$predict( + data = newdata + , start_iteration = start_iteration + , num_iteration = num_iteration + , rawscore = rawscore + , predleaf = predleaf + , predcontrib = predcontrib + , header = header + , params = params + ) + if (type == "class") { + if (object$params$objective %in% .BINARY_OBJECTIVES()) { + pred <- as.integer(pred >= 0.5) + } else if (object$params$objective %in% .MULTICLASS_OBJECTIVES()) { + pred <- max.col(pred) - 1L + } + } + if (!is.null(object$data_processor)) { + pred <- object$data_processor$process_predictions( + pred = pred + , type = type + ) + } + return(pred) +} + +#' @title Configure Fast Single-Row Predictions +#' @description Pre-configures a LightGBM model object to produce fast single-row predictions +#' for a given input data type, prediction type, and parameters. +#' @details Calling this function multiple times with different parameters might not override +#' the previous configuration and might trigger undefined behavior. +#' +#' Any saved configuration for fast predictions might be lost after making a single-row +#' prediction of a different type than what was configured (except for types "response" and +#' "class", which can be switched between each other at any time without losing the configuration). +#' +#' In some situations, setting a fast prediction configuration for one type of prediction +#' might cause the prediction function to keep using that configuration for single-row +#' predictions even if the requested type of prediction is different from what was configured. +#' +#' Note that this function will not accept argument \code{type="class"} - for such cases, one +#' can pass \code{type="response"} to this function and then \code{type="class"} to the +#' \code{predict} function - the fast configuration will not be lost or altered if the switch +#' is between "response" and "class". +#' +#' The configuration does not survive de-serializations, so it has to be generated +#' anew in every R process that is going to use it (e.g. if loading a model object +#' through \code{readRDS}, whatever configuration was there previously will be lost). +#' +#' Requesting a different prediction type or passing parameters to \link{predict.lgb.Booster} +#' will cause it to ignore the fast-predict configuration and take the slow route instead +#' (but be aware that an existing configuration might not always be overridden by supplying +#' different parameters or prediction type, so make sure to check that the output is what +#' was expected when a prediction is to be made on a single row for something different than +#' what is configured). +#' +#' Note that, if configuring a non-default prediction type (such as leaf indices), +#' then that type must also be passed in the call to \link{predict.lgb.Booster} in +#' order for it to use the configuration. This also applies for \code{start_iteration} +#' and \code{num_iteration}, but \bold{the \code{params} list must be empty} in the call to \code{predict}. +#' +#' Predictions about feature contributions do not allow a fast route for CSR inputs, +#' and as such, this function will produce an error if passing \code{csr=TRUE} and +#' \code{type = "contrib"} together. +#' @inheritParams lgb_predict_shared_params +#' @param model LightGBM model object (class \code{lgb.Booster}). +#' +#' \bold{The object will be modified in-place}. +#' @param csr Whether the prediction function is going to be called on sparse CSR inputs. +#' If \code{FALSE}, will be assumed that predictions are going to be called on single-row +#' regular R matrices. +#' @return The same \code{model} that was passed as input, invisibly, with the desired +#' configuration stored inside it and available to be used in future calls to +#' \link{predict.lgb.Booster}. +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' library(lightgbm) +#' data(mtcars) +#' X <- as.matrix(mtcars[, -1L]) +#' y <- mtcars[, 1L] +#' dtrain <- lgb.Dataset(X, label = y, params = list(max_bin = 5L)) +#' params <- list( +#' min_data_in_leaf = 2L +#' , num_threads = 2L +#' ) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , obj = "regression" +#' , nrounds = 5L +#' , verbose = -1L +#' ) +#' lgb.configure_fast_predict(model) +#' +#' x_single <- X[11L, , drop = FALSE] +#' predict(model, x_single) +#' +#' # Will not use it if the prediction to be made +#' # is different from what was configured +#' predict(model, x_single, type = "leaf") +#' } +#' @export +lgb.configure_fast_predict <- function(model, + csr = FALSE, + start_iteration = NULL, + num_iteration = NULL, + type = "response", + params = list()) { + if (!.is_Booster(x = model)) { + stop("lgb.configure_fast_predict: model should be an ", sQuote("lgb.Booster", q = FALSE)) + } + if (type == "class") { + stop("type='class' is not supported for 'lgb.configure_fast_predict'. Use 'response' instead.") + } + + rawscore <- FALSE + predleaf <- FALSE + predcontrib <- FALSE + if (type == "raw") { + rawscore <- TRUE + } else if (type == "leaf") { + predleaf <- TRUE + } else if (type == "contrib") { + predcontrib <- TRUE + } + + if (csr && predcontrib) { + stop("'lgb.configure_fast_predict' does not support feature contributions for CSR data.") + } + model$configure_fast_predict( + csr = csr + , start_iteration = start_iteration + , num_iteration = num_iteration + , rawscore = rawscore + , predleaf = predleaf + , predcontrib = predcontrib + , params = params + ) + return(invisible(model)) +} + +#' @name print.lgb.Booster +#' @title Print method for LightGBM model +#' @description Show summary information about a LightGBM model object (same as \code{summary}). +#' +#' \emph{New in version 4.0.0} +#' +#' @param x Object of class \code{lgb.Booster} +#' @param ... Not used +#' @return The same input \code{x}, returned as invisible. +#' @export +print.lgb.Booster <- function(x, ...) { + # nolint start + handle <- x$.__enclos_env__$private$handle + handle_is_null <- .is_null_handle(handle) + + if (!handle_is_null) { + ntrees <- x$current_iter() + if (ntrees == 1L) { + cat("LightGBM Model (1 tree)\n") + } else { + cat(sprintf("LightGBM Model (%d trees)\n", ntrees)) + } + } else { + cat("LightGBM Model\n") + } + + if (!handle_is_null) { + obj <- x$params$objective + if (is.null(obj)) { + obj <- "(default)" + } + if (obj == "none") { + obj <- "custom" + } + num_class <- x$.__enclos_env__$private$num_class + if (num_class == 1L) { + cat(sprintf("Objective: %s\n", obj)) + } else { + cat(sprintf("Objective: %s (%d classes)\n" + , obj + , num_class)) + } + } else { + cat("(Booster handle is invalid)\n") + } + + if (!handle_is_null) { + ncols <- .Call(LGBM_BoosterGetNumFeature_R, handle) + cat(sprintf("Fitted to dataset with %d columns\n", ncols)) + } + # nolint end + + return(invisible(x)) +} + +#' @name summary.lgb.Booster +#' @title Summary method for LightGBM model +#' @description Show summary information about a LightGBM model object (same as \code{print}). +#' +#' \emph{New in version 4.0.0} +#' +#' @param object Object of class \code{lgb.Booster} +#' @param ... Not used +#' @return The same input \code{object}, returned as invisible. +#' @export +summary.lgb.Booster <- function(object, ...) { + print(object) +} + +#' @name lgb.load +#' @title Load LightGBM model +#' @description Load LightGBM takes in either a file path or model string. +#' If both are provided, Load will default to loading from file +#' @param filename path of model file +#' @param model_str a str containing the model (as a \code{character} or \code{raw} vector) +#' +#' @return lgb.Booster +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +#' params <- list( +#' objective = "regression" +#' , metric = "l2" +#' , min_data = 1L +#' , learning_rate = 1.0 +#' , num_threads = 2L +#' ) +#' valids <- list(test = dtest) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 5L +#' , valids = valids +#' , early_stopping_rounds = 3L +#' ) +#' model_file <- tempfile(fileext = ".txt") +#' lgb.save(model, model_file) +#' load_booster <- lgb.load(filename = model_file) +#' model_string <- model$save_model_to_string(NULL) # saves best iteration +#' load_booster_from_str <- lgb.load(model_str = model_string) +#' } +#' @export +lgb.load <- function(filename = NULL, model_str = NULL) { + + filename_provided <- !is.null(filename) + model_str_provided <- !is.null(model_str) + + if (filename_provided) { + if (!is.character(filename)) { + stop("lgb.load: filename should be character") + } + filename <- path.expand(filename) + if (!file.exists(filename)) { + stop(sprintf("lgb.load: file '%s' passed to filename does not exist", filename)) + } + return(invisible(Booster$new(modelfile = filename))) + } + + if (model_str_provided) { + if (!is.raw(model_str) && !is.character(model_str)) { + stop("lgb.load: model_str should be a character/raw vector") + } + return(invisible(Booster$new(model_str = model_str))) + } + + stop("lgb.load: either filename or model_str must be given") +} + +#' @name lgb.save +#' @title Save LightGBM model +#' @description Save LightGBM model +#' @param booster Object of class \code{lgb.Booster} +#' @param filename Saved filename +#' @param num_iteration Number of iterations to save, NULL or <= 0 means use best iteration +#' @param start_iteration Index (1-based) of the first boosting round to save. +#' For example, passing \code{start_iteration=5, num_iteration=3} for a regression model +#' means "save the fifth, sixth, and seventh tree" +#' +#' \emph{New in version 4.4.0} +#' +#' @return lgb.Booster +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' library(lightgbm) +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +#' params <- list( +#' objective = "regression" +#' , metric = "l2" +#' , min_data = 1L +#' , learning_rate = 1.0 +#' , num_threads = 2L +#' ) +#' valids <- list(test = dtest) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 10L +#' , valids = valids +#' , early_stopping_rounds = 5L +#' ) +#' lgb.save(model, tempfile(fileext = ".txt")) +#' } +#' @export +lgb.save <- function( + booster, filename, num_iteration = NULL, start_iteration = 1L + ) { + + if (!.is_Booster(x = booster)) { + stop("lgb.save: booster should be an ", sQuote("lgb.Booster", q = FALSE)) + } + + if (!(is.character(filename) && length(filename) == 1L)) { + stop("lgb.save: filename should be a string") + } + filename <- path.expand(filename) + + # Store booster + return( + invisible(booster$save_model( + filename = filename + , num_iteration = num_iteration + , start_iteration = start_iteration + )) + ) + +} + +#' @name lgb.dump +#' @title Dump LightGBM model to json +#' @description Dump LightGBM model to json +#' @param booster Object of class \code{lgb.Booster} +#' @param num_iteration Number of iterations to be dumped. NULL or <= 0 means use best iteration +#' @param start_iteration Index (1-based) of the first boosting round to dump. +#' For example, passing \code{start_iteration=5, num_iteration=3} for a regression model +#' means "dump the fifth, sixth, and seventh tree" +#' +#' \emph{New in version 4.4.0} +#' +#' @return json format of model +#' +#' @examples +#' \donttest{ +#' library(lightgbm) +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +#' params <- list( +#' objective = "regression" +#' , metric = "l2" +#' , min_data = 1L +#' , learning_rate = 1.0 +#' , num_threads = 2L +#' ) +#' valids <- list(test = dtest) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 10L +#' , valids = valids +#' , early_stopping_rounds = 5L +#' ) +#' json_model <- lgb.dump(model) +#' } +#' @export +lgb.dump <- function(booster, num_iteration = NULL, start_iteration = 1L) { + + if (!.is_Booster(x = booster)) { + stop("lgb.dump: booster should be an ", sQuote("lgb.Booster", q = FALSE)) + } + + # Return booster at requested iteration + return( + booster$dump_model( + num_iteration = num_iteration, start_iteration = start_iteration + ) + ) + +} + +#' @name lgb.get.eval.result +#' @title Get record evaluation result from booster +#' @description Given a \code{lgb.Booster}, return evaluation results for a +#' particular metric on a particular dataset. +#' @param booster Object of class \code{lgb.Booster} +#' @param data_name Name of the dataset to return evaluation results for. +#' @param eval_name Name of the evaluation metric to return results for. +#' @param iters An integer vector of iterations you want to get evaluation results for. If NULL +#' (the default), evaluation results for all iterations will be returned. +#' @param is_err TRUE will return evaluation error instead +#' +#' @return numeric vector of evaluation result +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' # train a regression model +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +#' params <- list( +#' objective = "regression" +#' , metric = "l2" +#' , min_data = 1L +#' , learning_rate = 1.0 +#' , num_threads = 2L +#' ) +#' valids <- list(test = dtest) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 5L +#' , valids = valids +#' ) +#' +#' # Examine valid data_name values +#' print(setdiff(names(model$record_evals), "start_iter")) +#' +#' # Examine valid eval_name values for dataset "test" +#' print(names(model$record_evals[["test"]])) +#' +#' # Get L2 values for "test" dataset +#' lgb.get.eval.result(model, "test", "l2") +#' } +#' @export +lgb.get.eval.result <- function(booster, data_name, eval_name, iters = NULL, is_err = FALSE) { + + if (!.is_Booster(x = booster)) { + stop("lgb.get.eval.result: Can only use ", sQuote("lgb.Booster", q = FALSE), " to get eval result") + } + + if (!is.character(data_name) || !is.character(eval_name)) { + stop("lgb.get.eval.result: data_name and eval_name should be characters") + } + + # NOTE: "start_iter" exists in booster$record_evals but is not a valid data_name + data_names <- setdiff(names(booster$record_evals), "start_iter") + if (!(data_name %in% data_names)) { + stop(paste0( + "lgb.get.eval.result: data_name " + , shQuote(data_name) + , " not found. Only the following datasets exist in record evals: [" + , toString(data_names) + , "]" + )) + } + + # Check if evaluation result is existing + eval_names <- names(booster$record_evals[[data_name]]) + if (!(eval_name %in% eval_names)) { + stop(paste0( + "lgb.get.eval.result: eval_name " + , shQuote(eval_name) + , " not found. Only the following eval_names exist for dataset " + , shQuote(data_name) + , ": [" + , toString(eval_names) + , "]" + )) + } + + result <- booster$record_evals[[data_name]][[eval_name]][[.EVAL_KEY()]] + + # Check if error is requested + if (is_err) { + result <- booster$record_evals[[data_name]][[eval_name]][[.EVAL_ERR_KEY()]] + } + + if (is.null(iters)) { + return(as.numeric(result)) + } + + # Parse iteration and booster delta + iters <- as.integer(iters) + delta <- booster$record_evals$start_iter - 1.0 + iters <- iters - delta + + return(as.numeric(result[iters])) +} diff --git a/R-package/R/lgb.DataProcessor.R b/R-package/R/lgb.DataProcessor.R new file mode 100644 index 0000000..c35ce4f --- /dev/null +++ b/R-package/R/lgb.DataProcessor.R @@ -0,0 +1,94 @@ +DataProcessor <- R6::R6Class( + classname = "lgb.DataProcessor", + public = list( + factor_levels = NULL, + + process_label = function(label, objective, params) { + + if (is.character(label)) { + label <- factor(label) + } + + if (is.factor(label)) { + + self$factor_levels <- levels(label) + if (length(self$factor_levels) <= 1L) { + stop("Labels to predict is a factor with <2 possible values.") + } + + label <- as.numeric(label) - 1.0 + out <- list(label = label) + if (length(self$factor_levels) == 2L) { + if (objective == "auto") { + objective <- "binary" + } + if (!(objective %in% .BINARY_OBJECTIVES())) { + stop("Two-level factors as labels only allowed for objective='binary' or objective='auto'.") + } + } else { + if (objective == "auto") { + objective <- "multiclass" + } + if (!(objective %in% .MULTICLASS_OBJECTIVES())) { + stop( + sprintf( + "Factors with >2 levels as labels only allowed for multi-class objectives. Got: %s (allowed: %s)" + , objective + , toString(.MULTICLASS_OBJECTIVES()) + ) + ) + } + data_num_class <- length(self$factor_levels) + params <- .check_wrapper_param( + main_param_name = "num_class" + , params = params + , alternative_kwarg_value = data_num_class + ) + if (params[["num_class"]] != data_num_class) { + warning( + sprintf( + "Found num_class=%d in params, but 'label' is a factor with %d levels. 'num_class' will be ignored." + , params[["num_class"]] + , data_num_class + ) + ) + params$num_class <- data_num_class + } + } + out$objective <- objective + out$params <- params + return(out) + + } else { + + label <- as.numeric(label) + if (objective == "auto") { + objective <- "regression" + } + out <- list( + label = label + , objective = objective + , params = params + ) + return(out) + + } + }, + + process_predictions = function(pred, type) { + if (NROW(self$factor_levels)) { + if (type == "class") { + pred <- as.integer(pred) + 1L + attributes(pred)$levels <- self$factor_levels + attributes(pred)$class <- "factor" + } else if (type %in% c("response", "raw")) { + if (is.matrix(pred) && ncol(pred) == length(self$factor_levels)) { + colnames(pred) <- self$factor_levels + } + } + } + + return(pred) + } + ) +) diff --git a/R-package/R/lgb.Dataset.R b/R-package/R/lgb.Dataset.R new file mode 100644 index 0000000..af8aec9 --- /dev/null +++ b/R-package/R/lgb.Dataset.R @@ -0,0 +1,1290 @@ +#' @name lgb_shared_dataset_params +#' @title Shared Dataset parameter docs +#' @description Parameter docs for fields used in \code{lgb.Dataset} construction +#' @param label vector of labels to use as the target variable +#' @param weight numeric vector of sample weights +#' @param init_score initial score is the base prediction lightgbm will boost from +#' @param group used for learning-to-rank tasks. An integer vector describing how to +#' group rows together as ordered results from the same set of candidate results +#' to be ranked. For example, if you have a 100-document dataset with +#' \code{group = c(10, 20, 40, 10, 10, 10)}, that means that you have 6 groups, +#' where the first 10 records are in the first group, records 11-30 are in the +#' second group, etc. +#' @details This page contains shared documentation for dataset-related parameters used throughout the package. +#' @keywords internal +NULL + +# [description] List of valid keys for "info" arguments in lgb.Dataset. +# Wrapped in a function to take advantage of lazy evaluation +# (so it doesn't matter what order R sources files during installation). +# [return] A character vector of names. +.INFO_KEYS <- function() { + return(c("label", "weight", "init_score", "group")) +} + +#' @importFrom methods is +#' @importFrom R6 R6Class +#' @importFrom utils modifyList +Dataset <- R6::R6Class( + + classname = "lgb.Dataset", + cloneable = FALSE, + public = list( + + # Initialize will create a starter dataset + initialize = function(data, + params = list(), + reference = NULL, + colnames = NULL, + categorical_feature = NULL, + predictor = NULL, + free_raw_data = TRUE, + used_indices = NULL, + label = NULL, + weight = NULL, + group = NULL, + init_score = NULL) { + + # validate inputs early to avoid unnecessary computation + if (!(is.null(reference) || .is_Dataset(reference))) { + stop("lgb.Dataset: If provided, reference must be a ", sQuote("lgb.Dataset", q = FALSE)) + } + if (!(is.null(predictor) || .is_Predictor(predictor))) { + stop("lgb.Dataset: If provided, predictor must be a ", sQuote("lgb.Predictor", q = FALSE)) + } + + info <- list() + if (!is.null(label)) { + info[["label"]] <- label + } + if (!is.null(weight)) { + info[["weight"]] <- weight + } + if (!is.null(group)) { + info[["group"]] <- group + } + if (!is.null(init_score)) { + info[["init_score"]] <- init_score + } + + # Check for matrix format + if (is.matrix(data)) { + # Check whether matrix is the correct type first ("double") + if (storage.mode(data) != "double") { + storage.mode(data) <- "double" + } + } + + # Setup private attributes + private$raw_data <- data + private$params <- params + private$reference <- reference + private$colnames <- colnames + + private$categorical_feature <- categorical_feature + private$predictor <- predictor + private$free_raw_data <- free_raw_data + private$used_indices <- sort(used_indices, decreasing = FALSE) + private$info <- info + private$version <- 0L + + return(invisible(NULL)) + + }, + + create_valid = function(data, + label = NULL, + weight = NULL, + group = NULL, + init_score = NULL, + params = list()) { + + # the Dataset's existing parameters should be overwritten by any passed in to this call + params <- modifyList(private$params, params) + + # Create new dataset + ret <- Dataset$new( + data = data + , params = params + , reference = self + , colnames = private$colnames + , categorical_feature = private$categorical_feature + , predictor = private$predictor + , free_raw_data = private$free_raw_data + , used_indices = NULL + , label = label + , weight = weight + , group = group + , init_score = init_score + ) + + return(invisible(ret)) + + }, + + # Dataset constructor + construct = function() { + + # Check for handle null + if (!.is_null_handle(x = private$handle)) { + return(invisible(self)) + } + + # Get feature names + cnames <- NULL + if (is.matrix(private$raw_data) || methods::is(private$raw_data, "dgCMatrix")) { + cnames <- colnames(private$raw_data) + } + + # set feature names if they do not exist + if (is.null(private$colnames) && !is.null(cnames)) { + private$colnames <- as.character(cnames) + } + + # Get categorical feature index + if (!is.null(private$categorical_feature)) { + + # Check for character name + if (is.character(private$categorical_feature)) { + + cate_indices <- as.list(match(private$categorical_feature, private$colnames) - 1L) + + # Provided indices, but some indices are missing? + if (anyNA(cate_indices)) { + stop( + "lgb.Dataset.construct: supplied an unknown feature in categorical_feature: " + , sQuote(private$categorical_feature[is.na(cate_indices)], q = FALSE) + ) + } + + } else { + + # Check if more categorical features were output over the feature space + data_is_not_filename <- !is.character(private$raw_data) + if ( + data_is_not_filename + && !is.null(private$raw_data) + && is.null(private$used_indices) + && max(private$categorical_feature) > ncol(private$raw_data) + ) { + stop( + "lgb.Dataset.construct: supplied a too large value in categorical_feature: " + , max(private$categorical_feature) + , " but only " + , ncol(private$raw_data) + , " features" + ) + } + + # Store indices as [0, n-1] indexed instead of [1, n] indexed + cate_indices <- as.list(private$categorical_feature - 1L) + + } + + # Store indices for categorical features + private$params$categorical_feature <- cate_indices + + } + + # Generate parameter str + params_str <- .params2str(params = private$params) + + # Get handle of reference dataset + ref_handle <- NULL + if (!is.null(private$reference)) { + ref_handle <- private$reference$.__enclos_env__$private$get_handle() + } + + # not subsetting, constructing from raw data + if (is.null(private$used_indices)) { + + if (is.null(private$raw_data)) { + stop(paste0( + "Attempting to create a Dataset without any raw data. " + , "This can happen if the Dataset's finalizer was called or if this Dataset was saved with saveRDS(). " + , "To avoid this error in the future, use lgb.Dataset.save() or " + , "Dataset$save_binary() to save lightgbm Datasets." + )) + } + + # Are we using a data file? + if (is.character(private$raw_data)) { + + handle <- .Call( + LGBM_DatasetCreateFromFile_R + , path.expand(private$raw_data) + , params_str + , ref_handle + ) + + } else if (is.matrix(private$raw_data)) { + + # Are we using a matrix? + handle <- .Call( + LGBM_DatasetCreateFromMat_R + , private$raw_data + , nrow(private$raw_data) + , ncol(private$raw_data) + , params_str + , ref_handle + ) + + } else if (methods::is(private$raw_data, "dgCMatrix")) { + if (length(private$raw_data@p) > 2147483647L) { + stop("Cannot support large CSC matrix") + } + # Are we using a dgCMatrix (sparse matrix column compressed) + handle <- .Call( + LGBM_DatasetCreateFromCSC_R + , private$raw_data@p + , private$raw_data@i + , private$raw_data@x + , length(private$raw_data@p) + , length(private$raw_data@x) + , nrow(private$raw_data) + , params_str + , ref_handle + ) + + } else { + + # Unknown data type + stop( + "lgb.Dataset.construct: does not support constructing from " + , sQuote(class(private$raw_data), q = FALSE) + ) + + } + + } else { + + # Reference is empty + if (is.null(private$reference)) { + stop("lgb.Dataset.construct: reference cannot be NULL for constructing data subset") + } + + # Construct subset + handle <- .Call( + LGBM_DatasetGetSubset_R + , ref_handle + , c(private$used_indices) + , length(private$used_indices) + , params_str + ) + + } + if (.is_null_handle(x = handle)) { + stop("lgb.Dataset.construct: cannot create Dataset handle") + } + # Setup class and private type + class(handle) <- "lgb.Dataset.handle" + private$handle <- handle + + # Set feature names + if (!is.null(private$colnames)) { + self$set_colnames(colnames = private$colnames) + } + + # Ensure that private$colnames matches the feature names on the C++ side. This line is necessary + # in cases like constructing from a file or from a matrix with no column names. + private$colnames <- .Call( + LGBM_DatasetGetFeatureNames_R + , private$handle + ) + + # Load init score if requested + if (!is.null(private$predictor) && is.null(private$used_indices)) { + + # Setup initial scores + init_score <- private$predictor$predict( + data = private$raw_data + , rawscore = TRUE + ) + + # Not needed to transpose, for is col_marjor + init_score <- as.vector(init_score) + private$info$init_score <- init_score + + } + + # Should we free raw data? + if (isTRUE(private$free_raw_data)) { + private$raw_data <- NULL + } + + # Get private information + if (length(private$info) > 0L) { + + # Set infos + for (i in seq_along(private$info)) { + + p <- private$info[i] + self$set_field( + field_name = names(p) + , data = p[[1L]] + ) + + } + + } + + # Get label information existence + if (is.null(self$get_field(field_name = "label"))) { + stop("lgb.Dataset.construct: label should be set") + } + + return(invisible(self)) + + }, + + # Dimension function + dim = function() { + + # Check for handle + if (!.is_null_handle(x = private$handle)) { + + num_row <- 0L + num_col <- 0L + + # Get numeric data and numeric features + .Call( + LGBM_DatasetGetNumData_R + , private$handle + , num_row + ) + .Call( + LGBM_DatasetGetNumFeature_R + , private$handle + , num_col + ) + return( + c(num_row, num_col) + ) + + } else if (is.matrix(private$raw_data) || methods::is(private$raw_data, "dgCMatrix")) { + + # Check if dgCMatrix (sparse matrix column compressed) + # NOTE: requires Matrix package + return(dim(private$raw_data)) + + } else { + + # Trying to work with unknown dimensions is not possible + stop( + "dim: cannot get dimensions before dataset has been constructed, " + , "please call lgb.Dataset.construct explicitly" + ) + + } + + }, + + # Get number of bins for feature + get_feature_num_bin = function(feature) { + if (.is_null_handle(x = private$handle)) { + stop("Cannot get number of bins in feature before constructing Dataset.") + } + if (is.character(feature)) { + feature_name <- feature + feature <- which(private$colnames == feature_name) + if (length(feature) == 0L) { + stop(sprintf("feature '%s' not found", feature_name)) + } + } + num_bin <- integer(1L) + .Call( + LGBM_DatasetGetFeatureNumBin_R + , private$handle + , feature - 1L + , num_bin + ) + return(num_bin) + }, + + # Get column names + get_colnames = function() { + + # Check for handle + if (!.is_null_handle(x = private$handle)) { + private$colnames <- .Call( + LGBM_DatasetGetFeatureNames_R + , private$handle + ) + return(private$colnames) + + } else if (is.matrix(private$raw_data) || methods::is(private$raw_data, "dgCMatrix")) { + + # Check if dgCMatrix (sparse matrix column compressed) + return(colnames(private$raw_data)) + + } else { + + # Trying to work with unknown formats is not possible + stop( + "Dataset$get_colnames(): cannot get column names before dataset has been constructed, please call " + , "lgb.Dataset.construct() explicitly" + ) + + } + + }, + + # Set column names + set_colnames = function(colnames) { + + # Check column names non-existence + if (is.null(colnames)) { + return(invisible(self)) + } + + # Check empty column names + colnames <- as.character(colnames) + if (length(colnames) == 0L) { + return(invisible(self)) + } + + # Write column names + private$colnames <- colnames + if (!.is_null_handle(x = private$handle)) { + + # Merge names with tab separation + merged_name <- paste(as.list(private$colnames), collapse = "\t") + .Call( + LGBM_DatasetSetFeatureNames_R + , private$handle + , merged_name + ) + + } + + return(invisible(self)) + + }, + + get_field = function(field_name) { + + # Check if attribute key is in the known attribute list + if (!is.character(field_name) || length(field_name) != 1L || !field_name %in% .INFO_KEYS()) { + stop( + "Dataset$get_field(): field_name must be one of the following: " + , toString(sQuote(.INFO_KEYS(), q = FALSE)) + ) + } + + # Check for info name and handle + if (is.null(private$info[[field_name]])) { + + if (.is_null_handle(x = private$handle)) { + stop("Cannot perform Dataset$get_field() before constructing Dataset.") + } + + # Get field size of info + info_len <- 0L + .Call( + LGBM_DatasetGetFieldSize_R + , private$handle + , field_name + , info_len + ) + + if (info_len > 0L) { + + # Get back fields + if (field_name == "group") { + ret <- integer(info_len) + } else { + ret <- numeric(info_len) + } + + .Call( + LGBM_DatasetGetField_R + , private$handle + , field_name + , ret + ) + + private$info[[field_name]] <- ret + + } + } + + return(private$info[[field_name]]) + + }, + + set_field = function(field_name, data) { + + # Check if attribute key is in the known attribute list + if (!is.character(field_name) || length(field_name) != 1L || !field_name %in% .INFO_KEYS()) { + stop( + "Dataset$set_field(): field_name must be one of the following: " + , toString(sQuote(.INFO_KEYS(), q = FALSE)) + ) + } + + # Check for type of information + data <- if (field_name == "group") { + as.integer(data) + } else { + as.numeric(data) + } + + # Store information privately + private$info[[field_name]] <- data + + if (!.is_null_handle(x = private$handle) && !is.null(data)) { + + if (length(data) > 0L) { + + .Call( + LGBM_DatasetSetField_R + , private$handle + , field_name + , data + , length(data) + ) + + private$version <- private$version + 1L + + } + + } + + return(invisible(self)) + + }, + + slice = function(idxset) { + + return( + Dataset$new( + data = NULL + , params = private$params + , reference = self + , colnames = private$colnames + , categorical_feature = private$categorical_feature + , predictor = private$predictor + , free_raw_data = private$free_raw_data + , used_indices = sort(idxset, decreasing = FALSE) + ) + ) + + }, + + # [description] Update Dataset parameters. If it has not been constructed yet, + # this operation just happens on the R side (updating private$params). + # If it has been constructed, parameters will be updated on the C++ side. + update_params = function(params) { + if (length(params) == 0L) { + return(invisible(self)) + } + new_params <- utils::modifyList(private$params, params) + if (.is_null_handle(x = private$handle)) { + private$params <- new_params + } else { + tryCatch({ + .Call( + LGBM_DatasetUpdateParamChecking_R + , .params2str(params = private$params) + , .params2str(params = new_params) + ) + private$params <- new_params + }, error = function(e) { + # If updating failed but raw data is not available, raise an error because + # achieving what the user asked for is not possible + if (is.null(private$raw_data)) { + stop(e) + } + + # If updating failed but raw data is available, modify the params + # on the R side and re-set ("deconstruct") the Dataset + private$params <- new_params + private$finalize() + }) + } + return(invisible(self)) + + }, + + # [description] Get only Dataset-specific parameters. This is primarily used by + # Booster to update its parameters based on the characteristics of + # a Dataset. It should not be used by other methods in this class, + # since "verbose" is not a Dataset parameter and needs to be passed + # through to avoid globally re-setting verbosity. + get_params = function() { + dataset_params <- unname(unlist(.DATASET_PARAMETERS())) + ret <- list() + for (param_key in names(private$params)) { + if (param_key %in% dataset_params) { + ret[[param_key]] <- private$params[[param_key]] + } + } + return(ret) + }, + + # Set categorical feature parameter + set_categorical_feature = function(categorical_feature) { + + # Check for identical input + if (identical(private$categorical_feature, categorical_feature)) { + return(invisible(self)) + } + + # Check for empty data + if (is.null(private$raw_data)) { + stop("set_categorical_feature: cannot set categorical feature after freeing raw data, + please set ", sQuote("free_raw_data = FALSE"), " when you construct lgb.Dataset") + } + + # Overwrite categorical features + private$categorical_feature <- categorical_feature + + # Finalize and return self + private$finalize() + return(invisible(self)) + + }, + + set_reference = function(reference) { + + # setting reference to this same Dataset object doesn't require any changes + if (identical(private$reference, reference)) { + return(invisible(self)) + } + + # changing the reference removes the Dataset object on the C++ side, so it should only + # be done if you still have the raw_data available, so that the new Dataset can be reconstructed + if (is.null(private$raw_data)) { + stop("set_reference: cannot set reference after freeing raw data, + please set ", sQuote("free_raw_data = FALSE"), " when you construct lgb.Dataset") + } + + if (!.is_Dataset(reference)) { + stop("set_reference: Can only use lgb.Dataset as a reference") + } + + # Set known references + self$set_categorical_feature(categorical_feature = reference$.__enclos_env__$private$categorical_feature) + self$set_colnames(colnames = reference$get_colnames()) + private$set_predictor(predictor = reference$.__enclos_env__$private$predictor) + + # Store reference + private$reference <- reference + + # Finalize and return self + private$finalize() + return(invisible(self)) + + }, + + # Save binary model + save_binary = function(fname) { + + # Store binary data + self$construct() + .Call( + LGBM_DatasetSaveBinary_R + , private$handle + , path.expand(fname) + ) + return(invisible(self)) + } + + ), + private = list( + handle = NULL, + raw_data = NULL, + params = list(), + reference = NULL, + colnames = NULL, + categorical_feature = NULL, + predictor = NULL, + free_raw_data = TRUE, + used_indices = NULL, + info = NULL, + version = 0L, + + # finalize() will free up the handles + finalize = function() { + .Call( + LGBM_DatasetFree_R + , private$handle + ) + private$handle <- NULL + return(invisible(NULL)) + }, + + get_handle = function() { + + # Get handle and construct if needed + if (.is_null_handle(x = private$handle)) { + self$construct() + } + return(private$handle) + + }, + + set_predictor = function(predictor) { + + if (identical(private$predictor, predictor)) { + return(invisible(self)) + } + + # Check for empty data + if (is.null(private$raw_data)) { + stop("set_predictor: cannot set predictor after free raw data, + please set ", sQuote("free_raw_data = FALSE"), " when you construct lgb.Dataset") + } + + # Check for empty predictor + if (!is.null(predictor)) { + + # Predictor is unknown + if (!.is_Predictor(predictor)) { + stop("set_predictor: Can only use lgb.Predictor as predictor") + } + + } + + # Store predictor + private$predictor <- predictor + + # Finalize and return self + private$finalize() + return(invisible(self)) + + } + + ) +) + +#' @title Construct \code{lgb.Dataset} object +#' @description LightGBM does not train on raw data. +#' It discretizes continuous features into histogram bins, tries to +#' combine categorical features, and automatically handles missing and +# infinite values. +#' +#' The \code{Dataset} class handles that preprocessing, and holds that +#' alternative representation of the input data. +#' @inheritParams lgb_shared_dataset_params +#' @param data a \code{matrix} object, a \code{dgCMatrix} object, +#' a character representing a path to a text file (CSV, TSV, or LibSVM), +#' or a character representing a path to a binary \code{lgb.Dataset} file +#' @param params a list of parameters. See +#' \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#dataset-parameters}{ +#' The "Dataset Parameters" section of the documentation} for a list of parameters +#' and valid values. +#' @param reference reference dataset. When LightGBM creates a Dataset, it does some preprocessing like binning +#' continuous features into histograms. If you want to apply the same bin boundaries from an existing +#' dataset to new \code{data}, pass that existing Dataset to this argument. +#' @param colnames names of columns +#' @param categorical_feature categorical features. This can either be a character vector of feature +#' names or an integer vector with the indices of the features (e.g. +#' \code{c(1L, 10L)} to say "the first and tenth columns"). +#' @param free_raw_data LightGBM constructs its data format, called a "Dataset", from tabular data. +#' By default, that Dataset object on the R side does not keep a copy of the raw data. +#' This reduces LightGBM's memory consumption, but it means that the Dataset object +#' cannot be changed after it has been constructed. If you'd prefer to be able to +#' change the Dataset object after construction, set \code{free_raw_data = FALSE}. +#' +#' @return constructed dataset +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data_file <- tempfile(fileext = ".data") +#' lgb.Dataset.save(dtrain, data_file) +#' dtrain <- lgb.Dataset(data_file) +#' lgb.Dataset.construct(dtrain) +#' } +#' @export +lgb.Dataset <- function(data, + params = list(), + reference = NULL, + colnames = NULL, + categorical_feature = NULL, + free_raw_data = TRUE, + label = NULL, + weight = NULL, + group = NULL, + init_score = NULL) { + + return( + invisible(Dataset$new( + data = data + , params = params + , reference = reference + , colnames = colnames + , categorical_feature = categorical_feature + , predictor = NULL + , free_raw_data = free_raw_data + , used_indices = NULL + , label = label + , weight = weight + , group = group + , init_score = init_score + )) + ) + +} + +#' @name lgb.Dataset.create.valid +#' @title Construct validation data +#' @description Construct validation data according to training data +#' @inheritParams lgb_shared_dataset_params +#' @param dataset \code{lgb.Dataset} object, training data +#' @param data a \code{matrix} object, a \code{dgCMatrix} object, +#' a character representing a path to a text file (CSV, TSV, or LibSVM), +#' or a character representing a path to a binary \code{Dataset} file +#' @param params a list of parameters. See +#' \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#dataset-parameters}{ +#' The "Dataset Parameters" section of the documentation} for a list of parameters +#' and valid values. If this is an empty list (the default), the validation Dataset +#' will have the same parameters as the Dataset passed to argument \code{dataset}. +#' +#' @return constructed dataset +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +#' +#' # parameters can be changed between the training data and validation set, +#' # for example to account for training data in a text file with a header row +#' # and validation data in a text file without it +#' train_file <- tempfile(pattern = "train_", fileext = ".csv") +#' write.table( +#' data.frame(y = rnorm(100L), x1 = rnorm(100L), x2 = rnorm(100L)) +#' , file = train_file +#' , sep = "," +#' , col.names = TRUE +#' , row.names = FALSE +#' , quote = FALSE +#' ) +#' +#' valid_file <- tempfile(pattern = "valid_", fileext = ".csv") +#' write.table( +#' data.frame(y = rnorm(100L), x1 = rnorm(100L), x2 = rnorm(100L)) +#' , file = valid_file +#' , sep = "," +#' , col.names = FALSE +#' , row.names = FALSE +#' , quote = FALSE +#' ) +#' +#' dtrain <- lgb.Dataset( +#' data = train_file +#' , params = list(has_header = TRUE) +#' ) +#' dtrain$construct() +#' +#' dvalid <- lgb.Dataset( +#' data = valid_file +#' , params = list(has_header = FALSE) +#' ) +#' dvalid$construct() +#' } +#' @export +lgb.Dataset.create.valid <- function(dataset, + data, + label = NULL, + weight = NULL, + group = NULL, + init_score = NULL, + params = list()) { + + if (!.is_Dataset(x = dataset)) { + stop("lgb.Dataset.create.valid: input data should be an lgb.Dataset object") + } + + # Create validation dataset + return(invisible( + dataset$create_valid( + data = data + , label = label + , weight = weight + , group = group + , init_score = init_score + , params = params + ) + )) + +} + +#' @name lgb.Dataset.construct +#' @title Construct Dataset explicitly +#' @description Construct Dataset explicitly +#' @param dataset Object of class \code{lgb.Dataset} +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' lgb.Dataset.construct(dtrain) +#' } +#' @return constructed dataset +#' @export +lgb.Dataset.construct <- function(dataset) { + + if (!.is_Dataset(x = dataset)) { + stop("lgb.Dataset.construct: input data should be an lgb.Dataset object") + } + + return(invisible(dataset$construct())) + +} + +#' @title Dimensions of an \code{lgb.Dataset} +#' @description Returns a vector of numbers of rows and of columns in an \code{lgb.Dataset}. +#' @param x Object of class \code{lgb.Dataset} +#' +#' @return a vector of numbers of rows and of columns +#' +#' @details +#' Note: since \code{nrow} and \code{ncol} internally use \code{dim}, they can also +#' be directly used with an \code{lgb.Dataset} object. +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' +#' stopifnot(nrow(dtrain) == nrow(train$data)) +#' stopifnot(ncol(dtrain) == ncol(train$data)) +#' stopifnot(all(dim(dtrain) == dim(train$data))) +#' } +#' @rdname dim +#' @export +dim.lgb.Dataset <- function(x) { + + if (!.is_Dataset(x = x)) { + stop("dim.lgb.Dataset: input data should be an lgb.Dataset object") + } + + return(x$dim()) + +} + +#' @title Handling of column names of \code{lgb.Dataset} +#' @description Only column names are supported for \code{lgb.Dataset}, thus setting of +#' row names would have no effect and returned row names would be NULL. +#' @param x object of class \code{lgb.Dataset} +#' @param value a list of two elements: the first one is ignored +#' and the second one is column names +#' +#' @details +#' Generic \code{dimnames} methods are used by \code{colnames}. +#' Since row names are irrelevant, it is recommended to use \code{colnames} directly. +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' lgb.Dataset.construct(dtrain) +#' dimnames(dtrain) +#' colnames(dtrain) +#' colnames(dtrain) <- make.names(seq_len(ncol(train$data))) +#' print(dtrain, verbose = TRUE) +#' } +#' @rdname dimnames.lgb.Dataset +#' @return A list with the dimension names of the dataset +#' @export +dimnames.lgb.Dataset <- function(x) { + + if (!.is_Dataset(x = x)) { + stop("dimnames.lgb.Dataset: input data should be an lgb.Dataset object") + } + + # Return dimension names + return(list(NULL, x$get_colnames())) + +} + +#' @rdname dimnames.lgb.Dataset +#' @export +`dimnames<-.lgb.Dataset` <- function(x, value) { + + # Check if invalid element list + if (!identical(class(value), "list") || length(value) != 2L) { + stop("invalid ", sQuote("value", q = FALSE), " given: must be a list of two elements") + } + + # Check for unknown row names + if (!is.null(value[[1L]])) { + stop("lgb.Dataset does not have rownames") + } + + if (is.null(value[[2L]])) { + + x$set_colnames(colnames = NULL) + return(x) + + } + + # Check for unmatching column size + if (ncol(x) != length(value[[2L]])) { + stop( + "can't assign " + , sQuote(length(value[[2L]]), q = FALSE) + , " colnames to an lgb.Dataset with " + , sQuote(ncol(x), q = FALSE) + , " columns" + ) + } + + # Set column names properly, and return + x$set_colnames(colnames = value[[2L]]) + return(x) + +} + +#' @title Slice a dataset +#' @description Get a new \code{lgb.Dataset} containing the specified rows of +#' original \code{lgb.Dataset} object +#' +#' \emph{Renamed from} \code{slice()} \emph{in 4.4.0} +#' +#' @param dataset Object of class \code{lgb.Dataset} +#' @param idxset an integer vector of indices of rows needed +#' @return constructed sub dataset +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' +#' dsub <- lgb.slice.Dataset(dtrain, seq_len(42L)) +#' lgb.Dataset.construct(dsub) +#' labels <- lightgbm::get_field(dsub, "label") +#' } +#' @export +lgb.slice.Dataset <- function(dataset, idxset) { + + if (!.is_Dataset(x = dataset)) { + stop("lgb.slice.Dataset: input dataset should be an lgb.Dataset object") + } + + return(invisible(dataset$slice(idxset = idxset))) + +} + +#' @name get_field +#' @title Get one attribute of a \code{lgb.Dataset} +#' @description Get one attribute of a \code{lgb.Dataset} +#' @param dataset Object of class \code{lgb.Dataset} +#' @param field_name String with the name of the attribute to get. One of the following. +#' \itemize{ +#' \item \code{label}: label lightgbm learns from ; +#' \item \code{weight}: to do a weight rescale ; +#' \item{\code{group}: used for learning-to-rank tasks. An integer vector describing how to +#' group rows together as ordered results from the same set of candidate results to be ranked. +#' For example, if you have a 100-document dataset with \code{group = c(10, 20, 40, 10, 10, 10)}, +#' that means that you have 6 groups, where the first 10 records are in the first group, +#' records 11-30 are in the second group, etc.} +#' \item \code{init_score}: initial score is the base prediction lightgbm will boost from. +#' } +#' @return requested attribute +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' lgb.Dataset.construct(dtrain) +#' +#' labels <- lightgbm::get_field(dtrain, "label") +#' lightgbm::set_field(dtrain, "label", 1 - labels) +#' +#' labels2 <- lightgbm::get_field(dtrain, "label") +#' stopifnot(all(labels2 == 1 - labels)) +#' } +#' @export +get_field <- function(dataset, field_name) { + UseMethod("get_field") +} + +#' @rdname get_field +#' @export +get_field.lgb.Dataset <- function(dataset, field_name) { + + # Check if dataset is not a dataset + if (!.is_Dataset(x = dataset)) { + stop("get_field.lgb.Dataset(): input dataset should be an lgb.Dataset object") + } + + return(dataset$get_field(field_name = field_name)) + +} + +#' @name set_field +#' @title Set one attribute of a \code{lgb.Dataset} object +#' @description Set one attribute of a \code{lgb.Dataset} +#' @param dataset Object of class \code{lgb.Dataset} +#' @param field_name String with the name of the attribute to set. One of the following. +#' \itemize{ +#' \item \code{label}: label lightgbm learns from ; +#' \item \code{weight}: to do a weight rescale ; +#' \item{\code{group}: used for learning-to-rank tasks. An integer vector describing how to +#' group rows together as ordered results from the same set of candidate results to be ranked. +#' For example, if you have a 100-document dataset with \code{group = c(10, 20, 40, 10, 10, 10)}, +#' that means that you have 6 groups, where the first 10 records are in the first group, +#' records 11-30 are in the second group, etc.} +#' \item \code{init_score}: initial score is the base prediction lightgbm will boost from. +#' } +#' @param data The data for the field. See examples. +#' @return The \code{lgb.Dataset} you passed in. +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' lgb.Dataset.construct(dtrain) +#' +#' labels <- lightgbm::get_field(dtrain, "label") +#' lightgbm::set_field(dtrain, "label", 1 - labels) +#' +#' labels2 <- lightgbm::get_field(dtrain, "label") +#' stopifnot(all.equal(labels2, 1 - labels)) +#' } +#' @export +set_field <- function(dataset, field_name, data) { + UseMethod("set_field") +} + +#' @rdname set_field +#' @export +set_field.lgb.Dataset <- function(dataset, field_name, data) { + + if (!.is_Dataset(x = dataset)) { + stop("set_field.lgb.Dataset: input dataset should be an lgb.Dataset object") + } + + return(invisible(dataset$set_field(field_name = field_name, data = data))) +} + +#' @name lgb.Dataset.set.categorical +#' @title Set categorical feature of \code{lgb.Dataset} +#' @description Set the categorical features of an \code{lgb.Dataset} object. Use this function +#' to tell LightGBM which features should be treated as categorical. +#' @param dataset object of class \code{lgb.Dataset} +#' @param categorical_feature categorical features. This can either be a character vector of feature +#' names or an integer vector with the indices of the features (e.g. +#' \code{c(1L, 10L)} to say "the first and tenth columns"). +#' @return the dataset you passed in +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data_file <- tempfile(fileext = ".data") +#' lgb.Dataset.save(dtrain, data_file) +#' dtrain <- lgb.Dataset(data_file) +#' lgb.Dataset.set.categorical(dtrain, 1L:2L) +#' } +#' @rdname lgb.Dataset.set.categorical +#' @export +lgb.Dataset.set.categorical <- function(dataset, categorical_feature) { + + if (!.is_Dataset(x = dataset)) { + stop("lgb.Dataset.set.categorical: input dataset should be an lgb.Dataset object") + } + + return(invisible(dataset$set_categorical_feature(categorical_feature = categorical_feature))) + +} + +#' @name lgb.Dataset.set.reference +#' @title Set reference of \code{lgb.Dataset} +#' @description If you want to use validation data, you should set reference to training data +#' @param dataset object of class \code{lgb.Dataset} +#' @param reference object of class \code{lgb.Dataset} +#' +#' @return the dataset you passed in +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' # create training Dataset +#' data(agaricus.train, package ="lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' +#' # create a validation Dataset, using dtrain as a reference +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' dtest <- lgb.Dataset(test$data, label = test$label) +#' lgb.Dataset.set.reference(dtest, dtrain) +#' } +#' @rdname lgb.Dataset.set.reference +#' @export +lgb.Dataset.set.reference <- function(dataset, reference) { + + if (!.is_Dataset(x = dataset)) { + stop("lgb.Dataset.set.reference: input dataset should be an lgb.Dataset object") + } + + return(invisible(dataset$set_reference(reference = reference))) +} + +#' @name lgb.Dataset.save +#' @title Save \code{lgb.Dataset} to a binary file +#' @description Please note that \code{init_score} is not saved in binary file. +#' If you need it, please set it again after loading Dataset. +#' @param dataset object of class \code{lgb.Dataset} +#' @param fname object filename of output file +#' +#' @return the dataset you passed in +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' lgb.Dataset.save(dtrain, tempfile(fileext = ".bin")) +#' } +#' @export +lgb.Dataset.save <- function(dataset, fname) { + + if (!.is_Dataset(x = dataset)) { + stop("lgb.Dataset.save: input dataset should be an lgb.Dataset object") + } + + if (!is.character(fname)) { + stop("lgb.Dataset.save: fname should be a character or a file connection") + } + + return(invisible(dataset$save_binary(fname = fname))) +} diff --git a/R-package/R/lgb.Predictor.R b/R-package/R/lgb.Predictor.R new file mode 100644 index 0000000..0b71a52 --- /dev/null +++ b/R-package/R/lgb.Predictor.R @@ -0,0 +1,529 @@ +#' @importFrom methods is new +#' @importFrom R6 R6Class +#' @importFrom utils read.delim +#' @importClassesFrom Matrix dsparseMatrix dsparseVector dgCMatrix dgRMatrix CsparseMatrix RsparseMatrix +Predictor <- R6::R6Class( + + classname = "lgb.Predictor", + cloneable = FALSE, + public = list( + + # Initialize will create a starter model + initialize = function(modelfile, params = list(), fast_predict_config = list()) { + private$params <- .params2str(params = params) + handle <- NULL + + if (is.character(modelfile)) { + + # Create handle on it + handle <- .Call( + LGBM_BoosterCreateFromModelfile_R + , path.expand(modelfile) + ) + private$need_free_handle <- TRUE + + } else if (methods::is(modelfile, "lgb.Booster.handle") || inherits(modelfile, "externalptr")) { + + # Check if model file is a booster handle already + handle <- modelfile + private$need_free_handle <- FALSE + + } else if (.is_Booster(modelfile)) { + + handle <- modelfile$get_handle() + private$need_free_handle <- FALSE + + } else { + + stop("lgb.Predictor: modelfile must be either a character filename or an lgb.Booster.handle") + + } + + private$fast_predict_config <- fast_predict_config + + # Override class and store it + class(handle) <- "lgb.Booster.handle" + private$handle <- handle + + return(invisible(NULL)) + + }, + + # Get current iteration + current_iter = function() { + + cur_iter <- 0L + .Call( + LGBM_BoosterGetCurrentIteration_R + , private$handle + , cur_iter + ) + return(cur_iter) + + }, + + # Predict from data + predict = function(data, + start_iteration = NULL, + num_iteration = NULL, + rawscore = FALSE, + predleaf = FALSE, + predcontrib = FALSE, + header = FALSE) { + + # Check if number of iterations is existing - if not, then set it to -1 (use all) + if (is.null(num_iteration)) { + num_iteration <- -1L + } + # Check if start iterations is existing - if not, then set it to 0 (start from the first iteration) + if (is.null(start_iteration)) { + start_iteration <- 0L + } + + # Check if data is a file name and not a matrix + if (identical(class(data), "character") && length(data) == 1L) { + + data <- path.expand(data) + + # Data is a filename, create a temporary file with a "lightgbm_" pattern in it + tmp_filename <- tempfile(pattern = "lightgbm_") + on.exit(unlink(tmp_filename), add = TRUE) + + # Predict from temporary file + .Call( + LGBM_BoosterPredictForFile_R + , private$handle + , data + , as.integer(header) + , as.integer(rawscore) + , as.integer(predleaf) + , as.integer(predcontrib) + , as.integer(start_iteration) + , as.integer(num_iteration) + , private$params + , tmp_filename + ) + + # Get predictions from file + preds <- utils::read.delim(tmp_filename, header = FALSE, sep = "\t") + num_row <- nrow(preds) + preds <- as.vector(t(preds)) + + } else if (predcontrib && inherits(data, c("dsparseMatrix", "dsparseVector"))) { + + ncols <- .Call(LGBM_BoosterGetNumFeature_R, private$handle) + ncols_out <- integer(1L) + .Call(LGBM_BoosterGetNumClasses_R, private$handle, ncols_out) + ncols_out <- (ncols + 1L) * max(ncols_out, 1L) + if (is.na(ncols_out)) { + ncols_out <- as.numeric(ncols + 1L) * as.numeric(max(ncols_out, 1L)) + } + if (!inherits(data, "dsparseVector") && ncols_out > .Machine$integer.max) { + stop("Resulting matrix of feature contributions is too large for R to handle.") + } + + if (inherits(data, "dsparseVector")) { + + if (length(data) > ncols) { + stop(sprintf("Model was fitted to data with %d columns, input data has %.0f columns." + , ncols + , length(data))) + } + res <- .Call( + LGBM_BoosterPredictSparseOutput_R + , private$handle + , c(0L, as.integer(length(data@x))) + , data@i - 1L + , data@x + , TRUE + , 1L + , ncols + , start_iteration + , num_iteration + , private$params + ) + out <- methods::new("dsparseVector") + out@i <- res$indices + 1L + out@x <- res$data + out@length <- ncols_out + return(out) + + } else if (inherits(data, "dgRMatrix")) { + + if (ncol(data) > ncols) { + stop(sprintf("Model was fitted to data with %d columns, input data has %.0f columns." + , ncols + , ncol(data))) + } + res <- .Call( + LGBM_BoosterPredictSparseOutput_R + , private$handle + , data@p + , data@j + , data@x + , TRUE + , nrow(data) + , ncols + , start_iteration + , num_iteration + , private$params + ) + out <- methods::new("dgRMatrix") + out@p <- res$indptr + out@j <- res$indices + out@x <- res$data + out@Dim <- as.integer(c(nrow(data), ncols_out)) + + } else if (inherits(data, "dgCMatrix")) { + + if (ncol(data) != ncols) { + stop(sprintf("Model was fitted to data with %d columns, input data has %.0f columns." + , ncols + , ncol(data))) + } + res <- .Call( + LGBM_BoosterPredictSparseOutput_R + , private$handle + , data@p + , data@i + , data@x + , FALSE + , nrow(data) + , ncols + , start_iteration + , num_iteration + , private$params + ) + out <- methods::new("dgCMatrix") + out@p <- res$indptr + out@i <- res$indices + out@x <- res$data + out@Dim <- as.integer(c(nrow(data), length(res$indptr) - 1L)) + + } else { + + stop(sprintf("Predictions on sparse inputs are only allowed for '%s', '%s', '%s' - got: %s" + , "dsparseVector" + , "dgRMatrix" + , "dgCMatrix" + , toString(class(data)))) + } + + if (NROW(row.names(data))) { + out@Dimnames[[1L]] <- row.names(data) + } + return(out) + + } else { + + # Not a file, we need to predict from R object + num_row <- nrow(data) + if (is.null(num_row)) { + num_row <- 1L + } + + npred <- 0L + + # Check number of predictions to do + .Call( + LGBM_BoosterCalcNumPredict_R + , private$handle + , as.integer(num_row) + , as.integer(rawscore) + , as.integer(predleaf) + , as.integer(predcontrib) + , as.integer(start_iteration) + , as.integer(num_iteration) + , npred + ) + + # Pre-allocate empty vector + preds <- numeric(npred) + + # Check if data is a matrix + if (is.matrix(data)) { + # this if() prevents the memory and computational costs + # of converting something that is already "double" to "double" + if (storage.mode(data) != "double") { + storage.mode(data) <- "double" + } + + if (nrow(data) == 1L) { + + use_fast_config <- private$check_can_use_fast_predict_config( + csr = FALSE + , rawscore = rawscore + , predleaf = predleaf + , predcontrib = predcontrib + , start_iteration = start_iteration + , num_iteration = num_iteration + ) + + if (use_fast_config) { + .Call( + LGBM_BoosterPredictForMatSingleRowFast_R + , private$fast_predict_config$handle + , data + , preds + ) + } else { + .Call( + LGBM_BoosterPredictForMatSingleRow_R + , private$handle + , data + , rawscore + , predleaf + , predcontrib + , start_iteration + , num_iteration + , private$params + , preds + ) + } + + } else { + .Call( + LGBM_BoosterPredictForMat_R + , private$handle + , data + , as.integer(nrow(data)) + , as.integer(ncol(data)) + , as.integer(rawscore) + , as.integer(predleaf) + , as.integer(predcontrib) + , as.integer(start_iteration) + , as.integer(num_iteration) + , private$params + , preds + ) + } + + } else if (inherits(data, "dsparseVector")) { + + if (length(self$fast_predict_config)) { + ncols <- self$fast_predict_config$ncols + use_fast_config <- private$check_can_use_fast_predict_config( + csr = TRUE + , rawscore = rawscore + , predleaf = predleaf + , predcontrib = predcontrib + , start_iteration = start_iteration + , num_iteration = num_iteration + ) + } else { + ncols <- .Call(LGBM_BoosterGetNumFeature_R, private$handle) + use_fast_config <- FALSE + } + + if (length(data) > ncols) { + stop(sprintf("Model was fitted to data with %d columns, input data has %.0f columns." + , ncols + , length(data))) + } + + if (use_fast_config) { + .Call( + LGBM_BoosterPredictForCSRSingleRowFast_R + , self$fast_predict_config$handle + , data@i - 1L + , data@x + , preds + ) + } else { + .Call( + LGBM_BoosterPredictForCSRSingleRow_R + , private$handle + , data@i - 1L + , data@x + , ncols + , as.integer(rawscore) + , as.integer(predleaf) + , as.integer(predcontrib) + , start_iteration + , num_iteration + , private$params + , preds + ) + } + + } else if (inherits(data, "dgRMatrix")) { + + ncols <- .Call(LGBM_BoosterGetNumFeature_R, private$handle) + if (ncol(data) > ncols) { + stop(sprintf("Model was fitted to data with %d columns, input data has %.0f columns." + , ncols + , ncol(data))) + } + + if (nrow(data) == 1L) { + + if (length(self$fast_predict_config)) { + ncols <- self$fast_predict_config$ncols + use_fast_config <- private$check_can_use_fast_predict_config( + csr = TRUE + , rawscore = rawscore + , predleaf = predleaf + , predcontrib = predcontrib + , start_iteration = start_iteration + , num_iteration = num_iteration + ) + } else { + ncols <- .Call(LGBM_BoosterGetNumFeature_R, private$handle) + use_fast_config <- FALSE + } + + if (use_fast_config) { + .Call( + LGBM_BoosterPredictForCSRSingleRowFast_R + , self$fast_predict_config$handle + , data@j + , data@x + , preds + ) + } else { + .Call( + LGBM_BoosterPredictForCSRSingleRow_R + , private$handle + , data@j + , data@x + , ncols + , as.integer(rawscore) + , as.integer(predleaf) + , as.integer(predcontrib) + , start_iteration + , num_iteration + , private$params + , preds + ) + } + + } else { + + .Call( + LGBM_BoosterPredictForCSR_R + , private$handle + , data@p + , data@j + , data@x + , ncols + , as.integer(rawscore) + , as.integer(predleaf) + , as.integer(predcontrib) + , start_iteration + , num_iteration + , private$params + , preds + ) + + } + + } else if (methods::is(data, "dgCMatrix")) { + if (length(data@p) > 2147483647L) { + stop("Cannot support large CSC matrix") + } + # Check if data is a dgCMatrix (sparse matrix, column compressed format) + .Call( + LGBM_BoosterPredictForCSC_R + , private$handle + , data@p + , data@i + , data@x + , length(data@p) + , length(data@x) + , nrow(data) + , as.integer(rawscore) + , as.integer(predleaf) + , as.integer(predcontrib) + , as.integer(start_iteration) + , as.integer(num_iteration) + , private$params + , preds + ) + + } else { + + stop("predict: cannot predict on data of class ", sQuote(class(data), q = FALSE)) + + } + } + + # Check if number of rows is strange (not a multiple of the dataset rows) + if (length(preds) %% num_row != 0L) { + stop( + "predict: prediction length " + , sQuote(length(preds), q = FALSE) + , " is not a multiple of nrows(data): " + , sQuote(num_row, q = FALSE) + ) + } + + # Get number of cases per row + npred_per_case <- length(preds) / num_row + + # Data reshaping + if (npred_per_case > 1L || predleaf || predcontrib) { + preds <- matrix(preds, ncol = npred_per_case, byrow = TRUE) + } + + # Keep row names if possible + if (NROW(row.names(data)) && NROW(data) == NROW(preds)) { + if (is.null(dim(preds))) { + names(preds) <- row.names(data) + } else { + row.names(preds) <- row.names(data) + } + } + + return(preds) + } + + ), + private = list( + handle = NULL + , need_free_handle = FALSE + , params = "" + , fast_predict_config = list() + , check_can_use_fast_predict_config = function(csr, + rawscore, + predleaf, + predcontrib, + start_iteration, + num_iteration) { + + if (!NROW(private$fast_predict_config)) { + return(FALSE) + } + + if (.is_null_handle(private$fast_predict_config$handle)) { + warning(paste0("Model had fast CSR predict configuration, but it is inactive." + , " Try re-generating it through 'lgb.configure_fast_predict'.")) + return(FALSE) + } + + if (isTRUE(csr) != private$fast_predict_config$csr) { + return(FALSE) + } + + return( + private$params == "" && + private$fast_predict_config$rawscore == rawscore && + private$fast_predict_config$predleaf == predleaf && + private$fast_predict_config$predcontrib == predcontrib && + .equal_or_both_null(private$fast_predict_config$start_iteration, start_iteration) && + .equal_or_both_null(private$fast_predict_config$num_iteration, num_iteration) + ) + } + + # finalize() will free up the handles + , finalize = function() { + if (private$need_free_handle) { + .Call( + LGBM_BoosterFree_R + , private$handle + ) + private$handle <- NULL + } + return(invisible(NULL)) + } + ) +) diff --git a/R-package/R/lgb.convert_with_rules.R b/R-package/R/lgb.convert_with_rules.R new file mode 100644 index 0000000..1d2748a --- /dev/null +++ b/R-package/R/lgb.convert_with_rules.R @@ -0,0 +1,207 @@ +# [description] get all column classes of a data.table or data.frame. +# This function collapses the result of class() into a single string +.get_column_classes <- function(df) { + return( + vapply( + X = df + , FUN = function(x) { + paste(class(x), collapse = ",") + } + , FUN.VALUE = character(1L) + ) + ) +} + +# [description] check a data frame or data table for columns that are any +# type other than numeric and integer. This is used by lgb.convert_with_rules() +# to warn if more action is needed by users +# before a dataset can be converted to a lgb.Dataset. +.warn_for_unconverted_columns <- function(df, function_name) { + column_classes <- .get_column_classes(df = df) + unconverted_columns <- column_classes[!(column_classes %in% c("numeric", "integer"))] + if (length(unconverted_columns) > 0L) { + col_detail_string <- toString( + paste0( + names(unconverted_columns) + , " (" + , unconverted_columns + , ")" + ) + ) + msg <- paste0( + function_name + , ": " + , length(unconverted_columns) + , " columns are not numeric or integer. These need to be dropped or converted to " + , "be used in an lgb.Dataset object. " + , col_detail_string + ) + warning(msg) + } + return(invisible(NULL)) +} + +.LGB_CONVERT_DEFAULT_FOR_LOGICAL_NA <- function() { + return(-1L) +} +.LGB_CONVERT_DEFAULT_FOR_NON_LOGICAL_NA <- function() { + return(0L) +} + + +#' @name lgb.convert_with_rules +#' @title Data preparator for LightGBM datasets with rules (integer) +#' @description Attempts to prepare a clean dataset to prepare to put in a \code{lgb.Dataset}. +#' Factor, character, and logical columns are converted to integer. Missing values +#' in factors and characters will be filled with 0L. Missing values in logicals +#' will be filled with -1L. +#' +#' This function returns and optionally takes in "rules" the describe exactly +#' how to convert values in columns. +#' +#' Columns that contain only NA values will be converted by this function but will +#' not show up in the returned \code{rules}. +#' +#' NOTE: In previous releases of LightGBM, this function was called \code{lgb.prepare_rules2}. +#' @param data A data.frame or data.table to prepare. +#' @param rules A set of rules from the data preparator, if already used. This should be an R list, +#' where names are column names in \code{data} and values are named character +#' vectors whose names are column values and whose values are new values to +#' replace them with. +#' @return A list with the cleaned dataset (\code{data}) and the rules (\code{rules}). +#' Note that the data must be converted to a matrix format (\code{as.matrix}) for input in +#' \code{lgb.Dataset}. +#' +#' @examples +#' \donttest{ +#' data(iris) +#' +#' str(iris) +#' +#' new_iris <- lgb.convert_with_rules(data = iris) +#' str(new_iris$data) +#' +#' data(iris) # Erase iris dataset +#' iris$Species[1L] <- "NEW FACTOR" # Introduce junk factor (NA) +#' +#' # Use conversion using known rules +#' # Unknown factors become 0, excellent for sparse datasets +#' newer_iris <- lgb.convert_with_rules(data = iris, rules = new_iris$rules) +#' +#' # Unknown factor is now zero, perfect for sparse datasets +#' newer_iris$data[1L, ] # Species became 0 as it is an unknown factor +#' +#' newer_iris$data[1L, 5L] <- 1.0 # Put back real initial value +#' +#' # Is the newly created dataset equal? YES! +#' all.equal(new_iris$data, newer_iris$data) +#' +#' # Can we test our own rules? +#' data(iris) # Erase iris dataset +#' +#' # We remapped values differently +#' personal_rules <- list( +#' Species = c( +#' "setosa" = 3L +#' , "versicolor" = 2L +#' , "virginica" = 1L +#' ) +#' ) +#' newest_iris <- lgb.convert_with_rules(data = iris, rules = personal_rules) +#' str(newest_iris$data) # SUCCESS! +#' } +#' @importFrom data.table set +#' @export +lgb.convert_with_rules <- function(data, rules = NULL) { + + column_classes <- .get_column_classes(df = data) + + is_data_table <- data.table::is.data.table(x = data) + is_data_frame <- is.data.frame(data) + + if (!(is_data_table || is_data_frame)) { + stop( + "lgb.convert_with_rules: you provided " + , paste(class(data), collapse = " & ") + , " but data should have class data.frame or data.table" + ) + } + + # if user didn't provide rules, create them + if (is.null(rules)) { + rules <- list() + columns_to_fix <- which(column_classes %in% c("character", "factor", "logical")) + + for (i in columns_to_fix) { + + col_values <- data[[i]] + + # Get unique values + if (is.factor(col_values)) { + unique_vals <- levels(col_values) + unique_vals <- unique_vals[!is.na(unique_vals)] + mini_numeric <- seq_along(unique_vals) # respect ordinal + } else if (is.character(col_values)) { + unique_vals <- as.factor(unique(col_values)) + unique_vals <- unique_vals[!is.na(unique_vals)] + mini_numeric <- as.integer(unique_vals) # no respect for ordinal + } else if (is.logical(col_values)) { + unique_vals <- c(FALSE, TRUE) + mini_numeric <- c(0L, 1L) + } + + # don't add rules for all-NA columns + if (length(unique_vals) > 0L) { + col_name <- names(data)[i] + rules[[col_name]] <- mini_numeric + names(rules[[col_name]]) <- unique_vals + } + } + } + + for (col_name in names(rules)) { + if (column_classes[[col_name]] == "logical") { + default_value_for_na <- .LGB_CONVERT_DEFAULT_FOR_LOGICAL_NA() + } else { + default_value_for_na <- .LGB_CONVERT_DEFAULT_FOR_NON_LOGICAL_NA() + } + if (is_data_table) { + data.table::set( + x = data + , j = col_name + , value = unname(rules[[col_name]][data[[col_name]]]) + ) + data[is.na(get(col_name)), (col_name) := default_value_for_na] + } else { + data[[col_name]] <- unname(rules[[col_name]][data[[col_name]]]) + data[is.na(data[col_name]), col_name] <- default_value_for_na + } + } + + # if any all-NA columns exist, they won't be in rules. Convert them + all_na_cols <- which( + sapply( + X = data + , FUN = function(x) { + (is.factor(x) || is.character(x) || is.logical(x)) && all(is.na(unique(x))) + } + ) + ) + for (col_name in all_na_cols) { + if (column_classes[[col_name]] == "logical") { + default_value_for_na <- .LGB_CONVERT_DEFAULT_FOR_LOGICAL_NA() + } else { + default_value_for_na <- .LGB_CONVERT_DEFAULT_FOR_NON_LOGICAL_NA() + } + if (is_data_table) { + data[, (col_name) := rep(default_value_for_na, .N)] + } else { + data[[col_name]] <- default_value_for_na + } + } + + .warn_for_unconverted_columns(df = data, function_name = "lgb.convert_with_rules") + + return(list(data = data, rules = rules)) + +} diff --git a/R-package/R/lgb.cv.R b/R-package/R/lgb.cv.R new file mode 100644 index 0000000..1f6fcf6 --- /dev/null +++ b/R-package/R/lgb.cv.R @@ -0,0 +1,617 @@ +#' @importFrom R6 R6Class +CVBooster <- R6::R6Class( + classname = "lgb.CVBooster", + cloneable = FALSE, + public = list( + best_iter = -1L, + best_score = NA, + record_evals = list(), + boosters = list(), + initialize = function(x) { + self$boosters <- x + return(invisible(NULL)) + }, + reset_parameter = function(new_params) { + for (x in self$boosters) { + x[["booster"]]$reset_parameter(params = new_params) + } + return(invisible(self)) + } + ) +) + +#' @name lgb.cv +#' @title Main CV logic for LightGBM +#' @description Cross validation logic used by LightGBM +#' @inheritParams lgb_shared_params +#' @param nfold the original dataset is randomly partitioned into \code{nfold} equal size subsamples. +#' @param record Boolean, TRUE will record iteration message to \code{booster$record_evals} +#' @param showsd \code{boolean}, whether to show standard deviation of cross validation. +#' This parameter defaults to \code{TRUE}. Setting it to \code{FALSE} can lead to a +#' slight speedup by avoiding unnecessary computation. +#' @param stratified a \code{boolean} indicating whether sampling of folds should be stratified +#' by the values of outcome labels. +#' @param folds \code{list} provides a possibility to use a list of pre-defined CV folds +#' (each element must be a vector of test fold's indices). When folds are supplied, +#' the \code{nfold} and \code{stratified} parameters are ignored. +#' @param callbacks List of callback functions that are applied at each iteration. +#' @param reset_data Boolean, setting it to TRUE (not the default value) will transform the booster model +#' into a predictor model which frees up memory and the original datasets +#' @param eval_train_metric \code{boolean}, whether to add the cross validation results on the +#' training data. This parameter defaults to \code{FALSE}. Setting it to \code{TRUE} +#' will increase run time. +#' @inheritSection lgb_shared_params Early Stopping +#' @return a trained model \code{lgb.CVBooster}. +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' params <- list( +#' objective = "regression" +#' , metric = "l2" +#' , min_data = 1L +#' , learning_rate = 1.0 +#' , num_threads = 2L +#' ) +#' model <- lgb.cv( +#' params = params +#' , data = dtrain +#' , nrounds = 5L +#' , nfold = 3L +#' ) +#' } +#' +#' @importFrom data.table data.table setorderv +#' @export +lgb.cv <- function(params = list() + , data + , nrounds = 100L + , nfold = 3L + , obj = NULL + , eval = NULL + , verbose = 1L + , record = TRUE + , eval_freq = 1L + , showsd = TRUE + , stratified = TRUE + , folds = NULL + , init_model = NULL + , early_stopping_rounds = NULL + , callbacks = list() + , reset_data = FALSE + , serializable = TRUE + , eval_train_metric = FALSE + ) { + + if (nrounds <= 0L) { + stop("nrounds should be greater than zero") + } + if (!.is_Dataset(x = data)) { + stop("lgb.cv: data must be an lgb.Dataset instance") + } + + # set some parameters, resolving the way they were passed in with other parameters + # in `params`. + # this ensures that the model stored with Booster$save() correctly represents + # what was passed in + params <- .check_wrapper_param( + main_param_name = "verbosity" + , params = params + , alternative_kwarg_value = verbose + ) + params <- .check_wrapper_param( + main_param_name = "num_iterations" + , params = params + , alternative_kwarg_value = nrounds + ) + params <- .check_wrapper_param( + main_param_name = "metric" + , params = params + , alternative_kwarg_value = NULL + ) + params <- .check_wrapper_param( + main_param_name = "objective" + , params = params + , alternative_kwarg_value = obj + ) + params <- .check_wrapper_param( + main_param_name = "early_stopping_round" + , params = params + , alternative_kwarg_value = early_stopping_rounds + ) + early_stopping_rounds <- params[["early_stopping_round"]] + + # extract any function objects passed for objective or metric + fobj <- NULL + if (is.function(params$objective)) { + fobj <- params$objective + params$objective <- "none" + } + + # If eval is a single function, store it as a 1-element list + # (for backwards compatibility). If it is a list of functions, store + # all of them. This makes it possible to pass any mix of strings like "auc" + # and custom functions to eval + params <- .check_eval(params = params, eval = eval) + eval_functions <- list(NULL) + if (is.function(eval)) { + eval_functions <- list(eval) + } + if (methods::is(eval, "list")) { + eval_functions <- Filter( + f = is.function + , x = eval + ) + } + + # Init predictor to empty + predictor <- NULL + + # Check for boosting from a trained model + if (is.character(init_model)) { + predictor <- Predictor$new(modelfile = init_model) + } else if (.is_Booster(x = init_model)) { + predictor <- init_model$to_predictor() + } + + # Set the iteration to start from / end to (and check for boosting from a trained model, again) + begin_iteration <- 1L + if (!is.null(predictor)) { + begin_iteration <- predictor$current_iter() + 1L + } + end_iteration <- begin_iteration + params[["num_iterations"]] - 1L + + # pop interaction_constraints off of params. It needs some preprocessing on the + # R side before being passed into the Dataset object + interaction_constraints <- params[["interaction_constraints"]] + params["interaction_constraints"] <- NULL + + # Construct datasets, if needed + data$update_params(params = params) + data$construct() + + # Check interaction constraints + params[["interaction_constraints"]] <- .check_interaction_constraints( + interaction_constraints = interaction_constraints + , column_names = data$get_colnames() + ) + + # Update parameters with parsed parameters + data$update_params(params = params) + + # Create the predictor set + data$.__enclos_env__$private$set_predictor(predictor = predictor) + + if (!is.null(folds)) { + + # Check for list of folds or for single value + if (!identical(class(folds), "list") || length(folds) < 2L) { + stop(sQuote("folds"), " must be a list with 2 or more elements that are vectors of indices for each CV-fold") + } + + } else { + + if (nfold <= 1L) { + stop(sQuote("nfold"), " must be > 1") + } + + # Create folds + folds <- .generate_cv_folds( + nfold = nfold + , nrows = nrow(data) + , stratified = stratified + , label = get_field(dataset = data, field_name = "label") + , group = get_field(dataset = data, field_name = "group") + , params = params + ) + + } + + # Add printing log callback + if (params[["verbosity"]] > 0L && eval_freq > 0L) { + callbacks <- .add_cb(cb_list = callbacks, cb = cb_print_evaluation(period = eval_freq)) + } + + # Add evaluation log callback + if (record) { + callbacks <- .add_cb(cb_list = callbacks, cb = cb_record_evaluation()) + } + + # Did user pass parameters that indicate they want to use early stopping? + using_early_stopping <- !is.null(early_stopping_rounds) && early_stopping_rounds > 0L + + boosting_param_names <- .PARAMETER_ALIASES()[["boosting"]] + using_dart <- any( + sapply( + X = boosting_param_names + , FUN = function(param) { + identical(params[[param]], "dart") + } + ) + ) + + # Cannot use early stopping with 'dart' boosting + if (using_dart) { + if (using_early_stopping) { + warning("Early stopping is not available in 'dart' mode.") + } + using_early_stopping <- FALSE + + # Remove the cb_early_stop() function if it was passed in to callbacks + callbacks <- Filter( + f = function(cb_func) { + !identical(attr(cb_func, "name"), "cb_early_stop") + } + , x = callbacks + ) + } + + # If user supplied early_stopping_rounds, add the early stopping callback + if (using_early_stopping) { + callbacks <- .add_cb( + cb_list = callbacks + , cb = cb_early_stop( + stopping_rounds = early_stopping_rounds + , first_metric_only = isTRUE(params[["first_metric_only"]]) + , verbose = params[["verbosity"]] > 0L + ) + ) + } + + cb <- .categorize_callbacks(cb_list = callbacks) + + # Construct booster for each fold. The data.table() code below is used to + # guarantee that indices are sorted while keeping init_score and weight together + # with the correct indices. Note that it takes advantage of the fact that + # someDT$some_column returns NULL is 'some_column' does not exist in the data.table + bst_folds <- lapply( + X = seq_along(folds) + , FUN = function(k) { + + # For learning-to-rank, each fold is a named list with two elements: + # * `fold` = an integer vector of row indices + # * `group` = an integer vector describing which groups are in the fold + # For classification or regression tasks, it will just be an integer + # vector of row indices + folds_have_group <- "group" %in% names(folds[[k]]) + if (folds_have_group) { + test_indices <- folds[[k]]$fold + test_group_indices <- folds[[k]]$group + test_groups <- get_field(dataset = data, field_name = "group")[test_group_indices] + train_groups <- get_field(dataset = data, field_name = "group")[-test_group_indices] + } else { + test_indices <- folds[[k]] + } + train_indices <- seq_len(nrow(data))[-test_indices] + + # set up test set + indexDT <- data.table::data.table( + indices = test_indices + , weight = get_field(dataset = data, field_name = "weight")[test_indices] + , init_score = get_field(dataset = data, field_name = "init_score")[test_indices] + ) + data.table::setorderv(x = indexDT, cols = "indices", order = 1L) + dtest <- lgb.slice.Dataset(data, indexDT$indices) + set_field(dataset = dtest, field_name = "weight", data = indexDT$weight) + set_field(dataset = dtest, field_name = "init_score", data = indexDT$init_score) + + # set up training set + indexDT <- data.table::data.table( + indices = train_indices + , weight = get_field(dataset = data, field_name = "weight")[train_indices] + , init_score = get_field(dataset = data, field_name = "init_score")[train_indices] + ) + data.table::setorderv(x = indexDT, cols = "indices", order = 1L) + dtrain <- lgb.slice.Dataset(data, indexDT$indices) + set_field(dataset = dtrain, field_name = "weight", data = indexDT$weight) + set_field(dataset = dtrain, field_name = "init_score", data = indexDT$init_score) + + if (folds_have_group) { + set_field(dataset = dtest, field_name = "group", data = test_groups) + set_field(dataset = dtrain, field_name = "group", data = train_groups) + } + + booster <- Booster$new(params = params, train_set = dtrain) + if (isTRUE(eval_train_metric)) { + booster$add_valid(data = dtrain, name = "train") + } + booster$add_valid(data = dtest, name = "valid") + return( + list(booster = booster) + ) + } + ) + + # Create new booster + cv_booster <- CVBooster$new(x = bst_folds) + + # Callback env + env <- CB_ENV$new() + env$model <- cv_booster + env$begin_iteration <- begin_iteration + env$end_iteration <- end_iteration + + # Start training model using number of iterations to start and end with + for (i in seq.int(from = begin_iteration, to = end_iteration)) { + + # Overwrite iteration in environment + env$iteration <- i + env$eval_list <- list() + + for (f in cb$pre_iter) { + f(env) + } + + # Update one boosting iteration + msg <- lapply(cv_booster$boosters, function(fd) { + fd$booster$update(fobj = fobj) + out <- list() + for (eval_function in eval_functions) { + out <- append(out, fd$booster$eval_valid(feval = eval_function)) + } + return(out) + }) + + # Prepare collection of evaluation results + merged_msg <- .merge_cv_result( + msg = msg + , showsd = showsd + ) + + # Write evaluation result in environment + env$eval_list <- merged_msg$eval_list + + # Check for standard deviation requirement + if (showsd) { + env$eval_err_list <- merged_msg$eval_err_list + } + + # Loop through env + for (f in cb$post_iter) { + f(env) + } + + # Check for early stopping and break if needed + if (env$met_early_stop) break + + } + + # When early stopping is not activated, we compute the best iteration / score ourselves + # based on the first first metric + if (record && is.na(env$best_score)) { + # when using a custom eval function, the metric name is returned from the + # function, so figure it out from record_evals + if (!is.null(eval_functions[1L])) { + first_metric <- names(cv_booster$record_evals[["valid"]])[1L] + } else { + first_metric <- cv_booster$.__enclos_env__$private$eval_names[1L] + } + .find_best <- which.min + if (isTRUE(env$eval_list[[1L]]$higher_better[1L])) { + .find_best <- which.max + } + cv_booster$best_iter <- unname( + .find_best( + unlist( + cv_booster$record_evals[["valid"]][[first_metric]][[.EVAL_KEY()]] + ) + ) + ) + cv_booster$best_score <- cv_booster$record_evals[["valid"]][[first_metric]][[.EVAL_KEY()]][[cv_booster$best_iter]] + } + # Propagate the best_iter attribute from the cv_booster to the individual boosters + for (bst in cv_booster$boosters) { + bst$booster$best_iter <- cv_booster$best_iter + } + + if (reset_data) { + lapply(cv_booster$boosters, function(fd) { + # Store temporarily model data elsewhere + booster_old <- list( + best_iter = fd$booster$best_iter + , best_score = fd$booster$best_score + , record_evals = fd$booster$record_evals + ) + # Reload model + fd$booster <- lgb.load(model_str = fd$booster$save_model_to_string()) + fd$booster$best_iter <- booster_old$best_iter + fd$booster$best_score <- booster_old$best_score + fd$booster$record_evals <- booster_old$record_evals + }) + } + + if (serializable) { + lapply(cv_booster$boosters, function(model) model$booster$save_raw()) + } + + return(cv_booster) + +} + +# Generates random (stratified if needed) CV folds +.generate_cv_folds <- function(nfold, nrows, stratified, label, group, params) { + + # Check for group existence + if (is.null(group)) { + + # Shuffle + rnd_idx <- sample.int(nrows) + + # Request stratified folds + if (isTRUE(stratified) && params$objective %in% c("binary", "multiclass") && length(label) == length(rnd_idx)) { + + y <- label[rnd_idx] + y <- as.factor(y) + folds <- .stratified_folds(y = y, k = nfold) + + } else { + + # Make simple non-stratified folds + folds <- list() + + # Loop through each fold + for (i in seq_len(nfold)) { + kstep <- length(rnd_idx) %/% (nfold - i + 1L) + folds[[i]] <- rnd_idx[seq_len(kstep)] + rnd_idx <- rnd_idx[-seq_len(kstep)] + } + + } + + } else { + + # When doing group, stratified is not possible (only random selection) + if (nfold > length(group)) { + stop("\nYou requested too many folds for the number of available groups.\n") + } + + # Degroup the groups + ungrouped <- inverse.rle(list(lengths = group, values = seq_along(group))) + + # Can't stratify, shuffle + rnd_idx <- sample.int(length(group)) + + # Make simple non-stratified folds + folds <- list() + + # Loop through each fold + for (i in seq_len(nfold)) { + kstep <- length(rnd_idx) %/% (nfold - i + 1L) + folds[[i]] <- list( + fold = which(ungrouped %in% rnd_idx[seq_len(kstep)]) + , group = rnd_idx[seq_len(kstep)] + ) + rnd_idx <- rnd_idx[-seq_len(kstep)] + } + + } + + return(folds) + +} + +# Creates CV folds stratified by the values of y. +# It was borrowed from caret::createFolds and simplified +# by always returning an unnamed list of fold indices. +#' @importFrom stats quantile +.stratified_folds <- function(y, k) { + + # Group the numeric data based on their magnitudes + # and sample within those groups. + # When the number of samples is low, we may have + # issues further slicing the numeric data into + # groups. The number of groups will depend on the + # ratio of the number of folds to the sample size. + # At most, we will use quantiles. If the sample + # is too small, we just do regular unstratified CV + if (is.numeric(y)) { + + cuts <- length(y) %/% k + if (cuts < 2L) { + cuts <- 2L + } + if (cuts > 5L) { + cuts <- 5L + } + y <- cut( + y + , unique(stats::quantile(y, probs = seq.int(0.0, 1.0, length.out = cuts))) + , include.lowest = TRUE + ) + + } + + if (k < length(y)) { + + # Reset levels so that the possible levels and + # the levels in the vector are the same + y <- as.factor(as.character(y)) + numInClass <- table(y) + foldVector <- vector(mode = "integer", length(y)) + + # For each class, balance the fold allocation as far + # as possible, then resample the remainder. + # The final assignment of folds is also randomized. + for (i in seq_along(numInClass)) { + + # Create a vector of integers from 1:k as many times as possible without + # going over the number of samples in the class. Note that if the number + # of samples in a class is less than k, nothing is produced here. + seqVector <- rep(seq_len(k), numInClass[i] %/% k) + + # Add enough random integers to get length(seqVector) == numInClass[i] + if (numInClass[i] %% k > 0L) { + seqVector <- c(seqVector, sample.int(k, numInClass[i] %% k)) + } + + # Shuffle the integers for fold assignment and assign to this classes's data + foldVector[y == dimnames(numInClass)$y[i]] <- sample(seqVector) + + } + + } else { + + foldVector <- seq(along = y) + + } + + out <- split(seq(along = y), foldVector) + names(out) <- NULL + return(out) +} + +.merge_cv_result <- function(msg, showsd) { + + if (length(msg) == 0L) { + stop("lgb.cv: size of cv result error") + } + + eval_len <- length(msg[[1L]]) + + if (eval_len == 0L) { + stop("lgb.cv: should provide at least one metric for CV") + } + + # Get evaluation results using a list apply + eval_result <- lapply(seq_len(eval_len), function(j) { + as.numeric(lapply(seq_along(msg), function(i) { + msg[[i]][[j]]$value })) + }) + + # Get evaluation. Just taking the first element here to + # get structure (name, higher_better, data_name) + ret_eval <- msg[[1L]] + + for (j in seq_len(eval_len)) { + ret_eval[[j]]$value <- mean(eval_result[[j]]) + } + + ret_eval_err <- NULL + + # Check for standard deviation + if (showsd) { + + # Parse standard deviation + for (j in seq_len(eval_len)) { + ret_eval_err <- c( + ret_eval_err + , sqrt(mean(eval_result[[j]] ^ 2L) - mean(eval_result[[j]]) ^ 2L) + ) + } + + ret_eval_err <- as.list(ret_eval_err) + + } + + return( + list( + eval_list = ret_eval + , eval_err_list = ret_eval_err + ) + ) + +} diff --git a/R-package/R/lgb.drop_serialized.R b/R-package/R/lgb.drop_serialized.R new file mode 100644 index 0000000..6100f61 --- /dev/null +++ b/R-package/R/lgb.drop_serialized.R @@ -0,0 +1,21 @@ +#' @name lgb.drop_serialized +#' @title Drop serialized raw bytes in a LightGBM model object +#' @description If a LightGBM model object was produced with argument `serializable=TRUE`, the R object will keep +#' a copy of the underlying C++ object as raw bytes, which can be used to reconstruct such object after getting +#' serialized and de-serialized, but at the cost of extra memory usage. If these raw bytes are not needed anymore, +#' they can be dropped through this function in order to save memory. Note that the object will be modified in-place. +#' +#' \emph{New in version 4.0.0} +#' +#' @param model \code{lgb.Booster} object which was produced with `serializable=TRUE`. +#' +#' @return \code{lgb.Booster} (the same `model` object that was passed as input, as invisible). +#' @seealso \link{lgb.restore_handle}, \link{lgb.make_serializable}. +#' @export +lgb.drop_serialized <- function(model) { + if (!.is_Booster(x = model)) { + stop("lgb.drop_serialized: model should be an ", sQuote("lgb.Booster", q = FALSE)) + } + model$drop_raw() + return(invisible(model)) +} diff --git a/R-package/R/lgb.importance.R b/R-package/R/lgb.importance.R new file mode 100644 index 0000000..d60507c --- /dev/null +++ b/R-package/R/lgb.importance.R @@ -0,0 +1,83 @@ +#' @name lgb.importance +#' @title Compute feature importance in a model +#' @description Creates a \code{data.table} of feature importances in a model. +#' @param model object of class \code{lgb.Booster}. +#' @param percentage whether to show importance in relative percentage. +#' +#' @return For a tree model, a \code{data.table} with the following columns: +#' \itemize{ +#' \item{\code{Feature}: Feature names in the model.} +#' \item{\code{Gain}: The total gain of this feature's splits.} +#' \item{\code{Cover}: The number of observation related to this feature.} +#' \item{\code{Frequency}: The number of times a feature split in trees.} +#' } +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' +#' params <- list( +#' objective = "binary" +#' , learning_rate = 0.1 +#' , max_depth = -1L +#' , min_data_in_leaf = 1L +#' , min_sum_hessian_in_leaf = 1.0 +#' , num_threads = 2L +#' ) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 5L +#' ) +#' +#' tree_imp1 <- lgb.importance(model, percentage = TRUE) +#' tree_imp2 <- lgb.importance(model, percentage = FALSE) +#' } +#' @importFrom data.table := setnames setorderv +#' @export +lgb.importance <- function(model, percentage = TRUE) { + + if (!.is_Booster(x = model)) { + stop("'model' has to be an object of class lgb.Booster") + } + + # Setup importance + tree_dt <- lgb.model.dt.tree(model = model) + + # Extract elements + tree_imp_dt <- tree_dt[ + !is.na(split_index) + , .(Gain = sum(split_gain), Cover = sum(internal_count), Frequency = .N) + , by = "split_feature" + ] + + data.table::setnames( + x = tree_imp_dt + , old = "split_feature" + , new = "Feature" + ) + + # Sort features by Gain + data.table::setorderv( + x = tree_imp_dt + , cols = "Gain" + , order = -1L + ) + + # Check if relative values are requested + if (percentage) { + tree_imp_dt[, `:=`( + Gain = Gain / sum(Gain) + , Cover = Cover / sum(Cover) + , Frequency = Frequency / sum(Frequency) + )] + } + + # adding an empty [] to ensure the table is printed the first time print.data.table() is called + return(tree_imp_dt[]) + +} diff --git a/R-package/R/lgb.interpret.R b/R-package/R/lgb.interpret.R new file mode 100644 index 0000000..5293ab4 --- /dev/null +++ b/R-package/R/lgb.interpret.R @@ -0,0 +1,256 @@ +#' @name lgb.interpret +#' @title Compute feature contribution of prediction +#' @description Computes feature contribution components of rawscore prediction. +#' @param model object of class \code{lgb.Booster}. +#' @param data a matrix object or a dgCMatrix object. +#' @param idxset an integer vector of indices of rows needed. +#' @param num_iteration number of iteration want to predict with, NULL or <= 0 means use best iteration. +#' +#' @return For regression, binary classification and lambdarank model, a \code{list} of \code{data.table} +#' with the following columns: +#' \itemize{ +#' \item{\code{Feature}: Feature names in the model.} +#' \item{\code{Contribution}: The total contribution of this feature's splits.} +#' } +#' For multiclass classification, a \code{list} of \code{data.table} with the Feature column and +#' Contribution columns to each class. +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' Logit <- function(x) log(x / (1.0 - x)) +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' set_field( +#' dataset = dtrain +#' , field_name = "init_score" +#' , data = rep(Logit(mean(train$label)), length(train$label)) +#' ) +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' +#' params <- list( +#' objective = "binary" +#' , learning_rate = 0.1 +#' , max_depth = -1L +#' , min_data_in_leaf = 1L +#' , min_sum_hessian_in_leaf = 1.0 +#' , num_threads = 2L +#' ) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 3L +#' ) +#' +#' tree_interpretation <- lgb.interpret(model, test$data, 1L:5L) +#' } +#' @importFrom data.table as.data.table +#' @export +lgb.interpret <- function(model, + data, + idxset, + num_iteration = NULL) { + + # Get tree model + tree_dt <- lgb.model.dt.tree(model = model, num_iteration = num_iteration) + + # Check number of classes + num_class <- model$.__enclos_env__$private$num_class + + # Get vector list + tree_interpretation_dt_list <- vector(mode = "list", length = length(idxset)) + + # Get parsed predictions of data + pred_mat <- t( + model$predict( + data = data[idxset, , drop = FALSE] + , num_iteration = num_iteration + , predleaf = TRUE + ) + ) + leaf_index_dt <- data.table::as.data.table(x = pred_mat) + leaf_index_mat_list <- lapply( + X = leaf_index_dt + , FUN = matrix + , ncol = num_class + , byrow = TRUE + ) + + # Get list of trees + tree_index_mat_list <- lapply( + X = leaf_index_mat_list + , FUN = function(x) { + matrix(seq_along(x) - 1L, ncol = num_class, byrow = TRUE) + } + ) + + for (i in seq_along(idxset)) { + tree_interpretation_dt_list[[i]] <- .single_row_interpret( + tree_dt = tree_dt + , num_class = num_class + , tree_index_mat = tree_index_mat_list[[i]] + , leaf_index_mat = leaf_index_mat_list[[i]] + ) + } + + return(tree_interpretation_dt_list) + +} + +#' @name lgb.interprete +#' @title DEPRECATED - use lgb.interpret() instead +#' @description Alias for \code{lgb.interpret}. +#' @param ... Arguments passed through to \code{lgb.interpret} +#' @export +lgb.interprete <- function(...) { + warning("lgb.interprete() is deprecated and will be removed in a future release. Use lgb.interpret() instead.") + return(lgb.interpret(...)) +} + +#' @importFrom data.table data.table +single.tree.interpret <- function(tree_dt, + tree_id, + leaf_id) { + + # Match tree id + single_tree_dt <- tree_dt[tree_index == tree_id, ] + + # Get leaves + leaf_dt <- single_tree_dt[leaf_index == leaf_id, .(leaf_index, leaf_parent, leaf_value)] + + # Get nodes + node_dt <- single_tree_dt[!is.na(split_index), .(split_index, split_feature, node_parent, internal_value)] + + # Prepare sequences + feature_seq <- character(0L) + value_seq <- numeric(0L) + + # Get to root from leaf + leaf_to_root <- function(parent_id, current_value) { + + value_seq <<- c(current_value, value_seq) + + if (!is.na(parent_id)) { + + # Not null means existing node + this_node <- node_dt[split_index == parent_id, ] + feature_seq <<- c(this_node[["split_feature"]], feature_seq) + leaf_to_root( + parent_id = this_node[["node_parent"]] + , current_value = this_node[["internal_value"]] + ) + + } + + } + + # Perform leaf to root conversion + leaf_to_root( + parent_id = leaf_dt[["leaf_parent"]] + , current_value = leaf_dt[["leaf_value"]] + ) + + return( + data.table::data.table( + Feature = feature_seq + , Contribution = diff.default(value_seq) + ) + ) + +} + +#' @importFrom data.table := rbindlist setorder +.multiple_tree_interpret <- function(tree_dt, + tree_index, + leaf_index) { + + interp_dt <- data.table::rbindlist( + l = mapply( + FUN = single.tree.interpret + , tree_id = tree_index + , leaf_id = leaf_index + , MoreArgs = list( + tree_dt = tree_dt + ) + , SIMPLIFY = FALSE + , USE.NAMES = TRUE + ) + , use.names = TRUE + ) + + interp_dt <- interp_dt[, .(Contribution = sum(Contribution)), by = "Feature"] + + # Sort features in descending order by contribution + interp_dt[, abs_contribution := abs(Contribution)] + data.table::setorder( + x = interp_dt + , -abs_contribution + ) + + # Drop absolute value of contribution (only needed for sorting) + interp_dt[, abs_contribution := NULL] + + return(interp_dt) + +} + +#' @importFrom data.table set setnames +.single_row_interpret <- function(tree_dt, num_class, tree_index_mat, leaf_index_mat) { + + # Prepare vector list + tree_interpretation <- vector(mode = "list", length = num_class) + + # Loop throughout each class + for (i in seq_len(num_class)) { + + next_interp_dt <- .multiple_tree_interpret( + tree_dt = tree_dt + , tree_index = tree_index_mat[, i] + , leaf_index = leaf_index_mat[, i] + ) + + if (num_class > 1L) { + data.table::setnames( + x = next_interp_dt + , old = "Contribution" + , new = paste("Class", i - 1L) + ) + } + + tree_interpretation[[i]] <- next_interp_dt + + } + + if (num_class == 1L) { + + tree_interpretation_dt <- tree_interpretation[[1L]] + + } else { + + # Full interpretation elements + tree_interpretation_dt <- Reduce( + f = function(x, y) { + merge(x, y, by = "Feature", all = TRUE) + } + , x = tree_interpretation + ) + + # Loop throughout each tree + for (j in 2L:ncol(tree_interpretation_dt)) { + + data.table::set( + x = tree_interpretation_dt + , i = which(is.na(tree_interpretation_dt[[j]])) + , j = j + , value = 0.0 + ) + + } + + } + + return(tree_interpretation_dt) +} diff --git a/R-package/R/lgb.make_serializable.R b/R-package/R/lgb.make_serializable.R new file mode 100644 index 0000000..19a4252 --- /dev/null +++ b/R-package/R/lgb.make_serializable.R @@ -0,0 +1,21 @@ +#' @name lgb.make_serializable +#' @title Make a LightGBM object serializable by keeping raw bytes +#' @description If a LightGBM model object was produced with argument `serializable=FALSE`, the R object will not +#' be serializable (e.g. cannot save and load with \code{saveRDS} and \code{readRDS}) as it will lack the raw bytes +#' needed to reconstruct its underlying C++ object. This function can be used to forcibly produce those serialized +#' raw bytes and make the object serializable. Note that the object will be modified in-place. +#' +#' \emph{New in version 4.0.0} +#' +#' @param model \code{lgb.Booster} object which was produced with `serializable=FALSE`. +#' +#' @return \code{lgb.Booster} (the same `model` object that was passed as input, as invisible). +#' @seealso \link{lgb.restore_handle}, \link{lgb.drop_serialized}. +#' @export +lgb.make_serializable <- function(model) { + if (!.is_Booster(x = model)) { + stop("lgb.make_serializable: model should be an ", sQuote("lgb.Booster", q = FALSE)) + } + model$save_raw() + return(invisible(model)) +} diff --git a/R-package/R/lgb.model.dt.tree.R b/R-package/R/lgb.model.dt.tree.R new file mode 100644 index 0000000..ac1b2f9 --- /dev/null +++ b/R-package/R/lgb.model.dt.tree.R @@ -0,0 +1,192 @@ +#' @name lgb.model.dt.tree +#' @title Parse a LightGBM model json dump +#' @description Parse a LightGBM model json dump into a \code{data.table} structure. +#' @param model object of class \code{lgb.Booster}. +#' @param num_iteration Number of iterations to include. NULL or <= 0 means use best iteration. +#' @param start_iteration Index (1-based) of the first boosting round to include in the output. +#' For example, passing \code{start_iteration=5, num_iteration=3} for a regression model +#' means "return information about the fifth, sixth, and seventh trees". +#' +#' \emph{New in version 4.4.0} +#' +#' @return +#' A \code{data.table} with detailed information about model trees' nodes and leaves. +#' +#' The columns of the \code{data.table} are: +#' +#' \itemize{ +#' \item{\code{tree_index}: ID of a tree in a model (integer)} +#' \item{\code{split_index}: ID of a node in a tree (integer)} +#' \item{\code{split_feature}: for a node, it's a feature name (character); +#' for a leaf, it simply labels it as \code{"NA"}} +#' \item{\code{node_parent}: ID of the parent node for current node (integer)} +#' \item{\code{leaf_index}: ID of a leaf in a tree (integer)} +#' \item{\code{leaf_parent}: ID of the parent node for current leaf (integer)} +#' \item{\code{split_gain}: Split gain of a node} +#' \item{\code{threshold}: Splitting threshold value of a node} +#' \item{\code{decision_type}: Decision type of a node} +#' \item{\code{default_left}: Determine how to handle NA value, TRUE -> Left, FALSE -> Right} +#' \item{\code{internal_value}: Node value} +#' \item{\code{internal_count}: The number of observation collected by a node} +#' \item{\code{leaf_value}: Leaf value} +#' \item{\code{leaf_count}: The number of observation collected by a leaf} +#' } +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' +#' params <- list( +#' objective = "binary" +#' , learning_rate = 0.01 +#' , num_leaves = 63L +#' , max_depth = -1L +#' , min_data_in_leaf = 1L +#' , min_sum_hessian_in_leaf = 1.0 +#' , num_threads = 2L +#' ) +#' model <- lgb.train(params, dtrain, 10L) +#' +#' tree_dt <- lgb.model.dt.tree(model) +#' } +#' @importFrom data.table := rbindlist +#' @importFrom jsonlite fromJSON +#' @export +lgb.model.dt.tree <- function( + model, num_iteration = NULL, start_iteration = 1L + ) { + + json_model <- lgb.dump( + booster = model + , num_iteration = num_iteration + , start_iteration = start_iteration + ) + + parsed_json_model <- jsonlite::fromJSON( + txt = json_model + , simplifyVector = TRUE + , simplifyDataFrame = FALSE + , simplifyMatrix = FALSE + , flatten = FALSE + ) + + # Parse tree model + tree_list <- lapply( + X = parsed_json_model$tree_info + , FUN = .single_tree_parse + ) + + # Combine into single data.table + tree_dt <- data.table::rbindlist(l = tree_list, use.names = TRUE) + + # Substitute feature index with the actual feature name + + # Since the index comes from C++ (which is 0-indexed), be sure + # to add 1 (e.g. index 28 means the 29th feature in feature_names) + split_feature_indx <- tree_dt[, split_feature] + 1L + + # Get corresponding feature names. Positions in split_feature_indx + # which are NA will result in an NA feature name + feature_names <- parsed_json_model$feature_names[split_feature_indx] + tree_dt[, split_feature := feature_names] + + return(tree_dt) +} + + +#' @importFrom data.table := data.table rbindlist +.single_tree_parse <- function(lgb_tree) { + tree_info_cols <- c( + "split_index" + , "split_feature" + , "split_gain" + , "threshold" + , "decision_type" + , "default_left" + , "internal_value" + , "internal_count" + ) + + # Traverse tree function + pre_order_traversal <- function(env = NULL, tree_node_leaf, current_depth = 0L, parent_index = NA_integer_) { + + if (is.null(env)) { + # Setup initial default data.table with default types + env <- new.env(parent = emptyenv()) + env$single_tree_dt <- list() + env$single_tree_dt[[1L]] <- data.table::data.table( + tree_index = integer(0L) + , depth = integer(0L) + , split_index = integer(0L) + , split_feature = integer(0L) + , node_parent = integer(0L) + , leaf_index = integer(0L) + , leaf_parent = integer(0L) + , split_gain = numeric(0L) + , threshold = numeric(0L) + , decision_type = character(0L) + , default_left = character(0L) + , internal_value = integer(0L) + , internal_count = integer(0L) + , leaf_value = integer(0L) + , leaf_count = integer(0L) + ) + # start tree traversal + pre_order_traversal( + env = env + , tree_node_leaf = tree_node_leaf + , current_depth = current_depth + , parent_index = parent_index + ) + } else { + + # Check if split index is not null in leaf + if (!is.null(tree_node_leaf$split_index)) { + + # update data.table + env$single_tree_dt[[length(env$single_tree_dt) + 1L]] <- c( + tree_node_leaf[tree_info_cols] + , list("depth" = current_depth, "node_parent" = parent_index) + ) + + # Traverse tree again both left and right + pre_order_traversal( + env = env + , tree_node_leaf = tree_node_leaf$left_child + , current_depth = current_depth + 1L + , parent_index = tree_node_leaf$split_index + ) + pre_order_traversal( + env = env + , tree_node_leaf = tree_node_leaf$right_child + , current_depth = current_depth + 1L + , parent_index = tree_node_leaf$split_index + ) + } else if (!is.null(tree_node_leaf$leaf_index)) { + + # update list + env$single_tree_dt[[length(env$single_tree_dt) + 1L]] <- c( + tree_node_leaf[c("leaf_index", "leaf_value", "leaf_count")] + , list("depth" = current_depth, "leaf_parent" = parent_index) + ) + } + } + return(env$single_tree_dt) + } + + # Traverse structure and rowbind everything + single_tree_dt <- data.table::rbindlist( + pre_order_traversal(tree_node_leaf = lgb_tree$tree_structure) + , use.names = TRUE + , fill = TRUE + ) + + # Store index + single_tree_dt[, tree_index := lgb_tree$tree_index] + + return(single_tree_dt) +} diff --git a/R-package/R/lgb.plot.importance.R b/R-package/R/lgb.plot.importance.R new file mode 100644 index 0000000..b8a90ca --- /dev/null +++ b/R-package/R/lgb.plot.importance.R @@ -0,0 +1,99 @@ +#' @name lgb.plot.importance +#' @title Plot feature importance as a bar graph +#' @description Plot previously calculated feature importance: Gain, Cover and Frequency, as a bar graph. +#' @param tree_imp a \code{data.table} returned by \code{\link{lgb.importance}}. +#' @param top_n maximal number of top features to include into the plot. +#' @param measure the name of importance measure to plot, can be "Gain", "Cover" or "Frequency". +#' @param left_margin (base R barplot) allows to adjust the left margin size to fit feature names. +#' @param cex (base R barplot) passed as \code{cex.names} parameter to \code{\link[graphics]{barplot}}. +#' Set a number smaller than 1.0 to make the bar labels smaller than R's default and values +#' greater than 1.0 to make them larger. +#' +#' @details +#' The graph represents each feature as a horizontal bar of length proportional to the defined importance of a feature. +#' Features are shown ranked in a decreasing importance order. +#' +#' @return +#' The \code{lgb.plot.importance} function creates a \code{barplot} +#' and silently returns a processed data.table with \code{top_n} features sorted by defined importance. +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' +#' params <- list( +#' objective = "binary" +#' , learning_rate = 0.1 +#' , min_data_in_leaf = 1L +#' , min_sum_hessian_in_leaf = 1.0 +#' , num_threads = 2L +#' ) +#' +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 5L +#' ) +#' +#' tree_imp <- lgb.importance(model, percentage = TRUE) +#' lgb.plot.importance(tree_imp, top_n = 5L, measure = "Gain") +#' } +#' @importFrom graphics barplot par +#' @export +lgb.plot.importance <- function(tree_imp, + top_n = 10L, + measure = "Gain", + left_margin = 10L, + cex = NULL + ) { + + # Check for measurement (column names) correctness + measure <- match.arg( + measure + , choices = c("Gain", "Cover", "Frequency") + , several.ok = FALSE + ) + + # Get top N importance (defaults to 10) + top_n <- min(top_n, nrow(tree_imp)) + + # Parse importance + tree_imp <- tree_imp[order(abs(get(measure)), decreasing = TRUE), ][seq_len(top_n), ] + + # Attempt to setup a correct cex + if (is.null(cex)) { + cex <- 2.5 / log2(1.0 + top_n) + } + + # Refresh plot + op <- graphics::par(no.readonly = TRUE) + on.exit(graphics::par(op)) + + graphics::par( + mar = c( + op$mar[1L] + , left_margin + , op$mar[3L] + , op$mar[4L] + ) + ) + + tree_imp[rev(seq_len(.N)), + graphics::barplot( + height = get(measure) + , names.arg = Feature + , horiz = TRUE + , border = NA + , main = "Feature Importance" + , xlab = measure + , cex.names = cex + , las = 1L + )] + + return(invisible(tree_imp)) + +} diff --git a/R-package/R/lgb.plot.interpretation.R b/R-package/R/lgb.plot.interpretation.R new file mode 100644 index 0000000..d9e0e3e --- /dev/null +++ b/R-package/R/lgb.plot.interpretation.R @@ -0,0 +1,166 @@ +#' @name lgb.plot.interpretation +#' @title Plot feature contribution as a bar graph +#' @description Plot previously calculated feature contribution as a bar graph. +#' @param tree_interpretation_dt a \code{data.table} returned by \code{\link{lgb.interpret}}. +#' @param top_n maximal number of top features to include into the plot. +#' @param cols the column numbers of layout, will be used only for multiclass classification feature contribution. +#' @param left_margin (base R barplot) allows to adjust the left margin size to fit feature names. +#' @param cex (base R barplot) passed as \code{cex.names} parameter to \code{barplot}. +#' +#' @details +#' The graph represents each feature as a horizontal bar of length proportional to the defined +#' contribution of a feature. Features are shown ranked in a decreasing contribution order. +#' +#' @return +#' The \code{lgb.plot.interpretation} function creates a \code{barplot}. +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' Logit <- function(x) { +#' log(x / (1.0 - x)) +#' } +#' data(agaricus.train, package = "lightgbm") +#' labels <- agaricus.train$label +#' dtrain <- lgb.Dataset( +#' agaricus.train$data +#' , label = labels +#' ) +#' set_field( +#' dataset = dtrain +#' , field_name = "init_score" +#' , data = rep(Logit(mean(labels)), length(labels)) +#' ) +#' +#' data(agaricus.test, package = "lightgbm") +#' +#' params <- list( +#' objective = "binary" +#' , learning_rate = 0.1 +#' , max_depth = -1L +#' , min_data_in_leaf = 1L +#' , min_sum_hessian_in_leaf = 1.0 +#' , num_threads = 2L +#' ) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 5L +#' ) +#' +#' tree_interpretation <- lgb.interpret( +#' model = model +#' , data = agaricus.test$data +#' , idxset = 1L:5L +#' ) +#' lgb.plot.interpretation( +#' tree_interpretation_dt = tree_interpretation[[1L]] +#' , top_n = 3L +#' ) +#' } +#' @importFrom data.table setnames +#' @importFrom graphics barplot par +#' @export +lgb.plot.interpretation <- function(tree_interpretation_dt, + top_n = 10L, + cols = 1L, + left_margin = 10L, + cex = NULL) { + + num_class <- ncol(tree_interpretation_dt) - 1L + + # Refresh plot + op <- graphics::par(no.readonly = TRUE) + on.exit(graphics::par(op)) + + # Do some magic plotting + bottom_margin <- 3.0 + top_margin <- 2.0 + right_margin <- op$mar[4L] + + graphics::par( + mar = c( + bottom_margin + , left_margin + , top_margin + , right_margin + ) + ) + + if (num_class == 1L) { + + # Only one class, plot straight away + .multiple_tree_plot_interpretation( + tree_interpretation = tree_interpretation_dt + , top_n = top_n + , title = NULL + , cex = cex + ) + + } else { + + # More than one class, shape data first + layout_mat <- matrix( + seq.int(to = cols * ceiling(num_class / cols)) + , ncol = cols + , nrow = ceiling(num_class / cols) + ) + + # Shape output + graphics::par(mfcol = c(nrow(layout_mat), ncol(layout_mat))) + + # Loop throughout all classes + for (i in seq_len(num_class)) { + + # Prepare interpretation, perform T, get the names, and plot straight away + plot_dt <- tree_interpretation_dt[, c(1L, i + 1L), with = FALSE] + data.table::setnames( + x = plot_dt + , old = names(plot_dt) + , new = c("Feature", "Contribution") + ) + .multiple_tree_plot_interpretation( + tree_interpretation = plot_dt + , top_n = top_n + , title = paste("Class", i - 1L) + , cex = cex + ) + + } + } + return(invisible(NULL)) +} + +#' @importFrom graphics barplot +.multiple_tree_plot_interpretation <- function(tree_interpretation, + top_n, + title, + cex) { + + # Parse tree + tree_interpretation <- tree_interpretation[order(abs(Contribution), decreasing = TRUE), ][seq_len(min(top_n, .N)), ] + + # Attempt to setup a correct cex + if (is.null(cex)) { + cex <- 2.5 / log2(1.0 + top_n) + } + + # create plot + tree_interpretation[abs(Contribution) > 0.0, bar_color := "firebrick"] + tree_interpretation[Contribution == 0.0, bar_color := "steelblue"] + tree_interpretation[rev(seq_len(.N)), + graphics::barplot( + height = Contribution + , names.arg = Feature + , horiz = TRUE + , col = bar_color + , border = NA + , main = title + , cex.names = cex + , las = 1L + )] + + return(invisible(NULL)) + +} diff --git a/R-package/R/lgb.restore_handle.R b/R-package/R/lgb.restore_handle.R new file mode 100644 index 0000000..407f275 --- /dev/null +++ b/R-package/R/lgb.restore_handle.R @@ -0,0 +1,47 @@ +#' @name lgb.restore_handle +#' @title Restore the C++ component of a de-serialized LightGBM model +#' @description After a LightGBM model object is de-serialized through functions such as \code{save} or +#' \code{saveRDS}, its underlying C++ object will be blank and needs to be restored to able to use it. Such +#' object is restored automatically when calling functions such as \code{predict}, but this function can be +#' used to forcibly restore it beforehand. Note that the object will be modified in-place. +#' +#' \emph{New in version 4.0.0} +#' +#' @details Be aware that fast single-row prediction configurations are not restored through this +#' function. If you wish to make fast single-row predictions using a \code{lgb.Booster} loaded this way, +#' call \link{lgb.configure_fast_predict} on the loaded \code{lgb.Booster} object. +#' @param model \code{lgb.Booster} object which was de-serialized and whose underlying C++ object and R handle +#' need to be restored. +#' +#' @return \code{lgb.Booster} (the same `model` object that was passed as input, invisibly). +#' @seealso \link{lgb.make_serializable}, \link{lgb.drop_serialized}. +#' @examples +#' \donttest{ +#' library(lightgbm) +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data("agaricus.train") +#' model <- lightgbm( +#' agaricus.train$data +#' , agaricus.train$label +#' , params = list(objective = "binary") +#' , nrounds = 5L +#' , verbose = 0 +#' , num_threads = 2L +#' ) +#' fname <- tempfile(fileext="rds") +#' saveRDS(model, fname) +#' +#' model_new <- readRDS(fname) +#' model_new$check_null_handle() +#' lgb.restore_handle(model_new) +#' model_new$check_null_handle() +#' } +#' @export +lgb.restore_handle <- function(model) { + if (!.is_Booster(x = model)) { + stop("lgb.restore_handle: model should be an ", sQuote("lgb.Booster", q = FALSE)) + } + model$restore_handle() + return(invisible(model)) +} diff --git a/R-package/R/lgb.train.R b/R-package/R/lgb.train.R new file mode 100644 index 0000000..4b16cac --- /dev/null +++ b/R-package/R/lgb.train.R @@ -0,0 +1,383 @@ +#' @name lgb.train +#' @title Main training logic for LightGBM +#' @description Low-level R interface to train a LightGBM model. Unlike \code{\link{lightgbm}}, +#' this function is focused on performance (e.g. speed, memory efficiency). It is also +#' less likely to have breaking API changes in new releases than \code{\link{lightgbm}}. +#' @inheritParams lgb_shared_params +#' @param valids a list of \code{lgb.Dataset} objects, used for validation +#' @param record Boolean, TRUE will record iteration message to \code{booster$record_evals} +#' @param callbacks List of callback functions that are applied at each iteration. +#' @param reset_data Boolean, setting it to TRUE (not the default value) will transform the +#' booster model into a predictor model which frees up memory and the +#' original datasets +#' @inheritSection lgb_shared_params Early Stopping +#' @return a trained booster model \code{lgb.Booster}. +#' +#' @examples +#' \donttest{ +#' \dontshow{setLGBMthreads(2L)} +#' \dontshow{data.table::setDTthreads(1L)} +#' data(agaricus.train, package = "lightgbm") +#' train <- agaricus.train +#' dtrain <- lgb.Dataset(train$data, label = train$label) +#' data(agaricus.test, package = "lightgbm") +#' test <- agaricus.test +#' dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +#' params <- list( +#' objective = "regression" +#' , metric = "l2" +#' , min_data = 1L +#' , learning_rate = 1.0 +#' , num_threads = 2L +#' ) +#' valids <- list(test = dtest) +#' model <- lgb.train( +#' params = params +#' , data = dtrain +#' , nrounds = 5L +#' , valids = valids +#' , early_stopping_rounds = 3L +#' ) +#' } +#' +#' @export +lgb.train <- function(params = list(), + data, + nrounds = 100L, + valids = list(), + obj = NULL, + eval = NULL, + verbose = 1L, + record = TRUE, + eval_freq = 1L, + init_model = NULL, + early_stopping_rounds = NULL, + callbacks = list(), + reset_data = FALSE, + serializable = TRUE) { + + # validate inputs early to avoid unnecessary computation + if (nrounds <= 0L) { + stop("nrounds should be greater than zero") + } + if (!.is_Dataset(x = data)) { + stop("lgb.train: data must be an lgb.Dataset instance") + } + if (length(valids) > 0L) { + if (!identical(class(valids), "list") || !all(vapply(valids, .is_Dataset, logical(1L)))) { + stop("lgb.train: valids must be a list of lgb.Dataset elements") + } + evnames <- names(valids) + if (is.null(evnames) || !all(nzchar(evnames))) { + stop("lgb.train: each element of valids must have a name") + } + } + + # set some parameters, resolving the way they were passed in with other parameters + # in `params`. + # this ensures that the model stored with Booster$save() correctly represents + # what was passed in + params <- .check_wrapper_param( + main_param_name = "verbosity" + , params = params + , alternative_kwarg_value = verbose + ) + params <- .check_wrapper_param( + main_param_name = "num_iterations" + , params = params + , alternative_kwarg_value = nrounds + ) + params <- .check_wrapper_param( + main_param_name = "metric" + , params = params + , alternative_kwarg_value = NULL + ) + params <- .check_wrapper_param( + main_param_name = "objective" + , params = params + , alternative_kwarg_value = obj + ) + params <- .check_wrapper_param( + main_param_name = "early_stopping_round" + , params = params + , alternative_kwarg_value = early_stopping_rounds + ) + early_stopping_rounds <- params[["early_stopping_round"]] + + # extract any function objects passed for objective or metric + fobj <- NULL + if (is.function(params$objective)) { + fobj <- params$objective + params$objective <- "none" + } + + # If eval is a single function, store it as a 1-element list + # (for backwards compatibility). If it is a list of functions, store + # all of them. This makes it possible to pass any mix of strings like "auc" + # and custom functions to eval + params <- .check_eval(params = params, eval = eval) + eval_functions <- list(NULL) + if (is.function(eval)) { + eval_functions <- list(eval) + } + if (methods::is(eval, "list")) { + eval_functions <- Filter( + f = is.function + , x = eval + ) + } + + # Init predictor to empty + predictor <- NULL + + # Check for boosting from a trained model + if (is.character(init_model)) { + predictor <- Predictor$new(modelfile = init_model) + } else if (.is_Booster(x = init_model)) { + predictor <- init_model$to_predictor() + } + + # Set the iteration to start from / end to (and check for boosting from a trained model, again) + begin_iteration <- 1L + if (!is.null(predictor)) { + begin_iteration <- predictor$current_iter() + 1L + } + end_iteration <- begin_iteration + params[["num_iterations"]] - 1L + + # pop interaction_constraints off of params. It needs some preprocessing on the + # R side before being passed into the Dataset object + interaction_constraints <- params[["interaction_constraints"]] + params["interaction_constraints"] <- NULL + + # Construct datasets, if needed + data$update_params(params = params) + data$construct() + + # Check interaction constraints + params[["interaction_constraints"]] <- .check_interaction_constraints( + interaction_constraints = interaction_constraints + , column_names = data$get_colnames() + ) + + # Update parameters with parsed parameters + data$update_params(params) + + # Create the predictor set + data$.__enclos_env__$private$set_predictor(predictor) + + valid_contain_train <- FALSE + train_data_name <- "train" + reduced_valid_sets <- list() + + # Parse validation datasets + if (length(valids) > 0L) { + + for (key in names(valids)) { + + # Use names to get validation datasets + valid_data <- valids[[key]] + + # Check for duplicate train/validation dataset + if (identical(data, valid_data)) { + valid_contain_train <- TRUE + train_data_name <- key + next + } + + # Update parameters, data + valid_data$update_params(params) + valid_data$set_reference(data) + reduced_valid_sets[[key]] <- valid_data + + } + + } + + # Add printing log callback + if (params[["verbosity"]] > 0L && eval_freq > 0L) { + callbacks <- .add_cb( + cb_list = callbacks + , cb = cb_print_evaluation(period = eval_freq) + ) + } + + # Add evaluation log callback + if (record && length(valids) > 0L) { + callbacks <- .add_cb( + cb_list = callbacks + , cb = cb_record_evaluation() + ) + } + + # Did user pass parameters that indicate they want to use early stopping? + using_early_stopping <- !is.null(early_stopping_rounds) && early_stopping_rounds > 0L + + boosting_param_names <- .PARAMETER_ALIASES()[["boosting"]] + using_dart <- any( + sapply( + X = boosting_param_names + , FUN = function(param) { + identical(params[[param]], "dart") + } + ) + ) + + # Cannot use early stopping with 'dart' boosting + if (using_dart) { + if (using_early_stopping) { + warning("Early stopping is not available in 'dart' mode.") + } + using_early_stopping <- FALSE + + # Remove the cb_early_stop() function if it was passed in to callbacks + callbacks <- Filter( + f = function(cb_func) { + !identical(attr(cb_func, "name"), "cb_early_stop") + } + , x = callbacks + ) + } + + # If user supplied early_stopping_rounds, add the early stopping callback + if (using_early_stopping) { + callbacks <- .add_cb( + cb_list = callbacks + , cb = cb_early_stop( + stopping_rounds = early_stopping_rounds + , first_metric_only = isTRUE(params[["first_metric_only"]]) + , verbose = params[["verbosity"]] > 0L + ) + ) + } + + cb <- .categorize_callbacks(cb_list = callbacks) + + # Construct booster with datasets + booster <- Booster$new(params = params, train_set = data) + if (valid_contain_train) { + booster$set_train_data_name(name = train_data_name) + } + + for (key in names(reduced_valid_sets)) { + booster$add_valid(data = reduced_valid_sets[[key]], name = key) + } + + # Callback env + env <- CB_ENV$new() + env$model <- booster + env$begin_iteration <- begin_iteration + env$end_iteration <- end_iteration + + # Start training model using number of iterations to start and end with + for (i in seq.int(from = begin_iteration, to = end_iteration)) { + + # Overwrite iteration in environment + env$iteration <- i + env$eval_list <- list() + + # Loop through "pre_iter" element + for (f in cb$pre_iter) { + f(env) + } + + # Update one boosting iteration + booster$update(fobj = fobj) + + # Prepare collection of evaluation results + eval_list <- list() + + # Collection: Has validation dataset? + if (length(valids) > 0L) { + + # Get evaluation results with passed-in functions + for (eval_function in eval_functions) { + + # Validation has training dataset? + if (valid_contain_train) { + eval_list <- append(eval_list, booster$eval_train(feval = eval_function)) + } + + eval_list <- append(eval_list, booster$eval_valid(feval = eval_function)) + } + + # Calling booster$eval_valid() will get + # evaluation results with the metrics in params$metric by calling LGBM_BoosterGetEval_R", + # so need to be sure that gets called, which it wouldn't be above if no functions + # were passed in + if (length(eval_functions) == 0L) { + if (valid_contain_train) { + eval_list <- append(eval_list, booster$eval_train(feval = eval_function)) + } + eval_list <- append(eval_list, booster$eval_valid(feval = eval_function)) + } + + } + + # Write evaluation result in environment + env$eval_list <- eval_list + + # Loop through env + for (f in cb$post_iter) { + f(env) + } + + # Check for early stopping and break if needed + if (env$met_early_stop) break + + } + + # check if any valids were given other than the training data + non_train_valid_names <- names(valids)[!(names(valids) == train_data_name)] + first_valid_name <- non_train_valid_names[1L] + + # When early stopping is not activated, we compute the best iteration / score ourselves by + # selecting the first metric and the first dataset + if (record && length(non_train_valid_names) > 0L && is.na(env$best_score)) { + + # when using a custom eval function, the metric name is returned from the + # function, so figure it out from record_evals + if (!is.null(eval_functions[1L])) { + first_metric <- names(booster$record_evals[[first_valid_name]])[1L] + } else { + first_metric <- booster$.__enclos_env__$private$eval_names[1L] + } + + .find_best <- which.min + if (isTRUE(env$eval_list[[1L]]$higher_better[1L])) { + .find_best <- which.max + } + booster$best_iter <- unname( + .find_best( + unlist( + booster$record_evals[[first_valid_name]][[first_metric]][[.EVAL_KEY()]] + ) + ) + ) + booster$best_score <- booster$record_evals[[first_valid_name]][[first_metric]][[.EVAL_KEY()]][[booster$best_iter]] + } + + # Check for booster model conversion to predictor model + if (reset_data) { + + # Store temporarily model data elsewhere + booster_old <- list( + best_iter = booster$best_iter + , best_score = booster$best_score + , record_evals = booster$record_evals + ) + + # Reload model + booster <- lgb.load(model_str = booster$save_model_to_string()) + booster$best_iter <- booster_old$best_iter + booster$best_score <- booster_old$best_score + booster$record_evals <- booster_old$record_evals + + } + + if (serializable) { + booster$save_raw() + } + + return(booster) + +} diff --git a/R-package/R/lightgbm.R b/R-package/R/lightgbm.R new file mode 100644 index 0000000..4c09ed7 --- /dev/null +++ b/R-package/R/lightgbm.R @@ -0,0 +1,368 @@ +#' @name lgb_shared_params +#' @title Shared parameter docs +#' @description Parameter docs shared by \code{lgb.train}, \code{lgb.cv}, and \code{lightgbm} +#' @param callbacks List of callback functions that are applied at each iteration. +#' @param data a \code{lgb.Dataset} object, used for training. Some functions, such as \code{\link{lgb.cv}}, +#' may allow you to pass other types of data like \code{matrix} and then separately supply +#' \code{label} as a keyword argument. +#' @param early_stopping_rounds int. Activates early stopping. When this parameter is non-null, +#' training will stop if the evaluation of any metric on any validation set +#' fails to improve for \code{early_stopping_rounds} consecutive boosting rounds. +#' If training stops early, the returned model will have attribute \code{best_iter} +#' set to the iteration number of the best iteration. +#' @param eval evaluation function(s). This can be a character vector, function, or list with a mixture of +#' strings and functions. +#' +#' \itemize{ +#' \item{\bold{a. character vector}: +#' If you provide a character vector to this argument, it should contain strings with valid +#' evaluation metrics. +#' See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#metric}{ +#' The "metric" section of the documentation} +#' for a list of valid metrics. +#' } +#' \item{\bold{b. function}: +#' You can provide a custom evaluation function. This +#' should accept the keyword arguments \code{preds} and \code{dtrain} and should return a named +#' list with three elements: +#' \itemize{ +#' \item{\code{name}: A string with the name of the metric, used for printing +#' and storing results. +#' } +#' \item{\code{value}: A single number indicating the value of the metric for the +#' given predictions and true values +#' } +#' \item{ +#' \code{higher_better}: A boolean indicating whether higher values indicate a better fit. +#' For example, this would be \code{FALSE} for metrics like MAE or RMSE. +#' } +#' } +#' } +#' \item{\bold{c. list}: +#' If a list is given, it should only contain character vectors and functions. +#' These should follow the requirements from the descriptions above. +#' } +#' } +#' @param eval_freq evaluation output frequency, only effective when verbose > 0 and \code{valids} has been provided +#' @param init_model path of model file or \code{lgb.Booster} object, will continue training from this model +#' @param nrounds number of training rounds +#' @param obj objective function, can be character or custom objective function. Examples include +#' \code{regression}, \code{regression_l1}, \code{huber}, +#' \code{binary}, \code{lambdarank}, \code{multiclass}, \code{multiclass} +#' @param params a list of parameters. See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html}{ +#' the "Parameters" section of the documentation} for a list of parameters and valid values. +#' @param verbose verbosity for output, if <= 0 and \code{valids} has been provided, also will disable the +#' printing of evaluation during training +#' @param serializable whether to make the resulting objects serializable through functions such as +#' \code{save} or \code{saveRDS} (see section "Model serialization"). +#' @section Early Stopping: +#' +#' "early stopping" refers to stopping the training process if the model's performance on a given +#' validation set does not improve for several consecutive iterations. +#' +#' If multiple arguments are given to \code{eval}, their order will be preserved. If you enable +#' early stopping by setting \code{early_stopping_rounds} in \code{params}, by default all +#' metrics will be considered for early stopping. +#' +#' If you want to only consider the first metric for early stopping, pass +#' \code{first_metric_only = TRUE} in \code{params}. Note that if you also specify \code{metric} +#' in \code{params}, that metric will be considered the "first" one. If you omit \code{metric}, +#' a default metric will be used based on your choice for the parameter \code{obj} (keyword argument) +#' or \code{objective} (passed into \code{params}). +#' +#' \bold{NOTE:} if using \code{boosting_type="dart"}, any early stopping configuration will be ignored +#' and early stopping will not be performed. +#' @section Model serialization: +#' +#' LightGBM model objects can be serialized and de-serialized through functions such as \code{save} +#' or \code{saveRDS}, but similarly to libraries such as 'xgboost', serialization works a bit differently +#' from typical R objects. In order to make models serializable in R, a copy of the underlying C++ object +#' as serialized raw bytes is produced and stored in the R model object, and when this R object is +#' de-serialized, the underlying C++ model object gets reconstructed from these raw bytes, but will only +#' do so once some function that uses it is called, such as \code{predict}. In order to forcibly +#' reconstruct the C++ object after deserialization (e.g. after calling \code{readRDS} or similar), one +#' can use the function \link{lgb.restore_handle} (for example, if one makes predictions in parallel or in +#' forked processes, it will be faster to restore the handle beforehand). +#' +#' Producing and keeping these raw bytes however uses extra memory, and if they are not required, +#' it is possible to avoid producing them by passing `serializable=FALSE`. In such cases, these raw +#' bytes can be added to the model on demand through function \link{lgb.make_serializable}. +#' +#' \emph{New in version 4.0.0} +#' +#' @keywords internal +NULL + +#' @name lightgbm +#' @title Train a LightGBM model +#' @description High-level R interface to train a LightGBM model. Unlike \code{\link{lgb.train}}, this function +#' is focused on compatibility with other statistics and machine learning interfaces in R. +#' This focus on compatibility means that this interface may experience more frequent breaking API changes +#' than \code{\link{lgb.train}}. +#' For efficiency-sensitive applications, or for applications where breaking API changes across releases +#' is very expensive, use \code{\link{lgb.train}}. +#' @inheritParams lgb_shared_params +#' @param label Vector of labels, used if \code{data} is not an \code{\link{lgb.Dataset}} +#' @param weights Sample / observation weights for rows in the input data. If \code{NULL}, will assume that all +#' observations / rows have the same importance / weight. +#' +#' \emph{Changed from 'weight', in version 4.0.0} +#' +#' @param objective Optimization objective (e.g. `"regression"`, `"binary"`, etc.). +#' For a list of accepted objectives, see +#' \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#objective}{ +#' the "objective" item of the "Parameters" section of the documentation}. +#' +#' If passing \code{"auto"} and \code{data} is not of type \code{lgb.Dataset}, the objective will +#' be determined according to what is passed for \code{label}:\itemize{ +#' \item If passing a factor with two variables, will use objective \code{"binary"}. +#' \item If passing a factor with more than two variables, will use objective \code{"multiclass"} +#' (note that parameter \code{num_class} in this case will also be determined automatically from +#' \code{label}). +#' \item Otherwise (or if passing \code{lgb.Dataset} as input), will use objective \code{"regression"}. +#' } +#' +#' \emph{New in version 4.0.0} +#' +#' @param init_score initial score is the base prediction lightgbm will boost from +#' +#' \emph{New in version 4.0.0} +#' +#' @param num_threads Number of parallel threads to use. For best speed, this should be set to the number of +#' physical cores in the CPU - in a typical x86-64 machine, this corresponds to half the +#' number of maximum threads. +#' +#' Be aware that using too many threads can result in speed degradation in smaller datasets +#' (see the parameters documentation for more details). +#' +#' If passing zero, will use the default number of threads configured for OpenMP +#' (typically controlled through an environment variable \code{OMP_NUM_THREADS}). +#' +#' If passing \code{NULL} (the default), will try to use the number of physical cores in the +#' system, but be aware that getting the number of cores detected correctly requires package +#' \code{RhpcBLASctl} to be installed. +#' +#' This parameter gets overridden by \code{num_threads} and its aliases under \code{params} +#' if passed there. +#' +#' \emph{New in version 4.0.0} +#' +#' @param colnames Character vector of features. Only used if \code{data} is not an \code{\link{lgb.Dataset}}. +#' @param categorical_feature categorical features. This can either be a character vector of feature +#' names or an integer vector with the indices of the features (e.g. +#' \code{c(1L, 10L)} to say "the first and tenth columns"). +#' Only used if \code{data} is not an \code{\link{lgb.Dataset}}. +#' +#' @param ... Additional arguments passed to \code{\link{lgb.train}}. For example +#' \itemize{ +#' \item{\code{valids}: a list of \code{lgb.Dataset} objects, used for validation} +#' \item{\code{obj}: objective function, can be character or custom objective function. Examples include +#' \code{regression}, \code{regression_l1}, \code{huber}, +#' \code{binary}, \code{lambdarank}, \code{multiclass}, \code{multiclass}} +#' \item{\code{eval}: evaluation function, can be (a list of) character or custom eval function} +#' \item{\code{record}: Boolean, TRUE will record iteration message to \code{booster$record_evals}} +#' \item{\code{reset_data}: Boolean, setting it to TRUE (not the default value) will transform the booster model +#' into a predictor model which frees up memory and the original datasets} +#' } +#' @inheritSection lgb_shared_params Early Stopping +#' @return a trained \code{lgb.Booster} +#' @export +lightgbm <- function(data, + label = NULL, + weights = NULL, + params = list(), + nrounds = 100L, + verbose = 1L, + eval_freq = 1L, + early_stopping_rounds = NULL, + init_model = NULL, + callbacks = list(), + serializable = TRUE, + objective = "auto", + init_score = NULL, + num_threads = NULL, + colnames = NULL, + categorical_feature = NULL, + ...) { + + # validate inputs early to avoid unnecessary computation + if (nrounds <= 0L) { + stop("nrounds should be greater than zero") + } + + if (is.null(num_threads)) { + num_threads <- .get_default_num_threads() + } + params <- .check_wrapper_param( + main_param_name = "num_threads" + , params = params + , alternative_kwarg_value = num_threads + ) + params <- .check_wrapper_param( + main_param_name = "verbosity" + , params = params + , alternative_kwarg_value = verbose + ) + + # Process factors as labels and auto-determine objective + if (!.is_Dataset(data)) { + data_processor <- DataProcessor$new() + temp <- data_processor$process_label( + label = label + , objective = objective + , params = params + ) + label <- temp$label + objective <- temp$objective + params <- temp$params + rm(temp) + } else { + data_processor <- NULL + if (objective == "auto") { + objective <- "regression" + } + } + + # Set data to a temporary variable + dtrain <- data + + # Check whether data is lgb.Dataset, if not then create lgb.Dataset manually + if (!.is_Dataset(x = dtrain)) { + dtrain <- lgb.Dataset( + data = data + , label = label + , weight = weights + , init_score = init_score + , categorical_feature = categorical_feature + , colnames = colnames + ) + } + + train_args <- list( + "params" = params + , "data" = dtrain + , "nrounds" = nrounds + , "obj" = objective + , "verbose" = params[["verbosity"]] + , "eval_freq" = eval_freq + , "early_stopping_rounds" = early_stopping_rounds + , "init_model" = init_model + , "callbacks" = callbacks + , "serializable" = serializable + ) + train_args <- append(train_args, list(...)) + + if (! "valids" %in% names(train_args)) { + train_args[["valids"]] <- list() + } + + # Train a model using the regular way + bst <- do.call( + what = lgb.train + , args = train_args + ) + bst$data_processor <- data_processor + + return(bst) +} + +#' @name agaricus.train +#' @title Training part from Mushroom Data Set +#' @description This data set is originally from the Mushroom data set, +#' UCI Machine Learning Repository. +#' This data set includes the following fields: +#' +#' \itemize{ +#' \item{\code{label}: the label for each record} +#' \item{\code{data}: a sparse Matrix of \code{dgCMatrix} class, with 126 columns.} +#' } +#' +#' @references +#' https://archive.ics.uci.edu/ml/datasets/Mushroom +#' +#' Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository +#' [https://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +#' School of Information and Computer Science. +#' +#' @docType data +#' @keywords datasets +#' @usage data(agaricus.train) +#' @format A list containing a label vector, and a dgCMatrix object with 6513 +#' rows and 127 variables +NULL + +#' @name agaricus.test +#' @title Test part from Mushroom Data Set +#' @description This data set is originally from the Mushroom data set, +#' UCI Machine Learning Repository. +#' This data set includes the following fields: +#' +#' \itemize{ +#' \item{\code{label}: the label for each record} +#' \item{\code{data}: a sparse Matrix of \code{dgCMatrix} class, with 126 columns.} +#' } +#' @references +#' https://archive.ics.uci.edu/ml/datasets/Mushroom +#' +#' Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository +#' [https://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +#' School of Information and Computer Science. +#' +#' @docType data +#' @keywords datasets +#' @usage data(agaricus.test) +#' @format A list containing a label vector, and a dgCMatrix object with 1611 +#' rows and 126 variables +NULL + +#' @name bank +#' @title Bank Marketing Data Set +#' @description This data set is originally from the Bank Marketing data set, +#' UCI Machine Learning Repository. +#' +#' It contains only the following: bank.csv with 10% of the examples and 17 inputs, +#' randomly selected from 3 (older version of this dataset with less inputs). +#' +#' @references +#' https://archive.ics.uci.edu/ml/datasets/Bank+Marketing +#' +#' S. Moro, P. Cortez and P. Rita. (2014) +#' A Data-Driven Approach to Predict the Success of Bank Telemarketing. Decision Support Systems +#' +#' @docType data +#' @keywords datasets +#' @usage data(bank) +#' @format A data.table with 4521 rows and 17 variables +NULL + +# Various imports +#' @import methods +#' @importFrom Matrix Matrix +#' @importFrom R6 R6Class +#' @useDynLib lightgbm , .registration = TRUE +NULL + +# Suppress false positive warnings from R CMD CHECK about +# "unrecognized global variable" +globalVariables(c( + "." + , ".N" + , ".SD" + , "abs_contribution" + , "bar_color" + , "Contribution" + , "Cover" + , "Feature" + , "Frequency" + , "Gain" + , "internal_count" + , "internal_value" + , "leaf_index" + , "leaf_parent" + , "leaf_value" + , "node_parent" + , "split_feature" + , "split_gain" + , "split_index" + , "tree_index" +)) diff --git a/R-package/R/metrics.R b/R-package/R/metrics.R new file mode 100644 index 0000000..12a8047 --- /dev/null +++ b/R-package/R/metrics.R @@ -0,0 +1,38 @@ +# [description] List of metrics known to LightGBM. The most up to date list can be found +# at https://lightgbm.readthedocs.io/en/latest/Parameters.html#metric-parameters +# +# [return] A named logical vector, where each key is a metric name and each value is a boolean. +# TRUE if higher values of the metric are desirable, FALSE if lower values are desirable. +# Note that only the 'main' metrics are stored here, not aliases, since only the 'main' metrics +# are returned from the C++ side. For example, if you use `metric = "mse"` in your code, +# the metric name `"l2"` will be returned. +.METRICS_HIGHER_BETTER <- function() { + return( + c( + "l1" = FALSE + , "l2" = FALSE + , "mape" = FALSE + , "rmse" = FALSE + , "quantile" = FALSE + , "huber" = FALSE + , "fair" = FALSE + , "poisson" = FALSE + , "gamma" = FALSE + , "gamma_deviance" = FALSE + , "tweedie" = FALSE + , "ndcg" = TRUE + , "map" = TRUE + , "auc" = TRUE + , "average_precision" = TRUE + , "r2" = TRUE + , "binary_logloss" = FALSE + , "binary_error" = FALSE + , "auc_mu" = TRUE + , "multi_logloss" = FALSE + , "multi_error" = FALSE + , "cross_entropy" = FALSE + , "cross_entropy_lambda" = FALSE + , "kullback_leibler" = FALSE + ) + ) +} diff --git a/R-package/R/multithreading.R b/R-package/R/multithreading.R new file mode 100644 index 0000000..a8d6b51 --- /dev/null +++ b/R-package/R/multithreading.R @@ -0,0 +1,51 @@ +#' @name setLGBMThreads +#' @title Set maximum number of threads used by LightGBM +#' @description LightGBM attempts to speed up many operations by using multi-threading. +#' The number of threads used in those operations can be controlled via the +#' \code{num_threads} parameter passed through \code{params} to functions like +#' \link{lgb.train} and \link{lgb.Dataset}. However, some operations (like materializing +#' a model from a text file) are done via code paths that don't explicitly accept thread-control +#' configuration. +#' +#' Use this function to set the maximum number of threads LightGBM will use for such operations. +#' +#' This function affects all LightGBM operations in the same process. +#' +#' So, for example, if you call \code{setLGBMthreads(4)}, no other multi-threaded LightGBM +#' operation in the same process will use more than 4 threads. +#' +#' Call \code{setLGBMthreads(-1)} to remove this limitation. +#' @param num_threads maximum number of threads to be used by LightGBM in multi-threaded operations +#' @return NULL +#' @seealso \link{getLGBMthreads} +#' @export +setLGBMthreads <- function(num_threads) { + .Call( + LGBM_SetMaxThreads_R, + num_threads + ) + return(invisible(NULL)) +} + +#' @name getLGBMThreads +#' @title Get default number of threads used by LightGBM +#' @description LightGBM attempts to speed up many operations by using multi-threading. +#' The number of threads used in those operations can be controlled via the +#' \code{num_threads} parameter passed through \code{params} to functions like +#' \link{lgb.train} and \link{lgb.Dataset}. However, some operations (like materializing +#' a model from a text file) are done via code paths that don't explicitly accept thread-control +#' configuration. +#' +#' Use this function to see the default number of threads LightGBM will use for such operations. +#' @return number of threads as an integer. \code{-1} means that in situations where parameter \code{num_threads} is +#' not explicitly supplied, LightGBM will choose a number of threads to use automatically. +#' @seealso \link{setLGBMthreads} +#' @export +getLGBMthreads <- function() { + out <- 0L + .Call( + LGBM_GetMaxThreads_R, + out + ) + return(out) +} diff --git a/R-package/R/utils.R b/R-package/R/utils.R new file mode 100644 index 0000000..17e6106 --- /dev/null +++ b/R-package/R/utils.R @@ -0,0 +1,260 @@ +.is_Booster <- function(x) { + return(all(c("R6", "lgb.Booster") %in% class(x))) # nolint: class_equals. +} + +.is_Dataset <- function(x) { + return(all(c("R6", "lgb.Dataset") %in% class(x))) # nolint: class_equals. +} + +.is_Predictor <- function(x) { + return(all(c("R6", "lgb.Predictor") %in% class(x))) # nolint: class_equals. +} + +.is_null_handle <- function(x) { + if (is.null(x)) { + return(TRUE) + } + return( + isTRUE(.Call(LGBM_HandleIsNull_R, x)) + ) +} + +.params2str <- function(params) { + + if (!identical(class(params), "list")) { + stop("params must be a list") + } + + names(params) <- gsub(".", "_", names(params), fixed = TRUE) + param_names <- names(params) + ret <- list() + + # Perform key value join + for (i in seq_along(params)) { + + # If a parameter has multiple values, join those values together with commas. + # trimws() is necessary because format() will pad to make strings the same width + val <- paste( + trimws( + format( + x = unname(params[[i]]) + , scientific = FALSE + ) + ) + , collapse = "," + ) + if (nchar(val) <= 0L) next # Skip join + + # Join key value + pair <- paste(c(param_names[[i]], val), collapse = "=") + ret <- c(ret, pair) + + } + + if (length(ret) == 0L) { + return("") + } + + return(paste(ret, collapse = " ")) + +} + +# [description] +# +# Besides applying checks, this function +# +# 1. turns feature *names* into 1-based integer positions, then +# 2. adds an extra list element with skipped features, then +# 3. turns 1-based integer positions into 0-based positions, and finally +# 4. collapses the values of each list element into a string like "[0, 1]". +# +.check_interaction_constraints <- function(interaction_constraints, column_names) { + if (is.null(interaction_constraints)) { + return(list()) + } + if (!identical(class(interaction_constraints), "list")) { + stop("interaction_constraints must be a list") + } + + column_indices <- seq_along(column_names) + + # Convert feature names to 1-based integer positions and apply checks + for (j in seq_along(interaction_constraints)) { + constraint <- interaction_constraints[[j]] + + if (is.character(constraint)) { + constraint_indices <- match(constraint, column_names) + } else if (is.numeric(constraint)) { + constraint_indices <- as.integer(constraint) + } else { + stop("every element in interaction_constraints must be a character vector or numeric vector") + } + + # Features outside range? + bad <- !(constraint_indices %in% column_indices) + if (any(bad)) { + stop( + "unknown feature(s) in interaction_constraints: " + , toString(sQuote(constraint[bad], q = FALSE)) + ) + } + + interaction_constraints[[j]] <- constraint_indices + } + + # Add missing features as new interaction set + remaining_indices <- setdiff( + column_indices, sort(unique(unlist(interaction_constraints))) + ) + if (length(remaining_indices) > 0L) { + interaction_constraints <- c( + interaction_constraints, list(remaining_indices) + ) + } + + # Turn indices 0-based and convert to string + for (j in seq_along(interaction_constraints)) { + interaction_constraints[[j]] <- paste0( + "[", paste(interaction_constraints[[j]] - 1L, collapse = ","), "]" + ) + } + return(interaction_constraints) +} + + +# [description] +# Take any character values from eval and store them in params$metric. +# This has to account for the fact that `eval` could be a character vector, +# a function, a list of functions, or a list with a mix of strings and +# functions +.check_eval <- function(params, eval) { + + if (is.null(params$metric)) { + params$metric <- list() + } else if (is.character(params$metric)) { + params$metric <- as.list(params$metric) + } + + # if 'eval' is a character vector or list, find the character + # elements and add them to 'metric' + if (!is.function(eval)) { + for (i in seq_along(eval)) { + element <- eval[[i]] + if (is.character(element)) { + params$metric <- append(params$metric, element) + } + } + } + + # If more than one character metric was given, then "None" should + # not be included + if (length(params$metric) > 1L) { + params$metric <- Filter( + f = function(metric) { + !(metric %in% .NO_METRIC_STRINGS()) + } + , x = params$metric + ) + } + + # duplicate metrics should be filtered out + params$metric <- as.list(unique(unlist(params$metric))) + + return(params) +} + + +# [description] +# +# Resolve differences between passed-in keyword arguments, parameters, +# and parameter aliases. This function exists because some functions in the +# package take in parameters through their own keyword arguments other than +# the `params` list. +# +# If the same underlying parameter is provided multiple +# ways, the first item in this list is used: +# +# 1. the main (non-alias) parameter found in `params` +# 2. the alias with the highest priority found in `params` +# 3. the keyword argument passed in +# +# For example, "num_iterations" can also be provided to lgb.train() +# via keyword "nrounds". lgb.train() will choose one value for this parameter +# based on the first match in this list: +# +# 1. params[["num_iterations]] +# 2. the highest priority alias of "num_iterations" found in params +# 3. the nrounds keyword argument +# +# If multiple aliases are found in `params` for the same parameter, they are +# all removed before returning `params`. +# +# [return] +# params with num_iterations set to the chosen value, and other aliases +# of num_iterations removed +.check_wrapper_param <- function(main_param_name, params, alternative_kwarg_value) { + + aliases <- .PARAMETER_ALIASES()[[main_param_name]] + aliases_provided <- aliases[aliases %in% names(params)] + aliases_provided <- aliases_provided[aliases_provided != main_param_name] + + # prefer the main parameter + if (!is.null(params[[main_param_name]])) { + for (param in aliases_provided) { + params[[param]] <- NULL + } + return(params) + } + + # if the main parameter wasn't provided, prefer the first alias + if (length(aliases_provided) > 0L) { + first_param <- aliases_provided[1L] + params[[main_param_name]] <- params[[first_param]] + for (param in aliases_provided) { + params[[param]] <- NULL + } + return(params) + } + + # if not provided in params at all, use the alternative value provided + # through a keyword argument from lgb.train(), lgb.cv(), etc. + params[[main_param_name]] <- alternative_kwarg_value + return(params) +} + +#' @importFrom parallel detectCores +.get_default_num_threads <- function() { + if (requireNamespace("RhpcBLASctl", quietly = TRUE)) { # nolint: undesirable_function. + return(RhpcBLASctl::get_num_cores()) + } else { + msg <- "Optional package 'RhpcBLASctl' not found." + cores <- 0L + if (Sys.info()["sysname"] != "Linux") { + cores <- parallel::detectCores(logical = FALSE) + if (is.na(cores) || cores < 0L) { + cores <- 0L + } + } + if (cores == 0L) { + msg <- paste(msg, "Will use default number of OpenMP threads.", sep = " ") + } else { + msg <- paste(msg, "Detection of CPU cores might not be accurate.", sep = " ") + } + warning(msg) + return(cores) + } +} + +.equal_or_both_null <- function(a, b) { + if (is.null(a)) { + if (!is.null(b)) { + return(FALSE) + } + return(TRUE) + } else { + if (is.null(b)) { + return(FALSE) + } + return(a == b) + } +} diff --git a/R-package/README.md b/R-package/README.md new file mode 100644 index 0000000..6a40abf --- /dev/null +++ b/R-package/README.md @@ -0,0 +1,508 @@ +# LightGBM R-package + +[![CRAN Version](https://www.r-pkg.org/badges/version/lightgbm)](https://cran.r-project.org/package=lightgbm) +[![Downloads](https://cranlogs.r-pkg.org/badges/grand-total/lightgbm)](https://cran.r-project.org/package=lightgbm) +[![API Docs](https://readthedocs.org/projects/lightgbm/badge/?version=latest)](https://lightgbm.readthedocs.io/en/latest/R/reference/) + + + +### Contents + +* [Installation](#installation) + - [Installing the CRAN Package](#installing-the-cran-package) + - [Installing from Source with CMake](#install) + - [Installing a GPU-enabled Build](#installing-a-gpu-enabled-build) + - [Installing Precompiled Binaries](#installing-precompiled-binaries) + - [Installing from a Pre-compiled lib_lightgbm](#lib_lightgbm) +* [Examples](#examples) +* [Testing](#testing) + - [Running the Tests](#running-the-tests) + - [Code Coverage](#code-coverage) +* [Updating Documentation](#updating-documentation) +* [Preparing a CRAN Package](#preparing-a-cran-package) +* [Known Issues](#known-issues) + +Installation +------------ + +For the easiest installation, go to ["Installing the CRAN package"](#installing-the-cran-package). + +If you experience any issues with that, try ["Installing from Source with CMake"](#install). This can produce a more efficient version of the library on Windows systems with Visual Studio. + +To build a GPU-enabled version of the package, follow the steps in ["Installing a GPU-enabled Build"](#installing-a-gpu-enabled-build). + +If any of the above options do not work for you or do not meet your needs, please let the maintainers know by [opening an issue](https://github.com/lightgbm-org/LightGBM/issues). + +When your package installation is done, you can check quickly if your LightGBM R-package is working by running the following: + +```r +library(lightgbm) +data(agaricus.train, package='lightgbm') +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +model <- lgb.cv( + params = list( + objective = "regression" + , metric = "l2" + ) + , data = dtrain +) +``` + +### Installing the CRAN package + +`{lightgbm}` is [available on CRAN](https://cran.r-project.org/package=lightgbm), and can be installed with the following R code. + +```r +install.packages("lightgbm", repos = "https://cran.r-project.org") +``` + +This is the easiest way to install `{lightgbm}`. It does not require `CMake` or `Visual Studio`, and should work well on many different operating systems and compilers. + +Each CRAN package is also available on [LightGBM releases](https://github.com/lightgbm-org/LightGBM/releases), with a name like `lightgbm-{VERSION}-r-cran.tar.gz`. + +#### Custom Installation (Linux, Mac) + +The steps above should work on most systems, but users with highly-customized environments might want to change how R builds packages from source. + +To change the compiler used when installing the CRAN package, you can create a file `~/.R/Makevars` which overrides `CC` (`C` compiler) and `CXX` (`C++` compiler). + +For example, to use `gcc-14` instead of `clang` on macOS, you could use something like the following: + +```make +# ~/.R/Makevars +CC=gcc-14 +CC17=gcc-14 +CXX=g++-14 +CXX17=g++-14 +``` + +To check the values R is using, run the following: + +```shell +R CMD config --all +``` + +### Installing from Source with CMake + +You need to install git and [CMake](https://cmake.org/) first. + +Note: this method is only supported on 64-bit systems. If you need to run LightGBM on 32-bit Windows (i386), follow the instructions in ["Installing the CRAN Package"](#installing-the-cran-package). + +#### Windows Preparation + +NOTE: Windows users may need to run with administrator rights (either R or the command prompt, depending on the way you are installing this package). + +Installing a 64-bit version of [Rtools](https://cran.r-project.org/bin/windows/Rtools/) is mandatory. + +After installing `Rtools` and `CMake`, be sure the following paths are added to the environment variable `PATH`. These may have been automatically added when installing other software. + +* `Rtools` + - If you have `Rtools` 4.0, example: + - `C:\rtools40\mingw64\bin` + - `C:\rtools40\usr\bin` + - If you have `Rtools` 4.2+, example: + - `C:\rtools42\x86_64-w64-mingw32.static.posix\bin` + - `C:\rtools42\usr\bin` + - **NOTE**: this is e.g. `rtools43\` for R 4.3 +* `CMake` + - example: `C:\Program Files\CMake\bin` +* `R` + - example: `C:\Program Files\R\R-4.5.1\bin` + +NOTE: Two `Rtools` paths are required from `Rtools` 4.0 onwards because paths and the list of included software was changed in `Rtools` 4.0. + +NOTE: `Rtools42` and later take a very different approach to the compiler toolchain than previous releases, and how you install it changes what is required to build packages. See ["Howto: Building R 4.2 and packages on Windows"](https://cran.r-project.org/bin/windows/base/howto-R-4.2.html). + +#### Windows Toolchain Options + +A "toolchain" refers to the collection of software used to build the library. The R-package can be built with three different toolchains. + +**Warning for Windows users**: it is recommended to use *Visual Studio* for its better multi-threading efficiency in Windows for many core systems. For very simple systems (dual core computers or worse), MinGW64 is recommended for maximum performance. If you do not know what to choose, it is recommended to use [Visual Studio](https://visualstudio.microsoft.com/downloads/), the default compiler. **Do not try using MinGW in Windows on many core systems. It may result in 10x slower results than Visual Studio.** + +**Visual Studio (default)** + +By default, the package will be built with [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/). + +**MSYS2 (R 4.x)** + +If you are using R 4.x and installation fails with Visual Studio, `LightGBM` will fall back to using [MSYS2](https://www.msys2.org/). This should work with the tools already bundled in `Rtools` 4.0. + +If you want to force `LightGBM` to use MSYS2 (for any R version), pass `--use-msys2` to the installation script. + +```shell +Rscript build_r.R --use-msys2 +``` + +**MinGW** + +If you want to force `LightGBM` to use [MinGW](https://www.mingw-w64.org/) (for any R version), pass `--use-mingw` to the installation script. + +```shell +Rscript build_r.R --use-mingw +``` + +#### Mac OS Preparation + +You can perform installation either with **Apple Clang** or **gcc**. In case you prefer **Apple Clang**, you should install **OpenMP** (details for installation can be found in [Installation Guide](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Installation-Guide.rst#apple-clang)) first. In case you prefer **gcc**, you need to install it (details for installation can be found in [Installation Guide](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Installation-Guide.rst#gcc)) and set some environment variables to tell R to use `gcc` and `g++`. If you install these from Homebrew, your versions of `g++` and `gcc` are most likely in `/usr/local/bin`, as shown below. + +``` +# replace 8 with version of gcc installed on your machine +export CXX=/usr/local/bin/g++-8 CC=/usr/local/bin/gcc-8 +``` + +#### Install with CMake + +After following the "preparation" steps above for your operating system, build and install the R-package with the following commands: + +```sh +git clone --recursive https://github.com/lightgbm-org/LightGBM +cd LightGBM +Rscript build_r.R +``` + +The `build_r.R` script builds the package in a temporary directory called `lightgbm_r`. It will destroy and recreate that directory each time you run the script. That script supports the following command-line options: + +- `--no-build-vignettes`: Skip building vignettes. +- `-j[jobs]`: Number of threads to use when compiling LightGBM. E.g., `-j4` will try to compile 4 objects at a time. + - by default, this script uses single-thread compilation + - for best results, set `-j` to the number of physical CPUs +- `--skip-install`: Build the package tarball, but do not install it. +- `--use-gpu`: Build a GPU-enabled version of the library. +- `--use-mingw`: Force the use of MinGW toolchain, regardless of R version. +- `--use-msys2`: Force the use of MSYS2 toolchain, regardless of R version. + +Note: for the build with Visual Studio/VS Build Tools in Windows, you should use the Windows CMD or PowerShell. + +### Installing a GPU-enabled Build + +You will need to install Boost and OpenCL first: details for installation can be found in [Installation-Guide](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Installation-Guide.rst#build-gpu-version). + +After installing these other libraries, follow the steps in ["Installing from Source with CMake"](#install). When you reach the step that mentions `build_r.R`, pass the flag `--use-gpu`. + +```shell +Rscript build_r.R --use-gpu +``` + +You may also need or want to provide additional configuration, depending on your setup. For example, you may need to provide locations for Boost and OpenCL. + +```shell +Rscript build_r.R \ + --use-gpu \ + --opencl-library=/usr/lib/x86_64-linux-gnu/libOpenCL.so \ + --boost-librarydir=/usr/lib/x86_64-linux-gnu +``` + +The following options correspond to the [CMake FindBoost options](https://cmake.org/cmake/help/latest/module/FindBoost.html) by the same names. + +* `--boost-root` +* `--boost-dir` +* `--boost-include-dir` +* `--boost-librarydir` + +The following options correspond to the [CMake FindOpenCL options](https://cmake.org/cmake/help/latest/module/FindOpenCL.html) by the same names. + +* `--opencl-include-dir` +* `--opencl-library` + +### Installing Precompiled Binaries + +Precompiled binaries for Mac and Windows are prepared by CRAN a few days after each release to CRAN. They can be installed with the following R code. + +```r +install.packages( + "lightgbm" + , type = "both" + , repos = "https://cran.r-project.org" +) +``` + +These packages do not require compilation, so they will be faster and easier to install than packages that are built from source. + +CRAN does not prepare precompiled binaries for Linux, and as of this writing neither does this project. + +### Installing from a Pre-compiled lib_lightgbm + +Previous versions of LightGBM offered the ability to first compile the C++ library (`lib_lightgbm.{dll,dylib,so}`) and then build an R-package that wraps it. + +As of version 3.0.0, this is no longer supported. If building from source is difficult for you, please [open an issue](https://github.com/lightgbm-org/LightGBM/issues). + +Examples +-------- + +Please visit [demo](https://github.com/lightgbm-org/LightGBM/tree/main/R-package/demo): + +* [Basic walkthrough of wrappers](https://github.com/lightgbm-org/LightGBM/blob/main/R-package/demo/basic_walkthrough.R) +* [Boosting from existing prediction](https://github.com/lightgbm-org/LightGBM/blob/main/R-package/demo/boost_from_prediction.R) +* [Early Stopping](https://github.com/lightgbm-org/LightGBM/blob/main/R-package/demo/early_stopping.R) +* [Cross Validation](https://github.com/lightgbm-org/LightGBM/blob/main/R-package/demo/cross_validation.R) +* [Multiclass Training/Prediction](https://github.com/lightgbm-org/LightGBM/blob/main/R-package/demo/multiclass.R) +* [Leaf (in)Stability](https://github.com/lightgbm-org/LightGBM/blob/main/R-package/demo/leaf_stability.R) +* [Weight-Parameter Adjustment Relationship](https://github.com/lightgbm-org/LightGBM/blob/main/R-package/demo/weight_param.R) + +Testing +------- + +The R-package's unit tests are run automatically on every commit, via integrations like [GitHub Actions](https://github.com/lightgbm-org/LightGBM/actions). Adding new tests in `R-package/tests/testthat` is a valuable way to improve the reliability of the R-package. + +### Running the Tests + +While developing the R-package, run the code below to run the unit tests. + +```shell +sh build-cran-package.sh \ + --no-build-vignettes + +R CMD INSTALL --with-keep.source lightgbm*.tar.gz +cd R-package/tests +Rscript testthat.R +``` + +To run the tests with more verbose logs, set environment variable `LIGHTGBM_TEST_VERBOSITY` to a valid value for parameter [`verbosity`](https://lightgbm.readthedocs.io/en/latest/Parameters.html#verbosity). + +```shell +export LIGHTGBM_TEST_VERBOSITY=1 +cd R-package/tests +Rscript testthat.R +``` + +### Code Coverage + +When adding tests, you may want to use test coverage to identify untested areas and to check if the tests you've added are covering all branches of the intended code. + +The example below shows how to generate code coverage for the R-package on a macOS or Linux setup. To adjust for your environment, refer to [the customization step described above](#custom-installation-linux-mac). + +```shell +# Install +sh build-cran-package.sh \ + --no-build-vignettes + +# Get coverage +Rscript -e " \ + library(covr); + coverage <- covr::package_coverage('./lightgbm_r', type = 'tests', quiet = FALSE); + print(coverage); + covr::report(coverage, file = file.path(getwd(), 'coverage.html'), browse = TRUE); + " +``` + +Updating Documentation +---------------------- + +The R-package uses [`{roxygen2}`](https://CRAN.R-project.org/package=roxygen2) to generate its documentation. +The generated `DESCRIPTION`, `NAMESPACE`, and `man/` files are checked into source control. +To regenerate those files, run the following. + +```shell +Rscript \ + --vanilla \ + -e "install.packages('roxygen2', repos = 'https://cran.rstudio.com')" + +sh build-cran-package.sh --no-build-vignettes +R CMD INSTALL \ + --with-keep.source \ + ./lightgbm_*.tar.gz + +cd R-package +Rscript \ + --vanilla \ + -e "roxygen2::roxygenize(load = 'installed')" +``` + +Preparing a CRAN Package +------------------------ + +This section is primarily for maintainers, but may help users and contributors to understand the structure of the R-package. + +Most of `LightGBM` uses `CMake` to handle tasks like setting compiler and linker flags, including header file locations, and linking to other libraries. Because CRAN packages typically do not assume the presence of `CMake`, the R-package uses an alternative method that is in the CRAN-supported toolchain for building R packages with C++ code: `Autoconf`. + +For more information on this approach, see ["Writing R Extensions"](https://cran.r-project.org/doc/manuals/r-release/R-exts.html#Configure-and-cleanup). + +### Build a CRAN Package + +From the root of the repository, run the following. + +```shell +git submodule update --init --recursive +sh build-cran-package.sh +``` + +This will create a file `lightgbm_${VERSION}.tar.gz`, where `VERSION` is the version of `LightGBM`. + +That script supports the following command-line options: + +- `--no-build-vignettes`: Skip building vignettes. +- `--r-executable=[path-to-executable]`: Use an alternative build of R. + +### Standard Installation from CRAN Package + +After building the package, install it with a command like the following: + +```shell +R CMD install lightgbm_*.tar.gz +``` + +### Changing the CRAN Package + +A lot of details are handled automatically by `R CMD build` and `R CMD install`, so it can be difficult to understand how the files in the R-package are related to each other. An extensive treatment of those details is available in ["Writing R Extensions"](https://cran.r-project.org/doc/manuals/r-release/R-exts.html). + +This section briefly explains the key files for building a CRAN package. To update the package, edit the files relevant to your change and re-run the steps in [Build a CRAN Package](#build-a-cran-package). + +**Linux or Mac** + +At build time, `configure` will be run and used to create a file `Makevars`, using `Makevars.in` as a template. + +1. Edit `configure.ac`. +2. Create `configure` with `autoconf`. Do not edit it by hand. This file must be generated on Ubuntu 22.04. + + If you have an Ubuntu 22.04 environment available, run the provided script from the root of the `LightGBM` repository. + + ```shell + ./R-package/recreate-configure.sh + ``` + + If you do not have easy access to an Ubuntu 22.04 environment, the `configure` script can be generated using Docker by running the code below from the root of this repo. + + ```shell + docker run \ + --rm \ + -v $(pwd):/opt/LightGBM \ + -w /opt/LightGBM \ + ubuntu:22.04 \ + ./R-package/recreate-configure.sh + ``` + + The version of `autoconf` used by this project is stored in `R-package/AUTOCONF_UBUNTU_VERSION`. To update that version, update that file and run the commands above. To see available versions, see https://packages.ubuntu.com/search?keywords=autoconf. + +3. Edit `src/Makevars.in`. + +Alternatively, GitHub Actions can re-generate this file for you. + +1. navigate to https://github.com/lightgbm-org/LightGBM/actions/workflows/r_configure.yml +2. click "Run workflow" (drop-down) +3. enter the branch from the pull request for the `pr-branch` input +4. click "Run workflow" (button) + +**Configuring for Windows** + +At build time, `configure.win` will be run and used to create a file `Makevars.win`, using `Makevars.win.in` as a template. + +1. Edit `configure.win` directly. +2. Edit `src/Makevars.win.in`. + +### Testing the CRAN Package + +`{lightgbm}` is tested automatically on every commit, across many combinations of operating system, R version, and compiler. This section describes how to test the package locally while you are developing. + +#### Windows, Mac, and Linux + +```shell +sh build-cran-package.sh +R CMD check --as-cran lightgbm_*.tar.gz +``` + +#### ASAN and UBSAN + +All packages uploaded to CRAN must pass builds using `gcc` and `clang`, instrumented with two sanitizers: the Address Sanitizer (ASAN) and the Undefined Behavior Sanitizer (UBSAN). + +For more background, see + +* [this blog post](https://dirk.eddelbuettel.com/code/sanitizers.html) +* [top-level CRAN documentation on these checks](https://cran.r-project.org/web/checks/check_issue_kinds.html) +* [CRAN's configuration of these checks](https://www.stats.ox.ac.uk/pub/bdr/memtests/README.txt) + +You can replicate these checks locally using Docker. +For more information on the image used for testing, see https://github.com/wch/r-debug. + +In the code below, environment variable `R_CUSTOMIZATION` should be set to one of two values. + +* `"san"` = replicates CRAN's `gcc-ASAN` and `gcc-UBSAN` checks +* `"csan"` = replicates CRAN's `clang-ASAN` and `clang-UBSAN` checks + +```shell +docker run \ + --rm \ + -it \ + -v $(pwd):/opt/LightGBM \ + -w /opt/LightGBM \ + --env R_CUSTOMIZATION=san \ + wch1/r-debug:latest \ + /bin/bash + +# install dependencies +RDscript${R_CUSTOMIZATION} \ + -e "install.packages(c('R6', 'data.table', 'jsonlite', 'knitr', 'markdown', 'Matrix', 'RhpcBLASctl', 'testthat'), repos = 'https://cran.r-project.org', Ncpus = parallel::detectCores())" + +# install lightgbm +sh build-cran-package.sh --r-executable=RD${R_CUSTOMIZATION} +RD${R_CUSTOMIZATION} \ + CMD INSTALL lightgbm_*.tar.gz + +# run tests +cd R-package/tests +rm -f ./tests.log +RDscript${R_CUSTOMIZATION} testthat.R >> tests.log 2>&1 + +# check that tests passed +echo "test exit code: $?" +tail -300 ./tests.log +``` + +#### Valgrind + +All packages uploaded to CRAN must be built and tested without raising any issues from `valgrind`. `valgrind` is a profiler that can catch serious issues like memory leaks and illegal writes. For more information, see [this blog post](https://reside-ic.github.io/blog/debugging-and-fixing-crans-additional-checks-errors/). + +You can replicate these checks locally using Docker. Note that instrumented versions of R built to use `valgrind` run much slower, and these tests may take as long as 20 minutes to run. + +```shell +docker run \ + --rm \ + -v $(pwd):/opt/LightGBM \ + -w /opt/LightGBM \ + -it \ + wch1/r-debug + +RDscriptvalgrind -e "install.packages(c('R6', 'data.table', 'jsonlite', 'knitr', 'markdown', 'Matrix', 'RhpcBLASctl', 'testthat'), repos = 'https://cran.rstudio.com', Ncpus = parallel::detectCores())" + +sh build-cran-package.sh \ + --r-executable=RDvalgrind + +RDvalgrind CMD INSTALL \ + --preclean \ + --install-tests \ + lightgbm_*.tar.gz + +cd R-package/tests + +RDvalgrind \ + --no-readline \ + --vanilla \ + -d "valgrind --tool=memcheck --leak-check=full --track-origins=yes" \ + -f testthat.R \ +2>&1 \ +| tee out.log \ +| cat +``` + +These tests can also be triggered on a pull request branch, using GitHub Actions. + +1. navigate to https://github.com/lightgbm-org/LightGBM/actions/workflows/r_valgrind.yml +2. click "Run workflow" (drop-down) +3. enter the branch from the pull request for the `pr-branch` input +4. enter the pull request ID for the `pr-number` input +5. click "Run workflow" (button) + +Or by using the GitHub CLI, using a command similar to this: + +```shell +gh workflow run \ + --repo lightgbm-org/LightGBM \ + r_valgrind.yml \ + -f pr-branch=ci/fix-rerun-workflow \ + -f pr-number=7072 +``` + +Known Issues +------------ + +For information about known issues with the R-package, see the [R-package section of LightGBM's main FAQ page](https://lightgbm.readthedocs.io/en/latest/FAQ.html#r-package). diff --git a/R-package/cleanup b/R-package/cleanup new file mode 100755 index 0000000..aa84af6 --- /dev/null +++ b/R-package/cleanup @@ -0,0 +1,6 @@ +#!/bin/sh +rm -f aclocal.m4 +rm -rf ./autom4te.cache +rm -f config.log +rm -f config.status +rm -f src/Makevars diff --git a/R-package/configure b/R-package/configure new file mode 100755 index 0000000..673a785 --- /dev/null +++ b/R-package/configure @@ -0,0 +1,3072 @@ +#! /bin/sh +# Guess values for system-dependent variables and create Makefiles. +# Generated by GNU Autoconf 2.71 for lightgbm 4.6.0.99. +# +# +# Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation, +# Inc. +# +# +# This configure script is free software; the Free Software Foundation +# gives unlimited permission to copy, distribute and modify it. +## -------------------- ## +## M4sh Initialization. ## +## -------------------- ## + +# Be more Bourne compatible +DUALCASE=1; export DUALCASE # for MKS sh +as_nop=: +if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 +then : + emulate sh + NULLCMD=: + # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which + # is contrary to our usage. Disable this feature. + alias -g '${1+"$@"}'='"$@"' + setopt NO_GLOB_SUBST +else $as_nop + case `(set -o) 2>/dev/null` in #( + *posix*) : + set -o posix ;; #( + *) : + ;; +esac +fi + + + +# Reset variables that may have inherited troublesome values from +# the environment. + +# IFS needs to be set, to space, tab, and newline, in precisely that order. +# (If _AS_PATH_WALK were called with IFS unset, it would have the +# side effect of setting IFS to empty, thus disabling word splitting.) +# Quoting is to prevent editors from complaining about space-tab. +as_nl=' +' +export as_nl +IFS=" "" $as_nl" + +PS1='$ ' +PS2='> ' +PS4='+ ' + +# Ensure predictable behavior from utilities with locale-dependent output. +LC_ALL=C +export LC_ALL +LANGUAGE=C +export LANGUAGE + +# We cannot yet rely on "unset" to work, but we need these variables +# to be unset--not just set to an empty or harmless value--now, to +# avoid bugs in old shells (e.g. pre-3.0 UWIN ksh). This construct +# also avoids known problems related to "unset" and subshell syntax +# in other old shells (e.g. bash 2.01 and pdksh 5.2.14). +for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH +do eval test \${$as_var+y} \ + && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : +done + +# Ensure that fds 0, 1, and 2 are open. +if (exec 3>&0) 2>/dev/null; then :; else exec 0&1) 2>/dev/null; then :; else exec 1>/dev/null; fi +if (exec 3>&2) ; then :; else exec 2>/dev/null; fi + +# The user is always right. +if ${PATH_SEPARATOR+false} :; then + PATH_SEPARATOR=: + (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { + (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || + PATH_SEPARATOR=';' + } +fi + + +# Find who we are. Look in the path if we contain no directory separator. +as_myself= +case $0 in #(( + *[\\/]* ) as_myself=$0 ;; + *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + test -r "$as_dir$0" && as_myself=$as_dir$0 && break + done +IFS=$as_save_IFS + + ;; +esac +# We did not find ourselves, most probably we were run as `sh COMMAND' +# in which case we are not to be found in the path. +if test "x$as_myself" = x; then + as_myself=$0 +fi +if test ! -f "$as_myself"; then + printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + exit 1 +fi + + +# Use a proper internal environment variable to ensure we don't fall + # into an infinite loop, continuously re-executing ourselves. + if test x"${_as_can_reexec}" != xno && test "x$CONFIG_SHELL" != x; then + _as_can_reexec=no; export _as_can_reexec; + # We cannot yet assume a decent shell, so we have to provide a +# neutralization value for shells without unset; and this also +# works around shells that cannot unset nonexistent variables. +# Preserve -v and -x to the replacement shell. +BASH_ENV=/dev/null +ENV=/dev/null +(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV +case $- in # (((( + *v*x* | *x*v* ) as_opts=-vx ;; + *v* ) as_opts=-v ;; + *x* ) as_opts=-x ;; + * ) as_opts= ;; +esac +exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} +# Admittedly, this is quite paranoid, since all the known shells bail +# out after a failed `exec'. +printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 +exit 255 + fi + # We don't want this to propagate to other subprocesses. + { _as_can_reexec=; unset _as_can_reexec;} +if test "x$CONFIG_SHELL" = x; then + as_bourne_compatible="as_nop=: +if test \${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 +then : + emulate sh + NULLCMD=: + # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which + # is contrary to our usage. Disable this feature. + alias -g '\${1+\"\$@\"}'='\"\$@\"' + setopt NO_GLOB_SUBST +else \$as_nop + case \`(set -o) 2>/dev/null\` in #( + *posix*) : + set -o posix ;; #( + *) : + ;; +esac +fi +" + as_required="as_fn_return () { (exit \$1); } +as_fn_success () { as_fn_return 0; } +as_fn_failure () { as_fn_return 1; } +as_fn_ret_success () { return 0; } +as_fn_ret_failure () { return 1; } + +exitcode=0 +as_fn_success || { exitcode=1; echo as_fn_success failed.; } +as_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; } +as_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; } +as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; } +if ( set x; as_fn_ret_success y && test x = \"\$1\" ) +then : + +else \$as_nop + exitcode=1; echo positional parameters were not saved. +fi +test x\$exitcode = x0 || exit 1 +blah=\$(echo \$(echo blah)) +test x\"\$blah\" = xblah || exit 1 +test -x / || exit 1" + as_suggested=" as_lineno_1=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_1a=\$LINENO + as_lineno_2=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_2a=\$LINENO + eval 'test \"x\$as_lineno_1'\$as_run'\" != \"x\$as_lineno_2'\$as_run'\" && + test \"x\`expr \$as_lineno_1'\$as_run' + 1\`\" = \"x\$as_lineno_2'\$as_run'\"' || exit 1" + if (eval "$as_required") 2>/dev/null +then : + as_have_required=yes +else $as_nop + as_have_required=no +fi + if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null +then : + +else $as_nop + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +as_found=false +for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + as_found=: + case $as_dir in #( + /*) + for as_base in sh bash ksh sh5; do + # Try only shells that exist, to save several forks. + as_shell=$as_dir$as_base + if { test -f "$as_shell" || test -f "$as_shell.exe"; } && + as_run=a "$as_shell" -c "$as_bourne_compatible""$as_required" 2>/dev/null +then : + CONFIG_SHELL=$as_shell as_have_required=yes + if as_run=a "$as_shell" -c "$as_bourne_compatible""$as_suggested" 2>/dev/null +then : + break 2 +fi +fi + done;; + esac + as_found=false +done +IFS=$as_save_IFS +if $as_found +then : + +else $as_nop + if { test -f "$SHELL" || test -f "$SHELL.exe"; } && + as_run=a "$SHELL" -c "$as_bourne_compatible""$as_required" 2>/dev/null +then : + CONFIG_SHELL=$SHELL as_have_required=yes +fi +fi + + + if test "x$CONFIG_SHELL" != x +then : + export CONFIG_SHELL + # We cannot yet assume a decent shell, so we have to provide a +# neutralization value for shells without unset; and this also +# works around shells that cannot unset nonexistent variables. +# Preserve -v and -x to the replacement shell. +BASH_ENV=/dev/null +ENV=/dev/null +(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV +case $- in # (((( + *v*x* | *x*v* ) as_opts=-vx ;; + *v* ) as_opts=-v ;; + *x* ) as_opts=-x ;; + * ) as_opts= ;; +esac +exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} +# Admittedly, this is quite paranoid, since all the known shells bail +# out after a failed `exec'. +printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 +exit 255 +fi + + if test x$as_have_required = xno +then : + printf "%s\n" "$0: This script requires a shell more modern than all" + printf "%s\n" "$0: the shells that I found on your system." + if test ${ZSH_VERSION+y} ; then + printf "%s\n" "$0: In particular, zsh $ZSH_VERSION has bugs and should" + printf "%s\n" "$0: be upgraded to zsh 4.3.4 or later." + else + printf "%s\n" "$0: Please tell bug-autoconf@gnu.org about your system, +$0: including any error possibly output before this +$0: message. Then install a modern shell, or manually run +$0: the script under such a shell if you do have one." + fi + exit 1 +fi +fi +fi +SHELL=${CONFIG_SHELL-/bin/sh} +export SHELL +# Unset more variables known to interfere with behavior of common tools. +CLICOLOR_FORCE= GREP_OPTIONS= +unset CLICOLOR_FORCE GREP_OPTIONS + +## --------------------- ## +## M4sh Shell Functions. ## +## --------------------- ## +# as_fn_unset VAR +# --------------- +# Portably unset VAR. +as_fn_unset () +{ + { eval $1=; unset $1;} +} +as_unset=as_fn_unset + + +# as_fn_set_status STATUS +# ----------------------- +# Set $? to STATUS, without forking. +as_fn_set_status () +{ + return $1 +} # as_fn_set_status + +# as_fn_exit STATUS +# ----------------- +# Exit the shell with STATUS, even in a "trap 0" or "set -e" context. +as_fn_exit () +{ + set +e + as_fn_set_status $1 + exit $1 +} # as_fn_exit +# as_fn_nop +# --------- +# Do nothing but, unlike ":", preserve the value of $?. +as_fn_nop () +{ + return $? +} +as_nop=as_fn_nop + +# as_fn_mkdir_p +# ------------- +# Create "$as_dir" as a directory, including parents if necessary. +as_fn_mkdir_p () +{ + + case $as_dir in #( + -*) as_dir=./$as_dir;; + esac + test -d "$as_dir" || eval $as_mkdir_p || { + as_dirs= + while :; do + case $as_dir in #( + *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *) as_qdir=$as_dir;; + esac + as_dirs="'$as_qdir' $as_dirs" + as_dir=`$as_dirname -- "$as_dir" || +$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$as_dir" : 'X\(//\)[^/]' \| \ + X"$as_dir" : 'X\(//\)$' \| \ + X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || +printf "%s\n" X"$as_dir" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + test -d "$as_dir" && break + done + test -z "$as_dirs" || eval "mkdir $as_dirs" + } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" + + +} # as_fn_mkdir_p + +# as_fn_executable_p FILE +# ----------------------- +# Test if FILE is an executable regular file. +as_fn_executable_p () +{ + test -f "$1" && test -x "$1" +} # as_fn_executable_p +# as_fn_append VAR VALUE +# ---------------------- +# Append the text in VALUE to the end of the definition contained in VAR. Take +# advantage of any shell optimizations that allow amortized linear growth over +# repeated appends, instead of the typical quadratic growth present in naive +# implementations. +if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null +then : + eval 'as_fn_append () + { + eval $1+=\$2 + }' +else $as_nop + as_fn_append () + { + eval $1=\$$1\$2 + } +fi # as_fn_append + +# as_fn_arith ARG... +# ------------------ +# Perform arithmetic evaluation on the ARGs, and store the result in the +# global $as_val. Take advantage of shells that can avoid forks. The arguments +# must be portable across $(()) and expr. +if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null +then : + eval 'as_fn_arith () + { + as_val=$(( $* )) + }' +else $as_nop + as_fn_arith () + { + as_val=`expr "$@" || test $? -eq 1` + } +fi # as_fn_arith + +# as_fn_nop +# --------- +# Do nothing but, unlike ":", preserve the value of $?. +as_fn_nop () +{ + return $? +} +as_nop=as_fn_nop + +# as_fn_error STATUS ERROR [LINENO LOG_FD] +# ---------------------------------------- +# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are +# provided, also output the error to LOG_FD, referencing LINENO. Then exit the +# script with STATUS, using 1 if that was 0. +as_fn_error () +{ + as_status=$1; test $as_status -eq 0 && as_status=1 + if test "$4"; then + as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + fi + printf "%s\n" "$as_me: error: $2" >&2 + as_fn_exit $as_status +} # as_fn_error + +if expr a : '\(a\)' >/dev/null 2>&1 && + test "X`expr 00001 : '.*\(...\)'`" = X001; then + as_expr=expr +else + as_expr=false +fi + +if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then + as_basename=basename +else + as_basename=false +fi + +if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then + as_dirname=dirname +else + as_dirname=false +fi + +as_me=`$as_basename -- "$0" || +$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ + X"$0" : 'X\(//\)$' \| \ + X"$0" : 'X\(/\)' \| . 2>/dev/null || +printf "%s\n" X/"$0" | + sed '/^.*\/\([^/][^/]*\)\/*$/{ + s//\1/ + q + } + /^X\/\(\/\/\)$/{ + s//\1/ + q + } + /^X\/\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + +# Avoid depending upon Character Ranges. +as_cr_letters='abcdefghijklmnopqrstuvwxyz' +as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' +as_cr_Letters=$as_cr_letters$as_cr_LETTERS +as_cr_digits='0123456789' +as_cr_alnum=$as_cr_Letters$as_cr_digits + + + as_lineno_1=$LINENO as_lineno_1a=$LINENO + as_lineno_2=$LINENO as_lineno_2a=$LINENO + eval 'test "x$as_lineno_1'$as_run'" != "x$as_lineno_2'$as_run'" && + test "x`expr $as_lineno_1'$as_run' + 1`" = "x$as_lineno_2'$as_run'"' || { + # Blame Lee E. McMahon (1931-1989) for sed's syntax. :-) + sed -n ' + p + /[$]LINENO/= + ' <$as_myself | + sed ' + s/[$]LINENO.*/&-/ + t lineno + b + :lineno + N + :loop + s/[$]LINENO\([^'$as_cr_alnum'_].*\n\)\(.*\)/\2\1\2/ + t loop + s/-\n.*// + ' >$as_me.lineno && + chmod +x "$as_me.lineno" || + { printf "%s\n" "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } + + # If we had to re-execute with $CONFIG_SHELL, we're ensured to have + # already done that, so ensure we don't try to do so again and fall + # in an infinite loop. This has already happened in practice. + _as_can_reexec=no; export _as_can_reexec + # Don't try to exec as it changes $[0], causing all sort of problems + # (the dirname of $[0] is not the place where we might find the + # original and so on. Autoconf is especially sensitive to this). + . "./$as_me.lineno" + # Exit status is that of the last command. + exit +} + + +# Determine whether it's possible to make 'echo' print without a newline. +# These variables are no longer used directly by Autoconf, but are AC_SUBSTed +# for compatibility with existing Makefiles. +ECHO_C= ECHO_N= ECHO_T= +case `echo -n x` in #((((( +-n*) + case `echo 'xy\c'` in + *c*) ECHO_T=' ';; # ECHO_T is single tab character. + xy) ECHO_C='\c';; + *) echo `echo ksh88 bug on AIX 6.1` > /dev/null + ECHO_T=' ';; + esac;; +*) + ECHO_N='-n';; +esac + +# For backward compatibility with old third-party macros, we provide +# the shell variables $as_echo and $as_echo_n. New code should use +# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. +as_echo='printf %s\n' +as_echo_n='printf %s' + + +rm -f conf$$ conf$$.exe conf$$.file +if test -d conf$$.dir; then + rm -f conf$$.dir/conf$$.file +else + rm -f conf$$.dir + mkdir conf$$.dir 2>/dev/null +fi +if (echo >conf$$.file) 2>/dev/null; then + if ln -s conf$$.file conf$$ 2>/dev/null; then + as_ln_s='ln -s' + # ... but there are two gotchas: + # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. + # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. + # In both cases, we have to default to `cp -pR'. + ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || + as_ln_s='cp -pR' + elif ln conf$$.file conf$$ 2>/dev/null; then + as_ln_s=ln + else + as_ln_s='cp -pR' + fi +else + as_ln_s='cp -pR' +fi +rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file +rmdir conf$$.dir 2>/dev/null + +if mkdir -p . 2>/dev/null; then + as_mkdir_p='mkdir -p "$as_dir"' +else + test -d ./-p && rmdir ./-p + as_mkdir_p=false +fi + +as_test_x='test -x' +as_executable_p=as_fn_executable_p + +# Sed expression to map a string onto a valid CPP name. +as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" + +# Sed expression to map a string onto a valid variable name. +as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" + + +test -n "$DJDIR" || exec 7<&0 &1 + +# Name of the host. +# hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status, +# so uname gets run too. +ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q` + +# +# Initializations. +# +ac_default_prefix=/usr/local +ac_clean_files= +ac_config_libobj_dir=. +LIBOBJS= +cross_compiling=no +subdirs= +MFLAGS= +MAKEFLAGS= + +# Identity of this package. +PACKAGE_NAME='lightgbm' +PACKAGE_TARNAME='lightgbm' +PACKAGE_VERSION='4.6.0.99' +PACKAGE_STRING='lightgbm 4.6.0.99' +PACKAGE_BUGREPORT='' +PACKAGE_URL='' + +ac_subst_vars='LTLIBOBJS +LIBOBJS +LGB_CPPFLAGS +OPENMP_LIB +OPENMP_CXXFLAGS +target_alias +host_alias +build_alias +LIBS +ECHO_T +ECHO_N +ECHO_C +DEFS +mandir +localedir +libdir +psdir +pdfdir +dvidir +htmldir +infodir +docdir +oldincludedir +includedir +runstatedir +localstatedir +sharedstatedir +sysconfdir +datadir +datarootdir +libexecdir +sbindir +bindir +program_transform_name +prefix +exec_prefix +PACKAGE_URL +PACKAGE_BUGREPORT +PACKAGE_STRING +PACKAGE_VERSION +PACKAGE_TARNAME +PACKAGE_NAME +PATH_SEPARATOR +SHELL' +ac_subst_files='' +ac_user_opts=' +enable_option_checking +' + ac_precious_vars='build_alias +host_alias +target_alias' + + +# Initialize some variables set by options. +ac_init_help= +ac_init_version=false +ac_unrecognized_opts= +ac_unrecognized_sep= +# The variables have the same names as the options, with +# dashes changed to underlines. +cache_file=/dev/null +exec_prefix=NONE +no_create= +no_recursion= +prefix=NONE +program_prefix=NONE +program_suffix=NONE +program_transform_name=s,x,x, +silent= +site= +srcdir= +verbose= +x_includes=NONE +x_libraries=NONE + +# Installation directory options. +# These are left unexpanded so users can "make install exec_prefix=/foo" +# and all the variables that are supposed to be based on exec_prefix +# by default will actually change. +# Use braces instead of parens because sh, perl, etc. also accept them. +# (The list follows the same order as the GNU Coding Standards.) +bindir='${exec_prefix}/bin' +sbindir='${exec_prefix}/sbin' +libexecdir='${exec_prefix}/libexec' +datarootdir='${prefix}/share' +datadir='${datarootdir}' +sysconfdir='${prefix}/etc' +sharedstatedir='${prefix}/com' +localstatedir='${prefix}/var' +runstatedir='${localstatedir}/run' +includedir='${prefix}/include' +oldincludedir='/usr/include' +docdir='${datarootdir}/doc/${PACKAGE_TARNAME}' +infodir='${datarootdir}/info' +htmldir='${docdir}' +dvidir='${docdir}' +pdfdir='${docdir}' +psdir='${docdir}' +libdir='${exec_prefix}/lib' +localedir='${datarootdir}/locale' +mandir='${datarootdir}/man' + +ac_prev= +ac_dashdash= +for ac_option +do + # If the previous option needs an argument, assign it. + if test -n "$ac_prev"; then + eval $ac_prev=\$ac_option + ac_prev= + continue + fi + + case $ac_option in + *=?*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;; + *=) ac_optarg= ;; + *) ac_optarg=yes ;; + esac + + case $ac_dashdash$ac_option in + --) + ac_dashdash=yes ;; + + -bindir | --bindir | --bindi | --bind | --bin | --bi) + ac_prev=bindir ;; + -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*) + bindir=$ac_optarg ;; + + -build | --build | --buil | --bui | --bu) + ac_prev=build_alias ;; + -build=* | --build=* | --buil=* | --bui=* | --bu=*) + build_alias=$ac_optarg ;; + + -cache-file | --cache-file | --cache-fil | --cache-fi \ + | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c) + ac_prev=cache_file ;; + -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \ + | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*) + cache_file=$ac_optarg ;; + + --config-cache | -C) + cache_file=config.cache ;; + + -datadir | --datadir | --datadi | --datad) + ac_prev=datadir ;; + -datadir=* | --datadir=* | --datadi=* | --datad=*) + datadir=$ac_optarg ;; + + -datarootdir | --datarootdir | --datarootdi | --datarootd | --dataroot \ + | --dataroo | --dataro | --datar) + ac_prev=datarootdir ;; + -datarootdir=* | --datarootdir=* | --datarootdi=* | --datarootd=* \ + | --dataroot=* | --dataroo=* | --dataro=* | --datar=*) + datarootdir=$ac_optarg ;; + + -disable-* | --disable-*) + ac_useropt=`expr "x$ac_option" : 'x-*disable-\(.*\)'` + # Reject names that are not valid shell variable names. + expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && + as_fn_error $? "invalid feature name: \`$ac_useropt'" + ac_useropt_orig=$ac_useropt + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` + case $ac_user_opts in + *" +"enable_$ac_useropt" +"*) ;; + *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--disable-$ac_useropt_orig" + ac_unrecognized_sep=', ';; + esac + eval enable_$ac_useropt=no ;; + + -docdir | --docdir | --docdi | --doc | --do) + ac_prev=docdir ;; + -docdir=* | --docdir=* | --docdi=* | --doc=* | --do=*) + docdir=$ac_optarg ;; + + -dvidir | --dvidir | --dvidi | --dvid | --dvi | --dv) + ac_prev=dvidir ;; + -dvidir=* | --dvidir=* | --dvidi=* | --dvid=* | --dvi=* | --dv=*) + dvidir=$ac_optarg ;; + + -enable-* | --enable-*) + ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'` + # Reject names that are not valid shell variable names. + expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && + as_fn_error $? "invalid feature name: \`$ac_useropt'" + ac_useropt_orig=$ac_useropt + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` + case $ac_user_opts in + *" +"enable_$ac_useropt" +"*) ;; + *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--enable-$ac_useropt_orig" + ac_unrecognized_sep=', ';; + esac + eval enable_$ac_useropt=\$ac_optarg ;; + + -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \ + | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \ + | --exec | --exe | --ex) + ac_prev=exec_prefix ;; + -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \ + | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \ + | --exec=* | --exe=* | --ex=*) + exec_prefix=$ac_optarg ;; + + -gas | --gas | --ga | --g) + # Obsolete; use --with-gas. + with_gas=yes ;; + + -help | --help | --hel | --he | -h) + ac_init_help=long ;; + -help=r* | --help=r* | --hel=r* | --he=r* | -hr*) + ac_init_help=recursive ;; + -help=s* | --help=s* | --hel=s* | --he=s* | -hs*) + ac_init_help=short ;; + + -host | --host | --hos | --ho) + ac_prev=host_alias ;; + -host=* | --host=* | --hos=* | --ho=*) + host_alias=$ac_optarg ;; + + -htmldir | --htmldir | --htmldi | --htmld | --html | --htm | --ht) + ac_prev=htmldir ;; + -htmldir=* | --htmldir=* | --htmldi=* | --htmld=* | --html=* | --htm=* \ + | --ht=*) + htmldir=$ac_optarg ;; + + -includedir | --includedir | --includedi | --included | --include \ + | --includ | --inclu | --incl | --inc) + ac_prev=includedir ;; + -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \ + | --includ=* | --inclu=* | --incl=* | --inc=*) + includedir=$ac_optarg ;; + + -infodir | --infodir | --infodi | --infod | --info | --inf) + ac_prev=infodir ;; + -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*) + infodir=$ac_optarg ;; + + -libdir | --libdir | --libdi | --libd) + ac_prev=libdir ;; + -libdir=* | --libdir=* | --libdi=* | --libd=*) + libdir=$ac_optarg ;; + + -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \ + | --libexe | --libex | --libe) + ac_prev=libexecdir ;; + -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \ + | --libexe=* | --libex=* | --libe=*) + libexecdir=$ac_optarg ;; + + -localedir | --localedir | --localedi | --localed | --locale) + ac_prev=localedir ;; + -localedir=* | --localedir=* | --localedi=* | --localed=* | --locale=*) + localedir=$ac_optarg ;; + + -localstatedir | --localstatedir | --localstatedi | --localstated \ + | --localstate | --localstat | --localsta | --localst | --locals) + ac_prev=localstatedir ;; + -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \ + | --localstate=* | --localstat=* | --localsta=* | --localst=* | --locals=*) + localstatedir=$ac_optarg ;; + + -mandir | --mandir | --mandi | --mand | --man | --ma | --m) + ac_prev=mandir ;; + -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*) + mandir=$ac_optarg ;; + + -nfp | --nfp | --nf) + # Obsolete; use --without-fp. + with_fp=no ;; + + -no-create | --no-create | --no-creat | --no-crea | --no-cre \ + | --no-cr | --no-c | -n) + no_create=yes ;; + + -no-recursion | --no-recursion | --no-recursio | --no-recursi \ + | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r) + no_recursion=yes ;; + + -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \ + | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \ + | --oldin | --oldi | --old | --ol | --o) + ac_prev=oldincludedir ;; + -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \ + | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \ + | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*) + oldincludedir=$ac_optarg ;; + + -prefix | --prefix | --prefi | --pref | --pre | --pr | --p) + ac_prev=prefix ;; + -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*) + prefix=$ac_optarg ;; + + -program-prefix | --program-prefix | --program-prefi | --program-pref \ + | --program-pre | --program-pr | --program-p) + ac_prev=program_prefix ;; + -program-prefix=* | --program-prefix=* | --program-prefi=* \ + | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*) + program_prefix=$ac_optarg ;; + + -program-suffix | --program-suffix | --program-suffi | --program-suff \ + | --program-suf | --program-su | --program-s) + ac_prev=program_suffix ;; + -program-suffix=* | --program-suffix=* | --program-suffi=* \ + | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*) + program_suffix=$ac_optarg ;; + + -program-transform-name | --program-transform-name \ + | --program-transform-nam | --program-transform-na \ + | --program-transform-n | --program-transform- \ + | --program-transform | --program-transfor \ + | --program-transfo | --program-transf \ + | --program-trans | --program-tran \ + | --progr-tra | --program-tr | --program-t) + ac_prev=program_transform_name ;; + -program-transform-name=* | --program-transform-name=* \ + | --program-transform-nam=* | --program-transform-na=* \ + | --program-transform-n=* | --program-transform-=* \ + | --program-transform=* | --program-transfor=* \ + | --program-transfo=* | --program-transf=* \ + | --program-trans=* | --program-tran=* \ + | --progr-tra=* | --program-tr=* | --program-t=*) + program_transform_name=$ac_optarg ;; + + -pdfdir | --pdfdir | --pdfdi | --pdfd | --pdf | --pd) + ac_prev=pdfdir ;; + -pdfdir=* | --pdfdir=* | --pdfdi=* | --pdfd=* | --pdf=* | --pd=*) + pdfdir=$ac_optarg ;; + + -psdir | --psdir | --psdi | --psd | --ps) + ac_prev=psdir ;; + -psdir=* | --psdir=* | --psdi=* | --psd=* | --ps=*) + psdir=$ac_optarg ;; + + -q | -quiet | --quiet | --quie | --qui | --qu | --q \ + | -silent | --silent | --silen | --sile | --sil) + silent=yes ;; + + -runstatedir | --runstatedir | --runstatedi | --runstated \ + | --runstate | --runstat | --runsta | --runst | --runs \ + | --run | --ru | --r) + ac_prev=runstatedir ;; + -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \ + | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \ + | --run=* | --ru=* | --r=*) + runstatedir=$ac_optarg ;; + + -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) + ac_prev=sbindir ;; + -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ + | --sbi=* | --sb=*) + sbindir=$ac_optarg ;; + + -sharedstatedir | --sharedstatedir | --sharedstatedi \ + | --sharedstated | --sharedstate | --sharedstat | --sharedsta \ + | --sharedst | --shareds | --shared | --share | --shar \ + | --sha | --sh) + ac_prev=sharedstatedir ;; + -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \ + | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \ + | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \ + | --sha=* | --sh=*) + sharedstatedir=$ac_optarg ;; + + -site | --site | --sit) + ac_prev=site ;; + -site=* | --site=* | --sit=*) + site=$ac_optarg ;; + + -srcdir | --srcdir | --srcdi | --srcd | --src | --sr) + ac_prev=srcdir ;; + -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*) + srcdir=$ac_optarg ;; + + -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \ + | --syscon | --sysco | --sysc | --sys | --sy) + ac_prev=sysconfdir ;; + -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \ + | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*) + sysconfdir=$ac_optarg ;; + + -target | --target | --targe | --targ | --tar | --ta | --t) + ac_prev=target_alias ;; + -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*) + target_alias=$ac_optarg ;; + + -v | -verbose | --verbose | --verbos | --verbo | --verb) + verbose=yes ;; + + -version | --version | --versio | --versi | --vers | -V) + ac_init_version=: ;; + + -with-* | --with-*) + ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'` + # Reject names that are not valid shell variable names. + expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && + as_fn_error $? "invalid package name: \`$ac_useropt'" + ac_useropt_orig=$ac_useropt + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` + case $ac_user_opts in + *" +"with_$ac_useropt" +"*) ;; + *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--with-$ac_useropt_orig" + ac_unrecognized_sep=', ';; + esac + eval with_$ac_useropt=\$ac_optarg ;; + + -without-* | --without-*) + ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'` + # Reject names that are not valid shell variable names. + expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && + as_fn_error $? "invalid package name: \`$ac_useropt'" + ac_useropt_orig=$ac_useropt + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` + case $ac_user_opts in + *" +"with_$ac_useropt" +"*) ;; + *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--without-$ac_useropt_orig" + ac_unrecognized_sep=', ';; + esac + eval with_$ac_useropt=no ;; + + --x) + # Obsolete; use --with-x. + with_x=yes ;; + + -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \ + | --x-incl | --x-inc | --x-in | --x-i) + ac_prev=x_includes ;; + -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \ + | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*) + x_includes=$ac_optarg ;; + + -x-libraries | --x-libraries | --x-librarie | --x-librari \ + | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l) + ac_prev=x_libraries ;; + -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \ + | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) + x_libraries=$ac_optarg ;; + + -*) as_fn_error $? "unrecognized option: \`$ac_option' +Try \`$0 --help' for more information" + ;; + + *=*) + ac_envvar=`expr "x$ac_option" : 'x\([^=]*\)='` + # Reject names that are not valid shell variable names. + case $ac_envvar in #( + '' | [0-9]* | *[!_$as_cr_alnum]* ) + as_fn_error $? "invalid variable name: \`$ac_envvar'" ;; + esac + eval $ac_envvar=\$ac_optarg + export $ac_envvar ;; + + *) + # FIXME: should be removed in autoconf 3.0. + printf "%s\n" "$as_me: WARNING: you should use --build, --host, --target" >&2 + expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && + printf "%s\n" "$as_me: WARNING: invalid host type: $ac_option" >&2 + : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}" + ;; + + esac +done + +if test -n "$ac_prev"; then + ac_option=--`echo $ac_prev | sed 's/_/-/g'` + as_fn_error $? "missing argument to $ac_option" +fi + +if test -n "$ac_unrecognized_opts"; then + case $enable_option_checking in + no) ;; + fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; + *) printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; + esac +fi + +# Check all directory arguments for consistency. +for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ + datadir sysconfdir sharedstatedir localstatedir includedir \ + oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ + libdir localedir mandir runstatedir +do + eval ac_val=\$$ac_var + # Remove trailing slashes. + case $ac_val in + */ ) + ac_val=`expr "X$ac_val" : 'X\(.*[^/]\)' \| "X$ac_val" : 'X\(.*\)'` + eval $ac_var=\$ac_val;; + esac + # Be sure to have absolute directory names. + case $ac_val in + [\\/$]* | ?:[\\/]* ) continue;; + NONE | '' ) case $ac_var in *prefix ) continue;; esac;; + esac + as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val" +done + +# There might be people who depend on the old broken behavior: `$host' +# used to hold the argument of --host etc. +# FIXME: To remove some day. +build=$build_alias +host=$host_alias +target=$target_alias + +# FIXME: To remove some day. +if test "x$host_alias" != x; then + if test "x$build_alias" = x; then + cross_compiling=maybe + elif test "x$build_alias" != "x$host_alias"; then + cross_compiling=yes + fi +fi + +ac_tool_prefix= +test -n "$host_alias" && ac_tool_prefix=$host_alias- + +test "$silent" = yes && exec 6>/dev/null + + +ac_pwd=`pwd` && test -n "$ac_pwd" && +ac_ls_di=`ls -di .` && +ac_pwd_ls_di=`cd "$ac_pwd" && ls -di .` || + as_fn_error $? "working directory cannot be determined" +test "X$ac_ls_di" = "X$ac_pwd_ls_di" || + as_fn_error $? "pwd does not report name of working directory" + + +# Find the source files, if location was not specified. +if test -z "$srcdir"; then + ac_srcdir_defaulted=yes + # Try the directory containing this script, then the parent directory. + ac_confdir=`$as_dirname -- "$as_myself" || +$as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$as_myself" : 'X\(//\)[^/]' \| \ + X"$as_myself" : 'X\(//\)$' \| \ + X"$as_myself" : 'X\(/\)' \| . 2>/dev/null || +printf "%s\n" X"$as_myself" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + srcdir=$ac_confdir + if test ! -r "$srcdir/$ac_unique_file"; then + srcdir=.. + fi +else + ac_srcdir_defaulted=no +fi +if test ! -r "$srcdir/$ac_unique_file"; then + test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .." + as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir" +fi +ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work" +ac_abs_confdir=`( + cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg" + pwd)` +# When building in place, set srcdir=. +if test "$ac_abs_confdir" = "$ac_pwd"; then + srcdir=. +fi +# Remove unnecessary trailing slashes from srcdir. +# Double slashes in file names in object file debugging info +# mess up M-x gdb in Emacs. +case $srcdir in +*/) srcdir=`expr "X$srcdir" : 'X\(.*[^/]\)' \| "X$srcdir" : 'X\(.*\)'`;; +esac +for ac_var in $ac_precious_vars; do + eval ac_env_${ac_var}_set=\${${ac_var}+set} + eval ac_env_${ac_var}_value=\$${ac_var} + eval ac_cv_env_${ac_var}_set=\${${ac_var}+set} + eval ac_cv_env_${ac_var}_value=\$${ac_var} +done + +# +# Report the --help message. +# +if test "$ac_init_help" = "long"; then + # Omit some internal or obsolete options to make the list less imposing. + # This message is too long to be a string in the A/UX 3.1 sh. + cat <<_ACEOF +\`configure' configures lightgbm 4.6.0.99 to adapt to many kinds of systems. + +Usage: $0 [OPTION]... [VAR=VALUE]... + +To assign environment variables (e.g., CC, CFLAGS...), specify them as +VAR=VALUE. See below for descriptions of some of the useful variables. + +Defaults for the options are specified in brackets. + +Configuration: + -h, --help display this help and exit + --help=short display options specific to this package + --help=recursive display the short help of all the included packages + -V, --version display version information and exit + -q, --quiet, --silent do not print \`checking ...' messages + --cache-file=FILE cache test results in FILE [disabled] + -C, --config-cache alias for \`--cache-file=config.cache' + -n, --no-create do not create output files + --srcdir=DIR find the sources in DIR [configure dir or \`..'] + +Installation directories: + --prefix=PREFIX install architecture-independent files in PREFIX + [$ac_default_prefix] + --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX + [PREFIX] + +By default, \`make install' will install all the files in +\`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc. You can specify +an installation prefix other than \`$ac_default_prefix' using \`--prefix', +for instance \`--prefix=\$HOME'. + +For better control, use the options below. + +Fine tuning of the installation directories: + --bindir=DIR user executables [EPREFIX/bin] + --sbindir=DIR system admin executables [EPREFIX/sbin] + --libexecdir=DIR program executables [EPREFIX/libexec] + --sysconfdir=DIR read-only single-machine data [PREFIX/etc] + --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] + --localstatedir=DIR modifiable single-machine data [PREFIX/var] + --runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run] + --libdir=DIR object code libraries [EPREFIX/lib] + --includedir=DIR C header files [PREFIX/include] + --oldincludedir=DIR C header files for non-gcc [/usr/include] + --datarootdir=DIR read-only arch.-independent data root [PREFIX/share] + --datadir=DIR read-only architecture-independent data [DATAROOTDIR] + --infodir=DIR info documentation [DATAROOTDIR/info] + --localedir=DIR locale-dependent data [DATAROOTDIR/locale] + --mandir=DIR man documentation [DATAROOTDIR/man] + --docdir=DIR documentation root [DATAROOTDIR/doc/lightgbm] + --htmldir=DIR html documentation [DOCDIR] + --dvidir=DIR dvi documentation [DOCDIR] + --pdfdir=DIR pdf documentation [DOCDIR] + --psdir=DIR ps documentation [DOCDIR] +_ACEOF + + cat <<\_ACEOF +_ACEOF +fi + +if test -n "$ac_init_help"; then + case $ac_init_help in + short | recursive ) echo "Configuration of lightgbm 4.6.0.99:";; + esac + cat <<\_ACEOF + +Report bugs to the package provider. +_ACEOF +ac_status=$? +fi + +if test "$ac_init_help" = "recursive"; then + # If there are subdirs, report their specific --help. + for ac_dir in : $ac_subdirs_all; do test "x$ac_dir" = x: && continue + test -d "$ac_dir" || + { cd "$srcdir" && ac_pwd=`pwd` && srcdir=. && test -d "$ac_dir"; } || + continue + ac_builddir=. + +case "$ac_dir" in +.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; +*) + ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` + # A ".." for each directory in $ac_dir_suffix. + ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + case $ac_top_builddir_sub in + "") ac_top_builddir_sub=. ac_top_build_prefix= ;; + *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; + esac ;; +esac +ac_abs_top_builddir=$ac_pwd +ac_abs_builddir=$ac_pwd$ac_dir_suffix +# for backward compatibility: +ac_top_builddir=$ac_top_build_prefix + +case $srcdir in + .) # We are building in place. + ac_srcdir=. + ac_top_srcdir=$ac_top_builddir_sub + ac_abs_top_srcdir=$ac_pwd ;; + [\\/]* | ?:[\\/]* ) # Absolute name. + ac_srcdir=$srcdir$ac_dir_suffix; + ac_top_srcdir=$srcdir + ac_abs_top_srcdir=$srcdir ;; + *) # Relative name. + ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix + ac_top_srcdir=$ac_top_build_prefix$srcdir + ac_abs_top_srcdir=$ac_pwd/$srcdir ;; +esac +ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix + + cd "$ac_dir" || { ac_status=$?; continue; } + # Check for configure.gnu first; this name is used for a wrapper for + # Metaconfig's "Configure" on case-insensitive file systems. + if test -f "$ac_srcdir/configure.gnu"; then + echo && + $SHELL "$ac_srcdir/configure.gnu" --help=recursive + elif test -f "$ac_srcdir/configure"; then + echo && + $SHELL "$ac_srcdir/configure" --help=recursive + else + printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2 + fi || ac_status=$? + cd "$ac_pwd" || { ac_status=$?; break; } + done +fi + +test -n "$ac_init_help" && exit $ac_status +if $ac_init_version; then + cat <<\_ACEOF +lightgbm configure 4.6.0.99 +generated by GNU Autoconf 2.71 + +Copyright (C) 2021 Free Software Foundation, Inc. +This configure script is free software; the Free Software Foundation +gives unlimited permission to copy, distribute and modify it. +_ACEOF + exit +fi + +## ------------------------ ## +## Autoconf initialization. ## +## ------------------------ ## +ac_configure_args_raw= +for ac_arg +do + case $ac_arg in + *\'*) + ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + esac + as_fn_append ac_configure_args_raw " '$ac_arg'" +done + +case $ac_configure_args_raw in + *$as_nl*) + ac_safe_unquote= ;; + *) + ac_unsafe_z='|&;<>()$`\\"*?[ '' ' # This string ends in space, tab. + ac_unsafe_a="$ac_unsafe_z#~" + ac_safe_unquote="s/ '\\([^$ac_unsafe_a][^$ac_unsafe_z]*\\)'/ \\1/g" + ac_configure_args_raw=` printf "%s\n" "$ac_configure_args_raw" | sed "$ac_safe_unquote"`;; +esac + +cat >config.log <<_ACEOF +This file contains any messages produced by compilers while +running configure, to aid debugging if configure makes a mistake. + +It was created by lightgbm $as_me 4.6.0.99, which was +generated by GNU Autoconf 2.71. Invocation command line was + + $ $0$ac_configure_args_raw + +_ACEOF +exec 5>>config.log +{ +cat <<_ASUNAME +## --------- ## +## Platform. ## +## --------- ## + +hostname = `(hostname || uname -n) 2>/dev/null | sed 1q` +uname -m = `(uname -m) 2>/dev/null || echo unknown` +uname -r = `(uname -r) 2>/dev/null || echo unknown` +uname -s = `(uname -s) 2>/dev/null || echo unknown` +uname -v = `(uname -v) 2>/dev/null || echo unknown` + +/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null || echo unknown` +/bin/uname -X = `(/bin/uname -X) 2>/dev/null || echo unknown` + +/bin/arch = `(/bin/arch) 2>/dev/null || echo unknown` +/usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null || echo unknown` +/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null || echo unknown` +/usr/bin/hostinfo = `(/usr/bin/hostinfo) 2>/dev/null || echo unknown` +/bin/machine = `(/bin/machine) 2>/dev/null || echo unknown` +/usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null || echo unknown` +/bin/universe = `(/bin/universe) 2>/dev/null || echo unknown` + +_ASUNAME + +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + printf "%s\n" "PATH: $as_dir" + done +IFS=$as_save_IFS + +} >&5 + +cat >&5 <<_ACEOF + + +## ----------- ## +## Core tests. ## +## ----------- ## + +_ACEOF + + +# Keep a trace of the command line. +# Strip out --no-create and --no-recursion so they do not pile up. +# Strip out --silent because we don't want to record it for future runs. +# Also quote any args containing shell meta-characters. +# Make two passes to allow for proper duplicate-argument suppression. +ac_configure_args= +ac_configure_args0= +ac_configure_args1= +ac_must_keep_next=false +for ac_pass in 1 2 +do + for ac_arg + do + case $ac_arg in + -no-create | --no-c* | -n | -no-recursion | --no-r*) continue ;; + -q | -quiet | --quiet | --quie | --qui | --qu | --q \ + | -silent | --silent | --silen | --sile | --sil) + continue ;; + *\'*) + ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + esac + case $ac_pass in + 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;; + 2) + as_fn_append ac_configure_args1 " '$ac_arg'" + if test $ac_must_keep_next = true; then + ac_must_keep_next=false # Got value, back to normal. + else + case $ac_arg in + *=* | --config-cache | -C | -disable-* | --disable-* \ + | -enable-* | --enable-* | -gas | --g* | -nfp | --nf* \ + | -q | -quiet | --q* | -silent | --sil* | -v | -verb* \ + | -with-* | --with-* | -without-* | --without-* | --x) + case "$ac_configure_args0 " in + "$ac_configure_args1"*" '$ac_arg' "* ) continue ;; + esac + ;; + -* ) ac_must_keep_next=true ;; + esac + fi + as_fn_append ac_configure_args " '$ac_arg'" + ;; + esac + done +done +{ ac_configure_args0=; unset ac_configure_args0;} +{ ac_configure_args1=; unset ac_configure_args1;} + +# When interrupted or exit'd, cleanup temporary files, and complete +# config.log. We remove comments because anyway the quotes in there +# would cause problems or look ugly. +# WARNING: Use '\'' to represent an apostrophe within the trap. +# WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug. +trap 'exit_status=$? + # Sanitize IFS. + IFS=" "" $as_nl" + # Save into config.log some information that might help in debugging. + { + echo + + printf "%s\n" "## ---------------- ## +## Cache variables. ## +## ---------------- ##" + echo + # The following way of writing the cache mishandles newlines in values, +( + for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do + eval ac_val=\$$ac_var + case $ac_val in #( + *${as_nl}*) + case $ac_var in #( + *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + esac + case $ac_var in #( + _ | IFS | as_nl) ;; #( + BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( + *) { eval $ac_var=; unset $ac_var;} ;; + esac ;; + esac + done + (set) 2>&1 | + case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #( + *${as_nl}ac_space=\ *) + sed -n \ + "s/'\''/'\''\\\\'\'''\''/g; + s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p" + ;; #( + *) + sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" + ;; + esac | + sort +) + echo + + printf "%s\n" "## ----------------- ## +## Output variables. ## +## ----------------- ##" + echo + for ac_var in $ac_subst_vars + do + eval ac_val=\$$ac_var + case $ac_val in + *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; + esac + printf "%s\n" "$ac_var='\''$ac_val'\''" + done | sort + echo + + if test -n "$ac_subst_files"; then + printf "%s\n" "## ------------------- ## +## File substitutions. ## +## ------------------- ##" + echo + for ac_var in $ac_subst_files + do + eval ac_val=\$$ac_var + case $ac_val in + *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; + esac + printf "%s\n" "$ac_var='\''$ac_val'\''" + done | sort + echo + fi + + if test -s confdefs.h; then + printf "%s\n" "## ----------- ## +## confdefs.h. ## +## ----------- ##" + echo + cat confdefs.h + echo + fi + test "$ac_signal" != 0 && + printf "%s\n" "$as_me: caught signal $ac_signal" + printf "%s\n" "$as_me: exit $exit_status" + } >&5 + rm -f core *.core core.conftest.* && + rm -f -r conftest* confdefs* conf$$* $ac_clean_files && + exit $exit_status +' 0 +for ac_signal in 1 2 13 15; do + trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal +done +ac_signal=0 + +# confdefs.h avoids OS command line length limits that DEFS can exceed. +rm -f -r conftest* confdefs.h + +printf "%s\n" "/* confdefs.h */" > confdefs.h + +# Predefined preprocessor variables. + +printf "%s\n" "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h + +printf "%s\n" "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h + +printf "%s\n" "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h + +printf "%s\n" "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h + +printf "%s\n" "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h + +printf "%s\n" "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h + + +# Let the site file select an alternate cache file if it wants to. +# Prefer an explicitly selected file to automatically selected ones. +if test -n "$CONFIG_SITE"; then + ac_site_files="$CONFIG_SITE" +elif test "x$prefix" != xNONE; then + ac_site_files="$prefix/share/config.site $prefix/etc/config.site" +else + ac_site_files="$ac_default_prefix/share/config.site $ac_default_prefix/etc/config.site" +fi + +for ac_site_file in $ac_site_files +do + case $ac_site_file in #( + */*) : + ;; #( + *) : + ac_site_file=./$ac_site_file ;; +esac + if test -f "$ac_site_file" && test -r "$ac_site_file"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 +printf "%s\n" "$as_me: loading site script $ac_site_file" >&6;} + sed 's/^/| /' "$ac_site_file" >&5 + . "$ac_site_file" \ + || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "failed to load site script $ac_site_file +See \`config.log' for more details" "$LINENO" 5; } + fi +done + +if test -r "$cache_file"; then + # Some versions of bash will fail to source /dev/null (special files + # actually), so we avoid doing that. DJGPP emulates it as a regular file. + if test /dev/null != "$cache_file" && test -f "$cache_file"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 +printf "%s\n" "$as_me: loading cache $cache_file" >&6;} + case $cache_file in + [\\/]* | ?:[\\/]* ) . "$cache_file";; + *) . "./$cache_file";; + esac + fi +else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 +printf "%s\n" "$as_me: creating cache $cache_file" >&6;} + >$cache_file +fi + +# Check that the precious variables saved in the cache have kept the same +# value. +ac_cache_corrupted=false +for ac_var in $ac_precious_vars; do + eval ac_old_set=\$ac_cv_env_${ac_var}_set + eval ac_new_set=\$ac_env_${ac_var}_set + eval ac_old_val=\$ac_cv_env_${ac_var}_value + eval ac_new_val=\$ac_env_${ac_var}_value + case $ac_old_set,$ac_new_set in + set,) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5 +printf "%s\n" "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;} + ac_cache_corrupted=: ;; + ,set) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5 +printf "%s\n" "$as_me: error: \`$ac_var' was not set in the previous run" >&2;} + ac_cache_corrupted=: ;; + ,);; + *) + if test "x$ac_old_val" != "x$ac_new_val"; then + # differences in whitespace do not lead to failure. + ac_old_val_w=`echo x $ac_old_val` + ac_new_val_w=`echo x $ac_new_val` + if test "$ac_old_val_w" != "$ac_new_val_w"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5 +printf "%s\n" "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;} + ac_cache_corrupted=: + else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5 +printf "%s\n" "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;} + eval $ac_var=\$ac_old_val + fi + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5 +printf "%s\n" "$as_me: former value: \`$ac_old_val'" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5 +printf "%s\n" "$as_me: current value: \`$ac_new_val'" >&2;} + fi;; + esac + # Pass precious variables to config.status. + if test "$ac_new_set" = set; then + case $ac_new_val in + *\'*) ac_arg=$ac_var=`printf "%s\n" "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; + *) ac_arg=$ac_var=$ac_new_val ;; + esac + case " $ac_configure_args " in + *" '$ac_arg' "*) ;; # Avoid dups. Use of quotes ensures accuracy. + *) as_fn_append ac_configure_args " '$ac_arg'" ;; + esac + fi +done +if $ac_cache_corrupted; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 +printf "%s\n" "$as_me: error: changes in the environment can compromise the build" >&2;} + as_fn_error $? "run \`${MAKE-make} distclean' and/or \`rm $cache_file' + and start over" "$LINENO" 5 +fi +## -------------------- ## +## Main body of script. ## +## -------------------- ## + +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + + + +########################### +# find compiler and flags # +########################### + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking location of R" >&5 +printf %s "checking location of R... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${R_HOME}" >&5 +printf "%s\n" "${R_HOME}" >&6; } + +# set up CPP flags +# find the compiler and compiler flags used by R. +: ${R_HOME=`R HOME`} +if test -z "${R_HOME}"; then + echo "could not determine R_HOME" + exit 1 +fi +CXX17=`"${R_HOME}/bin/R" CMD config CXX17` +CXX17STD=`"${R_HOME}/bin/R" CMD config CXX17STD` +CXX="${CXX17} ${CXX17STD}" +CPPFLAGS=`"${R_HOME}/bin/R" CMD config CPPFLAGS` +CXXFLAGS=`"${R_HOME}/bin/R" CMD config CXX17FLAGS` +LDFLAGS=`"${R_HOME}/bin/R" CMD config LDFLAGS` +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + + +# LightGBM-specific flags +LGB_CPPFLAGS="" + +######### +# Eigen # +######### + +LGB_CPPFLAGS="${LGB_CPPFLAGS} -DEIGEN_MPL2_ONLY -DEIGEN_DONT_PARALLELIZE" + +############### +# MM_PREFETCH # +############### + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether MM_PREFETCH works" >&5 +printf %s "checking whether MM_PREFETCH works... " >&6; } +ac_mmprefetch=no +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + + #include + +int +main (void) +{ + + int a = 0; + _mm_prefetch(&a, _MM_HINT_NTA); + return 0; + + + ; + return 0; +} + + +_ACEOF +${CXX} ${CPPFLAGS} ${CXXFLAGS} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_mmprefetch=yes +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${ac_mmprefetch}" >&5 +printf "%s\n" "${ac_mmprefetch}" >&6; } +if test "${ac_mmprefetch}" = yes; then + LGB_CPPFLAGS="${LGB_CPPFLAGS} -DMM_PREFETCH=1" +fi + +############ +# MM_ALLOC # +############ + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether MM_MALLOC works" >&5 +printf %s "checking whether MM_MALLOC works... " >&6; } +ac_mm_malloc=no +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + + #include + +int +main (void) +{ + + char *a = (char*)_mm_malloc(8, 16); + _mm_free(a); + return 0; + + + ; + return 0; +} + + +_ACEOF +${CXX} ${CPPFLAGS} ${CXXFLAGS} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_mm_malloc=yes +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${ac_mm_malloc}" >&5 +printf "%s\n" "${ac_mm_malloc}" >&6; } +if test "${ac_mm_malloc}" = yes; then + LGB_CPPFLAGS="${LGB_CPPFLAGS} -DMM_MALLOC=1" +fi + +########## +# OpenMP # +########## + +OPENMP_CXXFLAGS="" + +if test `uname -s` = "Linux" +then + OPENMP_CXXFLAGS="\$(SHLIB_OPENMP_CXXFLAGS)" +fi + +if test `uname -s` = "Darwin" +then + OPENMP_CXXFLAGS='-Xclang -fopenmp' + OPENMP_LIB='-lomp' + + # libomp 15.0+ from brew is keg-only (i.e. not symlinked into the standard paths search by the linker), + # so need to search in other locations. + # See https://github.com/Homebrew/homebrew-core/issues/112107#issuecomment-1278042927. + # + # If Homebrew is found and libomp was installed with it, this code adds the necessary + # flags for the compiler to find libomp headers and for the linker to find libomp.dylib. + HOMEBREW_LIBOMP_PREFIX="" + if command -v brew >/dev/null 2>&1; then + ac_brew_openmp=no + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether OpenMP was installed via Homebrew" >&5 +printf %s "checking whether OpenMP was installed via Homebrew... " >&6; } + brew --prefix libomp >/dev/null 2>&1 && ac_brew_openmp=yes + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${ac_brew_openmp}" >&5 +printf "%s\n" "${ac_brew_openmp}" >&6; } + if test "${ac_brew_openmp}" = yes; then + HOMEBREW_LIBOMP_PREFIX=`brew --prefix libomp` + OPENMP_CXXFLAGS="${OPENMP_CXXFLAGS} -I${HOMEBREW_LIBOMP_PREFIX}/include" + OPENMP_LIB="${OPENMP_LIB} -L${HOMEBREW_LIBOMP_PREFIX}/lib" + fi + fi + ac_pkg_openmp=no + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether OpenMP will work in a package" >&5 +printf %s "checking whether OpenMP will work in a package... " >&6; } + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + + #include + +int +main (void) +{ + + return (omp_get_max_threads() <= 1); + + + ; + return 0; +} + + +_ACEOF + ${CXX} ${CPPFLAGS} ${CXXFLAGS} ${LDFLAGS} ${OPENMP_CXXFLAGS} ${OPENMP_LIB} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_pkg_openmp=yes + + # -Xclang is not portable (it is clang-specific) + # if compilation above failed, try without that flag + if test "${ac_pkg_openmp}" = no; then + if test -f "./conftest"; then + rm ./conftest + fi + OPENMP_CXXFLAGS="-fopenmp" + ${CXX} ${CPPFLAGS} ${CXXFLAGS} ${LDFLAGS} ${OPENMP_CXXFLAGS} ${OPENMP_LIB} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_pkg_openmp=yes + fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${ac_pkg_openmp}" >&5 +printf "%s\n" "${ac_pkg_openmp}" >&6; } + if test "${ac_pkg_openmp}" = no; then + OPENMP_CXXFLAGS='' + OPENMP_LIB='' + echo '***********************************************************************************************' + echo ' OpenMP is unavailable on this macOS system. LightGBM code will run single-threaded as a result.' + echo ' To use all CPU cores for training jobs, you should install OpenMP by running' + echo '' + echo ' brew install libomp' + echo '***********************************************************************************************' + fi +fi + +# substitute variables from this script into Makevars.in + + + +ac_config_files="$ac_config_files src/Makevars" + + +# write out Autoconf output +cat >confcache <<\_ACEOF +# This file is a shell script that caches the results of configure +# tests run on this system so they can be shared between configure +# scripts and configure runs, see configure's option --config-cache. +# It is not useful on other systems. If it contains results you don't +# want to keep, you may remove or edit it. +# +# config.status only pays attention to the cache file if you give it +# the --recheck option to rerun configure. +# +# `ac_cv_env_foo' variables (set or unset) will be overridden when +# loading this file, other *unset* `ac_cv_foo' will be assigned the +# following values. + +_ACEOF + +# The following way of writing the cache mishandles newlines in values, +# but we know of no workaround that is simple, portable, and efficient. +# So, we kill variables containing newlines. +# Ultrix sh set writes to stderr and can't be redirected directly, +# and sets the high bit in the cache file unless we assign to the vars. +( + for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do + eval ac_val=\$$ac_var + case $ac_val in #( + *${as_nl}*) + case $ac_var in #( + *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + esac + case $ac_var in #( + _ | IFS | as_nl) ;; #( + BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( + *) { eval $ac_var=; unset $ac_var;} ;; + esac ;; + esac + done + + (set) 2>&1 | + case $as_nl`(ac_space=' '; set) 2>&1` in #( + *${as_nl}ac_space=\ *) + # `set' does not quote correctly, so add quotes: double-quote + # substitution turns \\\\ into \\, and sed turns \\ into \. + sed -n \ + "s/'/'\\\\''/g; + s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" + ;; #( + *) + # `set' quotes correctly as required by POSIX, so do not add quotes. + sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" + ;; + esac | + sort +) | + sed ' + /^ac_cv_env_/b end + t clear + :clear + s/^\([^=]*\)=\(.*[{}].*\)$/test ${\1+y} || &/ + t end + s/^\([^=]*\)=\(.*\)$/\1=${\1=\2}/ + :end' >>confcache +if diff "$cache_file" confcache >/dev/null 2>&1; then :; else + if test -w "$cache_file"; then + if test "x$cache_file" != "x/dev/null"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 +printf "%s\n" "$as_me: updating cache $cache_file" >&6;} + if test ! -f "$cache_file" || test -h "$cache_file"; then + cat confcache >"$cache_file" + else + case $cache_file in #( + */* | ?:*) + mv -f confcache "$cache_file"$$ && + mv -f "$cache_file"$$ "$cache_file" ;; #( + *) + mv -f confcache "$cache_file" ;; + esac + fi + fi + else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 +printf "%s\n" "$as_me: not updating unwritable cache $cache_file" >&6;} + fi +fi +rm -f confcache + +test "x$prefix" = xNONE && prefix=$ac_default_prefix +# Let make expand exec_prefix. +test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' + +# Transform confdefs.h into DEFS. +# Protect against shell expansion while executing Makefile rules. +# Protect against Makefile macro expansion. +# +# If the first sed substitution is executed (which looks for macros that +# take arguments), then branch to the quote section. Otherwise, +# look for a macro that doesn't take arguments. +ac_script=' +:mline +/\\$/{ + N + s,\\\n,, + b mline +} +t clear +:clear +s/^[ ]*#[ ]*define[ ][ ]*\([^ (][^ (]*([^)]*)\)[ ]*\(.*\)/-D\1=\2/g +t quote +s/^[ ]*#[ ]*define[ ][ ]*\([^ ][^ ]*\)[ ]*\(.*\)/-D\1=\2/g +t quote +b any +:quote +s/[ `~#$^&*(){}\\|;'\''"<>?]/\\&/g +s/\[/\\&/g +s/\]/\\&/g +s/\$/$$/g +H +:any +${ + g + s/^\n// + s/\n/ /g + p +} +' +DEFS=`sed -n "$ac_script" confdefs.h` + + +ac_libobjs= +ac_ltlibobjs= +U= +for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue + # 1. Remove the extension, and $U if already installed. + ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' + ac_i=`printf "%s\n" "$ac_i" | sed "$ac_script"` + # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR + # will be set to the directory where LIBOBJS objects are built. + as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext" + as_fn_append ac_ltlibobjs " \${LIBOBJDIR}$ac_i"'$U.lo' +done +LIBOBJS=$ac_libobjs + +LTLIBOBJS=$ac_ltlibobjs + + + +: "${CONFIG_STATUS=./config.status}" +ac_write_fail=0 +ac_clean_files_save=$ac_clean_files +ac_clean_files="$ac_clean_files $CONFIG_STATUS" +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 +printf "%s\n" "$as_me: creating $CONFIG_STATUS" >&6;} +as_write_fail=0 +cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1 +#! $SHELL +# Generated by $as_me. +# Run this file to recreate the current configuration. +# Compiler output produced by configure, useful for debugging +# configure, is in config.log if it exists. + +debug=false +ac_cs_recheck=false +ac_cs_silent=false + +SHELL=\${CONFIG_SHELL-$SHELL} +export SHELL +_ASEOF +cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1 +## -------------------- ## +## M4sh Initialization. ## +## -------------------- ## + +# Be more Bourne compatible +DUALCASE=1; export DUALCASE # for MKS sh +as_nop=: +if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 +then : + emulate sh + NULLCMD=: + # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which + # is contrary to our usage. Disable this feature. + alias -g '${1+"$@"}'='"$@"' + setopt NO_GLOB_SUBST +else $as_nop + case `(set -o) 2>/dev/null` in #( + *posix*) : + set -o posix ;; #( + *) : + ;; +esac +fi + + + +# Reset variables that may have inherited troublesome values from +# the environment. + +# IFS needs to be set, to space, tab, and newline, in precisely that order. +# (If _AS_PATH_WALK were called with IFS unset, it would have the +# side effect of setting IFS to empty, thus disabling word splitting.) +# Quoting is to prevent editors from complaining about space-tab. +as_nl=' +' +export as_nl +IFS=" "" $as_nl" + +PS1='$ ' +PS2='> ' +PS4='+ ' + +# Ensure predictable behavior from utilities with locale-dependent output. +LC_ALL=C +export LC_ALL +LANGUAGE=C +export LANGUAGE + +# We cannot yet rely on "unset" to work, but we need these variables +# to be unset--not just set to an empty or harmless value--now, to +# avoid bugs in old shells (e.g. pre-3.0 UWIN ksh). This construct +# also avoids known problems related to "unset" and subshell syntax +# in other old shells (e.g. bash 2.01 and pdksh 5.2.14). +for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH +do eval test \${$as_var+y} \ + && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : +done + +# Ensure that fds 0, 1, and 2 are open. +if (exec 3>&0) 2>/dev/null; then :; else exec 0&1) 2>/dev/null; then :; else exec 1>/dev/null; fi +if (exec 3>&2) ; then :; else exec 2>/dev/null; fi + +# The user is always right. +if ${PATH_SEPARATOR+false} :; then + PATH_SEPARATOR=: + (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { + (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || + PATH_SEPARATOR=';' + } +fi + + +# Find who we are. Look in the path if we contain no directory separator. +as_myself= +case $0 in #(( + *[\\/]* ) as_myself=$0 ;; + *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + case $as_dir in #((( + '') as_dir=./ ;; + */) ;; + *) as_dir=$as_dir/ ;; + esac + test -r "$as_dir$0" && as_myself=$as_dir$0 && break + done +IFS=$as_save_IFS + + ;; +esac +# We did not find ourselves, most probably we were run as `sh COMMAND' +# in which case we are not to be found in the path. +if test "x$as_myself" = x; then + as_myself=$0 +fi +if test ! -f "$as_myself"; then + printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + exit 1 +fi + + + +# as_fn_error STATUS ERROR [LINENO LOG_FD] +# ---------------------------------------- +# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are +# provided, also output the error to LOG_FD, referencing LINENO. Then exit the +# script with STATUS, using 1 if that was 0. +as_fn_error () +{ + as_status=$1; test $as_status -eq 0 && as_status=1 + if test "$4"; then + as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + fi + printf "%s\n" "$as_me: error: $2" >&2 + as_fn_exit $as_status +} # as_fn_error + + + +# as_fn_set_status STATUS +# ----------------------- +# Set $? to STATUS, without forking. +as_fn_set_status () +{ + return $1 +} # as_fn_set_status + +# as_fn_exit STATUS +# ----------------- +# Exit the shell with STATUS, even in a "trap 0" or "set -e" context. +as_fn_exit () +{ + set +e + as_fn_set_status $1 + exit $1 +} # as_fn_exit + +# as_fn_unset VAR +# --------------- +# Portably unset VAR. +as_fn_unset () +{ + { eval $1=; unset $1;} +} +as_unset=as_fn_unset + +# as_fn_append VAR VALUE +# ---------------------- +# Append the text in VALUE to the end of the definition contained in VAR. Take +# advantage of any shell optimizations that allow amortized linear growth over +# repeated appends, instead of the typical quadratic growth present in naive +# implementations. +if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null +then : + eval 'as_fn_append () + { + eval $1+=\$2 + }' +else $as_nop + as_fn_append () + { + eval $1=\$$1\$2 + } +fi # as_fn_append + +# as_fn_arith ARG... +# ------------------ +# Perform arithmetic evaluation on the ARGs, and store the result in the +# global $as_val. Take advantage of shells that can avoid forks. The arguments +# must be portable across $(()) and expr. +if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null +then : + eval 'as_fn_arith () + { + as_val=$(( $* )) + }' +else $as_nop + as_fn_arith () + { + as_val=`expr "$@" || test $? -eq 1` + } +fi # as_fn_arith + + +if expr a : '\(a\)' >/dev/null 2>&1 && + test "X`expr 00001 : '.*\(...\)'`" = X001; then + as_expr=expr +else + as_expr=false +fi + +if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then + as_basename=basename +else + as_basename=false +fi + +if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then + as_dirname=dirname +else + as_dirname=false +fi + +as_me=`$as_basename -- "$0" || +$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ + X"$0" : 'X\(//\)$' \| \ + X"$0" : 'X\(/\)' \| . 2>/dev/null || +printf "%s\n" X/"$0" | + sed '/^.*\/\([^/][^/]*\)\/*$/{ + s//\1/ + q + } + /^X\/\(\/\/\)$/{ + s//\1/ + q + } + /^X\/\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + +# Avoid depending upon Character Ranges. +as_cr_letters='abcdefghijklmnopqrstuvwxyz' +as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' +as_cr_Letters=$as_cr_letters$as_cr_LETTERS +as_cr_digits='0123456789' +as_cr_alnum=$as_cr_Letters$as_cr_digits + + +# Determine whether it's possible to make 'echo' print without a newline. +# These variables are no longer used directly by Autoconf, but are AC_SUBSTed +# for compatibility with existing Makefiles. +ECHO_C= ECHO_N= ECHO_T= +case `echo -n x` in #((((( +-n*) + case `echo 'xy\c'` in + *c*) ECHO_T=' ';; # ECHO_T is single tab character. + xy) ECHO_C='\c';; + *) echo `echo ksh88 bug on AIX 6.1` > /dev/null + ECHO_T=' ';; + esac;; +*) + ECHO_N='-n';; +esac + +# For backward compatibility with old third-party macros, we provide +# the shell variables $as_echo and $as_echo_n. New code should use +# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. +as_echo='printf %s\n' +as_echo_n='printf %s' + +rm -f conf$$ conf$$.exe conf$$.file +if test -d conf$$.dir; then + rm -f conf$$.dir/conf$$.file +else + rm -f conf$$.dir + mkdir conf$$.dir 2>/dev/null +fi +if (echo >conf$$.file) 2>/dev/null; then + if ln -s conf$$.file conf$$ 2>/dev/null; then + as_ln_s='ln -s' + # ... but there are two gotchas: + # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. + # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. + # In both cases, we have to default to `cp -pR'. + ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || + as_ln_s='cp -pR' + elif ln conf$$.file conf$$ 2>/dev/null; then + as_ln_s=ln + else + as_ln_s='cp -pR' + fi +else + as_ln_s='cp -pR' +fi +rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file +rmdir conf$$.dir 2>/dev/null + + +# as_fn_mkdir_p +# ------------- +# Create "$as_dir" as a directory, including parents if necessary. +as_fn_mkdir_p () +{ + + case $as_dir in #( + -*) as_dir=./$as_dir;; + esac + test -d "$as_dir" || eval $as_mkdir_p || { + as_dirs= + while :; do + case $as_dir in #( + *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *) as_qdir=$as_dir;; + esac + as_dirs="'$as_qdir' $as_dirs" + as_dir=`$as_dirname -- "$as_dir" || +$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$as_dir" : 'X\(//\)[^/]' \| \ + X"$as_dir" : 'X\(//\)$' \| \ + X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || +printf "%s\n" X"$as_dir" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + test -d "$as_dir" && break + done + test -z "$as_dirs" || eval "mkdir $as_dirs" + } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" + + +} # as_fn_mkdir_p +if mkdir -p . 2>/dev/null; then + as_mkdir_p='mkdir -p "$as_dir"' +else + test -d ./-p && rmdir ./-p + as_mkdir_p=false +fi + + +# as_fn_executable_p FILE +# ----------------------- +# Test if FILE is an executable regular file. +as_fn_executable_p () +{ + test -f "$1" && test -x "$1" +} # as_fn_executable_p +as_test_x='test -x' +as_executable_p=as_fn_executable_p + +# Sed expression to map a string onto a valid CPP name. +as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" + +# Sed expression to map a string onto a valid variable name. +as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" + + +exec 6>&1 +## ----------------------------------- ## +## Main body of $CONFIG_STATUS script. ## +## ----------------------------------- ## +_ASEOF +test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1 + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +# Save the log message, to keep $0 and so on meaningful, and to +# report actual input values of CONFIG_FILES etc. instead of their +# values after options handling. +ac_log=" +This file was extended by lightgbm $as_me 4.6.0.99, which was +generated by GNU Autoconf 2.71. Invocation command line was + + CONFIG_FILES = $CONFIG_FILES + CONFIG_HEADERS = $CONFIG_HEADERS + CONFIG_LINKS = $CONFIG_LINKS + CONFIG_COMMANDS = $CONFIG_COMMANDS + $ $0 $@ + +on `(hostname || uname -n) 2>/dev/null | sed 1q` +" + +_ACEOF + +case $ac_config_files in *" +"*) set x $ac_config_files; shift; ac_config_files=$*;; +esac + + + +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +# Files that config.status was made for. +config_files="$ac_config_files" + +_ACEOF + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +ac_cs_usage="\ +\`$as_me' instantiates files and other configuration actions +from templates according to the current configuration. Unless the files +and actions are specified as TAGs, all are instantiated by default. + +Usage: $0 [OPTION]... [TAG]... + + -h, --help print this help, then exit + -V, --version print version number and configuration settings, then exit + --config print configuration, then exit + -q, --quiet, --silent + do not print progress messages + -d, --debug don't remove temporary files + --recheck update $as_me by reconfiguring in the same conditions + --file=FILE[:TEMPLATE] + instantiate the configuration file FILE + +Configuration files: +$config_files + +Report bugs to the package provider." + +_ACEOF +ac_cs_config=`printf "%s\n" "$ac_configure_args" | sed "$ac_safe_unquote"` +ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"` +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +ac_cs_config='$ac_cs_config_escaped' +ac_cs_version="\\ +lightgbm config.status 4.6.0.99 +configured by $0, generated by GNU Autoconf 2.71, + with options \\"\$ac_cs_config\\" + +Copyright (C) 2021 Free Software Foundation, Inc. +This config.status script is free software; the Free Software Foundation +gives unlimited permission to copy, distribute and modify it." + +ac_pwd='$ac_pwd' +srcdir='$srcdir' +test -n "\$AWK" || AWK=awk +_ACEOF + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +# The default lists apply if the user does not specify any file. +ac_need_defaults=: +while test $# != 0 +do + case $1 in + --*=?*) + ac_option=`expr "X$1" : 'X\([^=]*\)='` + ac_optarg=`expr "X$1" : 'X[^=]*=\(.*\)'` + ac_shift=: + ;; + --*=) + ac_option=`expr "X$1" : 'X\([^=]*\)='` + ac_optarg= + ac_shift=: + ;; + *) + ac_option=$1 + ac_optarg=$2 + ac_shift=shift + ;; + esac + + case $ac_option in + # Handling of the options. + -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) + ac_cs_recheck=: ;; + --version | --versio | --versi | --vers | --ver | --ve | --v | -V ) + printf "%s\n" "$ac_cs_version"; exit ;; + --config | --confi | --conf | --con | --co | --c ) + printf "%s\n" "$ac_cs_config"; exit ;; + --debug | --debu | --deb | --de | --d | -d ) + debug=: ;; + --file | --fil | --fi | --f ) + $ac_shift + case $ac_optarg in + *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + '') as_fn_error $? "missing file argument" ;; + esac + as_fn_append CONFIG_FILES " '$ac_optarg'" + ac_need_defaults=false;; + --he | --h | --help | --hel | -h ) + printf "%s\n" "$ac_cs_usage"; exit ;; + -q | -quiet | --quiet | --quie | --qui | --qu | --q \ + | -silent | --silent | --silen | --sile | --sil | --si | --s) + ac_cs_silent=: ;; + + # This is an error. + -*) as_fn_error $? "unrecognized option: \`$1' +Try \`$0 --help' for more information." ;; + + *) as_fn_append ac_config_targets " $1" + ac_need_defaults=false ;; + + esac + shift +done + +ac_configure_extra_args= + +if $ac_cs_silent; then + exec 6>/dev/null + ac_configure_extra_args="$ac_configure_extra_args --silent" +fi + +_ACEOF +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +if \$ac_cs_recheck; then + set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion + shift + \printf "%s\n" "running CONFIG_SHELL=$SHELL \$*" >&6 + CONFIG_SHELL='$SHELL' + export CONFIG_SHELL + exec "\$@" +fi + +_ACEOF +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +exec 5>>config.log +{ + echo + sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX +## Running $as_me. ## +_ASBOX + printf "%s\n" "$ac_log" +} >&5 + +_ACEOF +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +_ACEOF + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 + +# Handling of arguments. +for ac_config_target in $ac_config_targets +do + case $ac_config_target in + "src/Makevars") CONFIG_FILES="$CONFIG_FILES src/Makevars" ;; + + *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; + esac +done + + +# If the user did not use the arguments to specify the items to instantiate, +# then the envvar interface is used. Set only those that are not. +# We use the long form for the default assignment because of an extremely +# bizarre bug on SunOS 4.1.3. +if $ac_need_defaults; then + test ${CONFIG_FILES+y} || CONFIG_FILES=$config_files +fi + +# Have a temporary directory for convenience. Make it in the build tree +# simply because there is no reason against having it here, and in addition, +# creating and moving files from /tmp can sometimes cause problems. +# Hook for its removal unless debugging. +# Note that there is a small window in which the directory will not be cleaned: +# after its creation but before its name has been assigned to `$tmp'. +$debug || +{ + tmp= ac_tmp= + trap 'exit_status=$? + : "${ac_tmp:=$tmp}" + { test ! -d "$ac_tmp" || rm -fr "$ac_tmp"; } && exit $exit_status +' 0 + trap 'as_fn_exit 1' 1 2 13 15 +} +# Create a (secure) tmp directory for tmp files. + +{ + tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null` && + test -d "$tmp" +} || +{ + tmp=./conf$$-$RANDOM + (umask 077 && mkdir "$tmp") +} || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5 +ac_tmp=$tmp + +# Set up the scripts for CONFIG_FILES section. +# No need to generate them if there are no CONFIG_FILES. +# This happens for instance with `./config.status config.h'. +if test -n "$CONFIG_FILES"; then + + +ac_cr=`echo X | tr X '\015'` +# On cygwin, bash can eat \r inside `` if the user requested igncr. +# But we know of no other shell where ac_cr would be empty at this +# point, so we can use a bashism as a fallback. +if test "x$ac_cr" = x; then + eval ac_cr=\$\'\\r\' +fi +ac_cs_awk_cr=`$AWK 'BEGIN { print "a\rb" }' /dev/null` +if test "$ac_cs_awk_cr" = "a${ac_cr}b"; then + ac_cs_awk_cr='\\r' +else + ac_cs_awk_cr=$ac_cr +fi + +echo 'BEGIN {' >"$ac_tmp/subs1.awk" && +_ACEOF + + +{ + echo "cat >conf$$subs.awk <<_ACEOF" && + echo "$ac_subst_vars" | sed 's/.*/&!$&$ac_delim/' && + echo "_ACEOF" +} >conf$$subs.sh || + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 +ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'` +ac_delim='%!_!# ' +for ac_last_try in false false false false false :; do + . ./conf$$subs.sh || + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 + + ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X` + if test $ac_delim_n = $ac_delim_num; then + break + elif $ac_last_try; then + as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 + else + ac_delim="$ac_delim!$ac_delim _$ac_delim!! " + fi +done +rm -f conf$$subs.sh + +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK && +_ACEOF +sed -n ' +h +s/^/S["/; s/!.*/"]=/ +p +g +s/^[^!]*!// +:repl +t repl +s/'"$ac_delim"'$// +t delim +:nl +h +s/\(.\{148\}\)..*/\1/ +t more1 +s/["\\]/\\&/g; s/^/"/; s/$/\\n"\\/ +p +n +b repl +:more1 +s/["\\]/\\&/g; s/^/"/; s/$/"\\/ +p +g +s/.\{148\}// +t nl +:delim +h +s/\(.\{148\}\)..*/\1/ +t more2 +s/["\\]/\\&/g; s/^/"/; s/$/"/ +p +b +:more2 +s/["\\]/\\&/g; s/^/"/; s/$/"\\/ +p +g +s/.\{148\}// +t delim +' >$CONFIG_STATUS || ac_write_fail=1 +rm -f conf$$subs.awk +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +_ACAWK +cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && + for (key in S) S_is_set[key] = 1 + FS = "" + +} +{ + line = $ 0 + nfields = split(line, field, "@") + substed = 0 + len = length(field[1]) + for (i = 2; i < nfields; i++) { + key = field[i] + keylen = length(key) + if (S_is_set[key]) { + value = S[key] + line = substr(line, 1, len) "" value "" substr(line, len + keylen + 3) + len += length(value) + length(field[++i]) + substed = 1 + } else + len += 1 + keylen + } + + print line +} + +_ACAWK +_ACEOF +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then + sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g" +else + cat +fi < "$ac_tmp/subs1.awk" > "$ac_tmp/subs.awk" \ + || as_fn_error $? "could not setup config files machinery" "$LINENO" 5 +_ACEOF + +# VPATH may cause trouble with some makes, so we remove sole $(srcdir), +# ${srcdir} and @srcdir@ entries from VPATH if srcdir is ".", strip leading and +# trailing colons and then remove the whole line if VPATH becomes empty +# (actually we leave an empty line to preserve line numbers). +if test "x$srcdir" = x.; then + ac_vpsub='/^[ ]*VPATH[ ]*=[ ]*/{ +h +s/// +s/^/:/ +s/[ ]*$/:/ +s/:\$(srcdir):/:/g +s/:\${srcdir}:/:/g +s/:@srcdir@:/:/g +s/^:*// +s/:*$// +x +s/\(=[ ]*\).*/\1/ +G +s/\n// +s/^[^=]*=[ ]*$// +}' +fi + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +fi # test -n "$CONFIG_FILES" + + +eval set X " :F $CONFIG_FILES " +shift +for ac_tag +do + case $ac_tag in + :[FHLC]) ac_mode=$ac_tag; continue;; + esac + case $ac_mode$ac_tag in + :[FHL]*:*);; + :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;; + :[FH]-) ac_tag=-:-;; + :[FH]*) ac_tag=$ac_tag:$ac_tag.in;; + esac + ac_save_IFS=$IFS + IFS=: + set x $ac_tag + IFS=$ac_save_IFS + shift + ac_file=$1 + shift + + case $ac_mode in + :L) ac_source=$1;; + :[FH]) + ac_file_inputs= + for ac_f + do + case $ac_f in + -) ac_f="$ac_tmp/stdin";; + *) # Look for the file first in the build tree, then in the source tree + # (if the path is not absolute). The absolute path cannot be DOS-style, + # because $ac_f cannot contain `:'. + test -f "$ac_f" || + case $ac_f in + [\\/$]*) false;; + *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";; + esac || + as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;; + esac + case $ac_f in *\'*) ac_f=`printf "%s\n" "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac + as_fn_append ac_file_inputs " '$ac_f'" + done + + # Let's still pretend it is `configure' which instantiates (i.e., don't + # use $as_me), people would be surprised to read: + # /* config.h. Generated by config.status. */ + configure_input='Generated from '` + printf "%s\n" "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' + `' by configure.' + if test x"$ac_file" != x-; then + configure_input="$ac_file. $configure_input" + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 +printf "%s\n" "$as_me: creating $ac_file" >&6;} + fi + # Neutralize special characters interpreted by sed in replacement strings. + case $configure_input in #( + *\&* | *\|* | *\\* ) + ac_sed_conf_input=`printf "%s\n" "$configure_input" | + sed 's/[\\\\&|]/\\\\&/g'`;; #( + *) ac_sed_conf_input=$configure_input;; + esac + + case $ac_tag in + *:-:* | *:-) cat >"$ac_tmp/stdin" \ + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; + esac + ;; + esac + + ac_dir=`$as_dirname -- "$ac_file" || +$as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$ac_file" : 'X\(//\)[^/]' \| \ + X"$ac_file" : 'X\(//\)$' \| \ + X"$ac_file" : 'X\(/\)' \| . 2>/dev/null || +printf "%s\n" X"$ac_file" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q'` + as_dir="$ac_dir"; as_fn_mkdir_p + ac_builddir=. + +case "$ac_dir" in +.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; +*) + ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` + # A ".." for each directory in $ac_dir_suffix. + ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + case $ac_top_builddir_sub in + "") ac_top_builddir_sub=. ac_top_build_prefix= ;; + *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; + esac ;; +esac +ac_abs_top_builddir=$ac_pwd +ac_abs_builddir=$ac_pwd$ac_dir_suffix +# for backward compatibility: +ac_top_builddir=$ac_top_build_prefix + +case $srcdir in + .) # We are building in place. + ac_srcdir=. + ac_top_srcdir=$ac_top_builddir_sub + ac_abs_top_srcdir=$ac_pwd ;; + [\\/]* | ?:[\\/]* ) # Absolute name. + ac_srcdir=$srcdir$ac_dir_suffix; + ac_top_srcdir=$srcdir + ac_abs_top_srcdir=$srcdir ;; + *) # Relative name. + ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix + ac_top_srcdir=$ac_top_build_prefix$srcdir + ac_abs_top_srcdir=$ac_pwd/$srcdir ;; +esac +ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix + + + case $ac_mode in + :F) + # + # CONFIG_FILE + # + +_ACEOF + +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +# If the template does not know about datarootdir, expand it. +# FIXME: This hack should be removed a few years after 2.60. +ac_datarootdir_hack=; ac_datarootdir_seen= +ac_sed_dataroot=' +/datarootdir/ { + p + q +} +/@datadir@/p +/@docdir@/p +/@infodir@/p +/@localedir@/p +/@mandir@/p' +case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in +*datarootdir*) ac_datarootdir_seen=yes;; +*@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*) + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 +printf "%s\n" "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} +_ACEOF +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 + ac_datarootdir_hack=' + s&@datadir@&$datadir&g + s&@docdir@&$docdir&g + s&@infodir@&$infodir&g + s&@localedir@&$localedir&g + s&@mandir@&$mandir&g + s&\\\${datarootdir}&$datarootdir&g' ;; +esac +_ACEOF + +# Neutralize VPATH when `$srcdir' = `.'. +# Shell code in configure.ac might set extrasub. +# FIXME: do we really want to maintain this feature? +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +ac_sed_extra="$ac_vpsub +$extrasub +_ACEOF +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +:t +/@[a-zA-Z_][a-zA-Z_0-9]*@/!b +s|@configure_input@|$ac_sed_conf_input|;t t +s&@top_builddir@&$ac_top_builddir_sub&;t t +s&@top_build_prefix@&$ac_top_build_prefix&;t t +s&@srcdir@&$ac_srcdir&;t t +s&@abs_srcdir@&$ac_abs_srcdir&;t t +s&@top_srcdir@&$ac_top_srcdir&;t t +s&@abs_top_srcdir@&$ac_abs_top_srcdir&;t t +s&@builddir@&$ac_builddir&;t t +s&@abs_builddir@&$ac_abs_builddir&;t t +s&@abs_top_builddir@&$ac_abs_top_builddir&;t t +$ac_datarootdir_hack +" +eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$ac_tmp/subs.awk" \ + >$ac_tmp/out || as_fn_error $? "could not create $ac_file" "$LINENO" 5 + +test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && + { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && + { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ + "$ac_tmp/out"`; test -z "$ac_out"; } && + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir' +which seems to be undefined. Please make sure it is defined" >&5 +printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir' +which seems to be undefined. Please make sure it is defined" >&2;} + + rm -f "$ac_tmp/stdin" + case $ac_file in + -) cat "$ac_tmp/out" && rm -f "$ac_tmp/out";; + *) rm -f "$ac_file" && mv "$ac_tmp/out" "$ac_file";; + esac \ + || as_fn_error $? "could not create $ac_file" "$LINENO" 5 + ;; + + + + esac + +done # for ac_tag + + +as_fn_exit 0 +_ACEOF +ac_clean_files=$ac_clean_files_save + +test $ac_write_fail = 0 || + as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5 + + +# configure is writing to config.log, and then calls config.status. +# config.status does its own redirection, appending to config.log. +# Unfortunately, on DOS this fails, as config.log is still kept open +# by configure, so config.status won't be able to write to it; its +# output is simply discarded. So we exec the FD to /dev/null, +# effectively closing config.log, so it can be properly (re)opened and +# appended to by config.status. When coming back to configure, we +# need to make the FD available again. +if test "$no_create" != yes; then + ac_cs_success=: + ac_config_status_args= + test "$silent" = yes && + ac_config_status_args="$ac_config_status_args --quiet" + exec 5>/dev/null + $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false + exec 5>>config.log + # Use ||, not &&, to avoid exiting from the if with $? = 1, which + # would make configure fail if this is the last instruction. + $ac_cs_success || as_fn_exit 1 +fi +if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 +printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} +fi + + diff --git a/R-package/configure.ac b/R-package/configure.ac new file mode 100644 index 0000000..d0f0462 --- /dev/null +++ b/R-package/configure.ac @@ -0,0 +1,172 @@ +### configure.ac -*- Autoconf -*- +# Template used by Autoconf to generate 'configure' script. For more see: +# * https://unconj.ca/blog/an-autoconf-primer-for-r-package-authors.html +# * https://cran.r-project.org/doc/manuals/r-release/R-exts.html#Configure-and-cleanup + +AC_PREREQ(2.69) +AC_INIT([lightgbm], [~~VERSION~~], [], [lightgbm], []) + +########################### +# find compiler and flags # +########################### + +AC_MSG_CHECKING([location of R]) +AC_MSG_RESULT([${R_HOME}]) + +# set up CPP flags +# find the compiler and compiler flags used by R. +: ${R_HOME=`R HOME`} +if test -z "${R_HOME}"; then + echo "could not determine R_HOME" + exit 1 +fi +CXX17=`"${R_HOME}/bin/R" CMD config CXX17` +CXX17STD=`"${R_HOME}/bin/R" CMD config CXX17STD` +CXX="${CXX17} ${CXX17STD}" +CPPFLAGS=`"${R_HOME}/bin/R" CMD config CPPFLAGS` +CXXFLAGS=`"${R_HOME}/bin/R" CMD config CXX17FLAGS` +LDFLAGS=`"${R_HOME}/bin/R" CMD config LDFLAGS` +AC_LANG(C++) + +# LightGBM-specific flags +LGB_CPPFLAGS="" + +######### +# Eigen # +######### + +LGB_CPPFLAGS="${LGB_CPPFLAGS} -DEIGEN_MPL2_ONLY -DEIGEN_DONT_PARALLELIZE" + +############### +# MM_PREFETCH # +############### + +AC_MSG_CHECKING([whether MM_PREFETCH works]) +ac_mmprefetch=no +AC_LANG_CONFTEST( + [ + AC_LANG_PROGRAM( + [[ + #include + ]], + [[ + int a = 0; + _mm_prefetch(&a, _MM_HINT_NTA); + return 0; + ]] + ) + ] +) +${CXX} ${CPPFLAGS} ${CXXFLAGS} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_mmprefetch=yes +AC_MSG_RESULT([${ac_mmprefetch}]) +if test "${ac_mmprefetch}" = yes; then + LGB_CPPFLAGS="${LGB_CPPFLAGS} -DMM_PREFETCH=1" +fi + +############ +# MM_ALLOC # +############ + +AC_MSG_CHECKING([whether MM_MALLOC works]) +ac_mm_malloc=no +AC_LANG_CONFTEST( + [ + AC_LANG_PROGRAM( + [[ + #include + ]], + [[ + char *a = (char*)_mm_malloc(8, 16); + _mm_free(a); + return 0; + ]] + ) + ] +) +${CXX} ${CPPFLAGS} ${CXXFLAGS} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_mm_malloc=yes +AC_MSG_RESULT([${ac_mm_malloc}]) +if test "${ac_mm_malloc}" = yes; then + LGB_CPPFLAGS="${LGB_CPPFLAGS} -DMM_MALLOC=1" +fi + +########## +# OpenMP # +########## + +OPENMP_CXXFLAGS="" + +if test `uname -s` = "Linux" +then + OPENMP_CXXFLAGS="\$(SHLIB_OPENMP_CXXFLAGS)" +fi + +if test `uname -s` = "Darwin" +then + OPENMP_CXXFLAGS='-Xclang -fopenmp' + OPENMP_LIB='-lomp' + + # libomp 15.0+ from brew is keg-only (i.e. not symlinked into the standard paths search by the linker), + # so need to search in other locations. + # See https://github.com/Homebrew/homebrew-core/issues/112107#issuecomment-1278042927. + # + # If Homebrew is found and libomp was installed with it, this code adds the necessary + # flags for the compiler to find libomp headers and for the linker to find libomp.dylib. + HOMEBREW_LIBOMP_PREFIX="" + if command -v brew >/dev/null 2>&1; then + ac_brew_openmp=no + AC_MSG_CHECKING([whether OpenMP was installed via Homebrew]) + brew --prefix libomp >/dev/null 2>&1 && ac_brew_openmp=yes + AC_MSG_RESULT([${ac_brew_openmp}]) + if test "${ac_brew_openmp}" = yes; then + HOMEBREW_LIBOMP_PREFIX=`brew --prefix libomp` + OPENMP_CXXFLAGS="${OPENMP_CXXFLAGS} -I${HOMEBREW_LIBOMP_PREFIX}/include" + OPENMP_LIB="${OPENMP_LIB} -L${HOMEBREW_LIBOMP_PREFIX}/lib" + fi + fi + ac_pkg_openmp=no + AC_MSG_CHECKING([whether OpenMP will work in a package]) + AC_LANG_CONFTEST( + [ + AC_LANG_PROGRAM( + [[ + #include + ]], + [[ + return (omp_get_max_threads() <= 1); + ]] + ) + ] + ) + ${CXX} ${CPPFLAGS} ${CXXFLAGS} ${LDFLAGS} ${OPENMP_CXXFLAGS} ${OPENMP_LIB} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_pkg_openmp=yes + + # -Xclang is not portable (it is clang-specific) + # if compilation above failed, try without that flag + if test "${ac_pkg_openmp}" = no; then + if test -f "./conftest"; then + rm ./conftest + fi + OPENMP_CXXFLAGS="-fopenmp" + ${CXX} ${CPPFLAGS} ${CXXFLAGS} ${LDFLAGS} ${OPENMP_CXXFLAGS} ${OPENMP_LIB} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_pkg_openmp=yes + fi + + AC_MSG_RESULT([${ac_pkg_openmp}]) + if test "${ac_pkg_openmp}" = no; then + OPENMP_CXXFLAGS='' + OPENMP_LIB='' + echo '***********************************************************************************************' + echo ' OpenMP is unavailable on this macOS system. LightGBM code will run single-threaded as a result.' + echo ' To use all CPU cores for training jobs, you should install OpenMP by running' + echo '' + echo ' brew install libomp' + echo '***********************************************************************************************' + fi +fi + +# substitute variables from this script into Makevars.in +AC_SUBST(OPENMP_CXXFLAGS) +AC_SUBST(OPENMP_LIB) +AC_SUBST(LGB_CPPFLAGS) +AC_CONFIG_FILES([src/Makevars]) + +# write out Autoconf output +AC_OUTPUT diff --git a/R-package/configure.win b/R-package/configure.win new file mode 100755 index 0000000..050d9a3 --- /dev/null +++ b/R-package/configure.win @@ -0,0 +1,102 @@ +# Script used to generate `Makevars.win` from `Makevars.win.in` +# on Windows + +########################### +# find compiler and flags # +########################### + +R_EXE="${R_HOME}/bin${R_ARCH_BIN}/R" + +CXX17=`"${R_EXE}" CMD config CXX17` +CXX17STD=`"${R_EXE}" CMD config CXX17STD` +CXX="${CXX17} ${CXX17STD}" +CXXFLAGS=`"${R_EXE}" CMD config CXX17FLAGS` +CPPFLAGS=`"${R_EXE}" CMD config CPPFLAGS` + +# LightGBM-specific flags +LGB_CPPFLAGS="" + +######### +# Eigen # +######### + +LGB_CPPFLAGS="${LGB_CPPFLAGS} -DEIGEN_MPL2_ONLY -DEIGEN_DONT_PARALLELIZE" + +############### +# MM_PREFETCH # +############### + +ac_mm_prefetch="no" + +cat > conftest.cpp < +int main() { + int a = 0; + _mm_prefetch(&a, _MM_HINT_NTA); + return 0; +} +EOL + +${CXX} ${CXXFLAGS} ${CPPFLAGS} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_mm_prefetch="yes" +rm -f ./conftest +rm -f ./conftest.cpp +echo "checking whether MM_PREFETCH works...${ac_mm_prefetch}" + +if test "${ac_mm_prefetch}" = "yes"; +then + LGB_CPPFLAGS="${LGB_CPPFLAGS} -DMM_PREFETCH=1" +fi + +############ +# MM_ALLOC # +############ +ac_mm_malloc="no" + +cat > conftest.cpp < +int main() { + char *a = (char*)_mm_malloc(8, 16); + _mm_free(a); + return 0; +} +EOL + +${CXX} ${CXXFLAGS} ${CPPFLAGS} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_mm_malloc="yes" +rm -f ./conftest +rm -f ./conftest.cpp +echo "checking whether MM_MALLOC works...${ac_mm_malloc}" + +if test "${ac_mm_malloc}" = "yes"; +then + LGB_CPPFLAGS="${LGB_CPPFLAGS} -DMM_MALLOC=1" +fi + +############# +# INET_PTON # +############# + +ac_inet_pton="no" + +cat > conftest.cpp < +int main() { + int (*fptr)(int, const char*, void*); + fptr = &inet_pton; + return 0; +} +EOL + +${CXX} ${CXXFLAGS} ${CPPFLAGS} -o conftest conftest.cpp 2>/dev/null && ./conftest && ac_inet_pton="yes" +rm -f ./conftest +rm -f ./conftest.cpp +echo "checking whether INET_PTON works...${ac_inet_pton}" + +if test "${ac_inet_pton}" = "yes"; +then + LGB_CPPFLAGS="${LGB_CPPFLAGS} -DWIN_HAS_INET_PTON=1" +fi + +# Generate Makevars.win from Makevars.win.in +sed -e \ + "s/@LGB_CPPFLAGS@/$LGB_CPPFLAGS/" \ + < src/Makevars.win.in > src/Makevars.win diff --git a/R-package/cran-comments.md b/R-package/cran-comments.md new file mode 100644 index 0000000..08bbad0 --- /dev/null +++ b/R-package/cran-comments.md @@ -0,0 +1,962 @@ +# CRAN Submission History + +## v4.6.0 - Submission 1 - (February 13, 2025) + +### CRAN response + +Accepted to CRAN + +### Maintainer Notes + +This release fixed several issues reported by CRAN. +Bashisms in `configure` + +```text +possible bashism in configure.ac line 63 (should be VAR="${VAR}foo"): + LGB_CPPFLAGS+=" -DMM_PREFETCH=1" +possible bashism in configure.ac line 89 (should be VAR="${VAR}foo"): + LGB_CPPFLAGS+=" -DMM_MALLOC=1" +``` + +Compilation errors on GCC 15. + +```text +io/json11.cpp:97:28: error: 'uint8_t' does not name a type + 97 | } else if (static_cast(ch) == 0xe2 && + | ^~~~~~~ +io/json11.cpp:97:28: note: 'uint8_t' is defined in header ''; this is probably fixable by adding '#include ' +io/json11.cpp:98:28: error: 'uint8_t' does not name a type + 98 | static_cast(value[i + 1]) == 0x80 && +``` + +This release contains fixes for those issues. + +## v4.5.0 - Submission 1 - (July 25, 2024) + +### CRAN response + +Accepted to CRAN + +### Maintainer Notes + +This release was a response to a request from CRAN. +On July 4, 2024, CRAN notified us that the following compiler warnings raised by `gcc` 14 needed to be fixed by August 3, 2024. + +```text +Result: WARN + Found the following significant warnings: + io/dense_bin.hpp:617:27: warning: template-id not allowed for constructor in C++20 [-Wtemplate-id-cdtor] + io/multi_val_dense_bin.hpp:346:26: warning: template-id not allowed for constructor in C++20 [-Wtemplate-id-cdtor] + io/multi_val_sparse_bin.hpp:433:36: warning: template-id not allowed for constructor in C++20 [-Wtemplate-id-cdtor] + io/sparse_bin.hpp:785:19: warning: template-id not allowed for constructor in C++20 [-Wtemplate-id-cdtor] + See ‘/data/gannet/ripley/R/packages/tests-devel/lightgbm.Rcheck/00install.out’ for details. +``` + +This release contains fixes for those issues. + +## v4.4.0 - Submission 1 - (June 14, 2024) + +### CRAN response + +Accepted to CRAN + +### Maintainer Notes + +This was a standard release of `{lightgbm}`, not intended to fix any particular R-specific issues. + +## v4.3.0 - Submission 1 - (January 18, 2024) + +### CRAN response + +Accepted to CRAN + +### Maintainer Notes + +This submission was put up in response to CRAN saying the package would be archived if the following +warning was not fixed within 14 days. + +```text +/usr/local/clang-trunk/bin/../include/c++/v1/__fwd/string_view.h:22:41: +warning: 'char_traits' is deprecated: +char_traits for T not equal to char, wchar_t, char8_t, char16_t or char32_t is non-standard and is provided for a temporary period. +It will be removed in LLVM 19, so please migrate off of it. [-Wdeprecated-declarations] +``` + +See https://github.com/lightgbm-org/LightGBM/issues/6264. + +## v4.2.0 - Submission 1 - (December 7, 2023) + +### CRAN response + +Accepted to CRAN + +### Maintainer Notes + +This submission included many changes from the last 2 years, as well as fixes for a warning +CRAN said could cause the package to be archived: https://github.com/lightgbm-org/LightGBM/issues/6221. + +## v4.1.0 - not submitted + +v4.1.0 was not submitted to CRAN, because https://github.com/lightgbm-org/LightGBM/issues/5987 had not been resolved. + +## v4.0.0 - Submission 2 - (July 19, 2023) + +### CRAN response + +> Dear maintainer, +> package lightgbm_4.0.0.tar.gz does not pass the incoming checks automatically. + +The logs linked from those messages showed one issue remaining on Debian (0 on Windows). + +```text +* checking examples ... [7s/4s] NOTE +Examples with CPU time > 2.5 times elapsed time + user system elapsed ratio +lgb.restore_handle 1.206 0.085 0.128 10.08 +``` + +### Maintainer Notes + +Chose to document the issue and need for a fix in https://github.com/lightgbm-org/LightGBM/issues/5987, but not resubmit, +to avoid annoying CRAN maintainers. + +## v4.0.0 - Submission 1 - (July 16, 2023) + +### CRAN response + +> Dear maintainer, +> package lightgbm_4.0.0.tar.gz does not pass the incoming checks automatically. + +The logs linked from those messages showed the following issues from `R CMD check`. + +```text +* checking S3 generic/method consistency ... NOTE +Mismatches for apparent methods not registered: +merge: + function(x, y, ...) +merge.eval.string: + function(env) + +format: + function(x, ...) +format.eval.string: + function(eval_res, eval_err) +See section 'Registering S3 methods' in the 'Writing R Extensions' +manual. +``` + +```text +* checking examples ... [8s/4s] NOTE +Examples with CPU time > 2.5 times elapsed time + user system elapsed ratio +lgb.restore_handle 1.819 0.128 0.165 11.8 +``` + +### Maintainer Notes + +Attempted to fix these with https://github.com/lightgbm-org/LightGBM/pull/5988 and resubmitted. + +## v3.3.5 - Submission 2 - (January 16, 2023) + +### CRAN response + +> Reason was +> +> Flavor: r-devel-windows-x86_64 +> Check: OOverall checktime, Result: NOTE +> Overall checktime 14 min > 10 min +> +> but the maintainer cannot do much to reduce this, so I triggered revdep checks now. +> Please reply to the archival message in case the issue is not fixable easily. +> +> Best, +> Uwe Ligges + +### Maintainer Notes + +This was technically not a "resubmission". +We asked CRAN why the first v3.3.5 submission had been archived, and they responded with the response above... and then v3.3.5 passed all checks with no further work from LightGBM maintainers. + +## v3.3.5 - Submission 1 - (January 11, 2023) + +### CRAN response + +Archived without a response. + +### Maintainer Notes + +Submitted with the following comment. + +> This submission contains {lightgbm} 3.3.5 + +> Per CRAN's policies, I am submitting it on behalf of the project's maintainer (Yu Shi), with his permission. + +> This submission includes patches to address the following warnings observed on the fedora and debian CRAN checks. + +> Found the following significant warnings: +> io/json11.cpp:207:47: warning: unqualified call to 'std::move' [-Wunqualified-std-cast-call] +> io/json11.cpp:216:51: warning: unqualified call to 'std::move' [-Wunqualified-std-cast-call] +> io/json11.cpp:225:53: warning: unqualified call to 'std::move' [-Wunqualified-std-cast-call] +> io/json11.cpp:268:60: warning: unqualified call to 'std::move' [-Wunqualified-std-cast-call] +> io/json11.cpp:272:36: warning: unqualified call to 'std::move' [-Wunqualified-std-cast-call] +> io/json11.cpp:276:37: warning: unqualified call to 'std::move' [-Wunqualified-std-cast-call] +> io/json11.cpp:381:41: warning: unqualified call to 'std::move' [-Wunqualified-std-cast-call] +> io/json11.cpp:150:39: warning: unqualified call to 'std::move' [-Wunqualified-std-cast-call] + +Thank you very much for your time and consideration. + +## v3.3.4 - Submission 1 - (December 15, 2022) + +### CRAN response + +Accepted to CRAN + +### Maintainer Notes + +Submitted with the following comment: + +> This submission contains {lightgbm} 3.3.4 + +> Per CRAN's policies, I am submitting it on behalf of the project's maintainer (Yu Shi), with his permission. + +> This submission includes patches to address the following warnings observed on the fedora and debian CRAN checks. +> +> Compiled code should not call entry points which might terminate R nor write to stdout/stderr instead of to the console, nor use Fortran I/O nor system RNGs nor [v]sprintf. + +> Thank you very much for your time and consideration. + +## v3.3.3 - Submission 1 - (October 10, 2022) + +### CRAN response + +Accepted to CRAN + +### Maintainer Notes + +Submitted with the following comment: + +> This submission contains {lightgbm} 3.3.3. + +> Per CRAN's policies, I am submitting on it on behalf of the project's maintainer (Yu Shi), with his permission (https://github.com/lightgbm-org/LightGBM/pull/5525). + +> This submission includes two patches: +> * a change to testing to avoid a failed test related to non-ASCII strings on the `r-devel-linux-x86_64-debian-clang` check flavor (https://github.com/lightgbm-org/LightGBM/pull/5526) +> * modifications to allow compatibility with the RTools42 build toolchain (https://github.com/lightgbm-org/LightGBM/pull/5503) + +> Thank you very much for your time and consideration. + +## v3.3.2 - Submission 1 - (January 7, 2022) + +### CRAN response + +Accepted to CRAN on January 14, 2022. + +### Maintainer Notes + +In this submission, we uploaded a patch that CRAN stuff provided us via e-mail. The full text of the e-mail from CRAN: + +```text +Dear maintainers, + +This concerns the CRAN packages + +Cairo cepreader gpboost httpuv ipaddress lightgbm proj4 prophet +RcppCWB RcppParallel RDieHarder re2 redux rgeolocate RGtk2 tth +udunits2 unrtf + +maintained by one of you: + +Andreas Blaette andreas.blaette@uni-due.de: RcppCWB +David Hall david.hall.physics@gmail.com: ipaddress +Dirk Eddelbuettel edd@debian.org: RDieHarder +Fabio Sigrist fabiosigrist@gmail.com: gpboost +Friedrich Leisch Friedrich.Leisch@R-project.org: tth +Girish Palya girishji@gmail.com: re2 +James Hiebert hiebert@uvic.ca: udunits2 +Jari Oksanen jhoksane@gmail.com: cepreader +Kevin Ushey kevin@rstudio.com: RcppParallel +ORPHANED: RGtk2 +Os Keyes ironholds@gmail.com: rgeolocate +Rich FitzJohn rich.fitzjohn@gmail.com: redux +Sean Taylor sjtz@pm.me: prophet +Simon Urbanek simon.urbanek@r-project.org: proj4 +Simon Urbanek Simon.Urbanek@r-project.org: Cairo +Winston Chang winston@rstudio.com: httpuv +Yu Shi yushi2@microsoft.com: lightgbm + +your packages need to be updated for R-devel/R 4.2 to work on Windows, +following the recent switch to UCRT and Rtools42. + +Sorry for the group message, please feel free to respond individually +regarding your package or ask specifically about what needs to be fixed. + +I've created patches for you, so please review them and fix your packages: + +https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fsvn.r-project.org%2FR-dev-web%2Ftrunk%2FWindowsBuilds%2Fwinutf8%2Fucrt3%2Fr_packages%2Fpatches%2FCRAN%2F&data=04%7C01%7Cyushi2%40microsoft.com%7C8e6c353d1a8842c81eeb08d9bef5d835%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637750786169848244%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=rFGf7Y4Dvo6g1kzV%2BeAJDLGm1TUtzQsLsavElTw6H1U%3D&reserved=0 + +You can apply them as follows + +tar xfz package_1.0.0.tar.gz + +wget +https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fsvn.r-project.org%2FR-dev-web%2Ftrunk%2FWindowsBuilds%2Fwinutf8%2Fucrt3%2Fr_packages%2Fpatches%2FCRAN%2Fpackage.diff&data=04%7C01%7Cyushi2%40microsoft.com%7C8e6c353d1a8842c81eeb08d9bef5d835%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637750786169848244%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=iyTjhoqvzj3IbQ8HGCZeh1IQl34FAGpIdVyZWkzNvO0%3D&reserved=0 + +patch --binary < package.diff + +These patches are currently automatically applied by R-devel on Windows +at installation time, which makes most of your packages pass their +checks (as OK or NOTE), but please check your results carefully and +carefully review the patches. Usually these changes were because of +newer GCC or newer MinGW in the toolchain, but some for other reasons, +and some of them will definitely have to be improved so that the package +keeps building also for older versions of R using Rtools40. We have only +been testing the patches with UCRT (and Rtools42) on Windows. + +For more information, please see + +https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fdeveloper.r-project.org%2FBlog%2Fpublic%2F2021%2F12%2F07%2Fupcoming-changes-in-r-4.2-on-windows%2F&data=04%7C01%7Cyushi2%40microsoft.com%7C8e6c353d1a8842c81eeb08d9bef5d835%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637750786169848244%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=SY77zgtbDbHvTxTgPLOoe%2Fw5OZDhXvJoxpVOoEaKoYo%3D&reserved=0 +https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fdeveloper.r-project.org%2FWindowsBuilds%2Fwinutf8%2Fucrt3%2Fhowto.html&data=04%7C01%7Cyushi2%40microsoft.com%7C8e6c353d1a8842c81eeb08d9bef5d835%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637750786169848244%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=dlVJ4nhQlmDPd56bHoVsWZuRfrUUorvOWxoUTmVDM%2Bg%3D&reserved=0 + +Once you add your patches/fix the issues, your package will probably +show a warning during R CMD check (as patching would be attempted to be +applied again). That's ok, at that point please let me know and I will +remove my patch from the repository of automatically applied patches. + +If you end up just applying the patch as is, there is probably no need +testing on your end, but you can do so using Winbuilder, r-hub, github +actions (e.g. https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fkalibera%2Fucrt3&data=04%7C01%7Cyushi2%40microsoft.com%7C8e6c353d1a8842c81eeb08d9bef5d835%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637750786169848244%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=msqoPzqDStlAUn%2Bb6gGevwFPD%2FaNL5dTxiNud2Sqzy8%3D&reserved=0). + +If you wanted to test locally on your Windows machine and do not have a +UCRT version of R-devel yet, please uninstall your old version of +R-devel, delete the old library used with that, install a new UCRT +version of R-devel , and install Rtools42. You can keep Rtools40 +installed if you need it with R 4.1 or earlier. + +Currently, the new R-devel can be downloaded from +https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fwww.r-project.org%2Fnosvn%2Fwinutf8%2Fucrt3%2Fweb%2Frdevel.html&data=04%7C01%7Cyushi2%40microsoft.com%7C8e6c353d1a8842c81eeb08d9bef5d835%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637750786169848244%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=0hCwONzLmcW0GIXNqiOZQEIuhNA%2BjHhQvXsofs8J98o%3D&reserved=0 + +And Rtools42 from +https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fwww.r-project.org%2Fnosvn%2Fwinutf8%2Fucrt3%2Fweb%2Frtools.html&data=04%7C01%7Cyushi2%40microsoft.com%7C8e6c353d1a8842c81eeb08d9bef5d835%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637750786169848244%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=WLWLbOyQKbaYz8gkfKz2sqoGknjIOtl1aGAhUF%2Bpylg%3D&reserved=0 + +If you end up testing locally, you can use R_INSTALL_TIME_PATCHES +environment variable to disable the automated patching, see the "howto" +document above. That way you could also see what the original issue was +causing. + +If you wanted to find libraries to link for yourself, e.g. in a newer +version of your package, please look for "Using findLinkingOrder with +Rtools42 (tiff package example)" in the "howto" document above. I +created the patches for you manually before we finished this script, so +you may be able to create a shorter version using it, but - it's +probably not worth the effort. + +If you wanted to try in a virtual machine, but did not have a license, +you can use also an automated setup of a free trial VM from +https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fdeveloper.r-project.org%2FBlog%2Fpublic%2F2021%2F03%2F18%2Fvirtual-windows-machine-for-checking-r-packages&data=04%7C01%7Cyushi2%40microsoft.com%7C8e6c353d1a8842c81eeb08d9bef5d835%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637750786169848244%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=aFFQYuC9CoBwBiLgZHi8N3yUnSiHu5Xtdqb2YBiMIHQ%3D&reserved=0 + +(but that needs a very good and un-metered network connection to install) + +Please let us know if you have any questions. + +Thanks, +Tomas & Uwe +``` + +## v3.3.1 - Submission 1 - (October 27, 2021) + +### CRAN response + +Accepted to CRAN on October 30, 2021. + +CRAN completed its checks and preparation of binaries on November 6, 2021. + +### Maintainer Notes + +Submitted v3.3.1 to CRAN, with the following fixes for the issues that caused CRAN to reject v3.3.0 and archive the package: + +* https://github.com/lightgbm-org/LightGBM/pull/4673 +* https://github.com/lightgbm-org/LightGBM/pull/4714 + +Submitted with the following comment: + +> This submission contains {lightgbm} 3.3.1. +> Per CRAN's policies, I am submitting on it on behalf of the project's maintainer (Yu Shi), with his permission (https://github.com/lightgbm-org/LightGBM/pull/4715#issuecomment-952537783). + +> {lightgbm} was removed from CRAN on October 25, 2021 due to issues detected in the gcc-ASAN and clang-ASAN checks. To the best of our knowledge, we believe this release fixes those issues. We have introduced automated testing that we believe faithfully reproduces CRAN's tests with sanitizers (https://github.com/lightgbm-org/LightGBM/pull/4678). + +> Thank you very much for your time and consideration. + +Progress on the submission was tracked in https://github.com/lightgbm-org/LightGBM/issues/4713. + +## v3.3.0 - Submission 1 - (October 8, 2021) + +### CRAN response + +`{lightgbm}` was removed from CRAN entirely on October 25, 2021. + +On October 12, 2021, maintainers received the following message from CRAN (ripley@stats.ox.ac.uk): + +> Dear maintainer, + +> Please see the problems shown on https://cran.r-project.org/web/checks/check_results_lightgbm.html + +> Please correct before 2021-10-25 to safely retain your package on CRAN. + +> Do remember to look at the 'Additional issues'. + +> The CRAN Team + +We failed to produce a new submission prior to that date, so the package was removed entirely. + +See https://github.com/lightgbm-org/LightGBM/issues/4713 for additional background and links explaining the specific failed CRAN checks. + +### Maintainer Notes + +In this submission, we attempted to switch the maintainer of the package (in the CRAN official sense) from Guolin Ke to Yu Shi. +Did this by adding a note in the CRAN submission web form explaining Guolin's departure from Microsoft. + +## v3.2.1 - Submission 1 - (April 12, 2021) + +### CRAN response + +Accepted to CRAN. + +### Maintainer Notes + +## v3.2.0 - Submission 1 - (March 22, 2021) + +### CRAN response + +Package is failing checks in the `r-devel-linux-x86_64-debian-clang` environment (described [here](https://cran.r-project.org/web/checks/check_flavors.html#r-devel-linux-x86_64-debian-clang)). Specifically, one unit test on the use of non-ASCII feature names in `Booster$dump_model()` fails. + +> Apparently your package fails its checks in a strict Latin-1* locale, +e.g. under Linux using LANG=en_US.iso88591 (see the debian-clang +results). + +> Please correct before 2021-04-21 to safely retain your package on CRAN. + +### Maintainer Notes + +Submitted a version 3.2.1 to correct the errors noted. + +## v3.1.1 - Submission 1 - (December 7, 2020) + +### CRAN response + +Accepted to CRAN, December 8. + +### Maintainer Notes + +Submitted a fix to 3.1.0 that skips some learning-to-rank tests on 32-bit Windows. + +## v3.1.0 - Submission 1 - (November 15, 2020) + +### CRAN response + +Accepted to CRAN, November 18. + +On November 21, found out that the CRAN's `r-oldrel-windows-ix86+x86_64` check was failing, with an issue similar to the one faced on Solaris and fixed in https://github.com/lightgbm-org/LightGBM/pull/3534. + +CRAN did not ask for a re-submission, but this was fixed in 3.1.1. + +### Maintainer Notes + +This package was submitted with the following information in the "optional comments" box. + +```text +Hello, + +I'm submitting {lightgbm} 3.1.0 on behalf of the maintainer, Guolin Ke. I am a co-author on the package, and he has asked me to handle this submission. We saw in https://cran.r-project.org/web/packages/policies.html#Submission that this is permitted. + +{lightgbm} was removed from CRAN in October for issues found by valgrind checks. We have invested significant effort in addressing those issues and creating an automatic test that tries to replicate CRAN's valgrind checks: https://github.com/lightgbm-org/LightGBM/blob/742d72f8bb051105484fd5cca11620493ffb0b2b/.github/workflows/r_valgrind.yml. + +We see two warnings from valgrind that we believe are not problematic. + +==2063== Conditional jump or move depends on uninitialised value(s) +==2063== at 0x49CF138: gregexpr_Regexc (grep.c:2439) +==2063== by 0x49D1F13: do_regexpr (grep.c:3100) +==2063== by 0x49A0058: bcEval (eval.c:7121) +==2063== by 0x498B67F: Rf_eval (eval.c:727) +==2063== by 0x498E414: R_execClosure (eval.c:1895) +==2063== by 0x498E0C7: Rf_applyClosure (eval.c:1821) +==2063== by 0x499FC8C: bcEval (eval.c:7089) +==2063== by 0x498B67F: Rf_eval (eval.c:727) +==2063== by 0x498B1CB: forcePromise (eval.c:555) +==2063== by 0x49963AB: FORCE_PROMISE (eval.c:5142) +==2063== by 0x4996566: getvar (eval.c:5183) +==2063== by 0x499D1A5: bcEval (eval.c:6873) +==2063== Uninitialised value was created by a stack allocation +==2063== at 0x49CEC37: gregexpr_Regexc (grep.c:2369) + +This seems to be related to R itself and not any code in {lightgbm}. + +==2063== 336 bytes in 1 blocks are possibly lost in loss record 153 of 2,709 +==2063== at 0x483DD99: calloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) +==2063== by 0x40149CA: allocate_dtv (dl-tls.c:286) +==2063== by 0x40149CA: _dl_allocate_tls (dl-tls.c:532) +==2063== by 0x5702322: allocate_stack (allocatestack.c:622) +==2063== by 0x5702322: pthread_create@@GLIBC_2.2.5 (pthread_create.c:660) +==2063== by 0x56D0DDA: ??? (in /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0) +==2063== by 0x56C88E0: GOMP_parallel (in /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0) +==2063== by 0x1544D29C: LGBM_DatasetCreateFromCSC (c_api.cpp:1286) +==2063== by 0x1546F980: LGBM_DatasetCreateFromCSC_R (lightgbm_R.cpp:91) +==2063== by 0x4941E2F: R_doDotCall (dotcode.c:634) +==2063== by 0x494CCC6: do_dotcall (dotcode.c:1281) +==2063== by 0x499FB01: bcEval (eval.c:7078) +==2063== by 0x498B67F: Rf_eval (eval.c:727) +==2063== by 0x498E414: R_execClosure (eval.c:1895) + +We believe this is a false positive, and related to a misunderstanding between valgrind and openmp (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=36298). + +We have also added automated tests with ASAN/UBSAN to our testing setup, and have checked the package on Solaris 10 and found no issues. + +Thanks for your time and consideration. +``` + +## v3.0.0.2 - Submission 1 - (September 29, 2020) + +### CRAN response + +First response was a message talking about failing checks on 3.0.0. + +```text +package lightgbm_3.0.0.2.tar.gz has been auto-processed. +The auto-check found additional issues for the last version released on CRAN: +gcc-UBSAN +valgrind +CRAN incoming checks do not test for these additional issues and you will need an appropriately instrumented build of R to reproduce these. +Hence please reply-all and explain: Have these been fixed? + +Please correct before 2020-10-05 to safely retain your package on CRAN. + +There is still a valgrind error. This did not happen when tested on +submission, but the tests did run until timeout at 4 hours. When you +write illegally, corruption is common. + +Illegal writes are serious errors. +``` + +Then in later responses to email correspondence with CRAN, CRAN expressed frustration with the number of failed submission and banned this package from new submissions for a month. + +The content of that frustrated message was regrettable and it does not need to be preserved forever in this file. + +### Maintainer Notes + +The 3.0.0.x series is officially not making it to CRAN. We will wait until November, and try again. + +Detailed plan about what will be tried before November 2020 to increase the likelihood of success for that package: https://github.com/lightgbm-org/LightGBM/pull/3338#issuecomment-702756840. + +## v3.0.0.1 - Submission 1 - (September 24, 2020) + +### CRAN response + +```text +Thanks, we see: + +Still lots of alignment errors, such as + +lightgbm.Rcheck/tests/testthat.Rout:io/dataset_loader.cpp:340:59: +runtime error: reference binding to misaligned address 0x7f51fefad81e for type 'const value_type', which requires 4 byte alignment +lightgbm.Rcheck/tests/testthat.Rout:/usr/include/c++/10/bits/stl_vector.h:1198:21: +runtime error: reference binding to misaligned address 0x7f51fefad81e for type 'const int', which requires 4 byte alignment lightgbm.Rcheck/tests/testthat.Rout:/usr/include/c++/10/bits/vector.tcc:449:28:runtime +error: reference binding to misaligned address 0x7f51fefad81e for type 'const type', which requires 4 byte alignment +lightgbm.Rcheck/tests/testthat.Rout:/usr/include/c++/10/bits/move.h:77:36: +runtime error: reference binding to misaligned address 0x7f51fefad81e for type 'const int', which requires 4 byte alignment +lightgbm.Rcheck/tests/testthat.Rout:/usr/include/c++/10/bits/alloc_traits.h:512:17: +runtime error: reference binding to misaligned address 0x7f51fefad81e for type 'const type', which requires 4 byte alignment + +Please fix and resubmit. +``` + +### Maintainer Notes + +Ok, these are the notes from the UBSAN tests. Was able to reproduce them with https://github.com/lightgbm-org/LightGBM/pull/3338#issuecomment-700399862, and they were fixed in https://github.com/lightgbm-org/LightGBM/pull/3415. + +Struggling to replicate the valgrind result (running `R CMD check --use-valgrind` returns no issues), so trying submission again. Hoping that the fixes for mis-alignment fix the other errors too. + +## v3.0.0 - Submission 6 - (September 24, 2020) + +### CRAN response + +Failing pre-checks. + +### `R CMD check` results + +```text +* checking CRAN incoming feasibility ... WARNING +Maintainer: ‘Guolin Ke ’ + +Insufficient package version (submitted: 3.0.0, existing: 3.0.0) + +Days since last update: 4 +``` + +### Maintainer Notes + +Did not think the version needed to be incremented if submitting a package in response to CRAN saying "you are failing checks and will be kicked off if you don't fix it", but I guess you do! + +This can be fixed by just re-submitting but with the version changed from `3.0.0` to `3.0.0.1`. + +## v3.0.0 - Submission 5 - (September 11, 2020) + +### CRAN Response + +Accepted to CRAN! + +Please correct the problems below before 2020-10-05 to safely retain your package on CRAN: + +```text +checking installed package size ... NOTE + installed size is 49.7Mb + sub-directories of 1Mb or more: + libs 49.1Mb + +"network/socket_wrapper.hpp", line 30: Error: Could not open include file. +"network/socket_wrapper.hpp", line 216: Error: The type "ifaddrs" is incomplete. +"network/socket_wrapper.hpp", line 217: Error: The type "ifaddrs" is incomplete. +"network/socket_wrapper.hpp", line 220: Error: The type "ifaddrs" is incomplete. +"network/socket_wrapper.hpp", line 222: Error: The type "ifaddrs" is incomplete. +"network/socket_wrapper.hpp", line 214: Error: The function "getifaddrs" must have a prototype. +"network/socket_wrapper.hpp", line 228: Error: The function "freeifaddrs" must have a prototype. +"network/linkers_socket.cpp", line 76: Warning: A non-POD object of type "std::chrono::duration>" passed as a variable argument to function "static LightGBM::Log::Info(const char*, ...)". +7 Error(s) and 1 Warning(s) detected. +*** Error code 2 +make: Fatal error: Command failed for target `network/linkers_socket.o' +Current working directory /tmp/RtmpNfaavG/R.INSTALL40a84f70130a/lightgbm/src +ERROR: compilation failed for package ‘lightgbm’ +* removing ‘/home/ripley/R/Lib32/lightgbm’ +``` + +### Maintainer Notes + +Added a patch that `psutil` has used to fix missing `ifaddrs.h` on Solaris 10: https://github.com/lightgbm-org/LightGBM/issues/629#issuecomment-665091451. + +## v3.0.0 - Submission 4 - (September 4, 2020) + +### CRAN Response + +> Thanks, if the running time is the only reason to wrap the examples in +\donttest, please replace \donttest by \donttest (\donttest examples are +not executed in the CRAN checks). + +> Please replace cat() by message() or warning() in your functions (except +for print() and summary() functions). Messages and warnings can be +suppressed if needed. + +> Missing Rd-tags: + lightgbm/man/dimnames.lgb.Dataset.Rd: \value + lightgbm/man/lgb.Dataset.construct.Rd: \value + lightgbm/man/lgb.prepare.Rd: \value + ... + +> Please add the tag and explain in detail the returned objects. + +### Maintainer Notes + +Responded to CRAN with the following: + +All examples have been wrapped with `\donttest` as requested. We have replied to Swetlana Herbrandt asking for clarification on the donttest news item in the R 4.0.2 changelog (https://cran.r-project.org/doc/manuals/r-devel/NEWS.html). + +All uses of `cat()` have been replaced with `print()`. We chose `print()` over `message()` because it's important that they be written to stdout alongside all the other logs coming from the library's C++ code. `message()` and `warning()` write to stderr. + +All exported objects now have `\value{}` statements in their documentation files in `man/`. + +**We also replied directly to CRAN's feedback email** + +> Swetlana, + +> Thank you for your comments. I've just created a new submission that I believe addresses them. + +> Can you help us understand something? In your message you said "\donttest examples are +not executed in the CRAN checks)", but in https://cran.r-project.org/doc/manuals/r-devel/NEWS.html we see the following: + +> > "`R CMD check --as-cran` now runs \donttest examples (which are run by example()) instead of instructing the tester to do so. This can be temporarily circumvented during development by setting environment variable `_R_CHECK_DONTTEST_EXAMPLES_` to a false value." + +> Could you help us understand how both of those statements can be true? + +## v3.0.0 - Submission 3 - (August 29, 2020) + +### CRAN response + +* Please write references in the description of the DESCRIPTION file in +the form + - authors (year) doi:... + - authors (year) arXiv:... + - authors (year, ISBN:...) +* if those are not available: authors (year) https:... with no space after 'doi:', 'arXiv:', 'https:' and angle brackets for auto-linking. +* (If you want to add a title as well please put it in quotes: "Title") + +* \donttest{} should only be used if the example really cannot be executed (e.g. because of missing additional software, missing API keys, ...) by the user. That's why wrapping examples in \donttest{} adds the comment ("# Not run:") as a warning for the user. Does not seem necessary. Please unwrap the examples if they are executable in < 5 sec, or replace +\donttest{} with \donttest{}. + +* Please do not modify the global environment (e.g. by using <<-) in your +functions. This is not allowed by the CRAN policies. + +* Please always add all authors, contributors and copyright holders in the Authors@R field with the appropriate roles. From CRAN policies you agreed to: "The ownership of copyright and intellectual property rights of all components of the package must be clear and unambiguous (including from the authors specification in the DESCRIPTION file). Where code is copied (or derived) from the work of others (including from R itself), care must be taken that any copyright/license statements are preserved and authorship is not misrepresented." e.g.: Microsoft Corporation, Dropbox Inc. Please explain in the submission comments what you did about this issue. + +Please fix and resubmit + +### Maintainer Notes + +Responded to CRAN with the following: + +The paper citation has been adjusted as requested. We were using 'glmnet' as a guide on how to include the URL but maybe they are no longer in compliance with CRAN policies: https://github.com/cran/glmnet/blob/b1a4b50de01e0cd24343959d7cf86452bac17b26/DESCRIPTION + +All authors from the original LightGBM paper have been added to Authors@R as `"aut"`. We have also added Microsoft and DropBox, Inc. as `"cph"` (copyright holders). These roles were chosen based on the guidance in https://journal.r-project.org/archive/2012/RJ-2012-009/index.html. + +lightgbm's code does use `<<-`, but it does not modify the global environment. The uses of `<<-` in R/lgb.interprete.R and R/callback.R are in functions which are called in an environment created by the lightgbm functions that call them, and this operator is used to reach one level up into the calling function's environment. + +We chose to wrap our examples in `\donttest{}` because we found, through testing on https://r-hub.github.io/rhub/ and in our own continuous integration environments, that their run time varies a lot between platforms, and we cannot guarantee that all examples will run in under 5 seconds. We intentionally chose `\donttest{}` over `\donttest{}` because this item in the R 4.0.0 changelog (https://cran.r-project.org/doc/manuals/r-devel/NEWS.html) seems to indicate that \donttest will be ignored by CRAN's automated checks: + +> "`R CMD check --as-cran` now runs \donttest examples (which are run by example()) instead of instructing the tester to do so. This can be temporarily circumvented during development by setting environment variable `_R_CHECK_DONTTEST_EXAMPLES_` to a false value." + +We run all examples with `R CMD check --as-cran --run-dontrun` in our continuous integration tests on every commit to the package, so we have high confidence that they are working correctly. + +## v3.0.0 - Submission 2 - (August 28, 2020) + +### CRAN response + +Failing pre-checks. + +### `R CMD check` results + +* Debian: 2 NOTEs + + ```text + * checking CRAN incoming feasibility ... NOTE + Maintainer: 'Guolin Ke ' + + New submission + + Possibly mis-spelled words in DESCRIPTION: + Guolin (13:52) + Ke (13:48) + LightGBM (14:20) + al (13:62) + et (13:59) + + * checking top-level files ... NOTE + Non-standard files/directories found at top level: + 'docs' 'lightgbm-hex-logo.png' 'lightgbm-hex-logo.svg' + ``` + +* Windows: 2 NOTEs + + ```text + * checking CRAN incoming feasibility ... NOTE + Maintainer: 'Guolin Ke ' + + New submission + + Possibly mis-spelled words in DESCRIPTION: + Guolin (13:52) + Ke (13:48) + LightGBM (14:20) + al (13:62) + et (13:59) + + * checking top-level files ... NOTE + Non-standard files/directories found at top level: + 'docs' 'lightgbm-hex-logo.png' 'lightgbm-hex-logo.svg' + ``` + +### Maintainer Notes + +We should tell them the misspellings note is a false positive. + +For the note about included files, that is my fault. I had extra files laying around when I generated the package. I'm surprised to see `docs/` in that list, since it is ignored in `.Rbuildignore`. I even tested that with [the exact code Rbuildignore uses](https://github.com/wch/r-source/blob/9d13622f41cfa0f36db2595bd6a5bf93e2010e21/src/library/tools/R/build.R#L85). For now, I added `rm -r docs/` to `build-cran-package.sh`. We can figure out what is happening with `.Rbuildignore` in the future, but it shouldn't block a release. + +## v3.0.0 - Submission 1 - (August 24, 2020) + +NOTE: 3.0.0-1 was never released to CRAN. CRAN was on vacation August 14-24, 2020, and in that time version 3.0.0-1 (a release candidate) became 3.0.0. + +### CRAN response + +> Please only ship the CRAN template for the MIT license. + +> Is there some reference about the method you can add in the Description field in the form Authors (year) doi:.....? + +> Please fix and resubmit. + +### `R CMD check` results + +* Debian: 1 NOTE + + ```text + * checking CRAN incoming feasibility ... NOTE + Maintainer: ‘Guolin Ke ’ + + New submission + + License components with restrictions and base license permitting such: + MIT + file LICENSE + ``` + +* Windows: 1 NOTE + + ```text + * checking CRAN incoming feasibility ... NOTE + Maintainer: 'Guolin Ke ' + + New submission + + License components with restrictions and base license permitting such: + MIT + file LICENSE + ``` + +### Maintainer Notes + +Tried updating `LICENSE` file to this template: + +```yaml +YEAR: 2016 +COPYRIGHT HOLDER: Microsoft Corporation +``` + +Added a citation and link for [the main paper](https://proceedings.neurips.cc/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html) in `DESCRIPTION`. + +## v3.0.0-1 - Submission 3 - (August 12, 2020) + +### CRAN response + +Failing pre-checks. + +### `R CMD check` results + +* Debian: 1 NOTE + + ```text + * checking CRAN incoming feasibility ... NOTE + Maintainer: ‘Guolin Ke ’ + + New submission + + License components with restrictions and base license permitting such: + MIT + file LICENSE + ``` + +* Windows: 1 ERROR, 1 NOTE + + ```text + * checking CRAN incoming feasibility ... NOTE + Maintainer: ‘Guolin Ke ’ + + New submission + + License components with restrictions and base license permitting such: + MIT + file LICENSE + + ** running tests for arch 'i386' ... [9s] ERROR + Running 'testthat.R' [8s] + Running the tests in 'tests/testthat.R' failed. + Complete output: + > library(testthat) + > library(lightgbm) + Loading required package: R6 + > + > test_check( + + package = "lightgbm" + + , stop_on_failure = TRUE + + , stop_on_warning = FALSE + + ) + -- 1. Error: predictions do not fail for integer input (@test_Predictor.R#7) -- + lgb.Dataset.construct: cannot create Dataset handle + Backtrace: + 1. lightgbm::lgb.train(...) + 2. data$construct() + ``` + +### Maintainer Notes + +The "checking CRAN incoming feasibility" NOTE can be safely ignored. It only shows up the first time you submit a package to CRAN. + +So the only thing I see broken right now is the test error on 32-bit Windows. This is documented in https://github.com/lightgbm-org/LightGBM/issues/3187. + +## v3.0.0-1 - Submission 2 - (August 10, 2020) + +### CRAN response + +Failing pre-checks. + +### `R CMD check` results + +* Debian: 2 NOTEs + + ```text + * checking CRAN incoming feasibility ... NOTE + Maintainer: ‘Guolin Ke ’ + + New submission + + License components with restrictions and base license permitting such: + MIT + file LICENSE + + Non-standard files/directories found at top level: + ‘cran-comments.md’ ‘docs’ + ``` + +* Windows: 1 ERROR, 2 NOTEs + + ```text + * checking CRAN incoming feasibility ... NOTE + Maintainer: 'Guolin Ke ' + + New submission + + License components with restrictions and base license permitting such: + MIT + file LICENSE + + * checking top-level files ... NOTE + Non-standard files/directories found at top level: + 'cran-comments.md' 'docs' + + ** checking whether the package can be loaded ... ERROR + Loading this package had a fatal error status code 1 + Loading log: + Error: package 'lightgbm' is not installed for 'arch = i386' + Execution halted + ``` + +### Maintainer Notes + +Seems removing `Biarch` field didn't work. Noticed this in the install logs: + +> Warning: this package has a non-empty 'configure.win' file, so building only the main architecture + +Tried adding `Biarch: true` to `DESCRIPTION` to overcome this. + +NOTE about non-standard files was the result of a mistake in `.Rbuildignore` syntax, and something strange with how `cran-comments.md` line in `.Rbuildignore` was treated. Updated `.Rbuildignore` and added an `rm cran-comments.md` to `build-cran-package.sh`. + +## v3.0.0-1 - Submission 1 - (August 9, 2020) + +### CRAN response + +Failing pre-checks. + +### `R CMD check` results + +* Debian: 1 NOTE + + ```text + Possibly mis-spelled words in DESCRIPTION: + LightGBM (12:88, 19:41, 20:60, 20:264) + ``` + +* Windows: 1 ERROR, 1 NOTE + + ```text + Possibly mis-spelled words in DESCRIPTION: + LightGBM (12:88, 19:41, 20:60, 20:264) + + ** checking whether the package can be loaded ... ERROR + Loading this package had a fatal error status code 1 + Loading log: + Error: package 'lightgbm' is not installed for 'arch = i386' + Execution halted + ``` + +### Maintainer Notes + +Thought the issue on Windows was caused by `Biarch: false` in `DESCRIPTION`. Removed `Biarch` field. + +Thought the "misspellings" issue could be resolved by adding single quotes around LightGBM, like `'LightGBM'`. diff --git a/R-package/demo/00Index b/R-package/demo/00Index new file mode 100644 index 0000000..f5b6144 --- /dev/null +++ b/R-package/demo/00Index @@ -0,0 +1,10 @@ +basic_walkthrough Basic feature walkthrough +boost_from_prediction Boosting from existing prediction +categorical_features_rules Categorical Feature Preparation with Rules +cross_validation Cross Validation +early_stopping Early Stop in training +efficient_many_training Efficiency for Many Model Trainings +multiclass Multiclass training/prediction +multiclass_custom_objective Multiclass with Custom Objective Function +leaf_stability Leaf (in)Stability example +weight_param Weight-Parameter adjustment relationship diff --git a/R-package/demo/basic_walkthrough.R b/R-package/demo/basic_walkthrough.R new file mode 100644 index 0000000..c9ac484 --- /dev/null +++ b/R-package/demo/basic_walkthrough.R @@ -0,0 +1,153 @@ +library(lightgbm) + +# We load in the agaricus dataset +# In this example, we are aiming to predict whether a mushroom is edible +data(agaricus.train, package = "lightgbm") +data(agaricus.test, package = "lightgbm") +train <- agaricus.train +test <- agaricus.test + +# The loaded data is stored in sparseMatrix, and label is a numeric vector in {0,1} +class(train$label) +class(train$data) + +# Set parameters for model training +train_params <- list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , nthread = 2L +) + +#--------------------Basic Training using lightgbm---------------- +# This is the basic usage of lightgbm you can put matrix in data field +# Note: we are putting in sparse matrix here, lightgbm naturally handles sparse input +# Use sparse matrix when your feature is sparse (e.g. when you are using one-hot encoding vector) +print("Training lightgbm with sparseMatrix") +bst <- lightgbm( + data = train$data + , params = train_params + , label = train$label + , nrounds = 2L +) + +# Alternatively, you can put in dense matrix, i.e. basic R-matrix +print("Training lightgbm with Matrix") +bst <- lightgbm( + data = as.matrix(train$data) + , params = train_params + , label = train$label + , nrounds = 2L +) + +# You can also put in lgb.Dataset object, which stores label, data and other meta datas needed for advanced features +print("Training lightgbm with lgb.Dataset") +dtrain <- lgb.Dataset( + data = train$data + , label = train$label +) +bst <- lightgbm( + data = dtrain + , params = train_params + , nrounds = 2L +) + +# Verbose = 0,1,2 +print("Train lightgbm with verbose 0, no message") +bst <- lightgbm( + data = dtrain + , params = train_params + , nrounds = 2L + , verbose = 0L +) + +print("Train lightgbm with verbose 1, print evaluation metric") +bst <- lightgbm( + data = dtrain + , params = train_params + , nrounds = 2L + , verbose = 1L +) + +print("Train lightgbm with verbose 2, also print information about tree") +bst <- lightgbm( + data = dtrain + , params = train_params + , nrounds = 2L + , verbose = 2L +) + +# You can also specify data as file path to a LibSVM/TCV/CSV format input +# Since we do not have this file with us, the following line is just for illustration +# bst <- lightgbm( +# data = "agaricus.train.svm" +# , num_leaves = 4L +# , learning_rate = 1.0 +# , nrounds = 2L +# , objective = "binary" +# ) + +#--------------------Basic prediction using lightgbm-------------- +# You can do prediction using the following line +# You can put in Matrix, sparseMatrix, or lgb.Dataset +pred <- predict(bst, test$data) +err <- mean(as.numeric(pred > 0.5) != test$label) +print(paste("test-error=", err)) + +#--------------------Save and load models------------------------- +# Save model to binary local file +lgb.save(bst, "lightgbm.model") + +# Load binary model to R +bst2 <- lgb.load("lightgbm.model") +pred2 <- predict(bst2, test$data) + +# pred2 should be identical to pred +print(paste("sum(abs(pred2-pred))=", sum(abs(pred2 - pred)))) + +#--------------------Advanced features --------------------------- +# To use advanced features, we need to put data in lgb.Dataset +dtrain <- lgb.Dataset(data = train$data, label = train$label, free_raw_data = FALSE) +dtest <- lgb.Dataset.create.valid(dtrain, data = test$data, label = test$label) + +#--------------------Using validation set------------------------- +# valids is a list of lgb.Dataset, each of them is tagged with name +valids <- list(train = dtrain, test = dtest) + +# To train with valids, use lgb.train, which contains more advanced features +# valids allows us to monitor the evaluation result on all data in the list +print("Train lightgbm using lgb.train with valids") +bst <- lgb.train( + data = dtrain + , params = train_params + , nrounds = 2L + , valids = valids +) + +# We can change evaluation metrics, or use multiple evaluation metrics +print("Train lightgbm using lgb.train with valids, watch logloss and error") +bst <- lgb.train( + data = dtrain + , params = train_params + , nrounds = 2L + , valids = valids + , eval = c("binary_error", "binary_logloss") +) + +# lgb.Dataset can also be saved using lgb.Dataset.save +lgb.Dataset.save(dtrain, "dtrain.buffer") + +# To load it in, simply call lgb.Dataset +dtrain2 <- lgb.Dataset("dtrain.buffer") +bst <- lgb.train( + data = dtrain2 + , params = train_params + , nrounds = 2L + , valids = valids +) + +# information can be extracted from lgb.Dataset using get_field() +label <- get_field(dtest, "label") +pred <- predict(bst, test$data) +err <- as.numeric(sum(as.integer(pred > 0.5) != label)) / length(label) +print(paste("test-error=", err)) diff --git a/R-package/demo/boost_from_prediction.R b/R-package/demo/boost_from_prediction.R new file mode 100644 index 0000000..68d33a7 --- /dev/null +++ b/R-package/demo/boost_from_prediction.R @@ -0,0 +1,38 @@ +library(lightgbm) + +# Load in the agaricus dataset +data(agaricus.train, package = "lightgbm") +data(agaricus.test, package = "lightgbm") +dtrain <- lgb.Dataset(agaricus.train$data, label = agaricus.train$label) +dtest <- lgb.Dataset.create.valid(dtrain, data = agaricus.test$data, label = agaricus.test$label) + +valids <- list(eval = dtest, train = dtrain) +#--------------------Advanced features --------------------------- +# advanced: start from an initial base prediction +print("Start running example to start from an initial prediction") + +# Train lightgbm for 1 round +param <- list( + num_leaves = 4L + , learning_rate = 1.0 + , nthread = 2L + , objective = "binary" +) +bst <- lgb.train(param, dtrain, 1L, valids = valids) + +# Note: we need the margin value instead of transformed prediction in set_init_score +ptrain <- predict(bst, agaricus.train$data, type = "raw") +ptest <- predict(bst, agaricus.test$data, type = "raw") + +# set the init_score property of dtrain and dtest +# base margin is the base prediction we will boost from +set_field(dtrain, "init_score", ptrain) +set_field(dtest, "init_score", ptest) + +print("This is result of boost from initial prediction") +bst <- lgb.train( + params = param + , data = dtrain + , nrounds = 5L + , valids = valids +) diff --git a/R-package/demo/categorical_features_rules.R b/R-package/demo/categorical_features_rules.R new file mode 100644 index 0000000..97af5a7 --- /dev/null +++ b/R-package/demo/categorical_features_rules.R @@ -0,0 +1,100 @@ +# Here we are going to try training a model with categorical features + +# Load libraries +library(data.table) +library(lightgbm) + +# Load data and look at the structure +# +# Classes 'data.table' and 'data.frame': 4521 obs. of 17 variables: +# $ age : int 30 33 35 30 59 35 36 39 41 43 ... +# $ job : chr "unemployed" "services" "management" "management" ... +# $ marital : chr "married" "married" "single" "married" ... +# $ education: chr "primary" "secondary" "tertiary" "tertiary" ... +# $ default : chr "no" "no" "no" "no" ... +# $ balance : int 1787 4789 1350 1476 0 747 307 147 221 -88 ... +# $ housing : chr "no" "yes" "yes" "yes" ... +# $ loan : chr "no" "yes" "no" "yes" ... +# $ contact : chr "cellular" "cellular" "cellular" "unknown" ... +# $ day : int 19 11 16 3 5 23 14 6 14 17 ... +# $ month : chr "oct" "may" "apr" "jun" ... +# $ duration : int 79 220 185 199 226 141 341 151 57 313 ... +# $ campaign : int 1 1 1 4 1 2 1 2 2 1 ... +# $ pdays : int -1 339 330 -1 -1 176 330 -1 -1 147 ... +# $ previous : int 0 4 1 0 0 3 2 0 0 2 ... +# $ poutcome : chr "unknown" "failure" "failure" "unknown" ... +# $ y : chr "no" "no" "no" "no" ... +data(bank, package = "lightgbm") +str(bank) + +# We are dividing the dataset into two: one train, one validation +bank_train <- bank[1L:4000L, ] +bank_test <- bank[4001L:4521L, ] + +# We must now transform the data to fit in LightGBM +# For this task, we use lgb.convert_with_rules +# The function transforms the data into a fittable data +# +# Classes 'data.table' and 'data.frame': 521 obs. of 17 variables: +# $ age : int 53 36 58 26 34 55 55 34 41 38 ... +# $ job : num 1 10 10 9 10 2 2 3 3 4 ... +# $ marital : num 1 2 1 3 3 2 2 2 1 1 ... +# $ education: num 2 2 2 2 2 1 2 3 2 2 ... +# $ default : num 1 1 1 1 1 1 1 1 1 1 ... +# $ balance : int 26 191 -123 -147 179 1086 471 105 1588 70 ... +# $ housing : num 2 1 1 1 1 2 2 2 2 1 ... +# $ loan : num 1 1 1 1 1 1 1 1 2 1 ... +# $ contact : num 1 1 1 3 1 1 3 3 3 1 ... +# $ day : int 7 31 5 4 19 6 30 28 20 27 ... +# $ month : num 9 2 2 7 2 9 9 9 7 11 ... +# $ duration : int 56 69 131 95 294 146 58 249 10 255 ... +# $ campaign : int 1 1 2 2 3 1 2 2 8 3 ... +# $ pdays : int 359 -1 -1 -1 -1 272 -1 -1 -1 148 ... +# $ previous : int 1 0 0 0 0 2 0 0 0 1 ... +# $ poutcome : num 1 4 4 4 4 1 4 4 4 3 ... +# $ y : num 1 1 1 1 1 1 1 1 1 2 ... +bank_rules <- lgb.convert_with_rules(data = bank_train) +bank_train <- bank_rules$data +bank_test <- lgb.convert_with_rules(data = bank_test, rules = bank_rules$rules)$data +str(bank_test) + +# Remove 1 to label because it must be between 0 and 1 +bank_train$y <- bank_train$y - 1L +bank_test$y <- bank_test$y - 1L + +# Data input to LightGBM must be a matrix, without the label +my_data_train <- as.matrix(bank_train[, 1L:16L, with = FALSE]) +my_data_test <- as.matrix(bank_test[, 1L:16L, with = FALSE]) + +# Creating the LightGBM dataset with categorical features +# The categorical features can be passed to lgb.train to not copy and paste a lot +dtrain <- lgb.Dataset( + data = my_data_train + , label = bank_train$y + , categorical_feature = c(2L, 3L, 4L, 5L, 7L, 8L, 9L, 11L, 16L) +) +dtest <- lgb.Dataset.create.valid( + dtrain + , data = my_data_test + , label = bank_test$y +) + +# We can now train a model +params <- list( + objective = "binary" + , metric = "l2" + , min_data = 1L + , learning_rate = 0.1 + , min_hessian = 1.0 + , max_depth = 2L +) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 100L + , valids = list(train = dtrain, valid = dtest) +) + +# Try to find split_feature: 11 +# If you find it, it means it used a categorical feature in the first tree +lgb.dump(model, num_iteration = 1L) diff --git a/R-package/demo/cross_validation.R b/R-package/demo/cross_validation.R new file mode 100644 index 0000000..90d4379 --- /dev/null +++ b/R-package/demo/cross_validation.R @@ -0,0 +1,72 @@ +library(lightgbm) + +# load in the agaricus dataset +data(agaricus.train, package = "lightgbm") +data(agaricus.test, package = "lightgbm") +dtrain <- lgb.Dataset(agaricus.train$data, label = agaricus.train$label) +dtest <- lgb.Dataset.create.valid(dtrain, data = agaricus.test$data, label = agaricus.test$label) + +nrounds <- 2L +param <- list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" +) + +print("Running cross validation") +# Do cross validation, this will print result out as +# [iteration] metric_name:mean_value+std_value +# std_value is standard deviation of the metric +lgb.cv( + param + , dtrain + , nrounds + , nfold = 5L + , eval = "binary_error" +) + +print("Running cross validation, disable standard deviation display") +# do cross validation, this will print result out as +# [iteration] metric_name:mean_value+std_value +# std_value is standard deviation of the metric +lgb.cv( + param + , dtrain + , nrounds + , nfold = 5L + , eval = "binary_error" + , showsd = FALSE +) + +# You can also do cross validation with customized loss function +print("Running cross validation, with cutomsized loss function") + +logregobj <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + preds <- 1.0 / (1.0 + exp(-preds)) + grad <- preds - labels + hess <- preds * (1.0 - preds) + return(list(grad = grad, hess = hess)) +} + +# User-defined evaluation function returns a pair (metric_name, result, higher_better) +# NOTE: when you do customized loss function, the default prediction value is margin +# This may make built-in evaluation metric calculate wrong results +# For example, we are doing logistic loss, the prediction is score before logistic transformation +# Keep this in mind when you use the customization, and maybe you need write customized evaluation function +evalerror <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + preds <- 1.0 / (1.0 + exp(-preds)) + err <- as.numeric(sum(labels != (preds > 0.5))) / length(labels) + return(list(name = "error", value = err, higher_better = FALSE)) +} + +# train with customized objective +lgb.cv( + params = param + , data = dtrain + , nrounds = nrounds + , obj = logregobj + , eval = evalerror + , nfold = 5L +) diff --git a/R-package/demo/early_stopping.R b/R-package/demo/early_stopping.R new file mode 100644 index 0000000..4435dd1 --- /dev/null +++ b/R-package/demo/early_stopping.R @@ -0,0 +1,51 @@ +library(lightgbm) + +# Load in the agaricus dataset +data(agaricus.train, package = "lightgbm") +data(agaricus.test, package = "lightgbm") + +dtrain <- lgb.Dataset(agaricus.train$data, label = agaricus.train$label) +dtest <- lgb.Dataset.create.valid(dtrain, data = agaricus.test$data, label = agaricus.test$label) + +# Note: for customized objective function, we leave objective as default +# Note: what we are getting is margin value in prediction +# You must know what you are doing +param <- list( + num_leaves = 4L + , learning_rate = 1.0 +) +valids <- list(eval = dtest) +num_round <- 20L + +# User define objective function, given prediction, return gradient and second order gradient +# This is loglikelihood loss +logregobj <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + preds <- 1.0 / (1.0 + exp(-preds)) + grad <- preds - labels + hess <- preds * (1.0 - preds) + return(list(grad = grad, hess = hess)) +} + +# User-defined evaluation function returns a pair (metric_name, result, higher_better) +# NOTE: when you do customized loss function, the default prediction value is margin +# This may make built-in evaluation metric calculate wrong results +# For example, we are doing logistic loss, the prediction is score before logistic transformation +# The built-in evaluation error assumes input is after logistic transformation +# Keep this in mind when you use the customization, and maybe you need write customized evaluation function +evalerror <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + err <- as.numeric(sum(labels != (preds > 0.5))) / length(labels) + return(list(name = "error", value = err, higher_better = FALSE)) +} +print("Start training with early Stopping setting") + +bst <- lgb.train( + param + , dtrain + , num_round + , valids + , obj = logregobj + , eval = evalerror + , early_stopping_round = 3L +) diff --git a/R-package/demo/efficient_many_training.R b/R-package/demo/efficient_many_training.R new file mode 100644 index 0000000..ffdae9f --- /dev/null +++ b/R-package/demo/efficient_many_training.R @@ -0,0 +1,36 @@ +# Efficient training means training without giving up too much RAM +# In the case of many trainings (like 100+ models), RAM will be eaten very quickly +# Therefore, it is essential to know a strategy to deal with such issue + +# More results can be found here: https://github.com/lightgbm-org/LightGBM/issues/879#issuecomment-326656580 +# Quote: "@Laurae2 Thanks for nice easily reproducible example (unlike mine). +# With reset=FALSE you get after 500 iterations (not 1000): OS reports 27GB usage, while R gc() reports 1.5GB. +# Just doing reset=TRUE will already improve things: OS reports 4.6GB. +# Doing reset=TRUE and calling gc() in the loop will have OS 1.3GB. Thanks for the latest tip." + +# Load library +library(lightgbm) + +# Generate fictive data of size 1M x 100 +set.seed(11111L) +x_data <- matrix(rnorm(n = 100000000L, mean = 0.0, sd = 100.0), nrow = 1000000L, ncol = 100L) +y_data <- rnorm(n = 1000000L, mean = 0.0, sd = 5.0) + +# Create lgb.Dataset for training +data <- lgb.Dataset(x_data, label = y_data) +data$construct() + +# Loop through a training of 1000 models, please check your RAM on your task manager +# It MUST remain constant (if not increasing very slightly) +gbm <- list() + +for (i in 1L:1000L) { + print(i) + gbm[[i]] <- lgb.train( + params = list(objective = "regression") + , data = data + , 1L + , reset_data = TRUE + ) + gc(verbose = FALSE) +} diff --git a/R-package/demo/leaf_stability.R b/R-package/demo/leaf_stability.R new file mode 100644 index 0000000..0733f31 --- /dev/null +++ b/R-package/demo/leaf_stability.R @@ -0,0 +1,256 @@ +# We are going to look at how iterating too much might generate observation instability. +# Obviously, we are in a controlled environment, without issues (real rules). +# Do not do this in a real scenario. + +library(lightgbm) + +# define helper functions for creating plots + +# output of `RColorBrewer::brewer.pal(10, "RdYlGn")`, hardcooded here to avoid a dependency +.diverging_palette <- c( + "#A50026", "#D73027", "#F46D43", "#FDAE61", "#FEE08B" + , "#D9EF8B", "#A6D96A", "#66BD63", "#1A9850", "#006837" +) + +.prediction_depth_plot <- function(df) { + plot( + x = df$X + , y = df$Y + , type = "p" + , main = "Prediction Depth" + , xlab = "Leaf Bin" + , ylab = "Prediction Probability" + , pch = 19L + , col = .diverging_palette[df$binned + 1L] + ) + legend( + "topright" + , title = "bin" + , legend = sort(unique(df$binned)) + , pch = 19L + , col = .diverging_palette[sort(unique(df$binned + 1L))] + , cex = 0.7 + ) +} + +.prediction_depth_spread_plot <- function(df) { + plot( + x = df$binned + , xlim = c(0L, 9L) + , y = df$Z + , type = "p" + , main = "Prediction Depth Spread" + , xlab = "Leaf Bin" + , ylab = "Logloss" + , pch = 19L + , col = .diverging_palette[df$binned + 1L] + ) + legend( + "topright" + , title = "bin" + , legend = sort(unique(df$binned)) + , pch = 19L + , col = .diverging_palette[sort(unique(df$binned + 1L))] + , cex = 0.7 + ) +} + +.depth_density_plot <- function(df) { + plot( + x = density(df$Y) + , xlim = c(min(df$Y), max(df$Y)) + , type = "p" + , main = "Depth Density" + , xlab = "Prediction Probability" + , ylab = "Bin Density" + , pch = 19L + , col = .diverging_palette[df$binned + 1L] + ) + legend( + "topright" + , title = "bin" + , legend = sort(unique(df$binned)) + , pch = 19L + , col = .diverging_palette[sort(unique(df$binned + 1L))] + , cex = 0.7 + ) +} + +# load some data +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) + +# setup parameters and we train a model +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 0.1 + , bagging_fraction = 0.1 + , bagging_freq = 1L + , bagging_seed = 1L +) +valids <- list(test = dtest) +model <- lgb.train( + params + , dtrain + , 50L + , valids +) + +# We create a data.frame with the following structure: +# X = average leaf of the observation throughout all trees +# Y = prediction probability (clamped to [1e-15, 1-1e-15]) +# Z = logloss +# binned = binned quantile of average leaf +new_data <- data.frame( + X = rowMeans(predict( + model + , agaricus.test$data + , type = "leaf" + )) + , Y = pmin( + pmax( + predict(model, agaricus.test$data) + , 1e-15 + ) + , 1.0 - 1e-15 + ) +) +new_data$Z <- -1.0 * (agaricus.test$label * log(new_data$Y) + (1L - agaricus.test$label) * log(1L - new_data$Y)) +new_data$binned <- .bincode( + x = new_data$X + , breaks = quantile( + x = new_data$X + , probs = seq_len(9L) / 10.0 + ) + , right = TRUE + , include.lowest = TRUE +) +new_data$binned[is.na(new_data$binned)] <- 0L + +# We can check the binned content +table(new_data$binned) + +# We can plot the binned content +# On the second plot, we clearly notice the lower the bin (the lower the leaf value), the higher the loss +# On the third plot, it is smooth! +.prediction_depth_plot(df = new_data) +.prediction_depth_spread_plot(df = new_data) +.depth_density_plot(df = new_data) + +# Now, let's show with other parameters +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 +) +model2 <- lgb.train( + params + , dtrain + , 100L + , valids +) + +# We create the data structure, but for model2 +new_data2 <- data.frame( + X = rowMeans(predict( + model2 + , agaricus.test$data + , type = "leaf" + )) + , Y = pmin( + pmax( + predict( + model2 + , agaricus.test$data + ) + , 1e-15 + ) + , 1.0 - 1e-15 + ) +) +new_data2$Z <- -1.0 * (agaricus.test$label * log(new_data2$Y) + (1L - agaricus.test$label) * log(1L - new_data2$Y)) +new_data2$binned <- .bincode( + x = new_data2$X + , breaks = quantile( + x = new_data2$X + , probs = seq_len(9L) / 10.0 + ) + , right = TRUE + , include.lowest = TRUE +) +new_data2$binned[is.na(new_data2$binned)] <- 0L + +# We can check the binned content +table(new_data2$binned) + +# We can plot the binned content +# On the second plot, we clearly notice the lower the bin (the lower the leaf value), the higher the loss +# On the third plot, it is clearly not smooth! We are severely overfitting the data, but the rules are +# real thus it is not an issue +# However, if the rules were not true, the loss would explode. +.prediction_depth_plot(df = new_data2) +.prediction_depth_spread_plot(df = new_data2) +.depth_density_plot(df = new_data2) + +# Now, try with very severe overfitting +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 +) +model3 <- lgb.train( + params + , dtrain + , 1000L + , valids +) + +# We create the data structure, but for model3 +new_data3 <- data.frame( + X = rowMeans(predict( + model3 + , agaricus.test$data + , type = "leaf" + )) + , Y = pmin( + pmax( + predict( + model3 + , agaricus.test$data + ) + , 1e-15 + ) + , 1.0 - 1e-15 + ) +) +new_data3$Z <- -1.0 * (agaricus.test$label * log(new_data3$Y) + (1L - agaricus.test$label) * log(1L - new_data3$Y)) +new_data3$binned <- .bincode( + x = new_data3$X + , breaks = quantile( + x = new_data3$X + , probs = seq_len(9L) / 10.0 + ) + , right = TRUE + , include.lowest = TRUE +) +new_data3$binned[is.na(new_data3$binned)] <- 0L + +# We can check the binned content +table(new_data3$binned) + +# We can plot the binned content +# On the third plot, it is clearly not smooth! We are severely overfitting the data, but the rules +# are real thus it is not an issue. +# However, if the rules were not true, the loss would explode. See the sudden spikes? +.depth_density_plot(df = new_data3) + +# Compare with our second model, the difference is severe. This is smooth. +.depth_density_plot(df = new_data2) diff --git a/R-package/demo/multiclass.R b/R-package/demo/multiclass.R new file mode 100644 index 0000000..35441cc --- /dev/null +++ b/R-package/demo/multiclass.R @@ -0,0 +1,70 @@ +library(lightgbm) + +# We load the default iris dataset shipped with R +data(iris) + +# We must convert factors to numeric +# They must be starting from number 0 to use multiclass +# For instance: 0, 1, 2, 3, 4, 5... +iris$Species <- as.numeric(as.factor(iris$Species)) - 1L + +# We cut the data set into 80% train and 20% validation +# The 10 last samples of each class are for validation + +train <- as.matrix(iris[c(1L:40L, 51L:90L, 101L:140L), ]) +test <- as.matrix(iris[c(41L:50L, 91L:100L, 141L:150L), ]) +dtrain <- lgb.Dataset(data = train[, 1L:4L], label = train[, 5L]) +dtest <- lgb.Dataset.create.valid(dtrain, data = test[, 1L:4L], label = test[, 5L]) +valids <- list(test = dtest) + +# Method 1 of training +params <- list( + objective = "multiclass" + , metric = "multi_error" + , num_class = 3L + , min_data = 1L + , learning_rate = 1.0 +) +model <- lgb.train( + params + , dtrain + , 100L + , valids + , early_stopping_rounds = 10L +) + +# We can predict on test data, outputs a 90-length vector +# Order: obs1 class1, obs1 class2, obs1 class3, obs2 class1, obs2 class2, obs2 class3... +my_preds <- predict(model, test[, 1L:4L]) + +# Method 2 of training, identical +params <- list( + min_data = 1L + , learning_rate = 1.0 + , objective = "multiclass" + , metric = "multi_error" + , num_class = 3L +) +model <- lgb.train( + params + , dtrain + , 100L + , valids + , early_stopping_rounds = 10L +) + +# We can predict on test data, identical +my_preds <- predict(model, test[, 1L:4L]) + +# A (30x3) matrix with the predictions +# class1 class2 class3 +# obs1 obs1 obs1 +# obs2 obs2 obs2 +# .... .... .... +my_preds <- predict(model, test[, 1L:4L]) + +# We can also get the predicted scores before the Sigmoid/Softmax application +my_preds <- predict(model, test[, 1L:4L], type = "raw") + +# We can also get the leaf index +my_preds <- predict(model, test[, 1L:4L], type = "leaf") diff --git a/R-package/demo/multiclass_custom_objective.R b/R-package/demo/multiclass_custom_objective.R new file mode 100644 index 0000000..150813b --- /dev/null +++ b/R-package/demo/multiclass_custom_objective.R @@ -0,0 +1,117 @@ +library(lightgbm) + +# We load the default iris dataset shipped with R +data(iris) + +# We must convert factors to numeric +# They must be starting from number 0 to use multiclass +# For instance: 0, 1, 2, 3, 4, 5... +iris$Species <- as.numeric(as.factor(iris$Species)) - 1L + +# Create imbalanced training data (20, 30, 40 examples for classes 0, 1, 2) +train <- as.matrix(iris[c(1L:20L, 51L:80L, 101L:140L), ]) +# The 10 last samples of each class are for validation +test <- as.matrix(iris[c(41L:50L, 91L:100L, 141L:150L), ]) + +dtrain <- lgb.Dataset(data = train[, 1L:4L], label = train[, 5L]) +dtest <- lgb.Dataset.create.valid(dtrain, data = test[, 1L:4L], label = test[, 5L]) +valids <- list(train = dtrain, test = dtest) + +# Method 1 of training with built-in multiclass objective +# Note: need to turn off boost from average to match custom objective +# (https://github.com/lightgbm-org/LightGBM/issues/1846) +params <- list( + min_data = 1L + , learning_rate = 1.0 + , num_class = 3L + , boost_from_average = FALSE + , metric = "multi_logloss" +) +model_builtin <- lgb.train( + params + , dtrain + , 100L + , valids + , early_stopping_rounds = 10L + , obj = "multiclass" +) + +preds_builtin <- predict(model_builtin, test[, 1L:4L], type = "raw") +probs_builtin <- exp(preds_builtin) / rowSums(exp(preds_builtin)) + +# Method 2 of training with custom objective function + +# User defined objective function, given prediction, return gradient and second order gradient +custom_multiclass_obj <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + + # preds is a matrix with rows corresponding to samples and columns corresponding to choices + preds <- matrix(preds, nrow = length(labels)) + + # to prevent overflow, normalize preds by row + preds <- preds - apply(preds, MARGIN = 1L, max) + prob <- exp(preds) / rowSums(exp(preds)) + + # compute gradient + grad <- prob + subset_index <- as.matrix( + data.frame( + seq_along(labels) + , labels + 1L + , fix.empty.names = FALSE + ) + , nrow = length(labels) + , dimnames = NULL + ) + grad[subset_index] <- grad[subset_index] - 1L + + # compute hessian (approximation) + hess <- 2.0 * prob * (1.0 - prob) + + return(list(grad = grad, hess = hess)) +} + +# define custom metric +custom_multiclass_metric <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + preds <- matrix(preds, nrow = length(labels)) + preds <- preds - apply(preds, 1L, max) + prob <- exp(preds) / rowSums(exp(preds)) + + subset_index <- as.matrix( + data.frame( + seq_along(labels) + , labels + 1L + , fix.empty.names = FALSE + ) + , nrow = length(labels) + , dimnames = NULL + ) + return(list( + name = "error" + , value = -mean(log(prob[subset_index])) + , higher_better = FALSE + )) +} + +params <- list( + min_data = 1L + , learning_rate = 1.0 + , num_class = 3L +) +model_custom <- lgb.train( + params + , dtrain + , 100L + , valids + , early_stopping_rounds = 10L + , obj = custom_multiclass_obj + , eval = custom_multiclass_metric +) + +preds_custom <- predict(model_custom, test[, 1L:4L], type = "raw") +probs_custom <- exp(preds_custom) / rowSums(exp(preds_custom)) + +# compare predictions +stopifnot(identical(probs_builtin, probs_custom)) +stopifnot(identical(preds_builtin, preds_custom)) diff --git a/R-package/demo/weight_param.R b/R-package/demo/weight_param.R new file mode 100644 index 0000000..8fd8ae1 --- /dev/null +++ b/R-package/demo/weight_param.R @@ -0,0 +1,104 @@ +# This demo R code is to provide a demonstration of hyperparameter adjustment +# when scaling weights for appropriate learning +# As with any optimizers, bad parameters can impair performance + +# Load library +library(lightgbm) + +# We will train a model with the following scenarii: +# - Run 1: sum of weights equal to 6513 (x 1e-5) without adjusted regularization (not learning) +# - Run 2: sum of weights equal to 6513 (x 1e-5) adjusted regularization (learning) +# - Run 3: sum of weights equal to 6513 with adjusted regularization (learning) + +# Setup small weights +weights1 <- rep(1e-5, 6513L) +weights2 <- rep(1e-5, 1611L) + +# Load data and create datasets +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label, weight = weights1) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label, weight = weights2) +valids <- list(test = dtest) + +# Run 1: sum of weights equal to 6513 (x 1e-5) without adjusted regularization (not learning) +# It cannot learn because regularization is too large! +# min_sum_hessian alone is bigger than the sum of weights, thus you will never learn anything +params <- list( + objective = "regression" + , metric = "l2" + , device = "cpu" + , min_sum_hessian = 10.0 + , num_leaves = 7L + , max_depth = 3L + , nthread = 1L + , min_data = 1L + , learning_rate = 1.0 +) +model <- lgb.train( + params + , dtrain + , 50L + , valids + , early_stopping_rounds = 10L +) +weight_loss <- as.numeric(model$record_evals$test$l2$eval) +plot(weight_loss) # Shows how poor the learning was: a straight line! + +# Run 2: sum of weights equal to 6513 (x 1e-5) with adjusted regularization (learning) +# Adjusted regularization just consisting in multiplicating results by 1e4 (x10000) +# Notice how it learns, there is no issue as we adjusted regularization ourselves +params <- list( + objective = "regression" + , metric = "l2" + , device = "cpu" + , min_sum_hessian = 1e-4 + , num_leaves = 7L + , max_depth = 3L + , nthread = 1L + , min_data = 1L + , learning_rate = 1.0 +) +model <- lgb.train( + params + , dtrain + , 50L + , valids + , early_stopping_rounds = 10L +) +small_weight_loss <- as.numeric(model$record_evals$test$l2$eval) +plot(small_weight_loss) # It learns! + +# Run 3: sum of weights equal to 6513 with adjusted regularization (learning) +dtrain <- lgb.Dataset(train$data, label = train$label) +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +valids <- list(test = dtest) + +# Setup parameters and run model... +params <- list( + objective = "regression" + , metric = "l2" + , device = "cpu" + , min_sum_hessian = 10.0 + , num_leaves = 7L + , max_depth = 3L + , nthread = 1L + , min_data = 1L + , learning_rate = 1.0 +) +model <- lgb.train( + params + , dtrain + , 50L + , valids + , early_stopping_rounds = 10L +) +large_weight_loss <- as.numeric(model$record_evals$test$l2$eval) +plot(large_weight_loss) # It learns! + + +# Do you want to compare the learning? They both converge. +plot(small_weight_loss, large_weight_loss) +curve(1.0 * x, from = 0L, to = 0.02, add = TRUE) diff --git a/R-package/inst/Makevars b/R-package/inst/Makevars new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/R-package/inst/Makevars @@ -0,0 +1 @@ + diff --git a/R-package/inst/make-r-def.R b/R-package/inst/make-r-def.R new file mode 100644 index 0000000..ddde385 --- /dev/null +++ b/R-package/inst/make-r-def.R @@ -0,0 +1,98 @@ +# [description] +# Create a definition file (.def) from a .dll file, using objdump. +# +# [usage] +# +# Rscript make-r-def.R something.dll something.def +# +# [references] +# * https://www.cs.colorado.edu/~main/cs1300/doc/mingwfaq.html + +args <- commandArgs(trailingOnly = TRUE) + +IN_DLL_FILE <- args[[1L]] +OUT_DEF_FILE <- args[[2L]] +DLL_BASE_NAME <- basename(IN_DLL_FILE) + +message(sprintf("Creating '%s' from '%s'", OUT_DEF_FILE, IN_DLL_FILE)) + +# system() will not raise an R exception if the process called +# fails. Wrapping it here to get that behavior. +# +# system() introduces a lot of overhead, at least on Windows, +# so trying processx if it is available +.pipe_shell_command_to_stdout <- function(command, args, out_file) { + has_processx <- suppressMessages({ + suppressWarnings({ + require("processx") # nolint: undesirable_function. + }) + }) + if (has_processx) { + p <- processx::process$new( + command = command + , args = args + , stdout = out_file + , windows_verbatim_args = FALSE + ) + invisible(p$wait()) + } else { + message(paste0( + "Using system2() to run shell commands. Installing " + , "'processx' with install.packages('processx') might " + , "make this faster." + )) + # shQuote() is necessary here since one of the arguments + # is a file-path to R.dll, which may have spaces. processx + # does such quoting but system2() does not + exit_code <- system2( + command = command + , args = shoQuote(args) + , stdout = out_file + ) + if (exit_code != 0L) { + stop(paste0("Command failed with exit code: ", exit_code)) + } + } + return(invisible(NULL)) +} + +# use objdump to dump all the symbols +OBJDUMP_FILE <- "objdump-out.txt" +.pipe_shell_command_to_stdout( + command = "objdump" + , args = c("-p", IN_DLL_FILE) + , out_file = OBJDUMP_FILE +) + +objdump_results <- readLines(OBJDUMP_FILE) +invisible(file.remove(OBJDUMP_FILE)) + +# Only one table in the objdump results matters for our purposes, +# see https://www.cs.colorado.edu/~main/cs1300/doc/mingwfaq.html +start_index <- which( + grepl( + pattern = "[Ordinal/Name Pointer] Table" # nolint: non_portable_path. + , x = objdump_results + , fixed = TRUE + ) +) +empty_lines <- which(objdump_results == "") +end_of_table <- empty_lines[empty_lines > start_index][1L] + +# Read the contents of the table +exported_symbols <- objdump_results[(start_index + 1L):end_of_table] +exported_symbols <- gsub("\t", "", exported_symbols, fixed = TRUE) +exported_symbols <- gsub(".*\\] ", "", exported_symbols) +exported_symbols <- gsub(" ", "", exported_symbols, fixed = TRUE) + +# Write R.def file +writeLines( + text = c( + paste0("LIBRARY \"", DLL_BASE_NAME, "\"") + , "EXPORTS" + , exported_symbols + ) + , con = OUT_DEF_FILE + , sep = "\n" +) +message(sprintf("Successfully created '%s'", OUT_DEF_FILE)) diff --git a/R-package/man/agaricus.test.Rd b/R-package/man/agaricus.test.Rd new file mode 100644 index 0000000..de0fe26 --- /dev/null +++ b/R-package/man/agaricus.test.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lightgbm.R +\docType{data} +\name{agaricus.test} +\alias{agaricus.test} +\title{Test part from Mushroom Data Set} +\format{ +A list containing a label vector, and a dgCMatrix object with 1611 +rows and 126 variables +} +\usage{ +data(agaricus.test) +} +\description{ +This data set is originally from the Mushroom data set, + UCI Machine Learning Repository. + This data set includes the following fields: + + \itemize{ + \item{\code{label}: the label for each record} + \item{\code{data}: a sparse Matrix of \code{dgCMatrix} class, with 126 columns.} + } +} +\references{ +https://archive.ics.uci.edu/ml/datasets/Mushroom + +Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository +[https://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +School of Information and Computer Science. +} +\keyword{datasets} diff --git a/R-package/man/agaricus.train.Rd b/R-package/man/agaricus.train.Rd new file mode 100644 index 0000000..5af43b7 --- /dev/null +++ b/R-package/man/agaricus.train.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lightgbm.R +\docType{data} +\name{agaricus.train} +\alias{agaricus.train} +\title{Training part from Mushroom Data Set} +\format{ +A list containing a label vector, and a dgCMatrix object with 6513 +rows and 127 variables +} +\usage{ +data(agaricus.train) +} +\description{ +This data set is originally from the Mushroom data set, + UCI Machine Learning Repository. + This data set includes the following fields: + + \itemize{ + \item{\code{label}: the label for each record} + \item{\code{data}: a sparse Matrix of \code{dgCMatrix} class, with 126 columns.} + } +} +\references{ +https://archive.ics.uci.edu/ml/datasets/Mushroom + +Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository +[https://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +School of Information and Computer Science. +} +\keyword{datasets} diff --git a/R-package/man/bank.Rd b/R-package/man/bank.Rd new file mode 100644 index 0000000..c74842e --- /dev/null +++ b/R-package/man/bank.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lightgbm.R +\docType{data} +\name{bank} +\alias{bank} +\title{Bank Marketing Data Set} +\format{ +A data.table with 4521 rows and 17 variables +} +\usage{ +data(bank) +} +\description{ +This data set is originally from the Bank Marketing data set, + UCI Machine Learning Repository. + + It contains only the following: bank.csv with 10% of the examples and 17 inputs, + randomly selected from 3 (older version of this dataset with less inputs). +} +\references{ +https://archive.ics.uci.edu/ml/datasets/Bank+Marketing + +S. Moro, P. Cortez and P. Rita. (2014) +A Data-Driven Approach to Predict the Success of Bank Telemarketing. Decision Support Systems +} +\keyword{datasets} diff --git a/R-package/man/dim.Rd b/R-package/man/dim.Rd new file mode 100644 index 0000000..69332d0 --- /dev/null +++ b/R-package/man/dim.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{dim.lgb.Dataset} +\alias{dim.lgb.Dataset} +\title{Dimensions of an \code{lgb.Dataset}} +\usage{ +\method{dim}{lgb.Dataset}(x) +} +\arguments{ +\item{x}{Object of class \code{lgb.Dataset}} +} +\value{ +a vector of numbers of rows and of columns +} +\description{ +Returns a vector of numbers of rows and of columns in an \code{lgb.Dataset}. +} +\details{ +Note: since \code{nrow} and \code{ncol} internally use \code{dim}, they can also +be directly used with an \code{lgb.Dataset} object. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) + +stopifnot(nrow(dtrain) == nrow(train$data)) +stopifnot(ncol(dtrain) == ncol(train$data)) +stopifnot(all(dim(dtrain) == dim(train$data))) +} +} diff --git a/R-package/man/dimnames.lgb.Dataset.Rd b/R-package/man/dimnames.lgb.Dataset.Rd new file mode 100644 index 0000000..85f2085 --- /dev/null +++ b/R-package/man/dimnames.lgb.Dataset.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{dimnames.lgb.Dataset} +\alias{dimnames.lgb.Dataset} +\alias{dimnames<-.lgb.Dataset} +\title{Handling of column names of \code{lgb.Dataset}} +\usage{ +\method{dimnames}{lgb.Dataset}(x) + +\method{dimnames}{lgb.Dataset}(x) <- value +} +\arguments{ +\item{x}{object of class \code{lgb.Dataset}} + +\item{value}{a list of two elements: the first one is ignored +and the second one is column names} +} +\value{ +A list with the dimension names of the dataset +} +\description{ +Only column names are supported for \code{lgb.Dataset}, thus setting of + row names would have no effect and returned row names would be NULL. +} +\details{ +Generic \code{dimnames} methods are used by \code{colnames}. +Since row names are irrelevant, it is recommended to use \code{colnames} directly. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +lgb.Dataset.construct(dtrain) +dimnames(dtrain) +colnames(dtrain) +colnames(dtrain) <- make.names(seq_len(ncol(train$data))) +print(dtrain, verbose = TRUE) +} +} diff --git a/R-package/man/figures/logo.svg b/R-package/man/figures/logo.svg new file mode 100644 index 0000000..4af2394 --- /dev/null +++ b/R-package/man/figures/logo.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/R-package/man/getLGBMThreads.Rd b/R-package/man/getLGBMThreads.Rd new file mode 100644 index 0000000..21af4f4 --- /dev/null +++ b/R-package/man/getLGBMThreads.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/multithreading.R +\name{getLGBMThreads} +\alias{getLGBMThreads} +\alias{getLGBMthreads} +\title{Get default number of threads used by LightGBM} +\usage{ +getLGBMthreads() +} +\value{ +number of threads as an integer. \code{-1} means that in situations where parameter \code{num_threads} is + not explicitly supplied, LightGBM will choose a number of threads to use automatically. +} +\description{ +LightGBM attempts to speed up many operations by using multi-threading. + The number of threads used in those operations can be controlled via the + \code{num_threads} parameter passed through \code{params} to functions like + \link{lgb.train} and \link{lgb.Dataset}. However, some operations (like materializing + a model from a text file) are done via code paths that don't explicitly accept thread-control + configuration. + + Use this function to see the default number of threads LightGBM will use for such operations. +} +\seealso{ +\link{setLGBMthreads} +} diff --git a/R-package/man/get_field.Rd b/R-package/man/get_field.Rd new file mode 100644 index 0000000..e2562cc --- /dev/null +++ b/R-package/man/get_field.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{get_field} +\alias{get_field} +\alias{get_field.lgb.Dataset} +\title{Get one attribute of a \code{lgb.Dataset}} +\usage{ +get_field(dataset, field_name) + +\method{get_field}{lgb.Dataset}(dataset, field_name) +} +\arguments{ +\item{dataset}{Object of class \code{lgb.Dataset}} + +\item{field_name}{String with the name of the attribute to get. One of the following. +\itemize{ + \item \code{label}: label lightgbm learns from ; + \item \code{weight}: to do a weight rescale ; + \item{\code{group}: used for learning-to-rank tasks. An integer vector describing how to + group rows together as ordered results from the same set of candidate results to be ranked. + For example, if you have a 100-document dataset with \code{group = c(10, 20, 40, 10, 10, 10)}, + that means that you have 6 groups, where the first 10 records are in the first group, + records 11-30 are in the second group, etc.} + \item \code{init_score}: initial score is the base prediction lightgbm will boost from. +}} +} +\value{ +requested attribute +} +\description{ +Get one attribute of a \code{lgb.Dataset} +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +lgb.Dataset.construct(dtrain) + +labels <- lightgbm::get_field(dtrain, "label") +lightgbm::set_field(dtrain, "label", 1 - labels) + +labels2 <- lightgbm::get_field(dtrain, "label") +stopifnot(all(labels2 == 1 - labels)) +} +} diff --git a/R-package/man/lgb.Dataset.Rd b/R-package/man/lgb.Dataset.Rd new file mode 100644 index 0000000..16b241d --- /dev/null +++ b/R-package/man/lgb.Dataset.Rd @@ -0,0 +1,82 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{lgb.Dataset} +\alias{lgb.Dataset} +\title{Construct \code{lgb.Dataset} object} +\usage{ +lgb.Dataset( + data, + params = list(), + reference = NULL, + colnames = NULL, + categorical_feature = NULL, + free_raw_data = TRUE, + label = NULL, + weight = NULL, + group = NULL, + init_score = NULL +) +} +\arguments{ +\item{data}{a \code{matrix} object, a \code{dgCMatrix} object, +a character representing a path to a text file (CSV, TSV, or LibSVM), +or a character representing a path to a binary \code{lgb.Dataset} file} + +\item{params}{a list of parameters. See +\href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#dataset-parameters}{ +The "Dataset Parameters" section of the documentation} for a list of parameters +and valid values.} + +\item{reference}{reference dataset. When LightGBM creates a Dataset, it does some preprocessing like binning +continuous features into histograms. If you want to apply the same bin boundaries from an existing +dataset to new \code{data}, pass that existing Dataset to this argument.} + +\item{colnames}{names of columns} + +\item{categorical_feature}{categorical features. This can either be a character vector of feature +names or an integer vector with the indices of the features (e.g. +\code{c(1L, 10L)} to say "the first and tenth columns").} + +\item{free_raw_data}{LightGBM constructs its data format, called a "Dataset", from tabular data. +By default, that Dataset object on the R side does not keep a copy of the raw data. +This reduces LightGBM's memory consumption, but it means that the Dataset object +cannot be changed after it has been constructed. If you'd prefer to be able to +change the Dataset object after construction, set \code{free_raw_data = FALSE}.} + +\item{label}{vector of labels to use as the target variable} + +\item{weight}{numeric vector of sample weights} + +\item{group}{used for learning-to-rank tasks. An integer vector describing how to +group rows together as ordered results from the same set of candidate results +to be ranked. For example, if you have a 100-document dataset with +\code{group = c(10, 20, 40, 10, 10, 10)}, that means that you have 6 groups, +where the first 10 records are in the first group, records 11-30 are in the +second group, etc.} + +\item{init_score}{initial score is the base prediction lightgbm will boost from} +} +\value{ +constructed dataset +} +\description{ +LightGBM does not train on raw data. + It discretizes continuous features into histogram bins, tries to + combine categorical features, and automatically handles missing and + + The \code{Dataset} class handles that preprocessing, and holds that + alternative representation of the input data. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data_file <- tempfile(fileext = ".data") +lgb.Dataset.save(dtrain, data_file) +dtrain <- lgb.Dataset(data_file) +lgb.Dataset.construct(dtrain) +} +} diff --git a/R-package/man/lgb.Dataset.construct.Rd b/R-package/man/lgb.Dataset.construct.Rd new file mode 100644 index 0000000..e400e0a --- /dev/null +++ b/R-package/man/lgb.Dataset.construct.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{lgb.Dataset.construct} +\alias{lgb.Dataset.construct} +\title{Construct Dataset explicitly} +\usage{ +lgb.Dataset.construct(dataset) +} +\arguments{ +\item{dataset}{Object of class \code{lgb.Dataset}} +} +\value{ +constructed dataset +} +\description{ +Construct Dataset explicitly +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +lgb.Dataset.construct(dtrain) +} +} diff --git a/R-package/man/lgb.Dataset.create.valid.Rd b/R-package/man/lgb.Dataset.create.valid.Rd new file mode 100644 index 0000000..fc50dff --- /dev/null +++ b/R-package/man/lgb.Dataset.create.valid.Rd @@ -0,0 +1,95 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{lgb.Dataset.create.valid} +\alias{lgb.Dataset.create.valid} +\title{Construct validation data} +\usage{ +lgb.Dataset.create.valid( + dataset, + data, + label = NULL, + weight = NULL, + group = NULL, + init_score = NULL, + params = list() +) +} +\arguments{ +\item{dataset}{\code{lgb.Dataset} object, training data} + +\item{data}{a \code{matrix} object, a \code{dgCMatrix} object, +a character representing a path to a text file (CSV, TSV, or LibSVM), +or a character representing a path to a binary \code{Dataset} file} + +\item{label}{vector of labels to use as the target variable} + +\item{weight}{numeric vector of sample weights} + +\item{group}{used for learning-to-rank tasks. An integer vector describing how to +group rows together as ordered results from the same set of candidate results +to be ranked. For example, if you have a 100-document dataset with +\code{group = c(10, 20, 40, 10, 10, 10)}, that means that you have 6 groups, +where the first 10 records are in the first group, records 11-30 are in the +second group, etc.} + +\item{init_score}{initial score is the base prediction lightgbm will boost from} + +\item{params}{a list of parameters. See +\href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#dataset-parameters}{ +The "Dataset Parameters" section of the documentation} for a list of parameters +and valid values. If this is an empty list (the default), the validation Dataset +will have the same parameters as the Dataset passed to argument \code{dataset}.} +} +\value{ +constructed dataset +} +\description{ +Construct validation data according to training data +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) + +# parameters can be changed between the training data and validation set, +# for example to account for training data in a text file with a header row +# and validation data in a text file without it +train_file <- tempfile(pattern = "train_", fileext = ".csv") +write.table( + data.frame(y = rnorm(100L), x1 = rnorm(100L), x2 = rnorm(100L)) + , file = train_file + , sep = "," + , col.names = TRUE + , row.names = FALSE + , quote = FALSE +) + +valid_file <- tempfile(pattern = "valid_", fileext = ".csv") +write.table( + data.frame(y = rnorm(100L), x1 = rnorm(100L), x2 = rnorm(100L)) + , file = valid_file + , sep = "," + , col.names = FALSE + , row.names = FALSE + , quote = FALSE +) + +dtrain <- lgb.Dataset( + data = train_file + , params = list(has_header = TRUE) +) +dtrain$construct() + +dvalid <- lgb.Dataset( + data = valid_file + , params = list(has_header = FALSE) +) +dvalid$construct() +} +} diff --git a/R-package/man/lgb.Dataset.save.Rd b/R-package/man/lgb.Dataset.save.Rd new file mode 100644 index 0000000..b03c2c5 --- /dev/null +++ b/R-package/man/lgb.Dataset.save.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{lgb.Dataset.save} +\alias{lgb.Dataset.save} +\title{Save \code{lgb.Dataset} to a binary file} +\usage{ +lgb.Dataset.save(dataset, fname) +} +\arguments{ +\item{dataset}{object of class \code{lgb.Dataset}} + +\item{fname}{object filename of output file} +} +\value{ +the dataset you passed in +} +\description{ +Please note that \code{init_score} is not saved in binary file. + If you need it, please set it again after loading Dataset. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +lgb.Dataset.save(dtrain, tempfile(fileext = ".bin")) +} +} diff --git a/R-package/man/lgb.Dataset.set.categorical.Rd b/R-package/man/lgb.Dataset.set.categorical.Rd new file mode 100644 index 0000000..5dfcc9a --- /dev/null +++ b/R-package/man/lgb.Dataset.set.categorical.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{lgb.Dataset.set.categorical} +\alias{lgb.Dataset.set.categorical} +\title{Set categorical feature of \code{lgb.Dataset}} +\usage{ +lgb.Dataset.set.categorical(dataset, categorical_feature) +} +\arguments{ +\item{dataset}{object of class \code{lgb.Dataset}} + +\item{categorical_feature}{categorical features. This can either be a character vector of feature +names or an integer vector with the indices of the features (e.g. +\code{c(1L, 10L)} to say "the first and tenth columns").} +} +\value{ +the dataset you passed in +} +\description{ +Set the categorical features of an \code{lgb.Dataset} object. Use this function + to tell LightGBM which features should be treated as categorical. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data_file <- tempfile(fileext = ".data") +lgb.Dataset.save(dtrain, data_file) +dtrain <- lgb.Dataset(data_file) +lgb.Dataset.set.categorical(dtrain, 1L:2L) +} +} diff --git a/R-package/man/lgb.Dataset.set.reference.Rd b/R-package/man/lgb.Dataset.set.reference.Rd new file mode 100644 index 0000000..a4efbfa --- /dev/null +++ b/R-package/man/lgb.Dataset.set.reference.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{lgb.Dataset.set.reference} +\alias{lgb.Dataset.set.reference} +\title{Set reference of \code{lgb.Dataset}} +\usage{ +lgb.Dataset.set.reference(dataset, reference) +} +\arguments{ +\item{dataset}{object of class \code{lgb.Dataset}} + +\item{reference}{object of class \code{lgb.Dataset}} +} +\value{ +the dataset you passed in +} +\description{ +If you want to use validation data, you should set reference to training data +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +# create training Dataset +data(agaricus.train, package ="lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) + +# create a validation Dataset, using dtrain as a reference +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset(test$data, label = test$label) +lgb.Dataset.set.reference(dtest, dtrain) +} +} diff --git a/R-package/man/lgb.configure_fast_predict.Rd b/R-package/man/lgb.configure_fast_predict.Rd new file mode 100644 index 0000000..9cd4339 --- /dev/null +++ b/R-package/man/lgb.configure_fast_predict.Rd @@ -0,0 +1,144 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{lgb.configure_fast_predict} +\alias{lgb.configure_fast_predict} +\title{Configure Fast Single-Row Predictions} +\usage{ +lgb.configure_fast_predict( + model, + csr = FALSE, + start_iteration = NULL, + num_iteration = NULL, + type = "response", + params = list() +) +} +\arguments{ +\item{model}{LightGBM model object (class \code{lgb.Booster}). + + \bold{The object will be modified in-place}.} + +\item{csr}{Whether the prediction function is going to be called on sparse CSR inputs. +If \code{FALSE}, will be assumed that predictions are going to be called on single-row +regular R matrices.} + +\item{start_iteration}{int or None, optional (default=None) +Start index of the iteration to predict. +If None or <= 0, starts from the first iteration.} + +\item{num_iteration}{int or None, optional (default=None) +Limit number of iterations in the prediction. +If None, if the best iteration exists and start_iteration is None or <= 0, the +best iteration is used; otherwise, all iterations from start_iteration are used. +If <= 0, all iterations from start_iteration are used (no limits).} + +\item{type}{Type of prediction to output. Allowed types are:\itemize{ + \item \code{"response"}: will output the predicted score according to the objective function being + optimized (depending on the link function that the objective uses), after applying any necessary + transformations - for example, for \code{objective="binary"}, it will output class probabilities. + \item \code{"class"}: for classification objectives, will output the class with the highest predicted + probability. For other objectives, will output the same as "response". Note that \code{"class"} is + not a supported type for \link{lgb.configure_fast_predict} (see the documentation of that function + for more details). + \item \code{"raw"}: will output the non-transformed numbers (sum of predictions from boosting iterations' + results) from which the "response" number is produced for a given objective function - for example, + for \code{objective="binary"}, this corresponds to log-odds. For many objectives such as + "regression", since no transformation is applied, the output will be the same as for "response". + \item \code{"leaf"}: will output the index of the terminal node / leaf at which each observations falls + in each tree in the model, outputted as integers, with one column per tree. + \item \code{"contrib"}: will return the per-feature contributions for each prediction, including an + intercept (each feature will produce one column). + } + + Note that, if using custom objectives, types "class" and "response" will not be available and will + default towards using "raw" instead. + + If the model was fit through function \link{lightgbm} and it was passed a factor as labels, + passing the prediction type through \code{params} instead of through this argument might + result in factor levels for classification objectives not being applied correctly to the + resulting output. + + \emph{New in version 4.0.0}} + +\item{params}{a list of additional named parameters. See +\href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#predict-parameters}{ +the "Predict Parameters" section of the documentation} for a list of parameters and +valid values. Where these conflict with the values of keyword arguments to this function, +the values in \code{params} take precedence.} +} +\value{ +The same \code{model} that was passed as input, invisibly, with the desired + configuration stored inside it and available to be used in future calls to + \link{predict.lgb.Booster}. +} +\description{ +Pre-configures a LightGBM model object to produce fast single-row predictions + for a given input data type, prediction type, and parameters. +} +\details{ +Calling this function multiple times with different parameters might not override + the previous configuration and might trigger undefined behavior. + + Any saved configuration for fast predictions might be lost after making a single-row + prediction of a different type than what was configured (except for types "response" and + "class", which can be switched between each other at any time without losing the configuration). + + In some situations, setting a fast prediction configuration for one type of prediction + might cause the prediction function to keep using that configuration for single-row + predictions even if the requested type of prediction is different from what was configured. + + Note that this function will not accept argument \code{type="class"} - for such cases, one + can pass \code{type="response"} to this function and then \code{type="class"} to the + \code{predict} function - the fast configuration will not be lost or altered if the switch + is between "response" and "class". + + The configuration does not survive de-serializations, so it has to be generated + anew in every R process that is going to use it (e.g. if loading a model object + through \code{readRDS}, whatever configuration was there previously will be lost). + + Requesting a different prediction type or passing parameters to \link{predict.lgb.Booster} + will cause it to ignore the fast-predict configuration and take the slow route instead + (but be aware that an existing configuration might not always be overridden by supplying + different parameters or prediction type, so make sure to check that the output is what + was expected when a prediction is to be made on a single row for something different than + what is configured). + + Note that, if configuring a non-default prediction type (such as leaf indices), + then that type must also be passed in the call to \link{predict.lgb.Booster} in + order for it to use the configuration. This also applies for \code{start_iteration} + and \code{num_iteration}, but \bold{the \code{params} list must be empty} in the call to \code{predict}. + + Predictions about feature contributions do not allow a fast route for CSR inputs, + and as such, this function will produce an error if passing \code{csr=TRUE} and + \code{type = "contrib"} together. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +library(lightgbm) +data(mtcars) +X <- as.matrix(mtcars[, -1L]) +y <- mtcars[, 1L] +dtrain <- lgb.Dataset(X, label = y, params = list(max_bin = 5L)) +params <- list( + min_data_in_leaf = 2L + , num_threads = 2L +) +model <- lgb.train( + params = params + , data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = -1L +) +lgb.configure_fast_predict(model) + +x_single <- X[11L, , drop = FALSE] +predict(model, x_single) + +# Will not use it if the prediction to be made +# is different from what was configured +predict(model, x_single, type = "leaf") +} +} diff --git a/R-package/man/lgb.convert_with_rules.Rd b/R-package/man/lgb.convert_with_rules.Rd new file mode 100644 index 0000000..a8a6a00 --- /dev/null +++ b/R-package/man/lgb.convert_with_rules.Rd @@ -0,0 +1,74 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.convert_with_rules.R +\name{lgb.convert_with_rules} +\alias{lgb.convert_with_rules} +\title{Data preparator for LightGBM datasets with rules (integer)} +\usage{ +lgb.convert_with_rules(data, rules = NULL) +} +\arguments{ +\item{data}{A data.frame or data.table to prepare.} + +\item{rules}{A set of rules from the data preparator, if already used. This should be an R list, +where names are column names in \code{data} and values are named character +vectors whose names are column values and whose values are new values to +replace them with.} +} +\value{ +A list with the cleaned dataset (\code{data}) and the rules (\code{rules}). + Note that the data must be converted to a matrix format (\code{as.matrix}) for input in + \code{lgb.Dataset}. +} +\description{ +Attempts to prepare a clean dataset to prepare to put in a \code{lgb.Dataset}. + Factor, character, and logical columns are converted to integer. Missing values + in factors and characters will be filled with 0L. Missing values in logicals + will be filled with -1L. + + This function returns and optionally takes in "rules" the describe exactly + how to convert values in columns. + + Columns that contain only NA values will be converted by this function but will + not show up in the returned \code{rules}. + + NOTE: In previous releases of LightGBM, this function was called \code{lgb.prepare_rules2}. +} +\examples{ +\donttest{ +data(iris) + +str(iris) + +new_iris <- lgb.convert_with_rules(data = iris) +str(new_iris$data) + +data(iris) # Erase iris dataset +iris$Species[1L] <- "NEW FACTOR" # Introduce junk factor (NA) + +# Use conversion using known rules +# Unknown factors become 0, excellent for sparse datasets +newer_iris <- lgb.convert_with_rules(data = iris, rules = new_iris$rules) + +# Unknown factor is now zero, perfect for sparse datasets +newer_iris$data[1L, ] # Species became 0 as it is an unknown factor + +newer_iris$data[1L, 5L] <- 1.0 # Put back real initial value + +# Is the newly created dataset equal? YES! +all.equal(new_iris$data, newer_iris$data) + +# Can we test our own rules? +data(iris) # Erase iris dataset + +# We remapped values differently +personal_rules <- list( + Species = c( + "setosa" = 3L + , "versicolor" = 2L + , "virginica" = 1L + ) +) +newest_iris <- lgb.convert_with_rules(data = iris, rules = personal_rules) +str(newest_iris$data) # SUCCESS! +} +} diff --git a/R-package/man/lgb.cv.Rd b/R-package/man/lgb.cv.Rd new file mode 100644 index 0000000..2fa22f1 --- /dev/null +++ b/R-package/man/lgb.cv.Rd @@ -0,0 +1,164 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.cv.R +\name{lgb.cv} +\alias{lgb.cv} +\title{Main CV logic for LightGBM} +\usage{ +lgb.cv( + params = list(), + data, + nrounds = 100L, + nfold = 3L, + obj = NULL, + eval = NULL, + verbose = 1L, + record = TRUE, + eval_freq = 1L, + showsd = TRUE, + stratified = TRUE, + folds = NULL, + init_model = NULL, + early_stopping_rounds = NULL, + callbacks = list(), + reset_data = FALSE, + serializable = TRUE, + eval_train_metric = FALSE +) +} +\arguments{ +\item{params}{a list of parameters. See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html}{ +the "Parameters" section of the documentation} for a list of parameters and valid values.} + +\item{data}{a \code{lgb.Dataset} object, used for training. Some functions, such as \code{\link{lgb.cv}}, +may allow you to pass other types of data like \code{matrix} and then separately supply +\code{label} as a keyword argument.} + +\item{nrounds}{number of training rounds} + +\item{nfold}{the original dataset is randomly partitioned into \code{nfold} equal size subsamples.} + +\item{obj}{objective function, can be character or custom objective function. Examples include +\code{regression}, \code{regression_l1}, \code{huber}, +\code{binary}, \code{lambdarank}, \code{multiclass}, \code{multiclass}} + +\item{eval}{evaluation function(s). This can be a character vector, function, or list with a mixture of + strings and functions. + + \itemize{ + \item{\bold{a. character vector}: + If you provide a character vector to this argument, it should contain strings with valid + evaluation metrics. + See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#metric}{ + The "metric" section of the documentation} + for a list of valid metrics. + } + \item{\bold{b. function}: + You can provide a custom evaluation function. This + should accept the keyword arguments \code{preds} and \code{dtrain} and should return a named + list with three elements: + \itemize{ + \item{\code{name}: A string with the name of the metric, used for printing + and storing results. + } + \item{\code{value}: A single number indicating the value of the metric for the + given predictions and true values + } + \item{ + \code{higher_better}: A boolean indicating whether higher values indicate a better fit. + For example, this would be \code{FALSE} for metrics like MAE or RMSE. + } + } + } + \item{\bold{c. list}: + If a list is given, it should only contain character vectors and functions. + These should follow the requirements from the descriptions above. + } + }} + +\item{verbose}{verbosity for output, if <= 0 and \code{valids} has been provided, also will disable the +printing of evaluation during training} + +\item{record}{Boolean, TRUE will record iteration message to \code{booster$record_evals}} + +\item{eval_freq}{evaluation output frequency, only effective when verbose > 0 and \code{valids} has been provided} + +\item{showsd}{\code{boolean}, whether to show standard deviation of cross validation. +This parameter defaults to \code{TRUE}. Setting it to \code{FALSE} can lead to a +slight speedup by avoiding unnecessary computation.} + +\item{stratified}{a \code{boolean} indicating whether sampling of folds should be stratified +by the values of outcome labels.} + +\item{folds}{\code{list} provides a possibility to use a list of pre-defined CV folds +(each element must be a vector of test fold's indices). When folds are supplied, +the \code{nfold} and \code{stratified} parameters are ignored.} + +\item{init_model}{path of model file or \code{lgb.Booster} object, will continue training from this model} + +\item{early_stopping_rounds}{int. Activates early stopping. When this parameter is non-null, +training will stop if the evaluation of any metric on any validation set +fails to improve for \code{early_stopping_rounds} consecutive boosting rounds. +If training stops early, the returned model will have attribute \code{best_iter} +set to the iteration number of the best iteration.} + +\item{callbacks}{List of callback functions that are applied at each iteration.} + +\item{reset_data}{Boolean, setting it to TRUE (not the default value) will transform the booster model +into a predictor model which frees up memory and the original datasets} + +\item{serializable}{whether to make the resulting objects serializable through functions such as +\code{save} or \code{saveRDS} (see section "Model serialization").} + +\item{eval_train_metric}{\code{boolean}, whether to add the cross validation results on the +training data. This parameter defaults to \code{FALSE}. Setting it to \code{TRUE} +will increase run time.} +} +\value{ +a trained model \code{lgb.CVBooster}. +} +\description{ +Cross validation logic used by LightGBM +} +\section{Early Stopping}{ + + + "early stopping" refers to stopping the training process if the model's performance on a given + validation set does not improve for several consecutive iterations. + + If multiple arguments are given to \code{eval}, their order will be preserved. If you enable + early stopping by setting \code{early_stopping_rounds} in \code{params}, by default all + metrics will be considered for early stopping. + + If you want to only consider the first metric for early stopping, pass + \code{first_metric_only = TRUE} in \code{params}. Note that if you also specify \code{metric} + in \code{params}, that metric will be considered the "first" one. If you omit \code{metric}, + a default metric will be used based on your choice for the parameter \code{obj} (keyword argument) + or \code{objective} (passed into \code{params}). + + \bold{NOTE:} if using \code{boosting_type="dart"}, any early stopping configuration will be ignored + and early stopping will not be performed. +} + +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , num_threads = 2L +) +model <- lgb.cv( + params = params + , data = dtrain + , nrounds = 5L + , nfold = 3L +) +} + +} diff --git a/R-package/man/lgb.drop_serialized.Rd b/R-package/man/lgb.drop_serialized.Rd new file mode 100644 index 0000000..ab4642b --- /dev/null +++ b/R-package/man/lgb.drop_serialized.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.drop_serialized.R +\name{lgb.drop_serialized} +\alias{lgb.drop_serialized} +\title{Drop serialized raw bytes in a LightGBM model object} +\usage{ +lgb.drop_serialized(model) +} +\arguments{ +\item{model}{\code{lgb.Booster} object which was produced with `serializable=TRUE`.} +} +\value{ +\code{lgb.Booster} (the same `model` object that was passed as input, as invisible). +} +\description{ +If a LightGBM model object was produced with argument `serializable=TRUE`, the R object will keep +a copy of the underlying C++ object as raw bytes, which can be used to reconstruct such object after getting +serialized and de-serialized, but at the cost of extra memory usage. If these raw bytes are not needed anymore, +they can be dropped through this function in order to save memory. Note that the object will be modified in-place. + + \emph{New in version 4.0.0} +} +\seealso{ +\link{lgb.restore_handle}, \link{lgb.make_serializable}. +} diff --git a/R-package/man/lgb.dump.Rd b/R-package/man/lgb.dump.Rd new file mode 100644 index 0000000..e356251 --- /dev/null +++ b/R-package/man/lgb.dump.Rd @@ -0,0 +1,54 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{lgb.dump} +\alias{lgb.dump} +\title{Dump LightGBM model to json} +\usage{ +lgb.dump(booster, num_iteration = NULL, start_iteration = 1L) +} +\arguments{ +\item{booster}{Object of class \code{lgb.Booster}} + +\item{num_iteration}{Number of iterations to be dumped. NULL or <= 0 means use best iteration} + +\item{start_iteration}{Index (1-based) of the first boosting round to dump. + For example, passing \code{start_iteration=5, num_iteration=3} for a regression model + means "dump the fifth, sixth, and seventh tree" + + \emph{New in version 4.4.0}} +} +\value{ +json format of model +} +\description{ +Dump LightGBM model to json +} +\examples{ +\donttest{ +library(lightgbm) +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , num_threads = 2L +) +valids <- list(test = dtest) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 10L + , valids = valids + , early_stopping_rounds = 5L +) +json_model <- lgb.dump(model) +} +} diff --git a/R-package/man/lgb.get.eval.result.Rd b/R-package/man/lgb.get.eval.result.Rd new file mode 100644 index 0000000..0dc7eb0 --- /dev/null +++ b/R-package/man/lgb.get.eval.result.Rd @@ -0,0 +1,69 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{lgb.get.eval.result} +\alias{lgb.get.eval.result} +\title{Get record evaluation result from booster} +\usage{ +lgb.get.eval.result( + booster, + data_name, + eval_name, + iters = NULL, + is_err = FALSE +) +} +\arguments{ +\item{booster}{Object of class \code{lgb.Booster}} + +\item{data_name}{Name of the dataset to return evaluation results for.} + +\item{eval_name}{Name of the evaluation metric to return results for.} + +\item{iters}{An integer vector of iterations you want to get evaluation results for. If NULL +(the default), evaluation results for all iterations will be returned.} + +\item{is_err}{TRUE will return evaluation error instead} +} +\value{ +numeric vector of evaluation result +} +\description{ +Given a \code{lgb.Booster}, return evaluation results for a + particular metric on a particular dataset. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +# train a regression model +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , num_threads = 2L +) +valids <- list(test = dtest) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 5L + , valids = valids +) + +# Examine valid data_name values +print(setdiff(names(model$record_evals), "start_iter")) + +# Examine valid eval_name values for dataset "test" +print(names(model$record_evals[["test"]])) + +# Get L2 values for "test" dataset +lgb.get.eval.result(model, "test", "l2") +} +} diff --git a/R-package/man/lgb.importance.Rd b/R-package/man/lgb.importance.Rd new file mode 100644 index 0000000..5099643 --- /dev/null +++ b/R-package/man/lgb.importance.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.importance.R +\name{lgb.importance} +\alias{lgb.importance} +\title{Compute feature importance in a model} +\usage{ +lgb.importance(model, percentage = TRUE) +} +\arguments{ +\item{model}{object of class \code{lgb.Booster}.} + +\item{percentage}{whether to show importance in relative percentage.} +} +\value{ +For a tree model, a \code{data.table} with the following columns: +\itemize{ + \item{\code{Feature}: Feature names in the model.} + \item{\code{Gain}: The total gain of this feature's splits.} + \item{\code{Cover}: The number of observation related to this feature.} + \item{\code{Frequency}: The number of times a feature split in trees.} +} +} +\description{ +Creates a \code{data.table} of feature importances in a model. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) + +params <- list( + objective = "binary" + , learning_rate = 0.1 + , max_depth = -1L + , min_data_in_leaf = 1L + , min_sum_hessian_in_leaf = 1.0 + , num_threads = 2L +) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 5L +) + +tree_imp1 <- lgb.importance(model, percentage = TRUE) +tree_imp2 <- lgb.importance(model, percentage = FALSE) +} +} diff --git a/R-package/man/lgb.interpret.Rd b/R-package/man/lgb.interpret.Rd new file mode 100644 index 0000000..0526125 --- /dev/null +++ b/R-package/man/lgb.interpret.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.interpret.R +\name{lgb.interpret} +\alias{lgb.interpret} +\title{Compute feature contribution of prediction} +\usage{ +lgb.interpret(model, data, idxset, num_iteration = NULL) +} +\arguments{ +\item{model}{object of class \code{lgb.Booster}.} + +\item{data}{a matrix object or a dgCMatrix object.} + +\item{idxset}{an integer vector of indices of rows needed.} + +\item{num_iteration}{number of iteration want to predict with, NULL or <= 0 means use best iteration.} +} +\value{ +For regression, binary classification and lambdarank model, a \code{list} of \code{data.table} + with the following columns: + \itemize{ + \item{\code{Feature}: Feature names in the model.} + \item{\code{Contribution}: The total contribution of this feature's splits.} + } + For multiclass classification, a \code{list} of \code{data.table} with the Feature column and + Contribution columns to each class. +} +\description{ +Computes feature contribution components of rawscore prediction. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +Logit <- function(x) log(x / (1.0 - x)) +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +set_field( + dataset = dtrain + , field_name = "init_score" + , data = rep(Logit(mean(train$label)), length(train$label)) +) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test + +params <- list( + objective = "binary" + , learning_rate = 0.1 + , max_depth = -1L + , min_data_in_leaf = 1L + , min_sum_hessian_in_leaf = 1.0 + , num_threads = 2L +) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 3L +) + +tree_interpretation <- lgb.interpret(model, test$data, 1L:5L) +} +} diff --git a/R-package/man/lgb.interprete.Rd b/R-package/man/lgb.interprete.Rd new file mode 100644 index 0000000..0a0e74f --- /dev/null +++ b/R-package/man/lgb.interprete.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.interpret.R +\name{lgb.interprete} +\alias{lgb.interprete} +\title{DEPRECATED - use lgb.interpret() instead} +\usage{ +lgb.interprete(...) +} +\arguments{ +\item{...}{Arguments passed through to \code{lgb.interpret}} +} +\description{ +Alias for \code{lgb.interpret}. +} diff --git a/R-package/man/lgb.load.Rd b/R-package/man/lgb.load.Rd new file mode 100644 index 0000000..f145db5 --- /dev/null +++ b/R-package/man/lgb.load.Rd @@ -0,0 +1,52 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{lgb.load} +\alias{lgb.load} +\title{Load LightGBM model} +\usage{ +lgb.load(filename = NULL, model_str = NULL) +} +\arguments{ +\item{filename}{path of model file} + +\item{model_str}{a str containing the model (as a \code{character} or \code{raw} vector)} +} +\value{ +lgb.Booster +} +\description{ +Load LightGBM takes in either a file path or model string. + If both are provided, Load will default to loading from file +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , num_threads = 2L +) +valids <- list(test = dtest) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 5L + , valids = valids + , early_stopping_rounds = 3L +) +model_file <- tempfile(fileext = ".txt") +lgb.save(model, model_file) +load_booster <- lgb.load(filename = model_file) +model_string <- model$save_model_to_string(NULL) # saves best iteration +load_booster_from_str <- lgb.load(model_str = model_string) +} +} diff --git a/R-package/man/lgb.make_serializable.Rd b/R-package/man/lgb.make_serializable.Rd new file mode 100644 index 0000000..476b934 --- /dev/null +++ b/R-package/man/lgb.make_serializable.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.make_serializable.R +\name{lgb.make_serializable} +\alias{lgb.make_serializable} +\title{Make a LightGBM object serializable by keeping raw bytes} +\usage{ +lgb.make_serializable(model) +} +\arguments{ +\item{model}{\code{lgb.Booster} object which was produced with `serializable=FALSE`.} +} +\value{ +\code{lgb.Booster} (the same `model` object that was passed as input, as invisible). +} +\description{ +If a LightGBM model object was produced with argument `serializable=FALSE`, the R object will not +be serializable (e.g. cannot save and load with \code{saveRDS} and \code{readRDS}) as it will lack the raw bytes +needed to reconstruct its underlying C++ object. This function can be used to forcibly produce those serialized +raw bytes and make the object serializable. Note that the object will be modified in-place. + + \emph{New in version 4.0.0} +} +\seealso{ +\link{lgb.restore_handle}, \link{lgb.drop_serialized}. +} diff --git a/R-package/man/lgb.model.dt.tree.Rd b/R-package/man/lgb.model.dt.tree.Rd new file mode 100644 index 0000000..df36b6a --- /dev/null +++ b/R-package/man/lgb.model.dt.tree.Rd @@ -0,0 +1,67 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.model.dt.tree.R +\name{lgb.model.dt.tree} +\alias{lgb.model.dt.tree} +\title{Parse a LightGBM model json dump} +\usage{ +lgb.model.dt.tree(model, num_iteration = NULL, start_iteration = 1L) +} +\arguments{ +\item{model}{object of class \code{lgb.Booster}.} + +\item{num_iteration}{Number of iterations to include. NULL or <= 0 means use best iteration.} + +\item{start_iteration}{Index (1-based) of the first boosting round to include in the output. + For example, passing \code{start_iteration=5, num_iteration=3} for a regression model + means "return information about the fifth, sixth, and seventh trees". + + \emph{New in version 4.4.0}} +} +\value{ +A \code{data.table} with detailed information about model trees' nodes and leaves. + +The columns of the \code{data.table} are: + +\itemize{ + \item{\code{tree_index}: ID of a tree in a model (integer)} + \item{\code{split_index}: ID of a node in a tree (integer)} + \item{\code{split_feature}: for a node, it's a feature name (character); + for a leaf, it simply labels it as \code{"NA"}} + \item{\code{node_parent}: ID of the parent node for current node (integer)} + \item{\code{leaf_index}: ID of a leaf in a tree (integer)} + \item{\code{leaf_parent}: ID of the parent node for current leaf (integer)} + \item{\code{split_gain}: Split gain of a node} + \item{\code{threshold}: Splitting threshold value of a node} + \item{\code{decision_type}: Decision type of a node} + \item{\code{default_left}: Determine how to handle NA value, TRUE -> Left, FALSE -> Right} + \item{\code{internal_value}: Node value} + \item{\code{internal_count}: The number of observation collected by a node} + \item{\code{leaf_value}: Leaf value} + \item{\code{leaf_count}: The number of observation collected by a leaf} +} +} +\description{ +Parse a LightGBM model json dump into a \code{data.table} structure. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) + +params <- list( + objective = "binary" + , learning_rate = 0.01 + , num_leaves = 63L + , max_depth = -1L + , min_data_in_leaf = 1L + , min_sum_hessian_in_leaf = 1.0 + , num_threads = 2L +) +model <- lgb.train(params, dtrain, 10L) + +tree_dt <- lgb.model.dt.tree(model) +} +} diff --git a/R-package/man/lgb.plot.importance.Rd b/R-package/man/lgb.plot.importance.Rd new file mode 100644 index 0000000..bdf354d --- /dev/null +++ b/R-package/man/lgb.plot.importance.Rd @@ -0,0 +1,64 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.plot.importance.R +\name{lgb.plot.importance} +\alias{lgb.plot.importance} +\title{Plot feature importance as a bar graph} +\usage{ +lgb.plot.importance( + tree_imp, + top_n = 10L, + measure = "Gain", + left_margin = 10L, + cex = NULL +) +} +\arguments{ +\item{tree_imp}{a \code{data.table} returned by \code{\link{lgb.importance}}.} + +\item{top_n}{maximal number of top features to include into the plot.} + +\item{measure}{the name of importance measure to plot, can be "Gain", "Cover" or "Frequency".} + +\item{left_margin}{(base R barplot) allows to adjust the left margin size to fit feature names.} + +\item{cex}{(base R barplot) passed as \code{cex.names} parameter to \code{\link[graphics]{barplot}}. +Set a number smaller than 1.0 to make the bar labels smaller than R's default and values +greater than 1.0 to make them larger.} +} +\value{ +The \code{lgb.plot.importance} function creates a \code{barplot} +and silently returns a processed data.table with \code{top_n} features sorted by defined importance. +} +\description{ +Plot previously calculated feature importance: Gain, Cover and Frequency, as a bar graph. +} +\details{ +The graph represents each feature as a horizontal bar of length proportional to the defined importance of a feature. +Features are shown ranked in a decreasing importance order. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) + +params <- list( + objective = "binary" + , learning_rate = 0.1 + , min_data_in_leaf = 1L + , min_sum_hessian_in_leaf = 1.0 + , num_threads = 2L +) + +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 5L +) + +tree_imp <- lgb.importance(model, percentage = TRUE) +lgb.plot.importance(tree_imp, top_n = 5L, measure = "Gain") +} +} diff --git a/R-package/man/lgb.plot.interpretation.Rd b/R-package/man/lgb.plot.interpretation.Rd new file mode 100644 index 0000000..b7905cc --- /dev/null +++ b/R-package/man/lgb.plot.interpretation.Rd @@ -0,0 +1,81 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.plot.interpretation.R +\name{lgb.plot.interpretation} +\alias{lgb.plot.interpretation} +\title{Plot feature contribution as a bar graph} +\usage{ +lgb.plot.interpretation( + tree_interpretation_dt, + top_n = 10L, + cols = 1L, + left_margin = 10L, + cex = NULL +) +} +\arguments{ +\item{tree_interpretation_dt}{a \code{data.table} returned by \code{\link{lgb.interpret}}.} + +\item{top_n}{maximal number of top features to include into the plot.} + +\item{cols}{the column numbers of layout, will be used only for multiclass classification feature contribution.} + +\item{left_margin}{(base R barplot) allows to adjust the left margin size to fit feature names.} + +\item{cex}{(base R barplot) passed as \code{cex.names} parameter to \code{barplot}.} +} +\value{ +The \code{lgb.plot.interpretation} function creates a \code{barplot}. +} +\description{ +Plot previously calculated feature contribution as a bar graph. +} +\details{ +The graph represents each feature as a horizontal bar of length proportional to the defined +contribution of a feature. Features are shown ranked in a decreasing contribution order. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +Logit <- function(x) { + log(x / (1.0 - x)) +} +data(agaricus.train, package = "lightgbm") +labels <- agaricus.train$label +dtrain <- lgb.Dataset( + agaricus.train$data + , label = labels +) +set_field( + dataset = dtrain + , field_name = "init_score" + , data = rep(Logit(mean(labels)), length(labels)) +) + +data(agaricus.test, package = "lightgbm") + +params <- list( + objective = "binary" + , learning_rate = 0.1 + , max_depth = -1L + , min_data_in_leaf = 1L + , min_sum_hessian_in_leaf = 1.0 + , num_threads = 2L +) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 5L +) + +tree_interpretation <- lgb.interpret( + model = model + , data = agaricus.test$data + , idxset = 1L:5L +) +lgb.plot.interpretation( + tree_interpretation_dt = tree_interpretation[[1L]] + , top_n = 3L +) +} +} diff --git a/R-package/man/lgb.restore_handle.Rd b/R-package/man/lgb.restore_handle.Rd new file mode 100644 index 0000000..37922c0 --- /dev/null +++ b/R-package/man/lgb.restore_handle.Rd @@ -0,0 +1,54 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.restore_handle.R +\name{lgb.restore_handle} +\alias{lgb.restore_handle} +\title{Restore the C++ component of a de-serialized LightGBM model} +\usage{ +lgb.restore_handle(model) +} +\arguments{ +\item{model}{\code{lgb.Booster} object which was de-serialized and whose underlying C++ object and R handle +need to be restored.} +} +\value{ +\code{lgb.Booster} (the same `model` object that was passed as input, invisibly). +} +\description{ +After a LightGBM model object is de-serialized through functions such as \code{save} or +\code{saveRDS}, its underlying C++ object will be blank and needs to be restored to able to use it. Such +object is restored automatically when calling functions such as \code{predict}, but this function can be +used to forcibly restore it beforehand. Note that the object will be modified in-place. + + \emph{New in version 4.0.0} +} +\details{ +Be aware that fast single-row prediction configurations are not restored through this +function. If you wish to make fast single-row predictions using a \code{lgb.Booster} loaded this way, +call \link{lgb.configure_fast_predict} on the loaded \code{lgb.Booster} object. +} +\examples{ +\donttest{ +library(lightgbm) +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data("agaricus.train") +model <- lightgbm( + agaricus.train$data + , agaricus.train$label + , params = list(objective = "binary") + , nrounds = 5L + , verbose = 0 + , num_threads = 2L +) +fname <- tempfile(fileext="rds") +saveRDS(model, fname) + +model_new <- readRDS(fname) +model_new$check_null_handle() +lgb.restore_handle(model_new) +model_new$check_null_handle() +} +} +\seealso{ +\link{lgb.make_serializable}, \link{lgb.drop_serialized}. +} diff --git a/R-package/man/lgb.save.Rd b/R-package/man/lgb.save.Rd new file mode 100644 index 0000000..3a532e9 --- /dev/null +++ b/R-package/man/lgb.save.Rd @@ -0,0 +1,56 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{lgb.save} +\alias{lgb.save} +\title{Save LightGBM model} +\usage{ +lgb.save(booster, filename, num_iteration = NULL, start_iteration = 1L) +} +\arguments{ +\item{booster}{Object of class \code{lgb.Booster}} + +\item{filename}{Saved filename} + +\item{num_iteration}{Number of iterations to save, NULL or <= 0 means use best iteration} + +\item{start_iteration}{Index (1-based) of the first boosting round to save. + For example, passing \code{start_iteration=5, num_iteration=3} for a regression model + means "save the fifth, sixth, and seventh tree" + + \emph{New in version 4.4.0}} +} +\value{ +lgb.Booster +} +\description{ +Save LightGBM model +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +library(lightgbm) +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , num_threads = 2L +) +valids <- list(test = dtest) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 10L + , valids = valids + , early_stopping_rounds = 5L +) +lgb.save(model, tempfile(fileext = ".txt")) +} +} diff --git a/R-package/man/lgb.slice.Dataset.Rd b/R-package/man/lgb.slice.Dataset.Rd new file mode 100644 index 0000000..c40ec0d --- /dev/null +++ b/R-package/man/lgb.slice.Dataset.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{lgb.slice.Dataset} +\alias{lgb.slice.Dataset} +\title{Slice a dataset} +\usage{ +lgb.slice.Dataset(dataset, idxset) +} +\arguments{ +\item{dataset}{Object of class \code{lgb.Dataset}} + +\item{idxset}{an integer vector of indices of rows needed} +} +\value{ +constructed sub dataset +} +\description{ +Get a new \code{lgb.Dataset} containing the specified rows of + original \code{lgb.Dataset} object + + \emph{Renamed from} \code{slice()} \emph{in 4.4.0} +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) + +dsub <- lgb.slice.Dataset(dtrain, seq_len(42L)) +lgb.Dataset.construct(dsub) +labels <- lightgbm::get_field(dsub, "label") +} +} diff --git a/R-package/man/lgb.train.Rd b/R-package/man/lgb.train.Rd new file mode 100644 index 0000000..2a527ce --- /dev/null +++ b/R-package/man/lgb.train.Rd @@ -0,0 +1,153 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.train.R +\name{lgb.train} +\alias{lgb.train} +\title{Main training logic for LightGBM} +\usage{ +lgb.train( + params = list(), + data, + nrounds = 100L, + valids = list(), + obj = NULL, + eval = NULL, + verbose = 1L, + record = TRUE, + eval_freq = 1L, + init_model = NULL, + early_stopping_rounds = NULL, + callbacks = list(), + reset_data = FALSE, + serializable = TRUE +) +} +\arguments{ +\item{params}{a list of parameters. See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html}{ +the "Parameters" section of the documentation} for a list of parameters and valid values.} + +\item{data}{a \code{lgb.Dataset} object, used for training. Some functions, such as \code{\link{lgb.cv}}, +may allow you to pass other types of data like \code{matrix} and then separately supply +\code{label} as a keyword argument.} + +\item{nrounds}{number of training rounds} + +\item{valids}{a list of \code{lgb.Dataset} objects, used for validation} + +\item{obj}{objective function, can be character or custom objective function. Examples include +\code{regression}, \code{regression_l1}, \code{huber}, +\code{binary}, \code{lambdarank}, \code{multiclass}, \code{multiclass}} + +\item{eval}{evaluation function(s). This can be a character vector, function, or list with a mixture of + strings and functions. + + \itemize{ + \item{\bold{a. character vector}: + If you provide a character vector to this argument, it should contain strings with valid + evaluation metrics. + See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#metric}{ + The "metric" section of the documentation} + for a list of valid metrics. + } + \item{\bold{b. function}: + You can provide a custom evaluation function. This + should accept the keyword arguments \code{preds} and \code{dtrain} and should return a named + list with three elements: + \itemize{ + \item{\code{name}: A string with the name of the metric, used for printing + and storing results. + } + \item{\code{value}: A single number indicating the value of the metric for the + given predictions and true values + } + \item{ + \code{higher_better}: A boolean indicating whether higher values indicate a better fit. + For example, this would be \code{FALSE} for metrics like MAE or RMSE. + } + } + } + \item{\bold{c. list}: + If a list is given, it should only contain character vectors and functions. + These should follow the requirements from the descriptions above. + } + }} + +\item{verbose}{verbosity for output, if <= 0 and \code{valids} has been provided, also will disable the +printing of evaluation during training} + +\item{record}{Boolean, TRUE will record iteration message to \code{booster$record_evals}} + +\item{eval_freq}{evaluation output frequency, only effective when verbose > 0 and \code{valids} has been provided} + +\item{init_model}{path of model file or \code{lgb.Booster} object, will continue training from this model} + +\item{early_stopping_rounds}{int. Activates early stopping. When this parameter is non-null, +training will stop if the evaluation of any metric on any validation set +fails to improve for \code{early_stopping_rounds} consecutive boosting rounds. +If training stops early, the returned model will have attribute \code{best_iter} +set to the iteration number of the best iteration.} + +\item{callbacks}{List of callback functions that are applied at each iteration.} + +\item{reset_data}{Boolean, setting it to TRUE (not the default value) will transform the +booster model into a predictor model which frees up memory and the +original datasets} + +\item{serializable}{whether to make the resulting objects serializable through functions such as +\code{save} or \code{saveRDS} (see section "Model serialization").} +} +\value{ +a trained booster model \code{lgb.Booster}. +} +\description{ +Low-level R interface to train a LightGBM model. Unlike \code{\link{lightgbm}}, + this function is focused on performance (e.g. speed, memory efficiency). It is also + less likely to have breaking API changes in new releases than \code{\link{lightgbm}}. +} +\section{Early Stopping}{ + + + "early stopping" refers to stopping the training process if the model's performance on a given + validation set does not improve for several consecutive iterations. + + If multiple arguments are given to \code{eval}, their order will be preserved. If you enable + early stopping by setting \code{early_stopping_rounds} in \code{params}, by default all + metrics will be considered for early stopping. + + If you want to only consider the first metric for early stopping, pass + \code{first_metric_only = TRUE} in \code{params}. Note that if you also specify \code{metric} + in \code{params}, that metric will be considered the "first" one. If you omit \code{metric}, + a default metric will be used based on your choice for the parameter \code{obj} (keyword argument) + or \code{objective} (passed into \code{params}). + + \bold{NOTE:} if using \code{boosting_type="dart"}, any early stopping configuration will be ignored + and early stopping will not be performed. +} + +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , num_threads = 2L +) +valids <- list(test = dtest) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 5L + , valids = valids + , early_stopping_rounds = 3L +) +} + +} diff --git a/R-package/man/lgb_predict_shared_params.Rd b/R-package/man/lgb_predict_shared_params.Rd new file mode 100644 index 0000000..71a3941 --- /dev/null +++ b/R-package/man/lgb_predict_shared_params.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{lgb_predict_shared_params} +\alias{lgb_predict_shared_params} +\title{Shared prediction parameter docs} +\arguments{ +\item{type}{Type of prediction to output. Allowed types are:\itemize{ + \item \code{"response"}: will output the predicted score according to the objective function being + optimized (depending on the link function that the objective uses), after applying any necessary + transformations - for example, for \code{objective="binary"}, it will output class probabilities. + \item \code{"class"}: for classification objectives, will output the class with the highest predicted + probability. For other objectives, will output the same as "response". Note that \code{"class"} is + not a supported type for \link{lgb.configure_fast_predict} (see the documentation of that function + for more details). + \item \code{"raw"}: will output the non-transformed numbers (sum of predictions from boosting iterations' + results) from which the "response" number is produced for a given objective function - for example, + for \code{objective="binary"}, this corresponds to log-odds. For many objectives such as + "regression", since no transformation is applied, the output will be the same as for "response". + \item \code{"leaf"}: will output the index of the terminal node / leaf at which each observations falls + in each tree in the model, outputted as integers, with one column per tree. + \item \code{"contrib"}: will return the per-feature contributions for each prediction, including an + intercept (each feature will produce one column). + } + + Note that, if using custom objectives, types "class" and "response" will not be available and will + default towards using "raw" instead. + + If the model was fit through function \link{lightgbm} and it was passed a factor as labels, + passing the prediction type through \code{params} instead of through this argument might + result in factor levels for classification objectives not being applied correctly to the + resulting output. + + \emph{New in version 4.0.0}} + +\item{start_iteration}{int or None, optional (default=None) +Start index of the iteration to predict. +If None or <= 0, starts from the first iteration.} + +\item{num_iteration}{int or None, optional (default=None) +Limit number of iterations in the prediction. +If None, if the best iteration exists and start_iteration is None or <= 0, the +best iteration is used; otherwise, all iterations from start_iteration are used. +If <= 0, all iterations from start_iteration are used (no limits).} + +\item{params}{a list of additional named parameters. See +\href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#predict-parameters}{ +the "Predict Parameters" section of the documentation} for a list of parameters and +valid values. Where these conflict with the values of keyword arguments to this function, +the values in \code{params} take precedence.} +} +\description{ +Shared prediction parameter docs +} +\details{ +This page contains shared documentation for prediction-related parameters used throughout the package. +} +\keyword{internal} diff --git a/R-package/man/lgb_shared_dataset_params.Rd b/R-package/man/lgb_shared_dataset_params.Rd new file mode 100644 index 0000000..5e51d46 --- /dev/null +++ b/R-package/man/lgb_shared_dataset_params.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{lgb_shared_dataset_params} +\alias{lgb_shared_dataset_params} +\title{Shared Dataset parameter docs} +\arguments{ +\item{label}{vector of labels to use as the target variable} + +\item{weight}{numeric vector of sample weights} + +\item{init_score}{initial score is the base prediction lightgbm will boost from} + +\item{group}{used for learning-to-rank tasks. An integer vector describing how to +group rows together as ordered results from the same set of candidate results +to be ranked. For example, if you have a 100-document dataset with +\code{group = c(10, 20, 40, 10, 10, 10)}, that means that you have 6 groups, +where the first 10 records are in the first group, records 11-30 are in the +second group, etc.} +} +\description{ +Parameter docs for fields used in \code{lgb.Dataset} construction +} +\details{ +This page contains shared documentation for dataset-related parameters used throughout the package. +} +\keyword{internal} diff --git a/R-package/man/lgb_shared_params.Rd b/R-package/man/lgb_shared_params.Rd new file mode 100644 index 0000000..0394f66 --- /dev/null +++ b/R-package/man/lgb_shared_params.Rd @@ -0,0 +1,115 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lightgbm.R +\name{lgb_shared_params} +\alias{lgb_shared_params} +\title{Shared parameter docs} +\arguments{ +\item{callbacks}{List of callback functions that are applied at each iteration.} + +\item{data}{a \code{lgb.Dataset} object, used for training. Some functions, such as \code{\link{lgb.cv}}, +may allow you to pass other types of data like \code{matrix} and then separately supply +\code{label} as a keyword argument.} + +\item{early_stopping_rounds}{int. Activates early stopping. When this parameter is non-null, +training will stop if the evaluation of any metric on any validation set +fails to improve for \code{early_stopping_rounds} consecutive boosting rounds. +If training stops early, the returned model will have attribute \code{best_iter} +set to the iteration number of the best iteration.} + +\item{eval}{evaluation function(s). This can be a character vector, function, or list with a mixture of + strings and functions. + + \itemize{ + \item{\bold{a. character vector}: + If you provide a character vector to this argument, it should contain strings with valid + evaluation metrics. + See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#metric}{ + The "metric" section of the documentation} + for a list of valid metrics. + } + \item{\bold{b. function}: + You can provide a custom evaluation function. This + should accept the keyword arguments \code{preds} and \code{dtrain} and should return a named + list with three elements: + \itemize{ + \item{\code{name}: A string with the name of the metric, used for printing + and storing results. + } + \item{\code{value}: A single number indicating the value of the metric for the + given predictions and true values + } + \item{ + \code{higher_better}: A boolean indicating whether higher values indicate a better fit. + For example, this would be \code{FALSE} for metrics like MAE or RMSE. + } + } + } + \item{\bold{c. list}: + If a list is given, it should only contain character vectors and functions. + These should follow the requirements from the descriptions above. + } + }} + +\item{eval_freq}{evaluation output frequency, only effective when verbose > 0 and \code{valids} has been provided} + +\item{init_model}{path of model file or \code{lgb.Booster} object, will continue training from this model} + +\item{nrounds}{number of training rounds} + +\item{obj}{objective function, can be character or custom objective function. Examples include +\code{regression}, \code{regression_l1}, \code{huber}, +\code{binary}, \code{lambdarank}, \code{multiclass}, \code{multiclass}} + +\item{params}{a list of parameters. See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html}{ +the "Parameters" section of the documentation} for a list of parameters and valid values.} + +\item{verbose}{verbosity for output, if <= 0 and \code{valids} has been provided, also will disable the +printing of evaluation during training} + +\item{serializable}{whether to make the resulting objects serializable through functions such as +\code{save} or \code{saveRDS} (see section "Model serialization").} +} +\description{ +Parameter docs shared by \code{lgb.train}, \code{lgb.cv}, and \code{lightgbm} +} +\section{Early Stopping}{ + + + "early stopping" refers to stopping the training process if the model's performance on a given + validation set does not improve for several consecutive iterations. + + If multiple arguments are given to \code{eval}, their order will be preserved. If you enable + early stopping by setting \code{early_stopping_rounds} in \code{params}, by default all + metrics will be considered for early stopping. + + If you want to only consider the first metric for early stopping, pass + \code{first_metric_only = TRUE} in \code{params}. Note that if you also specify \code{metric} + in \code{params}, that metric will be considered the "first" one. If you omit \code{metric}, + a default metric will be used based on your choice for the parameter \code{obj} (keyword argument) + or \code{objective} (passed into \code{params}). + + \bold{NOTE:} if using \code{boosting_type="dart"}, any early stopping configuration will be ignored + and early stopping will not be performed. +} + +\section{Model serialization}{ + + + LightGBM model objects can be serialized and de-serialized through functions such as \code{save} + or \code{saveRDS}, but similarly to libraries such as 'xgboost', serialization works a bit differently + from typical R objects. In order to make models serializable in R, a copy of the underlying C++ object + as serialized raw bytes is produced and stored in the R model object, and when this R object is + de-serialized, the underlying C++ model object gets reconstructed from these raw bytes, but will only + do so once some function that uses it is called, such as \code{predict}. In order to forcibly + reconstruct the C++ object after deserialization (e.g. after calling \code{readRDS} or similar), one + can use the function \link{lgb.restore_handle} (for example, if one makes predictions in parallel or in + forked processes, it will be faster to restore the handle beforehand). + + Producing and keeping these raw bytes however uses extra memory, and if they are not required, + it is possible to avoid producing them by passing `serializable=FALSE`. In such cases, these raw + bytes can be added to the model on demand through function \link{lgb.make_serializable}. + + \emph{New in version 4.0.0} +} + +\keyword{internal} diff --git a/R-package/man/lightgbm.Rd b/R-package/man/lightgbm.Rd new file mode 100644 index 0000000..7abd7f6 --- /dev/null +++ b/R-package/man/lightgbm.Rd @@ -0,0 +1,150 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lightgbm.R +\name{lightgbm} +\alias{lightgbm} +\title{Train a LightGBM model} +\usage{ +lightgbm( + data, + label = NULL, + weights = NULL, + params = list(), + nrounds = 100L, + verbose = 1L, + eval_freq = 1L, + early_stopping_rounds = NULL, + init_model = NULL, + callbacks = list(), + serializable = TRUE, + objective = "auto", + init_score = NULL, + num_threads = NULL, + colnames = NULL, + categorical_feature = NULL, + ... +) +} +\arguments{ +\item{data}{a \code{lgb.Dataset} object, used for training. Some functions, such as \code{\link{lgb.cv}}, +may allow you to pass other types of data like \code{matrix} and then separately supply +\code{label} as a keyword argument.} + +\item{label}{Vector of labels, used if \code{data} is not an \code{\link{lgb.Dataset}}} + +\item{weights}{Sample / observation weights for rows in the input data. If \code{NULL}, will assume that all + observations / rows have the same importance / weight. + + \emph{Changed from 'weight', in version 4.0.0}} + +\item{params}{a list of parameters. See \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html}{ +the "Parameters" section of the documentation} for a list of parameters and valid values.} + +\item{nrounds}{number of training rounds} + +\item{verbose}{verbosity for output, if <= 0 and \code{valids} has been provided, also will disable the +printing of evaluation during training} + +\item{eval_freq}{evaluation output frequency, only effective when verbose > 0 and \code{valids} has been provided} + +\item{early_stopping_rounds}{int. Activates early stopping. When this parameter is non-null, +training will stop if the evaluation of any metric on any validation set +fails to improve for \code{early_stopping_rounds} consecutive boosting rounds. +If training stops early, the returned model will have attribute \code{best_iter} +set to the iteration number of the best iteration.} + +\item{init_model}{path of model file or \code{lgb.Booster} object, will continue training from this model} + +\item{callbacks}{List of callback functions that are applied at each iteration.} + +\item{serializable}{whether to make the resulting objects serializable through functions such as +\code{save} or \code{saveRDS} (see section "Model serialization").} + +\item{objective}{Optimization objective (e.g. `"regression"`, `"binary"`, etc.). + For a list of accepted objectives, see + \href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#objective}{ + the "objective" item of the "Parameters" section of the documentation}. + + If passing \code{"auto"} and \code{data} is not of type \code{lgb.Dataset}, the objective will + be determined according to what is passed for \code{label}:\itemize{ + \item If passing a factor with two variables, will use objective \code{"binary"}. + \item If passing a factor with more than two variables, will use objective \code{"multiclass"} + (note that parameter \code{num_class} in this case will also be determined automatically from + \code{label}). + \item Otherwise (or if passing \code{lgb.Dataset} as input), will use objective \code{"regression"}. + } + + \emph{New in version 4.0.0}} + +\item{init_score}{initial score is the base prediction lightgbm will boost from + + \emph{New in version 4.0.0}} + +\item{num_threads}{Number of parallel threads to use. For best speed, this should be set to the number of + physical cores in the CPU - in a typical x86-64 machine, this corresponds to half the + number of maximum threads. + + Be aware that using too many threads can result in speed degradation in smaller datasets + (see the parameters documentation for more details). + + If passing zero, will use the default number of threads configured for OpenMP + (typically controlled through an environment variable \code{OMP_NUM_THREADS}). + + If passing \code{NULL} (the default), will try to use the number of physical cores in the + system, but be aware that getting the number of cores detected correctly requires package + \code{RhpcBLASctl} to be installed. + + This parameter gets overridden by \code{num_threads} and its aliases under \code{params} + if passed there. + + \emph{New in version 4.0.0}} + +\item{colnames}{Character vector of features. Only used if \code{data} is not an \code{\link{lgb.Dataset}}.} + +\item{categorical_feature}{categorical features. This can either be a character vector of feature +names or an integer vector with the indices of the features (e.g. +\code{c(1L, 10L)} to say "the first and tenth columns"). +Only used if \code{data} is not an \code{\link{lgb.Dataset}}.} + +\item{...}{Additional arguments passed to \code{\link{lgb.train}}. For example +\itemize{ + \item{\code{valids}: a list of \code{lgb.Dataset} objects, used for validation} + \item{\code{obj}: objective function, can be character or custom objective function. Examples include + \code{regression}, \code{regression_l1}, \code{huber}, + \code{binary}, \code{lambdarank}, \code{multiclass}, \code{multiclass}} + \item{\code{eval}: evaluation function, can be (a list of) character or custom eval function} + \item{\code{record}: Boolean, TRUE will record iteration message to \code{booster$record_evals}} + \item{\code{reset_data}: Boolean, setting it to TRUE (not the default value) will transform the booster model + into a predictor model which frees up memory and the original datasets} +}} +} +\value{ +a trained \code{lgb.Booster} +} +\description{ +High-level R interface to train a LightGBM model. Unlike \code{\link{lgb.train}}, this function + is focused on compatibility with other statistics and machine learning interfaces in R. + This focus on compatibility means that this interface may experience more frequent breaking API changes + than \code{\link{lgb.train}}. + For efficiency-sensitive applications, or for applications where breaking API changes across releases + is very expensive, use \code{\link{lgb.train}}. +} +\section{Early Stopping}{ + + + "early stopping" refers to stopping the training process if the model's performance on a given + validation set does not improve for several consecutive iterations. + + If multiple arguments are given to \code{eval}, their order will be preserved. If you enable + early stopping by setting \code{early_stopping_rounds} in \code{params}, by default all + metrics will be considered for early stopping. + + If you want to only consider the first metric for early stopping, pass + \code{first_metric_only = TRUE} in \code{params}. Note that if you also specify \code{metric} + in \code{params}, that metric will be considered the "first" one. If you omit \code{metric}, + a default metric will be used based on your choice for the parameter \code{obj} (keyword argument) + or \code{objective} (passed into \code{params}). + + \bold{NOTE:} if using \code{boosting_type="dart"}, any early stopping configuration will be ignored + and early stopping will not be performed. +} + diff --git a/R-package/man/predict.lgb.Booster.Rd b/R-package/man/predict.lgb.Booster.Rd new file mode 100644 index 0000000..bcb2f3f --- /dev/null +++ b/R-package/man/predict.lgb.Booster.Rd @@ -0,0 +1,157 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{predict.lgb.Booster} +\alias{predict.lgb.Booster} +\title{Predict method for LightGBM model} +\usage{ +\method{predict}{lgb.Booster}( + object, + newdata, + type = "response", + start_iteration = NULL, + num_iteration = NULL, + header = FALSE, + params = list(), + ... +) +} +\arguments{ +\item{object}{Object of class \code{lgb.Booster}} + +\item{newdata}{a \code{matrix} object, a \code{dgCMatrix}, a \code{dgRMatrix} object, a \code{dsparseVector} object, + or a character representing a path to a text file (CSV, TSV, or LibSVM). + + For sparse inputs, if predictions are only going to be made for a single row, it will be faster to + use CSR format, in which case the data may be passed as either a single-row CSR matrix (class + \code{dgRMatrix} from package \code{Matrix}) or as a sparse numeric vector (class + \code{dsparseVector} from package \code{Matrix}). + + If single-row predictions are going to be performed frequently, it is recommended to + pre-configure the model object for fast single-row sparse predictions through function + \link{lgb.configure_fast_predict}. + + \emph{Changed from 'data', in version 4.0.0}} + +\item{type}{Type of prediction to output. Allowed types are:\itemize{ + \item \code{"response"}: will output the predicted score according to the objective function being + optimized (depending on the link function that the objective uses), after applying any necessary + transformations - for example, for \code{objective="binary"}, it will output class probabilities. + \item \code{"class"}: for classification objectives, will output the class with the highest predicted + probability. For other objectives, will output the same as "response". Note that \code{"class"} is + not a supported type for \link{lgb.configure_fast_predict} (see the documentation of that function + for more details). + \item \code{"raw"}: will output the non-transformed numbers (sum of predictions from boosting iterations' + results) from which the "response" number is produced for a given objective function - for example, + for \code{objective="binary"}, this corresponds to log-odds. For many objectives such as + "regression", since no transformation is applied, the output will be the same as for "response". + \item \code{"leaf"}: will output the index of the terminal node / leaf at which each observations falls + in each tree in the model, outputted as integers, with one column per tree. + \item \code{"contrib"}: will return the per-feature contributions for each prediction, including an + intercept (each feature will produce one column). + } + + Note that, if using custom objectives, types "class" and "response" will not be available and will + default towards using "raw" instead. + + If the model was fit through function \link{lightgbm} and it was passed a factor as labels, + passing the prediction type through \code{params} instead of through this argument might + result in factor levels for classification objectives not being applied correctly to the + resulting output. + + \emph{New in version 4.0.0}} + +\item{start_iteration}{int or None, optional (default=None) +Start index of the iteration to predict. +If None or <= 0, starts from the first iteration.} + +\item{num_iteration}{int or None, optional (default=None) +Limit number of iterations in the prediction. +If None, if the best iteration exists and start_iteration is None or <= 0, the +best iteration is used; otherwise, all iterations from start_iteration are used. +If <= 0, all iterations from start_iteration are used (no limits).} + +\item{header}{only used for prediction for text file. True if text file has header} + +\item{params}{a list of additional named parameters. See +\href{https://lightgbm.readthedocs.io/en/latest/Parameters.html#predict-parameters}{ +the "Predict Parameters" section of the documentation} for a list of parameters and +valid values. Where these conflict with the values of keyword arguments to this function, +the values in \code{params} take precedence.} + +\item{...}{ignored} +} +\value{ +For prediction types that are meant to always return one output per observation (e.g. when predicting + \code{type="response"} or \code{type="raw"} on a binary classification or regression objective), will + return a vector with one element per row in \code{newdata}. + + For prediction types that are meant to return more than one output per observation (e.g. when predicting + \code{type="response"} or \code{type="raw"} on a multi-class objective, or when predicting + \code{type="leaf"}, regardless of objective), will return a matrix with one row per observation in + \code{newdata} and one column per output. + + For \code{type="leaf"} predictions, will return a matrix with one row per observation in \code{newdata} + and one column per tree. Note that for multiclass objectives, LightGBM trains one tree per class at each + boosting iteration. That means that, for example, for a multiclass model with 3 classes, the leaf + predictions for the first class can be found in columns 1, 4, 7, 10, etc. + + For \code{type="contrib"}, will return a matrix of SHAP values with one row per observation in + \code{newdata} and columns corresponding to features. For regression, ranking, cross-entropy, and binary + classification objectives, this matrix contains one column per feature plus a final column containing the + Shapley base value. For multiclass objectives, this matrix will represent \code{num_classes} such matrices, + in the order "feature contributions for first class, feature contributions for second class, feature + contributions for third class, etc.". + + If the model was fit through function \link{lightgbm} and it was passed a factor as labels, predictions + returned from this function will retain the factor levels (either as values for \code{type="class"}, or + as column names for \code{type="response"} and \code{type="raw"} for multi-class objectives). Note that + passing the requested prediction type under \code{params} instead of through \code{type} might result in + the factor levels not being present in the output. +} +\description{ +Predicted values based on class \code{lgb.Booster} + + \emph{New in version 4.0.0} +} +\details{ +If the model object has been configured for fast single-row predictions through + \link{lgb.configure_fast_predict}, this function will use the prediction parameters + that were configured for it - as such, extra prediction parameters should not be passed + here, otherwise the configuration will be ignored and the slow route will be taken. +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +data(agaricus.test, package = "lightgbm") +test <- agaricus.test +dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) +params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , num_threads = 2L +) +valids <- list(test = dtest) +model <- lgb.train( + params = params + , data = dtrain + , nrounds = 5L + , valids = valids +) +preds <- predict(model, test$data) + +# pass other prediction parameters +preds <- predict( + model, + test$data, + params = list( + predict_disable_shape_check = TRUE + ) +) +} +} diff --git a/R-package/man/print.lgb.Booster.Rd b/R-package/man/print.lgb.Booster.Rd new file mode 100644 index 0000000..27a2849 --- /dev/null +++ b/R-package/man/print.lgb.Booster.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{print.lgb.Booster} +\alias{print.lgb.Booster} +\title{Print method for LightGBM model} +\usage{ +\method{print}{lgb.Booster}(x, ...) +} +\arguments{ +\item{x}{Object of class \code{lgb.Booster}} + +\item{...}{Not used} +} +\value{ +The same input \code{x}, returned as invisible. +} +\description{ +Show summary information about a LightGBM model object (same as \code{summary}). + + \emph{New in version 4.0.0} +} diff --git a/R-package/man/setLGBMThreads.Rd b/R-package/man/setLGBMThreads.Rd new file mode 100644 index 0000000..53336fc --- /dev/null +++ b/R-package/man/setLGBMThreads.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/multithreading.R +\name{setLGBMThreads} +\alias{setLGBMThreads} +\alias{setLGBMthreads} +\title{Set maximum number of threads used by LightGBM} +\usage{ +setLGBMthreads(num_threads) +} +\arguments{ +\item{num_threads}{maximum number of threads to be used by LightGBM in multi-threaded operations} +} +\description{ +LightGBM attempts to speed up many operations by using multi-threading. + The number of threads used in those operations can be controlled via the + \code{num_threads} parameter passed through \code{params} to functions like + \link{lgb.train} and \link{lgb.Dataset}. However, some operations (like materializing + a model from a text file) are done via code paths that don't explicitly accept thread-control + configuration. + + Use this function to set the maximum number of threads LightGBM will use for such operations. + + This function affects all LightGBM operations in the same process. + + So, for example, if you call \code{setLGBMthreads(4)}, no other multi-threaded LightGBM + operation in the same process will use more than 4 threads. + + Call \code{setLGBMthreads(-1)} to remove this limitation. +} +\seealso{ +\link{getLGBMthreads} +} diff --git a/R-package/man/set_field.Rd b/R-package/man/set_field.Rd new file mode 100644 index 0000000..2ceebfb --- /dev/null +++ b/R-package/man/set_field.Rd @@ -0,0 +1,50 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Dataset.R +\name{set_field} +\alias{set_field} +\alias{set_field.lgb.Dataset} +\title{Set one attribute of a \code{lgb.Dataset} object} +\usage{ +set_field(dataset, field_name, data) + +\method{set_field}{lgb.Dataset}(dataset, field_name, data) +} +\arguments{ +\item{dataset}{Object of class \code{lgb.Dataset}} + +\item{field_name}{String with the name of the attribute to set. One of the following. +\itemize{ + \item \code{label}: label lightgbm learns from ; + \item \code{weight}: to do a weight rescale ; + \item{\code{group}: used for learning-to-rank tasks. An integer vector describing how to + group rows together as ordered results from the same set of candidate results to be ranked. + For example, if you have a 100-document dataset with \code{group = c(10, 20, 40, 10, 10, 10)}, + that means that you have 6 groups, where the first 10 records are in the first group, + records 11-30 are in the second group, etc.} + \item \code{init_score}: initial score is the base prediction lightgbm will boost from. +}} + +\item{data}{The data for the field. See examples.} +} +\value{ +The \code{lgb.Dataset} you passed in. +} +\description{ +Set one attribute of a \code{lgb.Dataset} +} +\examples{ +\donttest{ +\dontshow{setLGBMthreads(2L)} +\dontshow{data.table::setDTthreads(1L)} +data(agaricus.train, package = "lightgbm") +train <- agaricus.train +dtrain <- lgb.Dataset(train$data, label = train$label) +lgb.Dataset.construct(dtrain) + +labels <- lightgbm::get_field(dtrain, "label") +lightgbm::set_field(dtrain, "label", 1 - labels) + +labels2 <- lightgbm::get_field(dtrain, "label") +stopifnot(all.equal(labels2, 1 - labels)) +} +} diff --git a/R-package/man/summary.lgb.Booster.Rd b/R-package/man/summary.lgb.Booster.Rd new file mode 100644 index 0000000..bd43088 --- /dev/null +++ b/R-package/man/summary.lgb.Booster.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lgb.Booster.R +\name{summary.lgb.Booster} +\alias{summary.lgb.Booster} +\title{Summary method for LightGBM model} +\usage{ +\method{summary}{lgb.Booster}(object, ...) +} +\arguments{ +\item{object}{Object of class \code{lgb.Booster}} + +\item{...}{Not used} +} +\value{ +The same input \code{object}, returned as invisible. +} +\description{ +Show summary information about a LightGBM model object (same as \code{print}). + + \emph{New in version 4.0.0} +} diff --git a/R-package/pkgdown/_pkgdown.yml b/R-package/pkgdown/_pkgdown.yml new file mode 100644 index 0000000..2fd6405 --- /dev/null +++ b/R-package/pkgdown/_pkgdown.yml @@ -0,0 +1,111 @@ +template: + bootstrap: 5 + params: + bootswatch: cerulean + +site: + root: '' + title: LightGBM, Light Gradient Boosting Machine + +repo: + url: + home: https://github.com/lightgbm-org/LightGBM/ + source: https://github.com/lightgbm-org/LightGBM/tree/main/R-package/ + issue: https://github.com/lightgbm-org/LightGBM/issues/ + user: https://github.com/ + +development: + mode: unreleased + +authors: + Yu Shi: + href: https://github.com/shiyu1994 + html: Yu Shi + Guolin Ke: + href: https://github.com/guolinke + html: Guolin Ke + Damien Soukhavong: + href: https://github.com/Laurae2 + html: Damien Soukhavong + Yachen Yan: + href: https://github.com/yanyachen + html: Yachen Yan + James Lamb: + href: https://github.com/jameslamb + html: James Lamb + +navbar: + title: LightGBM + type: default + left: + - icon: fa-reply fa-lg + href: ../ + - icon: fa-home fa-lg + href: index.html + - text: Articles + href: articles/index.html + - text: Reference + href: reference/index.html + right: + - icon: fa-github fa-lg + href: https://github.com/lightgbm-org/LightGBM/tree/main/R-package + +reference: + - title: Datasets + desc: Datasets included with the R-package + contents: + - '`agaricus.train`' + - '`agaricus.test`' + - '`bank`' + - title: Data Input / Output + desc: Data I/O required for LightGBM + contents: + - '`dim.lgb.Dataset`' + - '`dimnames.lgb.Dataset`' + - '`get_field`' + - '`set_field`' + - '`lgb.Dataset`' + - '`lgb.Dataset.construct`' + - '`lgb.Dataset.create.valid`' + - '`lgb.Dataset.save`' + - '`lgb.Dataset.set.categorical`' + - '`lgb.Dataset.set.reference`' + - '`lgb.convert_with_rules`' + - '`lgb.slice.Dataset`' + - title: Machine Learning + desc: Train models with LightGBM and then use them to make predictions on new data + contents: + - '`lightgbm`' + - '`lgb.train`' + - '`predict.lgb.Booster`' + - '`lgb.cv`' + - '`lgb.configure_fast_predict`' + - title: Saving / Loading Models + desc: Save and load LightGBM models + contents: + - '`lgb.dump`' + - '`lgb.save`' + - '`lgb.load`' + - '`lgb.model.dt.tree`' + - '`lgb.drop_serialized`' + - '`lgb.make_serializable`' + - '`lgb.restore_handle`' + - title: Model Interpretation + desc: Analyze your models + contents: + - '`lgb.get.eval.result`' + - '`lgb.importance`' + - '`lgb.interpret`' + - '`lgb.plot.importance`' + - '`lgb.plot.interpretation`' + - '`print.lgb.Booster`' + - '`summary.lgb.Booster`' + - title: Multithreading Control + desc: Manage degree of parallelism used by LightGBM + contents: + - '`getLGBMThreads`' + - '`setLGBMThreads`' + - title: Deprecated + desc: Functionality that will be removed in the future + contents: + - '`lgb.interprete`' diff --git a/R-package/pkgdown/favicon/apple-touch-icon-120x120.png b/R-package/pkgdown/favicon/apple-touch-icon-120x120.png new file mode 100644 index 0000000000000000000000000000000000000000..67bafad5468affca25d774641d62557776c75333 GIT binary patch literal 9412 zcmZ{K1yCG8x9;K|7ALs7%i>OONeFJiCAd3ToZ#+Ga25!WY-I<#1n(662bxxmis=L4M^IHu?985Ay004latR$}muYLbL=&0~g#0oSGuaIqJ z)MWsGuZdWX<|y!Y8cQWDbpXJh2><|x0|58%PvCt3z>5a}I4}bMM6&<@64$(rcjE8{ zR0~x_dBDrRyXad*D*OwYr?UDRv>kMEG(5`wzr4%<0KK)cyo|Q*uj71su+Cuq%_p6` z70B&3dw+y!aJ*yVXO>Q|&0C-w8d1Cf)0AC`pRJ^t&F3LyzIwd)RO?xqtpo%jAkN38 zpXH4|zQ;)WG+lpR3xHMmOXUS{uSS-U`*-Hw{w-2p|L#?NAaebo=74*Dt0AKt5h|^Q z8vIHdNfW#VSlk^iqg)*XMeOgtY9^{Jt3pqk{J}2$2!xU=VFei@kD)zk; z1Ja``8a7;3#%AiZ|Mex$irgs2%UQB~x*EYmETSx74`~b8lRgF%cOp)`Q*USyKnbCa z7or}(1qXzv(MqEW>wdhLub?7!NlHs&wX$m_+fSx#cZeI0t3k42GGzJvNF|Kf1@JE* zuCpk9%6iAqTAE_^@Xe~(96ZlTbAlR-ObF;pdi&0{Auu*qPdEFBecBE}zY|$3K&r(9 z&D?m#ASOV&|9r{fs3^s5hIPiVe=Du+AQpX?@iT$SKNimm*+_K;aE#L~A%&oT#KlSr zBy?On-7J$$jenp4&MNtB-Z-N0n)coy@Yq@;(Mlj?@s$*kgNb^KCkqjK?Fg3N$u+bv z5J}Sw==7BKVAGglzD2(wZuhlr$;JH+{%IIbo4fA4*r=hIrbb-_wN(R~{*e*DdosrC zLb$)xzmfbhyL;7`P>^C%JVW5fsVoB(w_{I2+oIi&p%JUKRsE?>>~z&|_I~7iCg$vv z8|77Y=p#q3#I|EO&TT50tU zct(tQDnDf-X?Yu!_mv_{16f>4`Ync})Xr|1EXkp_Oi+>a7L>7pXy5@kwJ!958JRST zTs|f`xDI6vtw5$`9DRzlzaT%x%t4vW(1G^L5w1Am3IapKo0H;HE>n|jnQ8gu(oDx@-{@w>__5cP-B5g#EqL9>mE{TCFb)Y0hKz`HE6Ln4u8Hs$$`ipNPC@ zcT|)76sF=j)@LU+xGuGV!tPqMP)~_~s<&j}K*AmL6H1r!V_Jk1i=zgkvJi=$Q<-jz zM(kRRO(`Fi-sjsC-n=slifDV``E#Bg9){U_#v1ST><{}EhvxARLrs_|TN+}Us;hd) zM#yIwh3o5Wke9v`T1ZQeas0DB4Vfc5*j7O&>bkAJoc)WfIL?zpiP?~Dw!GkM zYWk<-OtsPp3YA)3OSNWKiXjRNAYBpTl9urXPMUu;!Qns+W^P_9N2xBo^5`Tu0@k4X zknF(?F_6vyYS<>^xt;Hd3Y|Kp();3o|X~6g_q#qZ*{WN3k*)%wBMT#-hgsjnw%X zlR1n#WIe)0z^-EbX0xM3+{T@T}Q~z*@{es@^R2O}Jqrf1|nP;{@D?Xu9?lIM`J* zTjfqy zO&wbHsQtS+*vh28!Sd5drngIFe8`YoRp`^4Df(vfPEufR9)`@&EiNzs#A%Q8VIy23 z6I}U*ZLA8S=vgzC84QcyBtSSu;KjRR5%^ z!zFRFv<06o#*%cb@B}nBb`7#qHd*_w9MlnJG4Zhv_Kn+0hv6hfv1S+oG%SwbzeF)l zvc`q>h9{IDi>@nu@-IGs+lq1wYJBYlkb?(PFxCYJmDLqlcn&1-I5opGf>ypnSwz&=87uYF=ASe51rnNFjpW%zj!=&T4}2n!A$al2h)G z18Xnp+iH~4*%C zSe*jun(Rja4Pe~_&m-(K~ zbdYsM)5qMIHxL=JP-mpAeGf}a<#qQkSM*amQhYWCqIyWAVCT0$+mzVO`kLV+GKvyz zXct#&D8euzXT6|)SMP>Z`)XBmx-_6yMXF1?c+ub#D#>Zk^3~q?f|)6mpN&ed79H{{ zfojP>-u*W-(Wx_EyLC9jnuAbH>^Pk;Uv`O8T%{_Rz@{-|WA5WPnhr?N8_R_HeUhL+ z$eIvCYSx}Rol3shTmdeL>d!nZPC954OQsc$` zA;`~J9F@_=qcCj=kZb>yE(pTm#L#Eqkw`f*$G9rUQ#1Azy|A z{nl5`l2+qH+)itqp0!;DOC#*S`8;?w$@H#jBC?W3A6=Lt{1fM#W9?UrRvxp>i`P2| zY7C#W@OfX`u2Pt8gfbg@ciEOgG8`|3|!ouYpx6RB))sVjE1bbLg&sL~*>S&ggGEtxNYy2Xs~ zfVH2}4|jsx&AwcVt(bIDu8^eTPsG(NlfN;X5jUfcd77t@KXN__5~shpZZM(23Hg)m zOOTgNwJ@$O+dAy0%=KNcV`lR-cEf`fpVB>g!H}S~31fCiJAJnQRg~u+{K}#OhCSZg z#C=+QvWb;nGR^b0)-FarCK}j?ZI#=QrmB)k!_3OiZ>TiJH^^NJMIsNc@Ww7Itmae4RPi0aiJT!uJW8M#WT?3Umvc}fZNT|aunSKEeHE|M zhYQOjZo@+wtWA6#B}H%lWR#`o18VH!jyppI7i5Q?BMF{5g~sIUr<|DNnz5d17tsB_ zyghSzVV~74w!XZo$E4gX;6n~}4WniyJpFWXypPvHYLaS3o9vx5fOM#A+zl-CEu=Th zb^F50v048EFVLrPJYMc`z@O+LCayA0-G?`n(&vYssr<$6;GH*N$y1BnR97kSCOyN9 z;zz2y{R&$JAVkz^!Lg?J5Xiw0`J35oT@+uaD_;C>deesdr7i>*TKxCyiJW8T_I4XY zmpfMaYyJcgS*!-MJBFtPdPad22<}UV!4m|6k&I#J#>7<4()ObjqroC($ItKJeI|XS zF=RbL(tl)u4l3zRvd5%CVA&ZXC*J_GUsm` zih;EG@E#HSVAEJ?aZcCCQXUsR=Hvk7@|%l$AvelrL?@$hqZZnQJ)>3R^=vDTOIInk zwI9m&T=DP_rsp}VQC>eI{#wW#R?S}we#5WpRJlJiyO|TkBvNrnrXzKD0e8q`{M$`V zD6XRdd*>1Y7Vzp6VLjJ^*Y7Q$Ue!xkB7jyNtry%XOA-R2AG2duN+4JfRq=R1mTIpe z?uaFOM1T%oYTk;POn-GoXmnBY~vyYrz-fJMMJnGhO&WmlN~;Hu@L6@#bALIq>A ze`q>tCM~HDAlOw7Y`%_3C%@nwWEfh>DC~%_SBAA~To3j7BBjMZpOcCNiLYePXFc;r zeqR@Qs{U${dziQ9`K9{FNS>&76Z~eKqE{}}>DfHGkQBDA*t4aDCy4Ao!>h}4{Bb;N zL1>b`AM?1%K59AGYTs~(WsF<_rPol-IIb76fXRf7?pi%v^rh?RBq|+0AwLNMJ z5N?eKFlzS}cVS;&3+i|`L)CSWz+G7WUPkdQ9XuKv(#t!nh5odPmc$tqrDM$hVy)l` zkEWPucG?>&OKTjN8qa7Y|Ni${YZcJu9pn;xIfW4F8{U?tqeA&rTTg}K<w9V_#(RH~E$R9HiM%?OY*q9ird)Ucnv#XBvd(>@PP z#$NRvqtcKH8wFli1SM$|wBg1-u$INejUIcKruAW>CKqR}wmPZJ4DeucjK(yt{=t|x zoPih#mBX?^>~lCiy#&3Y77b6sFC$zM5mEd0hY1yjDsK7M=H)r4tI3Y{TflAk3mAby z+!t)EngMbKLWoeF?s+e7mVdxb)#c$g0h2Blp1Ava}t?|~XnSAa$N*DfDW}_M0ERT1o z3PCi}yH`fewDpR(2L~UQ8iFOLySnqL*(!jZaK-s;CKx0n^1U1u+z)11oxeFTp`bif z*4J;B60zUy*Run%1gtSrE7X`oM6ydtEHb(txQyBy|2!hIm;D$!7{aA~#*-{elnA(S z-S0slbbTX(p!s~ShfOI;NQ<0d&1-+bFceRo2ysNkrBd5uj^Oxk6P!w7v)`~Oez9M= zv$scICa*FQzy1tb?ey7ew=I0bI(VI0XFGqilzfUqDVkO+7o}xtI?>VMAQE)JV2G|@ z62Pe`{#W%P;8wvQ-r7K|fG^u^dpJ3xKqEL<63a9=TT}8Lir1Yp^1j^lA>St{Ir%d% zfh|Y$nkf>Sa)_JyQ{F)}TOcp*O2A9d)$wv>!8iYu+}zG75$gsrlr#02U4rfaG`u51E@3$uI9v**W zR25yd*bu?5U){u?N11<&jWN_KQYn7Fj~UMeeRbH=cs4)KzVP4q>T^nEYSI8;Wteuo z+H6@c8Qtg&!$3wHdpdS8xZ5?-2vLEsvaxNoOFb5R`SOJcHg9N|!}QN}nG1apKgl5( zI(VP1se60-VBTL_UZk;h-A;6><4>dEQtT{fN-8V=@X^rJ++&t{fX~PXN1y{OO#DgU z8C44IzUF5-dc4k$!gVYxJtPEjCQ8S~uYO&-2SWs((6DJ}X$xj5G%pu$rQ}&7>~o;T zT(2m_e@sw{O*}mHX-XEu)teDW)@ro9b^hq!z^k4LU4%xzv&^!wvItK~z_TR7lXIBd z=*=o0Nv=<5@SppL-QUBbt~O)!aC02!byw5v*9r{n=n&1|{-9A*Ogp_-vOT=i<;M?b z_wySV92PRTD-3GQeqW)n>D~@l{N_N?YX%%`)Hl$NUOyf7%${9X@HpE*0Bmh-xkh1k zkG|=|=Ln~fpr-_1T)6*KQO(_d4e=Uqd1h*!Ps;#tQqD|Hf~^fS?3%O7K48x<@beRZ zQwSKfg<|S3dRjTi$jBz<0^nt3ZKdnS=_q*4?i#By+PU_pv>=0Q#9YKg6LE(3P@@;6 z(E-u5Om*k2A|eafWfeUPk&aDG>D4M73F{@rqUbKlKM};UflY|zQzZqwoILmWGi(0l zsG`dl2Z{rpQMLJ5!;7l(ayf*nD-x6r=?ELBn42(C)7p#~m3L7Z?3f+rq>pS-}4A z0Ylo&4R20RFrV#kmM%1%II|@t9C&nhp;}#Ct*oN5^^CTgl$`xXzR<$5-FBX%uIu9G zC{^0DsI1}b(aD{K1szw6Rt&_b&BB1#&(CkH!v}@3knk)r=zC&p(#2V-I z>=y0u{9a`wbGhwuDyEz7vUM5yXE%3b^sx6i=;6osXsIHMDo@xgbY*2Fw#9eN7L(Sl zvVL#zYw5q_Z#k4;ebNNe($yWpdU@!kko1ghsnKh4+g4CWq#DmdqcYp*384`4kNd7? zEc4NjjajpRj-S8O{!kW(j{I8Gk%hZUAdzypn?Zs;KY778Y~j%QvV5H5L+cTRZaq{f z5q6zhUKrj~Cc($p7T0{%uG188hobBHCFZR(l4RzVqR{Ml#py$Ka6HWr`?C#+rI(i= z{F>DkOP}W$Evc=IYn;|X?4^-KKR7(}yRTYr^qsPRhei9xA37_+dz7A|2SYP2x2)e+ zS}6fyvrub;w9NGPhK4FVr!H2MSVwOk)*r_jIo5oSy}XZqMU4L5iX?Y2dkjBMZm_er zXVrY(a?>Rywpa!&nEFuiyL9993*616YPugSz5{4iHpumyRx~%qSq>*$jLmpppoaaE zX)I-r51{^Z#rtyZ@-p<9fH`mSi3G3o92q<))c2z~^0rH)_VVHSR8v6#H9ei^XssO!ys+k<`aS63;)ox{cUx7Vq@qH{UL)4p zDB^v^0{7u7YGE*RQCLMqWwFzTTX1d;Z+n~v+uhyc-2YpiWKhmDzb)P7)FKob9nCEo zoY&FOp;S0CJ*}j!TKfHaL8)TmHlH?s?0Zb7wZL*~WHE*hU0p{3g?_#g+NQef77Mkb7~GuUP;EPVj_w!801WZky) zn1THMmM2FKE;i2IdhhpHsHm{9(3)Y2zgAJfv@91A)x2vaL`nH`lb@?B<@Q4qmV9n9 z;#4snO*gOWj6nP4Nhd2ir}aat6MQAQapSwcP*0sGes4KFJuMvkDD0pMNx*)VpL>2e!lIU$uJ6(!xOSJX_%UpHo#urt+CF2ucfUm zukM9&etvG{1?xNZ_ISK=dw%-xY2s&JpZ5~gYzE_+QCi5S$9VteZRbqHZT4_e)Gc6A z1?_uudFPz*yJzBW9m*QIWO>z|t+}9J^Zw0&JE-v1Hi43c^3ZR0w}V+_-Fn0E8BK}7 z7$WvxN8n^2BGFIg;h~}Aq%3JIE&S;u3aQ|{?%+FG&8OY&r7rvTxv4dTS&?SiAv+(O zTeA3zLryjuPs|Vw!JiPG1fuJ43~J(YtBtPWRJ|9E7+Dvm<7ay*fVhab2<_%ifhE&c z=L$kDb^OBKhHnmDg~~O;_UL(e@#J82$axM+v^v{YaCYo?@2YCMdw#Hl)VLYr_V#EngLh6Q%yb5w*rSZD4J@o>Y~d zrQ;l{B7)pJg0f8m>@lag`a#qF2>)3Ff}r39O27XJ_O zKwDyq@Ex%LO@ca(zz*vY@%gB%M7=_ANB1>NBtjy>3|OC)qR?f*jRZ@#-QX)x^?~Oy zHIfrrxC@n%deVxxEU$wj_)(3S;q`RW?;hC=k&s_Fvg8m3_`V$ULFlH@su=`0Mml$* zC{aWxPn!g$)h-|I@4;>yLUpeWY{fiPAG6r=&$b#K0bH-Anu3dfzGPQqG?Rw@H?np@ zy>{O+u_G}I%fhly9)bIut{|nfIZ}>?zW$mToL9tRd!!|A9HU~rvG-Y_pF#(BWP-Q8 z;Yrkh{|j0&H(%49;tF!OWo^zVr<;_ef7nP8$5J9Fc6V2tfckpm=lXzDG;}fWz_ermQc)VQtP2=n0jPGy`}mS4e??AI6gr_Oe+?y%>J8dPB(xF}T8NB>pb{f>%hfoc7ypu@D8!=d0L_+I zr7Z(I;W!~2)ctRqP~_h4A4({?2GPP?;p!DcIqQ*+eHZiug_WO&_86LR?I+3zm_6{i z*c(dk^G*3n$vLtY{Wcs$a;}wcXXuz;h zV1N>hJ2y$fmo=i#Ctl$NdoNZ%gg?T&m(PX;_z|j3uvl|IRvHX#p+Lwxg6AWb>;q_$Gpe;&qzJ- zz->72Mu3n=2-FUR;kNr#O%8ULyrs4idvautzQtfN`TgY_byrY%dk*mw305>`BR@Lr zdmN#m%o85QfH?V?#%7Cw4;67b>Fp|w!PnLP&~zrTZs*m8ifIip7hjj&yw5U2H&`29 zid@D`;BjGp@BhM;P2mR4&)DFpWn^WU32R33qoG2iLT>Cgho!iuz9E<3FlH7BRxr`6 zG8;a|yPCjX;h(Vi4+|XZpEbsplsWl6(}cA5IM@6Qyr<9WWb8zNu118 z86%{~>?s)c5=$L)5+;4NWv*!?6Q{@%T$DOQKFtpH19dF|YJ2d8;n@E~r7d@OB|RzG zaGNs1e~1V`6;<8Y*Bt9xyXiVi4>5UPo`tSGgyV&vnFOYJ!jqbL70SMDSQ@_PCcrr@ zl+<&tooRacb-s7bRPSVS{FWBY2rvFEZ zh`&C1w}CQw39Pe7qi!O&Ys!qNWD>?!bhHHMZATwhNyibIIrL7Ha+#X`-Ys>SI6f1K zpB$q)tkBd=6)7=x4`)NUd+PC91%cCqNk3XBB zm?XgGwycUKO?vbnF=d!k6;4bE_fXkDDJ@!FxG5mWn(3IRc8cRtbtR40u>23VB`mma z>D!`6e=Owl3)=B+afyIb78K6GVf1{X?`dW3X)S8$ZVj&hd^~(SoIJvud_vkhLZZAv zqI@FkJba=&Jh)Cq2>)LPCl@Pw8^8a(Lomf+n`U%z n_Q$~30L1{>C|IdTg$r;%g6DZ5E0_)!0Z@LUAzvqJ7W#hx27JJY literal 0 HcmV?d00001 diff --git a/R-package/pkgdown/favicon/apple-touch-icon-152x152.png b/R-package/pkgdown/favicon/apple-touch-icon-152x152.png new file mode 100644 index 0000000000000000000000000000000000000000..ac348c65fb20ed24af6c6fcb04711fa56037f95c GIT binary patch literal 12243 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 Y5gvxyl>~PxM>aCi6M?tvgd0|X21?jE+^-5>ky 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 literal 0 HcmV?d00001 diff --git a/R-package/pkgdown/favicon/apple-touch-icon-60x60.png b/R-package/pkgdown/favicon/apple-touch-icon-60x60.png new file mode 100644 index 0000000000000000000000000000000000000000..a84c66005688a76f40e6eaf27f33c126e2de59ed GIT binary patch literal 3946 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

e zKWq>>=sZ^k-2d-Dc9f+*c!>P9^qvuI5i=7}v-Vy;;R66@ytUL-OoJ8<^Fo45e>9;Z zZu6eH@(yq55l5YvQ>&opsQMTjgrgD4p4F6}4c)_LZk8`Fij9}eKRUhkH}^WuD>$ZB zZtlEP5?+WRi#G5IAF1P>5tF>z>Q9|>n#gOI`?smfri`0IrP^q>Dm^ScoF^V~f3e_M z+cfz?XIiJvE{BW?AW42DjU>4e;PNg$!OgPxbij*?)DOg4qHpB@0kINmylTGH$}ft1 zx2-Kn<^PMq3CXY-876%FJ@Oh<@=$^~f&OuAUk~@DVZVLY0bnfuz%X_D-J@p23Dt=N z74@`bgU8*INVupAmjg~3u19Qigd~vnIVcHTkt8*0FD~#OK?&p!`H~yOhO<*M*56(QI__6gi)1G64~+_6ydbc=%N?&sA|ztQ$H<&n z<21(R`S({fNX+FQgXLGBA=TdwM)e~vqK>F<168A11siZ^Ihaw}0-Z4a3I3Gqz1DLn zgD!VtYaYgtPtnh5so7UjI}x4}uXDmasJ)1twZ;~r@i;^yD@-;VCJ><)k*$Ka02=%N z>JvN6+TS+b-r#e!1ZPh?F9AYf2=_d$Fa(6~?R`stu`Nvw)NJC}aevX@;_Z!lN?QV3 zrVxUb0Sz4^j~NN=0~IpspstZTm9SZunZ(mWgD$5&OzIXYyKY9+;X@zJ_SEx~( zgBljZ5r0z;!nY(e24w*{`EUooQL>F^y%C$7D;C?ZgtG4aiu90vt^fID#9% z)|r3f;HXP57w**HuNSHJjjrI4p&^7xereJva{hal$UB@2_=!{giE;k(Vm{&gDNt3U z!$x^ePr^o?UvM6h$EX@kf@0sYlj#Ik>YzjAct{g5Q8wiLbMi*>>_;RgeV~M#|RTH#USR=i>YaTwuI-oLcGbwD_G(-eo9}Jo#=pV)wo%Cjy!$PP)m6&=wXV3KuPC zFkv#!Ds?wTKlJ_heuN^}Uf`uEqOd=c*Px~WzRsGCc+uwUhUDv-90Uc|7!O(E=mbUvh#lS8eD0DUSeoa#*O?O z-6ljmp({TH$rhCU`S2e7ZpChZ%%$ivQ#l-G}EA-&mJ%zcv*0dt;dJ)^Ey1 zvJw;0-xt{#U>|idpLHQ~B}6W^-)|Dl^5*5>ht7esnGz~9q+6`y>lQZtu_925pozxC z-`r7>(tDdI2d(Oz^Wdb%gR>RI(HMvg!cT*gth7JJ--AVvZTy)bQQZszn7|FXk892gpxF|0ozPQe-pbP3ojQWn{;~E zO5H#$(9(Aof4rTHMPJy*$gnhq2ki(sjIoTv{2_)Mjj;S3Ru=I|GF~d8E6eS&@{bh=*F1kSUY$z<;r_6(`P~#j7DT@3X^* z)0|&B!Z(RFaV@~Wc&No zbheDU%3)A$6S|ffPF}+812-F$jCwqn!?O0vy0UJ+#YX4_?DS%gl&|hE?ZAyq56D4L zmC(HrKe+Qng9bneE#agRI0)t2wSpCJ*Qh0g?cnzM?r zkzg|<`TNSVwx+zjKpJSGFNvGJ5tQ_pNK4xwcf5=r_*S6_ynF}qht=Xy$uBp8sRDkR z8>afuNB%If1fBk5!`j7MG>+&?G6e-)YNfGqX)9P!2ApM70UI} ziYpq2K4BK?_j{6hhfk3k5#ZNB|CLP)aULhq0n7Z5BoyT0V57$Ly2p=Eqfe0|}0tu^o-GgwP(jrq2$l4$=}H`;qB}gcF$!LZfAcqF+1a z2BoWoJxfHSgvFO0s{Ppj{TQzd62;+-&({u}%3&m@9$U~hHDMhIP{!AyDf;CGs$j8i z2@bNp0D^Jy)AYCdS9^4Zxee`;!jQmkZTOY-jFqJ`aX)_ojUH?JN}TUb#1~)U=or#E zuf_JD3c-4CwZF+BBBdBjBxf|ez$K@7Ce?q7u|`&*GuuX=e|6{M7mHRKIO7&C_TqthWXFkMhV2w=q()SId%P`Ge2Doqn8DKDC1-?wv{z%_?Zu z+qiaMQr1+(3WoyWm`~9kE38c5X&jZOI)(V2HCl|Gp`g6gx2+dd=$lbw;bu08XF^I} z-MiAMNR8XnLg599_50^ieKL!GvNR;@;!gzFjFZ zgKdAVN*fpHj#stx_swJ4WHFA8KI%v2mJCM0L1M=A7P@DaGUHH9d&=ohUWf`Mo;dKT z#|QP0`?`3;vb5I2ZULjcF&y$*I|au?`w9H*#sYTp&oEUp9Z~WzBtIIVNqNlnodA+% z*=Cy7D_C^x)6%`O0Vu*7>^kMU>zykAwilA2zkmNu#j)@+9?BT6N!FL#2L>9On7r#} z$@_v4CT|f+Fw`byh4o?E+3-HhTv;}cryR8tE1fuEMb%iWvsIV zj@C1Rk*O$JrHXTUYE=P&M37{zBD_*=W}tyr&tYh)uOpL(8gBQ6ityYBZd4;EWen7vzbd^bBISsSmnJGyq}T?@9G zBt3_Q$G6+ySecL!Iu8#I%gX`gL?-cA*H7?L=hRpV=8?o4ai%9{{>=>ZbyszR^BtZH zS!3DV^S^YVo3WHArybp%b?~J8KZ%fp1goz;oCN>K9%9IT5Vq5HR8uP}41gt3e}#Qa zC^K;Fv7)7>-Wkj3%~$G7aBF-|Ddh#(DG|TEi6%${DJ;w>LI%ciK9`i#Io<}kOy=}T zKN9W%13f6&xOe(|?6PwAK)FRmx|ppUo0sc0(#x8s1qug$orthI_LFlpt+^_12n(M% zE=pO1pZ~YVym#$3`rP(Oh}7%Q``fpwoEmxIve=rgf>!%$(r3>WhbE6lmoJybDUbhB zZ*Ol)NJzZehqd^wT>olv+aNGLE#B- zalP~;_E5BF#GRkl;!Yz*1MKmKA2$jxnkC|RHt(ALX*I?E-v)!kj5IjtHljf>No11yKtjt0ezm1(-tlKn|Mfpq2O~9cSsA7asl@KRJQ9UR zL~Qx!;Y>5Lv&}-pT&mN}TpOn{$(x2|X78LA8B3=<_A@9|sLsyLhQ`LWygy2<%TScM z*S7>w>Q|LJ&p7DmdHr(})E1DR+)QWvL`51qG&BT2*3=|_PwU7o)iN{VnRd%yeYS}6 zrMj92ltcrJO{<44Z(O)npUS8F@sW*%uEE*d-Q7Qb{@kdjPph)2hVtiKOlbByY~LRm9(GRxT8B5b%ADG$KS=h^ zE+%f;wO}}zRgPa+pb&-1p7(QUaNpDbfk3Fe+{!wb+_VxWBhYu-%%U#hKK$(FV*AXh z;b%LXjE9FuSg^zc8wR7-VqL(Gu+id>{LQ|eLE-TJLPFQGdwBBXYIu&NG$|&soQ64A zm79i><{h%Bw6rvKp-tAIjxj~?jQAnQ-#}Yx;hN&D!zaSQ?5iPod*O+NJ$ynnZ@kGk#xzl#r5?xeCwO=h2pk~+?&{7 zd2$tRmvDcD=QTh6`Z>4RjNts&>7hXA~ z>$kbNAoaI*c!Y+QmYP;pOq+u#zZkmi=vO;;T~@*_?ar{+@|p?tB2k~wpRfMeKqxT9xEw*dME&BZ@*3`A)Mf!yO$Rd+>uLwNtOu=7v=J#R0=Gx zb?CKucz%}f*PdeH{}~Q+^h|vMh9X%Ld zfQD7bdv0Ub1%lTZTUD1d!&c=2Dx^dd+70Z5;7p|`*(W5WaS7kV7}V)1`~=1nga@^uvo+r^;BnedJ)zvF z37+B0neO*5K60;`x6{vS@{NG~Ggg*Zzq2^ARs!F+5$z+=En^F-uVNH+sg|LIDY9Q(&1@3f5+0^rIYj;-KrGgK(>I>OaGJjk^Wg#QnVFY@LdonK^ z7r>9Q{qNEuJu4!QV2;0P}WocY*)^gmCa?#{A(=@E((iIFivmrqxNO!=vR;kCf0 literal 0 HcmV?d00001 diff --git a/R-package/pkgdown/favicon/apple-touch-icon.png b/R-package/pkgdown/favicon/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..07f55fa6ca35161e778760785707765da1726222 GIT binary patch literal 14484 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 literal 0 HcmV?d00001 diff --git a/R-package/recreate-configure.sh b/R-package/recreate-configure.sh new file mode 100755 index 0000000..5fcda5b --- /dev/null +++ b/R-package/recreate-configure.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +# recreates 'configure' from 'configure.ac' +# this script should run on Ubuntu 22.04 +AUTOCONF_VERSION=$(cat R-package/AUTOCONF_UBUNTU_VERSION) + +# R packages cannot have versions like 3.0.0rc1, but +# 3.0.0-1 is acceptable +LGB_VERSION=$(sed "s/rc/-/g" < VERSION.txt) + +# this script changes configure.ac. Copying to a temporary file +# so changes to configure.ac don't get committed in git +TMP_CONFIGURE_AC=".configure.ac" + +echo "Creating 'configure' script with Autoconf ${AUTOCONF_VERSION}" + +apt update +apt-get install \ + --no-install-recommends \ + -y \ + autoconf="${AUTOCONF_VERSION}" + +cd R-package + +cp configure.ac ${TMP_CONFIGURE_AC} +sed -i.bak -e "s/~~VERSION~~/${LGB_VERSION}/" ${TMP_CONFIGURE_AC} + +autoconf \ + --output configure \ + ${TMP_CONFIGURE_AC} \ + || exit 1 + +rm ${TMP_CONFIGURE_AC} + +rm -r autom4te.cache || echo "no autoconf cache found" + +echo "done creating 'configure' script" diff --git a/R-package/src/Makevars.in b/R-package/src/Makevars.in new file mode 100644 index 0000000..ec2c067 --- /dev/null +++ b/R-package/src/Makevars.in @@ -0,0 +1,59 @@ +CXX_STD = CXX17 + +PKGROOT=. + +LGB_CPPFLAGS = \ + @LGB_CPPFLAGS@ \ + -DUSE_SOCKET \ + -DLGB_R_BUILD + +PKG_CPPFLAGS = \ + -I$(PKGROOT)/include \ + $(LGB_CPPFLAGS) + +PKG_CXXFLAGS = \ + @OPENMP_CXXFLAGS@ \ + -pthread + +PKG_LIBS = \ + @OPENMP_CXXFLAGS@ \ + @OPENMP_LIB@ \ + -pthread + +OBJECTS = \ + boosting/boosting.o \ + boosting/gbdt.o \ + boosting/gbdt_model_text.o \ + boosting/gbdt_prediction.o \ + boosting/prediction_early_stop.o \ + boosting/sample_strategy.o \ + io/bin.o \ + io/config.o \ + io/config_auto.o \ + io/dataset.o \ + io/dataset_loader.o \ + io/file_io.o \ + io/json11.o \ + io/metadata.o \ + io/parser.o \ + io/train_share_states.o \ + io/tree.o \ + metric/dcg_calculator.o \ + metric/metric.o \ + objective/objective_function.o \ + network/linker_topo.o \ + network/linkers_mpi.o \ + network/linkers_socket.o \ + network/network.o \ + treelearner/data_parallel_tree_learner.o \ + treelearner/feature_histogram.o \ + treelearner/feature_parallel_tree_learner.o \ + treelearner/gpu_tree_learner.o \ + treelearner/gradient_discretizer.o \ + treelearner/linear_tree_learner.o \ + treelearner/serial_tree_learner.o \ + treelearner/tree_learner.o \ + treelearner/voting_parallel_tree_learner.o \ + utils/openmp_wrapper.o \ + c_api.o \ + lightgbm_R.o diff --git a/R-package/src/Makevars.win.in b/R-package/src/Makevars.win.in new file mode 100644 index 0000000..efe7796 --- /dev/null +++ b/R-package/src/Makevars.win.in @@ -0,0 +1,60 @@ +CXX_STD = CXX17 + +PKGROOT=. + +LGB_CPPFLAGS = \ + @LGB_CPPFLAGS@ \ + -DUSE_SOCKET \ + -DLGB_R_BUILD + +PKG_CPPFLAGS = \ + -I$(PKGROOT)/include \ + $(LGB_CPPFLAGS) + +PKG_CXXFLAGS = \ + ${SHLIB_OPENMP_CXXFLAGS} \ + ${SHLIB_PTHREAD_FLAGS} + +PKG_LIBS = \ + ${SHLIB_OPENMP_CXXFLAGS} \ + ${SHLIB_PTHREAD_FLAGS} \ + -lws2_32 \ + -liphlpapi + +OBJECTS = \ + boosting/boosting.o \ + boosting/gbdt.o \ + boosting/gbdt_model_text.o \ + boosting/gbdt_prediction.o \ + boosting/prediction_early_stop.o \ + boosting/sample_strategy.o \ + io/bin.o \ + io/config.o \ + io/config_auto.o \ + io/dataset.o \ + io/dataset_loader.o \ + io/file_io.o \ + io/json11.o \ + io/metadata.o \ + io/parser.o \ + io/train_share_states.o \ + io/tree.o \ + metric/dcg_calculator.o \ + metric/metric.o \ + objective/objective_function.o \ + network/linker_topo.o \ + network/linkers_mpi.o \ + network/linkers_socket.o \ + network/network.o \ + treelearner/data_parallel_tree_learner.o \ + treelearner/feature_histogram.o \ + treelearner/feature_parallel_tree_learner.o \ + treelearner/gpu_tree_learner.o \ + treelearner/gradient_discretizer.o \ + treelearner/linear_tree_learner.o \ + treelearner/serial_tree_learner.o \ + treelearner/tree_learner.o \ + treelearner/voting_parallel_tree_learner.o \ + utils/openmp_wrapper.o \ + c_api.o \ + lightgbm_R.o diff --git a/R-package/src/install.libs.R b/R-package/src/install.libs.R new file mode 100644 index 0000000..1bced56 --- /dev/null +++ b/R-package/src/install.libs.R @@ -0,0 +1,242 @@ +# User options +use_gpu <- FALSE +make_args_from_build_script <- character(0L) + +# For Windows, the package will be built with Visual Studio +# unless you set one of these to TRUE +use_mingw <- FALSE +use_msys2 <- FALSE + +if (use_mingw && use_msys2) { + stop("Cannot use both MinGW and MSYS2. Please choose only one.") +} + +if (.Machine$sizeof.pointer != 8L) { + stop("LightGBM only supports 64-bit R, please check the version of R and Rtools.") +} + +# Get some paths +source_dir <- file.path(R_PACKAGE_SOURCE, "src", fsep = "/") +build_dir <- file.path(source_dir, "build", fsep = "/") +inst_dir <- file.path(R_PACKAGE_SOURCE, "inst", fsep = "/") + +# system() will not raise an R exception if the process called +# fails. Wrapping it here to get that behavior. +# +# system() introduces a lot of overhead, at least on Windows, +# so trying processx if it is available +.run_shell_command <- function(cmd, args, strict = TRUE) { + on_windows <- .Platform$OS.type == "windows" + has_processx <- suppressMessages({ + suppressWarnings({ + require("processx") # nolint: undesirable_function, unused_import. + }) + }) + if (has_processx && on_windows) { + result <- processx::run( + command = cmd + , args = args + , windows_verbatim_args = TRUE + , error_on_status = FALSE + , echo = TRUE + ) + exit_code <- result$status + } else { + if (on_windows) { + message(paste0( + "Using system() to run shell commands. Installing " + , "'processx' with install.packages('processx') might " + , "make this faster." + )) + } + cmd <- paste0(cmd, " ", paste(args, collapse = " ")) + exit_code <- system(cmd) + } + + if (exit_code != 0L && isTRUE(strict)) { + stop(paste0("Command failed with exit code: ", exit_code)) + } + return(invisible(exit_code)) +} + +# try to generate Visual Studio build files +.generate_vs_makefiles <- function(cmake_args) { + vs_versions <- c( + "Visual Studio 17 2022" + , "Visual Studio 16 2019" + , "Visual Studio 15 2017" + ) + working_vs_version <- NULL + for (vs_version in vs_versions) { + message(sprintf("Trying '%s'", vs_version)) + # if the build directory is not empty, clean it + if (file.exists("CMakeCache.txt")) { + file.remove("CMakeCache.txt") + } + vs_cmake_args <- c( + cmake_args + , "-G" + , shQuote(vs_version) + , "-A" + , "x64" + ) + exit_code <- .run_shell_command("cmake", c(vs_cmake_args, ".."), strict = FALSE) + if (exit_code == 0L) { + message(sprintf("Successfully created build files for '%s'", vs_version)) + return(invisible(TRUE)) + } + + } + return(invisible(FALSE)) +} + +# Move in CMakeLists.txt +write_succeeded <- file.copy( + file.path(inst_dir, "bin", "CMakeLists.txt") + , "CMakeLists.txt" + , overwrite = TRUE +) +if (!write_succeeded) { + stop("Copying CMakeLists.txt failed") +} + +# Prepare building package +dir.create( + build_dir + , recursive = TRUE + , showWarnings = FALSE +) +setwd(build_dir) + +use_visual_studio <- !(use_mingw || use_msys2) + +# If using MSVC to build, pull in the script used +# to create R.def from R.dll +if (WINDOWS && use_visual_studio) { + write_succeeded <- file.copy( + file.path(inst_dir, "make-r-def.R") + , file.path(build_dir, "make-r-def.R") + , overwrite = TRUE + ) + if (!write_succeeded) { + stop("Copying make-r-def.R failed") + } +} + +# Prepare installation steps +cmake_args <- c( + "-D__BUILD_FOR_R=ON" + # pass in R version, to help FindLibR find the R library + , sprintf("-DCMAKE_R_VERSION='%s.%s'", R.Version()[["major"]], R.Version()[["minor"]]) + # ensure CMake build respects how R is configured (`R CMD config SHLIB_EXT`) + , sprintf("-DCMAKE_SHARED_LIBRARY_SUFFIX_CXX='%s'", SHLIB_EXT) +) +build_cmd <- "make" +build_args <- c("_lightgbm", make_args_from_build_script) +lib_folder <- file.path(source_dir, fsep = "/") + +# add in command-line arguments +# NOTE: build_r.R replaces the line below +command_line_args <- NULL +cmake_args <- c(cmake_args, command_line_args) + +WINDOWS_BUILD_TOOLS <- list( + "MinGW" = c( + build_tool = "mingw32-make.exe" + , makefile_generator = "MinGW Makefiles" + ) + , "MSYS2" = c( + build_tool = "make.exe" + , makefile_generator = "MSYS Makefiles" + ) +) + +if (use_mingw) { + windows_toolchain <- "MinGW" +} else if (use_msys2) { + windows_toolchain <- "MSYS2" +} else { + # Rtools 4.0 moved from MinGW to MSYS toolchain. If user tries + # Visual Studio install but that fails, fall back to the toolchain + # supported in Rtools + windows_toolchain <- "MSYS2" +} +windows_build_tool <- WINDOWS_BUILD_TOOLS[[windows_toolchain]][["build_tool"]] +windows_makefile_generator <- WINDOWS_BUILD_TOOLS[[windows_toolchain]][["makefile_generator"]] + +if (use_gpu) { + cmake_args <- c(cmake_args, "-DUSE_GPU=ON") +} + +# the checks below might already run `cmake -G`. If they do, set this flag +# to TRUE to avoid re-running it later +makefiles_already_generated <- FALSE + +# Check if Windows installation (for gcc vs Visual Studio) +if (WINDOWS) { + if (!use_visual_studio) { + message(sprintf("Trying to build with %s", windows_toolchain)) + # Must build twice for Windows due sh.exe in Rtools + cmake_args <- c(cmake_args, "-G", shQuote(windows_makefile_generator)) + .run_shell_command("cmake", c(cmake_args, ".."), strict = FALSE) + build_cmd <- windows_build_tool + build_args <- c("_lightgbm", make_args_from_build_script) + } else { + visual_studio_succeeded <- .generate_vs_makefiles(cmake_args) + if (!isTRUE(visual_studio_succeeded)) { + warning(sprintf("Building with Visual Studio failed. Attempting with %s", windows_toolchain)) + # Must build twice for Windows due sh.exe in Rtools + cmake_args <- c(cmake_args, "-G", shQuote(windows_makefile_generator)) + .run_shell_command("cmake", c(cmake_args, ".."), strict = FALSE) + build_cmd <- windows_build_tool + build_args <- c("_lightgbm", make_args_from_build_script) + } else { + build_cmd <- "cmake" + build_args <- c("--build", ".", "--target", "_lightgbm", "--config", "Release") + lib_folder <- file.path(source_dir, "Release", fsep = "/") + makefiles_already_generated <- TRUE + } + } +} else { + .run_shell_command("cmake", c(cmake_args, "..")) + makefiles_already_generated <- TRUE +} + +# generate build files +if (!makefiles_already_generated) { + .run_shell_command("cmake", c(cmake_args, "..")) +} + +# build the library +message(paste0("Building lightgbm", SHLIB_EXT)) +.run_shell_command(build_cmd, build_args) +src <- file.path(lib_folder, paste0("lightgbm", SHLIB_EXT), fsep = "/") + +# Packages with install.libs.R need to copy some artifacts into the +# expected places in the package structure. +# see https://cran.r-project.org/doc/manuals/r-devel/R-exts.html#Package-subdirectories, +# especially the paragraph on install.libs.R +dest <- file.path(R_PACKAGE_DIR, paste0("libs", R_ARCH), fsep = "/") +dir.create(dest, recursive = TRUE, showWarnings = FALSE) +if (file.exists(src)) { + message(paste0("Found library file: ", src, " to move to ", dest)) + file.copy(src, dest, overwrite = TRUE) + + symbols_file <- file.path(source_dir, "symbols.rds") + if (file.exists(symbols_file)) { + file.copy(symbols_file, dest, overwrite = TRUE) + } + +} else { + stop(paste0("Cannot find lightgbm", SHLIB_EXT)) +} + +# clean up the "build" directory +if (dir.exists(build_dir)) { + message("Removing 'build/' directory") + unlink( + x = build_dir + , recursive = TRUE + , force = TRUE + ) +} diff --git a/R-package/src/lightgbm_R.cpp b/R-package/src/lightgbm_R.cpp new file mode 100644 index 0000000..1a18f4a --- /dev/null +++ b/R-package/src/lightgbm_R.cpp @@ -0,0 +1,1513 @@ +/*! + * Copyright (c) 2017-2026 Microsoft Corporation. All rights reserved. + * Copyright (c) 2017-2026 The LightGBM developers. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + */ + +#include "lightgbm_R.h" + +#include +#include +#include +#include + +#include +#include + +#ifndef R_NO_REMAP +#define R_NO_REMAP +#endif + +#ifndef R_USE_C99_IN_CXX +#define R_USE_C99_IN_CXX +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +R_altrep_class_t lgb_altrepped_char_vec; +R_altrep_class_t lgb_altrepped_int_arr; +R_altrep_class_t lgb_altrepped_dbl_arr; + +template +void delete_cpp_array(SEXP R_ptr) { + T *ptr_to_cpp_obj = static_cast(R_ExternalPtrAddr(R_ptr)); + delete[] ptr_to_cpp_obj; + R_ClearExternalPtr(R_ptr); +} + +void delete_cpp_char_vec(SEXP R_ptr) { + std::vector *ptr_to_cpp_obj = static_cast*>(R_ExternalPtrAddr(R_ptr)); + delete ptr_to_cpp_obj; + R_ClearExternalPtr(R_ptr); +} + +// Note: MSVC has issues with Altrep classes, so they are disabled for it. +// See: https://github.com/lightgbm-org/LightGBM/pull/6213#issuecomment-2111025768 +#ifdef _MSC_VER +# define LGB_NO_ALTREP +#endif + +#ifndef LGB_NO_ALTREP +SEXP make_altrepped_raw_vec(void *void_ptr) { + std::unique_ptr> *ptr_to_cpp_vec = static_cast>*>(void_ptr); + SEXP R_ptr = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + SEXP R_raw = Rf_protect(R_new_altrep(lgb_altrepped_char_vec, R_NilValue, R_NilValue)); + + R_SetExternalPtrAddr(R_ptr, ptr_to_cpp_vec->get()); + R_RegisterCFinalizerEx(R_ptr, delete_cpp_char_vec, TRUE); + ptr_to_cpp_vec->release(); + + R_set_altrep_data1(R_raw, R_ptr); + Rf_unprotect(2); + return R_raw; +} +#else +SEXP make_r_raw_vec(void *void_ptr) { + std::unique_ptr> *ptr_to_cpp_vec = static_cast>*>(void_ptr); + R_xlen_t len = ptr_to_cpp_vec->get()->size(); + SEXP out = Rf_protect(Rf_allocVector(RAWSXP, len)); + std::copy(ptr_to_cpp_vec->get()->begin(), ptr_to_cpp_vec->get()->end(), reinterpret_cast(RAW(out))); + Rf_unprotect(1); + return out; +} +#define make_altrepped_raw_vec make_r_raw_vec +#endif + +std::vector* get_ptr_from_altrepped_raw(SEXP R_raw) { + return static_cast*>(R_ExternalPtrAddr(R_altrep_data1(R_raw))); +} + +R_xlen_t get_altrepped_raw_len(SEXP R_raw) { + return get_ptr_from_altrepped_raw(R_raw)->size(); +} + +const void* get_altrepped_raw_dataptr_or_null(SEXP R_raw) { + return get_ptr_from_altrepped_raw(R_raw)->data(); +} + +void* get_altrepped_raw_dataptr(SEXP R_raw, Rboolean writable) { + return get_ptr_from_altrepped_raw(R_raw)->data(); +} + +#ifndef LGB_NO_ALTREP +template +R_altrep_class_t get_altrep_class_for_type() { + if (std::is_same::value) { + return lgb_altrepped_dbl_arr; + } else { + return lgb_altrepped_int_arr; + } +} +#else +template +SEXPTYPE get_sexptype_class_for_type() { + if (std::is_same::value) { + return REALSXP; + } else { + return INTSXP; + } +} + +template +T* get_r_vec_ptr(SEXP x) { + if (std::is_same::value) { + return static_cast(static_cast(REAL(x))); + } else { + return static_cast(static_cast(INTEGER(x))); + } +} +#endif + +template +struct arr_and_len { + T *arr; + int64_t len; +}; + +#ifndef LGB_NO_ALTREP +template +SEXP make_altrepped_vec_from_arr(void *void_ptr) { + T *arr = static_cast*>(void_ptr)->arr; + uint64_t len = static_cast*>(void_ptr)->len; + SEXP R_ptr = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + SEXP R_len = Rf_protect(Rf_allocVector(REALSXP, 1)); + SEXP R_vec = Rf_protect(R_new_altrep(get_altrep_class_for_type(), R_NilValue, R_NilValue)); + + REAL(R_len)[0] = static_cast(len); + R_SetExternalPtrAddr(R_ptr, arr); + R_RegisterCFinalizerEx(R_ptr, delete_cpp_array, TRUE); + + R_set_altrep_data1(R_vec, R_ptr); + R_set_altrep_data2(R_vec, R_len); + Rf_unprotect(3); + return R_vec; +} +#else +template +SEXP make_R_vec_from_arr(void *void_ptr) { + T *arr = static_cast*>(void_ptr)->arr; + uint64_t len = static_cast*>(void_ptr)->len; + SEXP out = Rf_protect(Rf_allocVector(get_sexptype_class_for_type(), len)); + std::copy(arr, arr + len, get_r_vec_ptr(out)); + Rf_unprotect(1); + return out; +} +#define make_altrepped_vec_from_arr make_R_vec_from_arr +#endif + +R_xlen_t get_altrepped_vec_len(SEXP R_vec) { + return static_cast(Rf_asReal(R_altrep_data2(R_vec))); +} + +const void* get_altrepped_vec_dataptr_or_null(SEXP R_vec) { + return R_ExternalPtrAddr(R_altrep_data1(R_vec)); +} + +void* get_altrepped_vec_dataptr(SEXP R_vec, Rboolean writable) { + return R_ExternalPtrAddr(R_altrep_data1(R_vec)); +} + +#define COL_MAJOR (0) + +#define MAX_LENGTH_ERR_MSG 1024 +char R_errmsg_buffer[MAX_LENGTH_ERR_MSG]; +struct LGBM_R_ErrorClass { SEXP cont_token; }; +void LGBM_R_save_exception_msg(const std::exception &err); +void LGBM_R_save_exception_msg(const std::string &err); + +#define R_API_BEGIN() \ + try { +#define R_API_END() } \ + catch(LGBM_R_ErrorClass &cont) { R_ContinueUnwind(cont.cont_token); } \ + catch(std::exception& ex) { LGBM_R_save_exception_msg(ex); } \ + catch(std::string& ex) { LGBM_R_save_exception_msg(ex); } \ + catch(...) { Rf_error("unknown exception"); } \ + Rf_error("%s", R_errmsg_buffer); \ + return R_NilValue; /* <- won't be reached */ + +#define CHECK_CALL(x) \ + if ((x) != 0) { \ + throw std::runtime_error(LGBM_GetLastError()); \ + } + +// These are helper functions to allow doing a stack unwind +// after an R allocation error, which would trigger a long jump. +void LGBM_R_save_exception_msg(const std::exception &err) { + std::snprintf(R_errmsg_buffer, MAX_LENGTH_ERR_MSG, "%s\n", err.what()); +} + +void LGBM_R_save_exception_msg(const std::string &err) { + std::snprintf(R_errmsg_buffer, MAX_LENGTH_ERR_MSG, "%s\n", err.c_str()); +} + +SEXP wrapped_R_string(void *len) { + return Rf_allocVector(STRSXP, *(reinterpret_cast(len))); +} + +SEXP wrapped_R_raw(void *len) { + return Rf_allocVector(RAWSXP, *(reinterpret_cast(len))); +} + +SEXP wrapped_R_int(void *len) { + return Rf_allocVector(INTSXP, *(reinterpret_cast(len))); +} + +SEXP wrapped_R_real(void *len) { + return Rf_allocVector(REALSXP, *(reinterpret_cast(len))); +} + +SEXP wrapped_Rf_mkChar(void *txt) { + return Rf_mkChar(reinterpret_cast(txt)); +} + +void throw_R_memerr(void *ptr_cont_token, Rboolean jump) { + if (jump) { + LGBM_R_ErrorClass err{*(reinterpret_cast(ptr_cont_token))}; + throw err; + } +} + +SEXP safe_R_string(R_xlen_t len, SEXP *cont_token) { + return R_UnwindProtect(wrapped_R_string, reinterpret_cast(&len), throw_R_memerr, cont_token, *cont_token); +} + +SEXP safe_R_raw(R_xlen_t len, SEXP *cont_token) { + return R_UnwindProtect(wrapped_R_raw, reinterpret_cast(&len), throw_R_memerr, cont_token, *cont_token); +} + +SEXP safe_R_int(R_xlen_t len, SEXP *cont_token) { + return R_UnwindProtect(wrapped_R_int, reinterpret_cast(&len), throw_R_memerr, cont_token, *cont_token); +} + +SEXP safe_R_real(R_xlen_t len, SEXP *cont_token) { + return R_UnwindProtect(wrapped_R_real, reinterpret_cast(&len), throw_R_memerr, cont_token, *cont_token); +} + +SEXP safe_R_mkChar(char *txt, SEXP *cont_token) { + return R_UnwindProtect(wrapped_Rf_mkChar, reinterpret_cast(txt), throw_R_memerr, cont_token, *cont_token); +} + +using LightGBM::Common::Split; +using LightGBM::Log; + +SEXP LGBM_HandleIsNull_R(SEXP handle) { + return Rf_ScalarLogical(R_ExternalPtrAddr(handle) == NULL); +} + +void _DatasetFinalizer(SEXP handle) { + LGBM_DatasetFree_R(handle); +} + +SEXP LGBM_NullBoosterHandleError_R() { + Rf_error( + "Attempting to use a Booster which no longer exists and/or cannot be restored. " + "This can happen if the Booster's finalizer was called " + "or if this Booster was saved through saveRDS() using 'serializable=FALSE'."); + return R_NilValue; +} + +void _AssertBoosterHandleNotNull(SEXP handle) { + if (Rf_isNull(handle) || !R_ExternalPtrAddr(handle)) { + LGBM_NullBoosterHandleError_R(); + } +} + +void _AssertDatasetHandleNotNull(SEXP handle) { + if (Rf_isNull(handle) || !R_ExternalPtrAddr(handle)) { + Rf_error( + "Attempting to use a Dataset which no longer exists. " + "This can happen if the Dataset's finalizer was called or if this Dataset was saved with saveRDS(). " + "To avoid this error in the future, use lgb.Dataset.save() or Dataset$save_binary() to save lightgbm Datasets."); + } +} + +SEXP LGBM_DatasetCreateFromFile_R(SEXP filename, + SEXP parameters, + SEXP reference) { + R_API_BEGIN(); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + DatasetHandle handle = nullptr; + DatasetHandle ref = nullptr; + if (!Rf_isNull(reference)) { + ref = R_ExternalPtrAddr(reference); + } + const char* filename_ptr = CHAR(Rf_protect(Rf_asChar(filename))); + const char* parameters_ptr = CHAR(Rf_protect(Rf_asChar(parameters))); + CHECK_CALL(LGBM_DatasetCreateFromFile(filename_ptr, parameters_ptr, ref, &handle)); + R_SetExternalPtrAddr(ret, handle); + R_RegisterCFinalizerEx(ret, _DatasetFinalizer, TRUE); + Rf_unprotect(3); + return ret; + R_API_END(); +} + +SEXP LGBM_DatasetCreateFromCSC_R(SEXP indptr, + SEXP indices, + SEXP data, + SEXP num_indptr, + SEXP nelem, + SEXP num_row, + SEXP parameters, + SEXP reference) { + R_API_BEGIN(); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + const int* p_indptr = INTEGER(indptr); + const int* p_indices = INTEGER(indices); + const double* p_data = REAL(data); + int64_t nindptr = static_cast(Rf_asInteger(num_indptr)); + int64_t ndata = static_cast(Rf_asInteger(nelem)); + int64_t nrow = static_cast(Rf_asInteger(num_row)); + const char* parameters_ptr = CHAR(Rf_protect(Rf_asChar(parameters))); + DatasetHandle handle = nullptr; + DatasetHandle ref = nullptr; + if (!Rf_isNull(reference)) { + ref = R_ExternalPtrAddr(reference); + } + CHECK_CALL(LGBM_DatasetCreateFromCSC(p_indptr, C_API_DTYPE_INT32, p_indices, + p_data, C_API_DTYPE_FLOAT64, nindptr, ndata, + nrow, parameters_ptr, ref, &handle)); + R_SetExternalPtrAddr(ret, handle); + R_RegisterCFinalizerEx(ret, _DatasetFinalizer, TRUE); + Rf_unprotect(2); + return ret; + R_API_END(); +} + +SEXP LGBM_DatasetCreateFromMat_R(SEXP data, + SEXP num_row, + SEXP num_col, + SEXP parameters, + SEXP reference) { + R_API_BEGIN(); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + int32_t nrow = static_cast(Rf_asInteger(num_row)); + int32_t ncol = static_cast(Rf_asInteger(num_col)); + double* p_mat = REAL(data); + const char* parameters_ptr = CHAR(Rf_protect(Rf_asChar(parameters))); + DatasetHandle handle = nullptr; + DatasetHandle ref = nullptr; + if (!Rf_isNull(reference)) { + ref = R_ExternalPtrAddr(reference); + } + CHECK_CALL(LGBM_DatasetCreateFromMat(p_mat, C_API_DTYPE_FLOAT64, nrow, ncol, COL_MAJOR, + parameters_ptr, ref, &handle)); + R_SetExternalPtrAddr(ret, handle); + R_RegisterCFinalizerEx(ret, _DatasetFinalizer, TRUE); + Rf_unprotect(2); + return ret; + R_API_END(); +} + +SEXP LGBM_DatasetGetSubset_R(SEXP handle, + SEXP used_row_indices, + SEXP len_used_row_indices, + SEXP parameters) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + int32_t len = static_cast(Rf_asInteger(len_used_row_indices)); + std::unique_ptr idxvec(new int32_t[len]); + // convert from one-based to zero-based index + const int *used_row_indices_ = INTEGER(used_row_indices); +#ifndef _MSC_VER +#pragma omp simd +#endif + for (int32_t i = 0; i < len; ++i) { + idxvec[i] = static_cast(used_row_indices_[i] - 1); + } + const char* parameters_ptr = CHAR(Rf_protect(Rf_asChar(parameters))); + DatasetHandle res = nullptr; + CHECK_CALL(LGBM_DatasetGetSubset(R_ExternalPtrAddr(handle), + idxvec.get(), len, parameters_ptr, + &res)); + R_SetExternalPtrAddr(ret, res); + R_RegisterCFinalizerEx(ret, _DatasetFinalizer, TRUE); + Rf_unprotect(2); + return ret; + R_API_END(); +} + +SEXP LGBM_DatasetSetFeatureNames_R(SEXP handle, + SEXP feature_names) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + auto vec_names = Split(CHAR(Rf_protect(Rf_asChar(feature_names))), '\t'); + int len = static_cast(vec_names.size()); + std::unique_ptr vec_sptr(new const char*[len]); + for (int i = 0; i < len; ++i) { + vec_sptr[i] = vec_names[i].c_str(); + } + CHECK_CALL(LGBM_DatasetSetFeatureNames(R_ExternalPtrAddr(handle), + vec_sptr.get(), len)); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetGetFeatureNames_R(SEXP handle) { + SEXP cont_token = Rf_protect(R_MakeUnwindCont()); + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + SEXP feature_names; + int len = 0; + CHECK_CALL(LGBM_DatasetGetNumFeature(R_ExternalPtrAddr(handle), &len)); + const size_t reserved_string_size = 256; + std::vector> names(len); + std::vector ptr_names(len); + for (int i = 0; i < len; ++i) { + names[i].resize(reserved_string_size); + ptr_names[i] = names[i].data(); + } + int out_len; + size_t required_string_size; + CHECK_CALL( + LGBM_DatasetGetFeatureNames( + R_ExternalPtrAddr(handle), + len, &out_len, + reserved_string_size, &required_string_size, + ptr_names.data())); + // if any feature names were larger than allocated size, + // allow for a larger size and try again + if (required_string_size > reserved_string_size) { + for (int i = 0; i < len; ++i) { + names[i].resize(required_string_size); + ptr_names[i] = names[i].data(); + } + CHECK_CALL( + LGBM_DatasetGetFeatureNames( + R_ExternalPtrAddr(handle), + len, + &out_len, + required_string_size, + &required_string_size, + ptr_names.data())); + } + CHECK_EQ(len, out_len); + feature_names = Rf_protect(safe_R_string(static_cast(len), &cont_token)); + for (int i = 0; i < len; ++i) { + SET_STRING_ELT(feature_names, i, safe_R_mkChar(ptr_names[i], &cont_token)); + } + Rf_unprotect(2); + return feature_names; + R_API_END(); +} + +SEXP LGBM_DatasetSaveBinary_R(SEXP handle, + SEXP filename) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + const char* filename_ptr = CHAR(Rf_protect(Rf_asChar(filename))); + CHECK_CALL(LGBM_DatasetSaveBinary(R_ExternalPtrAddr(handle), + filename_ptr)); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetFree_R(SEXP handle) { + R_API_BEGIN(); + if (!Rf_isNull(handle) && R_ExternalPtrAddr(handle)) { + CHECK_CALL(LGBM_DatasetFree(R_ExternalPtrAddr(handle))); + R_ClearExternalPtr(handle); + } + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetSetField_R(SEXP handle, + SEXP field_name, + SEXP field_data, + SEXP num_element) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + int len = Rf_asInteger(num_element); + const char* name = CHAR(Rf_protect(Rf_asChar(field_name))); + if (!strcmp("group", name) || !strcmp("query", name)) { + CHECK_CALL(LGBM_DatasetSetField(R_ExternalPtrAddr(handle), name, INTEGER(field_data), len, C_API_DTYPE_INT32)); + } else if (!strcmp("init_score", name)) { + CHECK_CALL(LGBM_DatasetSetField(R_ExternalPtrAddr(handle), name, REAL(field_data), len, C_API_DTYPE_FLOAT64)); + } else { + std::unique_ptr vec(new float[len]); + std::copy(REAL(field_data), REAL(field_data) + len, vec.get()); + CHECK_CALL(LGBM_DatasetSetField(R_ExternalPtrAddr(handle), name, vec.get(), len, C_API_DTYPE_FLOAT32)); + } + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetGetField_R(SEXP handle, + SEXP field_name, + SEXP field_data) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + const char* name = CHAR(Rf_protect(Rf_asChar(field_name))); + int out_len = 0; + int out_type = 0; + const void* res; + CHECK_CALL(LGBM_DatasetGetField(R_ExternalPtrAddr(handle), name, &out_len, &res, &out_type)); + if (!strcmp("group", name) || !strcmp("query", name)) { + auto p_data = reinterpret_cast(res); + // convert from boundaries to size + int *field_data_ = INTEGER(field_data); +#ifndef _MSC_VER +#pragma omp simd +#endif + for (int i = 0; i < out_len - 1; ++i) { + field_data_[i] = p_data[i + 1] - p_data[i]; + } + } else if (!strcmp("init_score", name)) { + auto p_data = reinterpret_cast(res); + std::copy(p_data, p_data + out_len, REAL(field_data)); + } else { + auto p_data = reinterpret_cast(res); + std::copy(p_data, p_data + out_len, REAL(field_data)); + } + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetGetFieldSize_R(SEXP handle, + SEXP field_name, + SEXP out) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + const char* name = CHAR(Rf_protect(Rf_asChar(field_name))); + int out_len = 0; + int out_type = 0; + const void* res; + CHECK_CALL(LGBM_DatasetGetField(R_ExternalPtrAddr(handle), name, &out_len, &res, &out_type)); + if (!strcmp("group", name) || !strcmp("query", name)) { + out_len -= 1; + } + INTEGER(out)[0] = out_len; + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetUpdateParamChecking_R(SEXP old_params, + SEXP new_params) { + R_API_BEGIN(); + const char* old_params_ptr = CHAR(Rf_protect(Rf_asChar(old_params))); + const char* new_params_ptr = CHAR(Rf_protect(Rf_asChar(new_params))); + CHECK_CALL(LGBM_DatasetUpdateParamChecking(old_params_ptr, new_params_ptr)); + Rf_unprotect(2); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetGetNumData_R(SEXP handle, SEXP out) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + int nrow; + CHECK_CALL(LGBM_DatasetGetNumData(R_ExternalPtrAddr(handle), &nrow)); + INTEGER(out)[0] = nrow; + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetGetNumFeature_R(SEXP handle, + SEXP out) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + int nfeature; + CHECK_CALL(LGBM_DatasetGetNumFeature(R_ExternalPtrAddr(handle), &nfeature)); + INTEGER(out)[0] = nfeature; + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_DatasetGetFeatureNumBin_R(SEXP handle, SEXP feature_idx, SEXP out) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(handle); + int feature = Rf_asInteger(feature_idx); + int nbins; + CHECK_CALL(LGBM_DatasetGetFeatureNumBin(R_ExternalPtrAddr(handle), feature, &nbins)); + INTEGER(out)[0] = nbins; + return R_NilValue; + R_API_END(); +} + +// --- start Booster interfaces + +void _BoosterFinalizer(SEXP handle) { + LGBM_BoosterFree_R(handle); +} + +SEXP LGBM_BoosterFree_R(SEXP handle) { + R_API_BEGIN(); + if (!Rf_isNull(handle) && R_ExternalPtrAddr(handle)) { + CHECK_CALL(LGBM_BoosterFree(R_ExternalPtrAddr(handle))); + R_ClearExternalPtr(handle); + } + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterCreate_R(SEXP train_data, + SEXP parameters) { + R_API_BEGIN(); + _AssertDatasetHandleNotNull(train_data); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + const char* parameters_ptr = CHAR(Rf_protect(Rf_asChar(parameters))); + BoosterHandle handle = nullptr; + CHECK_CALL(LGBM_BoosterCreate(R_ExternalPtrAddr(train_data), parameters_ptr, &handle)); + R_SetExternalPtrAddr(ret, handle); + R_RegisterCFinalizerEx(ret, _BoosterFinalizer, TRUE); + Rf_unprotect(2); + return ret; + R_API_END(); +} + +SEXP LGBM_BoosterCreateFromModelfile_R(SEXP filename) { + R_API_BEGIN(); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + int out_num_iterations = 0; + const char* filename_ptr = CHAR(Rf_protect(Rf_asChar(filename))); + BoosterHandle handle = nullptr; + CHECK_CALL(LGBM_BoosterCreateFromModelfile(filename_ptr, &out_num_iterations, &handle)); + R_SetExternalPtrAddr(ret, handle); + R_RegisterCFinalizerEx(ret, _BoosterFinalizer, TRUE); + Rf_unprotect(2); + return ret; + R_API_END(); +} + +SEXP LGBM_BoosterLoadModelFromString_R(SEXP model_str) { + R_API_BEGIN(); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + SEXP temp = NULL; + int n_protected = 1; + int out_num_iterations = 0; + const char* model_str_ptr = nullptr; + switch (TYPEOF(model_str)) { + case RAWSXP: { + model_str_ptr = reinterpret_cast(RAW(model_str)); + break; + } + case CHARSXP: { + model_str_ptr = reinterpret_cast(CHAR(model_str)); + break; + } + case STRSXP: { + temp = Rf_protect(STRING_ELT(model_str, 0)); + n_protected++; + model_str_ptr = reinterpret_cast(CHAR(temp)); + } + } + BoosterHandle handle = nullptr; + CHECK_CALL(LGBM_BoosterLoadModelFromString(model_str_ptr, &out_num_iterations, &handle)); + R_SetExternalPtrAddr(ret, handle); + R_RegisterCFinalizerEx(ret, _BoosterFinalizer, TRUE); + Rf_unprotect(n_protected); + return ret; + R_API_END(); +} + +SEXP LGBM_BoosterMerge_R(SEXP handle, + SEXP other_handle) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + _AssertBoosterHandleNotNull(other_handle); + CHECK_CALL(LGBM_BoosterMerge(R_ExternalPtrAddr(handle), R_ExternalPtrAddr(other_handle))); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterAddValidData_R(SEXP handle, + SEXP valid_data) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + _AssertDatasetHandleNotNull(valid_data); + CHECK_CALL(LGBM_BoosterAddValidData(R_ExternalPtrAddr(handle), R_ExternalPtrAddr(valid_data))); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterResetTrainingData_R(SEXP handle, + SEXP train_data) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + _AssertDatasetHandleNotNull(train_data); + CHECK_CALL(LGBM_BoosterResetTrainingData(R_ExternalPtrAddr(handle), R_ExternalPtrAddr(train_data))); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterResetParameter_R(SEXP handle, + SEXP parameters) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + const char* parameters_ptr = CHAR(Rf_protect(Rf_asChar(parameters))); + CHECK_CALL(LGBM_BoosterResetParameter(R_ExternalPtrAddr(handle), parameters_ptr)); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterGetNumClasses_R(SEXP handle, + SEXP out) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int num_class; + CHECK_CALL(LGBM_BoosterGetNumClasses(R_ExternalPtrAddr(handle), &num_class)); + INTEGER(out)[0] = num_class; + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterGetNumFeature_R(SEXP handle) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int out = 0; + CHECK_CALL(LGBM_BoosterGetNumFeature(R_ExternalPtrAddr(handle), &out)); + return Rf_ScalarInteger(out); + R_API_END(); +} + +SEXP LGBM_BoosterUpdateOneIter_R(SEXP handle) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int produced_empty_tree = 0; + CHECK_CALL(LGBM_BoosterUpdateOneIter(R_ExternalPtrAddr(handle), &produced_empty_tree)); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterUpdateOneIterCustom_R(SEXP handle, + SEXP grad, + SEXP hess, + SEXP len) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int produced_empty_tree = 0; + int int_len = Rf_asInteger(len); + std::unique_ptr tgrad(new float[int_len]), thess(new float[int_len]); + std::copy(REAL(grad), REAL(grad) + int_len, tgrad.get()); + std::copy(REAL(hess), REAL(hess) + int_len, thess.get()); + CHECK_CALL(LGBM_BoosterUpdateOneIterCustom(R_ExternalPtrAddr(handle), tgrad.get(), thess.get(), + &produced_empty_tree)); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterRollbackOneIter_R(SEXP handle) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + CHECK_CALL(LGBM_BoosterRollbackOneIter(R_ExternalPtrAddr(handle))); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterGetCurrentIteration_R(SEXP handle, SEXP out) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int out_iteration; + CHECK_CALL(LGBM_BoosterGetCurrentIteration(R_ExternalPtrAddr(handle), &out_iteration)); + INTEGER(out)[0] = out_iteration; + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterNumModelPerIteration_R(SEXP handle, SEXP out) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int models_per_iter; + CHECK_CALL(LGBM_BoosterNumModelPerIteration(R_ExternalPtrAddr(handle), &models_per_iter)); + INTEGER(out)[0] = models_per_iter; + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterNumberOfTotalModel_R(SEXP handle, SEXP out) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int total_models; + CHECK_CALL(LGBM_BoosterNumberOfTotalModel(R_ExternalPtrAddr(handle), &total_models)); + INTEGER(out)[0] = total_models; + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterGetUpperBoundValue_R(SEXP handle, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + double* ptr_ret = REAL(out_result); + CHECK_CALL(LGBM_BoosterGetUpperBoundValue(R_ExternalPtrAddr(handle), ptr_ret)); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterGetLowerBoundValue_R(SEXP handle, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + double* ptr_ret = REAL(out_result); + CHECK_CALL(LGBM_BoosterGetLowerBoundValue(R_ExternalPtrAddr(handle), ptr_ret)); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterGetEvalNames_R(SEXP handle) { + SEXP cont_token = Rf_protect(R_MakeUnwindCont()); + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + SEXP eval_names; + int len; + CHECK_CALL(LGBM_BoosterGetEvalCounts(R_ExternalPtrAddr(handle), &len)); + const size_t reserved_string_size = 128; + std::vector> names(len); + std::vector ptr_names(len); + for (int i = 0; i < len; ++i) { + names[i].resize(reserved_string_size); + ptr_names[i] = names[i].data(); + } + + int out_len; + size_t required_string_size; + CHECK_CALL( + LGBM_BoosterGetEvalNames( + R_ExternalPtrAddr(handle), + len, &out_len, + reserved_string_size, &required_string_size, + ptr_names.data())); + // if any eval names were larger than allocated size, + // allow for a larger size and try again + if (required_string_size > reserved_string_size) { + for (int i = 0; i < len; ++i) { + names[i].resize(required_string_size); + ptr_names[i] = names[i].data(); + } + CHECK_CALL( + LGBM_BoosterGetEvalNames( + R_ExternalPtrAddr(handle), + len, + &out_len, + required_string_size, + &required_string_size, + ptr_names.data())); + } + CHECK_EQ(out_len, len); + eval_names = Rf_protect(safe_R_string(static_cast(len), &cont_token)); + for (int i = 0; i < len; ++i) { + SET_STRING_ELT(eval_names, i, safe_R_mkChar(ptr_names[i], &cont_token)); + } + Rf_unprotect(2); + return eval_names; + R_API_END(); +} + +SEXP LGBM_BoosterGetEval_R(SEXP handle, + SEXP data_idx, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int len; + CHECK_CALL(LGBM_BoosterGetEvalCounts(R_ExternalPtrAddr(handle), &len)); + double* ptr_ret = REAL(out_result); + int out_len; + CHECK_CALL(LGBM_BoosterGetEval(R_ExternalPtrAddr(handle), Rf_asInteger(data_idx), &out_len, ptr_ret)); + CHECK_EQ(out_len, len); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterGetNumPredict_R(SEXP handle, + SEXP data_idx, + SEXP out) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int64_t len; + CHECK_CALL(LGBM_BoosterGetNumPredict(R_ExternalPtrAddr(handle), Rf_asInteger(data_idx), &len)); + INTEGER(out)[0] = static_cast(len); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterGetPredict_R(SEXP handle, + SEXP data_idx, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + double* ptr_ret = REAL(out_result); + int64_t out_len; + CHECK_CALL(LGBM_BoosterGetPredict(R_ExternalPtrAddr(handle), Rf_asInteger(data_idx), &out_len, ptr_ret)); + return R_NilValue; + R_API_END(); +} + +int GetPredictType(SEXP is_rawscore, SEXP is_leafidx, SEXP is_predcontrib) { + int pred_type = C_API_PREDICT_NORMAL; + if (Rf_asInteger(is_rawscore)) { + pred_type = C_API_PREDICT_RAW_SCORE; + } + if (Rf_asInteger(is_leafidx)) { + pred_type = C_API_PREDICT_LEAF_INDEX; + } + if (Rf_asInteger(is_predcontrib)) { + pred_type = C_API_PREDICT_CONTRIB; + } + return pred_type; +} + +SEXP LGBM_BoosterPredictForFile_R(SEXP handle, + SEXP data_filename, + SEXP data_has_header, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP result_filename) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + const char* data_filename_ptr = CHAR(Rf_protect(Rf_asChar(data_filename))); + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + const char* result_filename_ptr = CHAR(Rf_protect(Rf_asChar(result_filename))); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + CHECK_CALL(LGBM_BoosterPredictForFile(R_ExternalPtrAddr(handle), data_filename_ptr, + Rf_asInteger(data_has_header), pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), parameter_ptr, + result_filename_ptr)); + Rf_unprotect(3); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterCalcNumPredict_R(SEXP handle, + SEXP num_row, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP out_len) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + int64_t len = 0; + CHECK_CALL(LGBM_BoosterCalcNumPredict(R_ExternalPtrAddr(handle), Rf_asInteger(num_row), + pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), &len)); + INTEGER(out_len)[0] = static_cast(len); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterPredictForCSC_R(SEXP handle, + SEXP indptr, + SEXP indices, + SEXP data, + SEXP num_indptr, + SEXP nelem, + SEXP num_row, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + const int* p_indptr = INTEGER(indptr); + const int32_t* p_indices = reinterpret_cast(INTEGER(indices)); + const double* p_data = REAL(data); + int64_t nindptr = static_cast(Rf_asInteger(num_indptr)); + int64_t ndata = static_cast(Rf_asInteger(nelem)); + int64_t nrow = static_cast(Rf_asInteger(num_row)); + double* ptr_ret = REAL(out_result); + int64_t out_len; + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + CHECK_CALL(LGBM_BoosterPredictForCSC(R_ExternalPtrAddr(handle), + p_indptr, C_API_DTYPE_INT32, p_indices, + p_data, C_API_DTYPE_FLOAT64, nindptr, ndata, + nrow, pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), parameter_ptr, &out_len, ptr_ret)); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterPredictForCSR_R(SEXP handle, + SEXP indptr, + SEXP indices, + SEXP data, + SEXP ncols, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + int64_t out_len; + CHECK_CALL(LGBM_BoosterPredictForCSR(R_ExternalPtrAddr(handle), + INTEGER(indptr), C_API_DTYPE_INT32, INTEGER(indices), + REAL(data), C_API_DTYPE_FLOAT64, + Rf_xlength(indptr), Rf_xlength(data), Rf_asInteger(ncols), + pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), + parameter_ptr, &out_len, REAL(out_result))); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterPredictForCSRSingleRow_R(SEXP handle, + SEXP indices, + SEXP data, + SEXP ncols, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + int nnz = static_cast(Rf_xlength(data)); + const int indptr[] = {0, nnz}; + int64_t out_len; + CHECK_CALL(LGBM_BoosterPredictForCSRSingleRow(R_ExternalPtrAddr(handle), + indptr, C_API_DTYPE_INT32, INTEGER(indices), + REAL(data), C_API_DTYPE_FLOAT64, + 2, nnz, Rf_asInteger(ncols), + pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), + parameter_ptr, &out_len, REAL(out_result))); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +void LGBM_FastConfigFree_wrapped(SEXP handle) { + LGBM_FastConfigFree(static_cast(R_ExternalPtrAddr(handle))); +} + +SEXP LGBM_BoosterPredictForCSRSingleRowFastInit_R(SEXP handle, + SEXP ncols, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + FastConfigHandle out_fastConfig; + CHECK_CALL(LGBM_BoosterPredictForCSRSingleRowFastInit(R_ExternalPtrAddr(handle), + pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), + C_API_DTYPE_FLOAT64, Rf_asInteger(ncols), + parameter_ptr, &out_fastConfig)); + R_SetExternalPtrAddr(ret, out_fastConfig); + R_RegisterCFinalizerEx(ret, LGBM_FastConfigFree_wrapped, TRUE); + Rf_unprotect(2); + return ret; + R_API_END(); +} + +SEXP LGBM_BoosterPredictForCSRSingleRowFast_R(SEXP handle_fastConfig, + SEXP indices, + SEXP data, + SEXP out_result) { + R_API_BEGIN(); + int nnz = static_cast(Rf_xlength(data)); + const int indptr[] = {0, nnz}; + int64_t out_len; + CHECK_CALL(LGBM_BoosterPredictForCSRSingleRowFast(R_ExternalPtrAddr(handle_fastConfig), + indptr, C_API_DTYPE_INT32, INTEGER(indices), + REAL(data), + 2, nnz, + &out_len, REAL(out_result))); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterPredictForMat_R(SEXP handle, + SEXP data, + SEXP num_row, + SEXP num_col, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + int32_t nrow = static_cast(Rf_asInteger(num_row)); + int32_t ncol = static_cast(Rf_asInteger(num_col)); + const double* p_mat = REAL(data); + double* ptr_ret = REAL(out_result); + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + int64_t out_len; + CHECK_CALL(LGBM_BoosterPredictForMat(R_ExternalPtrAddr(handle), + p_mat, C_API_DTYPE_FLOAT64, nrow, ncol, COL_MAJOR, + pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), parameter_ptr, &out_len, ptr_ret)); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +struct SparseOutputPointers { + void* indptr; + int32_t* indices; + void* data; + SparseOutputPointers(void* indptr, int32_t* indices, void* data) + : indptr(indptr), indices(indices), data(data) {} +}; + +void delete_SparseOutputPointers(SparseOutputPointers *ptr) { + LGBM_BoosterFreePredictSparse(ptr->indptr, ptr->indices, ptr->data, C_API_DTYPE_INT32, C_API_DTYPE_FLOAT64); + delete ptr; +} + +SEXP LGBM_BoosterPredictSparseOutput_R(SEXP handle, + SEXP indptr, + SEXP indices, + SEXP data, + SEXP is_csr, + SEXP nrows, + SEXP ncols, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter) { + SEXP cont_token = Rf_protect(R_MakeUnwindCont()); + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + const char* out_names[] = {"indptr", "indices", "data", ""}; + SEXP out = Rf_protect(Rf_mkNamed(VECSXP, out_names)); + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + + int64_t out_len[2]; + void *out_indptr; + int32_t *out_indices; + void *out_data; + + CHECK_CALL(LGBM_BoosterPredictSparseOutput(R_ExternalPtrAddr(handle), + INTEGER(indptr), C_API_DTYPE_INT32, INTEGER(indices), + REAL(data), C_API_DTYPE_FLOAT64, + Rf_xlength(indptr), Rf_xlength(data), + Rf_asLogical(is_csr)? Rf_asInteger(ncols) : Rf_asInteger(nrows), + C_API_PREDICT_CONTRIB, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), + parameter_ptr, + Rf_asLogical(is_csr)? C_API_MATRIX_TYPE_CSR : C_API_MATRIX_TYPE_CSC, + out_len, &out_indptr, &out_indices, &out_data)); + + std::unique_ptr pointers_struct = { + new SparseOutputPointers( + out_indptr, + out_indices, + out_data), + &delete_SparseOutputPointers + }; + + arr_and_len indptr_str{static_cast(out_indptr), out_len[1]}; + SET_VECTOR_ELT( + out, 0, + R_UnwindProtect(make_altrepped_vec_from_arr, + static_cast(&indptr_str), throw_R_memerr, &cont_token, cont_token)); + pointers_struct->indptr = nullptr; + + arr_and_len indices_str{static_cast(out_indices), out_len[0]}; + SET_VECTOR_ELT( + out, 1, + R_UnwindProtect(make_altrepped_vec_from_arr, + static_cast(&indices_str), throw_R_memerr, &cont_token, cont_token)); + pointers_struct->indices = nullptr; + + arr_and_len data_str{static_cast(out_data), out_len[0]}; + SET_VECTOR_ELT( + out, 2, + R_UnwindProtect(make_altrepped_vec_from_arr, + static_cast(&data_str), throw_R_memerr, &cont_token, cont_token)); + pointers_struct->data = nullptr; + + Rf_unprotect(3); + return out; + R_API_END(); +} + +SEXP LGBM_BoosterPredictForMatSingleRow_R(SEXP handle, + SEXP data, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + double* ptr_ret = REAL(out_result); + int64_t out_len; + CHECK_CALL(LGBM_BoosterPredictForMatSingleRow(R_ExternalPtrAddr(handle), + REAL(data), C_API_DTYPE_FLOAT64, Rf_xlength(data), 1, + pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), + parameter_ptr, &out_len, ptr_ret)); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterPredictForMatSingleRowFastInit_R(SEXP handle, + SEXP ncols, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int pred_type = GetPredictType(is_rawscore, is_leafidx, is_predcontrib); + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + const char* parameter_ptr = CHAR(Rf_protect(Rf_asChar(parameter))); + FastConfigHandle out_fastConfig; + CHECK_CALL(LGBM_BoosterPredictForMatSingleRowFastInit(R_ExternalPtrAddr(handle), + pred_type, Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), + C_API_DTYPE_FLOAT64, Rf_asInteger(ncols), + parameter_ptr, &out_fastConfig)); + R_SetExternalPtrAddr(ret, out_fastConfig); + R_RegisterCFinalizerEx(ret, LGBM_FastConfigFree_wrapped, TRUE); + Rf_unprotect(2); + return ret; + R_API_END(); +} + +SEXP LGBM_BoosterPredictForMatSingleRowFast_R(SEXP handle_fastConfig, + SEXP data, + SEXP out_result) { + R_API_BEGIN(); + int64_t out_len; + CHECK_CALL(LGBM_BoosterPredictForMatSingleRowFast(R_ExternalPtrAddr(handle_fastConfig), + REAL(data), &out_len, REAL(out_result))); + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_BoosterSaveModel_R(SEXP handle, + SEXP num_iteration, + SEXP feature_importance_type, + SEXP filename, + SEXP start_iteration) { + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + const char* filename_ptr = CHAR(Rf_protect(Rf_asChar(filename))); + CHECK_CALL(LGBM_BoosterSaveModel(R_ExternalPtrAddr(handle), Rf_asInteger(start_iteration), Rf_asInteger(num_iteration), Rf_asInteger(feature_importance_type), filename_ptr)); + Rf_unprotect(1); + return R_NilValue; + R_API_END(); +} + +// Note: for some reason, MSVC crashes when an error is thrown here +// if the buffer variable is defined as 'std::unique_ptr>', +// but not if it is defined as ''. +#ifndef _MSC_VER +SEXP LGBM_BoosterSaveModelToString_R(SEXP handle, + SEXP num_iteration, + SEXP feature_importance_type, + SEXP start_iteration) { + SEXP cont_token = Rf_protect(R_MakeUnwindCont()); + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int64_t out_len = 0; + int64_t buf_len = 1024 * 1024; + int num_iter = Rf_asInteger(num_iteration); + int start_iter = Rf_asInteger(start_iteration); + int importance_type = Rf_asInteger(feature_importance_type); + std::unique_ptr> inner_char_buf(new std::vector(buf_len)); + CHECK_CALL(LGBM_BoosterSaveModelToString(R_ExternalPtrAddr(handle), start_iter, num_iter, importance_type, buf_len, &out_len, inner_char_buf->data())); + inner_char_buf->resize(out_len); + if (out_len > buf_len) { + CHECK_CALL(LGBM_BoosterSaveModelToString(R_ExternalPtrAddr(handle), start_iter, num_iter, importance_type, out_len, &out_len, inner_char_buf->data())); + } + SEXP out = R_UnwindProtect(make_altrepped_raw_vec, &inner_char_buf, throw_R_memerr, &cont_token, cont_token); + Rf_unprotect(1); + return out; + R_API_END(); +} +#else +SEXP LGBM_BoosterSaveModelToString_R(SEXP handle, + SEXP num_iteration, + SEXP feature_importance_type, + SEXP start_iteration) { + SEXP cont_token = Rf_protect(R_MakeUnwindCont()); + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + int64_t out_len = 0; + int64_t buf_len = 1024 * 1024; + int num_iter = Rf_asInteger(num_iteration); + int start_iter = Rf_asInteger(start_iteration); + int importance_type = Rf_asInteger(feature_importance_type); + std::vector inner_char_buf(buf_len); + CHECK_CALL(LGBM_BoosterSaveModelToString(R_ExternalPtrAddr(handle), start_iter, num_iter, importance_type, buf_len, &out_len, inner_char_buf.data())); + SEXP model_str = Rf_protect(safe_R_raw(out_len, &cont_token)); + // if the model string was larger than the initial buffer, call the function again, writing directly to the R object + if (out_len > buf_len) { + CHECK_CALL(LGBM_BoosterSaveModelToString(R_ExternalPtrAddr(handle), start_iter, num_iter, importance_type, out_len, &out_len, reinterpret_cast(RAW(model_str)))); + } else { + std::copy(inner_char_buf.begin(), inner_char_buf.begin() + out_len, reinterpret_cast(RAW(model_str))); + } + Rf_unprotect(2); + return model_str; + R_API_END(); +} +#endif + +SEXP LGBM_BoosterDumpModel_R(SEXP handle, + SEXP num_iteration, + SEXP feature_importance_type, + SEXP start_iteration) { + SEXP cont_token = Rf_protect(R_MakeUnwindCont()); + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + SEXP model_str; + int64_t out_len = 0; + int64_t buf_len = 1024 * 1024; + int num_iter = Rf_asInteger(num_iteration); + int start_iter = Rf_asInteger(start_iteration); + int importance_type = Rf_asInteger(feature_importance_type); + std::vector inner_char_buf(buf_len); + CHECK_CALL(LGBM_BoosterDumpModel(R_ExternalPtrAddr(handle), start_iter, num_iter, importance_type, buf_len, &out_len, inner_char_buf.data())); + // if the model string was larger than the initial buffer, allocate a bigger buffer and try again + if (out_len > buf_len) { + inner_char_buf.resize(out_len); + CHECK_CALL(LGBM_BoosterDumpModel(R_ExternalPtrAddr(handle), start_iter, num_iter, importance_type, out_len, &out_len, inner_char_buf.data())); + } + model_str = Rf_protect(safe_R_string(static_cast(1), &cont_token)); + SET_STRING_ELT(model_str, 0, safe_R_mkChar(inner_char_buf.data(), &cont_token)); + Rf_unprotect(2); + return model_str; + R_API_END(); +} + +SEXP LGBM_DumpParamAliases_R() { + SEXP cont_token = Rf_protect(R_MakeUnwindCont()); + R_API_BEGIN(); + SEXP aliases_str; + int64_t out_len = 0; + int64_t buf_len = 1024 * 1024; + std::vector inner_char_buf(buf_len); + CHECK_CALL(LGBM_DumpParamAliases(buf_len, &out_len, inner_char_buf.data())); + // if aliases string was larger than the initial buffer, allocate a bigger buffer and try again + if (out_len > buf_len) { + inner_char_buf.resize(out_len); + CHECK_CALL(LGBM_DumpParamAliases(out_len, &out_len, inner_char_buf.data())); + } + aliases_str = Rf_protect(safe_R_string(static_cast(1), &cont_token)); + SET_STRING_ELT(aliases_str, 0, safe_R_mkChar(inner_char_buf.data(), &cont_token)); + Rf_unprotect(2); + return aliases_str; + R_API_END(); +} + +SEXP LGBM_BoosterGetLoadedParam_R(SEXP handle) { + SEXP cont_token = Rf_protect(R_MakeUnwindCont()); + R_API_BEGIN(); + _AssertBoosterHandleNotNull(handle); + SEXP params_str; + int64_t out_len = 0; + int64_t buf_len = 1024 * 1024; + std::vector inner_char_buf(buf_len); + CHECK_CALL(LGBM_BoosterGetLoadedParam(R_ExternalPtrAddr(handle), buf_len, &out_len, inner_char_buf.data())); + // if aliases string was larger than the initial buffer, allocate a bigger buffer and try again + if (out_len > buf_len) { + inner_char_buf.resize(out_len); + CHECK_CALL(LGBM_BoosterGetLoadedParam(R_ExternalPtrAddr(handle), out_len, &out_len, inner_char_buf.data())); + } + params_str = Rf_protect(safe_R_string(static_cast(1), &cont_token)); + SET_STRING_ELT(params_str, 0, safe_R_mkChar(inner_char_buf.data(), &cont_token)); + Rf_unprotect(2); + return params_str; + R_API_END(); +} + +SEXP LGBM_GetMaxThreads_R(SEXP out) { + R_API_BEGIN(); + int num_threads; + CHECK_CALL(LGBM_GetMaxThreads(&num_threads)); + INTEGER(out)[0] = num_threads; + return R_NilValue; + R_API_END(); +} + +SEXP LGBM_SetMaxThreads_R(SEXP num_threads) { + R_API_BEGIN(); + int new_num_threads = Rf_asInteger(num_threads); + CHECK_CALL(LGBM_SetMaxThreads(new_num_threads)); + return R_NilValue; + R_API_END(); +} + +// .Call() calls +static const R_CallMethodDef CallEntries[] = { + {"LGBM_HandleIsNull_R" , (DL_FUNC) &LGBM_HandleIsNull_R , 1}, + {"LGBM_DatasetCreateFromFile_R" , (DL_FUNC) &LGBM_DatasetCreateFromFile_R , 3}, + {"LGBM_DatasetCreateFromCSC_R" , (DL_FUNC) &LGBM_DatasetCreateFromCSC_R , 8}, + {"LGBM_DatasetCreateFromMat_R" , (DL_FUNC) &LGBM_DatasetCreateFromMat_R , 5}, + {"LGBM_DatasetGetSubset_R" , (DL_FUNC) &LGBM_DatasetGetSubset_R , 4}, + {"LGBM_DatasetSetFeatureNames_R" , (DL_FUNC) &LGBM_DatasetSetFeatureNames_R , 2}, + {"LGBM_DatasetGetFeatureNames_R" , (DL_FUNC) &LGBM_DatasetGetFeatureNames_R , 1}, + {"LGBM_DatasetSaveBinary_R" , (DL_FUNC) &LGBM_DatasetSaveBinary_R , 2}, + {"LGBM_DatasetFree_R" , (DL_FUNC) &LGBM_DatasetFree_R , 1}, + {"LGBM_DatasetSetField_R" , (DL_FUNC) &LGBM_DatasetSetField_R , 4}, + {"LGBM_DatasetGetFieldSize_R" , (DL_FUNC) &LGBM_DatasetGetFieldSize_R , 3}, + {"LGBM_DatasetGetField_R" , (DL_FUNC) &LGBM_DatasetGetField_R , 3}, + {"LGBM_DatasetUpdateParamChecking_R" , (DL_FUNC) &LGBM_DatasetUpdateParamChecking_R , 2}, + {"LGBM_DatasetGetNumData_R" , (DL_FUNC) &LGBM_DatasetGetNumData_R , 2}, + {"LGBM_DatasetGetNumFeature_R" , (DL_FUNC) &LGBM_DatasetGetNumFeature_R , 2}, + {"LGBM_DatasetGetFeatureNumBin_R" , (DL_FUNC) &LGBM_DatasetGetFeatureNumBin_R , 3}, + {"LGBM_BoosterCreate_R" , (DL_FUNC) &LGBM_BoosterCreate_R , 2}, + {"LGBM_BoosterFree_R" , (DL_FUNC) &LGBM_BoosterFree_R , 1}, + {"LGBM_BoosterCreateFromModelfile_R" , (DL_FUNC) &LGBM_BoosterCreateFromModelfile_R , 1}, + {"LGBM_BoosterLoadModelFromString_R" , (DL_FUNC) &LGBM_BoosterLoadModelFromString_R , 1}, + {"LGBM_BoosterMerge_R" , (DL_FUNC) &LGBM_BoosterMerge_R , 2}, + {"LGBM_BoosterAddValidData_R" , (DL_FUNC) &LGBM_BoosterAddValidData_R , 2}, + {"LGBM_BoosterResetTrainingData_R" , (DL_FUNC) &LGBM_BoosterResetTrainingData_R , 2}, + {"LGBM_BoosterResetParameter_R" , (DL_FUNC) &LGBM_BoosterResetParameter_R , 2}, + {"LGBM_BoosterGetNumClasses_R" , (DL_FUNC) &LGBM_BoosterGetNumClasses_R , 2}, + {"LGBM_BoosterGetNumFeature_R" , (DL_FUNC) &LGBM_BoosterGetNumFeature_R , 1}, + {"LGBM_BoosterGetLoadedParam_R" , (DL_FUNC) &LGBM_BoosterGetLoadedParam_R , 1}, + {"LGBM_BoosterUpdateOneIter_R" , (DL_FUNC) &LGBM_BoosterUpdateOneIter_R , 1}, + {"LGBM_BoosterUpdateOneIterCustom_R" , (DL_FUNC) &LGBM_BoosterUpdateOneIterCustom_R , 4}, + {"LGBM_BoosterRollbackOneIter_R" , (DL_FUNC) &LGBM_BoosterRollbackOneIter_R , 1}, + {"LGBM_BoosterGetCurrentIteration_R" , (DL_FUNC) &LGBM_BoosterGetCurrentIteration_R , 2}, + {"LGBM_BoosterNumModelPerIteration_R" , (DL_FUNC) &LGBM_BoosterNumModelPerIteration_R , 2}, + {"LGBM_BoosterNumberOfTotalModel_R" , (DL_FUNC) &LGBM_BoosterNumberOfTotalModel_R , 2}, + {"LGBM_BoosterGetUpperBoundValue_R" , (DL_FUNC) &LGBM_BoosterGetUpperBoundValue_R , 2}, + {"LGBM_BoosterGetLowerBoundValue_R" , (DL_FUNC) &LGBM_BoosterGetLowerBoundValue_R , 2}, + {"LGBM_BoosterGetEvalNames_R" , (DL_FUNC) &LGBM_BoosterGetEvalNames_R , 1}, + {"LGBM_BoosterGetEval_R" , (DL_FUNC) &LGBM_BoosterGetEval_R , 3}, + {"LGBM_BoosterGetNumPredict_R" , (DL_FUNC) &LGBM_BoosterGetNumPredict_R , 3}, + {"LGBM_BoosterGetPredict_R" , (DL_FUNC) &LGBM_BoosterGetPredict_R , 3}, + {"LGBM_BoosterPredictForFile_R" , (DL_FUNC) &LGBM_BoosterPredictForFile_R , 10}, + {"LGBM_BoosterCalcNumPredict_R" , (DL_FUNC) &LGBM_BoosterCalcNumPredict_R , 8}, + {"LGBM_BoosterPredictForCSC_R" , (DL_FUNC) &LGBM_BoosterPredictForCSC_R , 14}, + {"LGBM_BoosterPredictForCSR_R" , (DL_FUNC) &LGBM_BoosterPredictForCSR_R , 12}, + {"LGBM_BoosterPredictForCSRSingleRow_R" , (DL_FUNC) &LGBM_BoosterPredictForCSRSingleRow_R , 11}, + {"LGBM_BoosterPredictForCSRSingleRowFastInit_R", (DL_FUNC) &LGBM_BoosterPredictForCSRSingleRowFastInit_R, 8}, + {"LGBM_BoosterPredictForCSRSingleRowFast_R" , (DL_FUNC) &LGBM_BoosterPredictForCSRSingleRowFast_R , 4}, + {"LGBM_BoosterPredictSparseOutput_R" , (DL_FUNC) &LGBM_BoosterPredictSparseOutput_R , 10}, + {"LGBM_BoosterPredictForMat_R" , (DL_FUNC) &LGBM_BoosterPredictForMat_R , 11}, + {"LGBM_BoosterPredictForMatSingleRow_R" , (DL_FUNC) &LGBM_BoosterPredictForMatSingleRow_R , 9}, + {"LGBM_BoosterPredictForMatSingleRowFastInit_R", (DL_FUNC) &LGBM_BoosterPredictForMatSingleRowFastInit_R, 8}, + {"LGBM_BoosterPredictForMatSingleRowFast_R" , (DL_FUNC) &LGBM_BoosterPredictForMatSingleRowFast_R , 3}, + {"LGBM_BoosterSaveModel_R" , (DL_FUNC) &LGBM_BoosterSaveModel_R , 5}, + {"LGBM_BoosterSaveModelToString_R" , (DL_FUNC) &LGBM_BoosterSaveModelToString_R , 4}, + {"LGBM_BoosterDumpModel_R" , (DL_FUNC) &LGBM_BoosterDumpModel_R , 4}, + {"LGBM_NullBoosterHandleError_R" , (DL_FUNC) &LGBM_NullBoosterHandleError_R , 0}, + {"LGBM_DumpParamAliases_R" , (DL_FUNC) &LGBM_DumpParamAliases_R , 0}, + {"LGBM_GetMaxThreads_R" , (DL_FUNC) &LGBM_GetMaxThreads_R , 1}, + {"LGBM_SetMaxThreads_R" , (DL_FUNC) &LGBM_SetMaxThreads_R , 1}, + {NULL, NULL, 0} +}; + +LIGHTGBM_C_EXPORT void R_init_lightgbm(DllInfo *dll); + +void R_init_lightgbm(DllInfo *dll) { + R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); + R_useDynamicSymbols(dll, FALSE); + +#ifndef LGB_NO_ALTREP + lgb_altrepped_char_vec = R_make_altraw_class("lgb_altrepped_char_vec", "lightgbm", dll); + R_set_altrep_Length_method(lgb_altrepped_char_vec, get_altrepped_raw_len); + R_set_altvec_Dataptr_method(lgb_altrepped_char_vec, get_altrepped_raw_dataptr); + R_set_altvec_Dataptr_or_null_method(lgb_altrepped_char_vec, get_altrepped_raw_dataptr_or_null); + + lgb_altrepped_int_arr = R_make_altinteger_class("lgb_altrepped_int_arr", "lightgbm", dll); + R_set_altrep_Length_method(lgb_altrepped_int_arr, get_altrepped_vec_len); + R_set_altvec_Dataptr_method(lgb_altrepped_int_arr, get_altrepped_vec_dataptr); + R_set_altvec_Dataptr_or_null_method(lgb_altrepped_int_arr, get_altrepped_vec_dataptr_or_null); + + lgb_altrepped_dbl_arr = R_make_altreal_class("lgb_altrepped_dbl_arr", "lightgbm", dll); + R_set_altrep_Length_method(lgb_altrepped_dbl_arr, get_altrepped_vec_len); + R_set_altvec_Dataptr_method(lgb_altrepped_dbl_arr, get_altrepped_vec_dataptr); + R_set_altvec_Dataptr_or_null_method(lgb_altrepped_dbl_arr, get_altrepped_vec_dataptr_or_null); +#endif +} diff --git a/R-package/src/lightgbm_R.h b/R-package/src/lightgbm_R.h new file mode 100644 index 0000000..52143d8 --- /dev/null +++ b/R-package/src/lightgbm_R.h @@ -0,0 +1,907 @@ +/*! + * Copyright (c) 2017-2026 Microsoft Corporation. All rights reserved. + * Copyright (c) 2017-2026 The LightGBM developers. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + */ +#ifndef LIGHTGBM_R_PACKAGE_SRC_LIGHTGBM_R_H_ +#define LIGHTGBM_R_PACKAGE_SRC_LIGHTGBM_R_H_ + +#include + +#ifndef R_NO_REMAP +#define R_NO_REMAP +#endif + +#ifndef R_USE_C99_IN_CXX +#define R_USE_C99_IN_CXX +#endif + +#include + +/*! +* \brief check if an R external pointer (like a Booster or Dataset handle) is a null pointer +* \param handle handle for a Booster, Dataset, or Predictor +* \return R logical, TRUE if the handle is a null pointer +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_HandleIsNull_R( + SEXP handle +); + +/*! +* \brief Throw a standardized error message when encountering a null Booster handle +* \return No return, will throw an error +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_NullBoosterHandleError_R(); + +// --- start Dataset interface + +/*! +* \brief load Dataset from file like the command_line LightGBM does +* \param filename the name of the file +* \param parameters additional parameters +* \param reference used to align bin mapper with other Dataset, nullptr means not used +* \return Dataset handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetCreateFromFile_R( + SEXP filename, + SEXP parameters, + SEXP reference +); + +/*! +* \brief create a Dataset from Compressed Sparse Column (CSC) format +* \param indptr pointer to row headers +* \param indices findex +* \param data fvalue +* \param num_indptr number of cols in the matrix + 1 +* \param nelem number of nonzero elements in the matrix +* \param num_row number of rows +* \param parameters additional parameters +* \param reference used to align bin mapper with other Dataset, nullptr means not used +* \return Dataset handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetCreateFromCSC_R( + SEXP indptr, + SEXP indices, + SEXP data, + SEXP num_indptr, + SEXP nelem, + SEXP num_row, + SEXP parameters, + SEXP reference +); + +/*! +* \brief create Dataset from dense matrix +* \param data matrix data +* \param num_row number of rows +* \param num_col number columns +* \param parameters additional parameters +* \param reference used to align bin mapper with other Dataset, nullptr means not used +* \return Dataset handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetCreateFromMat_R( + SEXP data, + SEXP num_row, + SEXP num_col, + SEXP parameters, + SEXP reference +); + +/*! +* \brief Create subset of a Dataset +* \param handle handle of full Dataset +* \param used_row_indices Indices used in subset +* \param len_used_row_indices length of Indices used in subset +* \param parameters additional parameters +* \return Dataset handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetGetSubset_R( + SEXP handle, + SEXP used_row_indices, + SEXP len_used_row_indices, + SEXP parameters +); + +/*! +* \brief save feature names to Dataset +* \param handle handle +* \param feature_names feature names +* \return R character vector of feature names +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetSetFeatureNames_R( + SEXP handle, + SEXP feature_names +); + +/*! +* \brief get feature names from Dataset +* \param handle Dataset handle +* \return an R character vector with feature names from the Dataset or NULL if no feature names +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetGetFeatureNames_R( + SEXP handle +); + +/*! +* \brief save Dataset to binary file +* \param handle an instance of Dataset +* \param filename file name +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetSaveBinary_R( + SEXP handle, + SEXP filename +); + +/*! +* \brief free Dataset +* \param handle an instance of Dataset +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetFree_R( + SEXP handle +); + +/*! +* \brief set vector to a content in info +* Note: group and group_id only work for C_API_DTYPE_INT32 +* label and weight only work for C_API_DTYPE_FLOAT32 +* \param handle an instance of Dataset +* \param field_name field name, can be label, weight, group, group_id +* \param field_data pointer to vector +* \param num_element number of element in field_data +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetSetField_R( + SEXP handle, + SEXP field_name, + SEXP field_data, + SEXP num_element +); + +/*! +* \brief get size of info vector from Dataset +* \param handle an instance of Dataset +* \param field_name field name +* \param out size of info vector from Dataset +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetGetFieldSize_R( + SEXP handle, + SEXP field_name, + SEXP out +); + +/*! +* \brief get info vector from Dataset +* \param handle an instance of Dataset +* \param field_name field name +* \param field_data pointer to vector +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetGetField_R( + SEXP handle, + SEXP field_name, + SEXP field_data +); + +/*! + * \brief Raise errors for attempts to update Dataset parameters. + * Some parameters cannot be updated after construction. + * \param old_params Current Dataset parameters + * \param new_params New Dataset parameters + * \return R NULL value + */ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetUpdateParamChecking_R( + SEXP old_params, + SEXP new_params +); + +/*! +* \brief get number of data. +* \param handle the handle to the Dataset +* \param out The address to hold number of data +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetGetNumData_R( + SEXP handle, + SEXP out +); + +/*! +* \brief get number of features +* \param handle the handle to the Dataset +* \param out The output of number of features +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetGetNumFeature_R( + SEXP handle, + SEXP out +); + +/*! +* \brief get number of bins for feature +* \param handle the handle to the Dataset +* \param feature the index of the feature +* \param out The output of number of bins +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DatasetGetFeatureNumBin_R( + SEXP handle, + SEXP feature, + SEXP out +); + +// --- start Booster interfaces + +/*! +* \brief create a new boosting learner +* \param train_data training Dataset +* \param parameters format: 'key1=value1 key2=value2' +* \return Booster handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterCreate_R( + SEXP train_data, + SEXP parameters +); + +/*! +* \brief free Booster +* \param handle handle to be freed +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterFree_R( + SEXP handle +); + +/*! +* \brief load an existing Booster from model file +* \param filename filename of model +* \return Booster handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterCreateFromModelfile_R( + SEXP filename +); + +/*! +* \brief load an existing Booster from a string +* \param model_str string containing the model +* \return Booster handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterLoadModelFromString_R( + SEXP model_str +); + +/*! +* \brief Get parameters as JSON string. +* \param handle Booster handle +* \return R character vector (length=1) with parameters in JSON format +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetLoadedParam_R( + SEXP handle +); + +/*! +* \brief Merge model in two Boosters to first handle +* \param handle handle primary Booster handle, will merge other handle to this +* \param other_handle secondary Booster handle +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterMerge_R( + SEXP handle, + SEXP other_handle +); + +/*! +* \brief Add new validation to Booster +* \param handle Booster handle +* \param valid_data validation Dataset +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterAddValidData_R( + SEXP handle, + SEXP valid_data +); + +/*! +* \brief Reset training data for Booster +* \param handle Booster handle +* \param train_data training Dataset +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterResetTrainingData_R( + SEXP handle, + SEXP train_data +); + +/*! +* \brief Reset config for current Booster +* \param handle Booster handle +* \param parameters format: 'key1=value1 key2=value2' +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterResetParameter_R( + SEXP handle, + SEXP parameters +); + +/*! +* \brief Get number of classes +* \param handle Booster handle +* \param out number of classes +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetNumClasses_R( + SEXP handle, + SEXP out +); + +/*! +* \brief Get number of features. +* \param handle Booster handle +* \return Total number of features, as R integer +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetNumFeature_R( + SEXP handle +); + +/*! +* \brief update the model in one round +* \param handle Booster handle +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterUpdateOneIter_R( + SEXP handle +); + +/*! +* \brief update the model, by directly specifying gradient and second order gradient, +* this can be used to support customized loss function +* \param handle Booster handle +* \param grad gradient statistics +* \param hess second order gradient statistics +* \param len length of grad/hess +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterUpdateOneIterCustom_R( + SEXP handle, + SEXP grad, + SEXP hess, + SEXP len +); + +/*! +* \brief Rollback one iteration +* \param handle Booster handle +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterRollbackOneIter_R( + SEXP handle +); + +/*! +* \brief Get iteration of current boosting rounds +* \param handle Booster handle +* \param out iteration of boosting rounds +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetCurrentIteration_R( + SEXP handle, + SEXP out +); + +/*! + * \brief Get number of trees per iteration + * \param handle Booster handle + * \param out Number of trees per iteration + * \return R NULL value + */ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterNumModelPerIteration_R( + SEXP handle, + SEXP out +); + +/*! + * \brief Get total number of trees + * \param handle Booster handle + * \param out Total number of trees of Booster + * \return R NULL value + */ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterNumberOfTotalModel_R( + SEXP handle, + SEXP out +); + +/*! +* \brief Get model upper bound value. +* \param handle Handle of Booster +* \param[out] out_results Result pointing to max value +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetUpperBoundValue_R( + SEXP handle, + SEXP out_result +); + +/*! +* \brief Get model lower bound value. +* \param handle Handle of Booster +* \param[out] out_results Result pointing to min value +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetLowerBoundValue_R( + SEXP handle, + SEXP out_result +); + +/*! +* \brief Get names of eval metrics +* \param handle Handle of booster +* \return R character vector with names of eval metrics +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetEvalNames_R( + SEXP handle +); + +/*! +* \brief get evaluation for training data and validation data +* \param handle Booster handle +* \param data_idx 0:training data, 1: 1st valid data, 2:2nd valid data ... +* \param out_result float array containing result +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetEval_R( + SEXP handle, + SEXP data_idx, + SEXP out_result +); + +/*! +* \brief Get number of prediction for training data and validation data +* \param handle Booster handle +* \param data_idx 0:training data, 1: 1st valid data, 2:2nd valid data ... +* \param out size of predict +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetNumPredict_R( + SEXP handle, + SEXP data_idx, + SEXP out +); + +/*! +* \brief Get prediction for training data and validation data. +* This can be used to support customized eval function +* \param handle Booster handle +* \param data_idx 0:training data, 1: 1st valid data, 2:2nd valid data ... +* \param out_result, used to store predict result, should pre-allocate memory +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterGetPredict_R( + SEXP handle, + SEXP data_idx, + SEXP out_result +); + +/*! +* \brief make prediction for file +* \param handle Booster handle +* \param data_filename filename of data file +* \param data_has_header data file has header or not +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration Start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \param result_filename filename of file to write predictions to +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForFile_R( + SEXP handle, + SEXP data_filename, + SEXP data_has_header, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP result_filename +); + +/*! +* \brief Get number of prediction +* \param handle Booster handle +* \param num_row number of rows in input +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration Start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param out_len length of prediction +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterCalcNumPredict_R( + SEXP handle, + SEXP num_row, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP out_len +); + +/*! +* \brief make prediction for a new Dataset +* Note: should pre-allocate memory for out_result, +* for normal and raw score: its length is equal to num_class * num_data +* for leaf index, its length is equal to num_class * num_data * num_iteration +* for feature contributions, its length is equal to num_class * num_data * (num_features + 1) +* \param handle Booster handle +* \param indptr pointer to row headers +* \param indices findex +* \param data fvalue +* \param num_indptr number of cols in the matrix + 1 +* \param nelem number of non-zero elements in the matrix +* \param num_row number of rows +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \param out_result prediction result +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForCSC_R( + SEXP handle, + SEXP indptr, + SEXP indices, + SEXP data, + SEXP num_indptr, + SEXP nelem, + SEXP num_row, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result +); + +/*! +* \brief make prediction for a new Dataset +* Note: should pre-allocate memory for out_result, +* for normal and raw score: its length is equal to num_class * num_data +* for leaf index, its length is equal to num_class * num_data * num_iteration +* for feature contributions, its length is equal to num_class * num_data * (num_features + 1) +* \param handle Booster handle +* \param indptr array with the index pointer of the data in CSR format +* \param indices array with the non-zero indices of the data in CSR format +* \param data array with the non-zero values of the data in CSR format +* \param ncols number of columns in the data +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \param out_result prediction result +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForCSR_R( + SEXP handle, + SEXP indptr, + SEXP indices, + SEXP data, + SEXP ncols, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result +); + +/*! +* \brief make prediction for a single row of data +* Note: should pre-allocate memory for out_result, +* for normal and raw score: its length is equal to num_class +* for leaf index, its length is equal to num_class * num_iteration +* for feature contributions, its length is equal to num_class * (num_features + 1) +* \param handle Booster handle +* \param indices array corresponding to the indices of the columns with non-zero values of the row to predict on +* \param data array corresponding to the non-zero values of row to predict on +* \param ncols number of columns in the data +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \param out_result prediction result +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForCSRSingleRow_R( + SEXP handle, + SEXP indices, + SEXP data, + SEXP ncols, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result +); + +/*! +* \brief Initialize and return a fast configuration handle to use with ``LGBM_BoosterPredictForCSRSingleRowFast_R``. +* \param handle Booster handle +* \param ncols number columns in the data +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \return Fast configuration handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForCSRSingleRowFastInit_R( + SEXP handle, + SEXP ncols, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter +); + +/*! +* \brief make prediction for a single row of data +* Note: should pre-allocate memory for out_result, +* for normal and raw score: its length is equal to num_class +* for leaf index, its length is equal to num_class * num_iteration +* for feature contributions, its length is equal to num_class * (num_features + 1) +* \param handle_fastConfig Fast configuration handle +* \param indices array corresponding to the indices of the columns with non-zero values of the row to predict on +* \param data array corresponding to the non-zero values of row to predict on +* \param out_result prediction result +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForCSRSingleRowFast_R( + SEXP handle_fastConfig, + SEXP indices, + SEXP data, + SEXP out_result +); + +/*! +* \brief make feature contribution prediction for a new Dataset +* \param handle Booster handle +* \param indptr array with the index pointer of the data in CSR or CSC format +* \param indices array with the non-zero indices of the data in CSR or CSC format +* \param data array with the non-zero values of the data in CSR or CSC format +* \param is_csr whether the input data is in CSR format or not (pass FALSE for CSC) +* \param nrows number of rows in the data +* \param ncols number of columns in the data +* \param start_iteration start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \return An R list with entries "indptr", "indices", "data", constituting the +* feature contributions in sparse format, in the same storage order as +* the input data. +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictSparseOutput_R( + SEXP handle, + SEXP indptr, + SEXP indices, + SEXP data, + SEXP is_csr, + SEXP nrows, + SEXP ncols, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter +); + +/*! +* \brief make prediction for a new Dataset +* Note: should pre-allocate memory for out_result, +* for normal and raw score: its length is equal to num_class * num_data +* for leaf index, its length is equal to num_class * num_data * num_iteration +* for feature contributions, its length is equal to num_class * num_data * (num_features + 1) +* \param handle Booster handle +* \param data pointer to the data space +* \param num_row number of rows +* \param num_col number columns +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \param out_result prediction result +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForMat_R( + SEXP handle, + SEXP data, + SEXP num_row, + SEXP num_col, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result +); + +/*! +* \brief make prediction for a single row of data +* Note: should pre-allocate memory for out_result, +* for normal and raw score: its length is equal to num_class +* for leaf index, its length is equal to num_class * num_iteration +* for feature contributions, its length is equal to num_class * (num_features + 1) +* \param handle Booster handle +* \param data array corresponding to the row to predict on +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \param out_result prediction result +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForMatSingleRow_R( + SEXP handle, + SEXP data, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter, + SEXP out_result +); + +/*! +* \brief Initialize and return a fast configuration handle to use with ``LGBM_BoosterPredictForMatSingleRowFast_R``. +* \param handle Booster handle +* \param ncols number columns in the data +* \param is_rawscore 1 to get raw predictions, before transformations like +* converting to probabilities, 0 otherwise +* \param is_leafidx 1 to get record of which leaf in each tree +* observations fell into, 0 otherwise +* \param is_predcontrib 1 to get feature contributions, 0 otherwise +* \param start_iteration start index of the iteration to predict +* \param num_iteration number of iteration for prediction, <= 0 means no limit +* \param parameter additional parameters +* \return Fast configuration handle +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForMatSingleRowFastInit_R( + SEXP handle, + SEXP ncols, + SEXP is_rawscore, + SEXP is_leafidx, + SEXP is_predcontrib, + SEXP start_iteration, + SEXP num_iteration, + SEXP parameter +); + +/*! +* \brief make prediction for a single row of data +* Note: should pre-allocate memory for out_result, +* for normal and raw score: its length is equal to num_class +* for leaf index, its length is equal to num_class * num_iteration +* for feature contributions, its length is equal to num_class * (num_features + 1) +* \param handle_fastConfig Fast configuration handle +* \param data array corresponding to the row to predict on +* \param out_result prediction result +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterPredictForMatSingleRowFast_R( + SEXP handle_fastConfig, + SEXP data, + SEXP out_result +); + +/*! +* \brief save model into file +* \param handle Booster handle +* \param num_iteration, <= 0 means save all +* \param feature_importance_type type of feature importance, 0: split, 1: gain +* \param filename file name +* \param start_iteration Starting iteration (0 based) +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterSaveModel_R( + SEXP handle, + SEXP num_iteration, + SEXP feature_importance_type, + SEXP filename, + SEXP start_iteration +); + +/*! +* \brief create string containing model +* \param handle Booster handle +* \param num_iteration, <= 0 means save all +* \param feature_importance_type type of feature importance, 0: split, 1: gain +* \param start_iteration Starting iteration (0 based) +* \return R character vector (length=1) with model string +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterSaveModelToString_R( + SEXP handle, + SEXP num_iteration, + SEXP feature_importance_type, + SEXP start_iteration +); + +/*! +* \brief dump model to JSON +* \param handle Booster handle +* \param num_iteration, <= 0 means save all +* \param feature_importance_type type of feature importance, 0: split, 1: gain +* \param start_iteration Index of starting iteration (0 based) +* \return R character vector (length=1) with model JSON +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_BoosterDumpModel_R( + SEXP handle, + SEXP num_iteration, + SEXP feature_importance_type, + SEXP start_iteration +); + +/*! +* \brief Dump parameter aliases to JSON +* \return R character vector (length=1) with aliases JSON +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_DumpParamAliases_R(); + +/*! +* \brief Get current maximum number of threads used by LightGBM routines in this process. +* \param[out] out current maximum number of threads used by LightGBM. -1 means defaulting to omp_get_num_threads(). +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_GetMaxThreads_R( + SEXP out +); + + +/*! +* \brief Set maximum number of threads used by LightGBM routines in this process. +* \param num_threads maximum number of threads used by LightGBM. -1 means defaulting to omp_get_num_threads(). +* \return R NULL value +*/ +LIGHTGBM_C_EXPORT SEXP LGBM_SetMaxThreads_R( + SEXP num_threads +); + +#endif // LIGHTGBM_R_PACKAGE_SRC_LIGHTGBM_R_H_ diff --git a/R-package/tests/testthat.R b/R-package/tests/testthat.R new file mode 100644 index 0000000..c5393f8 --- /dev/null +++ b/R-package/tests/testthat.R @@ -0,0 +1,9 @@ +library(testthat) +library(lightgbm) # nolint: unused_import. + +test_check( + package = "lightgbm" + , stop_on_failure = TRUE + , stop_on_warning = FALSE + , reporter = testthat::SummaryReporter$new() +) diff --git a/R-package/tests/testthat/helper.R b/R-package/tests/testthat/helper.R new file mode 100644 index 0000000..9d91892 --- /dev/null +++ b/R-package/tests/testthat/helper.R @@ -0,0 +1,54 @@ +# ref for this file: +# +# * https://r-pkgs.org/testing-design.html#testthat-helper-files +# * https://r-pkgs.org/testing-design.html#testthat-setup-files + +# LightGBM-internal fix to comply with CRAN policy of only using up to 2 threads in tests and example. +# +# per https://cran.r-project.org/web/packages/policies.html +# +# > If running a package uses multiple threads/cores it must never use more than two simultaneously: +# the check farm is a shared resource and will typically be running many checks simultaneously. +# +.LGB_MAX_THREADS <- 2L +setLGBMthreads(.LGB_MAX_THREADS) + +# control data.table parallelism +# ref: https://github.com/Rdatatable/data.table/issues/5658 +data.table::setDTthreads(1L) + +# by default, how much should results in tests be allowed to differ from hard-coded expected numbers? +.LGB_NUMERIC_TOLERANCE <- 1e-6 + +# are the tests running on Windows? +.LGB_ON_WINDOWS <- .Platform$OS.type == "windows" +.LGB_ON_32_BIT_WINDOWS <- .LGB_ON_WINDOWS && .Machine$sizeof.pointer != 8L + +# are the tests running in a UTF-8 locale? +.LGB_UTF8_LOCALE <- all(endsWith( + Sys.getlocale(category = "LC_CTYPE") + , "UTF-8" +)) + +# control how many loud LightGBM's logger is in tests +.LGB_VERBOSITY <- as.integer( + Sys.getenv("LIGHTGBM_TEST_VERBOSITY", "-1") +) + + +# [description] +# test that every element of 'x' is in 'y' +# +# testthat::expect_in() was added in {testthat} v3.1.19. +# This is here to support a similar interface on older {testthat} versions. +.expect_in <- function(x, y) { + if (exists("expect_in")) { + expect_in(x, y) + } else { + missing_items <- x[!(x %in% y)] + if (length(missing_items) != 0L) { + error_msg <- paste0("Some expected items not found: ", toString(missing_items)) + stop(error_msg) + } + } +} diff --git a/R-package/tests/testthat/test_Predictor.R b/R-package/tests/testthat/test_Predictor.R new file mode 100644 index 0000000..99cae26 --- /dev/null +++ b/R-package/tests/testthat/test_Predictor.R @@ -0,0 +1,706 @@ +library(Matrix) + +test_that("Predictor's finalizer should not fail", { + X <- as.matrix(as.integer(iris[, "Species"]), ncol = 1L) + y <- iris[["Sepal.Length"]] + dtrain <- lgb.Dataset(X, label = y) + bst <- lgb.train( + data = dtrain + , params = list( + objective = "regression" + , num_threads = .LGB_MAX_THREADS + ) + , verbose = .LGB_VERBOSITY + , nrounds = 3L + ) + model_file <- tempfile(fileext = ".model") + bst$save_model(filename = model_file) + predictor <- Predictor$new(modelfile = model_file) + + expect_true(.is_Predictor(predictor)) + + expect_false(.is_null_handle(predictor$.__enclos_env__$private$handle)) + + predictor$.__enclos_env__$private$finalize() + expect_true(.is_null_handle(predictor$.__enclos_env__$private$handle)) + + # calling finalize() a second time shouldn't cause any issues + predictor$.__enclos_env__$private$finalize() + expect_true(.is_null_handle(predictor$.__enclos_env__$private$handle)) +}) + +test_that("predictions do not fail for integer input", { + X <- as.matrix(as.integer(iris[, "Species"]), ncol = 1L) + y <- iris[["Sepal.Length"]] + dtrain <- lgb.Dataset(X, label = y) + fit <- lgb.train( + data = dtrain + , params = list( + objective = "regression" + , num_threads = .LGB_MAX_THREADS + ) + , verbose = .LGB_VERBOSITY + , nrounds = 3L + ) + X_double <- X[c(1L, 51L, 101L), , drop = FALSE] + X_integer <- X_double + storage.mode(X_double) <- "double" + pred_integer <- predict(fit, X_integer) + pred_double <- predict(fit, X_double) + expect_equal(pred_integer, pred_double) +}) + +test_that("start_iteration works correctly", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + train <- agaricus.train + test <- agaricus.test + dtrain <- lgb.Dataset( + agaricus.train$data + , label = agaricus.train$label + ) + dtest <- lgb.Dataset.create.valid( + dtrain + , agaricus.test$data + , label = agaricus.test$label + ) + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + num_leaves = 4L + , learning_rate = 0.6 + , objective = "binary" + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 50L + , valids = list("test" = dtest) + , early_stopping_rounds = 2L + ) + expect_true(.is_Booster(bst)) + pred1 <- predict(bst, newdata = test$data, type = "raw") + pred_contrib1 <- predict(bst, test$data, type = "contrib") + pred2 <- rep(0.0, length(pred1)) + pred_contrib2 <- rep(0.0, length(pred2)) + step <- 11L + end_iter <- 49L + if (bst$best_iter != -1L) { + end_iter <- bst$best_iter - 1L + } + start_iters <- seq(0L, end_iter, by = step) + for (start_iter in start_iters) { + n_iter <- min(c(end_iter - start_iter + 1L, step)) + inc_pred <- predict(bst, test$data + , start_iteration = start_iter + , num_iteration = n_iter + , type = "raw" + ) + inc_pred_contrib <- bst$predict(test$data + , start_iteration = start_iter + , num_iteration = n_iter + , predcontrib = TRUE + ) + pred2 <- pred2 + inc_pred + pred_contrib2 <- pred_contrib2 + inc_pred_contrib + } + expect_equal(pred2, pred1) + expect_equal(pred_contrib2, pred_contrib1) + + pred_leaf1 <- predict(bst, test$data, type = "leaf") + pred_leaf2 <- predict(bst, test$data, start_iteration = 0L, num_iteration = end_iter + 1L, type = "leaf") + expect_equal(pred_leaf1, pred_leaf2) +}) + +test_that("Feature contributions from sparse inputs produce sparse outputs", { + data(mtcars) + X <- as.matrix(mtcars[, -1L]) + y <- as.numeric(mtcars[, 1L]) + dtrain <- lgb.Dataset(X, label = y, params = list(max_bins = 5L)) + bst <- lgb.train( + data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(min_data_in_leaf = 5L, num_threads = .LGB_MAX_THREADS) + ) + + pred_dense <- predict(bst, X, type = "contrib") + + Xcsc <- as(X, "CsparseMatrix") + pred_csc <- predict(bst, Xcsc, type = "contrib") + expect_s4_class(pred_csc, "dgCMatrix") + expect_equal(unname(pred_dense), unname(as.matrix(pred_csc))) + + Xcsr <- as(X, "RsparseMatrix") + pred_csr <- predict(bst, Xcsr, type = "contrib") + expect_s4_class(pred_csr, "dgRMatrix") + expect_equal(as(pred_csr, "CsparseMatrix"), pred_csc) + + Xspv <- as(X[1L, , drop = FALSE], "sparseVector") + pred_spv <- predict(bst, Xspv, type = "contrib") + expect_s4_class(pred_spv, "dsparseVector") + expect_equal(Matrix::t(as(pred_spv, "CsparseMatrix")), unname(pred_csc[1L, , drop = FALSE])) +}) + +test_that("Sparse feature contribution predictions do not take inputs with wrong number of columns", { + data(mtcars) + X <- as.matrix(mtcars[, -1L]) + y <- as.numeric(mtcars[, 1L]) + dtrain <- lgb.Dataset(X, label = y, params = list(max_bins = 5L)) + bst <- lgb.train( + data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(min_data_in_leaf = 5L, num_threads = .LGB_MAX_THREADS) + ) + + X_wrong <- X[, c(1L:10L, 1L:10L)] + X_wrong <- as(X_wrong, "CsparseMatrix") + expect_error(predict(bst, X_wrong, type = "contrib"), regexp = "input data has 20 columns") + + X_wrong <- as(X_wrong, "RsparseMatrix") + expect_error(predict(bst, X_wrong, type = "contrib"), regexp = "input data has 20 columns") + + X_wrong <- as(X_wrong, "CsparseMatrix") + X_wrong <- X_wrong[, 1L:3L] + expect_error(predict(bst, X_wrong, type = "contrib"), regexp = "input data has 3 columns") +}) + +test_that("Feature contribution predictions do not take non-general CSR or CSC inputs", { + set.seed(123L) + y <- runif(25L) + Dmat <- matrix(runif(625L), nrow = 25L, ncol = 25L) + Dmat <- crossprod(Dmat) + Dmat <- as(Dmat, "symmetricMatrix") + SmatC <- as(Dmat, "sparseMatrix") + SmatR <- as(SmatC, "RsparseMatrix") + + dtrain <- lgb.Dataset(as.matrix(Dmat), label = y, params = list(max_bins = 5L)) + bst <- lgb.train( + data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(min_data_in_leaf = 5L, num_threads = .LGB_MAX_THREADS) + ) + + expect_error( + predict(bst, SmatC, type = "contrib") + , regexp = "Predictions on sparse inputs are only allowed for 'dsparseVector', 'dgRMatrix', 'dgCMatrix' - got: dsCMatrix" # nolint: line_length. + ) + expect_error( + predict(bst, SmatR, type = "contrib") + , regexp = "Predictions on sparse inputs are only allowed for 'dsparseVector', 'dgRMatrix', 'dgCMatrix' - got: dsRMatrix" # nolint: line_length. + ) +}) + +test_that("predict() params should override keyword argument for raw-score predictions", { + data(agaricus.train, package = "lightgbm") + X <- agaricus.train$data + y <- agaricus.train$label + bst <- lgb.train( + data = lgb.Dataset( + data = X + , label = y + , params = list( + data_seed = 708L + , min_data_in_bin = 5L + ) + ) + , params = list( + objective = "binary" + , min_data_in_leaf = 1L + , seed = 708L + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 10L + , verbose = .LGB_VERBOSITY + ) + + # check that the predictions from predict.lgb.Booster() really look like raw score predictions + preds_prob <- predict(bst, X) + preds_raw_s3_keyword <- predict(bst, X, type = "raw") + preds_prob_from_raw <- 1.0 / (1.0 + exp(-preds_raw_s3_keyword)) + expect_equal(preds_prob, preds_prob_from_raw, tolerance = .LGB_NUMERIC_TOLERANCE) + accuracy <- sum(as.integer(preds_prob_from_raw > 0.5) == y) / length(y) + expect_equal(accuracy, 1.0) + + # should get the same results from Booster$predict() method + preds_raw_r6_keyword <- bst$predict(X, rawscore = TRUE) + expect_equal(preds_raw_s3_keyword, preds_raw_r6_keyword) + + # using a parameter alias of predict_raw_score should result in raw scores being returned + aliases <- .PARAMETER_ALIASES()[["predict_raw_score"]] + expect_true(length(aliases) > 1L) + for (rawscore_alias in aliases) { + params <- as.list( + stats::setNames( + object = TRUE + , nm = rawscore_alias + ) + ) + preds_raw_s3_param <- predict(bst, X, params = params) + preds_raw_r6_param <- bst$predict(X, params = params) + expect_equal(preds_raw_s3_keyword, preds_raw_s3_param) + expect_equal(preds_raw_s3_keyword, preds_raw_r6_param) + } +}) + +test_that("predict() params should override keyword argument for leaf-index predictions", { + data(mtcars) + X <- as.matrix(mtcars[, which(names(mtcars) != "mpg")]) + y <- as.numeric(mtcars[, "mpg"]) + bst <- lgb.train( + data = lgb.Dataset( + data = X + , label = y + , params = list( + min_data_in_bin = 1L + , data_seed = 708L + ) + ) + , params = list( + objective = "regression" + , min_data_in_leaf = 1L + , seed = 708L + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 10L + , verbose = .LGB_VERBOSITY + ) + + # check that predictions really look like leaf index predictions + preds_leaf_s3_keyword <- predict(bst, X, type = "leaf") + expect_true(is.matrix(preds_leaf_s3_keyword)) + expect_equal(dim(preds_leaf_s3_keyword), c(nrow(X), bst$current_iter())) + expect_true(min(preds_leaf_s3_keyword) >= 0L) + trees_dt <- lgb.model.dt.tree(bst) + max_leaf_by_tree_from_dt <- trees_dt[, .(idx = max(leaf_index, na.rm = TRUE)), by = tree_index]$idx + max_leaf_by_tree_from_preds <- apply(preds_leaf_s3_keyword, 2L, max, na.rm = TRUE) + expect_equal(max_leaf_by_tree_from_dt, max_leaf_by_tree_from_preds) + + # should get the same results from Booster$predict() method + preds_leaf_r6_keyword <- bst$predict(X, predleaf = TRUE) + expect_equal(preds_leaf_s3_keyword, preds_leaf_r6_keyword) + + # using a parameter alias of predict_leaf_index should result in leaf indices being returned + aliases <- .PARAMETER_ALIASES()[["predict_leaf_index"]] + expect_true(length(aliases) > 1L) + for (predleaf_alias in aliases) { + params <- as.list( + stats::setNames( + object = TRUE + , nm = predleaf_alias + ) + ) + preds_leaf_s3_param <- predict(bst, X, params = params) + preds_leaf_r6_param <- bst$predict(X, params = params) + expect_equal(preds_leaf_s3_keyword, preds_leaf_s3_param) + expect_equal(preds_leaf_s3_keyword, preds_leaf_r6_param) + } +}) + +test_that("predict() params should override keyword argument for feature contributions", { + data(mtcars) + X <- as.matrix(mtcars[, which(names(mtcars) != "mpg")]) + y <- as.numeric(mtcars[, "mpg"]) + bst <- lgb.train( + data = lgb.Dataset( + data = X + , label = y + , params = list( + min_data_in_bin = 1L + , data_seed = 708L + ) + ) + , params = list( + objective = "regression" + , min_data_in_leaf = 1L + , seed = 708L + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 10L + , verbose = .LGB_VERBOSITY + ) + + # check that predictions really look like feature contributions + preds_contrib_s3_keyword <- predict(bst, X, type = "contrib") + num_features <- ncol(X) + shap_base_value <- unname(preds_contrib_s3_keyword[, ncol(preds_contrib_s3_keyword)]) + expect_true(is.matrix(preds_contrib_s3_keyword)) + expect_equal(dim(preds_contrib_s3_keyword), c(nrow(X), num_features + 1L)) + expect_equal(length(unique(shap_base_value)), 1L) + expect_equal(mean(y), shap_base_value[1L]) + expect_equal(predict(bst, X), rowSums(preds_contrib_s3_keyword)) + + # should get the same results from Booster$predict() method + preds_contrib_r6_keyword <- bst$predict(X, predcontrib = TRUE) + expect_equal(preds_contrib_s3_keyword, preds_contrib_r6_keyword) + + # using a parameter alias of predict_contrib should result in feature contributions being returned + aliases <- .PARAMETER_ALIASES()[["predict_contrib"]] + expect_true(length(aliases) > 1L) + for (predcontrib_alias in aliases) { + params <- as.list( + stats::setNames( + object = TRUE + , nm = predcontrib_alias + ) + ) + preds_contrib_s3_param <- predict(bst, X, params = params) + preds_contrib_r6_param <- bst$predict(X, params = params) + expect_equal(preds_contrib_s3_keyword, preds_contrib_s3_param) + expect_equal(preds_contrib_s3_keyword, preds_contrib_r6_param) + } +}) + +.expect_has_row_names <- function(pred, X) { + if (is.vector(pred)) { + rnames <- names(pred) + } else { + rnames <- row.names(pred) + } + expect_false(is.null(rnames)) + expect_true(is.vector(rnames)) + expect_true(length(rnames) > 0L) + expect_equal(row.names(X), rnames) +} + +.expect_doesnt_have_row_names <- function(pred) { + if (is.vector(pred)) { + expect_null(names(pred)) + } else { + expect_null(row.names(pred)) + } +} + +.check_all_row_name_expectations <- function(bst, X) { + + # dense matrix with row names + pred <- predict(bst, X) + .expect_has_row_names(pred, X) + pred <- predict(bst, X, type = "raw") + .expect_has_row_names(pred, X) + pred <- predict(bst, X, type = "leaf") + .expect_has_row_names(pred, X) + pred <- predict(bst, X, type = "contrib") + .expect_has_row_names(pred, X) + + # dense matrix without row names + Xcopy <- X + row.names(Xcopy) <- NULL + pred <- predict(bst, Xcopy) + .expect_doesnt_have_row_names(pred) + + # sparse matrix with row names + Xcsc <- as(X, "CsparseMatrix") + pred <- predict(bst, Xcsc) + .expect_has_row_names(pred, Xcsc) + pred <- predict(bst, Xcsc, type = "raw") + .expect_has_row_names(pred, Xcsc) + pred <- predict(bst, Xcsc, type = "leaf") + .expect_has_row_names(pred, Xcsc) + pred <- predict(bst, Xcsc, type = "contrib") + .expect_has_row_names(pred, Xcsc) + pred <- predict(bst, as(Xcsc, "RsparseMatrix"), type = "contrib") + .expect_has_row_names(pred, Xcsc) + + # sparse matrix without row names + Xcopy <- Xcsc + row.names(Xcopy) <- NULL + pred <- predict(bst, Xcopy) + .expect_doesnt_have_row_names(pred) +} + +test_that("predict() keeps row names from data (regression)", { + data("mtcars") + X <- as.matrix(mtcars[, -1L]) + y <- as.numeric(mtcars[, 1L]) + dtrain <- lgb.Dataset( + X + , label = y + , params = list( + max_bins = 5L + , min_data_in_bin = 1L + ) + ) + bst <- lgb.train( + data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(min_data_in_leaf = 1L, num_threads = .LGB_MAX_THREADS) + ) + .check_all_row_name_expectations(bst, X) +}) + +test_that("predict() keeps row names from data (binary classification)", { + data(agaricus.train, package = "lightgbm") + X <- as.matrix(agaricus.train$data) + y <- agaricus.train$label + row.names(X) <- paste0("rname", seq(1L, nrow(X))) + dtrain <- lgb.Dataset(X, label = y, params = list(max_bins = 5L)) + bst <- lgb.train( + data = dtrain + , obj = "binary" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_threads = .LGB_MAX_THREADS) + ) + .check_all_row_name_expectations(bst, X) +}) + +test_that("predict() keeps row names from data (multi-class classification)", { + data(iris) + y <- as.numeric(iris$Species) - 1.0 + X <- as.matrix(iris[, names(iris) != "Species"]) + row.names(X) <- paste0("rname", seq(1L, nrow(X))) + dtrain <- lgb.Dataset(X, label = y, params = list(max_bins = 5L)) + bst <- lgb.train( + data = dtrain + , obj = "multiclass" + , params = list(num_class = 3L, num_threads = .LGB_MAX_THREADS) + , nrounds = 5L + , verbose = .LGB_VERBOSITY + ) + .check_all_row_name_expectations(bst, X) +}) + +test_that("predictions for regression and binary classification are returned as vectors", { + data(mtcars) + X <- as.matrix(mtcars[, -1L]) + y <- as.numeric(mtcars[, 1L]) + dtrain <- lgb.Dataset( + X + , label = y + , params = list( + max_bins = 5L + , min_data_in_bin = 1L + ) + ) + model <- lgb.train( + data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(min_data_in_leaf = 1L, num_threads = .LGB_MAX_THREADS) + ) + pred <- predict(model, X) + expect_true(is.vector(pred)) + expect_equal(length(pred), nrow(X)) + pred <- predict(model, X, type = "raw") + expect_true(is.vector(pred)) + expect_equal(length(pred), nrow(X)) + + data(agaricus.train, package = "lightgbm") + X <- agaricus.train$data + y <- agaricus.train$label + dtrain <- lgb.Dataset(X, label = y) + model <- lgb.train( + data = dtrain + , obj = "binary" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_threads = .LGB_MAX_THREADS) + ) + pred <- predict(model, X) + expect_true(is.vector(pred)) + expect_equal(length(pred), nrow(X)) + pred <- predict(model, X, type = "raw") + expect_true(is.vector(pred)) + expect_equal(length(pred), nrow(X)) +}) + +test_that("predictions for multiclass classification are returned as matrix", { + data(iris) + X <- as.matrix(iris[, -5L]) + y <- as.numeric(iris$Species) - 1.0 + dtrain <- lgb.Dataset(X, label = y) + model <- lgb.train( + data = dtrain + , obj = "multiclass" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_class = 3L, num_threads = .LGB_MAX_THREADS) + ) + pred <- predict(model, X) + expect_true(is.matrix(pred)) + expect_equal(nrow(pred), nrow(X)) + expect_equal(ncol(pred), 3L) + pred <- predict(model, X, type = "raw") + expect_true(is.matrix(pred)) + expect_equal(nrow(pred), nrow(X)) + expect_equal(ncol(pred), 3L) +}) + +test_that("Single-row predictions are identical to multi-row ones", { + data(mtcars) + X <- as.matrix(mtcars[, -1L]) + y <- mtcars[, 1L] + dtrain <- lgb.Dataset(X, label = y, params = list(max_bin = 5L)) + params <- list(min_data_in_leaf = 2L, num_threads = .LGB_MAX_THREADS) + model <- lgb.train( + params = params + , data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = -1L + ) + + x1 <- X[1L, , drop = FALSE] + x11 <- X[11L, , drop = FALSE] + x1_spv <- as(x1, "sparseVector") + x11_spv <- as(x11, "sparseVector") + x1_csr <- as(x1, "RsparseMatrix") + x11_csr <- as(x11, "RsparseMatrix") + + pred_all <- predict(model, X) + pred1_wo_config <- predict(model, x1) + pred11_wo_config <- predict(model, x11) + pred1_spv_wo_config <- predict(model, x1_spv) + pred11_spv_wo_config <- predict(model, x11_spv) + pred1_csr_wo_config <- predict(model, x1_csr) + pred11_csr_wo_config <- predict(model, x11_csr) + + lgb.configure_fast_predict(model) + pred1_w_config <- predict(model, x1) + pred11_w_config <- predict(model, x11) + + model <- lgb.train( + params = params + , data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = -1L + ) + lgb.configure_fast_predict(model, csr = TRUE) + pred1_spv_w_config <- predict(model, x1_spv) + pred11_spv_w_config <- predict(model, x11_spv) + pred1_csr_w_config <- predict(model, x1_csr) + pred11_csr_w_config <- predict(model, x11_csr) + + expect_equal(pred1_wo_config, pred_all[1L]) + expect_equal(pred11_wo_config, pred_all[11L]) + expect_equal(pred1_spv_wo_config, unname(pred_all[1L])) + expect_equal(pred11_spv_wo_config, unname(pred_all[11L])) + expect_equal(pred1_csr_wo_config, pred_all[1L]) + expect_equal(pred11_csr_wo_config, pred_all[11L]) + + expect_equal(pred1_w_config, pred_all[1L]) + expect_equal(pred11_w_config, pred_all[11L]) + expect_equal(pred1_spv_w_config, unname(pred_all[1L])) + expect_equal(pred11_spv_w_config, unname(pred_all[11L])) + expect_equal(pred1_csr_w_config, pred_all[1L]) + expect_equal(pred11_csr_w_config, pred_all[11L]) +}) + +test_that("Fast-predict configuration accepts non-default prediction types", { + data(mtcars) + X <- as.matrix(mtcars[, -1L]) + y <- mtcars[, 1L] + dtrain <- lgb.Dataset(X, label = y, params = list(max_bin = 5L)) + params <- list(min_data_in_leaf = 2L, num_threads = .LGB_MAX_THREADS) + model <- lgb.train( + params = params + , data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = -1L + ) + + x1 <- X[1L, , drop = FALSE] + x11 <- X[11L, , drop = FALSE] + + pred_all <- predict(model, X, type = "leaf") + pred1_wo_config <- predict(model, x1, type = "leaf") + pred11_wo_config <- predict(model, x11, type = "leaf") + expect_equal(pred1_wo_config, pred_all[1L, , drop = FALSE]) + expect_equal(pred11_wo_config, pred_all[11L, , drop = FALSE]) + + lgb.configure_fast_predict(model, type = "leaf") + pred1_w_config <- predict(model, x1, type = "leaf") + pred11_w_config <- predict(model, x11, type = "leaf") + expect_equal(pred1_w_config, pred_all[1L, , drop = FALSE]) + expect_equal(pred11_w_config, pred_all[11L, , drop = FALSE]) +}) + +test_that("Fast-predict configuration does not block other prediction types", { + data(mtcars) + X <- as.matrix(mtcars[, -1L]) + y <- mtcars[, 1L] + dtrain <- lgb.Dataset(X, label = y, params = list(max_bin = 5L)) + params <- list(min_data_in_leaf = 2L, num_threads = .LGB_MAX_THREADS) + model <- lgb.train( + params = params + , data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = -1L + ) + + x1 <- X[1L, , drop = FALSE] + x11 <- X[11L, , drop = FALSE] + + pred_all <- predict(model, X) + pred_all_leaf <- predict(model, X, type = "leaf") + + lgb.configure_fast_predict(model) + pred1_w_config <- predict(model, x1) + pred11_w_config <- predict(model, x11) + pred1_leaf_w_config <- predict(model, x1, type = "leaf") + pred11_leaf_w_config <- predict(model, x11, type = "leaf") + + expect_equal(pred1_w_config, pred_all[1L]) + expect_equal(pred11_w_config, pred_all[11L]) + expect_equal(pred1_leaf_w_config, pred_all_leaf[1L, , drop = FALSE]) + expect_equal(pred11_leaf_w_config, pred_all_leaf[11L, , drop = FALSE]) +}) + +test_that("predict type='class' returns predicted class for classification objectives", { + data(agaricus.train, package = "lightgbm") + X <- as.matrix(agaricus.train$data) + y <- agaricus.train$label + dtrain <- lgb.Dataset(X, label = y, params = list(max_bins = 5L)) + bst <- lgb.train( + data = dtrain + , obj = "binary" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_threads = .LGB_MAX_THREADS) + ) + pred <- predict(bst, X, type = "class") + expect_true(all(pred %in% c(0L, 1L))) + + data(iris) + X <- as.matrix(iris[, -5L]) + y <- as.numeric(iris$Species) - 1.0 + dtrain <- lgb.Dataset(X, label = y) + model <- lgb.train( + data = dtrain + , obj = "multiclass" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_class = 3L, num_threads = .LGB_MAX_THREADS) + ) + pred <- predict(model, X, type = "class") + expect_true(all(pred %in% c(0L, 1L, 2L))) +}) + +test_that("predict type='class' returns values in the target's range for regression objectives", { + data(agaricus.train, package = "lightgbm") + X <- as.matrix(agaricus.train$data) + y <- agaricus.train$label + dtrain <- lgb.Dataset(X, label = y, params = list(max_bins = 5L)) + bst <- lgb.train( + data = dtrain + , obj = "regression" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_threads = .LGB_MAX_THREADS) + ) + pred <- predict(bst, X, type = "class") + expect_true(!any(pred %in% c(0.0, 1.0))) +}) diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R new file mode 100644 index 0000000..4029b7b --- /dev/null +++ b/R-package/tests/testthat/test_basic.R @@ -0,0 +1,3863 @@ +data(agaricus.train, package = "lightgbm") +data(agaricus.test, package = "lightgbm") +train <- agaricus.train +test <- agaricus.test + +set.seed(708L) + +# [description] Every time this function is called, it adds 0.1 +# to an accumulator then returns the current value. +# This is used to mock the situation where an evaluation +# metric increases every iteration +ACCUMULATOR_NAME <- "INCREASING_METRIC_ACCUMULATOR" +assign(x = ACCUMULATOR_NAME, value = 0.0, envir = .GlobalEnv) + +.increasing_metric <- function(preds, dtrain) { + if (!exists(ACCUMULATOR_NAME, envir = .GlobalEnv)) { + assign(ACCUMULATOR_NAME, 0.0, envir = .GlobalEnv) + } + assign( + x = ACCUMULATOR_NAME + , value = get(ACCUMULATOR_NAME, envir = .GlobalEnv) + 0.1 + , envir = .GlobalEnv + ) + return(list( + name = "increasing_metric" + , value = get(ACCUMULATOR_NAME, envir = .GlobalEnv) + , higher_better = TRUE + )) +} + +# [description] Evaluation function that always returns the +# same value +CONSTANT_METRIC_VALUE <- 0.2 +.constant_metric <- function(preds, dtrain) { + return(list( + name = "constant_metric" + , value = CONSTANT_METRIC_VALUE + , higher_better = FALSE + )) +} + +# sample datasets to test early stopping +DTRAIN_RANDOM_REGRESSION <- lgb.Dataset( + data = as.matrix(rnorm(100L), ncol = 1L, drop = FALSE) + , label = rnorm(100L) + , params = list(num_threads = .LGB_MAX_THREADS) +) +DVALID_RANDOM_REGRESSION <- lgb.Dataset( + data = as.matrix(rnorm(50L), ncol = 1L, drop = FALSE) + , label = rnorm(50L) + , params = list(num_threads = .LGB_MAX_THREADS) +) +DTRAIN_RANDOM_CLASSIFICATION <- lgb.Dataset( + data = as.matrix(rnorm(120L), ncol = 1L, drop = FALSE) + , label = sample(c(0L, 1L), size = 120L, replace = TRUE) + , params = list(num_threads = .LGB_MAX_THREADS) +) +DVALID_RANDOM_CLASSIFICATION <- lgb.Dataset( + data = as.matrix(rnorm(37L), ncol = 1L, drop = FALSE) + , label = sample(c(0L, 1L), size = 37L, replace = TRUE) + , params = list(num_threads = .LGB_MAX_THREADS) +) + +test_that("train and predict binary classification", { + nrounds <- 10L + bst <- lightgbm( + data = train$data + , label = train$label + , params = list( + num_leaves = 5L + , objective = "binary" + , metric = "binary_error" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + , valids = list( + "train" = lgb.Dataset( + data = train$data + , label = train$label + ) + ) + ) + expect_false(is.null(bst$record_evals)) + record_results <- lgb.get.eval.result(bst, "train", "binary_error") + expect_lt(min(record_results), 0.02) + + pred <- predict(bst, test$data) + expect_equal(length(pred), 1611L) + + pred1 <- predict(bst, train$data, num_iteration = 1L) + expect_equal(length(pred1), 6513L) + err_pred1 <- sum((pred1 > 0.5) != train$label) / length(train$label) + err_log <- record_results[1L] + expect_lt(abs(err_pred1 - err_log), .LGB_NUMERIC_TOLERANCE) +}) + + +test_that("train and predict softmax", { + set.seed(708L) + X_mat <- as.matrix(iris[, -5L]) + lb <- as.numeric(iris$Species) - 1L + + bst <- lightgbm( + data = X_mat + , label = lb + , params = list( + num_leaves = 4L + , learning_rate = 0.05 + , min_data = 20L + , min_hessian = 10.0 + , objective = "multiclass" + , metric = "multi_error" + , num_class = 3L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 20L + , valids = list( + "train" = lgb.Dataset( + data = X_mat + , label = lb + ) + ) + ) + + expect_false(is.null(bst$record_evals)) + record_results <- lgb.get.eval.result(bst, "train", "multi_error") + expect_lt(min(record_results), 0.06) + + pred <- predict(bst, as.matrix(iris[, -5L])) + expect_equal(length(pred), nrow(iris) * 3L) +}) + + +test_that("use of multiple eval metrics works", { + metrics <- list("binary_error", "auc", "binary_logloss") + bst <- lightgbm( + data = train$data + , label = train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , metric = metrics + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 10L + , valids = list( + "train" = lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + ) + ) + expect_false(is.null(bst$record_evals)) + expect_named( + bst$record_evals[["train"]] + , unlist(metrics) + , ignore.order = FALSE + , ignore.case = FALSE + ) +}) + +test_that("lgb.Booster.upper_bound() and lgb.Booster.lower_bound() work as expected for binary classification", { + set.seed(708L) + nrounds <- 10L + bst <- lightgbm( + data = train$data + , label = train$label + , params = list( + num_leaves = 5L + , objective = "binary" + , metric = "binary_error" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + ) + expect_true(abs(bst$lower_bound() - -1.590853) < .LGB_NUMERIC_TOLERANCE) + expect_true(abs(bst$upper_bound() - 1.871015) < .LGB_NUMERIC_TOLERANCE) +}) + +test_that("lgb.Booster.upper_bound() and lgb.Booster.lower_bound() work as expected for regression", { + set.seed(708L) + nrounds <- 10L + bst <- lightgbm( + data = train$data + , label = train$label + , params = list( + num_leaves = 5L + , objective = "regression" + , metric = "l2" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + ) + expect_true(abs(bst$lower_bound() - 0.1513859) < .LGB_NUMERIC_TOLERANCE) + expect_true(abs(bst$upper_bound() - 0.9080349) < .LGB_NUMERIC_TOLERANCE) +}) + +test_that("lightgbm() rejects negative or 0 value passed to nrounds", { + dtrain <- lgb.Dataset(train$data, label = train$label) + params <- list(objective = "regression", metric = "l2,l1", num_threads = .LGB_MAX_THREADS) + for (nround_value in c(-10L, 0L)) { + expect_error({ + bst <- lightgbm( + data = dtrain + , params = params + , nrounds = nround_value + ) + }, "nrounds should be greater than zero") + } +}) + +test_that("lightgbm() accepts nrounds as either a top-level argument or parameter", { + nrounds <- 15L + + set.seed(708L) + top_level_bst <- lightgbm( + data = train$data + , label = train$label + , nrounds = nrounds + , params = list( + objective = "regression" + , metric = "l2" + , num_leaves = 5L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + + set.seed(708L) + param_bst <- lightgbm( + data = train$data + , label = train$label + , params = list( + objective = "regression" + , metric = "l2" + , num_leaves = 5L + , nrounds = nrounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + + set.seed(708L) + both_customized <- lightgbm( + data = train$data + , label = train$label + , nrounds = 20L + , params = list( + objective = "regression" + , metric = "l2" + , num_leaves = 5L + , nrounds = nrounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + + top_level_l2 <- top_level_bst$eval_train()[[1L]][["value"]] + params_l2 <- param_bst$eval_train()[[1L]][["value"]] + both_l2 <- both_customized$eval_train()[[1L]][["value"]] + + # check type just to be sure the subsetting didn't return a NULL + expect_true(is.numeric(top_level_l2)) + expect_true(is.numeric(params_l2)) + expect_true(is.numeric(both_l2)) + + # check that model produces identical performance + expect_identical(top_level_l2, params_l2) + expect_identical(both_l2, params_l2) + + expect_identical(param_bst$current_iter(), top_level_bst$current_iter()) + expect_identical(param_bst$current_iter(), both_customized$current_iter()) + expect_identical(param_bst$current_iter(), nrounds) + +}) + +test_that("lightgbm() performs evaluation on validation sets if they are provided", { + set.seed(708L) + dvalid1 <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid2 <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + bst <- lightgbm( + data = train$data + , label = train$label + , params = list( + num_leaves = 5L + , objective = "binary" + , metric = c( + "binary_error" + , "auc" + ) + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + , valids = list( + "valid1" = dvalid1 + , "valid2" = dvalid2 + , "train" = lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + ) + ) + + expect_named( + bst$record_evals + , c("train", "valid1", "valid2", "start_iter") + , ignore.order = TRUE + , ignore.case = FALSE + ) + for (valid_name in c("train", "valid1", "valid2")) { + eval_results <- bst$record_evals[[valid_name]][["binary_error"]] + expect_length(eval_results[["eval"]], nrounds) + } + expect_true(abs(bst$record_evals[["train"]][["binary_error"]][["eval"]][[1L]] - 0.02226317) < .LGB_NUMERIC_TOLERANCE) + expect_true(abs(bst$record_evals[["valid1"]][["binary_error"]][["eval"]][[1L]] - 0.02226317) < .LGB_NUMERIC_TOLERANCE) + expect_true(abs(bst$record_evals[["valid2"]][["binary_error"]][["eval"]][[1L]] - 0.02226317) < .LGB_NUMERIC_TOLERANCE) +}) + +test_that("training continuation works", { + dtrain <- lgb.Dataset( + train$data + , label = train$label + , free_raw_data = FALSE + , params = list(num_threads = .LGB_MAX_THREADS) + ) + watchlist <- list(train = dtrain) + param <- list( + objective = "binary" + , metric = "binary_logloss" + , num_leaves = 5L + , learning_rate = 1.0 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + + # train for 10 consecutive iterations + bst <- lgb.train(param, dtrain, nrounds = 10L, watchlist) + err_bst <- lgb.get.eval.result(bst, "train", "binary_logloss", 10L) + + # train for 5 iterations, save, load, train for 5 more + bst1 <- lgb.train(param, dtrain, nrounds = 5L, watchlist) + model_file <- tempfile(fileext = ".model") + lgb.save(bst1, model_file) + bst2 <- lgb.train(param, dtrain, nrounds = 5L, watchlist, init_model = bst1) + err_bst2 <- lgb.get.eval.result(bst2, "train", "binary_logloss", 10L) + + # evaluation metrics should be nearly identical for the model trained in 10 coonsecutive + # iterations and the one trained in 5-then-5. + expect_lt(abs(err_bst - err_bst2), 0.01) +}) + +test_that("cv works", { + dtrain <- lgb.Dataset(train$data, label = train$label) + params <- list( + objective = "regression" + , metric = "l2,l1" + , min_data = 1L + , learning_rate = 1.0 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- lgb.cv( + params + , dtrain + , 10L + , nfold = 5L + , early_stopping_rounds = 10L + ) + expect_false(is.null(bst$record_evals)) +}) + +test_that("CVBooster$reset_parameter() works as expected", { + dtrain <- lgb.Dataset(train$data, label = train$label) + n_folds <- 2L + cv_bst <- lgb.cv( + params = list( + objective = "regression" + , min_data = 1L + , num_leaves = 7L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = 3L + , nfold = n_folds + ) + expect_true(methods::is(cv_bst, "lgb.CVBooster")) + expect_length(cv_bst$boosters, n_folds) + for (bst in cv_bst$boosters) { + expect_equal(bst[["booster"]]$params[["num_leaves"]], 7L) + } + cv_bst$reset_parameter(list(num_leaves = 11L)) + for (bst in cv_bst$boosters) { + expect_equal(bst[["booster"]]$params[["num_leaves"]], 11L) + } +}) + +test_that("lgb.cv() rejects negative or 0 value passed to nrounds", { + dtrain <- lgb.Dataset(train$data, label = train$label, params = list(num_threads = 2L)) + params <- list( + objective = "regression" + , metric = "l2,l1" + , min_data = 1L + , num_threads = .LGB_MAX_THREADS + ) + for (nround_value in c(-10L, 0L)) { + expect_error({ + bst <- lgb.cv( + params + , dtrain + , nround_value + , nfold = 5L + ) + }, "nrounds should be greater than zero") + } +}) + +test_that("lgb.cv() throws an informative error if 'data' is not an lgb.Dataset", { + bad_values <- list( + 4L + , "hello" + , list(a = TRUE, b = seq_len(10L)) + , data.frame(x = seq_len(5L), y = seq_len(5L)) + , data.table::data.table(x = seq_len(5L), y = seq_len(5L)) + , matrix(data = seq_len(10L), 2L, 5L) + ) + for (val in bad_values) { + expect_error({ + bst <- lgb.cv( + params = list( + objective = "regression" + , metric = "l2,l1" + , min_data = 1L + ) + , data = val + , 10L + , nfold = 5L + ) + }, regexp = "lgb.cv: data must be an lgb.Dataset instance", fixed = TRUE) + } +}) + +test_that("lightgbm.cv() gives the correct best_score and best_iter for a metric where higher values are better", { + set.seed(708L) + dtrain <- lgb.Dataset( + data = as.matrix(runif(n = 500L, min = 0.0, max = 15.0), drop = FALSE) + , label = rep(c(0L, 1L), 250L) + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + cv_bst <- lgb.cv( + data = dtrain + , nfold = 5L + , nrounds = nrounds + , params = list( + objective = "binary" + , metric = "auc,binary_error" + , learning_rate = 1.5 + , num_leaves = 5L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + expect_true(methods::is(cv_bst, "lgb.CVBooster")) + expect_named( + cv_bst$record_evals + , c("start_iter", "valid") + , ignore.order = FALSE + , ignore.case = FALSE + ) + auc_scores <- unlist(cv_bst$record_evals[["valid"]][["auc"]][["eval"]]) + expect_length(auc_scores, nrounds) + expect_identical(cv_bst$best_iter, which.max(auc_scores)) + expect_identical(cv_bst$best_score, auc_scores[which.max(auc_scores)]) +}) + +test_that("lgb.cv() fit on linearly-relatead data improves when using linear learners", { + set.seed(708L) + .new_dataset <- function() { + X <- matrix(rnorm(1000L), ncol = 1L) + return(lgb.Dataset( + data = X + , label = 2L * X + runif(nrow(X), 0L, 0.1) + , params = list(num_threads = .LGB_MAX_THREADS) + )) + } + + params <- list( + objective = "regression" + , verbose = -1L + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , num_threads = .LGB_MAX_THREADS + ) + + dtrain <- .new_dataset() + cv_bst <- lgb.cv( + data = dtrain + , nrounds = 10L + , params = params + , nfold = 5L + ) + expect_true(methods::is(cv_bst, "lgb.CVBooster")) + + dtrain <- .new_dataset() + cv_bst_linear <- lgb.cv( + data = dtrain + , nrounds = 10L + , params = utils::modifyList(params, list(linear_tree = TRUE)) + , nfold = 5L + ) + expect_true(methods::is(cv_bst_linear, "lgb.CVBooster")) + + expect_true(cv_bst_linear$best_score < cv_bst$best_score) +}) + +test_that("lgb.cv() respects showsd argument", { + dtrain <- lgb.Dataset(train$data, label = train$label, params = list(num_threads = .LGB_MAX_THREADS)) + params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + nrounds <- 5L + set.seed(708L) + bst_showsd <- lgb.cv( + params = params + , data = dtrain + , nrounds = nrounds + , nfold = 3L + , showsd = TRUE + ) + evals_showsd <- bst_showsd$record_evals[["valid"]][["l2"]] + set.seed(708L) + bst_no_showsd <- lgb.cv( + params = params + , data = dtrain + , nrounds = nrounds + , nfold = 3L + , showsd = FALSE + ) + evals_no_showsd <- bst_no_showsd$record_evals[["valid"]][["l2"]] + expect_equal( + evals_showsd[["eval"]] + , evals_no_showsd[["eval"]] + ) + expect_true(methods::is(evals_showsd[["eval_err"]], "list")) + expect_equal(length(evals_showsd[["eval_err"]]), nrounds) + expect_identical(evals_no_showsd[["eval_err"]], list()) +}) + +test_that("lgb.cv() raises an informative error for unrecognized objectives", { + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + expect_error({ + capture.output({ + bst <- lgb.cv( + data = dtrain + , params = list( + objective_type = "not_a_real_objective" + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + }, type = "message") + }, regexp = "Unknown objective type name: not_a_real_objective") +}) + +test_that("lgb.cv() respects parameter aliases for objective", { + nrounds <- 3L + nfold <- 4L + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + cv_bst <- lgb.cv( + data = dtrain + , params = list( + num_leaves = 5L + , application = "binary" + , num_iterations = nrounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nfold = nfold + ) + expect_equal(cv_bst$best_iter, nrounds) + expect_named(cv_bst$record_evals[["valid"]], "binary_logloss") + expect_length(cv_bst$record_evals[["valid"]][["binary_logloss"]][["eval"]], nrounds) + expect_length(cv_bst$boosters, nfold) +}) + +test_that("lgb.cv() prefers objective in params to keyword argument", { + data("EuStockMarkets") + cv_bst <- lgb.cv( + data = lgb.Dataset( + data = EuStockMarkets[, c("SMI", "CAC", "FTSE")] + , label = EuStockMarkets[, "DAX"] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + , params = list( + application = "regression_l1" + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 5L + , obj = "regression_l2" + ) + for (bst_list in cv_bst$boosters) { + bst <- bst_list[["booster"]] + expect_equal(bst$params$objective, "regression_l1") + # NOTE: using save_model_to_string() since that is the simplest public API in the R-package + # allowing access to the "objective" attribute of the Booster object on the C++ side + model_txt_lines <- strsplit( + x = bst$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(model_txt_lines == "objective=regression_l1")) + expect_false(any(model_txt_lines == "objective=regression_l2")) + } +}) + +test_that("lgb.cv() respects parameter aliases for metric", { + nrounds <- 3L + nfold <- 4L + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + cv_bst <- lgb.cv( + data = dtrain + , params = list( + num_leaves = 5L + , objective = "binary" + , num_iterations = nrounds + , metric_types = c("auc", "binary_logloss") + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nfold = nfold + ) + expect_equal(cv_bst$best_iter, nrounds) + expect_named(cv_bst$record_evals[["valid"]], c("auc", "binary_logloss")) + expect_length(cv_bst$record_evals[["valid"]][["binary_logloss"]][["eval"]], nrounds) + expect_length(cv_bst$record_evals[["valid"]][["auc"]][["eval"]], nrounds) + expect_length(cv_bst$boosters, nfold) +}) + +test_that("lgb.cv() respects eval_train_metric argument", { + dtrain <- lgb.Dataset(train$data, label = train$label) + params <- list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + nrounds <- 5L + set.seed(708L) + bst_train <- lgb.cv( + params = params + , data = dtrain + , nrounds = nrounds + , nfold = 3L + , showsd = FALSE + , eval_train_metric = TRUE + ) + set.seed(708L) + bst_no_train <- lgb.cv( + params = params + , data = dtrain + , nrounds = nrounds + , nfold = 3L + , showsd = FALSE + , eval_train_metric = FALSE + ) + expect_equal( + bst_train$record_evals[["valid"]][["l2"]] + , bst_no_train$record_evals[["valid"]][["l2"]] + ) + expect_true("train" %in% names(bst_train$record_evals)) + expect_false("train" %in% names(bst_no_train$record_evals)) + expect_true(methods::is(bst_train$record_evals[["train"]][["l2"]][["eval"]], "list")) + expect_equal( + length(bst_train$record_evals[["train"]][["l2"]][["eval"]]) + , nrounds + ) +}) + +test_that("lgb.train() works as expected with multiple eval metrics", { + metrics <- c("binary_error", "auc", "binary_logloss") + bst <- lgb.train( + data = lgb.Dataset( + train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + , nrounds = 10L + , params = list( + objective = "binary" + , metric = metrics + , learning_rate = 1.0 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , valids = list( + "train" = lgb.Dataset( + train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + ) + ) + expect_false(is.null(bst$record_evals)) + expect_named( + bst$record_evals[["train"]] + , unlist(metrics) + , ignore.order = FALSE + , ignore.case = FALSE + ) +}) + +test_that("lgb.train() raises an informative error for unrecognized objectives", { + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + ) + expect_error({ + capture.output({ + bst <- lgb.train( + data = dtrain + , params = list( + objective_type = "not_a_real_objective" + , verbosity = .LGB_VERBOSITY + ) + ) + }, type = "message") + }, regexp = "Unknown objective type name: not_a_real_objective") +}) + +test_that("lgb.train() respects parameter aliases for objective", { + nrounds <- 3L + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + bst <- lgb.train( + data = dtrain + , params = list( + num_leaves = 5L + , application = "binary" + , num_iterations = nrounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , valids = list( + "the_training_data" = dtrain + ) + ) + expect_named(bst$record_evals[["the_training_data"]], "binary_logloss") + expect_length(bst$record_evals[["the_training_data"]][["binary_logloss"]][["eval"]], nrounds) + expect_equal(bst$params[["objective"]], "binary") +}) + +test_that("lgb.train() prefers objective in params to keyword argument", { + data("EuStockMarkets") + bst <- lgb.train( + data = lgb.Dataset( + data = EuStockMarkets[, c("SMI", "CAC", "FTSE")] + , label = EuStockMarkets[, "DAX"] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + , params = list( + loss = "regression_l1" + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 5L + , obj = "regression_l2" + ) + expect_equal(bst$params$objective, "regression_l1") + # NOTE: using save_model_to_string() since that is the simplest public API in the R-package + # allowing access to the "objective" attribute of the Booster object on the C++ side + model_txt_lines <- strsplit( + x = bst$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(model_txt_lines == "objective=regression_l1")) + expect_false(any(model_txt_lines == "objective=regression_l2")) +}) + +test_that("lgb.train() respects parameter aliases for metric", { + nrounds <- 3L + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + bst <- lgb.train( + data = dtrain + , params = list( + num_leaves = 5L + , objective = "binary" + , num_iterations = nrounds + , metric_types = c("auc", "binary_logloss") + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , valids = list( + "train" = dtrain + ) + ) + record_results <- bst$record_evals[["train"]] + expect_equal(sort(names(record_results)), c("auc", "binary_logloss")) + expect_length(record_results[["auc"]][["eval"]], nrounds) + expect_length(record_results[["binary_logloss"]][["eval"]], nrounds) + expect_equal(bst$params[["metric"]], list("auc", "binary_logloss")) +}) + +test_that("lgb.train() rejects negative or 0 value passed to nrounds", { + dtrain <- lgb.Dataset(train$data, label = train$label, params = list(num_threads = .LGB_MAX_THREADS)) + params <- list( + objective = "regression" + , metric = "l2,l1" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + for (nround_value in c(-10L, 0L)) { + expect_error({ + bst <- lgb.train( + params + , dtrain + , nround_value + ) + }, "nrounds should be greater than zero") + } +}) + + +test_that("lgb.train() accepts nrounds as either a top-level argument or parameter", { + nrounds <- 15L + + set.seed(708L) + top_level_bst <- lgb.train( + data = lgb.Dataset( + train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + , nrounds = nrounds + , params = list( + objective = "regression" + , metric = "l2" + , num_leaves = 5L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + + set.seed(708L) + param_bst <- lgb.train( + data = lgb.Dataset( + train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + , params = list( + objective = "regression" + , metric = "l2" + , num_leaves = 5L + , nrounds = nrounds + , verbose = .LGB_VERBOSITY + ) + ) + + set.seed(708L) + both_customized <- lgb.train( + data = lgb.Dataset( + train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + , nrounds = 20L + , params = list( + objective = "regression" + , metric = "l2" + , num_leaves = 5L + , nrounds = nrounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + + top_level_l2 <- top_level_bst$eval_train()[[1L]][["value"]] + params_l2 <- param_bst$eval_train()[[1L]][["value"]] + both_l2 <- both_customized$eval_train()[[1L]][["value"]] + + # check type just to be sure the subsetting didn't return a NULL + expect_true(is.numeric(top_level_l2)) + expect_true(is.numeric(params_l2)) + expect_true(is.numeric(both_l2)) + + # check that model produces identical performance + expect_identical(top_level_l2, params_l2) + expect_identical(both_l2, params_l2) + + expect_identical(param_bst$current_iter(), top_level_bst$current_iter()) + expect_identical(param_bst$current_iter(), both_customized$current_iter()) + expect_identical(param_bst$current_iter(), nrounds) + +}) + + +test_that("lgb.train() throws an informative error if 'data' is not an lgb.Dataset", { + bad_values <- list( + 4L + , "hello" + , list(a = TRUE, b = seq_len(10L)) + , data.frame(x = seq_len(5L), y = seq_len(5L)) + , data.table::data.table(x = seq_len(5L), y = seq_len(5L)) + , matrix(data = seq_len(10L), 2L, 5L) + ) + for (val in bad_values) { + expect_error({ + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "l2,l1" + , verbose = .LGB_VERBOSITY + ) + , data = val + , 10L + ) + }, regexp = "data must be an lgb.Dataset instance", fixed = TRUE) + } +}) + +test_that("lgb.train() throws an informative error if 'valids' is not a list of lgb.Dataset objects", { + valids <- list( + "valid1" = data.frame(x = rnorm(5L), y = rnorm(5L)) + , "valid2" = data.frame(x = rnorm(5L), y = rnorm(5L)) + ) + expect_error({ + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "l2,l1" + , verbose = .LGB_VERBOSITY + ) + , data = lgb.Dataset(train$data, label = train$label) + , 10L + , valids = valids + ) + }, regexp = "valids must be a list of lgb.Dataset elements") +}) + +test_that("lgb.train() errors if 'valids' is a list of lgb.Dataset objects but some do not have names", { + valids <- list( + "valid1" = lgb.Dataset(matrix(rnorm(10L), 5L, 2L)) + , lgb.Dataset(matrix(rnorm(10L), 2L, 5L)) + ) + expect_error({ + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "l2,l1" + , verbose = .LGB_VERBOSITY + ) + , data = lgb.Dataset(train$data, label = train$label) + , 10L + , valids = valids + ) + }, regexp = "each element of valids must have a name") +}) + +test_that("lgb.train() throws an informative error if 'valids' contains lgb.Dataset objects but none have names", { + valids <- list( + lgb.Dataset(matrix(rnorm(10L), 5L, 2L)) + , lgb.Dataset(matrix(rnorm(10L), 2L, 5L)) + ) + expect_error({ + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "l2,l1" + , verbose = .LGB_VERBOSITY + ) + , data = lgb.Dataset(train$data, label = train$label) + , 10L + , valids = valids + ) + }, regexp = "each element of valids must have a name") +}) + +test_that("lgb.train() works with force_col_wise and force_row_wise", { + set.seed(1234L) + nrounds <- 10L + dtrain <- lgb.Dataset( + train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + params <- list( + objective = "binary" + , metric = "binary_error" + , force_col_wise = TRUE + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst_col_wise <- lgb.train( + params = params + , data = dtrain + , nrounds = nrounds + ) + + params <- list( + objective = "binary" + , metric = "binary_error" + , force_row_wise = TRUE + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst_row_wise <- lgb.train( + params = params + , data = dtrain + , nrounds = nrounds + ) + + expected_error <- 0.003070782 + expect_equal(bst_col_wise$eval_train()[[1L]][["value"]], expected_error) + expect_equal(bst_row_wise$eval_train()[[1L]][["value"]], expected_error) + + # check some basic details of the boosters just to be sure force_col_wise + # and force_row_wise are not causing any weird side effects + for (bst in list(bst_row_wise, bst_col_wise)) { + expect_equal(bst$current_iter(), nrounds) + parsed_model <- jsonlite::fromJSON(bst$dump_model()) + expect_equal(parsed_model$objective, "binary sigmoid:1") + expect_false(parsed_model$average_output) + } +}) + +test_that("lgb.train() works as expected with sparse features", { + set.seed(708L) + num_obs <- 70000L + trainDF <- data.frame( + y = sample(c(0L, 1L), size = num_obs, replace = TRUE) + , x = sample(c(1.0:10.0, rep(NA_real_, 50L)), size = num_obs, replace = TRUE) + ) + dtrain <- lgb.Dataset( + data = as.matrix(trainDF[["x"]], drop = FALSE) + , label = trainDF[["y"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 1L + bst <- lgb.train( + params = list( + objective = "binary" + , min_data = 1L + , min_data_in_bin = 1L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + ) + + expect_true(.is_Booster(bst)) + expect_equal(bst$current_iter(), nrounds) + parsed_model <- jsonlite::fromJSON(bst$dump_model()) + expect_equal(parsed_model$objective, "binary sigmoid:1") + expect_false(parsed_model$average_output) + expected_error <- 0.6931268 + expect_true(abs(bst$eval_train()[[1L]][["value"]] - expected_error) < .LGB_NUMERIC_TOLERANCE) +}) + +test_that("lgb.train() works with early stopping for classification", { + trainDF <- data.frame( + "feat1" = rep(c(5.0, 10.0), 500L) + , "target" = rep(c(0L, 1L), 500L) + ) + validDF <- data.frame( + "feat1" = rep(c(5.0, 10.0), 50L) + , "target" = rep(c(0L, 1L), 50L) + ) + dtrain <- lgb.Dataset( + data = as.matrix(trainDF[["feat1"]], drop = FALSE) + , label = trainDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid <- lgb.Dataset( + data = as.matrix(validDF[["feat1"]], drop = FALSE) + , label = validDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + + ################################ + # train with no early stopping # + ################################ + bst <- lgb.train( + params = list( + objective = "binary" + , metric = "binary_error" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + ) + + # a perfect model should be trivial to obtain, but all 10 rounds + # should happen + expect_equal(bst$best_score, 0.0) + expect_equal(bst$best_iter, 1L) + expect_equal(length(bst$record_evals[["valid1"]][["binary_error"]][["eval"]]), nrounds) + + ############################# + # train with early stopping # + ############################# + early_stopping_rounds <- 5L + bst <- lgb.train( + params = list( + objective = "binary" + , metric = "binary_error" + , early_stopping_rounds = early_stopping_rounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + ) + + # a perfect model should be trivial to obtain, and only 6 rounds + # should have happen (1 with improvement, 5 consecutive with no improvement) + expect_equal(bst$best_score, 0.0) + expect_equal(bst$best_iter, 1L) + expect_equal( + length(bst$record_evals[["valid1"]][["binary_error"]][["eval"]]) + , early_stopping_rounds + 1L + ) + +}) + +test_that("lgb.train() treats early_stopping_rounds<=0 as disabling early stopping", { + set.seed(708L) + trainDF <- data.frame( + "feat1" = rep(c(5.0, 10.0), 500L) + , "target" = rep(c(0L, 1L), 500L) + ) + validDF <- data.frame( + "feat1" = rep(c(5.0, 10.0), 50L) + , "target" = rep(c(0L, 1L), 50L) + ) + dtrain <- lgb.Dataset( + data = as.matrix(trainDF[["feat1"]], drop = FALSE) + , label = trainDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid <- lgb.Dataset( + data = as.matrix(validDF[["feat1"]], drop = FALSE) + , label = validDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 5L + + for (value in c(-5L, 0L)) { + + #----------------------------# + # passed as keyword argument # + #----------------------------# + bst <- lgb.train( + params = list( + objective = "binary" + , metric = "binary_error" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + , early_stopping_rounds = value + ) + + # a perfect model should be trivial to obtain, but all 10 rounds + # should happen + expect_equal(bst$best_score, 0.0) + expect_equal(bst$best_iter, 1L) + expect_equal(length(bst$record_evals[["valid1"]][["binary_error"]][["eval"]]), nrounds) + + #---------------------------# + # passed as parameter alias # + #---------------------------# + bst <- lgb.train( + params = list( + objective = "binary" + , metric = "binary_error" + , n_iter_no_change = value + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + ) + + # a perfect model should be trivial to obtain, but all 10 rounds + # should happen + expect_equal(bst$best_score, 0.0) + expect_equal(bst$best_iter, 1L) + expect_equal(length(bst$record_evals[["valid1"]][["binary_error"]][["eval"]]), nrounds) + } +}) + +test_that("lgb.train() works with early stopping for classification with a metric that should be maximized", { + set.seed(708L) + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid <- lgb.Dataset( + data = test$data + , label = test$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + + ############################# + # train with early stopping # + ############################# + early_stopping_rounds <- 5L + # the harsh max_depth guarantees that AUC improves over at least the first few iterations + bst_auc <- lgb.train( + params = list( + objective = "binary" + , metric = "auc" + , max_depth = 3L + , early_stopping_rounds = early_stopping_rounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + ) + bst_binary_error <- lgb.train( + params = list( + objective = "binary" + , metric = "binary_error" + , max_depth = 3L + , early_stopping_rounds = early_stopping_rounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + ) + + # early stopping should have been hit for binary_error (higher_better = FALSE) + eval_info <- bst_binary_error$.__enclos_env__$private$get_eval_info() + expect_identical(eval_info, "binary_error") + expect_identical( + unname(bst_binary_error$.__enclos_env__$private$higher_better_inner_eval) + , FALSE + ) + expect_identical(bst_binary_error$best_iter, 1L) + expect_identical(bst_binary_error$current_iter(), early_stopping_rounds + 1L) + expect_true(abs(bst_binary_error$best_score - 0.01613904) < .LGB_NUMERIC_TOLERANCE) + + # early stopping should not have been hit for AUC (higher_better = TRUE) + eval_info <- bst_auc$.__enclos_env__$private$get_eval_info() + expect_identical(eval_info, "auc") + expect_identical( + unname(bst_auc$.__enclos_env__$private$higher_better_inner_eval) + , TRUE + ) + expect_identical(bst_auc$best_iter, 9L) + expect_identical(bst_auc$current_iter(), nrounds) + expect_true(abs(bst_auc$best_score - 0.9999969) < .LGB_NUMERIC_TOLERANCE) +}) + +test_that("lgb.train() works with early stopping for regression", { + set.seed(708L) + trainDF <- data.frame( + "feat1" = rep(c(10.0, 100.0), 500L) + , "target" = rep(c(-50.0, 50.0), 500L) + ) + validDF <- data.frame( + "feat1" = rep(50.0, 4L) + , "target" = rep(50.0, 4L) + ) + dtrain <- lgb.Dataset( + data = as.matrix(trainDF[["feat1"]], drop = FALSE) + , label = trainDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid <- lgb.Dataset( + data = as.matrix(validDF[["feat1"]], drop = FALSE) + , label = validDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + + ################################ + # train with no early stopping # + ################################ + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "rmse" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + ) + + # the best possible model should come from the first iteration, but + # all 10 training iterations should happen + expect_equal(bst$best_score, 55.0) + expect_equal(bst$best_iter, 1L) + expect_equal(length(bst$record_evals[["valid1"]][["rmse"]][["eval"]]), nrounds) + + ############################# + # train with early stopping # + ############################# + early_stopping_rounds <- 5L + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "rmse" + , early_stopping_rounds = early_stopping_rounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + ) + + # the best model should be from the first iteration, and only 6 rounds + # should have happen (1 with improvement, 5 consecutive with no improvement) + expect_equal(bst$best_score, 55.0) + expect_equal(bst$best_iter, 1L) + expect_equal( + length(bst$record_evals[["valid1"]][["rmse"]][["eval"]]) + , early_stopping_rounds + 1L + ) +}) + +test_that("lgb.train() does not stop early if early_stopping_rounds is not given", { + set.seed(708L) + + increasing_metric_starting_value <- get( + ACCUMULATOR_NAME + , envir = .GlobalEnv + ) + nrounds <- 10L + metrics <- list( + .constant_metric + , .increasing_metric + ) + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "None" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_REGRESSION + , nrounds = nrounds + , valids = list("valid1" = DVALID_RANDOM_REGRESSION) + , eval = metrics + ) + + # Only the two functions provided to "eval" should have been evaluated + expect_equal(length(bst$record_evals[["valid1"]]), 2L) + + # all 10 iterations should have happen, and the best_iter should be + # the first one (based on constant_metric) + best_iter <- 1L + expect_equal(bst$best_iter, best_iter) + + # best_score should be taken from the first metric + expect_equal( + bst$best_score + , bst$record_evals[["valid1"]][["constant_metric"]][["eval"]][[best_iter]] + ) + + # early stopping should not have happened. Even though constant_metric + # had 9 consecutive iterations with no improvement, it is ignored because of + # first_metric_only = TRUE + expect_equal( + length(bst$record_evals[["valid1"]][["constant_metric"]][["eval"]]) + , nrounds + ) + expect_equal( + length(bst$record_evals[["valid1"]][["increasing_metric"]][["eval"]]) + , nrounds + ) +}) + +test_that("If first_metric_only is not given or is FALSE, lgb.train() decides to stop early based on all metrics", { + set.seed(708L) + + early_stopping_rounds <- 3L + param_variations <- list( + list( + objective = "regression" + , metric = "None" + , early_stopping_rounds = early_stopping_rounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , list( + objective = "regression" + , metric = "None" + , early_stopping_rounds = early_stopping_rounds + , first_metric_only = FALSE + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + + for (params in param_variations) { + + nrounds <- 10L + bst <- lgb.train( + params = params + , data = DTRAIN_RANDOM_REGRESSION + , nrounds = nrounds + , valids = list( + "valid1" = DVALID_RANDOM_REGRESSION + ) + , eval = list( + .increasing_metric + , .constant_metric + ) + ) + + # Only the two functions provided to "eval" should have been evaluated + expect_equal(length(bst$record_evals[["valid1"]]), 2L) + + # early stopping should have happened, and should have stopped early_stopping_rounds + 1 rounds in + # because constant_metric never improves + # + # the best iteration should be the last one, because increasing_metric was first + # and gets better every iteration + best_iter <- early_stopping_rounds + 1L + expect_equal(bst$best_iter, best_iter) + + # best_score should be taken from "increasing_metric" because it was first + expect_equal( + bst$best_score + , bst$record_evals[["valid1"]][["increasing_metric"]][["eval"]][[best_iter]] + ) + + # early stopping should not have happened. even though increasing_metric kept + # getting better, early stopping should have happened because "constant_metric" + # did not improve + expect_equal( + length(bst$record_evals[["valid1"]][["constant_metric"]][["eval"]]) + , early_stopping_rounds + 1L + ) + expect_equal( + length(bst$record_evals[["valid1"]][["increasing_metric"]][["eval"]]) + , early_stopping_rounds + 1L + ) + } + +}) + +test_that("If first_metric_only is TRUE, lgb.train() decides to stop early based on only the first metric", { + set.seed(708L) + nrounds <- 10L + early_stopping_rounds <- 3L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "None" + , early_stopping_rounds = early_stopping_rounds + , first_metric_only = TRUE + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_REGRESSION + , nrounds = nrounds + , valids = list( + "valid1" = DVALID_RANDOM_REGRESSION + ) + , eval = list( + .increasing_metric + , .constant_metric + ) + ) + + # Only the two functions provided to "eval" should have been evaluated + expect_equal(length(bst$record_evals[["valid1"]]), 2L) + + # all 10 iterations should happen, and the best_iter should be the final one + expect_equal(bst$best_iter, nrounds) + + # best_score should be taken from "increasing_metric" + expect_equal( + bst$best_score + , increasing_metric_starting_value + 0.1 * nrounds + ) + + # early stopping should not have happened. Even though constant_metric + # had 9 consecutive iterations with no improvement, it is ignored because of + # first_metric_only = TRUE + expect_equal( + length(bst$record_evals[["valid1"]][["constant_metric"]][["eval"]]) + , nrounds + ) + expect_equal( + length(bst$record_evals[["valid1"]][["increasing_metric"]][["eval"]]) + , nrounds + ) +}) + +test_that("lgb.train() works when a mixture of functions and strings are passed to eval", { + set.seed(708L) + nrounds <- 10L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "None" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_REGRESSION + , nrounds = nrounds + , valids = list( + "valid1" = DVALID_RANDOM_REGRESSION + ) + , eval = list( + .increasing_metric + , "rmse" + , .constant_metric + , "l2" + ) + ) + + # all 4 metrics should have been used + expect_named( + bst$record_evals[["valid1"]] + , expected = c("rmse", "l2", "increasing_metric", "constant_metric") + , ignore.order = TRUE + , ignore.case = FALSE + ) + + # the difference metrics shouldn't have been mixed up with each other + results <- bst$record_evals[["valid1"]] + expect_true(abs(results[["rmse"]][["eval"]][[1L]] - 1.105012) < .LGB_NUMERIC_TOLERANCE) + expect_true(abs(results[["l2"]][["eval"]][[1L]] - 1.221051) < .LGB_NUMERIC_TOLERANCE) + expected_increasing_metric <- increasing_metric_starting_value + 0.1 + expect_true( + abs( + results[["increasing_metric"]][["eval"]][[1L]] - expected_increasing_metric + ) < .LGB_NUMERIC_TOLERANCE + ) + expect_true(abs(results[["constant_metric"]][["eval"]][[1L]] - CONSTANT_METRIC_VALUE) < .LGB_NUMERIC_TOLERANCE) + +}) + +test_that("lgb.train() works when a list of strings or a character vector is passed to eval", { + + # testing list and character vector, as well as length-1 and length-2 + eval_variations <- list( + c("binary_error", "binary_logloss") + , "binary_logloss" + , list("binary_error", "binary_logloss") + , list("binary_logloss") + ) + + for (eval_variation in eval_variations) { + + set.seed(708L) + nrounds <- 10L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.train( + params = list( + objective = "binary" + , metric = "None" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_CLASSIFICATION + , nrounds = nrounds + , valids = list( + "valid1" = DVALID_RANDOM_CLASSIFICATION + ) + , eval = eval_variation + ) + + # both metrics should have been used + expect_named( + bst$record_evals[["valid1"]] + , expected = unlist(eval_variation) + , ignore.order = TRUE + , ignore.case = FALSE + ) + + # the difference metrics shouldn't have been mixed up with each other + results <- bst$record_evals[["valid1"]] + if ("binary_error" %in% unlist(eval_variation)) { + expect_true(abs(results[["binary_error"]][["eval"]][[1L]] - 0.4864865) < .LGB_NUMERIC_TOLERANCE) + } + if ("binary_logloss" %in% unlist(eval_variation)) { + expect_true(abs(results[["binary_logloss"]][["eval"]][[1L]] - 0.6932548) < .LGB_NUMERIC_TOLERANCE) + } + } +}) + +test_that("lgb.train() works when you specify both 'metric' and 'eval' with strings", { + set.seed(708L) + nrounds <- 10L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.train( + params = list( + objective = "binary" + , metric = "binary_error" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_CLASSIFICATION + , nrounds = nrounds + , valids = list( + "valid1" = DVALID_RANDOM_CLASSIFICATION + ) + , eval = "binary_logloss" + ) + + # both metrics should have been used + expect_named( + bst$record_evals[["valid1"]] + , expected = c("binary_error", "binary_logloss") + , ignore.order = TRUE + , ignore.case = FALSE + ) + + # the difference metrics shouldn't have been mixed up with each other + results <- bst$record_evals[["valid1"]] + expect_true(abs(results[["binary_error"]][["eval"]][[1L]] - 0.4864865) < .LGB_NUMERIC_TOLERANCE) + expect_true(abs(results[["binary_logloss"]][["eval"]][[1L]] - 0.6932548) < .LGB_NUMERIC_TOLERANCE) +}) + +test_that("lgb.train() works when you give a function for eval", { + set.seed(708L) + nrounds <- 10L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.train( + params = list( + objective = "binary" + , metric = "None" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_CLASSIFICATION + , nrounds = nrounds + , valids = list( + "valid1" = DVALID_RANDOM_CLASSIFICATION + ) + , eval = .constant_metric + ) + + # the difference metrics shouldn't have been mixed up with each other + results <- bst$record_evals[["valid1"]] + expect_true(abs(results[["constant_metric"]][["eval"]][[1L]] - CONSTANT_METRIC_VALUE) < .LGB_NUMERIC_TOLERANCE) +}) + +test_that("lgb.train() works with early stopping for regression with a metric that should be minimized", { + set.seed(708L) + trainDF <- data.frame( + "feat1" = rep(c(10.0, 100.0), 500L) + , "target" = rep(c(-50.0, 50.0), 500L) + ) + validDF <- data.frame( + "feat1" = rep(50.0, 4L) + , "target" = rep(50.0, 4L) + ) + dtrain <- lgb.Dataset( + data = as.matrix(trainDF[["feat1"]], drop = FALSE) + , label = trainDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid <- lgb.Dataset( + data = as.matrix(validDF[["feat1"]], drop = FALSE) + , label = validDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + + ############################# + # train with early stopping # + ############################# + early_stopping_rounds <- 5L + bst <- lgb.train( + params = list( + objective = "regression" + , metric = c( + "mape" + , "rmse" + , "mae" + ) + , min_data_in_bin = 5L + , early_stopping_rounds = early_stopping_rounds + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid + ) + ) + + # the best model should be from the first iteration, and only 6 rounds + # should have happened (1 with improvement, 5 consecutive with no improvement) + expect_equal(bst$best_score, 1.1) + expect_equal(bst$best_iter, 1L) + expect_equal( + length(bst$record_evals[["valid1"]][["mape"]][["eval"]]) + , early_stopping_rounds + 1L + ) + + # Booster should understand that all three of these metrics should be minimized + eval_info <- bst$.__enclos_env__$private$get_eval_info() + expect_identical(eval_info, c("mape", "rmse", "l1")) + expect_identical( + unname(bst$.__enclos_env__$private$higher_better_inner_eval) + , rep(FALSE, 3L) + ) +}) + + +test_that("lgb.train() supports non-ASCII feature names", { + # content below is equivalent to + # + # feature_names <- c("F_零", "F_一", "F_二", "F_三") + # + # but using rawToChar() to avoid weird issues when {testthat} + # sources files and converts their encodings prior to evaluating the code + feature_names <- c( + rawToChar(as.raw(c(0x46, 0x5f, 0xe9, 0x9b, 0xb6))) + , rawToChar(as.raw(c(0x46, 0x5f, 0xe4, 0xb8, 0x80))) + , rawToChar(as.raw(c(0x46, 0x5f, 0xe4, 0xba, 0x8c))) + , rawToChar(as.raw(c(0x46, 0x5f, 0xe4, 0xb8, 0x89))) + ) + dtrain <- lgb.Dataset( + data = matrix(rnorm(400L), ncol = 4L) + , label = rnorm(100L) + , params = list(num_threads = .LGB_MAX_THREADS) + , colnames = feature_names + ) + bst <- lgb.train( + data = dtrain + , nrounds = 5L + , obj = "regression" + , params = list( + metric = "rmse" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + expect_true(.is_Booster(bst)) + dumped_model <- jsonlite::fromJSON(bst$dump_model()) + + # UTF-8 strings are not well-supported on Windows + # * https://developer.r-project.org/Blog/public/2020/05/02/utf-8-support-on-windows/ + # * https://developer.r-project.org/Blog/public/2020/07/30/windows/utf-8-build-of-r-and-cran-packages/index.html + if (.LGB_UTF8_LOCALE && !.LGB_ON_WINDOWS) { + expect_identical( + dumped_model[["feature_names"]] + , feature_names + ) + } else { + expect_identical( + dumped_model[["feature_names"]] + , iconv(feature_names, to = "UTF-8") + ) + } +}) + +test_that("lgb.train() works with integer, double, and numeric data", { + data(mtcars) + X <- as.matrix(mtcars[, -1L]) + y <- mtcars[, 1L, drop = TRUE] + expected_mae <- 4.263667 + for (data_mode in c("numeric", "double", "integer")) { + mode(X) <- data_mode + nrounds <- 10L + bst <- lightgbm( + data = X + , label = y + , params = list( + objective = "regression" + , min_data_in_bin = 1L + , min_data_in_leaf = 1L + , learning_rate = 0.01 + , seed = 708L + , verbose = .LGB_VERBOSITY + ) + , nrounds = nrounds + ) + + # should have trained for 10 iterations and found splits + modelDT <- lgb.model.dt.tree(bst) + expect_equal(modelDT[, max(tree_index)], nrounds - 1L) + expect_gt(nrow(modelDT), nrounds * 3L) + + # should have achieved expected performance + preds <- predict(bst, X) + mae <- mean(abs(y - preds)) + expect_true(abs(mae - expected_mae) < .LGB_NUMERIC_TOLERANCE) + } +}) + +test_that("lgb.train() updates params based on keyword arguments", { + dtrain <- lgb.Dataset( + data = matrix(rnorm(400L), ncol = 4L) + , label = rnorm(100L) + , params = list(num_threads = .LGB_MAX_THREADS) + ) + + # defaults from keyword arguments should be used if not specified in params + invisible( + capture.output({ + bst <- lgb.train( + data = dtrain + , obj = "regression" + , params = list(num_threads = .LGB_MAX_THREADS) + ) + }) + ) + expect_equal(bst$params[["verbosity"]], 1L) + expect_equal(bst$params[["num_iterations"]], 100L) + + # main param names should be preferred to keyword arguments + invisible( + capture.output({ + bst <- lgb.train( + data = dtrain + , obj = "regression" + , params = list( + "verbosity" = 5L + , "num_iterations" = 2L + , num_threads = .LGB_MAX_THREADS + ) + ) + }) + ) + expect_equal(bst$params[["verbosity"]], 5L) + expect_equal(bst$params[["num_iterations"]], 2L) + + # aliases should be preferred to keyword arguments, and converted to main parameter name + invisible( + capture.output({ + bst <- lgb.train( + data = dtrain + , obj = "regression" + , params = list( + "verbose" = 5L + , "num_boost_round" = 2L + , num_threads = .LGB_MAX_THREADS + ) + ) + }) + ) + expect_equal(bst$params[["verbosity"]], 5L) + expect_false("verbose" %in% bst$params) + expect_equal(bst$params[["num_iterations"]], 2L) + expect_false("num_boost_round" %in% bst$params) +}) + +test_that("when early stopping is not activated, best_iter and best_score come from valids and not training data", { + set.seed(708L) + trainDF <- data.frame( + "feat1" = rep(c(10.0, 100.0), 500L) + , "target" = rep(c(-50.0, 50.0), 500L) + ) + validDF <- data.frame( + "feat1" = rep(50.0, 4L) + , "target" = rep(50.0, 4L) + ) + dtrain <- lgb.Dataset( + data = as.matrix(trainDF[["feat1"]], drop = FALSE) + , label = trainDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid1 <- lgb.Dataset( + data = as.matrix(validDF[["feat1"]], drop = FALSE) + , label = validDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid2 <- lgb.Dataset( + data = as.matrix(validDF[1L:10L, "feat1"], drop = FALSE) + , label = validDF[1L:10L, "target"] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + train_params <- list( + objective = "regression" + , metric = "rmse" + , learning_rate = 1.5 + , num_leaves = 5L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + + # example 1: two valids, neither are the training data + bst <- lgb.train( + data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid1 + , "valid2" = dvalid2 + ) + , params = train_params + ) + expect_named( + bst$record_evals + , c("start_iter", "valid1", "valid2") + , ignore.order = FALSE + , ignore.case = FALSE + ) + rmse_scores <- unlist(bst$record_evals[["valid1"]][["rmse"]][["eval"]]) + expect_length(rmse_scores, nrounds) + expect_identical(bst$best_iter, which.min(rmse_scores)) + expect_identical(bst$best_score, rmse_scores[which.min(rmse_scores)]) + + # example 2: train first (called "train") and two valids + bst <- lgb.train( + data = dtrain + , nrounds = nrounds + , valids = list( + "train" = dtrain + , "valid1" = dvalid1 + , "valid2" = dvalid2 + ) + , params = train_params + ) + expect_named( + bst$record_evals + , c("start_iter", "train", "valid1", "valid2") + , ignore.order = FALSE + , ignore.case = FALSE + ) + rmse_scores <- unlist(bst$record_evals[["valid1"]][["rmse"]][["eval"]]) + expect_length(rmse_scores, nrounds) + expect_identical(bst$best_iter, which.min(rmse_scores)) + expect_identical(bst$best_score, rmse_scores[which.min(rmse_scores)]) + + # example 3: train second (called "train") and two valids + bst <- lgb.train( + data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid1 + , "train" = dtrain + , "valid2" = dvalid2 + ) + , params = train_params + ) + # note that "train" still ends up as the first one + expect_named( + bst$record_evals + , c("start_iter", "train", "valid1", "valid2") + , ignore.order = FALSE + , ignore.case = FALSE + ) + rmse_scores <- unlist(bst$record_evals[["valid1"]][["rmse"]][["eval"]]) + expect_length(rmse_scores, nrounds) + expect_identical(bst$best_iter, which.min(rmse_scores)) + expect_identical(bst$best_score, rmse_scores[which.min(rmse_scores)]) + + # example 4: train third (called "train") and two valids + bst <- lgb.train( + data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid1 + , "valid2" = dvalid2 + , "train" = dtrain + ) + , params = train_params + ) + # note that "train" still ends up as the first one + expect_named( + bst$record_evals + , c("start_iter", "train", "valid1", "valid2") + , ignore.order = FALSE + , ignore.case = FALSE + ) + rmse_scores <- unlist(bst$record_evals[["valid1"]][["rmse"]][["eval"]]) + expect_length(rmse_scores, nrounds) + expect_identical(bst$best_iter, which.min(rmse_scores)) + expect_identical(bst$best_score, rmse_scores[which.min(rmse_scores)]) + + # example 5: train second (called "something-random-we-would-not-hardcode") and two valids + bst <- lgb.train( + data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid1 + , "something-random-we-would-not-hardcode" = dtrain + , "valid2" = dvalid2 + ) + , params = train_params + ) + # note that "something-random-we-would-not-hardcode" was recognized as the training + # data even though it isn't named "train" + expect_named( + bst$record_evals + , c("start_iter", "something-random-we-would-not-hardcode", "valid1", "valid2") + , ignore.order = FALSE + , ignore.case = FALSE + ) + rmse_scores <- unlist(bst$record_evals[["valid1"]][["rmse"]][["eval"]]) + expect_length(rmse_scores, nrounds) + expect_identical(bst$best_iter, which.min(rmse_scores)) + expect_identical(bst$best_score, rmse_scores[which.min(rmse_scores)]) + + # example 6: the only valid supplied is the training data + bst <- lgb.train( + data = dtrain + , nrounds = nrounds + , valids = list( + "train" = dtrain + ) + , params = train_params + ) + expect_identical(bst$best_iter, -1L) + expect_identical(bst$best_score, NA_real_) +}) + +test_that("lightgbm.train() gives the correct best_score and best_iter for a metric where higher values are better", { + set.seed(708L) + trainDF <- data.frame( + "feat1" = runif(n = 500L, min = 0.0, max = 15.0) + , "target" = rep(c(0L, 1L), 500L) + ) + validDF <- data.frame( + "feat1" = runif(n = 50L, min = 0.0, max = 15.0) + , "target" = rep(c(0L, 1L), 50L) + ) + dtrain <- lgb.Dataset( + data = as.matrix(trainDF[["feat1"]], drop = FALSE) + , label = trainDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid1 <- lgb.Dataset( + data = as.matrix(validDF[1L:25L, "feat1"], drop = FALSE) + , label = validDF[1L:25L, "target"] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + bst <- lgb.train( + data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid1 + , "something-random-we-would-not-hardcode" = dtrain + ) + , params = list( + objective = "binary" + , metric = "auc" + , learning_rate = 1.5 + , num_leaves = 5L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + # note that "something-random-we-would-not-hardcode" was recognized as the training + # data even though it isn't named "train" + expect_named( + bst$record_evals + , c("start_iter", "something-random-we-would-not-hardcode", "valid1") + , ignore.order = FALSE + , ignore.case = FALSE + ) + auc_scores <- unlist(bst$record_evals[["valid1"]][["auc"]][["eval"]]) + expect_length(auc_scores, nrounds) + expect_identical(bst$best_iter, which.max(auc_scores)) + expect_identical(bst$best_score, auc_scores[which.max(auc_scores)]) +}) + +test_that("using lightgbm() without early stopping, best_iter and best_score come from valids and not training data", { + set.seed(708L) + # example: train second (called "something-random-we-would-not-hardcode"), two valids, + # and a metric where higher values are better ("auc") + trainDF <- data.frame( + "feat1" = runif(n = 500L, min = 0.0, max = 15.0) + , "target" = rep(c(0L, 1L), 500L) + ) + validDF <- data.frame( + "feat1" = runif(n = 50L, min = 0.0, max = 15.0) + , "target" = rep(c(0L, 1L), 50L) + ) + dtrain <- lgb.Dataset( + data = as.matrix(trainDF[["feat1"]], drop = FALSE) + , label = trainDF[["target"]] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid1 <- lgb.Dataset( + data = as.matrix(validDF[1L:25L, "feat1"], drop = FALSE) + , label = validDF[1L:25L, "target"] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dvalid2 <- lgb.Dataset( + data = as.matrix(validDF[26L:50L, "feat1"], drop = FALSE) + , label = validDF[26L:50L, "target"] + , params = list(num_threads = .LGB_MAX_THREADS) + ) + nrounds <- 10L + bst <- lightgbm( + data = dtrain + , nrounds = nrounds + , valids = list( + "valid1" = dvalid1 + , "something-random-we-would-not-hardcode" = dtrain + , "valid2" = dvalid2 + ) + , params = list( + objective = "binary" + , metric = "auc" + , learning_rate = 1.5 + , num_leaves = 5L + , num_threads = .LGB_MAX_THREADS + ) + , verbose = -7L + ) + # when verbose <= 0 is passed to lightgbm(), 'valids' is passed through to lgb.train() + # untouched. If you set verbose to > 0, the training data will still be first but called "train" + expect_named( + bst$record_evals + , c("start_iter", "something-random-we-would-not-hardcode", "valid1", "valid2") + , ignore.order = FALSE + , ignore.case = FALSE + ) + auc_scores <- unlist(bst$record_evals[["valid1"]][["auc"]][["eval"]]) + expect_length(auc_scores, nrounds) + expect_identical(bst$best_iter, which.max(auc_scores)) + expect_identical(bst$best_score, auc_scores[which.max(auc_scores)]) +}) + +test_that("lgb.cv() works when you specify both 'metric' and 'eval' with strings", { + set.seed(708L) + nrounds <- 10L + nfolds <- 4L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.cv( + params = list( + objective = "binary" + , metric = "binary_error" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_CLASSIFICATION + , nrounds = nrounds + , nfold = nfolds + , eval = "binary_logloss" + ) + + # both metrics should have been used + expect_named( + bst$record_evals[["valid"]] + , expected = c("binary_error", "binary_logloss") + , ignore.order = TRUE + , ignore.case = FALSE + ) + + # the difference metrics shouldn't have been mixed up with each other + results <- bst$record_evals[["valid"]] + expect_true(abs(results[["binary_error"]][["eval"]][[1L]] - 0.5005654) < .LGB_NUMERIC_TOLERANCE) + expect_true(abs(results[["binary_logloss"]][["eval"]][[1L]] - 0.7011232) < .LGB_NUMERIC_TOLERANCE) + + # all boosters should have been created + expect_length(bst$boosters, nfolds) +}) + +test_that("lgb.cv() works when you give a function for eval", { + set.seed(708L) + nrounds <- 10L + nfolds <- 3L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.cv( + params = list( + objective = "binary" + , metric = "None" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_CLASSIFICATION + , nfold = nfolds + , nrounds = nrounds + , eval = .constant_metric + ) + + # the difference metrics shouldn't have been mixed up with each other + results <- bst$record_evals[["valid"]] + expect_true(abs(results[["constant_metric"]][["eval"]][[1L]] - CONSTANT_METRIC_VALUE) < .LGB_NUMERIC_TOLERANCE) + expect_named(results, "constant_metric") +}) + +test_that("If first_metric_only is TRUE, lgb.cv() decides to stop early based on only the first metric", { + set.seed(708L) + nrounds <- 10L + nfolds <- 5L + early_stopping_rounds <- 3L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.cv( + params = list( + objective = "regression" + , metric = "None" + , early_stopping_rounds = early_stopping_rounds + , first_metric_only = TRUE + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_REGRESSION + , nfold = nfolds + , nrounds = nrounds + , eval = list( + .increasing_metric + , .constant_metric + ) + ) + + # Only the two functions provided to "eval" should have been evaluated + expect_named(bst$record_evals[["valid"]], c("increasing_metric", "constant_metric")) + + # all 10 iterations should happen, and the best_iter should be the final one + expect_equal(bst$best_iter, nrounds) + + # best_score should be taken from "increasing_metric" + # + # this expected value looks magical and confusing, but it's because + # evaluation metrics are averaged over all folds. + # + # consider 5-fold CV with a metric that adds 0.1 to a global accumulator + # each time it's called + # + # * iter 1: [0.1, 0.2, 0.3, 0.4, 0.5] (mean = 0.3) + # * iter 2: [0.6, 0.7, 0.8, 0.9, 1.0] (mean = 1.3) + # * iter 3: [1.1, 1.2, 1.3, 1.4, 1.5] (mean = 1.8) + # + cv_value <- increasing_metric_starting_value + mean(seq_len(nfolds) / 10.0) + (nrounds - 1L) * 0.1 * nfolds + expect_equal(bst$best_score, cv_value) + + # early stopping should not have happened. Even though constant_metric + # had 9 consecutive iterations with no improvement, it is ignored because of + # first_metric_only = TRUE + expect_equal( + length(bst$record_evals[["valid"]][["constant_metric"]][["eval"]]) + , nrounds + ) + expect_equal( + length(bst$record_evals[["valid"]][["increasing_metric"]][["eval"]]) + , nrounds + ) +}) + +test_that("early stopping works with lgb.cv()", { + set.seed(708L) + nrounds <- 10L + nfolds <- 5L + early_stopping_rounds <- 3L + increasing_metric_starting_value <- get(ACCUMULATOR_NAME, envir = .GlobalEnv) + bst <- lgb.cv( + params = list( + objective = "regression" + , metric = "None" + , early_stopping_rounds = early_stopping_rounds + , first_metric_only = TRUE + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = DTRAIN_RANDOM_REGRESSION + , nfold = nfolds + , nrounds = nrounds + , eval = list( + .constant_metric + , .increasing_metric + ) + ) + + # only the two functions provided to "eval" should have been evaluated + expect_named(bst$record_evals[["valid"]], c("constant_metric", "increasing_metric")) + + # best_iter should be based on the first metric. Since constant_metric + # never changes, its first iteration was the best oone + expect_equal(bst$best_iter, 1L) + + # best_score should be taken from the first metric + expect_equal(bst$best_score, 0.2) + + # early stopping should have happened, since constant_metric was the first + # one passed to eval and it will not improve over consecutive iterations + # + # note that this test is identical to the previous one, but with the + # order of the eval metrics switched + expect_equal( + length(bst$record_evals[["valid"]][["constant_metric"]][["eval"]]) + , early_stopping_rounds + 1L + ) + expect_equal( + length(bst$record_evals[["valid"]][["increasing_metric"]][["eval"]]) + , early_stopping_rounds + 1L + ) + + # every booster's predict method should use best_iter as num_iteration in predict + random_data <- as.matrix(rnorm(10L), ncol = 1L, drop = FALSE) + for (x in bst$boosters) { + expect_equal(x$booster$best_iter, bst$best_iter) + expect_gt(x$booster$current_iter(), bst$best_iter) + preds_iter <- predict(x$booster, random_data, num_iteration = bst$best_iter) + preds_no_iter <- predict(x$booster, random_data) + expect_equal(preds_iter, preds_no_iter) + } +}) + +test_that("lgb.cv() respects changes to logging verbosity", { + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + # (verbose = 1) should be INFO and WARNING level logs + lgb_cv_logs <- capture.output({ + cv_bst <- lgb.cv( + params = list(num_threads = .LGB_MAX_THREADS) + , nfold = 2L + , nrounds = 5L + , data = dtrain + , obj = "binary" + , verbose = 1L + ) + }) + expect_true(any(grepl("[LightGBM] [Info]", lgb_cv_logs, fixed = TRUE))) + expect_true(any(grepl("[LightGBM] [Warning]", lgb_cv_logs, fixed = TRUE))) + + # (verbose = 0) should be WARNING level logs only + lgb_cv_logs <- capture.output({ + cv_bst <- lgb.cv( + params = list(num_threads = .LGB_MAX_THREADS) + , nfold = 2L + , nrounds = 5L + , data = dtrain + , obj = "binary" + , verbose = 0L + ) + }) + expect_false(any(grepl("[LightGBM] [Info]", lgb_cv_logs, fixed = TRUE))) + expect_true(any(grepl("[LightGBM] [Warning]", lgb_cv_logs, fixed = TRUE))) + + # (verbose = -1) no logs + lgb_cv_logs <- capture.output({ + cv_bst <- lgb.cv( + params = list(num_threads = .LGB_MAX_THREADS) + , nfold = 2L + , nrounds = 5L + , data = dtrain + , obj = "binary" + , verbose = -1L + ) + }) + # NOTE: this is not length(lgb_cv_logs) == 0 because lightgbm's + # dependencies might print other messages + expect_false(any(grepl("[LightGBM] [Info]", lgb_cv_logs, fixed = TRUE))) + expect_false(any(grepl("[LightGBM] [Warning]", lgb_cv_logs, fixed = TRUE))) +}) + +test_that("lgb.cv() updates params based on keyword arguments", { + dtrain <- lgb.Dataset( + data = matrix(rnorm(400L), ncol = 4L) + , label = rnorm(100L) + , params = list(num_threads = .LGB_MAX_THREADS) + ) + + # defaults from keyword arguments should be used if not specified in params + invisible( + capture.output({ + cv_bst <- lgb.cv( + data = dtrain + , obj = "regression" + , params = list(num_threads = .LGB_MAX_THREADS) + , nfold = 2L + ) + }) + ) + + for (bst in cv_bst$boosters) { + bst_params <- bst[["booster"]]$params + expect_equal(bst_params[["verbosity"]], 1L) + expect_equal(bst_params[["num_iterations"]], 100L) + } + + # main param names should be preferred to keyword arguments + invisible( + capture.output({ + cv_bst <- lgb.cv( + data = dtrain + , obj = "regression" + , params = list( + "verbosity" = 5L + , "num_iterations" = 2L + , num_threads = .LGB_MAX_THREADS + ) + , nfold = 2L + ) + }) + ) + for (bst in cv_bst$boosters) { + bst_params <- bst[["booster"]]$params + expect_equal(bst_params[["verbosity"]], 5L) + expect_equal(bst_params[["num_iterations"]], 2L) + } + + # aliases should be preferred to keyword arguments, and converted to main parameter name + invisible( + capture.output({ + cv_bst <- lgb.cv( + data = dtrain + , obj = "regression" + , params = list( + "verbose" = 5L + , "num_boost_round" = 2L + , num_threads = .LGB_MAX_THREADS + ) + , nfold = 2L + ) + }) + ) + for (bst in cv_bst$boosters) { + bst_params <- bst[["booster"]]$params + expect_equal(bst_params[["verbosity"]], 5L) + expect_false("verbose" %in% bst_params) + expect_equal(bst_params[["num_iterations"]], 2L) + expect_false("num_boost_round" %in% bst_params) + } + +}) + +test_that("lgb.train() fit on linearly-relatead data improves when using linear learners", { + set.seed(708L) + .new_dataset <- function() { + X <- matrix(rnorm(100L), ncol = 1L) + return(lgb.Dataset( + data = X + , label = 2L * X + runif(nrow(X), 0L, 0.1) + , params = list(num_threads = .LGB_MAX_THREADS) + )) + } + + params <- list( + objective = "regression" + , verbose = .LGB_VERBOSITY + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , num_threads = .LGB_MAX_THREADS + ) + + dtrain <- .new_dataset() + bst <- lgb.train( + data = dtrain + , nrounds = 10L + , params = params + , valids = list("train" = dtrain) + ) + expect_true(.is_Booster(bst)) + + dtrain <- .new_dataset() + bst_linear <- lgb.train( + data = dtrain + , nrounds = 10L + , params = utils::modifyList(params, list(linear_tree = TRUE)) + , valids = list("train" = dtrain) + ) + expect_true(.is_Booster(bst_linear)) + + bst_last_mse <- bst$record_evals[["train"]][["l2"]][["eval"]][[10L]] + bst_lin_last_mse <- bst_linear$record_evals[["train"]][["l2"]][["eval"]][[10L]] + expect_true(bst_lin_last_mse < bst_last_mse) +}) + + +test_that("lgb.train() with linear learner fails already-constructed dataset with linear=false", { + set.seed(708L) + params <- list( + objective = "regression" + , verbose = .LGB_VERBOSITY + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , num_threads = .LGB_MAX_THREADS + ) + + dtrain <- lgb.Dataset( + data = matrix(rnorm(100L), ncol = 1L) + , label = rnorm(100L) + , params = list(num_threads = .LGB_MAX_THREADS) + ) + dtrain$construct() + expect_error({ + capture.output({ + bst_linear <- lgb.train( + data = dtrain + , nrounds = 10L + , params = utils::modifyList(params, list(linear_tree = TRUE)) + ) + }, type = "message") + }, regexp = "Cannot change linear_tree after constructed Dataset handle") +}) + +test_that("lgb.train() works with linear learners even if Dataset has missing values", { + set.seed(708L) + .new_dataset <- function() { + values <- rnorm(100L) + values[sample(seq_along(values), size = 10L)] <- NA_real_ + X <- matrix( + data = sample(values, size = 100L) + , ncol = 1L + ) + return(lgb.Dataset( + data = X + , label = 2L * X + runif(nrow(X), 0L, 0.1) + , params = list(num_threads = .LGB_MAX_THREADS) + )) + } + + params <- list( + objective = "regression" + , verbose = .LGB_VERBOSITY + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , num_threads = .LGB_MAX_THREADS + ) + + dtrain <- .new_dataset() + bst <- lgb.train( + data = dtrain + , nrounds = 10L + , params = params + , valids = list("train" = dtrain) + ) + expect_true(.is_Booster(bst)) + + dtrain <- .new_dataset() + bst_linear <- lgb.train( + data = dtrain + , nrounds = 10L + , params = utils::modifyList(params, list(linear_tree = TRUE)) + , valids = list("train" = dtrain) + ) + expect_true(.is_Booster(bst_linear)) + + bst_last_mse <- bst$record_evals[["train"]][["l2"]][["eval"]][[10L]] + bst_lin_last_mse <- bst_linear$record_evals[["train"]][["l2"]][["eval"]][[10L]] + expect_true(bst_lin_last_mse < bst_last_mse) +}) + +test_that("lgb.train() works with linear learners, bagging, and a Dataset that has missing values", { + set.seed(708L) + .new_dataset <- function() { + values <- rnorm(100L) + values[sample(seq_along(values), size = 10L)] <- NA_real_ + X <- matrix( + data = sample(values, size = 100L) + , ncol = 1L + ) + return(lgb.Dataset( + data = X + , label = 2L * X + runif(nrow(X), 0L, 0.1) + , params = list(num_threads = .LGB_MAX_THREADS) + )) + } + + params <- list( + objective = "regression" + , verbose = .LGB_VERBOSITY + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , bagging_freq = 1L + , subsample = 0.8 + , num_threads = .LGB_MAX_THREADS + ) + + dtrain <- .new_dataset() + bst <- lgb.train( + data = dtrain + , nrounds = 10L + , params = params + , valids = list("train" = dtrain) + ) + expect_true(.is_Booster(bst)) + + dtrain <- .new_dataset() + bst_linear <- lgb.train( + data = dtrain + , nrounds = 10L + , params = utils::modifyList(params, list(linear_tree = TRUE)) + , valids = list("train" = dtrain) + ) + expect_true(.is_Booster(bst_linear)) + + bst_last_mse <- bst$record_evals[["train"]][["l2"]][["eval"]][[10L]] + bst_lin_last_mse <- bst_linear$record_evals[["train"]][["l2"]][["eval"]][[10L]] + expect_true(bst_lin_last_mse < bst_last_mse) +}) + +test_that("lgb.train() works with linear learners and data where a feature has only 1 non-NA value", { + set.seed(708L) + .new_dataset <- function() { + values <- c(rnorm(100L), rep(NA_real_, 100L)) + values[118L] <- rnorm(1L) + X <- matrix( + data = values + , ncol = 2L + ) + return(lgb.Dataset( + data = X + , label = 2L * X[, 1L] + runif(nrow(X), 0L, 0.1) + , params = list( + feature_pre_filter = FALSE + , num_threads = .LGB_MAX_THREADS + ) + )) + } + + params <- list( + objective = "regression" + , verbose = -1L + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , num_threads = .LGB_MAX_THREADS + ) + + dtrain <- .new_dataset() + bst_linear <- lgb.train( + data = dtrain + , nrounds = 10L + , params = utils::modifyList(params, list(linear_tree = TRUE)) + ) + expect_true(.is_Booster(bst_linear)) +}) + +test_that("lgb.train() works with linear learners when Dataset has categorical features", { + set.seed(708L) + .new_dataset <- function() { + X <- matrix(numeric(200L), nrow = 100L, ncol = 2L) + X[, 1L] <- rnorm(100L) + X[, 2L] <- sample(seq_len(4L), size = 100L, replace = TRUE) + return(lgb.Dataset( + data = X + , label = 2L * X[, 1L] + runif(nrow(X), 0L, 0.1) + , params = list(num_threads = .LGB_MAX_THREADS) + )) + } + + params <- list( + objective = "regression" + , verbose = -1L + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , categorical_feature = 1L + , num_threads = .LGB_MAX_THREADS + ) + + dtrain <- .new_dataset() + bst <- lgb.train( + data = dtrain + , nrounds = 10L + , params = params + , valids = list("train" = dtrain) + ) + expect_true(.is_Booster(bst)) + + dtrain <- .new_dataset() + bst_linear <- lgb.train( + data = dtrain + , nrounds = 10L + , params = utils::modifyList(params, list(linear_tree = TRUE)) + , valids = list("train" = dtrain) + ) + expect_true(.is_Booster(bst_linear)) + + bst_last_mse <- bst$record_evals[["train"]][["l2"]][["eval"]][[10L]] + bst_lin_last_mse <- bst_linear$record_evals[["train"]][["l2"]][["eval"]][[10L]] + expect_true(bst_lin_last_mse < bst_last_mse) +}) + +test_that("lgb.train() throws an informative error if interaction_constraints is not a list", { + dtrain <- lgb.Dataset(train$data, label = train$label) + params <- list(objective = "regression", interaction_constraints = "[1,2],[3]") + expect_error({ + bst <- lightgbm( + data = dtrain + , params = params + , nrounds = 2L + ) + }, "interaction_constraints must be a list") +}) + +test_that(paste0("lgb.train() throws an informative error if the members of interaction_constraints ", + "are not character or numeric vectors"), { + dtrain <- lgb.Dataset(train$data, label = train$label) + params <- list(objective = "regression", interaction_constraints = list(list(1L, 2L), list(3L))) + expect_error({ + bst <- lightgbm( + data = dtrain + , params = params + , nrounds = 2L + ) + }, "every element in interaction_constraints must be a character vector or numeric vector") +}) + +test_that("lgb.train() throws an informative error if interaction_constraints contains a too large index", { + dtrain <- lgb.Dataset(train$data, label = train$label) + params <- list(objective = "regression", + interaction_constraints = list(c(1L, ncol(train$data) + 1L:2L), 3L)) + expect_error( + lightgbm(data = dtrain, params = params, nrounds = 2L) + , "unknown feature(s) in interaction_constraints: '127', '128'" + , fixed = TRUE + ) +}) + +test_that(paste0("lgb.train() gives same result when interaction_constraints is specified as a list of ", + "character vectors, numeric vectors, or a combination"), { + set.seed(1L) + dtrain <- lgb.Dataset(train$data, label = train$label, params = list(num_threads = .LGB_MAX_THREADS)) + + params <- list( + objective = "regression" + , interaction_constraints = list(c(1L, 2L), 3L) + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- lightgbm( + data = dtrain + , params = params + , nrounds = 2L + ) + pred1 <- bst$predict(test$data) + + cnames <- colnames(train$data) + params <- list( + objective = "regression" + , interaction_constraints = list(c(cnames[[1L]], cnames[[2L]]), cnames[[3L]]) + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- lightgbm( + data = dtrain + , params = params + , nrounds = 2L + ) + pred2 <- bst$predict(test$data) + + params <- list( + objective = "regression" + , interaction_constraints = list(c(cnames[[1L]], cnames[[2L]]), 3L) + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- lightgbm( + data = dtrain + , params = params + , nrounds = 2L + ) + pred3 <- bst$predict(test$data) + + expect_equal(pred1, pred2) + expect_equal(pred2, pred3) + +}) + +test_that(paste0("lgb.train() gives same results when using interaction_constraints and specifying colnames"), { + set.seed(1L) + dtrain <- lgb.Dataset( + train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + + params <- list( + objective = "regression" + , interaction_constraints = list(c(1L, 2L), 3L) + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- lightgbm( + data = dtrain + , params = params + , nrounds = 2L + ) + pred1 <- bst$predict(test$data) + + new_colnames <- paste0(colnames(train$data), "_x") + dtrain$set_colnames(new_colnames) + params <- list( + objective = "regression" + , interaction_constraints = list(c(new_colnames[1L], new_colnames[2L]), new_colnames[3L]) + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- lightgbm( + data = dtrain + , params = params + , nrounds = 2L + ) + pred2 <- bst$predict(test$data) + + expect_equal(pred1, pred2) + +}) + +test_that("Interaction constraints add missing features correctly as new group", { + dtrain <- lgb.Dataset( + train$data[, 1L:6L] # Pick only some columns + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + + list_of_constraints <- list( + list(3L, 1L:2L) + , list("cap-shape=convex", c("cap-shape=bell", "cap-shape=conical")) + ) + + for (constraints in list_of_constraints) { + params <- list( + objective = "regression" + , interaction_constraints = constraints + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- lightgbm(data = dtrain, params = params, nrounds = 10L) + + expected_list <- list("[2]", "[0,1]", "[3,4,5]") + expect_equal(bst$params$interaction_constraints, expected_list) + + expected_string <- "[interaction_constraints: [2],[0,1],[3,4,5]]" + expect_true( + grepl(expected_string, bst$save_model_to_string(), fixed = TRUE) + ) + } +}) + +.generate_trainset_for_monotone_constraints_tests <- function(x3_to_categorical) { + n_samples <- 3000L + x1_positively_correlated_with_y <- runif(n = n_samples, min = 0.0, max = 1.0) + x2_negatively_correlated_with_y <- runif(n = n_samples, min = 0.0, max = 1.0) + x3_negatively_correlated_with_y <- runif(n = n_samples, min = 0.0, max = 1.0) + if (x3_to_categorical) { + x3_negatively_correlated_with_y <- as.integer(x3_negatively_correlated_with_y / 0.01) + categorical_features <- "feature_3" + } else { + categorical_features <- NULL + } + X <- matrix( + data = c( + x1_positively_correlated_with_y + , x2_negatively_correlated_with_y + , x3_negatively_correlated_with_y + ) + , ncol = 3L + ) + zs <- rnorm(n = n_samples, mean = 0.0, sd = 0.01) + scales <- 10.0 * (runif(n = 6L, min = 0.0, max = 1.0) + 0.5) + y <- ( + scales[1L] * x1_positively_correlated_with_y + + sin(scales[2L] * pi * x1_positively_correlated_with_y) + - scales[3L] * x2_negatively_correlated_with_y + - cos(scales[4L] * pi * x2_negatively_correlated_with_y) + - scales[5L] * x3_negatively_correlated_with_y + - cos(scales[6L] * pi * x3_negatively_correlated_with_y) + + zs + ) + return(lgb.Dataset( + data = X + , label = y + , categorical_feature = categorical_features + , free_raw_data = FALSE + , colnames = c("feature_1", "feature_2", "feature_3") + , params = list(num_threads = .LGB_MAX_THREADS) + )) +} + +.is_increasing <- function(y) { + return(all(diff(y) >= 0.0)) +} + +.is_decreasing <- function(y) { + return(all(diff(y) <= 0.0)) +} + +.is_non_monotone <- function(y) { + return(any(diff(y) < 0.0) & any(diff(y) > 0.0)) +} + +# R equivalent of numpy.linspace() +.linspace <- function(start_val, stop_val, num) { + weights <- (seq_len(num) - 1L) / (num - 1L) + return(start_val + weights * (stop_val - start_val)) +} + +.is_correctly_constrained <- function(learner, x3_to_categorical) { + iterations <- 10L + n <- 1000L + variable_x <- .linspace(0L, 1L, n) + fixed_xs_values <- .linspace(0L, 1L, n) + for (i in seq_len(iterations)) { + fixed_x <- fixed_xs_values[i] * rep(1.0, n) + monotonically_increasing_x <- matrix( + data = c(variable_x, fixed_x, fixed_x) + , ncol = 3L + ) + monotonically_increasing_y <- predict( + learner + , monotonically_increasing_x + ) + + monotonically_decreasing_x <- matrix( + data = c(fixed_x, variable_x, fixed_x) + , ncol = 3L + ) + monotonically_decreasing_y <- predict( + learner + , monotonically_decreasing_x + ) + + if (x3_to_categorical) { + non_monotone_data <- c( + fixed_x + , fixed_x + , as.integer(variable_x / 0.01) + ) + } else { + non_monotone_data <- c(fixed_x, fixed_x, variable_x) + } + non_monotone_x <- matrix( + data = non_monotone_data + , ncol = 3L + ) + non_monotone_y <- predict( + learner + , non_monotone_x + ) + if (!(.is_increasing(monotonically_increasing_y) && + .is_decreasing(monotonically_decreasing_y) && + .is_non_monotone(non_monotone_y) + )) { + return(FALSE) + } + } + return(TRUE) +} + +for (x3_to_categorical in c(TRUE, FALSE)) { + set.seed(708L) + dtrain <- .generate_trainset_for_monotone_constraints_tests( + x3_to_categorical = x3_to_categorical + ) + for (monotone_constraints_method in c("basic", "intermediate", "advanced")) { + test_msg <- paste0( + "lgb.train() supports monotone constraints (" + , "categoricals=" + , x3_to_categorical + , ", method=" + , monotone_constraints_method + , ")" + ) + test_that(test_msg, { + params <- list( + min_data = 20L + , num_leaves = 20L + , monotone_constraints = c(1L, -1L, 0L) + , monotone_constraints_method = monotone_constraints_method + , use_missing = FALSE + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + constrained_model <- lgb.train( + params = params + , data = dtrain + , obj = "regression_l2" + , nrounds = 100L + ) + expect_true({ + .is_correctly_constrained( + learner = constrained_model + , x3_to_categorical = x3_to_categorical + ) + }) + }) + } +} + +test_that("lightgbm() accepts objective as function argument and under params", { + bst1 <- lightgbm( + data = train$data + , label = train$label + , params = list(objective = "regression_l1", num_threads = .LGB_MAX_THREADS) + , nrounds = 5L + , verbose = .LGB_VERBOSITY + ) + expect_equal(bst1$params$objective, "regression_l1") + model_txt_lines <- strsplit( + x = bst1$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(model_txt_lines == "objective=regression_l1")) + expect_false(any(model_txt_lines == "objective=regression_l2")) + + bst2 <- lightgbm( + data = train$data + , label = train$label + , objective = "regression_l1" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + ) + expect_equal(bst2$params$objective, "regression_l1") + model_txt_lines <- strsplit( + x = bst2$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(model_txt_lines == "objective=regression_l1")) + expect_false(any(model_txt_lines == "objective=regression_l2")) +}) + +test_that("lightgbm() prioritizes objective under params over objective as function argument", { + bst1 <- lightgbm( + data = train$data + , label = train$label + , objective = "regression" + , params = list(objective = "regression_l1", num_threads = .LGB_MAX_THREADS) + , nrounds = 5L + , verbose = .LGB_VERBOSITY + ) + expect_equal(bst1$params$objective, "regression_l1") + model_txt_lines <- strsplit( + x = bst1$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(model_txt_lines == "objective=regression_l1")) + expect_false(any(model_txt_lines == "objective=regression_l2")) + + bst2 <- lightgbm( + data = train$data + , label = train$label + , objective = "regression" + , params = list(loss = "regression_l1", num_threads = .LGB_MAX_THREADS) + , nrounds = 5L + , verbose = .LGB_VERBOSITY + ) + expect_equal(bst2$params$objective, "regression_l1") + model_txt_lines <- strsplit( + x = bst2$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(model_txt_lines == "objective=regression_l1")) + expect_false(any(model_txt_lines == "objective=regression_l2")) +}) + +test_that("lightgbm() accepts init_score as function argument", { + bst1 <- lightgbm( + data = train$data + , label = train$label + , objective = "binary" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_threads = .LGB_MAX_THREADS) + ) + pred1 <- predict(bst1, train$data, type = "raw") + + bst2 <- lightgbm( + data = train$data + , label = train$label + , init_score = pred1 + , objective = "binary" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_threads = .LGB_MAX_THREADS) + ) + pred2 <- predict(bst2, train$data, type = "raw") + + expect_true(any(pred1 != pred2)) +}) + +test_that("lightgbm() defaults to 'regression' objective if objective not otherwise provided", { + bst <- lightgbm( + data = train$data + , label = train$label + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_threads = .LGB_MAX_THREADS) + ) + expect_equal(bst$params$objective, "regression") + model_txt_lines <- strsplit( + x = bst$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(model_txt_lines == "objective=regression")) + expect_false(any(model_txt_lines == "objective=regression_l1")) +}) + +test_that("lightgbm() accepts 'num_threads' as either top-level argument or under params", { + bst <- lightgbm( + data = train$data + , label = train$label + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , num_threads = 1L + ) + expect_equal(bst$params$num_threads, 1L) + model_txt_lines <- strsplit( + x = bst$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(grepl("[num_threads: 1]", model_txt_lines, fixed = TRUE))) + + bst <- lightgbm( + data = train$data + , label = train$label + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list(num_threads = 1L) + ) + expect_equal(bst$params$num_threads, 1L) + model_txt_lines <- strsplit( + x = bst$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(grepl("[num_threads: 1]", model_txt_lines, fixed = TRUE))) + + bst <- lightgbm( + data = train$data + , label = train$label + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , num_threads = 10L + , params = list(num_threads = 1L) + ) + expect_equal(bst$params$num_threads, 1L) + model_txt_lines <- strsplit( + x = bst$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(grepl("[num_threads: 1]", model_txt_lines, fixed = TRUE))) +}) + +test_that("lightgbm() accepts 'weight' and 'weights'", { + data(mtcars) + X <- as.matrix(mtcars[, -1L]) + y <- as.numeric(mtcars[, 1L]) + w <- rep(1.0, nrow(X)) + model <- lightgbm( + X + , y + , weights = w + , obj = "regression" + , nrounds = 5L + , verbose = .LGB_VERBOSITY + , params = list( + min_data_in_bin = 1L + , min_data_in_leaf = 1L + , num_threads = .LGB_MAX_THREADS + ) + ) + expect_equal(model$.__enclos_env__$private$train_set$get_field("weight"), w) + + # Avoid a bad CRAN check due to partial argument matches + lgb_args <- list( + X + , y + , weight = w + , obj = "regression" + , nrounds = 5L + , verbose = -1L + ) + model <- do.call(lightgbm, lgb_args) + expect_equal(model$.__enclos_env__$private$train_set$get_field("weight"), w) +}) + +.assert_has_expected_logs <- function(log_txt, lgb_info, lgb_warn, early_stopping, valid_eval_msg) { + expect_identical( + object = any(grepl("[LightGBM] [Info]", log_txt, fixed = TRUE)) + , expected = lgb_info + ) + expect_identical( + object = any(grepl("[LightGBM] [Warning]", log_txt, fixed = TRUE)) + , expected = lgb_warn + ) + expect_identical( + object = any(grepl("Will train until there is no improvement in 5 rounds", log_txt, fixed = TRUE)) + , expected = early_stopping + ) + expect_identical( + object = any(grepl("Did not meet early stopping", log_txt, fixed = TRUE)) + , expected = early_stopping + ) + expect_identical( + object = any(grepl("valid's auc\\:[0-9]+", log_txt)) + , expected = valid_eval_msg + ) +} + +.assert_has_expected_record_evals <- function(fitted_model) { + record_evals <- fitted_model$record_evals + expect_equal(record_evals$start_iter, 1L) + if (inherits(fitted_model, "lgb.CVBooster")) { + expected_valid_auc <- c(0.979056, 0.9844697, 0.9900813, 0.9908026, 0.9935588) + } else { + expected_valid_auc <- c(0.9805752, 0.9805752, 0.9934957, 0.9934957, 0.9949372) + } + expect_equal( + object = unlist(record_evals[["valid"]][["auc"]][["eval"]]) + , expected = expected_valid_auc + , tolerance = .LGB_NUMERIC_TOLERANCE + ) + expect_named(record_evals, c("start_iter", "valid"), ignore.order = TRUE, ignore.case = FALSE) + expect_equal(record_evals[["valid"]][["auc"]][["eval_err"]], list()) +} + +.train_for_verbosity_test <- function(train_function, verbose_kwarg, verbose_param) { + set.seed(708L) + nrounds <- 5L + params <- list( + num_leaves = 5L + , objective = "binary" + , metric = "auc" + , early_stopping_round = nrounds + , num_threads = .LGB_MAX_THREADS + # include a nonsense parameter just to trigger a WARN-level log + , nonsense_param = 1.0 + ) + if (!is.null(verbose_param)) { + params[["verbose"]] <- verbose_param + } + train_kwargs <- list( + params = params + , nrounds = nrounds + ) + if (!is.null(verbose_kwarg)) { + train_kwargs[["verbose"]] <- verbose_kwarg + } + function_name <- deparse(substitute(train_function)) + if (function_name == "lgb.train") { + train_kwargs[["data"]] <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + train_kwargs[["valids"]] <- list( + "valid" = lgb.Dataset(data = test$data, label = test$label) + ) + } else if (function_name == "lightgbm") { + train_kwargs[["data"]] <- train$data + train_kwargs[["label"]] <- train$label + train_kwargs[["valids"]] <- list( + "valid" = lgb.Dataset(data = test$data, label = test$label) + ) + } else if (function_name == "lgb.cv") { + train_kwargs[["data"]] <- lgb.Dataset( + data = train$data + , label = train$label + ) + train_kwargs[["nfold"]] <- 3L + train_kwargs[["showsd"]] <- FALSE + } + log_txt <- capture.output({ + bst <- do.call( + what = train_function + , args = train_kwargs + ) + }) + return(list(booster = bst, logs = log_txt)) +} + +test_that("lgb.train() only prints eval metrics when expected to", { + + # regardless of value passed to keyword argument 'verbose', value in params + # should take precedence + for (verbose_keyword_arg in c(-5L, -1L, 0L, 1L, 5L)) { + + # (verbose = -1) should not be any logs, should be record evals + out <- .train_for_verbosity_test( + train_function = lgb.train + , verbose_kwarg = verbose_keyword_arg + , verbose_param = -1L + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = FALSE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose = 0) should be only WARN-level LightGBM logs + out <- .train_for_verbosity_test( + train_function = lgb.train + , verbose_kwarg = verbose_keyword_arg + , verbose_param = 0L + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = TRUE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose > 0) should be INFO- and WARN-level LightGBM logs, and record eval messages + out <- .train_for_verbosity_test( + train_function = lgb.train + , verbose_kwarg = verbose_keyword_arg + , verbose_param = 1L + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = TRUE + , lgb_warn = TRUE + , early_stopping = TRUE + , valid_eval_msg = TRUE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + } + + # if verbosity isn't specified in `params`, changing keyword argument `verbose` should + # alter what messages are printed + + # (verbose = -1) should not be any logs, should be record evals + out <- .train_for_verbosity_test( + train_function = lgb.train + , verbose_kwarg = -1L + , verbose_param = NULL + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = FALSE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose = 0) should be only WARN-level LightGBM logs + out <- .train_for_verbosity_test( + train_function = lgb.train + , verbose_kwarg = 0L + , verbose_param = NULL + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = TRUE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose > 0) should be INFO- and WARN-level LightGBM logs, and record eval messages + out <- .train_for_verbosity_test( + train_function = lgb.train + , verbose_kwarg = 1L + , verbose_param = NULL + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = TRUE + , lgb_warn = TRUE + , early_stopping = TRUE + , valid_eval_msg = TRUE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) +}) + +test_that("lightgbm() only prints eval metrics when expected to", { + + # regardless of value passed to keyword argument 'verbose', value in params + # should take precedence + for (verbose_keyword_arg in c(-5L, -1L, 0L, 1L, 5L)) { + + # (verbose = -1) should not be any logs, train should not be in valids + out <- .train_for_verbosity_test( + train_function = lightgbm + , verbose_kwarg = verbose_keyword_arg + , verbose_param = -1L + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = FALSE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose = 0) should be only WARN-level LightGBM logs, train should not be in valids + out <- .train_for_verbosity_test( + train_function = lightgbm + , verbose_kwarg = verbose_keyword_arg + , verbose_param = 0L + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = TRUE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose > 0) should be INFO- and WARN-level LightGBM logs, and record eval messages, and + # train should be in valids + out <- .train_for_verbosity_test( + train_function = lightgbm + , verbose_kwarg = verbose_keyword_arg + , verbose_param = 1L + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = TRUE + , lgb_warn = TRUE + , early_stopping = TRUE + , valid_eval_msg = TRUE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + } + + # if verbosity isn't specified in `params`, changing keyword argument `verbose` should + # alter what messages are printed + + # (verbose = -1) should not be any logs, train should not be in valids + out <- .train_for_verbosity_test( + train_function = lightgbm + , verbose_kwarg = -1L + , verbose_param = NULL + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = FALSE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose = 0) should be only WARN-level LightGBM logs, train should not be in valids + out <- .train_for_verbosity_test( + train_function = lightgbm + , verbose_kwarg = 0L + , verbose_param = NULL + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = TRUE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose > 0) should be INFO- and WARN-level LightGBM logs, and record eval messages, and + # train should be in valids + out <- .train_for_verbosity_test( + train_function = lightgbm + , verbose_kwarg = 1L + , verbose_param = NULL + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = TRUE + , lgb_warn = TRUE + , early_stopping = TRUE + , valid_eval_msg = TRUE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) +}) + +test_that("lgb.cv() only prints eval metrics when expected to", { + + # regardless of value passed to keyword argument 'verbose', value in params + # should take precedence + for (verbose_keyword_arg in c(-5L, -1L, 0L, 1L, 5L)) { + + # (verbose = -1) should not be any logs, should be record evals + out <- .train_for_verbosity_test( + verbose_kwarg = verbose_keyword_arg + , verbose_param = -1L + , train_function = lgb.cv + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = FALSE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose = 0) should be only WARN-level LightGBM logs + out <- .train_for_verbosity_test( + verbose_kwarg = verbose_keyword_arg + , verbose_param = 0L + , train_function = lgb.cv + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = TRUE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose > 0) should be INFO- and WARN-level LightGBM logs, and record eval messages + out <- .train_for_verbosity_test( + verbose_kwarg = verbose_keyword_arg + , verbose_param = 1L + , train_function = lgb.cv + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = TRUE + , lgb_warn = TRUE + , early_stopping = TRUE + , valid_eval_msg = TRUE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + } + + # if verbosity isn't specified in `params`, changing keyword argument `verbose` should + # alter what messages are printed + + # (verbose = -1) should not be any logs, should be record evals + out <- .train_for_verbosity_test( + verbose_kwarg = verbose_keyword_arg + , verbose_param = -1L + , train_function = lgb.cv + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = FALSE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose = 0) should be only WARN-level LightGBM logs + out <- .train_for_verbosity_test( + verbose_kwarg = verbose_keyword_arg + , verbose_param = 0L + , train_function = lgb.cv + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = FALSE + , lgb_warn = TRUE + , early_stopping = FALSE + , valid_eval_msg = FALSE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) + + # (verbose > 0) should be INFO- and WARN-level LightGBM logs, and record eval messages + out <- .train_for_verbosity_test( + verbose_kwarg = verbose_keyword_arg + , verbose_param = 1L + , train_function = lgb.cv + ) + .assert_has_expected_logs( + log_txt = out[["logs"]] + , lgb_info = TRUE + , lgb_warn = TRUE + , early_stopping = TRUE + , valid_eval_msg = TRUE + ) + .assert_has_expected_record_evals( + fitted_model = out[["booster"]] + ) +}) + +test_that("lightgbm() changes objective='auto' appropriately", { + # Regression + data("mtcars") + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1L]) + model <- lightgbm(x, y, objective = "auto", verbose = .LGB_VERBOSITY, nrounds = 5L, num_threads = .LGB_MAX_THREADS) + expect_equal(model$params$objective, "regression") + model_txt_lines <- strsplit( + x = model$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(grepl("objective=regression", model_txt_lines, fixed = TRUE))) + expect_false(any(grepl("objective=regression_l1", model_txt_lines, fixed = TRUE))) + + # Binary classification + x <- train$data + y <- factor(train$label) + model <- lightgbm(x, y, objective = "auto", verbose = .LGB_VERBOSITY, nrounds = 5L, num_threads = .LGB_MAX_THREADS) + expect_equal(model$params$objective, "binary") + model_txt_lines <- strsplit( + x = model$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(grepl("objective=binary", model_txt_lines, fixed = TRUE))) + + # Multi-class classification + data("iris") + y <- factor(iris$Species) + x <- as.matrix(iris[, -5L]) + model <- lightgbm(x, y, objective = "auto", verbose = .LGB_VERBOSITY, nrounds = 5L, num_threads = .LGB_MAX_THREADS) + expect_equal(model$params$objective, "multiclass") + expect_equal(model$params$num_class, 3L) + model_txt_lines <- strsplit( + x = model$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(grepl("objective=multiclass", model_txt_lines, fixed = TRUE))) +}) + +test_that("lightgbm() determines number of classes for non-default multiclass objectives", { + data("iris") + y <- factor(iris$Species) + x <- as.matrix(iris[, -5L]) + model <- lightgbm( + x + , y + , objective = "multiclassova" + , verbose = .LGB_VERBOSITY + , nrounds = 5L + , num_threads = .LGB_MAX_THREADS + ) + expect_equal(model$params$objective, "multiclassova") + expect_equal(model$params$num_class, 3L) + model_txt_lines <- strsplit( + x = model$save_model_to_string() + , split = "\n" + , fixed = TRUE + )[[1L]] + expect_true(any(grepl("objective=multiclassova", model_txt_lines, fixed = TRUE))) +}) + +test_that("lightgbm() doesn't accept binary classification with non-binary factors", { + data("iris") + y <- factor(iris$Species) + x <- as.matrix(iris[, -5L]) + expect_error({ + lightgbm(x, y, objective = "binary", verbose = .LGB_VERBOSITY, nrounds = 5L, num_threads = .LGB_MAX_THREADS) + }, regexp = "Factors with >2 levels as labels only allowed for multi-class objectives") +}) + +test_that("lightgbm() doesn't accept multi-class classification with binary factors", { + data("iris") + y <- as.character(iris$Species) + y[y == "setosa"] <- "versicolor" + y <- factor(y) + x <- as.matrix(iris[, -5L]) + expect_error({ + lightgbm(x, y, objective = "multiclass", verbose = .LGB_VERBOSITY, nrounds = 5L, num_threads = .LGB_MAX_THREADS) + }, regexp = "Two-level factors as labels only allowed for objective='binary'") +}) + +test_that("lightgbm() model predictions retain factor levels for multiclass classification", { + data("iris") + y <- factor(iris$Species) + x <- as.matrix(iris[, -5L]) + model <- lightgbm(x, y, objective = "auto", verbose = .LGB_VERBOSITY, nrounds = 5L, num_threads = .LGB_MAX_THREADS) + + pred <- predict(model, x, type = "class") + expect_true(is.factor(pred)) + expect_equal(levels(pred), levels(y)) + + pred <- predict(model, x, type = "response") + expect_equal(colnames(pred), levels(y)) + + pred <- predict(model, x, type = "raw") + expect_equal(colnames(pred), levels(y)) +}) + +test_that("lightgbm() model predictions retain factor levels for binary classification", { + data("iris") + y <- as.character(iris$Species) + y[y == "setosa"] <- "versicolor" + y <- factor(y) + x <- as.matrix(iris[, -5L]) + model <- lightgbm(x, y, objective = "auto", verbose = .LGB_VERBOSITY, nrounds = 5L, num_threads = .LGB_MAX_THREADS) + + pred <- predict(model, x, type = "class") + expect_true(is.factor(pred)) + expect_equal(levels(pred), levels(y)) + + pred <- predict(model, x, type = "response") + expect_true(is.vector(pred)) + expect_true(is.numeric(pred)) + expect_false(any(pred %in% y)) + + pred <- predict(model, x, type = "raw") + expect_true(is.vector(pred)) + expect_true(is.numeric(pred)) + expect_false(any(pred %in% y)) +}) + +test_that("lightgbm() accepts named categorical_features", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1L]) + model <- lightgbm( + x + , y + , categorical_feature = "cyl" + , verbose = .LGB_VERBOSITY + , nrounds = 5L + , num_threads = .LGB_MAX_THREADS + ) + expect_true(length(model$params$categorical_feature) > 0L) +}) + +test_that("lightgbm() correctly sets objective when passing lgb.Dataset as input", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1L]) + ds <- lgb.Dataset(x, label = y) + model <- lightgbm( + ds + , objective = "auto" + , verbose = .LGB_VERBOSITY + , nrounds = 5L + , num_threads = .LGB_MAX_THREADS + ) + expect_equal(model$params$objective, "regression") +}) + +test_that("Evaluation metrics aren't printed as a single-element vector", { + log_txt <- capture_output({ + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1L]) + cv_result <- lgb.cv( + data = lgb.Dataset(x, label = y) + , params = list( + objective = "regression" + , metric = "l2" + , min_data_in_leaf = 5L + , max_depth = 3L + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 2L + , nfold = 3L + , verbose = 1L + , eval_train_metric = TRUE + ) + }) + expect_false(grepl("[1] \"[1]", log_txt, fixed = TRUE)) +}) diff --git a/R-package/tests/testthat/test_custom_objective.R b/R-package/tests/testthat/test_custom_objective.R new file mode 100644 index 0000000..a1baf00 --- /dev/null +++ b/R-package/tests/testthat/test_custom_objective.R @@ -0,0 +1,85 @@ +data(agaricus.train, package = "lightgbm") +data(agaricus.test, package = "lightgbm") +dtrain <- lgb.Dataset(agaricus.train$data, label = agaricus.train$label) +dtest <- lgb.Dataset(agaricus.test$data, label = agaricus.test$label) +watchlist <- list(eval = dtest, train = dtrain) + +logregobj <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + preds <- 1.0 / (1.0 + exp(-preds)) + grad <- preds - labels + hess <- preds * (1.0 - preds) + return(list(grad = grad, hess = hess)) +} + +# User-defined evaluation function returns a pair (metric_name, result, higher_better) +# NOTE: when you do customized loss function, the default prediction value is margin +# This may make built-in evaluation metric calculate wrong results +# Keep this in mind when you use the customization, and maybe you need write customized evaluation function +evalerror <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + preds <- 1.0 / (1.0 + exp(-preds)) + err <- as.numeric(sum(labels != (preds > 0.5))) / length(labels) + return(list( + name = "error" + , value = err + , higher_better = FALSE + )) +} + +param <- list( + num_leaves = 8L + , learning_rate = 1.0 + , objective = logregobj + , metric = "auc" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS +) +num_round <- 10L + +test_that("custom objective works", { + bst <- lgb.train(param, dtrain, num_round, watchlist, eval = evalerror) + expect_false(is.null(bst$record_evals)) +}) + +test_that("using a custom objective, custom eval, and no other metrics works", { + set.seed(708L) + bst <- lgb.train( + params = list( + num_leaves = 8L + , learning_rate = 1.0 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = 4L + , valids = watchlist + , obj = logregobj + , eval = evalerror + ) + expect_false(is.null(bst$record_evals)) + expect_equal(bst$best_iter, 4L) + expect_true(abs(bst$best_score - 0.000621) < .LGB_NUMERIC_TOLERANCE) + + eval_results <- bst$eval_valid(feval = evalerror)[[1L]] + expect_true(eval_results[["data_name"]] == "eval") + expect_true(abs(eval_results[["value"]] - 0.0006207325) < .LGB_NUMERIC_TOLERANCE) + expect_true(eval_results[["name"]] == "error") + expect_false(eval_results[["higher_better"]]) +}) + +test_that("using a custom objective that returns wrong shape grad or hess raises an informative error", { + bad_grad <- function(preds, dtrain) { + return(list(grad = numeric(0L), hess = rep(1.0, length(preds)))) + } + bad_hess <- function(preds, dtrain) { + return(list(grad = rep(1.0, length(preds)), hess = numeric(0L))) + } + params <- list(num_leaves = 3L, verbose = .LGB_VERBOSITY) + expect_error({ + lgb.train(params = params, data = dtrain, obj = bad_grad) + }, sprintf("Expected custom objective function to return grad with length %d, got 0.", nrow(dtrain))) + expect_error({ + lgb.train(params = params, data = dtrain, obj = bad_hess) + }, sprintf("Expected custom objective function to return hess with length %d, got 0.", nrow(dtrain))) +}) diff --git a/R-package/tests/testthat/test_dataset.R b/R-package/tests/testthat/test_dataset.R new file mode 100644 index 0000000..0676061 --- /dev/null +++ b/R-package/tests/testthat/test_dataset.R @@ -0,0 +1,665 @@ +data(agaricus.train, package = "lightgbm") +train_data <- agaricus.train$data[seq_len(1000L), ] +train_label <- agaricus.train$label[seq_len(1000L)] + +data(agaricus.test, package = "lightgbm") +test_data <- agaricus.test$data[1L:100L, ] +test_label <- agaricus.test$label[1L:100L] + +test_that("lgb.Dataset: basic construction, saving, loading", { + # from sparse matrix + dtest1 <- lgb.Dataset( + test_data + , label = test_label + , params = list( + verbose = .LGB_VERBOSITY + ) + ) + # from dense matrix + dtest2 <- lgb.Dataset(as.matrix(test_data), label = test_label) + expect_equal(get_field(dtest1, "label"), get_field(dtest2, "label")) + + # save to a local file + tmp_file <- tempfile("lgb.Dataset_") + lgb.Dataset.save(dtest1, tmp_file) + # read from a local file + dtest3 <- lgb.Dataset( + tmp_file + , params = list( + verbose = .LGB_VERBOSITY + ) + ) + lgb.Dataset.construct(dtest3) + unlink(tmp_file) + expect_equal(get_field(dtest1, "label"), get_field(dtest3, "label")) +}) + +test_that("lgb.Dataset: get_field & set_field", { + dtest <- lgb.Dataset(test_data) + dtest$construct() + + set_field(dtest, "label", test_label) + labels <- get_field(dtest, "label") + expect_equal(test_label, get_field(dtest, "label")) + + expect_true(length(get_field(dtest, "weight")) == 0L) + expect_true(length(get_field(dtest, "init_score")) == 0L) + + # any other label should error + expect_error( + set_field(dtest, "asdf", test_label) + , regexp = "Dataset$set_field(): field_name must be one of the following: 'label', 'weight', 'init_score', 'group'" # nolint: line_length. + , fixed = TRUE + ) +}) + +test_that("lgb.Dataset: slice, dim", { + dtest <- lgb.Dataset(test_data, label = test_label) + lgb.Dataset.construct(dtest) + expect_equal(dim(dtest), dim(test_data)) + dsub1 <- lgb.slice.Dataset(dtest, seq_len(42L)) + lgb.Dataset.construct(dsub1) + expect_equal(nrow(dsub1), 42L) + expect_equal(ncol(dsub1), ncol(test_data)) +}) + +test_that("Dataset$set_reference() on a constructed Dataset fails if raw data has been freed", { + dtrain <- lgb.Dataset(train_data, label = train_label) + dtrain$construct() + dtest <- lgb.Dataset(test_data, label = test_label) + dtest$construct() + expect_error({ + dtest$set_reference(dtrain) + }, regexp = "cannot set reference after freeing raw data") +}) + +test_that("Dataset$set_reference() fails if reference is not a Dataset", { + dtrain <- lgb.Dataset( + train_data + , label = train_label + , free_raw_data = FALSE + ) + expect_error({ + dtrain$set_reference(reference = data.frame(x = rnorm(10L))) + }, regexp = "Can only use lgb.Dataset as a reference") + + # passing NULL when the Dataset already has a reference raises an error + dtest <- lgb.Dataset( + test_data + , label = test_label + , free_raw_data = FALSE + ) + dtrain$set_reference(dtest) + expect_error({ + dtrain$set_reference(reference = NULL) + }, regexp = "Can only use lgb.Dataset as a reference") +}) + +test_that("Dataset$set_reference() setting reference to the same Dataset has no side effects", { + dtrain <- lgb.Dataset( + train_data + , label = train_label + , free_raw_data = FALSE + , categorical_feature = c(2L, 3L) + ) + dtrain$construct() + + cat_features_before <- dtrain$.__enclos_env__$private$categorical_feature + colnames_before <- dtrain$get_colnames() + predictor_before <- dtrain$.__enclos_env__$private$predictor + + dtrain$set_reference(dtrain) + expect_identical( + cat_features_before + , dtrain$.__enclos_env__$private$categorical_feature + ) + expect_identical( + colnames_before + , dtrain$get_colnames() + ) + expect_identical( + predictor_before + , dtrain$.__enclos_env__$private$predictor + ) +}) + +test_that("Dataset$set_reference() updates categorical_feature, colnames, and predictor", { + dtrain <- lgb.Dataset( + train_data + , label = train_label + , free_raw_data = FALSE + , categorical_feature = c(2L, 3L) + ) + dtrain$construct() + bst <- Booster$new( + train_set = dtrain + , params = list(verbose = -1L, num_threads = .LGB_MAX_THREADS) + ) + dtrain$.__enclos_env__$private$predictor <- bst$to_predictor() + + test_original_feature_names <- paste0("feature_col_", seq_len(ncol(test_data))) + dtest <- lgb.Dataset( + test_data + , label = test_label + , free_raw_data = FALSE + , colnames = test_original_feature_names + ) + dtest$construct() + + # at this point, dtest should not have categorical_feature + expect_null(dtest$.__enclos_env__$private$predictor) + expect_null(dtest$.__enclos_env__$private$categorical_feature) + expect_identical( + dtest$get_colnames() + , test_original_feature_names + ) + + dtest$set_reference(dtrain) + + # after setting reference to dtrain, those attributes should have dtrain's values + expect_true(methods::is( + dtest$.__enclos_env__$private$predictor + , "lgb.Predictor" + )) + expect_identical( + dtest$.__enclos_env__$private$predictor$.__enclos_env__$private$handle + , dtrain$.__enclos_env__$private$predictor$.__enclos_env__$private$handle + ) + expect_identical( + dtest$.__enclos_env__$private$categorical_feature + , dtrain$.__enclos_env__$private$categorical_feature + ) + expect_identical( + dtest$get_colnames() + , dtrain$get_colnames() + ) + expect_false( + identical(dtest$get_colnames(), test_original_feature_names) + ) +}) + +test_that("lgb.Dataset: colnames", { + dtest <- lgb.Dataset(test_data, label = test_label) + expect_equal(colnames(dtest), colnames(test_data)) + lgb.Dataset.construct(dtest) + expect_equal(colnames(dtest), colnames(test_data)) + expect_error({ + colnames(dtest) <- "asdf" + }, regexp = "can't assign '1' colnames to an lgb.Dataset with '126' columns") + new_names <- make.names(seq_len(ncol(test_data))) + expect_silent({ + colnames(dtest) <- new_names + }) + expect_equal(colnames(dtest), new_names) +}) + +test_that("lgb.Dataset: nrow is correct for a very sparse matrix", { + nr <- 1000L + x <- Matrix::rsparsematrix(nr, 100L, density = 0.0005) + # we want it very sparse, so that last rows are empty + expect_lt(max(x@i), nr) + dtest <- lgb.Dataset(x) + expect_equal(dim(dtest), dim(x)) +}) + +test_that("lgb.Dataset: Dataset should be able to construct from matrix and return non-null handle", { + rawData <- matrix(runif(1000L), ncol = 10L) + ref_handle <- NULL + handle <- .Call( + LGBM_DatasetCreateFromMat_R + , rawData + , nrow(rawData) + , ncol(rawData) + , lightgbm:::.params2str(params = list()) + , ref_handle + ) + expect_true(methods::is(handle, "externalptr")) + expect_false(is.null(handle)) + .Call(LGBM_DatasetFree_R, handle) + handle <- NULL +}) + +test_that("cpp errors should be raised as proper R errors", { + testthat::skip_if( + Sys.getenv("COMPILER", "") == "MSVC" + , message = "Skipping on Visual Studio" + ) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset( + train$data + , label = train$label + , init_score = seq_len(10L) + ) + expect_error({ + capture.output({ + dtrain$construct() + }, type = "message") + }, regexp = "Initial score size doesn't match data size") +}) + +test_that("lgb.Dataset$set_field() should convert 'group' to integer", { + ds <- lgb.Dataset( + data = matrix(rnorm(100L), nrow = 50L, ncol = 2L) + , label = sample(c(0L, 1L), size = 50L, replace = TRUE) + ) + ds$construct() + current_group <- ds$get_field("group") + expect_null(current_group) + group_as_numeric <- rep(25.0, 2L) + ds$set_field("group", group_as_numeric) + expect_identical(ds$get_field("group"), as.integer(group_as_numeric)) +}) + +test_that("lgb.Dataset should throw an error if 'reference' is provided but of the wrong format", { + data(agaricus.test, package = "lightgbm") + test_data <- agaricus.test$data[1L:100L, ] + test_label <- agaricus.test$label[1L:100L] + # Try to trick lgb.Dataset() into accepting bad input + expect_error({ + dtest <- lgb.Dataset( + data = test_data + , label = test_label + , reference = data.frame(x = seq_len(10L), y = seq_len(10L)) + ) + }, regexp = "reference must be a") +}) + +test_that("Dataset$new() should throw an error if 'predictor' is provided but of the wrong format", { + data(agaricus.test, package = "lightgbm") + test_data <- agaricus.test$data[1L:100L, ] + test_label <- agaricus.test$label[1L:100L] + expect_error({ + dtest <- Dataset$new( + data = test_data + , label = test_label + , predictor = data.frame(x = seq_len(10L), y = seq_len(10L)) + ) + }, regexp = "predictor must be a", fixed = TRUE) +}) + +test_that("Dataset$get_params() successfully returns parameters if you passed them", { + # note that this list uses one "main" parameter (feature_pre_filter) and one that + # is an alias (is_sparse), to check that aliases are handled correctly + params <- list( + "feature_pre_filter" = TRUE + , "is_sparse" = FALSE + ) + ds <- lgb.Dataset( + test_data + , label = test_label + , params = params + ) + returned_params <- ds$get_params() + expect_identical(class(returned_params), "list") + expect_identical(length(params), length(returned_params)) + expect_identical(sort(names(params)), sort(names(returned_params))) + for (param_name in names(params)) { + expect_identical(params[[param_name]], returned_params[[param_name]]) + } +}) + +test_that("Dataset$get_params() ignores irrelevant parameters", { + params <- list( + "feature_pre_filter" = TRUE + , "is_sparse" = FALSE + , "nonsense_parameter" = c(1.0, 2.0, 5.0) + ) + ds <- lgb.Dataset( + test_data + , label = test_label + , params = params + ) + returned_params <- ds$get_params() + expect_false("nonsense_parameter" %in% names(returned_params)) +}) + +test_that("Dataset$update_parameters() does nothing for empty inputs", { + ds <- lgb.Dataset( + test_data + , label = test_label + ) + initial_params <- ds$get_params() + expect_identical(initial_params, list()) + + # update_params() should return "self" so it can be chained + res <- ds$update_params( + params = list() + ) + expect_true(.is_Dataset(res)) + + new_params <- ds$get_params() + expect_identical(new_params, initial_params) +}) + +test_that("Dataset$update_params() works correctly for recognized Dataset parameters", { + ds <- lgb.Dataset( + test_data + , label = test_label + ) + initial_params <- ds$get_params() + expect_identical(initial_params, list()) + + new_params <- list( + "data_random_seed" = 708L + , "enable_bundle" = FALSE + ) + res <- ds$update_params( + params = new_params + ) + expect_true(.is_Dataset(res)) + + updated_params <- ds$get_params() + for (param_name in names(new_params)) { + expect_identical(new_params[[param_name]], updated_params[[param_name]]) + } +}) + +test_that("Dataset's finalizer should not fail on an already-finalized Dataset", { + dtest <- lgb.Dataset( + data = test_data + , label = test_label + ) + expect_true(.is_null_handle(dtest$.__enclos_env__$private$handle)) + + dtest$construct() + expect_false(.is_null_handle(dtest$.__enclos_env__$private$handle)) + + dtest$.__enclos_env__$private$finalize() + expect_true(.is_null_handle(dtest$.__enclos_env__$private$handle)) + + # calling finalize() a second time shouldn't cause any issues + dtest$.__enclos_env__$private$finalize() + expect_true(.is_null_handle(dtest$.__enclos_env__$private$handle)) +}) + +test_that("lgb.Dataset: should be able to run lgb.train() immediately after using lgb.Dataset() on a file", { + dtest <- lgb.Dataset( + data = test_data + , label = test_label + , params = list( + verbose = .LGB_VERBOSITY + ) + ) + tmp_file <- tempfile(pattern = "lgb.Dataset_") + lgb.Dataset.save( + dataset = dtest + , fname = tmp_file + ) + + # read from a local file + dtest_read_in <- lgb.Dataset(data = tmp_file) + + param <- list( + objective = "binary" + , metric = "binary_logloss" + , num_leaves = 5L + , learning_rate = 1.0 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + + # should be able to train right away + bst <- lgb.train( + params = param + , data = dtest_read_in + ) + + expect_true(.is_Booster(x = bst)) +}) + +test_that("lgb.Dataset: should be able to run lgb.cv() immediately after using lgb.Dataset() on a file", { + dtest <- lgb.Dataset( + data = test_data + , label = test_label + , params = list( + verbosity = .LGB_VERBOSITY + ) + ) + tmp_file <- tempfile(pattern = "lgb.Dataset_") + lgb.Dataset.save( + dataset = dtest + , fname = tmp_file + ) + + # read from a local file + dtest_read_in <- lgb.Dataset(data = tmp_file) + + param <- list( + objective = "binary" + , metric = "binary_logloss" + , num_leaves = 5L + , learning_rate = 1.0 + , num_iterations = 5L + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + + # should be able to train right away + bst <- lgb.cv( + params = param + , data = dtest_read_in + ) + + expect_true(methods::is(bst, "lgb.CVBooster")) +}) + +test_that("lgb.Dataset: should be able to be used in lgb.cv() when constructed with categorical feature indices", { + data("mtcars") + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1L]) + categorical_feature <- which(names(mtcars) %in% c("cyl", "vs", "am", "gear", "carb")) - 1L + dtrain <- lgb.Dataset( + data = x + , label = y + , categorical_feature = categorical_feature + , free_raw_data = TRUE + , params = list(num_threads = .LGB_MAX_THREADS) + ) + # constructing the Dataset frees the raw data + dtrain$construct() + params <- list( + objective = "regression" + , num_leaves = 2L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + # cv should reuse the same categorical features without checking the indices + bst <- lgb.cv(params = params, data = dtrain, stratified = FALSE, nrounds = 1L) + expect_equal( + unlist(bst$boosters[[1L]]$booster$params$categorical_feature) + , categorical_feature - 1L # 0-based + ) +}) + + +test_that("lgb.Dataset: should be able to use and retrieve long feature names", { + # set one feature to a value longer than the default buffer size used + # in LGBM_DatasetGetFeatureNames_R + feature_names <- names(iris) + long_name <- strrep("a", 1000L) + feature_names[1L] <- long_name + names(iris) <- feature_names + # check that feature name survived the trip from R to C++ and back + dtrain <- lgb.Dataset( + data = as.matrix(iris[, -5L]) + , label = as.numeric(iris$Species) - 1L + ) + dtrain$construct() + col_names <- dtrain$get_colnames() + expect_equal(col_names[1L], long_name) + expect_equal(nchar(col_names[1L]), 1000L) +}) + +test_that("lgb.Dataset: should be able to create a Dataset from a text file with a header", { + train_file <- tempfile(pattern = "train_", fileext = ".csv") + write.table( + data.frame(y = rnorm(100L), x1 = rnorm(100L), x2 = rnorm(100L)) + , file = train_file + , sep = "," + , col.names = TRUE + , row.names = FALSE + , quote = FALSE + ) + + dtrain <- lgb.Dataset( + data = train_file + , params = list( + header = TRUE + , verbosity = .LGB_VERBOSITY + ) + ) + dtrain$construct() + expect_identical(dtrain$get_colnames(), c("x1", "x2")) + expect_identical(dtrain$get_params(), list(header = TRUE)) + expect_identical(dtrain$dim(), c(100L, 2L)) +}) + +test_that("lgb.Dataset: should be able to create a Dataset from a text file without a header", { + train_file <- tempfile(pattern = "train_", fileext = ".csv") + write.table( + data.frame(y = rnorm(100L), x1 = rnorm(100L), x2 = rnorm(100L)) + , file = train_file + , sep = "," + , col.names = FALSE + , row.names = FALSE + , quote = FALSE + ) + + dtrain <- lgb.Dataset( + data = train_file + , params = list( + header = FALSE + , verbosity = .LGB_VERBOSITY + ) + ) + dtrain$construct() + expect_identical(dtrain$get_colnames(), c("Column_0", "Column_1")) + expect_identical(dtrain$get_params(), list(header = FALSE)) + expect_identical(dtrain$dim(), c(100L, 2L)) +}) + +test_that("Dataset: method calls on a Dataset with a null handle should raise an informative error and not segfault", { + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset(train$data, label = train$label) + dtrain$construct() + dvalid <- dtrain$create_valid( + data = train$data[seq_len(100L), ] + , label = train$label[seq_len(100L)] + ) + dvalid$construct() + tmp_file <- tempfile(fileext = ".rds") + saveRDS(dtrain, tmp_file) + rm(dtrain) + dtrain <- readRDS(tmp_file) + expect_error({ + dtrain$construct() + }, regexp = "Attempting to create a Dataset without any raw data") + expect_error({ + dtrain$dim() + }, regexp = "cannot get dimensions before dataset has been constructed") + expect_error({ + dtrain$get_colnames() + }, regexp = "cannot get column names before dataset has been constructed") + expect_error({ + dtrain$get_feature_num_bin(1L) + }, regexp = "Cannot get number of bins in feature before constructing Dataset.") + expect_error({ + dtrain$save_binary(fname = tempfile(fileext = ".bin")) + }, regexp = "Attempting to create a Dataset without any raw data") + expect_error({ + dtrain$set_categorical_feature(categorical_feature = 1L) + }, regexp = "cannot set categorical feature after freeing raw data") + expect_error({ + dtrain$set_reference(reference = dvalid) + }, regexp = "cannot set reference after freeing raw data") + + tmp_valid_file <- tempfile(fileext = ".rds") + saveRDS(dvalid, tmp_valid_file) + rm(dvalid) + dvalid <- readRDS(tmp_valid_file) + dtrain <- lgb.Dataset( + train$data + , label = train$label + , free_raw_data = FALSE + ) + dtrain$construct() + expect_error({ + dtrain$set_reference(reference = dvalid) + }, regexp = "cannot get column names before dataset has been constructed") +}) + +test_that("lgb.Dataset$get_feature_num_bin() works", { + raw_df <- data.frame( + all_random = runif(100L) + , two_vals = rep(c(1.0, 2.0), 50L) + , three_vals = c(rep(c(0.0, 1.0, 2.0), 33L), 0.0) + , two_vals_plus_missing = c(rep(c(1.0, 2.0), 49L), NA_real_, NA_real_) + , all_zero = rep(0.0, 100L) + , categorical = sample.int(2L, 100L, replace = TRUE) + ) + n_features <- ncol(raw_df) + raw_mat <- data.matrix(raw_df) + min_data_in_bin <- 2L + ds <- lgb.Dataset( + raw_mat + , params = list(min_data_in_bin = min_data_in_bin) + , categorical_feature = n_features + ) + ds$construct() + expected_num_bins <- c( + 100L %/% min_data_in_bin + 1L # extra bin for zero + , 3L # 0, 1, 2 + , 3L # 0, 1, 2 + , 4L # 0, 1, 2 + NA + , 0L # unused + , 3L # 1, 2 + NA + ) + actual_num_bins <- sapply(1L:n_features, ds$get_feature_num_bin) + expect_identical(actual_num_bins, expected_num_bins) + # test using defined feature names + bins_by_name <- sapply(colnames(raw_mat), ds$get_feature_num_bin) + expect_identical(unname(bins_by_name), expected_num_bins) + # test using default feature names + no_names_mat <- raw_mat + colnames(no_names_mat) <- NULL + ds_no_names <- lgb.Dataset( + no_names_mat + , params = list(min_data_in_bin = min_data_in_bin) + , categorical_feature = n_features + ) + ds_no_names$construct() + default_names <- lapply( + X = seq(1L, ncol(raw_mat)) + , FUN = function(i) { + sprintf("Column_%d", i - 1L) + } + ) + bins_by_default_name <- sapply(default_names, ds_no_names$get_feature_num_bin) + expect_identical(bins_by_default_name, expected_num_bins) +}) + +test_that("lgb.Dataset can be constructed with categorical features and without colnames", { + # check that dataset can be constructed + raw_mat <- matrix(rep(c(0L, 1L), 50L), ncol = 1L) + ds <- lgb.Dataset(raw_mat, categorical_feature = 1L)$construct() + sparse_mat <- as(raw_mat, "dgCMatrix") + ds2 <- lgb.Dataset(sparse_mat, categorical_feature = 1L)$construct() + # check that the column names are the default ones + expect_equal(ds$.__enclos_env__$private$colnames, "Column_0") + expect_equal(ds2$.__enclos_env__$private$colnames, "Column_0") + # check for error when index is greater than the number of columns + expect_error({ + lgb.Dataset(raw_mat, categorical_feature = 2L)$construct() + }, regexp = "supplied a too large value in categorical_feature: 2 but only 1 features") +}) + +test_that("lgb.Dataset.slice fails with a categorical feature index greater than the number of features", { + data <- matrix(runif(100L), nrow = 50L, ncol = 2L) + ds <- lgb.Dataset(data = data, categorical_feature = 3L) + subset <- ds$slice(1L:20L) + expect_error({ + subset$construct() + }, regexp = "supplied a too large value in categorical_feature: 3 but only 2 features") +}) diff --git a/R-package/tests/testthat/test_learning_to_rank.R b/R-package/tests/testthat/test_learning_to_rank.R new file mode 100644 index 0000000..e99aff4 --- /dev/null +++ b/R-package/tests/testthat/test_learning_to_rank.R @@ -0,0 +1,157 @@ +test_that("learning-to-rank with lgb.train() works as expected", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + # just keep a few features,to generate an model with imperfect fit + train <- agaricus.train + train_data <- train$data[1L:6000L, 1L:20L] + dtrain <- lgb.Dataset( + train_data + , label = train$label[1L:6000L] + , group = rep(150L, 40L) + ) + ndcg_at <- "1,2,3" + eval_names <- paste0("ndcg@", strsplit(ndcg_at, ",", fixed = TRUE)[[1L]]) + params <- list( + objective = "lambdarank" + , metric = "ndcg" + , ndcg_at = ndcg_at + , lambdarank_truncation_level = 3L + , learning_rate = 0.001 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + model <- lgb.train( + params = params + , data = dtrain + , nrounds = 10L + ) + expect_true(.is_Booster(model)) + + dumped_model <- jsonlite::fromJSON( + model$dump_model() + ) + expect_equal(dumped_model[["objective"]], "lambdarank") + expect_equal(dumped_model[["max_feature_idx"]], ncol(train_data) - 1L) + + # check that evaluation results make sense (0.0 < nDCG < 1.0) + eval_results <- model$eval_train() + expect_equal(length(eval_results), length(eval_names)) + for (result in eval_results) { + expect_true(result[["value"]] > 0.0) + expect_true(result[["value"]] < 1.0) + expect_true(result[["higher_better"]]) + expect_identical(result[["data_name"]], "training") + } + expect_identical( + sapply( + X = eval_results + , FUN = function(x) { + x$name + } + ) + , eval_names + ) + expect_equal(eval_results[[1L]][["value"]], 0.775) + if (!.LGB_ON_32_BIT_WINDOWS) { + expect_true(abs(eval_results[[2L]][["value"]] - 0.745986) < .LGB_NUMERIC_TOLERANCE) + expect_true(abs(eval_results[[3L]][["value"]] - 0.7351959) < .LGB_NUMERIC_TOLERANCE) + } +}) + +test_that("learning-to-rank with lgb.cv() works as expected", { + testthat::skip_if( + .LGB_ON_32_BIT_WINDOWS + , message = "Skipping on 32-bit Windows" + ) + set.seed(708L) + data(agaricus.train, package = "lightgbm") + # just keep a few features,to generate an model with imperfect fit + train <- agaricus.train + train_data <- train$data[1L:6000L, 1L:20L] + dtrain <- lgb.Dataset( + train_data + , label = train$label[1L:6000L] + , group = rep(150L, 40L) + ) + ndcg_at <- "1,2,3" + eval_names <- paste0("ndcg@", strsplit(ndcg_at, ",", fixed = TRUE)[[1L]]) + params <- list( + objective = "lambdarank" + , metric = "ndcg" + , ndcg_at = ndcg_at + , lambdarank_truncation_level = 3L + , label_gain = "0,1,3" + , min_data = 1L + , learning_rate = 0.01 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + nfold <- 4L + nrounds <- 10L + cv_bst <- lgb.cv( + params = params + , data = dtrain + , nrounds = nrounds + , nfold = nfold + ) + expect_true(methods::is(cv_bst, "lgb.CVBooster")) + expect_equal(length(cv_bst$boosters), nfold) + + # "valid" should contain results for each metric + eval_results <- cv_bst$record_evals[["valid"]] + eval_names <- c("ndcg@1", "ndcg@2", "ndcg@3") + expect_identical(names(eval_results), eval_names) + + # check that best score and iter make sense (0.0 < nDCG < 1.0) + best_iter <- cv_bst$best_iter + best_score <- cv_bst$best_score + expect_true(best_iter > 0L) + expect_true(best_iter <= nrounds) + expect_true(best_score > 0.0) + expect_true(best_score < 1.0) + expect_true(abs(best_score - 0.75) < .LGB_NUMERIC_TOLERANCE) + + # best_score should be set for the first metric + first_metric <- eval_names[[1L]] + expect_equal(best_score, eval_results[[first_metric]][["eval"]][[best_iter]]) + + for (eval_name in eval_names) { + results_for_this_metric <- eval_results[[eval_name]] + + # each set of metrics should have eval and eval_err + expect_identical(names(results_for_this_metric), c("eval", "eval_err")) + + # there should be one "eval" and "eval_err" per round + expect_equal(length(results_for_this_metric[["eval"]]), nrounds) + expect_equal(length(results_for_this_metric[["eval_err"]]), nrounds) + + # check that evaluation results make sense (0.0 < nDCG < 1.0) + all_evals <- unlist(results_for_this_metric[["eval"]]) + expect_true(all(all_evals > 0.0 & all_evals < 1.0)) + } + + # first and last value of each metric should be as expected + ndcg1_values <- c(0.675, 0.725, 0.65, 0.725, 0.75, 0.725, 0.75, 0.725, 0.75, 0.75) + expect_true(all(abs(unlist(eval_results[["ndcg@1"]][["eval"]]) - ndcg1_values) < .LGB_NUMERIC_TOLERANCE)) + + ndcg2_values <- c( + 0.6556574, 0.6669721, 0.6306574, 0.6476294, 0.6629581, + 0.6476294, 0.6629581, 0.6379581, 0.7113147, 0.6823008 + ) + expect_true(all(abs(unlist(eval_results[["ndcg@2"]][["eval"]]) - ndcg2_values) < .LGB_NUMERIC_TOLERANCE)) + + ndcg3_values <- c( + 0.6484639, 0.6571238, 0.6469279, 0.6540516, 0.6481857, + 0.6481857, 0.6481857, 0.6466496, 0.7027939, 0.6629898 + ) + expect_true(all(abs(unlist(eval_results[["ndcg@3"]][["eval"]]) - ndcg3_values) < .LGB_NUMERIC_TOLERANCE)) + + # check details of each booster + for (bst in cv_bst$boosters) { + dumped_model <- jsonlite::fromJSON( + bst$booster$dump_model() + ) + expect_equal(dumped_model[["objective"]], "lambdarank") + expect_equal(dumped_model[["max_feature_idx"]], ncol(train_data) - 1L) + } +}) diff --git a/R-package/tests/testthat/test_lgb.Booster.R b/R-package/tests/testthat/test_lgb.Booster.R new file mode 100644 index 0000000..7b178ca --- /dev/null +++ b/R-package/tests/testthat/test_lgb.Booster.R @@ -0,0 +1,1800 @@ +test_that("Booster's finalizer should not fail", { + X <- as.matrix(as.integer(iris[, "Species"]), ncol = 1L) + y <- iris[["Sepal.Length"]] + dtrain <- lgb.Dataset(X, label = y) + bst <- lgb.train( + data = dtrain + , params = list( + objective = "regression" + , num_threads = .LGB_MAX_THREADS + ) + , verbose = .LGB_VERBOSITY + , nrounds = 3L + ) + expect_true(.is_Booster(bst)) + + expect_false(.is_null_handle(bst$.__enclos_env__$private$handle)) + + bst$.__enclos_env__$private$finalize() + expect_true(.is_null_handle(bst$.__enclos_env__$private$handle)) + + # calling finalize() a second time shouldn't cause any issues + bst$.__enclos_env__$private$finalize() + expect_true(.is_null_handle(bst$.__enclos_env__$private$handle)) +}) + +test_that("lgb.get.eval.result() should throw an informative error if booster is not an lgb.Booster", { + bad_inputs <- list( + matrix(1.0:10.0, 2L, 5L) + , TRUE + , c("a", "b") + , NA + , 10L + , lgb.Dataset( + data = matrix(1.0:10.0, 2L, 5L) + , params = list() + ) + ) + for (bad_input in bad_inputs) { + expect_error({ + lgb.get.eval.result( + booster = bad_input + , data_name = "test" + , eval_name = "l2" + ) + }, regexp = "Can only use", fixed = TRUE) + } +}) + +test_that("lgb.get.eval.result() should throw an informative error for incorrect data_name", { + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + dtrain <- lgb.Dataset( + agaricus.train$data + , label = agaricus.train$label + ) + model <- lgb.train( + params = list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = 5L + , valids = list( + "test" = lgb.Dataset.create.valid( + dtrain + , agaricus.test$data + , label = agaricus.test$label + ) + ) + ) + expect_error({ + eval_results <- lgb.get.eval.result( + booster = model + , data_name = "testing" + , eval_name = "l2" + ) + }, regexp = "Only the following datasets exist in record evals: [test]", fixed = TRUE) +}) + +test_that("lgb.get.eval.result() should throw an informative error for incorrect eval_name", { + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + dtrain <- lgb.Dataset( + agaricus.train$data + , label = agaricus.train$label + ) + model <- lgb.train( + params = list( + objective = "regression" + , metric = "l2" + , min_data = 1L + , learning_rate = 1.0 + , verbose = .LGB_VERBOSITY + ) + , data = dtrain + , nrounds = 5L + , valids = list( + "test" = lgb.Dataset.create.valid( + dtrain + , agaricus.test$data + , label = agaricus.test$label + ) + ) + ) + expect_error({ + eval_results <- lgb.get.eval.result( + booster = model + , data_name = "test" + , eval_name = "l1" + ) + }, regexp = "Only the following eval_names exist for dataset.*\\: \\[l2\\]", fixed = FALSE) +}) + +test_that("lgb.load() gives the expected error messages given different incorrect inputs", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + train <- agaricus.train + test <- agaricus.test + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + objective = "binary" + , num_leaves = 4L + , learning_rate = 1.0 + , verbose = .LGB_VERBOSITY + ) + , nrounds = 2L + ) + + # you have to give model_str or filename + expect_error({ + lgb.load() + }, regexp = "either filename or model_str must be given") + expect_error({ + lgb.load(filename = NULL, model_str = NULL) + }, regexp = "either filename or model_str must be given") + + # if given, filename should be a string that points to an existing file + model_file <- tempfile(fileext = ".model") + expect_error({ + lgb.load(filename = list(model_file)) + }, regexp = "filename should be character") + file_to_check <- paste0("a.model") + while (file.exists(file_to_check)) { + file_to_check <- paste0("a", file_to_check) + } + expect_error({ + lgb.load(filename = file_to_check) + }, regexp = "passed to filename does not exist") + + # if given, model_str should be a string + expect_error({ + lgb.load(model_str = c(4.0, 5.0, 6.0)) + }, regexp = "lgb.load: model_str should be a character/raw vector") + +}) + +test_that("Loading a Booster from a text file works", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + train <- agaricus.train + test <- agaricus.test + params <- list( + num_leaves = 4L + , boosting = "rf" + , bagging_fraction = 0.8 + , bagging_freq = 1L + , boost_from_average = FALSE + , categorical_feature = c(1L, 2L) + , interaction_constraints = list(1L:2L, 3L, 4L:ncol(train$data)) + , feature_contri = rep(0.5, ncol(train$data)) + , metric = c("mape", "average_precision") + , learning_rate = 1.0 + , objective = "binary" + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = params + , nrounds = 2L + ) + expect_true(.is_Booster(bst)) + + pred <- predict(bst, test$data) + model_file <- tempfile(fileext = ".model") + lgb.save(bst, model_file) + + # finalize the booster and destroy it so you know we aren't cheating + bst$.__enclos_env__$private$finalize() + expect_null(bst$.__enclos_env__$private$handle) + rm(bst) + + bst2 <- lgb.load( + filename = model_file + ) + pred2 <- predict(bst2, test$data) + expect_identical(pred, pred2) + + # check that the parameters are loaded correctly + expect_equal(bst2$params[names(params)], params) +}) + +test_that("boosters with linear models at leaves can be written to text file and re-loaded successfully", { + X <- matrix(rnorm(100L), ncol = 1L) + labels <- 2L * X + runif(nrow(X), 0L, 0.1) + dtrain <- lgb.Dataset( + data = X + , label = labels + ) + + params <- list( + objective = "regression" + , verbose = -1L + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , num_threads = .LGB_MAX_THREADS + ) + + bst <- lgb.train( + data = dtrain + , nrounds = 10L + , params = params + , verbose = .LGB_VERBOSITY + ) + expect_true(.is_Booster(bst)) + + # save predictions, then write the model to a file and destroy it in R + preds <- predict(bst, X) + model_file <- tempfile(fileext = ".model") + lgb.save(bst, model_file) + bst$.__enclos_env__$private$finalize() + expect_null(bst$.__enclos_env__$private$handle) + rm(bst) + + # load the booster and make predictions...should be the same + bst2 <- lgb.load( + filename = model_file + ) + preds2 <- predict(bst2, X) + expect_identical(preds, preds2) +}) + + +test_that("Loading a Booster from a string works", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + train <- agaricus.train + test <- agaricus.test + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 2L + ) + expect_true(.is_Booster(bst)) + + pred <- predict(bst, test$data) + model_string <- bst$save_model_to_string() + + # finalize the booster and destroy it so you know we aren't cheating + bst$.__enclos_env__$private$finalize() + expect_null(bst$.__enclos_env__$private$handle) + rm(bst) + + bst2 <- lgb.load( + model_str = model_string + ) + pred2 <- predict(bst2, test$data) + expect_identical(pred, pred2) +}) + +test_that("Saving a large model to string should work", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + num_leaves = 100L + , learning_rate = 0.01 + , objective = "binary" + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 500L + , verbose = .LGB_VERBOSITY + ) + + pred <- predict(bst, train$data) + pred_leaf_indx <- predict(bst, train$data, type = "leaf") + pred_raw_score <- predict(bst, train$data, type = "raw") + model_string <- bst$save_model_to_string() + + # make sure this test is still producing a model bigger than the default + # buffer size used in LGBM_BoosterSaveModelToString_R + expect_gt(nchar(model_string), 1024L * 1024L) + + # finalize the booster and destroy it so you know we aren't cheating + bst$.__enclos_env__$private$finalize() + expect_null(bst$.__enclos_env__$private$handle) + rm(bst) + + # make sure a new model can be created from this string, and that it + # produces expected results + bst2 <- lgb.load( + model_str = model_string + ) + pred2 <- predict(bst2, train$data) + pred2_leaf_indx <- predict(bst2, train$data, type = "leaf") + pred2_raw_score <- predict(bst2, train$data, type = "raw") + expect_identical(pred, pred2) + expect_identical(pred_leaf_indx, pred2_leaf_indx) + expect_identical(pred_raw_score, pred2_raw_score) +}) + +test_that("Saving a large model to JSON should work", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + num_leaves = 100L + , learning_rate = 0.01 + , objective = "binary" + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 200L + , verbose = .LGB_VERBOSITY + ) + + model_json <- bst$dump_model() + + # make sure this test is still producing a model bigger than the default + # buffer size used in LGBM_BoosterDumpModel_R + expect_gt(nchar(model_json), 1024L * 1024L) + + # check that it is valid JSON that looks like a LightGBM model + model_list <- jsonlite::fromJSON(model_json) + expect_equal(model_list[["objective"]], "binary sigmoid:1") +}) + +test_that("If a string and a file are both passed to lgb.load() the file is used model_str is totally ignored", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + train <- agaricus.train + test <- agaricus.test + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 2L + ) + expect_true(.is_Booster(bst)) + + pred <- predict(bst, test$data) + model_file <- tempfile(fileext = ".model") + lgb.save(bst, model_file) + + # finalize the booster and destroy it so you know we aren't cheating + bst$.__enclos_env__$private$finalize() + expect_null(bst$.__enclos_env__$private$handle) + rm(bst) + + bst2 <- lgb.load( + filename = model_file + , model_str = 4.0 + ) + pred2 <- predict(bst2, test$data) + expect_identical(pred, pred2) +}) + +test_that("Creating a Booster from a Dataset should work", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + dtrain <- lgb.Dataset( + agaricus.train$data + , label = agaricus.train$label + ) + bst <- Booster$new( + params = list( + objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ), + train_set = dtrain + ) + expect_true(.is_Booster(bst)) + expect_equal(bst$current_iter(), 0L) + expect_true(is.na(bst$best_score)) + expect_true(all(bst$predict(agaricus.train$data) == 0.5)) +}) + +test_that("Creating a Booster from a Dataset with an existing predictor should work", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + nrounds <- 2L + bst <- lightgbm( + data = as.matrix(agaricus.train$data) + , label = agaricus.train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + ) + data(agaricus.test, package = "lightgbm") + dtest <- Dataset$new( + data = agaricus.test$data + , label = agaricus.test$label + , predictor = bst$to_predictor() + ) + bst_from_ds <- Booster$new( + train_set = dtest + , params = list( + verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + ) + expect_true(.is_Booster(bst)) + expect_equal(bst$current_iter(), nrounds) + expect_equal(bst$eval_train()[[1L]][["value"]], 0.1115352) + expect_true(.is_Booster(bst_from_ds)) + expect_equal(bst_from_ds$current_iter(), nrounds) + expect_equal(bst_from_ds$eval_train()[[1L]][["value"]], 5.65704892) + dumped_model <- jsonlite::fromJSON(bst$dump_model()) +}) + +test_that("Booster$eval() should work on a Dataset stored in a binary file", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset(train$data, label = train$label) + + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "l2" + , num_leaves = 4L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , nrounds = 2L + ) + + data(agaricus.test, package = "lightgbm") + test <- agaricus.test + dtest <- lgb.Dataset.create.valid( + dataset = dtrain + , data = test$data + , label = test$label + ) + dtest$construct() + + eval_in_mem <- bst$eval( + data = dtest + , name = "test" + ) + + test_file <- tempfile(pattern = "lgb.Dataset_") + lgb.Dataset.save( + dataset = dtest + , fname = test_file + ) + rm(dtest) + + eval_from_file <- bst$eval( + data = lgb.Dataset( + data = test_file + , params = list(verbose = .LGB_VERBOSITY, num_threads = .LGB_MAX_THREADS) + )$construct() + , name = "test" + ) + + expect_true(abs(eval_in_mem[[1L]][["value"]] - 0.1744423) < .LGB_NUMERIC_TOLERANCE) + # refer to https://github.com/lightgbm-org/LightGBM/issues/4680 + if (isTRUE(.LGB_ON_WINDOWS)) { + expect_equal(eval_in_mem, eval_from_file) + } else { + expect_identical(eval_in_mem, eval_from_file) + } +}) + +test_that("Booster$rollback_one_iter() should work as expected", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + data(agaricus.test, package = "lightgbm") + train <- agaricus.train + test <- agaricus.test + nrounds <- 5L + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + ) + expect_equal(bst$current_iter(), nrounds) + expect_true(.is_Booster(bst)) + logloss <- bst$eval_train()[[1L]][["value"]] + expect_equal(logloss, 0.01904786) + + x <- bst$rollback_one_iter() + + # rollback_one_iter() should return a booster and modify the original + # booster in place + expect_true(.is_Booster(x)) + expect_equal(bst$current_iter(), nrounds - 1L) + + # score should now come from the model as of 4 iterations + logloss <- bst$eval_train()[[1L]][["value"]] + expect_equal(logloss, 0.027915146) +}) + +test_that("Booster$update() passing a train_set works as expected", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + nrounds <- 2L + + # train with 2 rounds and then update + bst <- lightgbm( + data = as.matrix(agaricus.train$data) + , label = agaricus.train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + ) + expect_true(.is_Booster(bst)) + expect_equal(bst$current_iter(), nrounds) + bst$update( + train_set = Dataset$new( + data = agaricus.train$data + , label = agaricus.train$label + , params = list(verbose = .LGB_VERBOSITY) + ) + ) + expect_true(.is_Booster(bst)) + expect_equal(bst$current_iter(), nrounds + 1L) + + # train with 3 rounds directly + bst2 <- lightgbm( + data = as.matrix(agaricus.train$data) + , label = agaricus.train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + 1L + ) + expect_true(.is_Booster(bst2)) + expect_equal(bst2$current_iter(), nrounds + 1L) + + # model with 2 rounds + 1 update should be identical to 3 rounds + expect_equal(bst2$eval_train()[[1L]][["value"]], 0.04806585) + expect_equal(bst$eval_train()[[1L]][["value"]], bst2$eval_train()[[1L]][["value"]]) +}) + +test_that("Booster$update() throws an informative error if you provide a non-Dataset to update()", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + nrounds <- 2L + + # train with 2 rounds and then update + bst <- lightgbm( + data = as.matrix(agaricus.train$data) + , label = agaricus.train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = nrounds + ) + expect_error({ + bst$update( + train_set = data.frame(x = rnorm(10L)) + ) + }, regexp = "lgb.Booster.update: Only can use lgb.Dataset", fixed = TRUE) +}) + +test_that("Booster$num_trees_per_iter() works as expected", { + set.seed(708L) + + X <- data.matrix(iris[2L:4L]) + y_reg <- iris[, 1L] + y_binary <- as.integer(y_reg > median(y_reg)) + y_class <- as.integer(iris[, 5L]) - 1L + num_class <- 3L + + nrounds <- 10L + + # Regression and binary probabilistic classification (1 iteration = 1 tree) + fit_reg <- lgb.train( + params = list( + objective = "mse" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = lgb.Dataset(X, label = y_reg) + , nrounds = nrounds + ) + + fit_binary <- lgb.train( + params = list( + objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = lgb.Dataset(X, label = y_binary) + , nrounds = nrounds + ) + + # Multiclass probabilistic classification (1 iteration = num_class trees) + fit_class <- lgb.train( + params = list( + objective = "multiclass" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + , num_class = num_class + ) + , data = lgb.Dataset(X, label = y_class) + , nrounds = nrounds + ) + + expect_equal(fit_reg$num_trees_per_iter(), 1L) + expect_equal(fit_binary$num_trees_per_iter(), 1L) + expect_equal(fit_class$num_trees_per_iter(), num_class) +}) + +test_that("Booster$num_trees() and $num_iter() works (no early stopping)", { + set.seed(708L) + + X <- data.matrix(iris[2L:4L]) + y_reg <- iris[, 1L] + y_binary <- as.integer(y_reg > median(y_reg)) + y_class <- as.integer(iris[, 5L]) - 1L + num_class <- 3L + nrounds <- 10L + + # Regression and binary probabilistic classification (1 iteration = 1 tree) + fit_reg <- lgb.train( + params = list( + objective = "mse" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = lgb.Dataset(X, label = y_reg) + , nrounds = nrounds + ) + + fit_binary <- lgb.train( + params = list( + objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = lgb.Dataset(X, label = y_binary) + , nrounds = nrounds + ) + + # Multiclass probabilistic classification (1 iteration = num_class trees) + fit_class <- lgb.train( + params = list( + objective = "multiclass" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + , num_class = num_class + ) + , data = lgb.Dataset(X, label = y_class) + , nrounds = nrounds + ) + + expect_equal(fit_reg$num_trees(), nrounds) + expect_equal(fit_binary$num_trees(), nrounds) + expect_equal(fit_class$num_trees(), num_class * nrounds) + + expect_equal(fit_reg$num_iter(), nrounds) + expect_equal(fit_binary$num_iter(), nrounds) + expect_equal(fit_class$num_iter(), nrounds) +}) + +test_that("Booster$num_trees() and $num_iter() work (with early stopping)", { + set.seed(708L) + + X <- data.matrix(iris[2L:4L]) + y_reg <- iris[, 1L] + y_binary <- as.integer(y_reg > median(y_reg)) + y_class <- as.integer(iris[, 5L]) - 1L + train_ix <- c(1L:40L, 51L:90L, 101L:140L) + X_train <- X[train_ix, ] + X_valid <- X[-train_ix, ] + + num_class <- 3L + nrounds <- 1000L + early_stopping <- 2L + + # Regression and binary probabilistic classification (1 iteration = 1 tree) + fit_reg <- lgb.train( + params = list( + objective = "mse" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = lgb.Dataset(X_train, label = y_reg[train_ix]) + , valids = list(valid = lgb.Dataset(X_valid, label = y_reg[-train_ix])) + , nrounds = nrounds + , early_stopping_round = early_stopping + ) + + fit_binary <- lgb.train( + params = list( + objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , data = lgb.Dataset(X_train, label = y_binary[train_ix]) + , valids = list(valid = lgb.Dataset(X_valid, label = y_binary[-train_ix])) + , nrounds = nrounds + , early_stopping_round = early_stopping + ) + + # Multiclass probabilistic classification (1 iteration = num_class trees) + fit_class <- lgb.train( + params = list( + objective = "multiclass" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + , num_class = num_class + ) + , data = lgb.Dataset(X_train, label = y_class[train_ix]) + , valids = list(valid = lgb.Dataset(X_valid, label = y_class[-train_ix])) + , nrounds = nrounds + , early_stopping_round = early_stopping + ) + + expected_trees_reg <- fit_reg$best_iter + early_stopping + expected_trees_binary <- fit_binary$best_iter + early_stopping + expected_trees_class <- (fit_class$best_iter + early_stopping) * num_class + + expect_equal(fit_reg$num_trees(), expected_trees_reg) + expect_equal(fit_binary$num_trees(), expected_trees_binary) + expect_equal(fit_class$num_trees(), expected_trees_class) + + expect_equal(fit_reg$num_iter(), expected_trees_reg) + expect_equal(fit_binary$num_iter(), expected_trees_binary) + expect_equal(fit_class$num_iter(), expected_trees_class / num_class) +}) + +test_that("Booster should store parameters and Booster$reset_parameter() should update them", { + data(agaricus.train, package = "lightgbm") + dtrain <- lgb.Dataset( + agaricus.train$data + , label = agaricus.train$label + ) + # testing that this works for some cases that could break it: + # - multiple metrics + # - using "metric", "boosting", "num_class" in params + params <- list( + objective = "multiclass" + , max_depth = 4L + , bagging_fraction = 0.8 + , metric = c("multi_logloss", "multi_error") + , boosting = "gbdt" + , num_class = 5L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- Booster$new( + params = params + , train_set = dtrain + ) + expect_identical(bst$params, params) + + params[["bagging_fraction"]] <- 0.9 + ret_bst <- bst$reset_parameter(params = params) + expect_identical(ret_bst$params, params) + expect_identical(bst$params, params) +}) + +test_that("Booster$params should include dataset params, before and after Booster$reset_parameter()", { + data(agaricus.train, package = "lightgbm") + dtrain <- lgb.Dataset( + agaricus.train$data + , label = agaricus.train$label + , params = list( + max_bin = 17L + ) + ) + params <- list( + objective = "binary" + , max_depth = 4L + , bagging_fraction = 0.8 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- Booster$new( + params = params + , train_set = dtrain + ) + expect_identical( + bst$params + , list( + objective = "binary" + , max_depth = 4L + , bagging_fraction = 0.8 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + , max_bin = 17L + ) + ) + + params[["bagging_fraction"]] <- 0.9 + ret_bst <- bst$reset_parameter(params = params) + expected_params <- list( + objective = "binary" + , max_depth = 4L + , bagging_fraction = 0.9 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + , max_bin = 17L + ) + expect_identical(ret_bst$params, expected_params) + expect_identical(bst$params, expected_params) +}) + +test_that("Saving a model with different feature importance types works", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 2L + ) + expect_true(.is_Booster(bst)) + + .feat_importance_from_string <- function(model_string) { + file_lines <- strsplit(model_string, "\n", fixed = TRUE)[[1L]] + start_indx <- which(file_lines == "feature_importances:") + 1L + blank_line_indices <- which(file_lines == "") + end_indx <- blank_line_indices[blank_line_indices > start_indx][1L] - 1L + importances <- file_lines[start_indx: end_indx] + return(importances) + } + + GAIN_IMPORTANCE <- 1L + model_string <- bst$save_model_to_string(feature_importance_type = GAIN_IMPORTANCE) + expect_equal( + .feat_importance_from_string(model_string) + , c( + "odor=none=4010" + , "stalk-root=club=1163" + , "stalk-root=rooted=573" + , "stalk-surface-above-ring=silky=450" + , "spore-print-color=green=397" + , "gill-color=buff=281" + ) + ) + + SPLIT_IMPORTANCE <- 0L + model_string <- bst$save_model_to_string(feature_importance_type = SPLIT_IMPORTANCE) + expect_equal( + .feat_importance_from_string(model_string) + , c( + "odor=none=1" + , "gill-color=buff=1" + , "stalk-root=club=1" + , "stalk-root=rooted=1" + , "stalk-surface-above-ring=silky=1" + , "spore-print-color=green=1" + ) + ) +}) + +test_that("Saving a model with unknown importance type fails", { + set.seed(708L) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 2L + ) + expect_true(.is_Booster(bst)) + + UNSUPPORTED_IMPORTANCE <- 2L + expect_error({ + capture.output({ + model_string <- bst$save_model_to_string( + feature_importance_type = UNSUPPORTED_IMPORTANCE + ) + }, type = "message") + }, "Unknown importance type") +}) + + +.params_from_model_string <- function(model_str) { + file_lines <- strsplit(model_str, "\n", fixed = TRUE)[[1L]] + start_indx <- which(file_lines == "parameters:") + 1L + blank_line_indices <- which(file_lines == "") + end_indx <- blank_line_indices[blank_line_indices > start_indx][1L] - 1L + params <- file_lines[start_indx: end_indx] + return(params) +} + +test_that("all parameters are stored correctly with save_model_to_string()", { + dtrain <- lgb.Dataset( + data = matrix(rnorm(500L), nrow = 100L) + , label = rnorm(100L) + ) + bst <- lgb.train( + params = list( + objective = "mape" + , metric = c("l2", "mae") + , num_threads = .LGB_MAX_THREADS + , seed = 708L + , data_sample_strategy = "bagging" + , sub_row = 0.8234 + ) + , data = dtrain + , nrounds = 3L + , verbose = .LGB_VERBOSITY + ) + + # entries whose values should reflect params passed to lgb.train() + non_default_param_entries <- c( + "[objective: mape]" + # 'l1' was passed in with alias 'mae' + , "[metric: l2,l1]" + , "[data_sample_strategy: bagging]" + , "[seed: 708]" + # this was passed in with alias 'sub_row' + , "[bagging_fraction: 0.8234]" + , "[num_iterations: 3]" + ) + + # entries with default values of params + default_param_entries <- c( + "[boosting: gbdt]" + , "[tree_learner: serial]" + , "[device_type: cpu]" + , "[data: ]" + , "[valid: ]" + , "[learning_rate: 0.1]" + , "[num_leaves: 31]" + , sprintf("[num_threads: %i]", .LGB_MAX_THREADS) + , "[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: ]" + , "[force_col_wise: 0]" + , "[force_row_wise: 0]" + , "[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: ]" + , sprintf("[verbosity: %i]", .LGB_VERBOSITY) + , "[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]" + , "[gpu_use_dp: 0]" + , "[num_gpu: 1]" + ) + all_param_entries <- c(non_default_param_entries, default_param_entries) + + # parameters should match what was passed from the R-package + model_str <- bst$save_model_to_string() + params_in_file <- .params_from_model_string(model_str = model_str) + .expect_in(all_param_entries, params_in_file) + + # early stopping should be off by default + expect_equal(sum(startsWith(params_in_file, "[early_stopping_round:")), 1L) + expect_equal(sum(params_in_file == "[early_stopping_round: 0]"), 1L) + + # since save_model_to_string() is used when serializing with saveRDS(), check that parameters all + # roundtrip saveRDS()/loadRDS() successfully + rds_file <- tempfile() + saveRDS(bst, rds_file) + bst_rds <- readRDS(rds_file) + model_str <- bst_rds$save_model_to_string() + params_in_file <- .params_from_model_string(model_str = model_str) + .expect_in(all_param_entries, params_in_file) +}) + +test_that("early_stopping, num_iterations are stored correctly in model string even with aliases", { + dtrain <- lgb.Dataset( + data = matrix(rnorm(500L), nrow = 100L) + , label = rnorm(100L) + ) + dvalid <- lgb.Dataset( + data = matrix(rnorm(500L), nrow = 100L) + , label = rnorm(100L) + ) + + # num_iterations values (all different) + num_iterations <- 4L + num_boost_round <- 2L + n_iter <- 3L + nrounds_kwarg <- 6L + + # early_stopping_round values (all different) + early_stopping_round <- 2L + early_stopping_round_kwarg <- 3L + n_iter_no_change <- 4L + + params <- list( + objective = "regression" + , metric = "l2" + , num_boost_round = num_boost_round + , num_iterations = num_iterations + , n_iter = n_iter + , early_stopping_round = early_stopping_round + , n_iter_no_change = n_iter_no_change + , num_threads = .LGB_MAX_THREADS + ) + + bst <- lgb.train( + params = params + , data = dtrain + , nrounds = nrounds_kwarg + , early_stopping_rounds = early_stopping_round_kwarg + , valids = list( + "random_valid" = dvalid + ) + , verbose = .LGB_VERBOSITY + ) + + model_str <- bst$save_model_to_string() + params_in_file <- .params_from_model_string(model_str = model_str) + + # parameters should match what was passed from the R-package, and the "main" (non-alias) + # params values in `params` should be preferred to keyword argumentts or aliases + expect_equal(sum(startsWith(params_in_file, "[num_iterations:")), 1L) + expect_equal(sum(params_in_file == sprintf("[num_iterations: %s]", num_iterations)), 1L) + expect_equal(sum(startsWith(params_in_file, "[early_stopping_round:")), 1L) + expect_equal(sum(params_in_file == sprintf("[early_stopping_round: %s]", early_stopping_round)), 1L) + + # none of the aliases shouold have been written to the model file + expect_equal(sum(startsWith(params_in_file, "[num_boost_round:")), 0L) + expect_equal(sum(startsWith(params_in_file, "[n_iter:")), 0L) + expect_equal(sum(startsWith(params_in_file, "[n_iter_no_change:")), 0L) + +}) + +test_that("Booster: method calls Booster with a null handle should raise an informative error and not segfault", { + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset(train$data, label = train$label) + bst <- lgb.train( + params = list( + objective = "regression" + , metric = "l2" + , num_leaves = 8L + , num_threads = .LGB_MAX_THREADS + ) + , data = dtrain + , verbose = .LGB_VERBOSITY + , nrounds = 5L + , valids = list( + train = dtrain + ) + , serializable = FALSE + ) + tmp_file <- tempfile(fileext = ".rds") + saveRDS(bst, tmp_file) + rm(bst) + bst <- readRDS(tmp_file) + .expect_booster_error <- function(object) { + error_regexp <- "Attempting to use a Booster which no longer exists" + expect_error(object, regexp = error_regexp) + } + .expect_booster_error({ + bst$current_iter() + }) + .expect_booster_error({ + bst$dump_model() + }) + .expect_booster_error({ + bst$eval(data = dtrain, name = "valid") + }) + .expect_booster_error({ + bst$eval_train() + }) + .expect_booster_error({ + bst$lower_bound() + }) + .expect_booster_error({ + bst$predict(data = train$data[seq_len(5L), ]) + }) + .expect_booster_error({ + bst$reset_parameter(params = list(learning_rate = 0.123)) + }) + .expect_booster_error({ + bst$rollback_one_iter() + }) + .expect_booster_error({ + bst$save_raw() + }) + .expect_booster_error({ + bst$save_model(filename = tempfile(fileext = ".model")) + }) + .expect_booster_error({ + bst$save_model_to_string() + }) + .expect_booster_error({ + bst$update() + }) + .expect_booster_error({ + bst$upper_bound() + }) + predictor <- bst$to_predictor() + .expect_booster_error({ + predictor$current_iter() + }) + .expect_booster_error({ + predictor$predict(data = train$data[seq_len(5L), ]) + }) +}) + +test_that("Booster$new() using a Dataset with a null handle should raise an informative error and not segfault", { + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset(train$data, label = train$label) + dtrain$construct() + tmp_file <- tempfile(fileext = ".bin") + saveRDS(dtrain, tmp_file) + rm(dtrain) + dtrain <- readRDS(tmp_file) + expect_error({ + bst <- Booster$new( + train_set = dtrain + , params = list( + verbose = .LGB_VERBOSITY + ) + ) + }, regexp = "Attempting to create a Dataset without any raw data") +}) + +test_that("Booster$new() raises informative errors for malformed inputs", { + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset(train$data, label = train$label) + + # no inputs + expect_error({ + Booster$new() + }, regexp = "lgb.Booster: Need at least either training dataset, model file, or model_str") + + # unrecognized objective + expect_error({ + capture.output({ + Booster$new( + params = list(objective = "not_a_real_objective") + , train_set = dtrain + ) + }, type = "message") + }, regexp = "Unknown objective type name: not_a_real_objective") + + # train_set is not a Dataset + expect_error({ + Booster$new( + train_set = data.table::data.table(rnorm(1L:10L)) + ) + }, regexp = "lgb.Booster: Can only use lgb.Dataset as training data") + + # model file isn't a string + expect_error({ + Booster$new( + modelfile = list() + ) + }, regexp = "lgb.Booster: Can only use a string as model file path") + + # model file doesn't exist + expect_error({ + capture.output({ + Booster$new( + params = list() + , modelfile = "file-that-does-not-exist.model" + ) + }, type = "message") + }, regexp = "Could not open file-that-does-not-exist.model") + + # model file doesn't contain a valid LightGBM model + model_file <- tempfile(fileext = ".model") + writeLines( + text = c("make", "good", "predictions") + , con = model_file + ) + expect_error({ + capture.output({ + Booster$new( + params = list() + , modelfile = model_file + ) + }, type = "message") + }, regexp = "Unknown model format or submodel type in model file") + + # malformed model string + expect_error({ + capture.output({ + Booster$new( + params = list() + , model_str = "a\nb\n" + ) + }, type = "message") + }, regexp = "Model file doesn't specify the number of classes") + + # model string isn't character or raw + expect_error({ + Booster$new( + model_str = numeric() + ) + }, regexp = "lgb.Booster: Can only use a character/raw vector as model_str") +}) + +# this is almost identical to the test above it, but for lgb.cv(). A lot of code +# is duplicated between lgb.train() and lgb.cv(), and this will catch cases where +# one is updated and the other isn't +test_that("lgb.cv() correctly handles passing through params to the model file", { + dtrain <- lgb.Dataset( + data = matrix(rnorm(500L), nrow = 100L) + , label = rnorm(100L) + ) + + # num_iterations values (all different) + num_iterations <- 4L + num_boost_round <- 2L + n_iter <- 3L + nrounds_kwarg <- 6L + + # early_stopping_round values (all different) + early_stopping_round <- 2L + early_stopping_round_kwarg <- 3L + n_iter_no_change <- 4L + + params <- list( + objective = "regression" + , metric = "l2" + , num_boost_round = num_boost_round + , num_iterations = num_iterations + , n_iter = n_iter + , early_stopping_round = early_stopping_round + , n_iter_no_change = n_iter_no_change + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + + cv_bst <- lgb.cv( + params = params + , data = dtrain + , nrounds = nrounds_kwarg + , early_stopping_rounds = early_stopping_round_kwarg + , nfold = 3L + , verbose = .LGB_VERBOSITY + ) + + for (bst in cv_bst$boosters) { + model_str <- bst[["booster"]]$save_model_to_string() + params_in_file <- .params_from_model_string(model_str = model_str) + + # parameters should match what was passed from the R-package, and the "main" (non-alias) + # params values in `params` should be preferred to keyword argumentts or aliases + expect_equal(sum(startsWith(params_in_file, "[num_iterations:")), 1L) + expect_equal(sum(params_in_file == sprintf("[num_iterations: %s]", num_iterations)), 1L) + expect_equal(sum(startsWith(params_in_file, "[early_stopping_round:")), 1L) + expect_equal(sum(params_in_file == sprintf("[early_stopping_round: %s]", early_stopping_round)), 1L) + + # none of the aliases shouold have been written to the model file + expect_equal(sum(startsWith(params_in_file, "[num_boost_round:")), 0L) + expect_equal(sum(startsWith(params_in_file, "[n_iter:")), 0L) + expect_equal(sum(startsWith(params_in_file, "[n_iter_no_change:")), 0L) + } + +}) + +test_that("params (including dataset params) should be stored in .rds file for Booster", { + data(agaricus.train, package = "lightgbm") + dtrain <- lgb.Dataset( + agaricus.train$data + , label = agaricus.train$label + , params = list( + max_bin = 17L + ) + ) + params <- list( + objective = "binary" + , max_depth = 4L + , bagging_fraction = 0.8 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + bst <- Booster$new( + params = params + , train_set = dtrain + ) + bst_file <- tempfile(fileext = ".rds") + saveRDS(bst, file = bst_file) + + bst_from_file <- readRDS(file = bst_file) + expect_identical( + bst_from_file$params + , list( + objective = "binary" + , max_depth = 4L + , bagging_fraction = 0.8 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + , max_bin = 17L + ) + ) +}) + +test_that("Handle is automatically restored when calling predict", { + data(agaricus.train, package = "lightgbm") + bst <- lightgbm( + agaricus.train$data + , agaricus.train$label + , nrounds = 5L + , obj = "binary" + , params = list( + verbose = .LGB_VERBOSITY + ) + , num_threads = .LGB_MAX_THREADS + ) + bst_file <- tempfile(fileext = ".rds") + saveRDS(bst, file = bst_file) + + bst_from_file <- readRDS(file = bst_file) + + pred_before <- predict(bst, agaricus.train$data) + pred_after <- predict(bst_from_file, agaricus.train$data) + expect_equal(pred_before, pred_after) +}) + +test_that("boosters with linear models at leaves can be written to RDS and re-loaded successfully", { + X <- matrix(rnorm(100L), ncol = 1L) + labels <- 2L * X + runif(nrow(X), 0L, 0.1) + dtrain <- lgb.Dataset( + data = X + , label = labels + ) + + params <- list( + objective = "regression" + , verbose = .LGB_VERBOSITY + , metric = "mse" + , seed = 0L + , num_leaves = 2L + , num_threads = .LGB_MAX_THREADS + ) + + bst <- lgb.train( + data = dtrain + , nrounds = 10L + , params = params + ) + expect_true(.is_Booster(bst)) + + # save predictions, then write the model to a file and destroy it in R + preds <- predict(bst, X) + model_file <- tempfile(fileext = ".rds") + saveRDS(bst, file = model_file) + bst$.__enclos_env__$private$finalize() + expect_null(bst$.__enclos_env__$private$handle) + rm(bst) + + # load the booster and make predictions...should be the same + bst2 <- readRDS(file = model_file) + preds2 <- predict(bst2, X) + expect_identical(preds, preds2) +}) + +.have_same_handle <- function(model, other_model) { + expect_equal( + model$.__enclos_env__$private$handle + , other_model$.__enclos_env__$private$handle + ) +} + +.has_expected_content_for_fitted_model <- function(printed_txt) { + expect_true(any(startsWith(printed_txt, "LightGBM Model"))) + expect_true(any(startsWith(printed_txt, "Fitted to dataset"))) +} + +.has_expected_content_for_finalized_model <- function(printed_txt) { + expect_true(any(printed_txt == "LightGBM Model")) + expect_true(any(grepl("Booster handle is invalid", printed_txt, fixed = TRUE))) +} + +.check_methods_work <- function(model) { + + #--- should work for fitted models --- # + + # print() + log_txt <- capture.output({ + ret <- print(model) + }) + .have_same_handle(ret, model) + .has_expected_content_for_fitted_model(log_txt) + + # show() + log_txt <- capture.output({ + ret <- show(model) + }) + expect_null(ret) + .has_expected_content_for_fitted_model(log_txt) + + # summary() + log_txt <- capture.output({ + ret <- summary(model) + }) + .have_same_handle(ret, model) + .has_expected_content_for_fitted_model(log_txt) + + #--- should not fail for finalized models ---# + model$.__enclos_env__$private$finalize() + + # print() + log_txt <- capture.output({ + ret <- print(model) + }) + .has_expected_content_for_finalized_model(log_txt) + + # show() + .have_same_handle(ret, model) + log_txt <- capture.output({ + ret <- show(model) + }) + expect_null(ret) + .has_expected_content_for_finalized_model(log_txt) + + # summary() + log_txt <- capture.output({ + ret <- summary(model) + }) + .have_same_handle(ret, model) + .has_expected_content_for_finalized_model(log_txt) +} + +test_that("Booster's print, show, and summary work correctly for built-in objectives", { + data("mtcars") + model <- lgb.train( + params = list( + objective = "regression" + , min_data_in_leaf = 1L + , num_threads = .LGB_MAX_THREADS + ) + , data = lgb.Dataset( + as.matrix(mtcars[, -1L]) + , label = mtcars$mpg + , params = list( + min_data_in_bin = 1L + ) + ) + , verbose = .LGB_VERBOSITY + , nrounds = 5L + ) + .check_methods_work(model) + + data("iris") + model <- lgb.train( + params = list(objective = "multiclass", num_class = 3L, num_threads = .LGB_MAX_THREADS) + , data = lgb.Dataset( + as.matrix(iris[, -5L]) + , label = as.numeric(factor(iris$Species)) - 1.0 + ) + , verbose = .LGB_VERBOSITY + , nrounds = 5L + ) + .check_methods_work(model) +}) + +test_that("Booster's print, show, and summary work correctly for custom objective", { + .logregobj <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + preds <- 1.0 / (1.0 + exp(-preds)) + grad <- preds - labels + hess <- preds * (1.0 - preds) + return(list(grad = grad, hess = hess)) + } + + .evalerror <- function(preds, dtrain) { + labels <- get_field(dtrain, "label") + preds <- 1.0 / (1.0 + exp(-preds)) + err <- as.numeric(sum(labels != (preds > 0.5))) / length(labels) + return(list( + name = "error" + , value = err + , higher_better = FALSE + )) + } + + data("iris") + model <- lgb.train( + data = lgb.Dataset( + as.matrix(iris[, -5L]) + , label = as.numeric(iris$Species == "virginica") + ) + , obj = .logregobj + , eval = .evalerror + , verbose = .LGB_VERBOSITY + , nrounds = 5L + , params = list(num_threads = .LGB_MAX_THREADS) + ) + + .check_methods_work(model) +}) + +test_that("Booster's print, show, and summary work correctly when objective is not provided", { + data("iris") + model <- lgb.train( + data = lgb.Dataset( + as.matrix(iris[, seq_len(3L)]) + , label = iris[, 4L] + ) + , verbose = .LGB_VERBOSITY + , nrounds = 5L + , params = list(num_threads = .LGB_MAX_THREADS) + ) + + log_txt <- capture.output(print(model)) + expect_true(any(log_txt == "Objective: (default)")) + + .check_methods_work(model) +}) + +test_that("LGBM_BoosterGetNumFeature_R returns correct outputs", { + data("mtcars") + model <- lgb.train( + params = list( + objective = "regression" + , min_data_in_leaf = 1L + , num_threads = .LGB_MAX_THREADS + ) + , data = lgb.Dataset( + as.matrix(mtcars[, -1L]) + , label = mtcars$mpg + , params = list( + min_data_in_bin = 1L + ) + ) + , verbose = .LGB_VERBOSITY + , nrounds = 5L + ) + ncols <- .Call(LGBM_BoosterGetNumFeature_R, model$.__enclos_env__$private$handle) + expect_equal(ncols, ncol(mtcars) - 1L) + + data("iris") + model <- lgb.train( + params = list(objective = "multiclass", num_class = 3L) + , data = lgb.Dataset( + as.matrix(iris[, -5L]) + , label = as.numeric(factor(iris$Species)) - 1.0 + ) + , verbose = .LGB_VERBOSITY + , nrounds = 5L + ) + ncols <- .Call(LGBM_BoosterGetNumFeature_R, model$.__enclos_env__$private$handle) + expect_equal(ncols, ncol(iris) - 1L) +}) + +# Helper function that creates a fitted model with nrounds boosting rounds +.get_test_model <- function(nrounds) { + set.seed(1L) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list(objective = "binary", num_threads = .LGB_MAX_THREADS) + , nrounds = nrounds + , verbose = .LGB_VERBOSITY + ) + return(bst) +} + +# Simplified version of lgb.model.dt.tree() +.get_trees_from_dump <- function(x) { + parsed <- jsonlite::fromJSON( + txt = x + , simplifyVector = TRUE + , simplifyDataFrame = FALSE + , simplifyMatrix = FALSE + , flatten = FALSE + ) + return(lapply(parsed$tree_info, FUN = .single_tree_parse)) +} + +test_that("num_iteration and start_iteration work for lgb.dump()", { + bst <- .get_test_model(5L) + + first2 <- .get_trees_from_dump(lgb.dump(bst, num_iteration = 2L)) + last3 <- .get_trees_from_dump( + lgb.dump(bst, num_iteration = 3L, start_iteration = 3L) + ) + all5 <- .get_trees_from_dump(lgb.dump(bst)) + too_many <- .get_trees_from_dump(lgb.dump(bst, num_iteration = 10L)) + + expect_equal( + data.table::rbindlist(c(first2, last3)), data.table::rbindlist(all5) + ) + expect_equal(too_many, all5) +}) + +test_that("num_iteration and start_iteration work for lgb.save()", { + .get_n_trees <- function(x) { + return(length(.get_trees_from_dump(lgb.dump(x)))) + } + + .save_and_load <- function(bst, ...) { + model_file <- tempfile(fileext = ".model") + lgb.save(bst, model_file, ...) + return(lgb.load(model_file)) + } + + bst <- .get_test_model(5L) + n_first2 <- .get_n_trees(.save_and_load(bst, num_iteration = 2L)) + n_last3 <- .get_n_trees( + .save_and_load(bst, num_iteration = 3L, start_iteration = 3L) + ) + n_all5 <- .get_n_trees(.save_and_load(bst)) + n_too_many <- .get_n_trees(.save_and_load(bst, num_iteration = 10L)) + + expect_equal(n_first2, 2L) + expect_equal(n_last3, 3L) + expect_equal(n_all5, 5L) + expect_equal(n_too_many, 5L) +}) + +test_that("num_iteration and start_iteration work for save_model_to_string()", { + .get_n_trees_from_string <- function(x) { + return(sum(gregexpr("Tree=", x, fixed = TRUE)[[1L]] > 0L)) + } + + bst <- .get_test_model(5L) + + n_first2 <- .get_n_trees_from_string( + bst$save_model_to_string(num_iteration = 2L) + ) + n_last3 <- .get_n_trees_from_string( + bst$save_model_to_string(num_iteration = 3L, start_iteration = 3L) + ) + n_all5 <- .get_n_trees_from_string(bst$save_model_to_string()) + n_too_many <- .get_n_trees_from_string( + bst$save_model_to_string(num_iteration = 10L) + ) + + expect_equal(n_first2, 2L) + expect_equal(n_last3, 3L) + expect_equal(n_all5, 5L) + expect_equal(n_too_many, 5L) +}) diff --git a/R-package/tests/testthat/test_lgb.convert_with_rules.R b/R-package/tests/testthat/test_lgb.convert_with_rules.R new file mode 100644 index 0000000..39438f0 --- /dev/null +++ b/R-package/tests/testthat/test_lgb.convert_with_rules.R @@ -0,0 +1,246 @@ +test_that("lgb.convert_with_rules() rejects inputs that are not a data.table or data.frame", { + bad_inputs <- list( + matrix(1.0:10.0, 2L, 5L) + , TRUE + , c("a", "b") + , NA + , 10L + , lgb.Dataset( + data = matrix(1.0:10.0, 2L, 5L) + , params = list() + ) + ) + for (bad_input in bad_inputs) { + expect_error({ + conversion_result <- lgb.convert_with_rules(bad_input) + }, regexp = "lgb.convert_with_rules: you provided", fixed = TRUE) + } +}) + +test_that("lgb.convert_with_rules() should work correctly for a dataset with only character columns", { + testDF <- data.frame( + col1 = c("a", "b", "c") + , col2 = c("green", "green", "red") + , stringsAsFactors = FALSE + ) + testDT <- data.table::as.data.table(testDF) + for (input_data in list(testDF, testDT)) { + conversion_result <- lgb.convert_with_rules(input_data) + # dataset should have been converted to integer + converted_dataset <- conversion_result[["data"]] + expect_identical(class(input_data), class(converted_dataset)) + expect_identical(class(converted_dataset[["col1"]]), "integer") + expect_identical(class(converted_dataset[["col2"]]), "integer") + expect_identical(converted_dataset[["col1"]], c(1L, 2L, 3L)) + expect_identical(converted_dataset[["col2"]], c(1L, 1L, 2L)) + # rules should be returned and correct + rules <- conversion_result$rules + expect_true(methods::is(rules, "list")) + expect_length(rules, ncol(input_data)) + expect_identical(rules[["col1"]], c("a" = 1L, "b" = 2L, "c" = 3L)) + expect_identical(rules[["col2"]], c("green" = 1L, "red" = 2L)) + } +}) + +test_that("lgb.convert_with_rules() should work correctly for a dataset with only factor columns", { + testDF <- data.frame( + col1 = as.factor(c("a", "b", "c")) + , col2 = as.factor(c("green", "green", "red")) + , stringsAsFactors = FALSE + ) + testDT <- data.table::as.data.table(testDF) + for (input_data in list(testDF, testDT)) { + conversion_result <- lgb.convert_with_rules(input_data) + # dataset should have been converted to integer + converted_dataset <- conversion_result[["data"]] + expect_identical(class(input_data), class(converted_dataset)) + expect_identical(class(converted_dataset[["col1"]]), "integer") + expect_identical(class(converted_dataset[["col2"]]), "integer") + expect_identical(converted_dataset[["col1"]], c(1L, 2L, 3L)) + expect_identical(converted_dataset[["col2"]], c(1L, 1L, 2L)) + # rules should be returned and correct + rules <- conversion_result$rules + expect_true(methods::is(rules, "list")) + expect_length(rules, ncol(input_data)) + expect_identical(rules[["col1"]], c("a" = 1L, "b" = 2L, "c" = 3L)) + expect_identical(rules[["col2"]], c("green" = 1L, "red" = 2L)) + } +}) + +test_that("lgb.convert_with_rules() should not change a dataset with only integer columns", { + testDF <- data.frame( + col1 = 11L:15L + , col2 = 16L:20L + , stringsAsFactors = FALSE + ) + testDT <- data.table::as.data.table(testDF) + for (input_data in list(testDF, testDT)) { + conversion_result <- lgb.convert_with_rules(input_data) + # dataset should have been converted to integer + converted_dataset <- conversion_result[["data"]] + expect_identical(converted_dataset, input_data) + # rules should be returned and correct + rules <- conversion_result$rules + expect_identical(rules, list()) + } +}) + +test_that("lgb.convert_with_rules() should work correctly for a dataset with numeric, factor, and character columns", { + testDF <- data.frame( + character_col = c("a", "b", "c") + , numeric_col = c(1.0, 9.0, 10.0) + , factor_col = as.factor(c("n", "n", "y")) + , stringsAsFactors = FALSE + ) + testDT <- data.table::as.data.table(testDF) + for (input_data in list(testDF, testDT)) { + conversion_result <- lgb.convert_with_rules(input_data) + # dataset should have been converted to numeric + converted_dataset <- conversion_result[["data"]] + expect_identical(class(input_data), class(converted_dataset)) + expect_identical(class(converted_dataset[["character_col"]]), "integer") + expect_identical(class(converted_dataset[["factor_col"]]), "integer") + expect_identical(converted_dataset[["character_col"]], c(1L, 2L, 3L)) + expect_identical(converted_dataset[["factor_col"]], c(1L, 1L, 2L)) + # rules should be returned and correct + rules <- conversion_result$rules + expect_true(methods::is(rules, "list")) + expect_length(rules, 2L) + expect_identical(rules[["character_col"]], c("a" = 1L, "b" = 2L, "c" = 3L)) + expect_identical(rules[["factor_col"]], c("n" = 1L, "y" = 2L)) + + # today, lgb.convert_with_rules() does not convert numeric columns + expect_identical(class(converted_dataset[["numeric_col"]]), "numeric") + expect_identical(converted_dataset[["numeric_col"]], c(1.0, 9.0, 10.0)) + } +}) + +test_that("lgb.convert_with_rules() should convert missing values to the expected value", { + testDF <- data.frame( + character_col = c("a", NA_character_, "c") + , na_col = rep(NA, 3L) + , na_real_col = rep(NA_real_, 3L) + , na_int_col = rep(NA_integer_, 3L) + , na_character_col = rep(NA_character_, 3L) + , numeric_col = c(1.0, 9.0, NA_real_) + , factor_col = as.factor(c("n", "n", "y")) + , integer_col = c(1L, 9L, NA_integer_) + , stringsAsFactors = FALSE + ) + testDT <- data.table::as.data.table(testDF) + for (input_data in list(testDF, testDT)) { + conversion_result <- lgb.convert_with_rules(input_data) + # dataset should have been converted to integer + converted_dataset <- conversion_result[["data"]] + expect_identical(class(input_data), class(converted_dataset)) + + expect_identical(class(converted_dataset[["character_col"]]), "integer") + expect_identical(converted_dataset[["character_col"]], c(1L, 0L, 2L)) + + # does not try to fill 0s in for already-integer columns + expect_identical(class(converted_dataset[["integer_col"]]), "integer") + expect_identical(converted_dataset[["integer_col"]], c(1L, 9L, NA_integer_)) + expect_identical(class(converted_dataset[["na_int_col"]]), "integer") + expect_identical(converted_dataset[["na_int_col"]], rep(NA_integer_, nrow(converted_dataset))) + + expect_identical(class(converted_dataset[["factor_col"]]), "integer") + expect_identical(converted_dataset[["factor_col"]], c(1L, 1L, 2L)) + + # NAs in character columns should be converted to 0 + expect_identical(class(converted_dataset[["na_character_col"]]), "integer") + expect_identical(converted_dataset[["na_character_col"]], rep(0L, nrow(converted_dataset))) + + # logical should be converted to integer + expect_identical(class(converted_dataset[["na_col"]]), "integer") + expect_identical(converted_dataset[["na_col"]], rep(-1L, 3L)) + + # lgb.convert_with_rules() should not convert numeric columns to integer + expect_identical(class(converted_dataset[["na_real_col"]]), "numeric") + expect_identical(converted_dataset[["na_real_col"]], rep(NA_real_, nrow(converted_dataset))) + expect_identical(class(converted_dataset[["numeric_col"]]), "numeric") + expect_identical(converted_dataset[["numeric_col"]], c(1.0, 9.0, NA_real_)) + + # rules should be returned and correct + rules <- conversion_result$rules + expect_true(methods::is(rules, "list")) + expect_length(rules, 3L) + expect_identical(rules[["character_col"]], c("a" = 1L, "c" = 2L)) + expect_identical(rules[["factor_col"]], c("n" = 1L, "y" = 2L)) + expect_identical(rules[["na_col"]], stats::setNames(c(0L, 1L), c(FALSE, TRUE))) + } +}) + +test_that("lgb.convert_with_rules() should work correctly if you provide your own well-formed rules", { + testDF <- data.frame( + character_col = c("a", NA_character_, "c", "a", "a", "c") + , na_col = rep(NA, 6L) + , na_real_col = rep(NA_real_, 6L) + , na_int_col = rep(NA_integer_, 6L) + , na_character_col = rep(NA_character_, 6L) + , numeric_col = c(1.0, 9.0, NA_real_, 10.0, 11.0, 12.0) + , factor_col = as.factor(c("n", "n", "y", "y", "n", "n")) + , integer_col = c(1L, 9L, NA_integer_, 1L, 1L, 1L) + , stringsAsFactors = FALSE + ) + testDT <- data.table::as.data.table(testDF) + + # value used by lgb.convert_with_rules() when it encounters a categorical value that + # is not in the provided rules + UNKNOWN_FACTOR_VALUE <- 0L + UNKNOWN_LOGICAL_VALUE <- -1L + for (input_data in list(testDF, testDT)) { + custom_rules <- list( + "character_col" = c( + "a" = 5L + , "c" = -10L + ) + , "factor_col" = c( + "n" = 65L + , "y" = 66L + ) + ) + conversion_result <- lgb.convert_with_rules( + data = input_data + , rules = custom_rules + ) + + # dataset should have been converted to integer + converted_dataset <- conversion_result[["data"]] + expect_identical(class(input_data), class(converted_dataset)) + + expect_identical(class(converted_dataset[["character_col"]]), "integer") + expect_identical(converted_dataset[["character_col"]], c(5L, UNKNOWN_FACTOR_VALUE, -10L, 5L, 5L, -10L)) + + expect_identical(class(converted_dataset[["factor_col"]]), "integer") + expect_identical(converted_dataset[["factor_col"]], c(65L, 65L, 66L, 66L, 65L, 65L)) + + # columns not specified in rules are not going to be converted, unless they are all NA + for (col in c("na_real_col", "na_int_col", "numeric_col", "integer_col")) { + expect_identical(converted_dataset[[col]], input_data[[col]]) + } + + # non-numeric/integer columns that are all NA should have been filled in + expect_identical(converted_dataset[["na_col"]], rep(UNKNOWN_LOGICAL_VALUE, 6L)) + expect_identical(converted_dataset[["na_character_col"]], rep(UNKNOWN_FACTOR_VALUE, 6L)) + + # the rules you passed in should be returned unchanged + rules <- conversion_result$rules + expect_identical(rules, custom_rules) + } +}) + +test_that("lgb.convert_with_rules() should modify data.tables in-place", { + testDT <- data.table::data.table( + character_col = c("a", NA_character_, "c") + , na_col = rep(NA, 3L) + , na_real_col = rep(NA_real_, 3L) + , na_int_col = rep(NA_integer_, 3L) + , na_character_col = rep(NA_character_, 3L) + , numeric_col = c(1.0, 9.0, NA_real_) + , factor_col = as.factor(c("n", "n", "y")) + , integer_col = c(1L, 9L, NA_integer_) + ) + conversion_result <- lgb.convert_with_rules(testDT) + resultDT <- conversion_result[["data"]] + expect_identical(resultDT, testDT) +}) diff --git a/R-package/tests/testthat/test_lgb.importance.R b/R-package/tests/testthat/test_lgb.importance.R new file mode 100644 index 0000000..7dcf756 --- /dev/null +++ b/R-package/tests/testthat/test_lgb.importance.R @@ -0,0 +1,37 @@ +test_that("lgb.importance() should reject bad inputs", { + bad_inputs <- list( + .Machine$integer.max + , Inf + , -Inf + , NA + , NA_real_ + , -10L:10L + , list(c("a", "b", "c")) + , data.frame( + x = rnorm(20L) + , y = sample( + x = c(1L, 2L) + , size = 20L + , replace = TRUE + ) + ) + , data.table::data.table( + x = rnorm(20L) + , y = sample( + x = c(1L, 2L) + , size = 20L + , replace = TRUE + ) + ) + , lgb.Dataset( + data = matrix(rnorm(100L), ncol = 2L) + , label = matrix(sample(c(0L, 1L), 50L, replace = TRUE)) + ) + , "lightgbm.model" + ) + for (input in bad_inputs) { + expect_error({ + lgb.importance(input) + }, regexp = "'model' has to be an object of class lgb\\.Booster") + } +}) diff --git a/R-package/tests/testthat/test_lgb.interpret.R b/R-package/tests/testthat/test_lgb.interpret.R new file mode 100644 index 0000000..377ad3a --- /dev/null +++ b/R-package/tests/testthat/test_lgb.interpret.R @@ -0,0 +1,115 @@ +.sigmoid <- function(x) { + 1.0 / (1.0 + exp(-x)) +} +.logit <- function(x) { + log(x / (1.0 - x)) +} + +test_that("lgb.interpret works as expected for binary classification", { + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset(train$data, label = train$label) + set_field( + dataset = dtrain + , field_name = "init_score" + , data = rep( + .logit(mean(train$label)) + , length(train$label) + ) + ) + data(agaricus.test, package = "lightgbm") + test <- agaricus.test + params <- list( + objective = "binary" + , learning_rate = 0.01 + , num_leaves = 63L + , max_depth = -1L + , min_data_in_leaf = 1L + , min_sum_hessian_in_leaf = 1.0 + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + model <- lgb.train( + params = params + , data = dtrain + , nrounds = 3L + ) + num_trees <- 5L + tree_interpretation <- lgb.interpret( + model = model + , data = test$data + , idxset = seq_len(num_trees) + ) + expect_identical(class(tree_interpretation), "list") + expect_true(length(tree_interpretation) == num_trees) + expect_null(names(tree_interpretation)) + expect_true(all( + sapply( + X = tree_interpretation + , FUN = function(treeDT) { + checks <- c( + data.table::is.data.table(treeDT) + , identical(names(treeDT), c("Feature", "Contribution")) + , is.character(treeDT[, Feature]) + , is.numeric(treeDT[, Contribution]) + ) + return(all(checks)) + } + ) + )) +}) + +test_that("lgb.intereprete works as expected for multiclass classification", { + data(iris) + + # We must convert factors to numeric + # They must be starting from number 0 to use multiclass + # For instance: 0, 1, 2, 3, 4, 5... + iris$Species <- as.numeric(as.factor(iris$Species)) - 1L + + # Create imbalanced training data (20, 30, 40 examples for classes 0, 1, 2) + train <- as.matrix(iris[c(1L:20L, 51L:80L, 101L:140L), ]) + # The 10 last samples of each class are for validation + test <- as.matrix(iris[c(41L:50L, 91L:100L, 141L:150L), ]) + dtrain <- lgb.Dataset(data = train[, 1L:4L], label = train[, 5L]) + dtest <- lgb.Dataset.create.valid(dtrain, data = test[, 1L:4L], label = test[, 5L]) + params <- list( + objective = "multiclass" + , metric = "multi_logloss" + , num_class = 3L + , learning_rate = 0.00001 + , min_data = 1L + , verbose = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + model <- lgb.train( + params = params + , data = dtrain + , nrounds = 3L + ) + num_trees <- 5L + tree_interpretation <- lgb.interpret( + model = model + , data = test[, 1L:4L] + , idxset = seq_len(num_trees) + ) + expect_identical(class(tree_interpretation), "list") + expect_true(length(tree_interpretation) == num_trees) + expect_null(names(tree_interpretation)) + expect_true(all( + sapply( + X = tree_interpretation + , FUN = function(treeDT) { + checks <- c( + data.table::is.data.table(treeDT) + , identical(names(treeDT), c("Feature", "Class 0", "Class 1", "Class 2")) + , is.character(treeDT[, Feature]) + , is.numeric(treeDT[, `Class 0`]) + , is.numeric(treeDT[, `Class 1`]) + , is.numeric(treeDT[, `Class 2`]) + ) + return(all(checks)) + } + ) + )) +}) diff --git a/R-package/tests/testthat/test_lgb.model.dt.tree.R b/R-package/tests/testthat/test_lgb.model.dt.tree.R new file mode 100644 index 0000000..c27703e --- /dev/null +++ b/R-package/tests/testthat/test_lgb.model.dt.tree.R @@ -0,0 +1,184 @@ +NROUNDS <- 10L +MAX_DEPTH <- 3L +N <- nrow(iris) +X <- data.matrix(iris[2L:4L]) +FEAT <- colnames(X) +NCLASS <- nlevels(iris[, 5L]) + +model_reg <- lgb.train( + params = list( + objective = "regression" + , num_threads = .LGB_MAX_THREADS + , max.depth = MAX_DEPTH + ) + , data = lgb.Dataset(X, label = iris[, 1L]) + , verbose = .LGB_VERBOSITY + , nrounds = NROUNDS +) + +model_binary <- lgb.train( + params = list( + objective = "binary" + , num_threads = .LGB_MAX_THREADS + , max.depth = MAX_DEPTH + ) + , data = lgb.Dataset(X, label = iris[, 5L] == "setosa") + , verbose = .LGB_VERBOSITY + , nrounds = NROUNDS +) + +model_multiclass <- lgb.train( + params = list( + objective = "multiclass" + , num_threads = .LGB_MAX_THREADS + , max.depth = MAX_DEPTH + , num_classes = NCLASS + ) + , data = lgb.Dataset(X, label = as.integer(iris[, 5L]) - 1L) + , verbose = .LGB_VERBOSITY + , nrounds = NROUNDS +) + +model_rank <- lgb.train( + params = list( + objective = "lambdarank" + , num_threads = .LGB_MAX_THREADS + , max.depth = MAX_DEPTH + , lambdarank_truncation_level = 3L + ) + , data = lgb.Dataset( + X + , label = as.integer(iris[, 1L] > 5.8) + , group = rep(10L, times = 15L) + ) + , verbose = .LGB_VERBOSITY + , nrounds = NROUNDS +) + +models <- list( + reg = model_reg + , bin = model_binary + , multi = model_multiclass + , rank = model_rank +) + +for (model_name in names(models)) { + model <- models[[model_name]] + expected_n_trees <- NROUNDS + if (model_name == "multi") { + expected_n_trees <- NROUNDS * NCLASS + } + df <- as.data.frame(lgb.model.dt.tree(model)) + df_list <- split(df, f = df$tree_index, drop = TRUE) + + df_leaf <- df[!is.na(df$leaf_index), ] + df_internal <- df[is.na(df$leaf_index), ] + + test_that("lgb.model.dt.tree() returns the right number of trees", { + expect_equal(length(unique(df$tree_index)), expected_n_trees) + }) + + test_that("num_iteration can return less trees", { + expect_equal( + length(unique(lgb.model.dt.tree(model, num_iteration = 2L)$tree_index)) + , 2L * (if (model_name == "multi") NCLASS else 1L) + ) + }) + + test_that("Tree index from lgb.model.dt.tree() is in 0:(NROUNS-1)", { + expect_equal(unique(df$tree_index), (0L:(expected_n_trees - 1L))) + }) + + test_that("Depth calculated from lgb.model.dt.tree() respects max.depth", { + expect_true(max(df$depth) <= MAX_DEPTH) + }) + + test_that("Each tree from lgb.model.dt.tree() has single root node", { + expect_equal( + unname(sapply(df_list, function(df) sum(df$depth == 0L))) + , rep(1L, expected_n_trees) + ) + }) + + test_that("Each tree from lgb.model.dt.tree() has two depth 1 nodes", { + expect_equal( + unname(sapply(df_list, function(df) sum(df$depth == 1L))) + , rep(2L, expected_n_trees) + ) + }) + + test_that("leaves from lgb.model.dt.tree() do not have split info", { + internal_node_cols <- c( + "split_index" + , "split_feature" + , "split_gain" + , "threshold" + , "decision_type" + , "default_left" + , "internal_value" + , "internal_count" + ) + expect_true(all(is.na(df_leaf[internal_node_cols]))) + }) + + test_that("leaves from lgb.model.dt.tree() have valid leaf info", { + expect_true(all(df_leaf$leaf_index %in% 0L:(2.0^MAX_DEPTH - 1.0))) + expect_true(all(is.finite(df_leaf$leaf_value))) + expect_true(all(df_leaf$leaf_count > 0L & df_leaf$leaf_count <= N)) + }) + + test_that("non-leaves from lgb.model.dt.tree() do not have leaf info", { + leaf_node_cols <- c( + "leaf_index", "leaf_parent", "leaf_value", "leaf_count" + ) + expect_true(all(is.na(df_internal[leaf_node_cols]))) + }) + + test_that("non-leaves from lgb.model.dt.tree() have valid split info", { + expect_true( + all( + sapply( + split(df_internal, df_internal$tree_index), + function(x) all(x$split_index %in% 0L:(nrow(x) - 1L)) + ) + ) + ) + + expect_true(all(df_internal$split_feature %in% FEAT)) + + num_cols <- c("split_gain", "threshold", "internal_value") + expect_true(all(is.finite(unlist(df_internal[, num_cols])))) + + # range of decision type? + expect_true(all(df_internal$default_left %in% c(TRUE, FALSE))) + + counts <- df_internal$internal_count + expect_true(all(counts > 1L & counts <= N)) + }) +} + +test_that("num_iteration and start_iteration work as expected", { + set.seed(1L) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + bst <- lightgbm( + data = as.matrix(train$data) + , label = train$label + , params = list(objective = "binary", num_threads = .LGB_MAX_THREADS) + , nrounds = 5L + , verbose = .LGB_VERBOSITY + ) + + first2 <- lgb.model.dt.tree(bst, num_iteration = 2L) + last3 <- lgb.model.dt.tree(bst, num_iteration = 3L, start_iteration = 3L) + all5 <- lgb.model.dt.tree(bst) + too_many <- lgb.model.dt.tree(bst, num_iteration = 10L) + + expect_equal(data.table::rbindlist(list(first2, last3)), all5) + expect_equal(too_many, all5) + + # Check tree indices + expect_equal(unique(first2[["tree_index"]]), 0L:1L) + expect_equal(unique(last3[["tree_index"]]), 2L:4L) + expect_equal(unique(all5[["tree_index"]]), 0L:4L) +}) diff --git a/R-package/tests/testthat/test_lgb.plot.importance.R b/R-package/tests/testthat/test_lgb.plot.importance.R new file mode 100644 index 0000000..e7ff63f --- /dev/null +++ b/R-package/tests/testthat/test_lgb.plot.importance.R @@ -0,0 +1,47 @@ +test_that("lgb.plot.importance() should run without error for well-formed inputs", { + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset(train$data, label = train$label) + params <- list( + objective = "binary" + , learning_rate = 0.01 + , num_leaves = 63L + , max_depth = -1L + , min_data_in_leaf = 1L + , min_sum_hessian_in_leaf = 1.0 + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + model <- lgb.train(params, dtrain, 3L) + tree_imp <- lgb.importance(model, percentage = TRUE) + + # Check that there are no plots present before plotting + expect_null(dev.list()) + + args_no_cex <- list( + "tree_imp" = tree_imp + , top_n = 10L + , measure = "Gain" + ) + args_cex <- args_no_cex + args_cex[["cex"]] <- 0.75 + + for (arg_list in list(args_no_cex, args_cex)) { + + resDT <- do.call( + what = lgb.plot.importance + , args = arg_list + ) + + # Check that lgb.plot.importance() returns the data.table of the plotted data + expect_true(data.table::is.data.table(resDT)) + expect_named(resDT, c("Feature", "Gain", "Cover", "Frequency")) + + # Check that a plot was produced + expect_false(is.null(dev.list())) + + # remove all plots + dev.off() + expect_null(dev.list()) + } +}) diff --git a/R-package/tests/testthat/test_lgb.plot.interpretation.R b/R-package/tests/testthat/test_lgb.plot.interpretation.R new file mode 100644 index 0000000..b8359d5 --- /dev/null +++ b/R-package/tests/testthat/test_lgb.plot.interpretation.R @@ -0,0 +1,99 @@ +.sigmoid <- function(x) { + 1.0 / (1.0 + exp(-x)) +} +.logit <- function(x) { + log(x / (1.0 - x)) +} + +test_that("lgb.plot.interpretation works as expected for binary classification", { + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + dtrain <- lgb.Dataset(train$data, label = train$label) + set_field( + dataset = dtrain + , field_name = "init_score" + , data = rep( + .logit(mean(train$label)) + , length(train$label) + ) + ) + data(agaricus.test, package = "lightgbm") + test <- agaricus.test + params <- list( + objective = "binary" + , learning_rate = 0.01 + , num_leaves = 63L + , max_depth = -1L + , min_data_in_leaf = 1L + , min_sum_hessian_in_leaf = 1.0 + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + model <- lgb.train( + params = params + , data = dtrain + , nrounds = 3L + ) + num_trees <- 5L + tree_interpretation <- lgb.interpret( + model = model + , data = test$data + , idxset = seq_len(num_trees) + ) + expect_true({ + lgb.plot.interpretation( + tree_interpretation_dt = tree_interpretation[[1L]] + , top_n = 5L + ) + TRUE + }) + + # should also work when you explicitly pass cex + plot_res <- lgb.plot.interpretation( + tree_interpretation_dt = tree_interpretation[[1L]] + , top_n = 5L + , cex = 0.95 + ) + expect_null(plot_res) +}) + +test_that("lgb.plot.interpretation works as expected for multiclass classification", { + data(iris) + + # We must convert factors to numeric + # They must be starting from number 0 to use multiclass + # For instance: 0, 1, 2, 3, 4, 5... + iris$Species <- as.numeric(as.factor(iris$Species)) - 1L + + # Create imbalanced training data (20, 30, 40 examples for classes 0, 1, 2) + train <- as.matrix(iris[c(1L:20L, 51L:80L, 101L:140L), ]) + # The 10 last samples of each class are for validation + test <- as.matrix(iris[c(41L:50L, 91L:100L, 141L:150L), ]) + dtrain <- lgb.Dataset(data = train[, 1L:4L], label = train[, 5L]) + dtest <- lgb.Dataset.create.valid(dtrain, data = test[, 1L:4L], label = test[, 5L]) + params <- list( + objective = "multiclass" + , metric = "multi_logloss" + , num_class = 3L + , learning_rate = 0.00001 + , min_data = 1L + , num_threads = .LGB_MAX_THREADS + ) + model <- lgb.train( + params = params + , data = dtrain + , nrounds = 3L + , verbose = .LGB_VERBOSITY + ) + num_trees <- 5L + tree_interpretation <- lgb.interpret( + model = model + , data = test[, 1L:4L] + , idxset = seq_len(num_trees) + ) + plot_res <- lgb.plot.interpretation( + tree_interpretation_dt = tree_interpretation[[1L]] + , top_n = 5L + ) + expect_null(plot_res) +}) diff --git a/R-package/tests/testthat/test_metrics.R b/R-package/tests/testthat/test_metrics.R new file mode 100644 index 0000000..2974ec0 --- /dev/null +++ b/R-package/tests/testthat/test_metrics.R @@ -0,0 +1,10 @@ +test_that(".METRICS_HIGHER_BETTER() should be well formed", { + metrics <- .METRICS_HIGHER_BETTER() + metric_names <- names(.METRICS_HIGHER_BETTER()) + # should be a logical vector + expect_true(is.logical(metrics)) + # no metrics should be repeated + expect_true(length(unique(metric_names)) == length(metrics)) + # should not be any NAs + expect_false(anyNA(metrics)) +}) diff --git a/R-package/tests/testthat/test_multithreading.R b/R-package/tests/testthat/test_multithreading.R new file mode 100644 index 0000000..e2f3169 --- /dev/null +++ b/R-package/tests/testthat/test_multithreading.R @@ -0,0 +1,16 @@ +test_that("getLGBMthreads() and setLGBMthreads() work as expected", { + # works with integer input + ret <- setLGBMthreads(2L) + expect_null(ret) + expect_equal(getLGBMthreads(), 2L) + + # works with float input + ret <- setLGBMthreads(1.0) + expect_null(ret) + expect_equal(getLGBMthreads(), 1L) + + # setting to any negative number sets max threads to -1 + ret <- setLGBMthreads(-312L) + expect_null(ret) + expect_equal(getLGBMthreads(), -1L) +}) diff --git a/R-package/tests/testthat/test_parameters.R b/R-package/tests/testthat/test_parameters.R new file mode 100644 index 0000000..2e3aaa3 --- /dev/null +++ b/R-package/tests/testthat/test_parameters.R @@ -0,0 +1,169 @@ +data(agaricus.train, package = "lightgbm") +data(agaricus.test, package = "lightgbm") +train <- agaricus.train +test <- agaricus.test + +test_that("Feature penalties work properly", { + # Fit a series of models with varying penalty on most important variable + var_name <- "odor=none" + var_index <- which(train$data@Dimnames[[2L]] == var_name) + + bst <- lapply(seq(1.0, 0.0, by = -0.1), function(x) { + feature_penalties <- rep(1.0, ncol(train$data)) + feature_penalties[var_index] <- x + lightgbm( + data = train$data + , label = train$label + , params = list( + num_leaves = 5L + , learning_rate = 0.05 + , objective = "binary" + , feature_penalty = paste(feature_penalties, collapse = ",") + , metric = "binary_error" + , num_threads = .LGB_MAX_THREADS + ) + , nrounds = 5L + , verbose = -1L + ) + }) + + var_gain <- lapply(bst, function(x) lgb.importance(x)[Feature == var_name, Gain]) + var_cover <- lapply(bst, function(x) lgb.importance(x)[Feature == var_name, Cover]) + var_freq <- lapply(bst, function(x) lgb.importance(x)[Feature == var_name, Frequency]) + + # Ensure that feature gain, cover, and frequency decreases with stronger penalties + expect_true(all(diff(unlist(var_gain)) <= 0.0)) + expect_true(all(diff(unlist(var_cover)) <= 0.0)) + expect_true(all(diff(unlist(var_freq)) <= 0.0)) + + expect_lt(min(diff(unlist(var_gain))), 0.0) + expect_lt(min(diff(unlist(var_cover))), 0.0) + expect_lt(min(diff(unlist(var_freq))), 0.0) + + # Ensure that feature is not used when feature_penalty = 0 + expect_length(var_gain[[length(var_gain)]], 0L) +}) + +test_that(".PARAMETER_ALIASES() returns a named list of character vectors, where names are unique", { + param_aliases <- .PARAMETER_ALIASES() + expect_identical(class(param_aliases), "list") + expect_true(length(param_aliases) > 100L) + expect_true(is.character(names(param_aliases))) + expect_true(is.character(param_aliases[["boosting"]])) + expect_true(is.character(param_aliases[["early_stopping_round"]])) + expect_true(is.character(param_aliases[["num_iterations"]])) + expect_true(is.character(param_aliases[["pre_partition"]])) + expect_true(length(names(param_aliases)) == length(param_aliases)) + expect_true(all(sapply(param_aliases, is.character))) + expect_true(length(unique(names(param_aliases))) == length(param_aliases)) + expect_equal(sort(param_aliases[["task"]]), c("task", "task_type")) + expect_equal(param_aliases[["bagging_fraction"]], c("bagging_fraction", "bagging", "sub_row", "subsample")) +}) + +test_that(".PARAMETER_ALIASES() uses the internal session cache", { + + cache_key <- "PARAMETER_ALIASES" + + # clear cache, so this test isn't reliant on the order unit tests are run in + if (exists(cache_key, where = .lgb_session_cache_env)) { + rm(list = cache_key, envir = .lgb_session_cache_env) + } + expect_false(exists(cache_key, where = .lgb_session_cache_env)) + + # check that result looks correct for at least one parameter + iter_aliases <- .PARAMETER_ALIASES()[["num_iterations"]] + expect_true(is.character(iter_aliases)) + expect_true(all(c("num_round", "nrounds") %in% iter_aliases)) + + # patch the cache to check that .PARAMETER_ALIASES() checks it + assign( + x = cache_key + , value = list(num_iterations = c("test", "other_test")) + , envir = .lgb_session_cache_env + ) + iter_aliases <- .PARAMETER_ALIASES()[["num_iterations"]] + expect_equal(iter_aliases, c("test", "other_test")) + + # re-set cache so this doesn't interfere with other unit tests + if (exists(cache_key, where = .lgb_session_cache_env)) { + rm(list = cache_key, envir = .lgb_session_cache_env) + } + expect_false(exists(cache_key, where = .lgb_session_cache_env)) +}) + +test_that("training should warn if you use 'dart' boosting with early stopping", { + for (boosting_param in .PARAMETER_ALIASES()[["boosting"]]) { + params <- list( + num_leaves = 5L + , learning_rate = 0.05 + , objective = "binary" + , metric = "binary_error" + , num_threads = .LGB_MAX_THREADS + ) + params[[boosting_param]] <- "dart" + + # warning: early stopping requested + expect_warning({ + result <- lightgbm( + data = train$data + , label = train$label + , params = params + , nrounds = 2L + , verbose = .LGB_VERBOSITY + , early_stopping_rounds = 1L + ) + }, regexp = "Early stopping is not available in 'dart' mode") + + # no warning: early stopping not requested + expect_silent({ + result <- lightgbm( + data = train$data + , label = train$label + , params = params + , nrounds = 2L + , verbose = .LGB_VERBOSITY + , early_stopping_rounds = NULL + ) + }) + } +}) + +test_that("lgb.cv() should warn if you use 'dart' boosting with early stopping", { + for (boosting_param in .PARAMETER_ALIASES()[["boosting"]]) { + params <- list( + num_leaves = 5L + , objective = "binary" + , metric = "binary_error" + , num_threads = .LGB_MAX_THREADS + ) + params[[boosting_param]] <- "dart" + + # warning: early stopping requested + expect_warning({ + result <- lgb.cv( + data = lgb.Dataset( + data = train$data + , label = train$label + ) + , params = params + , nrounds = 2L + , verbose = .LGB_VERBOSITY + , early_stopping_rounds = 1L + ) + }, regexp = "Early stopping is not available in 'dart' mode") + + # no warning: early stopping not requested + expect_silent({ + result <- lgb.cv( + data = lgb.Dataset( + data = train$data + , label = train$label + ) + , params = params + , nrounds = 2L + , verbose = .LGB_VERBOSITY + , early_stopping_rounds = NULL + ) + }) + } +}) diff --git a/R-package/tests/testthat/test_utils.R b/R-package/tests/testthat/test_utils.R new file mode 100644 index 0000000..5da5cff --- /dev/null +++ b/R-package/tests/testthat/test_utils.R @@ -0,0 +1,167 @@ +test_that(".params2str() works as expected for empty lists", { + out_str <- .params2str( + params = list() + ) + expect_identical(class(out_str), "character") + expect_equal(out_str, "") +}) + +test_that(".params2str() works as expected for a key in params with multiple different-length elements", { + metrics <- c("a", "ab", "abc", "abcdefg") + params <- list( + objective = "magic" + , metric = metrics + , nrounds = 10L + , learning_rate = 0.0000001 + ) + out_str <- .params2str( + params = params + ) + expect_identical(class(out_str), "character") + expect_identical( + out_str + , "objective=magic metric=a,ab,abc,abcdefg nrounds=10 learning_rate=0.0000001" + ) +}) + +test_that(".params2str() passes through duplicated params", { + out_str <- .params2str( + params = list( + objective = "regression" + , bagging_fraction = 0.8 + , bagging_fraction = 0.5 # nolint: duplicate_argument. + ) + ) + expect_equal(out_str, "objective=regression bagging_fraction=0.8 bagging_fraction=0.5") +}) + +test_that(".check_eval works as expected with no metric", { + params <- .check_eval( + params = list(device = "cpu") + , eval = "binary_error" + ) + expect_named(params, c("device", "metric")) + expect_identical(params[["metric"]], list("binary_error")) +}) + +test_that(".check_eval adds eval to metric in params", { + params <- .check_eval( + params = list(metric = "auc") + , eval = "binary_error" + ) + expect_named(params, "metric") + expect_identical(params[["metric"]], list("auc", "binary_error")) +}) + +test_that(".check_eval adds eval to metric in params if two evaluation names are provided", { + params <- .check_eval( + params = list(metric = "auc") + , eval = c("binary_error", "binary_logloss") + ) + expect_named(params, "metric") + expect_identical(params[["metric"]], list("auc", "binary_error", "binary_logloss")) +}) + +test_that(".check_eval adds eval to metric in params if a list is provided", { + params <- .check_eval( + params = list(metric = "auc") + , eval = list("binary_error", "binary_logloss") + ) + expect_named(params, "metric") + expect_identical(params[["metric"]], list("auc", "binary_error", "binary_logloss")) +}) + +test_that(".check_eval drops duplicate metrics and preserves order", { + params <- .check_eval( + params = list(metric = "l1") + , eval = list("l2", "rmse", "l1", "rmse") + ) + expect_named(params, "metric") + expect_identical(params[["metric"]], list("l1", "l2", "rmse")) +}) + +test_that(".check_wrapper_param() uses passed-in keyword arg if no alias found in params", { + kwarg_val <- sample(seq_len(100L), size = 1L) + params <- .check_wrapper_param( + main_param_name = "num_iterations" + , params = list() + , alternative_kwarg_value = kwarg_val + ) + expect_equal(params[["num_iterations"]], kwarg_val) +}) + +test_that(".check_wrapper_param() prefers main parameter to alias and keyword arg", { + num_iterations <- sample(seq_len(100L), size = 1L) + kwarg_val <- sample(seq_len(100L), size = 1L) + params <- .check_wrapper_param( + main_param_name = "num_iterations" + , params = list( + num_iterations = num_iterations + , num_tree = sample(seq_len(100L), size = 1L) + , n_estimators = sample(seq_len(100L), size = 1L) + ) + , alternative_kwarg_value = kwarg_val + ) + expect_equal(params[["num_iterations"]], num_iterations) + + # aliases should be removed + expect_identical(params, list(num_iterations = num_iterations)) +}) + +test_that(".check_wrapper_param() prefers alias to keyword arg", { + n_estimators <- sample(seq_len(100L), size = 1L) + num_tree <- sample(seq_len(100L), size = 1L) + kwarg_val <- sample(seq_len(100L), size = 1L) + params <- .check_wrapper_param( + main_param_name = "num_iterations" + , params = list( + num_tree = num_tree + , n_estimators = n_estimators + ) + , alternative_kwarg_value = kwarg_val + ) + expect_equal(params[["num_iterations"]], num_tree) + expect_identical(params, list(num_iterations = num_tree)) + + # switching the order shouldn't switch which one is chosen + params2 <- .check_wrapper_param( + main_param_name = "num_iterations" + , params = list( + n_estimators = n_estimators + , num_tree = num_tree + ) + , alternative_kwarg_value = kwarg_val + ) + expect_equal(params2[["num_iterations"]], num_tree) + expect_identical(params2, list(num_iterations = num_tree)) +}) + +test_that(".equal_or_both_null produces expected results", { + expect_true(.equal_or_both_null(NULL, NULL)) + expect_false(.equal_or_both_null(1.0, NULL)) + expect_false(.equal_or_both_null(NULL, 1.0)) + expect_true(.equal_or_both_null(1.0, 1.0)) + expect_true(.equal_or_both_null(1.0, 1L)) + expect_false(.equal_or_both_null(NA, NULL)) + expect_false(.equal_or_both_null(NULL, NA)) + expect_false(.equal_or_both_null(10.0, 1L)) + expect_true(.equal_or_both_null(0L, 0L)) +}) + +test_that(".check_interaction_constraints() adds skipped features", { + ref <- letters[1L:5L] + ic_num <- list(1L, c(2L, 3L)) + ic_char <- list("a", c("b", "c")) + expected <- list("[0]", "[1,2]", "[3,4]") + + ic_checked_num <- .check_interaction_constraints( + interaction_constraints = ic_num, column_names = ref + ) + + ic_checked_char <- .check_interaction_constraints( + interaction_constraints = ic_char, column_names = ref + ) + + expect_equal(ic_checked_num, expected) + expect_equal(ic_checked_char, expected) +}) diff --git a/R-package/tests/testthat/test_weighted_loss.R b/R-package/tests/testthat/test_weighted_loss.R new file mode 100644 index 0000000..f9f9675 --- /dev/null +++ b/R-package/tests/testthat/test_weighted_loss.R @@ -0,0 +1,65 @@ +test_that("Gamma regression reacts on 'weight'", { + n <- 100L + set.seed(87L) + X <- matrix(runif(2L * n), ncol = 2L) + y <- X[, 1L] + X[, 2L] + runif(n) + X_pred <- X[1L:5L, ] + + params <- list(objective = "gamma", num_threads = .LGB_MAX_THREADS) + + # Unweighted + dtrain <- lgb.Dataset(X, label = y) + bst <- lgb.train( + params = params + , data = dtrain + , nrounds = 4L + , verbose = .LGB_VERBOSITY + ) + pred_unweighted <- predict(bst, X_pred) + + # Constant weight 1 + dtrain <- lgb.Dataset( + X + , label = y + , weight = rep(1.0, n) + ) + bst <- lgb.train( + params = params + , data = dtrain + , nrounds = 4L + , verbose = .LGB_VERBOSITY + ) + pred_weighted_1 <- predict(bst, X_pred) + + # Constant weight 2 + dtrain <- lgb.Dataset( + X + , label = y + , weight = rep(2.0, n) + ) + bst <- lgb.train( + params = params + , data = dtrain + , nrounds = 4L + , verbose = .LGB_VERBOSITY + ) + pred_weighted_2 <- predict(bst, X_pred) + + # Non-constant weights + dtrain <- lgb.Dataset( + X + , label = y + , weight = seq(0.0, 1.0, length.out = n) + ) + bst <- lgb.train( + params = params + , data = dtrain + , nrounds = 4L + , verbose = .LGB_VERBOSITY + ) + pred_weighted <- predict(bst, X_pred) + + expect_equal(pred_unweighted, pred_weighted_1) + expect_equal(pred_weighted_1, pred_weighted_2) + expect_false(all(pred_unweighted == pred_weighted)) +}) diff --git a/R-package/vignettes/basic_walkthrough.Rmd b/R-package/vignettes/basic_walkthrough.Rmd new file mode 100644 index 0000000..f9f4720 --- /dev/null +++ b/R-package/vignettes/basic_walkthrough.Rmd @@ -0,0 +1,127 @@ +--- +title: + "Basic Walkthrough" +description: > + This vignette describes how to train a LightGBM model for binary classification. +output: + markdown::html_format: + options: + toc: true + number_sections: true +vignette: > + %\VignetteIndexEntry{Basic Walkthrough} + %\VignetteEngine{knitr::knitr} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE + , comment = "#>" + , warning = FALSE + , message = FALSE +) +``` + +## Introduction + +Welcome to the world of [LightGBM](https://lightgbm.readthedocs.io/en/latest/), a highly efficient gradient boosting implementation (Ke et al. 2017). + +```{r} +library(lightgbm) +``` + +```{r, include=FALSE} +# limit number of threads used, to be respectful of CRAN's resources when it checks this vignette +data.table::setDTthreads(1L) +setLGBMthreads(2L) +``` + +This vignette will guide you through its basic usage. It will show how to build a simple binary classification model based on a subset of the `bank` dataset (Moro, Cortez, and Rita 2014). You will use the two input features "age" and "balance" to predict whether a client has subscribed a term deposit. + +## The dataset + +The dataset looks as follows. + +```{r} +data(bank, package = "lightgbm") + +bank[1L:5L, c("y", "age", "balance")] + +# Distribution of the response +table(bank$y) +``` + +## Training the model + +The R-package of LightGBM offers two functions to train a model: + +- `lgb.train()`: This is the main training logic. It offers full flexibility but requires a `Dataset` object created by the `lgb.Dataset()` function. +- `lightgbm()`: Simpler, but less flexible. Data can be passed without having to bother with `lgb.Dataset()`. + +### Using the `lightgbm()` function + +In a first step, you need to convert data to numeric. Afterwards, you are ready to fit the model by the `lightgbm()` function. + +```{r} +# Numeric response and feature matrix +y <- as.numeric(bank$y == "yes") +X <- data.matrix(bank[, c("age", "balance")]) + +# Train +fit <- lightgbm( + data = X + , label = y + , params = list( + num_leaves = 4L + , learning_rate = 1.0 + , objective = "binary" + ) + , nrounds = 10L + , verbose = -1L +) + +# Result +summary(predict(fit, X)) +``` + +It seems to have worked! And the predictions are indeed probabilities between 0 and 1. + +### Using the `lgb.train()` function + +Alternatively, you can go for the more flexible interface `lgb.train()`. Here, as an additional step, you need to prepare `y` and `X` by the data API `lgb.Dataset()` of LightGBM. Parameters are passed to `lgb.train()` as a named list. + +```{r} +# Data interface +dtrain <- lgb.Dataset(X, label = y) + +# Parameters +params <- list( + objective = "binary" + , num_leaves = 4L + , learning_rate = 1.0 +) + +# Train +fit <- lgb.train( + params + , data = dtrain + , nrounds = 10L + , verbose = -1L +) +``` + +Try it out! If stuck, visit LightGBM's [documentation](https://lightgbm.readthedocs.io/en/latest/R/index.html) for more details. + +```{r, echo = FALSE, results = "hide"} +# Cleanup +if (file.exists("lightgbm.model")) { + file.remove("lightgbm.model") +} +``` + +## References + +Ke, Guolin, Qi Meng, Thomas Finley, Taifeng Wang, Wei Chen, Weidong Ma, Qiwei Ye, and Tie-Yan Liu. 2017. "LightGBM: A Highly Efficient Gradient Boosting Decision Tree." In Advances in Neural Information Processing Systems 30 (NIPS 2017). + +Moro, Sérgio, Paulo Cortez, and Paulo Rita. 2014. "A Data-Driven Approach to Predict the Success of Bank Telemarketing." Decision Support Systems 62: 22–31. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d3b18f --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ + + +> [!NOTE] +> This project moved from `Microsoft/LightGBM` to `lightgbm-org/LightGBM` in March 2026. +> This repository is still the official LightGBM source code, managed by the same maintainers (including the creator of LightGBM). +> For details, see https://github.com/lightgbm-org/LightGBM/issues/7187 + +Light Gradient Boosting Machine +=============================== + +[![C++ GitHub Actions Build Status](https://github.com/lightgbm-org/LightGBM/actions/workflows/cpp.yml/badge.svg?branch=main)](https://github.com/lightgbm-org/LightGBM/actions/workflows/cpp.yml) +[![Python-package GitHub Actions Build Status](https://github.com/lightgbm-org/LightGBM/actions/workflows/python_package.yml/badge.svg?branch=main)](https://github.com/lightgbm-org/LightGBM/actions/workflows/python_package.yml) +[![R-package GitHub Actions Build Status](https://github.com/lightgbm-org/LightGBM/actions/workflows/r_package.yml/badge.svg?branch=main)](https://github.com/lightgbm-org/LightGBM/actions/workflows/r_package.yml) +[![CUDA Version GitHub Actions Build Status](https://github.com/lightgbm-org/LightGBM/actions/workflows/cuda.yml/badge.svg?branch=main)](https://github.com/lightgbm-org/LightGBM/actions/workflows/cuda.yml) +[![SWIG Wrapper GitHub Actions Build Status](https://github.com/lightgbm-org/LightGBM/actions/workflows/swig.yml/badge.svg?branch=main)](https://github.com/lightgbm-org/LightGBM/actions/workflows/swig.yml) +[![Static Analysis GitHub Actions Build Status](https://github.com/lightgbm-org/LightGBM/actions/workflows/static_analysis.yml/badge.svg?branch=main)](https://github.com/lightgbm-org/LightGBM/actions/workflows/static_analysis.yml) +[![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/1ys5ot401m0fep6l/branch/main?svg=true)](https://ci.appveyor.com/project/guolinke/lightgbm/branch/main) +[![Documentation Status](https://readthedocs.org/projects/lightgbm/badge/?version=latest)](https://lightgbm.readthedocs.io/) +[![Link checks](https://github.com/lightgbm-org/LightGBM/actions/workflows/lychee.yml/badge.svg?branch=main)](https://github.com/lightgbm-org/LightGBM/actions/workflows/lychee.yml) +[![License](https://img.shields.io/github/license/lightgbm-org/lightgbm.svg)](https://github.com/lightgbm-org/LightGBM/blob/main/LICENSE) +[![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver) +[![StackOverflow questions](https://img.shields.io/stackexchange/stackoverflow/t/lightgbm?logo=stackoverflow&logoColor=white&label=StackOverflow%20questions)](https://stackoverflow.com/questions/tagged/lightgbm?sort=votes) +[![Python Versions](https://img.shields.io/pypi/pyversions/lightgbm.svg?logo=python&logoColor=white)](https://pypi.org/project/lightgbm) +[![PyPI Version](https://img.shields.io/pypi/v/lightgbm.svg?logo=pypi&logoColor=white)](https://pypi.org/project/lightgbm) +[![conda Version](https://img.shields.io/conda/vn/conda-forge/lightgbm?logo=conda-forge&logoColor=white&label=conda)](https://anaconda.org/conda-forge/lightgbm) +[![CRAN Version](https://www.r-pkg.org/badges/version/lightgbm)](https://cran.r-project.org/package=lightgbm) +[![NuGet Version](https://img.shields.io/nuget/v/lightgbm?logo=nuget&logoColor=white)](https://www.nuget.org/packages/LightGBM) +[![Winget Version](https://img.shields.io/winget/v/Microsoft.LightGBM)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/m/Microsoft/LightGBM) + +LightGBM is a gradient boosting framework that uses tree based learning algorithms. It is designed to be distributed and efficient with the following advantages: + +- Faster training speed and higher efficiency. +- Lower memory usage. +- Better accuracy. +- Support of parallel, distributed, and GPU learning. +- Capable of handling large-scale data. + +For further details, please refer to [Features](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Features.rst). + +Benefiting from these advantages, LightGBM is being widely-used in many [winning solutions](https://github.com/lightgbm-org/LightGBM/blob/main/examples/README.md#machine-learning-challenge-winning-solutions) of machine learning competitions. + +[Comparison experiments](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Experiments.rst#comparison-experiment) on public datasets show that LightGBM can outperform existing boosting frameworks on both efficiency and accuracy, with significantly lower memory consumption. What's more, [distributed learning experiments](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Experiments.rst#parallel-experiment) show that LightGBM can achieve a linear speed-up by using multiple machines for training in specific settings. + +Get Started and Documentation +----------------------------- + +Our primary documentation is at https://lightgbm.readthedocs.io/ and is generated from this repository. If you are new to LightGBM, follow [the installation instructions](https://lightgbm.readthedocs.io/en/latest/Installation-Guide.html) on that site. + +Next you may want to read: + +- [**Examples**](https://github.com/lightgbm-org/LightGBM/tree/main/examples) showing command line usage of common tasks. +- [**Features**](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Features.rst) and algorithms supported by LightGBM. +- [**Parameters**](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Parameters.rst) is an exhaustive list of customization you can make. +- [**Distributed Learning**](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Parallel-Learning-Guide.rst) and [**GPU Learning**](https://github.com/lightgbm-org/LightGBM/blob/main/docs/GPU-Tutorial.rst) can speed up computation. +- [**FLAML**](https://www.microsoft.com/en-us/research/project/fast-and-lightweight-automl-for-large-scale-data/articles/flaml-a-fast-and-lightweight-automl-library/) provides automated tuning for LightGBM ([code examples](https://microsoft.github.io/FLAML/docs/Examples/AutoML-for-LightGBM/)). +- [**Optuna Hyperparameter Tuner**](https://medium.com/optuna/lightgbm-tuner-new-optuna-integration-for-hyperparameter-optimization-8b7095e99258) provides automated tuning for LightGBM hyperparameters ([code examples](https://github.com/optuna/optuna-examples/blob/main/lightgbm/lightgbm_tuner_simple.py)). +- [**Understanding LightGBM Parameters (and How to Tune Them using Neptune)**](https://neptune.ai/blog/lightgbm-parameters-guide). + +Documentation for contributors: + +- [**How we update readthedocs.io**](https://github.com/lightgbm-org/LightGBM/blob/main/docs/README.rst). +- Check out the [**Development Guide**](https://github.com/lightgbm-org/LightGBM/blob/main/docs/Development-Guide.rst). + +News +---- + +Please refer to changelogs at [GitHub releases](https://github.com/lightgbm-org/LightGBM/releases) page. + +External (Unofficial) Repositories +---------------------------------- + +Projects listed here offer alternative ways to use LightGBM. +They are not maintained or officially endorsed by the `LightGBM` development team. + +JPMML (Java PMML converter): https://github.com/jpmml/jpmml-lightgbm + +Nyoka (Python PMML converter): https://github.com/SoftwareAG/nyoka + +Treelite (model compiler for efficient deployment): https://github.com/dmlc/treelite + +lleaves (LLVM-based model compiler for efficient inference): https://github.com/siboehm/lleaves + +Hummingbird (model compiler into tensor computations): https://github.com/microsoft/hummingbird + +GBNet (use `LightGBM` as a [PyTorch Module](https://docs.pytorch.org/docs/stable/generated/torch.nn.Module.html)): https://github.com/mthorrell/gbnet + +cuML Forest Inference Library (GPU-accelerated inference): https://github.com/rapidsai/cuml + +nvForest (GPU-accelerated inference): https://github.com/rapidsai/nvforest + +daal4py (Intel CPU-accelerated inference): https://github.com/intel/scikit-learn-intelex/tree/master/daal4py + +m2cgen (model appliers for various languages): https://github.com/BayesWitnesses/m2cgen + +leaves (Go model applier): https://github.com/dmitryikh/leaves + +ONNXMLTools (ONNX converter): https://github.com/onnx/onnxmltools + +SHAP (model output explainer): https://github.com/slundberg/shap + +Shapash (model visualization and interpretation): https://github.com/MAIF/shapash + +dtreeviz (decision tree visualization and model interpretation): https://github.com/parrt/dtreeviz + +supertree (interactive visualization of decision trees): https://github.com/mljar/supertree + +SynapseML (LightGBM on Spark): https://github.com/microsoft/SynapseML + +Kubeflow Fairing (LightGBM on Kubernetes): https://github.com/kubeflow/fairing + +Kubeflow Operator (LightGBM on Kubernetes): https://github.com/kubeflow/xgboost-operator + +lightgbm_ray (LightGBM on Ray): https://github.com/ray-project/lightgbm_ray + +Ray (distributed computing framework): https://github.com/ray-project/ray + +Mars (LightGBM on Mars): https://github.com/mars-project/mars + +ML.NET (.NET/C#-package): https://github.com/dotnet/machinelearning + +LightGBM.NET (.NET/C#-package): https://github.com/rca22/LightGBM.Net + +LightGBM Ruby (Ruby gem): https://github.com/ankane/lightgbm-ruby + +LightGBM4j (Java high-level binding): https://github.com/metarank/lightgbm4j + +LightGBM4J (JVM interface for LightGBM written in Scala): https://github.com/seek-oss/lightgbm4j + +Julia-package: https://github.com/IQVIA-ML/LightGBM.jl + +lightgbm3 (Rust binding): https://github.com/Mottl/lightgbm3-rs + +MLServer (inference server for LightGBM): https://github.com/SeldonIO/MLServer + +MLflow (experiment tracking, model monitoring framework): https://github.com/mlflow/mlflow + +FLAML (AutoML library for hyperparameter optimization): https://github.com/microsoft/FLAML + +MLJAR AutoML (AutoML on tabular data): https://github.com/mljar/mljar-supervised + +Optuna (hyperparameter optimization framework): https://github.com/optuna/optuna + +LightGBMLSS (probabilistic modelling with LightGBM): https://github.com/StatMixedML/LightGBMLSS + +LightGBM-MoE (Mixture-of-Experts / regime-switching extension): https://github.com/kyo219/LightGBM-MoE + +darts (time series forecasting and anomaly detection with LightGBM): https://github.com/unit8co/darts + +mlforecast (time series forecasting with LightGBM): https://github.com/Nixtla/mlforecast + +skforecast (time series forecasting with LightGBM): https://github.com/JoaquinAmatRodrigo/skforecast + +`{bonsai}` (R `{parsnip}`-compliant interface): https://github.com/tidymodels/bonsai + +`{mlr3extralearners}` (R `{mlr3}`-compliant interface): https://github.com/mlr-org/mlr3extralearners + +lightgbm-transform (feature transformation binding): https://github.com/lightgbm-org/LightGBM-transform + +`postgresml` (LightGBM training and prediction in SQL, via a Postgres extension): https://github.com/postgresml/postgresml + +`pyodide` (run `lightgbm` Python-package in a web browser): https://github.com/pyodide/pyodide + +`vaex-ml` (Python DataFrame library with its own interface to LightGBM): https://github.com/vaexio/vaex + +Support +------- + +- Ask a question [on Stack Overflow with the `lightgbm` tag](https://stackoverflow.com/questions/ask?tags=lightgbm), we monitor this for new questions. +- Open **bug reports** and **feature requests** on [GitHub issues](https://github.com/lightgbm-org/LightGBM/issues). + +How to Contribute +----------------- + +Check [CONTRIBUTING](https://github.com/lightgbm-org/LightGBM/blob/main/CONTRIBUTING.md) page. + +Microsoft Open Source Code of Conduct +------------------------------------- + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +Reference Papers +---------------- + +Yu Shi, Guolin Ke, Zhuoming Chen, Shuxin Zheng, Tie-Yan Liu. "Quantized Training of Gradient Boosting Decision Trees" ([link](https://proceedings.neurips.cc/paper/2022/hash/77911ed9e6e864ca1a3d165b2c3cb258-Abstract.html)). Advances in Neural Information Processing Systems 35 (NeurIPS 2022), pp. 18822-18833. + +Guolin Ke, Qi Meng, Thomas Finley, Taifeng Wang, Wei Chen, Weidong Ma, Qiwei Ye, Tie-Yan Liu. "[LightGBM: A Highly Efficient Gradient Boosting Decision Tree](https://proceedings.neurips.cc/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html)". Advances in Neural Information Processing Systems 30 (NIPS 2017), pp. 3149-3157. + +Qi Meng, Guolin Ke, Taifeng Wang, Wei Chen, Qiwei Ye, Zhi-Ming Ma, Tie-Yan Liu. "[A Communication-Efficient Parallel Algorithm for Decision Tree](https://proceedings.neurips.cc/paper/2016/hash/10a5ab2db37feedfdeaab192ead4ac0e-Abstract.html)". Advances in Neural Information Processing Systems 29 (NIPS 2016), pp. 1279-1287. + +Huan Zhang, Si Si and Cho-Jui Hsieh. "[GPU Acceleration for Large-scale Tree Boosting](https://arxiv.org/abs/1706.08359)". SysML Conference, 2018. + +License +------- + +This project is licensed under the terms of the MIT license. See [LICENSE](https://github.com/lightgbm-org/LightGBM/blob/main/LICENSE) for additional details. diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..90707b3 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`lightgbm-org/LightGBM` +- 原始仓库:https://github.com/lightgbm-org/LightGBM +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..45edf19 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# Security + +## Reporting Security Issues + +> [!WARNING] +> Do not report security vulnerabilities through public GitHub issues! + +Instead, please open a private vulnerability report in this repository, on the "Security and quality" tab. + +See https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/privately-reporting-a-security-vulnerability + +## Report Details + +We prefer all communications to be in English. + +Reports should include the following: + +* reproducible example showing how the vulnerability can be exploited +* statement about the impact (including affected versions) + +And we'd appreciate if they also include: + +* statement about whether you are interested in implementing the fix yourself + +## Disclosure Policy + +This project is staffed exclusively by volunteers. +Please be patient and allow us time to respond before disclosing vulnerabilities. + +We prefer to coordinate disclosure privately, and are committed to giving credit for confirmed vulnerabilities. diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 0000000..6db5f0c --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +4.6.0.99 diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..31a4ccf --- /dev/null +++ b/biome.json @@ -0,0 +1,41 @@ +{ + "root": false, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": [ + "**", + "!**/lib", + "!build", + "!external_libs", + "!lightgbm-python", + "!lightgbm_r", + "!.pixi", + "!R-package/docs" + ] + }, + "formatter": { + "enabled": true, + "expand": "always", + "useEditorconfig": true, + "lineWidth": 120 + }, + "assist": { + "enabled": true, + "actions": { + "recommended": true + } + }, + "linter": { + "enabled": true, + "domains": { + "project": "all" + }, + "rules": { + "recommended": true + } + } +} diff --git a/build-cran-package.sh b/build-cran-package.sh new file mode 100755 index 0000000..fd6385b --- /dev/null +++ b/build-cran-package.sh @@ -0,0 +1,214 @@ +#!/bin/sh + +# [description] +# Prepare a source distribution of the R-package +# to be submitted to CRAN. +# +# [arguments] +# +# --r-executable Customize the R executable used by `R CMD build`. +# Useful if building the R-package in an environment with +# non-standard builds of R, such as those provided in +# https://github.com/wch/r-debug. +# +# --no-build-vignettes Pass this flag to skip creating vignettes. +# You might want to do this to avoid installing +# vignette-only dependencies, or to avoid +# portability issues. +# +# [usage] +# +# # default usage +# sh build-cran-package.sh +# +# # custom R build +# sh build-cran-package.sh --r-executable=RDvalgrind +# +# # skip vignette building +# sh build-cran-package.sh --no-build-vignettes + +set -e -u + +# Default values of arguments +BUILD_VIGNETTES=true +LGB_R_EXECUTABLE=R + +while [ $# -gt 0 ]; do + case "$1" in + --r-executable=*) + LGB_R_EXECUTABLE="${1#*=}" + ;; + --no-build-vignettes*) + BUILD_VIGNETTES=false + ;; + *) + echo "invalid argument '${1}'" + exit 1 + ;; + esac + shift +done + +echo "Building lightgbm with R executable: ${LGB_R_EXECUTABLE}" + +ORIG_WD="$(pwd)" +TEMP_R_DIR="$(pwd)/lightgbm_r" + +if test -d "${TEMP_R_DIR}"; then + rm -r "${TEMP_R_DIR}" +fi +mkdir -p "${TEMP_R_DIR}" + +CURRENT_DATE=$(date +'%Y-%m-%d') + +# R packages cannot have versions like 3.0.0rc1, but +# 3.0.0-1 is acceptable +LGB_VERSION=$(head -1 ./VERSION.txt | sed "s/rc/-/g") + +# move relevant files +cp -R R-package/* "${TEMP_R_DIR}" +cp -R include "${TEMP_R_DIR}/src/" +cp -R src/* "${TEMP_R_DIR}/src/" + +if ${BUILD_VIGNETTES} ; then + cp docs/logo/LightGBM_logo_black_text.svg "${TEMP_R_DIR}/vignettes/" +fi + +cp \ + external_libs/fast_double_parser/include/fast_double_parser.h \ + "${TEMP_R_DIR}/src/include/LightGBM/utils" + +mkdir -p "${TEMP_R_DIR}/src/include/LightGBM/utils/fmt" +cp \ + external_libs/fmt/include/fmt/*.h \ + "${TEMP_R_DIR}/src/include/LightGBM/utils/fmt" + +# including only specific files from Eigen, to keep the R-package +# small and avoid redistributing code with licenses incompatible with +# LightGBM's license +EIGEN_R_DIR="${TEMP_R_DIR}/src/include/Eigen" +mkdir -p "${EIGEN_R_DIR}" + +modules="Cholesky Core Dense Eigenvalues Geometry Householder Jacobi LU QR SVD" +for eigen_module in ${modules}; do + cp "external_libs/eigen/Eigen/${eigen_module}" "${EIGEN_R_DIR}/${eigen_module}" + if [ "${eigen_module}" != "Dense" ]; then + mkdir -p "${EIGEN_R_DIR}/src/${eigen_module}/" + cp -R "external_libs/eigen/Eigen/src/${eigen_module}"/* "${EIGEN_R_DIR}/src/${eigen_module}/" + fi +done + +mkdir -p "${EIGEN_R_DIR}/src/misc" +cp -R external_libs/eigen/Eigen/src/misc/* "${EIGEN_R_DIR}/src/misc/" + +mkdir -p "${EIGEN_R_DIR}/src/plugins" +cp -R external_libs/eigen/Eigen/src/plugins/* "${EIGEN_R_DIR}/src/plugins/" + +cd "${TEMP_R_DIR}" + + # Remove files not needed for CRAN + echo "Removing files not needed for CRAN" + rm src/install.libs.R + rm -r inst/ + rm -r pkgdown/ + rm cran-comments.md + rm AUTOCONF_UBUNTU_VERSION + rm recreate-configure.sh + + # files only used by the lightgbm CLI aren't needed for + # the R-package + rm src/application/application.cpp + rm src/include/LightGBM/application.h + rm src/main.cpp + + # configure.ac and DESCRIPTION have placeholders for version + # and date so they don't have to be updated manually + sed -i.bak -e "s/~~VERSION~~/${LGB_VERSION}/" configure.ac + sed -i.bak -e "s/~~VERSION~~/${LGB_VERSION}/" DESCRIPTION + sed -i.bak -e "s/~~DATE~~/${CURRENT_DATE}/" DESCRIPTION + + # Remove 'region', 'endregion', and 'warning' pragmas. + # This won't change the correctness of the code. CRAN does + # not allow you to use compiler flag '-Wno-unknown-pragmas' or + # pragmas that suppress warnings. + echo "Removing unknown pragmas in headers" + find . \( -name '*.h' -o -name '*.hpp' -o -name '*.cpp' \) -exec \ + sed \ + -i.bak \ + -e 's/^.*#pragma clang diagnostic.*$//' \ + -e 's/^.*#pragma diag_suppress.*$//' \ + -e 's/^.*#pragma GCC diagnostic.*$//' \ + -e 's/^.*#pragma region.*$//' \ + -e 's/^.*#pragma endregion.*$//' \ + -e 's/^.*#pragma warning.*$//' \ + {} + + + # 'processx' is listed as a 'Suggests' dependency in DESCRIPTION + # because it is used in install.libs.R, a file that is not + # included in the CRAN distribution of the package + sed \ + -i.bak \ + '/processx/d' \ + DESCRIPTION + + echo "Cleaning sed backup files" + find . -name '*.bak' -exec rm {} \; + +cd "${ORIG_WD}" + +if ${BUILD_VIGNETTES} ; then + "${LGB_R_EXECUTABLE}" CMD build \ + --keep-empty-dirs \ + lightgbm_r + + echo "removing object files created by vignettes" + rm -rf ./_tmp + mkdir _tmp + TARBALL_NAME="lightgbm_${LGB_VERSION}.tar.gz" + mv "${TARBALL_NAME}" _tmp/ + + echo "untarring ${TARBALL_NAME}" + cd _tmp + tar -xf "${TARBALL_NAME}" > /dev/null 2>&1 + rm -f "${TARBALL_NAME}" + echo "done untarring ${TARBALL_NAME}" + + # Object files are left behind from compiling the library to generate vignettes. + # Approaches like using tar --exclude=*.so to exclude them are not portable + # (for example, don't work with some versions of tar on Windows). + # + # Removing them manually here removes the need to use tar --exclude. + # + # For background, see https://github.com/lightgbm-org/LightGBM/pull/3946#pullrequestreview-799415812. + rm -f ./lightgbm/src/*.o + rm -f ./lightgbm/src/boosting/*.o + rm -f ./lightgbm/src/io/*.o + rm -f ./lightgbm/src/metric/*.o + rm -f ./lightgbm/src/network/*.o + rm -f ./lightgbm/src/objective/*.o + rm -f ./lightgbm/src/treelearner/*.o + rm -f ./lightgbm/src/utils/*.o + + echo "re-tarring ${TARBALL_NAME}" + # --no-xattrs is the default in GNU tar but not some distributions of BSD tar. + # Enable it here to avoid errors on macOS. + # ref: https://stackoverflow.com/a/74373784/3986677 + tar \ + -cz \ + --no-xattrs \ + -f "${TARBALL_NAME}" \ + lightgbm \ + > /dev/null 2>&1 + mv "${TARBALL_NAME}" ../ + cd .. + echo "Done creating ${TARBALL_NAME}" + + rm -rf ./_tmp +else + "${LGB_R_EXECUTABLE}" CMD build \ + --keep-empty-dirs \ + --no-build-vignettes \ + lightgbm_r +fi + +echo "Done building R-package" diff --git a/build-python.sh b/build-python.sh new file mode 100755 index 0000000..4c8247c --- /dev/null +++ b/build-python.sh @@ -0,0 +1,435 @@ +#!/bin/sh + +# [description] +# +# Prepare a source distribution (sdist) or built distribution (wheel) +# of the Python-package, and optionally install it. +# +# [usage] +# +# # build sdist and put it in dist/ +# sh ./build-python.sh sdist +# +# # build wheel and put it in dist/ +# sh ./build-python.sh bdist_wheel [OPTIONS] +# +# # compile lib_lightgbm and install the Python-package wrapping it +# sh ./build-python.sh install [OPTIONS] +# +# # install the Python-package using a pre-compiled lib_lightgbm +# # (assumes lib_lightgbm.{dll,so} is located at the root of the repo) +# sh ./build-python.sh install --precompile +# +# [options] +# +# --boost-dir=FILEPATH +# Directory with Boost package configuration file. +# --boost-include-dir=FILEPATH +# Directory containing Boost headers. +# --boost-librarydir=FILEPATH +# Preferred Boost library directory. +# --boost-root=FILEPATH +# Boost preferred installation prefix. +# --opencl-include-dir=FILEPATH +# OpenCL include directory. +# --opencl-library=FILEPATH +# Path to OpenCL library. +# --bit32 +# Compile 32-bit version. +# --cuda +# Compile CUDA version. +# --gpu +# Compile GPU version. +# --integrated-opencl +# Compile integrated OpenCL version. +# --mingw +# Compile with MinGW. +# --mpi +# Compile MPI version. +# --no-isolation +# Assume all build and install dependencies are already installed, +# don't go to the internet to get them. +# --nomp +# Compile version without OpenMP support. +# --precompile +# Use precompiled library. +# Only used with 'install' command. +# --rocm +# Compile ROCm version. +# --time-costs +# Compile version that outputs time costs for different internal routines. +# --user +# Install into user-specific instead of global site-packages directory. +# Only used with 'install' command. + +set -e -u + +echo "[INFO] building lightgbm" + +# Default values of arguments +INSTALL="false" +BUILD_SDIST="false" +BUILD_WHEEL="false" + +PIP_INSTALL_ARGS="" +BUILD_ARGS="" +PRECOMPILE="false" + +while [ $# -gt 0 ]; do + case "$1" in + ############################ + # sub-commands of setup.py # + ############################ + install) + INSTALL="true" + ;; + sdist) + BUILD_SDIST="true" + ;; + bdist_wheel) + BUILD_WHEEL="true" + ;; + ############################ + # customized library paths # + ############################ + --boost-dir|--boost-dir=*) + if echo "$1" | grep -q '^*=*$'; + then shift; + fi + BOOST_DIR="${1#*=}" + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.Boost_DIR='${BOOST_DIR}'" + ;; + --boost-include-dir|--boost-include-dir=*) + if echo "$1" | grep -q '^*=*$'; + then shift; + fi + BOOST_INCLUDE_DIR="${1#*=}" + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.Boost_INCLUDE_DIR='${BOOST_INCLUDE_DIR}'" + ;; + --boost-librarydir|--boost-librarydir=*) + if echo "$1" | grep -q '^*=*$'; + then shift; + fi + BOOST_LIBRARY_DIR="${1#*=}" + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.BOOST_LIBRARYDIR='${BOOST_LIBRARY_DIR}'" + ;; + --boost-root|--boost-root=*) + if echo "$1" | grep -q '^*=*$'; + then shift; + fi + BOOST_ROOT="${1#*=}" + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.Boost_ROOT='${BOOST_ROOT}'" + ;; + --opencl-include-dir|--opencl-include-dir=*) + if echo "$1" | grep -q '^*=*$'; + then shift; + fi + OPENCL_INCLUDE_DIR="${1#*=}" + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.OpenCL_INCLUDE_DIR='${OPENCL_INCLUDE_DIR}'" + ;; + --opencl-library|--opencl-library=*) + if echo "$1" | grep -q '^*=*$'; + then shift; + fi + OPENCL_LIBRARY="${1#*=}" + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.OpenCL_LIBRARY='${OPENCL_LIBRARY}'" + ;; + ######### + # flags # + ######### + --bit32) + echo "[INFO] Attempting to build 32-bit version of LightGBM, which is only supported on Windows with Visual Studio." + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.args=-AWin32" + ;; + --cuda) + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.USE_CUDA=ON" + ;; + --rocm) + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.USE_ROCM=ON" + ;; + --gpu) + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.USE_GPU=ON" + ;; + --integrated-opencl) + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.__INTEGRATE_OPENCL=ON" + ;; + --mingw) + # ref: https://stackoverflow.com/a/45104058/3986677 + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.CMAKE_SH=CMAKE_SH-NOTFOUND" + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.args=-G'MinGW Makefiles'" + ;; + --mpi) + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.USE_MPI=ON" + ;; + --no-isolation) + BUILD_ARGS="${BUILD_ARGS} --no-isolation" + PIP_INSTALL_ARGS="${PIP_INSTALL_ARGS} --no-build-isolation" + ;; + --nomp) + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.USE_OPENMP=OFF" + ;; + --precompile) + PRECOMPILE="true" + ;; + --time-costs) + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.USE_TIMETAG=ON" + ;; + --user) + PIP_INSTALL_ARGS="${PIP_INSTALL_ARGS} --user" + ;; + *) + echo "[ERROR] invalid argument '${1}'. Aborting" + exit 1 + ;; + esac + shift +done + +# ref: https://cmake.org/cmake/help/latest/variable/CMAKE_CUDA_ARCHITECTURES.html +if [ -n "${CUDAARCHS:-}" ]; then + BUILD_ARGS="${BUILD_ARGS} --config-setting=cmake.define.CMAKE_CUDA_ARCHITECTURES=${CUDAARCHS}" +fi + +python -m pip install --prefer-binary 'build>=0.10.0' + +# create a new directory that just contains the files needed +# to build the Python-package +create_isolated_source_dir() { + rm -rf \ + ./lightgbm-python \ + ./lightgbm \ + ./python-package/build \ + ./python-package/build_cpp \ + ./python-package/compile \ + ./python-package/dist \ + ./python-package/lightgbm.egg-info + + cp -R ./python-package ./lightgbm-python + + cp LICENSE ./lightgbm-python/ + cp VERSION.txt ./lightgbm-python/lightgbm/VERSION.txt + + cp -R ./cmake ./lightgbm-python + cp CMakeLists.txt ./lightgbm-python + cp -R ./include ./lightgbm-python + cp -R ./src ./lightgbm-python + cp -R ./swig ./lightgbm-python + + # include only specific files from external_libs, to keep the package + # small and avoid redistributing code with licenses incompatible with + # LightGBM's license + + ###################### + # fast_double_parser # + ###################### + mkdir -p ./lightgbm-python/external_libs/fast_double_parser + cp \ + external_libs/fast_double_parser/CMakeLists.txt \ + ./lightgbm-python/external_libs/fast_double_parser/CMakeLists.txt + cp \ + external_libs/fast_double_parser/LICENSE* \ + ./lightgbm-python/external_libs/fast_double_parser/ + + mkdir -p ./lightgbm-python/external_libs/fast_double_parser/include/ + cp \ + external_libs/fast_double_parser/include/fast_double_parser.h \ + ./lightgbm-python/external_libs/fast_double_parser/include/ + + ####### + # fmt # + ####### + mkdir -p ./lightgbm-python/external_libs/fmt + cp \ + external_libs/fast_double_parser/CMakeLists.txt \ + ./lightgbm-python/external_libs/fmt/CMakeLists.txt + cp \ + external_libs/fmt/LICENSE* \ + ./lightgbm-python/external_libs/fmt/ + + mkdir -p ./lightgbm-python/external_libs/fmt/include/fmt + cp \ + external_libs/fmt/include/fmt/*.h \ + ./lightgbm-python/external_libs/fmt/include/fmt/ + + ######### + # Eigen # + ######### + mkdir -p ./lightgbm-python/external_libs/eigen/Eigen + cp \ + external_libs/eigen/CMakeLists.txt \ + ./lightgbm-python/external_libs/eigen/CMakeLists.txt + + modules="Cholesky Core Dense Eigenvalues Geometry Householder Jacobi LU QR SVD" + for eigen_module in ${modules}; do + cp \ + "external_libs/eigen/Eigen/${eigen_module}" \ + "./lightgbm-python/external_libs/eigen/Eigen/${eigen_module}" + if [ "${eigen_module}" != "Dense" ]; then + mkdir -p "./lightgbm-python/external_libs/eigen/Eigen/src/${eigen_module}/" + cp \ + -R \ + "external_libs/eigen/Eigen/src/${eigen_module}"/* \ + "./lightgbm-python/external_libs/eigen/Eigen/src/${eigen_module}/" + fi + done + + mkdir -p ./lightgbm-python/external_libs/eigen/Eigen/misc + cp \ + -R \ + external_libs/eigen/Eigen/src/misc \ + ./lightgbm-python/external_libs/eigen/Eigen/src/misc/ + + mkdir -p ./lightgbm-python/external_libs/eigen/Eigen/plugins + cp \ + -R \ + external_libs/eigen/Eigen/src/plugins \ + ./lightgbm-python/external_libs/eigen/Eigen/src/plugins/ + + ################### + # compute (Boost) # + ################### + mkdir -p ./lightgbm-python/external_libs/compute + cp \ + -R \ + external_libs/compute/include \ + ./lightgbm-python/external_libs/compute/include/ + + ############# + # nanoarrow # + ############# + mkdir -p ./lightgbm-python/external_libs/nanoarrow + cp \ + external_libs/nanoarrow/CMakeLists.txt \ + external_libs/nanoarrow/LICENSE.txt \ + external_libs/nanoarrow/NOTICE.txt \ + ./lightgbm-python/external_libs/nanoarrow/ + cp -R \ + external_libs/nanoarrow/cmake \ + ./lightgbm-python/external_libs/nanoarrow/cmake/ + mkdir -p ./lightgbm-python/external_libs/nanoarrow/src/nanoarrow + cp \ + external_libs/nanoarrow/src/nanoarrow/nanoarrow.h \ + external_libs/nanoarrow/src/nanoarrow/nanoarrow.hpp \ + external_libs/nanoarrow/src/nanoarrow/nanoarrow_config.h.in \ + ./lightgbm-python/external_libs/nanoarrow/src/nanoarrow/ + mkdir -p ./lightgbm-python/external_libs/nanoarrow/src/nanoarrow/common + cp -R \ + external_libs/nanoarrow/src/nanoarrow/common/*.h \ + ./lightgbm-python/external_libs/nanoarrow/src/nanoarrow/common + cp -R \ + external_libs/nanoarrow/src/nanoarrow/common/*.c \ + ./lightgbm-python/external_libs/nanoarrow/src/nanoarrow/common + mkdir -p ./lightgbm-python/external_libs/nanoarrow/src/nanoarrow/hpp + cp -R \ + external_libs/nanoarrow/src/nanoarrow/hpp/*.hpp \ + ./lightgbm-python/external_libs/nanoarrow/src/nanoarrow/hpp +} + +create_isolated_source_dir + +cd ./lightgbm-python + +# if 'install' was passed, choose the type of package to build and install +if test "${INSTALL}" = true; then + if test "${PRECOMPILE}" = true; then + BUILD_SDIST=false + BUILD_WHEEL=true + BUILD_ARGS="" + rm -rf \ + ./cmake \ + ./CMakeLists.txt \ + ./external_libs \ + ./include \ + ./src \ + ./swig + # avoid trying to recompile, just use hatchling and copy in relevant files + sed -i.bak -e '/start:build-system/,/end:build-system/d' pyproject.toml + +# replace build backend configuration +cat >> ./pyproject.toml <=1.27.0"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +# do not consider .gitignore when choosing files to include / exclude +ignore-vcs = true +packages = ["lightgbm"] + +EOF + mkdir -p ./lightgbm/lib + if test -f ../lib_lightgbm.so; then + echo "[INFO] found pre-compiled lib_lightgbm.so" + cp ../lib_lightgbm.so ./lightgbm/lib/lib_lightgbm.so + elif test -f ../lib_lightgbm.dylib; then + echo "[INFO] found pre-compiled lib_lightgbm.dylib" + cp ../lib_lightgbm.dylib ./lightgbm/lib/lib_lightgbm.dylib + elif test -f ../lib_lightgbm.dll; then + echo "[INFO] found pre-compiled lib_lightgbm.dll" + cp ../lib_lightgbm.dll ./lightgbm/lib/lib_lightgbm.dll + elif test -f ../Release/lib_lightgbm.dll; then + echo "[INFO] found pre-compiled Release/lib_lightgbm.dll" + cp ../Release/lib_lightgbm.dll ./lightgbm/lib/lib_lightgbm.dll + elif test -f ../windows/x64/DLL/lib_lightgbm.dll; then + echo "[INFO] found pre-compiled windows/x64/DLL/lib_lightgbm.dll" + cp ../windows/x64/DLL/lib_lightgbm.dll ./lightgbm/lib/lib_lightgbm.dll + cp ../windows/x64/DLL/lib_lightgbm.lib ./lightgbm/lib/lib_lightgbm.lib + elif test -f ../windows/x64/Debug_DLL/lib_lightgbm.dll; then + echo "[INFO] found pre-compiled windows/x64/Debug_DLL/lib_lightgbm.dll" + cp ../windows/x64/Debug_DLL/lib_lightgbm.dll ./lightgbm/lib/lib_lightgbm.dll + cp ../windows/x64/Debug_DLL/lib_lightgbm.lib ./lightgbm/lib/lib_lightgbm.lib + else + echo "[ERROR] cannot find pre-compiled library. Aborting" + exit 1 + fi + rm -f ./*.bak + fi + + # at this point, if 'install' was passed but the package type wasn't indicated, prefer wheel + if test "${BUILD_SDIST}" = false && test "${BUILD_WHEEL}" = false; then + echo "[INFO] 'install' passed but no package type ('bdist_wheel', 'sdist') chosen. Defaulting to 'bdist_wheel'." + BUILD_SDIST="false" + BUILD_WHEEL="true" + fi +fi + +if test "${BUILD_SDIST}" = true; then + echo "[INFO] --- building sdist ---" + rm -f ../dist/*.tar.gz + # use xargs to work with args that contain whitespaces + # note that empty echo string leads to that xargs doesn't run the command + # in some implementations of xargs + # ref: https://stackoverflow.com/a/8296746 + echo "--sdist --outdir ../dist ${BUILD_ARGS} ." | xargs python -m build +fi + +if test "${BUILD_WHEEL}" = true; then + echo "[INFO] --- building wheel ---" + rm -f ../dist/*.whl || true + # use xargs to work with args that contain whitespaces + # note that empty echo string leads to that xargs doesn't run the command + # in some implementations of xargs + # ref: https://stackoverflow.com/a/8296746 + echo "--wheel --outdir ../dist ${BUILD_ARGS} ." | xargs python -m build +fi + +if test "${INSTALL}" = true; then + echo "[INFO] --- installing lightgbm ---" + cd .. + if test "${BUILD_WHEEL}" = true; then + PACKAGE_FILE="$(echo dist/lightgbm*.whl)" + else + PACKAGE_FILE="$(echo dist/lightgbm*.tar.gz)" + fi + # shellcheck disable=SC2086 + python -m pip install \ + ${PIP_INSTALL_ARGS} \ + --force-reinstall \ + --no-cache-dir \ + --no-deps \ + "${PACKAGE_FILE}" +fi + +echo "[INFO] cleaning up" +rm -rf ./lightgbm-python diff --git a/build_r.R b/build_r.R new file mode 100644 index 0000000..94bb75f --- /dev/null +++ b/build_r.R @@ -0,0 +1,435 @@ +# For macOS users who have decided to use gcc +# (replace 8 with version of gcc installed on your machine) +# NOTE: your gcc / g++ from Homebrew is probably in /usr/local/bin +#export CXX=/usr/local/bin/g++-8 CC=/usr/local/bin/gcc-8 +# Sys.setenv("CXX" = "/usr/local/bin/g++-8") +# Sys.setenv("CC" = "/usr/local/bin/gcc-8") + +args <- commandArgs(trailingOnly = TRUE) +INSTALL_AFTER_BUILD <- !("--skip-install" %in% args) +TEMP_R_DIR <- file.path(getwd(), "lightgbm_r") +TEMP_SOURCE_DIR <- file.path(TEMP_R_DIR, "src") + +# [description] +# Parse the content of commandArgs() into a structured +# list. This returns a list with two sections. +# * "flags" = a character of vector of flags like "--use-gpu" +# * "keyword_args" = a named character vector, where names +# refer to options and values are the option values. For +# example, c("--boost-librarydir" = "/usr/lib/x86_64-linux-gnu") +.parse_args <- function(args) { + out_list <- list( + "flags" = character(0L) + , "keyword_args" = character(0L) + , "make_args" = character(0L) + ) + for (arg in args) { + if (any(grepl("^\\-j[0-9]+", arg))) { # nolint: non_portable_path. + out_list[["make_args"]] <- arg + } else if (any(grepl("=", arg, fixed = TRUE))) { + split_arg <- strsplit(arg, "=", fixed = TRUE)[[1L]] + arg_name <- split_arg[[1L]] + arg_value <- split_arg[[2L]] + out_list[["keyword_args"]][[arg_name]] <- arg_value + } else { + out_list[["flags"]] <- c(out_list[["flags"]], arg) + } + } + return(out_list) +} +parsed_args <- .parse_args(args) + +SKIP_VIGNETTES <- "--no-build-vignettes" %in% parsed_args[["flags"]] +USING_GPU <- "--use-gpu" %in% parsed_args[["flags"]] +USING_MINGW <- "--use-mingw" %in% parsed_args[["flags"]] +USING_MSYS2 <- "--use-msys2" %in% parsed_args[["flags"]] + +# this maps command-line arguments to defines passed into CMake, +ARGS_TO_DEFINES <- c( + "--boost-root" = "-DBOOST_ROOT" + , "--boost-dir" = "-DBoost_DIR" + , "--boost-include-dir" = "-DBoost_INCLUDE_DIR" + , "--boost-librarydir" = "-DBOOST_LIBRARYDIR" + , "--opencl-include-dir" = "-DOpenCL_INCLUDE_DIR" + , "--opencl-library" = "-DOpenCL_LIBRARY" +) + +recognized_args <- c( + "--no-build-vignettes" + , "--skip-install" + , "--use-gpu" + , "--use-mingw" + , "--use-msys2" + , names(ARGS_TO_DEFINES) +) +given_args <- c( + parsed_args[["flags"]] + , names(parsed_args[["keyword_args"]]) +) +unrecognized_args <- setdiff(given_args, recognized_args) +if (length(unrecognized_args) > 0L) { + msg <- paste0( + "Unrecognized arguments: " + , toString(unrecognized_args) + ) + stop(msg) +} + +# [description] Replace statements in install.libs.R code based on +# command-line flags +.replace_flag <- function(variable_name, value, content) { + out <- gsub( + pattern = paste0(variable_name, " <-.*") + , replacement = paste0(variable_name, " <- ", as.character(value)) + , x = content + ) + return(out) +} + +install_libs_content <- readLines( + file.path("R-package", "src", "install.libs.R") +) +install_libs_content <- .replace_flag("use_gpu", USING_GPU, install_libs_content) +install_libs_content <- .replace_flag("use_mingw", USING_MINGW, install_libs_content) +install_libs_content <- .replace_flag("use_msys2", USING_MSYS2, install_libs_content) + +# set up extra flags based on keyword arguments +keyword_args <- parsed_args[["keyword_args"]] +if (length(keyword_args) > 0L) { + cmake_args_to_add <- NULL + for (i in seq_along(keyword_args)) { + arg_name <- names(keyword_args)[[i]] + define_name <- ARGS_TO_DEFINES[[arg_name]] + arg_value <- shQuote(normalizePath(keyword_args[[arg_name]], winslash = "/")) + cmake_args_to_add <- c(cmake_args_to_add, paste0(define_name, "=", arg_value)) + } + install_libs_content <- gsub( + pattern = paste0("command_line_args <- NULL") + , replacement = paste0( + "command_line_args <- c(\'" + , paste(cmake_args_to_add, collapse = "', '") + , "')" + ) + , x = install_libs_content + , fixed = TRUE + ) +} + +# if provided, set '-j' in 'make' commands in install.libs.R +if (length(parsed_args[["make_args"]]) > 0L) { + install_libs_content <- gsub( + pattern = "make_args_from_build_script <- character(0L)" + , replacement = paste0( + "make_args_from_build_script <- c(\"" + , paste(parsed_args[["make_args"]], collapse = "\", \"") + , "\")" + ) + , x = install_libs_content + , fixed = TRUE + ) +} + +# R returns FALSE (not a non-zero exit code) if a file copy operation +# breaks. Let's fix that +.handle_result <- function(res) { + if (!all(res)) { + stop("Copying files failed!") + } + return(invisible(NULL)) +} + +# system() will not raise an R exception if the process called +# fails. Wrapping it here to get that behavior. +# +# system() introduces a lot of overhead, at least on Windows, +# so trying processx if it is available +.run_shell_command <- function(cmd, args, strict = TRUE) { + on_windows <- .Platform$OS.type == "windows" + has_processx <- suppressMessages({ + suppressWarnings({ + require("processx") # nolint: undesirable_function, unused_import. + }) + }) + if (has_processx && on_windows) { + result <- processx::run( + command = cmd + , args = args + , windows_verbatim_args = TRUE + , error_on_status = FALSE + , echo = TRUE + ) + exit_code <- result$status + } else { + if (on_windows) { + message(paste0( + "Using system() to run shell commands. Installing " + , "'processx' with install.packages('processx') might " + , "make this faster." + )) + } + cmd <- paste0(cmd, " ", paste(args, collapse = " ")) + exit_code <- system(cmd) + } + + if (exit_code != 0L && isTRUE(strict)) { + stop(paste0("Command failed with exit code: ", exit_code)) + } + return(invisible(exit_code)) +} + +# Make a new temporary folder to work in +unlink(x = TEMP_R_DIR, recursive = TRUE) +dir.create(TEMP_R_DIR) + +# copy in the relevant files +result <- file.copy( + from = "R-package/./" + , to = sprintf("%s/", TEMP_R_DIR) + , recursive = TRUE + , overwrite = TRUE +) +.handle_result(result) + +# overwrite src/install.libs.R with new content based on command-line flags +writeLines( + text = install_libs_content + , con = file.path(TEMP_SOURCE_DIR, "install.libs.R") +) + +# Add blank Makevars files +result <- file.copy( + from = file.path(TEMP_R_DIR, "inst", "Makevars") + , to = file.path(TEMP_SOURCE_DIR, "Makevars") + , overwrite = TRUE +) +.handle_result(result) +result <- file.copy( + from = file.path(TEMP_R_DIR, "inst", "Makevars.win") + , to = file.path(TEMP_SOURCE_DIR, "Makevars.win") + , overwrite = TRUE +) +.handle_result(result) + +result <- file.copy( + from = "include/" + , to = sprintf("%s/", TEMP_SOURCE_DIR) + , recursive = TRUE + , overwrite = TRUE +) +.handle_result(result) + +result <- file.copy( + from = "src/" + , to = sprintf("%s/", TEMP_SOURCE_DIR) + , recursive = TRUE + , overwrite = TRUE +) +.handle_result(result) + +EIGEN_R_DIR <- file.path(TEMP_SOURCE_DIR, "include", "Eigen") +dir.create(EIGEN_R_DIR) + +eigen_modules <- c( + "Cholesky" + , "Core" + , "Dense" + , "Eigenvalues" + , "Geometry" + , "Householder" + , "Jacobi" + , "LU" + , "QR" + , "SVD" +) +for (eigen_module in eigen_modules) { + result <- file.copy( + from = file.path("external_libs", "eigen", "Eigen", eigen_module) + , to = EIGEN_R_DIR + , recursive = FALSE + , overwrite = TRUE + ) + .handle_result(result) +} + +dir.create(file.path(EIGEN_R_DIR, "src")) + +for (eigen_module in c(eigen_modules, "misc", "plugins")) { + if (eigen_module == "Dense") { + next + } + module_dir <- file.path(EIGEN_R_DIR, "src", eigen_module) + dir.create(module_dir, recursive = TRUE) + result <- file.copy( + from = sprintf("%s/", file.path("external_libs", "eigen", "Eigen", "src", eigen_module)) + , to = sprintf("%s/", file.path(EIGEN_R_DIR, "src")) + , recursive = TRUE + , overwrite = TRUE + ) + .handle_result(result) +} + +.replace_pragmas <- function(filepath) { + pragma_patterns <- c( + "^.*#pragma clang diagnostic.*$" + , "^.*#pragma diag_suppress.*$" + , "^.*#pragma GCC diagnostic.*$" + , "^.*#pragma region.*$" + , "^.*#pragma endregion.*$" + , "^.*#pragma warning.*$" + ) + content <- readLines(filepath) + for (pragma_pattern in pragma_patterns) { + content <- content[!grepl(pragma_pattern, content)] + } + writeLines(content, filepath) +} + +# remove pragmas that suppress warnings, to appease R CMD check +.replace_pragmas( + file.path(EIGEN_R_DIR, "src", "Core", "arch", "SSE", "Complex.h") +) +.replace_pragmas( + file.path(EIGEN_R_DIR, "src", "Core", "util", "DisableStupidWarnings.h") +) + +result <- file.copy( + from = "CMakeLists.txt" + , to = file.path(TEMP_R_DIR, "inst", "bin/") + , overwrite = TRUE +) +.handle_result(result) + +# remove CRAN-specific files +result <- file.remove( + file.path(TEMP_R_DIR, "cleanup") + , file.path(TEMP_R_DIR, "configure") + , file.path(TEMP_R_DIR, "configure.ac") + , file.path(TEMP_R_DIR, "configure.win") + , file.path(TEMP_SOURCE_DIR, "Makevars.in") + , file.path(TEMP_SOURCE_DIR, "Makevars.win.in") +) +.handle_result(result) + +#------------# +# submodules # +#------------# +EXTERNAL_LIBS_R_DIR <- file.path(TEMP_SOURCE_DIR, "external_libs") +dir.create(EXTERNAL_LIBS_R_DIR) +for (submodule in list.dirs( + path = "external_libs" + , full.names = FALSE + , recursive = FALSE +)) { + # compute/ is a submodule with boost, only needed if + # building the R-package with GPU support; + # eigen/ has a special treatment due to licensing aspects; + # nanoarrow/ is only needed by the Arrow-based C API entry points, which + # are excluded from the R build (the R API never calls into them). + if ((submodule == "compute" && !USING_GPU) + || submodule == "eigen" + || submodule == "nanoarrow") { + next + } + result <- file.copy( + from = sprintf("%s/", file.path("external_libs", submodule)) + , to = sprintf("%s/", EXTERNAL_LIBS_R_DIR) + , recursive = TRUE + , overwrite = TRUE + ) + .handle_result(result) +} + +# copy files into the place CMake expects +CMAKE_R_DIR <- file.path(TEMP_SOURCE_DIR, "cmake") +CMAKE_MODULES_R_DIR <- file.path(TEMP_SOURCE_DIR, "cmake", "modules") +dir.create(CMAKE_MODULES_R_DIR, recursive = TRUE) +result <- file.copy( + from = file.path("cmake", "modules", "FindLibR.cmake") + , to = sprintf("%s/", CMAKE_MODULES_R_DIR) + , overwrite = TRUE +) +result <- file.copy( + from = file.path("cmake", "Utils.cmake") + , to = sprintf("%s/", CMAKE_R_DIR) + , overwrite = TRUE +) +.handle_result(result) +for (src_file in c("lightgbm_R.cpp", "lightgbm_R.h")) { + result <- file.copy( + from = file.path(TEMP_SOURCE_DIR, src_file) + , to = file.path(TEMP_SOURCE_DIR, "src", src_file) + , overwrite = TRUE + ) + .handle_result(result) + result <- file.remove( + file.path(TEMP_SOURCE_DIR, src_file) + ) + .handle_result(result) +} + +result <- file.copy( + from = file.path("R-package", "inst", "make-r-def.R") + , to = file.path(TEMP_R_DIR, "inst", "bin/") + , overwrite = TRUE +) +.handle_result(result) + +# R packages cannot have versions like 3.0.0rc1, but +# 3.0.0-1 is acceptable +LGB_VERSION <- readLines("VERSION.txt")[1L] +LGB_VERSION <- gsub( + pattern = "rc" + , replacement = "-" + , x = LGB_VERSION + , fixed = TRUE +) + +# DESCRIPTION has placeholders for version +# and date so it doesn't have to be updated manually +DESCRIPTION_FILE <- file.path(TEMP_R_DIR, "DESCRIPTION") +description_contents <- readLines(DESCRIPTION_FILE) +description_contents <- gsub( + pattern = "~~VERSION~~" + , replacement = LGB_VERSION + , x = description_contents + , fixed = TRUE +) +description_contents <- gsub( + pattern = "~~DATE~~" + , replacement = as.character(Sys.Date()) + , x = description_contents + , fixed = TRUE +) +writeLines(description_contents, DESCRIPTION_FILE) + +# NOTE: --keep-empty-dirs is necessary to keep the deep paths expected +# by CMake while also meeting the CRAN req to create object files +# on demand +r_build_args <- c("CMD", "build", TEMP_R_DIR, "--keep-empty-dirs") +if (isTRUE(SKIP_VIGNETTES)) { + r_build_args <- c(r_build_args, "--no-build-vignettes") +} +.run_shell_command("R", r_build_args) + +# Install the package +version <- gsub( + pattern = "Version: ", + replacement = "", + x = grep( + pattern = "Version: " + , x = readLines(con = file.path(TEMP_R_DIR, "DESCRIPTION")) + , value = TRUE + , fixed = TRUE + ) + , fixed = TRUE +) +tarball <- file.path(getwd(), sprintf("lightgbm_%s.tar.gz", version)) + +install_cmd <- "R" +install_args <- c("CMD", "INSTALL", "--no-multiarch", "--with-keep.source", tarball) +if (INSTALL_AFTER_BUILD) { + .run_shell_command(install_cmd, install_args) +} else { + cmd <- paste0(install_cmd, " ", paste(install_args, collapse = " ")) + print(sprintf("Skipping installation. Install the package with command '%s'", cmd)) +} diff --git a/cmake/IntegratedOpenCL.cmake b/cmake/IntegratedOpenCL.cmake new file mode 100644 index 0000000..a4c47a1 --- /dev/null +++ b/cmake/IntegratedOpenCL.cmake @@ -0,0 +1,217 @@ +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(BOOST_VERSION_DOT "1.74") +string(REPLACE "." "_" BOOST_VERSION_UNDERSCORE ${BOOST_VERSION_DOT}) + +set(OPENCL_HEADER_REPOSITORY "https://github.com/KhronosGroup/OpenCL-Headers.git") +set(OPENCL_HEADER_TAG "1b2a1850f410aaaaeaa56cead5a179b5aea4918e") + +set(OPENCL_LOADER_REPOSITORY "https://github.com/KhronosGroup/OpenCL-ICD-Loader.git") +set(OPENCL_LOADER_TAG "98ca71fb9f8484f1cd1999f55224bf9e8d18693b") + +set(BOOST_REPOSITORY "https://github.com/boostorg/boost.git") +set(BOOST_TAG "boost-${BOOST_VERSION_DOT}.0") + +# Build Independent OpenCL library +include(FetchContent) +# lint_cmake: -readability/wonkycase +FetchContent_Declare(OpenCL-Headers GIT_REPOSITORY ${OPENCL_HEADER_REPOSITORY} GIT_TAG ${OPENCL_HEADER_TAG}) +FetchContent_GetProperties(OpenCL-Headers) +# lint_cmake: +readability/wonkycase +if(NOT OpenCL-Headers_POPULATED) +# lint_cmake: -readability/wonkycase + FetchContent_MakeAvailable(OpenCL-Headers) +# lint_cmake: +readability/wonkycase + message(STATUS "Populated OpenCL Headers") +endif() +set(OPENCL_ICD_LOADER_HEADERS_DIR ${opencl-headers_SOURCE_DIR} CACHE PATH "") # for OpenCL ICD Loader +set(OpenCL_INCLUDE_DIR ${opencl-headers_SOURCE_DIR} CACHE PATH "") # for Boost::Compute + +# lint_cmake: -readability/wonkycase +FetchContent_Declare( +# lint_cmake: +readability/wonkycase + OpenCL-ICD-Loader + GIT_REPOSITORY + ${OPENCL_LOADER_REPOSITORY} + GIT_TAG + ${OPENCL_LOADER_TAG} + EXCLUDE_FROM_ALL +) +# lint_cmake: -readability/wonkycase +FetchContent_GetProperties(OpenCL-ICD-Loader) +# lint_cmake: +readability/wonkycase +if(NOT OpenCL-ICD-Loader_POPULATED) +# lint_cmake: -readability/wonkycase + FetchContent_MakeAvailable(OpenCL-ICD-Loader) +# lint_cmake: +readability/wonkycase + if(WIN32) + set(USE_DYNAMIC_VCXX_RUNTIME ON) + endif() + message(STATUS "Populated OpenCL ICD Loader") +endif() +list(APPEND INTEGRATED_OPENCL_INCLUDES ${OPENCL_ICD_LOADER_HEADERS_DIR}) +list(APPEND INTEGRATED_OPENCL_DEFINITIONS CL_TARGET_OPENCL_VERSION=120) +if(WIN32) + list( + APPEND + INTEGRATED_OPENCL_LIBRARIES + ${opencl-icd-loader_BINARY_DIR}/Release/OpenCL.lib + cfgmgr32.lib + runtimeobject.lib + ) +else() + list( + APPEND + INTEGRATED_OPENCL_LIBRARIES + ${opencl-icd-loader_BINARY_DIR}/libOpenCL.a + ) + set_property(TARGET OpenCL PROPERTY POSITION_INDEPENDENT_CODE ON) +endif() + +# Build Independent Boost libraries +include(ExternalProject) +include(ProcessorCount) +# lint_cmake: -readability/wonkycase +ProcessorCount(J) +# lint_cmake: +readability/wonkycase +set(BOOST_BASE "${PROJECT_BINARY_DIR}/Boost") +set(BOOST_INCLUDE "${BOOST_BASE}/source" CACHE PATH "") +set(BOOST_LIBRARY "${BOOST_BASE}/source/stage/lib" CACHE PATH "") +if(WIN32) + if(MSVC) + # references: + # + # * range of MSVC versions: https://learn.microsoft.com/en-us/cpp/overview/compiler-versions + # * MSVC toolchain IDs: not sure... + # comments like https://learn.microsoft.com/en-us/answers/questions/769911/visual-studio-2019-build-tools-v143 + # + if(${MSVC_VERSION} GREATER 1929) + set(MSVC_TOOLCHAIN_ID "143") + elseif(${MSVC_VERSION} GREATER 1919) + set(MSVC_TOOLCHAIN_ID "142") + elseif(${MSVC_VERSION} GREATER 1909) + set(MSVC_TOOLCHAIN_ID "141") + else() + message(FATAL_ERROR "Unsupported MSVC version number: ${MSVC_VERSION}") + endif() + list( + APPEND + BOOST_BUILD_BYPRODUCTS + ${BOOST_LIBRARY}/libboost_filesystem-vc${MSVC_TOOLCHAIN_ID}-mt-x64-${BOOST_VERSION_UNDERSCORE}.lib + ${BOOST_LIBRARY}/libboost_system-vc${MSVC_TOOLCHAIN_ID}-mt-x64-${BOOST_VERSION_UNDERSCORE}.lib + ${BOOST_LIBRARY}/libboost_chrono-vc${MSVC_TOOLCHAIN_ID}-mt-x64-${BOOST_VERSION_UNDERSCORE}.lib + ) + else() + message(FATAL_ERROR "Integrated OpenCL build is not yet available for MinGW") + endif() + set(BOOST_BOOTSTRAP "${BOOST_BASE}/source/bootstrap.bat") + set(BOOST_BUILD "${BOOST_BASE}/source/b2.exe") + set(BOOST_FLAGS "") +else() + set(BOOST_BOOTSTRAP "${BOOST_BASE}/source/bootstrap.sh") + set(BOOST_BUILD "${BOOST_BASE}/source/b2") + set(BOOST_FLAGS "-fPIC") + list( + APPEND + BOOST_BUILD_BYPRODUCTS + ${BOOST_LIBRARY}/libboost_filesystem.a + ${BOOST_LIBRARY}/libboost_system.a + ${BOOST_LIBRARY}/libboost_chrono.a + ) +endif() +list( + APPEND + BOOST_SUBMODULES + "libs/algorithm" + "libs/align" + "libs/any" + "libs/array" + "libs/assert" + "libs/bind" + "libs/chrono" + "libs/compute" + "libs/concept_check" + "libs/config" + "libs/container" + "libs/container_hash" + "libs/core" + "libs/detail" + "libs/filesystem" + "libs/foreach" + "libs/format" + "libs/function" + "libs/function_types" + "libs/fusion" + "libs/headers" + "libs/integer" + "libs/io" + "libs/iterator" + "libs/lexical_cast" + "libs/math" + "libs/move" + "libs/mpl" + "libs/multi_index" + "libs/numeric/conversion" + "libs/optional" + "libs/predef" + "libs/preprocessor" + "libs/property_tree" + "libs/range" + "libs/ratio" + "libs/serialization" + "libs/smart_ptr" + "libs/static_assert" + "libs/system" + "libs/throw_exception" + "libs/tuple" + "libs/typeof" + "libs/type_index" + "libs/type_traits" + "libs/utility" + "libs/uuid" + "libs/winapi" + "tools/boost_install" + "tools/build" +) +# lint_cmake: -readability/wonkycase +ExternalProject_Add( +# lint_cmake: +readability/wonkycase + Boost + TMP_DIR "${BOOST_BASE}/tmp" + STAMP_DIR "${BOOST_BASE}/stamp" + DOWNLOAD_DIR "${BOOST_BASE}/download" + SOURCE_DIR "${BOOST_BASE}/source" + BINARY_DIR "${BOOST_BASE}/source" + INSTALL_DIR "${BOOST_BASE}/install" + GIT_REPOSITORY ${BOOST_REPOSITORY} + GIT_TAG ${BOOST_TAG} + GIT_SUBMODULES ${BOOST_SUBMODULES} + GIT_SHALLOW ON + UPDATE_COMMAND "" + PATCH_COMMAND "" + CONFIGURE_COMMAND ${BOOST_BOOTSTRAP} + BUILD_COMMAND + ${BOOST_BUILD} + -sBOOST_ROOT=${BOOST_BASE}/source + -a + -q + -j ${J} + --with-headers + --with-chrono + --with-filesystem + --with-system + link=static + runtime-link=shared + variant=release + threading=multi + cxxflags="${BOOST_FLAGS}" + INSTALL_COMMAND "" + # BUILD_BYPRODUCTS is necessary to support 'Ninja' builds. + # ref: + # - https://cmake.org/cmake/help/latest/module/ExternalProject.html + # - https://stackoverflow.com/a/65803911/3986677 + BUILD_BYPRODUCTS ${BOOST_BUILD_BYPRODUCTS} +) +list(APPEND INTEGRATED_OPENCL_INCLUDES ${BOOST_INCLUDE}) +list(APPEND INTEGRATED_OPENCL_LIBRARIES ${BOOST_BUILD_BYPRODUCTS}) + +set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) diff --git a/cmake/Sanitizer.cmake b/cmake/Sanitizer.cmake new file mode 100644 index 0000000..f990484 --- /dev/null +++ b/cmake/Sanitizer.cmake @@ -0,0 +1,51 @@ +# Set appropriate compiler and linker flags for sanitizers. +# +# Usage of this module: +# enable_sanitizers("address;leak") + +# Add flags +macro(enable_sanitizer sanitizer) + if(${sanitizer} MATCHES "address") + set(SAN_COMPILE_FLAGS "${SAN_COMPILE_FLAGS} -fsanitize=address") + + elseif(${sanitizer} MATCHES "thread") + set(SAN_COMPILE_FLAGS "${SAN_COMPILE_FLAGS} -fsanitize=thread") + + elseif(${sanitizer} MATCHES "leak") + set(SAN_COMPILE_FLAGS "${SAN_COMPILE_FLAGS} -fsanitize=leak") + + elseif(${sanitizer} MATCHES "undefined") + set(SAN_COMPILE_FLAGS "${SAN_COMPILE_FLAGS} -fsanitize=undefined -fno-sanitize-recover=undefined") + + else() + message(FATAL_ERROR "Sanitizer ${sanitizer} not supported.") + endif() +endmacro() + +macro(enable_sanitizers SANITIZERS) + # Check sanitizers compatibility. + foreach(_san ${SANITIZERS}) + string(TOLOWER ${_san} _san) + if(_san MATCHES "thread") + if(${_use_other_sanitizers}) + message(FATAL_ERROR "thread sanitizer is not compatible with ${_san} sanitizer.") + endif() + set(_use_thread_sanitizer 1) + else() + if(${_use_thread_sanitizer}) + message(FATAL_ERROR "${_san} sanitizer is not compatible with thread sanitizer.") + endif() + set(_use_other_sanitizers 1) + endif() + endforeach() + + message(STATUS "Sanitizers: ${SANITIZERS}") + + foreach(_san ${SANITIZERS}) + string(TOLOWER ${_san} _san) + enable_sanitizer(${_san}) + endforeach() + message(STATUS "Sanitizers compile flags: ${SAN_COMPILE_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SAN_COMPILE_FLAGS}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SAN_COMPILE_FLAGS}") +endmacro() diff --git a/cmake/Utils.cmake b/cmake/Utils.cmake new file mode 100644 index 0000000..1eaf9aa --- /dev/null +++ b/cmake/Utils.cmake @@ -0,0 +1,43 @@ +# Copyright (c) 2016-2026 The LightGBM developers. All rights reserved. +# Copyright (c) 2026 The XGBoost developers. +# Licensed under the MIT License. See LICENSE file in the project root for license information. + +# Generate CMAKE_CUDA_ARCHITECTURES form a list of architectures. +# +# lint_cmake: -linelength +# Adopted from XGBoost, see https://github.com/dmlc/xgboost/blob/41591eeff5b10caabe82d65fc7164b447eabd249/cmake/Utils.cmake#L62 +# lint_cmake: +linelength +# +function(compute_cmake_cuda_archs _cuda_version) + + # Set up defaults based on CUDA version + # Remember to update arch-specific tunings when supporting new archs. + # + # Reference for mapping of CUDA toolkit component versions to supported architectures ("compute capabilities"): + # https://en.wikipedia.org/wiki/CUDA#GPUs_supported + # + if(_cuda_version VERSION_GREATER_EQUAL "13.0") + set(CMAKE_CUDA_ARCHITECTURES 75 80 86 89 90 100 120) + elseif(_cuda_version VERSION_GREATER_EQUAL "12.9") + set(CMAKE_CUDA_ARCHITECTURES 70 75 80 86 89 90 100 120) + elseif(_cuda_version VERSION_GREATER_EQUAL "12.8") + set(CMAKE_CUDA_ARCHITECTURES 60 61 70 80 86 89 90 100 120) + elseif(_cuda_version VERSION_GREATER_EQUAL "11.8") + set(CMAKE_CUDA_ARCHITECTURES 60 61 70 80 86 89 90) + else() + message(FATAL_ERROR + "No default architecture list configured for CUDA version '${_cuda_version}'. " + "You can avoid this error and attempt building by manually passing '-DCMAKE_CUDA_ARCHITECTURES'." + ) + endif() + + # For the last architectures, generate both: + # + # - SASS via '-real', for performance + # - PTX via '-virtual', for forward compatibility + # + list(TRANSFORM CMAKE_CUDA_ARCHITECTURES APPEND "-real") + list(TRANSFORM CMAKE_CUDA_ARCHITECTURES REPLACE "([0-9]+)-real" "\\0;\\1-virtual" AT -1) + set(CMAKE_CUDA_ARCHITECTURES "${CMAKE_CUDA_ARCHITECTURES}" PARENT_SCOPE) + message(STATUS "CMAKE_CUDA_ARCHITECTURES: ${CMAKE_CUDA_ARCHITECTURES}") +endfunction() diff --git a/cmake/modules/FindLibR.cmake b/cmake/modules/FindLibR.cmake new file mode 100644 index 0000000..b52a5c1 --- /dev/null +++ b/cmake/modules/FindLibR.cmake @@ -0,0 +1,267 @@ +# CMake module used to find the location of R's +# dll and header files. +# +# Borrows heavily from xgboost's R package: +# +# * https://github.com/dmlc/xgboost/blob/master/cmake/modules/FindLibR.cmake +# +# Defines the following: +# LIBR_FOUND +# LIBR_HOME +# LIBR_EXECUTABLE +# LIBR_MSVC_CORE_LIBRARY +# LIBR_INCLUDE_DIRS +# LIBR_LIBS_DIR +# LIBR_CORE_LIBRARY +# and a CMake function to create R.lib for MSVC + +# lint_cmake: -convention/filename + +if(NOT R_ARCH) + if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") + set(R_ARCH "i386") + else() + set(R_ARCH "x64") + endif() +endif() + +if(NOT ("${R_ARCH}" STREQUAL "x64")) + message(FATAL_ERROR "LightGBM's R-package currently only supports 64-bit operating systems") +endif() + +# Creates R.lib and R.def in the build directory for linking with MSVC +# https://docs.microsoft.com/en-us/cpp/build/reference/link-input-files?redirectedfrom=MSDN&view=vs-2019 +function(create_rlib_for_msvc) + + message(STATUS "Creating R.lib and R.def") + + # various checks and warnings + if(NOT WIN32 OR NOT MSVC) + message(FATAL_ERROR "create_rlib_for_msvc() can only be used with MSVC") + endif() + + if(NOT EXISTS "${LIBR_CORE_LIBRARY}") + message(FATAL_ERROR "LIBR_CORE_LIBRARY, '${LIBR_CORE_LIBRARY}', not found") + endif() + + find_program(DLLTOOL_EXE dlltool) + + if(NOT DLLTOOL_EXE) + message(FATAL_ERROR "dlltool.exe not found!\nDo you have Rtools installed with its MinGW's bin/ in PATH?") + endif() + + set(LIBR_MSVC_CORE_LIBRARY "${CMAKE_CURRENT_BINARY_DIR}/R.lib" CACHE PATH "R.lib filepath") + + get_filename_component( + LIBR_RSCRIPT_EXECUTABLE_DIR + ${LIBR_EXECUTABLE} + DIRECTORY + ) + set(LIBR_RSCRIPT_EXECUTABLE "${LIBR_RSCRIPT_EXECUTABLE_DIR}/Rscript") + + execute_process( + COMMAND ${LIBR_RSCRIPT_EXECUTABLE} + "${CMAKE_CURRENT_BINARY_DIR}/make-r-def.R" + "${LIBR_CORE_LIBRARY}" "${CMAKE_CURRENT_BINARY_DIR}/R.def" + ) + execute_process( + COMMAND ${DLLTOOL_EXE} + "--input-def" "${CMAKE_CURRENT_BINARY_DIR}/R.def" + "--output-lib" "${LIBR_MSVC_CORE_LIBRARY}" + ) +endfunction() + +# R version information is used to search for R's libraries in +# the registry on Windows. Since this code is orchestrated by +# an R script (src/install.libs.R), that script uses R's built-ins to +# find the version of R and pass it through as a CMake variable +if(CMAKE_R_VERSION) + message(STATUS "R version passed into FindLibR.cmake: ${CMAKE_R_VERSION}") +elseif(WIN32) + message( + FATAL_ERROR + "Expected CMAKE_R_VERSION to be passed in on Windows but none was provided. Check src/install.libs.R" + ) +endif() + + +if(NOT LIBR_EXECUTABLE) + find_program( + LIBR_EXECUTABLE + NAMES R R.exe + ) + + # CRAN may run RD CMD CHECK instead of R CMD CHECK, + # which can lead to this infamous error: + # 'R' should not be used without a path -- see par. 1.6 of the manual + if(LIBR_EXECUTABLE MATCHES ".*\\.Rcheck.*") + unset(LIBR_EXECUTABLE CACHE) + endif() + + # ignore the R bundled with R.app on Mac, since that is GUI-only + if(LIBR_EXECUTABLE MATCHES ".+R\\.app.*") + unset(LIBR_EXECUTABLE CACHE) + endif() +endif() + +# Find R executable unless it has been provided directly or already found +if(NOT LIBR_EXECUTABLE) + if(APPLE) + + find_library(LIBR_LIBRARIES R) + + if(LIBR_LIBRARIES MATCHES ".*\\.framework") + set(LIBR_HOME "${LIBR_LIBRARIES}/Resources") + set(LIBR_EXECUTABLE "${LIBR_HOME}/R") + else() + get_filename_component(_LIBR_LIBRARIES "${LIBR_LIBRARIES}" REALPATH) + get_filename_component(_LIBR_LIBRARIES_DIR "${_LIBR_LIBRARIES}" DIRECTORY) + set(LIBR_EXECUTABLE "${_LIBR_LIBRARIES_DIR}/../bin/R") + endif() + + elseif(UNIX) + + # attempt to find R executable + if(NOT LIBR_EXECUTABLE) + find_program( + LIBR_EXECUTABLE + NO_DEFAULT_PATH + HINTS "${CMAKE_CURRENT_BINARY_DIR}" "/usr/bin" "/usr/lib/" "/usr/local/bin/" + NAMES R + ) + endif() + + # Windows + else() + + # if R executable not available, query R_HOME path from registry + if(NOT LIBR_HOME) + + # Try to find R's location in the registry + # ref: https://cran.r-project.org/bin/windows/base/rw-FAQ.html#Does-R-use-the-Registry_003f + get_filename_component( + LIBR_HOME + "[HKEY_LOCAL_MACHINE\\SOFTWARE\\R-core\\R\\${CMAKE_R_VERSION};InstallPath]" + ABSOLUTE + ) + endif() + + if(NOT LIBR_HOME) + get_filename_component( + LIBR_HOME + "[HKEY_CURRENT_USER\\SOFTWARE\\R-core\\R\\${CMAKE_R_VERSION};InstallPath]" + ABSOLUTE + ) + endif() + + if(NOT LIBR_HOME) + message( + FATAL_ERROR + "Unable to locate R executable.\ +\nEither add its location to PATH or provide it through the LIBR_EXECUTABLE CMake variable" + ) + endif() + + # set exe location based on R_ARCH + set(LIBR_EXECUTABLE "${LIBR_HOME}/bin/${R_ARCH}/R.exe") + + endif() + + if(NOT LIBR_EXECUTABLE) + message( + FATAL_ERROR + "Unable to locate R executable.\ +\nEither add its location to PATH or provide it through the LIBR_EXECUTABLE CMake variable" + ) + endif() + +endif() + +# ask R for the home path +execute_process( + COMMAND ${LIBR_EXECUTABLE} "--slave" "--vanilla" "-e" "cat(normalizePath(R.home(), winslash='/'))" + OUTPUT_VARIABLE LIBR_HOME +) + +# ask R for the include dir +execute_process( + COMMAND ${LIBR_EXECUTABLE} "--slave" "--vanilla" "-e" "cat(normalizePath(R.home('include'), winslash='/'))" + OUTPUT_VARIABLE LIBR_INCLUDE_DIRS +) + +# ask R for the lib dir +execute_process( + COMMAND ${LIBR_EXECUTABLE} "--slave" "--vanilla" "-e" "cat(normalizePath(R.home('lib'), winslash='/'))" + OUTPUT_VARIABLE LIBR_LIBS_DIR +) + +set(LIBR_HOME ${LIBR_HOME} CACHE PATH "R home directory") +set(LIBR_EXECUTABLE ${LIBR_EXECUTABLE} CACHE PATH "R executable") +set(LIBR_INCLUDE_DIRS ${LIBR_INCLUDE_DIRS} CACHE PATH "R include directory") +set(LIBR_LIBS_DIR ${LIBR_LIBS_DIR} CACHE PATH "Where R stores vendored third-party libraries") + +# where is R.so / R.dll / libR.so likely to be found? +set( + LIBR_PATH_HINTS + "${CMAKE_CURRENT_BINARY_DIR}" + "${LIBR_HOME}/lib" + "${LIBR_HOME}/bin/${R_ARCH}" + "${LIBR_HOME}/bin" + "${LIBR_LIBRARIES}" +) + +# look for the core R library +find_library( + LIBR_CORE_LIBRARY + NAMES R R.dll + HINTS ${LIBR_PATH_HINTS} +) + +# starting from CMake 3.17, find_library() will not find .dll files by default +# https://cmake.org/cmake/help/v3.17/release/3.17.html#other-changes +if(WIN32 AND NOT LIBR_CORE_LIBRARY) + find_file( + LIBR_CORE_LIBRARY + NAME R.dll + HINTS ${LIBR_PATH_HINTS} + ) +endif() + +set(LIBR_CORE_LIBRARY ${LIBR_CORE_LIBRARY} CACHE PATH "R core shared library") + +if(WIN32 AND MSVC) + + # create a local R.lib import library for R.dll if it doesn't exist + if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/R.lib") + create_rlib_for_msvc() + endif() + +endif() + +# define find requirements +include(FindPackageHandleStandardArgs) + +if(WIN32 AND MSVC) +# lint_cmake: -package/stdargs + find_package_handle_standard_args( +# lint_cmake: +package/stdargs + LibR DEFAULT_MSG + LIBR_HOME + LIBR_EXECUTABLE + LIBR_INCLUDE_DIRS + LIBR_LIBS_DIR + LIBR_CORE_LIBRARY + LIBR_MSVC_CORE_LIBRARY + ) +else() +# lint_cmake: -package/stdargs + find_package_handle_standard_args( +# lint_cmake: +package/stdargs + LibR DEFAULT_MSG + LIBR_HOME + LIBR_EXECUTABLE + LIBR_INCLUDE_DIRS + LIBR_LIBS_DIR + LIBR_CORE_LIBRARY + ) +endif() diff --git a/cmake/modules/FindNCCL.cmake b/cmake/modules/FindNCCL.cmake new file mode 100644 index 0000000..fe2928a --- /dev/null +++ b/cmake/modules/FindNCCL.cmake @@ -0,0 +1,84 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Tries to find NCCL headers and libraries. +# +# Usage of this module as follows: +# +# find_package(NCCL) +# +# Variables used by this module, they can change the default behaviour and need +# to be set before calling find_package: +# +# NCCL_ROOT - When set, this path is inspected instead of standard library +# locations as the root of the NCCL installation. +# The environment variable NCCL_ROOT overrides this variable. +# +# This module defines +# NCCL_FOUND, whether nccl has been found +# NCCL_INCLUDE_DIR, directory containing header +# NCCL_LIBRARY, directory containing nccl library +# NCCL_LIB_NAME, nccl library name +# USE_NCCL_LIB_PATH, when set, NCCL_LIBRARY path is also inspected for the +# location of the nccl library. This would disable +# switching between static and shared. +# +# It creates an imported target 'NCCL::NCCL' for convenience in linking. +# +# This module assumes that the CUDA toolkit has already been found. +# + +if(NCCL_LIBRARY) + if(NOT USE_NCCL_LIB_PATH) + # Don't cache NCCL_LIBRARY to enable switching between static and shared. + unset(NCCL_LIBRARY CACHE) + endif() +endif() + +if(BUILD_WITH_SHARED_NCCL) + # libnccl.so + set(NCCL_LIB_NAME nccl) + set(_nccl_lib_type "SHARED") +else() + # libnccl_static.a + set(NCCL_LIB_NAME nccl_static) + set(_nccl_lib_type "STATIC") +endif() + +find_path(NCCL_INCLUDE_DIR + NAMES nccl.h + HINTS $ENV{NCCL_ROOT}/include ${NCCL_ROOT}/include) + +find_library(NCCL_LIBRARY + NAMES ${NCCL_LIB_NAME} + HINTS $ENV{NCCL_ROOT}/lib ${NCCL_ROOT}/lib) + +message(STATUS "Using nccl library: ${NCCL_LIBRARY}") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(NCCL DEFAULT_MSG + NCCL_INCLUDE_DIR NCCL_LIBRARY) + +mark_as_advanced( + NCCL_INCLUDE_DIR + NCCL_LIBRARY +) + +if(NCCL_FOUND AND NOT TARGET NCCL::NCCL) + add_library(NCCL::NCCL ${_nccl_lib_type} IMPORTED) + set_target_properties(NCCL::NCCL PROPERTIES + IMPORTED_LOCATION "${NCCL_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${NCCL_INCLUDE_DIR}") +endif() + +unset(_nccl_lib_type) diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..54744d4 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,196 @@ +# Using LightGBM via Docker + +This directory contains `Dockerfile`s to make it easy to build and run LightGBM via [Docker](https://www.docker.com/). + +These builds of LightGBM all train on the CPU. For GPU-enabled builds, see [the gpu/ directory](./gpu). + +## Installing Docker + +Follow the general installation instructions [on the Docker site](https://docs.docker.com/install/): + +* [macOS](https://docs.docker.com/docker-for-mac/install/) +* [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/) +* [Windows](https://docs.docker.com/docker-for-windows/install/) + +## Using CLI Version of LightGBM via Docker + +Build an image with the LightGBM CLI. + +```shell +mkdir lightgbm-docker +cd lightgbm-docker +wget https://raw.githubusercontent.com/lightgbm-org/LightGBM/main/docker/dockerfile-cli +docker build \ + -t lightgbm-cli \ + -f dockerfile-cli \ + . +``` + +Once that completes, the built image can be used to run the CLI in a container. +To try it out, run the following. + +```shell +# configure the CLI +cat << EOF > train.conf +task = train +objective = binary +data = binary.train +num_trees = 10 +output_model = LightGBM-CLI-model.txt +EOF + +# get training data +curl -O https://raw.githubusercontent.com/lightgbm-org/LightGBM/main/examples/binary_classification/binary.train + +# train, and save model to a text file +docker run \ + --rm \ + --volume "${PWD}":/opt/training \ + --workdir /opt/training \ + lightgbm-cli \ + config=train.conf +``` + +After this runs, a LightGBM model can be found at `LightGBM-CLI-model.txt`. + +For more details on how to configure and use the LightGBM CLI, see https://lightgbm.readthedocs.io/en/latest/Quick-Start.html. + +## Running the Python-package Container + +Build an image with the LightGBM Python-package installed. + +```shell +mkdir lightgbm-docker +cd lightgbm-docker +wget https://raw.githubusercontent.com/lightgbm-org/LightGBM/main/docker/dockerfile-python +docker build \ + -t lightgbm-python \ + -f dockerfile-python \ + . +``` + +Once that completes, the built image can be used to run LightGBM's Python-package in a container. +Run the following to produce a model using the Python-package. + +```shell +# get training data +curl -O https://raw.githubusercontent.com/lightgbm-org/LightGBM/main/examples/binary_classification/binary.train + +# create training script +cat << EOF > train.py +import lightgbm as lgb +import numpy as np +params = { + "objective": "binary", + "num_trees": 10 +} + +bst = lgb.train( + train_set=lgb.Dataset("binary.train"), + params=params +) +bst.save_model("LightGBM-python-model.txt") +EOF + +# run training in a container +docker run \ + --rm \ + --volume "${PWD}":/opt/training \ + --workdir /opt/training \ + lightgbm-python \ + python train.py +``` + +After this runs, a LightGBM model can be found at `LightGBM-python-model.txt`. + +Or run an interactive Python session in a container. + +```shell +docker run \ + --rm \ + --volume "${PWD}":/opt/training \ + --workdir /opt/training \ + -it lightgbm-python \ + python +``` + +## Running the R-package Container + +Build an image with the LightGBM R-package installed. + +```shell +mkdir lightgbm-docker +cd lightgbm-docker +wget https://raw.githubusercontent.com/lightgbm-org/LightGBM/main/docker/dockerfile-r + +docker build \ + -t lightgbm-r \ + -f dockerfile-r \ + . +``` + +Once that completes, the built image can be used to run LightGBM's R-package in a container. +Run the following to produce a model using the R-package. + +```shell +# get training data +curl -O https://raw.githubusercontent.com/lightgbm-org/LightGBM/main/examples/binary_classification/binary.train + +# create training script +cat << EOF > train.R +library(lightgbm) +params <- list( + objective = "binary" + , num_trees = 10L +) + +bst <- lgb.train( + data = lgb.Dataset("binary.train"), + params = params +) +lgb.save(bst, "LightGBM-R-model.txt") +EOF + +# run training in a container +docker run \ + --rm \ + --volume "${PWD}":/opt/training \ + --workdir /opt/training \ + lightgbm-r \ + Rscript train.R +``` + +After this runs, a LightGBM model can be found at `LightGBM-R-model.txt`. + +Run the following to get an interactive R session in a container. + +```shell +docker run \ + --rm \ + -it lightgbm-r \ + R +``` + +To use [RStudio](https://www.rstudio.com/products/rstudio/), an interactive development environment, run the following. + +```shell +docker run \ + --rm \ + --env PASSWORD="lightgbm" \ + -p 8787:8787 \ + lightgbm-r +``` + +Then navigate to `localhost:8787` in your local web browser, and log in with username `rstudio` and password `lightgbm`. + +To target a different R version, pass any [valid rocker/verse tag](https://hub.docker.com/r/rocker/verse/tags) to `docker build`. + +For example, to test LightGBM with R 4.5: + +```shell +docker build \ + -t lightgbm-r-45 \ + -f dockerfile-r \ + --build-arg R_VERSION=4.5 \ + . +``` diff --git a/docker/dockerfile-cli b/docker/dockerfile-cli new file mode 100644 index 0000000..c76b8c8 --- /dev/null +++ b/docker/dockerfile-cli @@ -0,0 +1,36 @@ +FROM ubuntu:20.04 + +ENV \ + DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + build-essential \ + gcc \ + g++ \ + git \ + libomp-dev && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -L -o cmake.sh https://github.com/Kitware/CMake/releases/download/v3.29.2/cmake-3.29.2-linux-x86_64.sh && \ + chmod +x cmake.sh && \ + sh ./cmake.sh --prefix=/usr/local --skip-license && \ + rm cmake.sh + +RUN git clone \ + --recursive \ + --branch stable \ + --depth 1 \ + https://github.com/lightgbm-org/LightGBM && \ + cd ./LightGBM && \ + cmake -B build -S . && \ + cmake --build build -j4 && \ + cmake --install build && \ + cd "${HOME}" && \ + rm -rf LightGBM + +ENTRYPOINT ["lightgbm"] diff --git a/docker/dockerfile-python b/docker/dockerfile-python new file mode 100644 index 0000000..5696f77 --- /dev/null +++ b/docker/dockerfile-python @@ -0,0 +1,34 @@ +FROM ubuntu:20.04 + +ARG CONDA_DIR=/opt/miniforge + +ENV \ + DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + PATH=$CONDA_DIR/bin:$PATH + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + cmake \ + build-essential \ + gcc \ + g++ \ + curl \ + git \ + libomp-dev && \ + # python environment + curl -sL https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh -o miniforge.sh && \ + /bin/bash miniforge.sh -f -b -p $CONDA_DIR && \ + export PATH="$CONDA_DIR/bin:$PATH" && \ + conda config --set always_yes yes --set changeps1 no && \ + # lightgbm + conda install -q -y numpy scipy scikit-learn pandas && \ + git clone --recursive --branch stable --depth 1 https://github.com/lightgbm-org/LightGBM && \ + cd ./LightGBM && \ + sh ./build-python.sh install && \ + # clean + apt-get autoremove -y && apt-get clean && \ + conda clean -a -y && \ + rm -rf /usr/local/src/* diff --git a/docker/dockerfile-r b/docker/dockerfile-r new file mode 100644 index 0000000..ec13929 --- /dev/null +++ b/docker/dockerfile-r @@ -0,0 +1,16 @@ +ARG R_VERSION=latest +FROM rocker/verse:${R_VERSION} + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + libomp-dev && \ + git clone \ + --recursive \ + --branch stable \ + --depth 1 https://github.com/lightgbm-org/LightGBM && \ + cd ./LightGBM && \ + sh build-cran-package.sh --no-build-vignettes && \ + R CMD INSTALL ./lightgbm_*.tar.gz && \ + cd .. && \ + rm -rf ./LightGBM diff --git a/docker/gpu/README.md b/docker/gpu/README.md new file mode 100644 index 0000000..d359c9f --- /dev/null +++ b/docker/gpu/README.md @@ -0,0 +1,58 @@ +# Tiny Distroless Dockerfile for LightGBM GPU CLI-only Version + +`dockerfile-cli-only-distroless.gpu` - A multi-stage build based on the `nvidia/opencl:devel-ubuntu18.04` (build) and `distroless/cc-debian10` (production) images. LightGBM (CLI-only) can be utilized in GPU and CPU modes. The resulting image size is around 15 MB. + +--- + +# Small Dockerfile for LightGBM GPU CLI-only Version + +`dockerfile-cli-only.gpu` - A multi-stage build based on the `nvidia/opencl:devel` (build) and `nvidia/opencl:runtime` (production) images. LightGBM (CLI-only) can be utilized in GPU and CPU modes. The resulting image size is around 100 MB. + +--- + +# Dockerfile for LightGBM GPU Version with Python + +`dockerfile.gpu` - A docker file with LightGBM utilizing nvidia-docker. The file is based on the `nvidia/cuda:8.0-cudnn5-devel` image. +LightGBM can be utilized in GPU and CPU modes and via Python. + +## Contents + +- LightGBM (cpu + gpu) +- Python (conda) + scikit-learn, notebooks, pandas, matplotlib + +Running the container starts a Jupyter Notebook at `localhost:8888`. + +Jupyter password: `keras`. + +## Requirements + +Requires docker and [nvidia-docker](https://github.com/NVIDIA/nvidia-docker) on host machine. + +## Quickstart + +### Build Docker Image + +```sh +mkdir lightgbm-docker +cd lightgbm-docker +wget https://raw.githubusercontent.com/lightgbm-org/LightGBM/main/docker/gpu/dockerfile.gpu +docker build -f dockerfile.gpu -t lightgbm-gpu . +``` + +### Run Image + +```sh +nvidia-docker run --rm -d --name lightgbm-gpu -p 8888:8888 -v /home:/home lightgbm-gpu +``` + +### Attach with Command Line Access (if required) + +```sh +docker exec -it lightgbm-gpu bash +``` + +### Jupyter Notebook + +```sh +localhost:8888 +``` diff --git a/docker/gpu/dockerfile-cli-only-distroless.gpu b/docker/gpu/dockerfile-cli-only-distroless.gpu new file mode 100644 index 0000000..f8b07af --- /dev/null +++ b/docker/gpu/dockerfile-cli-only-distroless.gpu @@ -0,0 +1,75 @@ +# Copyright (c) 2020 The Rector and Visitors of the University of Virginia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without +# limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial portions +# of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +# TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +FROM nvidia/opencl:devel-ubuntu18.04 AS build +ARG DEBIAN_FRONTEND=noninteractive +ARG OPENCL_LIBRARIES=/usr/lib/x86_64-linux-gnu +ARG OPENCL_INCLUDE_DIR=/usr/include/CL + +# SYSTEM +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + ca-certificates \ + libglib2.0-0 \ + libxext6 \ + libsm6 \ + libxrender1 \ + cmake \ + libboost-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + gcc \ + g++ && \ + rm -rf /var/lib/apt/lists/* + +# LightGBM +WORKDIR /opt +RUN git clone --recursive --branch stable --depth 1 https://github.com/lightgbm-org/LightGBM && \ + cd LightGBM && \ + cmake -B build -S . -DUSE_GPU=1 -DOpenCL_LIBRARY=${OPENCL_LIBRARIES}/libOpenCL.so.1 -DOpenCL_INCLUDE_DIR=$OPENCL_INCLUDE_DIR && \ + OPENCL_HEADERS=$OPENCL_INCLUDE_DIR LIBOPENCL=$OPENCL_LIBRARIES cmake --build build + +FROM gcr.io/distroless/cc-debian10 +COPY --from=build \ + /opt/LightGBM/lightgbm \ + /opt/LightGBM/lib_lightgbm.so \ + /opt/LightGBM/ +COPY --from=build \ + /usr/lib/x86_64-linux-gnu/libOpenCL.so.1 \ + /usr/lib/x86_64-linux-gnu/libboost_filesystem.so.1.65.1 \ + /usr/lib/x86_64-linux-gnu/libboost_system.so.1.65.1 \ + /usr/lib/x86_64-linux-gnu/libgomp.so.1 \ + /usr/lib/x86_64-linux-gnu/libstdc++.so.6 \ + /usr/lib/x86_64-linux-gnu/ +COPY --from=build \ + /lib/x86_64-linux-gnu/libm.so.6 \ + /lib/x86_64-linux-gnu/libgcc_s.so.1 \ + /lib/x86_64-linux-gnu/libpthread.so.0 \ + /lib/x86_64-linux-gnu/libc.so.6 \ + /lib/x86_64-linux-gnu/libdl.so.2 \ + /lib/x86_64-linux-gnu/ +COPY --from=build \ + /lib64/ld-linux-x86-64.so.2 \ + /lib64/ +COPY --from=build /etc/OpenCL/vendors/nvidia.icd /etc/OpenCL/vendors/nvidia.icd + +ENV PATH /opt/LightGBM:${PATH} +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 + +ENTRYPOINT ["lightgbm"] diff --git a/docker/gpu/dockerfile-cli-only.gpu b/docker/gpu/dockerfile-cli-only.gpu new file mode 100644 index 0000000..8bd12a3 --- /dev/null +++ b/docker/gpu/dockerfile-cli-only.gpu @@ -0,0 +1,67 @@ +# Copyright (c) 2020 The Rector and Visitors of the University of Virginia +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without +# limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial portions +# of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +# TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +FROM nvidia/opencl:devel AS build +ARG DEBIAN_FRONTEND=noninteractive +ARG OPENCL_LIBRARIES=/usr/lib/x86_64-linux-gnu +ARG OPENCL_INCLUDE_DIR=/usr/include/CL + +# SYSTEM +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + ca-certificates \ + libglib2.0-0 \ + libxext6 \ + libsm6 \ + libxrender1 \ + cmake \ + libboost-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + gcc \ + g++ && \ + rm -rf /var/lib/apt/lists/* + +# LightGBM +WORKDIR /opt +RUN git clone --recursive --branch stable --depth 1 https://github.com/lightgbm-org/LightGBM && \ + cd LightGBM && \ + cmake -B build -S . -DUSE_GPU=1 -DOpenCL_LIBRARY=${OPENCL_LIBRARIES}/libOpenCL.so.1 -DOpenCL_INCLUDE_DIR=$OPENCL_INCLUDE_DIR && \ + OPENCL_HEADERS=$OPENCL_INCLUDE_DIR LIBOPENCL=$OPENCL_LIBRARIES cmake --build build + +FROM nvidia/opencl:runtime +RUN apt-get update && apt-get install -y --no-install-recommends \ + libxext6 \ + libsm6 \ + libxrender1 \ + libboost-system-dev \ + libboost-filesystem-dev \ + gcc \ + g++ && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=build \ + /opt/LightGBM/lightgbm \ + /opt/LightGBM/lib_lightgbm.so \ + /opt/LightGBM/ + +ENV PATH /opt/LightGBM:${PATH} +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 + +ENTRYPOINT ["lightgbm"] diff --git a/docker/gpu/dockerfile.gpu b/docker/gpu/dockerfile.gpu new file mode 100644 index 0000000..cd4be66 --- /dev/null +++ b/docker/gpu/dockerfile.gpu @@ -0,0 +1,122 @@ +FROM nvidia/cuda:8.0-cudnn5-devel + +################################################################################################################# +# Global +################################################################################################################# +# apt-get to skip any interactive post-install configuration steps with DEBIAN_FRONTEND=noninteractive and apt-get install -y + +ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 +ARG DEBIAN_FRONTEND=noninteractive + +################################################################################################################# +# Global Path Setting +################################################################################################################# + +ENV CUDA_HOME /usr/local/cuda +ENV LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:${CUDA_HOME}/lib64 +ENV LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:/usr/local/lib + +ENV OPENCL_LIBRARIES /usr/local/cuda/lib64 +ENV OPENCL_INCLUDE_DIR /usr/local/cuda/include + +################################################################################################################# +# TINI +################################################################################################################# + +# Install tini +ENV TINI_VERSION v0.14.0 +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini +RUN chmod +x /tini + +################################################################################################################# +# SYSTEM +################################################################################################################# +# update: downloads the package lists from the repositories and "updates" them to get information on the newest versions of packages and their +# dependencies. It will do this for all repositories and PPAs. + +RUN apt-get update && \ +apt-get install -y --no-install-recommends \ +build-essential \ +curl \ +bzip2 \ +ca-certificates \ +libglib2.0-0 \ +libxext6 \ +libsm6 \ +libxrender1 \ +git \ +vim \ +mercurial \ +subversion \ +cmake \ +libboost-dev \ +libboost-system-dev \ +libboost-filesystem-dev \ +gcc \ +g++ + +# Add OpenCL ICD files for LightGBM +RUN mkdir -p /etc/OpenCL/vendors && \ + echo "libnvidia-opencl.so.1" > /etc/OpenCL/vendors/nvidia.icd + +################################################################################################################# +# CONDA +################################################################################################################# + +ARG CONDA_DIR=/opt/miniforge +# add to path +ENV PATH $CONDA_DIR/bin:$PATH + +# Install miniforge +RUN echo "export PATH=$CONDA_DIR/bin:"'$PATH' > /etc/profile.d/conda.sh && \ + curl -sL https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh -o ~/miniforge.sh && \ + /bin/bash ~/miniforge.sh -b -p $CONDA_DIR && \ + rm ~/miniforge.sh + +RUN conda config --set always_yes yes --set changeps1 no && \ + conda create -y -q -n py3 numpy scipy scikit-learn jupyter notebook ipython pandas matplotlib + +################################################################################################################# +# LightGBM +################################################################################################################# + +RUN cd /usr/local/src && mkdir lightgbm && cd lightgbm && \ + git clone --recursive --branch stable --depth 1 https://github.com/lightgbm-org/LightGBM && \ + cd LightGBM && \ + cmake -B build -S . -DUSE_GPU=1 -DOpenCL_LIBRARY=/usr/local/cuda/lib64/libOpenCL.so -DOpenCL_INCLUDE_DIR=/usr/local/cuda/include/ && \ + OPENCL_HEADERS=/usr/local/cuda-8.0/targets/x86_64-linux/include LIBOPENCL=/usr/local/cuda-8.0/targets/x86_64-linux/lib cmake --build build + +ENV PATH /usr/local/src/lightgbm/LightGBM:${PATH} + +RUN /bin/bash -c "source activate py3 && cd /usr/local/src/lightgbm/LightGBM && sh ./build-python.sh install --precompile && source deactivate" + +################################################################################################################# +# System CleanUp +################################################################################################################# +# apt-get autoremove: used to remove packages that were automatically installed to satisfy dependencies for some package and that are no more needed. +# apt-get clean: removes the aptitude cache in /var/cache/apt/archives. You'd be amazed how much is in there! the only drawback is that the packages +# have to be downloaded again if you reinstall them. + +RUN apt-get autoremove -y && apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + conda clean -a -y + +################################################################################################################# +# JUPYTER +################################################################################################################# + +# password: keras +# password key: --NotebookApp.password='sha1:98b767162d34:8da1bc3c75a0f29145769edc977375a373407824' + +# Add a notebook profile. +RUN mkdir -p -m 700 ~/.jupyter/ && \ + echo "c.NotebookApp.ip = '*'" >> ~/.jupyter/jupyter_notebook_config.py + +VOLUME /home +WORKDIR /home + +# IPython +EXPOSE 8888 + +ENTRYPOINT [ "/tini", "--" ] +CMD /bin/bash -c "source activate py3 && jupyter notebook --allow-root --no-browser --NotebookApp.password='sha1:98b767162d34:8da1bc3c75a0f29145769edc977375a373407824' && source deactivate" diff --git a/docs/.lychee.toml b/docs/.lychee.toml new file mode 100644 index 0000000..5b68c46 --- /dev/null +++ b/docs/.lychee.toml @@ -0,0 +1,32 @@ +verbose = "info" +no_progress = false +cache = false +scheme = ["http", "https", "file"] +include_mail = false +include_fragments = true +no_ignore = true +insecure = false +require_https = true +accept = ["100..=103", "200..=299"] +user_agent = "curl/7.88.1" +header = {"User-Agent" = "curl/7.88.1"} +timeout = 30 +retry_wait_time = 10 +max_concurrency = 10 +# remove anchors from GitHub URLs to overcome https://github.com/lycheeverse/lychee/issues/1729 +remap = [ + '(?P^https://github\.com)/(?P.*)#(?P.*)$ $host/$path/', +] +exclude = [ + '^https://www\.swig\.org/download\.html$', + '^https://proceedings\.neurips\.cc/.*', + '^https://www\.amd\.com/en/support\.html$', + '^https://www\.jstor\.org/stable/2281952$', + '^https://dl\.acm\.org/doi/10\.1145/3298689\.3347033$', + '^https://packages\.ubuntu\.com/search.*', + '^https://stackoverflow\.com/.*', + '^https://.*\.stackexchange\.com/.*', +] +exclude_path = [ + "(^|/)docs/.*\\.rst", +] diff --git a/docs/Advanced-Topics.rst b/docs/Advanced-Topics.rst new file mode 100644 index 0000000..b2e8bc4 --- /dev/null +++ b/docs/Advanced-Topics.rst @@ -0,0 +1,121 @@ +Advanced Topics +=============== + +Missing Value Handle +-------------------- + +- LightGBM enables the missing value handle by default. Disable it by setting ``use_missing=false``. + +- LightGBM uses NA (NaN) to represent missing values by default. Change it to use zero by setting ``zero_as_missing=true``. + +- When ``zero_as_missing=false`` (default), the unrecorded values in sparse matrices (and LightSVM) are treated as zeros. + +- When ``zero_as_missing=true``, NA and zeros (including unrecorded values in sparse matrices (and LightSVM)) are treated as missing. + +Categorical Feature Support +--------------------------- + +- LightGBM offers good accuracy with integer-encoded categorical features. LightGBM applies + `Fisher (1958) `_ + to find the optimal split over categories as + `described here <./Features.rst#optimal-split-for-categorical-features>`_. This often performs better than one-hot encoding. + +- Use ``categorical_feature`` to specify the categorical features. + Refer to the parameter ``categorical_feature`` in `Parameters <./Parameters.rst#categorical_feature>`__. + +- Categorical features will be cast to ``int32`` (integer codes will be extracted from pandas categoricals in the Python-package) so they must be encoded as non-negative integers (negative values will be treated as missing) + less than ``Int32.MaxValue`` (2147483647). + It is best to use a contiguous range of integers started from zero. + Floating point numbers in categorical features will be rounded towards 0. + +- When using ``pandas.DataFrame`` inputs with columns of dtype ``category``, LightGBM will + align categories to those observed during training before converting them to integer values. + This ensures consistent encoding between training and prediction without additional preprocessing. + +- At ``predict()`` time, categories not seen during training will be treated as missing values. + +- Use ``min_data_per_group``, ``cat_smooth`` to deal with over-fitting (when ``#data`` is small or ``#category`` is large). + +- For a categorical feature with high cardinality (``#category`` is large), it often works best to + treat the feature as numeric, either by simply ignoring the categorical interpretation of the integers or + by embedding the categories in a low-dimensional numeric space. + +LambdaRank +---------- + +- The label should be of type ``int``, such that larger numbers correspond to higher relevance (e.g. 0:bad, 1:fair, 2:good, 3:perfect). + +- Use ``label_gain`` to set the gain(weight) of ``int`` label. + +- Use ``lambdarank_truncation_level`` to truncate the max DCG. + +Cost Efficient Gradient Boosting +-------------------------------- + +`Cost Efficient Gradient Boosting `_ (CEGB) makes it possible to penalise boosting based on the cost of obtaining feature values. +CEGB penalises learning in the following ways: + +- Each time a tree is split, a penalty of ``cegb_penalty_split`` is applied. +- When a feature is used for the first time, ``cegb_penalty_feature_coupled`` is applied. This penalty can be different for each feature and should be specified as one ``double`` per feature. +- When a feature is used for the first time for a data row, ``cegb_penalty_feature_lazy`` is applied. Like ``cegb_penalty_feature_coupled``, this penalty is specified as one ``double`` per feature. + +Each of the penalties above is scaled by ``cegb_tradeoff``. +Using this parameter, it is possible to change the overall strength of the CEGB penalties by changing only one parameter. + +Parameters Tuning +----------------- + +- Refer to `Parameters Tuning <./Parameters-Tuning.rst>`__. + +.. _Parallel Learning: + +Distributed Learning +-------------------- + +- Refer to `Distributed Learning Guide <./Parallel-Learning-Guide.rst>`__. + +GPU Support +----------- + +- Refer to `GPU Tutorial <./GPU-Tutorial.rst>`__ and `GPU Targets <./GPU-Targets.rst>`__. + +Support for Position Bias Treatment +------------------------------------ + +Often the relevance labels provided in Learning-to-Rank tasks might be derived from implicit user feedback (e.g., clicks) and therefore might be biased due to their position/location on the screen when having been presented to a user. +LightGBM can make use of positional data. + +For example, consider the case where you expect that the first 3 results from a search engine will be visible in users' browsers without scrolling, and all other results for a query would require scrolling. + +LightGBM could be told to account for the position bias from results being "above the fold" by providing a ``positions`` array encoded as follows: + +:: + + 0 + 0 + 0 + 1 + 1 + 0 + 0 + 0 + 1 + ... + +Where ``0 = "above the fold"`` and ``1 = "requires scrolling"``. +The specific values are not important, as long as they are consistent across all observations in the training data. +An encoding like ``100 = "above the fold"`` and ``17 = "requires scrolling"`` would result in exactly the same trained model. + +In that way, ``positions`` in LightGBM's API are similar to a categorical feature. +Just as with non-ordinal categorical features, an integer representation is just used for memory and computational efficiency... LightGBM does not care about the absolute or relative magnitude of the values. + +Unlike a categorical feature, however, ``positions`` are used to adjust the target to reduce the bias in predictions made by the trained model. + +The position file corresponds with training data file line by line, and has one position per line. And if the name of training data file is ``train.txt``, the position file should be named as ``train.txt.position`` and placed in the same folder as the data file. +In this case, LightGBM will load the position file automatically if it exists. The positions can also be specified through the ``Dataset`` constructor when using Python API. If the positions are specified in both approaches, the ``.position`` file will be ignored. + +Currently, implemented is an approach to model position bias by using an idea of Generalized Additive Models (`GAM `_) to linearly decompose the document score ``s`` into the sum of a relevance component ``f`` and a positional component ``g``: ``s(x, pos) = f(x) + g(pos)`` where the former component depends on the original query-document features and the latter depends on the position of an item. +During the training, the compound scoring function ``s(x, pos)`` is fit with a standard ranking algorithm (e.g., LambdaMART) which boils down to jointly learning the relevance component ``f(x)`` (it is later returned as an unbiased model) and the position factors ``g(pos)`` that help better explain the observed (biased) labels. +Similar score decomposition ideas have previously been applied for classification & pointwise ranking tasks with assumptions of binary labels and binary relevance (a.k.a. "two-tower" models, refer to the papers: `Towards Disentangling Relevance and Bias in Unbiased Learning to Rank `_, `PAL: a position-bias aware learning framework for CTR prediction in live recommender systems `_, `A General Framework for Debiasing in CTR Prediction `_). +In LightGBM, we adapt this idea to general pairwise Lerarning-to-Rank with arbitrary ordinal relevance labels. +Besides, GAMs have been used in the context of explainable ML (`Accurate Intelligible Models with Pairwise Interactions `_) to linearly decompose the contribution of each feature (and possibly their pairwise interactions) to the overall score, for subsequent analysis and interpretation of their effects in the trained models. diff --git a/docs/C-API.rst b/docs/C-API.rst new file mode 100644 index 0000000..2145cb6 --- /dev/null +++ b/docs/C-API.rst @@ -0,0 +1,4 @@ +C API +===== + +.. doxygenfile:: c_api.h diff --git a/docs/Development-Guide.rst b/docs/Development-Guide.rst new file mode 100644 index 0000000..ddcd509 --- /dev/null +++ b/docs/Development-Guide.rst @@ -0,0 +1,95 @@ +Development Guide +================= + +Algorithms +---------- + +Refer to `Features <./Features.rst>`__ for understanding of important algorithms used in LightGBM. + +Classes and Code Structure +-------------------------- + +Important Classes +~~~~~~~~~~~~~~~~~ + ++-------------------------+----------------------------------------------------------------------------------------+ +| Class | Description | ++=========================+========================================================================================+ +| ``Application`` | The entrance of application, including training and prediction logic | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``Bin`` | Data structure used for storing feature discrete values (converted from float values) | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``Boosting`` | Boosting interface (GBDT, DART, etc.) | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``Config`` | Stores parameters and configurations | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``Dataset`` | Stores information of dataset | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``DatasetLoader`` | Used to construct dataset | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``FeatureGroup`` | Stores the data of feature, could be multiple features | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``Metric`` | Evaluation metrics | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``Network`` | Network interfaces and communication algorithms | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``ObjectiveFunction`` | Objective functions used to train | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``Tree`` | Stores information of tree model | ++-------------------------+----------------------------------------------------------------------------------------+ +| ``TreeLearner`` | Used to learn trees | ++-------------------------+----------------------------------------------------------------------------------------+ + +Code Structure +~~~~~~~~~~~~~~ + ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| Path | Description | ++=====================+====================================================================================================================================+ +| ./include | Header files | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| ./include/utils | Some common functions | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| ./src/application | Implementations of training and prediction logic | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| ./src/boosting | Implementations of Boosting | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| ./src/io | Implementations of IO related classes, including ``Bin``, ``Config``, ``Dataset``, ``DatasetLoader``, ``Feature`` and ``Tree`` | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| ./src/metric | Implementations of metrics | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| ./src/network | Implementations of network functions | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| ./src/objective | Implementations of objective functions | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ +| ./src/treelearner | Implementations of tree learners | ++---------------------+------------------------------------------------------------------------------------------------------------------------------------+ + +Documents API +------------- + +Refer to `docs README <./README.rst>`__. + +C API +----- + +Refer to `C API <./C-API.rst>`__ or the comments in `c\_api.h `__ file, from which the documentation is generated. + +Tests +----- + +C++ unit tests are located in the ``./tests/cpp_tests`` folder and written with the help of Google Test framework. +To run tests locally first refer to the `Installation Guide <./Installation-Guide.rst#build-c-unit-tests>`__ for how to build tests and then simply run compiled executable file. +It is highly recommended to build tests with `sanitizers <./Installation-Guide.rst#sanitizers>`__. + +High Level Language Package +--------------------------- + +See the implementations at `Python-package `__ and `R-package `__. + +Questions +--------- + +Refer to `FAQ <./FAQ.rst>`__. + +Also feel free to open `issues `__ if you met problems. diff --git a/docs/Experiments.rst b/docs/Experiments.rst new file mode 100644 index 0000000..8350e31 --- /dev/null +++ b/docs/Experiments.rst @@ -0,0 +1,253 @@ +Experiments +=========== + +Comparison Experiment +--------------------- + +For the detailed experiment scripts and output logs, please refer to this `repo`_. + +History +^^^^^^^ + +08 Mar, 2020: update according to the latest master branch (`1b97eaf `__ for XGBoost, `bcad692 `__ for LightGBM). (``xgboost_exact`` is not updated for it is too slow.) + +27 Feb, 2017: first version. + +Data +^^^^ + +We used 5 datasets to conduct our comparison experiments. Details of data are listed in the following table: + ++-----------+-----------------------+---------------------------------------------------------------------------------+-------------+----------+----------------------------------------------+ +| Data | Task | Link | #Train\_Set | #Feature | Comments | ++===========+=======================+=================================================================================+=============+==========+==============================================+ +| Higgs | Binary classification | `link `__ | 10,500,000 | 28 | last 500,000 samples were used as test set | ++-----------+-----------------------+---------------------------------------------------------------------------------+-------------+----------+----------------------------------------------+ +| Yahoo LTR | Learning to rank | `link `__ | 473,134 | 700 | set1.train as train, set1.test as test | ++-----------+-----------------------+---------------------------------------------------------------------------------+-------------+----------+----------------------------------------------+ +| MS LTR | Learning to rank | `link `__ | 2,270,296 | 137 | {S1,S2,S3} as train set, {S5} as test set | ++-----------+-----------------------+---------------------------------------------------------------------------------+-------------+----------+----------------------------------------------+ +| Expo | Binary classification | `link `__ | 11,000,000 | 700 | last 1,000,000 samples were used as test set | ++-----------+-----------------------+---------------------------------------------------------------------------------+-------------+----------+----------------------------------------------+ +| Allstate | Binary classification | `link `__ | 13,184,290 | 4228 | last 1,000,000 samples were used as test set | ++-----------+-----------------------+---------------------------------------------------------------------------------+-------------+----------+----------------------------------------------+ + +Environment +^^^^^^^^^^^ + +We ran all experiments on a single Linux server (Azure ND24s) with the following specifications: + ++------------------+-----------------+---------------------+ +| OS | CPU | Memory | ++==================+=================+=====================+ +| Ubuntu 16.04 LTS | 2 \* E5-2690 v4 | 448GB | ++------------------+-----------------+---------------------+ + +Baseline +^^^^^^^^ + +We used `xgboost`_ as a baseline. + +Both xgboost and LightGBM were built with OpenMP support. + +Settings +^^^^^^^^ + +We set up total 3 settings for experiments. The parameters of these settings are: + +1. xgboost: + + .. code:: text + + eta = 0.1 + max_depth = 8 + num_round = 500 + nthread = 16 + tree_method = exact + min_child_weight = 100 + +2. xgboost\_hist (using histogram based algorithm): + + .. code:: text + + eta = 0.1 + num_round = 500 + nthread = 16 + min_child_weight = 100 + tree_method = hist + grow_policy = lossguide + max_depth = 0 + max_leaves = 255 + +3. LightGBM: + + .. code:: text + + learning_rate = 0.1 + num_leaves = 255 + num_trees = 500 + num_threads = 16 + min_data_in_leaf = 0 + min_sum_hessian_in_leaf = 100 + +xgboost grows trees depth-wise and controls model complexity by ``max_depth``. +LightGBM uses a leaf-wise algorithm instead and controls model complexity by ``num_leaves``. +So we cannot compare them in the exact same model setting. For the tradeoff, we use xgboost with ``max_depth=8``, which will have max number leaves to 255, to compare with LightGBM with ``num_leaves=255``. + +Other parameters are default values. + +Result +^^^^^^ + +Speed +''''' + +We compared speed using only the training task without any test or metric output. We didn't count the time for IO. +For the ranking tasks, since XGBoost and LightGBM implement different ranking objective functions, we used ``regression`` objective for speed benchmark, for the fair comparison. + +The following table is the comparison of time cost: + ++-----------+-----------+---------------+---------------+ +| Data | xgboost | xgboost\_hist | LightGBM | ++===========+===========+===============+===============+ +| Higgs | 3794.34 s | 165.575 s | **130.094 s** | ++-----------+-----------+---------------+---------------+ +| Yahoo LTR | 674.322 s | 131.462 s | **76.229 s** | ++-----------+-----------+---------------+---------------+ +| MS LTR | 1251.27 s | 98.386 s | **70.417 s** | ++-----------+-----------+---------------+---------------+ +| Expo | 1607.35 s | 137.65 s | **62.607 s** | ++-----------+-----------+---------------+---------------+ +| Allstate | 2867.22 s | 315.256 s | **148.231 s** | ++-----------+-----------+---------------+---------------+ + +LightGBM ran faster than xgboost on all experiment data sets. + +Accuracy +'''''''' + +We computed all accuracy metrics only on the test data set. + ++-----------+-----------------+----------+-------------------+--------------+ +| Data | Metric | xgboost | xgboost\_hist | LightGBM | ++===========+=================+==========+===================+==============+ +| Higgs | AUC | 0.839593 | 0.845314 | **0.845724** | ++-----------+-----------------+----------+-------------------+--------------+ +| Yahoo LTR | NDCG\ :sub:`1` | 0.719748 | 0.720049 | **0.732981** | +| +-----------------+----------+-------------------+--------------+ +| | NDCG\ :sub:`3` | 0.717813 | 0.722573 | **0.735689** | +| +-----------------+----------+-------------------+--------------+ +| | NDCG\ :sub:`5` | 0.737849 | 0.740899 | **0.75352** | +| +-----------------+----------+-------------------+--------------+ +| | NDCG\ :sub:`10` | 0.78089 | 0.782957 | **0.793498** | ++-----------+-----------------+----------+-------------------+--------------+ +| MS LTR | NDCG\ :sub:`1` | 0.483956 | 0.485115 | **0.517767** | +| +-----------------+----------+-------------------+--------------+ +| | NDCG\ :sub:`3` | 0.467951 | 0.47313 | **0.501063** | +| +-----------------+----------+-------------------+--------------+ +| | NDCG\ :sub:`5` | 0.472476 | 0.476375 | **0.504648** | +| +-----------------+----------+-------------------+--------------+ +| | NDCG\ :sub:`10` | 0.492429 | 0.496553 | **0.524252** | ++-----------+-----------------+----------+-------------------+--------------+ +| Expo | AUC | 0.756713 | 0.776224 | **0.776935** | ++-----------+-----------------+----------+-------------------+--------------+ +| Allstate | AUC | 0.607201 | **0.609465** | 0.609072 | ++-----------+-----------------+----------+-------------------+--------------+ + +Memory Consumption +'''''''''''''''''' + +We monitored RES while running training task. And we set ``two_round=true`` (this will increase data-loading time and +reduce peak memory usage but not affect training speed or accuracy) in LightGBM to reduce peak memory usage. + ++-----------+---------+---------------+--------------------+--------------------+ +| Data | xgboost | xgboost\_hist | LightGBM (col-wise)|LightGBM (row-wise) | ++===========+=========+===============+====================+====================+ +| Higgs | 4.853GB | 7.335GB | **0.897GB** | 1.401GB | ++-----------+---------+---------------+--------------------+--------------------+ +| Yahoo LTR | 1.907GB | 4.023GB | **1.741GB** | 2.161GB | ++-----------+---------+---------------+--------------------+--------------------+ +| MS LTR | 5.469GB | 7.491GB | **0.940GB** | 1.296GB | ++-----------+---------+---------------+--------------------+--------------------+ +| Expo | 1.553GB | 2.606GB | **0.555GB** | 0.711GB | ++-----------+---------+---------------+--------------------+--------------------+ +| Allstate | 6.237GB | 12.090GB | **1.116GB** | 1.755GB | ++-----------+---------+---------------+--------------------+--------------------+ + +Parallel Experiment +------------------- + +History +^^^^^^^ + +27 Feb, 2017: first version. + +Data +^^^^ + +We used a terabyte click log dataset to conduct parallel experiments. Details are listed in following table: + ++--------+-----------------------+---------+---------------+----------+ +| Data | Task | Link | #Data | #Feature | ++========+=======================+=========+===============+==========+ +| Criteo | Binary classification | `link`_ | 1,700,000,000 | 67 | ++--------+-----------------------+---------+---------------+----------+ + +This data contains 13 integer features and 26 categorical features for 24 days of click logs. +We statisticized the click-through rate (CTR) and count for these 26 categorical features from the first ten days. +Then we used next ten days' data, after replacing the categorical features by the corresponding CTR and count, as training data. +The processed training data have a total of 1.7 billions records and 67 features. + +Environment +^^^^^^^^^^^ + +We ran our experiments on 16 Windows servers with the following specifications: + ++---------------------+-----------------+---------------------+-------------------------------------------+ +| OS | CPU | Memory | Network Adapter | ++=====================+=================+=====================+===========================================+ +| Windows Server 2012 | 2 \* E5-2670 v2 | DDR3 1600Mhz, 256GB | Mellanox ConnectX-3, 54Gbps, RDMA support | ++---------------------+-----------------+---------------------+-------------------------------------------+ + +Settings +^^^^^^^^ + +.. code:: text + + learning_rate = 0.1 + num_leaves = 255 + num_trees = 100 + num_thread = 16 + tree_learner = data + +We used data parallel here because this data is large in ``#data`` but small in ``#feature``. Other parameters were default values. + +Results +^^^^^^^ + ++----------+---------------+---------------------------+ +| #Machine | Time per Tree | Memory Usage(per Machine) | ++==========+===============+===========================+ +| 1 | 627.8 s | 176GB | ++----------+---------------+---------------------------+ +| 2 | 311 s | 87GB | ++----------+---------------+---------------------------+ +| 4 | 156 s | 43GB | ++----------+---------------+---------------------------+ +| 8 | 80 s | 22GB | ++----------+---------------+---------------------------+ +| 16 | 42 s | 11GB | ++----------+---------------+---------------------------+ + +The results show that LightGBM achieves a linear speedup with distributed learning. + +GPU Experiments +--------------- + +Refer to `GPU Performance <./GPU-Performance.rst>`__. + +.. _repo: https://github.com/guolinke/boosting_tree_benchmarks + +.. _xgboost: https://github.com/dmlc/xgboost + +.. _link: https://ailab.criteo.com/download-criteo-1tb-click-logs-dataset/ diff --git a/docs/FAQ.rst b/docs/FAQ.rst new file mode 100644 index 0000000..c9354f4 --- /dev/null +++ b/docs/FAQ.rst @@ -0,0 +1,420 @@ +.. role:: raw-html(raw) + :format: html + +LightGBM FAQ +############ + +.. contents:: LightGBM Frequently Asked Questions + :depth: 1 + :local: + :backlinks: none + +------ + +Please post questions, feature requests, and bug reports at https://github.com/lightgbm-org/LightGBM/issues. + +This project is mostly maintained by volunteers, so please be patient. +If your request is time-sensitive or more than a month goes by without a response, please tag the maintainers below for help. + +- `@guolinke `__ **Guolin Ke** +- `@shiyu1994 `__ **Yu Shi** +- `@jameslamb `__ **James Lamb** +- `@jmoralez `__ **José Morales** +- `@borchero `__ **Oliver Borchert** +- `@mayer79 `__ **Michael Mayer** + +-------------- + +General LightGBM Questions +========================== + +.. contents:: + :local: + :backlinks: none + +1. Where do I find more details about LightGBM parameters? +---------------------------------------------------------- + +Take a look at `Parameters <./Parameters.rst>`__. + +2. On datasets with millions of features, training does not start (or starts after a very long time). +----------------------------------------------------------------------------------------------------- + +Use a smaller value for ``bin_construct_sample_cnt`` and a larger value for ``min_data``. + +3. When running LightGBM on a large dataset, my computer runs out of RAM. +------------------------------------------------------------------------- + +**Multiple Solutions**: set the ``histogram_pool_size`` parameter to the MB you want to use for LightGBM (histogram\_pool\_size + dataset size = approximately RAM used), +lower ``num_leaves`` or lower ``max_bin`` (see `lightgbm-org/LightGBM#562 `__). + +4. I am using Windows. Should I use Visual Studio or MinGW for compiling LightGBM? +---------------------------------------------------------------------------------- + +Visual Studio `performs best for LightGBM `__. + +5. When using LightGBM GPU, I cannot reproduce results over several runs. +------------------------------------------------------------------------- + +This is normal and expected behaviour, but you may try to use ``gpu_use_dp = true`` for reproducibility +(see `lightgbm-org/LightGBM#560 `__). +You may also use the CPU version. + +6. Bagging is not reproducible when changing the number of threads. +------------------------------------------------------------------- + +:raw-html:`` +LightGBM bagging is multithreaded, so its output depends on the number of threads used. +There is `no workaround currently `__. +:raw-html:`` + +Starting from `#2804 `__ bagging result doesn't depend on the number of threads. +So this issue should be solved in the latest version. + +7. I tried to use Random Forest mode, and LightGBM crashes! +----------------------------------------------------------- + +This is expected behaviour for arbitrary parameters. To enable Random Forest, +you must use ``bagging_fraction`` and ``feature_fraction`` different from 1, along with a ``bagging_freq``. +`This thread `__ includes an example. + +8. CPU usage is low (like 10%) in Windows when using LightGBM on very large datasets with many-core systems. +------------------------------------------------------------------------------------------------------------ + +Please use `Visual Studio `__ +as it may be `10x faster than MinGW `__ especially for very large trees. + +9. When I'm trying to specify a categorical column with the ``categorical_feature`` parameter, I get the following sequence of warnings, but there are no negative values in the column. +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +.. code-block:: console + + [LightGBM] [Warning] Met negative value in categorical features, will convert it to NaN + [LightGBM] [Warning] There are no meaningful features, as all feature values are constant. + +The column you're trying to pass via ``categorical_feature`` likely contains very large values. +Categorical features in LightGBM are limited by int32 range, +so you cannot pass values that are greater than ``Int32.MaxValue`` (2147483647) as categorical features (see `lightgbm-org/LightGBM#1359 `__). +You should convert them to integers ranging from zero to the number of categories first. + +10. LightGBM crashes randomly with the error like: ``Initializing libiomp5.dylib, but found libomp.dylib already initialized.`` +------------------------------------------------------------------------------------------------------------------------------- + +.. code-block:: console + + OMP: Error #15: Initializing libiomp5.dylib, but found libomp.dylib already initialized. + OMP: Hint: This means that multiple copies of the OpenMP runtime have been linked into the program. That is dangerous, since it can degrade performance or cause incorrect results. The best thing to do is to ensure that only a single OpenMP runtime is linked into the process, e.g. by avoiding static linking of the OpenMP runtime in any library. As an unsafe, unsupported, undocumented workaround you can set the environment variable KMP_DUPLICATE_LIB_OK=TRUE to allow the program to continue to execute, but that may cause crashes or silently produce incorrect results. For more information, please see http://www.intel.com/software/products/support/. + +**Possible Cause**: This error means that you have multiple OpenMP libraries installed on your machine and they conflict with each other. +(File extensions in the error message may differ depending on the operating system). + +If you are using Python distributed by Conda, then it is highly likely that the error is caused by the ``numpy`` package from Conda which includes the ``mkl`` package which in turn conflicts with the system-wide library. +In this case you can update the ``numpy`` package in Conda or replace the Conda's OpenMP library instance with system-wide one by creating a symlink to it in Conda environment folder ``$CONDA_PREFIX/lib``. + +**Solution**: Assuming you are using macOS with Homebrew, the command which overwrites OpenMP library files in the current active Conda environment with symlinks to the system-wide library ones installed by Homebrew: + +.. code-block:: bash + + ln -sf `ls -d "$(brew --cellar libomp)"/*/lib`/* $CONDA_PREFIX/lib + +The described above fix worked fine before the release of OpenMP 8.0.0 version. +Starting from 8.0.0 version, Homebrew formula for OpenMP includes ``-DLIBOMP_INSTALL_ALIASES=OFF`` option which leads to that the fix doesn't work anymore. +However, you can create symlinks to library aliases manually: + +.. code-block:: bash + + for LIBOMP_ALIAS in libgomp.dylib libiomp5.dylib libomp.dylib; do sudo ln -sf "$(brew --cellar libomp)"/*/lib/libomp.dylib $CONDA_PREFIX/lib/$LIBOMP_ALIAS; done + +Another workaround would be removing MKL optimizations from Conda's packages completely: + +.. code-block:: bash + + conda install nomkl + +If this is not your case, then you should find conflicting OpenMP library installations on your own and leave only one of them. + +11. LightGBM hangs when multithreading (OpenMP) and using forking in Linux at the same time. +-------------------------------------------------------------------------------------------- + +Use ``nthreads=1`` to disable multithreading of LightGBM. There is a bug with OpenMP which hangs forked sessions +with multithreading activated. A more expensive solution is to use new processes instead of using fork, however, +keep in mind it is creating new processes where you have to copy memory and load libraries (example: if you want to +fork 16 times your current process, then you will require to make 16 copies of your dataset in memory) +(see `lightgbm-org/LightGBM#1789 `__). + +An alternative, if multithreading is really necessary inside the forked sessions, would be to compile LightGBM with +Intel toolchain. Intel compilers are unaffected by this bug. + +For C/C++ users, any OpenMP feature cannot be used before the fork happens. If an OpenMP feature is used before the +fork happens (example: using OpenMP for forking), OpenMP will hang inside the forked sessions. Use new processes instead +and copy memory as required by creating new processes instead of forking (or, use Intel compilers). + +Cloud platform container services may cause LightGBM to hang, if they use Linux fork to run multiple containers on a +single instance. For example, LightGBM hangs in AWS Batch array jobs, which `use the ECS agent +`__ to manage multiple running jobs. Setting ``nthreads=1`` mitigates the issue. + +12. Why is early stopping not enabled by default in LightGBM? +------------------------------------------------------------- + +Early stopping involves choosing a validation set, a special type of holdout which is used to evaluate the current state of the model after each iteration to see if training can stop. + +In ``LightGBM``, `we have decided to require that users specify this set directly <./Parameters.rst#valid>`_. Many options exist for splitting training data into training, test, and validation sets. + +The appropriate splitting strategy depends on the task and domain of the data, information that a modeler has but which ``LightGBM`` as a general-purpose tool does not. + +13. Does LightGBM support direct loading data from zero-based or one-based LibSVM format file? +---------------------------------------------------------------------------------------------- + +LightGBM supports loading data from zero-based LibSVM format file directly. + +14. Why CMake cannot find the compiler when compiling LightGBM with MinGW? +-------------------------------------------------------------------------- + +.. code-block:: bash + + CMake Error: CMAKE_C_COMPILER not set, after EnableLanguage + CMake Error: CMAKE_CXX_COMPILER not set, after EnableLanguage + +This is a known issue of CMake when using MinGW. The easiest solution is to run again your ``cmake`` command to bypass the one time stopper from CMake. Or you can upgrade your version of CMake to at least version 3.17.0. + +See `lightgbm-org/LightGBM#3060 `__ for more details. + +15. Where can I find LightGBM's logo to use it in my presentation? +------------------------------------------------------------------ + +You can find LightGBM's logo in different file formats and resolutions `here `__. + +16. LightGBM crashes randomly or operating system hangs during or after running LightGBM. +----------------------------------------------------------------------------------------- + +**Possible Cause**: This behavior may indicate that you have multiple OpenMP libraries installed on your machine and they conflict with each other, similarly to the ``FAQ #10``. + +If you are using any Python-package that depends on ``threadpoolctl``, you also may see the following warning in your logs in this case: + +.. code-block:: console + + /root/miniconda/envs/test-env/lib/python3.8/site-packages/threadpoolctl.py:546: RuntimeWarning: + Found Intel OpenMP ('libiomp') and LLVM OpenMP ('libomp') loaded at + the same time. Both libraries are known to be incompatible and this + can cause random crashes or deadlocks on Linux when loaded in the + same Python program. + Using threadpoolctl may cause crashes or deadlocks. For more + information and possible workarounds, please see + https://github.com/joblib/threadpoolctl/blob/master/multiple_openmp.md + +Detailed description of conflicts between multiple OpenMP instances is provided in the `following document `__. + +**Solution**: Assuming you are using LightGBM Python-package and conda as a package manager, we strongly recommend using ``conda-forge`` channel as the only source of all your Python package installations because it contains built-in patches to workaround OpenMP conflicts. Some other workarounds are listed `here `__ under the "Workarounds for Intel OpenMP and LLVM OpenMP case" section. + +If this is not your case, then you should find conflicting OpenMP library installations on your own and leave only one of them. + +17. Loading LightGBM fails like: ``cannot allocate memory in static TLS block`` +------------------------------------------------------------------------------- + +When loading LightGBM, you may encounter errors like the following. + +.. code-block:: console + + lib/libgomp.so.1: cannot allocate memory in static TLS block + +This most commonly happens on aarch64 Linux systems. + +``gcc``'s OpenMP library (``libgomp.so``) tries to allocate a small amount of static thread-local storage ("TLS") +when it's dynamically loaded. + +That error can happen when the loader isn't able to find a large enough block of memory. + +On aarch64 Linux, processes and loaded libraries share the same pool of static TLS, +which makes such failures more likely. See these discussions: + +* https://bugzilla.redhat.com/show_bug.cgi?id=1722181#c6 +* https://gcc.gcc.gnu.narkive.com/vOXMQqLA/failure-to-dlopen-libgomp-due-to-static-tls-data + +If you are experiencing this issue when using the ``lightgbm`` Python-package, try upgrading +to at least ``v4.6.0``. + +For older versions of the Python-package, or for other LightGBM APIs, this issue can +often be avoided by loading ``libgomp.so.1``. That can be done directly by setting environment +variable ``LD_PRELOAD``, like this: + +.. code-block:: console + + export LD_PRELOAD=/root/miniconda3/envs/test-env/lib/libgomp.so.1 + +It can also be done indirectly by changing the order that other libraries are loaded +into processes, which varies by programming language and application type. + +For more details, see these discussions: + +* https://github.com/lightgbm-org/LightGBM/pull/6654#issuecomment-2352014275 +* https://github.com/lightgbm-org/LightGBM/issues/6509 +* https://maskray.me/blog/2021-02-14-all-about-thread-local-storage +* https://bugzilla.redhat.com/show_bug.cgi?id=1722181#c6 + +------ + +R-package +========= + +.. contents:: + :local: + :backlinks: none + +1. Any training command using LightGBM does not work after an error occurred during the training of a previous LightGBM model. +------------------------------------------------------------------------------------------------------------------------------ + +In older versions of the R-package (prior to ``v3.3.0``), this could happen occasionally and the solution was to run ``lgb.unloader(wipe = TRUE)`` to remove all LightGBM-related objects. Some conversation about this could be found in `lightgbm-org/LightGBM#698 `__. + +That is no longer necessary as of ``v3.3.0``, and function ``lgb.unloader()`` has since been removed from the R-package. + +2. I used ``setinfo()``, tried to print my ``lgb.Dataset``, and now the R console froze! +---------------------------------------------------------------------------------------- + +As of at least LightGBM v3.3.0, this issue has been resolved and printing a ``Dataset`` object does not cause the console to freeze. + +In older versions, avoid printing the ``Dataset`` after calling ``setinfo()``. + +As of LightGBM v4.0.0, ``setinfo()`` has been replaced by a new method, ``set_field()``. + +3. ``error in data.table::data.table()...argument 2 is NULL``. +-------------------------------------------------------------- + +If you are experiencing this error when running ``lightgbm``, you may be facing the same issue reported in `#2715 `_ and later in `#2989 `_. We have seen that in some situations, using ``data.table`` 1.11.x results in this error. To get around this, you can upgrade your version of ``data.table`` to at least version 1.12.0. + +4. ``package/dependency ‘Matrix’ is not available ...`` +------------------------------------------------------- + +In April 2024, ``Matrix==1.7-0`` was published to CRAN. +That version had a floor of ``R (>=4.4.0)``. +``{Matrix}`` is a hard runtime dependency of ``{lightgbm}``, so on any version of R older than ``4.4.0``, running ``install.packages("lightgbm")`` results in something like the following. + +.. code-block:: text + + package ‘Matrix’ is not available for this version of R + +To fix that without upgrading to R 4.4.0 or greater, manually install an older version of ``{Matrix}``. + +.. code-block:: R + + install.packages('https://cran.r-project.org/src/contrib/Archive/Matrix/Matrix_1.6-5.tar.gz', repos = NULL) + +------ + +Python-package +============== + +.. contents:: + :local: + :backlinks: none + +1. ``Error: setup script specifies an absolute path`` when installing from GitHub using ``python setup.py install``. +-------------------------------------------------------------------------------------------------------------------- + +.. note:: + As of v4.0.0, ``lightgbm`` does not support directly invoking ``setup.py``. + This answer refers only to versions of ``lightgbm`` prior to v4.0.0. + +.. code-block:: console + + error: Error: setup script specifies an absolute path: + /Users/Microsoft/LightGBM/python-package/lightgbm/../../lib_lightgbm.so + setup() arguments must *always* be /-separated paths relative to the setup.py directory, *never* absolute paths. + +This error should be solved in latest version. +If you still meet this error, try to remove ``lightgbm.egg-info`` folder in your Python-package and reinstall, +or check `this thread on stackoverflow `__. + +2. Error messages: ``Cannot ... before construct dataset``. +----------------------------------------------------------- + +I see error messages like... + +.. code-block:: console + + Cannot get/set label/weight/init_score/group/num_data/num_feature before construct dataset + +but I've already constructed a dataset by some code like: + +.. code-block:: python + + train = lightgbm.Dataset(X_train, y_train) + +or error messages like + +.. code-block:: console + + Cannot set predictor/reference/categorical feature after freed raw data, set free_raw_data=False when construct Dataset to avoid this. + +**Solution**: Because LightGBM constructs bin mappers to build trees, and train and valid Datasets within one Booster share the same bin mappers, +categorical features and feature names etc., the Dataset objects are constructed when constructing a Booster. +If you set ``free_raw_data=True`` (default), the raw data (with Python data struct) will be freed. +So, if you want to: + +- get label (or weight/init\_score/group/data) before constructing a dataset, it's same as get ``self.label``; + +- set label (or weight/init\_score/group) before constructing a dataset, it's same as ``self.label=some_label_array``; + +- get num\_data (or num\_feature) before constructing a dataset, you can get data with ``self.data``. + Then, if your data is ``numpy.ndarray``, use some code like ``self.data.shape``. But do not do this after subsetting the Dataset, because you'll get always ``None``; + +- set predictor (or reference/categorical feature) after constructing a dataset, + you should set ``free_raw_data=False`` or init a Dataset object with the same raw data. + +3. I encounter segmentation faults (segfaults) randomly after installing LightGBM from PyPI using ``pip install lightgbm``. +--------------------------------------------------------------------------------------------------------------------------- + +We are doing our best to provide universal wheels which have high running speed and are compatible with any hardware, OS, compiler, etc. at the same time. +However, sometimes it's just impossible to guarantee the possibility of usage of LightGBM in any specific environment (see `lightgbm-org/LightGBM#1743 `__). + +Therefore, the first thing you should try in case of segfaults is **compiling from the source** using ``pip install --no-binary lightgbm lightgbm``. +For the OS-specific prerequisites see https://github.com/lightgbm-org/LightGBM/blob/main/python-package/README.rst. + +Also, feel free to post a new issue in our GitHub repository. We always look at each case individually and try to find a root cause. + +4. I would like to install LightGBM from conda. What channel should I choose? +----------------------------------------------------------------------------- + +We strongly recommend installation from the ``conda-forge`` channel and not from the ``default`` one. + +For some specific examples, see `this comment `__. + +In addition, as of ``lightgbm==4.4.0``, the ``conda-forge`` package automatically supports CUDA-based GPU acceleration. + +5. How do I subclass ``scikit-learn`` estimators? +------------------------------------------------- + +For ``lightgbm <= 4.5.0``, copy all of the constructor arguments from the corresponding +``lightgbm`` class into the constructor of your custom estimator. + +For later versions, just ensure that the constructor of your custom estimator calls ``super().__init__()``. + +Consider the example below, which implements a regressor that allows creation of truncated predictions. +This pattern will work with ``lightgbm > 4.5.0``. + +.. code-block:: python + + import numpy as np + from lightgbm import LGBMRegressor + from sklearn.datasets import make_regression + + class TruncatedRegressor(LGBMRegressor): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def predict(self, X, max_score: float = np.inf): + preds = super().predict(X) + np.clip(preds, a_min=None, a_max=max_score, out=preds) + return preds + + X, y = make_regression(n_samples=1_000, n_features=4) + + reg_trunc = TruncatedRegressor().fit(X, y) + + preds = reg_trunc.predict(X) + print(f"mean: {preds.mean():.2f}, max: {preds.max():.2f}") + # mean: -6.81, max: 345.10 + + preds_trunc = reg_trunc.predict(X, max_score=preds.mean()) + print(f"mean: {preds_trunc.mean():.2f}, max: {preds_trunc.max():.2f}") + # mean: -56.50, max: -6.81 diff --git a/docs/Features.rst b/docs/Features.rst new file mode 100644 index 0000000..5b20f0f --- /dev/null +++ b/docs/Features.rst @@ -0,0 +1,298 @@ +Features +======== + +This is a conceptual overview of how LightGBM works\ `[1] <#references>`__. We assume familiarity with decision tree boosting algorithms to focus instead on aspects of LightGBM that may differ from other boosting packages. For detailed algorithms, please refer to the citations or source code. + +Optimization in Speed and Memory Usage +-------------------------------------- + +Many boosting tools use pre-sort-based algorithms\ `[2, 3] <#references>`__ (e.g. default algorithm in xgboost) for decision tree learning. It is a simple solution, but not easy to optimize. + +LightGBM uses histogram-based algorithms\ `[4, 5, 6] <#references>`__, which bucket continuous feature (attribute) values into discrete bins. This speeds up training and reduces memory usage. Advantages of histogram-based algorithms include the following: + +- **Reduced cost of calculating the gain for each split** + + - Pre-sort-based algorithms have time complexity ``O(#data)`` + + - Computing the histogram has time complexity ``O(#data)``, but this involves only a fast sum-up operation. Once the histogram is constructed, a histogram-based algorithm has time complexity ``O(#bins)``, and ``#bins`` is far smaller than ``#data``. + +- **Use histogram subtraction for further speedup** + + - To get one leaf's histograms in a binary tree, use the histogram subtraction of its parent and its neighbor + + - So it needs to construct histograms for only one leaf (with smaller ``#data`` than its neighbor). It then can get histograms of its neighbor by histogram subtraction with small cost (``O(#bins)``) + +- **Reduce memory usage** + + - Replaces continuous values with discrete bins. If ``#bins`` is small, can use small data type, e.g. uint8\_t, to store training data + + - No need to store additional information for pre-sorting feature values + +- **Reduce communication cost for distributed learning** + +Sparse Optimization +------------------- + +- Need only ``O(2 * #non_zero_data)`` to construct histogram for sparse features + +Optimization in Accuracy +------------------------ + +Leaf-wise (Best-first) Tree Growth +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Most decision tree learning algorithms grow trees by level (depth)-wise, like the following image: + +.. image:: ./_static/images/level-wise.png + :align: center + :alt: A diagram depicting level wise tree growth in which the best possible node is split one level down. The strategy results in a symmetric tree, where every node in a level has child nodes resulting in an additional layer of depth. + +LightGBM grows trees leaf-wise (best-first)\ `[7] <#references>`__. It will choose the leaf with max delta loss to grow. +Holding ``#leaf`` fixed, leaf-wise algorithms tend to achieve lower loss than level-wise algorithms. + +Leaf-wise may cause over-fitting when ``#data`` is small, so LightGBM includes the ``max_depth`` parameter to limit tree depth. However, trees still grow leaf-wise even when ``max_depth`` is specified. + +.. image:: ./_static/images/leaf-wise.png + :align: center + :alt: A diagram depicting leaf wise tree growth in which only the node with the highest loss change is split and not bother with the rest of the nodes in the same level. This results in an asymmetrical tree where subsequent splitting is happening only on one side of the tree. + +Optimal Split for Categorical Features +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is common to represent categorical features with one-hot encoding, but this approach is suboptimal for tree learners. Particularly for high-cardinality categorical features, a tree built on one-hot features tends to be unbalanced and needs to grow very deep to achieve good accuracy. + +Instead of one-hot encoding, the optimal solution is to split on a categorical feature by partitioning its categories into 2 subsets. If the feature has ``k`` categories, there are ``2^(k-1) - 1`` possible partitions. +But there is an efficient solution for regression trees\ `[8] <#references>`__. It needs about ``O(k * log(k))`` to find the optimal partition. + +The basic idea is to sort the categories according to the training objective at each split. +More specifically, LightGBM sorts the histogram (for a categorical feature) according to its accumulated values (``sum_gradient / sum_hessian``) and then finds the best split on the sorted histogram. + +Optimization in Network Communication +------------------------------------- + +It only needs to use some collective communication algorithms, like "All reduce", "All gather" and "Reduce scatter", in distributed learning of LightGBM. +LightGBM implements state-of-the-art algorithms\ `[9] <#references>`__. +These collective communication algorithms can provide much better performance than point-to-point communication. + +.. _Optimization in Parallel Learning: + +Optimization in Distributed Learning +------------------------------------ + +LightGBM provides the following distributed learning algorithms. + +Feature Parallel +~~~~~~~~~~~~~~~~ + +Traditional Algorithm +^^^^^^^^^^^^^^^^^^^^^ + +Feature parallel aims to parallelize the "Find Best Split" in the decision tree. The procedure of traditional feature parallel is: + +1. Partition data vertically (different machines have different feature set). + +2. Workers find local best split point {feature, threshold} on local feature set. + +3. Communicate local best splits with each other and get the best one. + +4. Worker with best split to perform split, then send the split result of data to other workers. + +5. Other workers split data according to received data. + +The shortcomings of traditional feature parallel: + +- Has computation overhead, since it cannot speed up "split", whose time complexity is ``O(#data)``. + Thus, feature parallel cannot speed up well when ``#data`` is large. + +- Need communication of split result, which costs about ``O(#data / 8)`` (one bit for one data). + +Feature Parallel in LightGBM +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since feature parallel cannot speed up well when ``#data`` is large, we make a little change: instead of partitioning data vertically, every worker holds the full data. +Thus, LightGBM doesn't need to communicate for split result of data since every worker knows how to split data. +And ``#data`` won't be larger, so it is reasonable to hold the full data in every machine. + +The procedure of feature parallel in LightGBM: + +1. Workers find local best split point {feature, threshold} on local feature set. + +2. Communicate local best splits with each other and get the best one. + +3. Perform best split. + +However, this feature parallel algorithm still suffers from computation overhead for "split" when ``#data`` is large. +So it will be better to use data parallel when ``#data`` is large. + +Data Parallel +~~~~~~~~~~~~~ + +Traditional Algorithm +^^^^^^^^^^^^^^^^^^^^^ + +Data parallel aims to parallelize the whole decision learning. The procedure of data parallel is: + +1. Partition data horizontally. + +2. Workers use local data to construct local histograms. + +3. Merge global histograms from all local histograms. + +4. Find best split from merged global histograms, then perform splits. + +The shortcomings of traditional data parallel: + +- High communication cost. + If using point-to-point communication algorithm, communication cost for one machine is about ``O(#machine * #feature * #bin)``. + If using collective communication algorithm (e.g. "All Reduce"), communication cost is about ``O(2 * #feature * #bin)`` (check cost of "All Reduce" in chapter 4.5 at `[9] <#references>`__). + +Data Parallel in LightGBM +^^^^^^^^^^^^^^^^^^^^^^^^^ + +We reduce communication cost of data parallel in LightGBM: + +1. Instead of "Merge global histograms from all local histograms", LightGBM uses "Reduce Scatter" to merge histograms of different (non-overlapping) features for different workers. + Then workers find the local best split on local merged histograms and sync up the global best split. + +2. As aforementioned, LightGBM uses histogram subtraction to speed up training. + Based on this, we can communicate histograms only for one leaf, and get its neighbor's histograms by subtraction as well. + +All things considered, data parallel in LightGBM has time complexity ``O(0.5 * #feature * #bin)``. + +Voting Parallel +~~~~~~~~~~~~~~~ + +Voting parallel further reduces the communication cost in `Data Parallel <#data-parallel>`__ to constant cost. +It uses two-stage voting to reduce the communication cost of feature histograms\ `[10] <#references>`__. + +GPU Support +----------- + +Thanks `@huanzhang12 `__ for contributing this feature. Please read `[11] <#references>`__ to get more details. + +- `GPU Installation <./Installation-Guide.rst#build-gpu-version>`__ + +- `GPU Tutorial <./GPU-Tutorial.rst>`__ + +Applications and Metrics +------------------------ + +LightGBM supports the following applications: + +- regression, the objective function is L2 loss + +- binary classification, the objective function is logloss + +- multi classification + +- cross-entropy, the objective function is logloss and supports training on non-binary labels + +- LambdaRank, the objective function is LambdaRank with NDCG + +LightGBM supports the following metrics: + +- L1 loss + +- L2 loss + +- Log loss + +- Classification error rate + +- AUC + +- NDCG + +- MAP + +- Multi-class log loss + +- Multi-class error rate + +- AUC-mu ``(new in v3.0.0)`` + +- Average precision ``(new in v3.1.0)`` + +- Fair + +- Huber + +- Poisson + +- Quantile + +- MAPE + +- Kullback-Leibler + +- Gamma + +- Tweedie + +For more details, please refer to `Parameters <./Parameters.rst#metric-parameters>`__. + +Other Features +-------------- + +- Limit ``max_depth`` of tree while grows tree leaf-wise + +- `DART `__ + +- L1/L2 regularization + +- Bagging + +- Column (feature) sub-sample + +- Continued train with input GBDT model + +- Continued train with the input score file + +- Weighted training + +- Validation metric output during training + +- Multiple validation data + +- Multiple metrics + +- Early stopping (both training and prediction) + +- Prediction for leaf index + +For more details, please refer to `Parameters <./Parameters.rst>`__. + +References +---------- + +[1] Guolin Ke, Qi Meng, Thomas Finley, Taifeng Wang, Wei Chen, Weidong Ma, Qiwei Ye, Tie-Yan Liu. "`LightGBM\: A Highly Efficient Gradient Boosting Decision Tree`_." Advances in Neural Information Processing Systems 30 (NIPS 2017), pp. 3149-3157. + +[2] Mehta, Manish, Rakesh Agrawal, and Jorma Rissanen. "SLIQ: A fast scalable classifier for data mining." International Conference on Extending Database Technology. Springer Berlin Heidelberg, 1996. + +[3] Shafer, John, Rakesh Agrawal, and Manish Mehta. "SPRINT: A scalable parallel classifier for data mining." Proc. 1996 Int. Conf. Very Large Data Bases. 1996. + +[4] Ranka, Sanjay, and V. Singh. "CLOUDS: A decision tree classifier for large datasets." Proceedings of the 4th Knowledge Discovery and Data Mining Conference. 1998. + +[5] Machado, F. P. "Communication and memory efficient parallel decision tree construction." (2003). + +[6] Li, Ping, Qiang Wu, and Christopher J. Burges. "Mcrank: Learning to rank using multiple classification and gradient boosting." Advances in Neural Information Processing Systems 20 (NIPS 2007). + +[7] Shi, Haijian. "Best-first decision tree learning." Diss. The University of Waikato, 2007. + +[8] Walter D. Fisher. "`On Grouping for Maximum Homogeneity`_." Journal of the American Statistical Association. Vol. 53, No. 284 (Dec., 1958), pp. 789-798. + +[9] Thakur, Rajeev, Rolf Rabenseifner, and William Gropp. "`Optimization of collective communication operations in MPICH`_." International Journal of High Performance Computing Applications 19.1 (2005), pp. 49-66. + +[10] Qi Meng, Guolin Ke, Taifeng Wang, Wei Chen, Qiwei Ye, Zhi-Ming Ma, Tie-Yan Liu. "`A Communication-Efficient Parallel Algorithm for Decision Tree`_." Advances in Neural Information Processing Systems 29 (NIPS 2016), pp. 1279-1287. + +[11] Huan Zhang, Si Si and Cho-Jui Hsieh. "`GPU Acceleration for Large-scale Tree Boosting`_." SysML Conference, 2018. + +.. _LightGBM\: A Highly Efficient Gradient Boosting Decision Tree: https://proceedings.neurips.cc/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html + +.. _On Grouping for Maximum Homogeneity: https://www.jstor.org/stable/2281952 + +.. _Optimization of collective communication operations in MPICH: https://www.mpich.org/2012/10/24/optimization-of-collective-communication-operations-in-mpich/ + +.. _A Communication-Efficient Parallel Algorithm for Decision Tree: https://proceedings.neurips.cc/paper/2016/hash/10a5ab2db37feedfdeaab192ead4ac0e-Abstract.html + +.. _GPU Acceleration for Large-scale Tree Boosting: https://arxiv.org/abs/1706.08359 diff --git a/docs/GPU-Performance.rst b/docs/GPU-Performance.rst new file mode 100644 index 0000000..1ece191 --- /dev/null +++ b/docs/GPU-Performance.rst @@ -0,0 +1,211 @@ +GPU Tuning Guide and Performance Comparison +=========================================== + +How It Works? +------------- + +In LightGBM, the main computation cost during training is building the feature histograms. We use an efficient algorithm on GPU to accelerate this process. +The implementation is highly modular, and works for all learning tasks (classification, ranking, regression, etc). GPU acceleration also works in distributed learning settings. +GPU algorithm implementation is based on OpenCL and can work with a wide range of GPUs. + +Supported Hardware +------------------ + +We target AMD Graphics Core Next (GCN) architecture and NVIDIA Maxwell and Pascal architectures. +Most AMD GPUs released after 2012 and NVIDIA GPUs released after 2014 should be supported. We have tested the GPU implementation on the following GPUs: + +- AMD RX 480 with AMDGPU-pro driver 16.60 on Ubuntu 16.10 + +- AMD R9 280X (aka Radeon HD 7970) with fglrx driver 15.302.2301 on Ubuntu 16.10 + +- NVIDIA GTX 1080 with driver 375.39 and CUDA 8.0 on Ubuntu 16.10 + +- NVIDIA Titan X (Pascal) with driver 367.48 and CUDA 8.0 on Ubuntu 16.04 + +- NVIDIA Tesla M40 with driver 375.39 and CUDA 7.5 on Ubuntu 16.04 + +Using the following hardware is discouraged: + +- NVIDIA Kepler (K80, K40, K20, most GeForce GTX 700 series GPUs) or earlier NVIDIA GPUs. They don't support hardware atomic operations in local memory space and thus histogram construction will be slow. + +- AMD VLIW4-based GPUs, including Radeon HD 6xxx series and earlier GPUs. These GPUs have been discontinued for years and are rarely seen nowadays. + +How to Achieve Good Speedup on GPU +---------------------------------- + +#. You want to run a few datasets that we have verified with good speedup (including Higgs, epsilon, Bosch, etc) to ensure your setup is correct. + If you have multiple GPUs, make sure to set ``gpu_platform_id`` and ``gpu_device_id`` to use the desired GPU. + Also make sure your system is idle (especially when using a shared computer) to get accuracy performance measurements. + +#. GPU works best on large scale and dense datasets. If dataset is too small, computing it on GPU is inefficient as the data transfer overhead can be significant. + If you have categorical features, use the ``categorical_column`` option and input them into LightGBM directly; do not convert them into one-hot variables. + +#. To get good speedup with GPU, it is suggested to use a smaller number of bins. + Setting ``max_bin=63`` is recommended, as it usually does not noticeably affect training accuracy on large datasets, but GPU training can be significantly faster than using the default bin size of 255. + For some dataset, even using 15 bins is enough (``max_bin=15``); using 15 bins will maximize GPU performance. Make sure to check the run log and verify that the desired number of bins is used. + +#. Try to use single precision training (``gpu_use_dp=false``) when possible, because most GPUs (especially NVIDIA consumer GPUs) have poor double-precision performance. + +Performance Comparison +---------------------- + +We evaluate the training performance of GPU acceleration on the following datasets: + ++-----------+----------------+----------+------------+-----------+------------+ +| Data | Task | Link | #Examples | #Features | Comments | ++===========+================+==========+============+===========+============+ +| Higgs | Binary | `link1`_ | 10,500,000 | 28 | use last | +| | classification | | | | 500,000 | +| | | | | | samples | +| | | | | | as test | +| | | | | | set | ++-----------+----------------+----------+------------+-----------+------------+ +| Epsilon | Binary | `link2`_ | 400,000 | 2,000 | use the | +| | classification | | | | provided | +| | | | | | test set | ++-----------+----------------+----------+------------+-----------+------------+ +| Bosch | Binary | `link3`_ | 1,000,000 | 968 | use the | +| | classification | | | | provided | +| | | | | | test set | ++-----------+----------------+----------+------------+-----------+------------+ +| Yahoo LTR | Learning to | `link4`_ | 473,134 | 700 | set1.train | +| | rank | | | | as train, | +| | | | | | set1.test | +| | | | | | as test | ++-----------+----------------+----------+------------+-----------+------------+ +| MS LTR | Learning to | `link5`_ | 2,270,296 | 137 | {S1,S2,S3} | +| | rank | | | | as train | +| | | | | | set, {S5} | +| | | | | | as test | +| | | | | | set | ++-----------+----------------+----------+------------+-----------+------------+ +| Expo | Binary | `link6`_ | 11,000,000 | 700 | use last | +| | classification | | | | 1,000,000 | +| | (Categorical) | | | | as test | +| | | | | | set | ++-----------+----------------+----------+------------+-----------+------------+ + +We used the following hardware to evaluate the performance of LightGBM GPU training. +Our CPU reference is **a high-end dual socket Haswell-EP Xeon server with 28 cores**; +GPUs include a budget GPU (RX 480) and a mainstream (GTX 1080) GPU installed on the same server. +It is worth mentioning that **the GPUs used are not the best GPUs in the market**; +if you are using a better GPU (like AMD RX 580, NVIDIA GTX 1080 Ti, Titan X Pascal, Titan Xp, Tesla P100, etc), you are likely to get a better speedup. + ++--------------------------------+----------------+------------------+---------------+ +| Hardware | Peak FLOPS | Peak Memory BW | Cost (MSRP) | ++================================+================+==================+===============+ +| AMD Radeon RX 480 | 5,161 GFLOPS | 256 GB/s | $199 | ++--------------------------------+----------------+------------------+---------------+ +| NVIDIA GTX 1080 | 8,228 GFLOPS | 320 GB/s | $499 | ++--------------------------------+----------------+------------------+---------------+ +| 2x Xeon E5-2683v3 (28 cores) | 1,792 GFLOPS | 133 GB/s | $3,692 | ++--------------------------------+----------------+------------------+---------------+ + +During benchmarking on CPU we used only 28 physical cores of the CPU, and did not use hyper-threading cores, +because we found that using too many threads actually makes performance worse. +The following shows the training configuration we used: + +:: + + max_bin = 63 + num_leaves = 255 + num_iterations = 500 + learning_rate = 0.1 + tree_learner = serial + task = train + is_training_metric = false + min_data_in_leaf = 1 + min_sum_hessian_in_leaf = 100 + ndcg_eval_at = 1,3,5,10 + device = gpu + gpu_platform_id = 0 + gpu_device_id = 0 + num_thread = 28 + +We use the configuration shown above, except for the Bosch dataset, we use a smaller ``learning_rate=0.015`` and set ``min_sum_hessian_in_leaf=5``. +For all GPU training we vary the max number of bins (255, 63 and 15). +The GPU implementation is from commit `0bb4a82`_ of LightGBM, when the GPU support was just merged in. + +The following table lists the accuracy on test set that CPU and GPU learner can achieve after 500 iterations. +GPU with the same number of bins can achieve a similar level of accuracy as on the CPU, despite using single precision arithmetic. +For most datasets, using 63 bins is sufficient. + ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| | CPU 255 bins | CPU 63 bins | CPU 15 bins | GPU 255 bins | GPU 63 bins | GPU 15 bins | ++===========================+================+===============+===============+================+===============+===============+ +| Higgs AUC | 0.845612 | 0.845239 | 0.841066 | 0.845612 | 0.845209 | 0.840748 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| Epsilon AUC | 0.950243 | 0.949952 | 0.948365 | 0.950057 | 0.949876 | 0.948365 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| Yahoo-LTR NDCG\ :sub:`1` | 0.730824 | 0.730165 | 0.729647 | 0.730936 | 0.732257 | 0.73114 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| Yahoo-LTR NDCG\ :sub:`3` | 0.738687 | 0.737243 | 0.736445 | 0.73698 | 0.739474 | 0.735868 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| Yahoo-LTR NDCG\ :sub:`5` | 0.756609 | 0.755729 | 0.754607 | 0.756206 | 0.757007 | 0.754203 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| Yahoo-LTR NDCG\ :sub:`10` | 0.79655 | 0.795827 | 0.795273 | 0.795894 | 0.797302 | 0.795584 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| Expo AUC | 0.776217 | 0.771566 | 0.743329 | 0.776285 | 0.77098 | 0.744078 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| MS-LTR NDCG\ :sub:`1` | 0.521265 | 0.521392 | 0.518653 | 0.521789 | 0.522163 | 0.516388 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| MS-LTR NDCG\ :sub:`3` | 0.503153 | 0.505753 | 0.501697 | 0.503886 | 0.504089 | 0.501691 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| MS-LTR NDCG\ :sub:`5` | 0.509236 | 0.510391 | 0.507193 | 0.509861 | 0.510095 | 0.50663 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| MS-LTR NDCG\ :sub:`10` | 0.527835 | 0.527304 | 0.524603 | 0.528009 | 0.527059 | 0.524722 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ +| Bosch AUC | 0.718115 | 0.721791 | 0.716677 | 0.717184 | 0.724761 | 0.717005 | ++---------------------------+----------------+---------------+---------------+----------------+---------------+---------------+ + +We record the wall clock time after 500 iterations, as shown in the figure below: + +.. image:: ./_static/images/gpu-performance-comparison.png + :align: center + :target: ./_static/images/gpu-performance-comparison.png + :alt: A performance chart which is a record of the wall clock time after 500 iterations on G P U for Higgs, epsilon, Bosch, Microsoft L T R, Expo and Yahoo L T R and bin size of 63 performs comparatively better. + +When using a GPU, it is advisable to use a bin size of 63 rather than 255, because it can speed up training significantly without noticeably affecting accuracy. +On CPU, using a smaller bin size only marginally improves performance, sometimes even slows down training, +like in Higgs (we can reproduce the same slowdown on two different machines, with different GCC versions). +We found that GPU can achieve impressive acceleration on large and dense datasets like Higgs and Epsilon. +Even on smaller and sparse datasets, a *budget* GPU can still compete and be faster than a 28-core Haswell server. + +Memory Usage +------------ + +The next table shows GPU memory usage reported by ``nvidia-smi`` during training with 63 bins. +We can see that even the largest dataset just uses about 1 GB of GPU memory, +indicating that our GPU implementation can scale to huge datasets over 10x larger than Bosch or Epsilon. +Also, we can observe that generally a larger dataset (using more GPU memory, like Epsilon or Bosch) has better speedup, +because the overhead of invoking GPU functions becomes significant when the dataset is small. + ++-------------------------+---------+-----------+---------+----------+--------+-------------+ +| Datasets | Higgs | Epsilon | Bosch | MS-LTR | Expo | Yahoo-LTR | ++=========================+=========+===========+=========+==========+========+=============+ +| GPU Memory Usage (MB) | 611 | 901 | 1067 | 413 | 405 | 291 | ++-------------------------+---------+-----------+---------+----------+--------+-------------+ + +Further Reading +--------------- + +You can find more details about the GPU algorithm and benchmarks in the +following article: + +Huan Zhang, Si Si and Cho-Jui Hsieh. `GPU Acceleration for Large-scale Tree Boosting`_. SysML Conference, 2018. + +.. _link1: https://archive.ics.uci.edu/dataset/280/higgs + +.. _link2: https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary.html + +.. _link3: https://www.kaggle.com/c/bosch-production-line-performance/data + +.. _link4: https://proceedings.mlr.press/v14/chapelle11a.html + +.. _link5: https://www.microsoft.com/en-us/research/project/mslr/ + +.. _link6: https://community.amstat.org/jointscsg-section/dataexpo/dataexpo2009 + +.. _0bb4a82: https://github.com/lightgbm-org/LightGBM/commit/0bb4a82 + +.. _GPU Acceleration for Large-scale Tree Boosting: https://arxiv.org/abs/1706.08359 diff --git a/docs/GPU-Targets.rst b/docs/GPU-Targets.rst new file mode 100644 index 0000000..973575c --- /dev/null +++ b/docs/GPU-Targets.rst @@ -0,0 +1,174 @@ +GPU SDK Correspondence and Device Targeting Table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +GPU Targets Table +================= + +OpenCL is a universal massively parallel programming framework that targets multiple backends (GPU, CPU, FPGA, etc). +Basically, to use a device from a vendor, you have to install drivers from that specific vendor. +Intel's and AMD's OpenCL runtime also include x86 CPU target support. +NVIDIA's OpenCL runtime only supports NVIDIA GPU (no CPU support). +In general, OpenCL CPU backends are quite slow, and should be used for testing and debugging only. + +You can find below a table of correspondence: + ++---------------------------+-----------------+-----------------+-----------------+--------------+ +| SDK | CPU Intel/AMD | GPU Intel | GPU AMD | GPU NVIDIA | ++===========================+=================+=================+=================+==============+ +| `Intel SDK for OpenCL`_ | Supported | Supported | Not Supported | Not Supported| ++---------------------------+-----------------+-----------------+-----------------+--------------+ +| AMD APP SDK \* | Supported | Not Supported | Supported | Not Supported| ++---------------------------+-----------------+-----------------+-----------------+--------------+ +| `PoCL`_ | Supported | Not Supported | Supported | Not Supported| ++---------------------------+-----------------+-----------------+-----------------+--------------+ +| `NVIDIA CUDA Toolkit`_ | Not Supported | Not Supported | Not Supported | Supported | ++---------------------------+-----------------+-----------------+-----------------+--------------+ + +Legend: + +\* AMD APP SDK is deprecated. On Windows, OpenCL is included in AMD graphics driver. On Linux, newer generation AMD cards are supported by the `ROCm`_ driver. You can download an archived copy of AMD APP SDK from our GitHub repo (`for Linux`_ and `for Windows`_). + + +-------------- + +Query OpenCL Devices in Your System +=================================== + +Your system might have multiple GPUs from different vendors ("platforms") installed. Setting up LightGBM GPU device requires two parameters: `OpenCL Platform ID <./Parameters.rst#gpu_platform_id>`__ (``gpu_platform_id``) and `OpenCL Device ID <./Parameters.rst#gpu_device_id>`__ (``gpu_device_id``). Generally speaking, each vendor provides an OpenCL platform, and devices from the same vendor have different device IDs under that platform. For example, if your system has an Intel integrated GPU and two discrete GPUs from AMD, you will have two OpenCL platforms (with ``gpu_platform_id=0`` and ``gpu_platform_id=1``). If the platform 0 is Intel, it has one device (``gpu_device_id=0``) representing the Intel GPU; if the platform 1 is AMD, it has two devices (``gpu_device_id=0``, ``gpu_device_id=1``) representing the two AMD GPUs. If you have a discrete GPU by AMD/NVIDIA and an integrated GPU by Intel, make sure to select the correct ``gpu_platform_id`` to use the discrete GPU as it usually provides better performance. + +On Windows, OpenCL devices can be queried using `GPUCapsViewer`_, under the OpenCL tab. Note that the platform and device IDs reported by this utility start from 1. So you should minus the reported IDs by 1. + +On Linux, OpenCL devices can be listed using the ``clinfo`` command. On Ubuntu, you can install ``clinfo`` by executing ``sudo apt-get install clinfo``. + + +Examples +=============== + +We provide test R code below, but you can use the language of your choice with the examples of your choices: + +.. code:: r + + library(lightgbm) + data(agaricus.train, package = "lightgbm") + train <- agaricus.train + train$data[, 1] <- 1:6513 + dtrain <- lgb.Dataset(train$data, label = train$label) + data(agaricus.test, package = "lightgbm") + test <- agaricus.test + dtest <- lgb.Dataset.create.valid(dtrain, test$data, label = test$label) + valids <- list(test = dtest) + + params <- list(objective = "regression", + metric = "rmse", + device = "gpu", + gpu_platform_id = 0, + gpu_device_id = 0, + nthread = 1, + boost_from_average = FALSE, + num_tree_per_iteration = 10, + max_bin = 32) + model <- lgb.train(params, + dtrain, + 2, + valids, + min_data = 1, + learning_rate = 1, + early_stopping_rounds = 10) + +Make sure you list the OpenCL devices in your system and set ``gpu_platform_id`` and ``gpu_device_id`` correctly. In the following examples, our system has 1 GPU platform (``gpu_platform_id = 0``) from AMD APP SDK. The first device ``gpu_device_id = 0`` is a GPU device (AMD Oland), and the second device ``gpu_device_id = 1`` is the x86 CPU backend. + +Example of using GPU (``gpu_platform_id = 0`` and ``gpu_device_id = 0`` in our system): + +.. code:: r + + > params <- list(objective = "regression", + + metric = "rmse", + + device = "gpu", + + gpu_platform_id = 0, + + gpu_device_id = 0, + + nthread = 1, + + boost_from_average = FALSE, + + num_tree_per_iteration = 10, + + max_bin = 32) + > model <- lgb.train(params, + + dtrain, + + 2, + + valids, + + min_data = 1, + + learning_rate = 1, + + early_stopping_rounds = 10) + [LightGBM] [Info] This is the GPU trainer!! + [LightGBM] [Info] Total Bins 232 + [LightGBM] [Info] Number of data: 6513, number of used features: 116 + [LightGBM] [Info] Using GPU Device: Oland, Vendor: Advanced Micro Devices, Inc. + [LightGBM] [Info] Compiling OpenCL Kernel with 16 bins... + [LightGBM] [Info] GPU programs have been built + [LightGBM] [Info] Size of histogram bin entry: 12 + [LightGBM] [Info] 40 dense feature groups (0.12 MB) transferred to GPU in 0.004211 secs. 76 sparse feature groups. + [LightGBM] [Info] No further splits with positive gain, best gain: -inf + [LightGBM] [Info] Trained a tree with leaves=16 and depth=8 + [1]: test's rmse:1.10643e-17 + [LightGBM] [Info] No further splits with positive gain, best gain: -inf + [LightGBM] [Info] Trained a tree with leaves=7 and depth=5 + [2]: test's rmse:0 + +Running on OpenCL CPU backend devices is in generally slow, and we observe crashes on some Windows and macOS systems. Make sure you check the ``Using GPU Device`` line in the log and it is not using a CPU. The above log shows that we are using ``Oland`` GPU from AMD and not CPU. + +Example of using CPU (``gpu_platform_id = 0``, ``gpu_device_id = 1``). The GPU device reported is ``Intel(R) Core(TM) i7-4600U CPU``, so it is using the CPU backend rather than a real GPU. + +.. code:: r + + > params <- list(objective = "regression", + + metric = "rmse", + + device = "gpu", + + gpu_platform_id = 0, + + gpu_device_id = 1, + + nthread = 1, + + boost_from_average = FALSE, + + num_tree_per_iteration = 10, + + max_bin = 32) + > model <- lgb.train(params, + + dtrain, + + 2, + + valids, + + min_data = 1, + + learning_rate = 1, + + early_stopping_rounds = 10) + [LightGBM] [Info] This is the GPU trainer!! + [LightGBM] [Info] Total Bins 232 + [LightGBM] [Info] Number of data: 6513, number of used features: 116 + [LightGBM] [Info] Using requested OpenCL platform 0 device 1 + [LightGBM] [Info] Using GPU Device: Intel(R) Core(TM) i7-4600U CPU @ 2.10GHz, Vendor: GenuineIntel + [LightGBM] [Info] Compiling OpenCL Kernel with 16 bins... + [LightGBM] [Info] GPU programs have been built + [LightGBM] [Info] Size of histogram bin entry: 12 + [LightGBM] [Info] 40 dense feature groups (0.12 MB) transferred to GPU in 0.004540 secs. 76 sparse feature groups. + [LightGBM] [Info] No further splits with positive gain, best gain: -inf + [LightGBM] [Info] Trained a tree with leaves=16 and depth=8 + [1]: test's rmse:1.10643e-17 + [LightGBM] [Info] No further splits with positive gain, best gain: -inf + [LightGBM] [Info] Trained a tree with leaves=7 and depth=5 + [2]: test's rmse:0 + + +Known issues: + +- Using a bad combination of ``gpu_platform_id`` and ``gpu_device_id`` can potentially lead to a **crash** due to OpenCL driver issues on some machines (you will lose your entire session content). Beware of it. + +- On some systems, if you have integrated graphics card (Intel HD Graphics) and a dedicated graphics card (AMD, NVIDIA), the dedicated graphics card will automatically override the integrated graphics card. The workaround is to disable your dedicated graphics card to be able to use your integrated graphics card. + +.. _Intel SDK for OpenCL: https://software.intel.com/en-us/articles/opencl-drivers + +.. _ROCm: https://rocmdocs.amd.com/en/latest/ + +.. _for Linux: https://github.com/lightgbm-org/LightGBM/releases/download/v2.0.12/AMD-APP-SDKInstaller-v3.0.130.136-GA-linux64.tar.bz2 + +.. _for Windows: https://github.com/lightgbm-org/LightGBM/releases/download/v2.0.12/AMD-APP-SDKInstaller-v3.0.130.135-GA-windows-F-x64.exe + +.. _NVIDIA CUDA Toolkit: https://developer.nvidia.com/cuda-downloads + +.. _clinfo: https://github.com/Oblomov/clinfo + +.. _GPUCapsViewer: https://www.ozone3d.net/gpu_caps_viewer/ + +.. _PoCL: https://portablecl.org/ diff --git a/docs/GPU-Tutorial.rst b/docs/GPU-Tutorial.rst new file mode 100644 index 0000000..4749c1e --- /dev/null +++ b/docs/GPU-Tutorial.rst @@ -0,0 +1,192 @@ +LightGBM GPU Tutorial +===================== + +The purpose of this document is to give you a quick step-by-step tutorial on GPU training. + +We will use the GPU instance on `Microsoft Azure cloud computing platform`_ for demonstration, +but you can use any machine with modern AMD or NVIDIA GPUs. + +GPU Setup +--------- + +You need to launch a ``NV`` type instance on Azure (available in East US, North Central US, South Central US, West Europe and Southeast Asia zones) +and select Ubuntu 16.04 LTS as the operating system. + +For testing, the smallest ``NV6`` type virtual machine is sufficient, which includes 1/2 M60 GPU, with 8 GB memory, 180 GB/s memory bandwidth and 4,825 GFLOPS peak computation power. +Don't use the ``NC`` type instance as the GPUs (K80) are based on an older architecture (Kepler). + +First we need to install minimal NVIDIA drivers and OpenCL development environment: + +:: + + sudo apt-get update + sudo apt-get install --no-install-recommends nvidia-375 + sudo apt-get install --no-install-recommends nvidia-opencl-icd-375 nvidia-opencl-dev opencl-headers + +After installing the drivers you need to restart the server. + +:: + + sudo init 6 + +After about 30 seconds, the server should be up again. + +If you are using an AMD GPU, you should download and install the `AMDGPU-Pro`_ driver and also install packages ``ocl-icd-libopencl1`` and ``ocl-icd-opencl-dev``. + +Build LightGBM +-------------- + +Now install necessary building tools and dependencies: + +:: + + sudo apt-get install --no-install-recommends git cmake build-essential libboost-dev libboost-system-dev libboost-filesystem-dev + +The ``NV6`` GPU instance has a 320 GB ultra-fast SSD mounted at ``/mnt``. +Let's use it as our workspace (skip this if you are using your own machine): + +:: + + sudo mkdir -p /mnt/workspace + sudo chown $(whoami):$(whoami) /mnt/workspace + cd /mnt/workspace + +Now we are ready to checkout LightGBM and compile it with GPU support: + +:: + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_GPU=1 +   # if you have installed NVIDIA CUDA to a customized location, you should specify paths to OpenCL headers and library like the following: + # cmake -B build -S . -DUSE_GPU=1 -DOpenCL_LIBRARY=/usr/local/cuda/lib64/libOpenCL.so -DOpenCL_INCLUDE_DIR=/usr/local/cuda/include/ + cmake --build build -j$(nproc) + +You will see two binaries are generated, ``lightgbm`` and ``lib_lightgbm.so``. + +If you are building on macOS, you probably need to remove macro ``BOOST_COMPUTE_USE_OFFLINE_CACHE`` in ``src/treelearner/gpu_tree_learner.h`` to avoid a known crash bug in Boost.Compute. + +Install Python Interface (optional) +----------------------------------- + +If you want to use the Python interface of LightGBM, you can install it now (along with some necessary Python-package dependencies): + +:: + + sudo apt-get -y install python3-pip python3-venv + sudo -H pip install numpy scipy scikit-learn -U + sudo sh ./build-python.sh install --precompile + +You need to set an additional parameter ``"device" : "gpu"`` (along with your other options like ``learning_rate``, ``num_leaves``, etc) to use GPU in Python. + +You can read our `Python-package Examples`_ for more information on how to use the Python interface. + +Dataset Preparation +------------------- + +Using the following commands to prepare the Higgs dataset: + +:: + + git clone https://github.com/guolinke/boosting_tree_benchmarks.git + cd boosting_tree_benchmarks/data + wget "https://archive.ics.uci.edu/ml/machine-learning-databases/00280/HIGGS.csv.gz" + gunzip HIGGS.csv.gz + python higgs2libsvm.py + cd ../.. + ln -s boosting_tree_benchmarks/data/higgs.train + ln -s boosting_tree_benchmarks/data/higgs.test + +Now we create a configuration file for LightGBM by running the following commands (please copy the entire block and run it as a whole): + +:: + + cat > lightgbm_gpu.conf <> lightgbm_gpu.conf + +GPU is enabled in the configuration file we just created by setting ``device=gpu``. +In this configuration we use the first GPU installed on the system (``gpu_platform_id=0`` and ``gpu_device_id=0``). If ``gpu_platform_id`` or ``gpu_device_id`` is not set, the default platform and GPU will be selected. +You might have multiple platforms (AMD/Intel/NVIDIA) or GPUs. You can use the `clinfo`_ utility to identify the GPUs on each platform. On Ubuntu, you can install ``clinfo`` by executing ``sudo apt-get install clinfo``. If you have a discrete GPU by AMD/NVIDIA and an integrated GPU by Intel, make sure to select the correct ``gpu_platform_id`` to use the discrete GPU. + +Run Your First Learning Task on GPU +----------------------------------- + +Now we are ready to start GPU training! + +First we want to verify the GPU works correctly. +Run the following command to train on GPU, and take a note of the AUC after 50 iterations: + +:: + + ./lightgbm config=lightgbm_gpu.conf data=higgs.train valid=higgs.test objective=binary metric=auc + +Now train the same dataset on CPU using the following command. You should observe a similar AUC: + +:: + + ./lightgbm config=lightgbm_gpu.conf data=higgs.train valid=higgs.test objective=binary metric=auc device=cpu + +Now we can make a speed test on GPU without calculating AUC after each iteration. + +:: + + ./lightgbm config=lightgbm_gpu.conf data=higgs.train objective=binary metric=auc + +Speed test on CPU: + +:: + + ./lightgbm config=lightgbm_gpu.conf data=higgs.train objective=binary metric=auc device=cpu + +You should observe over three times speedup on this GPU. + +The GPU acceleration can be used on other tasks/metrics (regression, multi-class classification, ranking, etc) as well. +For example, we can train the Higgs dataset on GPU as a regression task: + +:: + + ./lightgbm config=lightgbm_gpu.conf data=higgs.train objective=regression_l2 metric=l2 + +Also, you can compare the training speed with CPU: + +:: + + ./lightgbm config=lightgbm_gpu.conf data=higgs.train objective=regression_l2 metric=l2 device=cpu + +Further Reading +--------------- + +- `GPU Tuning Guide and Performance Comparison <./GPU-Performance.rst>`__ + +- `GPU SDK Correspondence and Device Targeting Table <./GPU-Targets.rst>`__ + +Reference +--------- + +Please kindly cite the following article in your publications if you find the GPU acceleration useful: + +Huan Zhang, Si Si and Cho-Jui Hsieh. "`GPU Acceleration for Large-scale Tree Boosting`_." SysML Conference, 2018. + +.. _Microsoft Azure cloud computing platform: https://azure.microsoft.com/ + +.. _AMDGPU-Pro: https://www.amd.com/en/support.html + +.. _Python-package Examples: https://github.com/lightgbm-org/LightGBM/tree/main/examples/python-guide + +.. _GPU Acceleration for Large-scale Tree Boosting: https://arxiv.org/abs/1706.08359 + +.. _clinfo: https://github.com/Oblomov/clinfo diff --git a/docs/GPU-Windows.rst b/docs/GPU-Windows.rst new file mode 100644 index 0000000..283b7f3 --- /dev/null +++ b/docs/GPU-Windows.rst @@ -0,0 +1,3 @@ +The content of this document was very outdated and is no longer available to avoid any misleadings. + +Starting from the ``3.2.0`` version LightGBM Python packages have been having built-in support of training on GPU devices. diff --git a/docs/Installation-Guide.rst b/docs/Installation-Guide.rst new file mode 100644 index 0000000..7dfe61a --- /dev/null +++ b/docs/Installation-Guide.rst @@ -0,0 +1,1143 @@ +Installation Guide +================== + +Versioning +~~~~~~~~~~ + +LightGBM releases use a 3-part version number, with this format: + +.. code:: + + {major}.{minor}.{patch} + +This version follows a scheme called Intended Effort Versioning ("Effver" for short). +Changes to a component of the version indicate how much effort it will likely take to update +code using a previous version. + +* ``major`` = updating will require significant effort +* ``minor`` = some effort +* ``patch`` = no or very little effort + +This means that **new minor versions can contain breaking changes**, but these are typically +small or limited to less-frequently-used parts of the project. + +For more details on why LightGBM uses EffVer instead of other schemes like semantic versioning, +see https://jacobtomlinson.dev/effver/. + +Nightly Packages +~~~~~~~~~~~~~~~~ + +When built from source on an unreleased commit, the package version takes the following form: + +.. code:: + + {major}.{minor}.{patch}.99 + +That ``.99`` is added to ensure that a version built from an unreleased commit is considered "newer" +than all previous releases, and "older" than all future releases. + +.. _nightly-builds: + +To download such artifacts, run the following from the root of this repository. + +.. code:: sh + + bash .ci/download-artifacts.sh ${COMMIT_ID} + +Where ``COMMIT_ID`` is the full commit SHA pointing to a commit on ``main``. +The artifacts can then be found in the ``release-artifacts/`` directory. + +For the Python package, nightly packages are also available via installers like ``pip``. +See `the python-package documentation`_ for details. + +General Installation Notes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All instructions below are aimed at compiling the 64-bit version of LightGBM. +It is worth compiling the 32-bit version only in very rare special cases involving environmental limitations. +The 32-bit version is slow and untested, so use it at your own risk and don't forget to adjust some of the commands below when installing. + +By default, instructions below will use **VS Build Tools** or **make** tool to compile the code. +It it possible to use `Ninja`_ tool instead of make on all platforms, but VS Build Tools cannot be replaced with Ninja. +You can add ``-G Ninja`` to CMake flags to use Ninja. + +By default, instructions below will produce a shared library file and an executable file with command-line interface. +You can add ``-DBUILD_CLI=OFF`` to CMake flags to disable the executable compilation. + +If you need to build a static library instead of a shared one, you can add ``-DBUILD_STATIC_LIB=ON`` to CMake flags. + +By default, instructions below will place header files into system-wide folder. +You can add ``-DINSTALL_HEADERS=OFF`` to CMake flags to disable headers installation. + +By default, on macOS, CMake is looking into Homebrew standard folders for finding dependencies (e.g. OpenMP). +You can add ``-DUSE_HOMEBREW_FALLBACK=OFF`` to CMake flags to disable this behaviour. + +Users who want to perform benchmarking can make LightGBM output time costs for different internal routines by adding ``-DUSE_TIMETAG=ON`` to CMake flags. + +It is possible to build LightGBM in debug mode. +In this mode all compiler optimizations are disabled and LightGBM performs more checks internally. +To enable debug mode you can add ``-DUSE_DEBUG=ON`` to CMake flags or choose ``Debug_*`` configuration (e.g. ``Debug_DLL``, ``Debug_mpi``) in Visual Studio depending on how you are building LightGBM. + +.. _sanitizers: + +In addition to the debug mode, LightGBM can be built with compiler sanitizers. +To enable them add ``-DUSE_SANITIZER=ON -DENABLED_SANITIZERS="address;leak;undefined"`` to CMake flags. +These values refer to the following supported sanitizers: + +- ``address`` - AddressSanitizer (ASan); +- ``leak`` - LeakSanitizer (LSan); +- ``undefined`` - UndefinedBehaviorSanitizer (UBSan); +- ``thread`` - ThreadSanitizer (TSan). + +Please note, that ThreadSanitizer cannot be used together with other sanitizers. +For more info and additional sanitizers' parameters please refer to the `following docs`_. +It is very useful to build `C++ unit tests <#build-c-unit-tests>`__ with sanitizers. + +.. contents:: **Contents** + :depth: 1 + :local: + :backlinks: none + +Windows +~~~~~~~ + +On Windows, LightGBM can be built using + +- **Visual Studio**; +- **CMake** and **VS Build Tools**; +- **CMake** and **MinGW**. + +Visual Studio (or VS Build Tools) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With GUI +******** + +1. Install `Visual Studio`_. + +2. Navigate to one of the releases at https://github.com/lightgbm-org/LightGBM/releases, download ``LightGBM-complete_source_code_zip.zip``, and unzip it. + +3. Go to ``LightGBM-complete_source_code_zip/windows`` folder. + +4. Open ``LightGBM.sln`` file with **Visual Studio**, choose ``Release`` configuration if you need executable file or ``DLL`` configuration if you need shared library and click ``Build`` -> ``Build Solution (Ctrl+Shift+B)``. + + If you have errors about **Platform Toolset**, go to ``Project`` -> ``Properties`` -> ``Configuration Properties`` -> ``General`` and select the toolset installed on your machine. + + If you have errors about **Windows SDK Version**, go to ``Project`` -> ``Properties`` -> ``Configuration Properties`` -> ``General`` and select the SDK installed on your machine. + +The ``.exe`` file will be in ``LightGBM-complete_source_code_zip/windows/x64/Release`` folder. +The ``.dll`` file will be in ``LightGBM-complete_source_code_zip/windows/x64/DLL`` folder. + +From Command Line +***************** + +1. Install `Git for Windows`_, `CMake`_ and `VS Build Tools`_ (**VS Build Tools** is not needed if **Visual Studio** is already installed). + +2. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -A x64 + cmake --build build --target ALL_BUILD --config Release + +The ``.exe`` and ``.dll`` files will be in ``LightGBM/Release`` folder. + +MinGW-w64 +^^^^^^^^^ + +1. Install `Git for Windows`_, `CMake`_ and `MinGW-w64`_. + +2. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -G "MinGW Makefiles" + cmake --build build -j4 + +The ``.exe`` and ``.dll`` files will be in ``LightGBM/`` folder. + +**Note**: You may need to run the ``cmake -B build -S . -G "MinGW Makefiles"`` one more time or add ``-DCMAKE_SH=CMAKE_SH-NOTFOUND`` to CMake flags if you encounter the ``sh.exe was found in your PATH`` error. + +It is recommended that you use **Visual Studio** since it has better multithreading efficiency in **Windows** for many-core systems +(see `Question 4 <./FAQ.rst#i-am-using-windows-should-i-use-visual-studio-or-mingw-for-compiling-lightgbm>`__ and `Question 8 <./FAQ.rst#cpu-usage-is-low-like-10-in-windows-when-using-lightgbm-on-very-large-datasets-with-many-core-systems>`__). + +Linux +~~~~~ + +On Linux, LightGBM can be built using + +- **CMake** and **gcc**; +- **CMake** and **Clang**. + +After compilation the executable and ``.so`` files will be in ``LightGBM/`` folder. + +gcc +^^^ + +1. Install `CMake`_ and **gcc**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . + cmake --build build -j4 + +Clang +^^^^^ + +1. Install `CMake`_, **Clang** and **OpenMP**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=clang++-14 CC=clang-14 # replace "14" with version of Clang installed on your machine + cmake -B build -S . + cmake --build build -j4 + +macOS +~~~~~ + +On macOS, LightGBM can be installed using + +- **Homebrew**; +- **MacPorts**; + +or can be built using + +- **CMake** and **Apple Clang**; +- **CMake** and **gcc**. + +Install Using ``Homebrew`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: sh + + brew install lightgbm + +Refer to https://formulae.brew.sh/formula/lightgbm for more details. + +Install Using ``MacPorts`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: sh + + sudo port install LightGBM + +Refer to https://ports.macports.org/port/LightGBM for more details. + +**Note**: Port for LightGBM is not maintained by LightGBM's maintainers. + +Build from GitHub +^^^^^^^^^^^^^^^^^ + +After compilation the executable and ``.dylib`` files will be in ``LightGBM/`` folder. + +Apple Clang +*********** + +1. Install `CMake`_ and **OpenMP**: + + .. code:: sh + + brew install cmake libomp + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . + cmake --build build -j4 + +gcc +*** + +1. Install `CMake`_ and **gcc**: + + .. code:: sh + + brew install cmake gcc + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=g++-7 CC=gcc-7 # replace "7" with version of gcc installed on your machine + cmake -B build -S . + cmake --build build -j4 + +Docker +~~~~~~ + +Refer to `Docker folder `__. + +Build Threadless Version (not Recommended) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default build version of LightGBM is based on OpenMP. +You can build LightGBM without OpenMP support but it is **strongly not recommended**. + +Windows +^^^^^^^ + +On Windows, a version of LightGBM without OpenMP support can be built using + +- **Visual Studio**; +- **CMake** and **VS Build Tools**; +- **CMake** and **MinGW**. + +Visual Studio (or VS Build Tools) +********************************* + +With GUI +-------- + +1. Install `Visual Studio`_. + +2. Navigate to one of the releases at https://github.com/lightgbm-org/LightGBM/releases, download ``LightGBM-complete_source_code_zip.zip``, and unzip it. + +3. Go to ``LightGBM-complete_source_code_zip/windows`` folder. + +4. Open ``LightGBM.sln`` file with **Visual Studio**, choose ``Release`` configuration if you need executable file or ``DLL`` configuration if you need shared library. + +5. Go to ``Project`` -> ``Properties`` -> ``Configuration Properties`` -> ``C/C++`` -> ``Language`` and change the ``OpenMP Support`` property to ``No (/openmp-)``. + +6. Get back to the project's main screen and click ``Build`` -> ``Build Solution (Ctrl+Shift+B)``. + + If you have errors about **Platform Toolset**, go to ``Project`` -> ``Properties`` -> ``Configuration Properties`` -> ``General`` and select the toolset installed on your machine. + + If you have errors about **Windows SDK Version**, go to ``Project`` -> ``Properties`` -> ``Configuration Properties`` -> ``General`` and select the SDK installed on your machine. + +The ``.exe`` file will be in ``LightGBM-complete_source_code_zip/windows/x64/Release`` folder. +The ``.dll`` file will be in ``LightGBM-complete_source_code_zip/windows/x64/DLL`` folder. + +From Command Line +----------------- + +1. Install `Git for Windows`_, `CMake`_ and `VS Build Tools`_ (**VS Build Tools** is not needed if **Visual Studio** is already installed). + +2. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -A x64 -DUSE_OPENMP=OFF + cmake --build build --target ALL_BUILD --config Release + +The ``.exe`` and ``.dll`` files will be in ``LightGBM/Release`` folder. + +MinGW-w64 +********* + +1. Install `Git for Windows`_, `CMake`_ and `MinGW-w64`_. + +2. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -G "MinGW Makefiles" -DUSE_OPENMP=OFF + cmake --build build -j4 + +The ``.exe`` and ``.dll`` files will be in ``LightGBM/`` folder. + +**Note**: You may need to run the ``cmake -B build -S . -G "MinGW Makefiles" -DUSE_OPENMP=OFF`` one more time or add ``-DCMAKE_SH=CMAKE_SH-NOTFOUND`` to CMake flags if you encounter the ``sh.exe was found in your PATH`` error. + +Linux +^^^^^ + +On Linux, a version of LightGBM without OpenMP support can be built using + +- **CMake** and **gcc**; +- **CMake** and **Clang**. + +After compilation the executable and ``.so`` files will be in ``LightGBM/`` folder. + +gcc +*** + +1. Install `CMake`_ and **gcc**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_OPENMP=OFF + cmake --build build -j4 + +Clang +***** + +1. Install `CMake`_ and **Clang**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=clang++-14 CC=clang-14 # replace "14" with version of Clang installed on your machine + cmake -B build -S . -DUSE_OPENMP=OFF + cmake --build build -j4 + +macOS +^^^^^ + +On macOS, a version of LightGBM without OpenMP support can be built using + +- **CMake** and **Apple Clang**; +- **CMake** and **gcc**. + +After compilation the executable and ``.dylib`` files will be in ``LightGBM/`` folder. + +Apple Clang +*********** + +1. Install `CMake`_: + + .. code:: sh + + brew install cmake + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_OPENMP=OFF + cmake --build build -j4 + +gcc +*** + +1. Install `CMake`_ and **gcc**: + + .. code:: sh + + brew install cmake gcc + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=g++-7 CC=gcc-7 # replace "7" with version of gcc installed on your machine + cmake -B build -S . -DUSE_OPENMP=OFF + cmake --build build -j4 + +Build MPI Version +~~~~~~~~~~~~~~~~~ + +The default build version of LightGBM is based on socket. LightGBM also supports MPI. +`MPI`_ is a high performance communication approach with `RDMA`_ support. + +If you need to run a distributed learning application with high performance communication, you can build the LightGBM with MPI support. + +Windows +^^^^^^^ + +On Windows, an MPI version of LightGBM can be built using + +- **MS MPI** and **Visual Studio**; +- **MS MPI**, **CMake** and **VS Build Tools**. + +**Note**: Building MPI version by **MinGW** is not supported due to the miss of MPI library in it. + +With GUI +******** + +1. You need to install `MS MPI`_ first. Both ``msmpisdk.msi`` and ``msmpisetup.exe`` are needed. + +2. Install `Visual Studio`_. + +3. Navigate to one of the releases at https://github.com/lightgbm-org/LightGBM/releases, download ``LightGBM-complete_source_code_zip.zip``, and unzip it. + +4. Go to ``LightGBM-complete_source_code_zip/windows`` folder. + +5. Open ``LightGBM.sln`` file with **Visual Studio**, choose ``Release_mpi`` configuration and click ``Build`` -> ``Build Solution (Ctrl+Shift+B)``. + + If you have errors about **Platform Toolset**, go to ``Project`` -> ``Properties`` -> ``Configuration Properties`` -> ``General`` and select the toolset installed on your machine. + + If you have errors about **Windows SDK Version**, go to ``Project`` -> ``Properties`` -> ``Configuration Properties`` -> ``General`` and select the SDK installed on your machine. + +The ``.exe`` file will be in ``LightGBM-complete_source_code_zip/windows/x64/Release_mpi`` folder. + +From Command Line +***************** + +1. You need to install `MS MPI`_ first. Both ``msmpisdk.msi`` and ``msmpisetup.exe`` are needed. + +2. Install `Git for Windows`_, `CMake`_ and `VS Build Tools`_ (**VS Build Tools** is not needed if **Visual Studio** is already installed). + +3. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -A x64 -DUSE_MPI=ON + cmake --build build --target ALL_BUILD --config Release + +The ``.exe`` and ``.dll`` files will be in ``LightGBM/Release`` folder. + +Linux +^^^^^ + +On Linux, an MPI version of LightGBM can be built using + +- **CMake**, **gcc** and **Open MPI**; +- **CMake**, **Clang** and **Open MPI**. + +After compilation the executable and ``.so`` files will be in ``LightGBM/`` folder. + +gcc +*** + +1. Install `CMake`_, **gcc** and `Open MPI`_. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_MPI=ON + cmake --build build -j4 + +Clang +***** + +1. Install `CMake`_, **Clang**, **OpenMP** and `Open MPI`_. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=clang++-14 CC=clang-14 # replace "14" with version of Clang installed on your machine + cmake -B build -S . -DUSE_MPI=ON + cmake --build build -j4 + +macOS +^^^^^ + +On macOS, an MPI version of LightGBM can be built using + +- **CMake**, **Open MPI** and **Apple Clang**; +- **CMake**, **Open MPI** and **gcc**. + +After compilation the executable and ``.dylib`` files will be in ``LightGBM/`` folder. + +Apple Clang +*********** + +1. Install `CMake`_, **OpenMP** and `Open MPI`_: + + .. code:: sh + + brew install cmake libomp open-mpi + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_MPI=ON + cmake --build build -j4 + +gcc +*** + +1. Install `CMake`_, `Open MPI`_ and **gcc**: + + .. code:: sh + + brew install cmake open-mpi gcc + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=g++-7 CC=gcc-7 # replace "7" with version of gcc installed on your machine + cmake -B build -S . -DUSE_MPI=ON + cmake --build build -j4 + +Build GPU Version +~~~~~~~~~~~~~~~~~ + +Windows +^^^^^^^ + +On Windows, a GPU version of LightGBM (``device_type=gpu``) can be built using + +- **OpenCL**, **Boost**, **CMake** and **VS Build Tools**; +- **OpenCL**, **Boost**, **CMake** and **MinGW**. + +If you use **MinGW**, the build procedure is similar to the build on Linux. + +Following procedure is for the **MSVC** (Microsoft Visual C++) build. + +1. Install `Git for Windows`_, `CMake`_ and `VS Build Tools`_ (**VS Build Tools** is not needed if **Visual Studio** is installed). + +2. Install **OpenCL** for Windows. The installation depends on the brand (NVIDIA, AMD, Intel) of your GPU card. + + - For running on Intel, get `Intel SDK for OpenCL`_. + + - For running on AMD, get AMD APP SDK. + + - For running on NVIDIA, get `CUDA Toolkit`_. + + Further reading and correspondence table: `GPU SDK Correspondence and Device Targeting Table <./GPU-Targets.rst>`__. + +3. Install `Boost Binaries`_. + + **Note**: Match your Visual C++ version: + + Visual Studio 2017 -> ``msvc-14.1-64.exe``, + + Visual Studio 2019 -> ``msvc-14.2-64.exe``, + + Visual Studio 2022 -> ``msvc-14.3-64.exe``. + +4. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -A x64 -DUSE_GPU=ON -DBOOST_ROOT=C:/local/boost_1_63_0 -DBOOST_LIBRARYDIR=C:/local/boost_1_63_0/lib64-msvc-14.3 + # if you have installed NVIDIA CUDA to a customized location, you should specify paths to OpenCL headers and library like the following: + # cmake -B build -S . -A x64 -DUSE_GPU=ON -DBOOST_ROOT=C:/local/boost_1_63_0 -DBOOST_LIBRARYDIR=C:/local/boost_1_63_0/lib64-msvc-14.3 -DOpenCL_LIBRARY="C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.0/lib/x64/OpenCL.lib" -DOpenCL_INCLUDE_DIR="C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.0/include" + cmake --build build --target ALL_BUILD --config Release + + **Note**: ``C:/local/boost_1_63_0`` and ``C:/local/boost_1_63_0/lib64-msvc-14.3`` are locations of your **Boost** binaries (assuming you've downloaded 1.63.0 version for Visual Studio 2022). + +The ``.exe`` and ``.dll`` files will be in ``LightGBM/Release`` folder. + +Linux +^^^^^ + +On Linux, a GPU version of LightGBM (``device_type=gpu``) can be built using + +- **CMake**, **OpenCL**, **Boost** and **gcc**; +- **CMake**, **OpenCL**, **Boost** and **Clang**. + +**OpenCL** headers and libraries are usually provided by GPU manufacture. +The generic OpenCL ICD packages (for example, Debian packages ``ocl-icd-libopencl1``, ``ocl-icd-opencl-dev``, ``pocl-opencl-icd``) can also be used. + +Required **Boost** libraries (Boost.Align, Boost.System, Boost.Filesystem, Boost.Chrono) should be provided by the following Debian packages: ``libboost-dev``, ``libboost-system-dev``, ``libboost-filesystem-dev``, ``libboost-chrono-dev``. + +After compilation the executable and ``.so`` files will be in ``LightGBM/`` folder. + +gcc +*** + +1. Install `CMake`_, **gcc**, **OpenCL** and **Boost**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_GPU=ON + # if you have installed NVIDIA CUDA to a customized location, you should specify paths to OpenCL headers and library like the following: + # cmake -B build -S . -DUSE_GPU=ON -DOpenCL_LIBRARY=/usr/local/cuda/lib64/libOpenCL.so -DOpenCL_INCLUDE_DIR=/usr/local/cuda/include/ + cmake --build build -j4 + +Clang +***** + +1. Install `CMake`_, **Clang**, **OpenMP**, **OpenCL** and **Boost**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=clang++-14 CC=clang-14 # replace "14" with version of Clang installed on your machine + cmake -B build -S . -DUSE_GPU=ON + # if you have installed NVIDIA CUDA to a customized location, you should specify paths to OpenCL headers and library like the following: + # cmake -B build -S . -DUSE_GPU=ON -DOpenCL_LIBRARY=/usr/local/cuda/lib64/libOpenCL.so -DOpenCL_INCLUDE_DIR=/usr/local/cuda/include/ + cmake --build build -j4 + +macOS +^^^^^ + +The GPU version is not supported on macOS. + +Docker +^^^^^^ + +Refer to `GPU Docker folder `__. + +Build CUDA Version +~~~~~~~~~~~~~~~~~~ + +The `original GPU version <#build-gpu-version>`__ of LightGBM (``device_type=gpu``) is based on OpenCL, and only computes histograms on GPUs, with other parts of training in CPUs. + +The CUDA-based version (``device_type=cuda``) is a separate implementation that runs significantly faster by putting all the training process on GPUs. It also supports multi-GPU, and multi-node multi-GPU training. +Use this version in Linux environments with an NVIDIA GPU with compute capability 6.0 or higher. + +Windows +^^^^^^^ + +The CUDA version is not supported on Windows. +Use the `GPU version <#build-gpu-version>`__ (``device_type=gpu``) for GPU acceleration on Windows. + +Linux +^^^^^ + +On Linux, a CUDA version of LightGBM can be built using + +- **CMake**, **gcc** and **CUDA**; +- **CMake**, **Clang** and **CUDA**. + +Please refer to `this detailed guide`_ for **CUDA** libraries installation. + +After compilation the executable and ``.so`` files will be in ``LightGBM/`` folder. + +.. note:: + + By default, the library will be built with support for a hard-coded list of GPU architectures + based on the detected CUDA Toolkit version. + + To build the library with support for more architectures, set ``CMAKE_CUDA_ARCHITECTURES``. + + .. code:: sh + + # example: all Blackwell arches, including DGX Spark + cmake -B build -S . -DUSE_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES="100;120;121-real;121-virtual" + + # example: just the local GPU + cmake -B build -S . -DUSE_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES="native" + +gcc +*** + +1. Install `CMake`_, **gcc** and **CUDA**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_CUDA=ON + cmake --build build -j4 + +Clang +***** + +1. Install `CMake`_, **Clang**, **OpenMP** and **CUDA**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=clang++-14 CC=clang-14 # replace "14" with version of Clang installed on your machine + cmake -B build -S . -DUSE_CUDA=ON + cmake --build build -j4 + +macOS +^^^^^ + +The CUDA version is not supported on macOS. + +Build ROCm Version +~~~~~~~~~~~~~~~~~~ + +The `original GPU version <#build-gpu-version>`__ of LightGBM (``device_type=gpu``) is based on OpenCL. + +The ROCm-based version (``device_type=cuda``) is a separate implementation. Yes, the ROCm version reuses the ``device_type=cuda`` as a convenience for users. Use this version in Linux environments with an AMD GPU. + +Windows +^^^^^^^ + +The ROCm version is not supported on Windows. +Use the `GPU version <#build-gpu-version>`__ (``device_type=gpu``) for GPU acceleration on Windows. + +Linux +^^^^^ + +On Linux, a ROCm version of LightGBM can be built using + +- **CMake**, **gcc** and **ROCm**; +- **CMake**, **Clang** and **ROCm**. + +Please refer to `the ROCm docs`_ for **ROCm** libraries installation. + +After compilation the executable and ``.so`` files will be in ``LightGBM/`` folder. + +gcc +*** + +1. Install `CMake`_, **gcc** and **ROCm**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_ROCM=ON + cmake --build build -j4 + +Clang +***** + +1. Install `CMake`_, **Clang**, **OpenMP** and **ROCm**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=clang++-14 CC=clang-14 # replace "14" with version of Clang installed on your machine + cmake -B build -S . -DUSE_ROCM=ON + cmake --build build -j4 + +macOS +^^^^^ + +The ROCm version is not supported on macOS. + +Build Java Wrapper +~~~~~~~~~~~~~~~~~~ + +Using the following instructions you can generate a JAR file containing the LightGBM `C API <./Development-Guide.rst#c-api>`__ wrapped by **SWIG**. + +After compilation the ``.jar`` file will be in ``LightGBM/build`` folder. + +Windows +^^^^^^^ + +On Windows, a Java wrapper of LightGBM can be built using + +- **Java**, **SWIG**, **CMake** and **VS Build Tools**; +- **Java**, **SWIG**, **CMake** and **MinGW**. + +VS Build Tools +************** + +1. Install `Git for Windows`_, `CMake`_ and `VS Build Tools`_ (**VS Build Tools** is not needed if **Visual Studio** is already installed). + +2. Install `SWIG`_ and **Java** (also make sure that ``JAVA_HOME`` environment variable is set properly). + +3. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -A x64 -DUSE_SWIG=ON + cmake --build build --target ALL_BUILD --config Release + +MinGW-w64 +********* + +1. Install `Git for Windows`_, `CMake`_ and `MinGW-w64`_. + +2. Install `SWIG`_ and **Java** (also make sure that ``JAVA_HOME`` environment variable is set properly). + +3. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -G "MinGW Makefiles" -DUSE_SWIG=ON + cmake --build build -j4 + +**Note**: You may need to run the ``cmake -B build -S . -G "MinGW Makefiles" -DUSE_SWIG=ON`` one more time or add ``-DCMAKE_SH=CMAKE_SH-NOTFOUND`` to CMake flags if you encounter the ``sh.exe was found in your PATH`` error. + +It is recommended to use **VS Build Tools (Visual Studio)** since it has better multithreading efficiency in **Windows** for many-core systems +(see `Question 4 <./FAQ.rst#i-am-using-windows-should-i-use-visual-studio-or-mingw-for-compiling-lightgbm>`__ and `Question 8 <./FAQ.rst#cpu-usage-is-low-like-10-in-windows-when-using-lightgbm-on-very-large-datasets-with-many-core-systems>`__). + +Linux +^^^^^ + +On Linux, a Java wrapper of LightGBM can be built using + +- **CMake**, **gcc**, **Java** and **SWIG**; +- **CMake**, **Clang**, **Java** and **SWIG**. + +gcc +*** + +1. Install `CMake`_, **gcc**, `SWIG`_ and **Java** (also make sure that ``JAVA_HOME`` environment variable is set properly). + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_SWIG=ON + cmake --build build -j4 + +Clang +***** + +1. Install `CMake`_, **Clang**, **OpenMP**, `SWIG`_ and **Java** (also make sure that ``JAVA_HOME`` environment variable is set properly). + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=clang++-14 CC=clang-14 # replace "14" with version of Clang installed on your machine + cmake -B build -S . -DUSE_SWIG=ON + cmake --build build -j4 + +macOS +^^^^^ + +On macOS, a Java wrapper of LightGBM can be built using + +- **CMake**, **Java**, **SWIG** and **Apple Clang**; +- **CMake**, **Java**, **SWIG** and **gcc**. + +Apple Clang +*********** + +1. Install `CMake`_, **Java** (also make sure that ``JAVA_HOME`` environment variable is set properly), `SWIG`_ and **OpenMP**: + + .. code:: sh + + brew install cmake openjdk swig libomp + export JAVA_HOME="$(brew --prefix openjdk)/libexec/openjdk.jdk/Contents/Home/" + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DUSE_SWIG=ON + cmake --build build -j4 + +gcc +*** + +1. Install `CMake`_, **Java** (also make sure that ``JAVA_HOME`` environment variable is set properly), `SWIG`_ and **gcc**: + + .. code:: sh + + brew install cmake openjdk swig gcc + export JAVA_HOME="$(brew --prefix openjdk)/libexec/openjdk.jdk/Contents/Home/" + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=g++-7 CC=gcc-7 # replace "7" with version of gcc installed on your machine + cmake -B build -S . -DUSE_SWIG=ON + cmake --build build -j4 + +Build Python-package +~~~~~~~~~~~~~~~~~~~~ + +Refer to `the python-package documentation`_. + +Build R-package +~~~~~~~~~~~~~~~ + +Refer to `R-package folder `__. + +Build C++ Unit Tests +~~~~~~~~~~~~~~~~~~~~ + +Windows +^^^^^^^ + +On Windows, C++ unit tests of LightGBM can be built using + +- **CMake** and **VS Build Tools**; +- **CMake** and **MinGW**. + +VS Build Tools +************** + +1. Install `Git for Windows`_, `CMake`_ and `VS Build Tools`_ (**VS Build Tools** is not needed if **Visual Studio** is already installed). + +2. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -A x64 -DBUILD_CPP_TEST=ON + cmake --build build --target testlightgbm --config Debug + +The ``.exe`` file will be in ``LightGBM/Debug`` folder. + +MinGW-w64 +********* + +1. Install `Git for Windows`_, `CMake`_ and `MinGW-w64`_. + +2. Run the following commands: + + .. code:: console + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -G "MinGW Makefiles" -DBUILD_CPP_TEST=ON + cmake --build build --target testlightgbm -j4 + +The ``.exe`` file will be in ``LightGBM/`` folder. + +**Note**: You may need to run the ``cmake -B build -S . -G "MinGW Makefiles" -DBUILD_CPP_TEST=ON`` one more time or add ``-DCMAKE_SH=CMAKE_SH-NOTFOUND`` to CMake flags if you encounter the ``sh.exe was found in your PATH`` error. + +Linux +^^^^^ + +On Linux, a C++ unit tests of LightGBM can be built using + +- **CMake** and **gcc**; +- **CMake** and **Clang**. + +After compilation the executable file will be in ``LightGBM/`` folder. + +gcc +*** + +1. Install `CMake`_ and **gcc**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DBUILD_CPP_TEST=ON + cmake --build build --target testlightgbm -j4 + +Clang +***** + +1. Install `CMake`_, **Clang** and **OpenMP**. + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=clang++-14 CC=clang-14 # replace "14" with version of Clang installed on your machine + cmake -B build -S . -DBUILD_CPP_TEST=ON + cmake --build build --target testlightgbm -j4 + +macOS +^^^^^ + +On macOS, a C++ unit tests of LightGBM can be built using + +- **CMake** and **Apple Clang**; +- **CMake** and **gcc**. + +After compilation the executable file will be in ``LightGBM/`` folder. + +Apple Clang +*********** + +1. Install `CMake`_ and **OpenMP**: + + .. code:: sh + + brew install cmake libomp + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + cmake -B build -S . -DBUILD_CPP_TEST=ON + cmake --build build --target testlightgbm -j4 + +gcc +*** + +1. Install `CMake`_ and **gcc**: + + .. code:: sh + + brew install cmake gcc + +2. Run the following commands: + + .. code:: sh + + git clone --recursive https://github.com/lightgbm-org/LightGBM + cd LightGBM + export CXX=g++-7 CC=gcc-7 # replace "7" with version of gcc installed on your machine + cmake -B build -S . -DBUILD_CPP_TEST=ON + cmake --build build --target testlightgbm -j4 + +.. _Visual Studio: https://visualstudio.microsoft.com/downloads/ + +.. _Git for Windows: https://git-scm.com/download/win + +.. _CMake: https://cmake.org/ + +.. _VS Build Tools: https://visualstudio.microsoft.com/downloads/ + +.. _MinGW-w64: https://www.mingw-w64.org/downloads/ + +.. _MPI: https://en.wikipedia.org/wiki/Message_Passing_Interface + +.. _RDMA: https://en.wikipedia.org/wiki/Remote_direct_memory_access + +.. _MS MPI: https://learn.microsoft.com/en-us/message-passing-interface/microsoft-mpi-release-notes + +.. _Open MPI: https://www.open-mpi.org/ + +.. _Intel SDK for OpenCL: https://software.intel.com/en-us/articles/opencl-drivers + +.. _CUDA Toolkit: https://developer.nvidia.com/cuda-downloads + +.. _Boost Binaries: https://sourceforge.net/projects/boost/files/boost-binaries/ + +.. _SWIG: https://www.swig.org/download.html + +.. _this detailed guide: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html + +.. _the python-package documentation: https://github.com/lightgbm-org/LightGBM/tree/main/python-package + +.. _the ROCm docs: https://rocm.docs.amd.com/projects/install-on-linux/en/latest/ + +.. _following docs: https://github.com/google/sanitizers/wiki + +.. _Ninja: https://ninja-build.org diff --git a/docs/Key-Events.md b/docs/Key-Events.md new file mode 100644 index 0000000..1d4b6fc --- /dev/null +++ b/docs/Key-Events.md @@ -0,0 +1 @@ +The content of this document was very outdated and is no longer available to avoid any misleadings. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..627e704 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = -W +SPHINXBUILD = sphinx-build +SPHINXPROJ = LightGBM +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/Parallel-Learning-Guide.rst b/docs/Parallel-Learning-Guide.rst new file mode 100644 index 0000000..965e257 --- /dev/null +++ b/docs/Parallel-Learning-Guide.rst @@ -0,0 +1,549 @@ +Distributed Learning Guide +========================== + +.. _Parallel Learning Guide: + +This guide describes distributed learning in LightGBM. Distributed learning allows the use of multiple machines to produce a single model. + +Follow the `Quick Start <./Quick-Start.rst>`__ to know how to use LightGBM first. + +How Distributed LightGBM Works +------------------------------ + +This section describes how distributed learning in LightGBM works. To learn how to do this in various programming languages and frameworks, please see `Integrations <#integrations>`__. + +Choose Appropriate Parallel Algorithm +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +LightGBM provides 3 distributed learning algorithms now. + ++--------------------+---------------------------+ +| Parallel Algorithm | How to Use | ++====================+===========================+ +| Data parallel | ``tree_learner=data`` | ++--------------------+---------------------------+ +| Feature parallel | ``tree_learner=feature`` | ++--------------------+---------------------------+ +| Voting parallel | ``tree_learner=voting`` | ++--------------------+---------------------------+ + +These algorithms are suited for different scenarios, which is listed in the following table: + ++-------------------------+-------------------+-----------------+ +| | #data is small | #data is large | ++=========================+===================+=================+ +| **#feature is small** | Feature Parallel | Data Parallel | ++-------------------------+-------------------+-----------------+ +| **#feature is large** | Feature Parallel | Voting Parallel | ++-------------------------+-------------------+-----------------+ + +More details about these parallel algorithms can be found in `optimization in distributed learning <./Features.rst#optimization-in-distributed-learning>`__. + +Integrations +------------ + +This section describes how to run distributed LightGBM training in various programming languages and frameworks. To learn how distributed learning in LightGBM works generally, please see `How Distributed LightGBM Works <#how-distributed-lightgbm-works>`__. + +Apache Spark +^^^^^^^^^^^^ + +Apache Spark users can use `SynapseML`_ for machine learning workflows with LightGBM. This project is not maintained by LightGBM's maintainers. + +See `this SynapseML example`_ for additional information on using LightGBM on Spark. + +.. note:: + + ``SynapseML`` is not maintained by LightGBM's maintainers. Bug reports or feature requests should be directed to https://github.com/microsoft/SynapseML/issues. + +Dask +^^^^ + +.. versionadded:: 3.2.0 + +LightGBM's Python-package supports distributed learning via `Dask`_. This integration is maintained by LightGBM's maintainers. + +.. warning:: + + Dask integration is only tested on macOS and Linux. + +Dask Examples +''''''''''''' + +For sample code using ``lightgbm.dask``, see `these Dask examples`_. + +Training with Dask +'''''''''''''''''' + +This section contains detailed information on performing LightGBM distributed training using Dask. + +Configuring the Dask Cluster +**************************** + +**Allocating Threads** + +When setting up a Dask cluster for training, give each Dask worker process at least two threads. If you do not do this, training might be substantially slower because communication work and training work will block each other. + +If you do not have other significant processes competing with Dask for resources, just accept the default ``nthreads`` from your chosen ``dask.distributed`` cluster. + +.. code:: python + + from distributed import Client, LocalCluster + + cluster = LocalCluster(n_workers=3) + client = Client(cluster) + +**Managing Memory** + +Use the Dask diagnostic dashboard or your preferred monitoring tool to monitor Dask workers' memory consumption during training. As described in `the Dask worker documentation`_, Dask workers will automatically start spilling data to disk if memory consumption gets too high. This can substantially slow down computations, since disk I/O is usually much slower than reading the same data from memory. + + `At 60% of memory load, [Dask will] spill least recently used data to disk` + +To reduce the risk of hitting memory limits, consider restarting each worker process before running any data loading or training code. + +.. code:: python + + client.restart() + +Setting Up Training Data +************************* + +The estimators in ``lightgbm.dask`` expect that matrix-like or array-like data are provided in Dask DataFrame, Dask Array, or (in some cases) Dask Series format. See `the Dask DataFrame documentation`_ and `the Dask Array documentation`_ for more information on how to create such data structures. + +.. image:: ./_static/images/dask-initial-setup.svg + :align: center + :width: 600px + :alt: On the left, rectangles showing a 5 by 5 grid for a local dataset. On the right, two circles representing Dask workers, one with a 3 by 5 grid and one with a 2 by 5 grid. + :target: ./_static/images/dask-initial-setup.svg + +While setting up for training, ``lightgbm`` will concatenate all of the partitions on a worker into a single dataset. Distributed training then proceeds with one LightGBM worker process per Dask worker. + +.. image:: ./_static/images/dask-concat.svg + :align: center + :width: 600px + :alt: A section labeled "before" showing two grids and a section labeled "after" showing a single grid that looks like the two from "before" stacked one on top of the other. + :target: ./_static/images/dask-concat.svg + +When setting up data partitioning for LightGBM training with Dask, try to follow these suggestions: + +* ensure that each worker in the cluster has some of the training data +* try to give each worker roughly the same amount of data, especially if your dataset is small +* if you plan to train multiple models (for example, to tune hyperparameters) on the same data, use ``client.persist()`` before training to materialize the data one time + +Using a Specific Dask Client +**************************** + +In most situations, you should not need to tell ``lightgbm.dask`` to use a specific Dask client. By default, the client returned by ``distributed.default_client()`` will be used. + +However, you might want to explicitly control the Dask client used by LightGBM if you have multiple active clients in the same session. This is useful in more complex workflows like running multiple training jobs on different Dask clusters. + +LightGBM's Dask estimators support setting an attribute ``client`` to control the client that is used. + +.. code:: python + + import lightgbm as lgb + from distributed import Client, LocalCluster + + cluster = LocalCluster() + client = Client(cluster) + + # option 1: keyword argument in constructor + dask_model = lgb.DaskLGBMClassifier(client=client) + + # option 2: set_params() after construction + dask_model = lgb.DaskLGBMClassifier() + dask_model.set_params(client=client) + +Using Specific Ports +******************** + +At the beginning of training, ``lightgbm.dask`` sets up a LightGBM network where each Dask worker runs one long-running task that acts as a LightGBM worker. During training, LightGBM workers communicate with each other over TCP sockets. By default, random open ports are used when creating these sockets. + +If the communication between Dask workers in the cluster used for training is restricted by firewall rules, you must tell LightGBM exactly what ports to use. + +**Option 1: provide a specific list of addresses and ports** + +LightGBM supports a parameter ``machines``, a comma-delimited string where each entry refers to one worker (host name or IP) and a port that that worker will accept connections on. If you provide this parameter to the estimators in ``lightgbm.dask``, LightGBM will not search randomly for ports. + +For example, consider the case where you are running one Dask worker process on each of the following IP addresses: + +.. code:: text + + 10.0.1.0 + 10.0.2.0 + 10.0.3.0 + +You could edit your firewall rules to allow traffic on one additional port on each of these hosts, then provide ``machines`` directly. + +.. code:: python + + import lightgbm as lgb + + machines = "10.0.1.0:12401,10.0.2.0:12402,10.0.3.0:15000" + dask_model = lgb.DaskLGBMRegressor(machines=machines) + +If you are running multiple Dask worker processes on physical host in the cluster, be sure that there are multiple entries for that IP address, with different ports. For example, if you were running a cluster with ``nprocs=2`` (2 Dask worker processes per machine), you might open two additional ports on each of these hosts, then provide ``machines`` as follows. + +.. code:: python + + import lightgbm as lgb + + machines = ",".join([ + "10.0.1.0:16000", + "10.0.1.0:16001", + "10.0.2.0:16000", + "10.0.2.0:16001", + ]) + dask_model = lgb.DaskLGBMRegressor(machines=machines) + +.. warning:: + + Providing ``machines`` gives you complete control over the networking details of training, but it also makes the training process fragile. Training will fail if you use ``machines`` and any of the following are true: + + * any of the ports mentioned in ``machines`` are not open when training begins + * some partitions of the training data are held by machines that that are not present in ``machines`` + * some machines mentioned in ``machines`` do not hold any of the training data + +**Option 2: specify one port to use on every worker** + +If you are only running one Dask worker process on each host, and if you can reliably identify a port that is open on every host, using ``machines`` is unnecessarily complicated. If ``local_listen_port`` is given and ``machines`` is not, LightGBM will not search for ports randomly, but it will limit the list of addresses in the LightGBM network to those Dask workers that have a piece of the training data. + +For example, consider the case where you are running one Dask worker process on each of the following IP addresses: + +.. code:: text + + 10.0.1.0 + 10.0.2.0 + 10.0.3.0 + +You could edit your firewall rules to allow communication between any of the workers over one port, then provide that port via parameter ``local_listen_port``. + +.. code:: python + + import lightgbm as lgb + + dask_model = lgb.DaskLGBMRegressor(local_listen_port=12400) + +.. warning:: + + Providing ``local_listen_port`` is slightly less fragile than ``machines`` because LightGBM will automatically figure out which workers have pieces of the training data. However, using this method, training can fail if any of the following are true: + + * the port ``local_listen_port`` is not open on any of the worker hosts + * any machine has multiple Dask worker processes running on it + +Using Custom Objective Functions with Dask +****************************************** + +.. versionadded:: 4.0.0 + +It is possible to customize the boosting process by providing a custom objective function written in Python. +See the Dask API's documentation for details on how to implement such functions. + +.. warning:: + + Custom objective functions used with ``lightgbm.dask`` will be called by each worker process on only that worker's local data. + +Follow the example below to use a custom implementation of the ``regression_l2`` objective. + +.. code:: python + + import dask.array as da + import lightgbm as lgb + import numpy as np + from distributed import Client, LocalCluster + + cluster = LocalCluster(n_workers=2) + client = Client(cluster) + + X = da.random.random((1000, 10), (500, 10)) + y = da.random.random((1000,), (500,)) + + def custom_l2_obj(y_true, y_pred): + grad = y_pred - y_true + hess = np.ones(len(y_true)) + return grad, hess + + dask_model = lgb.DaskLGBMRegressor( + objective=custom_l2_obj + ) + dask_model.fit(X, y) + +Prediction with Dask +'''''''''''''''''''' + +The estimators from ``lightgbm.dask`` can be used to create predictions based on data stored in Dask collections. In that interface, ``.predict()`` expects a Dask Array or Dask DataFrame, and returns a Dask Array of predictions. + +See `the Dask prediction example`_ for some sample code that shows how to perform Dask-based prediction. + +For model evaluation, consider using `the metrics functions from dask-ml`_. Those functions are intended to provide the same API as equivalent functions in ``sklearn.metrics``, but they use distributed computation powered by Dask to compute metrics without all of the input data ever needing to be on a single machine. + +Saving Dask Models +'''''''''''''''''' + +After training with Dask, you have several options for saving a fitted model. + +**Option 1: pickle the Dask estimator** + +LightGBM's Dask estimators can be pickled directly with ``cloudpickle``, ``joblib``, or ``pickle``. + +.. code:: python + + import dask.array as da + import pickle + import lightgbm as lgb + from distributed import Client, LocalCluster + + cluster = LocalCluster(n_workers=2) + client = Client(cluster) + + X = da.random.random((1000, 10), (500, 10)) + y = da.random.random((1000,), (500,)) + + dask_model = lgb.DaskLGBMRegressor() + dask_model.fit(X, y) + + with open("dask-model.pkl", "wb") as f: + pickle.dump(dask_model, f) + +A model saved this way can then later be loaded with whichever serialization library you used to save it. + +.. code:: python + + import pickle + with open("dask-model.pkl", "rb") as f: + dask_model = pickle.load(f) + +.. note:: + + If you explicitly set a Dask client (see `Using a Specific Dask Client <#using-a-specific-dask-client>`__), it will not be saved when pickling the estimator. When loading a Dask estimator from disk, if you need to use a specific client you can add it after loading with ``dask_model.set_params(client=client)``. + +**Option 2: pickle the sklearn estimator** + +The estimators available from ``lightgbm.dask`` can be converted to an instance of the equivalent class from ``lightgbm.sklearn``. Choosing this option allows you to use Dask for training but avoid depending on any Dask libraries at scoring time. + +.. code:: python + + import dask.array as da + import joblib + import lightgbm as lgb + from distributed import Client, LocalCluster + + cluster = LocalCluster(n_workers=2) + client = Client(cluster) + + X = da.random.random((1000, 10), (500, 10)) + y = da.random.random((1000,), (500,)) + + dask_model = lgb.DaskLGBMRegressor() + dask_model.fit(X, y) + + # convert to sklearn equivalent + sklearn_model = dask_model.to_local() + + print(type(sklearn_model)) + #> lightgbm.sklearn.LGBMRegressor + + joblib.dump(sklearn_model, "sklearn-model.joblib") + +A model saved this way can then later be loaded with whichever serialization library you used to save it. + +.. code:: python + + import joblib + + sklearn_model = joblib.load("sklearn-model.joblib") + +**Option 3: save the LightGBM Booster** + +The lowest-level model object in LightGBM is the ``lightgbm.Booster``. After training, you can extract a Booster from the Dask estimator. + +.. code:: python + + import dask.array as da + import lightgbm as lgb + from distributed import Client, LocalCluster + + cluster = LocalCluster(n_workers=2) + client = Client(cluster) + + X = da.random.random((1000, 10), (500, 10)) + y = da.random.random((1000,), (500,)) + + dask_model = lgb.DaskLGBMRegressor() + dask_model.fit(X, y) + + # get underlying Booster object + bst = dask_model.booster_ + +From the point forward, you can use any of the following methods to save the Booster: + +* serialize with ``cloudpickle``, ``joblib``, or ``pickle`` +* ``bst.dump_model()``: dump the model to a dictionary which could be written out as JSON +* ``bst.model_to_string()``: dump the model to a string in memory +* ``bst.save_model()``: write the output of ``bst.model_to_string()`` to a text file + +Kubeflow +^^^^^^^^ + +Kubeflow users can also use the `Kubeflow XGBoost Operator`_ for machine learning workflows with LightGBM. You can see `this example`_ for more details. + +Kubeflow integrations for LightGBM are not maintained by LightGBM's maintainers. + +.. note:: + + The Kubeflow integrations for LightGBM are not maintained by LightGBM's maintainers. Bug reports or feature requests should be directed to https://github.com/kubeflow/fairing/issues or https://github.com/kubeflow/xgboost-operator/issues. + +LightGBM CLI +^^^^^^^^^^^^ + +.. _Build Parallel Version: + +Preparation +''''''''''' + +By default, distributed learning with LightGBM uses socket-based communication. + +If you need to build distributed version with MPI support, please refer to `Installation Guide <./Installation-Guide.rst#build-mpi-version>`__. + +Socket Version +************** + +It needs to collect IP of all machines that want to run distributed learning in and allocate one TCP port (assume 12345 here) for all machines, +and change firewall rules to allow income of this port (12345). Then write these IP and ports in one file (assume ``mlist.txt``), like following: + +.. code:: text + + machine1_ip 12345 + machine2_ip 12345 + +MPI Version +*********** + +It needs to collect IP (or hostname) of all machines that want to run distributed learning in. +Then write these IP in one file (assume ``mlist.txt``) like following: + +.. code:: text + + machine1_ip + machine2_ip + +**Note**: For Windows users, need to start "smpd" to start MPI service. More details can be found `here`_. + +Run Distributed Learning +'''''''''''''''''''''''' + +.. _Run Parallel Learning: + +Socket Version +************** + +1. Edit following parameters in config file: + + ``tree_learner=your_parallel_algorithm``, edit ``your_parallel_algorithm`` (e.g. feature/data) here. + + ``num_machines=your_num_machines``, edit ``your_num_machines`` (e.g. 4) here. + + ``machine_list_file=mlist.txt``, ``mlist.txt`` is created in `Preparation section <#preparation>`__. + + ``local_listen_port=12345``, ``12345`` is allocated in `Preparation section <#preparation>`__. + +2. Copy data file, executable file, config file and ``mlist.txt`` to all machines. + +3. Run following command on all machines, you need to change ``your_config_file`` to real config file. + + For Windows: ``lightgbm.exe config=your_config_file`` + + For Linux: ``./lightgbm config=your_config_file`` + +MPI Version +*********** + +1. Edit following parameters in config file: + + ``tree_learner=your_parallel_algorithm``, edit ``your_parallel_algorithm`` (e.g. feature/data) here. + + ``num_machines=your_num_machines``, edit ``your_num_machines`` (e.g. 4) here. + +2. Copy data file, executable file, config file and ``mlist.txt`` to all machines. + + **Note**: MPI needs to be run in the **same path on all machines**. + +3. Run following command on one machine (not need to run on all machines), need to change ``your_config_file`` to real config file. + + For Windows: + + .. code:: console + + mpiexec.exe /machinefile mlist.txt lightgbm.exe config=your_config_file + + For Linux: + + .. code:: console + + mpiexec --machinefile mlist.txt ./lightgbm config=your_config_file + +Example +''''''' + +- `A simple distributed learning example`_ + +Ray +^^^ + +`Ray`_ is a Python-based framework for distributed computing. Ray provides LightGBM support through the Ray Train API with ``LightGBMTrainer`` and the `lightgbm_ray`_ project maintained within the official Ray GitHub organization. + +For the Ray Train API, see `the Ray documentation`_ for usage examples. + +For the lightgbm_ray project, see `the lightgbm_ray documentation`_ for usage examples. + +.. note:: + + ``lightgbm_ray`` and ``ray`` are not maintained by LightGBM's maintainers. Bug reports or feature requests should be directed to https://github.com/ray-project/lightgbm_ray/issues and https://github.com/ray-project/ray/issues respectively. + +Mars +^^^^ + +`Mars`_ is a tensor-based framework for large-scale data computation. LightGBM integration, maintained within the Mars GitHub repository, can be used to perform distributed LightGBM training using ``pymars``. + +See `the mars documentation`_ for usage examples. + +.. note:: + + ``Mars`` is not maintained by LightGBM's maintainers. Bug reports or feature requests should be directed to https://github.com/mars-project/mars/issues. + +.. _Dask: https://docs.dask.org/en/latest/ + +.. _SynapseML: https://aka.ms/spark + +.. _this SynapseML example: https://github.com/microsoft/SynapseML/tree/master/docs/Explore%20Algorithms/LightGBM + +.. _the Dask Array documentation: https://docs.dask.org/en/latest/array.html + +.. _the Dask DataFrame documentation: https://docs.dask.org/en/latest/dataframe.html + +.. _the Dask prediction example: https://github.com/lightgbm-org/LightGBM/blob/main/examples/python-guide/dask/prediction.py + +.. _the Dask worker documentation: https://distributed.dask.org/en/stable/worker-memory.html + +.. _the metrics functions from dask-ml: https://ml.dask.org/modules/api.html#dask-ml-metrics-metrics + +.. _these Dask examples: https://github.com/lightgbm-org/LightGBM/tree/main/examples/python-guide/dask + +.. _Kubeflow XGBoost Operator: https://github.com/kubeflow/xgboost-operator + +.. _this example: https://github.com/kubeflow/xgboost-operator/tree/master/config/samples/lightgbm-dist + +.. _here: https://www.youtube.com/watch?v=iqzXhp5TxUY + +.. _A simple distributed learning example: https://github.com/lightgbm-org/LightGBM/tree/main/examples/parallel_learning + +.. _lightgbm_ray: https://github.com/ray-project/lightgbm_ray + +.. _Ray: https://www.ray.io/ + +.. _the lightgbm_ray documentation: https://docs.ray.io/en/latest/tune/api_docs/integration.html#lightgbm-tune-integration-lightgbm + +.. _the Ray documentation: https://docs.ray.io/en/latest/train/api/api.html#lightgbm + +.. _Mars: https://mars-project.readthedocs.io/en/latest/ + +.. _the mars documentation: https://mars-project.readthedocs.io/en/latest/user_guide/learn/lightgbm.html diff --git a/docs/Parameters-Tuning.rst b/docs/Parameters-Tuning.rst new file mode 100644 index 0000000..9a3593f --- /dev/null +++ b/docs/Parameters-Tuning.rst @@ -0,0 +1,221 @@ +Parameters Tuning +================= + +This page contains parameters tuning guides for different scenarios. + +**List of other helpful links** + +- `Parameters <./Parameters.rst>`__ +- `Python API <./Python-API.rst>`__ +- `FLAML`_ for automated hyperparameter tuning +- `Optuna`_ for automated hyperparameter tuning + +Tune Parameters for the Leaf-wise (Best-first) Tree +--------------------------------------------------- + +LightGBM uses the `leaf-wise <./Features.rst#leaf-wise-best-first-tree-growth>`__ tree growth algorithm, while many other popular tools use depth-wise tree growth. +Compared with depth-wise growth, the leaf-wise algorithm can converge much faster. +However, the leaf-wise growth may be over-fitting if not used with the appropriate parameters. + +To get good results using a leaf-wise tree, these are some important parameters: + +1. ``num_leaves``. This is the main parameter to control the complexity of the tree model. + Theoretically, we can set ``num_leaves = 2^(max_depth)`` to obtain the same number of leaves as depth-wise tree. + However, this simple conversion is not good in practice. + A leaf-wise tree is typically much deeper than a depth-wise tree for a fixed number of leaves. Unconstrained depth can induce over-fitting. + Thus, when trying to tune the ``num_leaves``, we should let it be smaller than ``2^(max_depth)``. + For example, when the ``max_depth=7`` the depth-wise tree can get good accuracy, + but setting ``num_leaves`` to ``127`` may cause over-fitting, and setting it to ``70`` or ``80`` may get better accuracy than depth-wise. + +2. ``min_data_in_leaf``. This is a very important parameter to prevent over-fitting in a leaf-wise tree. + Its optimal value depends on the number of training samples and ``num_leaves``. + Setting it to a large value can avoid growing too deep a tree, but may cause under-fitting. + In practice, setting it to hundreds or thousands is enough for a large dataset. + +3. ``max_depth``. You also can use ``max_depth`` to limit the tree depth explicitly. + If you set ``max_depth``, also explicitly set ``num_leaves`` to some value ``<= 2^max_depth``. + +For Faster Speed +---------------- + +Add More Computational Resources +'''''''''''''''''''''''''''''''' + +On systems where it is available, LightGBM uses OpenMP to parallelize many operations. The maximum number of threads used by LightGBM is controlled by the parameter ``num_threads``. By default, this will defer to the default behavior of OpenMP (one thread per real CPU core or the value in environment variable ``OMP_NUM_THREADS``, if it is set). For best performance, set this to the number of **real** CPU cores available. + +You might be able to achieve faster training by moving to a machine with more available CPU cores. + +Using distributed (multi-machine) training might also reduce training time. See the `Distributed Learning Guide <./Parallel-Learning-Guide.rst>`_ for details. + +Use a GPU-enabled version of LightGBM +''''''''''''''''''''''''''''''''''''' + +You might find that training is faster using a GPU-enabled build of LightGBM. See the `GPU Tutorial <./GPU-Tutorial.rst>`__ for details. + +Grow Shallower Trees +'''''''''''''''''''' + +The total training time for LightGBM increases with the total number of tree nodes added. LightGBM comes with several parameters that can be used to control the number of nodes per tree. + +The suggestions below will speed up training, but might hurt training accuracy. + +Decrease ``max_depth`` +********************** + +This parameter is an integer that controls the maximum distance between the root node of each tree and a leaf node. Decrease ``max_depth`` to reduce training time. + +Decrease ``num_leaves`` +*********************** + +LightGBM adds nodes to trees based on the gain from adding that node, regardless of depth. This figure from `the feature documentation <./Features.rst#leaf-wise-best-first-tree-growth>`__ illustrates the process. + +.. image:: ./_static/images/leaf-wise.png + :align: center + :alt: Three consecutive images of decision trees, where each shows the tree with an additional two leaf nodes added. Shows that leaf-wise growth can result in trees that have some branches which are longer than others. + +Because of this growth strategy, it isn't straightforward to use ``max_depth`` alone to limit the complexity of trees. The ``num_leaves`` parameter sets the maximum number of nodes per tree. Decrease ``num_leaves`` to reduce training time. + +Increase ``min_gain_to_split`` +****************************** + +When adding a new tree node, LightGBM chooses the split point that has the largest gain. Gain is basically the reduction in training loss that results from adding a split point. By default, LightGBM sets ``min_gain_to_split`` to 0.0, which means "there is no improvement that is too small". However, in practice you might find that very small improvements in the training loss don't have a meaningful impact on the generalization error of the model. Increase ``min_gain_to_split`` to reduce training time. + +Increase ``min_data_in_leaf`` and ``min_sum_hessian_in_leaf`` +************************************************************* + +Depending on the size of the training data and the distribution of features, it's possible for LightGBM to add tree nodes that only describe a small number of observations. In the most extreme case, consider the addition of a tree node that only a single observation from the training data falls into. This is very unlikely to generalize well, and probably is a sign of overfitting. + +This can be prevented indirectly with parameters like ``max_depth`` and ``num_leaves``, but LightGBM also offers parameters to help you directly avoid adding these overly-specific tree nodes. + +- ``min_data_in_leaf``: Minimum number of observations that must fall into a tree node for it to be added. +- ``min_sum_hessian_in_leaf``: Minimum sum of the Hessian (second derivative of the objective function evaluated for each observation) for observations in a leaf. For some regression objectives, this is just the minimum number of records that have to fall into each node. For classification objectives, it represents a sum over a distribution of probabilities. See `this Stack Overflow answer `_ for a good description of how to reason about values of this parameter. + +Grow Less Trees +''''''''''''''' + +Decrease ``num_iterations`` +*************************** + +The ``num_iterations`` parameter controls the number of boosting rounds that will be performed. Since LightGBM uses decision trees as the learners, this can also be thought of as "number of trees". + +If you try changing ``num_iterations``, change the ``learning_rate`` as well. ``learning_rate`` will not have any impact on training time, but it will impact the training accuracy. As a general rule, if you reduce ``num_iterations``, you should increase ``learning_rate``. + +Choosing the right value of ``num_iterations`` and ``learning_rate`` is highly dependent on the data and objective, so these parameters are often chosen from a set of possible values through hyperparameter tuning. + +Decrease ``num_iterations`` to reduce training time. + +Use Early Stopping +****************** + +If early stopping is enabled, after each boosting round the model's training accuracy is evaluated against a validation set that contains data not available to the training process. That accuracy is then compared to the accuracy as of the previous boosting round. If the model's accuracy fails to improve for some number of consecutive rounds, LightGBM stops the training process. + +That "number of consecutive rounds" is controlled by the parameter ``early_stopping_round``. For example, ``early_stopping_round=1`` says "the first time accuracy on the validation set does not improve, stop training". + +Set ``early_stopping_round`` and provide a validation set to possibly reduce training time. + +Consider Fewer Splits +''''''''''''''''''''' + +The parameters described in previous sections control how many trees are constructed and how many nodes are constructed per tree. Training time can be further reduced by reducing the amount of time needed to add a tree node to the model. + +The suggestions below will speed up training, but might hurt training accuracy. + +Enable Feature Pre-Filtering When Creating Dataset +************************************************** + +By default, when a LightGBM ``Dataset`` object is constructed, some features will be filtered out based on the value of ``min_data_in_leaf``. + +For a simple example, consider a 1000-observation dataset with a feature called ``feature_1``. ``feature_1`` takes on only two values: 25.0 (995 observations) and 50.0 (5 observations). If ``min_data_in_leaf = 10``, there is no split for this feature which will result in a valid split at least one of the leaf nodes will only have 5 observations. + +Instead of reconsidering this feature and then ignoring it every iteration, LightGBM filters this feature out at before training, when the ``Dataset`` is constructed. + +If this default behavior has been overridden by setting ``feature_pre_filter=False``, set ``feature_pre_filter=True`` to reduce training time. + +Decrease ``max_bin`` or ``max_bin_by_feature`` When Creating Dataset +******************************************************************** + +LightGBM training `buckets continuous features into discrete bins <./Features.rst#optimization-in-speed-and-memory-usage>`_ to improve training speed and reduce memory requirements for training. This binning is done one time during ``Dataset`` construction. The number of splits considered when adding a node is ``O(#feature * #bin)``, so reducing the number of bins per feature can reduce the number of splits that need to be evaluated. + +``max_bin`` is controls the maximum number of bins that features will bucketed into. It is also possible to set this maximum feature-by-feature, by passing ``max_bin_by_feature``. + +Reduce ``max_bin`` or ``max_bin_by_feature`` to reduce training time. + +Increase ``min_data_in_bin`` When Creating Dataset +************************************************** + +Some bins might contain a small number of observations, which might mean that the effort of evaluating that bin's boundaries as possible split points isn't likely to change the final model very much. You can control the granularity of the bins by setting ``min_data_in_bin``. + +Increase ``min_data_in_bin`` to reduce training time. + +Decrease ``feature_fraction`` +***************************** + +By default, LightGBM considers all features in a ``Dataset`` during the training process. This behavior can be changed by setting ``feature_fraction`` to a value ``> 0`` and ``<= 1.0``. Setting ``feature_fraction`` to ``0.5``, for example, tells LightGBM to randomly select ``50%`` of features at the beginning of constructing each tree. This reduces the total number of splits that have to be evaluated to add each tree node. + +Decrease ``feature_fraction`` to reduce training time. + +Decrease ``max_cat_threshold`` +****************************** + +LightGBM uses a `custom approach for finding optimal splits for categorical features <./Advanced-Topics.html#categorical-feature-support>`_. In this process, LightGBM explores splits that break a categorical feature into two groups. These are sometimes called "k-vs.-rest" splits. Higher ``max_cat_threshold`` values correspond to more split points and larger possible group sizes to search. + +Decrease ``max_cat_threshold`` to reduce training time. + +Use Less Data +''''''''''''' + +Use Bagging +*********** + +By default, LightGBM uses all observations in the training data for each iteration. It is possible to instead tell LightGBM to randomly sample the training data. This process of training over multiple random samples without replacement is called "bagging". + +Set ``bagging_freq`` to an integer greater than 0 to control how often a new sample is drawn. Set ``bagging_fraction`` to a value ``> 0.0`` and ``< 1.0`` to control the size of the sample. For example, ``{"bagging_freq": 5, "bagging_fraction": 0.75}`` tells LightGBM "re-sample without replacement every 5 iterations, and draw samples of 75% of the training data". + +Decrease ``bagging_fraction`` to reduce training time. + + +Save Constructed Datasets with ``save_binary`` +'''''''''''''''''''''''''''''''''''''''''''''' + +This only applies to the LightGBM CLI. If you pass parameter ``save_binary``, the training dataset and all validations sets will be saved in a binary format understood by LightGBM. This can speed up training next time, because binning and other work done when constructing a ``Dataset`` does not have to be re-done. + + +For Better Accuracy +------------------- + +- Use large ``max_bin`` (may be slower) + +- Use small ``learning_rate`` with large ``num_iterations`` + +- Use large ``num_leaves`` (may cause over-fitting) + +- Use bigger training data + +- Try ``dart`` + +Deal with Over-fitting +---------------------- + +- Use small ``max_bin`` + +- Use small ``num_leaves`` + +- Use ``min_data_in_leaf`` and ``min_sum_hessian_in_leaf`` + +- Use bagging by set ``bagging_fraction`` and ``bagging_freq`` + +- Use feature sub-sampling by set ``feature_fraction`` + +- Use bigger training data + +- Try ``lambda_l1``, ``lambda_l2`` and ``min_gain_to_split`` for regularization + +- Try ``max_depth`` to avoid growing deep tree + +- Try ``extra_trees`` + +- Try increasing ``path_smooth`` + +.. _Optuna: https://medium.com/optuna/lightgbm-tuner-new-optuna-integration-for-hyperparameter-optimization-8b7095e99258 + +.. _FLAML: https://github.com/microsoft/FLAML diff --git a/docs/Parameters.rst b/docs/Parameters.rst new file mode 100644 index 0000000..9aaea5b --- /dev/null +++ b/docs/Parameters.rst @@ -0,0 +1,1483 @@ +.. List of parameters is auto generated by LightGBM\.ci\parameter-generator.py from LightGBM\include\LightGBM\config.h file. + +.. role:: raw-html(raw) + :format: html + +Parameters +========== + +This page contains descriptions of all parameters in LightGBM. + +**List of other helpful links** + +- `Python API <./Python-API.rst>`__ + +- `Parameters Tuning <./Parameters-Tuning.rst>`__ + +Parameters Format +----------------- + +Parameters are merged together in the following order (later items overwrite earlier ones): + +1. LightGBM's default values +2. special files for ``weight``, ``init_score``, ``query``, and ``positions`` (see `Others <#others>`__) +3. (CLI only) configuration in a file passed like ``config=train.conf`` +4. (CLI only) configuration passed via the command line +5. (Python, R) special keyword arguments to some functions (e.g. ``num_boost_round`` in ``train()``) +6. (Python, R) ``params`` function argument (including ``**kwargs`` in Python and ``...`` in R) +7. (C API) ``parameters`` or ``params`` function argument + +Many parameters have "aliases", alternative names which refer to the same configuration. + +Where a mix of the primary parameter name and aliases are given, the primary parameter name is always preferred to any aliases. + +For example, in Python: + +.. code-block:: python + + # use learning rate of 0.07, because 'learning_rate' + # is the primary parameter name + lgb.train( + params={ + "learning_rate": 0.07, + "shrinkage_rate": 0.12 + }, + train_set=dtrain + ) + +Where multiple aliases are given, and the primary parameter name is not, the first alias +appearing in the lists returned by ``Config::parameter2aliases()`` in the C++ library is used. +Those lists are hard-coded in a fairly arbitrary way... wherever possible, avoid relying on this behavior. + +For example, in Python: + +.. code-block:: python + + # use learning rate of 0.12, LightGBM has a hard-coded preference for 'shrinkage_rate' + # over any other aliases, and 'learning_rate' is not provided + lgb.train( + params={ + "eta": 0.19, + "shrinkage_rate": 0.12 + }, + train_set=dtrain + ) + +**CLI** + +The parameters format is ``key1=value1 key2=value2 ...``. +Parameters can be set both in config file and command line. +By using command line, parameters should not have spaces before and after ``=``. +By using config files, one line can only contain one parameter. You can use ``#`` to comment. + +**Python** + +Any parameters that accept multiple values should be passed as a Python list. + +.. code-block:: python + + params = { + "monotone_constraints": [-1, 0, 1] + } + + +**R** + +Any parameters that accept multiple values should be passed as an R list. + +.. code-block:: r + + params <- list( + monotone_constraints = c(-1, 0, 1) + ) + +.. start params list + +Core Parameters +--------------- + +- ``config`` :raw-html:`🔗︎`, default = ``""``, type = string, aliases: ``config_file`` + + - path of config file + + - **Note**: can be used only in CLI version + +- ``task`` :raw-html:`🔗︎`, default = ``train``, type = enum, options: ``train``, ``predict``, ``convert_model``, ``refit``, aliases: ``task_type`` + + - ``train``, for training, aliases: ``training`` + + - ``predict``, for prediction, aliases: ``prediction``, ``test`` + + - ``convert_model``, for converting model file into if-else format, see more information in `Convert Parameters <#convert-parameters>`__ + + - ``refit``, for refitting existing models with new data, aliases: ``refit_tree`` + + - ``save_binary``, load train (and validation) data then save dataset to binary file. Typical usage: ``save_binary`` first, then run multiple ``train`` tasks in parallel using the saved binary file + + - **Note**: can be used only in CLI version; for language-specific packages you can use the correspondent functions + +- ``objective`` :raw-html:`🔗︎`, default = ``regression``, type = enum, options: ``regression``, ``regression_l1``, ``huber``, ``fair``, ``poisson``, ``quantile``, ``mape``, ``gamma``, ``tweedie``, ``binary``, ``multiclass``, ``multiclassova``, ``cross_entropy``, ``cross_entropy_lambda``, ``lambdarank``, ``rank_xendcg``, aliases: ``objective_type``, ``app``, ``application``, ``loss`` + + - regression application + + - ``regression``, L2 loss, aliases: ``regression_l2``, ``l2``, ``mean_squared_error``, ``mse``, ``l2_root``, ``root_mean_squared_error``, ``rmse`` + + - ``regression_l1``, L1 loss, aliases: ``l1``, ``mean_absolute_error``, ``mae`` + + - ``huber``, `Huber loss `__ + + - ``fair``, `Fair loss `__ + + - ``poisson``, `Poisson regression `__ + + - ``quantile``, `Quantile regression `__ + + - ``mape``, `MAPE loss `__, aliases: ``mean_absolute_percentage_error`` + + - ``gamma``, Gamma regression with log-link. It might be useful, e.g., for modeling insurance claims severity, or for any target that might be `gamma-distributed `__ + + - ``tweedie``, Tweedie regression with log-link. It might be useful, e.g., for modeling total loss in insurance, or for any target that might be `tweedie-distributed `__ + + - binary classification application + + - ``binary``, binary `log loss `__ classification (or logistic regression) + + - requires labels in {0, 1}; see ``cross-entropy`` application for general probability labels in [0, 1] + + - multi-class classification application + + - ``multiclass``, `softmax `__ objective function, aliases: ``softmax`` + + - ``multiclassova``, `One-vs-All `__ binary objective function, aliases: ``multiclass_ova``, ``ova``, ``ovr`` + + - ``num_class`` should be set as well + + - cross-entropy application + + - ``cross_entropy``, objective function for cross-entropy (with optional linear weights), aliases: ``xentropy`` + + - ``cross_entropy_lambda``, alternative parameterization of cross-entropy, aliases: ``xentlambda`` + + - label is anything in interval [0, 1] + + - ranking application + + - ``lambdarank``, `lambdarank `__ objective. `label_gain <#label_gain>`__ can be used to set the gain (weight) of ``int`` label and all values in ``label`` must be smaller than number of elements in ``label_gain`` + + - ``rank_xendcg``, `XE_NDCG_MART `__ ranking objective function, aliases: ``xendcg``, ``xe_ndcg``, ``xe_ndcg_mart``, ``xendcg_mart`` + + - ``rank_xendcg`` is faster than and achieves the similar performance as ``lambdarank`` + + - label should be ``int`` type, and larger number represents the higher relevance (e.g. 0:bad, 1:fair, 2:good, 3:perfect) + + - custom objective function (gradients and hessians not computed directly by LightGBM) + + - ``custom`` + + - must be passed through parameters explicitly in the C API + + - **Note**: cannot be used in CLI version + +- ``boosting`` :raw-html:`🔗︎`, default = ``gbdt``, type = enum, options: ``gbdt``, ``rf``, ``dart``, aliases: ``boosting_type``, ``boost`` + + - ``gbdt``, traditional Gradient Boosting Decision Tree, aliases: ``gbrt`` + + - ``rf``, Random Forest, aliases: ``random_forest`` + + - ``dart``, `Dropouts meet Multiple Additive Regression Trees `__ + + - **Note**: internally, LightGBM uses ``gbdt`` mode for the first ``1 / learning_rate`` iterations + +- ``data_sample_strategy`` :raw-html:`🔗︎`, default = ``bagging``, type = enum, options: ``bagging``, ``goss`` + + - ``bagging``, Randomly Bagging Sampling + + - **Note**: ``bagging`` is only effective when ``bagging_freq > 0`` and ``bagging_fraction < 1.0`` + + - ``goss``, Gradient-based One-Side Sampling + + - *New in version 4.0.0* + +- ``data`` :raw-html:`🔗︎`, default = ``""``, type = string, aliases: ``train``, ``train_data``, ``train_data_file``, ``data_filename`` + + - path of training data, LightGBM will train from this data + + - **Note**: can be used only in CLI version + +- ``valid`` :raw-html:`🔗︎`, default = ``""``, type = string, aliases: ``test``, ``valid_data``, ``valid_data_file``, ``test_data``, ``test_data_file``, ``valid_filenames`` + + - path(s) of validation/test data, LightGBM will output metrics for these data + + - support multiple validation data, separated by ``,`` + + - **Note**: can be used only in CLI version + +- ``num_iterations`` :raw-html:`🔗︎`, default = ``100``, type = int, aliases: ``num_iteration``, ``n_iter``, ``num_tree``, ``num_trees``, ``num_round``, ``num_rounds``, ``nrounds``, ``num_boost_round``, ``n_estimators``, ``max_iter``, constraints: ``num_iterations >= 0`` + + - number of boosting iterations + + - **Note**: internally, LightGBM constructs ``num_class * num_iterations`` trees for multi-class classification problems + +- ``learning_rate`` :raw-html:`🔗︎`, default = ``0.1``, type = double, aliases: ``shrinkage_rate``, ``eta``, constraints: ``learning_rate > 0.0`` + + - shrinkage rate + + - in ``dart``, it also affects on normalization weights of dropped trees + +- ``num_leaves`` :raw-html:`🔗︎`, default = ``31``, type = int, aliases: ``num_leaf``, ``max_leaves``, ``max_leaf``, ``max_leaf_nodes``, constraints: ``1 < num_leaves <= 131072`` + + - max number of leaves in one tree + +- ``tree_learner`` :raw-html:`🔗︎`, default = ``serial``, type = enum, options: ``serial``, ``feature``, ``data``, ``voting``, aliases: ``tree``, ``tree_type``, ``tree_learner_type`` + + - ``serial``, single machine tree learner + + - ``feature``, feature parallel tree learner, aliases: ``feature_parallel`` + + - ``data``, data parallel tree learner, aliases: ``data_parallel`` + + - ``voting``, voting parallel tree learner, aliases: ``voting_parallel`` + + - refer to `Distributed Learning Guide <./Parallel-Learning-Guide.rst>`__ to get more details + +- ``num_threads`` :raw-html:`🔗︎`, default = ``0``, type = int, aliases: ``num_thread``, ``nthread``, ``nthreads``, ``n_jobs`` + + - used only in ``train``, ``prediction`` and ``refit`` tasks or in correspondent functions of language-specific packages + + - number of threads for LightGBM + + - ``0`` means default number of threads in OpenMP + + - for the best speed, set this to the number of **real CPU cores**, not the number of threads (most CPUs use `hyper-threading `__ to generate 2 threads per CPU core) + + - do not set it too large if your dataset is small (for instance, do not use 64 threads for a dataset with 10,000 rows) + + - be aware a task manager or any similar CPU monitoring tool might report that cores not being fully utilized. **This is normal** + + - for distributed learning, do not use all CPU cores because this will cause poor performance for the network communication + + - **Note**: please **don't** change this during training, especially when running multiple jobs simultaneously by external packages, otherwise it may cause undesirable errors + +- ``device_type`` :raw-html:`🔗︎`, default = ``cpu``, type = enum, options: ``cpu``, ``gpu``, ``cuda``, aliases: ``device`` + + - device for the tree learning + + - ``cpu`` supports all LightGBM functionality and is portable across the widest range of operating systems and hardware + + - ``cuda`` offers faster training than ``gpu`` or ``cpu``, but only works on GPUs supporting CUDA or ROCm + + - ``gpu`` can be faster than ``cpu`` and works on a wider range of GPUs than CUDA + + - **Note**: it is recommended to use the smaller ``max_bin`` (e.g. 63) to get the better speed up + + - **Note**: for the faster speed, GPU uses 32-bit float point to sum up by default, so this may affect the accuracy for some tasks. You can set ``gpu_use_dp=true`` to enable 64-bit float point, but it will slow down the training + + - **Note**: refer to `Installation Guide <./Installation-Guide.rst>`__ to build LightGBM with GPU, CUDA, or ROCm support + +- ``seed`` :raw-html:`🔗︎`, default = ``None``, type = int, aliases: ``random_seed``, ``random_state`` + + - this seed is used to generate other seeds, e.g. ``data_random_seed``, ``feature_fraction_seed``, etc. + + - by default, this seed is unused in favor of default values of other seeds + + - this seed has lower priority in comparison with other seeds, which means that it will be overridden, if you set other seeds explicitly + +- ``deterministic`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only with ``cpu`` device type + + - setting this to ``true`` should ensure the stable results when using the same data and the same parameters (and different ``num_threads``) + + - when you use the different seeds, different LightGBM versions, the binaries compiled by different compilers, or in different systems, the results are expected to be different + + - you can `raise issues `__ in LightGBM GitHub repo when you meet the unstable results + + - **Note**: setting this to ``true`` may slow down the training + + - **Note**: to avoid potential instability due to numerical issues, please set ``force_col_wise=true`` or ``force_row_wise=true`` when setting ``deterministic=true`` + +Learning Control Parameters +--------------------------- + +- ``force_col_wise`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only with ``cpu`` device type + + - set this to ``true`` to force col-wise histogram building + + - enabling this is recommended when: + + - the number of columns is large, or the total number of bins is large + + - ``num_threads`` is large, e.g. ``> 20`` + + - you want to reduce memory cost + + - **Note**: when both ``force_col_wise`` and ``force_row_wise`` are ``false``, LightGBM will firstly try them both, and then use the faster one. To remove the overhead of testing set the faster one to ``true`` manually + + - **Note**: this parameter cannot be used at the same time with ``force_row_wise``, choose only one of them + +- ``force_row_wise`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only with ``cpu`` device type + + - set this to ``true`` to force row-wise histogram building + + - enabling this is recommended when: + + - the number of data points is large, and the total number of bins is relatively small + + - ``num_threads`` is relatively small, e.g. ``<= 16`` + + - you want to use small ``bagging_fraction`` or ``goss`` sample strategy to speed up + + - **Note**: setting this to ``true`` will double the memory cost for Dataset object. If you have not enough memory, you can try setting ``force_col_wise=true`` + + - **Note**: when both ``force_col_wise`` and ``force_row_wise`` are ``false``, LightGBM will firstly try them both, and then use the faster one. To remove the overhead of testing set the faster one to ``true`` manually + + - **Note**: this parameter cannot be used at the same time with ``force_col_wise``, choose only one of them + +- ``histogram_pool_size`` :raw-html:`🔗︎`, default = ``-1.0``, type = double, aliases: ``hist_pool_size`` + + - max cache size in MB for historical histogram + + - ``< 0`` means no limit + +- ``max_depth`` :raw-html:`🔗︎`, default = ``-1``, type = int + + - limit the max depth for tree model. This is used to deal with over-fitting when ``#data`` is small. Tree still grows leaf-wise + + - ``<= 0`` means no limit + +- ``min_data_in_leaf`` :raw-html:`🔗︎`, default = ``20``, type = int, aliases: ``min_data_per_leaf``, ``min_data``, ``min_child_samples``, ``min_samples_leaf``, constraints: ``min_data_in_leaf >= 0`` + + - minimal number of data in one leaf. Can be used to deal with over-fitting + + - **Note**: this is an approximation based on the Hessian, so occasionally you may observe splits which produce leaf nodes that have less than this many observations + +- ``min_sum_hessian_in_leaf`` :raw-html:`🔗︎`, default = ``1e-3``, type = double, aliases: ``min_sum_hessian_per_leaf``, ``min_sum_hessian``, ``min_hessian``, ``min_child_weight``, constraints: ``min_sum_hessian_in_leaf >= 0.0`` + + - minimal sum hessian in one leaf. Like ``min_data_in_leaf``, it can be used to deal with over-fitting + +- ``bagging_fraction`` :raw-html:`🔗︎`, default = ``1.0``, type = double, aliases: ``sub_row``, ``subsample``, ``bagging``, constraints: ``0.0 < bagging_fraction <= 1.0`` + + - like ``feature_fraction``, but this will randomly select part of data without resampling + + - can be used to speed up training + + - can be used to deal with over-fitting + + - **Note**: to enable bagging, ``bagging_freq`` should be set to a non zero value as well + +- ``pos_bagging_fraction`` :raw-html:`🔗︎`, default = ``1.0``, type = double, aliases: ``pos_sub_row``, ``pos_subsample``, ``pos_bagging``, constraints: ``0.0 < pos_bagging_fraction <= 1.0`` + + - used only in ``binary`` application + + - used for imbalanced binary classification problem, will randomly sample ``#pos_samples * pos_bagging_fraction`` positive samples in bagging + + - should be used together with ``neg_bagging_fraction`` + + - set this to ``1.0`` to disable + + - **Note**: to enable this, you need to set ``bagging_freq`` and ``neg_bagging_fraction`` as well + + - **Note**: if both ``pos_bagging_fraction`` and ``neg_bagging_fraction`` are set to ``1.0``, balanced bagging is disabled + + - **Note**: if balanced bagging is enabled, ``bagging_fraction`` will be ignored + +- ``neg_bagging_fraction`` :raw-html:`🔗︎`, default = ``1.0``, type = double, aliases: ``neg_sub_row``, ``neg_subsample``, ``neg_bagging``, constraints: ``0.0 < neg_bagging_fraction <= 1.0`` + + - used only in ``binary`` application + + - used for imbalanced binary classification problem, will randomly sample ``#neg_samples * neg_bagging_fraction`` negative samples in bagging + + - should be used together with ``pos_bagging_fraction`` + + - set this to ``1.0`` to disable + + - **Note**: to enable this, you need to set ``bagging_freq`` and ``pos_bagging_fraction`` as well + + - **Note**: if both ``pos_bagging_fraction`` and ``neg_bagging_fraction`` are set to ``1.0``, balanced bagging is disabled + + - **Note**: if balanced bagging is enabled, ``bagging_fraction`` will be ignored + +- ``bagging_freq`` :raw-html:`🔗︎`, default = ``0``, type = int, aliases: ``subsample_freq`` + + - frequency for bagging + + - ``0`` means disable bagging; ``k`` means perform bagging at every ``k`` iteration. Every ``k``-th iteration, LightGBM will randomly select ``bagging_fraction * 100%`` of the data to use for the next ``k`` iterations + + - **Note**: bagging is only effective when ``0.0 < bagging_fraction < 1.0`` + +- ``bagging_seed`` :raw-html:`🔗︎`, default = ``3``, type = int, aliases: ``bagging_fraction_seed`` + + - random seed for bagging + +- ``bagging_by_query`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - whether to do bagging sample by query + + - *New in version 4.6.0* + +- ``feature_fraction`` :raw-html:`🔗︎`, default = ``1.0``, type = double, aliases: ``sub_feature``, ``colsample_bytree``, constraints: ``0.0 < feature_fraction <= 1.0`` + + - LightGBM will randomly select a subset of features on each iteration (tree) if ``feature_fraction`` is smaller than ``1.0``. For example, if you set it to ``0.8``, LightGBM will select 80% of features before training each tree + + - can be used to speed up training + + - can be used to deal with over-fitting + +- ``feature_fraction_bynode`` :raw-html:`🔗︎`, default = ``1.0``, type = double, aliases: ``sub_feature_bynode``, ``colsample_bynode``, constraints: ``0.0 < feature_fraction_bynode <= 1.0`` + + - LightGBM will randomly select a subset of features on each tree node if ``feature_fraction_bynode`` is smaller than ``1.0``. For example, if you set it to ``0.8``, LightGBM will select 80% of features at each tree node + + - can be used to deal with over-fitting + + - **Note**: unlike ``feature_fraction``, this cannot speed up training + + - **Note**: if both ``feature_fraction`` and ``feature_fraction_bynode`` are smaller than ``1.0``, the final fraction of each node is ``feature_fraction * feature_fraction_bynode`` + +- ``feature_fraction_seed`` :raw-html:`🔗︎`, default = ``2``, type = int + + - random seed for ``feature_fraction`` + +- ``extra_trees`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``extra_tree`` + + - use extremely randomized trees + + - if set to ``true``, when evaluating node splits LightGBM will check only one randomly-chosen threshold for each feature + + - can be used to speed up training + + - can be used to deal with over-fitting + +- ``extra_seed`` :raw-html:`🔗︎`, default = ``6``, type = int + + - random seed for selecting thresholds when ``extra_trees`` is true + +- ``early_stopping_round`` :raw-html:`🔗︎`, default = ``0``, type = int, aliases: ``early_stopping_rounds``, ``early_stopping``, ``n_iter_no_change`` + + - will stop training if one metric of one validation data doesn't improve in last ``early_stopping_round`` rounds + + - ``<= 0`` means disable + + - can be used to speed up training + +- ``early_stopping_min_delta`` :raw-html:`🔗︎`, default = ``0.0``, type = double, constraints: ``early_stopping_min_delta >= 0.0`` + + - when early stopping is used (i.e. ``early_stopping_round > 0``), require the early stopping metric to improve by at least this delta to be considered an improvement + + - *New in version 4.4.0* + +- ``first_metric_only`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - LightGBM allows you to provide multiple evaluation metrics. Set this to ``true``, if you want to use only the first metric for early stopping + +- ``max_delta_step`` :raw-html:`🔗︎`, default = ``0.0``, type = double, aliases: ``max_tree_output``, ``max_leaf_output`` + + - used to limit the max output of tree leaves + + - ``<= 0`` means no constraint + + - the final max output of leaves is ``learning_rate * max_delta_step`` + +- ``lambda_l1`` :raw-html:`🔗︎`, default = ``0.0``, type = double, aliases: ``reg_alpha``, ``l1_regularization``, constraints: ``lambda_l1 >= 0.0`` + + - L1 regularization + +- ``lambda_l2`` :raw-html:`🔗︎`, default = ``0.0``, type = double, aliases: ``reg_lambda``, ``lambda``, ``l2_regularization``, constraints: ``lambda_l2 >= 0.0`` + + - L2 regularization + +- ``linear_lambda`` :raw-html:`🔗︎`, default = ``0.0``, type = double, constraints: ``linear_lambda >= 0.0`` + + - linear tree regularization, corresponds to the parameter ``lambda`` in Eq. 3 of `Gradient Boosting with Piece-Wise Linear Regression Trees `__ + +- ``min_gain_to_split`` :raw-html:`🔗︎`, default = ``0.0``, type = double, aliases: ``min_split_gain``, constraints: ``min_gain_to_split >= 0.0`` + + - the minimal gain to perform split + + - can be used to speed up training + +- ``drop_rate`` :raw-html:`🔗︎`, default = ``0.1``, type = double, aliases: ``rate_drop``, constraints: ``0.0 <= drop_rate <= 1.0`` + + - used only in ``dart`` + + - dropout rate: a fraction of previous trees to drop during the dropout + +- ``max_drop`` :raw-html:`🔗︎`, default = ``50``, type = int + + - used only in ``dart`` + + - max number of dropped trees during one boosting iteration + + - ``<=0`` means no limit + +- ``skip_drop`` :raw-html:`🔗︎`, default = ``0.5``, type = double, constraints: ``0.0 <= skip_drop <= 1.0`` + + - used only in ``dart`` + + - probability of skipping the dropout procedure during a boosting iteration + +- ``xgboost_dart_mode`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only in ``dart`` + + - set this to ``true``, if you want to use XGBoost DART mode + +- ``uniform_drop`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only in ``dart`` + + - set this to ``true``, if you want to use uniform drop + +- ``drop_seed`` :raw-html:`🔗︎`, default = ``4``, type = int + + - used only in ``dart`` + + - random seed to choose dropping models + +- ``top_rate`` :raw-html:`🔗︎`, default = ``0.2``, type = double, constraints: ``0.0 <= top_rate <= 1.0`` + + - used only in ``goss`` + + - the retain ratio of large gradient data + +- ``other_rate`` :raw-html:`🔗︎`, default = ``0.1``, type = double, constraints: ``0.0 <= other_rate <= 1.0`` + + - used only in ``goss`` + + - the retain ratio of small gradient data + +- ``min_data_per_group`` :raw-html:`🔗︎`, default = ``100``, type = int, constraints: ``min_data_per_group > 0`` + + - used for the categorical features + + - minimal number of data per categorical group + +- ``max_cat_threshold`` :raw-html:`🔗︎`, default = ``32``, type = int, constraints: ``max_cat_threshold > 0`` + + - used for the categorical features + + - limit number of split points considered for categorical features. See `the documentation on how LightGBM finds optimal splits for categorical features <./Features.rst#optimal-split-for-categorical-features>`_ for more details + + - can be used to speed up training + +- ``cat_l2`` :raw-html:`🔗︎`, default = ``10.0``, type = double, constraints: ``cat_l2 >= 0.0`` + + - used for the categorical features + + - L2 regularization in categorical split + +- ``cat_smooth`` :raw-html:`🔗︎`, default = ``10.0``, type = double, constraints: ``cat_smooth >= 0.0`` + + - used for the categorical features + + - this can reduce the effect of noises in categorical features, especially for categories with few data + +- ``max_cat_to_onehot`` :raw-html:`🔗︎`, default = ``4``, type = int, constraints: ``max_cat_to_onehot > 0`` + + - used for the categorical features + + - when number of categories of one feature smaller than or equal to ``max_cat_to_onehot``, one-vs-other split algorithm will be used + +- ``top_k`` :raw-html:`🔗︎`, default = ``20``, type = int, aliases: ``topk``, constraints: ``top_k > 0`` + + - used only in ``voting`` tree learner, refer to `Voting parallel <./Parallel-Learning-Guide.rst#choose-appropriate-parallel-algorithm>`__ + + - set this to larger value for more accurate result, but it will slow down the training speed + +- ``monotone_constraints`` :raw-html:`🔗︎`, default = ``None``, type = multi-int, aliases: ``mc``, ``monotone_constraint``, ``monotonic_cst`` + + - used for constraints of monotonic features + + - ``1`` means increasing, ``-1`` means decreasing, ``0`` means non-constraint + + - you need to specify all features in order. For example, ``mc=-1,0,1`` means decreasing for the 1st feature, non-constraint for the 2nd feature and increasing for the 3rd feature + +- ``monotone_constraints_method`` :raw-html:`🔗︎`, default = ``basic``, type = enum, options: ``basic``, ``intermediate``, ``advanced``, aliases: ``monotone_constraining_method``, ``mc_method`` + + - used only if ``monotone_constraints`` is set + + - monotone constraints method + + - ``basic``, the most basic monotone constraints method. It does not slow down the training speed at all, but over-constrains the predictions + + - ``intermediate``, a `more advanced method `__, which may slow down the training speed very slightly. However, this method is much less constraining than the basic method and should significantly improve the results + + - ``advanced``, an `even more advanced method `__, which may slow down the training speed. However, this method is even less constraining than the intermediate method and should again significantly improve the results + +- ``monotone_penalty`` :raw-html:`🔗︎`, default = ``0.0``, type = double, aliases: ``monotone_splits_penalty``, ``ms_penalty``, ``mc_penalty``, constraints: ``monotone_penalty >= 0.0`` + + - used only if ``monotone_constraints`` is set + + - `monotone penalty `__: a penalization parameter X forbids any monotone splits on the first X (rounded down) level(s) of the tree. The penalty applied to monotone splits on a given depth is a continuous, increasing function the penalization parameter + + - if ``0.0`` (the default), no penalization is applied + +- ``feature_contri`` :raw-html:`🔗︎`, default = ``None``, type = multi-double, aliases: ``feature_contrib``, ``fc``, ``fp``, ``feature_penalty`` + + - used to control feature's split gain, will use ``gain[i] = max(0, feature_contri[i]) * gain[i]`` to replace the split gain of i-th feature + + - you need to specify all features in order + +- ``forcedsplits_filename`` :raw-html:`🔗︎`, default = ``""``, type = string, aliases: ``fs``, ``forced_splits_filename``, ``forced_splits_file``, ``forced_splits`` + + - path to a ``.json`` file that specifies splits to force at the top of every decision tree before best-first learning commences + + - ``.json`` file can be arbitrarily nested, and each split contains ``feature``, ``threshold`` fields, as well as ``left`` and ``right`` fields representing subsplits + + - categorical splits are forced in a one-hot fashion, with ``left`` representing the split containing the feature value and ``right`` representing other values + + - **Note**: the forced split logic will be ignored, if the split makes gain worse + + - see `this file `__ as an example + +- ``refit_decay_rate`` :raw-html:`🔗︎`, default = ``0.9``, type = double, constraints: ``0.0 <= refit_decay_rate <= 1.0`` + + - decay rate of ``refit`` task, will use ``leaf_output = refit_decay_rate * old_leaf_output + (1.0 - refit_decay_rate) * new_leaf_output`` to refit trees + + - used only in ``refit`` task in CLI version or as argument in ``refit`` function in language-specific package + +- ``cegb_tradeoff`` :raw-html:`🔗︎`, default = ``1.0``, type = double, constraints: ``cegb_tradeoff >= 0.0`` + + - cost-effective gradient boosting multiplier for all penalties + +- ``cegb_penalty_split`` :raw-html:`🔗︎`, default = ``0.0``, type = double, constraints: ``cegb_penalty_split >= 0.0`` + + - cost-effective gradient-boosting penalty for splitting a node + +- ``cegb_penalty_feature_lazy`` :raw-html:`🔗︎`, default = ``0,0,...,0``, type = multi-double + + - cost-effective gradient boosting penalty for using a feature + + - applied per data point + +- ``cegb_penalty_feature_coupled`` :raw-html:`🔗︎`, default = ``0,0,...,0``, type = multi-double + + - cost-effective gradient boosting penalty for using a feature + + - applied once per forest + +- ``path_smooth`` :raw-html:`🔗︎`, default = ``0``, type = double, constraints: ``path_smooth >= 0.0`` + + - controls smoothing applied to tree nodes + + - helps prevent overfitting on leaves with few samples + + - if ``0.0`` (the default), no smoothing is applied + + - if ``path_smooth > 0`` then ``min_data_in_leaf`` must be at least ``2`` + + - larger values give stronger regularization + + - the weight of each node is ``w * (n / path_smooth) / (n / path_smooth + 1) + w_p / (n / path_smooth + 1)``, where ``n`` is the number of samples in the node, ``w`` is the optimal node weight to minimise the loss (approximately ``-sum_gradients / sum_hessians``), and ``w_p`` is the weight of the parent node + + - note that the parent output ``w_p`` itself has smoothing applied, unless it is the root node, so that the smoothing effect accumulates with the tree depth + +- ``interaction_constraints`` :raw-html:`🔗︎`, default = ``""``, type = string + + - controls which features can appear in the same branch + + - by default interaction constraints are disabled, to enable them you can specify + + - for CLI, lists separated by commas, e.g. ``[0,1,2],[2,3]`` + + - for Python-package, list of lists, e.g. ``[[0, 1, 2], [2, 3]]`` + + - for R-package, list of character or numeric vectors, e.g. ``list(c("var1", "var2", "var3"), c("var3", "var4"))`` or ``list(c(1L, 2L, 3L), c(3L, 4L))``. Numeric vectors should use 1-based indexing, where ``1L`` is the first feature, ``2L`` is the second feature, etc. + + - any two features can only appear in the same branch only if there exists a constraint containing both features + +- ``verbosity`` :raw-html:`🔗︎`, default = ``1``, type = int, aliases: ``verbose`` + + - controls the level of LightGBM's verbosity + + - ``< 0``: Fatal, ``= 0``: Error (Warning), ``= 1``: Info, ``> 1``: Debug + +- ``input_model`` :raw-html:`🔗︎`, default = ``""``, type = string, aliases: ``model_input``, ``model_in`` + + - filename of input model + + - for ``prediction`` task, this model will be applied to prediction data + + - for ``train`` task, training will be continued from this model + + - **Note**: can be used only in CLI version + +- ``output_model`` :raw-html:`🔗︎`, default = ``LightGBM_model.txt``, type = string, aliases: ``model_output``, ``model_out`` + + - filename of output model in training + + - **Note**: can be used only in CLI version + +- ``saved_feature_importance_type`` :raw-html:`🔗︎`, default = ``0``, type = int + + - the feature importance type in the saved model file + + - ``0``: count-based feature importance (numbers of splits are counted); ``1``: gain-based feature importance (values of gain are counted) + + - **Note**: can be used only in CLI version + +- ``snapshot_freq`` :raw-html:`🔗︎`, default = ``-1``, type = int, aliases: ``save_period`` + + - frequency of saving model file snapshot + + - set this to positive value to enable this function. For example, the model file will be snapshotted at each iteration if ``snapshot_freq=1`` + + - **Note**: can be used only in CLI version + +- ``use_quantized_grad`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - whether to use gradient quantization when training + + - enabling this will discretize (quantize) the gradients and hessians into bins of ``num_grad_quant_bins`` + + - with quantized training, most arithmetics in the training process will be integer operations + + - gradient quantization can accelerate training, with little accuracy drop in most cases + + - **Note**: works only with ``cpu`` and ``cuda`` device type + + - *New in version 4.0.0* + +- ``num_grad_quant_bins`` :raw-html:`🔗︎`, default = ``4``, type = int + + - used only if ``use_quantized_grad=true`` + + - number of bins to quantization gradients and hessians + + - with more bins, the quantized training will be closer to full precision training + + - **Note**: works only with ``cpu`` and ``cuda`` device type + + - *New in version 4.0.0* + +- ``quant_train_renew_leaf`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only if ``use_quantized_grad=true`` + + - whether to renew the leaf values with original gradients when quantized training + + - renewing is very helpful for good quantized training accuracy for ranking objectives + + - **Note**: works only with ``cpu`` and ``cuda`` device type + + - *New in version 4.0.0* + +- ``stochastic_rounding`` :raw-html:`🔗︎`, default = ``true``, type = bool + + - used only if ``use_quantized_grad=true`` + + - whether to use stochastic rounding in gradient quantization + + - **Note**: works only with ``cpu`` and ``cuda`` device type + + - *New in version 4.0.0* + +IO Parameters +------------- + +Dataset Parameters +~~~~~~~~~~~~~~~~~~ + +- ``linear_tree`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``linear_trees`` + + - fit piecewise linear gradient boosting tree + + - tree splits are chosen in the usual way, but the model at each leaf is linear instead of constant + + - the linear model at each leaf includes all the numerical features in that leaf's branch + + - the first tree has constant leaf values + + - categorical features are used for splits as normal but are not used in the linear models + + - missing values should not be encoded as ``0``. Use ``np.nan`` for Python, ``NA`` for the CLI, and ``NA``, ``NA_real_``, or ``NA_integer_`` for R + + - it is recommended to rescale data before training so that features have similar mean and standard deviation + + - **Note**: works only with ``cpu``, ``gpu`` device type and ``serial`` tree learner + + - **Note**: ``regression_l1`` objective is not supported with linear tree boosting + + - **Note**: setting ``linear_tree=true`` significantly increases the memory use of LightGBM + + - **Note**: if you specify ``monotone_constraints``, constraints will be enforced when choosing the split points, but not when fitting the linear models on leaves + +- ``max_bin`` :raw-html:`🔗︎`, default = ``255``, type = int, aliases: ``max_bins``, constraints: ``max_bin > 1`` + + - max number of bins that feature values will be bucketed in + + - small number of bins may reduce training accuracy but may increase general power (deal with over-fitting) + + - LightGBM will auto compress memory according to ``max_bin``. For example, LightGBM will use ``uint8_t`` for feature value if ``max_bin=255`` + +- ``max_bin_by_feature`` :raw-html:`🔗︎`, default = ``None``, type = multi-int + + - max number of bins for each feature + + - if not specified, will use ``max_bin`` for all features + +- ``min_data_in_bin`` :raw-html:`🔗︎`, default = ``3``, type = int, constraints: ``min_data_in_bin > 0`` + + - minimal number of data inside one bin + + - use this to avoid one-data-one-bin (potential over-fitting) + +- ``bin_construct_sample_cnt`` :raw-html:`🔗︎`, default = ``200000``, type = int, aliases: ``subsample_for_bin``, constraints: ``bin_construct_sample_cnt > 0`` + + - number of data that sampled to construct feature discrete bins + + - setting this to larger value will give better training result, but may increase data loading time + + - set this to larger value if data is very sparse + + - **Note**: don't set this to small values, otherwise, you may encounter unexpected errors and poor accuracy + +- ``data_random_seed`` :raw-html:`🔗︎`, default = ``1``, type = int, aliases: ``data_seed`` + + - random seed for sampling data to construct histogram bins + +- ``is_enable_sparse`` :raw-html:`🔗︎`, default = ``true``, type = bool, aliases: ``is_sparse``, ``enable_sparse``, ``sparse`` + + - used to enable/disable sparse optimization + +- ``enable_bundle`` :raw-html:`🔗︎`, default = ``true``, type = bool, aliases: ``is_enable_bundle``, ``bundle`` + + - set this to ``false`` to disable Exclusive Feature Bundling (EFB), which is described in `LightGBM: A Highly Efficient Gradient Boosting Decision Tree `__ + + - **Note**: disabling this may cause the slow training speed for sparse datasets + +- ``use_missing`` :raw-html:`🔗︎`, default = ``true``, type = bool + + - set this to ``false`` to disable the special handle of missing value + +- ``zero_as_missing`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - set this to ``true`` to treat all zero as missing values (including the unshown values in LibSVM / sparse matrices) + + - set this to ``false`` to use ``na`` for representing missing values + +- ``feature_pre_filter`` :raw-html:`🔗︎`, default = ``true``, type = bool + + - set this to ``true`` (the default) to tell LightGBM to ignore the features that are unsplittable based on ``min_data_in_leaf`` + + - as dataset object is initialized only once and cannot be changed after that, you may need to set this to ``false`` when searching parameters with ``min_data_in_leaf``, otherwise features are filtered by ``min_data_in_leaf`` firstly if you don't reconstruct dataset object + + - **Note**: setting this to ``false`` may slow down the training + +- ``pre_partition`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``is_pre_partition`` + + - used for distributed learning (excluding the ``feature_parallel`` mode) + + - ``true`` if training data are pre-partitioned, and different machines use different partitions + +- ``two_round`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``two_round_loading``, ``use_two_round_loading`` + + - set this to ``true`` if data file is too big to fit in memory + + - by default, LightGBM will map data file to memory and load features from memory. This will provide faster data loading speed, but may cause run out of memory error when the data file is very big + + - **Note**: works only in case of loading data directly from text file + +- ``header`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``has_header`` + + - set this to ``true`` if input data has header + + - **Note**: works only in case of loading data directly from text file + +- ``label_column`` :raw-html:`🔗︎`, default = ``""``, type = int or string, aliases: ``label`` + + - used to specify the label column + + - use number for index, e.g. ``label=0`` means column\_0 is the label + + - add a prefix ``name:`` for column name, e.g. ``label=name:is_click`` + + - if omitted, the first column in the training data is used as the label + + - **Note**: works only in case of loading data directly from text file + +- ``weight_column`` :raw-html:`🔗︎`, default = ``""``, type = int or string, aliases: ``weight`` + + - used to specify the weight column + + - use number for index, e.g. ``weight=0`` means column\_0 is the weight + + - add a prefix ``name:`` for column name, e.g. ``weight=name:weight`` + + - **Note**: works only in case of loading data directly from text file + + - **Note**: index starts from ``0`` and it doesn't count the label column when passing type is ``int``, e.g. when label is column\_0, and weight is column\_1, the correct parameter is ``weight=0`` + + - **Note**: weights should be non-negative + +- ``group_column`` :raw-html:`🔗︎`, default = ``""``, type = int or string, aliases: ``group``, ``group_id``, ``query_column``, ``query``, ``query_id`` + + - used to specify the query/group id column + + - use number for index, e.g. ``query=0`` means column\_0 is the query id + + - add a prefix ``name:`` for column name, e.g. ``query=name:query_id`` + + - **Note**: works only in case of loading data directly from text file + + - **Note**: data should be grouped by query\_id, for more information, see `Query Data <#query-data>`__ + + - **Note**: index starts from ``0`` and it doesn't count the label column when passing type is ``int``, e.g. when label is column\_0 and query\_id is column\_1, the correct parameter is ``query=0`` + +- ``ignore_column`` :raw-html:`🔗︎`, default = ``""``, type = multi-int or string, aliases: ``ignore_feature``, ``blacklist`` + + - used to specify some ignoring columns in training + + - use number for index, e.g. ``ignore_column=0,1,2`` means column\_0, column\_1 and column\_2 will be ignored + + - add a prefix ``name:`` for column name, e.g. ``ignore_column=name:c1,c2,c3`` means c1, c2 and c3 will be ignored + + - **Note**: works only in case of loading data directly from text file + + - **Note**: index starts from ``0`` and it doesn't count the label column when passing type is ``int`` + + - **Note**: despite the fact that specified columns will be completely ignored during the training, they still should have a valid format allowing LightGBM to load file successfully + +- ``categorical_feature`` :raw-html:`🔗︎`, default = ``""``, type = multi-int or string, aliases: ``cat_feature``, ``categorical_column``, ``cat_column``, ``categorical_features`` + + - used to specify categorical features + + - use number for index, e.g. ``categorical_feature=0,1,2`` means column\_0, column\_1 and column\_2 are categorical features + + - add a prefix ``name:`` for column name, e.g. ``categorical_feature=name:c1,c2,c3`` means c1, c2 and c3 are categorical features + + - **Note**: all values will be cast to ``int32`` (integer codes will be extracted from pandas categoricals in the Python-package) + + - **Note**: index starts from ``0`` and it doesn't count the label column when passing type is ``int`` + + - **Note**: all values should be less than ``Int32.MaxValue`` (2147483647) + + - **Note**: using large values could be memory consuming. Tree decision rule works best when categorical features are presented by consecutive integers starting from zero + + - **Note**: all negative values will be treated as **missing values** + + - **Note**: the output cannot be monotonically constrained with respect to a categorical feature + + - **Note**: floating point numbers in categorical features will be rounded towards 0 + +- ``forcedbins_filename`` :raw-html:`🔗︎`, default = ``""``, type = string + + - path to a ``.json`` file that specifies bin upper bounds for some or all features + + - ``.json`` file should contain an array of objects, each containing the word ``feature`` (integer feature index) and ``bin_upper_bound`` (array of thresholds for binning) + + - see `this file `__ as an example + +- ``save_binary`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``is_save_binary``, ``is_save_binary_file`` + + - if ``true``, LightGBM will save the dataset (including validation data) to a binary file. This speed ups the data loading for the next time + + - **Note**: ``init_score`` is not saved in binary file + + - **Note**: can be used only in CLI version; for language-specific packages you can use the correspondent function + +- ``precise_float_parser`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - use precise floating point number parsing for text parser (e.g. CSV, TSV, LibSVM input) + + - **Note**: setting this to ``true`` may lead to much slower text parsing + +- ``parser_config_file`` :raw-html:`🔗︎`, default = ``""``, type = string + + - path to a ``.json`` file that specifies customized parser initialized configuration + + - see `lightgbm-transform `__ for usage examples + + - **Note**: ``lightgbm-transform`` is not maintained by LightGBM's maintainers. Bug reports or feature requests should go to `issues page `__ + + - *New in version 4.0.0* + +Predict Parameters +~~~~~~~~~~~~~~~~~~ + +- ``start_iteration_predict`` :raw-html:`🔗︎`, default = ``0``, type = int + + - used only in ``prediction`` task + + - used to specify from which iteration to start the prediction + + - ``<= 0`` means from the first iteration + +- ``num_iteration_predict`` :raw-html:`🔗︎`, default = ``-1``, type = int + + - used only in ``prediction`` task + + - used to specify how many trained iterations will be used in prediction + + - ``<= 0`` means no limit + +- ``predict_raw_score`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``is_predict_raw_score``, ``predict_rawscore``, ``raw_score`` + + - used only in ``prediction`` task + + - set this to ``true`` to predict only the raw scores + + - set this to ``false`` to predict transformed scores + +- ``predict_leaf_index`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``is_predict_leaf_index``, ``leaf_index`` + + - used only in ``prediction`` task + + - set this to ``true`` to predict with leaf index of all trees + +- ``predict_contrib`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``is_predict_contrib``, ``contrib`` + + - used only in ``prediction`` task + + - set this to ``true`` to estimate `SHAP values `__, which represent how each feature contributes to each prediction + + - produces ``#features + 1`` values where the last value is the expected value of the model output over the training data + + - **Note**: if you want to get more explanation for your model's predictions using SHAP values like SHAP interaction values, you can install `shap package `__ + + - **Note**: unlike the shap package, with ``predict_contrib`` we return a matrix with an extra column, where the last column is the expected value + + - **Note**: this feature is not implemented for linear trees + +- ``predict_disable_shape_check`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only in ``prediction`` task + + - control whether or not LightGBM raises an error when you try to predict on data with a different number of features than the training data + + - if ``false`` (the default), a fatal error will be raised if the number of features in the dataset you predict on differs from the number seen during training + + - if ``true``, LightGBM will attempt to predict on whatever data you provide. This is dangerous because you might get incorrect predictions, but you could use it in situations where it is difficult or expensive to generate some features and you are very confident that they were never chosen for splits in the model + + - **Note**: be very careful setting this parameter to ``true`` + +- ``pred_early_stop`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only in ``prediction`` task + + - used only in ``classification`` and ``ranking`` applications + + - used only for predicting normal or raw scores + + - if ``true``, will use early-stopping to speed up the prediction. May affect the accuracy + + - **Note**: cannot be used with ``rf`` boosting type or custom objective function + +- ``pred_early_stop_freq`` :raw-html:`🔗︎`, default = ``10``, type = int + + - used only in ``prediction`` task and if ``pred_early_stop=true`` + + - the frequency of checking early-stopping prediction + +- ``pred_early_stop_margin`` :raw-html:`🔗︎`, default = ``10.0``, type = double + + - used only in ``prediction`` task and if ``pred_early_stop=true`` + + - the threshold of margin in early-stopping prediction + +- ``output_result`` :raw-html:`🔗︎`, default = ``LightGBM_predict_result.txt``, type = string, aliases: ``predict_result``, ``prediction_result``, ``predict_name``, ``prediction_name``, ``pred_name``, ``name_pred`` + + - used only in ``prediction`` task + + - filename of prediction result + + - **Note**: can be used only in CLI version + +Convert Parameters +~~~~~~~~~~~~~~~~~~ + +- ``convert_model_language`` :raw-html:`🔗︎`, default = ``""``, type = string + + - used only in ``convert_model`` task + + - only ``cpp`` is supported yet; for conversion model to other languages consider using `m2cgen `__ utility + + - if ``convert_model_language`` is set and ``task=train``, the model will be also converted + + - **Note**: can be used only in CLI version + +- ``convert_model`` :raw-html:`🔗︎`, default = ``gbdt_prediction.cpp``, type = string, aliases: ``convert_model_file`` + + - used only in ``convert_model`` task + + - output filename of converted model + + - **Note**: can be used only in CLI version + +Objective Parameters +-------------------- + +- ``objective_seed`` :raw-html:`🔗︎`, default = ``5``, type = int + + - used only in ``rank_xendcg`` objective + + - random seed for objectives, if random process is needed + +- ``num_class`` :raw-html:`🔗︎`, default = ``1``, type = int, aliases: ``num_classes``, constraints: ``num_class > 0`` + + - used only in ``multi-class`` classification application + +- ``is_unbalance`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``unbalance``, ``unbalanced_sets`` + + - used only in ``binary`` and ``multiclassova`` applications + + - set this to ``true`` if training data are unbalanced + + - **Note**: while enabling this should increase the overall performance metric of your model, it will also result in poor estimates of the individual class probabilities + + - **Note**: this parameter cannot be used at the same time with ``scale_pos_weight``, choose only **one** of them + +- ``scale_pos_weight`` :raw-html:`🔗︎`, default = ``1.0``, type = double, constraints: ``scale_pos_weight > 0.0`` + + - used only in ``binary`` and ``multiclassova`` applications + + - weight of labels with positive class + + - **Note**: while enabling this should increase the overall performance metric of your model, it will also result in poor estimates of the individual class probabilities + + - **Note**: this parameter cannot be used at the same time with ``is_unbalance``, choose only **one** of them + +- ``sigmoid`` :raw-html:`🔗︎`, default = ``1.0``, type = double, constraints: ``sigmoid > 0.0`` + + - used only in ``binary`` and ``multiclassova`` classification and in ``lambdarank`` applications + + - parameter for the sigmoid function + +- ``boost_from_average`` :raw-html:`🔗︎`, default = ``true``, type = bool + + - used only in ``regression``, ``binary``, ``multiclassova`` and ``cross-entropy`` applications + + - adjusts initial score to the mean of labels for faster convergence + +- ``reg_sqrt`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - used only in ``regression`` application + + - used to fit ``sqrt(label)`` instead of original values and prediction result will be also automatically converted to ``prediction^2`` + + - might be useful in case of large-range labels + +- ``alpha`` :raw-html:`🔗︎`, default = ``0.9``, type = double, constraints: ``alpha > 0.0`` + + - used only in ``huber`` and ``quantile`` ``regression`` applications + + - parameter for `Huber loss `__ and `Quantile regression `__ + +- ``fair_c`` :raw-html:`🔗︎`, default = ``1.0``, type = double, constraints: ``fair_c > 0.0`` + + - used only in ``fair`` ``regression`` application + + - parameter for `Fair loss `__ + +- ``poisson_max_delta_step`` :raw-html:`🔗︎`, default = ``0.7``, type = double, constraints: ``poisson_max_delta_step > 0.0`` + + - used only in ``poisson`` ``regression`` application + + - parameter for `Poisson regression `__ to safeguard optimization + +- ``tweedie_variance_power`` :raw-html:`🔗︎`, default = ``1.5``, type = double, constraints: ``1.0 <= tweedie_variance_power < 2.0`` + + - used only in ``tweedie`` ``regression`` application + + - used to control the variance of the tweedie distribution + + - set this closer to ``2`` to shift towards a **Gamma** distribution + + - set this closer to ``1`` to shift towards a **Poisson** distribution + +- ``lambdarank_truncation_level`` :raw-html:`🔗︎`, default = ``30``, type = int, constraints: ``lambdarank_truncation_level > 0`` + + - used only in ``lambdarank`` application + + - controls the number of top-results to focus on during training, refer to "truncation level" in the Sec. 3 of `LambdaMART paper `__ + + - this parameter is closely related to the desirable cutoff ``k`` in the metric **NDCG@k** that we aim at optimizing the ranker for. The optimal setting for this parameter is likely to be slightly higher than ``k`` (e.g., ``k + 3``) to include more pairs of documents to train on, but perhaps not too high to avoid deviating too much from the desired target metric **NDCG@k** + +- ``lambdarank_norm`` :raw-html:`🔗︎`, default = ``true``, type = bool + + - used only in ``lambdarank`` application + + - set this to ``true`` to normalize the lambdas for different queries, and improve the performance for unbalanced data + + - set this to ``false`` to enforce the original lambdarank algorithm + +- ``label_gain`` :raw-html:`🔗︎`, default = ``0,1,3,7,15,31,63,...,2^30-1``, type = multi-double + + - used only in ``lambdarank`` application + + - relevant gain for labels. For example, the gain of label ``2`` is ``3`` in case of default label gains + + - separate by ``,`` + +- ``lambdarank_position_bias_regularization`` :raw-html:`🔗︎`, default = ``0.0``, type = double, constraints: ``lambdarank_position_bias_regularization >= 0.0`` + + - used only in ``lambdarank`` application when positional information is provided and position bias is modeled + + - larger values reduce the inferred position bias factors + + - *New in version 4.1.0* + +Metric Parameters +----------------- + +- ``metric`` :raw-html:`🔗︎`, default = ``""``, type = multi-enum, aliases: ``metrics``, ``metric_types`` + + - metric(s) to be evaluated on the evaluation set(s) + + - ``""`` (empty string or not specified) means that metric corresponding to specified ``objective`` will be used (this is possible only for pre-defined objective functions, otherwise no evaluation metric will be added) + + - ``"None"`` (string, **not** a ``None`` value) means that no metric will be registered, aliases: ``na``, ``null``, ``custom`` + + - ``l1``, absolute loss, aliases: ``mean_absolute_error``, ``mae``, ``regression_l1`` + + - ``l2``, square loss, aliases: ``mean_squared_error``, ``mse``, ``regression_l2``, ``regression`` + + - ``rmse``, root square loss, aliases: ``root_mean_squared_error``, ``l2_root`` + + - ``quantile``, `Quantile regression `__ + + - ``mape``, `MAPE loss `__, aliases: ``mean_absolute_percentage_error`` + + - ``huber``, `Huber loss `__ + + - ``fair``, `Fair loss `__ + + - ``poisson``, negative log-likelihood for `Poisson regression `__ + + - ``gamma``, negative log-likelihood for **Gamma** regression + + - ``gamma_deviance``, residual deviance for **Gamma** regression + + - ``tweedie``, negative log-likelihood for **Tweedie** regression + + - ``ndcg``, `NDCG `__, aliases: ``lambdarank``, ``rank_xendcg``, ``xendcg``, ``xe_ndcg``, ``xe_ndcg_mart``, ``xendcg_mart`` + + - ``map``, `MAP `__, aliases: ``mean_average_precision`` + + - ``auc``, `AUC `__ + + - ``average_precision``, `average precision score `__ + + - ``r2``, `R-squared `__ + + - ``binary_logloss``, `log loss `__, aliases: ``binary`` + + - ``binary_error``, for one sample: ``0`` for correct classification, ``1`` for error classification + + - ``auc_mu``, `AUC-mu `__ + + - ``multi_logloss``, log loss for multi-class classification, aliases: ``multiclass``, ``softmax``, ``multiclassova``, ``multiclass_ova``, ``ova``, ``ovr`` + + - ``multi_error``, error rate for multi-class classification + + - ``cross_entropy``, cross-entropy (with optional linear weights), aliases: ``xentropy`` + + - ``cross_entropy_lambda``, "intensity-weighted" cross-entropy, aliases: ``xentlambda`` + + - ``kullback_leibler``, `Kullback-Leibler divergence `__, aliases: ``kldiv`` + + - support multiple metrics, separated by ``,`` + +- ``metric_freq`` :raw-html:`🔗︎`, default = ``1``, type = int, aliases: ``output_freq``, constraints: ``metric_freq > 0`` + + - frequency for metric output + + - **Note**: can be used only in CLI version + +- ``is_provide_training_metric`` :raw-html:`🔗︎`, default = ``false``, type = bool, aliases: ``training_metric``, ``is_training_metric``, ``train_metric`` + + - set this to ``true`` to output metric result over training dataset + + - **Note**: can be used only in CLI version + +- ``eval_at`` :raw-html:`🔗︎`, default = ``1,2,3,4,5``, type = multi-int, aliases: ``ndcg_eval_at``, ``ndcg_at``, ``map_eval_at``, ``map_at`` + + - used only with ``ndcg`` and ``map`` metrics + + - `NDCG `__ and `MAP `__ evaluation positions, separated by ``,`` + +- ``multi_error_top_k`` :raw-html:`🔗︎`, default = ``1``, type = int, constraints: ``multi_error_top_k > 0`` + + - used only with ``multi_error`` metric + + - threshold for top-k multi-error metric + + - the error on each sample is ``0`` if the true class is among the top ``multi_error_top_k`` predictions, and ``1`` otherwise + + - more precisely, the error on a sample is ``0`` if there are at least ``num_classes - multi_error_top_k`` predictions strictly less than the prediction on the true class + + - when ``multi_error_top_k=1`` this is equivalent to the usual multi-error metric + +- ``auc_mu_weights`` :raw-html:`🔗︎`, default = ``None``, type = multi-double + + - used only with ``auc_mu`` metric + + - list representing flattened matrix (in row-major order) giving loss weights for classification errors + + - list should have ``n * n`` elements, where ``n`` is the number of classes + + - the matrix co-ordinate ``[i, j]`` should correspond to the ``i * n + j``-th element of the list + + - if not specified, will use equal weights for all classes + +Network Parameters +------------------ + +- ``num_machines`` :raw-html:`🔗︎`, default = ``1``, type = int, aliases: ``num_machine``, constraints: ``num_machines > 0`` + + - the number of machines for distributed learning application + + - this parameter is needed to be set in both **socket** and **MPI** versions + +- ``local_listen_port`` :raw-html:`🔗︎`, default = ``12400 (random for Dask-package)``, type = int, aliases: ``local_port``, ``port``, constraints: ``local_listen_port > 0`` + + - TCP listen port for local machines + + - **Note**: don't forget to allow this port in firewall settings before training + +- ``time_out`` :raw-html:`🔗︎`, default = ``120``, type = int, constraints: ``time_out > 0`` + + - socket time-out in minutes + +- ``machine_list_filename`` :raw-html:`🔗︎`, default = ``""``, type = string, aliases: ``machine_list_file``, ``machine_list``, ``mlist`` + + - path of file that lists machines for this distributed learning application + + - each line contains one IP and one port for one machine. The format is ``ip port`` (space as a separator) + + - **Note**: can be used only in CLI version + +- ``machines`` :raw-html:`🔗︎`, default = ``""``, type = string, aliases: ``workers``, ``nodes`` + + - list of machines in the following format: ``ip1:port1,ip2:port2`` + +GPU Parameters +-------------- + +- ``gpu_platform_id`` :raw-html:`🔗︎`, default = ``-1``, type = int + + - used only with ``gpu`` device type + + - OpenCL platform ID. Usually each GPU vendor exposes one OpenCL platform + + - ``-1`` means the system-wide default platform + + - **Note**: refer to `GPU Targets <./GPU-Targets.rst#query-opencl-devices-in-your-system>`__ for more details + +- ``gpu_device_id`` :raw-html:`🔗︎`, default = ``-1``, type = int + + - OpenCL device ID in the specified platform or CUDA device ID. Each GPU in the selected platform has a unique device ID + + - ``-1`` means the default device in the selected platform + + - in multi-GPU case (``num_gpu>1``) means ID of the master GPU + + - **Note**: refer to `GPU Targets <./GPU-Targets.rst#query-opencl-devices-in-your-system>`__ for more details + +- ``gpu_device_id_list`` :raw-html:`🔗︎`, default = ``""``, type = string + + - list of CUDA device IDs + + - **Note**: can be used only in CUDA implementation (``device_type="cuda"``) and when ``num_gpu>1`` + + - if empty, the devices with the smallest IDs will be used + +- ``gpu_use_dp`` :raw-html:`🔗︎`, default = ``false``, type = bool + + - set this to ``true`` to use double precision math on GPU (by default single precision is used) + + - **Note**: can be used only in OpenCL implementation (``device_type="gpu"``), in CUDA implementation only double precision is currently supported + +- ``num_gpu`` :raw-html:`🔗︎`, default = ``1``, type = int, constraints: ``num_gpu > 0`` + + - number of GPUs used for training in this node + + - **Note**: can be used only in CUDA implementation (``device_type="cuda"``) + + - if ``0``, only 1 GPU will be used + + - used in both single-machine and distributed learning applications + + - in distributed learning application, each machine can use different number of GPUs + +.. end params list + +Others +------ + +Continued Training with Input Score +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +LightGBM supports continued training with initial scores. +It uses an additional file to store these initial scores, like the following: + +:: + + 0.5 + -0.1 + 0.9 + ... + +It means the initial score of the first data row is ``0.5``, second is ``-0.1``, and so on. +The initial score file corresponds with data file line by line, and has per score per line. + +If the name of data file is ``train.txt``, the initial score file should be named as ``train.txt.init`` and placed in the same folder as the data file. +In this case, LightGBM will auto load initial score file if it exists. + +If binary data files exist for raw data file ``train.txt``, for example in the name ``train.txt.bin``, then the initial score file should be named as ``train.txt.bin.init``. + +Weight Data +~~~~~~~~~~~ + +LightGBM supports weighted training. +It uses an additional file to store weight data, like the following: + +:: + + 1.0 + 0.5 + 0.8 + ... + +It means the weight of the first data row is ``1.0``, second is ``0.5``, and so on. Weights should be non-negative. + +The weight file corresponds with data file line by line, and has per weight per line. + +And if the name of data file is ``train.txt``, the weight file should be named as ``train.txt.weight`` and placed in the same folder as the data file. +In this case, LightGBM will load the weight file automatically if it exists. + +Also, you can include weight column in your data file. +Please refer to the ``weight_column`` `parameter <#weight_column>`__ in above. + +Query Data +~~~~~~~~~~ + +For learning to rank, it needs query information for training data. + +LightGBM uses an additional file to store query data, like the following: + +:: + + 27 + 18 + 67 + ... + +For wrapper libraries like in Python and R, this information can also be provided as an array-like via the Dataset parameter ``group``. + +:: + + [27, 18, 67, ...] + +For example, if you have a 112-document dataset with ``group = [27, 18, 67]``, that means that you have 3 groups, where the first 27 records are in the first group, records 28-45 are in the second group, and records 46-112 are in the third group. + +**Note**: data should be ordered by the query. + +If the name of data file is ``train.txt``, the query file should be named as ``train.txt.query`` and placed in the same folder as the data file. +In this case, LightGBM will load the query file automatically if it exists. + +Also, you can include query/group id column in your data file. +Please refer to the ``group_column`` `parameter <#group_column>`__ in above. diff --git a/docs/Python-API.rst b/docs/Python-API.rst new file mode 100644 index 0000000..3e0dab7 --- /dev/null +++ b/docs/Python-API.rst @@ -0,0 +1,78 @@ +Python API +========== + +.. currentmodule:: lightgbm + +Data Structure API +------------------ + +.. autosummary:: + :toctree: pythonapi/ + + Dataset + Booster + CVBooster + Sequence + +Training API +------------ + +.. autosummary:: + :toctree: pythonapi/ + + train + cv + +Scikit-learn API +---------------- + +.. autosummary:: + :toctree: pythonapi/ + + LGBMModel + LGBMClassifier + LGBMRegressor + LGBMRanker + +Dask API +-------- + +.. versionadded:: 3.2.0 + +.. autosummary:: + :toctree: pythonapi/ + + DaskLGBMClassifier + DaskLGBMRegressor + DaskLGBMRanker + +Callbacks +--------- + +.. autosummary:: + :toctree: pythonapi/ + + early_stopping + log_evaluation + record_evaluation + reset_parameter + +Plotting +-------- + +.. autosummary:: + :toctree: pythonapi/ + + plot_importance + plot_split_value_histogram + plot_metric + plot_tree + create_tree_digraph + +Utilities +--------- + +.. autosummary:: + :toctree: pythonapi/ + + register_logger diff --git a/docs/Python-Intro.rst b/docs/Python-Intro.rst new file mode 100644 index 0000000..071aa5c --- /dev/null +++ b/docs/Python-Intro.rst @@ -0,0 +1,267 @@ +Python-package Introduction +=========================== + +This document gives a basic walk-through of LightGBM Python-package. + +**List of other helpful links** + +- `Python Examples `__ + +- `Python API <./Python-API.rst>`__ + +- `Parameters Tuning <./Parameters-Tuning.rst>`__ + +Install +------- + +The preferred way to install LightGBM is via pip: + +:: + + pip install lightgbm + +Refer to `Python-package`_ folder for the detailed installation guide. + +To verify your installation, try to ``import lightgbm`` in Python: + +:: + + import lightgbm as lgb + +Data Interface +-------------- + +The LightGBM Python module can load data from: + +- LibSVM (zero-based) / TSV / CSV format text file + +- NumPy 2D array(s), SciPy sparse matrix + +- pandas DataFrame, polars DataFrame, pyarrow Table + +- LightGBM binary file + +- LightGBM ``Sequence`` object(s) + +The data is stored in a ``Dataset`` object. + +Many of the examples in this page use functionality from ``numpy``. To run the examples, be sure to import ``numpy`` in your session. + +.. code:: python + + import numpy as np + +**To load a LibSVM (zero-based) text file or a LightGBM binary file into Dataset:** + +.. code:: python + + train_data = lgb.Dataset('train.svm.bin') + +**To load a numpy array into Dataset:** + +.. code:: python + + rng = np.random.default_rng() + data = rng.uniform(size=(500, 10)) # 500 entities, each contains 10 features + label = rng.integers(low=0, high=2, size=(500, )) # binary target + train_data = lgb.Dataset(data, label=label) + +**To load a scipy.sparse.csr\_matrix array into Dataset:** + +.. code:: python + + import scipy + csr = scipy.sparse.csr_matrix((dat, (row, col))) + train_data = lgb.Dataset(csr) + +**Load from Sequence objects:** + +We can implement ``Sequence`` interface to read binary files. The following example shows reading HDF5 file with ``h5py``. + +.. code:: python + + import h5py + + class HDFSequence(lgb.Sequence): + def __init__(self, hdf_dataset, batch_size): + self.data = hdf_dataset + self.batch_size = batch_size + + def __getitem__(self, idx): + return self.data[idx] + + def __len__(self): + return len(self.data) + + f = h5py.File('train.hdf5', 'r') + train_data = lgb.Dataset(HDFSequence(f['X'], 8192), label=f['Y'][:]) + +Features of using ``Sequence`` interface: + +- Data sampling uses random access, thus does not go through the whole dataset +- Reading data in batch, thus saves memory when constructing ``Dataset`` object +- Supports creating ``Dataset`` from multiple data files + +Please refer to ``Sequence`` `API doc <./Python-API.rst#data-structure-api>`__. + +`dataset_from_multi_hdf5.py `__ is a detailed example. + +**Saving Dataset into a LightGBM binary file will make loading faster:** + +.. code:: python + + train_data = lgb.Dataset('train.svm.txt') + train_data.save_binary('train.bin') + +**Create validation data:** + +.. code:: python + + validation_data = train_data.create_valid('validation.svm') + +or + +.. code:: python + + validation_data = lgb.Dataset('validation.svm', reference=train_data) + +In LightGBM, the validation data should be aligned with training data. + +**Specific feature names and categorical features:** + +.. code:: python + + train_data = lgb.Dataset(data, label=label, feature_name=['c1', 'c2', 'c3'], categorical_feature=['c3']) + +LightGBM can use categorical features as input directly. +It doesn't need to convert to one-hot encoding, and is much faster than one-hot encoding (about 8x speed-up). + +**Note**: You should convert your categorical features to ``int`` type before you construct ``Dataset``. + +**Weights can be set when needed:** + +.. code:: python + + rng = np.random.default_rng() + w = rng.uniform(size=(500, )) + train_data = lgb.Dataset(data, label=label, weight=w) + +or + +.. code:: python + + train_data = lgb.Dataset(data, label=label) + rng = np.random.default_rng() + w = rng.uniform(size=(500, )) + train_data.set_weight(w) + +And you can use ``Dataset.set_init_score()`` to set initial score, and ``Dataset.set_group()`` to set group/query data for ranking tasks. + +**Memory efficient usage:** + +The ``Dataset`` object in LightGBM is very memory-efficient, it only needs to save discrete bins. +However, Numpy/Array/Pandas object is memory expensive. +If you are concerned about your memory consumption, you can save memory by: + +1. Set ``free_raw_data=True`` (default is ``True``) when constructing the ``Dataset`` + +2. Explicitly set ``raw_data=None`` after the ``Dataset`` has been constructed + +3. Call ``gc`` + +Setting Parameters +------------------ + +LightGBM can use a dictionary to set `Parameters <./Parameters.rst>`__. +For instance: + +- Booster parameters: + + .. code:: python + + param = {'num_leaves': 31, 'objective': 'binary'} + param['metric'] = 'auc' + +- You can also specify multiple eval metrics: + + .. code:: python + + param['metric'] = ['auc', 'binary_logloss'] + +Training +-------- + +Training a model requires a parameter list and data set: + +.. code:: python + + num_round = 10 + bst = lgb.train(param, train_data, num_round, valid_sets=[validation_data]) + +After training, the model can be saved: + +.. code:: python + + bst.save_model('model.txt') + +The trained model can also be dumped to JSON format: + +.. code:: python + + json_model = bst.dump_model() + +A saved model can be loaded: + +.. code:: python + + bst = lgb.Booster(model_file='model.txt') # init model + +CV +-- + +Training with 5-fold CV: + +.. code:: python + + lgb.cv(param, train_data, num_round, nfold=5) + +Early Stopping +-------------- + +If you have a validation set, you can use early stopping to find the optimal number of boosting rounds. +Early stopping requires at least one set in ``valid_sets``. If there is more than one, it will use all of them except the training data: + +.. code:: python + + bst = lgb.train(param, train_data, num_round, valid_sets=valid_sets, callbacks=[lgb.early_stopping(stopping_rounds=5)]) + bst.save_model('model.txt', num_iteration=bst.best_iteration) + +The model will train until the validation score stops improving. +Validation score needs to improve at least every ``stopping_rounds`` to continue training. + +The index of iteration that has the best performance will be saved in the ``best_iteration`` field if early stopping logic is enabled by setting ``early_stopping`` callback. +Note that ``train()`` will return a model from the best iteration. + +This works with both metrics to minimize (L2, log loss, etc.) and to maximize (NDCG, AUC, etc.). +Note that if you specify more than one evaluation metric, all of them will be used for early stopping. +However, you can change this behavior and make LightGBM check only the first metric for early stopping by passing ``first_metric_only=True`` in ``early_stopping`` callback constructor. + +Prediction +---------- + +A model that has been trained or loaded can perform predictions on datasets: + +.. code:: python + + # 7 entities, each contains 10 features + rng = np.random.default_rng() + data = rng.uniform(size=(7, 10)) + ypred = bst.predict(data) + +If early stopping is enabled during training, you can get predictions from the best iteration with ``bst.best_iteration``: + +.. code:: python + + ypred = bst.predict(data, num_iteration=bst.best_iteration) + +.. _Python-package: https://github.com/lightgbm-org/LightGBM/tree/main/python-package diff --git a/docs/Quick-Start.rst b/docs/Quick-Start.rst new file mode 100644 index 0000000..0c2b676 --- /dev/null +++ b/docs/Quick-Start.rst @@ -0,0 +1,88 @@ +Quick Start +=========== + +This is a quick start guide for LightGBM CLI version. + +Follow the `Installation Guide <./Installation-Guide.rst>`__ to install LightGBM first. + +**List of other helpful links** + +- `Parameters <./Parameters.rst>`__ + +- `Parameters Tuning <./Parameters-Tuning.rst>`__ + +- `Python-package Quick Start <./Python-Intro.rst>`__ + +- `Python API <./Python-API.rst>`__ + +Training Data Format +-------------------- + +LightGBM supports input data files with `CSV`_, `TSV`_ and `LibSVM`_ (zero-based) formats. + +Files could be both with and without `headers <./Parameters.rst#header>`__. + +`Label column <./Parameters.rst#label_column>`__ could be specified both by index and by name. + +Some columns could be `ignored <./Parameters.rst#ignore_column>`__. + +Categorical Feature Support +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +LightGBM can use categorical features directly (without one-hot encoding). +The experiment on `Expo data`_ shows about 8x speed-up compared with one-hot encoding. + +For the setting details, please refer to the ``categorical_feature`` `parameter <./Parameters.rst#categorical_feature>`__. + +Weight and Query/Group Data +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +LightGBM also supports weighted training, it needs an additional `weight data <./Parameters.rst#weight-data>`__. +And it needs an additional `query data <./Parameters.rst#query-data>`_ for ranking task. + +Also, `weight <./Parameters.rst#weight_column>`__ and `query <./Parameters.rst#group_column>`__ data could be specified as columns in training data in the same manner as label. + +Parameters Quick Look +--------------------- + +The parameters format is ``key1=value1 key2=value2 ...``. + +Parameters can be set both in config file and command line. +If one parameter appears in both command line and config file, LightGBM will use the parameter from the command line. + +The most important parameters which new users should take a look at are located into `Core Parameters <./Parameters.rst#core-parameters>`__ +and the top of `Learning Control Parameters <./Parameters.rst#learning-control-parameters>`__ +sections of the full detailed list of `LightGBM's parameters <./Parameters.rst>`__. + +Run LightGBM +------------ + +:: + + lightgbm config=your_config_file other_args ... + +Parameters can be set both in the config file and command line, and the parameters in command line have higher priority than in the config file. +For example, the following command line will keep ``num_trees=10`` and ignore the same parameter in the config file. + +:: + + lightgbm config=train.conf num_trees=10 + +Examples +-------- + +- `Binary Classification `__ + +- `Regression `__ + +- `Lambdarank `__ + +- `Distributed Learning `__ + +.. _CSV: https://en.wikipedia.org/wiki/Comma-separated_values + +.. _TSV: https://en.wikipedia.org/wiki/Tab-separated_values + +.. _LibSVM: https://www.csie.ntu.edu.tw/~cjlin/libsvm/ + +.. _Expo data: https://community.amstat.org/jointscsg-section/dataexpo/dataexpo2009 diff --git a/docs/README.rst b/docs/README.rst new file mode 100644 index 0000000..5eaac9e --- /dev/null +++ b/docs/README.rst @@ -0,0 +1,69 @@ +Documentation +============= + +Documentation for LightGBM is generated using `Sphinx `__ +and `Breathe `__, which works on top of `Doxygen `__ output. + +List of parameters and their descriptions in `Parameters.rst <./Parameters.rst>`__ +is generated automatically from comments in `config file `__ +by `this script `__. + +After each commit on ``main``, documentation is updated and published to `Read the Docs `__. + +Build +----- + +It is not necessary to re-build this documentation while modifying LightGBM's source code. +The HTML files generated using ``Sphinx`` are not checked into source control. +However, you may want to build them locally during development to test changes. + +Docker +^^^^^^ + +The most reliable way to build the documentation locally is with Docker, using `the same images Read the Docs uses `_. + +Run the following from the root of this repository to pull the relevant image and run a container locally. + +.. code:: sh + + docker run \ + --rm \ + --user=0 \ + -v $(pwd):/opt/LightGBM \ + --env C_API=true \ + --env CONDA=/opt/miniforge \ + --env READTHEDOCS=true \ + --workdir=/opt/LightGBM/docs \ + --entrypoint="" \ + readthedocs/build:ubuntu-24.04-2024.06.17 \ + /bin/bash build-docs.sh + +When that code completes, open ``docs/_build/html/index.html`` in your browser. + +.. note:: + + The navigation in these locally-built docs does not link to the local copy of the R documentation. To view the local version of the R docs, open ``docs/_build/html/R/index.html`` in your browser. + +Without Docker +^^^^^^^^^^^^^^ + +You can build the documentation locally without Docker. Just install Doxygen and run in ``docs`` folder + +.. code:: sh + + pip install breathe sphinx 'sphinx_rtd_theme>=0.5' + make html + +Note that this will not build the R documentation. +Consider using common R utilities for documentation generation, if you need it. +Or use the Docker-based approach described above to build the R documentation locally. + +Optionally, you may also install ``scikit-learn`` and get richer documentation for the classes in ``Scikit-learn API``. + +If you faced any problems with Doxygen installation or you simply do not need documentation for C code, it is possible to build the documentation without it: + +.. code:: sh + + pip install sphinx 'sphinx_rtd_theme>=0.5' + export C_API=NO || set C_API=NO + make html diff --git a/docs/_static/images/dask-concat.svg b/docs/_static/images/dask-concat.svg new file mode 100644 index 0000000..7e0ca1f --- /dev/null +++ b/docs/_static/images/dask-concat.svg @@ -0,0 +1 @@ + diff --git a/docs/_static/images/dask-initial-setup.svg b/docs/_static/images/dask-initial-setup.svg new file mode 100644 index 0000000..ed18049 --- /dev/null +++ b/docs/_static/images/dask-initial-setup.svg @@ -0,0 +1 @@ + diff --git a/docs/_static/images/favicon.ico b/docs/_static/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7aa6981ad0caedc53c5bf19c632b269230f485b1 GIT binary patch literal 34494 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 literal 0 HcmV?d00001 diff --git a/docs/_static/images/gpu-performance-comparison.png b/docs/_static/images/gpu-performance-comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..f26d46bea29cefd1572cb7264aeba503e83f48c7 GIT binary patch literal 110367 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

XOB)(6-@y{^nq#pLSi0grse-BpVf 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%0DQEA+Yj&ul-pV8a}3G*RGvUP?5DNo(?2JaPDZf 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/_static/images/level-wise.png b/docs/_static/images/level-wise.png new file mode 100644 index 0000000000000000000000000000000000000000..b014ccb7ecfeb67b8f2dd8cadc96a7968314f389 GIT binary patch literal 14091 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 literal 0 HcmV?d00001 diff --git a/docs/_static/js/script.js b/docs/_static/js/script.js new file mode 100644 index 0000000..5babca3 --- /dev/null +++ b/docs/_static/js/script.js @@ -0,0 +1,52 @@ +$(() => { + /* Use wider container for the page content */ + $(".wy-nav-content").each(function () { + this.style.setProperty("max-width", "none", "important"); + }); + + /* List each class property item on a new line + https://github.com/lightgbm-org/LightGBM/issues/5073 */ + if (window.location.pathname.toLocaleLowerCase().indexOf("pythonapi") !== -1) { + $(".py.property").each(function () { + this.style.setProperty("display", "inline", "important"); + }); + } + + /* Collapse specified sections in the installation guide */ + if (window.location.pathname.toLocaleLowerCase().indexOf("installation-guide") !== -1) { + $( + '', + ).appendTo("body"); + const collapsible = [ + "#build-threadless-version-not-recommended", + "#build-mpi-version", + "#build-gpu-version", + "#build-cuda-version", + "#build-rocm-version", + "#build-java-wrapper", + "#build-python-package", + "#build-r-package", + "#build-c-unit-tests", + ]; + $.each(collapsible, (_, val) => { + const header = `${val} > :header:first`; + const content = `${val} :not(:header:first)`; + $(header).addClass("closed"); + $(content).hide(); + $(header).click(() => { + $(header).toggleClass("closed opened"); + $(content).slideToggle(0); + }); + }); + /* Uncollapse parent sections when nested section is specified in the URL or before navigate to it from navbar */ + function uncollapse(section) { + section.parents().each((_, val) => { + $(val).children(".closed").click(); + }); + } + uncollapse($(window.location.hash)); + $(".wy-menu.wy-menu-vertical li a.reference.internal").click(function () { + uncollapse($($(this).attr("href"))); + }); + } +}); diff --git a/docs/build-docs.sh b/docs/build-docs.sh new file mode 100644 index 0000000..682c800 --- /dev/null +++ b/docs/build-docs.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e -E -u -o pipefail + +rm -f ./_FIRST_RUN.flag + +export PATH="${CONDA}/bin:${PATH}" + +curl \ + -sL \ + -o "${HOME}/miniforge.sh" \ + https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh + +/bin/bash "${HOME}/miniforge.sh" -b -p "${CONDA}" + +conda config --set always_yes yes --set changeps1 no +conda update -q -y conda + +conda env create \ + --name docs-env \ + --file env.yml || exit 1 + +# shellcheck disable=SC1091 +source activate docs-env +make clean html || exit 1 + +echo "Done building docs. Open docs/_build/html/index.html in a web browser to view them." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..bff6781 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# LightGBM documentation build configuration file, created by +# sphinx-quickstart on Thu May 4 14:30:58 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute. +"""Sphinx configuration file.""" + +import datetime +import os +import sys +from pathlib import Path +from re import compile +from shutil import copytree +from subprocess import PIPE, Popen +from typing import Any, List + +import sphinx +from docutils.nodes import reference +from docutils.parsers.rst import Directive +from docutils.transforms import Transform +from sphinx.application import Sphinx +from sphinx.errors import VersionRequirementError + +CURR_PATH = Path(__file__).absolute().parent +LIB_PATH = CURR_PATH.parent / "python-package" +sys.path.insert(0, str(LIB_PATH)) + +INTERNAL_REF_REGEX = compile(r"(?P\.\/.+)(?P\.rst)(?P$|#)") +RTD_R_REF_REGEX = compile(r"(?Phttps://.+/)(?Platest)(?P/R/reference/)") + + +class InternalRefTransform(Transform): + """Replaces '.rst' with '.html' in all internal links like './[Something].rst[#anchor]'.""" + + default_priority = 210 + """Numerical priority of this transform, 0 through 999.""" + + def apply(self, **kwargs: Any) -> None: + """Apply the transform to the document tree.""" + for section in self.document.traverse(reference): + if section.get("refuri") is not None: + section["refuri"] = INTERNAL_REF_REGEX.sub(r"\g.html\g", section["refuri"]) + + +class IgnoredDirective(Directive): + """Stub for unknown directives.""" + + has_content = True + + def run(self) -> List: + """Do nothing.""" + return [] + + +# -- General configuration ------------------------------------------------ + +os.environ["LIGHTGBM_BUILD_DOC"] = "True" +C_API = os.environ.get("C_API", "").lower().strip() != "no" +RTD = bool(os.environ.get("READTHEDOCS", "")) +RTD_VERSION = os.environ.get("READTHEDOCS_VERSION", "stable") + +# If your documentation needs a minimal Sphinx version, state it here. +needs_sphinx = "2.1.0" # Due to sphinx.ext.napoleon, autodoc_typehints +if needs_sphinx > sphinx.__version__: + message = f"This project needs at least Sphinx v{needs_sphinx}" + raise VersionRequirementError(message) + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", +] + +autodoc_default_flags = ["members", "inherited-members", "show-inheritance"] +autodoc_default_options = { + "members": True, + "inherited-members": True, + "show-inheritance": True, +} +# mock out modules +autodoc_mock_imports = [ + "dask", + "dask.distributed", + "graphviz", + "matplotlib", + "narwhals", + "numpy", + "pandas", + "scipy", + "scipy.sparse", +] +try: + import sklearn # noqa: F401 +except ImportError: + autodoc_mock_imports.append("sklearn") +# hide type hints in API docs +autodoc_typehints = "none" + +# Generate autosummary pages. Output should be set with: `:toctree: pythonapi/` +autosummary_generate = ["Python-API.rst"] + +# Only the class' docstring is inserted. +autoclass_content = "class" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "LightGBM" +copyright = f"{datetime.datetime.now().year}, Microsoft Corporation" +author = "Microsoft Corporation" + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = str(CURR_PATH / "logo" / "LightGBM_logo_grey_text.svg") + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = str(CURR_PATH / "_static" / "images" / "favicon.ico") + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# The short X.Y version. +version = (CURR_PATH.parent / "VERSION.txt").read_text(encoding="utf-8").strip().replace("rc", "-rc") +# The full version, including alpha/beta/rc tags. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "default" + +# -- Configuration for C API docs generation ------------------------------ + +if C_API: + extensions.extend( + [ + "breathe", + ] + ) + breathe_projects = {"LightGBM": str(CURR_PATH / "doxyoutput" / "xml")} + breathe_default_project = "LightGBM" + breathe_domain_by_extension = { + "h": "c", + } + breathe_show_define_initializer = True + c_id_attributes = ["LIGHTGBM_C_EXPORT"] + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + "includehidden": False, + "logo_only": True, +} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = "LightGBMdoc" + +# -- Options for LaTeX output --------------------------------------------- + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +latex_logo = str(CURR_PATH / "logo" / "LightGBM_logo_black_text_small.png") + +# intersphinx configuration +intersphinx_mapping = { + "sklearn": ("https://scikit-learn.org/stable/", None), +} + + +def generate_doxygen_xml(app: Sphinx) -> None: + """Generate XML documentation for C API by Doxygen. + + Parameters + ---------- + app : sphinx.application.Sphinx + The application object representing the Sphinx process. + """ + doxygen_args = [ + f"INPUT={CURR_PATH.parent / 'include' / 'LightGBM' / 'c_api.h'}", + f"OUTPUT_DIRECTORY={CURR_PATH / 'doxyoutput'}", + "GENERATE_HTML=NO", + "GENERATE_LATEX=NO", + "GENERATE_XML=YES", + "XML_OUTPUT=xml", + "XML_PROGRAMLISTING=YES", + r'ALIASES="rst=\verbatim embed:rst:leading-asterisk"', + r'ALIASES+="endrst=\endverbatim"', + "ENABLE_PREPROCESSING=YES", + "MACRO_EXPANSION=YES", + "EXPAND_ONLY_PREDEF=NO", + "SKIP_FUNCTION_MACROS=NO", + "PREDEFINED=__cplusplus", + "SORT_BRIEF_DOCS=YES", + "WARN_AS_ERROR=YES", + ] + doxygen_input = "\n".join(doxygen_args) + doxygen_input = bytes(doxygen_input, "utf-8") + (CURR_PATH / "doxyoutput").mkdir(parents=True, exist_ok=True) + try: + # Warning! The following code can cause buffer overflows on RTD. + # Consider suppressing output completely if RTD project silently fails. + # Refer to https://github.com/svenevs/exhale + # /blob/fe7644829057af622e467bb529db6c03a830da99/exhale/deploy.py#L99-L111 + process = Popen(["doxygen", "-"], stdin=PIPE, stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate(doxygen_input) + output = "\n".join([i.decode("utf-8") for i in (stdout, stderr) if i is not None]) + if process.returncode != 0: + raise RuntimeError(output) + print(output) + except BaseException as e: + raise Exception(f"An error has occurred while executing Doxygen\n{e}") + + +def generate_r_docs(app: Sphinx) -> None: + """Generate documentation for R-package. + + Parameters + ---------- + app : sphinx.application.Sphinx + The application object representing the Sphinx process. + """ + commands = f""" + export TAR=/bin/tar + cd {CURR_PATH.parent} + export R_LIBS="$CONDA_PREFIX/lib/R/library" + sh build-cran-package.sh || exit 1 + R CMD INSTALL --with-keep.source lightgbm_*.tar.gz || exit 1 + Rscript .ci/build-docs.R || exit 1 + """ + try: + print("Building R-package documentation") + # Warning! The following code can cause buffer overflows on RTD. + # Consider suppressing output completely if RTD project silently fails. + # Refer to https://github.com/svenevs/exhale + # /blob/fe7644829057af622e467bb529db6c03a830da99/exhale/deploy.py#L99-L111 + process = Popen(["/bin/bash"], stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True) + stdout, stderr = process.communicate(commands) + output = "\n".join([i for i in (stdout, stderr) if i is not None]) + if process.returncode != 0: + raise RuntimeError(output) + print(output) + print("Done building R-package documentation") + except BaseException as e: + raise Exception(f"An error has occurred while generating documentation for R-package\n{e}") + + +def replace_reference_to_r_docs(app: Sphinx) -> None: + """Make reference to R-package documentation point to the actual version. + + Parameters + ---------- + app : sphinx.application.Sphinx + The application object representing the Sphinx process. + """ + index_doc_path = CURR_PATH / "index.rst" + with open(index_doc_path, "r+t", encoding="utf-8") as index_doc: + content = index_doc.read() + content = RTD_R_REF_REGEX.sub(rf"\g{RTD_VERSION}\g", content) + index_doc.seek(0) + index_doc.write(content) + + +def setup(app: Sphinx) -> None: + """Add new elements at Sphinx initialization time. + + Parameters + ---------- + app : sphinx.application.Sphinx + The application object representing the Sphinx process. + """ + first_run = not (CURR_PATH / "_FIRST_RUN.flag").exists() + if first_run and RTD: + (CURR_PATH / "_FIRST_RUN.flag").touch() + if C_API: + app.connect("builder-inited", generate_doxygen_xml) + else: + app.add_directive("doxygenfile", IgnoredDirective) + if first_run: + app.connect("builder-inited", generate_r_docs) + app.connect( + "build-finished", lambda app, _: copytree(CURR_PATH.parent / "R-package" / "docs", Path(app.outdir) / "R") + ) + app.connect("builder-inited", replace_reference_to_r_docs) + app.add_transform(InternalRefTransform) + add_js_file = getattr(app, "add_js_file", False) or app.add_javascript + add_js_file("js/script.js") diff --git a/docs/env.yml b/docs/env.yml new file mode 100644 index 0000000..1e76529 --- /dev/null +++ b/docs/env.yml @@ -0,0 +1,19 @@ +name: docs-env +channels: + - nodefaults + - conda-forge +dependencies: + - breathe>=4.36 + - doxygen>=1.13.2 + - python=3.14 + - r-base>=4.4 + - r-data.table=1.17.8 + - r-jsonlite=2.0.0 + - r-knitr=1.51 + - r-markdown=2.0 + - r-matrix=1.7_4 + - r-pkgdown=2.2.0 + - r-roxygen2=8.0.0 + - scikit-learn>=1.8.0 + - sphinx>=8.1.3 + - sphinx_rtd_theme>=3.0.1 diff --git a/docs/gcc-Tips.rst b/docs/gcc-Tips.rst new file mode 100644 index 0000000..1d4b6fc --- /dev/null +++ b/docs/gcc-Tips.rst @@ -0,0 +1 @@ +The content of this document was very outdated and is no longer available to avoid any misleadings. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..54ca853 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,58 @@ +.. LightGBM documentation master file, created by + sphinx-quickstart on Thu May 4 14:30:58 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +.. image:: ./logo/LightGBM_logo_black_text.svg + :align: center + :width: 600 + :alt: Light Gradient Boosting Machine logo. + +| + +Welcome to LightGBM's documentation! +==================================== + +**LightGBM** is a gradient boosting framework that uses tree based learning algorithms. It is designed to be distributed and efficient with the following advantages: + +- Faster training speed and higher efficiency. +- Lower memory usage. +- Better accuracy. +- Support of parallel, distributed, and GPU learning. +- Capable of handling large-scale data. + +For more details, please refer to `Features <./Features.rst>`__. + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + Installation Guide + Quick Start + Python Quick Start + Features + Experiments + Parameters + Parameters Tuning + C API + Python API + R API + Distributed Learning Guide + GPU Tutorial + Advanced Topics + FAQ + Development Guide + +.. toctree:: + :hidden: + + GPU-Performance + GPU-Targets + GPU-Windows + gcc-Tips + README + +Indices and Tables +================== + +* :ref:`genindex` diff --git a/docs/logo/LightGBM-logo-hex.cdr b/docs/logo/LightGBM-logo-hex.cdr new file mode 100644 index 0000000000000000000000000000000000000000..97ba1578fbe9fa64bed7e71ddf1465ffc1b06a17 GIT binary patch literal 426020 zcmZ^~1yo#3vo1Vj!r<-}9D=*MySoIJ;O?5>1PH<1HMqOGYjAghyTd=p`@LuV-?{g$ zMbC6sS65X(Rn@zvd!r}=e2)$QKm!1QKk<}EnmP*U006+B0ww`$ENo1jJ?u>l?d`2C zj18SF>}(m_>0E71=#A|hO|9uYjm_vy91Y#*P3(+aY)oyPX*@0L75^g=`me|c`|bcr z7yw{Z1pvSVi!`>gbp|ssm>4=6{{3L02UkyYm+jEiu-}>?rvjmJ`isb#?P8#-TaFV@ z!)y*A!{g{yF+U0oO|K4j*MGWuhps<)5b%f<+I}gz6^r`?iugAW1#RnFm;yzL!F7;8 zXP$4OliB#1nYo!;BiDLfI?Mj$)3_Vgdef*`jKaIl5u)pB$On<%Ksle^cK2}5RC!O* z>tmnvb30@*QC4xM??S0NLW)CoOWZ&mTLLFlJ_sHZH#R%Vo;1ci1@h@khBsH&cb9h> z0st3kqc{nJ85$dHTa&qN@39{qpVS7q)KAC+MbeY)^dAp0MUH}(S4q{=*F%cZKfe8r zZm+PKw&=O~SSpBfsTjCz|Ne}U7zkNp^t>|Ph_m(7Yxm>ysFvokecqDsDal87Vj+&U})@-s}C?BHG|!V zhV3dXlM39_O|O$q=#GBPr))Inc{P6crQ7ccxB$4DWc9YocywK8_Fcv%(R=PO1>e!8 zsjwAXSU_T46o;~jbADHrVDkCk1mgpxSJhlHzp{s5+wyMDWppE6a163bZfOt2=CVzn z{}jOtlxtW&#NJlj8p%)i@v(~iKt@wCPf`mn{$(snQH$b4N% z(GTXR-EW%p{^Tk9*jEe=EowW_n-2v)J>{!q?K{3j_iKBM5{UVla*kj|HVH3`FcCq8 zG?_sRSWc-N;WmtO)Va;S0h9EMY?3Y~>_u~ulxmz0>m@8`(JU@MZ;ONq-jX4fEbt$~ z8mG7j^Vfvf#2T+(6lTMx2}d}6REb=U|8!)ADHEuHQGiT3@VXcw{<84x1N)bcTS!|+ z%Zo>3a=MtMdS@cXRI@&+4*G_C5=`kmB?gDQ!|ix|dgvSP)~;OgcYJD!XfB$VpY-E* zk&$SxMi+GSC_HHmp4~g|!~8GY-bZA@V+&v~z6y5U^8h;qx-SmkCve0t5LYM+#S$~BMpI=E2~y%q(e7u>W<3YBCE3AmDG{%2Rzl;#WrL9fsDJE^4PO>+7@0(bss;+KJ zxvHnT zf64Swn&~ZNbznX0`OjX2X>#1Z4<<5J?6G`o}a~R z%Xh2UdB0XD9xYB6YwNCN`AL}!?QsHw)o-1?|E^Je^6K2=e%MwiKxr1Fk z?{Q_Tf2OSDKqucue3_=iUgip?bwCnbs$J5LD^&>(55Tm+FJ$>)F-k0Tn&mVtUrTFq&m+jXqB=YxS%Z{m zg#AxH(Zpk$|2Q43iwemQ|AX%L%R&NGhz;4Hvh zDXbL2wMs634zQfMhM`2~`Avk2D)9w>+aW=XObENz%CinRoW??I9V_5CfE#Cn?GyIy3C?M>5%A*-eg7bCxKo! zxn7D&>@i%Sw>Slt-6U7dZ_&*N>WbO2_E>r*ow4dyPxA@%M;j$K2lrq%=fV$;XQVW7 zT-?d+TM9__F#kvG%W%Cn9mFeMSY$TaNHHxH zC3)1{8IQGxL*|zczVE4@@jx?hFGlrOzJ5hdliLS2@D}YiXH>O)+tzP+NFP+vDBjZ4 zoN@$cENzBUJA<0}JtffiB5D%)fti$8Sekig#;sZZFV)i`@5q zMLAjLdWd~1LW%9uC&wk{?#*e;|MC7!{W~k^4>2QIX|as=6GiBSpmW@vMb`c)YBMng zfwp=b=aP{J31#3FBJQLeQZB-NsAt;uOD{ogl^70%>S5ko-h+Y5vbqH)5&P{Ih)~<6 zrnwN0iE=1wnU0?YvqxF&5;Jyw9Q$IgAI20Ip1oVxFK%BI{92q|mf{soi@TRo58uvr z8Aw~QlS$~YS3WsCGfs@H%ELb{vrOvGKHN;3Nr2|5<>BXC%+8{4*}3si8sEGLs>1X@aULA%_rl0U=zLP8Y2;qaxRQIttuX|9>&rp#{Dl5Xd^ zWo#teti_NX2wK5QZ^xK}`_qjUPBLY@?~9if=&1^X^kKTJf;&`57A6+9x^vQvnuW`M zoXwdjD-L>E%~t@feSCM1@mt`(w^n@+8xoWd006-|008fQZ>?+$ot#Y_6%EZz|Lm`1 zd`3(5qrE2mZfAdD4@4%?zdXIY37`RiKl@(m!+?V*q6C?VVp@I916$1@!QVjMdYHIly~{8%T* zg;_Mi>aU3$O6R`-4E6DE#1FNaN`rKqX6xwBQa{3xr;!iB|0yuligk)lQRL)L1li!^ z%^Xv}!pP|Zvta8$_N#`8@$le#PR6x3>wvwngDffLKpg*CjEBSb=82V!M(2zP7BR-| zy^U-U_wE8dHZIc2HHv^QLXdeVzX3Q!rSE{E_$D~u4ex*B3v=NW;SAoh^65bQpIcXZ zFtYz_UDIK_Rr=;SDNSjV7dO#_0-=XdiKJ3%F%E=*wTjZ&rYb1YWHuzkR8rc*?bP2w zhbf4NNQijZ7nL@Qv9tu(cMoIxBIgUTE;|R7GVJ!`Z9SI@=A0(d zQe|IP$ar=6414g(*PIg4e{j_LCVBNbz{eqw4!ufgPnZ6JI8U^#`Q6#-@bkRUW;KCb zoA0hoVE|c}TBTSaA0wWK+ECS~xK~9guX{MHSbXr;ZJ=y8QEydX^wF6p|9sYjk9*C-J_0AVvVP|9chxUq61WqSyU(woh+qTvakaCgm^u)=4-=g~D6tYeheKI3wp zgJzMth6x&P(Qv0l*{u>}j{l-splG;TO@q-esF7fUAWwIQ+h*su0k!SqTKa;9>2bAM z=W`cHxwCxDFCzY4;hQH*`?y<)`S%jO;z2Q^kZY32Jm%uT%7tidxx>lax1dijcz%Al z11D4*W^psZ4=*jPa}h-c`XLRR+pesZ=yyXMj+i6*R?FvyR~$t9L9Tf>QQAA%HyxoJ z*YsM3%j%a4uGC|h^1Ce5%SDxr3Le93rUAK*5eO;Ll#f;sD;b;B+SU{gBgQ+VbmECx zuhqI#8iNR~C96VjQHN|rh1$7F*D}ACG_qBT0@Pp=RH;>^^Mwz&mLBfOKU_M%di?CO zG$mk6&mxrPsWH_ogY{_cVb(pv#<)tn$&cod1GS84<|$Q)y{bgb>E=!!8thWD!z7B> zITaicP^227r;j!|Fn>p@vQoLmWun`II&`iYbu00D*|-~w;mtdE$j-3p(}gXn5ZE5# z(TeA>Hh{T#uA})dkN8U`?SA=7InUD|wO^6F_Y~cxRgvGB8#ZAU(LU%E_?+Jo-Ax=! zK-(Lrm_3r@{No;9N3@rP(CjUz-qB1a#L-MRs1h_C1k!+!n}4Kh-!HirYss0{sqg>0s3r z43nB%u%y?!KC*W9lU+xnf2WF z@l#ZD^`gzsSYwdD`%viG8+Ls&y&KBZ@OR()joG-$r&q8x+Ny zSjB|PSe2_d`+5x+vDG$%<6McC6y;iw+xlneY5R)o%vB5W@(ymO3?EgcN>Lf8BForq z<)i?&UWO9WBbHV9FiaEr-ev-pCz;qe!}G`pvy|5$OI&vO;+nWk(C&!kZX3q~n;cGr zQ0#7HKWVetLuHvq=!~@##=N>&c3gaOtsG-{7#$=rZIQ^rd65bdi_(!7Y{8OEIq@TH z*U+NH$mcozN^t^QT#8_nP3{YMSwu>G>X!%oG1ikhO9d8Ij-fa+4z?q#dDWPY_|z)JZ$v&_MOp2iK?3C636(&5tX2z zpc4D~vrpoO-{DE{(0Z2WwxMYfwSS{&$nz$FWFE*CbKiR&I7PQ+2uR)MOuaY;3 zW3c=TXeY|i5|G!OT1&+{D)I7Vxo%Iu*68lsvIbcNPwgC&q|vgLbl1>*=1Ewa?MuYm z=9SMD))3U#D9tU8)2X(7j*xPYZjceMrmjJZ&XCTKr?5+~r?Bn; zA8Tj=RqZu8-H=~DeW|yqYLZ2qGg59|>r`-0R6LBuugJZV*rcY9F#<8orakDhFzfLQl27o+2OjEG>*~W#IRueZb7T%b ze&(vAxmpF^>4x{@5n|>lVAZ2aq(&U$hNXJb&$ZXa+LOO*B7_?&Vpx37_)ST#9$x_= z4W&|BJ(+!H4a4V^%D|10+mj)0zO_n8qpEK1(0t7fq*<275cli;c0);pS~^8tR~otb zSlSpf_tmYpy*z|L<$-y?{u^ESY+YBmLhUbUN{TjCvkTO_>V<`cv`>M%ox$Td#CAoA zL#Fk3K3rc+e*UhUWHfA8%2x>EIE?fzKMow?fwr)-Mqisit(%Uma-FAfB^E82&DOhG z2-i1GWe`EH&il?yo))vD!Cb=Va1e7ftrap=l1qK+DE5RQN7BSBMo<#iv>A(W}ry2>Hv=AM}%s6dU)M$}^-d#P1%nSk6S z$}Cc0rRiJ`a(@vth&_d|5USWK$hKUJ`Tny@gUc1~q!*zNv`eEd?&zHp0((LYAVAI1 zP7lRkh#q^n)h|e)Ocubk^!su<$F>|aVwY=O1}mt_Q*-}Tng@GJEPt+VM@@8%{BCSW zpil3yn~9O;<|*@j9q@9lgdjgn@(w-)9#OEFe2q%_NXkfoWJEtCIaB6g5CauW>-M2F z9#hb$_QBpy9Pr=`F@I#|NKgvFX#|hlfAzZtA}~^6c3#|}RDSjtnjM0ZX5{^+8<+Bd z@q{1yPY`MARA@_ZkMiP{im9^hA4w5xcY5v%fs!w6x|x~1b#zpObL&5TnEm!H@>Lec zoEs0RA2Ho|wL&q)?%`(t`GnrO47Xpj7Dt&{E#;~fub<0&Ya5)I7(Vi{ z-Q=}rCgXO>!p+j~P_8Sd8Q4A|Gb z_Qs5_Q)LHl_GGq`qosr?V3NtWn5EXd*qNmU$wz+^m}-Tt)6dOe^20VY}q=Xq2!(9|XApj%<`g_G;MwIu8$b!XS zxB-^#X$WIUu=+2Pu6(=!}1%)&O{~@#r5&3^TBLY+Y8UH_(VWcd^ zs63Q-voEkoOk`N<&r9l)=C5m*^7*^n%?!)k{-wGHO=Wbf89(4&=IbCAnaevChFtEo ze0^Wp^nBL{^!#i$T(aAmUCM!0nTN@ad<9xhvW4Eg z6K6@uG;xF+)EY&E#^V@E#@iSJ>v`u_+O2Px{I;J9o{opH<_EutupEkIC0jqF%)q%W zx7b*8%c24Im-|n`H^2Lbuz~O{8^;^!%YP02BSppcbMOV`aWS3UjS94a zDbBTrPDQ)>5(2IM?3QBa*l{MQAoHj zyz+51q4XK9uNjvg=1W)od88}M^d)*P#nK;A@c8vQZz8s-lUjcvoZuA^-&Nz4o7lg| zjTs2lTtp@H-YhUU3F7-j(=R?vVy4@l@7MFXe%|X%XllVw3m0Lj0y{}Se)g<{Z@PJ3 z&*oQ2CjJChqShIGXQck7frx)6zebn7rx8weGOe~PZG6~2eu}`)PG**SpN$h}Mt%B> zoZ>;!V2+Z~G#47X^O5syDD8^UYMnv>c8NPJ)TJ;9YNw5-&|XM2)W!o(vQTgkws*okj6-9hKoqP=d}7n$W6L)}^sElrJ~ zW)8@9Vkp?V2BepfjBvER*Su+2K5;hHd3aY7y#2nRkY3vg^)@=WCp%IyB(P+I;bjUA(pxqzq>xEv zD2F+HSeRO-h_a&@i<0Eas$ z+Ash(sA5}=Cpz+;;H@B{Gy<9^o5y6w3sq3>tuTTNKjud&5$VWGQ@K+5kP2M!x;}JEC~|X=_@q1@%#DZWfVt4I6@vCdI12ZvINC}ua`Tw zJq0m`74qdRvM??cwA7KPmGoe!VWUQr!V8$SV zMQ@Jcl`edDtb;*L=X-QAMUc5 z>edh+^D7X%%giMGJfDw`{V8^D-a+K%Ny%PeH#n$6w5!Btb@EaXeL-cV(gJb5kXBb< zoe%cXtI5zb^&|G=G(wq`xX@k?ek0-!#;f=9S9|tjkKb6(4_XNKp&!+n@G_>wVNY%z zBm%RnZ1y}4j-fwKEhDU^p)j_@2+G|4Fu{;u!UWFpoy`<$!m6 zgRSD6!(9$zJ}eS+ag=?onUHu z4pn}8(bhb_C_L(S1J@^OeIxxvB8LR5qS@*e!z@1spa#%eY~D}BDTi(>|MOJmF6|-nK1G7$3I9Bg zCv1N{P20o3z(|0h?0#;8^hrPE!?N+o9{I?!+o;7uv^bIRo=V<+ly=_b)ba*HBnm9) zteiiLcK7-WuRzLAs@<5m%Wb7co`02-Zzel4-XFr$X8l{M!P@x`k`-H9`^0D zp)kKI>aRBOSpOgsu3M^6eqDpiwYB;_oM>#FTk>qb4FNAeLix$!J-0>+{Y7FwbP&pK z0Se(_RYO+cVvMCqN;2VJWTt)yYP%PrMIV_+KD}HF$jzveiGM?p;q~Kqct{7WX|q^_N;*WDNB+jlkyRUFy-O&|mpFFvhXCt{+ zM)m8Tzux(wS2+flh>^r$GtNGDlSsY!*?ST+Z++Hl2--b7Hw zyEOmIs5>uuetOXd@}GIWWi+habOFUP@%j3X2DToKxAlkTOAofk&YBJy*yok!TwczS z_-JffxgTptql~kxv88Smren;?5dAxxRRmsrz9GIM#s}A~)TpNLlf22(40Ys>(?u4{ zqWs#8JiqUb_3*NJTN~$X8z#MWHGcg%Z^q!eQHFJ?vcx2I&RL=HZ6B?{{)4poBH@U*>Z)2;QGd}UtyWn{H_(f zgZ1#HHcRK*Z@4uqGT8)$yUwnFJnfShilwmg$am@$$&?isn<~cpnjHGqS*w&2$fBOl z4F7#jeND^r10Q_;PXaF3|LYz=+``&a*22m8?U0XCT`H{qo zp*!5ukOT)Vy*WQ1S@|GMXLx1KCY<=o0|_ilUadFEZBAAL#&qC;+>J1@H?jC+FWG?_ z*;FPL%2>v->!Z8x#J(-mgzNKd+xqIh@4jg2k#+qdiLm7a8^;5vSqzTYB+{1Rk>3LK z;E11@Nc^kgYIJGr&Ya@>7PmCqnxcl3e&U3kRD9SWvetUHQG-G`9yL2ZoJ3!dZsOY5 z4IW?OK}!WCa{Mh)!EMXIyU3_7KiHskRDf<-&;=M}+m#0Hdzg|3(r!%!R zkv^C4Nh|MU{yD4@n86!kXUG~3yxi;4$vWS30%tjZ)$kq?3@LKm^k~GbAIgo0xfYCeMrpqLX2?rnN9a^Bg&2<(_IIdTkGh;k7A8gLZ3Cg$`Q!c0Xd;-P*!LVGCXmoByS_v=lwU@& z$ck4ubc9Aqh+6Z~8RdoQeu+$^sI0Pkhv)V=X*(Upx-{w{ zQV86`N!ZiBi_Ko%UpZB37zN3)fc-GY!EeUw8L8!9O&eistc8zv#o2FYSsH1)z3k_` zt?)-&OlN=jdvv`YRaIvOc@-Lj4%UMr%uuF=j z6e$Fa>mu#omzX)mmn$6>gCk4WtylWMw? zV~cZSJ}a2M0{>k~{&@jxE7{18j2-~^Q~tcIW$$R}YGLZ;#Nhnd#m30i(88MD-qt)! zQCtZy^B?l+fUdNWf4NY{#FLld^;`pnM$v5ZrigE+i)e091X4 zdp3jww_)rhwVVI|ROdehV$F=u9sJH&la#2Cs=MAvdPpjo8diUYKmf%@66miVTT)LV zPh`%Bh`zr2I-LhCsf40vL9K=yt@l1JV0t%}M9uSzC_Ee?p83M7hGh?LyiJN?ama*% zYyW|dzq*OV>5S_vbyW6r=XIYjO0tj8Jl@9!;#J}aE@Zb06e_y%_RGhpu{yguz{M2s z3iZ}YSf{~73(xM~m)*!Pq0atGF~34$@jCA55}$Pv*HIJI8UVLFkUl(ao#XWmH!FEA zgY>xzR7v*QT8e)lT6+q`+E1}mkG~1eE||34MR0dGHQX;Px&_fiv4EUv^|T4CPzlkE zWqViGH=M8}x+wHzY;``t@@X4d1WZS3xTDg?LS+kqcMT%2^x9)&TL731=yR-uG*cao zprXheu3R5_*r^ix#eF3(YFKD;MMsSNieRAk(XVMDN`+mWVky4b%_x6#=l(dZF(Qxi z8R=CDSF&r|0UBSDheV<{0Kq`M(XLdvz8|ekx~Zzt`w3KcHGM?ZPwCzza3RquiYwFg z#h&w|6F(o0U9fn2K6m)EUo=S)stmXQQlCkLG6DQ9C_JJ(#na{q1?6@| z=NHe<+I_9pjiqs%IW7WOOzd63CK}3<2+p8S(~tcqvXu~Dzn^s|)>EQjNH^8h>LwN_ z?3=$Q_Td8~X-sL-pgc+LEPF&8vS|Q(kJgc?d|OX89P8+3lk8Bzf4J|w&ba^shA~D& zQs{0{s;mJY1V)PX^yQeJUsKEOfPSo5DP9F_TSXT_{w^}$lVQjL9al8EGwLU?kh%<~ zdjxjDEPDZa{j_ZF2ePT;xL6iKs_~z(>9|{5-~B3ej`2f?pyIzG<}M$!vcn=0c7e|E z8tQ3ym_zIHR`m4B&5u$F(ys2ILOQ{CPD4-~@CIi^28m!=0|*`VzgBkur(zijTGb=H zt1xWsIq~D1d$6#fgL5OlZG)!+?eOMySBx?Y5;4cs>E{J;6_BU6HrwIfWE}twW6~IS{%1ZL?VNyp$PP;iEH~XQA*m z*6%GEs#y|0iVvSiABfJSiPFG5I*j>BE}=5jG-7ESL(CN;oH)p3ZLSLzSU_>QS5xy|M)r2Uh&qLXm5G zo&J$F5EPVc=x;Kp98FIqH=?I1QH!_HU_6{bk=LwzT1IzuI`Zo(-2?0)c;ud5gQ4?$ zA{WW5grSd-2W*|IgsT!yR7VJMxH?=pj-JZi;V-ABAwV$PebG{=c|>0%Nn?sl*?yDY zB5bsuwKK{+R8tq{$K}q@V4BRr{PyTO0Du8!@{U2KlB7@$@3q8<$_eELK38fj{*eAn zxjQg1>|&)y^ZSo4H%Vb6A*G_w$uJLpUE77S{ee~I}~23`9o@``hHTX zpKxNcVf(|TB#9LxKK&7r-2OU-eVkR05w2YJOoNRfef=f?Bqj1&lG$f-h1@313@~vO zP8yF8Y`bPd_=Y`hHG``4Li}}>7>trsDLOi2zQc-2A|kW@vX4u4bra@LOPq`o4J(5? z#Q+yt(h2-@s0m3v*^sg#Q5;O)bhz1*eJF(QtN4Q*#86!v56uYT@AEhjO2%cJJoGRz zPpR(wEX|L}>O5W{CkW^BE&*`mS?o?w0!kcgjr$n;rY-F>(h-nRSdGnvlbcYGsNb$h zhQzHml66nCruUlFC0c?b5whqSjSk*BPe{aHviuG_Z{6Cmoyah`sJUr};JrEe8=gPbl z8MzGhJM+)EqkvRLT?Ad{h=ecGS%M$YsM7vK3TNxd$B#d&GE)UF zPE^yAlCi%`P#93<@POpqaG(*6-gE(^N!B4TDhe@h>Rn!;X+F!B%h7vIn_10A|BFbeTDRKJwy9Ja5)68>Q) zp(;U_Bq5Pm(zNo|S%#-?yu7M*tf>EK+7$uX$K5F0>P^-vm*r`@kP$*rl17h<)07^H zVP9sg(?nIr+4$}>+$7SlMNTLq|6y54wfSd)M`O7{)1Z z9dPI8l!x;&jlf zr&+#rDxWnBhOi}6Y^SGai@V(aS>0n@zY)%dU{ALGiwZvW z-Ucxc!b6ZulwFyK3tB1_sp^;_)hTbQZ#x%dzbiuI;LkGt|Bcmk0=gcwX9oR{N z?4pv&yfRB^>ByHViP_klKF0CYY#G-g!*1V(-9BqBufUA%8KD7JK1$=qpGu=PV~q&1 zBO{TCijqTq4WwoJ0zwnQ-T1o~+FAI!7pk_h$keC>@L(GlN0s=_u7YDi5?c6cT4-mU zU|Muhfts=50>I4{bH{GV0EIC6r{T2REryofKPO z5CpLCES|Q}=rh7ADWS%w8#o9g@)f3TSSW-%dSkQp(~Y{BuTi3uRz=w$v7Z}0GPVh*3gtoS zi!-r-+z@|v1B9axhwHhsZr-E46OpPv%diw<>8S7ELX8qx%w$?7Mju$jMC10P&DwwInm&%yBTE4#dJ@?Fxv;}NI2I#*P`RnM?h zGZT?VY9(Vp7C$-M1B#IJ1+R;TH*{bbl`a+%Ugt{|7?9s4UR360e99f=+u*qC2XK5t z9nj_p6QE+s{^eimYsG8#tYiVe7(^mrZOxN&0WT{^0xrcY{+o~*zFKpvqGhtGO9CESe6_=B}86Jie@*0T#^z%T~_aleII6O0z$@-GWr3Z*7O zqhWyHr)dx~MfV&jN2dnb2RVgsB*Q<<;SjzbrJp~}#8V&hma?9vzzU&-JyFhq1>=Z} zMzf+cg|7fiu#{vE5zg0nHJq%@9DiQnhBihbr-IIfMaxMz0Qe&8QR>ph{zqbAXPUfW z0>s%`6|63LBGA3n5JVOvju`BMp&-q~ZR+}4ddlQ^GtX zgv>YKU!HRd|IktnOK-3Nl@Ou}07szpxC>kDExmc=zbsjxM%5(F!At!uB*TB%kZ?L5 zZ^T71{$roNnZF8T`6=cbs^8%rTOa^WFc4uQe9$vDKl3lg4^58F1Lu-?JT|L;Bp9s% zFpalbgOLq@p_LG{-Paxn#GN2yL!hP*=f47wU8j$l&)3#nwAI(+Q8>@$d;T&=3Jbd$ z8U@HxO&~~}F)v=}jEQD#2LC}MluYX(>_%VGg70J7m4ud0!Uac{A=nTAv!m|K5@iE! z^zByQ=$g!EhuL4X$1bogYO0YVh}~lwyL(k-Q>_Ax4AYDLhn(%{8Xpy{C$L;g2((AJYRV# z1i>Q4L&05+L=7(&-Lg5pqaAi<^2Wi?_! zu$_YlQ}A2@e*pqvRoZ=y)9ci{6n-w2uh#_8>6V*J<4|~myEQEU!?niWqm~qJ>MRua z1kM+?=F0O_r#brn1Sg3FfB<8_N3BH(>R*bT|96EAlCJnTaqj-4KbD&yC?2+raXt`# zGV1;VP%k{=iRMjtZfPr`ynHC(VjcH`@o^_88Vv==dpo`aHcnEZTYCAi?z?=2yO#&o z<=w&;1fl^WplXt1qxe9!x-YT6^0;T)?^U*p>gqo^GKZ6OdA^IOkm%mzQTEM=#(iY? zZ)6(!!=fS0S}!N!bC>QE^!dErZs(Q$P2U9sMbq==`v_jgl}hHK9u)yvDhW&2zd6Z> zyYT~j3vM};j?Yv&TcKnAbT1*r*7i3LH<>%pd<5hr$4Elp2&G^y&Ri;9m&2>`;ynMx z;}wD;b44~Om!hVH+VyC5F@_Y1*xwEn1cQfG5Wa=y+fTo;0*tusKjtNw0gm?BT4%2h zoibnF=*tU zZvFN*Y^nr@zeMD}M3i(Z!LB@Ip{_rg-rM}q1pXmmw!R~sC;m@jzc&Yb+h@yb-lFVK zKbcT_ZC_(X{F6Hdvw*n?EB9=r6BQOh;(RKEKl=TNfttJ(#4KF4uXqY8aF_qF7QBE} z`s#^H%+-~TBkle>g>_IRc=-|%Pj|_sj@3?r{v(x$!~fZFhpz${TQN=~+Y5|F#)p;*j57X%rscicx9)YU~y}+A>E} zsJZ@k0#{Np5t&mRzt{W!l>&(UbB=Y@^{g5e|JUpSl%;>PfuTiwSqi%U$4WzBT*6i< zcWQs&>;DkKl^8VMOq)KPI)cCkgP$lr_8=#YIkpR>OaE!(I{lAv8wU$Tw})D0um9>1 z2OkvZaOQ;{3aA;8|Kl^yhGGw!FRN$&iW3C;1mP_B;$L~^1E~yi_a0h-lozT0cxj+I zle!A^j~o1Bw!+^kHP}Y}otS^f=k=`HFoiVFL>8jI^2Nm+pVNd;?*B!Hw{@D?=pXIi z=Zj$Pe{8L?^&j17e2(B|dbc7UOvqCSk(3mS(Amps4WKCaKbKe;gy^)SYh?PPIRfT) zCwfMx7RWx)7fWR%m~?FyOa6L%p5vJv1{XPfcJR1qMrH36(m16& zKRZCtQV38cM%*2JYLQAU8qfnJ@mbZkcO~lEO3TO-+jK+W`6FzQzXN?(3 zuulNiCV;flv*iZ;^^NO87BR@6M|Q(LT%bw5`L%Z)RJm(ApQH1yVYMAKaQp!9Z~*AL zShQNz*Uv%Uuoyn+)tK4L^vg*GRHE7fXKASWO<&?m=3Af4!T+-1g+o*wxD2q-X(0aF zX77A61Xe<{f}_3?BIm=uP0ncmPO(MfGR?ASSL9~>VPRbbSs@ibxp%3=!`4#ySi zQOKpp;z}MaHH~yb!hOf*D_DQn>{%aAx{%rqj+|xxSnoR__#Cy^0ik#DI=r7*!RvLK zcK3@$quh3}8sq6gB|@Xw*cJ4U4YfR-SJYL*OmlYwy=*3 z%1fu3c7ye<&rP2^L(W^_MB2^i!|0;Z{fJZ9vitHu*ZTf=ve)&3gWb~+NBeS{*F@6Y zKxbN>gW|#npin+@ilfo4l5wZ$=EQRML#|L-V+an||!m+>WM5$+N;QhFqWoWAe? zESsg;C>va5EdlRV&DTiDhm$NeI zde`+xm{53GZWDSR>)YEH&fBr8A++m07yH3oLk{~-m{yAzltVn;9aX&Hb|De^+$#-& zI(68BMf75-trs2jKSbkZ5i$6^y#ja6NUmPf-;_CMo=PfQV>HBw0&Z=VJ&*{{<=FuI z=@&oStNe(dFR@5L{slu8!Gv>VIts4PvaA63?+~%KzQ`fgy?`6oZ@s+thK~a?H&53j zh|f0=6;E%Cb2AMSR+V$X2zW#5J`Nk5U9)*6(@><~;1hd*e4LTs^SV9mqui@4p4pqw z&&t#!mL3R&n5$Udc=2n+7)#8DmaS!udD zrwW0~4uC@Pc%w(;%7U=>{a)+-7Up!CFOy(2QDg4c?6T~te;b?eB@`%OG5gl#YwsIm zV${In_By{Th%`>U?t5f&CcxxcN7)!Ptq*g6bN?v#kA02W(;O;7)EC(b+O8hOEakyc zG8m*FMWKHE;{~bpYDNaa#Ev|iq=z9{AC~AJDZw>*7|y}910lh8YVm2M*40oLhaN1V zVI@Sy~+^qva@BWv)(S%BmNP`0w&tShXq4_d?4O4RlV$nj;G^3=K2Ia z5$E??)^gLLOWR2-T5SJ!f-{>v$TRgd_I`&9h!l29+IEc1n(6s6>^P6NF9CUcjGz6t zDdj!SN)C!slYungosS-wE%tWPQO=_KIm0t`a)|K&g|H?)J?kHaq$h~Ru4}!@ZJ-MU z0cR~maqT$743u{fr+Ss)?&L&l{+?da!{Bw5Bod;2=b7f#i(=A7~A@V4o!d?zc3m6YJnG0p!PSZ1O@A z=Ud#)h;2lHx{dBb6AdoY1KE4t+##$pq-&X-QWO?w$RRj@%V`dP`@rz(rwU6f=vCX?JMqnuSTCD|(1*sTsGYatj(P$bjSdiV~O;=~S zU*8-2H%&o=$kKVBlzW5V|KqzgQARm7lVGO=rbdOH2s_g%fKc|{-#Ul|87z86 zJaQ-)0)eP4lT@s=A-Dw52RH=~d3kMzDBqHNnkk%!W`7@-Va=TN2KX!0-yITOCYoJ2 zT$ov~lO=L@L?c>GmZP&UbDUTQfsj3|_6O=$h~Dsf;k2l!svfvUP!4!aIVrRg>}2D! z@H?0^oDOC*y+p5;y2cZ1C1kcfU<4cS83gUFWAjkGYl1d|P^sDIJs^c*K}uc<>QrM` zynS#zxj_&G7CC+KCdS<`AOgx}uqIohzJQPwp4M1zU9p#_q6y$Doe#}TO6H+j<(DKa zmKtFeCq;qLO$`97qeqZ6Ae;*HH~D~!w#S^);^9T0LTrQAXGoKhAMIxaM_m}ai;P(i z^|q@8b{pTotIma2{7T6Fbq&PF0&Kx&MDL3&^1R46@Dn8#A>9aYAv^#7QTEnRaV^om zXEz-hcXxMpch{i7EfCz@Y1|1KNJ4NYNN@@6POt_Zto=b{_b1zW@fFKKWNsf zbE;1DuDz@3)b~@>Pv6}*YA%js_)%>^Ppeh|SjQZ(&9A?KaJ5lkSbHQe%c1AVOe|U@ zh!?JLU|{4|ZW8uA!L*}^yq{)9v;|tOhR82VJ&oj1Enn%%hKSX@;%1V~vas&q;hP0r~~XHF&5ywK}g>uJFu1`6x|;50@I3pNvmp zDb#4+gZ9xVbJ7EHyU%3mSWbWU*@2gvk5A5d@w2#o;nX2eLUH%b?Enb37LDp~c9@7ay+S z!!Dqp83vq5vlhgwf~kP3;9vW(oS(j#ULGvQQ-yyIxZIQ2@25iG1^E{|U+N6H`?L%U z00ml|Z9%s0N&N1HRnV>!9| zOuLuohf8UCZrp{d@8>|*BBHR*yHk;f+mmL5V(#fr%NqQ=z`J~_bc5`m*5fa6KIuh` zlzZLLCb!pg`TVh9$K z+ST{?`xUY$Mw7RHexbOw`JZ{5o-P!Fv`9i2@fvS=%%m+69Oi zm$&(jD_hQTxcx?}P1@(npHBLIy1f2q+-eVa^CC1ya&P+4ukM`w=}|cs_w)0VOYru6 zY_#oC8JArloSSq3CUZ(P?lAfK)09Mv2tmdJ+3;9r)Hk|5US#XI7&m}Z4!s@G&ZZyLB1yPr#oKn$d_Wve@3Yg7TYUHu^Y`xfmR=JyS0 zXJcn|9ZPep!-(s>-Q4(y0tjg8%--zl^~mcA&Yln^>EvWOtP0#1QmwDNPsw1+BZQ-| z`V6)C5q_;dDq`?=8@CKKe;g^--PqX|1<>Q;EqOY@pZ#`tM0FV`)pGd2+HVAItYER^ zw`2j?>hca+a()=`m2>-R49$g1hGmsS&NlZr7I8kV-!4gN8+{n!v|5~kx#O*YqKB5; zR@oD2{F+F#2tTEQWs|iitrdxT7jUal-h$_@9~#coSn6oZRChGlBv5T+TNb#2U&J{` zokqzI+w`Rr*!`k^JZ{_BovmzsB;mr7=o-P{lXOfb#meWPw2U#(Zp7?F%Nx8hV_Z%f zAunDrG^}>0H{4n>AKhT`MWHww#C)ohE~2OF61g=wE%^z!P0#~ezSCPFj|CuR5im&8 zbHc6!Z!0nzHqzZJhhn27{PsNGtWwOl3;!Pr_0)f?7I1_zo6hkN^kHjrcnI&yz(-sU zcGS;PF>B+&fZIXLD83aKcz98Z$ePuZU2n3RFsQv_og~ray4Gl|f`R8IbHxIjfdS*; z?!slEs9e}8rZd>B!`Z|vNBC96wL>DaX5avZrj#KjVM;dcFkV5VCm-4!Is~fm>xk{@ zOYo?_%%suj`?Gi<~cA z{YTNTfRvr?YrgWabXR2Z`I=6&*yzvXk`d(yg9r9&nxTp$C8KdVY_a|#z(9w?4g<1N zb24Ev&{o*Z>p!!DrMj-_7dvK5RnrGHmjy;g0g#GJLuhV!N@ur{t8EvB3yw5r_orVB zzgG|B(UQ^nAxK!7N}-fgDr$K>vW(D^h_lSQgJ*!?pR0UFuh)xYEbl%|0BynSS32CA zcVI_uZbNDzUFi%OdfyjC;x{)xx#mux*S)_`b;ZZFh!XYr)!0J5Ow?_cu{U)x@-xUH zCY4esB9$DXmbP^=Rt4ZdcOqBKV(AxAouR;bg0i>0u~r}>0hB3L$;q(sXtjCu`qM8P z)m$#0$(2YH{PnIDQc|IHGGpX@f)_F9t|QA$UL^d&Z->IEP3Z;1o$_ zSy3%YQNZoz<_NXYCqecr;(384s=^GLQ@XFBy(%oGgm6{+LpdoiVj_m+kpbVfLuQrB zvH%QbW>n!=uIA6QdG;B(DlRAXIzf0)ToN_;S%g#(H9U9tH{QhzVO1U&!yjgYyvmW+ zbFayxx3IkW`gd|pn^gf65yTB6j1eu81RTMtWOpKga~_Cvzk#4yl-)KCaUu;q2DY72 z_P2~6IfY#+5H*sSq)zgi60UP$gaOcB8*LaCbyh#t240kFijhRYc#wn|h zrh)xeRIPU8URyn@j~PlK!N@ttHEZ3HnC>*90o*5nd-2yCFAV@K(30fA7aw;!FSrE{Q8fJJ#5U#c4#Tc4y=zC8(b&tqgnV|s%N z7*zgCU2JSpXoU!4B8%e|m z)Vx8C=nNAy#r2~5j<)fb$>N~BR3BJ{VG%TkisDSsxarvN^U4nOwdG*ZpMHDp*>zv01-1jkD59Y*?z6<00QuaWU~vNc0lXsBdZ9oI2~wVF09A|02-+gDpG(|tGqAKqY1ll zoUi61N4E@VziP2hpuJQg8I%BMo7nVJT9rR0#Z_T>ci1NdWsJS+YJZ-{+fp=fdHR}; zp&!9`Ch8PkEBYCABJC$FWk8UQg|o|WR}`H16A}hO)3LbS1haR#nKy_4C#||7H8d@} zKLY3sXLN4cZ-mrvTq0A~;wYP*3|H|QTPeZsAUd}v0zmd8K{Y9M3T)<{bkM#jAB&cf zPR6H=E>c0(Is_liQDS&^ zGM6iAF0CwbszKe_Xb@YUChiy8KaOP^sPsOAhPm|pr=mofZcZ+J)VH)!qg?w z@&}sLWhh-tO;NLW#mH#uduFA=0}&Q9#^PnI)wNc#Fs;UzRl0lw5X1j)S0?|%UFn!o zSZWXwLlvV~;diBH8V=;V`lG;@qpL?A6vW0e)=wL8i{)R==jfneKg~6)vb01|s7;?P557UP2F$?3Sfd?oVWod z3r;Jaj_#7$8*MEXKq_p0=kbTikWWBvJo2tyd=dFRjO9q*g6&0}zi1p1R0U zu|4*MNjK(@9hxjUr!5`Z)GJ5zA_l!e$PQFkrj21B3|^0vz!5|x`rIUm6Y%Oc!)$hi zW$}&OjBCLO10A{FhY(1BR{PGFmf4_5VBF(80^mpZ#zNwRWDEY2KLx&ldisw` z(>vH=*NRrmjI38GpTBdLkN9A&7e!!f7prdHvB`<)2wVGg$AQXj=iy-2%C43>IfZzd zfJpMv;~?VTMH~=0$6eF@!xnc`Hfr$+kmf^4y0iaIQxX}7Gu$mLZZ6U6(>Nwip5J3}qzr1gQj)5Ra4PKn!Ej*pPKMWfE8Y$aP z^#@a3$`Mip;MD8h!_FrCMXmQ`LX5SocS~AMA*2?F|C>6NmnpsWuSMcdIQ=tR@x$n_ z##8yb|2p6|5dPq6k&F-EHw{t{1prtAY@$yp4m1qE*k-3_G=R1MCQOOB>uP(Bdc|({ z3vii!*L%%?jJ{rTh%7tM(;PUU6xj9PMGr_k-DB8KIp>=F+JymRH%LiNUA9`xs?E+E(cZ>F5#)&DT%7a=%{Ux#uf zEK_>n;#KmMekqx_aU98^;2Ig@`xtn-u^4_^cOD=!T=6nksZu@5_O{**XjP+6v!Z`X zyM6Be1oCi^KgyokSQ*(Je_GTiqr@i$;mt`y^1=*lK{^^JXfR~^e6WTT8!w2tED|aS z-#85_%lr4HYpsRS_fAhoKOe)-BBw#(Gwmwlr%Mt|PjH;2+Kv>?u49@%@QAzjZ!dfO zg+455MkEGLLPX+?YET|pZr5*pFynvwM#2>Z)I@Qb; zZb7-blGXusR?A5vQY?mYu6tpecP}sA=() zAX-C>cKlIe|JTKey`~ll8fCbtHIG6;nIQd*kI9#$i^>6`-iAF_(Bqd%gN)|465k05 z%#-Mre`Nn!c%7wl+ne)p#EHjPsxPM@QAu~Yn4 zQ)H^NEhzT)FPx42p>EbpUVvXM-bnjGQChymS%Frsp-2e}0`T7_W! z-dPy-++;H6U4=eHb^p4(MLw8x|3-YPHYJWZm4Rr$Z~-}yZzSs+5yu5-f2DSB3@r@L zYmuu-Z>W_<8V{SeJ4gJ-OLc)#>ey#>Ctu3(zPGepgX_?$-*!7)k6`z-B^<~^Xzd1aFKl&DKB;^9KfgkT~XgKpm$;wdusC1zh!dmTJ zP00_7f!q4;$#7HmvxIkvaTv9dIfDcbM z7$FdkSFPgFyx7&Pk>57HGJFc;GRZZRT8>B=deG{_*w8dQ}p4*$WJ>YkhXuqUQKJk?G3^K7p4~d25`Y zb3AU7VW0AIHFsM>rl6LkBDC?H?N2+p=HWaZ5%HB{M#pRo1|hD61k=cH5Yc6HA$G4M zB}xhBY{npPeQGREbYm6Ctz#lk&4J5xOo6D-N*I6lU1joDt?i6w?(0;C%<%ec967!! z-y+!Yx?BpQBO`>C2QPQo1#&{mKz!X-ddXwR1e`UbXC$-`Q%3U_+)=KL2BnVy4d!SA zlbHJ#Uz$E+tN?EdaFd_~(P1;H_~J?Gk=;c)n%|Qw#CvvqBsXj&($O@MMN%1FyVz5}G+@L*znV!3YqjJc=htMQE;THn9A#A5v%!& z5->S~y~10*xoZDJ{WNB}*4skeFh1O9&!!L`k5aYMGX%)zj$zUoxkPRxtFj;?MYHaY zk`Tw&UuYe69)62+&d~_#+}1^kW6_T9mIR+(y1XJ;TR!hSPA`G2=#&Wa%Mq=q7c{*# zl5iP$w(&LvI^XtlGOiLB8-qgnKB&nlmqX1vk-D^rhjSh)r3#}~(uD=I@<}09vUUZ< zonuT$SpIk$+1p#l;(5+41NY4#ER8LODbV4N(uO2rh1BTEI6k!s_A9e$#vV}X>ANI< z|DkmGY@v@b5B5*UU1t|D(yNbEoRGkl$!vS+M6!|XkJL0v0p+m8W~lGmq7MZ5Tm(xj z1K2J|?^imcbAYHh`|(&s2nh@^(dcIN44|^qmjc%>slbnm^LCdDs__Vg{76L_R#Y>* zSoj23m+maSG9++gvc0Al1kn!xyhbliK5pY+1zS_zMIv>A0ea1#B=V);3&9eR1l**K zyVbXOxkpXoLkGxb!Ew^qJo)I2#if^r`?}}%HYNfmo#4wM`#t}wZMGX2ZzC|O3A_cK zg485n)1r8i5@-W^ew9`4RE9-$(|MQ~YZ7$x%-V%`&7#0NCiX`t2vlwCaVN-DePn-k zmL88>Ge!U%z5^LhZiLP0Cp!{p6W1w)`X~p0YlzIX8|bCLf7;}(q#L3QbSx@HE^3Wj zfN*hI@YxZ>VilbHq&f85{gs8HmKC9@_lVSQBvVe1Behxqb#Nq(gBD*xO|VZZV6dfI z-uQT2myvj=ZxFG&_+FNhsSE2;p^`4emMCr=@tRkiw?PrTR!(kns?m<+{$5$6G9?$a zqxt?QMda#ZP(jY22|5AK*%%A-`mBFfq}Zl3^Yzt$svhVD)s@S1_63kp6<=*R77OCt zPtW&NTMapst=d#vaeTSU%=MI})QTT;rEZsh@3Oj~#mVkiQpUK!*?mrX_z^Mj({vMU zzB+=;?I2-HFdYeOpg{Yr*IrtE1cioP)7s(cYO8*#0yl9t@YG{g4pWJ0`2?-pZq`{pQ1ehu!tW!C%U#a}nu9(M z$YmC#E`e4}^*OHa)UPtpSW)zXHYHNkm>~yoU*=M`*=?-Cn$A}gnSUGH4})M-#$@?= zPNbW^4_Esf{ajiV(-g5uU_xKtEV-oq;!%;VKHl_EZcp5Xy514#s(-QTB`JP?>y*!N zFZ%cj^hPn9d7-FH&};{R+CklneU3JD4(fXHi+#gy!XfImsG!G5loz%=Rpu_M>SWS9 zpsCZZeO(CFUuUx@N2~16cj=0PJ45-CO#qd%B2DWD(;>DqiDa*hj^>WnPbSF3L$lE5 zlk@8uuzojmcIIGC@nkxM?c5v?sKMH(X5UkpiP6QBcUhQ<{-pT`Ulm+Qu!9m z6@VW8V~1;KnjB4ZUzZ5CiesuXZHs=j&T6xv5t!?%9LV8S3 z^3#7v7txiI&t)eF|KkZ&rJZ6>>RDbpWgJLrAO6KLyMkp{i zsA0K3l)n>@f>27K5rdHIr$LGu;m{L0K5UZc{fRm0{`Yy{K&4nFg*dhrL-(1`I>;Lx zdkN|W_4lqa&*l+Q@OWVyBftq?Um4ARxywJOa!Z+gAD#D>Wqg$6VZbw9qDo?M|B`*1@4)OyWL9NSAxs@=NVN`F&TKsj%qVT0 z!_4Do5?P&A!GBZ|r(Ck|S`Z#>O+{1yBsct#r&z)9<8+Yne2HB}*3#ToaxYNP*bQhN zqxfCmZp!(fswiS1Gjipb-fIwao9g>%&z17rp2ASVK{zu&kj+^SxDStpzYk9MjyuH&qukEix zX$k+2-zz%6W@vZ-uPs+ol&;^{%fwHoj3)axT(pStt`9tP4KE8{up`k|`CB}>n>F9N zbo>2vIoB9kC?k(=#uBzdl>xReUzxX!pPPGw(!Up@p@?t&D(ut9=Y;8~mdcg{>r{U< zJtcZr{Ck}N%DC)e(W3dL3!iYhf;)j(XxRJT2sk3j>u)H`zYx)=OpVwWu6@u}PU&L- z{S!5Aiy)NhH^+P|#vCdpeJ$*GtJaUW$bk9(#%m_32H+-peDAd#k|X4i=BFl^l#(h4 z|9z%DWPGY>+ACGwAy52BNIubDJ@+iP-nixD{NLfMO(!n4eX@_w2n@}2vXI| z|2hvw@hkUYbehdFhnVTg7idg(CMWEr+W(df11rB?`WRO(caa&`(B)^sxTTmk<6d+o0JP-0#x+-$emTgMN;G>M$Iy@?Ua7 zi{XFC(uEOwHpU$PJUbsuJD5ftjP$qdB$Y+~t!h5FNGkQeWVj;c|G8U~dh%mxZH$@p zKX#)SiNB_M_SYGGlFR=~Ml<*?nIO6Sf08jHed7M-?rqx=wCntCFeoWV9)q$L9}K6M_(fu_RgHQyC5>2T*7Zun};jbh1=H4QsMQ; z=u%za;+gyf$;lX9k;69o#zHg(iMp4Dpc1gd;PI((jiVww1Sea5 z{zbL^2uGL4m1)P4*EN0l&O$Lu7n1qL36>5*WLkEz;*o^0S{eEyJn>oxlyeu&Rf*<( ziTc~o5M}0RP#hfEi3Ehu>oyvLB1H{{L3Kld4)2UelA!t%;VZvrek9^<-5YIWUYkqO z;G4}cA*4@kfoVF5&ajrOZ{P=nj|TOZw~6n&t|%lMmC^s)n+ArN+KE}b6tG*TMPzkv z)lqZ+Wob$H-C}g{^TI0gz_yU##0Ds+J=H-5(t=D3bW_B(AiYTFH4-nZKoW9q1&aTI zONKSl$I-0fsN?Bh^COKxEk`V15&$A^f2(gx6nxzqzR3Cwebk17Dkj87?;iZNo3)6v z97=HdT2f_NxM`$Bt!nK(F`a9?e3IXHod^f5O1h!)(LUknKB5Od?)0vlbr8m zKfh&E$clCEZ|-sL4u*6iYm0cl;|zv7kRXL`!4f{Nij1Y@$Fh4|ZJ#ms5DO^k>F!NR zBoixJBigmkNJYFx=R|ytPsa?<$wlY#oW&l)zoclgys4=bF*djBd=GR`>?9!e zaRHa_^zEA6Owr;S*cOnvN|WuGO;UF%r0gwcllD^zo6~*}=Bh%HmA04?yJ;J$Ci@SN zd8yia{xlS(Zo7$hB_|GR`KX}6j-WgAU?lhizrL__TtTyK4&x%=n5&kyB5(R;YEWz6 zlx_Sn6Trv@2->PtiMn_FlgP!OLVKsb7|J$rYuQdE5DUP@oS;CdK0j)aZoT`Z#(i^o zke_|@B^f0}hub;+VyIj<$T6hFna!jP6?VHW|7W5@1#i_?)0IfcPy#V!Zb`Ef}z(_{J?X2eJdbNuiNXC8PY> ze0R|j`~bE0FbJ_pA>$UXPOXB=`(v<+O%dNU%DN1Mt-v{6^>3hL_|q!Uzj*w{hH1WV zZHpC5!sbEAJL-mgJ{0~XN_CCZa|?)-wez}>lDXKNRX8aEj2pC(XvL!9f9zR@ksPqe z?57u33_!eA6#6{#Jy^D&PtY>$O8!O9y*azia?bOuNg5H9QpxlQU&_qQ_8u=Ig6e!> z5IfP@!J-=W$hQxldy67(7u~r^rMjk%BNOpX6)X4SjQ54~%MT@sQ4JIb zCwDtWe;#2m2W3g00C$ft%5tpUtPiMIvDkrjpr6#6&XPOfNgwjD4`>(Ft_3j3fuEUx zXxxOKbTnD3X%A1rL9;~dxL0LA0jz4Bjj9#yJQ#R4*qc0%w&_|krRxlNhFTlA=-xTP zP7o7&@PL3ZLO%n_Jpln_nIX1*Vy( ze-Q8Ui4u)=q(-Ud-F zdW`RI=^!B2Y4qYijN83G90xuOs?{N(ttAD%0x^?{_$Y6_M^0PlKv*SW!VZ>4?6Fb6TS zi1_~|oDY?~chaaU=X|*RO(()}=Qo%Dbyeu(mb4#Ix6)QW!c%BIq@KX9n4WQf5(@hw z&W8?_&91gT!46RUi07ki&bPWoa^Rpa4&=~P@a3_Dr1#PHdV%=6#Dj7gP`!0s=zzQ@|`4dg6ii`GS+Z z2T*xgqP4A5%ws}OmWU1eBC;esnO*Ff2<-p^0K!w}f`RQ6a+j^%5Auf_?;O)i3lkr2 zXvmft8AKqMMi()QqNE&F{#KV$z=lin?Maiy$7ObNA2DtE=RnWE*bkapUc5gfF<1Lk zWvSA>0d7VEoytCn_Pamz$i%#!c+XEE9vC69nU1!>1Io7EJ9a->4FUaZv76l*6Kahd z%eRminPgpDRIm^-9=^e**AE|d-`Rhn3vfL^KqohB_cnR4fWKFXE$Fq!%a;Y4Lc_E{ zUzR9N4+(+PN>a~tn(_&f?;#$pH-TZ^PD|4LX+g*kBwpQ7`dlz9VV=lJAQjk#niwbU zvBf%{xPG=tdd-Y;gTNI}!1e>!-qBjb={jF~m#joV464o&)2{eD!m139y_N|z7ZPa( z=6rbNdYTF7C!p7~^Y>&Ta#knNYH8V(wyq(+-@kB+HfsJuT z#@40NEM8yHpV(&dW}++0X|{L2mLAX0srU_d6LZ^gE*AM{00+u9B~S>Eb0c(Wa9xe+ z3W5O~%UVQZz;$EQhS&L9%!=$?(;*O!j}NzLV({#C)4XoIvfn7g$-+iJMHX;ZHZuVi zKLBo(V)Fze=4|y2!yJRCjT4?ySGsrlq3>}(CytTGG_@)FdDq>N0=9V!W zg)f%&6Zq`n6yKvwsBxbbI17=xAgH(>@hkn1bMn*g=;5;7D=qm%(g6jGwHdQ=f5fc( zlU4%CQEbhY&p8zj-iAU$X%gp2F}UgyfzqGp905hG_t>&2=XqmL@bEyEXKD}VW6rT9 zI!U=DrC&>hzOws;ep*_BB6?XXeGwWkjpXq>!E}F$7GfdQ;>Nrs+t&-|oK$a{>a!a9 zW=gibu53SI4NAUfzc>oCer~Ymt5|1qZ$DQyA$+_DV$a?9Ks4-Gj*g6wioaJQ9Ep;P zs}f;~XQ}fcS1$}13ESG`)qYi~&>A z@C!#7DuU0g-DjYp}i~)P>3(-?I9=50-3}3ii=<#499<^T`P34FUec9kHR?^;ky_g>&Z7e!J z5q+@K%Hm;Zy}aO5w#jOavG*Di_WQ@KjzDA@CKi8%WpRzs4v5lXvY|HU^MqW|_}LpU zvQ^N#0a6WZ1r_%IgVVN8d^`j zpPr?tWO{JFumQX<_$04KCK~kvSJ2=>)$(vG z6cmD*;Y#Q*!TEr{19QE~j`PCWiE^{$?CoI%IvQ2gC#7BF}?`)LqtE~~Kfe`cW|g3Z6@8UOOkUQp50 ziE*jQZQl0_PPjZ8aW5&Q$Q`5Z?!ei}&q82B(<9d&#(=3HFbE7KkVAOD6}<1Z=XQBq zVeX&Fwb9Mxzjo>3Rb)kn2^p+jif6_nm<^Xghco8^-(S?rASU>ujC5vL=|%B-$^GSLzdaCo=QfrJ67Z@G2j zJ7UGDK!Pn&jw7`)o_q*&hQU2t+jfY*TV(Q7xjz0xfDS^D~~ zM@1}B`dkJvzaoUqmj;ZtHqDu*1g>6=U9;nfwd%$VD`y*A*c_kD8PJ3p7gTxd@=-5} zTO=Y_z<}?!fFPKBL4)$hVwhuH%O=t7MR%a(=ffd}qip8q$o!y6jIVEJ)yggCZnAZ) z6x)>rxlXu~&sBE}v8-Rs?E;?M*c=qx{XwBv`4nn?aqPU6oJ4Gviog;tp!AXa*9$cG zb!^T%dn1A0i-tXMQ8b#InZG7K)NL|AL0qdJeXM!fBb5;`p5Ha*MZq~yqHz8Si#R_Z z{Ry|F%o&o_2s1Fec@5SN3UKboW;d4klxG&~#^-g0`Fy(k93_h<3JIU{m=Ws1Z}h|H zNcAT6eggFIhpAQ4*AjKmNV}Tc%zHl_`mt zL9XLV^axFxu#K(McSl0({Pez7}PN8-0{Uese_WW)*Td!0O zCv7?5I50~hmQ1~miyGT)a^Yw>y|RIi02*6-fHbjG9d9K5LQb3H1<(FTWj7W6hqmYO zfC7h9a>uZ@^kdW2^5_Yf60$Mo=XS9?PVHu``)onu9(6o@uYb;$!??XB0nXYE&m79A z@gq8H-9CRWeO&VutAArRWG@UMi>yZwP_m7?jg^Jq$7h%^f$Ghggd$|Xtu{S!3-f)8 zulzM(!Q(H?V%|ADf4PYyPysIO<^xp{L}&Hk?-iwrjxa9NdE0(u+HnL{`SxeZ{>bg{ z;|e&AHP1b0i1LsZ6M>RN@q#Ix-JfFn2 zx}S#{s706t?LGggP^SwM!X8g=NkQUyF&n6Ir2Uw+}O)g4&DH6GJ~h4?ZbEVxOa zcU&VShx9-E$S8Ytl%EQ)M0KTyNWi&&-v&5-J{a-gdZVGm|E^_5zG^8?bEUt#Xd>id zE#EE-56uzGbDk(-3Md3g$Z?hB(^MnNmt*Nic(`7bybDF|R-dba&pnO^HJ^ZKwq#8b z&mxx&VT!1kUV~-OCzq!;7$+7r*1A8ekxTA|92S3=ZX}^x)K7!pUVg;T;|^faK9zO1 zFhx>q57IJ5t4{%-9Dz585~)?tai^9-sYQT+d3*N5pe6}3`0lC=t1A@Y^9y4%9!&E) zU%+e+$mc|KfHXt}yr;|#Eswv&*GWzZ{C+l8mh{6{l59g%J3uF|Nj)|9XuC`M7OQ!i zQ7Rv7togXyrKZ!JB5G>#(Qjf}u%4Aj^RryWQr=Sh1)$L;HFgO19I`1#0586y@AOVC zUGk0Chiu;P+7+`_N8SI?i#W|%V89VG$LZ*Rh_N!$oTpa>>}?1b z1EF3$=;a$%u*VG`>brFPEh<^v?8(j0_yOqila8=H!Hw-Ff;ZdIDj+0d8CjV+A8eHZ^yk(gjCeh?ttc|7 z)P3vY7OwLym}ZbIBpn3ot-OUxN0Dax^df1lmx^lls_n#`VG_V!_N;hU|`+$I32h_ERF0{ zfLFRv*jXDurRhGBY}LXRGygwAZU2yOm}Ay=A?d*mF8MlWW&oo1f8+oD`4w;p-#NiK z;A%N=P%H-U-_pg=#hv5tC%dncVRbptEtwS1`pE!wZrqh-+1w93@r}%1%l=r z+JiWnX%7XzY0ee7(Tz&(M=F{X#E7tl%>A1JnWUd)A&vu!o6?~}gHR>bB6^FOi&+64 zc*YlMAIE$`OY%@^wOrEn-G~j4mGeY<6I1B&GKA2?%iD&|X|#Vn`2A&W8StVh8sEUW&`HJ%2tL%1`P9NU*=x8tEM^AMKbZDev#-&d?*lNKkqID>y zurqTZVJdeggQ)psbzEgNQ%VrI8P{NiGAW;^<08SDpZ1>-$!tCp z<>B7K1%69#e2a5U(9woYd2`1+E0NFxddf|2HK67KX=EjApIt8+6A_FEis@z3H9sH8 zYT{L!k>+v6Y7}_+_7#TEId_C8(U`~N`m2+T6^37lLfD-NDdt23+lbe# z+@186;hsuSl7lWApNIn0`9X}SE9_ey+E$KimZ^-~VQjzrKx>L4W+yWo`r}hLV;BRI zZSsqZaul zC?v?~#OBn=I2U4a|33A~`;BQt$Ptk&#dY7MUhrEqP;JSp#>LxcCp$cl$v#-4{ULh7 zoC6eB*Xe~GimAZY2kaGCYb8U zC1Wb!Z0%nn+dn+H?s|i|l^cCcf$-=>j*oRu+Zn@=E+;!yqDrayrqO5^CkFnlbd=}));FWgs}#JlVXf{=4Q^d z7KWP)mZc=Xg*E08riWFTGn7HS^00Wr`CSP;LoL^M1rgXo$IPFtfVDW$b6`v-$m5cj z5s|}~cv{h(lrqDlb&>kXBpVQT87vLbbVNECX+=0Z$^AU7GSZ5uy?pB(qa8+1;x-VP zOmm*5E%q&*KLQOWQb~_tEtVsEz4>!N#H0v#Phz)H7tE( ziO^Ck7$%l*K|CiGey_ZAiZ3k^7FA_3b`hMN_%rxT1zgsF zn01+-nsJPGj5dsxIuRAmUun0_dCkf18SDw7$?%lGPixr~H@Mh_H!$JF#eGn2P|i_K z9CgEEBFiWF0@>O9&3$We>vb!l%clEWw{wqvo`0UDqGo=p%&+WpUU_b5UViRJInP|s z-0WO)xlj4(tfXC*U4+Ai{lOQ(%2Q8tr=&5`qwG|n=SeZzCM~0o`_fV-%dCELm3Z=bJ$TWz zU9^R_@%Rt=Yx+m}ANpVYM!IEvpnV{_?YPy33PY)&@X#t~GumksYaec(Nz_ABO=3dg z%LINxFCsDS7d+^klLF3soet9cZUP3@Gv@9SxR#^bYCPYa{1;LA>jhM}D!E9wqbJD5 z;kVi+qS>bL)$yg{q46ZD*t2PG`y5{-8P2eUv!SrHv*sEQ=?@q=)$cXm+B&&>aQMh&Xroqc4<1^#c*2-<~sUT+?=b`3hOP|>sdy1D`J)K`4!@~>1 zhw~>aX1;WDefhL-3XLB`wsf-o;3F;KCsgGvy!CrwvB%&b@Y@XmL5a#cmH4qjzrpkC zbK>p9*1cAD<9(B2XVhld<_>2q9}}OXfbcsAMi54hLfTM7X}L|sw$zl3>4GVB4SLO2 zN1IFKOD328<*D}mfa&OUoK5XRk5#=XfyL%|pHX0=dgLloNJB{S6aNzl*buw|+k%x# z4s~aQ21ym=!As3cVT4VD$@Pd)xhk3uUnaMoqiv&X`$UFK!=?}OuX+>u-b4mbBq3p- z)F6crTHt76e(8d>nU93Mfv=(NpdO<>mVGO$RN!BTSBO*azR+yQcPM>G(X#5L-k@pR zYz$7n*O29}!e8&#O9^gSS6W>~d&Khvf8J96lJkpaXI;?G?!3On9-zynQ^5Wb%N5HO zdn}Zn4W0Zw11oM+DQ$5)Vn4#kL2yBENj)Cr*nUpiN}s_XrtGcuBS-Y@dX84gnNByp z4oXhxZRtXBr#gz-a_%LQ7=4ErBr_$;O-B){1nG{uk>{K{x_-PquD)n2JM++(pAH$D z@0HAAd|mqh@8Z-PtY5ayl>6PUH`;0E^a8?HiaG8%oG#|Ai)U(M3}->-U8bRiJtkj_ z@{DSm%sq>@M{jkfD~c=q_4aGn4aGVKT_!>sBO2`-LKqv^Svdk**{w~Ur@Z*`uHe%} z@V-$zUYy*30Dp^P*>cPh<2S(;yQ7i{H3}79Is%m{6&B@Hmo4Yc>x>D#46~r6)P`5J z$V>S`F;Pz~pvwBNWa(?WUwh5L>9^SzrMh~CSI#x&#}octx0BGqsJNi4AfY>Q zetCh*kcr#2R;9x7hTXp4pTWDp5i~&)NfQ%n@7P{dxmFf=otgLcjIH{H%zhtqtaK@D z&LoG6ri~^Kd4GL)(I*uh@VYjsA;{+1#0jCXun1?*_=nJZ#thwI(muI3v2R23P;u$6 zfU)B%idnH29>$x#i%7k(1vt6tTe;cd8D4w4yBo8X9e`q$9V_Nh#qrtY#BqA@d^>RJ z!|v6Agx`;g`%fs71WBe=CI@XBuH_!x!Fj(B?kI$zWKfZ4(&#o05tAdL9D%^~xVwy5 zy^flkc6`^vnc@4CpZl6$U4AIGP<4#7-|W1&t=ew(aGqK*@E{0k-pHBhh`W1tMH0;W z@X2J4QcQeLO!&mSCtSpG87Si69 z4oI*(8Q+a>Ec$f$-g9%}iSRud|GuBCd2d$M_3dfq=+&|3x8DzXL^al?nV&U7Jl5Fu zou1mWd~kSLk3Su=NMr@?+z|dgJt|Fjo*Vq%Z#euj*QR6RGu>E*2f4x!LBMvKf9Kl& zO*Ji@{2VyEJbYfgv~;oXd}Za~&7@1#6Vd2R+GRD9mBkP0Y zaCM3hV;B{W#X63D|6tk0RFvN7AYkIhWWhh^xH@tWx_Mj;IL>|>u=S&rW#oV%?cTb^ z=CJX~bc!}_S5iuHty(N?J4_N@Zmr-ug5?#3qN;ntXT-W^Y+yBbq43YxY4IpuluMT-iPm89DCn1mbPmL3*N^I)z$Du)pZeSR;akG#4|fsNE}i*@$n%)=Pr6R&r{|iyo?>Tl zJbEa{e43>w$)8pq9!uF`mk3+GaMAOP{4W4KK*GOqcYY!`HN|mVWXz#3XGiF!|DVbH ze>HF_9Q>c{?)(BoQD%Au*m0A)ySrmRN`6wRVKL4tyb%OC5_{C~zpCXZ z2-KYp0twu6)4>1JC5tIDK>r={|E>KW-kYD3&Ck^q%cf3PW=_93bBh&u)@kd%b3Eq8 z&tuxO&3|ZXwQ!SN%p$~M*^(Cf%S+!mtXUrGNL|sma$?orHLIKg*QTwbI2&9}uYbN_ z`o?u```j}<=$rJL2R!e3{j+tEx0g@2Z|*kw_KFn{*-h ze#(cm$>}y3t1>rd?aGeGiO)^XE6hh05DQsFqGBaj2Qfh#U~TXo#31qv>O6W3a~?a2 z8v)MZPZNfT1EfB37p0BbM60D&GRm21mW<8kP`D^w0Y61>M0h~tFWxM1k}j6blKn0D zD1WTDraYtSQrBoST23jtEK3(tzNccd-qA4I@XPqJ^15lbs;RoPhFgoN%dU@Y*xR_J zX?3$r3#jEy>qOg`_LdG^C$9_Jo!t}LySLA)f91)!C;tq*9-J6DJ=}6icbb0&dp74> z>`36~#`BBDCXKziFn+Q3lHoG%3gT+wcx*uHZalqtexmV~>^A03(%rrHT<_1l z|M|g$*Fd&X`SA=%tgCi?W)=!-zawDyR*%+fwGzCiTA>7!EMEP13m;_+&1iX^bYh+ zv>(PF7eLrW-c1i=?-2ya_R9Auf>j}!{kjACgQn2BL(O5G;r$V(BgYPpM_rG;8FTB% z-Prp_AH_X8_VW0f_zwwRPJB=Nl?+Omk~%5%Z`!Z)pBdjXzhr&NexLI;_f_8W{3itu z3hx$86kh|MhYUhHV0G{cgaXM!kxAT^bH2RQBlb^QLAv(u$uhZ^tu!EM;i_|9%?$!9NZGrx~DC$eNRVF=f19x?$Dly z-k84Q{mCb@28sqzL!@EGDc)(x8P(Z}b2THaqkZSkj9tEP`{L6}A1?oMb=tViHHYgi zH@4mkoCv=ae>?jQ;x6T$=)Uqn*+b)_n#YY#TAy}4>v`V)V({hFS5IDld28{`=Dp(w z*N@(xc7G2268kmzTh{lyANfCX{z>^2{X6iF+h4o?9RJBvr`(vDG40g!X)`v>jIc_b zRb&mHjhus=i=2nBfy^&hkYO9YFv4!nqOFUamw4LmSn9vb&%t-Or=#nNRVx>-nzPzs z^^Y}gogS^dv2N7)q)Y4iDp$=0;YO+(+P%OdWmD|tgPs9iTehs#jIvQ0Tvxm9^p`Bep# zg~pUh~RZ;2)O_=sj>7lYPT|{|QMXWx- zkY>!QgqsLeoN8rFRc%+@X#Ks0PfatL9b3Fx!`pJ&sU78=16>b#CiSl9+ufgfk~&}- z92@#@%J#I+nfSAWbNZ2Uqi@FMUf6W;@FnnN(Ur!l7sg**pLWCH=9Y;Aw-RrI?@;f` z?iubkJ?MRS`qBBvm!4dHdhyxl^Pv}=FRNcEUQ^%XzdiEq*!#E-(H{?d+VR=ti_Mp> zU$1>@`p*85^waI<=YJZ0rTw1wyXnutKM(#E{?GY;a|(OvjA@50anmI;N@tc^X=Vwn z39}34B+NZ9Z>!CU`7`FfUvSN~YoW%DxCp-(vjl9Pwlr#4fP?FDJ4cY?%N18v_N>yc z=B+_GrK}BI=i|J_WvdJUJM;R)Dcz_UK$}b z&jVysZgfIS_>tYQUPqnc79E>$?C0@U@wXF3PxK^KCn=KYDe%;^w8QDUGTbvAvaGVc zXFtigoZFjc%9j)n3Ui90i~YgQ5F5xp(8sXz@K%HxNkQeIqcJ`yne@%?gRdV5kqOi$Wz?Y`ZK*}ubulaI_td4*xn1t z7lBKf%Y9ewk59R__WFSv#WxibL$}`DS#me%Uh)0X2Nxbqethsr$*?Z#;Jkdx*B6VJG$& zrCI8RC6E;b9+KFb}OA8FoNx;ZCGl7yFi=wy$@|yR)3#>wo{)y-H2YVSWvqM z9VgX^iqTafJ#sc?7XN9y7z5)T@^8ibWp7x*!VWRdew%^XTMM~6feNfdp0Y6gmY0Zl}F(d=r~!|aT@x9_@G}0CPKhmyd86j^ZpANY3e+FCl;w`-Zx}GHq|n# z(8%|dJNOS#2g*ZXji`Q2=P@RFhl0EF3c6bcU1W#8#S8xIjdVLzblVzf-?wcr3Awht z*mMw?(zu0p4|%q_5PBIEXqX#Ui|Q|P-#H7tO*6-CDcX{&{j>~m>GGM04~XV-i37oi z_5pUKAL4yiBKH|Gws{OX4SA>bRNORFd?j^9GU{Uam4(5m2=2U(E(q2=(#_w9wDBt^ zqYx)X-x{M4qM?V}`G`L~0?2+OrOonaCTdp0?d=mNWaS!LHS!Ec_2vzIK6d>zEDeNd z=si!Jiiy#;P~0(_IZMbS%s%i%(k~3_Xf{cKdA@@~(qNg3m=q*1>D5_kEq(eph@zun zdf4O+%8Cjs={tD}>mn(I^u8c9S2 zz7t1mlx-_3CC(Q6GQ0^|L202gA&j>>`WwNXv((p>7%4u)$kK{H5R(PG{S@dK)MEKVy5>FA#nr};xvz|RkCe3O|8oNeZ zRzIb!g%DwSshx`N&~K!U<3meN<^9IbP<2Nw2Oh~cd((kWGH;vZ_-cqRf8u;K zLD@-Y72wC3k?PO*Ew%S3&w(qI^?4qEu>8&8V?e6*#@0IkSbb#PVf++k+aqV<>QV7% z9bwDxwiX#awRg9A3UID%gfaujY`BvP12$K)BToU-jnP{V0yA~T=GFo^%)Jjo2^Vj4 zjZEO5UJPv>!p}dGsPY29Cl`=I0h>-_&N%K~^ZCf_xX${0TQ1?cE9TC1!F^`Dy|Z1A zh?bqs<{{C$8=^Ul=(+OK>?P=VL@Bck9h|LZ5YP<~5IPOBX$yzG7CUL)CgwHV-CIHY z4U~~nQ@OjyqeSgk>Cu#~8+*Ca z6@tNy>`gLM4W3C6OQjBs`GVho9ql=dU`)S;( zJoe_Uiar4|y>)8kNBTg$Uf4^Et18FLq`DiXr>ZGi%AOsZMNZJvd8o)%D!*9{WGT(# zijFmM>U3`tb4q`3gVQQ^e)P1okhx5@@*6B0F^w|km|9TR95P1RZF&@ zKD~UORdhL~=O0G>xevzUbnC$f0uQRLClaNiAluL>nPg_;=>yiJp4xQxHqtTed#flC zhgv!MkK!XLu3cEN7qvq7Rnm@{!MQ6sfpS9F3ihDjNtwJ<)Q^xN&IDTMmdx?OU}s`^ z%W?f9zVe5p-)(8KRm5kyN#YX1FZNI2LBf9cdj4+wjYKpTf|mzl+28Q^jR(isHyS$WNa6Tc7uV%qNrwg zvG?@Wp>2g238vLAJ@CgQReyS zb*$y&d#BMd*D^}eDKSa?Q-cy(DXJMH-es8y{F-x9{5hV>_7G}=-ZF>zL$2GHYx%v? zw=-9e`KJ=3it38SM$s2ztU5=4Ef1lScp+LgcoHX4wd!~~OC?_$6u|V9ep_G55J|UL z-eMdehYhDmomyWvoD~5LcU5%3^qSc;6L;JwFZO3YDd!%a&vGxFzo&uGtiH0|mtmtg zW67i|$V-LE7IG+hmyQeM18vO-0%(MI(~E?nHtJn)zMV=hrOlQbZiQf-7r6JoUx>OmkXMn zsqC~|NGl>l+%$#d8!g`MIs1!5#pXxXh&)*y{*wisOrJGB z_%PqGfssGuuLZL$opKrVuUg^NZkPd9T;x z@%s4DQ?77I2$D8k>6yyO6<+G73R}TO#k|sNEI~F;y*SH99IaRxwNmg(R=snO_gd<= zW)g3$#A-?%`wRioI#L?b*i)XXdQj`Y&ylMtPhrkUwH22#C8Ael5r-uLKttSlh?k>2 zyt6aPE?9dNYG4iSJ za`{&j+tFDrLl=TT;w!jw)nj$*2t-AO=`p^9j@3^9_o4sj<^##eMvVe+IuxnI0jqq# z@~Oaq<)tMOyd4N6;sBtkd$l{58|943KXf*&r{WE53Dl#^o$^o8V|6#hF;t__lR4hw zC9dTC%QwoZC_x~Qu)wTrYO;jSN=%ho`Eu%Lg*%r72`kND_axbARDfD zw549Ms_fOWQSl*>6KFG+3#>GBRM`&1OZ57o?kdu?vevf3q9pa##-9nv3QAq)ex(dj zvwRCme5d@{GJ|-A$Qrbr>jw6aB-Kv^tg}B=v2g0xc0&rTC@4o) ziz9mEXz$`q+SQkC13EzPt9x6kR4vTlwtt_aYxK5qwSMmF6(>K9PTsplG+INKUt>AF;?;sOhpxQ!=BjE zuZ$RXv1UHQ)$W*PDWl%3b^;*fcw3M>la~^ZRaN0p2&X8GcYwat0%e>ieNh3i1teKP(DA?&1g|NHD-UaOrh|AS( zN+m_>VODnkQS4y=B){20>3PK%sRy?ck+Z8xn_4rf4fb`9q8rQJ)o6F$(|DTnkP5 ztT|~_u_!CE+HF=_hnT=qd~5F~-1owP4jwKfF{C*c`!LM9PK_PfQB#$Ged2uFn1