# Почему permutation importance для LightGBM считается долго и как ускорить её, не меняя результат Permutation importance задаёт обученной модели прямой вопрос: насколько хуже она работает, если перемешать один признак? В scikit-learn это один вызов, `sklearn.inspection.permutation_importance`, и на модели LightGBM он часто оказывается самым долгим шагом анализа. Почти всё его время уходит на одно действие. Одни настройки меняют только это время и оставляют каждый бит результата прежним, другие быстрее потому, что считают уже другое. Ниже — замеры обоих видов и скрипт, чтобы их повторить. ## Куда уходит время `permutation_importance(model, X, y, n_repeats=R)` один раз оценивает модель на проверочной выборке. Потом для каждого из F признаков и каждого из R повторов перемешивает столбец этого признака и оценивает модель заново. Каждая оценка — полное предсказание по всем N строкам, а каждое предсказание проходит каждое из T деревьев от корня до листа для каждой строки: ```text (1 + F × R) вызовов predict × N строк × T деревьев ``` Наш пример из [pi_cost.py](lightgbm-permutation-importance/pi_cost.py) — `LGBMClassifier` из 300 деревьев до 63 листьев, обученный на синтетических данных (`make_classification`). Проверочная выборка — 10 000 строк × 50 признаков, `n_repeats=5`, `scoring="roc_auc"`. Это 251 вызов predict и 753 млн проходов по деревьям. Один `predict_proba` по 10 000 строк занял 234 мс на одном ядре AMD Threadripper PRO 5975WX (Zen 3) и 179 мс на одном ядре AMD Ryzen 9 7950X3D (Zen 4). 251 × 234 мс — это 58,8 с, а весь вызов занял 59,2 с: на предсказания уходит около 99% времени, на перемешивание, копирование и подсчёт метрики — остальное. Значит, время пропорционально каждому из четырёх множителей. К модели относятся только деревья; если уменьшить F, R или N, изменится то, что измеряется. ## Тот же результат за меньшее время Все настройки в этой таблице дали побитово одинаковые важности: один и тот же SHA-256 всего массива признаки × повторы, на обеих машинах. Время — медиана трёх запусков вызова `permutation_importance`; процесс ограничен 8 процессорами на машине с Zen 3 и 4 на машине с Zen 4. | Настройка | Zen 3, 8 CPU | Zen 4, 4 CPU | |---|---:|---:| | LightGBM в одном потоке (`n_jobs=1` у модели), вход pandas | 59,6 с | 46,0 с | | то же с массивом numpy | 59,2 с | 45,9 с | | как есть по умолчанию: вход pandas, потоки LightGBM по умолчанию | 9,1 с | 13,0 с | | LightGBM на всех CPU, вход numpy | **8,6 с** | **12,5 с** | | `permutation_importance(n_jobs=<все CPU>)`, LightGBM в одном потоке на воркер | 10,4 с | 13,8 с | | `n_jobs=2`, LightGBM на половине CPU в каждом воркере | 10,4 с | 14,5 с | | `n_jobs=<все CPU>` и потоки LightGBM по умолчанию | 11,6 с | 15,2 с | **Задействуйте каждое ядро, но один раз.** LightGBM предсказывает строки параллельно. В его API для scikit-learn по умолчанию (`n_jobs=None`) берётся число физических ядер или число доступных процессу CPU, если их меньше, так что вызов «как есть» уже их использует: 9,1 с вместо 59,6 с. Если модель создана с `n_jobs=1`, что часто бывает в коде кросс-валидации, permutation importance идёт на одном ядре. Верните значение `model.set_params(n_jobs=None)` (по умолчанию в LightGBM) или числом ядер перед вызовом. **Потоки или процессы, но не то и другое сразу.** `permutation_importance(n_jobs=k)` распределяет признаки по k процессам-воркерам, а LightGBM в каждом воркере всё равно запускает свои потоки. Если `n_jobs` равен числу CPU, а у LightGBM настройки по умолчанию, получается 64 потока на 8 CPU: 11,6 с, на 36% дольше, чем если отдать CPU одному LightGBM. Если распараллеливаете по признакам, дайте модели один поток на воркер (`n_jobs=1`) или следите, чтобы воркеры × потоки равнялись числу CPU. На этих машинах собственные потоки LightGBM оказались быстрее процессов (8,6 с против 10,4 с и 12,5 с против 13,8 с). 50 признаков не делятся поровну на 8 воркеров, и в последнем круге часть из них простаивает; кроме того, время уходит на запуск воркеров и передачу им модели и данных. Что выиграет у вас, зависит от числа строк и признаков, поэтому замерьте оба варианта на своих данных. **Почему результат не меняется.** scikit-learn берёт из `random_state` одно зерно и перемешивает по нему каждый признак, каким бы ни был `n_jobs`. LightGBM считает предсказание каждой строки в одном потоке и складывает деревья в одном и том же порядке при любом числе потоков. Задайте `random_state` целым числом: при значении по умолчанию `None` каждый запуск перемешивает по-своему, и два запуска всё равно не совпадут. **pandas почти ничего не стоит, пока предсказание дорогое.** LightGBM переводит DataFrame в массив numpy при каждом вызове predict, здесь 251 раз. При 300 деревьях в одном потоке это 0,6% времени (59,6 с против 59,2 с); при LightGBM на всех CPU — 6% на Zen 3 (9,1 с против 8,6 с) и 4% на Zen 4. Для модели из 30 деревьев на 8 CPU с DataFrame вышло 1,75 с, с массивом numpy — 1,40 с. Передача `X.to_numpy()` дала здесь тот же результат: все столбцы числовые и стоят в том же порядке, что при обучении. Если есть категориальные столбцы pandas, оставьте DataFrame: LightGBM сам сопоставляет категории. ## Быстрее, но результат другой | Настройка (воркеры на всех CPU, LightGBM в одном потоке в каждом) | Zen 3 | Zen 4 | Результат | |---|---:|---:|---| | все строки, 5 повторов (эталон из таблицы выше) | 10,4 с | 13,8 с | эталон | | `max_samples=0.5` | 6,1 с | 8,0 с | другой | | `n_repeats=2` | 5,3 с | 6,8 с | другой | | вход, приведённый к float32 | 10,3 с | 13,9 с | здесь тот же, но это не гарантировано; не быстрее | - `max_samples` оценивает каждый признак на случайном подмножестве строк: это другая, более шумная оценка. - Меньше повторов — сдвигается среднее, а стандартное отклонение по двум повторам почти ничего не говорит. - Вход float32 здесь дал тот же результат и никакого выигрыша. LightGBM сравнивает вход с порогами, которые хранятся как 64-битные числа, поэтому значение, лежащее в пределах округления float32 от порога, может уйти в другую ветку. - Результат меняют по построению (их мы не замеряли): предсказание меньшим числом деревьев (`num_iteration`), ранняя остановка предсказания, меньшая или другая проверочная выборка, другая мера важности, например счётчики split или gain или значения SHAP. Для беглого взгляда это может быть правильным выбором. Просто числа будут другие. ## Более быстрая библиотека с теми же результатами: lightgbm-takt [lightgbm-takt](https://pypi.org/project/lightgbm-takt/) — наша бесплатная сборка LightGBM 4.7.0 для Linux x86-64. Это официальный пакет Python с заменённой библиотекой, поэтому `import lightgbm` и API остаются прежними: ```sh pip uninstall -y lightgbm && pip install lightgbm-takt ``` Её предсказания на той же машине совпадают с официальным пакетом бит в бит. Бесплатный пакет ускоряет однопоточные анализы с повторными вызовами — permutation importance, частичную зависимость, сценарии «что если» — на моделях до 300 деревьев. Замеры из нашей [витрины LightGBM](/showcase/lightgbm/README.html): весь процесс Python на одном ядре, множитель по тактам процессора относительно официального пакета 4.7.0: | Нагрузка | Бесплатно, Zen 3 | Бесплатно, Zen 4 | Полная версия, Zen 3 | Полная версия, Zen 4 | |---|---:|---:|---:|---:| | Permutation importance, 158 количественных признаков, классификатор | 3,30× | 2,74× | 16,75× | 18,35× | | Permutation importance, OpenML kick (категории, пропуски) | 2,63× | 2,82× | 12,97× | 14,64× | | Частичная зависимость + ICE, 20 признаков | 3,67× | 3,12× | 16,77× | 18,31× | | Сценарии «что если» (30) | 2,50× | 2,60× | — | — | «Бесплатно» измерено на моделях из 300 деревьев — это предел бесплатного пакета. «Полная версия» — коммерческая лицензия, измерена на моделях из 1 000 деревьев (количественные признаки) и 500 деревьев (kick); модели такого размера бесплатный пакет считает с официальной скоростью. На нагрузке из этой заметки, с тем же хешем результата, что у официального пакета, в каждом запуске: | Настройка | Zen 3, официальный | Zen 3, lightgbm-takt | Zen 4, официальный | Zen 4, lightgbm-takt | |---|---:|---:|---:|---:| | одно ядро | 59,2 с | 22,7 с | 45,9 с | 18,0 с | | `n_jobs=<все CPU>`, LightGBM в одном потоке в каждом | 10,4 с | **7,8 с** | 13,8 с | **10,2 с** | | LightGBM на всех CPU | 8,6 с | 9,4 с | 12,5 с | 10,9 с | | как есть по умолчанию (pandas, потоки по умолчанию) | 9,1 с | 9,2 с | 13,0 с | 11,0 с | С бесплатным пакетом используйте процессы-воркеры (`n_jobs` у `permutation_importance`) и один поток LightGBM на воркер: на обеих машинах это была самая быстрая настройка без изменения результата. С собственными потоками LightGBM чередующиеся запуски дали разную картину: на Zen 4 бесплатный пакет был быстрее официального пакета (медиана 10,96 с против 12,41 с), на Zen 3 — медленнее (9,35 с против 8,66 с). ## Запустите сами ```sh python -m venv a && a/bin/pip install lightgbm==4.7.0 scikit-learn pandas a/bin/python pi_cost.py prep work # данные и модель, около 2 с a/bin/python pi_cost.py count work # число вызовов predict и время одного вызова a/bin/python pi_cost.py all work 3 # все настройки по 3 запуска, около 12 минут на 8 CPU ``` Чтобы закрепить процессоры, добавьте перед командами `taskset -c 0-7` (или другой список CPU). Чтобы проверить lightgbm-takt, поставьте его во второе окружение и запустите там `pi_cost.py all work 3`; одинаковые хеши означают одинаковые результаты. Наши замеры — в [zen3-official.txt](lightgbm-permutation-importance/zen3-official.txt), [zen4-official.txt](lightgbm-permutation-importance/zen4-official.txt), [zen3-lightgbm-takt.txt](lightgbm-permutation-importance/zen3-lightgbm-takt.txt) и [zen4-lightgbm-takt.txt](lightgbm-permutation-importance/zen4-lightgbm-takt.txt). Измерено 12 октября 2026 года: LightGBM 4.7.0, scikit-learn 1.9.1, numpy 2.5.4, pandas 3.0.6, joblib 1.6.0, Python 3.12. Zen 3: Ubuntu 24.04, 8 CPU на 8 разных ядрах. Zen 4: Debian 13, 4 ядра. Раскрытие: TAKT продаёт ускоренные сборки библиотек вроде LightGBM, вывод которых на той же системе совпадает с оригиналом бит в бит; см. [пакет LightGBM](/showcase/lightgbm/README.html) и [lightgbm-takt на PyPI](https://pypi.org/project/lightgbm-takt/). Заметка подготовлена с AI-ассистентом по нашим измерениям; скрипты запускались на Linux, как описано выше. Исправления присылайте на hello@taktcycles.com.