Добавил:
при поддержке музыки группы Anacondaz Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
1
Добавлен:
07.06.2026
Размер:
1.99 Mб
Скачать

GM = G(b1+1, b2+1), HM = H(b1+1, b2+1) – объекты в корзинах b +1, …, b ;

GR = G − CG(b2+1), HR = H − CH(b2+1) – объекты в корзинах b +1, …, B−1.

Для каждой допустимой пары (удовлетворяющей HL, HM, HR ≥ minW)

вычисляется Gain3 по формуле (2.15) и сравнивается с текущим максимумом.

Число рассматриваемых пар равно B(B−1)/2. При B = 64 это 2016 пар на признак

– на несколько порядков меньше точного перебора при n > 100.

Рисунок 2.3 – Гистограммный поиск пары порогов разбиения Пороги определяются по номерам оптимальных корзин по формуле (2.19):

θ1 = xmin,j + (b1+1) δj , θ2 = xmin,j + (b2+1) δj (2.19)

В итоге алгоритм поиска оптимального троичного разбиения для одного признака имеет сложность O(n + B²): O(n) на построение гистограммы и O(B²/2)

на перебор пар. При B = 64 и n > 4096 доминирует слагаемое O(n).

2.2.7 Адаптивный режим: критерий выбора типа разбиения

Вадаптивном режиме алгоритм вычисляет в каждом узле оба варианта:

лучшее бинарное разбиение с выигрышем Gain2* и лучшее троичное с выигрышем Gain3*. Разбиение выполняется тем способом, который даёт больший выигрыш, при условии, что этот выигрыш положителен:

56

троичное, если Gain3* > Gain2* > 0;

бинарное, если Gain2* ≥ Gain3* > 0;

лист, если max(Gain3*, Gain2*) ≤ 0.

Блок-схема алгоритма выбора типа разбиения представлена на рисунке 2.4.

Рисунок 2.4 – Выбор типа разбиения в адаптивном режиме

Следует подчеркнуть, что троичное и бинарное разбиения ищутся по одному и тому же признаку j с теми же гистограммами, что не требует дополнительного прохода по данным. Поиск бинарного разбиения является частным случаем и выполняется за O(B), троичного – за O(B²/2).

Из соотношения (2.16) и доказанного свойства (2.18) следует, что в адаптивном режиме троичное разбиение предпочтительнее бинарного только тогда, когда выделяемая средняя группа объектов вносит достаточно большой дополнительный вклад, перекрывающий дополнительный штраф γ. Таким образом, адаптивный режим автоматически согласует глубину дерева и тип ветвления: там, где данные поддерживают выделение трёх отчётливых подгрупп,

строится тройной узел; там, где нет, – бинарный.

57

2.2.8 Анализ вычислительной сложности

Проведём сравнительный анализ вычислительной сложности поиска оптимального разбиения в трёх режимах работы алгоритма для одного узла при d признаках и n объектах. Анализ охватывает три фазы: построение гистограмм,

построение префиксных сумм и поиск оптимального разбиения.

Рисунок 2.5 – Зависимость числа листьев дерева от глубины

Таблица 2.5 – Вычислительная сложность поиска разбиения для одного узла

 

Фаза

 

 

Бинарный режим

 

 

Троичный режим

 

 

Адаптивный

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

режим

 

 

 

 

 

 

 

 

 

 

 

 

Построение гистограмм

 

O(n·d)

 

O(n·d)

 

O(n·d)

 

 

 

 

 

 

 

 

 

 

Построение префиксных

 

O(B·d)

 

O(B·d)

 

O(B·d)

сумм

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Поиск

оптимального

 

O(B·d)

 

O(B²·d)

 

O(B²·d)

разбиения

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Итого

(доминирующее

 

O(max(n,B)·d)

 

O(max(n,B²)·d)

 

O(max(n,B²)·d)

слагаемое)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

При n=3000, B=64, d=10

 

≈ 30 000 оп.

 

≈ 50 960 оп.

 

≈ 50 960 оп.

 

 

 

 

 

 

 

 

 

 

 

 

58

Из таблицы 2.5 следует, что при n = 3000 и B = 64 троичный режим требует примерно в 1.7 раза больше операций, чем бинарный, на один узел. Однако реальный прирост времени обучения ансамбля оказывается выше – в 5–8 раз – по нескольким причинам. Во-первых, при троичных разбиениях каждый узел порождает три поддерева вместо двух, что увеличивает общее число узлов. Во-

вторых, при той же максимальной глубине троичные деревья содержат значительно больше листьев (3d против 2d), что влечёт большее число вызовов алгоритма поиска разбиения. По этой причине при использовании троичного или адаптивного режима рекомендуется снижать параметр max_depth: для троичного режима глубина 4 даёт сопоставимое с бинарным режимом глубины 6 число листьев (34 = 81 против 26 = 64).

Описанный алгоритм применяется для каждого из d признаков и для каждого из T деревьев ансамбля. Полная сложность обучения ансамбля в бинарном режиме составляет O(T·n·d·depth), где depth – средняя глубина дерева;

в троичном режиме добавляется множитель порядка B/2 ≈ 32. Это делает гистограммный алгоритм значительно более привлекательным по сравнению с точным перебором (O(B²) вместо O(n²)) при сохранении высокого качества приближения.

2.3Программная реализация

2.3.1Общая архитектура программного комплекса

Разработанный программный комплекс состоит из шести компонентов,

организованных в два уровня. Первый уровень – вычислительное ядро на языке

C++ – содержит реализацию алгоритма и компилируется в разделяемую библиотеку. Второй уровень – Python-библиотека, вызывающая ядро через механизм ctypes. Такое разделение обоснованно с точки зрения требований,

сформулированных в подразделе 2.1: требование высокой скорости вычислений выполняется C++-ядром, требование удобства использования и совместимости с инструментами анализа данных – Python-интерфейсом.

59

Компоненты C++-уровня. Вычислительное ядро включает следующие

компоненты:

– data.h – модуль работы с данными: структуры хранения выборки,

загрузка и сохранение CSV-файлов, вычисление метрик качества,

генерация синтетических данных;

tree.h / tree.cpp – модуль дерева решений: структуры узла и дерева,

построение гистограмм, поиск оптимального разбиения (бинарного и троичного), рекурсивное построение дерева и предсказание;

gbm.h / gbm.cpp – модуль градиентного бустинга: параметры ансамбля,

вычисление градиентов и гессианов, итерационный цикл обучения,

предсказание ансамблем;

tgbm_api.h / tgbm_api.cpp – C API: набор функций с C-совместимыми сигнатурами, экспортируемых из разделяемой библиотеки и вызываемых из Python;

main.cpp – точка входа исполняемой программы для проведения

сравнительных экспериментов.

tgbm.py – компонент Python-уровня, модуль, содержащий класс

TernaryGBM, который при вызове методов обращается к функциям разделяемой библиотеки через ctypes. Архитектура программного комплекса представлена на рисунке 2.6.

Разделяемая библиотека (libtgbm.so на Linux и macOS, tgbm.dll на

Windows) собирается из файлов tree.cpp, gbm.cpp и tgbm_api.cpp с флагом - shared. Исполняемая программа для экспериментов компилируется из тех же файлов плюс main.cpp. Заголовочный файл data.h не компилируется отдельно – он включается директивой #include. Поддерживаются два способа сборки: через

CMake и прямым вызовом компилятора g++ из командной строки.

60

Рисунок 2.6 – Архитектура программного комплекса

2.3.2 Модуль дерева решений

Модуль дерева решений является центральным компонентом всего алгоритма. Именно в нём воплощены формулы (2.11)–(2.19), выведенные в подразделе 2.2. Модуль состоит из заголовочного файла tree.h с объявлениями всех структур и классов и файла реализации tree.cpp. Он не зависит ни от каких внешних библиотек: используется исключительно стандартная библиотека

C++17.

В основе модуля лежит перечисление NodeType, определяющее тип узла дерева. Значение LEAF соответствует листовому узлу, хранящему предсказание;

BINARY – внутреннему узлу с бинарным разбиением по одному порогу;

TERNARY – внутреннему узлу с троичным разбиением по двум порогам.

Применение именованного перечисления вместо числовых констант повышает читаемость кода и упрощает добавление новых типов разбиения в будущем.

Каждый узел дерева описывается структурой Node со следующими полями: type (NodeType) – тип узла; feature (int) – индекс признака разбиения,

61

для листа равен −1; threshold1 (double) – первый порог; threshold2 (double) –

второй порог, используется только при TERNARY; weight (double) –

оптимальный вес листа w* по формуле (2.11); left, mid, right (int) – индексы дочерних узлов в массиве, для листа равны −1, поле mid используется только при

TERNARY.

Всё дерево хранится как std::vector<Node>, где элемент с индексом 0

является корнем. Такое «плоское» представление выбрано намеренно: оно обеспечивает хорошую локальность данных при обходе, поскольку последовательные узлы располагаются в памяти близко и вероятнее всего окажутся в кэше процессора. Альтернативное представление через указатели потребовало бы разыменования на каждом шаге и значительно ухудшило бы кэш-поведение. Хранение дерева решений в виде плоского массива проиллюстрировано на рисунке 2.7.

Рисунок 2.7 – Хранение дерева решений в виде плоского массива Единственное ограничение плоского хранения: при рекурсивном

построении дерева вектор может перевыделить память (при вызове push_back),

что делает ранее полученные указатели недействительными. Поэтому в реализации используются индексы, а не указатели, и поля left, mid, right

заполняются только после завершения всех рекурсивных вызовов.

62

Все параметры построения дерева вынесены в отдельную структуру

TreeParams: max_depth, lambda, gamma, min_child_weight, n_bins, branching. Это позволяет передавать весь набор параметров одним объектом и создавать деревья с разными настройками, не дублируя аргументы в каждом вызове.

Результат поиска оптимального разбиения для одного признака хранится во внутренней структуре SplitInfo с полями: feature, threshold1, threshold2, is_ternary (тип), gain (значение выигрыша). Поле gain инициализируется значением -∞, что делает логику сравнения с текущим максимумом корректной при любом входном значении, в том числе, когда допустимых разбиений не найдено вовсе.

Метод Build принимает матрицу признаков, векторы градиентов и гессианов, а также набор индексов объектов, и строит дерево рекурсивно. Метод

Predict принимает вектор признаков одного объекта и возвращает предсказание через обход дерева от корня до листа. Статистические методы LeafCount, MaxDepth, AvgDepth, TernaryCount возвращают характеристики построенного дерева путём рекурсивного обхода.

2.3.3 Модуль градиентного бустинга

Модуль GBM реализует алгоритм градиентного бустинга, управляя итерационным процессом обучения ансамбля деревьев. Он использует модуль

Tree для построения каждого отдельного дерева, но сам не зависит от модуля

Data – это позволяет применять GBM с данными любого происхождения, а не только загруженными из CSV.

Перечисление Task задаёт тип задачи: REGRESSION или

BINARY_CLASSIFICATION. Тип задачи определяет формулы вычисления градиентов и гессианов (формулы (2.8) и (2.9) из подраздела 2.2), а также необходимость применения сигмоид-функции к предсказаниям при вызове

Predict.

63

Параметры ансамбля сосредоточены в структуре GBMParams: n_trees (число деревьев T), learning_rate (шаг обучения η), base_score (начальное предсказание F0), а также вложенная структура tree типа TreeParams.

Вложенность обеспечивает чёткое разделение между параметрами ансамбля и параметрами отдельного дерева.

Метод Fit реализует итерационный цикл градиентного бустинга. На каждой итерации: вычисляются градиенты и гессианы для всех объектов по текущим предсказаниям ансамбля; создаётся и обучается новое дерево методом

Tree::Build; предсказания ансамбля обновляются добавлением взвешенного (×η)

вклада нового дерева; готовое дерево добавляется в вектор trees_. Блок-схема цикла обучения представлена на рисунке 2.8.

Рисунок 2.8 – Итерационный цикл обучения ансамбля (метод Fit)

Метод ComputeGradients реализует формулы (2.8) и (2.9) для двух типов задач. Для регрессии: gi = Fi − yi, hi = 1. Для классификации: pi = σ(Fi), gi = pi − yi, hi = max(pi(1 − pi), ε), где ε = 10−6 защищает от вырождения при pi → 0 или pi

1.

64

Методы AvgTreeDepth, MaxTreeDepth, AvgLeafCount, AvgTernaryFrac

агрегируют соответствующие показатели по всем деревьям ансамбля. Метод

AvgTernaryFrac вычисляет долю узлов с троичным разбиением среди всех внутренних узлов всех деревьев – один из ключевых показателей для анализа результатов экспериментов.

2.3.4 Модуль работы с данными

Модуль Data реализован как единый заголовочный файл data.h,

содержащий только шаблонные и встроенные (inline) функции. Это означает, что он не требует отдельной компиляции – достаточно включить его заголовок.

Модуль изолирован от логики алгоритма и не содержит сведений о деревьях или бустинге.

Выборка хранится в структуре Dataset, содержащей матрицу признаков X (тип std::vector<std::vector<double>>), вектор целевых значений y и список имён признаков feature_names. Вспомогательные методы Rows() и Features()

возвращают число строк и столбцов.

Функция LoadCSV читает файл в формате CSV с заголовком, где последний столбец является целевой переменной. Функция SaveCSV сохраняет датасет в тот же формат. Сохранение используется в main.cpp после разбиения выборки: полученные файлы train.csv и test.csv затем читаются Python-скриптом для воспроизведения тех же условий эксперимента.

Перемешивает объекты с заданным зерном генератора случайных чисел и разделяет их в соотношении, задаваемом параметром test_frac (по умолчанию

0.2). Фиксированное зерно обеспечивает полную воспроизводимость разбиения.

Функция RMSE вычисляет среднеквадратичную ошибку, MAE – среднюю абсолютную ошибку, AUC – площадь под ROC-кривой методом трапеций. Эти функции используются в main.cpp для оценки качества каждого варианта алгоритма.

65

Соседние файлы в папке магистерская диссертация