diff --git a/bin/Lib/InspectionML.pas b/bin/Lib/InspectionML.pas index 0cd6b992d..709206689 100644 --- a/bin/Lib/InspectionML.pas +++ b/bin/Lib/InspectionML.pas @@ -51,14 +51,14 @@ begin if scoreFunc = nil then ArgumentNullError(ER_SCORE_FUNC_NULL); - if X.Rows <> y.Length then - DimensionError(ER_DIM_MISMATCH, X.Rows, y.Length); + if X.RowCount <> y.Length then + DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length); var baselinePred := model.Predict(X); var baselineScore := scoreFunc(y, baselinePred); - var n := X.Rows; - var p := X.Cols; + var n := X.RowCount; + var p := X.ColCount; var resultVec := new Vector(p); var rng := new System.Random(seed); diff --git a/bin/Lib/LinearAlgebraML.pas b/bin/Lib/LinearAlgebraML.pas index 6f10c8200..18e8b9575 100644 --- a/bin/Lib/LinearAlgebraML.pas +++ b/bin/Lib/LinearAlgebraML.pas @@ -73,8 +73,8 @@ type procedure SetData(i, j: integer; value: real) := data[i, j] := value; public data: array[,] of real; - property Rows: integer read data.GetLength(0); - property Cols: integer read data.GetLength(1); + property RowCount: integer read data.RowCount; + property ColCount: integer read data.ColCount; property Item[i, j: integer]: real read data[i, j] write SetData; default; @@ -140,9 +140,6 @@ type // ---------- Основные методы ---------- - function RowCount: integer := data.RowCount; - function ColCount: integer := data.ColCount; - /// Возвращает транспонированную матрицу Aᵀ function Transpose: Matrix; /// Проверяет, является ли матрица симметричной с заданным допуском. @@ -241,6 +238,8 @@ function SolveAuto(A: Matrix; b: Vector): Vector; /// Входные A и b изменяются. Возвращаемый вектор x имеет длину n. function SolveRidge(A: Matrix; b: Vector; lambda: real := 0): Vector; +function SolveLeastSquaresQR(A: Matrix; b: Vector): Vector; + implementation uses MLExceptions; @@ -291,7 +290,11 @@ const 'Матрица вырождена!!Matrix is singular'; ER_VECTOR_SIZE_MISMATCH = 'Несоответствие длины вектора: {0} и {1}!!Vector size mismatch: {0} and {1}'; - + ER_QR_REQUIRES_M_GE_N = + 'Для QR-разложения требуется m >= n!!QR decomposition requires m >= n'; + ER_SINGULAR_MATRIX = + 'Матрица вырождена или плохо обусловлена!!Matrix is singular or ill-conditioned'; + //----------------------------- // Vector //----------------------------- @@ -813,20 +816,20 @@ end; static procedure Matrix.CheckSameSize(A, B: Matrix); begin - if (A.Rows <> B.Rows) or (A.Cols <> B.Cols) then - DimensionError(ER_MATRIX_SIZE_MISMATCH, A.Rows, A.Cols, B.Rows, B.Cols); + if (A.RowCount <> B.RowCount) or (A.ColCount <> B.ColCount) then + DimensionError(ER_MATRIX_SIZE_MISMATCH, A.RowCount, A.ColCount, B.RowCount, B.ColCount); end; static procedure Matrix.CheckMulSize(A, B: Matrix); begin - if A.Cols <> B.Rows then - DimensionError(ER_MATRIX_MUL_SIZE_MISMATCH, A.Rows, A.Cols, B.Rows, B.Cols); + if A.ColCount <> B.RowCount then + DimensionError(ER_MATRIX_MUL_SIZE_MISMATCH, A.RowCount, A.ColCount, B.RowCount, B.ColCount); end; static procedure Matrix.CheckVecSize(A: Matrix; x: Vector); begin - if A.Cols <> x.Length then - DimensionError(ER_MATRIX_VECTOR_SIZE_MISMATCH, A.Rows, A.Cols, x.Length); + if A.ColCount <> x.Length then + DimensionError(ER_MATRIX_VECTOR_SIZE_MISMATCH, A.RowCount, A.ColCount, x.Length); end; @@ -834,26 +837,26 @@ end; static function Matrix.operator +(A, B: Matrix): Matrix; begin CheckSameSize(A, B); - Result := new Matrix(A.Rows, A.Cols); - for var i := 0 to A.Rows - 1 do - for var j := 0 to A.Cols - 1 do + Result := new Matrix(A.RowCount, A.ColCount); + for var i := 0 to A.RowCount - 1 do + for var j := 0 to A.ColCount - 1 do Result.data[i, j] := A.data[i, j] + B.data[i, j]; end; static function Matrix.operator -(A, B: Matrix): Matrix; begin CheckSameSize(A, B); - Result := new Matrix(A.Rows, A.Cols); - for var i := 0 to A.Rows - 1 do - for var j := 0 to A.Cols - 1 do + Result := new Matrix(A.RowCount, A.ColCount); + for var i := 0 to A.RowCount - 1 do + for var j := 0 to A.ColCount - 1 do Result.data[i, j] := A.data[i, j] - B.data[i, j]; end; function operator *(A: Matrix; alpha: real): Matrix; extensionmethod; begin - Result := new Matrix(A.Rows, A.Cols); - for var i := 0 to A.Rows - 1 do - for var j := 0 to A.Cols - 1 do + Result := new Matrix(A.RowCount, A.ColCount); + for var i := 0 to A.RowCount - 1 do + for var j := 0 to A.ColCount - 1 do Result.data[i, j] := alpha * A.data[i, j]; end; @@ -877,11 +880,11 @@ end; static function Matrix.operator *(A: Matrix; x: Vector): Vector; begin CheckVecSize(A, x); - Result := new Vector(A.Rows); - for var i := 0 to A.Rows - 1 do + Result := new Vector(A.RowCount); + for var i := 0 to A.RowCount - 1 do begin var s := 0.0; - for var j := 0 to A.Cols - 1 do + for var j := 0 to A.ColCount - 1 do s += A.data[i, j] * x[j]; Result[i] := s; end; @@ -890,13 +893,13 @@ end; static function Matrix.operator *(A, B: Matrix): Matrix; begin CheckMulSize(A, B); - Result := new Matrix(A.Rows, B.Cols); - for var i := 0 to A.Rows - 1 do - for var k := 0 to A.Cols - 1 do + Result := new Matrix(A.RowCount, B.ColCount); + for var i := 0 to A.RowCount - 1 do + for var k := 0 to A.ColCount - 1 do begin var aik := A.data[i, k]; if aik <> 0.0 then - for var j := 0 to B.Cols - 1 do + for var j := 0 to B.ColCount - 1 do Result.data[i, j] += aik * B.data[k, j]; end; end; @@ -904,8 +907,8 @@ end; static function Matrix.operator +=(A, B: Matrix): Matrix; begin CheckSameSize(A, B); - for var i := 0 to A.Rows - 1 do - for var j := 0 to A.Cols - 1 do + for var i := 0 to A.RowCount - 1 do + for var j := 0 to A.ColCount - 1 do A.data[i, j] += B.data[i, j]; Result := A; end; @@ -913,43 +916,43 @@ end; static function Matrix.operator -=(A, B: Matrix): Matrix; begin CheckSameSize(A, B); - for var i := 0 to A.Rows - 1 do - for var j := 0 to A.Cols - 1 do + for var i := 0 to A.RowCount - 1 do + for var j := 0 to A.ColCount - 1 do A.data[i, j] -= B.data[i, j]; Result := A; end; static function Matrix.operator *=(A: Matrix; alpha: real): Matrix; begin - for var i := 0 to A.Rows - 1 do - for var j := 0 to A.Cols - 1 do + for var i := 0 to A.RowCount - 1 do + for var j := 0 to A.ColCount - 1 do A.data[i, j] *= alpha; Result := A; end; function Matrix.Transpose: Matrix; begin - Result := new Matrix(Cols, Rows); - for var i := 0 to Rows - 1 do - for var j := 0 to Cols - 1 do + Result := new Matrix(ColCount, RowCount); + for var i := 0 to RowCount - 1 do + for var j := 0 to ColCount - 1 do Result.data[j, i] := data[i, j]; end; function Matrix.GetRow(i: integer): Vector; begin - if (i < 0) or (i >= Rows) then - ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, Rows); - Result := new Vector(Cols); - for var j := 0 to Cols - 1 do + if (i < 0) or (i >= RowCount) then + ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount); + Result := new Vector(ColCount); + for var j := 0 to ColCount - 1 do Result[j] := data[i, j]; end; function Matrix.GetCol(j: integer): Vector; begin - if (j < 0) or (j >= Cols) then - ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, Cols); - Result := new Vector(Rows); - for var i := 0 to Rows - 1 do + if (j < 0) or (j >= ColCount) then + ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, ColCount); + Result := new Vector(RowCount); + for var i := 0 to RowCount - 1 do Result[i] := data[i, j]; end; @@ -976,14 +979,14 @@ end; function Matrix.IsSymmetric(tol: real): boolean; begin - if Rows <> Cols then + if RowCount <> ColCount then begin Result := false; exit; end; - for var i := 0 to Rows - 1 do - for var j := i + 1 to Cols - 1 do + for var i := 0 to RowCount - 1 do + for var j := i + 1 to ColCount - 1 do if Abs(data[i, j] - data[j, i]) > tol then begin Result := false; @@ -995,13 +998,13 @@ end; function Matrix.EigenSymmetric(tol: real; maxIter: integer): (Vector, Matrix); begin - if Rows <> Cols then + if RowCount <> ColCount then ArgumentError(ER_MATRIX_NOT_SQUARE); if not IsSymmetric(tol) then ArgumentError(ER_MATRIX_NOT_SYMMETRIC); - var n := Rows; + var n := RowCount; var M := Clone; var V := Matrix.Identity(n); @@ -1136,8 +1139,8 @@ end; function Matrix.PCA(k: integer): (Matrix, Vector); begin - var m := Rows; - var n := Cols; + var m := RowCount; + var n := ColCount; if k < 1 then ArgumentError(ER_PCA_K_INVALID); @@ -1200,10 +1203,10 @@ end; // Helper function Cholesky(A: Matrix): Matrix; begin - if A.Rows <> A.Cols then + if A.RowCount <> A.ColCount then ArgumentError(ER_CHOLESKY_NOT_SQUARE); - var n := A.Rows; + var n := A.RowCount; var L := new Matrix(n, n); for var i := 0 to n - 1 do @@ -1229,10 +1232,10 @@ end; // Helper function LUDecompose(A: Matrix): (Matrix, array of integer); begin - if A.Rows <> A.Cols then + if A.RowCount <> A.ColCount then ArgumentError(ER_MATRIX_NOT_SQUARE); - var n := A.Rows; + var n := A.RowCount; var LU := A.Clone; var p := new integer[n]; @@ -1294,7 +1297,7 @@ end; // Helper function SolveLowerTriangularUnit(LU: Matrix; b: Vector): Vector; begin - var n := LU.Rows; + var n := LU.RowCount; var y := new Vector(n); for var i := 0 to n - 1 do @@ -1310,7 +1313,7 @@ end; function SolveLowerTriangular(L: Matrix; b: Vector): Vector; begin - var n := L.Rows; + var n := L.RowCount; var y := new Vector(n); for var i := 0 to n - 1 do @@ -1328,7 +1331,7 @@ end; // Helper function SolveUpperTriangular(U: Matrix; y: Vector): Vector; begin - var n := U.Rows; + var n := U.RowCount; var x := new Vector(n); for var i := n - 1 downto 0 do @@ -1344,11 +1347,11 @@ end; function Solve(A: Matrix; b: Vector): Vector; begin - if A.Rows <> A.Cols then + if A.RowCount <> A.ColCount then ArgumentError(ER_MATRIX_NOT_SQUARE); - if b.Length <> A.Rows then - DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.Rows); + if b.Length <> A.RowCount then + DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.RowCount); var (LU, p) := LUDecompose(A); var pb := Permute(b, p); @@ -1358,11 +1361,11 @@ end; function SolveSPD(A: Matrix; b: Vector): Vector; begin - if A.Rows <> A.Cols then + if A.RowCount <> A.ColCount then ArgumentError(ER_MATRIX_NOT_SQUARE); - if b.Length <> A.Rows then - DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.Rows); + if b.Length <> A.RowCount then + DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.RowCount); var L := Cholesky(A); var y := SolveLowerTriangular(L, b); @@ -1383,8 +1386,8 @@ begin if lambda < 0.0 then ArgumentError(ER_LAMBDA_NEGATIVE); - var m := A.Rows; - var n := A.Cols; + var m := A.RowCount; + var n := A.ColCount; if b.Length <> m then DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, m); @@ -1430,6 +1433,89 @@ begin end; +function SolveLeastSquaresQR(A: Matrix; b: Vector): Vector; +begin + if A.RowCount <> b.Length then + DimensionError(ER_DIM_MISMATCH, A.RowCount, b.Length); + + var m := A.RowCount; + var n := A.ColCount; + + if m < n then + ArgumentError(ER_QR_REQUIRES_M_GE_N); + + var R := A.Clone; + var y := b.Clone; + + for var k := 0 to n - 1 do + begin + // ---- вычислить норму столбца k начиная с строки k + var normx := 0.0; + for var i := k to m - 1 do + normx += R[i,k] * R[i,k]; + + normx := Sqrt(normx); + + if normx = 0 then + continue; + + // ---- знак для устойчивости + if R[k,k] >= 0 then + normx := -normx; + + // ---- построить Householder-вектор v + var beta := 1.0 / (normx * R[k,k] - normx * normx); + + R[k,k] -= normx; + + // ---- применить отражение к R + for var j := k + 1 to n - 1 do + begin + var s := 0.0; + for var i := k to m - 1 do + s += R[i,k] * R[i,j]; + + s *= beta; + + for var i := k to m - 1 do + R[i,j] -= s * R[i,k]; + end; + + // ---- применить отражение к y + var sy := 0.0; + for var i := k to m - 1 do + sy += R[i,k] * y[i]; + + sy *= beta; + + for var i := k to m - 1 do + y[i] -= sy * R[i,k]; + + // ---- восстановить диагональный элемент + R[k,k] := normx; + + for var i := k + 1 to m - 1 do + R[i,k] := 0.0; + end; + + // ---- Back substitution (решаем R[0:n,0:n] * x = y[0:n]) + var x := new Vector(n); + + for var i := n - 1 downto 0 do + begin + var sum := y[i]; + + for var j := i + 1 to n - 1 do + sum -= R[i,j] * x[j]; + + if Abs(R[i,i]) < 1e-14 then + ArgumentError(ER_SINGULAR_MATRIX); + + x[i] := sum / R[i,i]; + end; + + Result := x; +end; end. \ No newline at end of file diff --git a/bin/Lib/MLExceptions.pas b/bin/Lib/MLExceptions.pas index 5ad37d2d3..eb48c8105 100644 --- a/bin/Lib/MLExceptions.pas +++ b/bin/Lib/MLExceptions.pas @@ -15,6 +15,8 @@ const 'Метод Fit() не вызван!!Fit() must be called first'; ER_MODEL_NULL = 'Модель не может быть nil!!Model cannot be nil'; + ER_SELECTKBEST_K_INVALID = + 'Параметр k в SelectKBest должен быть >= 1. Передано: {0}.!!SelectKBest parameter k must be >= 1. Given: {0}.'; type /// Базовое исключение ML-библиотеки diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index cc28f558d..dc0b900e1 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -140,6 +140,8 @@ type /// и L2-регуляризацию (численная устойчивость). /// Используется при большом числе признаков, особенно если признаки коррелированы. /// Обучение выполняется методом покоординатного спуска +/// ВАЖНО: Модель чувствительна к масштабу признаков. +/// Всегда используйте StandardScaler в Pipeline перед ElasticNet. ElasticNet = class(IRegressor) private fLambda1: real; // L1 @@ -207,6 +209,10 @@ type fClassCount: integer; fClassToIndex: Dictionary; fIndexToClass: array of integer; + + fTol: real := 1e-6; + fCheckConvergence: boolean := true; + fMinImprovement: real := 1e-8; public /// lambda — коэффициент L2-регуляризации. /// lr — шаг градиентного спуска. @@ -261,6 +267,20 @@ type 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; /// Интерфейс критерия разбиения узла дерева. @@ -268,16 +288,21 @@ type ISplitCriterion = interface /// Вычисляет нечистоту для вектора целевых значений y. /// Чем меньше значение — тем "чище" узел. - function Impurity(y: Vector): real; + function Impurity(y: Vector; indices: array of integer): real; end; /// Критерий Джини. /// Используется в классификации. /// Минимизирует Gini-нечистоту, что приводит к более однородным по классам листьям GiniCriterion = class(ISplitCriterion) + private + fClassCount: integer; public -/// Вычисляет Gini impurity для текущего набора y. - function Impurity(y: Vector): real; + constructor Create(classCount: integer); + begin + fClassCount := classCount; + end; + function Impurity(y: Vector; indices: array of integer): real; end; /// Критерий дисперсии. @@ -287,7 +312,7 @@ type public /// Вычисляет дисперсию значений y. /// Чем меньше дисперсия — тем лучше узел. - function Impurity(y: Vector): real; + function Impurity(y: Vector; indices: array of integer): real; end; //============================ @@ -308,7 +333,7 @@ type fCriterion: ISplitCriterion; fFeatureImportances: Vector; fRandomSeed: integer; - fMaxFeatures := 0; + fMaxFeatures: integer := 0; fRowIndices: array of integer := nil; fRng: System.Random; @@ -316,9 +341,7 @@ type 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 FindBestSplitReg(X: Matrix; y: Vector; indices: array of integer): SplitResult; - function FindBestSplitCls(X: Matrix; y: Vector; indices: array of integer; classCount: integer): SplitResult; + //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; @@ -376,16 +399,13 @@ type fIndexToClass: array of integer; fClassCount: integer; - function PredictOne(x: Vector): integer; + function PredictOne(X: Matrix; rowIndex: integer): integer; function MajorityClass(y: Vector; indices: array of integer): integer; protected function LeafValue(y: Vector; indices: array of integer): real; override; function FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; override; - begin - Result := FindBestSplitCls(X, y, indices, fClassCount); - end; public /// Создает классификационное дерево: @@ -410,6 +430,10 @@ type /// Возвращает строковое представление модели. function ToString: string; override; + + function ClassCount: integer := fClassCount; + + function IndexToClass: array of integer := Copy(fIndexToClass); end; //============================ @@ -424,13 +448,16 @@ type private fLeafL2: real; - function PredictOne(x: Vector): 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; @@ -511,7 +538,9 @@ type fUserProvidedSeed: boolean; function ComputeMaxFeatures(p: integer): integer; - procedure BootstrapSample(X: Matrix; y: Vector; var Xb: Matrix; var yb: Vector); + + procedure BootstrapRowIndices(n: integer; var rows: array of integer); + public /// Создает случайный лес. /// • nTrees — число деревьев в ансамбле. @@ -593,9 +622,11 @@ type /// Строит ансамбль классификационных деревьев, обученных на /// bootstrap-подвыборках объектов и случайных подмножествах признаков. /// Итоговое предсказание формируется голосованием деревьев или агрегацией вероятностей классов - RandomForestClassifier = class(RandomForestBase, IClassifier) + RandomForestClassifier = class(RandomForestBase, IClassifier, IProbabilisticClassifier) private fTrees: array of DecisionTreeClassifier; + fIndexToClass: array of integer; + fClassCount: integer; public /// Создает классификационный случайный лес: /// • nTrees — число деревьев в ансамбле. @@ -621,6 +652,8 @@ type /// Для каждого объекта агрегируются предсказания всех деревьев. /// Итоговый класс определяется большинством голосов или максимальной суммарной вероятностью. function Predict(X: Matrix): Vector; override; + + function PredictProba(X: Matrix): Matrix; /// Создает глубокую копию случайного леса классификации. /// Копируются все деревья, параметры ансамбля и обученное состояние модели. @@ -632,6 +665,8 @@ type /// Возвращает строковое представление модели. function ToString: string; override; + + function GetClasses: array of real; end; { Gradient Boosting v1.0 — Freeze Checklist @@ -700,7 +735,7 @@ type fBestIteration: integer; fValLossHistory: List; - fBestValLoss: real; + fBestScoreLoss: real; fFeatureImportances: Vector; @@ -975,7 +1010,6 @@ type fNeighbors: array of Neighbor; // ==== common methods ==== - procedure CheckFitted; procedure ValidatePredictInput(X: Matrix); function SquaredL2(trainRow: integer; XTest: Matrix; testRow: integer): double; @@ -1004,6 +1038,8 @@ type /// Классификатор на основе алгоритма k ближайших соседей (kNN). /// Поддерживает равномерное (Uniform) и взвешенное по расстоянию (Distance) голосование. /// Реализует вероятностные предсказания через PredictProba + /// ВАЖНО: KNN чувствителен к масштабу признаков. + /// Всегда используйте StandardScaler или MinMaxScaler в Pipeline перед KNN. KNNClassifier = class(KNNBase, IProbabilisticClassifier) private // ==== classification state ==== @@ -1313,7 +1349,9 @@ type 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 /// Создаёт трансформер с использованием встроенного критерия: @@ -1392,7 +1430,11 @@ type {$endregion Transformers} + +var + /// Проверять ли входные данные моделей на NaN, Inf + ValidateFiniteInputs := true; implementation @@ -1455,9 +1497,132 @@ const ER_NAN_IN_X = 'x содержит NaN!!x contains NaN'; ER_K_EXCEEDS_SAMPLES = - 'k превышает число обучающих объектов!!k exceeds number of training 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.'; + {$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 @@ -1470,30 +1635,41 @@ end; function LinearRegression.Fit(X: Matrix; y: Vector): IModel; begin - var n := X.RowCount; + if ValidateFiniteInputs then + begin + CheckXForFit(X); + CheckYForFit(y); + end; + + var m := X.RowCount; - if n = 0 then + if m = 0 then ArgumentError(ER_EMPTY_DATASET); - if y.Length <> n then - DimensionError(ER_DIM_MISMATCH, y.Length, n); - // 1. Means + 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; - // 2. XtX and XtY - var Xt := X.Transpose; - var XtX := Xt * X; - var XtY := Xt * y; + // --- 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]; - // 3. Centering correction - XtX := XtX - n * Matrix.OuterProduct(meanX, meanX); - XtY := XtY - n * meanX * meanY; - - // 4. Solve - fcoef := Solve(XtX, XtY); + // --- center y + var yc := new Vector(m); + for var i := 0 to m - 1 do + yc[i] := y[i] - meanY; - // 5. Intercept + // --- solve via QR + fcoef := SolveLeastSquaresQR(Xc, yc); + + // --- intercept fintercept := meanY - meanX.Dot(fcoef); ffitted := true; @@ -1504,11 +1680,14 @@ function LinearRegression.Predict(X: Matrix): Vector; begin if not ffitted then NotFittedError(ER_FIT_NOT_CALLED); + + if ValidateFiniteInputs then + CheckXForPredict(X); if X.ColCount <> fcoef.Length then DimensionError(ER_DIM_MISMATCH, X.ColCount, fCoef.Length); - Result := X * fcoef + intercept; + Result := X * fcoef + fIntercept; end; function LinearRegression.ToString: string; @@ -1542,96 +1721,37 @@ static function Activations.ReLU(v: Vector): Vector := v.Apply(x -> (if x > 0 th 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 shifted := v.Apply(x -> Exp(x - maxVal)); - var sumExp := shifted.Sum; - if sumExp = 0 then - exit(new Vector(v.Length)); - Result := shifted / sumExp; -end; -//----------------------------- -// LogisticRegression -//----------------------------- + var sumExp := 0.0; -(*constructor LogisticRegression.Create(lambda: real; lr: real; epochs: integer); -begin - fLambda := lambda; - fLearningRate := lr; - fEpochs := epochs; - fFitted := false; -end; - -function LogisticRegression.Fit(X: Matrix; y: Vector): IModel; -begin - var n := X.RowCount; - var p := X.ColCount; - - if y.Length <> n then - DimensionError(ER_DIM_MISMATCH, y.Length, n); - - fCoef := new Vector(p); - fIntercept := 0.0; - - for var epoch := 1 to fEpochs do + for var i := 0 to n - 1 do begin - var z := X * fCoef + fIntercept; - var yHat := Activations.Sigmoid(z); - - var error := yHat - y; - - var gradW := (X.Transpose * error) / n; - - if fLambda > 0 then - gradW := gradW + fLambda * fCoef; - - var gradB := error.Sum / n; - - fCoef := fCoef - fLearningRate * gradW; - fIntercept -= fLearningRate * gradB; + var e := Exp(v[i] - maxVal); + resultVec[i] := e; + sumExp += e; end; - fFitted := true; - Result := Self; + 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; -function LogisticRegression.PredictProba(X: Matrix): Vector; -begin - if not fFitted then - NotFittedError(ER_FIT_NOT_CALLED); - - Result := Activations.Sigmoid(X * fCoef + fIntercept); -end; - -function LogisticRegression.Predict(X: Matrix; threshold: real): Vector; -begin - var p := PredictProba(X); - Result := new Vector(p.Length); - - for var i := 0 to p.Length - 1 do - if p[i] >= threshold then - Result[i] := 1.0 - else - Result[i] := 0.0; -end; - -function LogisticRegression.Predict(X: Matrix): Vector; -begin - Result := Predict(X, 0.5); -end; - -function LogisticRegression.ToString: string; -begin - Result := 'LogisticRegression(lambda=' + fLambda + - ', lr=' + fLearningRate + ', epochs=' + fEpochs + ')'; -end; - -function LogisticRegression.Clone: IModel; -begin - Result := new LogisticRegression(fLambda, fLearningRate, fEpochs); -end;*) - - //----------------------------- // RidgeRegression //----------------------------- @@ -1646,6 +1766,15 @@ end; function RidgeRegression.Fit(X: Matrix; y: Vector): IModel; begin + if 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); @@ -1682,7 +1811,17 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); + if X = nil then + ArgumentNullError(ER_X_NULL); + + if 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; @@ -1735,10 +1874,32 @@ end; function ElasticNet.Fit(X: Matrix; y: Vector): IModel; begin + if X = nil then + ArgumentNullError(ER_X_NULL); + + if y = nil then + ArgumentNullError(ER_Y_NULL); + + if 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); - // Loss(β) = ||y - (Xβ + b)||² + λ1||β||1 + λ2||β||2² + 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; @@ -1757,7 +1918,9 @@ begin yc[i] -= muY; fCoef := new Vector(p); - var residual := yc.Clone; // initial residual = yc (since β=0) + var residual := yc.Clone; // since β = 0 + + var prevLoss := real.PositiveInfinity; for var iter := 0 to fMaxIter - 1 do begin @@ -1789,6 +1952,37 @@ begin fCoef[j] := newBeta; 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 + 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; @@ -1804,7 +1998,17 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); + if X = nil then + ArgumentNullError(ER_X_NULL); + + if 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; @@ -1833,7 +2037,7 @@ begin end; //----------------------------- -// MulticlassLogisticRegression +// LogisticRegression //----------------------------- constructor LogisticRegression.Create(lambda: real; lr: real; epochs: integer); @@ -1846,9 +2050,33 @@ end; function LogisticRegression.Fit(X: Matrix; y: Vector): IModel; begin + if X = nil then + ArgumentNullError(ER_X_NULL); + + if y = nil then + ArgumentNullError(ER_Y_NULL); + + if 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); + var m := X.RowCount; var p := X.ColCount; @@ -1881,36 +2109,29 @@ begin end; // --- encode labels - var yEncoded := new Vector(m); - + var yEncoded := new integer[m]; for var i := 0 to m - 1 do - begin - var ir := Round(y[i]); // безопасно после проверки - yEncoded[i] := fClassToIndex[ir]; - end; - + yEncoded[i] := fClassToIndex[Round(y[i])]; + // --- init fW := new Matrix(p, fClassCount); fIntercept := new Vector(fClassCount); - - // --- one-hot - var YoneHot := new Matrix(m, fClassCount); - - for var i := 0 to m - 1 do - begin - var cls := Round(yEncoded[i]); // индекс класса 0..K-1 - YoneHot[i, cls] := 1.0; - end; + 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]; - // softmax (оставляем твою версию) + // --- stable softmax + loss accumulation + var loss := 0.0; + for var i := 0 to m - 1 do begin var maxVal := Z.RowMax(i); @@ -1922,23 +2143,75 @@ begin sumExp += Z[i,k]; end; + if sumExp <= 0 then + ArgumentError(ER_LOGISTIC_SOFTMAX_ZERO); + for var k := 0 to fClassCount - 1 do Z[i,k] /= sumExp; + + var yi := yEncoded[i]; + var prob := Z[i, yi]; + + if prob <= 0 then + prob := 1e-300; + + loss -= Ln(prob); end; - var diff := Z - YoneHot; + loss /= m; - var gradW := X.Transpose * diff; - gradW *= 1.0 / 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) < fTol 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]; + + for var k := 0 to fClassCount - 1 do + begin + var diff := Z[i,k]; + if k = yi then + diff -= 1.0; + + gradB[k] += diff; + + for var j := 0 to p - 1 do + gradW[j,k] += X[i,j] * diff; + end; + end; + + gradW *= 1.0 / m; + gradB *= 1.0 / m; if fLambda <> 0 then gradW += fLambda * fW; fW -= fLearningRate * gradW; - - var gradB := diff.ColumnSums; - gradB *= 1.0 / m; // ВАЖНО - fIntercept -= fLearningRate * gradB; end; @@ -1951,6 +2224,15 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); + if X = nil then + ArgumentNullError(ER_X_NULL); + + if 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; @@ -2044,46 +2326,45 @@ begin Result := m; end; -function GiniCriterion.Impurity(y: Vector): real; +function GiniCriterion.Impurity(y: Vector; indices: array of integer): real; begin - if y.Length = 0 then exit(0.0); + var n := indices.Length; + if n = 0 then exit(0.0); - var counts := Dict&; - foreach var v in y.data do - begin - var c := integer(v); - if counts.ContainsKey(c) then - counts[c] += 1 - else - counts[c] := 1; - end; + var counts := new integer[fClassCount]; + + foreach var i in indices do + counts[Round(y[i])] += 1; - var n := y.Length; var sumsq := 0.0; - foreach var kv in counts do - begin - var p := kv.Value / n; - sumsq += p * p; - end; + for var c := 0 to fClassCount - 1 do + if counts[c] > 0 then + begin + var p := counts[c] / n; + sumsq += p * p; + end; Result := 1.0 - sumsq; end; -function VarianceCriterion.Impurity(y: Vector): real; +function VarianceCriterion.Impurity(y: Vector; indices: array of integer): real; begin - if y.Length = 0 then exit(0.0); + var n := indices.Length; + if n = 0 then exit(0.0); - var mean := y.Mean; - var s := 0.0; + var sum := 0.0; + var sumsq := 0.0; - foreach var v in y.data do + foreach var i in indices do begin - var d := v - mean; - s += d*d; + var v := y[i]; + sum += v; + sumsq += v * v; end; - Result := s / y.Length; + var mean := sum / n; + Result := (sumsq / n) - mean * mean; end; function LeafClass(c: integer): DecisionTreeNode; @@ -2132,8 +2413,13 @@ begin Result := n; end; +const MAX_ALLOWED_TREE_DEPTH = 1000; + constructor DecisionTreeBase.Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; seed: integer); begin + if maxDepth > MAX_ALLOWED_TREE_DEPTH then + ArgumentError(ER_MAX_DEPTH_TOO_LARGE, maxDepth); + fMaxDepth := maxDepth; fMinSamplesSplit := minSamplesSplit; fMinSamplesLeaf := minSamplesLeaf; @@ -2230,7 +2516,7 @@ function DecisionTreeBase.BuildTree(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; begin // Ограничение глубины - if depth >= fMaxDepth then + if (fMaxDepth > 0) and (depth >= fMaxDepth) then exit(LeafNode(LeafValue(y, indices))); // Минимальное число объектов @@ -2241,7 +2527,7 @@ begin if IsPure(y, indices) then exit(LeafNode(LeafValue(y, indices))); - var parentImp := fCriterion.Impurity(y.SubvectorBy(indices)); + var parentImp := fCriterion.Impurity(y, indices); // Поиск лучшего разбиения var split := FindBestSplit(X, y, indices); @@ -2267,8 +2553,8 @@ begin var leftArr := left.ToArray; var rightArr := right.ToArray; - var leftImp := fCriterion.Impurity(y.SubvectorBy(leftArr)); - var rightImp := fCriterion.Impurity(y.SubvectorBy(rightArr)); + var leftImp := fCriterion.Impurity(y, leftArr); + var rightImp := fCriterion.Impurity(y, rightArr); var n := indices.Length; @@ -2281,8 +2567,8 @@ begin fFeatureImportances[split.Feature] += delta; // Рекурсия - var leftNode := BuildTree(X, y, left.ToArray, depth + 1); - var rightNode := BuildTree(X, y, right.ToArray, depth + 1); + var leftNode := BuildTree(X, y, leftArr, depth + 1); + var rightNode := BuildTree(X, y, rightArr, depth + 1); // Создание split-узла var node := new DecisionTreeNode; @@ -2295,130 +2581,14 @@ begin Result := node; end; -function DecisionTreeBase.FindBestSplit0(X: Matrix; y: Vector; indices: array of integer): SplitResult; -begin - var bestScore := 1e308; - var bestFeature := -1; - var bestThreshold := 0.0; - - var n := indices.Length; - if n < 2 then - begin - Result.Found := false; - exit; - end; - - // parent impurity (для feature importance) - 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 parentMean := sumAll / n; - var parentVar := (sumSqAll / n) - parentMean*parentMean; - - // --- перебор признаков - var features := GetFeatureSubset(X.Cols); - - foreach var j in features do - begin - // --- собрать пары (value, rowIndex) - var pairs: array of (real, integer); - SetLength(pairs, n); - - for var k := 0 to n-1 do - begin - var idx := indices[k]; - pairs[k] := (X[idx,j], idx); - end; - - // сортировка - pairs.Sort(p -> p.Item1); - - // подготовить y в этом порядке - var ySorted := new real[n]; - for var k := 0 to n-1 do - ySorted[k] := y[pairs[k].Item2]; - - var leftCount := 0; - var leftSum := 0.0; - var leftSumSq := 0.0; - - // идем по возможным split-позициям - for var k := 1 to n-1 do - begin - // переносим элемент k-1 влево - var v := ySorted[k-1]; - leftCount += 1; - leftSum += v; - leftSumSq += v*v; - - var rightCount := n - leftCount; - - if (leftCount < fMinSamplesLeaf) or - (rightCount < fMinSamplesLeaf) then - continue; - - var x1 := pairs[k-1].Item1; - var x2 := pairs[k].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; - - var weighted := - (leftCount / n) * leftVar + - (rightCount / n) * rightVar; - - if weighted < bestScore then - begin - bestScore := weighted; - bestFeature := j; - bestThreshold := (x1 + x2) / 2.0; - - if bestScore = 0.0 then - break; - end; - end; - end; - - Result.Found := bestFeature <> -1; - Result.Feature := bestFeature; - Result.Threshold := bestThreshold; - - // --- feature importance (optional) - if bestFeature <> -1 then - begin - var gain := parentVar - bestScore; - if gain > 0 then - fFeatureImportances[bestFeature] += gain; - end; -end; - -function DecisionTreeBase.FindBestSplitCls(X: Matrix; y: Vector; indices: array of integer; classCount: integer): SplitResult; -begin - Result := FindBestSplit0(X, y, indices); -end; +const EPS = 1e-12; function DecisionTreeBase.IsPure(y: Vector; indices: array of integer): boolean; begin - Result := fCriterion.Impurity(y.SubvectorBy(indices)) = 0.0; + Result := fCriterion.Impurity(y, indices) < EPS; end; -function DecisionTreeBase.FindBestSplitReg(X: Matrix; y: Vector; indices: array of integer): SplitResult; +function DecisionTreeRegressor.FindBestSplitReg(X: Matrix; y: Vector; indices: array of integer): SplitResult; begin var bestScore := 1e308; var bestFeature := -1; @@ -2505,7 +2675,7 @@ begin end; end; - var p := X.Cols; + var p := X.ColCount; // =============================== // FEATURE LOOP WITH SUBSET @@ -2539,22 +2709,141 @@ begin Result.Threshold := bestThreshold; end; -function DecisionTreeClassifier.PredictOne(x: Vector): integer; +//============================== +// DecisionTreeClassifier +//============================== + +function DecisionTreeClassifier.FindBestSplit(X: Matrix; y: Vector; indices: array of integer): SplitResult; +begin + var n := indices.Length; + if n <= 1 then + exit(SplitResult.Invalid); + + var p := X.ColCount; + + var bestFeature := -1; + var bestThreshold := 0.0; + var bestScore := real.MaxValue; + + // --- формируем список признаков и (при необходимости) выбираем подмножество без повторов + var feat := new integer[p]; + for var i := 0 to p - 1 do + feat[i] := i; + + var m := fMaxFeatures; + if (m <= 0) or (m > p) then + m := p; + + // partial Fisher–Yates: рандомизируем первые m позиций + for var i := 0 to m - 1 do + begin + var j := fRng.Next(i, p); + var tmp := feat[i]; + feat[i] := feat[j]; + feat[j] := tmp; + end; + + // --- перебираем только feat[0..m-1] + for var fi := 0 to m - 1 do + begin + var feature := feat[fi]; + + // пары (value, label) по текущему признаку + var values := new real[n]; + var labels := new integer[n]; + + for var i := 0 to n - 1 do + begin + var row := indices[i]; + values[i] := X[row, feature]; + + labels[i] := Round(y[row]); + end; + + // сортируем по values, а labels переставляются синхронно + System.Array.Sort(values, labels); + + var rightCounts := new integer[fClassCount]; + for var i := 0 to n - 1 do + rightCounts[labels[i]] += 1; + + var leftCounts := new integer[fClassCount]; + + var leftSize := 0; + var rightSize := n; + + for var i := 0 to n - 2 do + begin + var cls := labels[i]; + + leftCounts[cls] += 1; + rightCounts[cls] -= 1; + + leftSize += 1; + rightSize -= 1; + + if values[i] = values[i + 1] then + continue; + + if (leftSize < fMinSamplesLeaf) or (rightSize < fMinSamplesLeaf) then + continue; + + // gini(left) + var giniLeft := 1.0; + for var c := 0 to fClassCount - 1 do + begin + if leftCounts[c] > 0 then + begin + var q := leftCounts[c] / leftSize; + giniLeft -= q * q; + end; + end; + + // gini(right) + var giniRight := 1.0; + for var c := 0 to fClassCount - 1 do + begin + if rightCounts[c] > 0 then + begin + var q := rightCounts[c] / rightSize; + giniRight -= q * q; + end; + end; + + var weighted := + (leftSize / n) * giniLeft + + (rightSize / n) * giniRight; + + if weighted < bestScore then + begin + bestScore := weighted; + bestFeature := feature; + bestThreshold := (values[i] + values[i + 1]) * 0.5; + end; + end; + end; + + if bestFeature = -1 then + exit(SplitResult.Invalid); + + Result := SplitResult.Create(bestFeature, bestThreshold); +end; + +function DecisionTreeClassifier.PredictOne(X: Matrix; rowIndex: integer): integer; begin var node := fRoot; while not node.IsLeaf do begin - if x[node.FeatureIndex] <= node.Threshold then + if X[rowIndex, node.FeatureIndex] <= node.Threshold then node := node.Left else node := node.Right; end; - Result := fIndexToClass[integer(node.LeafValue)]; // internal class index + Result := integer(node.LeafValue); // внутренний индекс end; - function DecisionTreeClassifier.MajorityClass(y: Vector; indices: array of integer): integer; begin var counts := new integer[fClassCount]; @@ -2562,7 +2851,7 @@ begin // Подсчёт частот foreach var i in indices do begin - var c := integer(y[i]); + var c := Round(y[i]); counts[c] += 1; end; @@ -2583,18 +2872,42 @@ end; constructor DecisionTreeClassifier.Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; seed: integer); begin inherited Create(maxDepth, minSamplesSplit, minSamplesLeaf, seed); - fCriterion := new GiniCriterion; end; function DecisionTreeClassifier.Fit(X: Matrix; y: Vector): IModel; begin - if X.Rows <> y.Length then - DimensionError(ER_DIM_MISMATCH, X.Rows, y.Length); + if X = nil then + ArgumentNullError(ER_X_NULL); - if X.Rows = 0 then + if y = nil then + ArgumentNullError(ER_Y_NULL); + + if 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 fMinSamplesSplit < 2 then + ArgumentOutOfRangeError(ER_MINSAMPLESSPLIT_INVALID, fMinSamplesSplit); + + if fMinSamplesLeaf < 1 then + ArgumentOutOfRangeError(ER_MINSAMPLESLEAF_INVALID, fMinSamplesLeaf); + + if (fMaxDepth < 0) then + ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, fMaxDepth); + + if fMaxDepth > 10000 then + ArgumentError(ER_MAX_DEPTH_TOO_LARGE, fMaxDepth); - fFeatureImportances := new Vector(X.Cols); + + fFeatureImportances := new Vector(X.ColCount); // --- 1. Найти уникальные классы (с проверкой целочисленности) var classes := new HashSet; @@ -2621,6 +2934,8 @@ begin for var idx := 0 to fClassCount - 1 do fClassToIndex[fIndexToClass[idx]] := idx; + fCriterion := new GiniCriterion(fClassCount); + // --- 3. Создать закодированный y var yEncoded := new Vector(y.Length); @@ -2631,12 +2946,21 @@ begin end; // --- 4. Создать массив индексов строк - var indices := new integer[X.Rows]; - for var i := 0 to X.Rows - 1 do - indices[i] := i; + var indices: array of integer; + + if fRowIndices <> nil then + indices := fRowIndices + else + begin + indices := new integer[X.RowCount]; + for var i := 0 to X.RowCount - 1 do + indices[i] := i; + end; // --- 5. Построить дерево fRoot := BuildTree(X, yEncoded, indices, 0); + // Сбросить подмножество строк (не должно протекать на следующий Fit) + fRowIndices := nil; var s := fFeatureImportances.Sum; if s > 0 then @@ -2651,17 +2975,23 @@ end; function DecisionTreeClassifier.Predict(X: Matrix): Vector; begin if not fFitted then - NotFittedError(ER_FIT_NOT_CALLED);; + NotFittedError(ER_FIT_NOT_CALLED); - var n := X.Rows; + if X = nil then + ArgumentNullError(ER_X_NULL); + + if 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 begin - var row := X.GetRow(i); - var internalClass := PredictOne(row); - - // вернуть оригинальную метку + var internalClass := PredictOne(X, i); Result[i] := fIndexToClass[internalClass]; end; end; @@ -2750,21 +3080,48 @@ end; function DecisionTreeRegressor.Fit(X: Matrix; y: Vector): IModel; begin - if X.Rows <> y.Length then - DimensionError(ER_DIM_MISMATCH, X.Rows, y.Length); + if X = nil then + ArgumentNullError(ER_X_NULL); - if X.Rows = 0 then + if y = nil then + ArgumentNullError(ER_Y_NULL); + + if 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 fMinSamplesSplit < 2 then + ArgumentOutOfRangeError(ER_MINSAMPLESSPLIT_INVALID, fMinSamplesSplit); + + if fMinSamplesLeaf < 1 then + ArgumentOutOfRangeError(ER_MINSAMPLESLEAF_INVALID, fMinSamplesLeaf); + + if fLeafL2 < 0 then + ArgumentOutOfRangeError(ER_L2_NEGATIVE, fLeafL2); + + if fMaxDepth < 0 then + ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, fMaxDepth); + + if fMaxDepth > 10000 then + ArgumentError(ER_MAX_DEPTH_TOO_LARGE, fMaxDepth); - fFeatureImportances := new Vector(X.Cols); + fFeatureImportances := new Vector(X.ColCount); var indices: array of integer; // 🔹 Ключевое изменение if fRowIndices = nil then begin - SetLength(indices, X.Rows); - for var i := 0 to X.Rows - 1 do + SetLength(indices, X.RowCount); + for var i := 0 to X.RowCount - 1 do indices[i] := i; end else @@ -2784,19 +3141,19 @@ begin Result := Self; end; -function DecisionTreeRegressor.PredictOne(x: Vector): real; +function DecisionTreeRegressor.PredictOne(X: Matrix; rowIndex: integer): real; begin var node := fRoot; while not node.IsLeaf do begin - if x[node.FeatureIndex] <= node.Threshold then + if X[rowIndex, node.FeatureIndex] <= node.Threshold then node := node.Left else node := node.Right; end; - Result := node.LeafValue; + Result := node.LeafValue; // для регрессии это уже real end; function DecisionTreeRegressor.Predict(X: Matrix): Vector; @@ -2804,11 +3161,22 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); - var n := X.Rows; + if X = nil then + ArgumentNullError(ER_X_NULL); + + if 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.GetRow(i)); + Result[i] := PredictOne(X, i); end; function DecisionTreeRegressor.Clone: IModel; @@ -2884,24 +3252,11 @@ begin end; end; -procedure RandomForestBase.BootstrapSample(X: Matrix; y: Vector; - var Xb: Matrix; var yb: Vector); +procedure RandomForestBase.BootstrapRowIndices(n: integer; var rows: array of integer); begin - var n := X.Rows; - var p := X.Cols; - - Xb := new Matrix(n, p); - yb := new Vector(n); - + SetLength(rows, n); for var i := 0 to n - 1 do - begin - var idx := fRng.Next(n); - - for var j := 0 to p - 1 do - Xb[i,j] := X[idx,j]; - - yb[i] := y[idx]; - end; + rows[i] := fRng.Next(n); end; //----------------------------- @@ -2917,39 +3272,66 @@ end; function RandomForestRegressor.Fit(X: Matrix; y: Vector): IModel; begin - if X.Rows <> y.Length then - DimensionError(ER_DIM_MISMATCH, X.Rows, y.Length); + if X = nil then + ArgumentNullError(ER_X_NULL); - if X.Rows = 0 then + if y = nil then + ArgumentNullError(ER_Y_NULL); + + if 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 fNTrees <= 0 then + ArgumentOutOfRangeError(ER_NTREES_INVALID, fNTrees); + + if fMinSamplesSplit < 2 then + ArgumentOutOfRangeError(ER_MINSAMPLESSPLIT_INVALID, fMinSamplesSplit); + + if fMinSamplesLeaf < 1 then + ArgumentOutOfRangeError(ER_MINSAMPLESLEAF_INVALID, fMinSamplesLeaf); + + if fMaxDepth < 0 then + ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, fMaxDepth); + + if fMaxDepth > 10000 then + ArgumentError(ER_MAX_DEPTH_TOO_LARGE, fMaxDepth); + SetLength(fTrees, fNTrees); for var t := 0 to fNTrees - 1 do begin - var Xb: Matrix; - var yb: Vector; - - BootstrapSample(X, y, Xb, yb); - var treeSeed := fRng.Next(integer.MaxValue); - + var tree := new DecisionTreeRegressor( fMaxDepth, fMinSamplesSplit, fMinSamplesLeaf, treeSeed ); - - var p := X.Cols; + + var p := X.ColCount; var m := ComputeMaxFeatures(p); - - tree.fMaxFeatures := m; - tree.Fit(Xb, yb); + tree.fMaxFeatures := m; + + var rows: array of integer; + BootstrapRowIndices(X.RowCount, rows); + + tree.SetRowIndices(rows); + tree.Fit(X, y); + fTrees[t] := tree; end; - - fFitted := true; + + fFitted := true; Result := Self; end; @@ -2958,18 +3340,24 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); - var n := X.Rows; + if X = nil then + ArgumentNullError(ER_X_NULL); + + if ValidateFiniteInputs then + CheckXForPredict(X); + + var n := X.RowCount; var resultVec := new Vector(n); - for var t := 0 to fTrees.Length - 1 do - begin - var pred := fTrees[t].Predict(X); - for var i := 0 to n - 1 do - resultVec[i] += pred[i]; - end; - for var i := 0 to n - 1 do - resultVec[i] /= fTrees.Length; + 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; @@ -3047,38 +3435,68 @@ end; function RandomForestClassifier.Fit(X: Matrix; y: Vector): IModel; begin - if X.Rows <> y.Length then - DimensionError(ER_DIM_MISMATCH, X.Rows, y.Length); + if X = nil then + ArgumentNullError(ER_X_NULL); - if X.Rows = 0 then + if y = nil then + ArgumentNullError(ER_Y_NULL); + + if 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 fNTrees <= 0 then + ArgumentOutOfRangeError(ER_NTREES_INVALID, fNTrees); + + if fMinSamplesSplit < 2 then + ArgumentOutOfRangeError(ER_MINSAMPLESSPLIT_INVALID, fMinSamplesSplit); + + if fMinSamplesLeaf < 1 then + ArgumentOutOfRangeError(ER_MINSAMPLESLEAF_INVALID, fMinSamplesLeaf); + + if fMaxDepth < 0 then + ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, fMaxDepth); + + if fMaxDepth > MAX_ALLOWED_TREE_DEPTH then + ArgumentError(ER_MAX_DEPTH_TOO_LARGE, fMaxDepth); + SetLength(fTrees, fNTrees); - var p := X.Cols; - - for var t := 0 to fNTrees-1 do + for var t := 0 to fNTrees - 1 do begin - var Xb: Matrix; - var yb: Vector; - - BootstrapSample(X, y, Xb, yb); - var treeSeed := fRng.Next(integer.MaxValue); - + var tree := new DecisionTreeClassifier( fMaxDepth, fMinSamplesSplit, fMinSamplesLeaf, treeSeed ); - + + var p := X.ColCount; var m := ComputeMaxFeatures(p); + tree.fMaxFeatures := m; + + var rows: array of integer; + BootstrapRowIndices(X.RowCount, rows); + + tree.SetRowIndices(rows); + tree.Fit(X, y); - // классический RF для классификации - Sqrt(p) - tree.fMaxFeatures := m; - - tree.Fit(Xb, yb); + if t = 0 then // скопировать на первой итерации + begin + fClassCount := tree.ClassCount; + fIndexToClass := tree.IndexToClass; + end; + fTrees[t] := tree; end; @@ -3091,45 +3509,93 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); - var n := X.Rows; - var p := X.Cols; + if X = nil then + ArgumentNullError(ER_X_NULL); + + if ValidateFiniteInputs then + CheckXForPredict(X); + + var n := X.RowCount; + var treeCount := fTrees.Length; + + if treeCount = 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 + for var i := 0 to n - 1 do begin - var votes := new Dictionary; + // обнулить счётчики + for var c := 0 to fClassCount - 1 do + counts[c] := 0; - // сформировать вектор строки один раз - var row := new Vector(p); - for var j := 0 to p-1 do - row[j] := X[i,j]; - - for var t := 0 to fTrees.Length-1 do + // голосование + for var t := 0 to treeCount - 1 do begin - var cls := integer(fTrees[t].PredictOne(row)); - - if not votes.ContainsKey(cls) then - votes[cls] := 0; - - votes[cls] += 1; + var cls := fTrees[t].PredictOne(X, i); // внутренний индекс 0..K-1 + counts[cls] += 1; end; + // argmax var bestClass := 0; var bestCount := -1; - foreach var kv in votes do - if kv.Value > bestCount then + for var c := 0 to fClassCount - 1 do + if counts[c] > bestCount then begin - bestCount := kv.Value; - bestClass := kv.Key; + bestCount := counts[c]; + bestClass := c; end; - resultVec[i] := bestClass; + // вернуть исходную метку + resultVec[i] := fIndexToClass[bestClass]; end; Result := resultVec; 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 ValidateFiniteInputs then + CheckXForPredict(X); + + var n := X.RowCount; + var treeCount := fTrees.Length; + + if treeCount = 0 then + Error(ER_MODEL_NOT_INITIALIZED); + + var resultMat := new Matrix(n, fClassCount); + 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].PredictOne(X, i); // внутренний индекс + counts[cls] += 1; + end; + + // нормализация → вероятности + for var c := 0 to fClassCount - 1 do + resultMat[i, c] := counts[c] / treeCount; + end; + + Result := resultMat; +end; + function RandomForestClassifier.Clone: IModel; begin var rf := new RandomForestClassifier( @@ -3191,6 +3657,14 @@ begin ')'; 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; + //----------------------------- // GradientBoostingRegressor //----------------------------- @@ -3437,39 +3911,80 @@ function GradientBoostingRegressor.FitInternal( XVal: Matrix; yVal: Vector; useValidation: boolean): IModel; begin - // --- checks --- - if XTrain = nil then ArgumentNullError(ER_X_NULL); - if yTrain = nil then ArgumentNullError(ER_Y_NULL); + // --- null checks --- + if XTrain = nil then + ArgumentNullError(ER_X_NULL); - if XTrain.Rows <> yTrain.Length then - DimensionError(ER_XY_SIZE_MISMATCH); + if yTrain = nil then + ArgumentNullError(ER_Y_NULL); - if XTrain.Rows = 0 then + // --- finite checks --- + if 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 XVal = nil then + ArgumentNullError(ER_X_NULL); - if XVal.Rows <> yVal.Length then + if yVal = nil then + ArgumentNullError(ER_Y_NULL); + + if ValidateFiniteInputs then + begin + CheckXForPredict(XVal); + CheckYForFit(yVal); + end; + + if XVal.RowCount <> yVal.Length then DimensionError(ER_XY_SIZE_MISMATCH); - if XVal.Cols <> XTrain.Cols then + if XVal.ColCount <> XTrain.ColCount then DimensionError(ER_FEATURE_COUNT_MISMATCH); end; + // --- hyperparameter checks --- + if fNEstimators <= 0 then + ArgumentOutOfRangeError(ER_NESTIMATORS_INVALID, fNEstimators); + + if fLearningRate <= 0 then + ArgumentOutOfRangeError(ER_LEARNING_RATE_INVALID, fLearningRate); + + if (fSubsample <= 0.0) or (fSubsample > 1.0) then + ArgumentOutOfRangeError(ER_SUBSAMPLE_INVALID, fSubsample); + + if fMinSamplesSplit < 2 then + ArgumentOutOfRangeError(ER_MINSAMPLESSPLIT_INVALID, fMinSamplesSplit); + + if fMinSamplesLeaf < 1 then + ArgumentOutOfRangeError(ER_MINSAMPLESLEAF_INVALID, fMinSamplesLeaf); + + if fMaxDepth < 0 then + ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, fMaxDepth); + + if fMaxDepth > MAX_ALLOWED_TREE_DEPTH then + ArgumentError(ER_MAX_DEPTH_TOO_LARGE, fMaxDepth); + // --- init state --- fOOBLossHistory.Clear; fEstimators.Clear; - fFeatureCount := XTrain.Cols; + fFeatureCount := XTrain.ColCount; fTrainLossHistory.Clear; fValLossHistory.Clear; fBestIteration := -1; - fBestTrainLoss := real.PositiveInfinity; - fBestValLoss := real.PositiveInfinity; + fBestScoreLoss := real.PositiveInfinity; var noImprove := 0; @@ -3487,16 +4002,16 @@ begin for var i := 0 to nTrain - 1 do yPredTrain[i] := fInitValue; - // --- OOB state (used only if: subsample<1, no validation, and patience>0) --- + // --- OOB logic --- var useSubsample := fSubsample < 1.0; - var useOOB := (not useValidation) and useSubsample and (fEarlyStoppingPatience > 0); + var useOOB := + (not useValidation) and + useSubsample and + (fEarlyStoppingPatience > 0) and + fUseOOBEarlyStopping; - var yPredOOB := new Vector(nTrain); - var oobCount := new integer[nTrain]; - - if useOOB then - for var i := 0 to nTrain - 1 do - yPredOOB[i] := fInitValue; + var oobSum := new Vector(nTrain); // сумма вкладов OOB-деревьев + var oobCount := new integer[nTrain]; // сколько раз объект был OOB var yPredVal: Vector := nil; if useValidation then @@ -3510,12 +4025,12 @@ begin // --- boosting loop --- for var m := 0 to fNEstimators - 1 do begin - // pseudo-residuals on TRAIN only + // 1. residuals var r := new Vector(nTrain); ComputePseudoResiduals(yTrain, yPredTrain, r); var stageSeed := fRng.Next(integer.MaxValue); - + var tree := new DecisionTreeRegressor( fMaxDepth, fMinSamplesSplit, @@ -3524,18 +4039,32 @@ begin stageSeed ); - // --- subsample rows (and keep indices) --- + // --- subsample --- var rows: array of integer := nil; var used: array of boolean := nil; if useSubsample then begin - var k := Round(nTrain * fSubsample); + var k := Floor(nTrain * fSubsample); if k < 1 then k := 1; + if k > nTrain then k := nTrain; + + 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 + fRng.Next(nTrain - i); + var tmp := all[i]; + all[i] := all[j]; + all[j] := tmp; + end; rows := new integer[k]; for var i := 0 to k - 1 do - rows[i] := fRng.Next(nTrain); + rows[i] := all[i]; tree.SetRowIndices(rows); @@ -3550,13 +4079,11 @@ begin tree.Fit(XTrain, r); fEstimators.Add(tree); - // predict delta on all rows once var deltaTrain := tree.Predict(XTrain); - // --- update TRAIN prediction --- + // --- update TRAIN --- if useSubsample then begin - // update only sampled rows for var i := 0 to rows.Length - 1 do begin var idx := rows[i]; @@ -3569,16 +4096,18 @@ begin yPredTrain[i] += fLearningRate * deltaTrain[i]; end; - // --- OOB update --- + // --- update OOB --- if useOOB then + begin for var i := 0 to nTrain - 1 do if not used[i] then begin - yPredOOB[i] += fLearningRate * deltaTrain[i]; + oobSum[i] += fLearningRate * deltaTrain[i]; oobCount[i] += 1; end; + end; - // update VAL prediction (if used) + // --- update VAL --- if useValidation then begin var deltaVal := tree.Predict(XVal); @@ -3586,7 +4115,7 @@ begin yPredVal[i] += fLearningRate * deltaVal[i]; end; - // losses + // --- losses --- var trainLoss := ComputeTrainLoss(yTrain, yPredTrain); fTrainLossHistory.Add(trainLoss); @@ -3601,33 +4130,41 @@ begin 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 - mask[i] := oobCount[i] > 0; - if mask[i] then + 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); // ← добавили историю + fOOBLossHistory.Add(scoreLoss); end else begin scoreLoss := trainLoss; - fOOBLossHistory.Add(real.NaN); // чтобы длины совпадали + fOOBLossHistory.Add(real.NaN); end; end; - // early stopping + // --- early stopping --- if fEarlyStoppingPatience > 0 then begin - if (fBestValLoss - scoreLoss > MinImprovement) then + if (fBestScoreLoss - scoreLoss > MinImprovement) then begin - fBestValLoss := scoreLoss; + fBestScoreLoss := scoreLoss; fBestIteration := m; noImprove := 0; end @@ -3640,24 +4177,24 @@ begin end; end; - // if stopping enabled: cut trees after best iteration + // --- 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; @@ -3670,10 +4207,13 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if X.Cols <> fFeatureCount then + if ValidateFiniteInputs then + CheckXForPredict(X); + + if X.ColCount <> fFeatureCount then DimensionError(ER_FEATURE_COUNT_MISMATCH); - var n := X.Rows; + var n := X.RowCount; var yPred := new Vector(n); for var i := 0 to n - 1 do @@ -3697,25 +4237,25 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if X.Cols <> fFeatureCount then + if ValidateFiniteInputs then + CheckXForPredict(X); + + if X.ColCount <> fFeatureCount then DimensionError(ER_FEATURE_COUNT_MISMATCH); var totalTrees := fEstimators.Count; if (m < 0) or (m > totalTrees) then - ArgumentOutOfRangeError( - 'Stage m must be in range [0, {0}]!!Stage m must be in range [0, {0}]', - totalTrees - ); + ArgumentOutOfRangeError(ER_STAGE_OUT_OF_RANGE, m, totalTrees); - var n := X.Rows; + var n := X.RowCount; var yPred := new Vector(n); - // --- F0 + // F0 for var i := 0 to n - 1 do - yPred[i] := fInitValue; // mean(y) + yPred[i] := fInitValue; - // --- add first m trees + // добавить первые m деревьев for var t := 0 to m - 1 do begin var delta := fEstimators[t].Predict(X); @@ -3823,7 +4363,7 @@ begin // --- best iteration state --- copy.fBestIteration := fBestIteration; copy.fBestTrainLoss := fBestTrainLoss; - copy.fBestValLoss := fBestValLoss; + copy.fBestScoreLoss := fBestScoreLoss; // --- loss configuration --- copy.fLoss := fLoss; @@ -3860,19 +4400,27 @@ function GradientBoostingClassifier.FitInternal( XVal: Matrix; yVal: Vector; useValidation: boolean): IModel; begin - // --- checks + // --- null checks --- if XTrain = nil then ArgumentNullError(ER_X_NULL); if yTrain = nil then ArgumentNullError(ER_Y_NULL); - if XTrain.Rows <> yTrain.Length then - DimensionError(ER_XY_SIZE_MISMATCH); + // --- finite checks --- + if ValidateFiniteInputs then + begin + CheckXForFit(XTrain); + CheckYForFit(yTrain); + end; - if XTrain.Rows = 0 then + // --- 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 @@ -3881,20 +4429,48 @@ begin if yVal = nil then ArgumentNullError(ER_Y_NULL); - if XVal.Rows <> yVal.Length then + if ValidateFiniteInputs then + begin + CheckXForPredict(XVal); + CheckYForFit(yVal); + end; + + if XVal.RowCount <> yVal.Length then DimensionError(ER_XY_SIZE_MISMATCH); - if XVal.Cols <> XTrain.Cols then + if XVal.ColCount <> XTrain.ColCount then DimensionError(ER_FEATURE_COUNT_MISMATCH); end; + // --- hyperparameter checks --- + if fNEstimators <= 0 then + ArgumentOutOfRangeError(ER_NESTIMATORS_INVALID, fNEstimators); + + if fLearningRate <= 0 then + ArgumentOutOfRangeError(ER_LEARNING_RATE_INVALID, fLearningRate); + + if (fSubsample <= 0.0) or (fSubsample > 1.0) then + ArgumentOutOfRangeError(ER_SUBSAMPLE_INVALID, fSubsample); + + if fMinSamplesSplit < 2 then + ArgumentOutOfRangeError(ER_MINSAMPLESSPLIT_INVALID, fMinSamplesSplit); + + if fMinSamplesLeaf < 1 then + ArgumentOutOfRangeError(ER_MINSAMPLESLEAF_INVALID, fMinSamplesLeaf); + + if fMaxDepth < 0 then + ArgumentOutOfRangeError(ER_MAX_DEPTH_INVALID, fMaxDepth); + + if fMaxDepth > MAX_ALLOWED_TREE_DEPTH then + ArgumentError(ER_MAX_DEPTH_TOO_LARGE, fMaxDepth); + // --- reset state fOOBLossHistory.Clear; fEstimators.Clear; fTrainLossHistory.Clear; fValLossHistory.Clear; - fFeatureCount := XTrain.Cols; + fFeatureCount := XTrain.ColCount; fBestIteration := -1; fBestValLoss := real.PositiveInfinity; fFitted := false; @@ -3903,7 +4479,7 @@ begin BuildClassMapping(yTrain); var yEncoded := EncodeLabels(yTrain); - var nTrain := XTrain.Rows; + var nTrain := XTrain.RowCount; var classCount := fClassCount; // --- compute class priors @@ -3955,11 +4531,11 @@ begin if useValidation then begin - logitsVal := new Matrix(XVal.Rows, classCount); + logitsVal := new Matrix(XVal.RowCount, classCount); yValEncoded := EncodeLabels(yVal); // --- инициализация log-prior - for var i := 0 to XVal.Rows - 1 do + for var i := 0 to XVal.RowCount - 1 do for var cls := 0 to classCount - 1 do logitsVal[i, cls] := fInitLogits[cls]; end; @@ -3991,17 +4567,6 @@ begin var useSubsample := fSubsample < 1.0; var subIndices: array of integer := nil; - if useSubsample then - begin - var subCount := Round(fSubsample * nTrain); - if subCount < 1 then - subCount := 1; - - SetLength(subIndices, subCount); - for var i := 0 to subCount - 1 do - subIndices[i] := fRng.Next(nTrain); - end; - // inBag var inBag: array of boolean := nil; @@ -4066,7 +4631,7 @@ begin if useValidation then begin var deltaVal := tree.Predict(XVal); - for var i := 0 to XVal.Rows - 1 do + for var i := 0 to XVal.RowCount - 1 do logitsVal[i, cls] += fLearningRate * deltaVal[i]; end; end; @@ -4082,7 +4647,7 @@ begin if useValidation then begin - var probsVal := new Matrix(XVal.Rows, classCount); + var probsVal := new Matrix(XVal.RowCount, classCount); SoftmaxMatrix(logitsVal, probsVal); var valLoss := ComputeLogLoss(yValEncoded, probsVal); @@ -4208,8 +4773,8 @@ procedure GradientBoostingClassifier.SoftmaxMatrix( logits: Matrix; probs: Matrix); begin - var nSamples := logits.Rows; - var classCount := logits.Cols; + var nSamples := logits.RowCount; + var classCount := logits.ColCount; for var i := 0 to nSamples - 1 do begin @@ -4226,9 +4791,7 @@ begin end; end; -function GradientBoostingClassifier.ComputeLogLoss( - yEncoded: array of integer; - probs: Matrix): real; +function GradientBoostingClassifier.ComputeLogLoss(yEncoded: array of integer; probs: Matrix): real; begin var n := Length(yEncoded); var eps := 1e-15; @@ -4254,8 +4817,8 @@ function GradientBoostingClassifier.ComputeLogLossMasked( logits: Matrix; mask: array of boolean): real; begin - var n := logits.Rows; - var k := logits.Cols; + var n := logits.RowCount; + var k := logits.ColCount; var rowLogits := new real[k]; var rowProbs := new real[k]; @@ -4350,7 +4913,16 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); - var nSamples := X.Rows; + if X = nil then + ArgumentNullError(ER_X_NULL); + + if 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); @@ -4407,18 +4979,18 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if X.Cols <> fFeatureCount then + if 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( - 'Stage m must be in range [0, {0}]!!Stage m must be in range [0, {0}]', - total - ); + ArgumentOutOfRangeError(ER_STAGE_OUT_OF_RANGE, m, total); - var n := X.Rows; + var n := X.RowCount; var k := fClassCount; var logits := new Matrix(n, k); @@ -4452,7 +5024,7 @@ function GradientBoostingClassifier.PredictStage(X: Matrix; m: integer): Vector; begin var probs := PredictStageProba(X, m); - var n := probs.Rows; + var n := probs.RowCount; var resultVec := new Vector(n); for var i := 0 to n - 1 do @@ -4460,7 +5032,7 @@ begin var best := 0; var bestVal := probs[i,0]; - for var cls := 1 to probs.Cols - 1 do + for var cls := 1 to probs.ColCount - 1 do if probs[i,cls] > bestVal then begin bestVal := probs[i,cls]; @@ -4609,7 +5181,7 @@ begin var probs := PredictProba(X); - var nSamples := X.Rows; + var nSamples := X.RowCount; var classCount := fClassCount; Result := new Vector(nSamples); @@ -4645,14 +5217,14 @@ begin fIsFitted := False; end; -procedure KNNBase.CheckFitted; -begin - if not fIsFitted then - NotFittedError(ER_FIT_NOT_CALLED); -end; - procedure KNNBase.ValidatePredictInput(X: Matrix); begin + if X = nil then + ArgumentNullError(ER_X_NULL); + + if ValidateFiniteInputs then + CheckXForPredict(X); + if X.ColCount <> fXTrain.ColCount then DimensionError(ER_FEATURE_COUNT_MISMATCH); end; @@ -4717,6 +5289,8 @@ end; // KNNClassifier //----------------------------- +const KNN_EPS = 1e-12; + constructor KNNClassifier.Create(k: integer; weighting: KNNWeighting); begin inherited Create(k, weighting); @@ -4763,14 +5337,17 @@ begin 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); - // проверить NaN в X - for var i := 0 to X.RowCount - 1 do - for var j := 0 to X.ColCount - 1 do - if double.IsNaN(X[i,j]) then - ArgumentError(ER_NAN_IN_X); + if ValidateFiniteInputs then + begin + CheckXForFit(X); + CheckYForFit(y); + end; // копия train data fXTrain := X.Clone; // предполагаем, что Clone делает глубокую копию @@ -4853,7 +5430,7 @@ begin // exact match: если среди k ближайших есть dist=0, возвращаем его класс var exactCls := -1; for var t := 0 to fK - 1 do - if fNeighbors[t].dist = 0 then + if fNeighbors[t].dist < KNN_EPS then begin exactCls := fYEnc[fNeighbors[t].idx]; break; @@ -4904,10 +5481,25 @@ begin touchCount += 1; end; - var w := 1.0 / dist; - fVotes[cls] += w; + 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]; @@ -4934,9 +5526,6 @@ begin if not fIsFitted then NotFittedError(ER_FIT_NOT_CALLED); - if X = nil then - ArgumentNullError(ER_X_NULL); - ValidatePredictInput(X); var m := X.RowCount; @@ -4959,7 +5548,7 @@ begin // exact match: если среди k ближайших есть dist=0, вероятность 1 у его класса var exactCls := -1; for var t := 0 to fK - 1 do - if fNeighbors[t].dist = 0 then + if fNeighbors[t].dist < KNN_EPS then begin exactCls := fYEnc[fNeighbors[t].idx]; break; @@ -5003,13 +5592,19 @@ begin 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; @@ -5017,18 +5612,37 @@ begin fTouched[touchCount] := cls; touchCount += 1; end; - - var w := 1.0 / dist; + + var w := 1.0 / Sqrt(dist); // согласовано с Predict fVotes[cls] += w; sumW += w; end; - - // нормализация: сумма = sumW - for var k2 := 0 to touchCount - 1 do + + // 🔴 ОБЯЗАТЕЛЬНО проверить exact снова + + if exactCls <> -1 then begin - var cls := fTouched[k2]; - Result[i, cls] := fVotes[cls] / sumW; + 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 / fK; + for var k2 := 0 to touchCount - 1 do + begin + var cls := fTouched[k2]; + Result[i, cls] := uniform; + end; + end; end; end; end; @@ -5053,19 +5667,17 @@ begin 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); - // Проверка NaN в X - for var i := 0 to X.RowCount - 1 do - for var j := 0 to X.ColCount - 1 do - if double.IsNaN(X[i,j]) then - ArgumentError(ER_NAN_IN_X); - - // Проверка NaN в y - for var i := 0 to y.Length - 1 do - if double.IsNaN(y[i]) then - ArgumentError(ER_NAN_IN_Y); + if ValidateFiniteInputs then + begin + CheckXForFit(X); + CheckYForFit(y); + end; fXTrain := X.Clone; fYTrain := y.Clone; @@ -5073,8 +5685,7 @@ begin var n := fXTrain.RowCount; SetLength(fNeighbors, n); - fIsFitted := True; - + fIsFitted := true; Result := Self; end; @@ -5083,9 +5694,6 @@ begin if not fIsFitted then NotFittedError(ER_FIT_NOT_CALLED); - if X = nil then - ArgumentNullError(ER_X_NULL); - ValidatePredictInput(X); var m := X.RowCount; @@ -5095,20 +5703,20 @@ begin 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 ближайших (первые k элементов, порядок произвольный) + // --- выбрать k ближайших QuickSelect(fK - 1); - // exact match: ищем dist=0 среди k ближайших + // --- exact match var exactIdx := -1; for var t := 0 to fK - 1 do - if fNeighbors[t].dist = 0 then + if fNeighbors[t].dist < KNN_EPS then begin exactIdx := fNeighbors[t].idx; break; @@ -5120,10 +5728,11 @@ begin continue; end; + // --- uniform weighting if fWeighting = KNNWeighting.Uniform then begin - // среднее по k var sum := 0.0; + for var t := 0 to fK - 1 do sum += fYTrain[fNeighbors[t].idx]; @@ -5131,22 +5740,46 @@ begin end else begin - // weighted: веса 1 / dist (dist = squared distance) + // --- 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; - // dist=0 здесь уже не встречается из-за exact-match выше - var w := 1.0 / 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; - Result[i] := sumWY / sumW; + 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; @@ -5192,19 +5825,25 @@ 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 Pipeline(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); @@ -5237,15 +5876,34 @@ 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; foreach var t in fTransformers do begin + if t = nil then + ArgumentError(ER_PIPELINE_STEP_NULL); + if t is ISupervisedTransformer (var sup) then sup.Fit(Xt, y) - else t.Fit(Xt); + else + t.Fit(Xt); Xt := t.Transform(Xt); + + if Xt = nil then + ArgumentError(ER_PIPELINE_TRANSFORM_RETURNED_NULL); end; fModel.Fit(Xt, y); @@ -5259,11 +5917,22 @@ 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; @@ -5272,6 +5941,9 @@ 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; @@ -5281,8 +5953,11 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); + if fModel = nil then + ArgumentError(ER_MODEL_NULL); + if not (fModel is IProbabilisticClassifier) then - Error(ER_PROBA_NOT_SUPPORTED); + ArgumentError(ER_PROBA_NOT_SUPPORTED); var Xt := Transform(X); @@ -5635,94 +6310,140 @@ end; function SelectKBest.ComputeAnovaF(feature: Vector; y: Vector): real; begin - var mean0 := 0.0; - var mean1 := 0.0; - var n0 := 0; - var n1 := 0; - - for var i := 0 to y.Length - 1 do - if y[i] = 0 then - begin - mean0 += feature[i]; - n0 += 1; - end - else - begin - mean1 += feature[i]; - n1 += 1; - end; - - if (n0 = 0) or (n1 = 0) then + var n := feature.Length; + if n = 0 then exit(0.0); - mean0 /= n0; - mean1 /= n1; - - var grandMean := feature.Mean; - - var ssBetween := - n0 * Sqr(mean0 - grandMean) + - n1 * Sqr(mean1 - grandMean); - - var ssWithin := 0.0; + // --- построение отображения классов --- + var classToIndex := new Dictionary; + var uniqueClasses := new List; for var i := 0 to y.Length - 1 do - if y[i] = 0 then - ssWithin += Sqr(feature[i] - mean0) - else - ssWithin += Sqr(feature[i] - mean1); + begin + var cls := Round(y[i]); + if not classToIndex.ContainsKey(cls) then + begin + classToIndex[cls] := uniqueClasses.Count; + uniqueClasses.Add(cls); + end; + end; + + var classCount := uniqueClasses.Count; + if classCount < 2 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 := classToIndex[Round(y[i])]; + counts[classIdx] += 1; + means[classIdx] += feature[i]; + end; + + for var classIdx := 0 to classCount - 1 do + if counts[classIdx] > 0 then + means[classIdx] /= counts[classIdx]; + + // --- SS_between --- + var ssBetween := 0.0; + for var classIdx := 0 to classCount - 1 do + if counts[classIdx] > 0 then + begin + var diff := means[classIdx] - globalMean; + ssBetween += counts[classIdx] * diff * diff; + end; + + // --- SS_within --- + var ssWithin := 0.0; + for var i := 0 to n - 1 do + begin + var classIdx := classToIndex[Round(y[i])]; + var diff := feature[i] - means[classIdx]; + ssWithin += diff * diff; + end; if ssWithin = 0 then - exit(1e308); + exit(0.0); - Result := (ssBetween) / (ssWithin / (y.Length - 2)); + var msBetween := ssBetween / (classCount - 1); + var msWithin := ssWithin / (n - classCount); + + if msWithin = 0 then + exit(0.0); + + Result := msBetween / msWithin; end; function SelectKBest.ComputeChiSquare(feature: Vector; y: Vector): real; begin - var n := y.Length; - - var sum0 := 0.0; - var sum1 := 0.0; - var total := 0.0; - var n0 := 0; - var n1 := 0; + var n := feature.Length; + if n = 0 then + exit(0.0); + // --- проверка неотрицательности --- for var i := 0 to n - 1 do - begin - var v := feature[i]; - if v < 0 then + if feature[i] < 0 then ArgumentError(ER_CHI_SQUARE_NEGATIVE); - total += v; + // --- построение отображения классов --- + var classToIndex := new Dictionary; + var uniqueClasses := new List; - if y[i] = 0 then + for var i := 0 to y.Length - 1 do + begin + var cls := Round(y[i]); + if not classToIndex.ContainsKey(cls) then begin - sum0 += v; - n0 += 1; - end - else - begin - sum1 += v; - n1 += 1; + classToIndex[cls] := uniqueClasses.Count; + uniqueClasses.Add(cls); end; end; - if (n0 = 0) or (n1 = 0) or (total = 0) then + var classCount := uniqueClasses.Count; + if classCount < 2 then exit(0.0); - var expected0 := total * n0 / n; - var expected1 := total * n1 / n; + var counts := new integer[classCount]; + var observedSums := new real[classCount]; - var chi := 0.0; + var totalSum := 0.0; - if expected0 > 0 then - chi += Sqr(sum0 - expected0) / expected0; + for var i := 0 to n - 1 do + begin + var classIdx := classToIndex[Round(y[i])]; + counts[classIdx] += 1; + observedSums[classIdx] += feature[i]; + totalSum += feature[i]; + end; - if expected1 > 0 then - chi += Sqr(sum1 - expected1) / expected1; + if totalSum = 0 then + exit(0.0); - Result := chi; + var globalMean := totalSum / n; + + var chiSquare := 0.0; + + for var classIdx := 0 to classCount - 1 do + begin + if counts[classIdx] = 0 then + continue; + + var expected := counts[classIdx] * globalMean; + + if expected > 0 then + begin + var diff := observedSums[classIdx] - expected; + chiSquare += diff * diff / expected; + end; + end; + + Result := chiSquare; end; function SelectKBest.ComputeScore(feature: Vector; y: Vector): real; @@ -5739,6 +6460,15 @@ end; function SelectKBest.Fit(X: Matrix; y: Vector): ITransformer; begin + 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 + ArgumentError(ER_SELECTKBEST_K_INVALID, fK); + var p := X.ColCount; var scores: array of (real, integer); @@ -5754,7 +6484,7 @@ begin if fScoreFunc <> nil then fScoreFunc(col, y) else - ComputeCorrelation(col, y); + ComputeScore(col, y); scores[j] := (s, j); end; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 042c6c03b..c6a59e015 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -187,16 +187,16 @@ begin if fFitted then Error(ER_PIPELINE_MODIFY_AFTER_FIT); - if step is IDataStep then + if step is IPreprocessor then // Data Step begin if (fMatrixSteps.Count > 0) or (fModel <> nil) then ArgumentError(ER_DATAPIPE_DF_AFTER_MATRIX); - + fDataSteps.Add(step as IPreprocessor); exit(Self); end; - if step is IMatrixStep then + if step is ITransformer then // Matrix Step begin if step is IModel then begin @@ -270,7 +270,7 @@ begin end; // 5) Модель - fModel.Fit(X, y); + fModel := fModel.Fit(X, y); fFitted := true; Result := Self; diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index d6980fadd..d35c53ee7 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -9,40 +9,44 @@ type public /// Делит данные на обучающую и тестовую выборки. /// testRatio — доля объектов, попадающих в тестовую выборку (по умолчанию 0.2). - /// Перемешивает объекты перед разбиением. - /// Возвращает (X_train, X_test, y_train, y_test). + /// Перед разбиением объекты перемешиваются. + /// Возвращает кортеж (X_train, X_test, y_train, y_test). static function TrainTestSplit(X: Matrix; y: Vector; - testRatio: real := 0.2; seed: integer := 0): (Matrix, Matrix, Vector, Vector); + testRatio: real := 0.2; seed: integer := -1): (Matrix, Matrix, Vector, Vector); /// Разбивает индексы объектов на k непересекающихся частей (fold). - /// На каждом шаге одна часть используется как тестовая, остальные — как обучающая. - /// Применяется для k-fold кросс-валидации. + /// На каждом шаге одна часть используется как тестовая, остальные — как обучающая выборка. + /// Используется для k-fold кросс-валидации. /// Возвращает последовательность пар (trainIdx, testIdx). - static function KFold(n, k: integer; seed: integer := 0): + static function KFold(n, k: integer; seed: integer := -1): sequence of (array of integer, array of integer); - /// Разбивает данные на k частей с сохранением пропорций классов. - /// В каждой части сохраняется примерно то же соотношение - /// объектов разных классов, что и во всей выборке. - /// Рекомендуется для задач классификации, - /// особенно при неравномерном распределении классов. - /// Возвращает последовательность пар (trainIdx, testIdx). +/// Разбивает данные на k частей (k-fold) с сохранением пропорций классов +/// (стратифицированная k-fold кросс-валидация). +/// В каждой части доля объектов каждого класса +/// максимально близка к их доле во всей выборке +/// (разница не превышает одного объекта на класс). +/// Рекомендуется для задач классификации, +/// особенно при несбалансированных классах. +/// Возвращает последовательность пар (trainIdx, testIdx) static function StratifiedKFold(y: Vector; k: integer; - seed: integer := 0): sequence of (array of integer, array of integer); + seed: integer := -1): sequence of (array of integer, array of integer); /// Выполняет k-fold кросс-валидацию модели. - /// На каждом шаге модель обучается на обучающей части и оценивается на тестовой. - /// metric — функция качества (например, Accuracy или MSE). + /// На каждом шаге модель обучается на обучающей части и оценивается на соответствующей тестовой части. + /// metric — функция качества, принимающая (y_true, y_pred) + /// и возвращающая значение метрики (например, Accuracy или MSE). /// Возвращает среднее значение метрики по всем частям. static function CrossValidate(model: IModel; X: Matrix; y: Vector; - k: integer; metric: (Vector,Vector) -> real; seed: integer := 0): real; + k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real; - /// Выполняет k-fold кросс-валидацию с сохранением пропорций классов. - /// Использует StratifiedKFold для разбиения данных. - /// Подходит для задач классификации. - /// Возвращает среднее значение метрики по всем частям. + /// Выполняет стратифицированную k-fold кросс-валидацию модели. + /// Разбиение данных выполняется методом StratifiedKFold + /// с сохранением пропорций классов в каждой части. + /// Рекомендуется для задач классификации, особенно при несбалансированных классах. + /// Возвращает среднее значение метрики по k разбиениям. static function StratifiedCrossValidate(model: IModel; X: Matrix; y: Vector; - k: integer; metric: (Vector,Vector) -> real; seed: integer := 0): real; + k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real; end; /// Класс для подбора гиперпараметров методом перебора по сетке (Grid Search). @@ -105,13 +109,17 @@ begin var n := X.RowCount; var p := X.ColCount; - var rnd := new System.Random(seed); + var rnd := if seed >= 0 then + new System.Random(seed) + else + new System.Random; // Перемешанные индексы var idx := (0..n-1).ToArray; idx := idx.OrderBy(i -> rnd.Next).ToArray; - var testSize := Round(n * testRatio); + var rawSize := Round(n * testRatio); + var testSize := Max(1, Min(n - 1, rawSize)); var trainSize := n - testSize; var X_train := new Matrix(trainSize, p); @@ -151,70 +159,144 @@ begin if (k < 2) or (k > n) then ArgumentError(ER_K_INVALID, k, n); - var rnd := new System.Random(seed); - var idx := (0..n-1).OrderBy(i -> rnd.Next).ToArray; + var rnd := if seed >= 0 then + new System.Random(seed) + else + new System.Random; + + // --- 1. Создаём массив индексов 0..n-1 + var idx := new integer[n]; + for var i := 0 to n - 1 do + idx[i] := i; + + // --- 2. Fisher–Yates shuffle (in-place, O(n)) + for var i := n - 1 downto 1 do + begin + var j := rnd.Next(i + 1); + var tmp := idx[i]; + idx[i] := idx[j]; + idx[j] := tmp; + end; var baseSize := n div k; var extra := n mod k; var start := 0; + // --- 3. Формируем фолды for var fold := 0 to k - 1 do begin - var size := baseSize + (if fold < extra then 1 else 0); + var size := baseSize + Ord(fold < extra); - var testIdx := idx.Skip(start).Take(size).ToArray; - var trainIdx := idx.Take(start).Concat(idx.Skip(start + size)).ToArray; + // --- test indices + var testIdx := new integer[size]; + System.Array.Copy(idx, start, testIdx, 0, size); + + // --- train indices + var trainSize := n - size; + var trainIdx := new integer[trainSize]; + + // левая часть + if start > 0 then + System.Array.Copy(idx, 0, trainIdx, 0, start); + + // правая часть + var tailCount := n - (start + size); + if tailCount > 0 then + System.Array.Copy(idx, start + size, trainIdx, start, tailCount); yield (trainIdx, testIdx); + start += size; end; end; -static function Validation.StratifiedKFold(y: Vector; k: integer; - seed: integer): sequence of (array of integer, array of integer); +static function Validation.StratifiedKFold(y: Vector; k: integer; seed: integer): + sequence of (array of integer, array of integer); begin var n := y.Length; if (k < 2) or (k > n) then ArgumentError(ER_K_INVALID_STRATIFIED, k, n); - var rnd := new System.Random(seed); + var rnd := + if seed >= 0 then new System.Random(seed) + else new System.Random; - var idx0 := (0..n-1) - .Where(i -> y[i] = 0) - .OrderBy(i -> rnd.Next) - .ToArray; + // 1) Собираем индексы по классам за один проход + var classMap := new Dictionary>(); + for var i := 0 to n - 1 do + begin + var v := y[i]; + var cls := integer(v); - var idx1 := (0..n-1) - .Where(i -> y[i] = 1) - .OrderBy(i -> rnd.Next) - .ToArray; + // Валидация: метки должны быть целыми (хотя тип double) + if Abs(v - cls) > 1e-12 then + ArgumentError(ER_STRATIFIED_LABELS_INVALID); - if idx0.Length + idx1.Length <> n then - ArgumentError(ER_STRATIFIED_LABELS_INVALID); + var lst: List; + if classMap.TryGetValue(cls, lst) then + lst.Add(i) + else + begin + lst := new List; + lst.Add(i); + classMap.Add(cls, lst); + end; + end; - var base0 := idx0.Length div k; - var extra0 := idx0.Length mod k; + // 2) Подготавливаем контейнеры фолдов + var folds := new List[k]; + for var f := 0 to k - 1 do + folds[f] := new List; - var base1 := idx1.Length div k; - var extra1 := idx1.Length mod k; + // 3) Для каждого класса: shuffle + равномерно раскладываем по k фолдам + foreach var pair in classMap do + begin + var indices := pair.Value; + var m := indices.Count; + // Fisher–Yates shuffle in-place + for var i := m - 1 downto 1 do + begin + var j := rnd.Next(i + 1); + var tmp := indices[i]; + indices[i] := indices[j]; + indices[j] := tmp; + end; + + // Равномерное распределение (разница размеров ≤ 1) + var baseSize := m div k; + var extra := m mod k; + var start := 0; + + for var fold := 0 to k - 1 do + begin + var size := baseSize + Ord(fold < extra); + for var t := 0 to size - 1 do + folds[fold].Add(indices[start + t]); + start += size; + end; + end; + + // 4) Возвращаем (trainIdx, testIdx) для каждого фолда for var fold := 0 to k - 1 do begin - var start0 := fold * base0 + Min(fold, extra0); - var size0 := base0 + (if fold < extra0 then 1 else 0); + var testIdx := folds[fold].ToArray(); - var start1 := fold * base1 + Min(fold, extra1); - var size1 := base1 + (if fold < extra1 then 1 else 0); + // boolean-mask вместо Contains -> O(n) + var mask := new boolean[n]; + foreach var id in testIdx do + mask[id] := true; - var testIdx := - idx0.Skip(start0).Take(size0) - .Concat(idx1.Skip(start1).Take(size1)) - .ToArray; + var trainSize := n - testIdx.Length; + var trainIdx := new integer[trainSize]; + var p := 0; - var trainIdx := - (0..n-1) - .Where(i -> not testIdx.Contains(i)) - .ToArray; + for var i := 0 to n - 1 do + if not mask[i] then + begin + trainIdx[p] := i; + p += 1; + end; yield (trainIdx, testIdx); end;