From 69f90b366643675b25a75118a85f1be782135e5b Mon Sep 17 00:00:00 2001 From: Mikhalkovich Stanislav Date: Sat, 4 Apr 2026 13:55:22 +0300 Subject: [PATCH] =?UTF-8?q?ML=20Pipeline=20-=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D1=80=D1=85?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/Lib/MLCoreABC.pas | 37 ++++-- bin/Lib/MLModelsABC.pas | 60 +++++---- bin/Lib/MLPipelineABC.pas | 257 ++++++++++++++++++++++++++++---------- 3 files changed, 255 insertions(+), 99 deletions(-) diff --git a/bin/Lib/MLCoreABC.pas b/bin/Lib/MLCoreABC.pas index 8bf43522f..85110869d 100644 --- a/bin/Lib/MLCoreABC.pas +++ b/bin/Lib/MLCoreABC.pas @@ -49,17 +49,23 @@ type /// Базовый интерфейс модели машинного обучения IModel = interface(IMatrixStep) - function Predict(X: Matrix): Vector; + // function Predict(X: Matrix): Vector; // вынес в IPredictiveModel function Clone: IModel; function Name: string; property IsFitted: boolean read; end; + /// Модель, реализующая отображение X → y (или аналогичный результат). + /// Поддерживает предсказание для новых данных + IPredictiveModel = interface(IModel) + function Predict(X: Matrix): Vector; + end; + /// Интерфейс модели с учителем (Supervised Model). /// Наследуется от базового интерфейса IModel. /// Предназначен для алгоритмов, обучающихся по признакам X /// с использованием целевых значений (y) - ISupervisedModel = interface(IModel) + ISupervisedModel = interface(IPredictiveModel) function Fit(X: Matrix; y: Vector): ISupervisedModel; end; @@ -71,14 +77,30 @@ type function Fit(X: Matrix): IUnsupervisedModel; end; - /// Интерфейс алгоритма кластеризации. - /// Наследуется от IUnsupervisedModel. + /// Интерфейс алгоритма кластеризации (unsupervised learning). /// Предназначен для моделей, разбивающих объекты на кластеры - /// и возвращающих индекс кластера для каждого объекта + /// без использования целевой переменной. + /// Основной сценарий использования — FitPredict, возвращающий + /// индекс кластера для каждого объекта обучающей выборки IClusterer = interface(IUnsupervisedModel) - function PredictLabels(X: Matrix): array of integer; + // function PredictLabels(X: Matrix): array of integer; + + /// Выполняет кластеризацию данных и возвращает массив индексов кластеров + /// (0,1,2,...) для каждого объекта. Значение -1 может использоваться + /// для обозначения шума (например, в DBSCAN). + function FitPredict(X: Matrix): array of integer; end; + /// Интерфейс кластеризатора, поддерживающего предсказание для новых данных. + /// Используется для алгоритмов, задающих явное отображение X → cluster + /// (например, KMeans через ближайший центр кластера) + IPredictiveClusterer = interface(IClusterer, IPredictiveModel) + /// Возвращает индекс кластера для каждого объекта из X + /// без повторного обучения модели. + /// Требует предварительного вызова Fit или FitPredict. + function PredictLabels(X: Matrix): array of integer; + end; + /// Интерфейс древовидной модели машинного обучения ITreeModel = interface(ISupervisedModel) function FeatureImportances: Vector; @@ -107,9 +129,6 @@ type /// Возвращает матрицу вероятностей размера (nSamples × nClasses). /// Столбцы соответствуют классам в порядке внутреннего кодирования модели function PredictProba(X: Matrix): Matrix; - - /// Возвращает массив меток классов в порядке столбцов PredictProba. - function GetClasses: array of real; end; /// Интерфейс регрессионной модели. diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 2201afe6b..29fc0ad99 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -719,7 +719,7 @@ type /// Строит ансамбль классификационных деревьев, обученных на /// bootstrap-подвыборках объектов и случайных подмножествах признаков. /// Итоговое предсказание формируется голосованием деревьев или агрегацией вероятностей классов - RandomForestClassifier = class(RandomForestBase, IClassifier, IProbabilisticClassifier) + RandomForestClassifier = class(RandomForestBase, IProbabilisticClassifier) private fTrees: array of DecisionTreeClassifier; fIndexToClass: array of integer; @@ -1125,7 +1125,7 @@ type /// Базовый абстрактный класс для алгоритма k ближайших соседей (kNN). /// Реализует общий механизм поиска k ближайших объектов, /// но не определяет способ агрегации (классификация или регрессия) - KNNBase = abstract class(IModel) + KNNBase = abstract class(IPredictiveModel) protected // ==== train state ==== fXTrain: Matrix; @@ -1255,7 +1255,7 @@ type /// Модель кластеризации методом k-средних (KMeans). /// Разбивает объекты на k кластеров на основе евклидова расстояния. /// Реализует алгоритм без учителя - KMeans = class(IClusterer) + KMeans = class(IPredictiveClusterer) private fNClusters: integer; fMaxIter: integer; @@ -1300,7 +1300,7 @@ type function PredictLabels(X: Matrix): array of integer; /// Выполняет обучение и сразу возвращает метки кластеров. - function FitPredict(X: Matrix): Vector; + function FitPredict(X: Matrix): array of integer; /// Создаёт глубокую копию модели. function Clone: IModel; @@ -1405,13 +1405,13 @@ type Pipeline = class(ISupervisedModel) private fTransformers: List; - fModel: IModel; + fModel: ISupervisedModel; fFitted: boolean; public /// Создаёт конвейер машинного обучения для заданной модели: /// • model — модель, которая будет обучена /// после последовательного применения всех преобразователей. - constructor Create(model: IModel); + constructor Create(model: ISupervisedModel); /// Создаёт пустой пайплайн (конвейер машинного обучения). /// Модель должна быть установлена через SetModel. @@ -1482,7 +1482,7 @@ type static function Build(params steps: array of IPipelineStep): Pipeline; /// Устанавливает или заменяет модель. - function SetModel(m: IModel): Pipeline; + function SetModel(m: ISupervisedModel): Pipeline; /// Добавляет преобразование в конец пайплайна function Add(t: ITransformer): Pipeline; @@ -1501,8 +1501,6 @@ type /// для вероятностной модели в конце пайплайна. function PredictProba(X: Matrix): Matrix; - function GetClasses: array of real; - /// Показывает, был ли пайплайн обучен (вызван метод Fit). property IsFitted: boolean read fFitted; @@ -2011,6 +2009,16 @@ const 'DBSCAN не поддерживает предсказание для новых данных!!DBSCAN does not support prediction for new data'; ER_CLASSES_NOT_AVAILABLE = 'Метки классов недоступны. Убедитесь, что модель обучена и метки установлены!!Class labels are not available. Ensure the model is fitted and class labels are set'; + ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL = + 'Последний шаг Pipeline должен быть supervised-моделью!!' + + 'Last Pipeline step must be a supervised model'; + 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'; + {$endregion ErrConstants} @@ -7239,10 +7247,10 @@ begin Result[i] := labels[i]; end; -function KMeans.FitPredict(X: Matrix): Vector; +function KMeans.FitPredict(X: Matrix): array of integer; begin Fit(X); - Result := Predict(X); + Result := PredictLabels(X); end; function KMeans.Clone: IModel; @@ -7477,7 +7485,7 @@ begin fFitted := false; end; -constructor Pipeline.Create(model: IModel); +constructor Pipeline.Create(model: ISupervisedModel); begin Create; if model = nil then @@ -7496,10 +7504,10 @@ begin if last = nil then ArgumentError(ER_PIPELINE_STEP_NULL, High(steps)); - if not (last is IModel) then - ArgumentError(ER_PIPELINE_LAST_NOT_MODEL); + if not (last is ISupervisedModel) then + ArgumentError(ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL); - var pipe := new Pipeline(last as IModel); + var pipe := new Pipeline(last as ISupervisedModel); // все шаги кроме последнего — трансформеры for var i := 0 to High(steps) - 1 do @@ -7527,7 +7535,7 @@ begin Result := Self; end; -function Pipeline.SetModel(m: IModel): Pipeline; +function Pipeline.SetModel(m: ISupervisedModel): Pipeline; begin if m = nil then ArgumentError(ER_MODEL_NULL); @@ -7637,14 +7645,6 @@ begin .PredictProba(Xt); end; -function Pipeline.GetClasses: array of real; -begin - if not (fModel is IProbabilisticClassifier) then - ArgumentError(ER_PROBA_NOT_SUPPORTED); - - Result := (fModel as IProbabilisticClassifier).GetClasses; -end; - function Pipeline.ToString: string; begin var sb := 'Pipeline (' + @@ -7674,7 +7674,12 @@ begin foreach var t in fTransformers do p.Add(t.Clone); - p.SetModel(fModel.Clone); + var m := fModel.Clone; + + if not (m is ISupervisedModel) then + Error(ER_INTERNAL_INVALID_MODEL_CLONE); + + p.SetModel(m as ISupervisedModel); p.fFitted := fFitted; @@ -7822,7 +7827,10 @@ begin var Xt := Transform(X); - Result := (fModel as IUnsupervisedModel).Predict(Xt); + if not (fModel is IPredictiveModel) then + Error(ER_PREDICT_NOT_SUPPORTED); + + Result := (fModel as IPredictiveModel).Predict(Xt); end; function UPipeline.ToString: string; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index c137a2cdb..dcad2df8b 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -32,7 +32,7 @@ type private fDataSteps: List; fMatrixSteps: List; - fModel: IModel; + fModel: ISupervisedModel; fTask: TaskKind; fTarget: string; @@ -50,7 +50,7 @@ type /// Принимает: /// • IPreprocessor (DataFrame-уровень) /// • ITransformer (Matrix-уровень) - /// • IModel (модель, должна быть последней) + /// • ISupervisedModel (модель, должна быть последней) /// Запрещено добавлять шаги после вызова Fit/FitTransform. function Add(step: IPipelineStep): DataPipeline; @@ -204,7 +204,7 @@ type private fDataSteps: List; fMatrixSteps: List; - fModel: IModel; + fModel: IUnsupervisedModel; fFeatures: array of string; fFinalFeatures: array of string; @@ -222,7 +222,7 @@ type /// Принимает: /// • IPreprocessor (DataFrame-уровень) /// • ITransformer (Matrix-уровень) - /// • IModel (модель, должна быть последней) + /// • IUnsupervisedModel (модель, должна быть последней) function Add(step: IPipelineStep): UDataPipeline; /// Строит unsupervised-конвейер из шагов обработки данных и модели. @@ -353,8 +353,27 @@ const ER_PIPELINE_TARGET_TRANSFORM_NOT_ALLOWED = 'Преобразование целевой переменной "{0}" запрещено в DataPipeline!!' + 'Transformation of target variable "{0}" is not allowed in DataPipeline'; - - + 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'; + ER_PIPELINE_LAST_NOT_UNSUPERVISED_MODEL = + 'Последний шаг Pipeline должен быть unsupervised-моделью!!' + + '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-моделью!!' + + 'Last Pipeline step must be a supervised model'; + ER_MODEL_NOT_CLASSIFIER = + 'Модель не является классификатором!!' + + 'Model is not a classifier'; //----------------------------- // DataPipeline //----------------------------- @@ -412,12 +431,12 @@ begin end; // --- Model (обязательно последний шаг) - if step is IModel then + if step is ISupervisedModel then begin if fModel <> nil then ArgumentError(ER_PIPELINE_MULTIPLE_MODELS); - fModel := step as IModel; + fModel := step as ISupervisedModel; exit(Self); end; @@ -432,6 +451,9 @@ class function DataPipeline.Build( params steps: array of IPipelineStep ): DataPipeline; begin + if not (task in [TaskKind.tkClassification, TaskKind.tkRegression]) then + ArgumentError(ER_DATAPIPE_INVALID_TASK); + if (target = nil) or (target = '') then ArgumentError(ER_TARGET_EMPTY); @@ -454,14 +476,48 @@ begin seen.Add(f); end; + if (steps = nil) or (Length(steps) = 0) then + ArgumentError(ER_PIPELINE_NO_STEPS); + + var last := steps[High(steps)]; + + if last = nil then + ArgumentError(ER_PIPELINE_STEP_NULL, High(steps)); + + if not (last is ISupervisedModel) then + ArgumentError(ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL); + + var modelFound := false; + + for var i := 0 to High(steps) do + begin + if steps[i] is ISupervisedModel then + begin + if modelFound then + ArgumentError(ER_PIPELINE_MULTIPLE_MODELS); + + modelFound := true; + end; + end; + + for var i := 0 to High(steps) - 1 do + begin + var step := steps[i]; + + if step = nil then + ArgumentError(ER_PIPELINE_STEP_NULL, i); + + if step is ISupervisedModel then + ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER); + end; + var p := new DataPipeline; p.fTarget := target; p.fFeatures := Copy(features); p.fTask := task; - if steps <> nil then - for var i := 0 to High(steps) do - p.Add(steps[i]); + for var i := 0 to High(steps) do + p.Add(steps[i]); Result := p; end; @@ -546,13 +602,15 @@ begin end; // --- 4) модель - if fModel is ISupervisedModel(var supModel) then - fModel := supModel.Fit(X, y) - else ArgumentError(ER_MODEL_NOT_SUPERVISED, fModel.GetType.Name); + fModel := fModel.Fit(X, y); if fTask = tkClassification then + begin if fModel is IClassifier(var cls) then - cls.SetClassLabels(classes); + cls.SetClassLabels(classes) + else + Error(ER_MODEL_NOT_CLASSIFIER); + end; fFitted := true; Result := Self; @@ -614,8 +672,11 @@ begin foreach var t in fMatrixSteps do X := t.Transform(X); + + if not (fModel is IPredictiveModel) then + Error(ER_PREDICT_NOT_SUPPORTED); - Result := fModel.Predict(X); + Result := (fModel as IPredictiveModel).Predict(X); end; function DataPipeline.PredictProba(df: DataFrame): Matrix; @@ -625,6 +686,9 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); + if fTask <> tkClassification then + ArgumentError(ER_NOT_CLASSIFICATION); + if not (fModel is IProbabilisticClassifier) then ArgumentError(ER_PROBA_NOT_SUPPORTED); @@ -633,9 +697,6 @@ begin if fFinalFeatures = nil then Error(ER_PIPELINE_FINALFEATURES); - if fTask <> tkClassification then - ArgumentError(ER_NOT_CLASSIFICATION); - for var i := 0 to High(fFinalFeatures) do if not current.HasColumn(fFinalFeatures[i]) then ArgumentError(ER_PIPELINE_FEATURE_NOT_FOUND, fFinalFeatures[i]); @@ -779,12 +840,12 @@ begin end; // --- Model (обязательно последний шаг) - if step is IModel then + if step is IUnsupervisedModel then begin if fModel <> nil then ArgumentError(ER_PIPELINE_MULTIPLE_MODELS); - fModel := step as IModel; + fModel := step as IUnsupervisedModel; exit(Self); end; @@ -811,8 +872,30 @@ begin seen.Add(f); end; + if (steps = nil) or (Length(steps) = 0) then + ArgumentError(ER_PIPELINE_NO_STEPS); + + var last := steps[High(steps)]; + + if last = nil then + ArgumentError(ER_PIPELINE_STEP_NULL, High(steps)); + + if not (last is IUnsupervisedModel) then + ArgumentError(ER_PIPELINE_LAST_NOT_UNSUPERVISED_MODEL); + + for var i := 0 to High(steps) - 1 do + begin + var step := steps[i]; + + if step = nil then + ArgumentError(ER_PIPELINE_STEP_NULL, i); + + if step is IUnsupervisedModel then + ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER); + end; + var p := new UDataPipeline; - p.fFeatures := features; + p.fFeatures := Copy(features); for var i := 0 to High(steps) do p.Add(steps[i]); @@ -878,10 +961,7 @@ begin end; // --- 4) модель - if fModel is IUnsupervisedModel(var unsupModel) then - fModel := unsupModel.Fit(X) - else - ArgumentError(ER_MODEL_NOT_UNSUPERVISED, fModel.GetType.Name); + fModel := fModel.Fit(X); fFitted := true; Result := Self; @@ -931,8 +1011,62 @@ begin if not (fModel is IClusterer) then ArgumentError(ER_MODEL_NOT_CLUSTERER, fModel.GetType.Name); - Fit(df); - Result := PredictLabels(df); + var current := df; + + // --- 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 + for var i := 0 to fMatrixSteps.Count - 1 do + begin + var t := fMatrixSteps[i]; + + if not (t is IUnsupervisedTransformer) then + ArgumentError(ER_MATRIXSTEP_NO_FIT, i, t.GetType.Name); + + var unsup := t as IUnsupervisedTransformer; + fMatrixSteps[i] := unsup.Fit(X); + + X := fMatrixSteps[i].Transform(X); + end; + + // --- 4) модель + var cl := fModel as IClusterer; + Result := cl.FitPredict(X); + + fFitted := true; end; procedure UDataPipeline.ValidateNumericFeatures(df: DataFrame); @@ -967,6 +1101,13 @@ begin ValidateNumericFeatures(current); + if fFinalFeatures = nil then + Error(ER_PIPELINE_FINALFEATURES); + + for var i := 0 to High(fFinalFeatures) do + if not current.HasColumn(fFinalFeatures[i]) then + ArgumentError(ER_PIPELINE_FEATURE_NOT_FOUND, fFinalFeatures[i]); + var X := current.ToMatrix(fFinalFeatures); foreach var t in fMatrixSteps do @@ -976,56 +1117,44 @@ begin end; function UDataPipeline.Predict(df: DataFrame): Vector; -begin - if fModel = nil then - ArgumentError(ER_MODEL_NULL); - - var X := TransformToMatrix(df); - - Result := fModel.Predict(X); -end; - -function UDataPipeline.PredictLabels(df: DataFrame): array of integer; -begin - if fModel = nil then - ArgumentError(ER_MODEL_NULL); - - if not (fModel is IClusterer) then - ArgumentError(ER_MODEL_NOT_CLUSTERER, fModel.GetType.Name); - - var cl := fModel as IClusterer; - - var X := TransformToMatrix(df); - - Result := cl.PredictLabels(X); -end; - -{function UDataPipeline.Predict(df: DataFrame): Vector; begin if df = nil then ArgumentNullError(ER_ARG_NULL, 'df'); + if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); if fModel = nil then ArgumentError(ER_MODEL_NULL); - var current := Transform(df); + var X := TransformToMatrix(df); - if fFinalFeatures = nil then - Error(ER_PIPELINE_FINALFEATURES); + if not (fModel is IPredictiveModel) then + Error(ER_PREDICT_NOT_SUPPORTED); - for var i := 0 to High(fFinalFeatures) do - if not current.HasColumn(fFinalFeatures[i]) then - ArgumentError(ER_PIPELINE_FEATURE_NOT_FOUND, fFinalFeatures[i]); + Result := (fModel as IPredictiveModel).Predict(X); +end; - var X := current.ToMatrix(fFinalFeatures); +function UDataPipeline.PredictLabels(df: DataFrame): array of integer; +begin + if df = nil then + ArgumentNullError(ER_ARG_NULL, 'df'); - foreach var t in fMatrixSteps do - X := t.Transform(X); + if not fFitted then + NotFittedError(ER_FIT_NOT_CALLED); - Result := fModel.Predict(X); -end;} + if fModel = nil then + ArgumentError(ER_MODEL_NULL); + + if not (fModel is IPredictiveClusterer) then + Error(ER_PREDICT_NOT_SUPPORTED); + + var cl := fModel as IPredictiveClusterer; + + var X := TransformToMatrix(df); + + Result := cl.PredictLabels(X); +end; procedure UDataPipeline.ValidateSchema(df: DataFrame); begin