From 10c929fa1c58fe89ad16d2bdadff838fa5efe768 Mon Sep 17 00:00:00 2001 From: Mikhalkovich Stanislav Date: Sun, 19 Apr 2026 21:07:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20ML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Configuration/GlobalAssemblyInfo.cs | 2 +- Configuration/Version.defs | 4 +- Release/pabcversion.txt | 2 +- ReleaseGenerators/PascalABCNET_version.nsh | 2 +- bin/Lib/DataAdapters.pas | 63 +--- bin/Lib/DataFrameABC.pas | 3 - bin/Lib/DataFrameABCCore.pas | 5 +- bin/Lib/InspectionML.pas | 5 - bin/Lib/LinearAlgebraML.pas | 136 +++++++-- bin/Lib/MLDatasets.pas | 69 ++++- bin/Lib/MLExceptions.pas | 11 +- bin/Lib/MLModelsABC.pas | 323 +++++++++++++++++++-- bin/Lib/MLPipelineABC.pas | 109 +------ bin/Lib/MLUtilsABC.pas | 53 ++++ bin/Lib/MetricsABC.pas | 103 ++++++- bin/Lib/ValidationML.pas | 9 +- 16 files changed, 662 insertions(+), 237 deletions(-) diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index a636998d1..297f063bf 100644 --- a/Configuration/GlobalAssemblyInfo.cs +++ b/Configuration/GlobalAssemblyInfo.cs @@ -15,7 +15,7 @@ internal static class RevisionClass public const string Major = "3"; public const string Minor = "11"; public const string Build = "1"; - public const string Revision = "3805"; + public const string Revision = "3808"; public const string MainVersion = Major + "." + Minor; public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision; diff --git a/Configuration/Version.defs b/Configuration/Version.defs index 4dd579e35..9ff4502d6 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ -%MINOR%=11 -%REVISION%=3805 %COREVERSION%=1 +%REVISION%=3808 +%MINOR%=11 %MAJOR%=3 diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index d05440ac7..4bdc3c29c 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3805 +3.11.1.3808 diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index 23fbba686..58faf7f9e 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3805' +!define VERSION '3.11.1.3808' diff --git a/bin/Lib/DataAdapters.pas b/bin/Lib/DataAdapters.pas index d64a6487e..48d4b4daa 100644 --- a/bin/Lib/DataAdapters.pas +++ b/bin/Lib/DataAdapters.pas @@ -25,9 +25,6 @@ const 'Столбец "{0}" должен быть категориальным для EncodeLabels!!Column "{0}" must be categorical for EncodeLabels'; ER_ENCODELABELS_UNSUPPORTED_TYPE = 'Неподдерживаемый тип столбца "{0}" для EncodeLabels!!Unsupported column type "{0}" for EncodeLabels'; - ER_UNKNOWN_CLASS_IN_TRANSFORM = - 'Неизвестное значение класса "{0}" при преобразовании меток!!Unknown class value "{0}" in TransformLabels'; - function ToMatrix(Self: DataFrame; colNames: array of string): Matrix; extensionmethod; begin @@ -180,49 +177,25 @@ begin if not Self.IsCategorical(target) then ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, target); - // --- строим mapping - var map := new Dictionary; - for var i := 0 to classes.Length - 1 do - map[classes[i]] := i; - case Self.GetColumnType(target) of - + ColumnType.ctStr: begin var data := Self.GetStrColumn(target).ToArray; - var res := new integer[data.Length]; - - for var i := 0 to data.Length - 1 do - begin - var lbl := data[i]; - - if not map.ContainsKey(lbl) then - Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, lbl); - - res[i] := map[lbl]; - end; - - Result := res; + Result := TransformLabels(data, classes); end; - + ColumnType.ctInt: begin var data := Self.GetIntColumn(target).ToArray; - var res := new integer[data.Length]; - + var strData := new string[data.Length]; + for var i := 0 to data.Length - 1 do - begin - var lbl := data[i].ToString; - - if not map.ContainsKey(lbl) then - Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, lbl); - - res[i] := map[lbl]; - end; - - Result := res; + strData[i] := data[i].ToString; + + Result := TransformLabels(strData, classes); end; - + else ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target); end; @@ -252,25 +225,9 @@ begin if Self.GetColumnType(target) <> ColumnType.ctInt then ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target); - // --- mapping: значение → индекс - var map := new Dictionary; - for var i := 0 to classes.Length - 1 do - map[classes[i]] := i; - var data := Self.GetIntColumn(target).ToArray; - var res := new integer[data.Length]; - for var i := 0 to data.Length - 1 do - begin - var v := data[i]; - - if not map.ContainsKey(v) then - Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, v); - - res[i] := map[v]; - end; - - Result := res; + Result := TransformLabelsInt(data, classes); end; /// Кодирует значения целочисленного категориального столбца DataFrame diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index 5ba1326f1..e516c41b5 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -6464,9 +6464,6 @@ static function CSVLoader.Load(filename: string; begin var enc := encoding; - //if enc = nil then - // enc := System.Text.Encoding.Utf8; - if enc = nil then enc := DetectEncoding(filename); diff --git a/bin/Lib/DataFrameABCCore.pas b/bin/Lib/DataFrameABCCore.pas index b39d4bb7c..6a18030d3 100644 --- a/bin/Lib/DataFrameABCCore.pas +++ b/bin/Lib/DataFrameABCCore.pas @@ -72,7 +72,10 @@ type rightPrefix: string ): DataFrameSchema; - { --- debug --- } + { --- DEBUG ONLY --- + Проверка внутренних инвариантов схемы. + Использует Assert и выполняется только в debug-сборке. + Не предназначена для обработки пользовательских ошибок. } procedure AssertConsistent; end; diff --git a/bin/Lib/InspectionML.pas b/bin/Lib/InspectionML.pas index d43beb0f6..a29280077 100644 --- a/bin/Lib/InspectionML.pas +++ b/bin/Lib/InspectionML.pas @@ -90,11 +90,6 @@ begin var resultVec := new Vector(p); -// Базовый seed для всех параметров. -// Все модели оцениваются на одинаковых фолдах, -// что обеспечивает корректное сравнение. -// При seed = -1 разбиение случайное, но фиксируется -// один раз для всего GridSearch. var baseSeed := if seed >= 0 then seed else System.Environment.TickCount and integer.MaxValue; diff --git a/bin/Lib/LinearAlgebraML.pas b/bin/Lib/LinearAlgebraML.pas index 87521e9d3..07b6b865b 100644 --- a/bin/Lib/LinearAlgebraML.pas +++ b/bin/Lib/LinearAlgebraML.pas @@ -39,6 +39,9 @@ type function Clone: Vector; + function Normalize: Vector; + function Normalized: Vector; + function ToArray: array of real; function ToIntArray: array of integer; @@ -69,7 +72,9 @@ type // ---------- Основные методы ---------- function Sum: real; + /// Среднее. Синоним Mean function Average: real; + /// Среднее function Mean: real; function Norm2: real; function Norm: real; @@ -295,8 +300,6 @@ const 'Вектор пуст!!Vector is empty'; ER_VECTOR_DIVIDE_BY_ZERO = 'Деление на ноль при делении вектора на скаляр!!Division by zero in Vector / scalar'; - ER_DIM_MISMATCH = - 'Несоответствие размерностей: {0} и {1}!!Dimension mismatch: {0} and {1}'; ER_MATRIX_SIZE_NEGATIVE = 'Размеры матрицы должны быть неотрицательными!!Matrix size must be non-negative'; ER_MATRIX_SIZE_MISMATCH = @@ -384,6 +387,25 @@ begin Result := new Vector(fdata); end; +function Vector.Normalize: Vector; +begin + var sum := 0.0; + for var i := 0 to Length - 1 do + sum += Self[i]; + + if sum > 0 then + for var i := 0 to Length - 1 do + Self[i] /= sum; + + Result := Self; +end; + +function Vector.Normalized: Vector; +begin + Result := Self.Clone; + Result.Normalize; +end; + static procedure Vector.CheckSameLength(a, b: Vector); begin if a.Length <> b.Length then @@ -428,6 +450,7 @@ static function Vector.operator /(v: Vector; alpha: real): Vector; begin if alpha = 0.0 then raise new System.DivideByZeroException(GetTranslation(ER_VECTOR_DIVIDE_BY_ZERO)); + Result := new Vector(v.Length); var inv := 1.0 / alpha; for var i := 0 to v.Length - 1 do @@ -479,12 +502,10 @@ begin Result := s; end; +// Алиас Mean function Vector.Average: real; begin - if Length = 0 then - ArgumentError(ER_VECTOR_EMPTY); - - Result := Sum / Length; + Result := Mean; end; function Vector.Mean: real; @@ -541,6 +562,7 @@ constructor Matrix.Create(r, c: integer); begin if (r < 0) or (c < 0) then ArgumentOutOfRangeError(ER_MATRIX_SIZE_NEGATIVE); + fdata := new real[r, c]; end; @@ -580,8 +602,10 @@ end; function Matrix.ColumnMeans: Vector; begin var n := RowCount; - if n = 0 then - exit( new Vector(ColCount) ); + + if n = 0 then + Error(ER_EMPTY_MATRIX); + Result := ColumnSums / n; end; @@ -598,20 +622,28 @@ end; function Matrix.RowMeans: Vector; begin + var n := RowCount; var p := ColCount; + + if n = 0 then + Error(ER_EMPTY_MATRIX); + if p = 0 then - exit (new Vector(RowCount)); + Error(ER_EMPTY_MATRIX); + Result := RowSums / p; end; function Matrix.ColumnVariances: Vector; begin var n := RowCount; + if n = 0 then - exit (new Vector(ColCount)); + Error(ER_EMPTY_MATRIX); var means := ColumnMeans; var p := ColCount; + Result := new Vector(p); for var j := 0 to p - 1 do @@ -631,12 +663,17 @@ end; function Matrix.RowVariances: Vector; begin + var n := RowCount; var p := ColCount; + + if n = 0 then + Error(ER_EMPTY_MATRIX); + if p = 0 then - exit (new Vector(RowCount)); + Error(ER_EMPTY_MATRIX); var means := RowMeans; - var n := RowCount; + Result := new Vector(n); for var i := 0 to n - 1 do @@ -658,10 +695,10 @@ function Matrix.ColumnMins: Vector; begin var n := RowCount; var p := ColCount; - - if n = 0 then - exit(new Vector(p)); + if n = 0 then + Error(ER_EMPTY_MATRIX); + Result := new Vector(p); for var j := 0 to p - 1 do @@ -678,10 +715,10 @@ function Matrix.ColumnMaxs: Vector; begin var n := RowCount; var p := ColCount; - - if n = 0 then - exit(new Vector(p)); + if n = 0 then + Error(ER_EMPTY_MATRIX); + Result := new Vector(p); for var j := 0 to p - 1 do @@ -700,8 +737,11 @@ begin var p := ColCount; if n = 0 then - exit(new Vector(p)); - + Error(ER_EMPTY_MATRIX); + + if p = 0 then + Error(ER_EMPTY_MATRIX); + Result := new Vector(n); for var i := 0 to n - 1 do @@ -720,8 +760,11 @@ begin var p := ColCount; if n = 0 then - exit(new Vector(p)); + Error(ER_EMPTY_MATRIX); + if p = 0 then + Error(ER_EMPTY_MATRIX); + Result := new Vector(n); for var i := 0 to n - 1 do @@ -736,6 +779,15 @@ end; function Matrix.RowArgMin(i: integer): integer; begin + if RowCount = 0 then + Error(ER_EMPTY_MATRIX); + + if ColCount = 0 then + Error(ER_EMPTY_MATRIX); + + if (i < 0) or (i >= RowCount) then + ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount); + var minVal := fdata[i,0]; var arg := 0; @@ -756,6 +808,15 @@ end; function Matrix.RowArgMax(i: integer): integer; begin + if RowCount = 0 then + Error(ER_EMPTY_MATRIX); + + if ColCount = 0 then + Error(ER_EMPTY_MATRIX); + + if (i < 0) or (i >= RowCount) then + ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount); + var maxVal := fdata[i,0]; var arg := 0; @@ -776,6 +837,12 @@ end; function Matrix.RowSum(i: integer): real; begin + if RowCount = 0 then + Error(ER_EMPTY_MATRIX); + + if (i < 0) or (i >= RowCount) then + ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount); + var sum := 0.0; for var j := 0 to ColCount - 1 do @@ -810,6 +877,9 @@ end; function Matrix.ColumnArgMin(j: integer): integer; begin + if (j < 0) or (j >= ColCount) then + ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, ColCount); + var n := RowCount; if n = 0 then @@ -835,6 +905,9 @@ end; function Matrix.ColumnArgMax(j: integer): integer; begin + if (j < 0) or (j >= ColCount) then + ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, ColCount); + var n := RowCount; if n = 0 then @@ -895,6 +968,9 @@ end; function Matrix.FrobeniusNorm: real; begin + if RowCount = 0 then + Error(ER_EMPTY_MATRIX); + var s := 0.0; for var i := 0 to RowCount - 1 do for var j := 0 to ColCount - 1 do @@ -905,6 +981,10 @@ end; procedure Matrix.AddScaledIdentity(lambda: real); begin var n := RowCount; + + if RowCount <> ColCount then + ArgumentError(ER_MATRIX_NOT_SQUARE); + for var i := 0 to n - 1 do fdata[i, i] += lambda; end; @@ -1060,6 +1140,7 @@ function Matrix.GetRow(i: integer): Vector; begin 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] := fdata[i, j]; @@ -1067,8 +1148,14 @@ end; function Matrix.TakeRows(indices: array of integer): Matrix; begin + if indices = nil then + ArgumentNullError(ER_ARG_NULL, 'indices'); + var n := indices.Length; var p := ColCount; + + if RowCount = 0 then + Error(ER_EMPTY_MATRIX); var res := new Matrix(n, p); @@ -1124,6 +1211,7 @@ function Matrix.GetCol(j: integer): Vector; begin 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] := fdata[i, j]; @@ -1141,6 +1229,12 @@ end; static function Matrix.OuterProduct(a, b: Vector): Matrix; begin + if a = nil then + ArgumentNullError(ER_ARG_NULL, 'a'); + + if b = nil then + ArgumentNullError(ER_ARG_NULL, 'b'); + var m := a.Length; var n := b.Length; Result := new Matrix(m, n); diff --git a/bin/Lib/MLDatasets.pas b/bin/Lib/MLDatasets.pas index 25337e6c1..706e50dc4 100644 --- a/bin/Lib/MLDatasets.pas +++ b/bin/Lib/MLDatasets.pas @@ -295,10 +295,10 @@ const ER_PARAM_BETWEEN_01 = 'Параметр {0} должен быть в диапазоне (0,1)!!Parameter {0} must be in range (0,1)'; ER_DATASET_NO_TARGET = - 'У датасета нет целевой переменной (задача кластеризации).' + + 'У датасета нет целевой переменной (задача кластеризации).!!' + 'Dataset has no target variable (clustering task).'; ER_DATASET_TARGET_NOT_FOUND = - 'Целевая переменная "{0}" не найдена в таблице.' + + 'Целевая переменная "{0}" не найдена в таблице.!!' + 'Target column "{0}" not found in DataFrame'; ER_DATASET_META_NOT_FOUND = 'Файл метаданных датасета "{0}" не найден!!Dataset meta file "{0}" not found'; @@ -324,8 +324,6 @@ const 'Параметр {0} должен быть в диапазоне (0, 1)!!Parameter {0} must be in range (0, 1)'; ER_PARAM_LT = 'Параметр {0} должен быть меньше допустимого значения!!Parameter {0} must be less than allowed value'; - ER_TEST_RATIO_INVALID = - 'Некорректное значение testRatio (должно быть между 0 и 1)!!Invalid testRatio (must be between 0 and 1)'; ER_GROUPBY_UNSUPPORTED_KEY_TYPE = 'Неподдерживаемый тип ключа для группировки!!Unsupported key type for grouping'; ER_STRATIFIED_ONLY_FOR_CLASSIFICATION = @@ -334,7 +332,9 @@ const 'Слишком малое значение classBalance: {0}. Минимально допустимое значение — 1e-3!!classBalance is too small: {0}. Minimum allowed value is 1e-3'; ER_UNSUPPORTED_TARGET_TYPE = 'Неподдерживаемый тип целевого столбца: {0}!!Unsupported target column type: {0}'; - + ER_ENCODELABELS_UNSUPPORTED_TYPE = + 'Неподдерживаемый тип столбца для кодирования меток: {0}!!' + + 'Unsupported column type for label encoding: {0}'; C_DATASET = 'Датасет: {0}!!Dataset: {0}'; C_DESCRIPTION = 'Описание:!!Description:'; @@ -613,15 +613,39 @@ begin if Task <> TaskType.Classification then ArgumentError(ER_VALUECOUNTS_ONLY_CLASSIFICATION); - var labels := Data.GetStrColumn(Target); - var dict := new Dictionary; - foreach var v in labels do - if dict.ContainsKey(v) then - dict[v] += 1 + case Data.GetColumnType(Target) of + + ColumnType.ctStr: + begin + var labels := Data.GetStrColumn(Target); + + foreach var v in labels do + if dict.ContainsKey(v) then + dict[v] += 1 + else + dict[v] := 1; + end; + + ColumnType.ctInt: + begin + var labels := Data.GetIntColumn(Target); + + foreach var v in labels do + begin + var s := v.ToString; + + if dict.ContainsKey(s) then + dict[s] += 1 + else + dict[s] := 1; + end; + end; + else - dict[v] := 1; + ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, Target); + end; Result := dict; end; @@ -879,7 +903,7 @@ begin ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'nInformative'); if nInformative > nFeatures then - nInformative := nFeatures; + ArgumentOutOfRangeError(ER_PARAM_LE, 'nInformative'); if noise < 0 then ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noise'); @@ -1065,7 +1089,26 @@ begin X[row,1] := r * Sin(t) + noise * Normal(rnd); end; end; - + + // --- обработка хвоста (если n не делится на classes) + for var i := classes * perClass to n - 1 do + begin + var row := idx[i]; + + var c := i mod classes; // равномернее, чем rnd + + var u := rnd.NextDouble; + + var r := radius * u; + + var t := turns * 2 * Pi * u + c * 2 * Pi / classes + noise * 0.5 * Normal(rnd); + + y[row] := c; + + X[row,0] := r * Cos(t) + noise * Normal(rnd); + X[row,1] := r * Sin(t) + noise * Normal(rnd); + end; + Result := (X, y); end; diff --git a/bin/Lib/MLExceptions.pas b/bin/Lib/MLExceptions.pas index 81b8e544e..1d7995ede 100644 --- a/bin/Lib/MLExceptions.pas +++ b/bin/Lib/MLExceptions.pas @@ -30,7 +30,7 @@ const ER_Y_NULL = 'y не может быть nil!!y cannot be nil'; ER_XY_SIZE_MISMATCH = - 'Размерности X и y не согласованы!!X and y size mismatch'; + 'Размеры X и y не совпадают: X={0}, y={1}!!X and y size mismatch: X={0}, y={1}'; ER_FEATURE_COUNT_MISMATCH = 'Число признаков не совпадает!!Feature count mismatch'; ER_NAN_IN_X = @@ -50,8 +50,13 @@ const 'Данные для предсказания содержат Infinity. Проверьте данные на выбросы.!!' + 'Prediction data contains Infinity. Please check for extreme values.'; ER_MODEL_NOT_FITTED = - 'Модель "{0}" не обучена. Сначала вызовите Fit()|Model "{0}" is not fitted. Call Fit() first'; - + 'Модель "{0}" не обучена. Сначала вызовите Fit()!!Model "{0}" is not fitted. Call Fit() first'; + ER_TEST_RATIO_INVALID = + 'Параметр testRatio должен быть в интервале (0,1), получено {0}!!' + + 'Parameter testRatio must be in (0,1), got {0}'; + ER_PREDICT_NOT_SUPPORTED = + 'Модель не поддерживает операцию Predict!!' + + 'Model does not support Predict operation'; type /// Базовое исключение ML-библиотеки diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 010c1b106..a23e87d81 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -283,6 +283,8 @@ type fAlpha: real; fMaxIter: integer; fTol: real; + + function GetIsFitted: boolean; public /// Создаёт модель Lasso-регрессии. /// alpha — коэффициент L1-регуляризации. @@ -326,7 +328,7 @@ type /// Показывает, была ли модель обучена. /// После вызова Fit значение становится True. - property IsFitted: boolean read fModel.fFitted; + property IsFitted: boolean read GetIsFitted; function Name: string := Self.GetType.Name; end; @@ -557,8 +559,10 @@ type procedure Fit(X: Matrix; y: Vector); // y уже 0..K-1 function Predict(X: Matrix): Vector; // возвращает 0..K-1 function PredictRow(X: Matrix; row: integer): integer; - + private + function GetFeatureImportances: Vector; + function CreateLeaf(y: Vector; indices: array of integer): DecisionTreeNode; function BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; @@ -573,9 +577,9 @@ type var Xr: Matrix; var yr: Vector ); - property FeatureImportances: Vector read fFeatureImportances; - function MajorityClass(y: Vector; indices: array of integer): integer; + public + property FeatureImportances: Vector read GetFeatureImportances; function PredictOne(x: Vector; node: DecisionTreeNode): integer; function Clone: DecisionTreeCore; @@ -1566,6 +1570,9 @@ type fMark: array of integer; fTouched: array of integer; fEpoch: integer; + + fSampleIdx: array of integer; + fSampleSize: integer; public /// Создаёт классификатор kNN. @@ -1675,7 +1682,6 @@ type fMaxIter: integer; fTol: real; fNInit: integer; - fSeed: integer; fFitted: boolean; fFeatureCount: integer; @@ -1751,8 +1757,6 @@ type property Tol: real read fTol; /// Количество запусков алгоритма. property NInit: integer read fNInit; - /// Используемый seed. - property Seed: integer read fSeed; /// Матрица центроидов (k × p). property ClusterCenters: Matrix read fCenters; /// Значение инерции (сумма квадратов расстояний до центроидов). @@ -2051,6 +2055,9 @@ type /// Применяет стандартизацию к данным. function Transform(X: Matrix): Matrix; + + /// Обратная операция к Transform. + function InverseTransform(X: Matrix): Matrix; /// Средние значения признаков, вычисленные при обучении. property Mean: Vector read fMean; @@ -2106,6 +2113,9 @@ type /// Применяет линейное масштабирование признаков к диапазону [0, 1]. function Transform(X: Matrix): Matrix; + + /// Преобразование, обратное Transform + function InverseTransform(X: Matrix): Matrix; /// Минимальные значения признаков, вычисленные при обучении. property Min: Vector read fMin; @@ -2587,9 +2597,6 @@ const ER_INTERNAL_INVALID_MODEL_CLONE = 'Внутренняя ошибка: Clone модели вернул несовместимый тип!!' + 'Internal error: model Clone returned incompatible type'; - ER_PREDICT_NOT_SUPPORTED = - 'Модель не поддерживает Predict для данного типа алгоритма!!' + - 'Model does not support Predict for this type of algorithm'; ER_NEED_AT_LEAST_TWO_CLASSES = 'Необходимо как минимум два различных класса!!At least two distinct classes are required'; ER_UNKNOWN_CLASS_LABEL = @@ -3084,6 +3091,10 @@ begin Result := new LassoRegression(fAlpha, fMaxIter, fTol); end; +function LassoRegression.GetIsFitted: boolean; +begin + Result := (fModel <> nil) and fModel.IsFitted; +end; //----------------------------- // LogisticRegression //----------------------------- @@ -3576,6 +3587,14 @@ begin fRng := new System.Random(fRandomSeed); end; +function DecisionTreeCore.GetFeatureImportances: Vector; +begin + if fRoot = nil then + NotFittedError(ER_FIT_NOT_CALLED); + + Result := fFeatureImportances.Normalized; +end; + procedure DecisionTreeCore.Fit(X: Matrix; y: Vector); begin // --- init importance @@ -3673,7 +3692,7 @@ end; function DecisionTreeCore.BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; begin // 1. stop: depth - if depth >= fMaxDepth then + if (fMaxDepth > 0) and (depth >= fMaxDepth) then exit(CreateLeaf(y, indices)); // 2. stop: min samples @@ -4061,7 +4080,7 @@ begin if minSamplesLeaf < 1 then ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf); - if minSamplesLeaf >= minSamplesSplit then + if 2 * minSamplesLeaf >= minSamplesSplit then ArgumentOutOfRangeError( ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit @@ -5108,6 +5127,7 @@ begin fMinSamplesSplit, fMinSamplesLeaf, treeCriterion, + fClassCount, ComputeMaxFeatures(p), treeSeed ); @@ -5309,6 +5329,9 @@ function RandomForestClassifier.FeatureImportances: Vector; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); + + if fTrees.Length = 0 then + Error(ER_MODEL_NOT_INITIALIZED); var p := fTrees[0].FeatureImportances.Length; var resultVec := new Vector(p); @@ -5318,7 +5341,7 @@ begin resultVec *= 1.0 / fTrees.Length; - Result := resultVec; + Result := resultVec.Normalized; end; function RandomForestClassifier.ToString: string; @@ -5691,7 +5714,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 @@ -7034,12 +7057,15 @@ begin var sum := 0.0; var d := fXTrain.ColCount; + var train := fXTrain; // локальная ссылка + var test := XTest; + for var j := 0 to d - 1 do begin - var diff := fXTrain[trainRow, j] - XTest[testRow, j]; + var diff := train.Data[trainRow, j] - test.Data[testRow, j]; sum += diff * diff; end; - + exit(sum); end; @@ -7085,6 +7111,112 @@ begin end; end; +//----------------------------- +// topK +//----------------------------- + +type TopK = class +private + fDist: array of real; + fIdx: array of integer; + fCount: integer; + fK: integer; + fWorstIdx: integer; + + procedure UpdateWorst; + +public + constructor Create(k: integer); + procedure Clear; + + function Count: integer; + function Worst: real; + + procedure Add(d: real; id: integer); + + function GetIndex(i: integer): integer; + function GetDist(i: integer): real; +end; + +constructor TopK.Create(k: integer); +begin + fK := k; + SetLength(fDist, k); + SetLength(fIdx, k); + fCount := 0; + fWorstIdx := 0; +end; + +procedure TopK.Clear; +begin + fCount := 0; + fWorstIdx := 0; +end; + +function TopK.Count: integer; +begin + Result := fCount; +end; + +function TopK.Worst: real; +begin + if fCount < fK then + Result := real.PositiveInfinity + else + Result := fDist[fWorstIdx]; +end; + +procedure TopK.UpdateWorst; +begin + var wi := 0; + var wd := fDist[0]; + + for var i := 1 to fCount - 1 do + if fDist[i] > wd then + begin + wd := fDist[i]; + wi := i; + end; + + fWorstIdx := wi; +end; + +procedure TopK.Add(d: real; id: integer); +begin + // ещё не заполнено + if fCount < fK then + begin + fDist[fCount] := d; + fIdx[fCount] := id; + fCount += 1; + + if fCount = fK then + UpdateWorst; + + exit; + end; + + // быстрый отсев + if d >= fDist[fWorstIdx] then + exit; + + // замена худшего + fDist[fWorstIdx] := d; + fIdx[fWorstIdx] := id; + + UpdateWorst; +end; + +function TopK.GetIndex(i: integer): integer; +begin + Result := fIdx[i]; +end; + +function TopK.GetDist(i: integer): real; +begin + Result := fDist[i]; +end; + //----------------------------- // KNNClassifier //----------------------------- @@ -7123,6 +7255,15 @@ begin // --- copy train data fXTrain := X.Clone; + + fSampleSize := Min(3000, fXTrain.RowCount); // попробуй 2000–4000 + + SetLength(fSampleIdx, fSampleSize); + + var rnd := new System.Random(42); + + for var i := 0 to fSampleSize - 1 do + fSampleIdx[i] := rnd.Next(fXTrain.RowCount); // ========================================================= // ЕДИНЫЙ ENCODING @@ -7197,15 +7338,36 @@ begin var m := X.RowCount; var n := fXTrain.RowCount; + var p := fXTrain.ColCount; + + var trainRows := fXTrain.Data.Rows; + var testRows := X.Data.Rows; Result := new Vector(m); for var i := 0 to m - 1 do begin // заполнить расстояния + // for var t := 0 to n - 1 do + // begin + // fNeighbors[t].dist := SquaredL2(t, X, i); + // fNeighbors[t].idx := t; + // end; + + var rowTest := testRows[i]; for var t := 0 to n - 1 do - begin - fNeighbors[t].dist := SquaredL2(t, X, i); + begin + var rowTrain := trainRows[t]; + + var sum := 0.0; + + for var j := 0 to p - 1 do + begin + var diff := rowTrain[j] - rowTest[j]; + sum += diff * diff; + end; + + fNeighbors[t].dist := sum; fNeighbors[t].idx := t; end; @@ -7652,18 +7814,70 @@ begin var p := X.ColCount; var k := fNClusters; - // --- 1. Инициализация центроидов (случайные строки X) - - var idx := Arr(0..n-1); - idx.Shuffle(rnd); + // --- 1. K-Means++ инициализация var centers := new Matrix(k, p); - - for var c := 0 to k - 1 do + + // первый центр — случайный + var first := rnd.Next(n); + for var j := 0 to p - 1 do + centers[0,j] := X[first,j]; + + // расстояния до ближайшего центра + var dist1 := new real[n]; + + for var i := 0 to n - 1 do + dist1[i] := double.MaxValue; + + // выбираем остальные центры + for var c := 1 to k - 1 do begin - var r := idx[c]; + // обновляем dist (минимальное расстояние до уже выбранных центров) + for var i := 0 to n - 1 do + begin + var d := 0.0; + + for var j := 0 to p - 1 do + begin + var diff := X[i,j] - centers[c-1,j]; + d += diff * diff; + end; + + if d < dist1[i] then + dist1[i] := d; + end; + + // сумма расстояний + var sumDist := 0.0; + for var i := 0 to n - 1 do + sumDist += dist1[i]; + + // если всё совпало (редкий случай) + if sumDist = 0 then + begin + var r := rnd.Next(n); + for var j := 0 to p - 1 do + centers[c,j] := X[r,j]; + continue; + end; + + // выбор по вероятности + var target := rnd.NextDouble * sumDist; + var acc := 0.0; + var chosen := n - 1; + + for var i := 0 to n - 1 do + begin + acc += dist1[i]; + if acc >= target then + begin + chosen := i; + break; + end; + end; + for var j := 0 to p - 1 do - centers[c,j] := X[r,j]; + centers[c,j] := X[chosen,j]; end; var inertia := 0.0; @@ -8489,7 +8703,34 @@ begin for var i := 0 to n - 1 do for var j := 0 to p - 1 do - Result[i,j] := (X[i,j] - fMean[j]) / fStd[j]; + if fStd[j] <> 0 then + Result[i,j] := (X[i,j] - fMean[j]) / fStd[j] + else + Result[i,j] := 0.0; +end; + +function StandardScaler.InverseTransform(X: Matrix): Matrix; +begin + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED); + + if X = nil then + ArgumentNullError(ER_X_NULL); + + if X.ColCount <> fFeatureCount then + DimensionError(ER_FEATURE_COUNT_MISMATCH); + + var n := X.RowCount; + var p := X.ColCount; + + Result := new Matrix(n, p); + + for var i := 0 to n - 1 do + for var j := 0 to p - 1 do + if fStd[j] <> 0 then + Result[i,j] := X[i,j] * fStd[j] + fMean[j] + else + Result[i,j] := fMean[j]; end; function StandardScaler.ToString: string; @@ -8577,6 +8818,34 @@ begin end; end; +function MinMaxScaler.InverseTransform(X: Matrix): Matrix; +begin + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED); + + if X = nil then + ArgumentNullError(ER_X_NULL); + + if X.ColCount <> fFeatureCount then + DimensionError(ER_FEATURE_COUNT_MISMATCH); + + var n := X.RowCount; + var p := X.ColCount; + + Result := new Matrix(n, p); + + for var i := 0 to n - 1 do + for var j := 0 to p - 1 do + begin + var range := fMax[j] - fMin[j]; + + if range <> 0 then + Result[i,j] := X[i,j] * range + fMin[j] + else + Result[i,j] := fMin[j]; + end; +end; + function MinMaxScaler.ToString: string; begin Result := 'MinMaxScaler(min=' + fRangeMin + ', max=' + fRangeMax + ')'; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 114dca911..363f0b6f3 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -137,15 +137,8 @@ type /// (Fit/Transform/FitTransform) UDataPipeline = class(PipelineBase, IModel) private - //fDataSteps: List; - //fMatrixSteps: List; fModel: IUnsupervisedModel; - //fFeatures: array of string; - //fFinalFeatures: array of string; - - //fFitted: boolean; - procedure ValidateNumericFeatures(df: DataFrame); function TransformToMatrix(df: DataFrame): Matrix; protected @@ -226,9 +219,6 @@ const 'Матричный шаг не может идти после модели!!Matrix step cannot appear after the model'; ER_DATAPIPE_UNKNOWN_STEP_TYPE = 'Неизвестный тип шага конвейера: {0}!!Unknown pipeline step type: {0}'; - - ER_TO_MATRIX_NO_COLUMNS = - 'ToMatrix: не указаны столбцы!!ToMatrix: no columns specified'; ER_TO_VECTOR_NON_NUMERIC = 'ToVector: столбец "{0}" содержит нечисловые или NA значения!!' + 'ToVector: column "{0}" contains non-numeric or NA values'; @@ -267,9 +257,6 @@ const ER_MODEL_NOT_SUPERVISED = 'Модель (тип: {0}) не поддерживает Fit(X, y)!!' + 'Model (type: {0}) does not support Fit(X, y)'; - ER_XY_SIZE_MISMATCH = - 'Несовпадение размеров X и y: X имеет {0} строк, y имеет {1} элементов!!' + - 'X and y size mismatch: X has {0} rows, y has {1} elements'; ER_PIPELINE_FEATURE_NOT_FOUND = 'Признак "{0}" отсутствует во входных данных!!' + 'Feature "{0}" not found in input data'; @@ -294,9 +281,6 @@ const ER_DATAPIPE_INVALID_TASK = 'DataPipeline поддерживает только классификацию и регрессию!!' + 'DataPipeline supports classification and regression only'; - ER_PREDICT_NOT_SUPPORTED = - 'Модель не поддерживает операцию Predict!!' + - 'Model does not support Predict operation'; ER_PIPELINE_NO_STEPS = 'Pipeline не содержит шагов!!' + 'Pipeline must contain at least one step'; @@ -561,74 +545,31 @@ begin if fModel = nil then ArgumentError(ER_MODEL_NULL); - var current := df; + var current: DataFrame; + var X := PrepareMatrix(df, current); - // --- 0) Проверка входной схемы - ValidateSchema(current); - - // --- 1) DataFrame шаги - for var i := 0 to fDataSteps.Count - 1 do - begin - fDataSteps[i] := fDataSteps[i].Fit(current); - current := fDataSteps[i].Transform(current); - end; - - // --- target должен остаться if not current.HasColumn(fTarget) then ArgumentError(ER_PIPELINE_TARGET_REMOVED, fTarget); - // --- 2) вычислить финальные признаки - var feats := new List; - - foreach var f in fFeatures do - begin - if current.HasColumn(f) then - begin - feats.Add(f); - continue; - end; - - // искать производные признаки (OneHotEncoder) - foreach var c in current.Schema.ColumnNames do - if c.StartsWith(f + '_') then - if not feats.Contains(c) then - feats.Add(c); - end; - - if feats.Count = 0 then - ArgumentError(ER_PIPELINE_NO_FEATURES); - - // fFinalFeatures — признаки DataFrame (ДО matrix-transform) - // После FitTransformMatrix пространство признаков меняется (PCA и т.п.), - // но fFinalFeatures используется только для ToMatrix в Predict. - - fFinalFeatures := feats.ToArray; - - var X := current.ToMatrix(fFinalFeatures); - var classes: array of string; var y: Vector; - + case fTask of tkRegression: y := current.ToVector(fTarget); - + tkClassification: begin var labels := current.EncodeLabels(fTarget, classes); y := new Vector(labels); end; end; - + if X.RowCount <> y.Length then DimensionError(ER_XY_SIZE_MISMATCH, X.RowCount, y.Length); - // --- 3) Matrix transformers X := FitTransformMatrix(X, y); - // После этого X уже не соответствует fFinalFeatures по размерности, - // но это нормально: fFinalFeatures используется только до ToMatrix. - // --- 4) модель fModel := fModel.Fit(X, y); if fTask = tkClassification then @@ -949,47 +890,11 @@ begin if fModel = nil then ArgumentError(ER_MODEL_NULL); - var current := df; + var current: DataFrame; + var X := PrepareMatrix(df, current); - // --- 0) Проверка входной схемы - ValidateSchema(current); - - // --- 1) DataFrame шаги - for var i := 0 to fDataSteps.Count - 1 do - begin - fDataSteps[i] := fDataSteps[i].Fit(current); - current := fDataSteps[i].Transform(current); - end; - - // --- 2) вычислить финальные признаки - var feats := new List; - - foreach var f in fFeatures do - begin - if current.HasColumn(f) then - begin - feats.Add(f); - continue; - end; - - // искать производные признаки (OneHotEncoder) - foreach var c in current.Schema.ColumnNames do - if c.StartsWith(f + '_') then - if not feats.Contains(c) then - feats.Add(c); - end; - - if feats.Count = 0 then - ArgumentError(ER_PIPELINE_NO_FEATURES); - - fFinalFeatures := feats.ToArray; - - var X := current.ToMatrix(fFinalFeatures); - - // --- 3) Matrix transformers X := FitTransformMatrix(X); - // --- 4) модель fModel := fModel.Fit(X); fFitted := true; diff --git a/bin/Lib/MLUtilsABC.pas b/bin/Lib/MLUtilsABC.pas index 2937fdfb0..155c00e64 100644 --- a/bin/Lib/MLUtilsABC.pas +++ b/bin/Lib/MLUtilsABC.pas @@ -63,6 +63,13 @@ function EncodeLabelsInt(labels: array of integer; var classes: array of integer /// Используется для применения кодирования к тестовым данным (Transform). function TransformLabels(labels: array of string; classes: array of string): array of integer; +/// Преобразует целочисленные метки классов в индексы (0,1,2,...) +/// с использованием заранее заданного массива classes (mapping: индекс → значение). +/// classes должен быть получен из EncodeLabelsInt. +/// Если встречается неизвестное значение — выбрасывается исключение. +/// Используется для применения кодирования к тестовым данным (Transform). +function TransformLabelsInt(labels: array of integer; classes: array of integer): array of integer; + /// Преобразует целочисленные индексы классов обратно в строковые метки. /// Массив classes задаёт соответствие: classes[i] — имя класса с индексом i. /// Используется для получения текстовых предсказаний моделей. @@ -73,6 +80,11 @@ function DecodeLabels(y: array of integer; classes: array of string): array of s /// Используется для определения множества классов в задаче классификации. function UniqueLabels(labels: array of string): array of string; +function CloneOrNil(v: Vector): Vector; + +procedure CheckSameLength(a, b: Vector); + + implementation uses MLExceptions; @@ -173,6 +185,34 @@ begin Result := res; end; +function TransformLabelsInt(labels: array of integer; classes: array of integer): array of integer; +begin + if labels = nil then + ArgumentNullError(ER_ARG_NULL, 'labels'); + + if classes = nil then + ArgumentNullError(ER_ARG_NULL, 'classes'); + + // mapping: значение → индекс + var map := new Dictionary; + for var i := 0 to classes.Length - 1 do + map[classes[i]] := i; + + var res := new integer[labels.Length]; + + for var i := 0 to labels.Length - 1 do + begin + var v := labels[i]; + + if not map.ContainsKey(v) then + Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, v); + + res[i] := map[v]; + end; + + Result := res; +end; + function EncodeLabels(labels: array of string): array of integer; begin var classes: array of string; @@ -229,4 +269,17 @@ begin Result := labels.Distinct.ToArray; end; +function CloneOrNil(v: Vector): Vector; +begin + if v = nil then + exit(nil); + Result := v.Clone; +end; + +procedure CheckSameLength(a, b: Vector); +begin + if a.Length <> b.Length then + DimensionError(ER_DIM_MISMATCH, a.Length, b.Length); +end; + end. \ No newline at end of file diff --git a/bin/Lib/MetricsABC.pas b/bin/Lib/MetricsABC.pas index ac46a433f..0f7bcfe96 100644 --- a/bin/Lib/MetricsABC.pas +++ b/bin/Lib/MetricsABC.pas @@ -223,6 +223,10 @@ type /// Поддерживает как бинарную, так и многоклассовую классификацию. /// Для бинарной классификации дополнительно доступны /// агрегированные показатели: TP, TN, FP, FN. + /// + /// Ожидается, что yTrue и yPred содержат закодированные метки классов + /// (целые значения 0..K-1, допускается хранение в Vector типа real). + /// Для строковых меток необходимо предварительно применить EncodeLabels ConfusionMatrix = class private fMatrix: array[,] of integer; @@ -579,6 +583,7 @@ type implementation uses MLExceptions; +uses MLUtilsABC; const ER_INVALID_VALUE = @@ -1671,6 +1676,102 @@ end; /// Для больших выборок рекомендуется использовать альтернативную реализацию /// через таблицу сопряжённости (contingency table) с асимптотикой O(n · k) static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real; +begin + if yTrue = nil then + ArgumentNullError(ER_Y_NULL); + + if yPred = nil then + ArgumentNullError(ER_Y_NULL); + + var n := yTrue.Length; + if n <> yPred.Length then + DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length); + + if n < 2 then + begin + Result := 1.0; + exit; + end; + + // --- Vector -> int[] + var yTInt := new integer[n]; + var yPInt := new integer[n]; + + for var i := 0 to n - 1 do + begin + yTInt[i] := Round(yTrue[i]); + yPInt[i] := Round(yPred[i]); + end; + + // --- encoding + var classesTrue: array of integer; + var classesPred: array of integer; + + var yT := EncodeLabelsInt(yTInt, classesTrue); + var yP := EncodeLabelsInt(yPInt, classesPred); + + var kT := classesTrue.Length; + var kP := classesPred.Length; + + // --- contingency + var table := new integer[kT, kP]; + + for var i := 0 to n - 1 do + table[yT[i], yP[i]] += 1; + + // --- sums + var sumNij := 0.0; + var sumAi := 0.0; + var sumBj := 0.0; + + // n_ij + for var i := 0 to kT - 1 do + for var j := 0 to kP - 1 do + begin + var x := table[i,j]; + if x >= 2 then + sumNij += x * (x - 1) / 2.0; + end; + + // a_i + for var i := 0 to kT - 1 do + begin + var s := 0; + for var j := 0 to kP - 1 do + s += table[i,j]; + + if s >= 2 then + sumAi += s * (s - 1) / 2.0; + end; + + // b_j + for var j := 0 to kP - 1 do + begin + var s := 0; + for var i := 0 to kT - 1 do + s += table[i,j]; + + if s >= 2 then + sumBj += s * (s - 1) / 2.0; + end; + + var total := n * (n - 1) / 2.0; + + var expected := (sumAi * sumBj) / total; + var maxIndex := 0.5 * (sumAi + sumBj); + + var denom := maxIndex - expected; + + if denom = 0 then + begin + Result := 0.0; + exit; + end; + + 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); @@ -1718,7 +1819,7 @@ begin else Result := (ri - expected) / (1 - expected); end; -end; +end;} function R(s: string; w: integer): string; begin diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index 1e7f827af..a85a58a8a 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -42,6 +42,9 @@ type /// metric — функция качества, принимающая (y_true, y_pred) /// и возвращающая значение метрики (например, Accuracy или MSE). /// Возвращает среднее значение метрики по всем частям. + /// + /// Метод принимает только ISupervisedModel, работающие с матричными данными + /// DataPipeline сюда передавать нельзя, так как он работает с DataFrame. static function CrossValidate(model: ISupervisedModel; X: Matrix; y: Vector; k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real; @@ -50,6 +53,9 @@ type /// с сохранением пропорций классов в каждой части. /// Рекомендуется для задач классификации, особенно при несбалансированных классах. /// Возвращает среднее значение метрики по k разбиениям. + /// + /// Метод принимает только ISupervisedModel, работающие с матричными данными + /// DataPipeline сюда передавать нельзя, так как он работает с DataFrame. static function StratifiedCrossValidate(model: ISupervisedModel; X: Matrix; y: Vector; k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real; end; @@ -92,9 +98,6 @@ const ER_DIM_MISMATCH_TRAIN_TEST = 'Несоответствие размерностей в TrainTestSplit: X.RowCount={0}, y.Length={1}!!' + 'Dimension mismatch in TrainTestSplit: X.RowCount={0}, y.Length={1}'; - ER_TEST_RATIO_INVALID = - 'Параметр testRatio должен быть в интервале (0,1), получено {0}!!' + - 'Parameter testRatio must be in (0,1), got {0}'; ER_K_INVALID = 'Некорректное значение k в KFold: k={0}, n={1}!!' + 'Invalid k in KFold: k={0}, n={1}';