8964 lines
250 KiB
ObjectPascal
8964 lines
250 KiB
ObjectPascal
/// Модуль моделей машинного обучения и матричных преобразований.
|
||
///
|
||
/// Содержит:
|
||
/// • функции активации
|
||
/// • модели: LinearRegression, LogisticRegression, ElasticNet,
|
||
/// kNN, DecisionTree, RandomForest, GradientBoosting,
|
||
/// KMeans, DBSCAN
|
||
/// • трансформеры признаков (StandardScaler, PCA и др.)
|
||
/// • Pipeline для объединения преобразований и модели.
|
||
///
|
||
/// Все алгоритмы работают с числовыми данными:
|
||
/// X — Matrix (объекты × признаки), y — Vector (целевая переменная).
|
||
unit MLModelsABC;
|
||
|
||
interface
|
||
|
||
uses MLCoreABC;
|
||
uses LinearAlgebraML;
|
||
|
||
type
|
||
{$region Activations}
|
||
/// Активационные функции для моделей
|
||
Activations = static class
|
||
public
|
||
/// Функция активации Sigmoid (логистическая функция).
|
||
/// Преобразует любое число в диапазон (0, 1).
|
||
/// Используется в логистической регрессии и других моделях для получения вероятности.
|
||
/// Формула: σ(x) = 1 / (1 + e^(−x)).
|
||
static function Sigmoid(v: Vector): Vector;
|
||
|
||
/// Функция активации Tanh (гиперболический тангенс).
|
||
/// Похожа на Sigmoid, но значения лежат в диапазоне (−1, 1).
|
||
/// Применяется в моделях, где требуется симметричная нелинейность относительно нуля.
|
||
/// Формула: tanh(x) = (e^x − e^(−x)) / (e^x + e^(−x)).
|
||
static function Tanh(v: Vector): Vector;
|
||
|
||
/// Функция активации ReLU (Rectified Linear Unit).
|
||
/// Отрицательные числа заменяет на 0,
|
||
/// положительные оставляет без изменений.
|
||
/// Используется как простое нелинейное преобразование
|
||
/// в моделях машинного обучения.
|
||
/// Формула: ReLU(x) = max(0, x).
|
||
static function ReLU(v: Vector): Vector;
|
||
|
||
/// Функция активации Softmax.
|
||
/// Преобразует набор значений в вероятностное распределение:
|
||
/// все элементы неотрицательны, их сумма равна 1.
|
||
/// Используется в задачах многоклассовой классификации.
|
||
/// Каждый элемент интерпретируется как вероятность класса.
|
||
/// Формула: softmax(x_i) = e^{x_i} / Σ e^{x_j}.
|
||
static function Softmax(v: Vector): Vector;
|
||
end;
|
||
{$endregion Activations}
|
||
|
||
{$region Models}
|
||
|
||
IModel = MLCoreABC.IModel;
|
||
|
||
/// Линейная регрессионная модель (метод наименьших квадратов).
|
||
/// Предсказывает числовое значение по линейной комбинации признаков
|
||
/// Используется в задачах регрессии при отсутствии выраженной
|
||
/// мультиколлинеарности и когда число признаков существенно меньше числа объектов
|
||
LinearRegression = class(IRegressor)
|
||
private
|
||
fCoef: Vector;
|
||
fIntercept: real;
|
||
fFitted: boolean;
|
||
public
|
||
constructor Create();
|
||
|
||
/// Обучает модель на числовых данных
|
||
/// X — матрица m × n (m объектов, n признаков)
|
||
/// y — вектор длины m
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
|
||
/// Предсказывает значения для матрицы признаков
|
||
/// Возвращает вектор длины m
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
/// Вектор коэффициентов модели (веса признаков).
|
||
/// Каждый элемент показывает вклад соответствующего признака в линейную комбинацию.
|
||
/// Доступен после обучения (Fit).
|
||
property Coefficients: Vector read fCoef;
|
||
|
||
/// Свободный член модели (смещение, bias).
|
||
/// Добавляется к линейной комбинации признаков.
|
||
/// Интерпретируется как значение предсказания, когда все признаки равны нулю.
|
||
property Intercept: real read fIntercept;
|
||
|
||
/// Показывает, была ли модель обучена.
|
||
/// После вызова Fit значение становится True.
|
||
/// Используется для проверки корректности вызова Predict.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
|
||
function Clone: IModel;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
/// Линейная регрессионная модель с L2-регуляризацией (Ridge).
|
||
/// Минимизирует функцию:
|
||
/// ‖y - (Xβ + b)‖² + λ ‖β‖².
|
||
/// Устойчива к мультиколлинеарности и плохо обусловленным данным.
|
||
/// Используется при коррелированных признаках
|
||
/// и в задачах, где важна численная стабильность решения
|
||
RidgeRegression = class(IRegressor)
|
||
private
|
||
fLambda: real;
|
||
fCoef: Vector;
|
||
fIntercept: real;
|
||
fFitted: boolean;
|
||
public
|
||
/// Создаёт модель Ridge-регрессии:
|
||
/// • lambda — коэффициент L2-регуляризации (0 — обычная линейная регрессия)
|
||
constructor Create(lambda: real := 1.0);
|
||
|
||
/// Обучает модель на числовых данных.
|
||
/// X — матрица m × n (m объектов, n признаков).
|
||
/// y — вектор длины m с непрерывными значениями.
|
||
/// Выполняется центрирование признаков и целевой переменной.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
|
||
/// Предсказывает непрерывные значения для объектов X.
|
||
/// Результат — вектор длины m.
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
/// Вектор коэффициентов модели (веса признаков).
|
||
/// Длина равна числу признаков.
|
||
/// Доступен после обучения (Fit).
|
||
property Coefficients: Vector read fCoef;
|
||
|
||
/// Свободный член модели (смещение, bias).
|
||
/// Не подвергается регуляризации.
|
||
property Intercept: real read fIntercept;
|
||
|
||
/// Коэффициент L2-регуляризации.
|
||
property Lambda: real read fLambda;
|
||
|
||
/// Показывает, была ли модель обучена.
|
||
/// После вызова Fit значение становится true.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
|
||
function Clone: IModel;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
/// Линейная регрессионная модель ElasticNet.
|
||
/// Минимизирует функцию:
|
||
/// ‖y - (Xβ + b)‖² + λ1 ‖β‖₁ + λ2 ‖β‖².
|
||
/// Объединяет L1-регуляризацию (разреженность, отбор признаков)
|
||
/// и L2-регуляризацию (численная устойчивость).
|
||
/// Используется при большом числе признаков, особенно если признаки коррелированы.
|
||
/// Обучение выполняется методом покоординатного спуска
|
||
/// ВАЖНО: Модель чувствительна к масштабу признаков.
|
||
/// Всегда используйте StandardScaler в Pipeline перед ElasticNet.
|
||
ElasticNet = class(IRegressor)
|
||
private
|
||
fLambda1: real; // L1
|
||
fLambda2: real; // L2
|
||
fMaxIter: integer;
|
||
fTol: real;
|
||
|
||
fCoef: Vector;
|
||
fIntercept: real;
|
||
fFitted: boolean;
|
||
/// Применяет оператор мягкого порога:
|
||
/// soft(z, γ) = sign(z) * max(|z| - γ, 0).
|
||
/// Используется для реализации L1-регуляризации.
|
||
function SoftThreshold(z, gamma: real): real;
|
||
public
|
||
/// Создаёт модель ElasticNet:
|
||
/// • lambda1 — коэффициент L1-регуляризации (>= 0).
|
||
/// • lambda2 — коэффициент L2-регуляризации (>= 0).
|
||
/// • maxIter — максимальное число итераций coordinate descent.
|
||
/// • tol — критерий остановки по изменению коэффициентов.
|
||
constructor Create(lambda1, lambda2: real; maxIter: integer := 1000; tol: real := 1e-6);
|
||
|
||
/// Обучает модель на числовых данных.
|
||
/// X — матрица m × n (m объектов, n признаков).
|
||
/// y — вектор длины m с непрерывными значениями.
|
||
/// Выполняется центрирование признаков и целевой переменной.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
|
||
/// Предсказывает непрерывные значения для объектов X.
|
||
/// Результат — вектор длины m.
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
/// Вектор коэффициентов модели (веса признаков).
|
||
/// Длина равна числу признаков.
|
||
/// Доступен после обучения (Fit).
|
||
property Coefficients: Vector read fCoef;
|
||
|
||
/// Свободный член модели (смещение, bias).
|
||
/// Не подвергается регуляризации.
|
||
property Intercept: real read fIntercept;
|
||
|
||
/// Показывает, была ли модель обучена.
|
||
/// После вызова Fit значение становится True.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
|
||
function Clone: IModel;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
/// Модель Lasso-регрессии (линейная регрессия с L1-регуляризацией).
|
||
/// Минимизирует квадратичную ошибку с L1-штрафом на веса, что приводит к разреженным решениям.
|
||
/// Внутренне реализована через ElasticNet с нулевой L2-регуляризацией
|
||
LassoRegression = class(IRegressor)
|
||
private
|
||
fModel: ElasticNet;
|
||
public
|
||
/// Создаёт модель Lasso-регрессии.
|
||
/// alpha — коэффициент L1-регуляризации.
|
||
/// maxIter — максимальное число итераций обучения.
|
||
/// tol — порог сходимости.
|
||
constructor Create(
|
||
alpha: real := 1.0;
|
||
maxIter: integer := 1000;
|
||
tol: real := 1e-6
|
||
);
|
||
|
||
/// Обучает модель на данных.
|
||
/// X — матрица признаков размера [nSamples x nFeatures].
|
||
/// y — вектор целевых значений длины nSamples.
|
||
/// Возвращает текущий экземпляр модели.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
|
||
/// Предсказывает значения целевой переменной для входных данных.
|
||
/// X — матрица признаков размера [nSamples x nFeatures].
|
||
/// Возвращает вектор предсказаний длины nSamples.
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
/// Создаёт полную копию модели вместе с обученными параметрами.
|
||
function Clone: IModel;
|
||
|
||
/// Показывает, была ли модель обучена.
|
||
/// После вызова Fit значение становится True.
|
||
property IsFitted: boolean read fModel.fFitted;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
/// Логистическая регрессия.
|
||
/// Поддерживает бинарную и многоклассовую классификацию.
|
||
/// Для нескольких классов используется softmax,
|
||
/// для двух классов — частный случай softmax.
|
||
/// Оптимизация выполняется по кросс-энтропийной функции потерь
|
||
/// с поддержкой L2-регуляризации
|
||
LogisticRegression = class(IProbabilisticClassifier)
|
||
private
|
||
fW: Matrix; // p x k
|
||
fIntercept: Vector; // k
|
||
fLambda: real;
|
||
fLearningRate: real;
|
||
fEpochs: integer;
|
||
fFitted: boolean;
|
||
fClassCount: integer;
|
||
fClassToIndex: Dictionary<integer, integer>;
|
||
fIndexToClass: array of integer;
|
||
|
||
fTol: real;
|
||
fCheckConvergence: boolean;
|
||
fMinImprovement: real;
|
||
|
||
fClassLabels: array of string; // В каждой модели классификации
|
||
|
||
function GetWeights: Matrix;
|
||
function GetIntercept: Vector;
|
||
public
|
||
/// lambda — коэффициент L2-регуляризации.
|
||
/// lr — шаг градиентного спуска.
|
||
/// epochs — число итераций обучения
|
||
constructor Create(
|
||
lambda: real := 0.0;
|
||
learningRate: real := 0.01;
|
||
epochs: integer := 1000;
|
||
tol: real := 1e-6;
|
||
checkConvergence: boolean := true;
|
||
minImprovement: real := 1e-8
|
||
);
|
||
|
||
/// Обучает модель логистической регрессии.
|
||
/// X — матрица признаков.
|
||
/// y — вектор меток классов (целочисленные значения).
|
||
/// Возвращает обученную модель.
|
||
/// После вызова IsFitted становится True.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
|
||
/// Возвращает матрицу вероятностей (m x k).
|
||
function PredictProba(X: Matrix): Matrix;
|
||
|
||
/// Возвращает массив меток классов в порядке столбцов PredictProba.
|
||
function GetClasses: array of real;
|
||
|
||
/// Возвращает предсказанные классы для объектов из X.
|
||
/// Результат — вектор значений, где каждый элемент соответствует классу объекта.
|
||
/// Порядок элементов соответствует строкам матрицы X.
|
||
/// Требует предварительного вызова Fit.
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
/// Возвращает предсказанные метки классов для объектов из X.
|
||
/// Каждый элемент результата — индекс класса (целое число).
|
||
/// Порядок элементов соответствует строкам матрицы X.
|
||
/// Требует предварительного вызова Fit.
|
||
function PredictLabels(X: Matrix): array of integer;
|
||
|
||
/// Показывает, была ли модель обучена.
|
||
/// Если false — вызов Predict или PredictProba приведет к ошибке.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
/// Возвращает строковое представление модели.
|
||
function ToString: string; override;
|
||
|
||
/// Создает глубокую копию модели.
|
||
function Clone: IModel;
|
||
|
||
/// Матрица коэффициентов модели (p × k).
|
||
property Weights: Matrix read GetWeights;
|
||
|
||
/// Вектор свободных членов (bias) для каждого класса.
|
||
property Intercept: Vector read GetIntercept;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
procedure SetClassLabels(classes: array of string);
|
||
|
||
function GetClassLabels: array of string;
|
||
end;
|
||
|
||
DecisionTreeNode = class
|
||
public
|
||
IsLeaf: boolean;
|
||
FeatureIndex: integer;
|
||
Threshold: real;
|
||
Left: DecisionTreeNode;
|
||
Right: DecisionTreeNode;
|
||
LeafValue: real;
|
||
|
||
/// Создает глубокую копию узла вместе со всеми подузлами
|
||
function Clone: DecisionTreeNode;
|
||
end;
|
||
|
||
/// Результат поиска лучшего разбиения узла дерева
|
||
SplitResult = record
|
||
/// Found = true, если допустимое разбиение найдено.
|
||
Found: boolean;
|
||
/// Feature — индекс признака, по которому делается split.
|
||
Feature: integer;
|
||
/// Threshold — пороговое значение признака.
|
||
Threshold: real;
|
||
|
||
static function Invalid: SplitResult;
|
||
begin
|
||
Result.Found := false;
|
||
Result.Feature := -1;
|
||
Result.Threshold := 0.0;
|
||
end;
|
||
|
||
static function Create(feature: integer; threshold: real): SplitResult;
|
||
begin
|
||
Result.Found := true;
|
||
Result.Feature := feature;
|
||
Result.Threshold := threshold;
|
||
end;
|
||
end;
|
||
|
||
/// Интерфейс критерия разбиения узла дерева.
|
||
/// Определяет функцию нечистоты (impurity), которая используется для оценки качества разбиения
|
||
ISplitCriterion = interface
|
||
/// Вычисляет нечистоту для вектора целевых значений y.
|
||
/// Чем меньше значение — тем "чище" узел.
|
||
function Impurity(y: Vector; indices: array of integer): real;
|
||
end;
|
||
|
||
/// Критерий Джини.
|
||
/// Используется в классификации.
|
||
/// Минимизирует Gini-нечистоту, что приводит к более однородным по классам листьям
|
||
GiniCriterion = class(ISplitCriterion)
|
||
private
|
||
fClassCount: integer;
|
||
public
|
||
constructor Create(classCount: integer);
|
||
begin
|
||
fClassCount := classCount;
|
||
end;
|
||
function Impurity(y: Vector; indices: array of integer): real;
|
||
end;
|
||
|
||
/// Критерий дисперсии.
|
||
/// Используется в регрессии.
|
||
/// Минимизирует внутригрупповую дисперсию значений целевой переменной
|
||
VarianceCriterion = class(ISplitCriterion)
|
||
public
|
||
/// Вычисляет дисперсию значений y.
|
||
/// Чем меньше дисперсия — тем лучше узел.
|
||
function Impurity(y: Vector; indices: array of integer): real;
|
||
end;
|
||
|
||
/// Критерий энтропии (Information Gain).
|
||
/// Используется в классификации.
|
||
/// Минимизирует энтропию распределения классов, стремясь к максимально "чистым" узлам.
|
||
/// Основан на теории информации: чем меньше энтропия, тем менее неопределённо распределение классов.
|
||
EntropyCriterion = class(ISplitCriterion)
|
||
private
|
||
fClassCount: integer;
|
||
public
|
||
constructor Create(classCount: integer);
|
||
|
||
/// Вычисляет энтропию распределения классов в узле.
|
||
/// Чем меньше значение — тем более однороден узел по классам.
|
||
function Impurity(y: Vector; indices: array of integer): real;
|
||
end;
|
||
|
||
/// Внутреннее ядро дерева решений (Decision Tree).
|
||
/// Реализует алгоритм построения и предсказания без учёта кодирования меток.
|
||
///
|
||
/// Ожидает, что целевые значения y уже закодированы в диапазон 0..K-1.
|
||
/// Возвращает предсказания в том же закодированном виде.
|
||
///
|
||
/// Не выполняет:
|
||
/// - кодирование/декодирование меток
|
||
/// - проверку корректности входных данных
|
||
///
|
||
/// Используется как вычислительный модуль внутри моделей
|
||
/// (например, DecisionTreeClassifier, RandomForest и др.)
|
||
DecisionTreeCore = class
|
||
private
|
||
fRoot: DecisionTreeNode;
|
||
fMaxDepth: integer;
|
||
fMinSamplesSplit: integer;
|
||
fMinSamplesLeaf: integer;
|
||
fCriterion: ISplitCriterion;
|
||
fClassCount: integer;
|
||
fMaxFeatures: integer;
|
||
fUserProvidedSeed: boolean;
|
||
fRandomSeed: integer;
|
||
fRng: System.Random;
|
||
fFeatureImportances: Vector;
|
||
|
||
public
|
||
constructor Create(
|
||
maxDepth: integer;
|
||
minSamplesSplit: integer;
|
||
minSamplesLeaf: integer;
|
||
criterion: ISplitCriterion;
|
||
maxFeatures: integer;
|
||
seed: integer := -1
|
||
);
|
||
|
||
procedure Fit(X: Matrix; y: Vector); // y уже 0..K-1
|
||
function Predict(X: Matrix): Vector; // возвращает 0..K-1
|
||
function PredictRow(X: Matrix; row: integer): integer;
|
||
|
||
private
|
||
function CreateLeaf(y: Vector; indices: array of integer): DecisionTreeNode;
|
||
|
||
function BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode;
|
||
|
||
function FindBestSplit(X: Matrix; y: Vector; indices: array of integer;
|
||
var bestF: integer; var bestT: real): boolean;
|
||
|
||
procedure Split(
|
||
X: Matrix; y: Vector;
|
||
f: integer; t: real;
|
||
var Xl: Matrix; var yl: Vector;
|
||
var Xr: Matrix; var yr: Vector
|
||
);
|
||
|
||
property FeatureImportances: Vector read fFeatureImportances;
|
||
|
||
function MajorityClass(y: Vector; indices: array of integer): integer;
|
||
|
||
function PredictOne(x: Vector; node: DecisionTreeNode): integer;
|
||
function Clone: DecisionTreeCore;
|
||
end;
|
||
|
||
//============================
|
||
// DecisionTreeBase
|
||
//============================
|
||
/// Базовый абстрактный класс дерева решений.
|
||
/// Используется только DecisionTreeRegressor
|
||
/// Classifier использует DecisionTreeCore
|
||
/// Реализует общую логику построения структуры дерева:
|
||
/// рекурсивное разбиение, контроль глубины,
|
||
/// минимального числа объектов и расчет важности признаков.
|
||
/// Конкретная логика вычисления значения листа и критерия разбиения задается в наследнике
|
||
DecisionTreeRegressorBase = abstract class(ITreeModel)
|
||
protected
|
||
fRoot: DecisionTreeNode;
|
||
fMaxDepth: integer;
|
||
fMinSamplesSplit: integer;
|
||
fMinSamplesLeaf: integer;
|
||
fFitted: boolean;
|
||
fCriterion: ISplitCriterion;
|
||
fFeatureImportances: Vector;
|
||
fRandomSeed: integer;
|
||
fMaxFeatures: integer := 0;
|
||
fRowIndices: array of integer := nil;
|
||
|
||
fRng: System.Random;
|
||
fUserProvidedSeed: boolean;
|
||
|
||
function BuildTree(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode;
|
||
|
||
//function FindBestSplit0(X: Matrix; y: Vector; indices: array of integer): SplitResult;
|
||
|
||
function FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; virtual; abstract;
|
||
function IsPure(y: Vector; indices: array of integer): boolean; virtual;
|
||
// --------------------------
|
||
|
||
function LeafValue(y: Vector; indices: array of integer): real; virtual; abstract;
|
||
function LeafNode(value: real): DecisionTreeNode;
|
||
procedure CopyBaseState(dest: DecisionTreeRegressorBase);
|
||
function GetFeatureSubset(nFeatures: integer): array of integer; virtual;
|
||
|
||
procedure SetRowIndices(rows: array of integer);
|
||
public
|
||
/// Создает дерево решений:
|
||
/// • maxDepth — максимальная глубина дерева.
|
||
/// • minSamplesSplit — минимальное число объектов для разбиения узла.
|
||
/// • minSamplesLeaf — минимальное число объектов в листе
|
||
constructor Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer;
|
||
criterion: ISplitCriterion; seed: integer);
|
||
|
||
/// Возвращает вектор важности признаков.
|
||
/// Важность вычисляется как суммарное уменьшение
|
||
/// нечистоты (impurity reduction) по всем разбиениям.
|
||
/// Значения нормированы так, что сумма равна 1.
|
||
function FeatureImportances: Vector;
|
||
|
||
/// Обучает дерево решений.
|
||
/// X — матрица признаков.
|
||
/// y — целевая переменная.
|
||
/// Реализация зависит от типа дерева (регрессия или классификация).
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel; virtual; abstract;
|
||
|
||
/// Выполняет предсказание для матрицы X.
|
||
/// Возвращает вектор прогнозов.
|
||
/// Для регрессии — вещественные значения.
|
||
/// Для классификации — метки классов.
|
||
function Predict(X: Matrix): Vector; virtual; abstract;
|
||
|
||
/// Создает глубокую копию дерева.
|
||
/// Копируется структура узлов, параметры и обученное состояние.
|
||
function Clone: IModel; virtual; abstract;
|
||
|
||
/// Возвращает true, если дерево обучено.
|
||
/// Если false — Predict вызовет ошибку.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
//============================
|
||
// DecisionTreeClassifier
|
||
//============================
|
||
/// Дерево решений для задачи классификации.
|
||
/// Использует критерий нечистоты (обычно Gini) для выбора оптимальных разбиений.
|
||
/// В листьях хранится наиболее частый класс
|
||
DecisionTreeClassifier = class(IClassifier)
|
||
private
|
||
fMaxDepth: integer;
|
||
fMinSamplesSplit: integer;
|
||
fMinSamplesLeaf: integer;
|
||
fFitted: boolean;
|
||
fCriterion: ISplitCriterion;
|
||
fFeatureImportances: Vector;
|
||
fRandomSeed: integer;
|
||
fMaxFeatures: integer := 0;
|
||
fUserProvidedSeed: boolean;
|
||
fRng: System.Random;
|
||
|
||
fCore: DecisionTreeCore;
|
||
fIndexToClass: array of integer;
|
||
fClassLabels: array of string;
|
||
|
||
public
|
||
/// Создает классификационное дерево:
|
||
/// • maxDepth — максимальная глубина дерева (-1 означает без ограничения).
|
||
/// • minSamplesSplit — минимальное число объектов для разбиения узла.
|
||
/// • minSamplesLeaf — минимальное число объектов в листе
|
||
constructor Create(maxDepth: integer := 10; minSamplesSplit: integer := 2; minSamplesLeaf: integer := 1;
|
||
criterion: ISplitCriterion := nil; seed: integer := -1);
|
||
|
||
/// Обучает классификационное дерево.
|
||
/// X — матрица признаков.
|
||
/// y — вектор целевых меток (целые значения).
|
||
/// Строит структуру дерева путем минимизации нечистоты в узлах.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
|
||
/// Выполняет предсказание меток классов для X.
|
||
/// Для каждого объекта возвращается класс, соответствующий листу дерева.
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
/// Возвращает предсказанные метки классов для объектов из X.
|
||
/// Каждый элемент результата — индекс класса (целое число).
|
||
/// Порядок элементов соответствует строкам матрицы X.
|
||
/// Требует предварительного вызова Fit.
|
||
function PredictLabels(X: Matrix): array of integer;
|
||
|
||
/// Создает глубокую копию дерева классификации.
|
||
/// Копируется структура узлов, параметры и обученное состояние.
|
||
function Clone: IModel;
|
||
|
||
/// Возвращает строковое представление модели.
|
||
function ToString: string; override;
|
||
|
||
function ClassCount: integer := fIndexToClass.Length;
|
||
|
||
function IndexToClass: array of integer := Copy(fIndexToClass);
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
procedure SetClassLabels(classes: array of string);
|
||
|
||
function GetClassLabels: array of string;
|
||
|
||
/// Возвращает true, если дерево обучено.
|
||
/// Если false — Predict вызовет ошибку.
|
||
property IsFitted: boolean read fFitted;
|
||
end;
|
||
|
||
//============================
|
||
// DecisionTreeRegressor
|
||
//============================
|
||
/// Дерево решений для задачи регрессии.
|
||
/// Наследуется от DecisionTreeBase.
|
||
/// Использует критерий дисперсии для выбора разбиений.
|
||
/// В листьях хранится среднее значение целевой переменной.
|
||
/// Поддерживает L2-регуляризацию значения листа (leafL2)
|
||
DecisionTreeRegressor = class(DecisionTreeRegressorBase, IRegressor)
|
||
private
|
||
fLeafL2: real;
|
||
|
||
function PredictOne(X: Matrix; rowIndex: integer): real;
|
||
|
||
protected
|
||
/// Вычисляет значение листа для набора индексов.
|
||
/// В регрессии это среднее целевой переменной
|
||
/// с учетом L2-регуляризации (если leafL2 > 0).
|
||
function LeafValue(y: Vector; indices: array of integer): real; override;
|
||
|
||
function FindBestSplitReg(X: Matrix; y: Vector; indices: array of integer): SplitResult;
|
||
|
||
/// Ищет лучшее разбиение узла по всем признакам и возможным порогам.
|
||
/// Критерий — максимальное уменьшение дисперсии.
|
||
function FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; override;
|
||
/// Проверяет, является ли узел "чистым".
|
||
/// Для регрессии это означает, что все значения y одинаковы
|
||
/// или разбиение больше не имеет смысла.
|
||
function IsPure(y: Vector; indices: array of integer): boolean; override;
|
||
|
||
public
|
||
/// Создает регрессионное дерево:
|
||
/// • maxDepth — максимальная глубина (-1 означает без ограничения).
|
||
/// • minSamplesSplit — минимальное число объектов для разбиения.
|
||
/// • minSamplesLeaf — минимальное число объектов в листе.
|
||
/// • leafL2 — коэффициент L2-регуляризации значения листа
|
||
constructor Create(maxDepth: integer := 10; minSamplesSplit: integer := 2; minSamplesLeaf: integer := 1;
|
||
leafL2: real := 0.0; seed: integer := -1);
|
||
|
||
/// Обучает регрессионное дерево.
|
||
/// X — матрица признаков.
|
||
/// y — вещественная целевая переменная.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel; override;
|
||
|
||
/// Выполняет предсказание для всех объектов X.
|
||
/// Возвращает вектор вещественных значений.
|
||
function Predict(X: Matrix): Vector; override;
|
||
|
||
/// Создает глубокую копию дерева регрессии.
|
||
/// Копируется структура узлов, параметры и обученное состояние.
|
||
function Clone: IModel; override;
|
||
|
||
/// Возвращает строковое представление модели.
|
||
function ToString: string; override;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
|
||
/// Режим выбора числа признаков при поиске разбиения.
|
||
/// Определяет, сколько признаков m из общего числа p
|
||
/// будет случайно выбрано для рассмотрения в узле.
|
||
/// Используется в Random Forest и других ансамблях
|
||
/// для увеличения разнообразия деревьев
|
||
TMaxFeaturesMode = (
|
||
/// Использовать все признаки: m = p
|
||
AllFeatures,
|
||
/// Использовать квадратный корень от числа признаков: m = sqrt(p)
|
||
/// Типичный выбор для классификации
|
||
SqrtFeatures,
|
||
/// Использовать log2 от числа признаков: m = log2(p)
|
||
/// Более агрессивное ограничение признаков
|
||
Log2Features,
|
||
/// Использовать половину признаков: m = p / 2
|
||
/// Компромисс между скоростью и разнообразием
|
||
HalfFeatures
|
||
);
|
||
|
||
//============================
|
||
// RandomForestRegressor
|
||
//============================
|
||
// RandomForest = bagging + feature-subset
|
||
// bagging = Делаем bootstrap-выборку (случайно с возвращением)
|
||
// feature-subset = в каждом дереве - случайные признаки
|
||
// RandomForest снижает корреляцию между деревьями, что хорошо
|
||
|
||
/// Базовый абстрактный класс случайного леса.
|
||
/// Реализует ансамбль из множества независимых деревьев,
|
||
/// обученных на случайных подвыборках данных и случайных подмножествах признаков.
|
||
/// Конкретная логика агрегирования предсказаний
|
||
/// определяется в наследниках (регрессия или классификация)
|
||
RandomForestBase = abstract class(IModel)
|
||
protected
|
||
fNTrees: integer;
|
||
fMaxDepth: integer;
|
||
fMinSamplesSplit: integer;
|
||
fMinSamplesLeaf: integer;
|
||
fMaxFeaturesMode: TMaxFeaturesMode;
|
||
fFitted: boolean;
|
||
|
||
fRandomSeed: integer;
|
||
fRng: System.Random;
|
||
fUserProvidedSeed: boolean;
|
||
|
||
fFeatureCount: integer;
|
||
|
||
fUseOOB: boolean;
|
||
fOOBScore: real;
|
||
fHasOOBScore: boolean;
|
||
|
||
function ComputeMaxFeatures(p: integer): integer;
|
||
|
||
procedure BootstrapRowIndices(n: integer; var rows: array of integer);
|
||
|
||
public
|
||
/// Создает случайный лес.
|
||
/// • nTrees — число деревьев в ансамбле.
|
||
/// • maxDepth — максимальная глубина каждого дерева.
|
||
/// • minSamplesSplit — минимальное число объектов для разбиения узла.
|
||
/// • minSamplesLeaf — минимальное число объектов в листе.
|
||
/// • maxFeatures — режим выбора числа признаков, рассматриваемых при поиске разбиения
|
||
constructor Create(
|
||
nTrees: integer;
|
||
maxDepth: integer;
|
||
minSamplesSplit: integer;
|
||
minSamplesLeaf: integer;
|
||
maxFeatures: TMaxFeaturesMode;
|
||
useOOB: boolean := False;
|
||
seed: integer := -1);
|
||
|
||
/// Обучает ансамбль деревьев на данных X и y.
|
||
/// Для каждого дерева используется bootstrap-подвыборка
|
||
/// и случайное подмножество признаков.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel; virtual; abstract;
|
||
|
||
/// Выполняет предсказание для матрицы X.
|
||
/// В регрессии — усреднение предсказаний деревьев.
|
||
/// В классификации — голосование (majority vote) или усреднение вероятностей.
|
||
function Predict(X: Matrix): Vector; virtual; abstract;
|
||
|
||
/// Создает глубокую копию случайного леса.
|
||
/// Копируются все деревья и параметры ансамбля.
|
||
function Clone: IModel; virtual; abstract;
|
||
|
||
/// Возвращает вектор важности признаков.
|
||
/// Обычно вычисляется как средняя важность по всем деревьям ансамбля.
|
||
function FeatureImportances: Vector; virtual; abstract;
|
||
|
||
/// Возвращает Out-Of-Bag (OOB) оценку качества модели.
|
||
/// OOB-оценка вычисляется на объектах, не вошедших в bootstrap-выборки деревьев.
|
||
/// • Для регрессии — среднеквадратичная ошибка (MSE).
|
||
/// • Для классификации — доля правильно классифицированных объектов (Accuracy).
|
||
/// Требует включённого режима computeOOB при создании модели.
|
||
/// Бросает исключение, если OOB не включён или модель не обучена
|
||
function OOBScore: real;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
property IsFitted: boolean read fFitted;
|
||
end;
|
||
|
||
/// Случайный лес для задачи регрессии.
|
||
/// Строит ансамбль регрессионных деревьев,
|
||
/// обученных на bootstrap-подвыборках данных и случайных подмножествах признаков.
|
||
/// Итоговое предсказание — среднее значение по всем деревьям ансамбля
|
||
RandomForestRegressor = class(RandomForestBase, IRegressor)
|
||
private
|
||
fTrees: array of DecisionTreeRegressor;
|
||
public
|
||
/// Создает регрессионный случайный лес:
|
||
/// • nTrees — число деревьев в ансамбле.
|
||
/// • maxDepth — максимальная глубина деревьев (-1 означает без ограничения).
|
||
/// • minSamplesSplit — минимальное число объектов для разбиения узла.
|
||
/// • minSamplesLeaf — минимальное число объектов в листе.
|
||
/// • maxFeaturesMode — режим выбора числа признаков, рассматриваемых при поиске разбиения
|
||
constructor Create(nTrees: integer := 100;
|
||
maxDepth: integer := -1;
|
||
minSamplesSplit: integer := 2;
|
||
minSamplesLeaf: integer := 1;
|
||
maxFeaturesMode: TMaxFeaturesMode := TMaxFeaturesMode.HalfFeatures;
|
||
computeOOB: boolean := False;
|
||
seed: integer := -1);
|
||
|
||
/// Обучает случайный лес на данных X и y.
|
||
/// Для каждого дерева используется bootstrap-выборка.
|
||
/// Возвращает обученную модель.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel; override;
|
||
|
||
/// Выполняет предсказание для X.
|
||
/// Итоговое значение — среднее предсказаний всех деревьев ансамбля.
|
||
function Predict(X: Matrix): Vector; override;
|
||
|
||
/// Создает глубокую копию случайного леса.
|
||
/// Копируются все деревья и параметры модели.
|
||
function Clone: IModel; override;
|
||
|
||
/// Возвращает усредненную важность признаков по всем деревьям ансамбля.
|
||
/// Значения нормированы так, что сумма равна 1.
|
||
function FeatureImportances: Vector; override;
|
||
|
||
/// Возвращает строковое представление модели.
|
||
function ToString: string; override;
|
||
end;
|
||
|
||
/// Случайный лес для задачи классификации.
|
||
/// Наследуется от RandomForestBase и реализует интерфейс IClassifier.
|
||
/// Строит ансамбль классификационных деревьев, обученных на
|
||
/// bootstrap-подвыборках объектов и случайных подмножествах признаков.
|
||
/// Итоговое предсказание формируется голосованием деревьев или агрегацией вероятностей классов
|
||
RandomForestClassifier = class(RandomForestBase, IProbabilisticClassifier)
|
||
private
|
||
fTrees: array of DecisionTreeCore;
|
||
fIndexToClass: array of integer;
|
||
fClassToIndex: Dictionary<integer, integer>;
|
||
fClassCount: integer;
|
||
fClassLabels: array of string;
|
||
public
|
||
/// Создает классификационный случайный лес:
|
||
/// • nTrees — число деревьев в ансамбле.
|
||
/// • maxDepth — максимальная глубина каждого дерева (-1 означает без ограничения).
|
||
/// • minSamplesSplit — минимальное число объектов для разбиения узла.
|
||
/// • minSamplesLeaf — минимальное число объектов в листе.
|
||
/// • maxFeaturesMode — режим выбора числа признаков при поиске разбиения, по умолчанию используется sqrt(p),
|
||
/// что является стандартом для классификации
|
||
constructor Create(nTrees: integer := 100;
|
||
maxDepth: integer := -1;
|
||
minSamplesSplit: integer := 2;
|
||
minSamplesLeaf: integer := 1;
|
||
maxFeaturesMode: TMaxFeaturesMode := TMaxFeaturesMode.SqrtFeatures;
|
||
computeOOB: boolean := False;
|
||
seed: integer := -1);
|
||
|
||
/// Обучает случайный лес на данных X и y.
|
||
/// Для каждого дерева используется bootstrap-выборка обучающих объектов.
|
||
/// В каждом узле рассматривается случайное подмножество признаков согласно maxFeaturesMode.
|
||
/// Возвращает обученную модель.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel; override;
|
||
|
||
/// Выполняет предсказание меток классов для матрицы X.
|
||
/// Для каждого объекта агрегируются предсказания всех деревьев.
|
||
/// Итоговый класс определяется большинством голосов или максимальной суммарной вероятностью.
|
||
function Predict(X: Matrix): Vector; override;
|
||
|
||
/// Возвращает предсказанные метки классов для объектов из X.
|
||
/// Каждый элемент результата — исходная метка класса (целое число).
|
||
/// Порядок элементов соответствует строкам матрицы X.
|
||
/// Требует предварительного вызова Fit.
|
||
function PredictLabels(X: Matrix): array of integer;
|
||
|
||
/// Возвращает оценки вероятностей принадлежности объектов из X к каждому классу.
|
||
/// Результат — матрица размера [nSamples x nClasses].
|
||
/// Элемент [i, k] содержит вероятность того, что объект i принадлежит классу k.
|
||
/// Порядок классов соответствует fIndexToClass.
|
||
/// Сумма вероятностей по строке равна 1.
|
||
/// Требует предварительного вызова Fit.
|
||
function PredictProba(X: Matrix): Matrix;
|
||
|
||
/// Создает глубокую копию случайного леса классификации.
|
||
/// Копируются все деревья, параметры ансамбля и обученное состояние модели.
|
||
function Clone: IModel; override;
|
||
|
||
/// Возвращает усредненную важность признаков по всем деревьям ансамбля.
|
||
/// Важность рассчитывается как суммарное уменьшение нечистоты, нормированное так, что сумма равна 1.
|
||
function FeatureImportances: Vector; override;
|
||
|
||
/// Возвращает строковое представление модели.
|
||
function ToString: string; override;
|
||
|
||
function GetClasses: array of real;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
procedure SetClassLabels(classes: array of string);
|
||
|
||
function GetClassLabels: array of string;
|
||
end;
|
||
|
||
{ Gradient Boosting v1.0 — Freeze Checklist
|
||
GradientBoostingRegressor
|
||
✔ SquaredError
|
||
✔ Huber
|
||
✔ Quantile
|
||
✔ L2 regularization в листьях
|
||
✔ Subsample (stochastic boosting)
|
||
✔ Validation early stopping
|
||
✔ OOB early stopping
|
||
✔ Train / Val / OOB history
|
||
✔ Staged prediction
|
||
✔ Feature importance
|
||
✔ Clone
|
||
|
||
GradientBoostingClassifier
|
||
✔ Multiclass softmax
|
||
✔ Корректное class mapping
|
||
✔ LogLoss
|
||
✔ Subsample
|
||
✔ Validation early stopping
|
||
✔ OOB early stopping
|
||
✔ History
|
||
✔ Staged prediction
|
||
✔ Feature importance
|
||
✔ Clone
|
||
}
|
||
|
||
/// Тип функции потерь для GradientBoostingRegressor.
|
||
/// SquaredError — классическая L2 (MSE).
|
||
/// Huber — робастная loss: квадратичная около нуля и линейная на хвостах.
|
||
/// Quantile — квантильная регрессия (асимметричная L1).
|
||
TGBLoss = (SquaredError, Huber, Quantile);
|
||
|
||
/// Gradient Boosting Regressor.
|
||
/// Реализует градиентный бустинг над деревьями решений.
|
||
/// Каждая новая модель обучается на псевдо-остатках предыдущей.
|
||
/// Поддерживает:
|
||
/// • разные loss-функции,
|
||
/// • subsample (stochastic boosting),
|
||
/// • early stopping (validation или OOB),
|
||
/// • L2-регуляризацию в листьях
|
||
/// • staged prediction
|
||
GradientBoostingRegressor = class(IRegressor)
|
||
private
|
||
fNEstimators: integer;
|
||
fLearningRate: real;
|
||
fMaxDepth: integer;
|
||
fMinSamplesSplit: integer;
|
||
fMinSamplesLeaf: integer;
|
||
fSubsample: real;
|
||
|
||
fEstimators: List<DecisionTreeRegressor>;
|
||
fInitValue: real;
|
||
fFitted: boolean;
|
||
fFeatureCount: integer;
|
||
// -----------
|
||
fLoss: TGBLoss;
|
||
fHuberDelta: real;
|
||
|
||
fEarlyStoppingPatience: integer;
|
||
|
||
fTrainLossHistory: List<real>;
|
||
fBestTrainLoss: real;
|
||
fBestIteration: integer;
|
||
|
||
fValLossHistory: List<real>;
|
||
fBestScoreLoss: real;
|
||
|
||
fFeatureImportances: Vector;
|
||
|
||
fQuantileAlpha: real;
|
||
|
||
fLeafL2: real;
|
||
fUseOOBEarlyStopping: boolean;
|
||
|
||
fOOBLossHistory: List<real>;
|
||
|
||
fRandomSeed: integer;
|
||
fRng: System.Random;
|
||
fUserProvidedSeed: boolean;
|
||
|
||
function ComputeTrainLoss(y, yPred: Vector): real;
|
||
procedure ComputePseudoResiduals(y, yPred: Vector; r: Vector);
|
||
|
||
function FitInternal(XTrain: Matrix; yTrain: Vector;
|
||
XVal: Matrix; yVal: Vector; useValidation: boolean): ISupervisedModel;
|
||
|
||
function ComputeQuantile(y: Vector; alpha: real): real;
|
||
|
||
function ComputeTrainLossMasked(yTrue, yPred: Vector; mask: array of boolean): real;
|
||
|
||
public
|
||
/// Создает новый GradientBoostingRegressor:
|
||
/// • nEstimators — число деревьев (итераций бустинга).
|
||
/// • learningRate — коэффициент shrinkage.
|
||
/// • maxDepth — максимальная глубина дерева.
|
||
/// • minSamplesSplit — минимальное число объектов для split.
|
||
/// • minSamplesLeaf — минимальное число объектов в листе.
|
||
/// • subsample — доля выборки на каждой итерации (0..1].
|
||
/// • randomSeed — зерно генератора случайных чисел.
|
||
/// • loss — функция потерь.
|
||
/// • huberDelta — параметр Huber loss.
|
||
/// • earlyStoppingPatience — число итераций без улучшения до остановки.
|
||
/// • quantileAlpha — уровень квантили для Quantile loss.
|
||
/// • leafL2 — L2-регуляризация значения листа.
|
||
/// • useOOBEarlyStopping — использовать OOB loss для ранней остановки
|
||
constructor Create(
|
||
nEstimators: integer := 100;
|
||
learningRate: real := 0.1;
|
||
maxDepth: integer := 3;
|
||
minSamplesSplit: integer := 2;
|
||
minSamplesLeaf: integer := 1;
|
||
subsample: real := 1.0;
|
||
loss: TGBLoss := TGBLoss.SquaredError;
|
||
huberDelta: real := 1.0;
|
||
earlyStoppingPatience: integer := 0;
|
||
quantileAlpha: real := 0.5;
|
||
leafL2: real := 0.0;
|
||
useOOBEarlyStopping: boolean := false;
|
||
seed: integer := -1
|
||
);
|
||
|
||
/// Обучает модель градиентного бустинга на обучающей выборке.
|
||
/// Обучение выполняется без early stopping.
|
||
/// Для предотвращения переобучения используйте FitWithValidation.
|
||
/// Early stopping работает только при наличии валидационной выборки
|
||
/// и параметре earlyStoppingPatience > 0.
|
||
/// Возвращает обученную модель.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
/// Предсказывает значения целевой переменной.
|
||
/// Используются все обученные деревья.
|
||
function Predict(X: Matrix): Vector;
|
||
/// Создает глубокую копию модели.
|
||
/// Копируются все деревья и внутреннее состояние.
|
||
function Clone: IModel;
|
||
|
||
/// Обучает модель градиентного бустинга с использованием валидационной выборки.
|
||
/// Поддерживает early stopping:
|
||
/// обучение останавливается, если метрика не улучшается
|
||
/// в течение earlyStoppingPatience итераций.
|
||
/// Возвращает обученную модель.
|
||
function FitWithValidation(XTrain: Matrix; yTrain: Vector; XVal: Matrix; yVal: Vector): ISupervisedModel;
|
||
|
||
/// История значения функции потерь на обучающей выборке.
|
||
/// Один элемент на итерацию бустинга.
|
||
property TrainLossHistory: List<real> read fTrainLossHistory;
|
||
/// История значения функции потерь на validation-выборке.
|
||
property ValLossHistory: List<real> read fValLossHistory;
|
||
/// Индекс итерации с лучшим значением функции потерь
|
||
/// (validation или OOB в зависимости от режима).
|
||
property BestIteration: integer read fBestIteration;
|
||
/// История OOB loss (если включен OOB early stopping).
|
||
property OOBLossHistory: List<real> read fOOBLossHistory;
|
||
|
||
/// Предсказание по первым m итерациям бустинга.
|
||
/// m может быть от 0 до TreeCount.
|
||
/// Используется для анализа обучения и переобучения.
|
||
function PredictStage(X: Matrix; m: integer): Vector;
|
||
/// Возвращает последовательность предсказаний
|
||
/// после каждой итерации бустинга.
|
||
/// Удобно для построения learning curve.
|
||
function StagedPredict(X: Matrix): sequence of Vector;
|
||
|
||
/// Возвращает текущее количество деревьев в ансамбле.
|
||
function TreeCount: integer := fEstimators.Count;
|
||
|
||
/// Возвращает нормированные importance признаков.
|
||
/// Значения суммируются по всем деревьям и нормируются к 1.
|
||
function FeatureImportances: Vector;
|
||
|
||
/// Возвращает строковое представление модели.
|
||
function ToString: string; override;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
property IsFitted: boolean read fFitted;
|
||
end;
|
||
|
||
/// Тип функции потерь для классификатора.
|
||
/// В текущей версии используется только LogLoss
|
||
/// (многоклассовая кросс-энтропия).
|
||
TGBCLoss = (LogLoss);
|
||
|
||
/// Gradient Boosting Classifier.
|
||
/// Реализует многоклассовый градиентный бустинг с использованием softmax и LogLoss.
|
||
/// На каждой итерации обучается по одному дереву для каждого класса (one-vs-all в логит-пространстве).
|
||
/// Поддерживает:
|
||
/// • subsample
|
||
/// • validation early stopping
|
||
/// • OOB early stopping
|
||
/// • staged prediction
|
||
GradientBoostingClassifier = class(IProbabilisticClassifier)
|
||
private
|
||
// hyperparams
|
||
fNEstimators: integer;
|
||
fLearningRate: real;
|
||
fMaxDepth: integer;
|
||
fMinSamplesSplit: integer;
|
||
fMinSamplesLeaf: integer;
|
||
fSubsample: real;
|
||
fEarlyStoppingPatience: integer;
|
||
|
||
// fitted state
|
||
fFitted: boolean;
|
||
fFeatureCount: integer;
|
||
|
||
fClassCount: integer;
|
||
fClasses: array of integer; // реальные метки классов (unique)
|
||
fClassIndex: Dictionary<integer,integer>; // label -> 0..K-1
|
||
|
||
// [m][k] дерево для класса k на итерации m
|
||
fEstimators: List<array of DecisionTreeRegressor>;
|
||
|
||
// init logits (как минимум нули; позже можно log-prior)
|
||
fInitLogits: array of real; // длины K
|
||
|
||
// diagnostics
|
||
fTrainLossHistory: List<real>;
|
||
fValLossHistory: List<real>;
|
||
fBestIteration: integer;
|
||
fBestScoreLoss: real;
|
||
|
||
fFeatureImportances: Vector;
|
||
|
||
fOOBLossHistory: List<real>;
|
||
|
||
fRandomSeed: integer;
|
||
fRng: System.Random;
|
||
fUserProvidedSeed: boolean;
|
||
|
||
fClassLabels: array of string;
|
||
|
||
private
|
||
function FitInternal(XTrain: Matrix; yTrain: Vector; XVal: Matrix; yVal: Vector; useValidation: boolean)
|
||
: ISupervisedModel;
|
||
|
||
//procedure BuildClassMapping(y: Vector);
|
||
//function ApplyLabelEncoding(y: Vector): array of integer;
|
||
|
||
procedure SoftmaxRow(var logits: array of real; var probs: array of real);
|
||
procedure SoftmaxMatrix(logits: Matrix; probs: Matrix);
|
||
|
||
function ComputeLogLoss(yEncoded: array of integer; probs: Matrix): real;
|
||
|
||
function ComputeLogLossMasked(yEncoded: array of integer; logits: Matrix;
|
||
mask: array of boolean): real;
|
||
|
||
public
|
||
/// Создает новый GradientBoostingClassifier:
|
||
/// • nEstimators — число итераций бустинга.
|
||
/// • learningRate — коэффициент shrinkage.
|
||
/// • maxDepth — максимальная глубина деревьев.
|
||
/// • minSamplesSplit — минимальное число объектов для split.
|
||
/// • minSamplesLeaf — минимальное число объектов в листе.
|
||
/// • subsample — доля обучающей выборки на каждой итерации.
|
||
/// • seed — зерно генератора случайных чисел.
|
||
/// • earlyStoppingPatience — число итераций без улучшения до ранней остановки
|
||
constructor Create(
|
||
nEstimators: integer := 200;
|
||
learningRate: real := 0.05;
|
||
maxDepth: integer := 3;
|
||
minSamplesSplit: integer := 2;
|
||
minSamplesLeaf: integer := 1;
|
||
subsample: real := 1.0;
|
||
earlyStoppingPatience: integer := 20;
|
||
seed: integer := -1);
|
||
|
||
/// Обучает модель градиентного бустинга на обучающей выборке.
|
||
/// Обучение выполняется без early stopping.
|
||
/// Для предотвращения переобучения используйте FitWithValidation.
|
||
/// Early stopping работает только при наличии валидационной выборки
|
||
/// и параметре earlyStoppingPatience > 0.
|
||
/// Возвращает обученную модель.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
|
||
/// Обучает модель градиентного бустинга с использованием валидационной выборки.
|
||
/// Поддерживает early stopping:
|
||
/// обучение останавливается, если метрика не улучшается в течение earlyStoppingPatience итераций.
|
||
/// Возвращает обученную модель.
|
||
function FitWithValidation(XTrain: Matrix; yTrain: Vector; XVal: Matrix; yVal: Vector): ISupervisedModel;
|
||
|
||
/// Предсказывает метки классов.
|
||
/// Возвращает исходные значения классов, а не внутренние индексы.
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
function PredictLabels(X: Matrix): array of integer;
|
||
|
||
/// Возвращает вероятности принадлежности к каждому классу.
|
||
/// Размер результата: [nSamples x nClasses].
|
||
/// Вероятности получаются через softmax.
|
||
function PredictProba(X: Matrix): Matrix;
|
||
|
||
function GetClasses: array of real;
|
||
|
||
/// Создает глубокую копию классификатора.
|
||
/// Копируются все деревья и внутреннее состояние.
|
||
function Clone: IModel;
|
||
|
||
/// История значения LogLoss на обучающей выборке.
|
||
/// Один элемент на итерацию бустинга.
|
||
property TrainLossHistory: List<real> read fTrainLossHistory;
|
||
/// История LogLoss на validation-наборе.
|
||
property ValLossHistory: List<real> read fValLossHistory;
|
||
/// Индекс итерации с лучшим значением функции потерь (validation или OOB).
|
||
property BestIteration: integer read fBestIteration;
|
||
/// История OOB LogLoss (если включен OOB early stopping).
|
||
property OOBLossHistory: List<real> read fOOBLossHistory;
|
||
|
||
/// Возвращает вероятности после первых m итераций бустинга.
|
||
/// m может быть от 0 до TreeCount.
|
||
/// Используется для анализа обучения.
|
||
function PredictStageProba(X: Matrix; m: integer): Matrix;
|
||
|
||
/// Предсказывает классы по первым m итерациям бустинга.
|
||
/// Удобно для построения learning curve.
|
||
function PredictStage(X: Matrix; m: integer): Vector;
|
||
|
||
/// Текущее количество деревьев в ансамбле.
|
||
function TreeCount: integer := fEstimators.Count;
|
||
|
||
/// Нормированные importance признаков.
|
||
/// Рассчитываются как суммарное уменьшение impurity
|
||
/// по всем деревьям и нормируются к 1.
|
||
function FeatureImportances: Vector;
|
||
|
||
/// Возвращает строковое представление модели.
|
||
function ToString: string; override;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
procedure SetClassLabels(classes: array of string);
|
||
|
||
function GetClassLabels: array of string;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// KNN
|
||
//-----------------------------
|
||
|
||
Neighbor = record
|
||
dist: double;
|
||
idx: integer;
|
||
end;
|
||
|
||
/// Режим взвешивания в алгоритме k ближайших соседей.
|
||
/// Uniform — равномерное голосование/усреднение.
|
||
/// Distance — веса обратно пропорциональны расстоянию (1 / dist)
|
||
KNNWeighting = (Uniform, Distance);
|
||
|
||
/// Базовый абстрактный класс для алгоритма k ближайших соседей (kNN).
|
||
/// Реализует общий механизм поиска k ближайших объектов,
|
||
/// но не определяет способ агрегации (классификация или регрессия)
|
||
KNNBase = abstract class(IPredictiveModel)
|
||
protected
|
||
// ==== train state ====
|
||
fXTrain: Matrix;
|
||
fK: integer;
|
||
fFitted: boolean;
|
||
|
||
fWeighting: KNNWeighting;
|
||
|
||
// ==== work buffers ====
|
||
fNeighbors: array of Neighbor;
|
||
|
||
// ==== common methods ====
|
||
procedure ValidatePredictInput(X: Matrix);
|
||
|
||
function SquaredL2(trainRow: integer; XTest: Matrix; testRow: integer): double;
|
||
|
||
procedure QuickSelect(k: integer);
|
||
function Partition(left, right: integer): integer;
|
||
|
||
public
|
||
/// Создаёт модель kNN.
|
||
/// k — число ближайших соседей (k > 0).
|
||
/// weighting — режим взвешивания соседей
|
||
constructor Create(k: integer; weighting: KNNWeighting := KNNWeighting.Uniform);
|
||
|
||
/// Обучает модель на матрице признаков X и целевом векторе y.
|
||
/// Возвращает текущий экземпляр модели
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel; virtual; abstract;
|
||
|
||
/// Выполняет предсказание для матрицы признаков X.
|
||
/// Возвращает вектор предсказанных значений или меток
|
||
function Predict(X: Matrix): Vector; virtual; abstract;
|
||
|
||
/// Создаёт глубокую копию модели
|
||
function Clone: IModel; virtual; abstract;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
property IsFitted: boolean read fFitted;
|
||
end;
|
||
|
||
/// Классификатор на основе алгоритма k ближайших соседей (kNN).
|
||
/// Поддерживает равномерное (Uniform) и взвешенное по расстоянию (Distance) голосование.
|
||
/// Реализует вероятностные предсказания через PredictProba
|
||
/// ВАЖНО: KNN чувствителен к масштабу признаков.
|
||
/// Всегда используйте StandardScaler или MinMaxScaler в Pipeline перед KNN.
|
||
KNNClassifier = class(KNNBase, IProbabilisticClassifier)
|
||
private
|
||
// ==== classification state ====
|
||
fYEnc: array of integer;
|
||
fClasses: array of double;
|
||
fClassCount: integer;
|
||
fClassLabels: array of string;
|
||
|
||
// ==== voting buffers ====
|
||
fVotes: array of double;
|
||
fMark: array of integer;
|
||
fTouched: array of integer;
|
||
fEpoch: integer;
|
||
|
||
public
|
||
/// Создаёт классификатор kNN.
|
||
/// k — число ближайших соседей (k > 0).
|
||
/// weighting — режим взвешивания голосов соседей
|
||
constructor Create(k: integer; weighting: KNNWeighting := KNNWeighting.Uniform);
|
||
|
||
/// Обучает модель на матрице признаков X и векторе меток y.
|
||
/// Метки классов могут быть произвольными числами.
|
||
/// Возвращает текущий экземпляр модели
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel; override;
|
||
|
||
/// Выполняет предсказание меток классов для объектов X.
|
||
/// Возвращает вектор предсказанных меток
|
||
function Predict(X: Matrix): Vector; override;
|
||
|
||
function PredictLabels(X: Matrix): array of integer;
|
||
|
||
/// Возвращает матрицу вероятностей размера (nSamples × nClasses).
|
||
/// Столбцы соответствуют классам в порядке, возвращаемом GetClasses()
|
||
function PredictProba(X: Matrix): Matrix;
|
||
|
||
/// Возвращает массив меток классов в порядке столбцов PredictProba
|
||
function GetClasses: array of real;
|
||
|
||
/// Создаёт глубокую копию классификатора
|
||
function Clone: IModel; override;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
procedure SetClassLabels(classes: array of string);
|
||
|
||
function GetClassLabels: array of string;
|
||
end;
|
||
|
||
|
||
/// Регрессор на основе алгоритма k ближайших соседей (kNN).
|
||
/// Поддерживает равномерное (Uniform) усреднение и взвешенное по расстоянию (Distance) усреднение.
|
||
/// Предсказание вычисляется как среднее (или взвешенное среднее) значений целевой переменной
|
||
/// по k ближайшим обучающим объектам
|
||
KNNRegressor = class(KNNBase, IRegressor)
|
||
private
|
||
fYTrain: Vector;
|
||
|
||
public
|
||
/// Создаёт регрессор kNN.
|
||
/// k — число ближайших соседей (k > 0).
|
||
/// weighting — режим взвешивания вкладов соседей
|
||
constructor Create(k: integer; weighting: KNNWeighting := KNNWeighting.Uniform);
|
||
|
||
/// Обучает модель на матрице признаков X и целевом векторе y.
|
||
/// Возвращает текущий экземпляр модели
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel; override;
|
||
|
||
/// Выполняет предсказание числовых значений для объектов X.
|
||
/// Возвращает вектор предсказанных значений
|
||
function Predict(X: Matrix): Vector; override;
|
||
|
||
/// Создаёт глубокую копию регрессора
|
||
function Clone: IModel; override;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
|
||
/// Модель кластеризации методом k-средних (KMeans).
|
||
/// Разбивает объекты на k кластеров на основе евклидова расстояния.
|
||
/// Реализует алгоритм без учителя
|
||
KMeans = class(IPredictiveClusterer)
|
||
private
|
||
fNClusters: integer;
|
||
fMaxIter: integer;
|
||
fTol: real;
|
||
fNInit: integer;
|
||
fSeed: integer;
|
||
|
||
fFitted: boolean;
|
||
fFeatureCount: integer;
|
||
|
||
fCenters: Matrix;
|
||
fInertia: real;
|
||
fIterations: integer;
|
||
fHasConverged: boolean;
|
||
|
||
fRandomSeed: integer;
|
||
fUserProvidedSeed: boolean;
|
||
fRng: System.Random;
|
||
|
||
function RunSingle(X: Matrix; rnd: System.Random): (Matrix, real, integer, boolean);
|
||
public
|
||
/// Создаёт модель KMeans.
|
||
/// nClusters — количество кластеров.
|
||
/// maxIter — максимальное число итераций.
|
||
/// tol — порог сходимости.
|
||
/// nInit — количество независимых запусков.
|
||
/// seed — значение генератора случайных чисел
|
||
constructor Create(
|
||
nClusters: integer;
|
||
maxIter: integer := 300;
|
||
tol: real := 1e-4;
|
||
nInit: integer := 10;
|
||
seed: integer := -1
|
||
);
|
||
|
||
/// Обучает модель по матрице признаков X.
|
||
/// Выполняет nInit запусков и выбирает решение с минимальной инерцией.
|
||
function Fit(X: Matrix): IUnsupervisedModel;
|
||
|
||
/// Возвращает индекс кластера для каждого объекта из X.
|
||
/// Требует предварительного вызова Fit.
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
/// Возвращает индекс кластера для каждого объекта из X.
|
||
/// Требует предварительного вызова Fit.
|
||
function PredictLabels(X: Matrix): array of integer;
|
||
|
||
/// Выполняет обучение и сразу возвращает метки кластеров.
|
||
function FitPredict(X: Matrix): array of integer;
|
||
|
||
/// Создаёт глубокую копию модели.
|
||
function Clone: IModel;
|
||
|
||
/// Количество кластеров
|
||
function ClustersCount: integer;
|
||
|
||
/// Количество кластеров.
|
||
property NClusters: integer read fNClusters;
|
||
/// Максимальное число итераций.
|
||
property MaxIter: integer read fMaxIter;
|
||
/// Порог сходимости.
|
||
property Tol: real read fTol;
|
||
/// Количество запусков алгоритма.
|
||
property NInit: integer read fNInit;
|
||
/// Используемый seed.
|
||
property Seed: integer read fSeed;
|
||
/// Матрица центроидов (k × p).
|
||
property ClusterCenters: Matrix read fCenters;
|
||
/// Значение инерции (сумма квадратов расстояний до центроидов).
|
||
property Inertia: real read fInertia;
|
||
/// Число выполненных итераций.
|
||
property Iterations: integer read fIterations;
|
||
/// Признак сходимости алгоритма.
|
||
property HasConverged: boolean read fHasConverged;
|
||
/// Число признаков модели.
|
||
property FeatureCount: integer read fFeatureCount;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
property IsFitted: boolean read fFitted;
|
||
end;
|
||
|
||
|
||
DBSCAN = class(IClusterer)
|
||
private
|
||
fEps: real;
|
||
fMinSamples: integer;
|
||
|
||
fFitted: boolean;
|
||
fFeatureCount: integer;
|
||
|
||
fLabels: array of integer;
|
||
fClusterCount: integer;
|
||
|
||
function RegionQuery(X: Matrix; i: integer; neighbors: List<integer>): integer;
|
||
|
||
public
|
||
/// Создаёт модель DBSCAN.
|
||
/// eps — радиус соседства.
|
||
/// minSamples — минимальное число точек в eps-окрестности.
|
||
/// seed — параметр для совместимости API.
|
||
constructor Create(
|
||
eps: real;
|
||
minSamples: integer := 5
|
||
);
|
||
|
||
/// Обучает модель на матрице признаков.
|
||
function Fit(X: Matrix): IUnsupervisedModel;
|
||
|
||
/// Возвращает метки кластеров.
|
||
function PredictLabels(X: Matrix): array of integer;
|
||
|
||
/// Возвращает метки после обучения.
|
||
function FitPredict(X: Matrix): array of integer;
|
||
|
||
/// Копия модели.
|
||
function Clone: IModel;
|
||
|
||
property Labels: array of integer read fLabels;
|
||
|
||
/// Количество кластеров, найденное моделью
|
||
function ClustersCount: integer;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
|
||
property IsFitted: boolean read fFitted;
|
||
end;
|
||
|
||
{$endregion Models}
|
||
|
||
{$region Pipeline}
|
||
/// Последовательный конвейер машинного обучения (supervised).
|
||
/// Гарантирует строгий порядок выполнения шагов:
|
||
/// [преобразователи] → [модель].
|
||
///
|
||
/// Поддерживает:
|
||
/// • преобразователи без учёта целевой переменной (unsupervised);
|
||
/// • преобразователи с учётом целевой переменной (supervised);
|
||
/// • одну финальную модель с учителем.
|
||
///
|
||
/// Все преобразователи применяются последовательно к признакам X,
|
||
/// после чего финальная модель обучается на (X, y).
|
||
///
|
||
/// Обеспечивает единый интерфейс Fit(X, y) / Predict(X)
|
||
/// и воспроизводимость полного процесса обучения
|
||
Pipeline = class(ISupervisedModel)
|
||
private
|
||
fTransformers: List<ITransformer>;
|
||
fModel: ISupervisedModel;
|
||
fFitted: boolean;
|
||
public
|
||
/// Создаёт конвейер машинного обучения для заданной модели:
|
||
/// • model — модель, которая будет обучена
|
||
/// после последовательного применения всех преобразователей.
|
||
constructor Create(model: ISupervisedModel);
|
||
|
||
/// Создаёт пустой пайплайн (конвейер машинного обучения).
|
||
/// Модель должна быть установлена через SetModel.
|
||
constructor Create;
|
||
|
||
{
|
||
// Pipeline.Build используется, когда данные уже представлены
|
||
// в виде числовой матрицы признаков X и вектора целевой переменной y.
|
||
// В этом случае DataFrame и препроцессоры уровня таблицы не требуются.
|
||
//
|
||
// Типичные ситуации:
|
||
// • экспериментирование с ML-алгоритмами
|
||
// • сравнение моделей
|
||
// • кросс-валидация
|
||
// • подбор гиперпараметров
|
||
// • тестирование моделей
|
||
//
|
||
// Pipeline объединяет несколько матричных преобразований (ITransformer)
|
||
// и модель (IModel) в единый объект, который можно обучать и использовать
|
||
// для предсказаний.
|
||
//
|
||
// В отличие от этого, DataPipeline.Build используется,
|
||
// когда исходные данные представлены в виде DataFrame
|
||
// и требуется выполнить препроцессинг таблицы
|
||
// (Imputer, OneHotEncoder, LabelEncoder и др.)
|
||
// перед преобразованием данных в Matrix/Vector.
|
||
|
||
var pipe1 :=
|
||
Pipeline.Build(
|
||
new StandardScaler,
|
||
new LogisticRegression
|
||
);
|
||
|
||
var pipe2 :=
|
||
Pipeline.Build(
|
||
new StandardScaler,
|
||
new PCATransformer(2),
|
||
new LogisticRegression
|
||
);
|
||
|
||
var score1 :=
|
||
Validation.CrossValidate(
|
||
pipe1,
|
||
X,
|
||
y,
|
||
5,
|
||
Metrics.Accuracy
|
||
);
|
||
|
||
var score2 :=
|
||
Validation.CrossValidate(
|
||
pipe2,
|
||
X,
|
||
y,
|
||
5,
|
||
Metrics.Accuracy
|
||
);
|
||
|
||
Println('Scaler + LogisticRegression = ', score1);
|
||
Println('Scaler + PCA + LogisticRegression = ', score2);
|
||
}
|
||
|
||
/// Строит конвейер машинного обучения из последовательности шагов.
|
||
/// Шаги указываются в порядке выполнения:
|
||
/// сначала преобразователи, затем модель.
|
||
/// Последний шаг обязан быть моделью (IModel).
|
||
/// Возвращает сконструированный конвейер.
|
||
static function Build(params steps: array of IPipelineStep): Pipeline;
|
||
|
||
/// Устанавливает или заменяет модель.
|
||
function SetModel(m: ISupervisedModel): Pipeline;
|
||
|
||
/// Добавляет преобразование в конец пайплайна
|
||
function Add(t: ITransformer): Pipeline;
|
||
|
||
/// Обучает преобразования и модель
|
||
/// Модель может быть только supervised а трансформеры - любыми
|
||
function Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
|
||
/// Применяет только преобразования (без модели)
|
||
function Transform(X: Matrix): Matrix;
|
||
|
||
/// Делает предсказание
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
/// Возвращает матрицу вероятностей (m x k)
|
||
/// для вероятностной модели в конце пайплайна.
|
||
function PredictProba(X: Matrix): Matrix;
|
||
|
||
/// Показывает, был ли пайплайн обучен (вызван метод Fit).
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
|
||
function Clone: IModel;
|
||
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
UPipeline = class(IUnsupervisedModel)
|
||
private
|
||
fTransformers: List<ITransformer>;
|
||
fModel: IModel;
|
||
fFitted: boolean;
|
||
public
|
||
constructor Create(model: IModel);
|
||
constructor Create;
|
||
|
||
static function Build(params steps: array of IPipelineStep): UPipeline;
|
||
|
||
function SetModel(m: IModel): UPipeline;
|
||
function Add(t: ITransformer): UPipeline;
|
||
|
||
function Fit(X: Matrix): IUnsupervisedModel;
|
||
function Transform(X: Matrix): Matrix;
|
||
function Predict(X: Matrix): Vector;
|
||
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
function Clone: IModel;
|
||
function Name: string := Self.GetType.Name;
|
||
end;
|
||
|
||
{$endregion Pipeline}
|
||
|
||
{$region Transformers}
|
||
|
||
/// Стандартизирует признаки: вычитает среднее
|
||
/// и делит на стандартное отклонение по каждому столбцу.
|
||
/// Используется для приведения признаков к сопоставимому масштабу
|
||
StandardScaler = class(IUnsupervisedTransformer)
|
||
private
|
||
fMean: Vector;
|
||
fStd: Vector;
|
||
fFeatureCount: integer;
|
||
fFitted: boolean;
|
||
public
|
||
/// Создаёт StandardScaler.
|
||
/// Параметры масштабирования (среднее и стандартное отклонение)
|
||
/// вычисляются при вызове Fit.
|
||
constructor Create(); begin end;
|
||
/// Вычисляет среднее и стандартное отклонение по каждому признаку.
|
||
function Fit(X: Matrix): IUnsupervisedTransformer;
|
||
|
||
/// Применяет стандартизацию к данным.
|
||
function Transform(X: Matrix): Matrix;
|
||
|
||
/// Средние значения признаков, вычисленные при обучении.
|
||
property Mean: Vector read fMean;
|
||
|
||
/// Стандартные отклонения признаков, вычисленные при обучении.
|
||
property Std: Vector read fStd;
|
||
|
||
/// Признак того, что преобразование обучено.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
|
||
function Clone: ITransformer;
|
||
end;
|
||
|
||
/// Масштабирует признаки в заданный диапазон
|
||
/// (по умолчанию [0, 1]) на основе минимального и максимального значения каждого столбца.
|
||
/// Используется для приведения признаков к единому масштабу без центрирования
|
||
MinMaxScaler = class(IUnsupervisedTransformer)
|
||
private
|
||
fMin: Vector;
|
||
fMax: Vector;
|
||
fFeatureCount: integer;
|
||
fFitted: boolean;
|
||
fRangeMin: real;
|
||
fRangeMax: real;
|
||
public
|
||
/// Создаёт MinMaxScaler с диапазоном [rangeMin, rangeMax].
|
||
/// По умолчанию масштабирование выполняется к [0, 1]
|
||
constructor Create(rangeMin: real := 0.0; rangeMax: real := 1.0);
|
||
/// Вычисляет минимальные и максимальные значения по каждому признаку.
|
||
function Fit(X: Matrix): IUnsupervisedTransformer;
|
||
|
||
/// Применяет линейное масштабирование признаков к диапазону [0, 1].
|
||
function Transform(X: Matrix): Matrix;
|
||
|
||
/// Минимальные значения признаков, вычисленные при обучении.
|
||
property Min: Vector read fMin;
|
||
|
||
/// Максимальные значения признаков, вычисленные при обучении.
|
||
property Max: Vector read fMax;
|
||
|
||
/// Признак того, что преобразование обучено.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
|
||
function Clone: ITransformer;
|
||
end;
|
||
|
||
/// Трансформер главных компонент (PCA).
|
||
/// Выполняет уменьшение размерности путём проекции данных
|
||
/// на первые k главных компонент.
|
||
/// На этапе Fit вычисляет главные компоненты ковариационной матрицы.
|
||
/// На этапе Transform проецирует данные:
|
||
/// Z = (X - μ) · W
|
||
PCATransformer = class(IUnsupervisedTransformer)
|
||
private
|
||
fK: integer;
|
||
fComponents: Matrix; // W
|
||
fMean: Vector; // μ
|
||
fFeatureCount: integer;
|
||
fFitted: boolean;
|
||
public
|
||
/// Создаёт PCA-трансформер:
|
||
/// • k — число главных компонент (k > 0).
|
||
constructor Create(k: integer);
|
||
|
||
/// Обучает трансформер на матрице признаков X.
|
||
/// X — матрица m × n.
|
||
function Fit(X: Matrix): IUnsupervisedTransformer;
|
||
|
||
/// Преобразует матрицу X в пространство главных компонент.
|
||
/// Возвращает матрицу m × k.
|
||
function Transform(X: Matrix): Matrix;
|
||
|
||
property Components: Matrix read fComponents;
|
||
property Mean: Vector read fMean;
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
function Clone: ITransformer;
|
||
end;
|
||
|
||
/// Трансформер, удаляющий признаки с малой дисперсией.
|
||
/// Удаляет столбцы X_j, для которых Var(X_j) < threshold.
|
||
/// Не использует целевую переменную (unsupervised)
|
||
VarianceThreshold = class(IUnsupervisedTransformer)
|
||
private
|
||
fThreshold: real;
|
||
fSelected: array of integer;
|
||
fFeatureCount: integer;
|
||
fFitted: boolean;
|
||
|
||
public
|
||
/// Создаёт VarianceThreshold:
|
||
/// • threshold — минимально допустимая дисперсия признака.
|
||
/// Столбцы с Var(X_j) < threshold удаляются.
|
||
constructor Create(threshold: real := 0.0);
|
||
|
||
/// Вычисляет дисперсии признаков и запоминает индексы
|
||
/// признаков, удовлетворяющих порогу.
|
||
function Fit(X: Matrix): IUnsupervisedTransformer;
|
||
|
||
/// Возвращает матрицу, содержащую только отобранные признаки.
|
||
function Transform(X: Matrix): Matrix;
|
||
|
||
/// Индексы выбранных признаков.
|
||
property SelectedFeatures: array of integer read fSelected;
|
||
|
||
/// Показывает, был ли выполнен Fit.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
function ToString: string; override;
|
||
function Clone: ITransformer;
|
||
end;
|
||
|
||
/// Тип критерия оценки признаков для SelectKBest.
|
||
/// Определяет способ вычисления значимости признака относительно целевой переменной
|
||
FeatureScore = (
|
||
/// Абсолютное значение коэффициента корреляции Пирсона
|
||
/// между признаком и целевой переменной.
|
||
/// Подходит для задач регрессии и бинарной классификации при линейной зависимости
|
||
Correlation,
|
||
|
||
/// F-статистика линейной регрессии (FRegression).
|
||
/// Оценивает статистическую значимость линейной связи
|
||
/// между признаком и целевой переменной.
|
||
/// Основан на коэффициенте детерминации (R²) и F-статистике.
|
||
/// Более строгий критерий, чем простая корреляция
|
||
FRegression,
|
||
|
||
/// ANOVA F-критерий.
|
||
/// Используется в задачах классификации.
|
||
/// Оценивает различие средних значений признака между различными классами
|
||
AnovaF,
|
||
|
||
/// Хи-квадрат (Chi-Square) критерий независимости.
|
||
/// Используется в задачах классификации.
|
||
/// Оценивает зависимость между признаком и классом
|
||
/// на основе различия наблюдаемых и ожидаемых частот.
|
||
/// Предполагает, что значения признака неотрицательны.
|
||
/// Часто применяется для текстовых данных и частотных представлений (bag-of-words)
|
||
ChiSquare
|
||
);
|
||
|
||
/// Преобразователь с учётом целевой переменной
|
||
/// Для каждого признака вычисляет score(X_j, y)
|
||
/// и оставляет k признаков с наибольшим значением score.
|
||
/// Может использовать встроенные критерии или пользовательскую функцию оценки
|
||
SelectKBest = class(ISupervisedTransformer)
|
||
private
|
||
fK: integer;
|
||
fScoreType: FeatureScore;
|
||
fScoreFunc: (Vector, Vector) -> real;
|
||
fSelected: array of integer;
|
||
fFeatureCount: integer;
|
||
fFitted: boolean;
|
||
|
||
function ComputeScore(feature: Vector; y: Vector): real;
|
||
function ComputeCorrelation(x: Vector; y: Vector): real;
|
||
function ComputeFRegression(feature: Vector; y: Vector): real;
|
||
// Multiclass version
|
||
function ComputeAnovaF(feature: Vector; y: Vector): real;
|
||
// Multiclass version
|
||
function ComputeChiSquare(feature: Vector; y: Vector): real;
|
||
public
|
||
/// Создаёт трансформер SelectKBest с использованием встроенного критерия:
|
||
/// • k — число отбираемых признаков.
|
||
/// • score — тип критерия (например, Correlation)
|
||
constructor Create(k: integer; score: FeatureScore := FeatureScore.Correlation);
|
||
|
||
/// Создаёт трансформер SelectKBest с пользовательской функцией оценки:
|
||
/// • scoreFunc — функция (feature, y) → real
|
||
constructor Create(k: integer; scoreFunc: (Vector, Vector) -> real);
|
||
|
||
/// Вычисляет оценки признаков и выбирает k лучших.
|
||
function Fit(X: Matrix; y: Vector): ISupervisedTransformer;
|
||
|
||
/// Возвращает матрицу, содержащую только выбранные признаки.
|
||
function Transform(X: Matrix): Matrix;
|
||
|
||
/// Индексы выбранных признаков.
|
||
property SelectedFeatures: array of integer read fSelected;
|
||
|
||
/// Показывает, был ли выполнен Fit.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
/// Возвращает строковое представление трансформера
|
||
function ToString: string; override;
|
||
|
||
/// Создает глубокую копию.
|
||
/// Копируются параметры и внутреннее состояние.
|
||
function Clone: ITransformer;
|
||
end;
|
||
|
||
/// Тип нормы для нормализации строк
|
||
NormType = (L1, L2);
|
||
|
||
/// Преобразователь нормализации по строкам.
|
||
/// Для каждой строки X_i выполняет нормализацию:
|
||
/// L1: x := x / ‖x‖₁
|
||
/// L2: x := x / ‖x‖₂
|
||
/// Используется перед моделями, чувствительными к масштабу
|
||
/// (LogisticRegression, SVM, L1-регуляризация)
|
||
Normalizer = class(IUnsupervisedTransformer)
|
||
private
|
||
fNormType: NormType;
|
||
fFeatureCount: integer;
|
||
fFitted: boolean;
|
||
public
|
||
/// Создает нормализатор:
|
||
/// • norm — тип нормы, используемой для масштабирования строки признаков.
|
||
/// По умолчанию используется L2-норма.
|
||
constructor Create(norm: NormType := NormType.L2);
|
||
|
||
/// Подготавливает трансформер к работе.
|
||
/// Для Normalizer этап обучения может быть формальным, так как параметры не накапливаются.
|
||
function Fit(X: Matrix): IUnsupervisedTransformer;
|
||
|
||
/// Применяет нормализацию к матрице X.
|
||
/// Каждая строка масштабируется так, чтобы ее норма соответствовала выбранному типу.
|
||
/// Возвращает новую матрицу с нормализованными объектами.
|
||
function Transform(X: Matrix): Matrix;
|
||
|
||
/// Показывает, был ли вызван метод Fit.
|
||
/// Если False, вызов Transform может привести к ошибке.
|
||
property IsFitted: boolean read fFitted;
|
||
|
||
/// Возвращает строковое представление трансформера
|
||
function ToString: string; override;
|
||
|
||
/// Создает глубокую копию нормализатора.
|
||
/// Копируются параметры и внутреннее состояние.
|
||
function Clone: ITransformer;
|
||
end;
|
||
|
||
{$endregion Transformers}
|
||
|
||
|
||
{$region Utility functions}
|
||
|
||
|
||
{$endregion Utility functions}
|
||
|
||
type
|
||
MLConfig = static class
|
||
public
|
||
/// Проверять ли входные данные моделей на NaN, Inf
|
||
static ValidateFiniteInputs: boolean := True;
|
||
end;
|
||
|
||
implementation
|
||
|
||
uses MLExceptions;
|
||
uses MLUtilsABC;
|
||
|
||
{$region ErrConstants}
|
||
const
|
||
ER_PIPELINE_NO_STEPS =
|
||
'Pipeline должен содержать хотя бы один шаг!!Pipeline requires at least one step';
|
||
ER_PIPELINE_LAST_NOT_MODEL =
|
||
'Последний шаг Pipeline должен быть моделью (IModel)!!Last step must be a model (IModel)';
|
||
ER_PIPELINE_INVALID_STEP_ORDER =
|
||
'Все шаги, кроме последнего, должны быть трансформерами!!All steps except the last must be transformers';
|
||
ER_TRANSFORMER_NULL =
|
||
'Трансформер не может быть nil!!Transformer cannot be nil';
|
||
ER_PROBA_NOT_SUPPORTED =
|
||
'Модель не поддерживает предсказание вероятностей!!Model does not support probability prediction';
|
||
ER_RANGE_INVALID =
|
||
'rangeMax должен быть больше rangeMin!!rangeMax must be greater than rangeMin';
|
||
ER_K_MUST_BE_POSITIVE =
|
||
'Параметр k должен быть > 0!!Parameter k must be > 0';
|
||
ER_K_EXCEEDS_FEATURES =
|
||
'k превышает число признаков!!k exceeds feature count';
|
||
ER_THRESHOLD_NEGATIVE =
|
||
'Порог threshold должен быть >= 0!!threshold must be >= 0';
|
||
ER_CHI_SQUARE_NEGATIVE =
|
||
'ChiSquare требует неотрицательные признаки!!ChiSquare requires non-negative features';
|
||
ER_UNKNOWN_FEATURE_SCORE =
|
||
'Неизвестный тип FeatureScore!!Unknown FeatureScore type';
|
||
ER_SELECTKBEST_FIT_INVALID =
|
||
'Для SelectKBest необходимо вызывать Fit(X, y)!!SelectKBest requires Fit(X, y)';
|
||
ER_FIT_NOT_CALLED =
|
||
'Необходимо вызвать Fit перед Predict!!Fit must be called before Predict';
|
||
ER_X_NULL =
|
||
'X не может быть nil!!X cannot be nil';
|
||
ER_Y_NULL =
|
||
'y не может быть nil!!y cannot be nil';
|
||
ER_XY_SIZE_MISMATCH =
|
||
'Размерности X и y не согласованы!!X and y size mismatch';
|
||
ER_FEATURE_COUNT_MISMATCH =
|
||
'Число признаков не совпадает!!Feature count mismatch';
|
||
ER_N_ESTIMATORS_NOT_POSITIVE =
|
||
'Параметр nEstimators должен быть > 0!!nEstimators must be > 0';
|
||
ER_LEARNING_RATE_NOT_POSITIVE =
|
||
'Параметр learningRate должен быть > 0!!learningRate must be > 0';
|
||
ER_SUBSAMPLE_OUT_OF_RANGE =
|
||
'Параметр subsample должен быть в диапазоне (0, 1]!!subsample must be in (0, 1]';
|
||
ER_LABELS_NOT_INTEGER =
|
||
'Метки классов должны быть целыми!!Class labels must be integers';
|
||
ER_N_ESTIMATORS_INVALID =
|
||
'nEstimators должен быть > 0!!nEstimators must be > 0';
|
||
ER_LEARNING_RATE_INVALID =
|
||
'learningRate должен быть > 0!!learningRate must be > 0';
|
||
ER_MAX_DEPTH_INVALID =
|
||
'maxDepth должен быть > 0!!maxDepth must be > 0';
|
||
ER_SUBSAMPLE_INVALID =
|
||
'subsample должен быть > 0!!subsample must be > 0';
|
||
ER_NAN_IN_Y =
|
||
'y содержит NaN!!y contains NaN';
|
||
ER_NAN_IN_X =
|
||
'x содержит NaN!!x contains NaN';
|
||
ER_K_EXCEEDS_SAMPLES =
|
||
'k превышает число обучающих объектов!!k exceeds number of training samples';
|
||
ER_MAX_DEPTH_TOO_LARGE =
|
||
'maxDepth слишком велик {0}. Возможен переполнение стека!!' +
|
||
'maxDepth is too large {0}. It may cause stack overflow';
|
||
ER_LOGISTIC_DIVERGED =
|
||
'Обучение LogisticRegression расходится. Уменьшите learningRate.' +
|
||
'LogisticRegression training diverged. Reduce learningRate.';
|
||
ER_LOGISTIC_SOFTMAX_ZERO =
|
||
'Softmax дал нулевую сумму экспонент. Уменьшите learningRate.' +
|
||
'Softmax produced zero sum of exponentials. Reduce learningRate.';
|
||
ER_LOGISTIC_INVALID_LOSS =
|
||
'Функция потерь стала NaN или Infinity. Обучение прервано.' +
|
||
'Loss became NaN or Infinity. Training stopped.';
|
||
ER_ELASTICNET_DIVERGED =
|
||
'ElasticNet расходится. Увеличьте регуляризацию или уменьшите tol.' +
|
||
'ElasticNet diverged. Increase regularization or reduce tol.';
|
||
ER_ELASTICNET_INVALID_LOSS =
|
||
'Функция потерь ElasticNet стала NaN или Infinity.' +
|
||
'ElasticNet loss became NaN or Infinity.';
|
||
ER_TRAINING_DATA_CONTAINS_NAN =
|
||
'Обучающие данные содержат NaN. Обработайте пропуски до вызова Fit.!!' +
|
||
'Training data contains NaN. Please handle missing values before calling Fit.';
|
||
ER_TRAINING_DATA_CONTAINS_INF =
|
||
'Обучающие данные содержат Infinity. Проверьте данные на выбросы.!!' +
|
||
'Training data contains Infinity. Please check for extreme values.';
|
||
ER_PREDICTION_DATA_CONTAINS_NAN =
|
||
'Данные для предсказания содержат NaN. Обработайте пропуски до вызова Predict.!!' +
|
||
'Prediction data contains NaN. Please handle missing values before calling Predict.';
|
||
ER_PREDICTION_DATA_CONTAINS_INF =
|
||
'Данные для предсказания содержат Infinity. Проверьте данные на выбросы.!!' +
|
||
'Prediction data contains Infinity. Please check for extreme values.';
|
||
ER_MAXITER_INVALID =
|
||
'maxIter должен быть положительным. Получено {0}.!!' +
|
||
'maxIter must be positive. Received {0}.';
|
||
ER_TOL_INVALID =
|
||
'tol должен быть положительным. Получено {0}.!!' +
|
||
'tol must be positive. Received {0}.';
|
||
ER_EPOCHS_INVALID =
|
||
'epochs должен быть положительным. Получено {0}.!!' +
|
||
'epochs must be positive. Received {0}.';
|
||
ER_MINSAMPLESSPLIT_INVALID =
|
||
'minSamplesSplit должен быть ≥ 2. Получено {0}.!!' +
|
||
'minSamplesSplit must be ≥ 2. Received {0}.';
|
||
ER_MINSAMPLESLEAF_INVALID =
|
||
'minSamplesLeaf должен быть ≥ 1. Получено {0}.!!' +
|
||
'minSamplesLeaf must be ≥ 1. Received {0}.';
|
||
ER_L2_NEGATIVE =
|
||
'L2-регуляризация листа не может быть отрицательной. Получено {0}.!!' +
|
||
'Leaf L2 regularization cannot be negative. Received {0}.';
|
||
ER_NTREES_INVALID =
|
||
'Число деревьев должно быть положительным. Получено {0}.!!' +
|
||
'Number of trees must be positive. Received {0}.';
|
||
ER_MODEL_NOT_INITIALIZED =
|
||
'Модель не инициализирована или не содержит деревьев.!!' +
|
||
'Model is not initialized or contains no trees.';
|
||
ER_NESTIMATORS_INVALID =
|
||
'nEstimators должен быть положительным. Получено {0}.!!' +
|
||
'nEstimators must be positive. Received {0}.';
|
||
ER_STAGE_OUT_OF_RANGE =
|
||
'Stage m должен быть в диапазоне [0, {1}]. Получено {0}.!!' +
|
||
'Stage m must be in range [0, {1}]. Received {0}.';
|
||
ER_PIPELINE_STEP_NULL =
|
||
'Шаг Pipeline с индексом {0} равен nil.!!' +
|
||
'Pipeline step at index {0} is nil.';
|
||
ER_PIPELINE_TRANSFORM_RETURNED_NULL =
|
||
'Шаг Pipeline вернул nil из Transform. Проверьте реализацию трансформера.!!' +
|
||
'Pipeline step returned nil from Transform. Check transformer implementation.';
|
||
ER_K_INVALID =
|
||
'Параметр k должен быть положительным: {0}.!!' +
|
||
'Parameter k must be positive: {0}.';
|
||
ER_THRESHOLD_INVALID =
|
||
'Порог дисперсии должен быть >= 0: {0}.!!' +
|
||
'Variance threshold must be >= 0: {0}.';
|
||
ER_ALL_FEATURES_REMOVED =
|
||
'Все признаки удалены VarianceThreshold. Уменьшите порог.!!' +
|
||
'All features were removed by VarianceThreshold. Decrease the threshold.';
|
||
ER_TRAINING_DIVERGED =
|
||
'Обучение разошлось. Попробуйте уменьшить learning rate.!!' +
|
||
'Training diverged. Try reducing the learning rate.';
|
||
ER_LABEL_INDEX_INVALID =
|
||
'Некорректный индекс класса при вычислении loss.!!' +
|
||
'Invalid class index during loss computation.';
|
||
ER_SOFTMAX_EMPTY =
|
||
'Softmax получил пустой вектор.!!' +
|
||
'Softmax received empty vector.';
|
||
ER_LOGISTIC_NEED_AT_LEAST_TWO_CLASSES =
|
||
'Для LogisticRegression нужно минимум 2 класса.!!' +
|
||
'LogisticRegression requires at least 2 classes.';
|
||
ER_MIN_SAMPLES_SPLIT_INVALID =
|
||
'minSamplesSplit должно быть >= 2 ({0}).!!' +
|
||
'minSamplesSplit must be >= 2 ({0}).';
|
||
ER_MIN_SAMPLES_LEAF_INVALID =
|
||
'minSamplesLeaf должно быть >= 1 ({0}).!!' +
|
||
'minSamplesLeaf must be >= 1 ({0}).';
|
||
ER_HUBER_DELTA_INVALID =
|
||
'huberDelta должно быть > 0 ({0}).!!' +
|
||
'huberDelta must be > 0 ({0}).';
|
||
ER_EARLY_STOPPING_INVALID =
|
||
'earlyStoppingPatience должно быть >= 0 ({0}).!!' +
|
||
'earlyStoppingPatience must be >= 0 ({0}).';
|
||
ER_QUANTILE_ALPHA_INVALID =
|
||
'quantileAlpha должно быть в (0,1) ({0}).!!' +
|
||
'quantileAlpha must be in (0,1) ({0}).';
|
||
ER_LEAFL2_INVALID =
|
||
'leafL2 должно быть >= 0 ({0}).!!' +
|
||
'leafL2 must be >= 0 ({0}).';
|
||
ER_MIN_LEAF_GE_SPLIT =
|
||
'minSamplesLeaf ({0}) должно быть меньше minSamplesSplit ({1}).!!' +
|
||
'minSamplesLeaf ({0}) must be less than minSamplesSplit ({1}).';
|
||
ER_OOB_NOT_ENABLED =
|
||
'OOB score не включен для этой модели. Установите computeOOB = true в конструкторе.!!' +
|
||
'OOB score is not enabled for this model. Set computeOOB = true in the constructor.';
|
||
ER_NINIT_INVALID =
|
||
'Некорректное значение nInit: {0}. Должно быть не меньше 1!!Invalid nInit value: {0}. Must be at least 1';
|
||
ER_INVALID_VALUE_AT =
|
||
'Некорректное значение в {0} на позиции {1}!!Invalid value in {0} at index {1}';
|
||
ER_EPS_INVALID =
|
||
'eps должен быть положительным!!eps must be positive';
|
||
ER_MINSAMPLES_INVALID =
|
||
'minSamples должен быть >= 1!!minSamples must be >= 1';
|
||
ER_PIPELINE_TRANSFORMER_NO_FIT =
|
||
'Трансформер с индексом {0} (тип: {1}) не поддерживает Fit!!Transformer at index {0} (type: {1}) does not support Fit';
|
||
ER_Model_NoFit =
|
||
'Модель (тип: {0}) не поддерживает Fit!!Model (type: {0}) does not support Fit';
|
||
ER_MODEL_NOT_UNSUPERVISED =
|
||
'Модель (тип: {0}) не является моделью без учителя!!' +
|
||
'Model (type: {0}) is not an unsupervised model';
|
||
ER_DBSCAN_PREDICT_ONLY_TRAIN_DATA =
|
||
'DBSCAN: Predict поддерживается только для обучающей выборки!!DBSCAN: Predict is only supported for training data';
|
||
ER_MODEL_NOT_FITTED =
|
||
'Модель "{0}" не обучена. Сначала вызовите Fit()|Model "{0}" is not fitted. Call Fit() first';
|
||
ER_DBSCAN_PREDICT_NEW_DATA =
|
||
'DBSCAN не поддерживает предсказание для новых данных!!DBSCAN does not support prediction for new data';
|
||
ER_CLASSES_NOT_AVAILABLE =
|
||
'Метки классов недоступны. Убедитесь, что модель обучена и метки установлены!!Class labels are not available. Ensure the model is fitted and class labels are set';
|
||
ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL =
|
||
'Последний шаг Pipeline должен быть supervised-моделью!!' +
|
||
'Last Pipeline step must be a supervised model';
|
||
ER_INTERNAL_INVALID_MODEL_CLONE =
|
||
'Внутренняя ошибка: Clone модели вернул несовместимый тип!!' +
|
||
'Internal error: model Clone returned incompatible type';
|
||
ER_PREDICT_NOT_SUPPORTED =
|
||
'Модель не поддерживает Predict для данного типа алгоритма!!' +
|
||
'Model does not support Predict for this type of algorithm';
|
||
ER_NEED_AT_LEAST_TWO_CLASSES =
|
||
'Необходимо как минимум два различных класса!!At least two distinct classes are required';
|
||
ER_UNKNOWN_CLASS_LABEL =
|
||
'Неизвестная метка класса: {0}!!Unknown class label: {0}';
|
||
|
||
{$endregion ErrConstants}
|
||
|
||
//-----------------------------
|
||
// Проверка на NuN/Inf
|
||
//-----------------------------
|
||
|
||
procedure CheckXForFit(X: Matrix);
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
for var i := 0 to X.RowCount - 1 do
|
||
for var j := 0 to X.ColCount - 1 do
|
||
begin
|
||
var v := X[i,j];
|
||
|
||
if double.IsNaN(v) then
|
||
ArgumentError(ER_TRAINING_DATA_CONTAINS_NAN);
|
||
|
||
if double.IsInfinity(v) then
|
||
ArgumentError(ER_TRAINING_DATA_CONTAINS_INF);
|
||
end;
|
||
end;
|
||
|
||
procedure CheckXForPredict(X: Matrix);
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
for var i := 0 to X.RowCount - 1 do
|
||
for var j := 0 to X.ColCount - 1 do
|
||
begin
|
||
var v := X[i,j];
|
||
|
||
if double.IsNaN(v) then
|
||
ArgumentError(ER_PREDICTION_DATA_CONTAINS_NAN);
|
||
|
||
if double.IsInfinity(v) then
|
||
ArgumentError(ER_PREDICTION_DATA_CONTAINS_INF);
|
||
end;
|
||
end;
|
||
|
||
procedure CheckYForFit(y: Vector);
|
||
begin
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
for var i := 0 to y.Length - 1 do
|
||
begin
|
||
var v := y[i];
|
||
|
||
if double.IsNaN(v) then
|
||
ArgumentError(ER_TRAINING_DATA_CONTAINS_NAN);
|
||
|
||
if double.IsInfinity(v) then
|
||
ArgumentError(ER_TRAINING_DATA_CONTAINS_INF);
|
||
end;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// LinearRegression
|
||
//-----------------------------
|
||
|
||
constructor LinearRegression.Create();
|
||
begin
|
||
ffitted := false;
|
||
end;
|
||
|
||
function LinearRegression.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
var m := X.RowCount;
|
||
|
||
if m = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if y.Length <> m then
|
||
DimensionError(ER_DIM_MISMATCH, y.Length, m);
|
||
|
||
var n := X.ColCount;
|
||
|
||
// --- means
|
||
var meanX := X.ColumnMeans;
|
||
var meanY := y.Mean;
|
||
|
||
// --- center X
|
||
var Xc := new Matrix(m, n);
|
||
for var i := 0 to m - 1 do
|
||
for var j := 0 to n - 1 do
|
||
Xc[i,j] := X[i,j] - meanX[j];
|
||
|
||
// --- center y
|
||
var yc := new Vector(m);
|
||
for var i := 0 to m - 1 do
|
||
yc[i] := y[i] - meanY;
|
||
|
||
// --- solve via QR
|
||
fcoef := SolveLeastSquaresQR(Xc, yc);
|
||
|
||
// --- intercept
|
||
fintercept := meanY - meanX.Dot(fcoef);
|
||
|
||
ffitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function LinearRegression.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not ffitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fcoef.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.ColCount, fCoef.Length);
|
||
|
||
Result := X * fcoef + fIntercept;
|
||
end;
|
||
|
||
function LinearRegression.ToString: string;
|
||
begin
|
||
Result := 'LinearRegression'
|
||
end;
|
||
|
||
function LinearRegression.Clone: IModel;
|
||
begin
|
||
var m := new LinearRegression;
|
||
|
||
m.fFitted := fFitted;
|
||
|
||
if fCoef <> nil then
|
||
m.fCoef := fCoef.Clone;
|
||
|
||
m.fIntercept := fIntercept;
|
||
|
||
Result := m;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// Activations
|
||
//-----------------------------
|
||
|
||
static function Activations.Sigmoid(v: Vector): Vector := v.Apply(x -> 1.0 / (1.0 + Exp(-x)));
|
||
|
||
static function Activations.Tanh(v: Vector): Vector := v.Apply(System.Math.Tanh);
|
||
|
||
static function Activations.ReLU(v: Vector): Vector := v.Apply(x -> (if x > 0 then x else 0.0));
|
||
|
||
static function Activations.Softmax(v: Vector): Vector;
|
||
begin
|
||
var n := v.Length;
|
||
var resultVec := new Vector(n);
|
||
|
||
if n = 0 then
|
||
exit(resultVec);
|
||
|
||
var maxVal := v.Max;
|
||
|
||
var sumExp := 0.0;
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var e := Exp(v[i] - maxVal);
|
||
resultVec[i] := e;
|
||
sumExp += e;
|
||
end;
|
||
|
||
if sumExp <= 0 then
|
||
begin
|
||
var uniform := 1.0 / n;
|
||
for var i := 0 to n - 1 do
|
||
resultVec[i] := uniform;
|
||
exit(resultVec);
|
||
end;
|
||
|
||
for var i := 0 to n - 1 do
|
||
resultVec[i] /= sumExp;
|
||
|
||
Result := resultVec;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// RidgeRegression
|
||
//-----------------------------
|
||
|
||
constructor RidgeRegression.Create(lambda: real);
|
||
begin
|
||
if lambda < 0 then
|
||
ArgumentError(ER_LAMBDA_NEGATIVE, lambda);
|
||
fLambda := lambda;
|
||
fFitted := false;
|
||
end;
|
||
|
||
function RidgeRegression.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
// Means
|
||
var muX := X.ColumnMeans;
|
||
var muY := y.Mean;
|
||
|
||
// Centered copies
|
||
var Xc := X.Clone;
|
||
var yc := y.Clone;
|
||
|
||
for var j := 0 to p - 1 do
|
||
for var i := 0 to n - 1 do
|
||
Xc[i, j] -= muX[j];
|
||
|
||
for var i := 0 to n - 1 do
|
||
yc[i] -= muY;
|
||
|
||
// Ridge solution
|
||
fCoef := SolveRidge(Xc, yc, fLambda);
|
||
|
||
// Intercept (NOT regularized)
|
||
fIntercept := muY - muX.Dot(fCoef);
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function RidgeRegression.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fCoef.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.ColCount, fCoef.Length);
|
||
|
||
Result := X * fCoef;
|
||
|
||
for var i := 0 to Result.Length - 1 do
|
||
Result[i] += fIntercept;
|
||
end;
|
||
|
||
function RidgeRegression.ToString: string;
|
||
begin
|
||
Result := 'RidgeRegression(lambda=' + fLambda + ')';
|
||
end;
|
||
|
||
function RidgeRegression.Clone: IModel;
|
||
begin
|
||
var m := new RidgeRegression(fLambda);
|
||
|
||
m.fFitted := fFitted;
|
||
|
||
if fCoef <> nil then
|
||
m.fCoef := fCoef.Clone;
|
||
|
||
m.fIntercept := fIntercept;
|
||
|
||
Result := m;
|
||
end;
|
||
|
||
|
||
//-----------------------------
|
||
// ElasticNet
|
||
//-----------------------------
|
||
|
||
constructor ElasticNet.Create(lambda1, lambda2: real; maxIter: integer; tol: real);
|
||
begin
|
||
if (lambda1 < 0) or (lambda2 < 0) then
|
||
ArgumentOutOfRangeError(ER_LAMBDA_NEGATIVE);
|
||
|
||
if maxIter <= 0 then
|
||
ArgumentOutOfRangeError(ER_MAXITER_INVALID, maxIter);
|
||
|
||
if tol <= 0 then
|
||
ArgumentOutOfRangeError(ER_TOL_INVALID, tol);
|
||
|
||
fLambda1 := lambda1;
|
||
fLambda2 := lambda2;
|
||
fMaxIter := maxIter;
|
||
fTol := tol;
|
||
fFitted := false;
|
||
end;
|
||
|
||
function ElasticNet.SoftThreshold(z, gamma: real): real;
|
||
begin
|
||
if z > gamma then
|
||
exit(z - gamma)
|
||
else if z < -gamma then
|
||
exit(z + gamma)
|
||
else
|
||
exit(0.0);
|
||
end;
|
||
|
||
function ElasticNet.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
if (fLambda1 < 0) or (fLambda2 < 0) then
|
||
ArgumentOutOfRangeError(ER_LAMBDA_NEGATIVE);
|
||
|
||
if fMaxIter <= 0 then
|
||
ArgumentOutOfRangeError(ER_MAXITER_INVALID, fMaxIter);
|
||
|
||
if fTol <= 0 then
|
||
ArgumentOutOfRangeError(ER_TOL_INVALID, fTol);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
var muX := X.ColumnMeans;
|
||
var muY := y.Mean;
|
||
|
||
var Xc := X.Clone;
|
||
var yc := y.Clone;
|
||
|
||
for var j := 0 to p - 1 do
|
||
for var i := 0 to n - 1 do
|
||
Xc[i,j] -= muX[j];
|
||
|
||
for var i := 0 to n - 1 do
|
||
yc[i] -= muY;
|
||
|
||
fCoef := new Vector(p);
|
||
var residual := yc.Clone; // since β = 0
|
||
|
||
var prevLoss := real.PositiveInfinity;
|
||
|
||
for var iter := 0 to fMaxIter - 1 do
|
||
begin
|
||
var maxChange := 0.0;
|
||
|
||
for var j := 0 to p - 1 do
|
||
begin
|
||
var oldBeta := fCoef[j];
|
||
|
||
var rho := 0.0;
|
||
var zj := 0.0;
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
rho += Xc[i,j] * (residual[i] + Xc[i,j] * oldBeta);
|
||
zj += Xc[i,j] * Xc[i,j];
|
||
end;
|
||
|
||
var denom := zj + fLambda2;
|
||
|
||
if denom > 0 then
|
||
begin
|
||
var newBeta := SoftThreshold(rho, fLambda1) / denom;
|
||
var delta := newBeta - oldBeta;
|
||
|
||
if delta <> 0 then
|
||
for var i := 0 to n - 1 do
|
||
residual[i] -= Xc[i,j] * delta;
|
||
|
||
if Abs(delta) > maxChange then
|
||
maxChange := Abs(delta);
|
||
|
||
fCoef[j] := newBeta;
|
||
end;
|
||
end;
|
||
|
||
// --- cheap divergence guard
|
||
if double.IsNaN(maxChange) or double.IsInfinity(maxChange) then
|
||
ArgumentError(ER_ELASTICNET_DIVERGED);
|
||
|
||
// --- каждые 10 итераций проверяем loss
|
||
if iter mod 10 = 0 then
|
||
begin
|
||
var rss := 0.0;
|
||
for var i := 0 to n - 1 do
|
||
rss += residual[i] * residual[i];
|
||
|
||
var l1 := 0.0;
|
||
var l2 := 0.0;
|
||
|
||
for var j := 0 to p - 1 do
|
||
begin
|
||
l1 += Abs(fCoef[j]);
|
||
l2 += fCoef[j] * fCoef[j];
|
||
end;
|
||
|
||
var currentLoss := rss / n + fLambda1*l1 + fLambda2*l2;
|
||
|
||
if double.IsNaN(currentLoss) or double.IsInfinity(currentLoss) then
|
||
ArgumentError(ER_ELASTICNET_INVALID_LOSS);
|
||
|
||
if Abs(prevLoss - currentLoss) < fTol then
|
||
break;
|
||
|
||
prevLoss := currentLoss;
|
||
end;
|
||
|
||
if maxChange < fTol then
|
||
break;
|
||
end;
|
||
|
||
fIntercept := muY - muX.Dot(fCoef);
|
||
fFitted := true;
|
||
|
||
Result := Self;
|
||
end;
|
||
|
||
function ElasticNet.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fCoef.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.ColCount, fCoef.Length);
|
||
|
||
Result := X * fCoef;
|
||
|
||
for var i := 0 to Result.Length - 1 do
|
||
Result[i] += fIntercept;
|
||
end;
|
||
|
||
function ElasticNet.ToString: string;
|
||
begin
|
||
Result :=
|
||
'ElasticNet(lambda1=' + fLambda1 +
|
||
', lambda2=' + fLambda2 +
|
||
', maxIter=' + fMaxIter +
|
||
', tol=' + fTol + ')';
|
||
end;
|
||
|
||
function ElasticNet.Clone: IModel;
|
||
begin
|
||
var m := new ElasticNet(fLambda1, fLambda2, fMaxIter, fTol);
|
||
|
||
m.fFitted := fFitted;
|
||
|
||
if fCoef <> nil then
|
||
m.fCoef := fCoef.Clone;
|
||
|
||
m.fIntercept := fIntercept;
|
||
|
||
Result := m;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// LassoRegression
|
||
//-----------------------------
|
||
|
||
constructor LassoRegression.Create(alpha: real; maxIter: integer; tol: real);
|
||
begin
|
||
// Lasso = ElasticNet с L2 = 0
|
||
fModel := new ElasticNet(alpha, 0.0, maxIter, tol);
|
||
end;
|
||
|
||
function LassoRegression.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
fModel.Fit(X, y);
|
||
Result := Self;
|
||
end;
|
||
|
||
function LassoRegression.Predict(X: Matrix): Vector;
|
||
begin
|
||
Result := fModel.Predict(X);
|
||
end;
|
||
|
||
function LassoRegression.Clone: IModel;
|
||
begin
|
||
var m := new LassoRegression;
|
||
m.fModel := ElasticNet(fModel.Clone);
|
||
Result := m;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// LogisticRegression
|
||
//-----------------------------
|
||
|
||
constructor LogisticRegression.Create(lambda: real; learningRate: real; epochs: integer;
|
||
tol: real; checkConvergence: boolean; minImprovement: real);
|
||
begin
|
||
fLambda := lambda;
|
||
fLearningRate := learningRate;
|
||
fEpochs := epochs;
|
||
fFitted := false;
|
||
fTol := tol;
|
||
fCheckConvergence := checkConvergence;
|
||
fMinImprovement := minImprovement;
|
||
end;
|
||
|
||
function LogisticRegression.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
if fLearningRate <= 0 then
|
||
ArgumentOutOfRangeError(ER_LEARNING_RATE_INVALID, fLearningRate);
|
||
|
||
if fEpochs <= 0 then
|
||
ArgumentOutOfRangeError(ER_EPOCHS_INVALID, fEpochs);
|
||
|
||
if fLambda < 0 then
|
||
ArgumentOutOfRangeError(ER_LAMBDA_NEGATIVE, fLambda);
|
||
|
||
if fCheckConvergence and (fTol <= 0) then
|
||
ArgumentOutOfRangeError(ER_TOL_INVALID, fTol);
|
||
|
||
var m := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
// --- convert to integer labels
|
||
var yInt := new integer[m];
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
var r := y[i];
|
||
var ir := Round(r);
|
||
|
||
if Abs(r - ir) > 1e-12 then
|
||
ArgumentError(ER_LABELS_NOT_INTEGER);
|
||
|
||
yInt[i] := ir;
|
||
end;
|
||
|
||
// --- encode (порядок первого появления)
|
||
var unique: array of integer;
|
||
var yEncoded := EncodeLabelsInt(yInt, unique);
|
||
|
||
fClassCount := unique.Length;
|
||
|
||
if fClassCount < 2 then
|
||
ArgumentError(ER_LOGISTIC_NEED_AT_LEAST_TWO_CLASSES);
|
||
|
||
// --- build mappings
|
||
fClassToIndex := new Dictionary<integer, integer>;
|
||
SetLength(fIndexToClass, fClassCount);
|
||
|
||
for var i := 0 to fClassCount - 1 do
|
||
begin
|
||
fClassToIndex[unique[i]] := i;
|
||
fIndexToClass[i] := unique[i];
|
||
end;
|
||
|
||
// --- init
|
||
fW := new Matrix(p, fClassCount);
|
||
fIntercept := new Vector(fClassCount);
|
||
|
||
var prevLoss := real.PositiveInfinity;
|
||
|
||
// --- training loop
|
||
for var epoch := 1 to fEpochs do
|
||
begin
|
||
var Z := X * fW;
|
||
|
||
// + intercept
|
||
for var i := 0 to m - 1 do
|
||
for var k := 0 to fClassCount - 1 do
|
||
Z[i,k] += fIntercept[k];
|
||
|
||
// --- stable softmax + loss accumulation
|
||
var loss := 0.0;
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
var maxVal := Z.RowMax(i);
|
||
var sumExp := 0.0;
|
||
|
||
for var k := 0 to fClassCount - 1 do
|
||
begin
|
||
Z[i,k] := Exp(Z[i,k] - maxVal);
|
||
sumExp += Z[i,k];
|
||
end;
|
||
|
||
if sumExp <= 0 then
|
||
begin
|
||
// численно вырожденный случай → равномерное распределение
|
||
for var k := 0 to fClassCount - 1 do
|
||
Z[i,k] := 1.0 / fClassCount;
|
||
end
|
||
else
|
||
begin
|
||
for var k := 0 to fClassCount - 1 do
|
||
Z[i,k] /= sumExp;
|
||
end;
|
||
|
||
var yi := yEncoded[i];
|
||
var prob := Z[i, yi];
|
||
|
||
// защита от log(0)
|
||
if prob < 1e-300 then
|
||
prob := 1e-300;
|
||
|
||
loss -= Ln(prob);
|
||
end;
|
||
|
||
loss /= m;
|
||
|
||
// --- L2 penalty
|
||
if fLambda <> 0 then
|
||
begin
|
||
var l2 := 0.0;
|
||
for var j := 0 to p - 1 do
|
||
for var k := 0 to fClassCount - 1 do
|
||
l2 += fW[j,k] * fW[j,k];
|
||
|
||
loss += 0.5 * fLambda * l2;
|
||
end;
|
||
|
||
// --- divergence check
|
||
if double.IsNaN(loss) or double.IsInfinity(loss) then
|
||
ArgumentError(ER_LOGISTIC_INVALID_LOSS);
|
||
|
||
// --- convergence check
|
||
if fCheckConvergence then
|
||
begin
|
||
if Abs(prevLoss - loss) < Max(fTol, fMinImprovement) then
|
||
break;
|
||
|
||
prevLoss := loss;
|
||
end;
|
||
|
||
// --- gradient
|
||
var gradW := new Matrix(p, fClassCount);
|
||
var gradB := new Vector(fClassCount);
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
var yi := yEncoded[i];
|
||
|
||
// заранее считаем diff[k]
|
||
var diffArr := new real[fClassCount];
|
||
|
||
for var k := 0 to fClassCount - 1 do
|
||
begin
|
||
var diff := Z[i,k];
|
||
if k = yi then
|
||
diff -= 1.0;
|
||
|
||
diffArr[k] := diff;
|
||
gradB[k] += diff;
|
||
end;
|
||
|
||
// теперь идём по j (строка X читается линейно)
|
||
for var j := 0 to p - 1 do
|
||
begin
|
||
var xij := X[i,j];
|
||
|
||
for var k := 0 to fClassCount - 1 do
|
||
gradW[j,k] += xij * diffArr[k];
|
||
end;
|
||
end;
|
||
|
||
|
||
gradW *= 1.0 / m;
|
||
gradB *= 1.0 / m;
|
||
|
||
if fLambda <> 0 then
|
||
gradW += fLambda * fW;
|
||
|
||
fW -= fLearningRate * gradW;
|
||
fIntercept -= fLearningRate * gradB;
|
||
end;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function LogisticRegression.PredictProba(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fW.RowCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fW.RowCount);
|
||
|
||
var m := X.RowCount;
|
||
|
||
var Z := X * fW;
|
||
|
||
for var i := 0 to m - 1 do
|
||
for var k := 0 to fClassCount - 1 do
|
||
Z[i,k] += fIntercept[k];
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
var maxVal := Z.RowMax(i);
|
||
|
||
var sumExp := 0.0;
|
||
|
||
for var k := 0 to fClassCount - 1 do
|
||
begin
|
||
Z[i,k] := Exp(Z[i,k] - maxVal);
|
||
sumExp += Z[i,k];
|
||
end;
|
||
|
||
for var k := 0 to fClassCount - 1 do
|
||
Z[i,k] /= sumExp;
|
||
end;
|
||
|
||
Result := Z;
|
||
end;
|
||
|
||
function LogisticRegression.GetClasses: array of real;
|
||
begin
|
||
SetLength(Result, fClassCount);
|
||
for var i := 0 to fClassCount - 1 do
|
||
Result[i] := fIndexToClass[i];
|
||
end;
|
||
|
||
function LogisticRegression.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
var P := PredictProba(X);
|
||
|
||
var m := P.RowCount;
|
||
Result := new Vector(m);
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
var internalIdx := P.RowArgMax(i);
|
||
Result[i] := fIndexToClass[internalIdx];
|
||
end;
|
||
end;
|
||
|
||
function LogisticRegression.PredictLabels(X: Matrix): array of integer;
|
||
begin
|
||
var v := Predict(X);
|
||
|
||
Result := new integer[v.Length];
|
||
|
||
for var i := 0 to v.Length - 1 do
|
||
Result[i] := Round(v[i]);
|
||
end;
|
||
|
||
function LogisticRegression.ToString: string;
|
||
begin
|
||
Result :=
|
||
'LogisticRegression(lambda=' + fLambda +
|
||
', lr=' + fLearningRate +
|
||
', epochs=' + fEpochs + ')';
|
||
end;
|
||
|
||
function LogisticRegression.Clone: IModel;
|
||
begin
|
||
var m := new LogisticRegression(
|
||
fLambda,
|
||
fLearningRate,
|
||
fEpochs
|
||
);
|
||
|
||
m.fFitted := fFitted;
|
||
m.fClassCount := fClassCount;
|
||
|
||
if fW <> nil then
|
||
m.fW := fW.Clone;
|
||
|
||
if fIntercept <> nil then
|
||
m.fIntercept := fIntercept.Clone;
|
||
|
||
if fClassToIndex <> nil then
|
||
begin
|
||
m.fClassToIndex := new Dictionary<integer, integer>;
|
||
foreach var kv in fClassToIndex do
|
||
m.fClassToIndex[kv.Key] := kv.Value;
|
||
end;
|
||
|
||
if fIndexToClass <> nil then
|
||
begin
|
||
SetLength(m.fIndexToClass, Length(fIndexToClass));
|
||
for var i := 0 to Length(fIndexToClass) - 1 do
|
||
m.fIndexToClass[i] := fIndexToClass[i];
|
||
end;
|
||
|
||
Result := m;
|
||
end;
|
||
|
||
function LogisticRegression.GetWeights: Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
Result := fW;
|
||
end;
|
||
|
||
function LogisticRegression.GetIntercept: Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
Result := fIntercept;
|
||
end;
|
||
|
||
procedure LogisticRegression.SetClassLabels(classes: array of string);
|
||
begin
|
||
fClassLabels := Copy(classes);
|
||
end;
|
||
|
||
function LogisticRegression.GetClassLabels: array of string;
|
||
begin
|
||
if fClassLabels = nil then
|
||
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
|
||
|
||
Result := fClassLabels;
|
||
end;
|
||
|
||
function GiniCriterion.Impurity(y: Vector; indices: array of integer): real;
|
||
begin
|
||
var n := indices.Length;
|
||
if n = 0 then
|
||
exit(0.0);
|
||
|
||
var counts := new integer[fClassCount];
|
||
|
||
foreach var idx in indices do
|
||
begin
|
||
var cls := Round(y[idx]);
|
||
|
||
if (cls < 0) or (cls >= fClassCount) then
|
||
ArgumentError(ER_LABEL_INDEX_INVALID);
|
||
|
||
counts[cls] += 1;
|
||
end;
|
||
|
||
var sumsq := 0.0;
|
||
|
||
for var c := 0 to fClassCount - 1 do
|
||
begin
|
||
if counts[c] > 0 then
|
||
begin
|
||
var p := counts[c] / n;
|
||
sumsq += p * p;
|
||
end;
|
||
end;
|
||
|
||
var g := 1.0 - sumsq;
|
||
|
||
if g < 0 then
|
||
g := 0.0; // защита от отрицательных из-за округления
|
||
|
||
Result := g;
|
||
end;
|
||
|
||
function VarianceCriterion.Impurity(y: Vector; indices: array of integer): real;
|
||
begin
|
||
var n := indices.Length;
|
||
if n = 0 then
|
||
exit(0.0);
|
||
|
||
var sum := 0.0;
|
||
var sumsq := 0.0;
|
||
|
||
foreach var idx in indices do
|
||
begin
|
||
var v := y[idx];
|
||
sum += v;
|
||
sumsq += v * v;
|
||
end;
|
||
|
||
var mean := sum / n;
|
||
var varValue := (sumsq / n) - mean * mean;
|
||
|
||
if varValue < 0 then
|
||
varValue := 0.0; // защита от FP-ошибок
|
||
|
||
Result := varValue;
|
||
end;
|
||
|
||
constructor EntropyCriterion.Create(classCount: integer);
|
||
begin
|
||
fclassCount := classCount;
|
||
end;
|
||
|
||
function EntropyCriterion.Impurity(y: Vector; indices: array of integer): real;
|
||
begin
|
||
var counts := new integer[fClassCount];
|
||
|
||
foreach var i in indices do
|
||
counts[Round(y[i])] += 1;
|
||
|
||
var total := indices.Length;
|
||
if total = 0 then exit(0.0);
|
||
|
||
var res := 0.0;
|
||
|
||
foreach var c in counts do
|
||
if c > 0 then
|
||
begin
|
||
var p := c / total;
|
||
res -= p * Ln(p);
|
||
end;
|
||
|
||
Result := res;
|
||
end;
|
||
|
||
|
||
function LeafClass(c: integer): DecisionTreeNode;
|
||
begin
|
||
var n := new DecisionTreeNode;
|
||
n.IsLeaf := true;
|
||
n.LeafValue := c;
|
||
Result := n;
|
||
end;
|
||
|
||
function LeafValue(v: real): DecisionTreeNode;
|
||
begin
|
||
var n := new DecisionTreeNode;
|
||
n.IsLeaf := true;
|
||
n.LeafValue := v;
|
||
Result := n;
|
||
end;
|
||
|
||
function SplitNode(feature: integer; threshold: real;
|
||
leftNode, rightNode: DecisionTreeNode): DecisionTreeNode;
|
||
begin
|
||
var n := new DecisionTreeNode;
|
||
n.IsLeaf := false;
|
||
n.FeatureIndex := feature;
|
||
n.Threshold := threshold;
|
||
n.Left := leftNode;
|
||
n.Right := rightNode;
|
||
Result := n;
|
||
end;
|
||
|
||
function DecisionTreeNode.Clone: DecisionTreeNode;
|
||
begin
|
||
var n := new DecisionTreeNode;
|
||
|
||
n.IsLeaf := IsLeaf;
|
||
n.FeatureIndex := FeatureIndex;
|
||
n.Threshold := Threshold;
|
||
n.LeafValue := LeafValue;
|
||
|
||
if Left <> nil then
|
||
n.Left := Left.Clone;
|
||
|
||
if Right <> nil then
|
||
n.Right := Right.Clone;
|
||
|
||
Result := n;
|
||
end;
|
||
|
||
const MAX_ALLOWED_TREE_DEPTH = 1000;
|
||
|
||
// DecisionTreeCore
|
||
|
||
constructor DecisionTreeCore.Create(
|
||
maxDepth: integer;
|
||
minSamplesSplit: integer;
|
||
minSamplesLeaf: integer;
|
||
criterion: ISplitCriterion;
|
||
maxFeatures: integer;
|
||
seed: integer
|
||
);
|
||
begin
|
||
fMaxDepth := maxDepth;
|
||
fMinSamplesSplit := minSamplesSplit;
|
||
fMinSamplesLeaf := minSamplesLeaf;
|
||
fCriterion := criterion;
|
||
fMaxFeatures := maxFeatures;
|
||
|
||
// --- seed (в твоём стиле)
|
||
if seed < 0 then
|
||
begin
|
||
fUserProvidedSeed := false;
|
||
fRandomSeed := System.Environment.TickCount and integer.MaxValue;
|
||
end
|
||
else
|
||
begin
|
||
fUserProvidedSeed := true;
|
||
fRandomSeed := seed;
|
||
end;
|
||
|
||
fRng := new System.Random(fRandomSeed);
|
||
end;
|
||
|
||
procedure DecisionTreeCore.Fit(X: Matrix; y: Vector);
|
||
begin
|
||
// --- init importance
|
||
fFeatureImportances := new Vector(X.ColCount);
|
||
|
||
// --- определить число классов (0..K-1)
|
||
fClassCount := 0;
|
||
|
||
for var i := 0 to y.Length - 1 do
|
||
begin
|
||
var v := Round(y[i]);
|
||
if v + 1 > fClassCount then
|
||
fClassCount := v + 1;
|
||
end;
|
||
|
||
// --- build tree
|
||
var indices := new integer[X.RowCount];
|
||
for var i := 0 to X.RowCount - 1 do
|
||
indices[i] := i;
|
||
|
||
fRoot := BuildNode(X, y, indices, 0);
|
||
|
||
// --- нормализация importance
|
||
var s := fFeatureImportances.Sum;
|
||
if s > 0 then
|
||
for var i := 0 to fFeatureImportances.Length - 1 do
|
||
fFeatureImportances[i] /= s;
|
||
end;
|
||
|
||
function DecisionTreeCore.Predict(X: Matrix): Vector;
|
||
begin
|
||
Result := new Vector(X.RowCount);
|
||
|
||
for var i := 0 to X.RowCount - 1 do
|
||
Result[i] := PredictOne(X.GetRow(i), fRoot);
|
||
end;
|
||
|
||
function DecisionTreeCore.PredictRow(X: Matrix; row: integer): integer;
|
||
var node: DecisionTreeNode;
|
||
begin
|
||
node := fRoot;
|
||
|
||
while not node.IsLeaf do
|
||
begin
|
||
if X[row, node.FeatureIndex] <= node.Threshold then
|
||
node := node.Left
|
||
else
|
||
node := node.Right;
|
||
end;
|
||
|
||
Result := Round(node.LeafValue);
|
||
end;
|
||
|
||
function DecisionTreeCore.PredictOne(x: Vector; node: DecisionTreeNode): integer;
|
||
begin
|
||
if node.IsLeaf then
|
||
exit(Round(node.LeafValue));
|
||
|
||
if x[node.FeatureIndex] <= node.Threshold then
|
||
Result := PredictOne(x, node.Left)
|
||
else
|
||
Result := PredictOne(x, node.Right);
|
||
end;
|
||
|
||
function DecisionTreeCore.MajorityClass(y: Vector; indices: array of integer): integer;
|
||
begin
|
||
var counts := new integer[fClassCount];
|
||
|
||
for var i := 0 to indices.Length - 1 do
|
||
begin
|
||
var cls := Round(y[indices[i]]);
|
||
counts[cls] += 1;
|
||
end;
|
||
|
||
var best := 0;
|
||
var bestCnt := counts[0];
|
||
|
||
for var k := 1 to fClassCount - 1 do
|
||
if counts[k] > bestCnt then
|
||
begin
|
||
bestCnt := counts[k];
|
||
best := k;
|
||
end;
|
||
|
||
Result := best;
|
||
end;
|
||
|
||
function DecisionTreeCore.CreateLeaf(y: Vector; indices: array of integer): DecisionTreeNode;
|
||
begin
|
||
Result := new DecisionTreeNode;
|
||
Result.IsLeaf := true;
|
||
Result.LeafValue := MajorityClass(y, indices);
|
||
end;
|
||
|
||
function DecisionTreeCore.BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode;
|
||
begin
|
||
// 1. stop: depth
|
||
if depth >= fMaxDepth then
|
||
exit(CreateLeaf(y, indices));
|
||
|
||
// 2. stop: min samples
|
||
if indices.Length < fMinSamplesSplit then
|
||
exit(CreateLeaf(y, indices));
|
||
|
||
// --- impurity родителя
|
||
var parentImp := fCriterion.Impurity(y, indices);
|
||
|
||
// 3. лучший сплит (пока используем старый API)
|
||
var bestFeature: integer;
|
||
var bestThreshold: real;
|
||
|
||
if not FindBestSplit(X, y, indices, bestFeature, bestThreshold) then
|
||
exit(CreateLeaf(y, indices));
|
||
|
||
// 4. split → только индексы
|
||
var leftList := new List<integer>;
|
||
var rightList := new List<integer>;
|
||
|
||
for var ii := 0 to indices.Length - 1 do
|
||
begin
|
||
var i := indices[ii];
|
||
|
||
if X[i, bestFeature] <= bestThreshold then
|
||
leftList.Add(i)
|
||
else
|
||
rightList.Add(i);
|
||
end;
|
||
|
||
if (leftList.Count < fMinSamplesLeaf) or (rightList.Count < fMinSamplesLeaf) then
|
||
exit(CreateLeaf(y, indices));
|
||
|
||
var leftIdx := leftList.ToArray;
|
||
var rightIdx := rightList.ToArray;
|
||
|
||
// --- impurity детей
|
||
var leftImp := fCriterion.Impurity(y, leftIdx);
|
||
var rightImp := fCriterion.Impurity(y, rightIdx);
|
||
|
||
var weighted :=
|
||
(leftIdx.Length * leftImp + rightIdx.Length * rightImp) / indices.Length;
|
||
|
||
var gain := parentImp - weighted;
|
||
|
||
// --- FEATURE IMPORTANCE
|
||
if gain > 0 then
|
||
fFeatureImportances[bestFeature] += gain;
|
||
|
||
// 5. recursion
|
||
var left := BuildNode(X, y, leftIdx, depth + 1);
|
||
var right := BuildNode(X, y, rightIdx, depth + 1);
|
||
|
||
// 6. node
|
||
Result := new DecisionTreeNode;
|
||
Result.IsLeaf := false;
|
||
Result.FeatureIndex := bestFeature;
|
||
Result.Threshold := bestThreshold;
|
||
Result.Left := left;
|
||
Result.Right := right;
|
||
end;
|
||
|
||
procedure DecisionTreeCore.Split(X: Matrix; y: Vector; f: integer; t: real;
|
||
var Xl: Matrix; var yl: Vector;
|
||
var Xr: Matrix; var yr: Vector
|
||
);
|
||
begin
|
||
var leftIdx := new List<integer>;
|
||
var rightIdx := new List<integer>;
|
||
|
||
// 1. Разделяем индексы
|
||
for var i := 0 to X.RowCount - 1 do
|
||
if X[i, f] <= t then
|
||
leftIdx.Add(i)
|
||
else
|
||
rightIdx.Add(i);
|
||
|
||
// 2. Формируем подматрицы и подвекторы
|
||
var leftArr := leftIdx.ToArray;
|
||
var rightArr := rightIdx.ToArray;
|
||
|
||
Xl := X.TakeRows(leftArr);
|
||
yl := y.SubvectorBy(leftArr);
|
||
|
||
Xr := X.TakeRows(rightArr);
|
||
yr := y.SubvectorBy(rightArr);
|
||
end;
|
||
|
||
function DecisionTreeCore.FindBestSplit(
|
||
X: Matrix;
|
||
y: Vector;
|
||
indices: array of integer;
|
||
var bestF: integer;
|
||
var bestT: real
|
||
): boolean;
|
||
begin
|
||
Result := false;
|
||
|
||
var n := indices.Length;
|
||
if n = 0 then exit;
|
||
|
||
var parentImp := fCriterion.Impurity(y, indices);
|
||
|
||
var bestScore := parentImp;
|
||
|
||
// --- выбор признаков (с учётом maxFeatures)
|
||
var featureCount := X.ColCount;
|
||
var features: array of integer;
|
||
|
||
if (fMaxFeatures > 0) and (fMaxFeatures < featureCount) then
|
||
begin
|
||
var perm := new integer[featureCount];
|
||
for var i := 0 to featureCount - 1 do
|
||
perm[i] := i;
|
||
|
||
for var i := 0 to fMaxFeatures - 1 do
|
||
begin
|
||
var j := i + fRng.Next(featureCount - i);
|
||
var tmp := perm[i];
|
||
perm[i] := perm[j];
|
||
perm[j] := tmp;
|
||
end;
|
||
|
||
SetLength(features, fMaxFeatures);
|
||
for var i := 0 to fMaxFeatures - 1 do
|
||
features[i] := perm[i];
|
||
end
|
||
else
|
||
begin
|
||
SetLength(features, featureCount);
|
||
for var i := 0 to featureCount - 1 do
|
||
features[i] := i;
|
||
end;
|
||
|
||
// --- перебор
|
||
for var fi := 0 to features.Length - 1 do
|
||
begin
|
||
var f := features[fi];
|
||
|
||
var leftBuf := new integer[n];
|
||
var rightBuf := new integer[n];
|
||
|
||
for var ii := 0 to n - 1 do
|
||
begin
|
||
var i := indices[ii];
|
||
var t := X[i, f];
|
||
|
||
var lcnt := 0;
|
||
var rcnt := 0;
|
||
|
||
for var jj := 0 to n - 1 do
|
||
begin
|
||
var j := indices[jj];
|
||
|
||
if X[j, f] <= t then
|
||
begin
|
||
leftBuf[lcnt] := j;
|
||
lcnt += 1;
|
||
end
|
||
else
|
||
begin
|
||
rightBuf[rcnt] := j;
|
||
rcnt += 1;
|
||
end;
|
||
end;
|
||
|
||
if (lcnt < fMinSamplesLeaf) or (rcnt < fMinSamplesLeaf) then
|
||
continue;
|
||
|
||
// создаём массивы нужного размера
|
||
var leftIdx := new integer[lcnt];
|
||
var rightIdx := new integer[rcnt];
|
||
|
||
for var i1 := 0 to lcnt - 1 do
|
||
leftIdx[i1] := leftBuf[i1];
|
||
|
||
for var i1 := 0 to rcnt - 1 do
|
||
rightIdx[i1] := rightBuf[i1];
|
||
|
||
var leftImp := fCriterion.Impurity(y, leftIdx);
|
||
var rightImp := fCriterion.Impurity(y, rightIdx);
|
||
|
||
var score :=
|
||
(leftIdx.Length * leftImp + rightIdx.Length * rightImp) / n;
|
||
|
||
if score < bestScore then
|
||
begin
|
||
bestScore := score;
|
||
bestF := f;
|
||
bestT := t;
|
||
Result := true;
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function DecisionTreeCore.Clone: DecisionTreeCore;
|
||
begin
|
||
Result := new DecisionTreeCore(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fCriterion,
|
||
fMaxFeatures,
|
||
fRandomSeed
|
||
);
|
||
|
||
// копируем дерево
|
||
if fRoot <> nil then
|
||
Result.fRoot := fRoot.Clone;
|
||
end;
|
||
|
||
// DecisionTreeBase
|
||
|
||
constructor DecisionTreeRegressorBase.Create(
|
||
maxDepth: integer;
|
||
minSamplesSplit: integer;
|
||
minSamplesLeaf: integer;
|
||
criterion: ISplitCriterion;
|
||
seed: integer);
|
||
begin
|
||
if maxDepth < -1 then
|
||
ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, maxDepth);
|
||
|
||
if maxDepth > MAX_ALLOWED_TREE_DEPTH then
|
||
ArgumentOutOfRangeError(ER_MAX_DEPTH_TOO_LARGE, maxDepth);
|
||
|
||
if minSamplesSplit < 2 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_SPLIT_INVALID, minSamplesSplit);
|
||
|
||
if minSamplesLeaf < 1 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
|
||
|
||
if minSamplesLeaf >= minSamplesSplit then
|
||
ArgumentOutOfRangeError(
|
||
ER_MIN_LEAF_GE_SPLIT,
|
||
minSamplesLeaf, minSamplesSplit
|
||
);
|
||
|
||
// --- parameters valid → assign
|
||
|
||
fMaxDepth := maxDepth;
|
||
fMinSamplesSplit := minSamplesSplit;
|
||
fMinSamplesLeaf := minSamplesLeaf;
|
||
fCriterion := criterion;
|
||
|
||
if seed < 0 then
|
||
begin
|
||
fUserProvidedSeed := false;
|
||
fRandomSeed := System.Environment.TickCount and integer.MaxValue;
|
||
end
|
||
else
|
||
begin
|
||
fUserProvidedSeed := true;
|
||
fRandomSeed := seed;
|
||
end;
|
||
|
||
fRng := new System.Random(fRandomSeed);
|
||
end;
|
||
|
||
procedure DecisionTreeRegressorBase.CopyBaseState(dest: DecisionTreeRegressorBase);
|
||
begin
|
||
dest.fMaxDepth := fMaxDepth;
|
||
dest.fMinSamplesSplit := fMinSamplesSplit;
|
||
dest.fMinSamplesLeaf := fMinSamplesLeaf;
|
||
dest.fFitted := fFitted;
|
||
dest.fRandomSeed := fRandomSeed;
|
||
dest.fMaxFeatures := fMaxFeatures;
|
||
|
||
dest.fUserProvidedSeed := fUserProvidedSeed;
|
||
|
||
if fUserProvidedSeed then
|
||
dest.fRng := new System.Random(fRandomSeed)
|
||
else
|
||
dest.fRng := new System.Random;
|
||
|
||
// MUST be stateless
|
||
if fCriterion <> nil then
|
||
dest.fCriterion := fCriterion;
|
||
|
||
if fFeatureImportances <> nil then
|
||
dest.fFeatureImportances := fFeatureImportances.Clone;
|
||
|
||
if fRoot <> nil then
|
||
dest.fRoot := fRoot.Clone;
|
||
|
||
if fRowIndices <> nil then
|
||
dest.fRowIndices := Copy(fRowIndices);
|
||
end;
|
||
|
||
function DecisionTreeRegressorBase.GetFeatureSubset(nFeatures: integer): array of integer;
|
||
begin
|
||
if (fMaxFeatures = 0) or (fMaxFeatures >= nFeatures) then
|
||
begin
|
||
Result := new integer[nFeatures];
|
||
for var i := 0 to nFeatures-1 do
|
||
Result[i] := i;
|
||
exit;
|
||
end;
|
||
|
||
var all := new List<integer>;
|
||
for var i := 0 to nFeatures-1 do
|
||
all.Add(i);
|
||
|
||
var subset := new integer[fMaxFeatures];
|
||
|
||
for var k := 0 to fMaxFeatures-1 do
|
||
begin
|
||
var idx := fRng.Next(all.Count);
|
||
subset[k] := all[idx];
|
||
all.RemoveAt(idx);
|
||
end;
|
||
|
||
Result := subset;
|
||
end;
|
||
|
||
procedure DecisionTreeRegressorBase.SetRowIndices(rows: array of integer);
|
||
begin
|
||
if Length(rows) = 0 then
|
||
ArgumentError('Row subset cannot be empty!!Row subset cannot be empty');
|
||
|
||
fRowIndices := Copy(rows);
|
||
end;
|
||
|
||
function DecisionTreeRegressorBase.FeatureImportances: Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if fFeatureImportances = nil then
|
||
exit(new Vector(0));
|
||
|
||
Result := fFeatureImportances.Clone;
|
||
end;
|
||
|
||
function DecisionTreeRegressorBase.LeafNode(value: real): DecisionTreeNode;
|
||
begin
|
||
var n := new DecisionTreeNode;
|
||
n.IsLeaf := true;
|
||
n.LeafValue := value;
|
||
Result := n;
|
||
end;
|
||
|
||
function DecisionTreeRegressorBase.BuildTree(X: Matrix; y: Vector;
|
||
indices: array of integer; depth: integer): DecisionTreeNode;
|
||
begin
|
||
if (fMaxDepth >= 0) and (depth >= fMaxDepth) then
|
||
exit(LeafNode(LeafValue(y, indices)));
|
||
|
||
if indices.Length < fMinSamplesSplit then
|
||
exit(LeafNode(LeafValue(y, indices)));
|
||
|
||
if IsPure(y, indices) then
|
||
exit(LeafNode(LeafValue(y, indices)));
|
||
|
||
var parentImp := fCriterion.Impurity(y, indices);
|
||
|
||
if double.IsNaN(parentImp) or double.IsInfinity(parentImp) then
|
||
exit(LeafNode(LeafValue(y, indices)));
|
||
|
||
var split := FindBestSplit(X, y, indices);
|
||
|
||
if not split.Found then
|
||
exit(LeafNode(LeafValue(y, indices)));
|
||
|
||
var left := new List<integer>;
|
||
var right := new List<integer>;
|
||
|
||
foreach var i in indices do
|
||
if X[i, split.Feature] <= split.Threshold then
|
||
left.Add(i)
|
||
else
|
||
right.Add(i);
|
||
|
||
if (left.Count < fMinSamplesLeaf) or
|
||
(right.Count < fMinSamplesLeaf) then
|
||
exit(LeafNode(LeafValue(y, indices)));
|
||
|
||
var leftArr := left.ToArray;
|
||
var rightArr := right.ToArray;
|
||
|
||
var leftImp := fCriterion.Impurity(y, leftArr);
|
||
var rightImp := fCriterion.Impurity(y, rightArr);
|
||
|
||
var n := indices.Length;
|
||
|
||
var weighted :=
|
||
(real(leftArr.Length) / n) * leftImp +
|
||
(real(rightArr.Length) / n) * rightImp;
|
||
|
||
var delta := parentImp - weighted;
|
||
|
||
if double.IsNaN(delta) or double.IsInfinity(delta) then
|
||
exit(LeafNode(LeafValue(y, indices)));
|
||
|
||
if delta < 0 then
|
||
delta := 0.0;
|
||
|
||
// КЛЮЧЕВОЙ PRODUCTION-ФИЛЬТР
|
||
if delta <= 0 then
|
||
exit(LeafNode(LeafValue(y, indices)));
|
||
|
||
fFeatureImportances[split.Feature] += delta;
|
||
|
||
var leftNode := BuildTree(X, y, leftArr, depth + 1);
|
||
var rightNode := BuildTree(X, y, rightArr, depth + 1);
|
||
|
||
var node := new DecisionTreeNode;
|
||
node.IsLeaf := false;
|
||
node.FeatureIndex := split.Feature;
|
||
node.Threshold := split.Threshold;
|
||
node.Left := leftNode;
|
||
node.Right := rightNode;
|
||
|
||
Result := node;
|
||
end;
|
||
|
||
const EPS = 1e-12;
|
||
|
||
function DecisionTreeRegressorBase.IsPure(y: Vector; indices: array of integer): boolean;
|
||
begin
|
||
Result := fCriterion.Impurity(y, indices) < EPS;
|
||
end;
|
||
|
||
function DecisionTreeRegressor.FindBestSplitReg(X: Matrix; y: Vector; indices: array of integer): SplitResult;
|
||
begin
|
||
var bestScore := real.PositiveInfinity;
|
||
var bestFeature := -1;
|
||
var bestThreshold := 0.0;
|
||
|
||
var n := indices.Length;
|
||
if n < 2 then
|
||
begin
|
||
Result.Found := false;
|
||
exit;
|
||
end;
|
||
|
||
// --- parent sums
|
||
var sumAll := 0.0;
|
||
var sumSqAll := 0.0;
|
||
|
||
for var i := 0 to n-1 do
|
||
begin
|
||
var v := y[indices[i]];
|
||
sumAll += v;
|
||
sumSqAll += v*v;
|
||
end;
|
||
|
||
var ProcessFeature: integer -> () := j ->
|
||
begin
|
||
var pairs: array of (real, integer);
|
||
SetLength(pairs, n);
|
||
|
||
for var i := 0 to n-1 do
|
||
begin
|
||
var idx := indices[i];
|
||
pairs[i] := (X[idx,j], idx);
|
||
end;
|
||
|
||
pairs.Sort(p -> p.Item1);
|
||
|
||
var leftCount := 0;
|
||
var leftSum := 0.0;
|
||
var leftSumSq := 0.0;
|
||
|
||
for var i := 1 to n-1 do
|
||
begin
|
||
var v := y[pairs[i-1].Item2];
|
||
|
||
leftCount += 1;
|
||
leftSum += v;
|
||
leftSumSq += v*v;
|
||
|
||
var rightCount := n - leftCount;
|
||
|
||
if (leftCount < fMinSamplesLeaf) or
|
||
(rightCount < fMinSamplesLeaf) then
|
||
continue;
|
||
|
||
var x1 := pairs[i-1].Item1;
|
||
var x2 := pairs[i].Item1;
|
||
|
||
if x1 = x2 then
|
||
continue;
|
||
|
||
var rightSum := sumAll - leftSum;
|
||
var rightSumSq := sumSqAll - leftSumSq;
|
||
|
||
var leftMean := leftSum / leftCount;
|
||
var rightMean := rightSum / rightCount;
|
||
|
||
var leftVar := (leftSumSq / leftCount) - leftMean*leftMean;
|
||
var rightVar := (rightSumSq / rightCount) - rightMean*rightMean;
|
||
|
||
if leftVar < 0 then leftVar := 0.0;
|
||
if rightVar < 0 then rightVar := 0.0;
|
||
|
||
var weighted :=
|
||
(real(leftCount) / n) * leftVar +
|
||
(real(rightCount) / n) * rightVar;
|
||
|
||
if double.IsNaN(weighted) or double.IsInfinity(weighted) then
|
||
continue;
|
||
|
||
if weighted < bestScore then
|
||
begin
|
||
bestScore := weighted;
|
||
bestFeature := j;
|
||
bestThreshold := (x1 + x2) * 0.5;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
var p := X.ColCount;
|
||
|
||
if (fMaxFeatures <= 0) or (fMaxFeatures >= p) then
|
||
begin
|
||
for var j := 0 to p-1 do
|
||
ProcessFeature(j);
|
||
end
|
||
else
|
||
begin
|
||
var feat := new integer[p];
|
||
for var i := 0 to p-1 do
|
||
feat[i] := i;
|
||
|
||
for var i := 0 to fMaxFeatures-1 do
|
||
begin
|
||
var r := i + fRng.Next(p - i);
|
||
var tmp := feat[i];
|
||
feat[i] := feat[r];
|
||
feat[r] := tmp;
|
||
end;
|
||
|
||
for var k := 0 to fMaxFeatures-1 do
|
||
ProcessFeature(feat[k]);
|
||
end;
|
||
|
||
Result.Found := bestFeature <> -1;
|
||
Result.Feature := bestFeature;
|
||
Result.Threshold := bestThreshold;
|
||
end;
|
||
|
||
//==============================
|
||
// DecisionTreeClassifier
|
||
//==============================
|
||
|
||
constructor DecisionTreeClassifier.Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; criterion: ISplitCriterion; seed: integer);
|
||
begin
|
||
if maxDepth = 0 then
|
||
ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, maxDepth);
|
||
|
||
if minSamplesSplit < 2 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_SPLIT_INVALID, minSamplesSplit);
|
||
|
||
if minSamplesLeaf < 1 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
|
||
|
||
if minSamplesLeaf >= minSamplesSplit then
|
||
ArgumentOutOfRangeError(ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit);
|
||
|
||
fMaxDepth := maxDepth;
|
||
fMinSamplesSplit := minSamplesSplit;
|
||
fMinSamplesLeaf := minSamplesLeaf;
|
||
fCriterion := if criterion = nil then new GiniCriterion else criterion;
|
||
|
||
if seed < 0 then
|
||
begin
|
||
fUserProvidedSeed := false;
|
||
fRandomSeed := System.Environment.TickCount and integer.MaxValue;
|
||
end
|
||
else
|
||
begin
|
||
fUserProvidedSeed := true;
|
||
fRandomSeed := seed;
|
||
end;
|
||
|
||
fRng := new System.Random(fRandomSeed);
|
||
end;
|
||
|
||
function DecisionTreeClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
fFeatureImportances := new Vector(X.ColCount);
|
||
|
||
// --- convert to integer labels
|
||
var m := y.Length;
|
||
var yInt := new integer[m];
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
var r := y[i];
|
||
var ir := Round(r);
|
||
|
||
if Abs(r - ir) > 1e-12 then
|
||
ArgumentError(ER_LABELS_NOT_INTEGER);
|
||
|
||
yInt[i] := ir;
|
||
end;
|
||
|
||
// --- encode
|
||
var classes: array of integer;
|
||
var yEncArr := EncodeLabelsInt(yInt, classes);
|
||
|
||
if classes.Length < 2 then
|
||
ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES);
|
||
|
||
fIndexToClass := classes;
|
||
|
||
// --- criterion
|
||
if fCriterion = nil then
|
||
fCriterion := new GiniCriterion(classes.Length);
|
||
|
||
// --- encoded vector
|
||
var yEncoded := new Vector(m);
|
||
for var i := 0 to m - 1 do
|
||
yEncoded[i] := yEncArr[i];
|
||
|
||
// --- Core
|
||
fCore := new DecisionTreeCore(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fCriterion,
|
||
fMaxFeatures,
|
||
fRandomSeed
|
||
);
|
||
|
||
fCore.Fit(X, yEncoded);
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function DecisionTreeClassifier.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureImportances.Length then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureImportances.Length);
|
||
|
||
var predIdx := fCore.Predict(X);
|
||
|
||
var n := predIdx.Length;
|
||
Result := new Vector(n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var k := Round(predIdx[i]);
|
||
Result[i] := fIndexToClass[k];
|
||
end;
|
||
end;
|
||
|
||
function DecisionTreeClassifier.PredictLabels(X: Matrix): array of integer;
|
||
begin
|
||
var v := Predict(X);
|
||
|
||
Result := new integer[v.Length];
|
||
|
||
for var i := 0 to v.Length - 1 do
|
||
Result[i] := Round(v[i]);
|
||
end;
|
||
|
||
function DecisionTreeClassifier.Clone: IModel;
|
||
begin
|
||
var m := new DecisionTreeClassifier(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fCriterion,
|
||
fRandomSeed
|
||
);
|
||
|
||
m.fMaxDepth := fMaxDepth;
|
||
m.fMinSamplesSplit := fMinSamplesSplit;
|
||
m.fMinSamplesLeaf := fMinSamplesLeaf;
|
||
m.fFitted := fFitted;
|
||
m.fRandomSeed := fRandomSeed;
|
||
m.fMaxFeatures := fMaxFeatures;
|
||
m.fUserProvidedSeed := fUserProvidedSeed;
|
||
|
||
if fUserProvidedSeed then
|
||
m.fRng := new System.Random(fRandomSeed)
|
||
else
|
||
m.fRng := new System.Random;
|
||
|
||
if fCriterion <> nil then
|
||
m.fCriterion := fCriterion;
|
||
|
||
if fFeatureImportances <> nil then
|
||
m.fFeatureImportances := fFeatureImportances.Clone;
|
||
|
||
// --- classes (единственный источник истины)
|
||
if fIndexToClass <> nil then
|
||
begin
|
||
SetLength(m.fIndexToClass, Length(fIndexToClass));
|
||
for var i := 0 to Length(fIndexToClass) - 1 do
|
||
m.fIndexToClass[i] := fIndexToClass[i];
|
||
end;
|
||
|
||
// --- core (глубокая копия дерева)
|
||
if fCore <> nil then
|
||
begin
|
||
// предполагаем, что Node.Clone уже есть
|
||
m.fCore := new DecisionTreeCore(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fCriterion,
|
||
fMaxFeatures,
|
||
fRandomSeed
|
||
);
|
||
|
||
m.fCore.fRoot := fCore.fRoot.Clone;
|
||
end;
|
||
|
||
Result := m;
|
||
end;
|
||
|
||
function DecisionTreeClassifier.ToString: string;
|
||
begin
|
||
Result :=
|
||
$'DecisionTreeClassifier(' +
|
||
$'maxDepth={fMaxDepth}, ' +
|
||
$'minSamplesSplit={fMinSamplesSplit}, ' +
|
||
$'minSamplesLeaf={fMinSamplesLeaf}' +
|
||
')';
|
||
end;
|
||
|
||
procedure DecisionTreeClassifier.SetClassLabels(classes: array of string);
|
||
begin
|
||
fClassLabels := Copy(classes);
|
||
end;
|
||
|
||
function DecisionTreeClassifier.GetClassLabels: array of string;
|
||
begin
|
||
if fClassLabels = nil then
|
||
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
|
||
|
||
Result := fClassLabels;
|
||
end;
|
||
|
||
// DecisionTreeRegressor
|
||
|
||
constructor DecisionTreeRegressor.Create(
|
||
maxDepth: integer;
|
||
minSamplesSplit: integer;
|
||
minSamplesLeaf: integer;
|
||
leafL2: real;
|
||
seed: integer
|
||
);
|
||
begin
|
||
inherited Create(
|
||
maxDepth,
|
||
minSamplesSplit,
|
||
minSamplesLeaf,
|
||
new VarianceCriterion,
|
||
seed
|
||
);
|
||
|
||
if leafL2 < 0 then
|
||
ArgumentOutOfRangeError(ER_LEAFL2_INVALID, leafL2);
|
||
|
||
fLeafL2 := leafL2;
|
||
end;
|
||
|
||
function DecisionTreeRegressor.LeafValue(y: Vector; indices: array of integer): real;
|
||
begin
|
||
var n := indices.Length;
|
||
|
||
if n = 0 then
|
||
exit(0.0); // безопасный fallback, не должен происходить
|
||
|
||
if fLeafL2 < 0 then
|
||
ArgumentOutOfRangeError(ER_LEAFL2_INVALID, fLeafL2);
|
||
|
||
var sum := 0.0;
|
||
|
||
foreach var idx in indices do
|
||
sum += y[idx];
|
||
|
||
var denom: real;
|
||
|
||
if fLeafL2 > 0 then
|
||
denom := n + fLeafL2
|
||
else
|
||
denom := n;
|
||
|
||
var value := sum / denom;
|
||
|
||
if double.IsNaN(value) or double.IsInfinity(value) then
|
||
value := 0.0; // защита от численного выброса
|
||
|
||
Result := value;
|
||
end;
|
||
|
||
function DecisionTreeRegressor.FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult;
|
||
begin
|
||
Result := FindBestSplitReg(X, y, indices);
|
||
end;
|
||
|
||
function DecisionTreeRegressor.IsPure(y: Vector; indices: array of integer): boolean;
|
||
begin
|
||
var first := y[indices[0]];
|
||
for var i := 1 to indices.Length-1 do
|
||
if y[indices[i]] <> first then
|
||
exit(false);
|
||
Result := true;
|
||
end;
|
||
|
||
function DecisionTreeRegressor.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
if fLeafL2 < 0 then
|
||
ArgumentOutOfRangeError(ER_L2_NEGATIVE, fLeafL2);
|
||
|
||
fFeatureImportances := new Vector(X.ColCount);
|
||
|
||
var indices: array of integer;
|
||
|
||
// 🔹 Ключевое изменение
|
||
if fRowIndices = nil then
|
||
begin
|
||
SetLength(indices, X.RowCount);
|
||
for var i := 0 to X.RowCount - 1 do
|
||
indices[i] := i;
|
||
end
|
||
else
|
||
indices := fRowIndices;
|
||
|
||
fRoot := BuildTree(X, y, indices, 0);
|
||
|
||
var s := fFeatureImportances.Sum;
|
||
if s > 0 then
|
||
for var i := 0 to fFeatureImportances.Length - 1 do
|
||
fFeatureImportances[i] /= s;
|
||
|
||
fFitted := true;
|
||
|
||
fRowIndices := nil;
|
||
|
||
Result := Self;
|
||
end;
|
||
|
||
function DecisionTreeRegressor.PredictOne(X: Matrix; rowIndex: integer): real;
|
||
begin
|
||
var node := fRoot;
|
||
|
||
while not node.IsLeaf do
|
||
begin
|
||
if X[rowIndex, node.FeatureIndex] <= node.Threshold then
|
||
node := node.Left
|
||
else
|
||
node := node.Right;
|
||
end;
|
||
|
||
Result := node.LeafValue; // для регрессии это уже real
|
||
end;
|
||
|
||
function DecisionTreeRegressor.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureImportances.Length then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH,
|
||
X.ColCount,
|
||
fFeatureImportances.Length);
|
||
|
||
var n := X.RowCount;
|
||
Result := new Vector(n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
Result[i] := PredictOne(X, i);
|
||
end;
|
||
|
||
function DecisionTreeRegressor.Clone: IModel;
|
||
begin
|
||
var m := new DecisionTreeRegressor(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fLeafL2,
|
||
fRandomSeed
|
||
);
|
||
|
||
CopyBaseState(m);
|
||
|
||
Result := m;
|
||
end;
|
||
|
||
function DecisionTreeRegressor.ToString: string;
|
||
begin
|
||
var s :=
|
||
$'DecisionTreeRegressor(maxDepth={fMaxDepth}, ' +
|
||
$'minSamplesSplit={fMinSamplesSplit}, ' +
|
||
$'minSamplesLeaf={fMinSamplesLeaf}';
|
||
|
||
if fLeafL2 <> 0.0 then
|
||
s += $', leafL2={fLeafL2}';
|
||
|
||
s += ')';
|
||
|
||
Result := s;
|
||
end;
|
||
|
||
|
||
//-----------------------------
|
||
// RandomForestBase
|
||
//-----------------------------
|
||
|
||
constructor RandomForestBase.Create(
|
||
nTrees: integer;
|
||
maxDepth: integer;
|
||
minSamplesSplit: integer;
|
||
minSamplesLeaf: integer;
|
||
maxFeatures: TMaxFeaturesMode;
|
||
useOOB: boolean;
|
||
seed: integer);
|
||
begin
|
||
if nTrees <= 0 then
|
||
ArgumentOutOfRangeError(ER_NTREES_INVALID, nTrees);
|
||
|
||
if maxDepth < -1 then
|
||
ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, maxDepth);
|
||
|
||
if minSamplesSplit < 2 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_SPLIT_INVALID, minSamplesSplit);
|
||
|
||
if minSamplesLeaf < 1 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
|
||
|
||
if minSamplesLeaf >= minSamplesSplit then
|
||
ArgumentOutOfRangeError(ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit);
|
||
|
||
fNTrees := nTrees;
|
||
fMaxDepth := maxDepth;
|
||
fMinSamplesSplit := minSamplesSplit;
|
||
fMinSamplesLeaf := minSamplesLeaf;
|
||
fMaxFeaturesMode := maxFeatures;
|
||
fFitted := false;
|
||
|
||
fFeatureCount := 0;
|
||
|
||
fUseOOB := useOOB;
|
||
fOOBScore := real.NaN;
|
||
|
||
if seed < 0 then
|
||
begin
|
||
fUserProvidedSeed := false;
|
||
fRandomSeed := System.Environment.TickCount and integer.MaxValue;
|
||
end
|
||
else
|
||
begin
|
||
fUserProvidedSeed := true;
|
||
fRandomSeed := seed;
|
||
end;
|
||
|
||
fRng := new System.Random(fRandomSeed);
|
||
end;
|
||
|
||
function RandomForestBase.OOBScore: real;
|
||
begin
|
||
if not fUseOOB then
|
||
Error(ER_OOB_NOT_ENABLED);
|
||
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
Result := fOOBScore;
|
||
end;
|
||
|
||
function RandomForestBase.ComputeMaxFeatures(p: integer): integer;
|
||
begin
|
||
case fMaxFeaturesMode of
|
||
AllFeatures: Result := p;
|
||
SqrtFeatures: Result := integer(Sqrt(p));
|
||
Log2Features: Result := integer(Log2(p));
|
||
HalfFeatures: Result := p div 2;
|
||
end;
|
||
if Result < 1 then
|
||
Result := 1;
|
||
end;
|
||
|
||
procedure RandomForestBase.BootstrapRowIndices(n: integer; var rows: array of integer);
|
||
begin
|
||
SetLength(rows, n);
|
||
for var i := 0 to n - 1 do
|
||
rows[i] := fRng.Next(n);
|
||
end;
|
||
|
||
//-----------------------------
|
||
// RandomForestRegressor
|
||
//-----------------------------
|
||
constructor RandomForestRegressor.Create(nTrees: integer; maxDepth: integer;
|
||
minSamplesSplit: integer; minSamplesLeaf: integer;
|
||
maxFeaturesMode: TMaxFeaturesMode; computeOOB: boolean; seed: integer);
|
||
begin
|
||
inherited Create(nTrees,maxDepth,minSamplesSplit,minSamplesLeaf,maxFeaturesMode,computeOOB,seed)
|
||
end;
|
||
|
||
function RandomForestRegressor.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
fFeatureCount := p;
|
||
|
||
SetLength(fTrees, fNTrees);
|
||
|
||
// --- OOB buffers (regression) ---
|
||
var oobSum: Vector := nil;
|
||
var oobCnt: array of integer := nil;
|
||
|
||
if fUseOOB then
|
||
begin
|
||
oobSum := new Vector(n);
|
||
oobCnt := new integer[n];
|
||
end;
|
||
|
||
for var t := 0 to fNTrees - 1 do
|
||
begin
|
||
var treeSeed := fRng.Next(integer.MaxValue);
|
||
|
||
var tree := new DecisionTreeRegressor(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
0.0, // leafL2 если нужно — подставь своё поле
|
||
treeSeed
|
||
);
|
||
|
||
var mfeat := ComputeMaxFeatures(p);
|
||
tree.fMaxFeatures := mfeat;
|
||
|
||
var rows: array of integer;
|
||
BootstrapRowIndices(n, rows);
|
||
|
||
tree.SetRowIndices(rows);
|
||
tree.Fit(X, y);
|
||
|
||
// --- OOB accumulate ---
|
||
if fUseOOB then
|
||
begin
|
||
var inBag := new boolean[n];
|
||
for var i := 0 to rows.Length - 1 do
|
||
inBag[rows[i]] := true;
|
||
|
||
for var i := 0 to n - 1 do
|
||
if not inBag[i] then
|
||
begin
|
||
oobSum[i] += tree.PredictOne(X, i);
|
||
oobCnt[i] += 1;
|
||
end;
|
||
end;
|
||
|
||
fTrees[t] := tree;
|
||
end;
|
||
|
||
// --- finalize OOB score: R^2 ---
|
||
fHasOOBScore := false;
|
||
fOOBScore := real.NaN;
|
||
|
||
if fUseOOB then
|
||
begin
|
||
var meanY := y.Mean;
|
||
|
||
var sse := 0.0;
|
||
var sst := 0.0;
|
||
var cnt := 0;
|
||
|
||
for var i := 0 to n - 1 do
|
||
if oobCnt[i] > 0 then
|
||
begin
|
||
var yhat := oobSum[i] / oobCnt[i];
|
||
var e := y[i] - yhat;
|
||
sse += e * e;
|
||
|
||
var d := y[i] - meanY;
|
||
sst += d * d;
|
||
|
||
cnt += 1;
|
||
end;
|
||
|
||
// если OOB почти нет — просто помечаем как недоступно
|
||
if cnt >= Max(1, n div 10) then
|
||
begin
|
||
if sst <= 0 then
|
||
fOOBScore := 0.0
|
||
else
|
||
fOOBScore := 1.0 - (sse / sst);
|
||
|
||
fHasOOBScore := true;
|
||
end;
|
||
end;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function RandomForestRegressor.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureCount);
|
||
|
||
var n := X.RowCount;
|
||
var resultVec := new Vector(n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var s := 0.0;
|
||
|
||
for var t := 0 to fTrees.Length - 1 do
|
||
s += fTrees[t].PredictOne(X, i);
|
||
|
||
resultVec[i] := s / fTrees.Length;
|
||
end;
|
||
|
||
Result := resultVec;
|
||
end;
|
||
|
||
function RandomForestRegressor.Clone: IModel;
|
||
begin
|
||
var rf := new RandomForestRegressor(
|
||
fNTrees,
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fMaxFeaturesMode,
|
||
fUseOOB,
|
||
fRandomSeed
|
||
);
|
||
|
||
// --- seed ---
|
||
rf.fRandomSeed := fRandomSeed;
|
||
rf.fUserProvidedSeed := fUserProvidedSeed;
|
||
|
||
if fUserProvidedSeed then
|
||
rf.fRng := new System.Random(fRandomSeed)
|
||
else
|
||
rf.fRng := new System.Random;
|
||
|
||
rf.fFitted := fFitted;
|
||
rf.fFeatureCount := fFeatureCount;
|
||
|
||
rf.fOOBScore := fOOBScore;
|
||
rf.fHasOOBScore := fHasOOBScore;
|
||
|
||
// --- trees ---
|
||
if fTrees <> nil then
|
||
begin
|
||
SetLength(rf.fTrees, fTrees.Length);
|
||
for var i := 0 to fTrees.Length - 1 do
|
||
rf.fTrees[i] := DecisionTreeRegressor(fTrees[i].Clone);
|
||
end;
|
||
|
||
Result := rf;
|
||
end;
|
||
|
||
function RandomForestRegressor.FeatureImportances: Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
var p := fTrees[0].FeatureImportances.Length;
|
||
var resultVec := new Vector(p);
|
||
|
||
for var t := 0 to fTrees.Length - 1 do
|
||
resultVec += fTrees[t].FeatureImportances;
|
||
|
||
resultVec *= 1.0 / fTrees.Length;
|
||
|
||
Result := resultVec;
|
||
end;
|
||
|
||
function RandomForestRegressor.ToString: string;
|
||
begin
|
||
var depthStr :=
|
||
if fMaxDepth = -1 then '∞'
|
||
else fMaxDepth.ToString;
|
||
|
||
var seedPart :=
|
||
if fUserProvidedSeed then
|
||
$', seed={fRandomSeed}'
|
||
else
|
||
'';
|
||
|
||
Result :=
|
||
$'RandomForestRegressor(' +
|
||
$'nTrees={fNTrees}, ' +
|
||
$'maxDepth={depthStr}, ' +
|
||
$'minSamplesSplit={fMinSamplesSplit}, ' +
|
||
$'minSamplesLeaf={fMinSamplesLeaf}, ' +
|
||
$'maxFeatures={fMaxFeaturesMode}' +
|
||
seedPart +
|
||
')';
|
||
end;
|
||
|
||
//-----------------------------
|
||
// RandomForestClassifier
|
||
//-----------------------------
|
||
|
||
constructor RandomForestClassifier.Create(nTrees: integer;
|
||
maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer;
|
||
maxFeaturesMode: TMaxFeaturesMode; computeOOB: boolean; seed: integer);
|
||
begin
|
||
inherited Create(nTrees,maxDepth,minSamplesSplit,minSamplesLeaf,maxFeaturesMode,computeOOB,seed);
|
||
end;
|
||
|
||
function RandomForestClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
fFeatureCount := p;
|
||
SetLength(fTrees, fNTrees);
|
||
|
||
// --- reset
|
||
fClassCount := 0;
|
||
fIndexToClass := nil;
|
||
|
||
// =========================================================
|
||
// ЕДИНЫЙ ENCODING (forest-level)
|
||
// =========================================================
|
||
|
||
var yInt := new integer[n];
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var r := y[i];
|
||
var ir := Round(r);
|
||
|
||
if Abs(r - ir) > 1e-12 then
|
||
ArgumentError(ER_LABELS_NOT_INTEGER);
|
||
|
||
yInt[i] := ir;
|
||
end;
|
||
|
||
var yEncArr := EncodeLabelsInt(yInt, fIndexToClass);
|
||
fClassCount := fIndexToClass.Length;
|
||
|
||
if fClassCount < 2 then
|
||
ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES);
|
||
|
||
var yEnc := new Vector(n);
|
||
for var i := 0 to n - 1 do
|
||
yEnc[i] := yEncArr[i];
|
||
|
||
// =========================================================
|
||
|
||
// --- OOB buffers
|
||
var oobVotes: array[,] of integer := nil;
|
||
var oobCnt: array of integer := nil;
|
||
|
||
if fUseOOB then
|
||
begin
|
||
oobVotes := new integer[n, fClassCount];
|
||
oobCnt := new integer[n];
|
||
end;
|
||
|
||
// --- training
|
||
for var t := 0 to fNTrees - 1 do
|
||
begin
|
||
var treeSeed := fRng.Next(integer.MaxValue);
|
||
|
||
var rows: array of integer;
|
||
BootstrapRowIndices(n, rows);
|
||
|
||
var XBoot := X.TakeRows(rows);
|
||
var yBoot := yEnc.SubvectorBy(rows);
|
||
|
||
var treeCriterion := new GiniCriterion(fClassCount);
|
||
|
||
var tree := new DecisionTreeCore(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
treeCriterion,
|
||
ComputeMaxFeatures(p),
|
||
treeSeed
|
||
);
|
||
|
||
tree.Fit(XBoot, yBoot);
|
||
|
||
// --- OOB accumulate
|
||
if fUseOOB then
|
||
begin
|
||
var inBag := new boolean[n];
|
||
for var i := 0 to rows.Length - 1 do
|
||
inBag[rows[i]] := true;
|
||
|
||
for var i := 0 to n - 1 do
|
||
if not inBag[i] then
|
||
begin
|
||
var cls := tree.PredictRow(X, i); // encoded class 0..K-1
|
||
oobVotes[i, cls] += 1;
|
||
oobCnt[i] += 1;
|
||
end;
|
||
end;
|
||
|
||
fTrees[t] := tree;
|
||
end;
|
||
|
||
// --- finalize OOB score
|
||
fHasOOBScore := false;
|
||
fOOBScore := real.NaN;
|
||
|
||
if fUseOOB then
|
||
begin
|
||
var correct := 0;
|
||
var cnt := 0;
|
||
|
||
for var i := 0 to n - 1 do
|
||
if oobCnt[i] > 0 then
|
||
begin
|
||
var bestK := 0;
|
||
var bestV := oobVotes[i, 0];
|
||
|
||
for var k := 1 to fClassCount - 1 do
|
||
if oobVotes[i, k] > bestV then
|
||
begin
|
||
bestV := oobVotes[i, k];
|
||
bestK := k;
|
||
end;
|
||
|
||
if bestK = yEncArr[i] then
|
||
correct += 1;
|
||
|
||
cnt += 1;
|
||
end;
|
||
|
||
if cnt >= Max(1, n div 10) then
|
||
begin
|
||
fOOBScore := correct / cnt;
|
||
fHasOOBScore := true;
|
||
end;
|
||
end;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function RandomForestClassifier.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureCount);
|
||
|
||
var n := X.RowCount;
|
||
var treeCount := fTrees.Length;
|
||
|
||
if treeCount = 0 then
|
||
Error(ER_MODEL_NOT_INITIALIZED);
|
||
|
||
if fClassCount <= 0 then
|
||
Error(ER_MODEL_NOT_INITIALIZED);
|
||
|
||
var resultVec := new Vector(n);
|
||
var counts := new integer[fClassCount];
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
// обнуление счётчиков
|
||
for var c := 0 to fClassCount - 1 do
|
||
counts[c] := 0;
|
||
|
||
// голосование деревьев
|
||
for var t := 0 to treeCount - 1 do
|
||
begin
|
||
var cls := fTrees[t].PredictRow(X, i); // <-- КЛЮЧЕВОЕ изменение
|
||
|
||
if (cls < 0) or (cls >= fClassCount) then
|
||
ArgumentError(ER_LABEL_INDEX_INVALID);
|
||
|
||
counts[cls] += 1;
|
||
end;
|
||
|
||
// выбор класса
|
||
var bestClass := 0;
|
||
var bestCount := counts[0];
|
||
|
||
for var c := 1 to fClassCount - 1 do
|
||
if counts[c] > bestCount then
|
||
begin
|
||
bestCount := counts[c];
|
||
bestClass := c;
|
||
end;
|
||
|
||
// декодирование
|
||
resultVec[i] := fIndexToClass[bestClass];
|
||
end;
|
||
|
||
Result := resultVec;
|
||
end;
|
||
|
||
function RandomForestClassifier.PredictLabels(X: Matrix): array of integer;
|
||
begin
|
||
var v := Predict(X);
|
||
|
||
Result := new integer[v.Length];
|
||
|
||
for var i := 0 to v.Length - 1 do
|
||
Result[i] := Round(v[i]);
|
||
end;
|
||
|
||
function RandomForestClassifier.PredictProba(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureCount);
|
||
|
||
var n := X.RowCount;
|
||
var treeCount := fTrees.Length;
|
||
|
||
if treeCount = 0 then
|
||
Error(ER_MODEL_NOT_INITIALIZED);
|
||
|
||
if fClassCount <= 0 then
|
||
Error(ER_MODEL_NOT_INITIALIZED);
|
||
|
||
Result := new Matrix(n, fClassCount);
|
||
var counts := new integer[fClassCount];
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
// reset
|
||
for var c := 0 to fClassCount - 1 do
|
||
counts[c] := 0;
|
||
|
||
// voting
|
||
for var t := 0 to treeCount - 1 do
|
||
begin
|
||
var cls := fTrees[t].PredictRow(X, i); // <-- главное изменение
|
||
|
||
if (cls < 0) or (cls >= fClassCount) then
|
||
ArgumentError(ER_LABEL_INDEX_INVALID);
|
||
|
||
counts[cls] += 1;
|
||
end;
|
||
|
||
// normalize
|
||
for var c := 0 to fClassCount - 1 do
|
||
Result[i, c] := counts[c] / treeCount;
|
||
end;
|
||
end;
|
||
|
||
function RandomForestClassifier.Clone: IModel;
|
||
begin
|
||
var rf := new RandomForestClassifier(
|
||
fNTrees,
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fMaxFeaturesMode,
|
||
fUseOOB,
|
||
fRandomSeed
|
||
);
|
||
|
||
// --- seed ---
|
||
rf.fRandomSeed := fRandomSeed;
|
||
rf.fUserProvidedSeed := fUserProvidedSeed;
|
||
|
||
if fUserProvidedSeed then
|
||
rf.fRng := new System.Random(fRandomSeed)
|
||
else
|
||
rf.fRng := new System.Random;
|
||
|
||
rf.fFitted := fFitted;
|
||
rf.fFeatureCount := fFeatureCount;
|
||
|
||
// --- classes ---
|
||
rf.fClassCount := fClassCount;
|
||
|
||
if fIndexToClass <> nil then
|
||
rf.fIndexToClass := Copy(fIndexToClass);
|
||
|
||
if fClassToIndex <> nil then
|
||
begin
|
||
rf.fClassToIndex := new Dictionary<integer, integer>;
|
||
foreach var kv in fClassToIndex do
|
||
rf.fClassToIndex.Add(kv.Key, kv.Value);
|
||
end;
|
||
|
||
// --- OOB ---
|
||
rf.fOOBScore := fOOBScore;
|
||
rf.fHasOOBScore := fHasOOBScore;
|
||
|
||
// --- trees ---
|
||
if fTrees <> nil then
|
||
begin
|
||
SetLength(rf.fTrees, fTrees.Length);
|
||
for var i := 0 to fTrees.Length - 1 do
|
||
rf.fTrees[i] := fTrees[i].Clone;
|
||
end;
|
||
|
||
Result := rf;
|
||
end;
|
||
|
||
function RandomForestClassifier.FeatureImportances: Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
var p := fTrees[0].FeatureImportances.Length;
|
||
var resultVec := new Vector(p);
|
||
|
||
for var t := 0 to fTrees.Length - 1 do
|
||
resultVec += fTrees[t].FeatureImportances;
|
||
|
||
resultVec *= 1.0 / fTrees.Length;
|
||
|
||
Result := resultVec;
|
||
end;
|
||
|
||
function RandomForestClassifier.ToString: string;
|
||
begin
|
||
var depthStr :=
|
||
if fMaxDepth = -1 then '∞'
|
||
else fMaxDepth.ToString;
|
||
|
||
var seedPart :=
|
||
if fUserProvidedSeed then
|
||
$', seed={fRandomSeed}'
|
||
else
|
||
'';
|
||
|
||
Result :=
|
||
$'RandomForestClassifier(' +
|
||
$'nTrees={fNTrees}, ' +
|
||
$'maxDepth={depthStr}, ' +
|
||
$'minSamplesSplit={fMinSamplesSplit}, ' +
|
||
$'minSamplesLeaf={fMinSamplesLeaf}, ' +
|
||
$'maxFeatures={fMaxFeaturesMode}' +
|
||
seedPart +
|
||
')';
|
||
end;
|
||
|
||
function RandomForestClassifier.GetClasses: array of real;
|
||
begin
|
||
Result := new real[fClassCount];
|
||
|
||
for var i := 0 to fClassCount - 1 do
|
||
Result[i] := fIndexToClass[i];
|
||
end;
|
||
|
||
procedure RandomForestClassifier.SetClassLabels(classes: array of string);
|
||
begin
|
||
fClassLabels := Copy(classes);
|
||
end;
|
||
|
||
function RandomForestClassifier.GetClassLabels: array of string;
|
||
begin
|
||
if fClassLabels = nil then
|
||
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
|
||
|
||
Result := fClassLabels;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// Helper GBC GBR
|
||
//-----------------------------
|
||
|
||
function BuildSubsampleIndices(nTrain: integer; subsample: real; rng: System.Random): array of integer;
|
||
begin
|
||
if nTrain <= 0 then
|
||
ArgumentOutOfRangeError(ER_EMPTY_DATASET);
|
||
|
||
if (subsample <= 0.0) or (subsample > 1.0) then
|
||
ArgumentOutOfRangeError(ER_SUBSAMPLE_INVALID, subsample);
|
||
|
||
var k := Floor(nTrain * subsample);
|
||
if k < 1 then
|
||
k := 1;
|
||
if k > nTrain then
|
||
k := nTrain;
|
||
|
||
// если берём всё — можно быстро вернуть [0..nTrain-1]
|
||
if k = nTrain then
|
||
begin
|
||
Result := new integer[nTrain];
|
||
for var i := 0 to nTrain - 1 do
|
||
Result[i] := i;
|
||
exit;
|
||
end;
|
||
|
||
var all := new integer[nTrain];
|
||
for var i := 0 to nTrain - 1 do
|
||
all[i] := i;
|
||
|
||
// partial Fisher–Yates (без повторений)
|
||
for var i := 0 to k - 1 do
|
||
begin
|
||
var j := i + rng.Next(nTrain - i);
|
||
var tmp := all[i];
|
||
all[i] := all[j];
|
||
all[j] := tmp;
|
||
end;
|
||
|
||
Result := new integer[k];
|
||
for var i := 0 to k - 1 do
|
||
Result[i] := all[i];
|
||
end;
|
||
|
||
//-----------------------------
|
||
// GradientBoostingRegressor
|
||
//-----------------------------
|
||
constructor GradientBoostingRegressor.Create(
|
||
nEstimators: integer;
|
||
learningRate: real;
|
||
maxDepth: integer;
|
||
minSamplesSplit: integer;
|
||
minSamplesLeaf: integer;
|
||
subsample: real;
|
||
loss: TGBLoss;
|
||
huberDelta: real;
|
||
earlyStoppingPatience: integer;
|
||
quantileAlpha: real;
|
||
leafL2: real;
|
||
useOOBEarlyStopping: boolean;
|
||
seed: integer);
|
||
begin
|
||
if nEstimators <= 0 then
|
||
ArgumentOutOfRangeError(ER_N_ESTIMATORS_NOT_POSITIVE);
|
||
|
||
if learningRate <= 0 then
|
||
ArgumentOutOfRangeError(ER_LEARNING_RATE_NOT_POSITIVE);
|
||
|
||
if (subsample <= 0) or (subsample > 1) then
|
||
ArgumentOutOfRangeError(ER_SUBSAMPLE_OUT_OF_RANGE);
|
||
|
||
if maxDepth <= 0 then
|
||
ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, maxDepth);
|
||
|
||
if minSamplesSplit < 2 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_SPLIT_INVALID, minSamplesSplit);
|
||
|
||
if minSamplesLeaf < 1 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
|
||
|
||
if maxDepth > MAX_ALLOWED_TREE_DEPTH then
|
||
ArgumentOutOfRangeError(ER_MAX_DEPTH_TOO_LARGE, maxDepth);
|
||
|
||
fNEstimators := nEstimators;
|
||
fLearningRate := learningRate;
|
||
fMaxDepth := maxDepth;
|
||
fMinSamplesSplit := minSamplesSplit;
|
||
fMinSamplesLeaf := minSamplesLeaf;
|
||
fSubsample := subsample;
|
||
|
||
fEstimators := new List<DecisionTreeRegressor>;
|
||
fFitted := false;
|
||
|
||
if seed < 0 then
|
||
begin
|
||
fUserProvidedSeed := false;
|
||
fRandomSeed := System.Environment.TickCount and integer.MaxValue;
|
||
end
|
||
else
|
||
begin
|
||
fUserProvidedSeed := true;
|
||
fRandomSeed := seed;
|
||
end;
|
||
|
||
fRng := new System.Random(fRandomSeed);
|
||
|
||
// -------------
|
||
if huberDelta <= 0 then
|
||
ArgumentOutOfRangeError(ER_HUBER_DELTA_INVALID, huberDelta);
|
||
|
||
if earlyStoppingPatience < 0 then
|
||
ArgumentOutOfRangeError(ER_EARLY_STOPPING_INVALID, earlyStoppingPatience);
|
||
|
||
if (quantileAlpha <= 0) or (quantileAlpha >= 1) then
|
||
ArgumentOutOfRangeError(ER_QUANTILE_ALPHA_INVALID, quantileAlpha);
|
||
|
||
if leafL2 < 0 then
|
||
ArgumentOutOfRangeError(ER_LEAFL2_INVALID, leafL2);
|
||
|
||
fLoss := loss;
|
||
fHuberDelta := huberDelta;
|
||
fEarlyStoppingPatience := earlyStoppingPatience;
|
||
|
||
fTrainLossHistory := new List<real>;
|
||
fValLossHistory := new List<real>;
|
||
|
||
fquantileAlpha := quantileAlpha;
|
||
|
||
fLeafL2 := leafL2;
|
||
fUseOOBEarlyStopping := useOOBEarlyStopping;
|
||
|
||
fOOBLossHistory := new List<real>;
|
||
end;
|
||
|
||
function GradientBoostingRegressor.ComputeTrainLoss(
|
||
y, yPred: Vector): real;
|
||
begin
|
||
var n := y.Length;
|
||
|
||
if n = 0 then
|
||
exit(0.0);
|
||
|
||
var sum := 0.0;
|
||
|
||
case fLoss of
|
||
|
||
TGBLoss.SquaredError:
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var e := y[i] - yPred[i];
|
||
sum += 0.5 * e * e;
|
||
end;
|
||
|
||
TGBLoss.Huber:
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var e := y[i] - yPred[i];
|
||
var ae := Abs(e);
|
||
|
||
if ae <= fHuberDelta then
|
||
sum += 0.5 * e * e
|
||
else
|
||
sum += fHuberDelta * (ae - 0.5 * fHuberDelta);
|
||
end;
|
||
|
||
TGBLoss.Quantile:
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var r := y[i] - yPred[i];
|
||
|
||
if r >= 0 then
|
||
sum += fQuantileAlpha * r
|
||
else
|
||
sum += (fQuantileAlpha - 1.0) * r;
|
||
end;
|
||
end;
|
||
|
||
var loss := sum / n;
|
||
|
||
if double.IsNaN(loss) or double.IsInfinity(loss) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
Result := loss;
|
||
end;
|
||
|
||
procedure GradientBoostingRegressor.ComputePseudoResiduals(
|
||
y, yPred: Vector; r: Vector);
|
||
begin
|
||
var n := y.Length;
|
||
|
||
case fLoss of
|
||
|
||
TGBLoss.SquaredError:
|
||
for var i := 0 to n - 1 do
|
||
r[i] := y[i] - yPred[i];
|
||
|
||
TGBLoss.Huber:
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var e := y[i] - yPred[i];
|
||
|
||
if Abs(e) <= fHuberDelta then
|
||
r[i] := e
|
||
else
|
||
r[i] := fHuberDelta * Sign(e);
|
||
end;
|
||
|
||
TGBLoss.Quantile:
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var diff := y[i] - yPred[i];
|
||
|
||
if diff > 0 then
|
||
r[i] := fQuantileAlpha
|
||
else
|
||
r[i] := fQuantileAlpha - 1.0;
|
||
end;
|
||
|
||
end;
|
||
end;
|
||
|
||
function GradientBoostingRegressor.ComputeQuantile(y: Vector; alpha: real): real;
|
||
begin
|
||
var n := y.Length;
|
||
|
||
if (alpha <= 0) or (alpha >= 1) then
|
||
ArgumentOutOfRangeError(
|
||
'Quantile alpha must be in (0,1)!!Quantile alpha must be in (0,1)'
|
||
);
|
||
|
||
// копируем данные
|
||
var data := new real[n];
|
||
for var i := 0 to n - 1 do
|
||
data[i] := y[i];
|
||
|
||
// сортировка
|
||
&Array.Sort(data);
|
||
|
||
// индекс квантиля
|
||
var k := Floor(alpha * (n - 1));
|
||
|
||
Result := data[k];
|
||
end;
|
||
|
||
function GradientBoostingRegressor.ComputeTrainLossMasked(yTrue, yPred: Vector;
|
||
mask: array of boolean): real;
|
||
begin
|
||
var n := yTrue.Length;
|
||
var sum := 0.0;
|
||
var count := 0;
|
||
|
||
case fLoss of
|
||
|
||
TGBLoss.SquaredError:
|
||
for var i := 0 to n - 1 do
|
||
if mask[i] then
|
||
begin
|
||
var e := yTrue[i] - yPred[i];
|
||
sum += 0.5 * e * e;
|
||
count += 1;
|
||
end;
|
||
|
||
TGBLoss.Huber:
|
||
for var i := 0 to n - 1 do
|
||
if mask[i] then
|
||
begin
|
||
var e := yTrue[i] - yPred[i];
|
||
var ae := Abs(e);
|
||
|
||
if ae <= fHuberDelta then
|
||
sum += 0.5 * e * e
|
||
else
|
||
sum += fHuberDelta * (ae - 0.5 * fHuberDelta);
|
||
|
||
count += 1;
|
||
end;
|
||
|
||
TGBLoss.Quantile:
|
||
for var i := 0 to n - 1 do
|
||
if mask[i] then
|
||
begin
|
||
var e := yTrue[i] - yPred[i];
|
||
if e > 0 then
|
||
sum += fQuantileAlpha * e
|
||
else
|
||
sum += (fQuantileAlpha - 1.0) * e;
|
||
count += 1;
|
||
end;
|
||
|
||
end;
|
||
|
||
if count = 0 then
|
||
exit(real.PositiveInfinity);
|
||
|
||
Result := sum / count;
|
||
end;
|
||
|
||
const MinImprovement = 1e-12;
|
||
|
||
function GradientBoostingRegressor.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
Result := FitInternal(X, y, nil, nil, false);
|
||
end;
|
||
|
||
function GradientBoostingRegressor.FitWithValidation(
|
||
XTrain: Matrix; yTrain: Vector;
|
||
XVal: Matrix; yVal: Vector): ISupervisedModel;
|
||
begin
|
||
Result := FitInternal(XTrain, yTrain, XVal, yVal, true);
|
||
end;
|
||
|
||
function GradientBoostingRegressor.FitInternal(
|
||
XTrain: Matrix; yTrain: Vector;
|
||
XVal: Matrix; yVal: Vector;
|
||
useValidation: boolean): ISupervisedModel;
|
||
begin
|
||
// --- null checks ---
|
||
if XTrain = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if yTrain = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
// --- finite checks ---
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(XTrain);
|
||
CheckYForFit(yTrain);
|
||
end;
|
||
|
||
// --- shape checks ---
|
||
if XTrain.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if XTrain.RowCount <> yTrain.Length then
|
||
DimensionError(ER_XY_SIZE_MISMATCH);
|
||
|
||
if useValidation then
|
||
begin
|
||
if XVal = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if yVal = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForPredict(XVal);
|
||
CheckYForFit(yVal);
|
||
end;
|
||
|
||
if XVal.RowCount <> yVal.Length then
|
||
DimensionError(ER_XY_SIZE_MISMATCH);
|
||
|
||
if XVal.ColCount <> XTrain.ColCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
end;
|
||
|
||
// --- init state ---
|
||
fOOBLossHistory.Clear;
|
||
fEstimators.Clear;
|
||
fFeatureCount := XTrain.ColCount;
|
||
|
||
fTrainLossHistory.Clear;
|
||
fValLossHistory.Clear;
|
||
|
||
fBestIteration := -1;
|
||
fBestScoreLoss := real.PositiveInfinity;
|
||
|
||
var noImprove := 0;
|
||
|
||
// --- F0 ---
|
||
case fLoss of
|
||
TGBLoss.Quantile:
|
||
fInitValue := ComputeQuantile(yTrain, fQuantileAlpha);
|
||
else
|
||
fInitValue := yTrain.Average;
|
||
end;
|
||
|
||
var nTrain := yTrain.Length;
|
||
|
||
var yPredTrain := new Vector(nTrain);
|
||
for var i := 0 to nTrain - 1 do
|
||
yPredTrain[i] := fInitValue;
|
||
|
||
// --- OOB logic ---
|
||
var useSubsample := fSubsample < 1.0;
|
||
var useOOB :=
|
||
(not useValidation) and
|
||
useSubsample and
|
||
(fEarlyStoppingPatience > 0) and
|
||
fUseOOBEarlyStopping;
|
||
|
||
var oobSum := new Vector(nTrain); // сумма вкладов OOB-деревьев
|
||
var oobCount := new integer[nTrain]; // сколько раз объект был OOB
|
||
|
||
var yPredVal: Vector := nil;
|
||
if useValidation then
|
||
begin
|
||
var nVal := yVal.Length;
|
||
yPredVal := new Vector(nVal);
|
||
for var i := 0 to nVal - 1 do
|
||
yPredVal[i] := fInitValue;
|
||
end;
|
||
|
||
// --- boosting loop ---
|
||
for var m := 0 to fNEstimators - 1 do
|
||
begin
|
||
// 1. residuals
|
||
var r := new Vector(nTrain);
|
||
ComputePseudoResiduals(yTrain, yPredTrain, r);
|
||
|
||
var stageSeed := fRng.Next(integer.MaxValue);
|
||
|
||
var tree := new DecisionTreeRegressor(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fLeafL2,
|
||
stageSeed
|
||
);
|
||
|
||
// --- subsample ---
|
||
var rows: array of integer := nil;
|
||
var used: array of boolean := nil;
|
||
|
||
if useSubsample then
|
||
begin
|
||
rows := BuildSubsampleIndices(nTrain, fSubsample, fRng);
|
||
tree.SetRowIndices(rows);
|
||
|
||
if useOOB then
|
||
begin
|
||
used := new boolean[nTrain];
|
||
for var i := 0 to rows.Length - 1 do
|
||
used[rows[i]] := true;
|
||
end;
|
||
end;
|
||
|
||
tree.Fit(XTrain, r);
|
||
fEstimators.Add(tree);
|
||
|
||
var deltaTrain := tree.Predict(XTrain);
|
||
|
||
// --- update TRAIN ---
|
||
for var i := 0 to nTrain - 1 do
|
||
yPredTrain[i] += fLearningRate * deltaTrain[i];
|
||
|
||
// --- update OOB ---
|
||
if useOOB then
|
||
begin
|
||
for var i := 0 to nTrain - 1 do
|
||
if not used[i] then
|
||
begin
|
||
oobSum[i] += fLearningRate * deltaTrain[i];
|
||
oobCount[i] += 1;
|
||
end;
|
||
end;
|
||
|
||
// --- update VAL ---
|
||
if useValidation then
|
||
begin
|
||
var deltaVal := tree.Predict(XVal);
|
||
for var i := 0 to yPredVal.Length - 1 do
|
||
yPredVal[i] += fLearningRate * deltaVal[i];
|
||
end;
|
||
|
||
// --- losses ---
|
||
var trainLoss := ComputeTrainLoss(yTrain, yPredTrain);
|
||
fTrainLossHistory.Add(trainLoss);
|
||
|
||
var scoreLoss := trainLoss;
|
||
|
||
if useValidation then
|
||
begin
|
||
var valLoss := ComputeTrainLoss(yVal, yPredVal);
|
||
fValLossHistory.Add(valLoss);
|
||
scoreLoss := valLoss;
|
||
end
|
||
else if useOOB then
|
||
begin
|
||
var mask := new boolean[nTrain];
|
||
var yPredOOB := new Vector(nTrain);
|
||
|
||
var cnt := 0;
|
||
|
||
for var i := 0 to nTrain - 1 do
|
||
begin
|
||
if oobCount[i] > 0 then
|
||
begin
|
||
mask[i] := true;
|
||
yPredOOB[i] :=
|
||
fInitValue + oobSum[i] / oobCount[i];
|
||
cnt += 1;
|
||
end
|
||
else
|
||
mask[i] := false;
|
||
end;
|
||
|
||
if cnt >= Max(1, nTrain div 10) then
|
||
begin
|
||
scoreLoss := ComputeTrainLossMasked(yTrain, yPredOOB, mask);
|
||
fOOBLossHistory.Add(scoreLoss);
|
||
end
|
||
else
|
||
begin
|
||
scoreLoss := trainLoss;
|
||
fOOBLossHistory.Add(real.NaN);
|
||
end;
|
||
end;
|
||
|
||
// --- early stopping ---
|
||
if fEarlyStoppingPatience > 0 then
|
||
begin
|
||
if (fBestScoreLoss - scoreLoss > MinImprovement) then
|
||
begin
|
||
fBestScoreLoss := scoreLoss;
|
||
fBestIteration := m;
|
||
noImprove := 0;
|
||
end
|
||
else
|
||
begin
|
||
noImprove += 1;
|
||
if noImprove >= fEarlyStoppingPatience then
|
||
break;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
// --- cut estimators ---
|
||
if (fEarlyStoppingPatience > 0) and (fBestIteration >= 0) then
|
||
begin
|
||
var keep := fBestIteration + 1;
|
||
|
||
if fEstimators.Count > keep then
|
||
fEstimators.RemoveRange(keep, fEstimators.Count - keep);
|
||
|
||
if fOOBLossHistory.Count > keep then
|
||
fOOBLossHistory.RemoveRange(keep, fOOBLossHistory.Count - keep);
|
||
|
||
if fTrainLossHistory.Count > keep then
|
||
fTrainLossHistory.RemoveRange(keep, fTrainLossHistory.Count - keep);
|
||
|
||
if fValLossHistory.Count > keep then
|
||
fValLossHistory.RemoveRange(keep, fValLossHistory.Count - keep);
|
||
end;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function GradientBoostingRegressor.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
var n := X.RowCount;
|
||
var yPred := new Vector(n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
yPred[i] := fInitValue;
|
||
|
||
foreach var tree in fEstimators do
|
||
begin
|
||
var delta := tree.Predict(X);
|
||
for var i := 0 to n - 1 do
|
||
yPred[i] += fLearningRate * delta[i];
|
||
end;
|
||
|
||
Result := yPred;
|
||
end;
|
||
|
||
function GradientBoostingRegressor.PredictStage(X: Matrix; m: integer): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureCount);
|
||
|
||
var totalTrees := fEstimators.Count;
|
||
|
||
if (m < 0) or (m > totalTrees) then
|
||
ArgumentOutOfRangeError(ER_STAGE_OUT_OF_RANGE, m, totalTrees);
|
||
|
||
var n := X.RowCount;
|
||
var yPred := new Vector(n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
yPred[i] := fInitValue;
|
||
|
||
for var t := 0 to m - 1 do
|
||
begin
|
||
var delta := fEstimators[t].Predict(X);
|
||
|
||
if delta.Length <> n then
|
||
DimensionError(ER_XY_SIZE_MISMATCH, delta.Length, n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var v := yPred[i] + fLearningRate * delta[i];
|
||
|
||
if double.IsNaN(v) or double.IsInfinity(v) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
yPred[i] := v;
|
||
end;
|
||
end;
|
||
|
||
Result := yPred;
|
||
end;
|
||
|
||
function GradientBoostingRegressor.StagedPredict(X: Matrix): sequence of Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureCount);
|
||
|
||
var n := X.RowCount;
|
||
var yPred := new Vector(n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
yPred[i] := fInitValue;
|
||
|
||
for var t := 0 to fEstimators.Count - 1 do
|
||
begin
|
||
var delta := fEstimators[t].Predict(X);
|
||
|
||
if delta.Length <> n then
|
||
DimensionError(ER_XY_SIZE_MISMATCH, delta.Length, n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var v := yPred[i] + fLearningRate * delta[i];
|
||
|
||
if double.IsNaN(v) or double.IsInfinity(v) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
yPred[i] := v;
|
||
end;
|
||
|
||
yield yPred.Clone; // ВАЖНО: clone!
|
||
end;
|
||
end;
|
||
|
||
function GradientBoostingRegressor.FeatureImportances: Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if fFeatureImportances <> nil then
|
||
exit(fFeatureImportances.Clone); // НЕ отдаём внутренний кэш
|
||
|
||
var importances := new Vector(fFeatureCount);
|
||
|
||
foreach var tree in fEstimators do
|
||
begin
|
||
var imp := tree.FeatureImportances;
|
||
|
||
if imp = nil then
|
||
continue;
|
||
|
||
if imp.Length <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH, imp.Length, fFeatureCount);
|
||
|
||
for var j := 0 to fFeatureCount - 1 do
|
||
begin
|
||
var v := imp[j];
|
||
|
||
if double.IsNaN(v) or double.IsInfinity(v) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
importances[j] += v;
|
||
end;
|
||
end;
|
||
|
||
var s := importances.Sum;
|
||
|
||
if double.IsNaN(s) or double.IsInfinity(s) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
if Abs(s) > 1e-15 then
|
||
for var j := 0 to fFeatureCount - 1 do
|
||
importances[j] /= s;
|
||
|
||
fFeatureImportances := importances;
|
||
|
||
Result := fFeatureImportances.Clone; // безопасный возврат
|
||
end;
|
||
|
||
function GradientBoostingRegressor.ToString: string;
|
||
begin
|
||
var s :=
|
||
$'GradientBoostingRegressor(' +
|
||
$'n={fNEstimators}, ' +
|
||
$'lr={fLearningRate}, ' +
|
||
$'maxDepth={fMaxDepth}, ' +
|
||
$'loss={fLoss}';
|
||
|
||
if fSubsample <> 1.0 then
|
||
s += $', subs={fSubsample}';
|
||
|
||
if fEarlyStoppingPatience > 0 then
|
||
s += $', earlyStop={fEarlyStoppingPatience}';
|
||
|
||
if fLoss = TGBLoss.Huber then
|
||
s += $', delta={fHuberDelta}';
|
||
|
||
if fLoss = TGBLoss.Quantile then
|
||
s += $', alpha={fQuantileAlpha}';
|
||
|
||
if fLeafL2 <> 0.0 then
|
||
s += $', leafL2={fLeafL2}';
|
||
|
||
if fUseOOBEarlyStopping then
|
||
s += ', OOB=true';
|
||
|
||
if fUserProvidedSeed then
|
||
s += $', seed={fRandomSeed}';
|
||
|
||
s += ')';
|
||
|
||
Result := s;
|
||
end;
|
||
|
||
function GradientBoostingRegressor.Clone: IModel;
|
||
begin
|
||
var copy := new GradientBoostingRegressor(
|
||
fNEstimators,
|
||
fLearningRate,
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fSubsample,
|
||
fLoss,
|
||
fHuberDelta,
|
||
fEarlyStoppingPatience,
|
||
fQuantileAlpha,
|
||
fLeafL2,
|
||
fUseOOBEarlyStopping,
|
||
fRandomSeed
|
||
);
|
||
|
||
// --- seed ---
|
||
copy.fRandomSeed := fRandomSeed;
|
||
copy.fUserProvidedSeed := fUserProvidedSeed;
|
||
|
||
if fUserProvidedSeed then
|
||
copy.fRng := new System.Random(fRandomSeed)
|
||
else
|
||
copy.fRng := new System.Random;
|
||
|
||
// --- basic state ---
|
||
copy.fInitValue := fInitValue;
|
||
copy.fFeatureCount := fFeatureCount;
|
||
copy.fFitted := fFitted;
|
||
|
||
// --- best iteration ---
|
||
copy.fBestIteration := fBestIteration;
|
||
copy.fBestTrainLoss := fBestTrainLoss;
|
||
copy.fBestScoreLoss := fBestScoreLoss;
|
||
|
||
// --- histories ---
|
||
copy.fTrainLossHistory.Clear;
|
||
copy.fTrainLossHistory.AddRange(fTrainLossHistory);
|
||
|
||
copy.fValLossHistory.Clear;
|
||
copy.fValLossHistory.AddRange(fValLossHistory);
|
||
|
||
copy.fOOBLossHistory.Clear;
|
||
copy.fOOBLossHistory.AddRange(fOOBLossHistory);
|
||
|
||
// --- estimators ---
|
||
copy.fEstimators.Clear;
|
||
foreach var tree in fEstimators do
|
||
copy.fEstimators.Add(tree.Clone as DecisionTreeRegressor);
|
||
|
||
Result := copy;
|
||
end;
|
||
|
||
|
||
//-----------------------------
|
||
// GradientBoostingClassifier
|
||
//-----------------------------
|
||
|
||
function GradientBoostingClassifier.FitInternal(
|
||
XTrain: Matrix; yTrain: Vector;
|
||
XVal: Matrix; yVal: Vector;
|
||
useValidation: boolean): ISupervisedModel;
|
||
begin
|
||
// --- null checks ---
|
||
if XTrain = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if yTrain = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
// --- finite checks ---
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(XTrain);
|
||
CheckYForFit(yTrain);
|
||
end;
|
||
|
||
// --- shape checks ---
|
||
if XTrain.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if XTrain.RowCount <> yTrain.Length then
|
||
DimensionError(ER_XY_SIZE_MISMATCH);
|
||
|
||
if useValidation then
|
||
begin
|
||
if XVal = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if yVal = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForPredict(XVal);
|
||
CheckYForFit(yVal);
|
||
end;
|
||
|
||
if XVal.RowCount <> yVal.Length then
|
||
DimensionError(ER_XY_SIZE_MISMATCH);
|
||
|
||
if XVal.ColCount <> XTrain.ColCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
end;
|
||
|
||
// --- reset state
|
||
fOOBLossHistory.Clear;
|
||
fEstimators.Clear;
|
||
fTrainLossHistory.Clear;
|
||
fValLossHistory.Clear;
|
||
|
||
fFeatureCount := XTrain.ColCount;
|
||
fBestIteration := -1;
|
||
fBestScoreLoss := real.PositiveInfinity;
|
||
fFitted := false;
|
||
|
||
var nTrain := XTrain.RowCount;
|
||
|
||
// =========================================================
|
||
// НОВЫЙ ЕДИНЫЙ ENCODING
|
||
// =========================================================
|
||
|
||
var yTrainInt := new integer[nTrain];
|
||
|
||
for var i := 0 to nTrain - 1 do
|
||
begin
|
||
var r := yTrain[i];
|
||
var ir := Round(r);
|
||
|
||
if Abs(r - ir) > 1e-12 then
|
||
ArgumentError(ER_LABELS_NOT_INTEGER);
|
||
|
||
yTrainInt[i] := ir;
|
||
end;
|
||
|
||
var yEncoded := EncodeLabelsInt(yTrainInt, fClasses);
|
||
|
||
fClassCount := fClasses.Length;
|
||
|
||
if fClassCount < 2 then
|
||
ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES);
|
||
|
||
fClassIndex := new Dictionary<integer, integer>;
|
||
for var cls := 0 to fClassCount - 1 do
|
||
fClassIndex[fClasses[cls]] := cls;
|
||
|
||
var classCount := fClassCount;
|
||
|
||
// =========================================================
|
||
|
||
// --- compute class priors
|
||
fInitLogits := new real[classCount];
|
||
|
||
var counts := new integer[classCount];
|
||
|
||
for var i := 0 to nTrain - 1 do
|
||
counts[yEncoded[i]] += 1;
|
||
|
||
for var cls := 0 to classCount - 1 do
|
||
begin
|
||
var pi := counts[cls] / nTrain;
|
||
|
||
if pi <= 0 then
|
||
fInitLogits[cls] := -20.0
|
||
else
|
||
fInitLogits[cls] := Ln(pi);
|
||
end;
|
||
|
||
// --- init logits
|
||
var logitsTrain := new Matrix(nTrain, classCount);
|
||
|
||
for var i := 0 to nTrain - 1 do
|
||
for var cls := 0 to classCount - 1 do
|
||
logitsTrain[i, cls] := fInitLogits[cls];
|
||
|
||
// --- OOB init
|
||
var useOOB := (not useValidation) and (fSubsample < 1.0);
|
||
|
||
var logitsOOB: Matrix := nil;
|
||
var oobCount: array of integer;
|
||
|
||
if useOOB then
|
||
begin
|
||
logitsOOB := new Matrix(nTrain, classCount);
|
||
SetLength(oobCount, nTrain);
|
||
|
||
for var i := 0 to nTrain - 1 do
|
||
begin
|
||
oobCount[i] := 0;
|
||
for var cls := 0 to classCount - 1 do
|
||
logitsOOB[i, cls] := fInitLogits[cls];
|
||
end;
|
||
end;
|
||
|
||
var logitsVal: Matrix := nil;
|
||
var yValEncoded: array of integer;
|
||
|
||
if useValidation then
|
||
begin
|
||
var nVal := yVal.Length;
|
||
|
||
yValEncoded := new integer[nVal];
|
||
|
||
for var i := 0 to nVal - 1 do
|
||
begin
|
||
var r := yVal[i];
|
||
var ir := Round(r);
|
||
|
||
if Abs(r - ir) > 1e-12 then
|
||
ArgumentError(ER_LABELS_NOT_INTEGER);
|
||
|
||
if not fClassIndex.ContainsKey(ir) then
|
||
ArgumentError(ER_UNKNOWN_CLASS_LABEL, ir);
|
||
|
||
yValEncoded[i] := fClassIndex[ir];
|
||
end;
|
||
|
||
logitsVal := new Matrix(XVal.RowCount, classCount);
|
||
|
||
for var i := 0 to XVal.RowCount - 1 do
|
||
for var cls := 0 to classCount - 1 do
|
||
logitsVal[i, cls] := fInitLogits[cls];
|
||
end;
|
||
|
||
var noImprove := 0;
|
||
|
||
// --- boosting loop
|
||
for var iter := 0 to fNEstimators - 1 do
|
||
begin
|
||
var probsTrain := new Matrix(nTrain, classCount);
|
||
SoftmaxMatrix(logitsTrain, probsTrain);
|
||
|
||
var residuals := new Matrix(nTrain, classCount);
|
||
|
||
for var i := 0 to nTrain - 1 do
|
||
for var cls := 0 to classCount - 1 do
|
||
begin
|
||
var yik := 0.0;
|
||
if yEncoded[i] = cls then
|
||
yik := 1.0;
|
||
|
||
residuals[i, cls] := yik - probsTrain[i, cls];
|
||
end;
|
||
|
||
var useSubsample := fSubsample < 1.0;
|
||
var subIndices: array of integer := nil;
|
||
var inBag: array of boolean := nil;
|
||
|
||
if useSubsample then
|
||
subIndices := BuildSubsampleIndices(nTrain, fSubsample, fRng);
|
||
|
||
if useOOB then
|
||
begin
|
||
SetLength(inBag, nTrain);
|
||
|
||
if useSubsample then
|
||
for var i := 0 to subIndices.Length - 1 do
|
||
inBag[subIndices[i]] := true
|
||
else
|
||
for var i := 0 to nTrain - 1 do
|
||
inBag[i] := true;
|
||
end;
|
||
|
||
var trees := new DecisionTreeRegressor[classCount];
|
||
|
||
for var cls := 0 to classCount - 1 do
|
||
begin
|
||
var rvec := new Vector(nTrain);
|
||
for var i := 0 to nTrain - 1 do
|
||
rvec[i] := residuals[i, cls];
|
||
|
||
var stageSeed := fRng.Next(integer.MaxValue);
|
||
|
||
var tree := new DecisionTreeRegressor(
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
seed := stageSeed
|
||
);
|
||
|
||
if useSubsample then
|
||
tree.SetRowIndices(subIndices);
|
||
|
||
tree.Fit(XTrain, rvec);
|
||
trees[cls] := tree;
|
||
|
||
var deltaTrain := tree.Predict(XTrain);
|
||
|
||
for var i := 0 to nTrain - 1 do
|
||
logitsTrain[i, cls] += fLearningRate * deltaTrain[i];
|
||
|
||
if useOOB then
|
||
begin
|
||
for var i := 0 to nTrain - 1 do
|
||
if not inBag[i] then
|
||
logitsOOB[i, cls] += fLearningRate * deltaTrain[i];
|
||
end;
|
||
|
||
if useValidation then
|
||
begin
|
||
var deltaVal := tree.Predict(XVal);
|
||
for var i := 0 to XVal.RowCount - 1 do
|
||
logitsVal[i, cls] += fLearningRate * deltaVal[i];
|
||
end;
|
||
end;
|
||
|
||
if useOOB then
|
||
begin
|
||
for var i := 0 to nTrain - 1 do
|
||
if not inBag[i] then
|
||
oobCount[i] += 1;
|
||
end;
|
||
|
||
fEstimators.Add(trees);
|
||
|
||
SoftmaxMatrix(logitsTrain, probsTrain);
|
||
var trainLoss := ComputeLogLoss(yEncoded, probsTrain);
|
||
fTrainLossHistory.Add(trainLoss);
|
||
|
||
var scoreLoss := trainLoss;
|
||
|
||
if useValidation then
|
||
begin
|
||
var probsVal := new Matrix(XVal.RowCount, classCount);
|
||
SoftmaxMatrix(logitsVal, probsVal);
|
||
|
||
var valLoss := ComputeLogLoss(yValEncoded, probsVal);
|
||
fValLossHistory.Add(valLoss);
|
||
|
||
scoreLoss := valLoss;
|
||
end
|
||
else if useOOB then
|
||
begin
|
||
var mask := new boolean[nTrain];
|
||
var cnt := 0;
|
||
|
||
for var i := 0 to nTrain - 1 do
|
||
begin
|
||
mask[i] := oobCount[i] > 0;
|
||
if mask[i] then
|
||
cnt += 1;
|
||
end;
|
||
|
||
if cnt >= Max(1, nTrain div 10) then
|
||
begin
|
||
scoreLoss := ComputeLogLossMasked(yEncoded, logitsOOB, mask);
|
||
fOOBLossHistory.Add(scoreLoss);
|
||
end
|
||
else
|
||
scoreLoss := trainLoss;
|
||
end;
|
||
|
||
if fEarlyStoppingPatience > 0 then
|
||
begin
|
||
if fBestScoreLoss - scoreLoss > MinImprovement then
|
||
begin
|
||
fBestScoreLoss := scoreLoss;
|
||
fBestIteration := iter;
|
||
noImprove := 0;
|
||
end
|
||
else
|
||
begin
|
||
noImprove += 1;
|
||
if noImprove >= fEarlyStoppingPatience then
|
||
break;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
if (fEarlyStoppingPatience > 0) and (fBestIteration >= 0) then
|
||
begin
|
||
var keep := fBestIteration + 1;
|
||
if fEstimators.Count > keep then
|
||
fEstimators.RemoveRange(keep, fEstimators.Count - keep);
|
||
if fOOBLossHistory.Count > keep then
|
||
fOOBLossHistory.RemoveRange(keep, fOOBLossHistory.Count - keep);
|
||
end;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
procedure GradientBoostingClassifier.SoftmaxRow(
|
||
var logits: array of real;
|
||
var probs: array of real);
|
||
begin
|
||
var classCount := Length(logits);
|
||
|
||
if classCount = 0 then
|
||
ArgumentError(ER_SOFTMAX_EMPTY);
|
||
|
||
if Length(probs) <> classCount then
|
||
DimensionError(ER_XY_SIZE_MISMATCH, Length(probs), classCount);
|
||
|
||
// --- проверка входных логитов
|
||
for var cls := 0 to classCount - 1 do
|
||
begin
|
||
var v := logits[cls];
|
||
|
||
if double.IsNaN(v) or double.IsInfinity(v) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
end;
|
||
|
||
// --- max trick
|
||
var maxLogit := logits[0];
|
||
for var cls := 1 to classCount - 1 do
|
||
if logits[cls] > maxLogit then
|
||
maxLogit := logits[cls];
|
||
|
||
var sumExp := 0.0;
|
||
|
||
for var cls := 0 to classCount - 1 do
|
||
begin
|
||
var shifted := logits[cls] - maxLogit;
|
||
var e := Exp(shifted);
|
||
|
||
if double.IsNaN(e) or double.IsInfinity(e) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
probs[cls] := e;
|
||
sumExp += e;
|
||
end;
|
||
|
||
if double.IsNaN(sumExp) or double.IsInfinity(sumExp) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
if sumExp <= 0 then
|
||
begin
|
||
var uniform := 1.0 / classCount;
|
||
for var cls := 0 to classCount - 1 do
|
||
probs[cls] := uniform;
|
||
exit;
|
||
end;
|
||
|
||
var inv := 1.0 / sumExp;
|
||
|
||
for var cls := 0 to classCount - 1 do
|
||
begin
|
||
var p := probs[cls] * inv;
|
||
|
||
// clamp для полной численной безопасности
|
||
if p < 0 then
|
||
p := 0.0
|
||
else if p > 1 then
|
||
p := 1.0;
|
||
|
||
probs[cls] := p;
|
||
end;
|
||
end;
|
||
|
||
procedure GradientBoostingClassifier.SoftmaxMatrix(
|
||
logits: Matrix;
|
||
probs: Matrix);
|
||
begin
|
||
var nSamples := logits.RowCount;
|
||
var classCount := logits.ColCount;
|
||
|
||
for var i := 0 to nSamples - 1 do
|
||
begin
|
||
var rowLogits := new real[classCount];
|
||
var rowProbs := new real[classCount];
|
||
|
||
for var cls := 0 to classCount - 1 do
|
||
rowLogits[cls] := logits[i, cls];
|
||
|
||
SoftmaxRow(rowLogits, rowProbs);
|
||
|
||
for var cls := 0 to classCount - 1 do
|
||
probs[i, cls] := rowProbs[cls];
|
||
end;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.ComputeLogLoss(
|
||
yEncoded: array of integer;
|
||
probs: Matrix): real;
|
||
begin
|
||
var n := Length(yEncoded);
|
||
|
||
if n = 0 then
|
||
exit(0.0);
|
||
|
||
if probs = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if probs.RowCount <> n then
|
||
DimensionError(ER_XY_SIZE_MISMATCH, probs.RowCount, n);
|
||
|
||
var eps := 1e-12;
|
||
var loss := 0.0;
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var cls := yEncoded[i];
|
||
|
||
if (cls < 0) or (cls >= probs.ColCount) then
|
||
ArgumentError(ER_LABEL_INDEX_INVALID);
|
||
|
||
var p := probs[i, cls];
|
||
|
||
if double.IsNaN(p) or double.IsInfinity(p) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
if p < eps then
|
||
p := eps
|
||
else if p > 1 - eps then
|
||
p := 1 - eps;
|
||
|
||
loss -= Ln(p);
|
||
end;
|
||
|
||
var resultLoss := loss / n;
|
||
|
||
if resultLoss < 0 then
|
||
resultLoss := 0.0;
|
||
|
||
if double.IsNaN(resultLoss) or double.IsInfinity(resultLoss) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
Result := resultLoss;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.ComputeLogLossMasked(
|
||
yEncoded: array of integer;
|
||
logits: Matrix;
|
||
mask: array of boolean): real;
|
||
begin
|
||
if logits = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
var n := logits.RowCount;
|
||
var k := logits.ColCount;
|
||
|
||
if k = 0 then
|
||
ArgumentError(ER_SOFTMAX_EMPTY);
|
||
|
||
if Length(yEncoded) <> n then
|
||
DimensionError(ER_XY_SIZE_MISMATCH, Length(yEncoded), n);
|
||
|
||
if Length(mask) <> n then
|
||
DimensionError(ER_XY_SIZE_MISMATCH, Length(mask), n);
|
||
|
||
var rowLogits := new real[k];
|
||
var rowProbs := new real[k];
|
||
|
||
var sum := 0.0;
|
||
var cnt := 0;
|
||
var eps := 1e-12;
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
if not mask[i] then
|
||
continue;
|
||
|
||
for var cls := 0 to k - 1 do
|
||
rowLogits[cls] := logits[i, cls];
|
||
|
||
SoftmaxRow(rowLogits, rowProbs);
|
||
|
||
var yi := yEncoded[i];
|
||
|
||
if (yi < 0) or (yi >= k) then
|
||
ArgumentError(ER_LABEL_INDEX_INVALID);
|
||
|
||
var p := rowProbs[yi];
|
||
|
||
if double.IsNaN(p) or double.IsInfinity(p) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
if p < eps then
|
||
p := eps
|
||
else if p > 1 - eps then
|
||
p := 1 - eps;
|
||
|
||
sum -= Ln(p);
|
||
cnt += 1;
|
||
end;
|
||
|
||
if cnt = 0 then
|
||
exit(real.PositiveInfinity); // корректно для OOB
|
||
|
||
var loss := sum / cnt;
|
||
|
||
if loss < 0 then
|
||
loss := 0.0;
|
||
|
||
if double.IsNaN(loss) or double.IsInfinity(loss) then
|
||
ArgumentError(ER_TRAINING_DIVERGED);
|
||
|
||
Result := loss;
|
||
end;
|
||
|
||
constructor GradientBoostingClassifier.Create(
|
||
nEstimators: integer;
|
||
learningRate: real;
|
||
maxDepth: integer;
|
||
minSamplesSplit: integer;
|
||
minSamplesLeaf: integer;
|
||
subsample: real;
|
||
earlyStoppingPatience: integer;
|
||
seed: integer);
|
||
begin
|
||
if nEstimators <= 0 then
|
||
ArgumentOutOfRangeError(ER_N_ESTIMATORS_INVALID, nEstimators);
|
||
|
||
if learningRate <= 0 then
|
||
ArgumentOutOfRangeError(ER_LEARNING_RATE_INVALID, learningRate);
|
||
|
||
if maxDepth <= 0 then
|
||
ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, maxDepth);
|
||
|
||
if minSamplesSplit < 2 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_SPLIT_INVALID, minSamplesSplit);
|
||
|
||
if minSamplesLeaf < 1 then
|
||
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
|
||
|
||
if (subsample <= 0) or (subsample > 1) then
|
||
ArgumentOutOfRangeError(ER_SUBSAMPLE_OUT_OF_RANGE, subsample);
|
||
|
||
if earlyStoppingPatience < 0 then
|
||
ArgumentOutOfRangeError(ER_EARLY_STOPPING_INVALID, earlyStoppingPatience);
|
||
|
||
fNEstimators := nEstimators;
|
||
fLearningRate := learningRate;
|
||
fMaxDepth := maxDepth;
|
||
fMinSamplesSplit := minSamplesSplit;
|
||
fMinSamplesLeaf := minSamplesLeaf;
|
||
fSubsample := subsample;
|
||
fEarlyStoppingPatience := earlyStoppingPatience;
|
||
|
||
fEstimators := new List<array of DecisionTreeRegressor>;
|
||
fTrainLossHistory := new List<real>;
|
||
fValLossHistory := new List<real>;
|
||
|
||
fFitted := false;
|
||
fFeatureCount := 0;
|
||
fClassCount := 0;
|
||
|
||
fBestIteration := -1;
|
||
fBestScoreLoss := real.PositiveInfinity;
|
||
|
||
fOOBLossHistory := new List<real>;
|
||
|
||
if seed < 0 then
|
||
begin
|
||
fUserProvidedSeed := False;
|
||
fRandomSeed := System.Environment.TickCount and integer.MaxValue;
|
||
end
|
||
else
|
||
begin
|
||
fUserProvidedSeed := True;
|
||
fRandomSeed := seed;
|
||
end;
|
||
|
||
fRng := new System.Random(fRandomSeed);
|
||
end;
|
||
|
||
function GradientBoostingClassifier.PredictProba(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
var nSamples := X.RowCount;
|
||
var classCount := fClassCount;
|
||
|
||
var logits := new Matrix(nSamples, classCount);
|
||
|
||
// --- F0
|
||
for var i := 0 to nSamples - 1 do
|
||
for var cls := 0 to classCount - 1 do
|
||
logits[i, cls] := fInitLogits[cls];
|
||
|
||
// --- накопление логитов
|
||
foreach var trees in fEstimators do
|
||
for var cls := 0 to classCount - 1 do
|
||
begin
|
||
var delta := trees[cls].Predict(X);
|
||
|
||
for var i := 0 to nSamples - 1 do
|
||
logits[i, cls] += fLearningRate * delta[i];
|
||
end;
|
||
|
||
var probs := new Matrix(nSamples, classCount);
|
||
|
||
// --- устойчивый softmax
|
||
for var i := 0 to nSamples - 1 do
|
||
begin
|
||
var maxVal := logits[i,0];
|
||
for var cls := 1 to classCount - 1 do
|
||
if logits[i,cls] > maxVal then
|
||
maxVal := logits[i,cls];
|
||
|
||
var sumExp := 0.0;
|
||
|
||
for var cls := 0 to classCount - 1 do
|
||
begin
|
||
probs[i,cls] := Exp(logits[i,cls] - maxVal);
|
||
sumExp += probs[i,cls];
|
||
end;
|
||
|
||
if Abs(sumExp) < 1e-12 then
|
||
begin
|
||
// численно вырожденный случай
|
||
var uniform := 1.0 / classCount;
|
||
for var cls := 0 to classCount - 1 do
|
||
probs[i,cls] := uniform;
|
||
end
|
||
else
|
||
begin
|
||
for var cls := 0 to classCount - 1 do
|
||
probs[i,cls] /= sumExp;
|
||
end;
|
||
end;
|
||
|
||
Result := probs;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.GetClasses: array of real;
|
||
begin
|
||
SetLength(Result, fClassCount);
|
||
for var i := 0 to fClassCount - 1 do
|
||
Result[i] := fClasses[i];
|
||
end;
|
||
|
||
function GradientBoostingClassifier.PredictStageProba(
|
||
X: Matrix; m: integer): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
var total := fEstimators.Count;
|
||
|
||
if (m < 0) or (m > total) then
|
||
ArgumentOutOfRangeError(ER_STAGE_OUT_OF_RANGE, m, total);
|
||
|
||
var n := X.RowCount;
|
||
var k := fClassCount;
|
||
|
||
var logits := new Matrix(n, k);
|
||
|
||
// --- F0
|
||
for var i := 0 to n - 1 do
|
||
for var cls := 0 to k - 1 do
|
||
logits[i, cls] := fInitLogits[cls];
|
||
|
||
// --- add first m boosting stages
|
||
for var t := 0 to m - 1 do
|
||
begin
|
||
var trees := fEstimators[t];
|
||
|
||
for var cls := 0 to k - 1 do
|
||
begin
|
||
var delta := trees[cls].Predict(X);
|
||
|
||
for var i := 0 to n - 1 do
|
||
logits[i, cls] += fLearningRate * delta[i];
|
||
end;
|
||
end;
|
||
|
||
var probs := new Matrix(n, k);
|
||
SoftmaxMatrix(logits, probs);
|
||
|
||
Result := probs;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.PredictStage(X: Matrix; m: integer): Vector;
|
||
begin
|
||
var probs := PredictStageProba(X, m);
|
||
|
||
var n := probs.RowCount;
|
||
var resultVec := new Vector(n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var best := 0;
|
||
var bestVal := probs[i,0];
|
||
|
||
for var cls := 1 to probs.ColCount - 1 do
|
||
if probs[i,cls] > bestVal then
|
||
begin
|
||
bestVal := probs[i,cls];
|
||
best := cls;
|
||
end;
|
||
|
||
resultVec[i] := fClasses[best];
|
||
end;
|
||
|
||
Result := resultVec;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.FeatureImportances: Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if fFeatureImportances <> nil then
|
||
exit(fFeatureImportances);
|
||
|
||
var importances := new Vector(fFeatureCount);
|
||
|
||
foreach var trees in fEstimators do
|
||
begin
|
||
for var cls := 0 to fClassCount - 1 do
|
||
begin
|
||
var imp := trees[cls].FeatureImportances;
|
||
|
||
for var j := 0 to fFeatureCount - 1 do
|
||
importances[j] += imp[j];
|
||
end;
|
||
end;
|
||
|
||
// нормализация
|
||
var s := importances.Sum;
|
||
if s > 0 then
|
||
for var j := 0 to fFeatureCount - 1 do
|
||
importances[j] /= s;
|
||
|
||
fFeatureImportances := importances;
|
||
|
||
Result := fFeatureImportances;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.ToString: string;
|
||
begin
|
||
var s :=
|
||
$'GradientBoostingClassifier(' +
|
||
$'n={fNEstimators}, ' +
|
||
$'lr={fLearningRate}, ' +
|
||
$'maxDepth={fMaxDepth}';
|
||
|
||
if fSubsample <> 1.0 then
|
||
s += $', subs={fSubsample}';
|
||
|
||
if fEarlyStoppingPatience > 0 then
|
||
s += $', earlyStop={fEarlyStoppingPatience}';
|
||
|
||
if fUserProvidedSeed then
|
||
s += $', seed={fRandomSeed}';
|
||
|
||
s += ')';
|
||
|
||
Result := s;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.Clone: IModel;
|
||
begin
|
||
var model := new GradientBoostingClassifier(
|
||
fNEstimators,
|
||
fLearningRate,
|
||
fMaxDepth,
|
||
fMinSamplesSplit,
|
||
fMinSamplesLeaf,
|
||
fSubsample,
|
||
fEarlyStoppingPatience,
|
||
fRandomSeed
|
||
);
|
||
|
||
// --- seed ---
|
||
model.fRandomSeed := fRandomSeed;
|
||
model.fUserProvidedSeed := fUserProvidedSeed;
|
||
|
||
if fUserProvidedSeed then
|
||
model.fRng := new System.Random(fRandomSeed)
|
||
else
|
||
model.fRng := new System.Random;
|
||
|
||
// --- fitted state ---
|
||
model.fFitted := fFitted;
|
||
model.fFeatureCount := fFeatureCount;
|
||
model.fClassCount := fClassCount;
|
||
|
||
// --- classes ---
|
||
if fClasses <> nil then
|
||
begin
|
||
SetLength(model.fClasses, Length(fClasses));
|
||
for var i := 0 to Length(fClasses) - 1 do
|
||
model.fClasses[i] := fClasses[i];
|
||
end;
|
||
|
||
if fClassIndex <> nil then
|
||
begin
|
||
model.fClassIndex := new Dictionary<integer, integer>;
|
||
foreach var kv in fClassIndex do
|
||
model.fClassIndex.Add(kv.Key, kv.Value);
|
||
end;
|
||
|
||
// --- estimators ---
|
||
foreach var trees in fEstimators do
|
||
begin
|
||
var newTrees := new DecisionTreeRegressor[Length(trees)];
|
||
for var cls := 0 to Length(trees) - 1 do
|
||
newTrees[cls] := trees[cls].Clone as DecisionTreeRegressor;
|
||
model.fEstimators.Add(newTrees);
|
||
end;
|
||
|
||
// --- histories ---
|
||
foreach var v in fTrainLossHistory do model.fTrainLossHistory.Add(v);
|
||
foreach var v in fValLossHistory do model.fValLossHistory.Add(v);
|
||
foreach var v in fOOBLossHistory do model.fOOBLossHistory.Add(v);
|
||
|
||
model.fBestIteration := fBestIteration;
|
||
model.fBestScoreLoss := fBestScoreLoss;
|
||
|
||
Result := model;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
Result := FitInternal(X, y, nil, nil, false);
|
||
end;
|
||
|
||
function GradientBoostingClassifier.FitWithValidation(
|
||
XTrain: Matrix; yTrain: Vector;
|
||
XVal: Matrix; yVal: Vector): ISupervisedModel;
|
||
begin
|
||
Result := FitInternal(XTrain, yTrain, XVal, yVal, true);
|
||
end;
|
||
|
||
function GradientBoostingClassifier.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
var probs := PredictProba(X);
|
||
|
||
var nSamples := X.RowCount;
|
||
var classCount := fClassCount;
|
||
|
||
Result := new Vector(nSamples);
|
||
|
||
for var i := 0 to nSamples - 1 do
|
||
begin
|
||
var bestClassIndex := 0;
|
||
var bestValue := probs[i, 0];
|
||
|
||
for var cls := 1 to classCount - 1 do
|
||
if probs[i, cls] > bestValue then
|
||
begin
|
||
bestValue := probs[i, cls];
|
||
bestClassIndex := cls;
|
||
end;
|
||
|
||
// возвращаем оригинальную метку
|
||
Result[i] := fClasses[bestClassIndex];
|
||
end;
|
||
end;
|
||
|
||
function GradientBoostingClassifier.PredictLabels(X: Matrix): array of integer;
|
||
begin
|
||
var v := Predict(X);
|
||
|
||
Result := new integer[v.Length];
|
||
|
||
for var i := 0 to v.Length - 1 do
|
||
Result[i] := Round(v[i]);
|
||
end;
|
||
|
||
procedure GradientBoostingClassifier.SetClassLabels(classes: array of string);
|
||
begin
|
||
fClassLabels := Copy(classes);
|
||
end;
|
||
|
||
function GradientBoostingClassifier.GetClassLabels: array of string;
|
||
begin
|
||
if fClassLabels = nil then
|
||
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
|
||
|
||
Result := fClassLabels;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// KNNBase
|
||
//-----------------------------
|
||
|
||
constructor KNNBase.Create(k: integer; weighting: KNNWeighting);
|
||
begin
|
||
if k < 1 then
|
||
ArgumentOutOfRangeError(ER_K_MUST_BE_POSITIVE);
|
||
|
||
fK := k;
|
||
fWeighting := weighting;
|
||
fFitted := False;
|
||
end;
|
||
|
||
procedure KNNBase.ValidatePredictInput(X: Matrix);
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fXTrain.ColCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
end;
|
||
|
||
function KNNBase.SquaredL2(trainRow: integer; XTest: Matrix; testRow: integer): double;
|
||
begin
|
||
var sum := 0.0;
|
||
var d := fXTrain.ColCount;
|
||
|
||
for var j := 0 to d - 1 do
|
||
begin
|
||
var diff := fXTrain[trainRow, j] - XTest[testRow, j];
|
||
sum += diff * diff;
|
||
end;
|
||
|
||
exit(sum);
|
||
end;
|
||
|
||
// QuickSelect(fK - 1); - так вызываем
|
||
|
||
procedure KNNBase.QuickSelect(k: integer);
|
||
begin
|
||
var left := 0;
|
||
var right := fNeighbors.Length - 1;
|
||
|
||
while true do
|
||
begin
|
||
var pivotIndex := Partition(left, right);
|
||
|
||
if pivotIndex = k then
|
||
exit
|
||
else if pivotIndex > k then
|
||
right := pivotIndex - 1
|
||
else
|
||
left := pivotIndex + 1;
|
||
end;
|
||
end;
|
||
|
||
function KNNBase.Partition(left, right: integer): integer;
|
||
begin
|
||
var pivot := fNeighbors[(left + right) div 2].dist;
|
||
|
||
var i := left;
|
||
var j := right;
|
||
|
||
while true do
|
||
begin
|
||
while fNeighbors[i].dist < pivot do i += 1;
|
||
while fNeighbors[j].dist > pivot do j -= 1;
|
||
|
||
if i >= j then
|
||
exit(j);
|
||
|
||
Swap(fNeighbors[i], fNeighbors[j]);
|
||
|
||
i += 1;
|
||
j -= 1;
|
||
end;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// KNNClassifier
|
||
//-----------------------------
|
||
|
||
const KNN_EPS = 1e-12;
|
||
|
||
constructor KNNClassifier.Create(k: integer; weighting: KNNWeighting);
|
||
begin
|
||
inherited Create(k, weighting);
|
||
end;
|
||
|
||
function KNNClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_XY_SIZE_MISMATCH);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if fK > X.RowCount then
|
||
ArgumentOutOfRangeError(ER_K_EXCEEDS_SAMPLES);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
var n := X.RowCount;
|
||
|
||
// --- copy train data
|
||
fXTrain := X.Clone;
|
||
|
||
// =========================================================
|
||
// ЕДИНЫЙ ENCODING
|
||
// =========================================================
|
||
|
||
var yInt := new integer[n];
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var r := y[i];
|
||
var ir := Round(r);
|
||
|
||
if Abs(r - ir) > 1e-12 then
|
||
ArgumentError(ER_LABELS_NOT_INTEGER);
|
||
|
||
yInt[i] := ir;
|
||
end;
|
||
|
||
var classesInt: array of integer;
|
||
var yEncArr := EncodeLabelsInt(yInt, classesInt);
|
||
|
||
fClassCount := classesInt.Length;
|
||
|
||
if fClassCount < 2 then
|
||
ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES);
|
||
|
||
// сохранить оригинальные метки (double API)
|
||
SetLength(fClasses, fClassCount);
|
||
for var i := 0 to fClassCount - 1 do
|
||
fClasses[i] := classesInt[i];
|
||
|
||
// сохранить encoded y
|
||
SetLength(fYEnc, n);
|
||
for var i := 0 to n - 1 do
|
||
fYEnc[i] := yEncArr[i];
|
||
|
||
// =========================================================
|
||
|
||
// --- buffers
|
||
SetLength(fNeighbors, n);
|
||
|
||
SetLength(fVotes, fClassCount);
|
||
SetLength(fMark, fClassCount);
|
||
SetLength(fTouched, fClassCount);
|
||
|
||
fEpoch := 0;
|
||
|
||
fFitted := true;
|
||
|
||
Result := Self;
|
||
end;
|
||
|
||
function KNNClassifier.GetClasses: array of real;
|
||
begin
|
||
Result := fClasses;
|
||
end;
|
||
|
||
function KNNClassifier.Clone: IModel;
|
||
begin
|
||
var clone := new KNNClassifier(fK, fWeighting);
|
||
|
||
if fFitted then
|
||
begin
|
||
clone.fXTrain := fXTrain.Clone;
|
||
clone.fClasses := fClasses.Clone as array of double;
|
||
clone.fYEnc := fYEnc.Clone as array of integer;
|
||
|
||
clone.fClassCount := fClassCount;
|
||
|
||
SetLength(clone.fNeighbors, fNeighbors.Length);
|
||
SetLength(clone.fVotes, fVotes.Length);
|
||
SetLength(clone.fMark, fMark.Length);
|
||
SetLength(clone.fTouched, fTouched.Length);
|
||
|
||
clone.fEpoch := 0;
|
||
clone.fFitted := true;
|
||
end;
|
||
|
||
Result := clone;
|
||
end;
|
||
|
||
function KNNClassifier.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
ValidatePredictInput(X);
|
||
|
||
var m := X.RowCount;
|
||
var n := fXTrain.RowCount;
|
||
|
||
Result := new Vector(m);
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
// заполнить расстояния
|
||
for var t := 0 to n - 1 do
|
||
begin
|
||
fNeighbors[t].dist := SquaredL2(t, X, i);
|
||
fNeighbors[t].idx := t;
|
||
end;
|
||
|
||
// выбрать k ближайших
|
||
QuickSelect(fK - 1);
|
||
|
||
// exact match: если среди k ближайших есть dist=0, возвращаем его класс
|
||
var exactCls := -1;
|
||
for var t := 0 to fK - 1 do
|
||
if fNeighbors[t].dist < KNN_EPS then
|
||
begin
|
||
exactCls := fYEnc[fNeighbors[t].idx];
|
||
break;
|
||
end;
|
||
|
||
if exactCls <> -1 then
|
||
begin
|
||
Result[i] := fClasses[exactCls];
|
||
continue;
|
||
end;
|
||
|
||
// voting (stamping)
|
||
fEpoch += 1;
|
||
var touchCount := 0;
|
||
|
||
if fWeighting = Uniform then
|
||
begin
|
||
for var t := 0 to fK - 1 do
|
||
begin
|
||
var trainIdx := fNeighbors[t].idx;
|
||
var cls := fYEnc[trainIdx];
|
||
|
||
if fMark[cls] <> fEpoch then
|
||
begin
|
||
fMark[cls] := fEpoch;
|
||
fVotes[cls] := 0.0;
|
||
fTouched[touchCount] := cls;
|
||
touchCount += 1;
|
||
end;
|
||
|
||
fVotes[cls] += 1.0;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
// weighted: веса 1 / dist (dist = squared distance)
|
||
for var t := 0 to fK - 1 do
|
||
begin
|
||
var trainIdx := fNeighbors[t].idx;
|
||
var cls := fYEnc[trainIdx];
|
||
var dist := fNeighbors[t].dist;
|
||
|
||
if fMark[cls] <> fEpoch then
|
||
begin
|
||
fMark[cls] := fEpoch;
|
||
fVotes[cls] := 0.0;
|
||
fTouched[touchCount] := cls;
|
||
touchCount += 1;
|
||
end;
|
||
|
||
if dist < KNN_EPS then
|
||
begin
|
||
// считаем как exact match
|
||
exactCls := cls;
|
||
break;
|
||
end
|
||
else
|
||
begin
|
||
var w := 1.0 / Sqrt(dist);
|
||
fVotes[cls] += w;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
if exactCls <> -1 then
|
||
begin
|
||
Result[i] := fClasses[exactCls];
|
||
continue;
|
||
end;
|
||
|
||
// argmax только по touched
|
||
var bestCls := fTouched[0];
|
||
var bestVotes := fVotes[bestCls];
|
||
|
||
for var k2 := 1 to touchCount - 1 do
|
||
begin
|
||
var cls := fTouched[k2];
|
||
var v := fVotes[cls];
|
||
|
||
if (v > bestVotes) or ((v = bestVotes) and (cls < bestCls)) then
|
||
begin
|
||
bestCls := cls;
|
||
bestVotes := v;
|
||
end;
|
||
end;
|
||
|
||
Result[i] := fClasses[bestCls];
|
||
end;
|
||
end;
|
||
|
||
function KNNClassifier.PredictLabels(X: Matrix): array of integer;
|
||
begin
|
||
var v := Predict(X);
|
||
|
||
Result := new integer[v.Length];
|
||
|
||
for var i := 0 to v.Length - 1 do
|
||
Result[i] := Round(v[i]);
|
||
end;
|
||
|
||
function KNNClassifier.PredictProba(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
ValidatePredictInput(X);
|
||
|
||
var m := X.RowCount;
|
||
var n := fXTrain.RowCount;
|
||
|
||
Result := new Matrix(m, fClassCount); // предполагаем нулевую инициализацию
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
// заполнить расстояния
|
||
for var t := 0 to n - 1 do
|
||
begin
|
||
fNeighbors[t].dist := SquaredL2(t, X, i);
|
||
fNeighbors[t].idx := t;
|
||
end;
|
||
|
||
// выбрать k ближайших
|
||
QuickSelect(fK - 1);
|
||
|
||
// exact match: если среди k ближайших есть dist=0, вероятность 1 у его класса
|
||
var exactCls := -1;
|
||
for var t := 0 to fK - 1 do
|
||
if fNeighbors[t].dist < KNN_EPS then
|
||
begin
|
||
exactCls := fYEnc[fNeighbors[t].idx];
|
||
break;
|
||
end;
|
||
|
||
if exactCls <> -1 then
|
||
begin
|
||
Result[i, exactCls] := 1.0;
|
||
continue;
|
||
end;
|
||
|
||
// voting (stamping)
|
||
fEpoch += 1;
|
||
var touchCount := 0;
|
||
|
||
if fWeighting = Uniform then
|
||
begin
|
||
for var t := 0 to fK - 1 do
|
||
begin
|
||
var trainIdx := fNeighbors[t].idx;
|
||
var cls := fYEnc[trainIdx];
|
||
|
||
if fMark[cls] <> fEpoch then
|
||
begin
|
||
fMark[cls] := fEpoch;
|
||
fVotes[cls] := 0.0;
|
||
fTouched[touchCount] := cls;
|
||
touchCount += 1;
|
||
end;
|
||
|
||
fVotes[cls] += 1.0;
|
||
end;
|
||
|
||
// нормализация: сумма = k
|
||
for var k2 := 0 to touchCount - 1 do
|
||
begin
|
||
var cls := fTouched[k2];
|
||
Result[i, cls] := fVotes[cls] / fK;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
var sumW := 0.0;
|
||
|
||
for var t := 0 to fK - 1 do
|
||
begin
|
||
var trainIdx := fNeighbors[t].idx;
|
||
var cls := fYEnc[trainIdx];
|
||
var dist := fNeighbors[t].dist;
|
||
|
||
if dist < KNN_EPS then
|
||
begin
|
||
exactCls := cls;
|
||
break;
|
||
end;
|
||
|
||
if fMark[cls] <> fEpoch then
|
||
begin
|
||
fMark[cls] := fEpoch;
|
||
fVotes[cls] := 0.0;
|
||
fTouched[touchCount] := cls;
|
||
touchCount += 1;
|
||
end;
|
||
|
||
var w := 1.0 / Sqrt(dist); // согласовано с Predict
|
||
fVotes[cls] += w;
|
||
sumW += w;
|
||
end;
|
||
|
||
// ОБЯЗАТЕЛЬНО проверить exact снова
|
||
|
||
if exactCls <> -1 then
|
||
begin
|
||
Result[i, exactCls] := 1.0;
|
||
continue;
|
||
end;
|
||
|
||
// нормализация
|
||
if sumW > 0 then
|
||
for var k2 := 0 to touchCount - 1 do
|
||
begin
|
||
var cls := fTouched[k2];
|
||
Result[i, cls] := fVotes[cls] / sumW;
|
||
end
|
||
else
|
||
begin
|
||
// fallback — равномерное распределение
|
||
var uniform := 1.0 / touchCount;
|
||
for var k2 := 0 to touchCount - 1 do
|
||
begin
|
||
var cls := fTouched[k2];
|
||
Result[i, cls] := uniform;
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure KNNClassifier.SetClassLabels(classes: array of string);
|
||
begin
|
||
fClassLabels := Copy(classes);
|
||
end;
|
||
|
||
function KNNClassifier.GetClassLabels: array of string;
|
||
begin
|
||
if fClassLabels = nil then
|
||
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
|
||
|
||
Result := fClassLabels;
|
||
end;
|
||
|
||
|
||
//-----------------------------
|
||
// KNNRegressor
|
||
//-----------------------------
|
||
|
||
constructor KNNRegressor.Create(k: integer; weighting: KNNWeighting);
|
||
begin
|
||
inherited Create(k, weighting);
|
||
end;
|
||
|
||
function KNNRegressor.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_XY_SIZE_MISMATCH);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if fK > X.RowCount then
|
||
ArgumentOutOfRangeError(ER_K_EXCEEDS_SAMPLES);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
begin
|
||
CheckXForFit(X);
|
||
CheckYForFit(y);
|
||
end;
|
||
|
||
fXTrain := X.Clone;
|
||
fYTrain := y.Clone;
|
||
|
||
var n := fXTrain.RowCount;
|
||
SetLength(fNeighbors, n);
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function KNNRegressor.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
ValidatePredictInput(X);
|
||
|
||
var m := X.RowCount;
|
||
var n := fXTrain.RowCount;
|
||
|
||
Result := new Vector(m);
|
||
|
||
for var i := 0 to m - 1 do
|
||
begin
|
||
// --- вычисление расстояний
|
||
for var t := 0 to n - 1 do
|
||
begin
|
||
fNeighbors[t].dist := SquaredL2(t, X, i);
|
||
fNeighbors[t].idx := t;
|
||
end;
|
||
|
||
// --- выбрать k ближайших
|
||
QuickSelect(fK - 1);
|
||
|
||
// --- exact match
|
||
var exactIdx := -1;
|
||
for var t := 0 to fK - 1 do
|
||
if fNeighbors[t].dist < KNN_EPS then
|
||
begin
|
||
exactIdx := fNeighbors[t].idx;
|
||
break;
|
||
end;
|
||
|
||
if exactIdx <> -1 then
|
||
begin
|
||
Result[i] := fYTrain[exactIdx];
|
||
continue;
|
||
end;
|
||
|
||
// --- uniform weighting
|
||
if fWeighting = KNNWeighting.Uniform then
|
||
begin
|
||
var sum := 0.0;
|
||
|
||
for var t := 0 to fK - 1 do
|
||
sum += fYTrain[fNeighbors[t].idx];
|
||
|
||
Result[i] := sum / fK;
|
||
end
|
||
else
|
||
begin
|
||
// --- distance weighting
|
||
var sumW := 0.0;
|
||
var sumWY := 0.0;
|
||
var exactFound := false;
|
||
var exactValue := 0.0;
|
||
|
||
for var t := 0 to fK - 1 do
|
||
begin
|
||
var idx := fNeighbors[t].idx;
|
||
var dist := fNeighbors[t].dist;
|
||
|
||
if dist < KNN_EPS then
|
||
begin
|
||
exactFound := true;
|
||
exactValue := fYTrain[idx];
|
||
break;
|
||
end;
|
||
|
||
var w := 1.0 / Sqrt(dist);
|
||
sumW += w;
|
||
sumWY += w * fYTrain[idx];
|
||
end;
|
||
|
||
if exactFound then
|
||
begin
|
||
Result[i] := exactValue;
|
||
continue;
|
||
end;
|
||
|
||
if sumW > 0 then
|
||
Result[i] := sumWY / sumW
|
||
else
|
||
begin
|
||
// fallback — обычное среднее
|
||
var sum := 0.0;
|
||
for var t := 0 to fK - 1 do
|
||
sum += fYTrain[fNeighbors[t].idx];
|
||
|
||
Result[i] := sum / fK;
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function KNNRegressor.Clone: IModel;
|
||
begin
|
||
var clone := new KNNRegressor(fK, fWeighting);
|
||
|
||
if fFitted then
|
||
begin
|
||
clone.fXTrain := fXTrain.Clone;
|
||
clone.fYTrain := fYTrain.Clone;
|
||
|
||
SetLength(clone.fNeighbors, fNeighbors.Length);
|
||
|
||
clone.fFitted := True;
|
||
end;
|
||
|
||
Result := clone;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// KMeans
|
||
//-----------------------------
|
||
|
||
constructor KMeans.Create(
|
||
nClusters: integer;
|
||
maxIter: integer;
|
||
tol: real;
|
||
nInit: integer;
|
||
seed: integer
|
||
);
|
||
begin
|
||
if nClusters < 1 then
|
||
ArgumentOutOfRangeError(ER_K_INVALID, nClusters);
|
||
|
||
if maxIter < 1 then
|
||
ArgumentOutOfRangeError(ER_MAXITER_INVALID, maxIter);
|
||
|
||
if tol <= 0 then
|
||
ArgumentOutOfRangeError(ER_TOL_INVALID, tol);
|
||
|
||
if nInit < 1 then
|
||
ArgumentOutOfRangeError(ER_NINIT_INVALID, nInit);
|
||
|
||
fNClusters := nClusters;
|
||
fMaxIter := maxIter;
|
||
fTol := tol;
|
||
fNInit := nInit;
|
||
|
||
// --- seed (единый стиль)
|
||
fRandomSeed := seed;
|
||
fUserProvidedSeed := seed >= 0;
|
||
|
||
if fUserProvidedSeed then
|
||
fRng := new System.Random(seed)
|
||
else
|
||
fRng := new System.Random;
|
||
|
||
// --- state
|
||
fFitted := False;
|
||
fFeatureCount := 0;
|
||
|
||
fCenters := nil;
|
||
fInertia := 0.0;
|
||
fIterations := 0;
|
||
fHasConverged := False;
|
||
end;
|
||
|
||
function KMeans.RunSingle(X: Matrix; rnd: System.Random): (Matrix, real, integer, boolean);
|
||
begin
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
var k := fNClusters;
|
||
|
||
// --- 1. Инициализация центроидов (случайные строки X)
|
||
|
||
var idx := Arr(0..n-1);
|
||
idx.Shuffle(rnd);
|
||
|
||
var centers := new Matrix(k, p);
|
||
|
||
for var c := 0 to k - 1 do
|
||
begin
|
||
var r := idx[c];
|
||
for var j := 0 to p - 1 do
|
||
centers[c,j] := X[r,j];
|
||
end;
|
||
|
||
var inertia := 0.0;
|
||
var converged := False;
|
||
var iterations := 0;
|
||
|
||
// --- 2. Основной цикл
|
||
for var iter := 1 to fMaxIter do
|
||
begin
|
||
iterations := iter;
|
||
|
||
// Assignment + накопление сумм
|
||
var counts := new integer[k];
|
||
var sums := new Matrix(k, p);
|
||
|
||
inertia := 0.0;
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var bestC := 0;
|
||
var bestDist := double.MaxValue;
|
||
|
||
for var c := 0 to k - 1 do
|
||
begin
|
||
var dist := 0.0;
|
||
|
||
for var j := 0 to p - 1 do
|
||
begin
|
||
var xij := X[i,j];
|
||
if double.IsNaN(xij) or double.IsInfinity(xij) then
|
||
ArgumentError(ER_INVALID_VALUE_AT, 'X', i);
|
||
|
||
var cj := centers[c,j];
|
||
if double.IsNaN(cj) or double.IsInfinity(cj) then
|
||
ArgumentError(ER_INVALID_VALUE_AT, 'Centers', c);
|
||
|
||
var d := xij - cj;
|
||
dist += d * d;
|
||
end;
|
||
|
||
if dist < bestDist then
|
||
begin
|
||
bestDist := dist;
|
||
bestC := c;
|
||
end;
|
||
end;
|
||
|
||
inertia += bestDist;
|
||
|
||
counts[bestC] += 1;
|
||
|
||
for var j := 0 to p - 1 do
|
||
sums[bestC,j] += X[i,j];
|
||
end;
|
||
|
||
// --- 3. Пересчёт центроидов
|
||
var maxShift := 0.0;
|
||
|
||
for var c := 0 to k - 1 do
|
||
begin
|
||
if counts[c] = 0 then
|
||
begin
|
||
// Пустой кластер — переинициализация случайной точкой
|
||
var r := rnd.Next(n);
|
||
for var j := 0 to p - 1 do
|
||
centers[c,j] := X[r,j];
|
||
continue;
|
||
end;
|
||
|
||
for var j := 0 to p - 1 do
|
||
begin
|
||
var newVal := sums[c,j] / counts[c];
|
||
var shift := Abs(newVal - centers[c,j]);
|
||
|
||
if shift > maxShift then
|
||
maxShift := shift;
|
||
|
||
centers[c,j] := newVal;
|
||
end;
|
||
end;
|
||
|
||
if maxShift < fTol then
|
||
begin
|
||
converged := True;
|
||
break;
|
||
end;
|
||
end;
|
||
|
||
Result := (centers, inertia, iterations, converged);
|
||
end;
|
||
|
||
function KMeans.Fit(X: Matrix): IUnsupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_ARG_NULL, 'X');
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
if n = 0 then
|
||
ArgumentError(ER_EMPTY_DATA, 'KMeans');
|
||
|
||
if p = 0 then
|
||
ArgumentError(ER_EMPTY_DATA, 'KMeans');
|
||
|
||
if fNClusters > n then
|
||
ArgumentOutOfRangeError(ER_K_INVALID, fNClusters);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForFit(X);
|
||
|
||
fFeatureCount := p;
|
||
|
||
// --- базовый RNG (уже создан в конструкторе)
|
||
var rndBase := fRng;
|
||
|
||
var bestInertia := 1e308;
|
||
var bestCenters: Matrix := nil;
|
||
var bestIterations := 0;
|
||
var bestConverged := False;
|
||
|
||
for var run := 1 to fNInit do
|
||
begin
|
||
// --- независимый RNG для каждого запуска
|
||
var runSeed := rndBase.Next(integer.MaxValue);
|
||
var rnd := new System.Random(runSeed);
|
||
|
||
var (centers, inertia, iters, converged) := RunSingle(X, rnd);
|
||
|
||
if inertia < bestInertia then
|
||
begin
|
||
bestInertia := inertia;
|
||
bestCenters := centers;
|
||
bestIterations := iters;
|
||
bestConverged := converged;
|
||
end;
|
||
end;
|
||
|
||
fCenters := bestCenters;
|
||
fInertia := bestInertia;
|
||
fIterations := bestIterations;
|
||
fHasConverged := bestConverged;
|
||
fFitted := True;
|
||
|
||
Result := Self;
|
||
end;
|
||
|
||
function KMeans.PredictLabels(X: Matrix): array of integer;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_ARG_NULL, 'X');
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_DIM_MISMATCH, X.ColCount, fFeatureCount);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
var k := fNClusters;
|
||
|
||
SetLength(Result, n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var bestC := 0;
|
||
var bestDist := 1e308;
|
||
|
||
for var c := 0 to k - 1 do
|
||
begin
|
||
var dist := 0.0;
|
||
|
||
for var j := 0 to p - 1 do
|
||
begin
|
||
var xij := X[i,j];
|
||
if double.IsNaN(xij) or double.IsInfinity(xij) then
|
||
ArgumentError(ER_INVALID_VALUE_AT, 'X', i);
|
||
|
||
var cj := fCenters[c,j];
|
||
if double.IsNaN(cj) or double.IsInfinity(cj) then
|
||
ArgumentError(ER_INVALID_VALUE_AT, 'Centers', c);
|
||
|
||
var d := xij - cj;
|
||
dist += d * d;
|
||
end;
|
||
|
||
if dist < bestDist then
|
||
begin
|
||
bestDist := dist;
|
||
bestC := c;
|
||
end;
|
||
end;
|
||
|
||
Result[i] := bestC; // 0..k-1
|
||
end;
|
||
end;
|
||
|
||
function KMeans.Predict(X: Matrix): Vector;
|
||
begin
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
var labels := PredictLabels(X);
|
||
|
||
var n := Length(labels);
|
||
Result := new Vector(n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
Result[i] := labels[i];
|
||
end;
|
||
|
||
function KMeans.FitPredict(X: Matrix): array of integer;
|
||
begin
|
||
Fit(X);
|
||
Result := PredictLabels(X);
|
||
end;
|
||
|
||
function KMeans.Clone: IModel;
|
||
begin
|
||
var km := new KMeans(
|
||
fNClusters,
|
||
fMaxIter,
|
||
fTol,
|
||
fNInit,
|
||
fRandomSeed
|
||
);
|
||
|
||
// --- seed ---
|
||
km.fRandomSeed := fRandomSeed;
|
||
km.fUserProvidedSeed := fUserProvidedSeed;
|
||
|
||
if fUserProvidedSeed then
|
||
km.fRng := new System.Random(fRandomSeed)
|
||
else
|
||
km.fRng := new System.Random;
|
||
|
||
// --- state ---
|
||
km.fFitted := fFitted;
|
||
km.fFeatureCount := fFeatureCount;
|
||
km.fInertia := fInertia;
|
||
km.fIterations := fIterations;
|
||
km.fHasConverged := fHasConverged;
|
||
|
||
// --- centers ---
|
||
if fCenters <> nil then
|
||
km.fCenters := fCenters.Clone;
|
||
|
||
Result := km;
|
||
end;
|
||
|
||
function KMeans.ClustersCount: integer;
|
||
begin
|
||
if not fFitted then
|
||
ArgumentError(ER_MODEL_NOT_FITTED, 'KMeans');
|
||
|
||
Result := fCenters.RowCount;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// DBSCAN
|
||
//-----------------------------
|
||
|
||
constructor DBSCAN.Create(
|
||
eps: real;
|
||
minSamples: integer
|
||
);
|
||
begin
|
||
if eps <= 0 then
|
||
ArgumentOutOfRangeError(ER_EPS_INVALID, eps);
|
||
|
||
if minSamples < 1 then
|
||
ArgumentOutOfRangeError(ER_MINSAMPLES_INVALID, minSamples);
|
||
|
||
fEps := eps;
|
||
fMinSamples := minSamples;
|
||
|
||
fFitted := False;
|
||
end;
|
||
|
||
function DBSCAN.RegionQuery(X: Matrix; i: integer; neighbors: List<integer>): integer;
|
||
begin
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
var eps2 := fEps * fEps;
|
||
|
||
neighbors.Clear;
|
||
|
||
for var j := 0 to n-1 do
|
||
begin
|
||
var dist := 0.0;
|
||
|
||
for var k := 0 to p-1 do
|
||
begin
|
||
var d := X[i,k] - X[j,k];
|
||
dist += d*d;
|
||
end;
|
||
|
||
if dist <= eps2 then
|
||
neighbors.Add(j);
|
||
end;
|
||
|
||
Result := neighbors.Count;
|
||
end;
|
||
|
||
function DBSCAN.Fit(X: Matrix): IUnsupervisedModel;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_ARG_NULL, 'X');
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForFit(X);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
if n = 0 then
|
||
ArgumentError(ER_EMPTY_DATA, 'DBSCAN');
|
||
|
||
fFeatureCount := p;
|
||
|
||
var labels := new integer[n];
|
||
|
||
for var i := 0 to n - 1 do
|
||
labels[i] := -1; // noise
|
||
|
||
var visited := new boolean[n];
|
||
|
||
var neighbors := new List<integer>;
|
||
var queue := new Queue<integer>;
|
||
|
||
var clusterId := 0;
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
if visited[i] then
|
||
continue;
|
||
|
||
visited[i] := True;
|
||
|
||
var count := RegionQuery(X, i, neighbors);
|
||
|
||
if count < fMinSamples then
|
||
continue;
|
||
|
||
labels[i] := clusterId;
|
||
|
||
queue.Clear;
|
||
|
||
foreach var j in neighbors do
|
||
queue.Enqueue(j);
|
||
|
||
while queue.Count > 0 do
|
||
begin
|
||
var q := queue.Dequeue;
|
||
|
||
if not visited[q] then
|
||
begin
|
||
visited[q] := True;
|
||
|
||
var count2 := RegionQuery(X, q, neighbors);
|
||
|
||
if count2 >= fMinSamples then
|
||
foreach var t in neighbors do
|
||
queue.Enqueue(t);
|
||
end;
|
||
|
||
if labels[q] = -1 then
|
||
labels[q] := clusterId;
|
||
end;
|
||
|
||
clusterId += 1;
|
||
end;
|
||
|
||
fClusterCount := clusterId;
|
||
fLabels := labels;
|
||
fFitted := True;
|
||
|
||
Result := Self;
|
||
end;
|
||
|
||
function DBSCAN.PredictLabels(X: Matrix): array of integer;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_ARG_NULL, 'X');
|
||
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if MLConfig.ValidateFiniteInputs then
|
||
CheckXForPredict(X);
|
||
|
||
if X.RowCount <> Length(fLabels) then
|
||
ArgumentError(ER_DBSCAN_PREDICT_NEW_DATA);
|
||
|
||
Result := Copy(fLabels);
|
||
end;
|
||
|
||
function DBSCAN.FitPredict(X: Matrix): array of integer;
|
||
begin
|
||
Fit(X);
|
||
Result := PredictLabels(X);
|
||
end;
|
||
|
||
function DBSCAN.Clone: IModel;
|
||
begin
|
||
var m := new DBSCAN(fEps, fMinSamples);
|
||
|
||
m.fFitted := fFitted;
|
||
m.fFeatureCount := fFeatureCount;
|
||
m.fClusterCount := fClusterCount;
|
||
|
||
if fLabels <> nil then
|
||
begin
|
||
var n := Length(fLabels);
|
||
SetLength(m.fLabels, n);
|
||
|
||
for var i := 0 to n - 1 do
|
||
m.fLabels[i] := fLabels[i];
|
||
end;
|
||
|
||
Result := m;
|
||
end;
|
||
|
||
function DBSCAN.ClustersCount: integer := fClusterCount;
|
||
|
||
//-----------------------------
|
||
// Pipeline
|
||
//-----------------------------
|
||
|
||
constructor Pipeline.Create;
|
||
begin
|
||
fTransformers := new List<ITransformer>;
|
||
fModel := nil;
|
||
fFitted := false;
|
||
end;
|
||
|
||
constructor Pipeline.Create(model: ISupervisedModel);
|
||
begin
|
||
Create;
|
||
if model = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
fModel := model;
|
||
end;
|
||
|
||
class function Pipeline.Build(params steps: array of IPipelineStep): Pipeline;
|
||
begin
|
||
if (steps = nil) or (Length(steps) = 0) then
|
||
ArgumentError(ER_PIPELINE_NO_STEPS);
|
||
|
||
// последний шаг
|
||
var last := steps[High(steps)];
|
||
|
||
if last = nil then
|
||
ArgumentError(ER_PIPELINE_STEP_NULL, High(steps));
|
||
|
||
if not (last is ISupervisedModel) then
|
||
ArgumentError(ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL);
|
||
|
||
var pipe := new Pipeline(last as ISupervisedModel);
|
||
|
||
// все шаги кроме последнего — трансформеры
|
||
for var i := 0 to High(steps) - 1 do
|
||
begin
|
||
var step := steps[i];
|
||
|
||
if step = nil then
|
||
ArgumentError(ER_PIPELINE_STEP_NULL, i);
|
||
|
||
if not (step is ITransformer) then
|
||
ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER);
|
||
|
||
pipe.Add(step as ITransformer);
|
||
end;
|
||
|
||
Result := pipe;
|
||
end;
|
||
|
||
function Pipeline.Add(t: ITransformer): Pipeline;
|
||
begin
|
||
if t = nil then
|
||
ArgumentError(ER_TRANSFORMER_NULL);
|
||
|
||
fTransformers.Add(t);
|
||
Result := Self;
|
||
end;
|
||
|
||
function Pipeline.SetModel(m: ISupervisedModel): Pipeline;
|
||
begin
|
||
if m = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
fModel := m;
|
||
Result := Self;
|
||
end;
|
||
|
||
function Pipeline.Fit(X: Matrix; y: Vector): ISupervisedModel;
|
||
begin
|
||
if fModel = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_XY_SIZE_MISMATCH);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
var Xt := X;
|
||
|
||
for var i := 0 to fTransformers.Count - 1 do
|
||
begin
|
||
var t := fTransformers[i];
|
||
|
||
if t = nil then
|
||
ArgumentError(ER_PIPELINE_STEP_NULL);
|
||
|
||
if t is ISupervisedTransformer(var sup) then
|
||
fTransformers[i] := sup.Fit(Xt, y)
|
||
else if t is IUnsupervisedTransformer(var unsup) then
|
||
fTransformers[i] := unsup.Fit(Xt)
|
||
else
|
||
ArgumentError(ER_PIPELINE_TRANSFORMER_NO_FIT, i, t.GetType.Name);
|
||
|
||
Xt := fTransformers[i].Transform(Xt);
|
||
|
||
if Xt = nil then
|
||
ArgumentError(ER_PIPELINE_TRANSFORM_RETURNED_NULL);
|
||
end;
|
||
|
||
if fModel is ISupervisedModel(var supModel) then
|
||
fModel := supModel.Fit(Xt, y)
|
||
else
|
||
ArgumentError(ER_Model_NoFit, fModel.GetType.Name);
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function Pipeline.Transform(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
var Xt := X;
|
||
|
||
foreach var t in fTransformers do
|
||
begin
|
||
if t = nil then
|
||
ArgumentError(ER_PIPELINE_STEP_NULL);
|
||
|
||
Xt := t.Transform(Xt);
|
||
|
||
if Xt = nil then
|
||
ArgumentError(ER_PIPELINE_TRANSFORM_RETURNED_NULL);
|
||
end;
|
||
|
||
Result := Xt;
|
||
end;
|
||
|
||
function Pipeline.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if fModel = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
var Xt := Transform(X);
|
||
Result := fModel.Predict(Xt);
|
||
end;
|
||
|
||
function Pipeline.PredictProba(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if fModel = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
if not (fModel is IProbabilisticClassifier) then
|
||
ArgumentError(ER_PROBA_NOT_SUPPORTED);
|
||
|
||
var Xt := Transform(X);
|
||
|
||
Result := (fModel as IProbabilisticClassifier)
|
||
.PredictProba(Xt);
|
||
end;
|
||
|
||
function Pipeline.ToString: string;
|
||
begin
|
||
var sb := 'Pipeline (' +
|
||
(if fFitted then 'trained' else 'not trained') + '):' + NewLine;
|
||
|
||
var idx := 1;
|
||
|
||
foreach var t in fTransformers do
|
||
begin
|
||
sb += ' [' + idx + '] ' + t.ToString + NewLine;
|
||
idx += 1;
|
||
end;
|
||
|
||
if fModel <> nil then
|
||
sb += ' [' + idx + '] ' + fModel.ToString;
|
||
|
||
Result := sb;
|
||
end;
|
||
|
||
function Pipeline.Clone: IModel;
|
||
begin
|
||
if fModel = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
var p := new Pipeline;
|
||
|
||
foreach var t in fTransformers do
|
||
p.Add(t.Clone);
|
||
|
||
var m := fModel.Clone;
|
||
|
||
if not (m is ISupervisedModel) then
|
||
Error(ER_INTERNAL_INVALID_MODEL_CLONE);
|
||
|
||
p.SetModel(m as ISupervisedModel);
|
||
|
||
p.fFitted := fFitted;
|
||
|
||
Result := p;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// UPipeline
|
||
//-----------------------------
|
||
constructor UPipeline.Create;
|
||
begin
|
||
fTransformers := new List<ITransformer>;
|
||
fModel := nil;
|
||
fFitted := false;
|
||
end;
|
||
|
||
constructor UPipeline.Create(model: IModel);
|
||
begin
|
||
Create;
|
||
if model = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
fModel := model;
|
||
end;
|
||
|
||
class function UPipeline.Build(params steps: array of IPipelineStep): UPipeline;
|
||
begin
|
||
if (steps = nil) or (Length(steps) = 0) then
|
||
ArgumentError(ER_PIPELINE_NO_STEPS);
|
||
|
||
var last := steps[High(steps)];
|
||
|
||
if last = nil then
|
||
ArgumentError(ER_PIPELINE_STEP_NULL, High(steps));
|
||
|
||
if not (last is IModel) then
|
||
ArgumentError(ER_PIPELINE_LAST_NOT_MODEL);
|
||
|
||
var pipe := new UPipeline(last as IModel);
|
||
|
||
for var i := 0 to High(steps) - 1 do
|
||
begin
|
||
var step := steps[i];
|
||
|
||
if step = nil then
|
||
ArgumentError(ER_PIPELINE_STEP_NULL, i);
|
||
|
||
if not (step is ITransformer) then
|
||
ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER);
|
||
|
||
pipe.Add(step as ITransformer);
|
||
end;
|
||
|
||
Result := pipe;
|
||
end;
|
||
|
||
function UPipeline.Add(t: ITransformer): UPipeline;
|
||
begin
|
||
if t = nil then
|
||
ArgumentError(ER_TRANSFORMER_NULL);
|
||
|
||
fTransformers.Add(t);
|
||
Result := Self;
|
||
end;
|
||
|
||
function UPipeline.SetModel(m: IModel): UPipeline;
|
||
begin
|
||
if m = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
fModel := m;
|
||
Result := Self;
|
||
end;
|
||
|
||
function UPipeline.Fit(X: Matrix): IUnsupervisedModel;
|
||
begin
|
||
if fModel = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
var Xt := X;
|
||
|
||
for var i := 0 to fTransformers.Count - 1 do
|
||
begin
|
||
var t := fTransformers[i];
|
||
|
||
if t = nil then
|
||
ArgumentError(ER_PIPELINE_STEP_NULL);
|
||
|
||
if t is IUnsupervisedTransformer(var unsup) then
|
||
fTransformers[i] := unsup.Fit(Xt)
|
||
else
|
||
ArgumentError(ER_PIPELINE_TRANSFORMER_NO_FIT, i, t.GetType.Name);
|
||
|
||
Xt := fTransformers[i].Transform(Xt);
|
||
|
||
if Xt = nil then
|
||
ArgumentError(ER_PIPELINE_TRANSFORM_RETURNED_NULL);
|
||
end;
|
||
|
||
if fModel is IUnsupervisedModel(var m) then
|
||
fModel := m.Fit(Xt)
|
||
else
|
||
ArgumentError(ER_MODEL_NOT_UNSUPERVISED, fModel.GetType.Name);
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function UPipeline.Transform(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
var Xt := X;
|
||
|
||
foreach var t in fTransformers do
|
||
begin
|
||
if t = nil then
|
||
ArgumentError(ER_PIPELINE_STEP_NULL);
|
||
|
||
Xt := t.Transform(Xt);
|
||
|
||
if Xt = nil then
|
||
ArgumentError(ER_PIPELINE_TRANSFORM_RETURNED_NULL);
|
||
end;
|
||
|
||
Result := Xt;
|
||
end;
|
||
|
||
function UPipeline.Predict(X: Matrix): Vector;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if fModel = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
var Xt := Transform(X);
|
||
|
||
if not (fModel is IPredictiveModel) then
|
||
Error(ER_PREDICT_NOT_SUPPORTED);
|
||
|
||
Result := (fModel as IPredictiveModel).Predict(Xt);
|
||
end;
|
||
|
||
function UPipeline.ToString: string;
|
||
begin
|
||
var sb := 'UPipeline (' +
|
||
(if fFitted then 'trained' else 'not trained') + '):' + NewLine;
|
||
|
||
var idx := 1;
|
||
|
||
foreach var t in fTransformers do
|
||
begin
|
||
sb += ' [' + idx + '] ' + t.ToString + NewLine;
|
||
idx += 1;
|
||
end;
|
||
|
||
if fModel <> nil then
|
||
sb += ' [' + idx + '] ' + fModel.ToString;
|
||
|
||
Result := sb;
|
||
end;
|
||
|
||
function UPipeline.Clone: IModel;
|
||
begin
|
||
if fModel = nil then
|
||
ArgumentError(ER_MODEL_NULL);
|
||
|
||
var p := new UPipeline;
|
||
|
||
foreach var t in fTransformers do
|
||
p.Add(t.Clone);
|
||
|
||
p.SetModel(fModel.Clone);
|
||
|
||
p.fFitted := fFitted;
|
||
|
||
Result := p;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// StandardScaler
|
||
//-----------------------------
|
||
|
||
function StandardScaler.Fit(X: Matrix): IUnsupervisedTransformer;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
fFeatureCount := X.ColCount;
|
||
|
||
fMean := X.ColumnMeans;
|
||
fStd := X.ColumnStd;
|
||
|
||
// защита от нулевой дисперсии
|
||
for var j := 0 to fStd.Length - 1 do
|
||
if Abs(fStd[j]) < 1e-12 then
|
||
fStd[j] := 1.0;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function StandardScaler.Transform(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
Result := new Matrix(n, p);
|
||
|
||
for var i := 0 to n - 1 do
|
||
for var j := 0 to p - 1 do
|
||
Result[i,j] := (X[i,j] - fMean[j]) / fStd[j];
|
||
end;
|
||
|
||
function StandardScaler.ToString: string;
|
||
begin
|
||
Result := 'StandardScaler';
|
||
end;
|
||
|
||
function StandardScaler.Clone: ITransformer;
|
||
begin
|
||
var s := new StandardScaler;
|
||
s.fMean := fMean.Clone;
|
||
s.fStd := fStd.Clone;
|
||
s.fFitted := fFitted;
|
||
s.fFeatureCount := fFeatureCount;
|
||
Result := s;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// MinMaxScaler
|
||
//-----------------------------
|
||
|
||
constructor MinMaxScaler.Create(rangeMin: real; rangeMax: real);
|
||
begin
|
||
if rangeMax <= rangeMin then
|
||
ArgumentError(ER_RANGE_INVALID);
|
||
|
||
fRangeMin := rangeMin;
|
||
fRangeMax := rangeMax;
|
||
fFitted := false;
|
||
end;
|
||
|
||
function MinMaxScaler.Fit(X: Matrix): IUnsupervisedTransformer;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
fFeatureCount := X.ColCount;
|
||
|
||
fMin := X.ColumnMins;
|
||
fMax := X.ColumnMaxs;
|
||
|
||
// защита от константных признаков
|
||
for var j := 0 to fMin.Length - 1 do
|
||
if Abs(fMax[j] - fMin[j]) < 1e-12 then
|
||
fMax[j] := fMin[j] + 1.0;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function MinMaxScaler.Transform(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
Result := new Matrix(n, p);
|
||
|
||
var scale := fRangeMax - fRangeMin;
|
||
|
||
for var j := 0 to p - 1 do
|
||
begin
|
||
var minVal := fMin[j];
|
||
var maxVal := fMax[j];
|
||
var denom := maxVal - minVal;
|
||
|
||
if Abs(denom) < 1e-12 then
|
||
begin
|
||
// Константный столбец → всё = fRangeMin
|
||
for var i := 0 to n - 1 do
|
||
Result[i,j] := fRangeMin;
|
||
end
|
||
else
|
||
begin
|
||
for var i := 0 to n - 1 do
|
||
Result[i,j] :=
|
||
fRangeMin + (X[i,j] - minVal) / denom * scale;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function MinMaxScaler.ToString: string;
|
||
begin
|
||
Result := 'MinMaxScaler(min=' + fRangeMin + ', max=' + fRangeMax + ')';
|
||
end;
|
||
|
||
function MinMaxScaler.Clone: ITransformer;
|
||
begin
|
||
var s := new MinMaxScaler;
|
||
s.fMin := fMin.Clone;
|
||
s.fMax := fMax.Clone;
|
||
s.fFitted := fFitted;
|
||
s.fRangeMin := fRangeMin;
|
||
s.fRangeMax := fRangeMax;
|
||
s.fFeatureCount := fFeatureCount;
|
||
Result := s;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// PCATransformer
|
||
//-----------------------------
|
||
|
||
constructor PCATransformer.Create(k: integer);
|
||
begin
|
||
if k <= 0 then
|
||
ArgumentError(ER_K_MUST_BE_POSITIVE);
|
||
|
||
fK := k;
|
||
fFitted := false;
|
||
end;
|
||
|
||
function PCATransformer.Fit(X: Matrix): IUnsupervisedTransformer;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if fK <= 0 then
|
||
ArgumentOutOfRangeError(ER_K_INVALID, fK);
|
||
|
||
if fK > X.ColCount then
|
||
ArgumentOutOfRangeError(ER_K_EXCEEDS_FEATURES, fK);
|
||
|
||
fFeatureCount := X.ColCount;
|
||
|
||
// --- центрирование
|
||
fMean := X.ColumnMeans;
|
||
|
||
var Xc := X.Clone;
|
||
|
||
for var j := 0 to X.ColCount - 1 do
|
||
for var i := 0 to X.RowCount - 1 do
|
||
Xc[i,j] -= fMean[j];
|
||
|
||
// --- PCA
|
||
var (W, variances) := Xc.PCA(fK);
|
||
|
||
fComponents := W;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function PCATransformer.Transform(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
// Центрирование без лишнего Clone
|
||
var Xc := new Matrix(n, p);
|
||
|
||
for var j := 0 to p - 1 do
|
||
for var i := 0 to n - 1 do
|
||
Xc[i,j] := X[i,j] - fMean[j];
|
||
|
||
Result := Xc * fComponents;
|
||
end;
|
||
|
||
function PCATransformer.ToString: string;
|
||
begin
|
||
Result := 'PCATransformer(k=' + fK + ')';
|
||
end;
|
||
|
||
function PCATransformer.Clone: ITransformer;
|
||
begin
|
||
var t := new PCATransformer(fK);
|
||
t.fComponents := fComponents.Clone;
|
||
t.fMean := fMean.Clone;
|
||
t.fFitted := fFitted;
|
||
t.fFeatureCount := fFeatureCount;
|
||
Result := t;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// VarianceThreshold
|
||
//-----------------------------
|
||
|
||
constructor VarianceThreshold.Create(threshold: real);
|
||
begin
|
||
if threshold < 0 then
|
||
ArgumentError(ER_THRESHOLD_NEGATIVE);
|
||
|
||
fThreshold := threshold;
|
||
fFitted := false;
|
||
end;
|
||
|
||
function VarianceThreshold.Fit(X: Matrix): IUnsupervisedTransformer;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if fThreshold < 0 then
|
||
ArgumentOutOfRangeError(ER_THRESHOLD_INVALID, fThreshold);
|
||
|
||
fFeatureCount := X.ColCount;
|
||
|
||
var vars := X.ColumnVariances;
|
||
|
||
var tmp := new List<integer>;
|
||
|
||
for var j := 0 to X.ColCount - 1 do
|
||
if vars[j] >= fThreshold then
|
||
tmp.Add(j);
|
||
|
||
if tmp.Count = 0 then
|
||
ArgumentError(ER_ALL_FEATURES_REMOVED);
|
||
|
||
fSelected := tmp.ToArray;
|
||
fFitted := true;
|
||
|
||
Result := Self;
|
||
end;
|
||
|
||
function VarianceThreshold.Transform(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
if fSelected = nil then
|
||
Error(ER_MODEL_NOT_INITIALIZED);
|
||
|
||
var n := X.RowCount;
|
||
var k := fSelected.Length;
|
||
|
||
var R := new Matrix(n, k);
|
||
|
||
for var i := 0 to n - 1 do
|
||
for var c := 0 to k - 1 do
|
||
R[i,c] := X[i, fSelected[c]];
|
||
|
||
Result := R;
|
||
end;
|
||
|
||
function VarianceThreshold.ToString: string;
|
||
begin
|
||
Result := 'VarianceThreshold(threshold=' + fThreshold + ')';
|
||
end;
|
||
|
||
function VarianceThreshold.Clone: ITransformer;
|
||
begin
|
||
var t := new VarianceThreshold(fThreshold);
|
||
t.fSelected := Copy(fSelected);
|
||
t.fFitted := fFitted;
|
||
t.fFeatureCount := fFeatureCount;
|
||
Result := t;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// SelectKBest
|
||
//-----------------------------
|
||
|
||
constructor SelectKBest.Create(k: integer; score: FeatureScore);
|
||
begin
|
||
if k <= 0 then
|
||
ArgumentError(ER_K_MUST_BE_POSITIVE);
|
||
|
||
fK := k;
|
||
fScoreType := score;
|
||
fScoreFunc := nil;
|
||
fFitted := false;
|
||
end;
|
||
|
||
constructor SelectKBest.Create(k: integer; scoreFunc: (Vector, Vector) -> real);
|
||
begin
|
||
if k <= 0 then
|
||
ArgumentError(ER_K_MUST_BE_POSITIVE);
|
||
|
||
fK := k;
|
||
fScoreFunc := scoreFunc;
|
||
fFitted := false;
|
||
end;
|
||
|
||
function SelectKBest.ComputeCorrelation(x: Vector; y: Vector): real;
|
||
begin
|
||
var mx := x.Mean;
|
||
var my := y.Mean;
|
||
|
||
var num := 0.0;
|
||
var dx := 0.0;
|
||
var dy := 0.0;
|
||
|
||
for var i := 0 to x.Length - 1 do
|
||
begin
|
||
var vx := x[i] - mx;
|
||
var vy := y[i] - my;
|
||
|
||
num += vx * vy;
|
||
dx += vx * vx;
|
||
dy += vy * vy;
|
||
end;
|
||
|
||
if (dx = 0) or (dy = 0) then
|
||
exit(0.0);
|
||
|
||
Result := Abs(num / Sqrt(dx * dy));
|
||
end;
|
||
|
||
function SelectKBest.ComputeFRegression(feature: Vector; y: Vector): real;
|
||
begin
|
||
var r := ComputeCorrelation(feature, y);
|
||
var n := feature.Length;
|
||
|
||
if Abs(r) >= 1 then
|
||
exit(1e308);
|
||
|
||
Result := (r*r / (1 - r*r)) * (n - 2);
|
||
end;
|
||
|
||
function SelectKBest.ComputeAnovaF(feature: Vector; y: Vector): real;
|
||
begin
|
||
var n := feature.Length;
|
||
if n = 0 then
|
||
exit(0.0);
|
||
|
||
// --- построение отображения классов ---
|
||
var classToIndex := new Dictionary<integer, integer>;
|
||
var uniqueClasses := new List<integer>;
|
||
var yEncoded := new integer[n];
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var r := y[i];
|
||
var ir := Round(r);
|
||
|
||
if Abs(r - ir) > 1e-12 then
|
||
ArgumentError(ER_LABELS_NOT_INTEGER);
|
||
|
||
if not classToIndex.ContainsKey(ir) then
|
||
begin
|
||
classToIndex[ir] := uniqueClasses.Count;
|
||
uniqueClasses.Add(ir);
|
||
end;
|
||
|
||
yEncoded[i] := classToIndex[ir];
|
||
end;
|
||
|
||
var classCount := uniqueClasses.Count;
|
||
if classCount < 2 then
|
||
exit(0.0);
|
||
|
||
if n <= classCount then
|
||
exit(0.0);
|
||
|
||
var counts := new integer[classCount];
|
||
var means := new real[classCount];
|
||
|
||
var globalMean := feature.Mean;
|
||
|
||
// --- накопление ---
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var classIdx := yEncoded[i];
|
||
counts[classIdx] += 1;
|
||
means[classIdx] += feature[i];
|
||
end;
|
||
|
||
for var c := 0 to classCount - 1 do
|
||
if counts[c] > 0 then
|
||
means[c] /= counts[c];
|
||
|
||
// --- SS_between ---
|
||
var ssBetween := 0.0;
|
||
for var c := 0 to classCount - 1 do
|
||
if counts[c] > 0 then
|
||
begin
|
||
var diff := means[c] - globalMean;
|
||
ssBetween += counts[c] * diff * diff;
|
||
end;
|
||
|
||
// --- SS_within ---
|
||
var ssWithin := 0.0;
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var classIdx := yEncoded[i];
|
||
var diff := feature[i] - means[classIdx];
|
||
ssWithin += diff * diff;
|
||
end;
|
||
|
||
if Abs(ssWithin) < 1e-12 then
|
||
exit(0.0);
|
||
|
||
var msBetween := ssBetween / (classCount - 1);
|
||
var msWithin := ssWithin / (n - classCount);
|
||
|
||
if Abs(msWithin) < 1e-12 then
|
||
exit(0.0);
|
||
|
||
Result := msBetween / msWithin;
|
||
end;
|
||
|
||
function SelectKBest.ComputeChiSquare(feature: Vector; y: Vector): real;
|
||
begin
|
||
var n := feature.Length;
|
||
if n = 0 then
|
||
exit(0.0);
|
||
|
||
if y.Length <> n then
|
||
DimensionError(ER_DIM_MISMATCH, y.Length, n);
|
||
|
||
// --- проверка неотрицательности ---
|
||
for var i := 0 to n - 1 do
|
||
if feature[i] < 0 then
|
||
ArgumentError(ER_CHI_SQUARE_NEGATIVE);
|
||
|
||
// --- построение отображения классов ---
|
||
var classToIndex := new Dictionary<integer, integer>;
|
||
var uniqueClasses := new List<integer>;
|
||
var yEncoded := new integer[n];
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var r := y[i];
|
||
var ir := Round(r);
|
||
|
||
if Abs(r - ir) > 1e-12 then
|
||
ArgumentError(ER_LABELS_NOT_INTEGER);
|
||
|
||
if not classToIndex.ContainsKey(ir) then
|
||
begin
|
||
classToIndex[ir] := uniqueClasses.Count;
|
||
uniqueClasses.Add(ir);
|
||
end;
|
||
|
||
yEncoded[i] := classToIndex[ir];
|
||
end;
|
||
|
||
var classCount := uniqueClasses.Count;
|
||
if classCount < 2 then
|
||
exit(0.0);
|
||
|
||
var counts := new integer[classCount];
|
||
var observedSums := new real[classCount];
|
||
var totalSum := 0.0;
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var classIdx := yEncoded[i];
|
||
counts[classIdx] += 1;
|
||
observedSums[classIdx] += feature[i];
|
||
totalSum += feature[i];
|
||
end;
|
||
|
||
if Abs(totalSum) < 1e-12 then
|
||
exit(0.0);
|
||
|
||
var globalMean := totalSum / n;
|
||
|
||
var chiSquare := 0.0;
|
||
|
||
for var c := 0 to classCount - 1 do
|
||
begin
|
||
if counts[c] = 0 then
|
||
continue;
|
||
|
||
var expected := counts[c] * globalMean;
|
||
|
||
if Abs(expected) > 1e-12 then
|
||
begin
|
||
var diff := observedSums[c] - expected;
|
||
chiSquare += diff * diff / expected;
|
||
end;
|
||
end;
|
||
|
||
Result := chiSquare;
|
||
end;
|
||
|
||
function SelectKBest.ComputeScore(feature: Vector; y: Vector): real;
|
||
begin
|
||
case fScoreType of
|
||
FeatureScore.Correlation: Result := ComputeCorrelation(feature, y);
|
||
FeatureScore.FRegression: Result := ComputeFRegression(feature, y);
|
||
FeatureScore.AnovaF: Result := ComputeAnovaF(feature, y);
|
||
FeatureScore.ChiSquare: Result := ComputeChiSquare(feature, y);
|
||
else
|
||
Error(ER_UNKNOWN_FEATURE_SCORE);
|
||
end;
|
||
end;
|
||
|
||
function SelectKBest.Fit(X: Matrix; y: Vector): ISupervisedTransformer;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if y = nil then
|
||
ArgumentNullError(ER_Y_NULL);
|
||
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
if fK <= 0 then
|
||
ArgumentOutOfRangeError(ER_SELECTKBEST_K_INVALID, fK);
|
||
|
||
fFeatureCount := X.ColCount;
|
||
|
||
var p := X.ColCount;
|
||
|
||
var scores: array of (real, integer);
|
||
SetLength(scores, p);
|
||
|
||
for var j := 0 to p - 1 do
|
||
begin
|
||
var col := new Vector(X.RowCount);
|
||
|
||
for var i := 0 to X.RowCount - 1 do
|
||
col[i] := X[i,j];
|
||
|
||
var s :=
|
||
if fScoreFunc <> nil then
|
||
fScoreFunc(col, y)
|
||
else
|
||
ComputeScore(col, y);
|
||
|
||
if double.IsNaN(s) or double.IsInfinity(s) then
|
||
s := real.NegativeInfinity;
|
||
|
||
scores[j] := (s, j);
|
||
end;
|
||
|
||
scores := scores
|
||
.OrderByDescending(t -> t.Item1)
|
||
.ToArray;
|
||
|
||
var k := Min(fK, p);
|
||
SetLength(fSelected, k);
|
||
|
||
for var i := 0 to k - 1 do
|
||
fSelected[i] := scores[i].Item2;
|
||
|
||
if k = 0 then
|
||
ArgumentError(ER_ALL_FEATURES_REMOVED);
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function SelectKBest.Transform(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
if fSelected = nil then
|
||
Error(ER_MODEL_NOT_INITIALIZED);
|
||
|
||
var n := X.RowCount;
|
||
var k := fSelected.Length;
|
||
|
||
var R := new Matrix(n, k);
|
||
|
||
for var i := 0 to n - 1 do
|
||
for var c := 0 to k - 1 do
|
||
R[i,c] := X[i, fSelected[c]];
|
||
|
||
Result := R;
|
||
end;
|
||
|
||
function SelectKBest.ToString: string;
|
||
begin
|
||
var scoreStr :=
|
||
if fScoreFunc <> nil then
|
||
'custom'
|
||
else
|
||
fScoreType.ToString;
|
||
|
||
Result := 'SelectKBest(k=' + fK + ', score=' + scoreStr + ')';
|
||
end;
|
||
|
||
function SelectKBest.Clone: ITransformer;
|
||
begin
|
||
var t := new SelectKBest(fK, fScoreType);
|
||
|
||
t.fScoreFunc := fScoreFunc;
|
||
|
||
if fSelected <> nil then
|
||
t.fSelected := Copy(fSelected);
|
||
|
||
t.fFeatureCount := fFeatureCount;
|
||
t.fFitted := fFitted;
|
||
|
||
Result := t;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// Normalizer
|
||
//-----------------------------
|
||
|
||
constructor Normalizer.Create(norm: NormType);
|
||
begin
|
||
fNormType := norm;
|
||
fFitted := false;
|
||
end;
|
||
|
||
function Normalizer.Fit(X: Matrix): IUnsupervisedTransformer;
|
||
begin
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.RowCount = 0 then
|
||
ArgumentError(ER_EMPTY_DATASET);
|
||
|
||
fFeatureCount := X.ColCount;
|
||
|
||
fFitted := true;
|
||
Result := Self;
|
||
end;
|
||
|
||
function Normalizer.Transform(X: Matrix): Matrix;
|
||
begin
|
||
if not fFitted then
|
||
NotFittedError(ER_FIT_NOT_CALLED);
|
||
|
||
if X = nil then
|
||
ArgumentNullError(ER_X_NULL);
|
||
|
||
if X.ColCount <> fFeatureCount then
|
||
DimensionError(ER_FEATURE_COUNT_MISMATCH);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
var R := new Matrix(n, p);
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var norm := 0.0;
|
||
|
||
case fNormType of
|
||
NormType.L1:
|
||
for var j := 0 to p - 1 do
|
||
norm += Abs(X[i,j]);
|
||
|
||
NormType.L2:
|
||
for var j := 0 to p - 1 do
|
||
norm += Sqr(X[i,j]);
|
||
end;
|
||
|
||
if fNormType = NormType.L2 then
|
||
norm := Sqrt(norm);
|
||
|
||
if Abs(norm) < 1e-12 then
|
||
begin
|
||
// нулевая строка — оставляем нулями
|
||
continue;
|
||
end;
|
||
|
||
for var j := 0 to p - 1 do
|
||
R[i,j] := X[i,j] / norm;
|
||
end;
|
||
|
||
Result := R;
|
||
end;
|
||
|
||
function Normalizer.ToString: string;
|
||
begin
|
||
Result :=
|
||
'Normalizer(norm=' + fNormType.ToString + ')';
|
||
end;
|
||
|
||
function Normalizer.Clone: ITransformer;
|
||
begin
|
||
var t := new Normalizer(fNormType);
|
||
t.fFitted := fFitted;
|
||
t.fFeatureCount := fFeatureCount;
|
||
Result := t;
|
||
end;
|
||
|
||
|
||
end. |