From be48d378beb39a8c15815c8d3d9b6caeb6b0802c Mon Sep 17 00:00:00 2001 From: Mikhalkovich Stanislav Date: Thu, 23 Apr 2026 16:30:27 +0300 Subject: [PATCH] =?UTF-8?q?ML=20-=20=D0=BE=D1=87=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=B0=D1=83=D0=B4=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/Lib/InspectionML.pas | 6 +- bin/Lib/LinearAlgebraML.pas | 18 ++++- bin/Lib/MLCoreABC.pas | 4 ++ bin/Lib/MLDatasets.pas | 6 +- bin/Lib/MLExceptions.pas | 2 +- bin/Lib/MLModelsABC.pas | 131 +++++++++++++++++------------------- bin/Lib/MLPipelineABC.pas | 33 +++++++-- bin/Lib/MetricsABC.pas | 54 +-------------- bin/Lib/PreprocessorABC.pas | 58 ++++++++++++++-- bin/Lib/ValidationML.pas | 42 ++++++++++-- 10 files changed, 204 insertions(+), 150 deletions(-) diff --git a/bin/Lib/InspectionML.pas b/bin/Lib/InspectionML.pas index a29280077..91090afc5 100644 --- a/bin/Lib/InspectionML.pas +++ b/bin/Lib/InspectionML.pas @@ -54,6 +54,7 @@ type implementation uses MLExceptions; +uses MLUtilsABC; const ER_SCORE_FUNC_NULL = @@ -90,9 +91,8 @@ begin var resultVec := new Vector(p); - var baseSeed := - if seed >= 0 then seed - else System.Environment.TickCount and integer.MaxValue; + var userProvidedSeed: boolean; + var baseSeed := ResolveRandomSeed(seed, userProvidedSeed); for var j := 0 to p - 1 do begin diff --git a/bin/Lib/LinearAlgebraML.pas b/bin/Lib/LinearAlgebraML.pas index 07b6b865b..53cb091b2 100644 --- a/bin/Lib/LinearAlgebraML.pas +++ b/bin/Lib/LinearAlgebraML.pas @@ -4,6 +4,9 @@ // Используется дисперсия генеральной совокупности (деление на n), // как принято в численных методах и алгоритмах машинного обучения. // +// Исключение: Matrix.PCA использует (n-1) для ковариационной матрицы +// согласно статистической политике MLABC (пункт 3). +// // См. статистическую политику в модуле MLABC. // ============================================================= @@ -339,7 +342,8 @@ const 'Матрица вырождена или плохо обусловлена!!Matrix is singular or ill-conditioned'; ER_EMPTY_MATRIX = 'Матрица пуста!!Matrix is empty'; - + ER_EIGEN_NOT_CONVERGED = + 'EigenSymmetric: не сошлось за {0} итераций (off={1}, tol={2})!!EigenSymmetric did not converge in {0} iterations (off={1}, tol={2})'; type MLNotSPDException = class(MLException); @@ -1276,10 +1280,11 @@ begin var M := Clone; var V := Matrix.Identity(n); + var off := 0.0; for var iter := 0 to maxIter - 1 do begin // --- Норма вне-диагонали - var off := 0.0; + off := 0.0; for var i := 0 to n - 1 do for var j := i + 1 to n - 1 do off += M[i, j] * M[i, j]; @@ -1358,6 +1363,12 @@ begin end; end; + var offNorm := Sqrt(off); + + if offNorm >= tol then + Error(ER_EIGEN_NOT_CONVERGED, maxIter, offNorm, tol); + + // --- Eigenvalues var values := new Vector(n); for var i := 0 to n - 1 do @@ -1449,7 +1460,8 @@ begin end; // --- Eigen - var (values, V) := C.EigenSymmetric; + var adaptiveIter := Max(100, n * n * 10); + var (values, V) := C.EigenSymmetric(1e-12, adaptiveIter); // --- Выбор k компонент var components := new Matrix(n, k); diff --git a/bin/Lib/MLCoreABC.pas b/bin/Lib/MLCoreABC.pas index 85110869d..82c44b0b3 100644 --- a/bin/Lib/MLCoreABC.pas +++ b/bin/Lib/MLCoreABC.pas @@ -166,6 +166,10 @@ type function Fit(X: Matrix): IUnsupervisedTransformer; end; + IColumnExpander = interface + function GetExpandedColumns(sourceColumn: string): array of string; + end; + implementation end. \ No newline at end of file diff --git a/bin/Lib/MLDatasets.pas b/bin/Lib/MLDatasets.pas index 706e50dc4..0732901b8 100644 --- a/bin/Lib/MLDatasets.pas +++ b/bin/Lib/MLDatasets.pas @@ -88,7 +88,9 @@ type /// • clusterStd — базовое стандартное отклонение /// • clusterStdVar — разброс std между кластерами (0 → одинаковые) /// • centerBox — диапазон генерации центров [-centerBox, centerBox] - /// • classBalance — равномерность кластеров (0..1, 1 = равномерно) + /// • classBalance — равномерность распределения объектов по кластерам (0..1] + /// • classBalance = 1.0 — строго равномерное распределение (детерминированное, не зависит от seed) + /// • classBalance < 1.0 — случайное распределение; чем меньше значение, тем выше дисбаланс в среднем /// • noisePoints — число шумовых точек (outliers) /// • shuffle — перемешивание /// • seed — генератор (seed < 0 → случайный) @@ -748,7 +750,7 @@ begin // --- вероятности кластеров var probs := new real[centers]; - if classBalance = 1 then + if Abs(classBalance - 1.0) < 1e-12 then begin for var c := 0 to centers - 1 do probs[c] := 1.0 / centers; diff --git a/bin/Lib/MLExceptions.pas b/bin/Lib/MLExceptions.pas index 1d7995ede..bda250059 100644 --- a/bin/Lib/MLExceptions.pas +++ b/bin/Lib/MLExceptions.pas @@ -32,7 +32,7 @@ const ER_XY_SIZE_MISMATCH = 'Размеры X и y не совпадают: X={0}, y={1}!!X and y size mismatch: X={0}, y={1}'; ER_FEATURE_COUNT_MISMATCH = - 'Число признаков не совпадает!!Feature count mismatch'; + 'Число признаков не совпадает: {0} и {1}!!Feature count mismatch: {0}, {1}'; ER_NAN_IN_X = 'X содержит NaN (пропуски)!!X contains NaN'; ER_NAN_IN_Y = diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 9de5e56f8..6ec1649f0 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -2475,7 +2475,15 @@ type {$endregion Utility functions} -type +type +/// Проверять ли входные данные моделей на NaN и Infinity. +/// +/// По умолчанию: True — модели валидируют вход и выбрасывают исключение. +/// +/// Установка в False отключает проверки ГЛОБАЛЬНО для всех моделей. +/// Использовать только если данные гарантированно очищены. +/// +/// При наличии NaN/Inf поведение моделей не определено MLConfig = static class public /// Проверять ли входные данные моделей на NaN, Inf @@ -2624,9 +2632,9 @@ const ER_LEAFL2_INVALID = 'leafL2 должно быть >= 0 ({0}).!!' + 'leafL2 must be >= 0 ({0}).'; - ER_MIN_LEAF_GE_SPLIT = - 'minSamplesLeaf ({0}) должно быть меньше minSamplesSplit ({1}).!!' + - 'minSamplesLeaf ({0}) must be less than minSamplesSplit ({1}).'; + ER_MIN_LEAF_GT_SPLIT = + 'minSamplesSplit ({1}) должен быть > 2 * minSamplesLeaf ({0}).!!' + + 'minSamplesSplit ({1}) must be > 2 * minSamplesLeaf ({0}).'; ER_OOB_NOT_ENABLED = 'OOB score не включен для этой модели. Установите computeOOB = true в конструкторе.!!' + 'OOB score is not enabled for this model. Set computeOOB = true in the constructor.'; @@ -3252,10 +3260,6 @@ begin // --- init fW := new Matrix(p, fClassCount); - var scale := 0.01; - for var j := 0 to p - 1 do - for var k := 0 to fClassCount - 1 do - fW.Data[j,k] := (Random - 0.5) * 2 * scale; fIntercept := new Vector(fClassCount); @@ -3718,7 +3722,7 @@ begin if fClassLabels = nil then ArgumentError(ER_CLASSES_NOT_AVAILABLE); - Result := fClassLabels; + Result := Copy(fClassLabels); end; function GiniCriterion.Impurity(y: Vector; indices: array of integer): real; @@ -4375,11 +4379,9 @@ begin if minSamplesLeaf < 1 then ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf); - if 2 * minSamplesLeaf >= minSamplesSplit then - ArgumentOutOfRangeError( - ER_MIN_LEAF_GE_SPLIT, - minSamplesLeaf, minSamplesSplit - ); + + if minSamplesSplit < 2 * minSamplesLeaf then + ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit); // --- parameters valid → assign @@ -4694,8 +4696,8 @@ begin if minSamplesLeaf < 1 then ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf); - if minSamplesLeaf >= minSamplesSplit then - ArgumentOutOfRangeError(ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit); + if minSamplesSplit < 2 * minSamplesLeaf then + ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit); if maxFeatures < 0 then ArgumentOutOfRangeError(ER_MAX_FEATURES_INVALID); @@ -4753,22 +4755,17 @@ begin var classes: array of integer; var yEncArr := EncodeLabelsInt(yInt, classes); - if fCriterion = nil then - fCriterion := new GiniCriterion(classes.Length); - if classes.Length < 2 then ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES); - fIndexToClass := classes; - // --- criterion if fCriterion = nil then fCriterion := new GiniCriterion(classes.Length); + fIndexToClass := classes; + // --- encoded vector - var yEncoded := new Vector(m); - for var i := 0 to m - 1 do - yEncoded[i] := yEncArr[i]; + var yEncoded := new Vector(yEncArr); // --- Core fCore := new DecisionTreeCore( @@ -4854,7 +4851,7 @@ begin if fClassLabels = nil then ArgumentError(ER_CLASSES_NOT_AVAILABLE); - Result := fClassLabels; + Result := Copy(fClassLabels); end; // DecisionTreeRegressor @@ -5003,9 +5000,7 @@ begin CheckXForPredict(X); if X.ColCount <> fFeatureImportances.Length then - DimensionError(ER_FEATURE_COUNT_MISMATCH, - X.ColCount, - fFeatureImportances.Length); + DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureImportances.Length); var n := X.RowCount; Result := new Vector(n); @@ -5066,8 +5061,8 @@ begin if minSamplesLeaf < 1 then ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf); - if minSamplesLeaf >= minSamplesSplit then - ArgumentOutOfRangeError(ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit); + if minSamplesSplit < 2 * minSamplesLeaf then + ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit); fNTrees := nTrees; fMaxDepth := maxDepth; @@ -5680,7 +5675,7 @@ begin if fClassLabels = nil then ArgumentError(ER_CLASSES_NOT_AVAILABLE); - Result := fClassLabels; + Result := Copy(fClassLabels); end; //----------------------------- @@ -5764,6 +5759,9 @@ begin if minSamplesLeaf < 1 then ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf); + if minSamplesSplit < 2 * minSamplesLeaf then + ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit); + if maxDepth > MAX_ALLOWED_TREE_DEPTH then ArgumentOutOfRangeError(ER_MAX_DEPTH_TOO_LARGE, maxDepth); @@ -6026,10 +6024,10 @@ begin end; if XVal.RowCount <> yVal.Length then - DimensionError(ER_XY_SIZE_MISMATCH); + DimensionError(ER_XY_SIZE_MISMATCH,XVal.RowCount,yVal.Length); if XVal.ColCount <> XTrain.ColCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,XVal.ColCount,XTrain.ColCount); end; // --- OOB checks --- @@ -6241,7 +6239,7 @@ begin CheckXForPredict(X); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var n := X.RowCount; var yPred := new Vector(n); @@ -6473,7 +6471,7 @@ begin ArgumentError(ER_EMPTY_DATASET); if XTrain.RowCount <> yTrain.Length then - DimensionError(ER_XY_SIZE_MISMATCH); + DimensionError(ER_XY_SIZE_MISMATCH,XTrain.RowCount,yTrain.Length); if useValidation then begin @@ -6490,10 +6488,10 @@ begin end; if XVal.RowCount <> yVal.Length then - DimensionError(ER_XY_SIZE_MISMATCH); + DimensionError(ER_XY_SIZE_MISMATCH,XVal.RowCount,yVal.Length); if XVal.ColCount <> XTrain.ColCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,XVal.ColCount,XTrain.ColCount); end; // --- reset state @@ -7007,6 +7005,9 @@ begin if minSamplesLeaf < 1 then ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf); + if minSamplesSplit < 2 * minSamplesLeaf then + ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit); + if (subsample <= 0) or (subsample > 1) then ArgumentOutOfRangeError(ER_SUBSAMPLE_OUT_OF_RANGE, subsample); @@ -7050,7 +7051,7 @@ begin CheckXForPredict(X); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var nSamples := X.RowCount; var classCount := fClassCount; @@ -7127,7 +7128,7 @@ begin CheckXForPredict(X); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var total := fEstimators.Count; @@ -7318,7 +7319,7 @@ begin if fClassLabels = nil then ArgumentError(ER_CLASSES_NOT_AVAILABLE); - Result := fClassLabels; + Result := Copy(fClassLabels); end; //----------------------------- @@ -7344,7 +7345,7 @@ begin CheckXForPredict(X); if X.ColCount <> fXTrain.ColCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fXTrain.ColCount); end; function KNNBase.SquaredL2(trainRow: integer; XTest: Matrix; testRow: integer): double; @@ -7532,7 +7533,7 @@ begin ArgumentNullError(ER_Y_NULL); if X.RowCount <> y.Length then - DimensionError(ER_XY_SIZE_MISMATCH); + DimensionError(ER_XY_SIZE_MISMATCH,X.RowCount,y.Length); if X.RowCount = 0 then ArgumentError(ER_EMPTY_DATASET); @@ -7900,7 +7901,7 @@ begin if fClassLabels = nil then ArgumentError(ER_CLASSES_NOT_AVAILABLE); - Result := fClassLabels; + Result := Copy(fClassLabels); end; @@ -7922,7 +7923,7 @@ begin ArgumentNullError(ER_Y_NULL); if X.RowCount <> y.Length then - DimensionError(ER_XY_SIZE_MISMATCH); + DimensionError(ER_XY_SIZE_MISMATCH,X.RowCount,y.Length); if X.RowCount = 0 then ArgumentError(ER_EMPTY_DATASET); @@ -8076,13 +8077,8 @@ begin fNInit := nInit; // --- seed (единый стиль) - fRandomSeed := seed; - fUserProvidedSeed := seed >= 0; - - if fUserProvidedSeed then - fRng := new System.Random(seed) - else - fRng := new System.Random; + fRandomSeed := ResolveRandomSeed(seed, fUserProvidedSeed); + fRng := new System.Random(fRandomSeed); // --- state fFitted := False; @@ -8636,7 +8632,7 @@ begin ArgumentNullError(ER_Y_NULL); if X.RowCount <> y.Length then - DimensionError(ER_XY_SIZE_MISMATCH); + DimensionError(ER_XY_SIZE_MISMATCH,X.RowCount,y.Length); if X.RowCount = 0 then ArgumentError(ER_EMPTY_DATASET); @@ -8962,11 +8958,6 @@ begin fMean := X.ColumnMeans; fStd := X.ColumnStd; - // защита от нулевой дисперсии - for var j := 0 to fStd.Length - 1 do - if Abs(fStd[j]) < 1e-12 then - fStd[j] := 1.0; - fFitted := true; Result := Self; end; @@ -8980,7 +8971,7 @@ begin ArgumentNullError(ER_X_NULL); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var n := X.RowCount; var p := X.ColCount; @@ -8988,11 +8979,11 @@ begin Result := new Matrix(n, p); for var i := 0 to n - 1 do - for var j := 0 to p - 1 do - if fStd[j] <> 0 then - Result[i,j] := (X[i,j] - fMean[j]) / fStd[j] - else - Result[i,j] := 0.0; + for var j := 0 to p - 1 do + if Abs(fStd[j]) < 1e-12 then + Result[i,j] := 0.0 + else + Result[i,j] := (X[i,j] - fMean[j]) / fStd[j]; end; function StandardScaler.InverseTransform(X: Matrix): Matrix; @@ -9004,7 +8995,7 @@ begin ArgumentNullError(ER_X_NULL); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var n := X.RowCount; var p := X.ColCount; @@ -9074,7 +9065,7 @@ begin ArgumentNullError(ER_X_NULL); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var n := X.RowCount; var p := X.ColCount; @@ -9113,7 +9104,7 @@ begin ArgumentNullError(ER_X_NULL); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var n := X.RowCount; var p := X.ColCount; @@ -9198,7 +9189,7 @@ begin ArgumentNullError(ER_X_NULL); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var n := X.RowCount; var p := X.ColCount; @@ -9275,7 +9266,7 @@ begin ArgumentNullError(ER_X_NULL); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); if fSelected = nil then Error(ER_MODEL_NOT_INITIALIZED); @@ -9602,7 +9593,7 @@ begin ArgumentNullError(ER_X_NULL); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); if fSelected = nil then Error(ER_MODEL_NOT_INITIALIZED); @@ -9671,7 +9662,7 @@ begin ArgumentNullError(ER_X_NULL); if X.ColCount <> fFeatureCount then - DimensionError(ER_FEATURE_COUNT_MISMATCH); + DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount); var n := X.RowCount; var p := X.ColCount; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 9d132ca50..8c1e1cd19 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -334,16 +334,35 @@ begin foreach var f in fFeatures do begin - if current.HasColumn(f) then + var expanded := false; + + // ищем expander-ы В ПРЯМОМ порядке pipeline + for var i := 0 to fDataSteps.Count - 1 do begin - feats.Add(f); - continue; + var expander := fDataSteps[i] as IColumnExpander; + if expander = nil then + continue; + + var cols := expander.GetExpandedColumns(f); + if (cols <> nil) and (cols.Length > 0) then + begin + foreach var c in cols do + if current.HasColumn(c) then + if not feats.Contains(c) then + feats.Add(c); + + expanded := true; + break; + end; end; - foreach var c in current.Schema.ColumnNames do - if c.StartsWith(f + '_') then - if not feats.Contains(c) then - feats.Add(c); + if expanded then + continue; + + // fallback: обычный столбец + if current.HasColumn(f) then + if not feats.Contains(f) then + feats.Add(f); end; if feats.Count = 0 then diff --git a/bin/Lib/MetricsABC.pas b/bin/Lib/MetricsABC.pas index a94176bde..a416b723a 100644 --- a/bin/Lib/MetricsABC.pas +++ b/bin/Lib/MetricsABC.pas @@ -1681,10 +1681,10 @@ end; static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real; begin if yTrue = nil then - ArgumentNullError(ER_Y_NULL); + ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then - ArgumentNullError(ER_Y_NULL); + ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; if n <> yPred.Length then @@ -1774,56 +1774,6 @@ begin Result := (sumNij - expected) / denom; end; -{static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real; -begin - if yTrue.Length <> yPred.Length then - DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); - - var n := yTrue.Length; - - if n < 2 then - Result := 1 - else - begin - var tp := 0; - var tn := 0; - var fp := 0; - var fn := 0; - - for var i := 0 to n-2 do - for var j := i+1 to n-1 do - begin - var sameTrue := yTrue[i] = yTrue[j]; - var samePred := yPred[i] = yPred[j]; - - if sameTrue and samePred then - tp += 1 - else if (not sameTrue) and (not samePred) then - tn += 1 - else if (not sameTrue) and samePred then - fp += 1 - else - fn += 1; - end; - - var total := tp + tn + fp + fn; - - var ri := (tp + tn) / total; - - var a := tp + fp; - var b := tp + fn; - var c := fn + tn; - var d := fp + tn; - - var expected := ((a*b) + (c*d)) / (total*total); - - if 1 - expected = 0 then - Result := 0 - else - Result := (ri - expected) / (1 - expected); - end; -end;} - function R(s: string; w: integer): string; begin Result := s.PadLeft(w); diff --git a/bin/Lib/PreprocessorABC.pas b/bin/Lib/PreprocessorABC.pas index 44315ad82..8fb88ca65 100644 --- a/bin/Lib/PreprocessorABC.pas +++ b/bin/Lib/PreprocessorABC.pas @@ -88,7 +88,7 @@ type /// Категории фиксируются при Fit /// Неизвестные категории кодируются нулями /// Пропущенные значения (NA) кодируются нулями - OneHotEncoder = class(IPreprocessor, IColumnBoundStep) + OneHotEncoder = class(IPreprocessor, IColumnBoundStep, IColumnExpander) private col: string; categories: array of string; @@ -115,6 +115,8 @@ type function ToString: string; override; property ColumnName: string read col; + + function GetExpandedColumns(sourceColumn: string): array of string; /// Создаёт копию препроцессора с той же конфигурацией. /// @@ -251,7 +253,13 @@ const ER_IMPUTER_STRATEGY_NOT_SUPPORTED = 'Стратегия импутации {0} не поддерживается!!Imputation strategy {0} is not supported'; ER_UNSUPPORTED_IMPUTE_STRATEGY = - 'Неподдерживаемая стратегия заполнения: {0}!!Unsupported impute strategy: {0}'; + 'Неподдерживаемая стратегия заполнения: {0}!!Unsupported impute strategy: {0}'; + ER_ONEHOT_NAME_EQUALS_SOURCE = + 'Сгенерированная колонка совпадает с исходной: {0}!!Generated column equals source column: {0}'; + ER_ONEHOT_COLUMN_COLLISION = + 'Конфликт имён колонок: {0}!!Column name collision: {0}'; + ER_ONEHOT_DUPLICATE_COLUMN = + 'Дублирующаяся сгенерированная колонка: {0}!!Duplicate generated column: {0}'; //----------------------------- @@ -407,6 +415,29 @@ begin Error(ER_ONEHOT_EMPTY_COLUMN, col); categories := values.ToArray; + + // --- проверка коллизий имён колонок + var used := new HashSet; + + for var i := 0 to categories.Length - 1 do + begin + var newName := col + '_' + categories[i]; + + // 1. совпадение с исходным именем + if newName = col then + Error(ER_ONEHOT_NAME_EQUALS_SOURCE, newName); + + // 2. коллизия с существующими колонками DataFrame + if df.HasColumn(newName) then + Error(ER_ONEHOT_COLUMN_COLLISION, newName); + + // 3. дубликаты среди сгенерированных колонок + if used.Contains(newName) then + Error(ER_ONEHOT_DUPLICATE_COLUMN, newName); + + used.Add(newName); + end; + fitted := true; Result := Self; end; @@ -465,6 +496,21 @@ begin Result := Transform(df); end; +function OneHotEncoder.GetExpandedColumns(sourceColumn: string): array of string; +begin + if not fitted then + NotFittedError(ER_FIT_NOT_CALLED); + + if sourceColumn <> col then + exit(nil); + + var res := new string[categories.Length]; + for var i := 0 to categories.Length - 1 do + res[i] := col + '_' + categories[i]; + + Result := res; +end; + function OneHotEncoder.ToString: string; begin Result := 'OneHotEncoder(column=' + col + ')'; @@ -615,13 +661,15 @@ begin if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name); + var capturedIdx := idx; + case strategy of isMean: begin var m := means[i]; res := res.ReplaceColumnFloat( name, - c -> (if c.IsValid(idx) then c.Float(idx) else m) + c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else m) ); end; @@ -665,7 +713,7 @@ begin res := res.ReplaceColumnFloat( name, - c -> (if c.IsValid(idx) then c.Float(idx) else r) + c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else r) ); end; end; @@ -675,7 +723,7 @@ begin var m := medians[i]; res := res.ReplaceColumnFloat( name, - c -> (if c.IsValid(idx) then c.Float(idx) else m) + c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else m) ); end; diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index e58ad6465..3e2e7ff7d 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -5,6 +5,24 @@ interface uses LinearAlgebraML, MLCoreABC; type +/// Методы для разбиения данных и оценки моделей. +/// +/// Содержит утилиты для: +/// • разделения выборки на обучающую и тестовую (TrainTestSplit) +/// • k-fold кросс-валидации (KFold) +/// • стратифицированной кросс-валидации (StratifiedKFold) +/// • оценки моделей через кросс-валидацию (CrossValidate, StratifiedCrossValidate) +/// +/// Методы возвращают индексы или подвыборки без изменения исходных данных. +/// +/// • KFold — простое разбиение без учёта распределения классов +/// • StratifiedKFold — сохраняет пропорции классов в каждом fold (для классификации) +/// +/// Для стратифицированных методов требуется: +/// • целочисленные метки классов +/// • число объектов каждого класса ≥ числа фолдов +/// +/// Все методы используют генератор случайных чисел (seed) для воспроизводимости Validation = static class private static function CrossValidateCore(model: ISupervisedModel; @@ -93,6 +111,7 @@ type implementation uses MLExceptions; +uses MLUtilsABC; const ER_DIM_MISMATCH_TRAIN_TEST = @@ -114,7 +133,9 @@ const 'At least 2 samples are required for {0}'; ER_STRATIFIED_CLASS_TOO_SMALL = 'Класс {0} содержит {1} объектов, что меньше числа фолдов ({2})!!Class {0} has {1} samples, which is less than the number of folds ({2})'; - + ER_STRATIFIED_K_TOO_LARGE = + 'Stratified CV: число фолдов ({0}) превышает минимальный размер класса ({1})!!Stratified CV: number of folds ({0}) exceeds smallest class size ({1})'; + //----------------------------- // Validation //----------------------------- @@ -263,12 +284,8 @@ begin 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); + System.Array.Copy(idx, 0, trainIdx, 0, start); + System.Array.Copy(idx, start + size, trainIdx, start, n - (start + size)); yield (trainIdx, testIdx); @@ -442,6 +459,17 @@ begin if (k < 2) or (k > X.RowCount) then ArgumentError(ER_K_INVALID_STRATIFIED, k, X.RowCount); + + var labels := y.ToIntArray; + var classCounts := labels.EachCount; + + var minCount := integer.MaxValue; + foreach var pair in classCounts do + if pair.Value < minCount then + minCount := pair.Value; + + if k > minCount then + ArgumentError(ER_STRATIFIED_K_TOO_LARGE, k, minCount); var baseSeed := if seed >= 0 then seed