diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index 6534ba9a5..a636998d1 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 = "3804"; + public const string Revision = "3805"; 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 a93f97fdf..4dd579e35 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ -%COREVERSION%=1 -%REVISION%=3804 %MINOR%=11 +%REVISION%=3805 +%COREVERSION%=1 %MAJOR%=3 diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index e49aea18b..d05440ac7 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3804 +3.11.1.3805 diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index 609925cd3..23fbba686 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3804' +!define VERSION '3.11.1.3805' diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index d93eb6a5c..5ba1326f1 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -2119,6 +2119,9 @@ end; function DataFrame.RowCount: integer; begin + //if (columns[0] is StrColumn (var sc)) and (sc.Data = nil) then + // raise new Exception('Data = nil detected'); + if columns.Count = 0 then Result := 0 else Result := columns[0].RowCount; diff --git a/bin/Lib/DataFrameABCCore.pas b/bin/Lib/DataFrameABCCore.pas index 2d73a9236..b39d4bb7c 100644 --- a/bin/Lib/DataFrameABCCore.pas +++ b/bin/Lib/DataFrameABCCore.pas @@ -109,8 +109,7 @@ type public constructor Create; begin end; constructor Create(name: string); - constructor Create(name: string; data: array of integer; - valid: array of boolean := nil); + constructor Create(name: string; values: array of integer; valid: array of boolean := nil); function TryGetNumericValue(i: integer; var value: real): boolean; override; /// Возвращает количество строк в столбце function RowCount: integer; override := Data.Length; @@ -123,7 +122,7 @@ type public constructor Create; begin end; constructor Create(name: string); - constructor Create(name: string; data: array of real; + constructor Create(name: string; values: array of real; valid: array of boolean := nil); function TryGetNumericValue(i: integer; var value: real): boolean; override; /// Возвращает количество строк в столбце @@ -137,7 +136,7 @@ type public constructor Create; begin end; constructor Create(name: string); - constructor Create(name: string; data: array of string; + constructor Create(name: string; values: array of string; valid: array of boolean := nil); function TryGetNumericValue(i: integer; var value: real): boolean; override; /// Возвращает количество строк в столбце @@ -155,7 +154,7 @@ type public constructor Create; begin end; constructor Create(name: string); - constructor Create(name: string; data: array of boolean; + constructor Create(name: string; values: array of boolean; valid: array of boolean := nil); function TryGetNumericValue(i: integer; var value: real): boolean; override; /// Возвращает количество строк в столбце @@ -564,13 +563,13 @@ end; // Columns //----------------------------- -constructor IntColumn.Create(name: string; data: array of integer; valid: array of boolean); +constructor IntColumn.Create(name: string; values: array of integer; valid: array of boolean); begin inherited Create; Info := new ColumnInfo(name, ctInt); - var n := Length(data); - Data := if n = 0 then [] else data; + var n := Length(values); + Self.Data := if n = 0 then [] else values; if valid = nil then IsValid := [True] * n @@ -588,8 +587,8 @@ begin inherited Create; Info := new ColumnInfo(name, ctInt); - Data := []; - IsValid := []; + Data := new integer[0]; + IsValid := new boolean[0]; end; function IntColumn.TryGetNumericValue(i: integer; var value: real): boolean; @@ -601,13 +600,13 @@ begin exit(True); end; -constructor FloatColumn.Create(name: string; data: array of real; valid: array of boolean); +constructor FloatColumn.Create(name: string; values: array of real; valid: array of boolean); begin inherited Create; Info := new ColumnInfo(name, ctFloat); - var n := Length(data); - Data := if n = 0 then [] else data; + var n := Length(values); + Self.Data := if n = 0 then [] else values; if valid = nil then IsValid := [True] * n @@ -625,8 +624,8 @@ begin inherited Create; Info := new ColumnInfo(name, ctFloat); - Data := []; - IsValid := []; + Data := new real[0]; + IsValid := new boolean[0]; end; function FloatColumn.TryGetNumericValue(i: integer; var value: real): boolean; @@ -638,13 +637,13 @@ begin exit(True); end; -constructor StrColumn.Create(name: string; data: array of string; valid: array of boolean); +constructor StrColumn.Create(name: string; values: array of string; valid: array of boolean); begin inherited Create; Info := new ColumnInfo(name, ctStr); - var n := Length(data); - Data := if n = 0 then [] else data; + var n := Length(values); + Self.Data := if n = 0 then [] else values; if valid = nil then IsValid := [True] * n @@ -662,8 +661,8 @@ begin inherited Create; Info := new ColumnInfo(name, ctStr); - Data := []; - IsValid := []; + Data := new string[0]; + IsValid := new boolean[0]; end; function StrColumn.TryGetNumericValue(i: integer; var value: real): boolean; @@ -671,13 +670,13 @@ begin exit(False); end; -constructor BoolColumn.Create(name: string; data: array of boolean; valid: array of boolean); +constructor BoolColumn.Create(name: string; values: array of boolean; valid: array of boolean); begin inherited Create; Info := new ColumnInfo(name, ctBool); - var n := Length(data); - Data := if n = 0 then [] else data; + var n := Length(values); + Self.Data := if n = 0 then [] else values; if valid = nil then IsValid := [True] * n @@ -695,8 +694,8 @@ begin inherited Create; Info := new ColumnInfo(name, ctBool); - Data := []; - IsValid := []; + Data := new boolean[0]; + IsValid := new boolean[0]; end; function BoolColumn.TryGetNumericValue(i: integer; var value: real): boolean; diff --git a/bin/Lib/InspectionML.pas b/bin/Lib/InspectionML.pas index 4747fdacd..d43beb0f6 100644 --- a/bin/Lib/InspectionML.pas +++ b/bin/Lib/InspectionML.pas @@ -45,7 +45,10 @@ type /// Вектор важностей признаков длины nFeatures. /// Чем больше значение, тем сильнее признак влияет на качество модели static function PermutationImportance(model: IPredictiveModel; X: Matrix; y: Vector; - scoreFunc: (Vector, Vector) -> real; nRepeats: integer := 5; seed: integer := -1): Vector; + scoreFunc: (Vector, Vector) -> real; + nRepeats: integer := 5; + higherIsBetter: boolean := True; + seed: integer := -1): Vector; end; implementation @@ -61,12 +64,11 @@ const static function Inspection.PermutationImportance( model: IPredictiveModel; - X: Matrix; - y: Vector; + X: Matrix; y: Vector; scoreFunc: (Vector, Vector) -> real; nRepeats: integer; - seed: integer -): Vector; + higherIsBetter: boolean; + seed: integer): Vector; begin if model = nil then ArgumentNullError(ER_MODEL_NULL); @@ -122,7 +124,10 @@ begin var permPred := model.Predict(Xperm); var permScore := scoreFunc(y, permPred); - acc += (baselineScore - permScore); + if higherIsBetter then + acc += (baselineScore - permScore) + else + acc += (permScore - baselineScore); end; resultVec[j] := acc / nRepeats; diff --git a/bin/Lib/MLExceptions.pas b/bin/Lib/MLExceptions.pas index c72288fb3..81b8e544e 100644 --- a/bin/Lib/MLExceptions.pas +++ b/bin/Lib/MLExceptions.pas @@ -4,7 +4,7 @@ interface const ER_DIM_MISMATCH = - 'Несоответствие размерностей: {0} и {1}!!Dimension mismatch: {0} and {1}'; + 'Размерности не совпадают: {0} и {1}!!Dimension mismatch: {0} and {1}'; ER_PARAM_VALUES_EMPTY = 'Список paramValues пуст!!paramValues is empty'; ER_EMPTY_DATASET = @@ -25,6 +25,33 @@ const 'В DataFrame отсутствует столбец "{0}"!!DataFrame does not contain column "{0}"'; ER_NOT_IMPLEMENTED = 'Метод {0} не реализован!!Method {0} is not implemented'; + ER_X_NULL = + 'X не может быть nil!!X cannot be nil'; + ER_Y_NULL = + 'y не может быть nil!!y cannot be nil'; + ER_XY_SIZE_MISMATCH = + 'Размерности X и y не согласованы!!X and y size mismatch'; + ER_FEATURE_COUNT_MISMATCH = + 'Число признаков не совпадает!!Feature count mismatch'; + ER_NAN_IN_X = + 'X содержит NaN (пропуски)!!X contains NaN'; + ER_NAN_IN_Y = + 'y содержит NaN (пропуски)!!y contains NaN'; + 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_MODEL_NOT_FITTED = + 'Модель "{0}" не обучена. Сначала вызовите Fit()|Model "{0}" is not fitted. Call Fit() first'; + type /// Базовое исключение ML-библиотеки diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 52e07360f..cb0c8aab3 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -1,17 +1,4 @@ -// ============================================================= -// СТАТИСТИЧЕСКОЕ СОГЛАШЕНИЕ (MLModelsABC) -// -// Модели и трансформеры используют статистические методы -// из LinearAlgebraML. -// -// В частности: -// • StandardScaler, VarianceThreshold и др. используют -// дисперсию с делением на n (population variance) -// -// См. статистическую политику в модуле MLABC. -// ============================================================= - -/// Модуль моделей машинного обучения и матричных преобразований. +/// Модуль моделей машинного обучения и матричных преобразований. /// /// Содержит: /// • функции активации @@ -25,6 +12,19 @@ /// X — Matrix (объекты × признаки), y — Vector (целевая переменная). unit MLModelsABC; +// ============================================================= +// СТАТИСТИЧЕСКОЕ СОГЛАШЕНИЕ (MLModelsABC) +// +// Модели и трансформеры используют статистические методы +// из LinearAlgebraML. +// +// В частности: +// • StandardScaler, VarianceThreshold и др. используют +// дисперсию с делением на n (population variance) +// +// См. статистическую политику в модуле MLABC. +// ============================================================= + interface uses MLCoreABC; @@ -205,7 +205,7 @@ type /// Обучение выполняется методом покоординатного спуска /// ВАЖНО: Модель чувствительна к масштабу признаков. /// Всегда используйте StandardScaler в Pipeline перед ElasticNet. - ElasticNet = class(IRegressor) + ElasticNet = class(IRegressor) private fLambda1: real; // L1 fLambda2: real; // L2 @@ -549,6 +549,7 @@ type minSamplesSplit: integer; minSamplesLeaf: integer; criterion: ISplitCriterion; + classCount: integer; maxFeatures: integer; seed: integer := -1 ); @@ -2023,7 +2024,11 @@ type /// Стандартизирует признаки: вычитает среднее /// и делит на стандартное отклонение по каждому столбцу. -/// Используется для приведения признаков к сопоставимому масштабу +/// Используется для приведения признаков к сопоставимому масштабу. +/// +/// Примечание: +/// • стандартное отклонение вычисляется с делением на n +/// (population variance, как в sklearn) StandardScaler = class(IUnsupervisedTransformer) private fMean: Vector; @@ -2438,16 +2443,6 @@ const 'Неизвестный тип FeatureScore!!Unknown FeatureScore type'; ER_SELECTKBEST_FIT_INVALID = 'Для SelectKBest необходимо вызывать Fit(X, y)!!SelectKBest requires Fit(X, y)'; - ER_FIT_NOT_CALLED = - 'Необходимо вызвать Fit перед Predict!!Fit must be called before Predict'; - ER_X_NULL = - 'X не может быть nil!!X cannot be nil'; - ER_Y_NULL = - 'y не может быть nil!!y cannot be nil'; - ER_XY_SIZE_MISMATCH = - 'Размерности X и y не согласованы!!X and y size mismatch'; - ER_FEATURE_COUNT_MISMATCH = - 'Число признаков не совпадает!!Feature count mismatch'; ER_N_ESTIMATORS_NOT_POSITIVE = 'Параметр nEstimators должен быть > 0!!nEstimators must be > 0'; ER_LEARNING_RATE_NOT_POSITIVE = @@ -2464,10 +2459,6 @@ const 'maxDepth должен быть > 0!!maxDepth must be > 0'; ER_SUBSAMPLE_INVALID = 'subsample должен быть > 0!!subsample must be > 0'; - ER_NAN_IN_Y = - 'y содержит NaN!!y contains NaN'; - ER_NAN_IN_X = - 'x содержит NaN!!x contains NaN'; ER_K_EXCEEDS_SAMPLES = 'k превышает число обучающих объектов!!k exceeds number of training samples'; ER_MAX_DEPTH_TOO_LARGE = @@ -2488,18 +2479,6 @@ const 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}.'; @@ -2598,8 +2577,6 @@ const 'Model (type: {0}) is not an unsupervised model'; ER_DBSCAN_PREDICT_ONLY_TRAIN_DATA = 'DBSCAN: Predict поддерживается только для обучающей выборки!!DBSCAN: Predict is only supported for training data'; - ER_MODEL_NOT_FITTED = - 'Модель "{0}" не обучена. Сначала вызовите Fit()|Model "{0}" is not fitted. Call Fit() first'; ER_DBSCAN_PREDICT_NEW_DATA = 'DBSCAN не поддерживает предсказание для новых данных!!DBSCAN does not support prediction for new data'; ER_CLASSES_NOT_AVAILABLE = @@ -2618,7 +2595,9 @@ const ER_UNKNOWN_CLASS_LABEL = 'Неизвестная метка класса: {0}!!Unknown class label: {0}'; ER_MAX_FEATURES_INVALID = - 'maxFeatures должен быть >= 0!!maxFeatures must be >= 0'; + 'maxFeatures должен быть >= 0!!maxFeatures must be >= 0'; + ER_OOB_REQUIRES_SUBSAMPLE = + 'Для OOB early stopping требуется subsample < 1.0!!OOB early stopping requires subsample < 1.0'; {$endregion ErrConstants} @@ -3526,7 +3505,6 @@ begin Result := res; end; - function LeafClass(c: integer): DecisionTreeNode; begin var n := new DecisionTreeNode; @@ -3582,6 +3560,7 @@ constructor DecisionTreeCore.Create( minSamplesSplit: integer; minSamplesLeaf: integer; criterion: ISplitCriterion; + classCount: integer; maxFeatures: integer; seed: integer ); @@ -3591,6 +3570,7 @@ begin fMinSamplesLeaf := minSamplesLeaf; fCriterion := criterion; fMaxFeatures := maxFeatures; + fClassCount := classCount; fRandomSeed := ResolveRandomSeed(seed, fUserProvidedSeed); fRng := new System.Random(fRandomSeed); @@ -3782,7 +3762,7 @@ begin yr := y.SubvectorBy(rightArr); end; -function DecisionTreeCore.FindBestSplit( +{function DecisionTreeCore.FindBestSplit( X: Matrix; y: Vector; indices: array of integer; @@ -3888,6 +3868,160 @@ begin end; end; end; +end;} + +// Новая реализация (O(n log n · p)) +function DecisionTreeCore.FindBestSplit( + X: Matrix; + y: Vector; + indices: array of integer; + var bestF: integer; + var bestT: real +): boolean; +begin + Result := false; + + var n := indices.Length; + if n < 2 then exit; + + var parentImp := fCriterion.Impurity(y, indices); + var bestScore := parentImp; + + var featureCount := X.ColCount; + + // --- выбор признаков (как было) + var features: array of integer; + + if (fMaxFeatures > 0) and (fMaxFeatures < featureCount) then + begin + var perm := new integer[featureCount]; + for var i := 0 to featureCount - 1 do + perm[i] := i; + + for var i := 0 to fMaxFeatures - 1 do + begin + var j := i + fRng.Next(featureCount - i); + var tmp := perm[i]; + perm[i] := perm[j]; + perm[j] := tmp; + end; + + SetLength(features, fMaxFeatures); + for var i := 0 to fMaxFeatures - 1 do + features[i] := perm[i]; + end + else + begin + SetLength(features, featureCount); + for var i := 0 to featureCount - 1 do + features[i] := i; + end; + + // --- перебор признаков + for var fi := 0 to features.Length - 1 do + begin + var f := features[fi]; + + // пары (value, index) + var pairs: array of (real, integer); + SetLength(pairs, n); + + for var i := 0 to n - 1 do + begin + var idx := indices[i]; + pairs[i] := (X[idx, f], idx); + end; + + pairs.Sort(p -> p.Item1); + + // --- counts + var leftCounts := new integer[fClassCount]; + var rightCounts := new integer[fClassCount]; + + for var i := 0 to n - 1 do + rightCounts[Round(y[pairs[i].Item2])] += 1; + + var leftCount := 0; + + // --- проход + for var i := 1 to n - 1 do + begin + var idx := pairs[i-1].Item2; + var cls := Round(y[idx]); + + leftCounts[cls] += 1; + rightCounts[cls] -= 1; + leftCount += 1; + + var rightCount := n - leftCount; + + if (leftCount < fMinSamplesLeaf) or (rightCount < fMinSamplesLeaf) then + continue; + + var x1 := pairs[i-1].Item1; + var x2 := pairs[i].Item1; + + if x1 = x2 then + continue; + + // --- impurity left + var leftImp := 0.0; + var rightImp := 0.0; + + if fCriterion is GiniCriterion then + begin + var sumsqL := 0.0; + var sumsqR := 0.0; + + for var c := 0 to fClassCount - 1 do + begin + if leftCounts[c] > 0 then + begin + var p := leftCounts[c] / leftCount; + sumsqL += p * p; + end; + + if rightCounts[c] > 0 then + begin + var p := rightCounts[c] / rightCount; + sumsqR += p * p; + end; + end; + + leftImp := 1.0 - sumsqL; + rightImp := 1.0 - sumsqR; + end + else + begin + // entropy + for var c := 0 to fClassCount - 1 do + begin + if leftCounts[c] > 0 then + begin + var p := leftCounts[c] / leftCount; + leftImp -= p * Ln(p); + end; + + if rightCounts[c] > 0 then + begin + var p := rightCounts[c] / rightCount; + rightImp -= p * Ln(p); + end; + end; + end; + + var score := + (leftCount * leftImp + rightCount * rightImp) / n; + + if score < bestScore then + begin + bestScore := score; + bestF := f; + bestT := (x1 + x2) * 0.5; + Result := true; + end; + end; + end; end; function DecisionTreeCore.Clone: DecisionTreeCore; @@ -5575,6 +5709,10 @@ begin if XVal.ColCount <> XTrain.ColCount then DimensionError(ER_FEATURE_COUNT_MISMATCH); end; + + // --- OOB checks --- + if fUseOOBEarlyStopping and (fSubsample >= 1.0) then + ArgumentError(ER_OOB_REQUIRES_SUBSAMPLE); // --- init state --- fOOBLossHistory.Clear; @@ -6735,7 +6873,7 @@ begin NotFittedError(ER_FIT_NOT_CALLED); if fFeatureImportances <> nil then - exit(fFeatureImportances); + exit(fFeatureImportances.Clone); var importances := new Vector(fFeatureCount); @@ -6758,7 +6896,7 @@ begin fFeatureImportances := importances; - Result := fFeatureImportances; + Result := fFeatureImportances.Clone; end; function GradientBoostingClassifier.ToString: string; @@ -7820,9 +7958,6 @@ begin var n := X.RowCount; var p := X.ColCount; - if n = 0 then - ArgumentError(ER_EMPTY_DATA, 'DBSCAN'); - fFeatureCount := p; var labels := new integer[n]; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 9669e1366..114dca911 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -285,7 +285,7 @@ const ER_CLASSES_NOT_AVAILABLE = 'Метки классов недоступны. Убедитесь, что конвейер обучен и задача — классификация!!Class labels are not available. Ensure the pipeline is fitted and the task is classification'; ER_LABELENCODER_TARGET_NOT_ALLOWED = - 'LabelEncoder нельзя применять к целевому столбцу. Используйте EncodeLabels!!LabelEncoder cannot be applied to the target column. Use EncodeLabels instead'; + 'LabelEncoder нельзя применять к целевой переменной — кодирование выполняется внутри модели!!LabelEncoder cannot be applied to target — encoding is handled internally by the model'; ER_ENCODELABELS_NOT_CATEGORICAL = 'Целевой столбец должен быть категориальным для задач классификации!!Target column must be categorical for classification tasks'; ER_PIPELINE_TARGET_TRANSFORM_NOT_ALLOWED = diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index 63b568954..1e7f827af 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -319,11 +319,12 @@ begin var cls := pair.Key; var cnt := pair.Value.Count; - if cnt < k then - ArgumentError( - ER_STRATIFIED_CLASS_TOO_SMALL, - cls, cnt, k - ); + // Класс может иметь меньше объектов, чем число фолдов. + // В этом случае он не будет представлен во всех фолдах. + // Это допустимо, но может привести к ошибкам при обучении модели. + + //if cnt < k then + // ArgumentError(ER_STRATIFIED_CLASS_TOO_SMALL, cls, cnt, k); end; // --- 2. Контейнеры фолдов