Почему permutation importance для LightGBM считается долго и как ускорить её, не меняя результат
Permutation importance задаёт обученной модели прямой вопрос: насколько хуже она работает, если перемешать
один признак? В scikit-learn это один вызов, sklearn.inspection.permutation_importance, и на модели LightGBM
он часто оказывается самым долгим шагом анализа. Почти всё его время уходит на одно действие. Одни настройки
меняют только это время и оставляют каждый бит результата прежним, другие быстрее потому, что считают уже
другое. Ниже — замеры обоих видов и скрипт, чтобы их повторить.
Куда уходит время
permutation_importance(model, X, y, n_repeats=R) один раз оценивает модель на проверочной выборке. Потом для
каждого из F признаков и каждого из R повторов перемешивает столбец этого признака и оценивает модель заново.
Каждая оценка — полное предсказание по всем N строкам, а каждое предсказание проходит каждое из T деревьев от
корня до листа для каждой строки:
(1 + F × R) вызовов predict × N строк × T деревьев
Наш пример из 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 — наша бесплатная сборка LightGBM 4.7.0 для Linux
x86-64. Это официальный пакет Python с заменённой библиотекой, поэтому import lightgbm и API остаются прежними:
pip uninstall -y lightgbm && pip install lightgbm-takt
Её предсказания на той же машине совпадают с официальным пакетом бит в бит. Бесплатный пакет ускоряет однопоточные анализы с повторными вызовами — permutation importance, частичную зависимость, сценарии «что если» — на моделях до 300 деревьев. Замеры из нашей витрины LightGBM: весь процесс 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 с).
Запустите сами
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,
zen4-official.txt,
zen3-lightgbm-takt.txt и
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 и lightgbm-takt на PyPI.
Заметка подготовлена с AI-ассистентом по нашим измерениям; скрипты запускались на Linux, как описано выше. Исправления присылайте на [email protected].
По желанию. Включая комментарии, вы соглашаетесь на использование сервиса TAKT (ABX DEVELOPMENT LLP) на comments.taktcycles.com, доставляемого через Cloudflare, и его подключения к GitHub. Получающие запросы сервисы получают данные соединения, включая ваш IP-адрес. Наш сервис получает публичный адрес и описание заметки. Функция обращается к сохранённому входу в комментарии и, если вы войдёте, запоминает вход в локальном хранилище этого сайта между посещениями, без установленного срока автоматического удаления. Читать комментарии можно без входа. Публикации и реакции общедоступны на GitHub; выбор предпросмотра также отправляет в GitHub ещё не опубликованный черновик. Отозвать разрешение можно кнопкой «Отключить комментарии и забыть вход». Подробнее, включая международные передачи, — в Privacy Notice и Recipients Schedule. Саму заметку можно читать без включения комментариев.
Обсудить заметку на GitHub. Комментарии — в открытом обсуждении GitHub Discussions. Ссылка ведёт на GitHub, где действуют его условия и политика конфиденциальности; читать можно без аккаунта, писать — с аккаунтом GitHub.