pascalabcnet/bin/Lib/MLPipelineABC.pas
Mikhalkovich Stanislav 828484f15a Новые ML примеры и тесты - в основном в DataFrame
EncodeLabels - убрана проверка категориальности, добавлен тип Bool
В DataFrame добавлен тип DateTime
Добавлен тип DataValue позволяющий писать df.Filter(r -> r['Col'] > 5)
2026-06-19 22:06:08 +03:00

1378 lines
44 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

unit MLPipelineABC;
interface
uses MLCoreABC;
uses PreprocessorABC;
uses DataFrameABC;
uses LinearAlgebraML;
type
TaskKind = (tkRegression, tkClassification);
type
DataPipeline = class;
SupervisedDataPipelineBase = class;
UnsupervisedDataPipelineBase = class;
ClassificationDataPipeline = class;
RegressionDataPipeline = class;
ClusteringDataPipeline = class;
PipelineBase = abstract class
protected
fDataSteps: List<IPreprocessor>;
fMatrixSteps: List<ITransformer>;
fFeatures: array of string;
fFinalFeatures: array of string;
fFitted: boolean;
procedure ValidateSchema(df: DataFrame); virtual; abstract;
class procedure ValidateFeatureList(features: array of string);
/// Вычисляет итоговый список признаков после всех DataFrame-преобразований.
///
/// Для каждого исходного признака из fFeatures:
/// • пытается получить расширенные столбцы через IColumnExpander;
/// • если расширения нет, использует сам исходный столбец.
///
/// Возвращает только те столбцы, которые реально присутствуют в current,
/// без повторов и в итоговом порядке для построения матрицы признаков.
///
/// Если после предобработки не осталось ни одного признака,
/// выбрасывается исключение
function ResolveFinalFeatures(current: DataFrame): array of string;
function PrepareMatrix(df: DataFrame; var current: DataFrame): Matrix;
function TransformDataFrame(df: DataFrame): DataFrame;
function FitTransformMatrix(var X: Matrix; y: Vector := nil): Matrix;
function TransformMatrix(X: Matrix): Matrix;
public
constructor Create;
/// Признак того, что был вызван Fit.
property IsFitted: boolean read fFitted;
end;
/// Фасад для построения табличных конвейеров.
DataPipeline = class
public
/// Строит табличный конвейер для задачи классификации.
static function BuildClassification(
target: string;
features: array of string;
params steps: array of IPipelineStep
): ClassificationDataPipeline;
/// Строит табличный конвейер для задачи регрессии.
static function BuildRegression(
target: string;
features: array of string;
params steps: array of IPipelineStep
): RegressionDataPipeline;
/// Строит табличный конвейер для задачи кластеризации.
static function BuildClustering(
features: array of string;
params steps: array of IPipelineStep
): ClusteringDataPipeline;
/// Строит только часть предварительной обработки для задачи классификации.
static function BuildClassificationPreprocessing(
target: string;
features: array of string;
params steps: array of IPipelineStep
): ClassificationDataPipeline;
/// Строит только часть предварительной обработки для задачи регрессии.
static function BuildRegressionPreprocessing(
target: string;
features: array of string;
params steps: array of IPipelineStep
): RegressionDataPipeline;
/// Строит только часть предварительной обработки для задачи кластеризации.
static function BuildClusteringPreprocessing(
features: array of string;
params steps: array of IPipelineStep
): ClusteringDataPipeline;
end;
/// Общая база для табличных конвейеров с учителем.
SupervisedDataPipelineBase = class(PipelineBase, IModel)
protected
fModel: IModel;
fTask: TaskKind;
fTarget: string;
procedure ValidateSchema(df: DataFrame); override;
function WithModelCore(model: IModel): SupervisedDataPipelineBase;
function FitCore(df: DataFrame): SupervisedDataPipelineBase;
function PredictCore(df: DataFrame): Vector;
public
/// Создаёт пустой конвейер
constructor Create;
/// Добавляет шаг в конец конвейера.
/// Принимает:
/// • IPreprocessor (табличный шаг)
/// • ITransformer (матричный шаг)
/// • IModel (модель, должна быть последней)
/// Запрещено добавлять шаги после вызова Fit.
function Add(step: IPipelineStep): SupervisedDataPipelineBase;
/// Применяет только обученные DataFrame-шаги pipeline и возвращает новый DataFrame.
///
/// Матричные шаги и модель при этом не используются.
/// Метод предназначен в основном для просмотра и анализа промежуточного
/// результата предобработки.
///
/// Полученный DataFrame обычно не следует повторно подавать в тот же pipeline,
/// так как DataFrame-преобразования будут выполнены ещё раз.
function Transform(df: DataFrame): DataFrame;
function ToString: string; override;
function Name: string := Self.GetType.Name;
function Clone: IModel;
end;
/// Табличный конвейер для задачи классификации.
/// Predict возвращает метки классов в том же виде,
/// в каком они были поданы модели при обучении.
/// PredictLabels возвращает исходные строковые метки классов.
ClassificationDataPipeline = class(SupervisedDataPipelineBase)
public
function WithModel(model: IClassifier): ClassificationDataPipeline;
function Fit(df: DataFrame): ClassificationDataPipeline;
/// Возвращает метки классов для объектов из DataFrame
/// в том же виде, в каком они были поданы модели при обучении.
function Predict(df: DataFrame): array of integer;
/// Возвращает строковые метки классов для объектов из DataFrame.
function PredictLabels(df: DataFrame): array of string;
function PredictProba(df: DataFrame): Matrix;
function GetEncodedLabels(df: DataFrame): array of integer;
function GetClassLabels: array of string;
end;
/// Табличный конвейер для задачи регрессии.
/// Predict возвращает числовой вектор предсказаний.
RegressionDataPipeline = class(SupervisedDataPipelineBase)
public
function WithModel(model: IRegressor): RegressionDataPipeline;
function Fit(df: DataFrame): RegressionDataPipeline;
/// Делает предсказание числовых значений для объектов из DataFrame.
/// Доступен после обучения конвейера (Fit).
function Predict(df: DataFrame): Vector;
end;
/// Общая база для табличных конвейеров без учителя.
///
/// Поддерживает два типа шагов:
/// • табличные преобразователи;
/// • матричные преобразователи и финальную модель без учителя.
///
/// Порядок шагов строгий:
/// • сначала идут табличные преобразователи;
/// • затем матричные преобразователи;
/// • модель без учителя добавляется последней и может быть только одна.
///
/// Конвейер работает только с признаками.
UnsupervisedDataPipelineBase = class(PipelineBase, IModel)
protected
fModel: IUnsupervisedModel;
procedure ValidateNumericFeatures(df: DataFrame);
function TransformToMatrix(df: DataFrame): Matrix;
procedure ValidateSchema(df: DataFrame); override;
function WithModelCore(model: IUnsupervisedModel): UnsupervisedDataPipelineBase;
function FitCore(df: DataFrame): UnsupervisedDataPipelineBase;
function FitPredictCore(df: DataFrame): array of integer;
function PredictCore(df: DataFrame): array of integer;
public
/// Создаёт пустой конвейер
constructor Create;
/// Добавляет шаг в конец конвейера.
/// Принимает:
/// • IPreprocessor (табличный шаг)
/// • ITransformer (матричный шаг)
/// • IUnsupervisedModel (модель, должна быть последней)
function Add(step: IPipelineStep): UnsupervisedDataPipelineBase;
/// Применяет только обученные DataFrame-шаги pipeline и возвращает новый DataFrame.
///
/// Матричные шаги и модель при этом не используются.
/// Метод предназначен в основном для просмотра и анализа промежуточного
/// результата предобработки.
///
/// Полученный DataFrame обычно не следует повторно подавать в тот же pipeline,
/// так как DataFrame-преобразования будут выполнены ещё раз.
function Transform(df: DataFrame): DataFrame;
function ToString: string; override;
function Clone: IModel;
function Name: string := Self.GetType.Name;
end;
/// Табличный конвейер для задачи кластеризации.
/// Predict возвращает номера кластеров.
ClusteringDataPipeline = class(UnsupervisedDataPipelineBase)
public
function WithModel(model: IUnsupervisedModel): ClusteringDataPipeline;
function Fit(df: DataFrame): ClusteringDataPipeline;
/// Обучает конвейер и сразу возвращает метки кластеров.
function FitPredict(df: DataFrame): array of integer;
/// Делает предсказание кластеров для объектов из DataFrame.
/// Возвращает номера кластеров.
function Predict(df: DataFrame): array of integer;
end;
implementation
uses MLExceptions;
uses DataAdapters;
uses MLUtilsABC;
const
ER_PIPELINE_MODIFY_AFTER_FIT =
'Нельзя добавлять шаг после вызова Fit()!!Cannot add step after Fit';
ER_PIPELINE_STEP_NULL =
'Шаг конвейера не может быть nil!!Pipeline step cannot be nil';
ER_DATAPIPE_DF_AFTER_MATRIX =
'Шаг DataFrame не может идти после матричных шагов!!DataFrame step cannot appear after matrix steps';
ER_PIPELINE_MULTIPLE_MODELS =
'В конвейере разрешена только одна модель!!Only one model is allowed in the pipeline';
ER_DATAPIPE_MATRIX_AFTER_MODEL =
'Матричный шаг не может идти после модели!!Matrix step cannot appear after the model';
ER_DATAPIPE_UNKNOWN_STEP_TYPE =
'Неизвестный тип шага конвейера: {0}!!Unknown pipeline step type: {0}';
ER_PROBA_NOT_SUPPORTED =
'Модель не поддерживает предсказание вероятностей!!Model does not support probability prediction';
ER_TARGET_EMPTY =
'Имя целевой переменной не задано!!Target column name is not specified';
ER_FEATURES_EMPTY =
'Список признаков пуст!!Feature list must not be empty';
ER_FEATURE_EMPTY =
'Имя признака не может быть пустым!!Feature name cannot be empty';
ER_DATAPIPE_TARGET_IN_FEATURES =
'Целевая переменная "{0}" не должна входить в список признаков!!Target variable "{0}" must not appear in feature list';
ER_TO_MATRIX_NON_NUMERIC =
'Столбец "{0}" содержит нечисловые или NA значения!!Column "{0}" contains non-numeric or NA values';
ER_DATAPIPE_TARGET_NOT_FOUND =
'Целевой столбец "{0}" не найден. Доступные столбцы: {1}!!' +
'Target column "{0}" not found. Available columns: {1}';
ER_DATAPIPE_FEATURE_NOT_FOUND =
'Признак "{0}" не найден в DataFrame. Доступные столбцы: {1}!!'+
'Feature "{0}" not found in DataFrame. Available columns: {1}';
ER_DATAPIPE_DUPLICATE_FEATURE =
'Повторяющийся признак: {0}!!Duplicate feature: {0}';
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}" была удалена на этапе предобработки конвейера!!' +
'Target column "{0}" was removed during pipeline preprocessing';
ER_PIPELINE_NO_FEATURES =
'После предобработки конвейера не осталось признаков для обучения модели!!' +
'No features remain after pipeline preprocessing';
ER_MATRIXSTEP_NO_FIT =
'Шаг матричного конвейера #{0} не поддерживает Fit!!Matrix step #{0} does not support Fit';
ER_Model_NoFit =
'Модель (тип: {0}) не поддерживает Fit!!Model (type: {0}) does not support Fit';
ER_MODEL_NOT_SUPERVISED =
'Модель (тип: {0}) не поддерживает Fit(X, y)!!' +
'Model (type: {0}) does not support Fit(X, y)';
ER_PIPELINE_FEATURE_NOT_FOUND =
'Признак "{0}" отсутствует во входных данных!!' +
'Feature "{0}" not found in input data';
ER_MODEL_NOT_UNSUPERVISED =
'Модель (тип: {0}) не является моделью без учителя!!' +
'Model (type: {0}) is not an unsupervised model';
ER_PIPELINE_FEATURE_NOT_NUMERIC =
'Признак "{0}" имеет тип {1} и должен быть числовым!!Feature "{0}" has type {1} but must be numeric';
ER_MODEL_NOT_CLUSTERER =
'Модель "{0}" не является алгоритмом кластеризации!!Model "{0}" is not a clustering algorithm';
ER_NOT_CLASSIFICATION =
'Операция доступна только для задач классификации!!Operation is only available for classification tasks';
ER_CLASSES_NOT_AVAILABLE =
'Метки классов недоступны. Убедитесь, что конвейер обучен и задача — классификация!!Class labels are not available. Ensure the pipeline is fitted and the task is classification';
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 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 =
'Целевой столбец "{0}" должен быть строковым, целочисленным или логическим для задач классификации!!' +
'Target column "{0}" must be a string, integer, or boolean column for classification tasks';
ER_REGRESSION_TARGET_MUST_BE_NUMERIC =
'Целевой столбец "{0}" должен быть числовым для задач регрессии!!Target column "{0}" must be numeric for regression tasks';
ER_PREPROCESSOR_ROWCOUNT_CHANGED =
'DataFrame-преобразователь не должен изменять число строк!!DataFrame preprocessor must preserve RowCount';
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_PIPELINE_NO_STEPS =
'Pipeline не содержит шагов!!' +
'Pipeline must contain at least one step';
ER_PIPELINE_LAST_NOT_UNSUPERVISED_MODEL =
'Последний шаг 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 должен быть моделью с учителем!!' +
'Last Pipeline step must be a supervised model';
ER_MODEL_NOT_CLASSIFIER =
'Модель не является классификатором!!' +
'Model is not a classifier';
ER_MODEL_CLONE_TYPE =
'Clone модели вернул неподдерживаемый тип!!Model Clone returned unsupported type';
ER_INVALID_MODEL_TYPE =
'Clone модели вернул неподдерживаемый тип (ожидается {0})!!Model Clone returned unsupported type (expected {0})';
//-----------------------------
// DataPipeline
//-----------------------------
constructor PipelineBase.Create;
begin
fDataSteps := new List<IPreprocessor>;
fMatrixSteps := new List<ITransformer>;
fFeatures := nil;
fFitted := false;
end;
class procedure PipelineBase.ValidateFeatureList(features: array of string);
begin
if (features = nil) or (features.Length = 0) then
ArgumentError(ER_FEATURES_EMPTY);
var seen := new HashSet<string>;
foreach var f in features do
begin
if (f = nil) or (f = '') then
ArgumentError(ER_FEATURE_EMPTY);
if f in seen then
ArgumentError(ER_DATAPIPE_DUPLICATE_FEATURE, f);
seen.Add(f);
end;
end;
function PipelineBase.ResolveFinalFeatures(current: DataFrame): array of string;
begin
if current = nil then
ArgumentNullError(ER_ARG_NULL, 'current');
var feats := new List<string>;
foreach var f in fFeatures do
begin
var expanded := false;
// ищем expander-ы В ПРЯМОМ порядке pipeline
for var i := 0 to fDataSteps.Count - 1 do
begin
var expander := fDataSteps[i] as IColumnExpander;
if expander = nil then
continue;
var cols := expander.GetExpandedColumns(f);
if (cols <> nil) and (cols.Length > 0) then
begin
foreach var c in cols do
if current.HasColumn(c) then
if c not in feats then
feats.Add(c);
expanded := true;
break;
end;
end;
if expanded then
continue;
// fallback: обычный столбец
if current.HasColumn(f) then
if f not in feats then
feats.Add(f);
end;
if feats.Count = 0 then
ArgumentError(ER_PIPELINE_NO_FEATURES);
Result := feats.ToArray;
end;
function PipelineBase.PrepareMatrix(df: DataFrame; var current: DataFrame): Matrix;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
current := df;
// --- schema
ValidateSchema(current);
// --- DataFrame steps
for var i := 0 to fDataSteps.Count - 1 do
begin
var prevRows := current.RowCount;
fDataSteps[i] := fDataSteps[i].Fit(current);
current := fDataSteps[i].Transform(current);
if current.RowCount <> prevRows then
Error(ER_PREPROCESSOR_ROWCOUNT_CHANGED);
end;
fFinalFeatures := ResolveFinalFeatures(current);
Result := current.ToMatrix(fFinalFeatures);
end;
function PipelineBase.TransformDataFrame(df: DataFrame): DataFrame;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
var current := df;
foreach var s in fDataSteps do
begin
var prevRows := current.RowCount;
current := s.Transform(current);
if current.RowCount <> prevRows then
Error(ER_PREPROCESSOR_ROWCOUNT_CHANGED);
end;
Result := current;
end;
function PipelineBase.FitTransformMatrix(var X: Matrix; y: Vector): Matrix;
begin
for var i := 0 to fMatrixSteps.Count - 1 do
begin
var t := fMatrixSteps[i];
if y <> nil then
begin
if t is ISupervisedTransformer(var sup) then
fMatrixSteps[i] := sup.Fit(X, y)
else if t is IUnsupervisedTransformer(var unsup) then
fMatrixSteps[i] := unsup.Fit(X)
else
ArgumentError(ER_MATRIXSTEP_NO_FIT, i);
end
else
begin
if t is IUnsupervisedTransformer(var unsup) then
fMatrixSteps[i] := unsup.Fit(X)
else
ArgumentError(ER_MATRIXSTEP_NO_FIT, i);
end;
X := fMatrixSteps[i].Transform(X);
end;
Result := X;
end;
function PipelineBase.TransformMatrix(X: Matrix): Matrix;
begin
foreach var t in fMatrixSteps do
X := t.Transform(X);
Result := X;
end;
//-----------------------------
// DataPipeline
//-----------------------------
constructor SupervisedDataPipelineBase.Create;
begin
inherited Create;
fModel := nil;
fTarget := '';
end;
function SupervisedDataPipelineBase.Add(step: IPipelineStep): SupervisedDataPipelineBase;
begin
// --- global invariants
if step = nil then
ArgumentError(ER_PIPELINE_STEP_NULL);
if fFitted then
Error(ER_PIPELINE_MODIFY_AFTER_FIT);
// --- target protection
// Любой шаг DataFrame, привязанный к одной или нескольким колонкам,
// не должен затрагивать целевую переменную (target).
// Проверка выполняется через интерфейсы IColumnBoundStep / IColumnsBoundStep
// без привязки к конкретным классам.
if step is IColumnBoundStep(var cstep) then
if cstep.ColumnName = fTarget then
ArgumentError(ER_PIPELINE_TARGET_TRANSFORM_NOT_ALLOWED, fTarget);
if step is IColumnsBoundStep(var mstep) then
if fTarget in mstep.Columns then
ArgumentError(ER_PIPELINE_TARGET_TRANSFORM_NOT_ALLOWED, fTarget);
// --- DataFrame step
if step is IPreprocessor then
begin
if (fMatrixSteps.Count > 0) or (fModel <> nil) then
ArgumentError(ER_DATAPIPE_DF_AFTER_MATRIX);
fDataSteps.Add(step as IPreprocessor);
exit(Self);
end;
// --- Matrix transformer
if step is ITransformer then
begin
if fModel <> nil then
ArgumentError(ER_DATAPIPE_MATRIX_AFTER_MODEL);
fMatrixSteps.Add(step as ITransformer);
exit(Self);
end;
// --- Model (обязательно последний шаг)
if step is IModel then
begin
if fModel <> nil then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
fModel := step as IModel;
exit(Self);
end;
ArgumentError(ER_DATAPIPE_UNKNOWN_STEP_TYPE, step.ToString);
Result := Self;
end;
class function DataPipeline.BuildClassificationPreprocessing(
target: string;
features: array of string;
params steps: array of IPipelineStep
): ClassificationDataPipeline;
begin
if (target = nil) or (target = '') then
ArgumentError(ER_TARGET_EMPTY);
PipelineBase.ValidateFeatureList(features);
foreach var f in features do
if f = target then
ArgumentError(ER_DATAPIPE_TARGET_IN_FEATURES, target);
if steps <> nil then
for var i := 0 to High(steps) do
begin
var step := steps[i];
if step = nil then
ArgumentError(ER_PIPELINE_STEP_NULL, i);
if step is IModel then
ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER);
end;
var p := new ClassificationDataPipeline;
p.fTarget := target;
p.fFeatures := Copy(features);
p.fTask := TaskKind.tkClassification;
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
class function DataPipeline.BuildRegressionPreprocessing(
target: string;
features: array of string;
params steps: array of IPipelineStep
): RegressionDataPipeline;
begin
if (target = nil) or (target = '') then
ArgumentError(ER_TARGET_EMPTY);
PipelineBase.ValidateFeatureList(features);
foreach var f in features do
if f = target then
ArgumentError(ER_DATAPIPE_TARGET_IN_FEATURES, target);
if steps <> nil then
for var i := 0 to High(steps) do
begin
var step := steps[i];
if step = nil then
ArgumentError(ER_PIPELINE_STEP_NULL, i);
if step is IModel then
ArgumentError(ER_PIPELINE_INVALID_STEP_ORDER);
end;
var p := new RegressionDataPipeline;
p.fTarget := target;
p.fFeatures := Copy(features);
p.fTask := TaskKind.tkRegression;
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
class function DataPipeline.BuildClassification(
target: string;
features: array of string;
params steps: array of IPipelineStep
): ClassificationDataPipeline;
begin
PipelineBase.ValidateFeatureList(features);
if (target = nil) or (target = '') then
ArgumentError(ER_TARGET_EMPTY);
foreach var f in features do
if f = target then
ArgumentError(ER_DATAPIPE_TARGET_IN_FEATURES, target);
var p := new ClassificationDataPipeline;
p.fTarget := target;
p.fFeatures := Copy(features);
p.fTask := TaskKind.tkClassification;
if steps <> nil then
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
class function DataPipeline.BuildRegression(
target: string;
features: array of string;
params steps: array of IPipelineStep
): RegressionDataPipeline;
begin
PipelineBase.ValidateFeatureList(features);
if (target = nil) or (target = '') then
ArgumentError(ER_TARGET_EMPTY);
foreach var f in features do
if f = target then
ArgumentError(ER_DATAPIPE_TARGET_IN_FEATURES, target);
var p := new RegressionDataPipeline;
p.fTarget := target;
p.fFeatures := Copy(features);
p.fTask := TaskKind.tkRegression;
if steps <> nil then
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
class function DataPipeline.BuildClustering(
features: array of string;
params steps: array of IPipelineStep
): ClusteringDataPipeline;
begin
PipelineBase.ValidateFeatureList(features);
var p := new ClusteringDataPipeline;
p.fFeatures := Copy(features);
if steps <> nil then
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
class function DataPipeline.BuildClusteringPreprocessing(
features: array of string;
params steps: array of IPipelineStep
): ClusteringDataPipeline;
begin
PipelineBase.ValidateFeatureList(features);
if steps <> nil then
for var i := 0 to High(steps) 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 ClusteringDataPipeline;
p.fFeatures := Copy(features);
if steps <> nil then
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
function SupervisedDataPipelineBase.WithModelCore(model: IModel): SupervisedDataPipelineBase;
begin
if model = nil then
ArgumentError(ER_MODEL_NULL);
var p := Clone as SupervisedDataPipelineBase;
if p = nil then
Error(ER_MODEL_CLONE_TYPE);
if p.fFitted then
Error(ER_PIPELINE_MODIFY_AFTER_FIT);
if p.fModel <> nil then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
p.Add(model as IPipelineStep);
Result := p;
end;
function SupervisedDataPipelineBase.FitCore(df: DataFrame): SupervisedDataPipelineBase;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
var current: DataFrame;
var X := PrepareMatrix(df, current);
if not current.HasColumn(fTarget) then
ArgumentError(ER_PIPELINE_TARGET_REMOVED, fTarget);
var classes: array of string;
var y: Vector;
var labels: array of integer;
case fTask of
tkRegression:
y := current.ToVector(fTarget);
tkClassification:
begin
labels := current.EncodeLabels(fTarget, classes);
y := new Vector(labels);
end;
end;
if X.RowCount <> y.Length then
DimensionError(ER_XY_SIZE_MISMATCH, X.RowCount, y.Length);
X := FitTransformMatrix(X, y);
case fTask of
tkRegression:
begin
if not (fModel is IRegressor) then
ArgumentError(ER_MODEL_NOT_SUPERVISED, fModel.GetType.Name);
fModel := (fModel as IRegressor).Fit(X, y);
end;
tkClassification:
begin
if not (fModel is IClassifier) then
ArgumentError(ER_MODEL_NOT_CLASSIFIER);
fModel := (fModel as IClassifier).Fit(X, labels);
end;
end;
if fTask = tkClassification then
begin
if fModel is IClassifierInternal(var cls) then
cls.SetClassLabels(classes)
else
Error(ER_MODEL_NOT_CLASSIFIER);
end;
fFitted := true;
Result := Self;
end;
function SupervisedDataPipelineBase.Transform(df: DataFrame): DataFrame := TransformDataFrame(df);
function SupervisedDataPipelineBase.PredictCore(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);
if not (fModel is IRegressor) then
Error(ER_PREDICT_NOT_SUPPORTED);
var current := Transform(df);
var X := current.ToMatrix(fFinalFeatures);
X := TransformMatrix(X);
Result := (fModel as IRegressor).Predict(X);
end;
function ClassificationDataPipeline.Predict(df: DataFrame): array of integer;
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);
if not (fModel is IClassifier) then
Error(ER_PREDICT_NOT_SUPPORTED);
var current := Transform(df);
var X := current.ToMatrix(fFinalFeatures);
X := TransformMatrix(X);
Result := (fModel as IClassifier).Predict(X);
end;
function ClassificationDataPipeline.WithModel(model: IClassifier): ClassificationDataPipeline;
begin
Result := inherited WithModelCore(model) as ClassificationDataPipeline;
end;
function ClassificationDataPipeline.Fit(df: DataFrame): ClassificationDataPipeline;
begin
inherited FitCore(df);
Result := Self;
end;
function ClassificationDataPipeline.PredictLabels(df: DataFrame): array of string;
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);
if not (fModel is IClassifier) then
Error(ER_PREDICT_NOT_SUPPORTED);
var current := Transform(df);
var X := current.ToMatrix(fFinalFeatures);
X := TransformMatrix(X);
Result := (fModel as IClassifier).PredictLabels(X);
end;
function ClassificationDataPipeline.PredictProba(df: DataFrame): Matrix;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
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);
var current := Transform(df);
var X := current.ToMatrix(fFinalFeatures);
X := TransformMatrix(X);
Result := (fModel as IProbabilisticClassifier).PredictProba(X);
end;
function ClassificationDataPipeline.GetEncodedLabels(df: DataFrame): array of integer;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fTask <> tkClassification then
ArgumentError(ER_NOT_CLASSIFICATION);
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not df.HasColumn(fTarget) then
ArgumentError(ER_COLUMN_NOT_FOUND, fTarget);
var classes := GetClassLabels;
Result := df.TransformLabels(fTarget, classes);
end;
function ClassificationDataPipeline.GetClassLabels: array of string;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fTask <> tkClassification then
ArgumentError(ER_NOT_CLASSIFICATION);
var cls := fModel as IClassifier;
if cls = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := cls.GetClassLabels;
end;
function RegressionDataPipeline.WithModel(model: IRegressor): RegressionDataPipeline;
begin
Result := inherited WithModelCore(model) as RegressionDataPipeline;
end;
function RegressionDataPipeline.Fit(df: DataFrame): RegressionDataPipeline;
begin
inherited FitCore(df);
Result := Self;
end;
function RegressionDataPipeline.Predict(df: DataFrame): Vector;
begin
Result := inherited PredictCore(df);
end;
procedure SupervisedDataPipelineBase.ValidateSchema(df: DataFrame);
begin
if fTarget = '' then
ArgumentError(ER_TARGET_EMPTY);
PipelineBase.ValidateFeatureList(fFeatures);
// target не должен входить в features
for var i := 0 to High(fFeatures) do
if fFeatures[i] = fTarget then
ArgumentError(ER_DATAPIPE_TARGET_IN_FEATURES, fTarget);
// проверка существования target
if not df.Schema.HasColumn(fTarget) then
begin
var cols := df.Schema.ColumnNames.JoinToString(', ');
ArgumentError(ER_DATAPIPE_TARGET_NOT_FOUND, fTarget, cols);
end;
case fTask of
tkClassification:
begin
var ct := df.GetColumnType(fTarget);
if not (ct in [ColumnType.ctStr, ColumnType.ctInt, ColumnType.ctBool]) then
ArgumentError(ER_CLASSIFICATION_TARGET_MUST_BE_CATEGORICAL_STR_OR_INT, fTarget);
end;
tkRegression:
begin
var ct := df.GetColumnType(fTarget);
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
ArgumentError(ER_REGRESSION_TARGET_MUST_BE_NUMERIC, fTarget);
end;
end;
for var i := 0 to High(fFeatures) do
begin
var f := fFeatures[i];
if not df.Schema.HasColumn(f) then
begin
var cols := df.Schema.ColumnNames.JoinToString(', ');
ArgumentError(ER_DATAPIPE_FEATURE_NOT_FOUND, f, cols);
end;
end;
end;
function SupervisedDataPipelineBase.ToString: string;
begin
var sb := 'SupervisedDataPipelineBase (' +
(if fFitted then 'trained' else 'not trained') + '):' + NewLine;
sb += ' Target: ' + fTarget + NewLine;
sb += ' Features: ' + fFeatures.JoinToString(', ') + NewLine;
var idx := 1;
foreach var s in fDataSteps do
begin
sb += ' [' + idx + '] ' + s.ToString + NewLine;
idx += 1;
end;
foreach var t in fMatrixSteps do
begin
sb += ' [' + idx + '] ' + t.ToString + NewLine;
idx += 1;
end;
if fModel <> nil then
sb += ' [' + idx + '] ' + fModel.ToString;
Result := sb;
end;
function SupervisedDataPipelineBase.Clone: IModel;
begin
var p: SupervisedDataPipelineBase;
if Self is ClassificationDataPipeline then
p := new ClassificationDataPipeline
else if Self is RegressionDataPipeline then
p := new RegressionDataPipeline
else
p := new SupervisedDataPipelineBase;
// --- конфигурация
p.fFeatures := Copy(fFeatures);
p.fTarget := fTarget;
p.fTask := fTask;
// --- шаги (глубокая копия)
p.fDataSteps := fDataSteps.Select(s -> s.Clone).ToList;
p.fMatrixSteps := fMatrixSteps.Select(s -> s.Clone).ToList;
// --- модель (глубокая копия конфигурации)
if fModel <> nil then
begin
var m := fModel.Clone;
if not (m is IModel) then
Error(ER_MODEL_CLONE_TYPE);
p.fModel := m as IModel;
end;
// --- состояние НЕ копируем
p.fFinalFeatures := nil;
p.fFitted := false;
Result := p;
end;
//-----------------------------
// UnsupervisedDataPipelineBase
//-----------------------------
constructor UnsupervisedDataPipelineBase.Create;
begin
inherited Create;
fModel := nil;
end;
function UnsupervisedDataPipelineBase.Add(step: IPipelineStep): UnsupervisedDataPipelineBase;
begin
if step = nil then
ArgumentError(ER_PIPELINE_STEP_NULL);
if fFitted then
Error(ER_PIPELINE_MODIFY_AFTER_FIT);
// --- DataFrame step
if step is IPreprocessor then
begin
if (fMatrixSteps.Count > 0) or (fModel <> nil) then
ArgumentError(ER_DATAPIPE_DF_AFTER_MATRIX);
fDataSteps.Add(step as IPreprocessor);
exit(Self);
end;
// --- Matrix transformer
if step is ITransformer then
begin
if fModel <> nil then
ArgumentError(ER_DATAPIPE_MATRIX_AFTER_MODEL);
fMatrixSteps.Add(step as ITransformer);
exit(Self);
end;
// --- Model (обязательно последний шаг)
if step is IUnsupervisedModel then
begin
if fModel <> nil then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
fModel := step as IUnsupervisedModel;
exit(Self);
end;
ArgumentError(ER_DATAPIPE_UNKNOWN_STEP_TYPE, step.ToString);
Result := Self;
end;
function UnsupervisedDataPipelineBase.WithModelCore(model: IUnsupervisedModel): UnsupervisedDataPipelineBase;
begin
if model = nil then
ArgumentError(ER_MODEL_NULL);
var p := Clone as UnsupervisedDataPipelineBase;
if p = nil then
Error(ER_INVALID_MODEL_TYPE, 'UnsupervisedDataPipelineBase');
if p.fFitted then
Error(ER_PIPELINE_MODIFY_AFTER_FIT);
if p.fModel <> nil then
ArgumentError(ER_PIPELINE_MULTIPLE_MODELS);
p.Add(model as IPipelineStep);
Result := p;
end;
function UnsupervisedDataPipelineBase.FitCore(df: DataFrame): UnsupervisedDataPipelineBase;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
var current: DataFrame;
var X := PrepareMatrix(df, current);
X := FitTransformMatrix(X);
fModel := fModel.Fit(X);
fFitted := true;
Result := Self;
end;
function UnsupervisedDataPipelineBase.Transform(df: DataFrame): DataFrame := TransformDataFrame(df);
function UnsupervisedDataPipelineBase.FitPredictCore(df: DataFrame): array of integer;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
if not (fModel is IClusterer) then
ArgumentError(ER_MODEL_NOT_CLUSTERER, fModel.GetType.Name);
var current := df;
// --- 0) Проверка входной схемы
ValidateSchema(current);
// --- 1) DataFrame шаги
for var i := 0 to fDataSteps.Count - 1 do
begin
var prevRows := current.RowCount;
fDataSteps[i] := fDataSteps[i].Fit(current);
current := fDataSteps[i].Transform(current);
if current.RowCount <> prevRows then
Error(ER_PREPROCESSOR_ROWCOUNT_CHANGED);
end;
// --- 2) вычислить финальные признаки
fFinalFeatures := ResolveFinalFeatures(current);
var X := current.ToMatrix(fFinalFeatures);
// --- 3) Matrix transformers
X := FitTransformMatrix(X);
// --- 4) модель
var cl := fModel as IClusterer;
Result := cl.FitPredict(X);
fFitted := true;
end;
procedure UnsupervisedDataPipelineBase.ValidateNumericFeatures(df: DataFrame);
begin
if fFinalFeatures = nil then
Error(ER_PIPELINE_FINALFEATURES);
var schema := df.Schema;
foreach var f in fFinalFeatures do
begin
var idx := schema.IndexOf(f);
if idx < 0 then
ArgumentError(ER_PIPELINE_FEATURE_NOT_FOUND, f);
var t := schema.ColumnTypeAt(idx);
if (t <> ColumnType.ctInt) and (t <> ColumnType.ctFloat) then
ArgumentError(ER_PIPELINE_FEATURE_NOT_NUMERIC, f, t.ToString);
end;
end;
function UnsupervisedDataPipelineBase.TransformToMatrix(df: DataFrame): Matrix;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
var current := Transform(df);
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);
X := TransformMatrix(X);
Result := X;
end;
function UnsupervisedDataPipelineBase.PredictCore(df: DataFrame): array of integer;
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 X := TransformToMatrix(df);
if not (fModel is IPredictiveClusterer) then
Error(ER_PREDICT_NOT_SUPPORTED);
Result := (fModel as IPredictiveClusterer).Predict(X);
end;
function ClusteringDataPipeline.WithModel(model: IUnsupervisedModel): ClusteringDataPipeline;
begin
Result := inherited WithModelCore(model) as ClusteringDataPipeline;
end;
function ClusteringDataPipeline.Fit(df: DataFrame): ClusteringDataPipeline;
begin
inherited FitCore(df);
Result := Self;
end;
function ClusteringDataPipeline.FitPredict(df: DataFrame): array of integer;
begin
Result := inherited FitPredictCore(df);
end;
function ClusteringDataPipeline.Predict(df: DataFrame): array of integer;
begin
Result := inherited PredictCore(df);
end;
procedure UnsupervisedDataPipelineBase.ValidateSchema(df: DataFrame);
begin
PipelineBase.ValidateFeatureList(fFeatures);
for var i := 0 to High(fFeatures) do
begin
var f := fFeatures[i];
if not df.Schema.HasColumn(f) then
begin
var cols := df.Schema.ColumnNames.JoinToString(', ');
ArgumentError(ER_DATAPIPE_FEATURE_NOT_FOUND, f, cols);
end;
end;
end;
function UnsupervisedDataPipelineBase.ToString: string;
begin
var sb := 'UnsupervisedDataPipelineBase (' +
(if fFitted then 'trained' else 'not trained') + '):' + NewLine;
var idx := 1;
foreach var s in fDataSteps do
begin
sb += ' [' + idx + '] ' + s.ToString + NewLine;
idx += 1;
end;
foreach var t in fMatrixSteps do
begin
sb += ' [' + idx + '] ' + t.ToString + NewLine;
idx += 1;
end;
if fModel <> nil then
sb += ' [' + idx + '] ' + fModel.ToString;
Result := sb;
end;
function UnsupervisedDataPipelineBase.Clone: IModel;
begin
var p: UnsupervisedDataPipelineBase;
if Self is ClusteringDataPipeline then
p := new ClusteringDataPipeline
else
p := new UnsupervisedDataPipelineBase;
// --- конфигурация
p.fFeatures := Copy(fFeatures);
// --- шаги (глубокая копия)
p.fDataSteps := fDataSteps.Select(s -> s.Clone).ToList;
p.fMatrixSteps := fMatrixSteps.Select(s -> s.Clone).ToList;
// --- модель
if fModel <> nil then
begin
var m := fModel.Clone;
if not (m is IUnsupervisedModel) then
Error(ER_INVALID_MODEL_TYPE, 'IUnsupervisedModel');
p.fModel := m as IUnsupervisedModel;
end;
// --- состояние НЕ копируем
p.fFinalFeatures := nil;
p.fFitted := false;
Result := p;
end;
end.