From 3938120846dda101f032dcbc0a21e9adc12da091 Mon Sep 17 00:00:00 2001 From: Mikhalkovich Stanislav Date: Thu, 28 May 2026 23:56:46 +0300 Subject: [PATCH] =?UTF-8?q?ML=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Небольшие правки ML-модулей --- .../TitanicRu/06_ModelComparison.pas | 5 +- .../TitanicRu/08_CrossValidation.pas | 4 +- .../TitanicRu/13_MisclassifiedPassengers.pas | 13 ++- .../TitanicRu/14_FeatureAblation.pas | 2 +- .../UsedCars/07_ModelComparison.pas | 3 +- .../UsedCars/08_CrossValidation.pas | 5 +- .../Core/001_labels_to_ints_strict.pas | 24 ++-- ...cs_accuracy_rejects_non_integer_labels.pas | 4 +- .../038_metrics_accuracy_rejects_nan_inf.pas | 4 +- ..._bool_columns_manual_encoding_contract.pas | 29 +++++ .../Examples/001_iris_07_pipeline_smoke.pas | 6 +- .../Examples/006_upipeline_kmeans_smoke.pas | 5 +- .../Regression/002_pipeline_predictlabels.pas | 6 +- .../003_logistic_predictlabels_contract.pas | 9 +- ...04_randomforest_predictlabels_contract.pas | 9 +- ...radientboosting_predictlabels_contract.pas | 9 +- .../006_knn_predictlabels_contract.pas | 9 +- .../009_pipeline_rowcount_invariant.pas | 6 +- ...10_decisiontree_predictlabels_contract.pas | 9 +- ...ipeline_getencodedlabels_unknown_class.pas | 6 +- .../013_upipeline_rowcount_invariant.pas | 5 +- ...eline_regression_rejects_string_target.pas | 6 +- ...sification_requires_categorical_target.pas | 6 +- .../023_pipeline_predictproba_shape.pas | 6 +- ...4_pipeline_target_in_features_rejected.pas | 6 +- ..._upipeline_duplicate_features_rejected.pas | 5 +- ...predictlabels_returns_original_strings.pas | 6 +- ...028_pipeline_target_bound_step_blocked.pas | 6 +- ...9_pipeline_target_columns_step_blocked.pas | 6 +- ...peline_int_categorical_target_contract.pas | 6 +- ...31_logistic_sparse_raw_labels_contract.pas | 9 +- ...andomforest_sparse_raw_labels_contract.pas | 9 +- ...entboosting_sparse_raw_labels_contract.pas | 9 +- ...ecisiontree_sparse_raw_labels_contract.pas | 9 +- .../035_knn_sparse_raw_labels_contract.pas | 9 +- ..._pipeline_predict_dense_encoded_labels.pas | 6 +- ..._contract_matches_documented_semantics.pas | 6 +- ...lassifier_predict_contract_is_explicit.pas | 9 +- bin/Lib/MLABC.pas | 31 ++++- bin/Lib/MLCoreABC.pas | 30 +++-- bin/Lib/MLDatasets.pas | 2 +- bin/Lib/MLModelsABC.pas | 106 ++++++++---------- bin/Lib/MLPipelineABC.pas | 52 +++++---- 43 files changed, 279 insertions(+), 233 deletions(-) create mode 100644 TestSuite/_MachineLearning/Core/044_bool_columns_manual_encoding_contract.pas diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/06_ModelComparison.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/06_ModelComparison.pas index f1656991f..6cf8e74ca 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/06_ModelComparison.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/06_ModelComparison.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; begin var ds := Datasets.TitanicRu; @@ -11,8 +11,7 @@ begin df.StratifiedTrainTestSplit(ds.Target, testRatio := 0.2, seed := 42); var prep := - DataPipeline.BuildPreprocessing( - TaskKind.tkClassification, + DataPipeline.BuildClassificationPreprocessing( target, features, new Imputer(['Возраст']), diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/08_CrossValidation.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/08_CrossValidation.pas index 7ddfaeb25..3653810ad 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/08_CrossValidation.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/08_CrossValidation.pas @@ -1,6 +1,6 @@ -uses MLABC; +uses MLABC; -function BuildPipe(features: array of string): DataPipeline; +function BuildPipe(features: array of string): ClassificationDataPipeline; begin Result := DataPipeline.BuildClassification( diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/13_MisclassifiedPassengers.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/13_MisclassifiedPassengers.pas index 34e6a9ab2..b77882c21 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/13_MisclassifiedPassengers.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/13_MisclassifiedPassengers.pas @@ -32,9 +32,9 @@ begin var pred := pipe.Predict(testDf); var y := pipe.GetEncodedLabels(testDf); - var predInt := pred.ToIntArray; - var yInt := y.ToIntArray; + var testPrepared := pipe.Transform(testDf); var cur := testFull.GetCursor; + var curPrepared := testPrepared.GetCursor; Println('TitanicRu: пассажиры, классифицированные неправильно'); Println; @@ -42,19 +42,22 @@ begin var shown := 0; while cur.MoveNext do begin + if not curPrepared.MoveNext then + break; + var i := cur.Position; - if yInt[i] <> predInt[i] then + if y[i] <> pred[i] then begin var id := cur.Int('Id'); var name := cur.Str('Имя'); var sex := cur.Str('Пол'); var pclass := cur.Int('Класс'); - var age := cur.Float('Возраст'); + var age := curPrepared.Float('Возраст'); var fare := cur.Float('ЦенаБилета'); Println($'Id={id}, {name}'); Println($' Пол={sex}, класс={pclass}, возраст={age:F1}, билет={fare:F1}'); - Println($' Истина: {LabelText(yInt[i])}, прогноз: {LabelText(predInt[i])}'); + Println($' Истина: {LabelText(y[i])}, прогноз: {LabelText(pred[i])}'); Println; shown += 1; diff --git a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/14_FeatureAblation.pas b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/14_FeatureAblation.pas index fe41e1e80..b72740b05 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/14_FeatureAblation.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/TitanicRu/14_FeatureAblation.pas @@ -1,6 +1,6 @@ uses MLABC; -function BuildPipe(features: array of string): DataPipeline; +function BuildPipe(features: array of string): ClassificationDataPipeline; begin Result := DataPipeline.BuildClassification( diff --git a/InstallerSamples/MachineLearning/08_Datasets/UsedCars/07_ModelComparison.pas b/InstallerSamples/MachineLearning/08_Datasets/UsedCars/07_ModelComparison.pas index 8209f136b..e247e13f9 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/UsedCars/07_ModelComparison.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/UsedCars/07_ModelComparison.pas @@ -19,8 +19,7 @@ begin var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 42); var prep := - DataPipeline.BuildPreprocessing( - TaskKind.tkRegression, + DataPipeline.BuildRegressionPreprocessing( target, features, new OrdinalEncoder('model'), diff --git a/InstallerSamples/MachineLearning/08_Datasets/UsedCars/08_CrossValidation.pas b/InstallerSamples/MachineLearning/08_Datasets/UsedCars/08_CrossValidation.pas index 1408bdd5f..73324e5f5 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/UsedCars/08_CrossValidation.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/UsedCars/08_CrossValidation.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; begin var ds := Datasets.UsedCarsPrice; @@ -19,8 +19,7 @@ begin var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 42); var prep := - DataPipeline.BuildPreprocessing( - TaskKind.tkRegression, + DataPipeline.BuildRegressionPreprocessing( target, features, new OrdinalEncoder('model'), diff --git a/TestSuite/_MachineLearning/Core/001_labels_to_ints_strict.pas b/TestSuite/_MachineLearning/Core/001_labels_to_ints_strict.pas index 728d157df..58b03872a 100644 --- a/TestSuite/_MachineLearning/Core/001_labels_to_ints_strict.pas +++ b/TestSuite/_MachineLearning/Core/001_labels_to_ints_strict.pas @@ -1,15 +1,21 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin - var y := new Vector(Arr(0.0, 0.9999999999999, 2.0000000000001)); - var labels := LabelsToInts(y); + var ds := new Dataset; + ds.Task := TaskType.Classification; + ds.Target := 'Target'; + ds.Data := new DataFrame; + ds.Data.AddStrColumn('Target', Arr('cat', 'dog', 'cat')); - Check(labels.Length = 3, 'Length mismatch'); - Check(labels[0] = 0, 'First label mismatch'); - Check(labels[1] = 1, 'Second label mismatch'); - Check(labels[2] = 2, 'Third label mismatch'); + var enc := new LabelEncoder; + var y := enc.FitTransform(ds); - var bad := new Vector(Arr(0.0, 1.2, 2.0)); - CheckRaises(procedure -> begin var tmp := LabelsToInts(bad); end, 'LabelsToInts must reject non-integer labels'); + Check(y.Length = 3, 'Length mismatch'); + Check(y[0] = 0, 'First label mismatch'); + Check(y[1] = 1, 'Second label mismatch'); + Check(y[2] = 0, 'Third label mismatch'); + Check(enc.Classes.Length = 2, 'Class count mismatch'); + Check(enc.Classes[0] = 'cat', 'First class mismatch'); + Check(enc.Classes[1] = 'dog', 'Second class mismatch'); end. diff --git a/TestSuite/_MachineLearning/Core/037_metrics_accuracy_rejects_non_integer_labels.pas b/TestSuite/_MachineLearning/Core/037_metrics_accuracy_rejects_non_integer_labels.pas index 4aeda3a2d..fce64d0f1 100644 --- a/TestSuite/_MachineLearning/Core/037_metrics_accuracy_rejects_non_integer_labels.pas +++ b/TestSuite/_MachineLearning/Core/037_metrics_accuracy_rejects_non_integer_labels.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -6,6 +6,6 @@ begin var yPred := new Vector(Arr(0.51, 1.0)); CheckRaises(procedure -> begin - var a := Metrics.Accuracy(yTrue, yPred); + var a := Metrics.Accuracy(yTrue.ToIntArray, yPred.ToIntArray); end, 'Accuracy must reject non-integer class labels'); end. diff --git a/TestSuite/_MachineLearning/Core/038_metrics_accuracy_rejects_nan_inf.pas b/TestSuite/_MachineLearning/Core/038_metrics_accuracy_rejects_nan_inf.pas index b9c041ebc..7298b3258 100644 --- a/TestSuite/_MachineLearning/Core/038_metrics_accuracy_rejects_nan_inf.pas +++ b/TestSuite/_MachineLearning/Core/038_metrics_accuracy_rejects_nan_inf.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -6,6 +6,6 @@ begin var yPred := new Vector(Arr(real.NaN, 1.0)); CheckRaises(procedure -> begin - var a := Metrics.Accuracy(yTrue, yPred); + var a := Metrics.Accuracy(yTrue.ToIntArray, yPred.ToIntArray); end, 'Accuracy must reject NaN and Infinity in class labels'); end. diff --git a/TestSuite/_MachineLearning/Core/044_bool_columns_manual_encoding_contract.pas b/TestSuite/_MachineLearning/Core/044_bool_columns_manual_encoding_contract.pas new file mode 100644 index 000000000..3d5d6f317 --- /dev/null +++ b/TestSuite/_MachineLearning/Core/044_bool_columns_manual_encoding_contract.pas @@ -0,0 +1,29 @@ +uses MLABC; +uses TestHelpers in '..\TestHelpers.pas'; + +begin + var df := new DataFrame; + df.AddBoolColumn('Flag', Arr(true, false, true, false)); + df.AddBoolColumn('IsNew', Arr(false, false, true, true)); + df.AddFloatColumn('X', Arr(1.5, 2.5, 3.5, 4.5)); + df.AddIntColumn('Target', Arr(0, 1, 0, 1)); + df := df.SetCategorical(['Target']); + + var X := df.ToMatrix(['Flag', 'IsNew', 'X']); + var y := df.ToVector('Target').ToIntArray; + + Check(X.RowCount = 4, 'ToMatrix row count mismatch'); + Check(X.ColCount = 3, 'ToMatrix col count mismatch'); + + Check(Abs(X[0,0] - 1.0) < 1e-12, 'Flag=true must encode as 1'); + Check(Abs(X[1,0] - 0.0) < 1e-12, 'Flag=false must encode as 0'); + Check(Abs(X[0,1] - 0.0) < 1e-12, 'IsNew=false must encode as 0'); + Check(Abs(X[2,1] - 1.0) < 1e-12, 'IsNew=true must encode as 1'); + Check(Abs(X[3,2] - 4.5) < 1e-12, 'Numeric column must stay unchanged'); + + var model := new DecisionTreeClassifier(maxDepth := 3); + model.Fit(X, y); + var pred := model.Predict(X); + + Check(pred.Length = df.RowCount, 'Manual bool-feature model prediction length mismatch'); +end. diff --git a/TestSuite/_MachineLearning/Examples/001_iris_07_pipeline_smoke.pas b/TestSuite/_MachineLearning/Examples/001_iris_07_pipeline_smoke.pas index 25a7a52db..5d96c2b32 100644 --- a/TestSuite/_MachineLearning/Examples/001_iris_07_pipeline_smoke.pas +++ b/TestSuite/_MachineLearning/Examples/001_iris_07_pipeline_smoke.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -6,8 +6,7 @@ begin var df := ds.Data; var pipe := - DataPipeline.Build( - TaskKind.tkClassification, + DataPipeline.BuildClassification( ds.Target, ds.Features, new StandardScaler, @@ -25,3 +24,4 @@ begin Check(y.Length = testDf.RowCount, 'Encoded labels length mismatch'); Check(Metrics.Accuracy(y, pred) > 0.8, 'Pipeline accuracy is unexpectedly low'); end. + diff --git a/TestSuite/_MachineLearning/Examples/006_upipeline_kmeans_smoke.pas b/TestSuite/_MachineLearning/Examples/006_upipeline_kmeans_smoke.pas index 3494ef0be..781bc69ea 100644 --- a/TestSuite/_MachineLearning/Examples/006_upipeline_kmeans_smoke.pas +++ b/TestSuite/_MachineLearning/Examples/006_upipeline_kmeans_smoke.pas @@ -1,11 +1,11 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin var ds := Datasets.Iris; var pipe := - UDataPipeline.Build( + DataPipeline.BuildClustering( ds.Features, new StandardScaler, new KMeans(3, seed := 1) @@ -15,3 +15,4 @@ begin Check(labels.Length = ds.Data.RowCount, 'UDataPipeline labels length mismatch'); end. + diff --git a/TestSuite/_MachineLearning/Regression/002_pipeline_predictlabels.pas b/TestSuite/_MachineLearning/Regression/002_pipeline_predictlabels.pas index 11078f51d..2d6a89ca4 100644 --- a/TestSuite/_MachineLearning/Regression/002_pipeline_predictlabels.pas +++ b/TestSuite/_MachineLearning/Regression/002_pipeline_predictlabels.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,8 +7,7 @@ begin var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); var pipe := - DataPipeline.Build( - TaskKind.tkClassification, + DataPipeline.BuildClassification( ds.Target, ds.Features, new StandardScaler, @@ -42,3 +41,4 @@ begin Check(classes[yi] = trueLabels[i], $'GetEncodedLabels[{i}] does not decode to true target'); end; end. + diff --git a/TestSuite/_MachineLearning/Regression/003_logistic_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/003_logistic_predictlabels_contract.pas index de7b6da52..726e2bf42 100644 --- a/TestSuite/_MachineLearning/Regression/003_logistic_predictlabels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/003_logistic_predictlabels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -21,9 +21,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); - Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); - Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + var pi := pred[i]; + Check((pi >= 0) and (pi < modelClasses.Length), $'Predict[{i}] out of range'); + Check(labels[i] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/004_randomforest_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/004_randomforest_predictlabels_contract.pas index 1491cb531..bc46063b6 100644 --- a/TestSuite/_MachineLearning/Regression/004_randomforest_predictlabels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/004_randomforest_predictlabels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -21,9 +21,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); - Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); - Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + var pi := pred[i]; + Check((pi >= 0) and (pi < modelClasses.Length), $'Predict[{i}] out of range'); + Check(labels[i] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/005_gradientboosting_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/005_gradientboosting_predictlabels_contract.pas index 9e615a1d8..907f934f8 100644 --- a/TestSuite/_MachineLearning/Regression/005_gradientboosting_predictlabels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/005_gradientboosting_predictlabels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -21,9 +21,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); - Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); - Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + var pi := pred[i]; + Check((pi >= 0) and (pi < modelClasses.Length), $'Predict[{i}] out of range'); + Check(labels[i] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/006_knn_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/006_knn_predictlabels_contract.pas index b526b13d6..7891ef8ff 100644 --- a/TestSuite/_MachineLearning/Regression/006_knn_predictlabels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/006_knn_predictlabels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -21,9 +21,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); - Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); - Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + var pi := pred[i]; + Check((pi >= 0) and (pi < modelClasses.Length), $'Predict[{i}] out of range'); + Check(labels[i] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/009_pipeline_rowcount_invariant.pas b/TestSuite/_MachineLearning/Regression/009_pipeline_rowcount_invariant.pas index 94c51dded..93386e429 100644 --- a/TestSuite/_MachineLearning/Regression/009_pipeline_rowcount_invariant.pas +++ b/TestSuite/_MachineLearning/Regression/009_pipeline_rowcount_invariant.pas @@ -1,4 +1,4 @@ -uses MLABC, PreprocessorABC; +uses MLABC, PreprocessorABC; uses TestHelpers in '..\TestHelpers.pas'; type @@ -32,8 +32,7 @@ end; begin var ds := Datasets.Iris; - var pipe := DataPipeline.Build( - TaskKind.tkClassification, + var pipe := DataPipeline.BuildClassification( ds.Target, ds.Features, new BadDropper, @@ -43,3 +42,4 @@ begin CheckRaises(procedure -> begin pipe.Fit(ds.Data); end, 'Pipeline must reject DataFrame preprocessors that change RowCount'); end. + diff --git a/TestSuite/_MachineLearning/Regression/010_decisiontree_predictlabels_contract.pas b/TestSuite/_MachineLearning/Regression/010_decisiontree_predictlabels_contract.pas index 1285fb5a3..f1fb63119 100644 --- a/TestSuite/_MachineLearning/Regression/010_decisiontree_predictlabels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/010_decisiontree_predictlabels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -21,9 +21,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check(Abs(pred[i] - pi) < 1e-12, $'Predict[{i}] is not an original integer label'); - Check((labels[i] >= 0) and (labels[i] < modelClasses.Length), $'PredictLabels[{i}] out of range'); - Check(modelClasses[labels[i]] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); + var pi := pred[i]; + Check((pi >= 0) and (pi < modelClasses.Length), $'Predict[{i}] out of range'); + Check(labels[i] = pi.ToString, $'Predict[{i}] and PredictLabels[{i}] are inconsistent'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/012_pipeline_getencodedlabels_unknown_class.pas b/TestSuite/_MachineLearning/Regression/012_pipeline_getencodedlabels_unknown_class.pas index 00e841aea..8b6d5b848 100644 --- a/TestSuite/_MachineLearning/Regression/012_pipeline_getencodedlabels_unknown_class.pas +++ b/TestSuite/_MachineLearning/Regression/012_pipeline_getencodedlabels_unknown_class.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -12,8 +12,7 @@ begin testDf.AddStrColumn('Target', Arr('cat', 'fox')); testDf := testDf.SetCategorical(['Target']); - var pipe := DataPipeline.Build( - TaskKind.tkClassification, + var pipe := DataPipeline.BuildClassification( 'Target', Arr($'X'), new LogisticRegression @@ -24,3 +23,4 @@ begin CheckRaises(procedure -> begin var y := pipe.GetEncodedLabels(testDf); end, 'GetEncodedLabels must reject unseen target classes'); end. + diff --git a/TestSuite/_MachineLearning/Regression/013_upipeline_rowcount_invariant.pas b/TestSuite/_MachineLearning/Regression/013_upipeline_rowcount_invariant.pas index 9567668f8..0686e5194 100644 --- a/TestSuite/_MachineLearning/Regression/013_upipeline_rowcount_invariant.pas +++ b/TestSuite/_MachineLearning/Regression/013_upipeline_rowcount_invariant.pas @@ -1,4 +1,4 @@ -uses MLABC, PreprocessorABC; +uses MLABC, PreprocessorABC; uses TestHelpers in '..\TestHelpers.pas'; type @@ -32,7 +32,7 @@ end; begin var ds := Datasets.Iris; - var pipe := UDataPipeline.Build( + var pipe := DataPipeline.BuildClustering( ds.Features, new BadDropper, new KMeans(3, seed := 1) @@ -41,3 +41,4 @@ begin CheckRaises(procedure -> begin var labels := pipe.FitPredict(ds.Data); end, 'UDataPipeline must reject DataFrame preprocessors that change RowCount'); end. + diff --git a/TestSuite/_MachineLearning/Regression/018_pipeline_regression_rejects_string_target.pas b/TestSuite/_MachineLearning/Regression/018_pipeline_regression_rejects_string_target.pas index f4b46427f..ca5515b55 100644 --- a/TestSuite/_MachineLearning/Regression/018_pipeline_regression_rejects_string_target.pas +++ b/TestSuite/_MachineLearning/Regression/018_pipeline_regression_rejects_string_target.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,8 +7,7 @@ begin df.AddStrColumn('Target', Arr('low', 'mid', 'high')); df := df.SetCategorical(['Target']); - var pipe := DataPipeline.Build( - TaskKind.tkRegression, + var pipe := DataPipeline.BuildRegression( 'Target', Arr($'X'), new LinearRegression @@ -17,3 +16,4 @@ begin CheckRaises(procedure -> begin pipe.Fit(df); end, 'Regression pipeline must reject non-numeric target'); end. + diff --git a/TestSuite/_MachineLearning/Regression/019_pipeline_classification_requires_categorical_target.pas b/TestSuite/_MachineLearning/Regression/019_pipeline_classification_requires_categorical_target.pas index 2f72ccd48..8c73a4e4b 100644 --- a/TestSuite/_MachineLearning/Regression/019_pipeline_classification_requires_categorical_target.pas +++ b/TestSuite/_MachineLearning/Regression/019_pipeline_classification_requires_categorical_target.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -6,8 +6,7 @@ begin df.AddFloatColumn('X', Arr(1.0, 2.0, 3.0, 4.0)); df.AddStrColumn('Target', Arr('cat', 'dog', 'cat', 'dog')); - var pipe := DataPipeline.Build( - TaskKind.tkClassification, + var pipe := DataPipeline.BuildClassification( 'Target', Arr($'X'), new LogisticRegression @@ -16,3 +15,4 @@ begin CheckRaises(procedure -> begin pipe.Fit(df); end, 'Classification pipeline must require categorical target'); end. + diff --git a/TestSuite/_MachineLearning/Regression/023_pipeline_predictproba_shape.pas b/TestSuite/_MachineLearning/Regression/023_pipeline_predictproba_shape.pas index df92fb040..1f050675a 100644 --- a/TestSuite/_MachineLearning/Regression/023_pipeline_predictproba_shape.pas +++ b/TestSuite/_MachineLearning/Regression/023_pipeline_predictproba_shape.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -6,8 +6,7 @@ begin var df := ds.Data; var pipe := - DataPipeline.Build( - TaskKind.tkClassification, + DataPipeline.BuildClassification( ds.Target, ds.Features, new StandardScaler, @@ -24,3 +23,4 @@ begin Check(proba.ColCount = classes.Length, 'Probability class count mismatch'); CheckProbabilityRowsSumToOne(proba); end. + diff --git a/TestSuite/_MachineLearning/Regression/024_pipeline_target_in_features_rejected.pas b/TestSuite/_MachineLearning/Regression/024_pipeline_target_in_features_rejected.pas index 10d24472e..ba85f3d52 100644 --- a/TestSuite/_MachineLearning/Regression/024_pipeline_target_in_features_rejected.pas +++ b/TestSuite/_MachineLearning/Regression/024_pipeline_target_in_features_rejected.pas @@ -1,10 +1,9 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin CheckRaises(procedure -> begin - var pipe := DataPipeline.Build( - TaskKind.tkClassification, + var pipe := DataPipeline.BuildClassification( 'Target', Arr($'X', $'Target'), new LogisticRegression @@ -12,3 +11,4 @@ begin end, 'DataPipeline.Build must reject target inside features'); end. + diff --git a/TestSuite/_MachineLearning/Regression/025_upipeline_duplicate_features_rejected.pas b/TestSuite/_MachineLearning/Regression/025_upipeline_duplicate_features_rejected.pas index ba605165d..8d6c1ad91 100644 --- a/TestSuite/_MachineLearning/Regression/025_upipeline_duplicate_features_rejected.pas +++ b/TestSuite/_MachineLearning/Regression/025_upipeline_duplicate_features_rejected.pas @@ -1,12 +1,13 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin CheckRaises(procedure -> begin - var pipe := UDataPipeline.Build( + var pipe := DataPipeline.BuildClustering( Arr($'X', $'X'), new KMeans(3, seed := 1) ); end, 'UDataPipeline.Build must reject duplicate features'); end. + diff --git a/TestSuite/_MachineLearning/Regression/026_pipeline_predictlabels_returns_original_strings.pas b/TestSuite/_MachineLearning/Regression/026_pipeline_predictlabels_returns_original_strings.pas index b19e7dbf7..77320cf71 100644 --- a/TestSuite/_MachineLearning/Regression/026_pipeline_predictlabels_returns_original_strings.pas +++ b/TestSuite/_MachineLearning/Regression/026_pipeline_predictlabels_returns_original_strings.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,8 +7,7 @@ begin var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); var pipe := - DataPipeline.Build( - TaskKind.tkClassification, + DataPipeline.BuildClassification( ds.Target, ds.Features, new StandardScaler, @@ -25,3 +24,4 @@ begin for var i := 0 to labels.Length - 1 do Check(labels[i] in classes, $'PredictLabels[{i}] must be one of pipeline class labels'); end. + diff --git a/TestSuite/_MachineLearning/Regression/028_pipeline_target_bound_step_blocked.pas b/TestSuite/_MachineLearning/Regression/028_pipeline_target_bound_step_blocked.pas index 592494065..170ce12ae 100644 --- a/TestSuite/_MachineLearning/Regression/028_pipeline_target_bound_step_blocked.pas +++ b/TestSuite/_MachineLearning/Regression/028_pipeline_target_bound_step_blocked.pas @@ -1,4 +1,4 @@ -uses MLABC, PreprocessorABC, MLCoreABC; +uses MLABC, PreprocessorABC, MLCoreABC; uses TestHelpers in '..\TestHelpers.pas'; type @@ -41,8 +41,7 @@ end; begin CheckRaises(procedure -> begin - var pipe := DataPipeline.Build( - TaskKind.tkClassification, + var pipe := DataPipeline.BuildClassification( 'Target', Arr($'X'), new BadTargetStep('Target'), @@ -51,3 +50,4 @@ begin end, 'DataPipeline must reject a bound preprocessor that targets the target column'); end. + diff --git a/TestSuite/_MachineLearning/Regression/029_pipeline_target_columns_step_blocked.pas b/TestSuite/_MachineLearning/Regression/029_pipeline_target_columns_step_blocked.pas index 5f027e78d..736e951e9 100644 --- a/TestSuite/_MachineLearning/Regression/029_pipeline_target_columns_step_blocked.pas +++ b/TestSuite/_MachineLearning/Regression/029_pipeline_target_columns_step_blocked.pas @@ -1,4 +1,4 @@ -uses MLABC, PreprocessorABC, MLCoreABC; +uses MLABC, PreprocessorABC, MLCoreABC; uses TestHelpers in '..\TestHelpers.pas'; type @@ -41,8 +41,7 @@ end; begin CheckRaises(procedure -> begin - var pipe := DataPipeline.Build( - TaskKind.tkClassification, + var pipe := DataPipeline.BuildClassification( 'Target', Arr($'X'), new BadTargetColumnsStep(Arr($'X', $'Target')), @@ -51,3 +50,4 @@ begin end, 'DataPipeline must reject a multi-column bound preprocessor that includes the target column'); end. + diff --git a/TestSuite/_MachineLearning/Regression/030_pipeline_int_categorical_target_contract.pas b/TestSuite/_MachineLearning/Regression/030_pipeline_int_categorical_target_contract.pas index 1cc1d3851..470cfd5c0 100644 --- a/TestSuite/_MachineLearning/Regression/030_pipeline_int_categorical_target_contract.pas +++ b/TestSuite/_MachineLearning/Regression/030_pipeline_int_categorical_target_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,8 +7,7 @@ begin df.AddIntColumn('Target', Arr(10, 20, 10, 20)); df := df.SetCategorical(['Target']); - var pipe := DataPipeline.Build( - TaskKind.tkClassification, + var pipe := DataPipeline.BuildClassification( 'Target', Arr($'X'), new LogisticRegression @@ -39,3 +38,4 @@ begin Check(predLabels[i] = classes[pi], $'PredictLabels[{i}] mismatch'); end; end. + diff --git a/TestSuite/_MachineLearning/Regression/031_logistic_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/031_logistic_sparse_raw_labels_contract.pas index a3e5d31cb..88b87042e 100644 --- a/TestSuite/_MachineLearning/Regression/031_logistic_sparse_raw_labels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/031_logistic_sparse_raw_labels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,7 +7,7 @@ begin X[2,0] := 1.0; X[3,0] := 1.1; X[4,0] := 2.0; X[5,0] := 2.1; - var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + var y := Arr(10, 10, 20, 20, 30, 30); var model := new LogisticRegression; model.Fit(X, y); @@ -30,9 +30,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + var pi := pred[i]; Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); - Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + Check(labels[i] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/032_randomforest_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/032_randomforest_sparse_raw_labels_contract.pas index 425fe8ed5..7e861e08e 100644 --- a/TestSuite/_MachineLearning/Regression/032_randomforest_sparse_raw_labels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/032_randomforest_sparse_raw_labels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,7 +7,7 @@ begin X[2,0] := 1.0; X[3,0] := 1.1; X[4,0] := 2.0; X[5,0] := 2.1; - var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + var y := Arr(10, 10, 20, 20, 30, 30); var model := new RandomForestClassifier(20, maxDepth := 6, seed := 1); model.Fit(X, y); @@ -25,9 +25,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + var pi := pred[i]; Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); - Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + Check(labels[i] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/033_gradientboosting_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/033_gradientboosting_sparse_raw_labels_contract.pas index 6f38b4c11..8249091d9 100644 --- a/TestSuite/_MachineLearning/Regression/033_gradientboosting_sparse_raw_labels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/033_gradientboosting_sparse_raw_labels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,7 +7,7 @@ begin X[2,0] := 1.0; X[3,0] := 1.1; X[4,0] := 2.0; X[5,0] := 2.1; - var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + var y := Arr(10, 10, 20, 20, 30, 30); var model := new GradientBoostingClassifier(20, learningRate := 0.1, maxDepth := 3, seed := 1); model.Fit(X, y); @@ -25,9 +25,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + var pi := pred[i]; Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); - Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + Check(labels[i] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/034_decisiontree_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/034_decisiontree_sparse_raw_labels_contract.pas index 90bcaf834..010758962 100644 --- a/TestSuite/_MachineLearning/Regression/034_decisiontree_sparse_raw_labels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/034_decisiontree_sparse_raw_labels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,7 +7,7 @@ begin X[2,0] := 1.0; X[3,0] := 1.1; X[4,0] := 2.0; X[5,0] := 2.1; - var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + var y := Arr(10, 10, 20, 20, 30, 30); var model := new DecisionTreeClassifier(maxDepth := 6, seed := 1); model.Fit(X, y); @@ -23,9 +23,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + var pi := pred[i]; Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); - Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + Check(labels[i] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/035_knn_sparse_raw_labels_contract.pas b/TestSuite/_MachineLearning/Regression/035_knn_sparse_raw_labels_contract.pas index 184f119ec..3bab331ad 100644 --- a/TestSuite/_MachineLearning/Regression/035_knn_sparse_raw_labels_contract.pas +++ b/TestSuite/_MachineLearning/Regression/035_knn_sparse_raw_labels_contract.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,7 +7,7 @@ begin X[2,0] := 1.0; X[3,0] := 1.1; X[4,0] := 2.0; X[5,0] := 2.1; - var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + var y := Arr(10, 10, 20, 20, 30, 30); var model := new KNNClassifier(3); model.Fit(X, y); @@ -25,9 +25,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); - Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); + var pi := pred[i]; Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original sparse label'); - Check(classes[labels[i]] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); + Check(labels[i] = pi.ToString, $'Predict and PredictLabels mismatch at {i}'); end; end. diff --git a/TestSuite/_MachineLearning/Regression/036_pipeline_predict_dense_encoded_labels.pas b/TestSuite/_MachineLearning/Regression/036_pipeline_predict_dense_encoded_labels.pas index 1e249fc5e..1db536664 100644 --- a/TestSuite/_MachineLearning/Regression/036_pipeline_predict_dense_encoded_labels.pas +++ b/TestSuite/_MachineLearning/Regression/036_pipeline_predict_dense_encoded_labels.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,8 +7,7 @@ begin var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); var pipe := - DataPipeline.Build( - TaskKind.tkClassification, + DataPipeline.BuildClassification( ds.Target, ds.Features, new StandardScaler, @@ -30,3 +29,4 @@ begin Check((pi >= 0) and (pi < classes.Length), $'Predict[{i}] out of dense class range'); end; end. + diff --git a/TestSuite/_MachineLearning/Regression/037_pipeline_predict_contract_matches_documented_semantics.pas b/TestSuite/_MachineLearning/Regression/037_pipeline_predict_contract_matches_documented_semantics.pas index 7deba4649..50526daad 100644 --- a/TestSuite/_MachineLearning/Regression/037_pipeline_predict_contract_matches_documented_semantics.pas +++ b/TestSuite/_MachineLearning/Regression/037_pipeline_predict_contract_matches_documented_semantics.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,8 +7,7 @@ begin var (trainDf, testDf) := df.TrainTestSplit(0.2, seed := 3); var pipe := - DataPipeline.Build( - TaskKind.tkClassification, + DataPipeline.BuildClassification( ds.Target, ds.Features, new StandardScaler, @@ -33,3 +32,4 @@ begin Check(predLabels[i] = classes[pi], $'PredictLabels[{i}] must decode Predict[{i}]'); end; end. + diff --git a/TestSuite/_MachineLearning/Regression/038_standalone_classifier_predict_contract_is_explicit.pas b/TestSuite/_MachineLearning/Regression/038_standalone_classifier_predict_contract_is_explicit.pas index 2f7e722e2..f3cd58782 100644 --- a/TestSuite/_MachineLearning/Regression/038_standalone_classifier_predict_contract_is_explicit.pas +++ b/TestSuite/_MachineLearning/Regression/038_standalone_classifier_predict_contract_is_explicit.pas @@ -1,4 +1,4 @@ -uses MLABC; +uses MLABC; uses TestHelpers in '..\TestHelpers.pas'; begin @@ -7,7 +7,7 @@ begin X[2,0] := 1.0; X[3,0] := 1.1; X[4,0] := 2.0; X[5,0] := 2.1; - var y := new Vector(Arr(10.0, 10.0, 20.0, 20.0, 30.0, 30.0)); + var y := Arr(10, 10, 20, 20, 30, 30); var model := new LogisticRegression; model.Fit(X, y); @@ -20,9 +20,8 @@ begin for var i := 0 to X.RowCount - 1 do begin - var pi := Round(pred[i]); + var pi := pred[i]; Check((pi = 10) or (pi = 20) or (pi = 30), $'Predict[{i}] must return original class label'); - Check((labels[i] >= 0) and (labels[i] < classes.Length), $'PredictLabels[{i}] out of range'); - Check(classes[labels[i]] = pi.ToString, $'PredictLabels[{i}] must point to Predict[{i}]'); + Check(labels[i] = pi.ToString, $'PredictLabels[{i}] must match Predict[{i}]'); end; end. diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index 1678ba1ed..273cb05d8 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -22,12 +22,15 @@ unit MLABC; // ============================================================= // ============================================================= -// PIPELINES +// КОНВЕЙЕРЫ // -// DataFrame-based: +// Табличные: // DataPipeline +// ClassificationDataPipeline +// RegressionDataPipeline +// ClusteringDataPipeline // -// Matrix/Vector-based: +// Матричные: // MatrixPipeline // ClassificationMatrixPipeline // RegressionMatrixPipeline @@ -37,6 +40,24 @@ unit MLABC; // для построения специализированных конвейеров. // ============================================================= +// ============================================================= +// ПОЛИТИКА PREDICT +// +// 1. Классификация: +// • Predict возвращает метки классов в том же виде, +// в каком они были поданы модели при обучении. +// • PredictLabels возвращает строковые метки классов. +// +// 2. Регрессия: +// • Predict возвращает вектор числовых предсказаний. +// +// 3. Кластеризация: +// • Predict и FitPredict возвращают номера кластеров. +// +// Внутреннее кодирование классов может использоваться внутри модели, +// но наружу через Predict оно не выдаётся. +// ============================================================= + interface uses LinearAlgebraML; @@ -68,6 +89,9 @@ type ConfusionMatrix = MetricsABC.ConfusionMatrix; DataPipeline = MLPipelineABC.DataPipeline; + ClassificationDataPipeline = MLPipelineABC.ClassificationDataPipeline; + RegressionDataPipeline = MLPipelineABC.RegressionDataPipeline; + ClusteringDataPipeline = MLPipelineABC.ClusteringDataPipeline; DataFrame = DataFrameABC.DataFrame; DataFrameCursor = DataFrameABCCore.DataFrameCursor; @@ -134,6 +158,7 @@ type Datasets = MLDatasets.Datasets; Dataset = MLDatasets.Dataset; LabelEncoder = MLDatasets.LabelEncoder; + TaskType = MLDatasets.TaskType; IModel = MLCoreABC.IModel; IUnsupervisedModel = MLCoreABC.IUnsupervisedModel; diff --git a/bin/Lib/MLCoreABC.pas b/bin/Lib/MLCoreABC.pas index 354243b51..689a20fc4 100644 --- a/bin/Lib/MLCoreABC.pas +++ b/bin/Lib/MLCoreABC.pas @@ -54,35 +54,45 @@ type property IsFitted: boolean read; end; - /// Интерфейс модели без учителя (Unsupervised Model). + /// Интерфейс модели без учителя. /// Наследуется от базового интерфейса IModel. /// Предназначен для алгоритмов, обучающихся только по признакам X - /// без использования целевых значений (y) + /// без использования целевых значений y. IUnsupervisedModel = interface(IModel) function Fit(X: Matrix): IUnsupervisedModel; end; - /// Интерфейс алгоритма кластеризации (unsupervised learning). + /// Интерфейс алгоритма кластеризации. /// Предназначен для моделей, разбивающих объекты на кластеры /// без использования целевой переменной. /// Основной сценарий использования — FitPredict, возвращающий - /// индекс кластера для каждого объекта обучающей выборки + /// номер кластера для каждого объекта обучающей выборки. + /// + /// Важно: + /// • номер кластера — это внутренний идентификатор алгоритма; + /// • он не связан с внешними метками классов, как в классификации; + /// • одинаковые по смыслу кластеры в разных запусках могут получить + /// разные номера. IClusterer = interface(IUnsupervisedModel) - // function PredictLabels(X: Matrix): array of integer; - - /// Выполняет кластеризацию данных и возвращает массив индексов кластеров + /// Выполняет кластеризацию данных и возвращает массив номеров кластеров /// (0,1,2,...) для каждого объекта. Значение -1 может использоваться /// для обозначения шума (например, в DBSCAN). + /// + /// Возвращаемые значения являются внутренними номерами кластеров, + /// а не внешними метками, заданными пользователем. function FitPredict(X: Matrix): array of integer; end; /// Интерфейс кластеризатора, поддерживающего предсказание для новых данных. - /// Используется для алгоритмов, задающих явное отображение X → cluster - /// (например, KMeans через ближайший центр кластера) + /// Используется для алгоритмов, задающих явное отображение X → кластер + /// (например, KMeans через ближайший центр кластера). IPredictiveClusterer = interface(IClusterer) - /// Возвращает внутренний индекс кластера для каждого объекта из X + /// Возвращает номер кластера для каждого объекта из X /// без повторного обучения модели. /// Требует предварительного вызова Fit или FitPredict. + /// + /// Возвращаемые значения являются внутренними номерами кластеров, + /// а не внешними метками классов. function Predict(X: Matrix): array of integer; end; diff --git a/bin/Lib/MLDatasets.pas b/bin/Lib/MLDatasets.pas index ce5e2f171..1705d82a1 100644 --- a/bin/Lib/MLDatasets.pas +++ b/bin/Lib/MLDatasets.pas @@ -351,7 +351,7 @@ const ER_VALUECOUNTS_ONLY_CLASSIFICATION = 'ValueCounts доступны только для задач классификации!!ValueCounts are only available for classification datasets'; ER_DATASET_TARGET_MISSING = - 'Target обязателен для задач обучения с учителем!!Target is required for supervised learning datasets'; + 'Target обязателен для задач с учителем!!Target is required for supervised learning datasets'; ER_PARAM_LE = 'Параметр {0} должен быть <= допустимого максимума!!Parameter {0} must be <= the allowed maximum value'; ER_PARAM_RANGE_01 = diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 5e49a18be..038cd899d 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -1366,9 +1366,10 @@ type /// Используется для анализа обучения. function PredictStageProba(X: Matrix; m: integer): Matrix; -/// Предсказывает классы по первым m итерациям бустинга. +/// Предсказывает метки классов по первым m итерациям бустинга. +/// Возвращает их в том же виде, в каком они были поданы модели при обучении. /// Удобно для построения learning curve. - function PredictStage(X: Matrix; m: integer): Vector; + function PredictStage(X: Matrix; m: integer): array of integer; /// Текущее количество деревьев в ансамбле. function TreeCount: integer := fEstimators.Count; @@ -1458,7 +1459,6 @@ type private // ==== classification state ==== fYEnc: array of integer; - fClasses: array of double; fClassCount: integer; fLabels: ClassifierLabelHelper; @@ -1594,7 +1594,7 @@ type /// • обученное состояние модели (центры кластеров) НЕ копируется методом Clone function Fit(X: Matrix): IUnsupervisedModel; - /// Возвращает внутренний индекс кластера для каждого объекта из X. + /// Возвращает номер кластера для каждого объекта из X. /// Требует предварительного вызова Fit. function Predict(X: Matrix): array of integer; @@ -1684,23 +1684,23 @@ type {$endregion Models} {$region MatrixPipeline} -/// Последовательный конвейер машинного обучения (supervised). +/// Общая база для матричных конвейеров с учителем. /// Гарантирует строгий порядок выполнения шагов: /// [преобразователи] → [модель]. /// /// Поддерживает: -/// • преобразователи без учёта целевой переменной (unsupervised); -/// • преобразователи с учётом целевой переменной (supervised); +/// • преобразователи без учёта целевой переменной; +/// • преобразователи с учётом целевой переменной; /// • одну финальную модель с учителем. /// /// Все преобразователи применяются последовательно к признакам X, /// после чего финальная модель обучается на (X, y). /// /// Обеспечивает единый интерфейс Fit(X, y) / Predict(X) -/// и воспроизводимость полного процесса обучения +/// и воспроизводимость полного процесса обучения. /// /// MatrixPipeline используется, когда данные уже представлены -/// в виде Matrix X и Vector y +/// в виде Matrix X и вектора y. MatrixPipeline = class(IModel) protected fTransformers: List; @@ -1775,8 +1775,8 @@ type /// Строит матричный конвейер для задачи классификации. /// Шаги указываются в порядке выполнения: -/// сначала преобразователи, затем модель. -/// Последний шаг должен быть моделью. + /// сначала преобразователи, затем модель. + /// Последний шаг должен быть моделью. static function BuildClassification(params steps: array of IPipelineStep): ClassificationMatrixPipeline; /// Строит матричный конвейер для задачи регрессии. @@ -1801,7 +1801,7 @@ type /// X — матрица m × n (m объектов, n признаков). /// y — вектор длины m с целевыми значениями. /// Последовательно обучает все трансформеры и применяет их к данным, -/// после чего обучает финальную supervised-модель. +/// после чего обучает финальную модель с учителем. /// /// Примечание: /// • обученное состояние пайплайна НЕ копируется методом Clone @@ -1855,47 +1855,36 @@ type function Fit(X: Matrix; y: Vector): IRegressor; reintroduce; end; -/// Последовательный конвейер машинного обучения (unsupervised). -/// Гарантирует строгий порядок выполнения шагов: -/// [преобразователи]. +/// Общая база для матричных конвейеров без учителя. /// -/// Поддерживает: -/// • преобразователи без учёта целевой переменной (unsupervised). -/// -/// Все преобразователи применяются последовательно к признакам X. -/// -/// Обеспечивает единый интерфейс Fit(X) / Transform(X) -/// и воспроизводимость полного процесса преобразования данных. -/// -/// Используется, когда данные уже представлены -/// в виде Matrix X и отсутствует целевая переменная +/// Поддерживает последовательность матричных преобразователей +/// и одну финальную модель без учителя. +/// Используется как базовый класс для специализированных +/// матричных конвейеров, например ClusteringMatrixPipeline. UnsupervisedMatrixPipelineBase = class(IUnsupervisedModel) protected fTransformers: List; fModel: IModel; fFitted: boolean; public - /// Создаёт конвейер машинного обучения для заданной модели: -/// • model — модель, которая будет применена -/// после последовательного применения всех преобразователей. -/// Модель должна реализовывать интерфейс IModel (без учёта целевой переменной) + /// Создаёт конвейер машинного обучения для заданной модели. + /// Модель применяется после последовательного выполнения всех преобразователей. constructor Create(model: IModel); -/// Создаёт пустой пайплайн (конвейер машинного обучения). -/// Модель должна быть установлена через SetModel + /// Создаёт пустой конвейер. + /// Модель должна быть установлена позже через SetModel. constructor Create; -/// Строит конвейер машинного обучения из последовательности шагов. -/// Шаги указываются в порядке выполнения: -/// сначала преобразователи, затем модель. -/// Последний шаг обязан быть моделью (IModel). -/// Возвращает сконструированный конвейер. + /// Строит матричный конвейер для задачи кластеризации. + /// Шаги указываются в порядке выполнения: + /// сначала преобразователи, затем модель. + /// Последний шаг должен быть моделью. static function Build(params steps: array of IPipelineStep): ClusteringMatrixPipeline; -/// Устанавливает или заменяет модель. + /// Устанавливает или заменяет модель. function SetModel(m: IModel): UnsupervisedMatrixPipelineBase; -/// Добавляет преобразование в конец пайплайна + /// Добавляет преобразование в конец пайплайна. function Add(t: ITransformer): UnsupervisedMatrixPipelineBase; /// Обучает пайплайн на данных. @@ -1938,6 +1927,12 @@ type function Name: string := Self.GetType.Name; end; + /// Матричный конвейер для задачи кластеризации. + /// FitPredict работает с любым кластеризатором. + /// Predict для новых данных доступен только тогда, + /// когда финальная модель реализует IPredictiveClusterer + /// (например, KMeans). + /// Для алгоритмов вроде DBSCAN используйте FitPredict. ClusteringMatrixPipeline = class(UnsupervisedMatrixPipelineBase) end; @@ -2093,7 +2088,7 @@ type /// Трансформер, удаляющий признаки с малой дисперсией. /// Удаляет столбцы X_j, для которых Var(X_j) < threshold. -/// Не использует целевую переменную (unsupervised) +/// Не использует целевую переменную. VarianceThreshold = class(IUnsupervisedTransformer) private fThreshold: real; @@ -2465,8 +2460,8 @@ const 'Метки классов недоступны. Убедитесь, что модель обучена и метки установлены!!Class labels are not available. Ensure the model is fitted and class labels are set'; ER_LABEL_INDEX_OUT_OF_RANGE = 'Индекс метки {0} вне диапазона [0, {1})!!Label index {0} is out of range [0, {1})'; - ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL = - 'Последний шаг Pipeline должен быть supervised-моделью!!' + + ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL = + 'Последний шаг Pipeline должен быть моделью с учителем!!' + 'Last Pipeline step must be a supervised model'; ER_INTERNAL_INVALID_MODEL_CLONE = 'Внутренняя ошибка: Clone модели вернул несовместимый тип!!' + @@ -2636,7 +2631,6 @@ begin if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); - CheckYForFit(y); end; var m := X.RowCount; @@ -2761,7 +2755,6 @@ begin if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); - CheckYForFit(y); end; if X.RowCount = 0 then @@ -3306,7 +3299,6 @@ begin if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); - CheckYForFit(y); end; if X.RowCount = 0 then @@ -4758,7 +4750,6 @@ begin if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); - CheckYForFit(y); end; if X.RowCount = 0 then @@ -5992,7 +5983,6 @@ begin if MLConfig.ValidateFiniteInputs then begin CheckXForFit(XTrain); - CheckYForFit(yTrain); end; // --- shape checks --- @@ -6013,7 +6003,6 @@ begin if MLConfig.ValidateFiniteInputs then begin CheckXForPredict(XVal); - CheckYForFit(yVal); end; if XVal.RowCount <> yVal.Length then @@ -7123,12 +7112,12 @@ begin Result := probs; end; -function GradientBoostingClassifier.PredictStage(X: Matrix; m: integer): Vector; +function GradientBoostingClassifier.PredictStage(X: Matrix; m: integer): array of integer; begin var probs := PredictStageProba(X, m); var n := probs.RowCount; - var resultVec := new Vector(n); + SetLength(Result, n); for var i := 0 to n - 1 do begin @@ -7142,10 +7131,8 @@ begin best := cls; end; - resultVec[i] := fClasses[best]; + Result[i] := fClasses[best]; end; - - Result := resultVec; end; function GradientBoostingClassifier.FeatureImportances: Vector; @@ -7506,11 +7493,6 @@ begin if fClassCount < 2 then ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES); - // сохранить оригинальные метки (double API) - SetLength(fClasses, fClassCount); - for var i := 0 to fClassCount - 1 do - fClasses[i] := classesInt[i]; - if fLabels = nil then fLabels := new ClassifierLabelHelper; fLabels.SetClassValues(classesInt); @@ -7867,7 +7849,6 @@ begin if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); - CheckYForFit(y); end; fXTrain := X.Clone; @@ -8701,6 +8682,7 @@ begin ArgumentError(ER_EMPTY_DATASET); var Xt := X; + var yVec: Vector := nil; for var i := 0 to fTransformers.Count - 1 do begin @@ -8709,8 +8691,12 @@ begin if t = nil then ArgumentError(ER_PIPELINE_STEP_NULL); - if t is ISupervisedTransformer then - ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER) + if t is ISupervisedTransformer(var sup) then + begin + if yVec = nil then + yVec := new Vector(y); + fTransformers[i] := sup.Fit(Xt, yVec); + end else if t is IUnsupervisedTransformer(var unsup) then fTransformers[i] := unsup.Fit(Xt) else diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index b5ef20f31..d3f57cfc3 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -38,7 +38,7 @@ type /// Возвращает только те столбцы, которые реально присутствуют в current, /// без повторов и в итоговом порядке для построения матрицы признаков. /// - /// Если после preprocessing не осталось ни одного признака, + /// Если после предобработки не осталось ни одного признака, /// выбрасывается исключение function ResolveFinalFeatures(current: DataFrame): array of string; @@ -116,8 +116,8 @@ type /// Добавляет шаг в конец конвейера. /// Принимает: - /// • IPreprocessor (DataFrame-уровень) - /// • ITransformer (Matrix-уровень) + /// • IPreprocessor (табличный шаг) + /// • ITransformer (матричный шаг) /// • IModel (модель, должна быть последней) /// Запрещено добавлять шаги после вызова Fit. function Add(step: IPipelineStep): SupervisedDataPipelineBase; @@ -126,7 +126,7 @@ type /// /// Матричные шаги и модель при этом не используются. /// Метод предназначен в основном для просмотра и анализа промежуточного - /// результата preprocessing. + /// результата предобработки. /// /// Полученный DataFrame обычно не следует повторно подавать в тот же pipeline, /// так как DataFrame-преобразования будут выполнены ещё раз. @@ -169,22 +169,17 @@ type end; /// Общая база для табличных конвейеров без учителя. - /// - /// UnsupervisedDataPipelineBase — конвейер подготовки данных и обучения модели без учителя на DataFrame. - /// - /// Поддерживает два уровня шагов: - /// • DataFrame-уровень: IPreprocessor (Fit/Transform над DataFrame) - /// • Matrix-уровень: IUnsupervisedTransformer и IUnsupervisedModel - /// (после преобразования DataFrame → Matrix) /// - /// Правила порядка шагов: - /// • Сначала идут только DataFrame-шаги (IPreprocessor). - /// • Затем — матричные шаги (IUnsupervisedTransformer). - /// • Модель (IUnsupervisedModel) добавляется последней и может быть только одна. + /// Поддерживает два типа шагов: + /// • табличные преобразователи; + /// • матричные преобразователи и финальную модель без учителя. /// - /// В конвейере используются: - /// • features — признаки (целевая переменная отсутствует). + /// Порядок шагов строгий: + /// • сначала идут табличные преобразователи; + /// • затем матричные преобразователи; + /// • модель без учителя добавляется последней и может быть только одна. /// + /// Конвейер работает только с признаками. UnsupervisedDataPipelineBase = class(PipelineBase, IModel) protected fModel: IUnsupervisedModel; @@ -202,8 +197,8 @@ type /// Добавляет шаг в конец конвейера. /// Принимает: - /// • IPreprocessor (DataFrame-уровень) - /// • ITransformer (Matrix-уровень) + /// • IPreprocessor (табличный шаг) + /// • ITransformer (матричный шаг) /// • IUnsupervisedModel (модель, должна быть последней) function Add(step: IPipelineStep): UnsupervisedDataPipelineBase; @@ -211,7 +206,7 @@ type /// /// Матричные шаги и модель при этом не используются. /// Метод предназначен в основном для просмотра и анализа промежуточного - /// результата preprocessing. + /// результата предобработки. /// /// Полученный DataFrame обычно не следует повторно подавать в тот же pipeline, /// так как DataFrame-преобразования будут выполнены ещё раз. @@ -226,7 +221,7 @@ type end; /// Табличный конвейер для задачи кластеризации. - /// Predict возвращает внутренние индексы кластеров. + /// Predict возвращает номера кластеров. ClusteringDataPipeline = class(UnsupervisedDataPipelineBase) public function WithModel(model: IUnsupervisedModel): ClusteringDataPipeline; @@ -234,7 +229,7 @@ type /// Обучает конвейер и сразу возвращает метки кластеров. function FitPredict(df: DataFrame): array of integer; /// Делает предсказание кластеров для объектов из DataFrame. - /// Возвращает внутренние индексы кластеров. + /// Возвращает номера кластеров. function Predict(df: DataFrame): array of integer; end; @@ -280,9 +275,11 @@ const ER_PIPELINE_FINALFEATURES = 'Внутренняя ошибка pipeline: итоговый набор признаков не определён. Возможно, Fit не был выполнен корректно!!Pipeline internal error: final feature set is not defined. Fit may not have been executed correctly'; ER_PIPELINE_TARGET_REMOVED = - 'Целевая переменная "{0}" была удалена на этапе preprocessing pipeline!!Target column "{0}" was removed during pipeline preprocessing'; + 'Целевая переменная "{0}" была удалена на этапе предобработки конвейера!!' + + 'Target column "{0}" was removed during pipeline preprocessing'; ER_PIPELINE_NO_FEATURES = - 'После preprocessing pipeline не осталось признаков для обучения модели!!No features remain after pipeline preprocessing'; + 'После предобработки конвейера не осталось признаков для обучения модели!!' + + 'No features remain after pipeline preprocessing'; ER_MATRIXSTEP_NO_FIT = 'Шаг матричного конвейера #{0} не поддерживает Fit!!Matrix step #{0} does not support Fit'; ER_Model_NoFit = @@ -307,7 +304,8 @@ const ER_LABEL_INDEX_OUT_OF_RANGE = 'Индекс метки {0} вне диапазона [0, {1})!!Label index {0} is out of range [0, {1})'; ER_ORDINALENCODER_TARGET_NOT_ALLOWED = - 'OrdinalEncoder нельзя применять к целевой переменной — кодирование выполняется внутри модели!!OrdinalEncoder cannot be applied to target — encoding is handled internally by the model'; + 'OrdinalEncoder нельзя применять к целевой переменной: её кодирование выполняется внутри модели!!' + + 'OrdinalEncoder cannot be applied to the target because target encoding is handled inside the model'; ER_ENCODELABELS_NOT_CATEGORICAL = 'Целевой столбец должен быть категориальным для задач классификации!!Target column must be categorical for classification tasks'; ER_CLASSIFICATION_TARGET_MUST_BE_CATEGORICAL_STR_OR_INT = @@ -327,13 +325,13 @@ const 'Pipeline не содержит шагов!!' + 'Pipeline must contain at least one step'; ER_PIPELINE_LAST_NOT_UNSUPERVISED_MODEL = - 'Последний шаг Pipeline должен быть unsupervised-моделью!!' + + 'Последний шаг Pipeline должен быть моделью без учителя!!' + 'Last Pipeline step must be an unsupervised model'; ER_PIPELINE_INVALID_STEP_ORDER = 'Неверный порядок шагов в Pipeline: модель должна быть последней!!' + 'Invalid pipeline step order: model must be the last step'; ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL = - 'Последний шаг Pipeline должен быть supervised-моделью!!' + + 'Последний шаг Pipeline должен быть моделью с учителем!!' + 'Last Pipeline step must be a supervised model'; ER_MODEL_NOT_CLASSIFIER = 'Модель не является классификатором!!' +