Исправления в ML модулях

This commit is contained in:
Mikhalkovich Stanislav 2026-03-31 12:38:47 +03:00
parent a8d8ce82bf
commit 91346d8cbe
19 changed files with 454 additions and 123 deletions

View file

@ -15,7 +15,7 @@ internal static class RevisionClass
public const string Major = "3";
public const string Minor = "11";
public const string Build = "1";
public const string Revision = "3784";
public const string Revision = "3788";
public const string MainVersion = Major + "." + Minor;
public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision;

View file

@ -1,4 +1,4 @@
%COREVERSION%=1
%REVISION%=3784
%REVISION%=3788
%MINOR%=11
%MAJOR%=3

View file

@ -1,6 +1,6 @@
uses MLABC;
procedure TestModel(model: IModel; Xtrain, Xtest: Matrix; ytrain, ytest: Vector);
procedure TestModel(model: ISupervisedModel; Xtrain, Xtest: Matrix; ytrain, ytest: Vector);
begin
model.Fit(Xtrain, ytrain);

View file

@ -1,6 +1,6 @@
uses MLABC;
procedure TestModel(model: IModel; X: Matrix; y: Vector);
procedure TestModel(model: ISupervisedModel; X: Matrix; y: Vector);
begin
var acc :=
Validation.StratifiedCrossValidate(
@ -21,7 +21,7 @@ begin
var X := df.ToMatrix(ds.Features);
var y := df.EncodeLabels(ds.Target);
var models: array of IModel := (
var models: array of ISupervisedModel := (
new LogisticRegression,
new DecisionTreeClassifier(seed := -1),
new RandomForestClassifier(seed := -1),

View file

@ -7,7 +7,7 @@ begin
var price := df.ToVector(ds.Target);
Plot.Hist(price, bins := 40);
Plot.Title('Распределение цен на квартиры в Москве');
Plot.Title := 'Распределение цен на квартиры в Москве';
Plot.XLabel('Цена (руб)');
Plot.YLabel('Количество');
end.

View file

@ -11,7 +11,7 @@ begin
seed := 1
);
Plot.Title('MakeBlobs синтетический датасет');
Plot.Title := 'MakeBlobs синтетический датасет';
var xs := X.Col(0);
var ys := X.Col(1);

View file

@ -1 +1 @@
3.11.1.3784
3.11.1.3788

View file

@ -1 +1 @@
!define VERSION '3.11.1.3784'
!define VERSION '3.11.1.3788'

View file

@ -18,6 +18,13 @@ function LabelsToInts(y: Vector): array of integer;
/// Используется при обучении моделей и визуализации.
function EncodeLabels(labels: array of string): array of integer;
/// Кодирует строковые метки классов в целочисленные индексы.
/// Каждому уникальному значению присваивается номер 0,1,2,...
/// Порядок кодирования соответствует порядку первого появления меток.
/// В параметр classes возвращается массив уникальных значений в порядке кодирования.
/// Используется при обучении моделей и визуализации
function EncodeLabels(labels: array of string; var classes: array of string): array of integer;
/// Преобразует целочисленные индексы классов обратно в строковые метки.
/// Массив classes задаёт соответствие: classes[i] имя класса с индексом i.
/// Используется для получения текстовых предсказаний моделей.
@ -42,7 +49,11 @@ const
'Столбец "{0}" содержит нечисловые или NA значения!!Column "{0}" contains non-numeric or NA values';
ER_ENCODELABELS_COLUMN_NOT_STRING =
'Столбец {0} должен быть строковым для EncodeLabels!!Column {0} must be string for EncodeLabels';
ER_ENCODELABELS_NOT_CATEGORICAL =
'Столбец "{0}" должен быть категориальным для EncodeLabels!!Column "{0}" must be categorical for EncodeLabels';
ER_ENCODELABELS_UNSUPPORTED_TYPE =
'Неподдерживаемый тип столбца "{0}" для EncodeLabels!!Unsupported column type "{0}" for EncodeLabels';
function LabelsToInts(y: Vector): array of integer;
begin
if y = nil then
@ -51,25 +62,39 @@ begin
Result := ArrGen(y.Length, i -> Round(y[i]));
end;
function EncodeLabels(labels: array of string): array of integer;
function EncodeLabels(labels: array of string; var classes: array of string): array of integer;
begin
if labels = nil then
ArgumentNullError(ER_ARG_NULL, 'labels');
var classes := labels.Distinct.Order.ToArray;
var classList := new List<string>;
var map := new Dictionary<string, integer>;
var map := new Dictionary<string,integer>;
for var i := 0 to classes.Length-1 do
map[classes[i]] := i;
// собираем классы в порядке первого появления
for var i := 0 to labels.Length - 1 do
begin
var lbl := labels[i];
if not map.ContainsKey(lbl) then
begin
map[lbl] := classList.Count;
classList.Add(lbl);
end;
end;
// кодируем
var res := new integer[labels.Length];
for var i := 0 to labels.Length-1 do
for var i := 0 to labels.Length - 1 do
res[i] := map[labels[i]];
classes := classList.ToArray;
Result := res;
end;
function EncodeLabels(labels: array of string): array of integer;
begin
var classes: array of string;
Result := EncodeLabels(labels, classes);
end;
function ToMatrix(Self: DataFrame; colNames: array of string): Matrix; extensionmethod;
begin
@ -117,6 +142,29 @@ begin
end;
end;
// Helper
function EncodeLabelsIntHelper(labels: array of integer): array of integer;
begin
var classList := new List<integer>;
var map := new Dictionary<integer, integer>;
for var i := 0 to labels.Length - 1 do
begin
var lbl := labels[i];
if not map.ContainsKey(lbl) then
begin
map[lbl] := classList.Count;
classList.Add(lbl);
end;
end;
var y := new integer[labels.Length];
for var i := 0 to labels.Length - 1 do
y[i] := map[labels[i]];
Result := y;
end;
/// Кодирует строковые метки классов в целочисленные индексы.
/// Каждому уникальному значению присваивается номер 0,1,2,...
/// Порядок кодирования соответствует порядку первого появления меток.
@ -132,12 +180,125 @@ begin
if not Self.HasColumn(target) then
ArgumentError(ER_COLUMN_NOT_FOUND, target);
if Self.GetColumnType(target) <> ColumnType.ctStr then
ArgumentError(ER_ENCODELABELS_COLUMN_NOT_STRING, target);
if not Self.IsCategorical(target) then
ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, target);
var labels := Self.GetStrColumn(target);
if Self.GetColumnType(target) = ColumnType.ctStr then
begin
var labels := Self.GetStrColumn(target);
Result := EncodeLabels(labels);
exit;
end;
Result := EncodeLabels(labels);
if Self.GetColumnType(target) = ColumnType.ctInt then
begin
var labels := Self.GetIntColumn(target).ToArray;
Result := EncodeLabelsIntHelper(labels);
exit;
end;
ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target);
end;
function EncodeLabelsInt(labels: array of integer; var classes: array of integer): array of integer;
begin
if labels = nil then
ArgumentNullError(ER_ARG_NULL, 'labels');
var classList := new List<integer>;
var map := new Dictionary<integer, integer>;
// собираем уникальные значения в порядке первого появления
for var i := 0 to labels.Length - 1 do
begin
var lbl := labels[i];
if not map.ContainsKey(lbl) then
begin
map[lbl] := classList.Count;
classList.Add(lbl);
end;
end;
// кодируем
var res := new integer[labels.Length];
for var i := 0 to labels.Length - 1 do
res[i] := map[labels[i]];
classes := classList.ToArray;
Result := res;
end;
/// Кодирует значения категориального столбца DataFrame в целочисленные индексы.
/// Каждому уникальному значению присваивается номер 0,1,2,...
/// Порядок кодирования соответствует порядку первого появления значений в столбце.
/// В параметр classes возвращается массив уникальных значений в порядке кодирования.
/// Работает только для категориальных столбцов типов string и integer.
/// Для целочисленных категориальных столбцов значения преобразуются в строки.
/// Используется при подготовке данных для задач классификации и визуализации.
function EncodeLabels(Self: DataFrame; target: string; var classes: array of string): array of integer; extensionmethod;
begin
if Self = nil then
ArgumentNullError(ER_ARG_NULL, 'Self');
if target = nil then
ArgumentNullError(ER_ARG_NULL, 'target');
if not Self.HasColumn(target) then
ArgumentError(ER_COLUMN_NOT_FOUND, target);
if not Self.IsCategorical(target) then
ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, target);
case Self.GetColumnType(target) of
ColumnType.ctStr:
begin
var labels := Self.GetStrColumn(target).ToArray;
Result := EncodeLabels(labels, classes);
end;
ColumnType.ctInt:
begin
var labels := Self.GetIntColumn(target).ToArray;
var intClasses: array of integer;
Result := EncodeLabelsInt(labels, intClasses);
// если API требует string classes:
classes := intClasses.Select(x -> x.ToString).ToArray;
end;
else
ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target);
end;
end;
/// Кодирует значения целочисленного категориального столбца DataFrame
/// в плотные целочисленные индексы 0,1,2,...
/// Каждому уникальному значению присваивается номер в порядке первого появления.
/// В параметр classes возвращается массив уникальных значений в порядке кодирования.
/// Работает только для категориальных столбцов типа integer.
/// Используется при подготовке данных для задач классификации и анализа.
function EncodeLabelsInt(Self: DataFrame; target: string; var classes: array of integer): array of integer; extensionmethod;
begin
if Self = nil then
ArgumentNullError(ER_ARG_NULL, 'Self');
if target = nil then
ArgumentNullError(ER_ARG_NULL, 'target');
if not Self.HasColumn(target) then
ArgumentError(ER_COLUMN_NOT_FOUND, target);
if not Self.IsCategorical(target) then
ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, target);
if Self.GetColumnType(target) <> ColumnType.ctInt then
ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target);
var labels := Self.GetIntColumn(target).ToArray;
Result := EncodeLabelsInt(labels, classes);
end;
function DecodeLabels(y: array of integer; classes: array of string): array of string;

View file

@ -104,6 +104,8 @@ type
function SetCategorical(names: array of string): DataFrame;
function IsCategorical(name: string): boolean;
property Item[name: string]: Column read GetColumn; default;
function GetColumns: sequence of Column;
@ -1548,7 +1550,7 @@ begin
// 🔥 берем из старой schema
if (fSchema <> nil) and (i < fSchema.ColumnCount) then
cats[i] := fSchema.IsCategorical[i]
cats[i] := fSchema.CategoricalFlags[i]
else
cats[i] := false;
end;
@ -2314,7 +2316,7 @@ begin
// копируем старые значения
for var i := 0 to n - 1 do
cats[i] := fSchema.IsCategorical[i];
cats[i] := fSchema.CategoricalFlags[i];
// применяем новые
foreach var name in names do
@ -2331,6 +2333,17 @@ begin
Result := new DataFrame(columns, newSchema);
end;
function DataFrame.IsCategorical(name: string): boolean;
begin
if name = nil then
ArgumentNullError(ER_ARG_NULL, 'name');
if not HasColumn(name) then
ArgumentError(ER_COLUMN_NOT_FOUND, name);
Result := Schema.IsCategorical(name);
end;
function DataFrame.Filter(pred: CursorPredicate): DataFrame;
begin
var cursor := GetCursor;
@ -2440,7 +2453,7 @@ begin
// 🔥 КЛЮЧЕВОЕ: перенос schema
var cats := new boolean[ColumnCount];
for var i := 0 to ColumnCount - 1 do
cats[i] := fSchema.IsCategorical[i];
cats[i] := fSchema.CategoricalFlags[i];
res.SetSchema(new DataFrameSchema(
fSchema.ColumnNames,
@ -2612,7 +2625,7 @@ begin
names[i] := newName;
types[i] := fSchema.Types[i];
cats[i] := fSchema.IsCategorical[i];
cats[i] := fSchema.CategoricalFlags[i];
end;
res.SetSchema(new DataFrameSchema(names, types, cats));
@ -3059,7 +3072,7 @@ begin
begin
names[j] := fSchema.ColumnNames[j];
types[j] := fSchema.Types[j];
cats[j] := fSchema.IsCategorical[j];
cats[j] := fSchema.CategoricalFlags[j];
end;
names[oldN] := name;
@ -3834,7 +3847,7 @@ begin
for var i := 0 to ColumnCount - 1 do
begin
var t := ColumnTypeToString(GetColumnType(i));
if fSchema.IsCategorical[i] then
if fSchema.CategoricalFlags[i] then
t += ' (categorical)';
types[i] := t;
@ -3974,7 +3987,7 @@ begin
var name := fSchema.ColumnNames[i];
namesArr[i] := name;
cats[i] := fSchema.IsCategorical[i];
cats[i] := fSchema.CategoricalFlags[i];
if toCast.Contains(name) then
types[i] := ctInt

View file

@ -16,7 +16,7 @@ type
private
fNames: array of string;
fTypes: array of ColumnType;
fIsCategorical: array of boolean;
fCategoricalFlags: array of boolean;
fIndexByName: Dictionary<string, integer>;
class function BuildIndex(names: array of string): Dictionary<string, integer>;
@ -24,13 +24,17 @@ type
property ColumnCount: integer read fNames.Length;
property ColumnNames: array of string read fNames;
property Types: array of ColumnType read fTypes;
property IsCategorical: array of boolean read fIsCategorical;
property CategoricalFlags: array of boolean read fCategoricalFlags;
function IndexOf(name: string): integer;
function HasColumn(name: string): boolean;
function ColumnTypeAt(i: integer): ColumnType;
function IsCategoricalAt(i: integer): boolean;
function GetColumnType(name: string): ColumnType;
function IsCategorical(name: string): boolean;
function NameAt(i: integer): string;
constructor Create(names: array of string; types: array of ColumnType;
@ -338,7 +342,7 @@ begin
fNames := Copy(names);
fTypes := Copy(types);
fIsCategorical :=
fCategoricalFlags :=
if isCategorical = nil then
new boolean[names.Length]
else
@ -372,7 +376,7 @@ begin
ctBool: t := 'bool';
end;
if fIsCategorical[i] then
if fCategoricalFlags[i] then
PABCSystem.Println(name, ':', t, '(categorical)')
else
PABCSystem.Println(name, ':', t);
@ -414,10 +418,20 @@ function DataFrameSchema.IsCategoricalAt(i: integer): boolean;
begin
if (i < 0) or (i >= ColumnCount) then
ArgumentOutOfRangeError(ER_INDEX_OUT_OF_RANGE, i, ColumnCount);
if fIsCategorical = nil then
if fCategoricalFlags = nil then
Result := false
else
Result := fIsCategorical[i];
Result := fCategoricalFlags[i];
end;
function DataFrameSchema.GetColumnType(name: string): ColumnType;
begin
Result := ColumnTypeAt(IndexOf(name));
end;
function DataFrameSchema.IsCategorical(name: string): boolean;
begin
Result := IsCategoricalAt(IndexOf(name));
end;
function DataFrameSchema.Select(indices: array of integer): DataFrameSchema;
@ -428,7 +442,7 @@ begin
var n := indices.Length;
var names := new string[n];
var types := new ColumnType[n];
var cats := if fIsCategorical = nil then nil else new boolean[n];
var cats := if fCategoricalFlags = nil then nil else new boolean[n];
for var i := 0 to n - 1 do
begin
@ -437,7 +451,7 @@ begin
ArgumentOutOfRangeError(ER_INDEX_OUT_OF_RANGE, k, ColumnCount);
names[i] := fNames[k];
types[i] := fTypes[k];
if cats <> nil then cats[i] := fIsCategorical[k];
if cats <> nil then cats[i] := fCategoricalFlags[k];
end;
Result := new DataFrameSchema(names, types, cats);
@ -474,7 +488,7 @@ begin
var names := Copy(fNames);
names[IndexOf(oldName)] := newName;
Result := new DataFrameSchema(names, fTypes, fIsCategorical);
Result := new DataFrameSchema(names, fTypes, fCategoricalFlags);
end;
function DataFrameSchema.WithCategorical(name: string; value: boolean): DataFrameSchema;
@ -482,7 +496,7 @@ begin
if not HasColumn(name) then
ArgumentError(ER_COLUMN_NOT_EXISTS, name);
var cats := if fIsCategorical = nil then new boolean[ColumnCount] else Copy(fIsCategorical);
var cats := if fCategoricalFlags = nil then new boolean[ColumnCount] else Copy(fCategoricalFlags);
cats[IndexOf(name)] := value;
Result := new DataFrameSchema(fNames, fTypes, cats);
@ -533,7 +547,7 @@ end;
procedure DataFrameSchema.AssertConsistent;
begin
Assert(fNames.Length = fTypes.Length);
if fIsCategorical <> nil then Assert(fIsCategorical.Length = fNames.Length);
if fCategoricalFlags <> nil then Assert(fCategoricalFlags.Length = fNames.Length);
Assert(fIndexByName.Count = fNames.Length);
end;

View file

@ -89,8 +89,11 @@ type
Datasets = MLDatasets.Datasets;
IModel = MLCoreABC.IModel;
ISupervisedModel = MLCoreABC.ISupervisedModel;
IUnSupervisedModel = MLCoreABC.IUnSupervisedModel;
UPipeline = MLModelsABC.UPipeline;
UDataPipeline = MLPipelineABC.UDataPipeline;
TaskKind = MLPipelineABC.TaskKind;
function LabelsToInts(y: Vector): array of integer;
function EncodeLabels(labels: array of string): array of integer;

View file

@ -64,8 +64,13 @@ type
/// Наследуется от IModel.
/// Предназначен для моделей, выполняющих классификацию (предсказание меток классов).
IClassifier = interface(ISupervisedModel)
/// Возвращает метки классов
/// Возвращает индексы классов (0,1,2,...)
function PredictLabels(X: Matrix): array of integer;
/// Возвращает метки классов в порядке кодирования
function GetClassLabels: array of string;
procedure SetClassLabels(classes: array of string);
end;
/// Интерфейс классификатора, возвращающего вероятности.

View file

@ -1151,7 +1151,13 @@ end;
static function Datasets.Iris: Dataset;
begin
Result := Load('Iris');
var ds := Load('Iris');
ds.Data := ds.Data.SetCategorical([
'species'
]);
Result := ds;
end;
static function Datasets.MoscowHousing: Dataset;

View file

@ -23,7 +23,7 @@ const
'Пустой набор данных для {0}!!Empty dataset for {0}';
ER_COLUMN_NOT_FOUND =
'В DataFrame отсутствует столбец "{0}"!!DataFrame does not contain column "{0}"';
type
/// Базовое исключение ML-библиотеки
MLException = class(Exception);

View file

@ -232,6 +232,8 @@ type
fCheckConvergence: boolean := true;
fMinImprovement: real := 1e-8;
fClassLabels: array of string; // В каждой модели классификации
function GetWeights: Matrix;
function GetIntercept: Vector;
public
@ -282,6 +284,10 @@ type
property Intercept: Vector read GetIntercept;
function Name: string := Self.GetType.Name;
procedure SetClassLabels(classes: array of string);
function GetClassLabels: array of string;
end;
DecisionTreeNode = class
@ -452,6 +458,8 @@ type
fClassToIndex: Dictionary<integer, integer>;
fIndexToClass: array of integer;
fClassCount: integer;
fClassLabels: array of string;
function PredictOne(X: Matrix; rowIndex: integer): integer;
function MajorityClass(y: Vector; indices: array of integer): integer;
@ -496,6 +504,10 @@ type
function IndexToClass: array of integer := Copy(fIndexToClass);
function Name: string := Self.GetType.Name;
procedure SetClassLabels(classes: array of string);
function GetClassLabels: array of string;
end;
//============================
@ -713,6 +725,7 @@ type
fIndexToClass: array of integer;
fClassToIndex: Dictionary<integer, integer>;
fClassCount: integer;
fClassLabels: array of string;
public
/// Создает классификационный случайный лес:
/// nTrees число деревьев в ансамбле.
@ -762,6 +775,10 @@ type
function GetClasses: array of real;
function Name: string := Self.GetType.Name;
procedure SetClassLabels(classes: array of string);
function GetClassLabels: array of string;
end;
{ Gradient Boosting v1.0 Freeze Checklist
@ -988,6 +1005,8 @@ type
fRandomSeed: integer;
fRng: System.Random;
fUserProvidedSeed: boolean;
fClassLabels: array of string;
private
function FitInternal(XTrain: Matrix; yTrain: Vector; XVal: Matrix; yVal: Vector; useValidation: boolean)
@ -1083,6 +1102,10 @@ type
function Name: string := Self.GetType.Name;
property IsFitted: boolean read fFitted;
procedure SetClassLabels(classes: array of string);
function GetClassLabels: array of string;
end;
//-----------------------------
@ -1155,6 +1178,7 @@ type
fYEnc: array of integer;
fClasses: array of double;
fClassCount: integer;
fClassLabels: array of string;
// ==== voting buffers ====
fVotes: array of double;
@ -1192,6 +1216,10 @@ type
function Clone: IModel; override;
function Name: string := Self.GetType.Name;
procedure SetClassLabels(classes: array of string);
function GetClassLabels: array of string;
end;
@ -1979,6 +2007,10 @@ const
'DBSCAN: Predict поддерживается только для обучающей выборки!!DBSCAN: Predict is only supported for training data';
ER_MODEL_NOT_FITTED =
'Модель "{0}" не обучена. Сначала вызовите Fit()|Model "{0}" is not fitted. Call Fit() first';
ER_DBSCAN_PREDICT_NEW_DATA =
'DBSCAN не поддерживает предсказание для новых данных!!DBSCAN does not support prediction for new data';
ER_CLASSES_NOT_AVAILABLE =
'Метки классов недоступны. Убедитесь, что модель обучена и метки установлены!!Class labels are not available. Ensure the model is fitted and class labels are set';
{$endregion ErrConstants}
@ -2799,6 +2831,19 @@ begin
Result := fIntercept;
end;
procedure LogisticRegression.SetClassLabels(classes: array of string);
begin
fClassLabels := Copy(classes);
end;
function LogisticRegression.GetClassLabels: array of string;
begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
end;
function GiniCriterion.Impurity(y: Vector; indices: array of integer): real;
begin
var n := indices.Length;
@ -3614,6 +3659,19 @@ begin
')';
end;
procedure DecisionTreeClassifier.SetClassLabels(classes: array of string);
begin
fClassLabels := Copy(classes);
end;
function DecisionTreeClassifier.GetClassLabels: array of string;
begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
end;
// DecisionTreeRegressor
constructor DecisionTreeRegressor.Create(maxDepth: integer; minSamplesSplit: integer;
@ -4513,6 +4571,19 @@ begin
Result[i] := fIndexToClass[i];
end;
procedure RandomForestClassifier.SetClassLabels(classes: array of string);
begin
fClassLabels := Copy(classes);
end;
function RandomForestClassifier.GetClassLabels: array of string;
begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
end;
//-----------------------------
// GradientBoostingRegressor
//-----------------------------
@ -6266,6 +6337,19 @@ begin
Result[i] := integer(v[i]);
end;
procedure GradientBoostingClassifier.SetClassLabels(classes: array of string);
begin
fClassLabels := Copy(classes);
end;
function GradientBoostingClassifier.GetClassLabels: array of string;
begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
end;
//-----------------------------
// KNNBase
//-----------------------------
@ -6720,6 +6804,20 @@ begin
end;
end;
procedure KNNClassifier.SetClassLabels(classes: array of string);
begin
fClassLabels := Copy(classes);
end;
function KNNClassifier.GetClassLabels: array of string;
begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
end;
//-----------------------------
// KNNRegressor
//-----------------------------
@ -7326,9 +7424,15 @@ end;
function DBSCAN.PredictLabels(X: Matrix): array of integer;
begin
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if X.RowCount <> Length(fLabels) then
ArgumentError(ER_DBSCAN_PREDICT_NEW_DATA);
Result := Copy(fLabels);
end;

View file

@ -7,6 +7,9 @@ uses PreprocessorABC;
uses DataFrameABC;
uses LinearAlgebraML;
type
TaskKind = (tkRegression, tkClassification);
type
/// DataPipeline конвейер подготовки данных и обучения модели с учителем на DataFrame.
///
@ -30,6 +33,7 @@ type
fDataSteps: List<IPreprocessor>;
fMatrixSteps: List<ITransformer>;
fModel: IModel;
fTask: TaskKind;
fTarget: string;
fFeatures: array of string;
@ -93,17 +97,23 @@ type
// ------------------------------------------------------------
// Пример 4. Классификация без Pipeline
var df := Datasets.Flowers;
// --- Encode target (DataFrame уровень)
df := df.SetCategorical(['species']);
var enc := new LabelEncoder('species');
df := enc.FitTransform(df); // DataFrame уровень
var labels := df.EncodeLabels('species');
var (X,y) := df.ToXY(['length','width'],'species');
// --- X, y
var X := df.ToMatrix(['length','width']);
var y := new Vector(labels);
// --- Matrix уровень
var scaler := new StandardScaler;
X := scaler.FitTransform(X); // Matrix уровень
X := scaler.FitTransform(X);
// --- Модель
var model := new LogisticRegression;
model.Fit(X,y);
model.Fit(X, y);
// Pipeline.Build используется, когда данные уже представлены
// в виде числовой матрицы признаков X и вектора целевой переменной y.
@ -128,11 +138,16 @@ type
/// Строит конвейер из шагов обработки данных и модели.
///
/// Используется в задачах с учителем (с target).
/// Используется в задачах с учителем (с target).
/// task задает тип модели - регрессия или классификация
/// Шаги выполняются последовательно:
/// DataFrame-преобразования (при необходимости) матричные шаги модель.
static function Build(target: string; features: array of string;
params steps: array of IPipelineStep): DataPipeline;
static function Build(
task: TaskKind;
target: string;
features: array of string;
params steps: array of IPipelineStep
): DataPipeline;
/// Обучает конвейер на DataFrame.
/// Семантика:
@ -157,9 +172,10 @@ type
/// Доступен только если конечная модель поддерживает IProbabilisticClassifier.
function PredictProba(df: DataFrame): Matrix;
/// Возвращает список классов в порядке столбцов PredictProba.
/// Доступен только если конечная модель поддерживает IProbabilisticClassifier.
function GetClasses: array of real;
/// Возвращает метки классов в порядке кодирования (0,1,2,...),
/// используемом при EncodeLabels.
/// Доступен только для задач классификации после Fit.
function GetClassLabels: array of string;
/// Признак того, что был вызван Fit или FitTransform.
property IsFitted: boolean read fFitted;
@ -326,7 +342,14 @@ const
'Признак "{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_LABELENCODER_TARGET_NOT_ALLOWED =
'LabelEncoder нельзя применять к целевому столбцу. Используйте EncodeLabels!!LabelEncoder cannot be applied to the target column. Use EncodeLabels instead';
ER_ENCODELABELS_NOT_CATEGORICAL =
'Целевой столбец должен быть категориальным для задач классификации!!Target column must be categorical for classification tasks';
//-----------------------------
// DataPipeline
//-----------------------------
@ -358,6 +381,10 @@ begin
fDataSteps.Add(step as IPreprocessor);
exit(Self);
end;
if step is LabelEncoder(var enc) then
if enc.ColumnName = fTarget then
ArgumentError(ER_LABELENCODER_TARGET_NOT_ALLOWED, fTarget);
// --- Matrix transformer
if step is ITransformer then
@ -383,15 +410,19 @@ begin
Result := Self;
end;
class function DataPipeline.Build(target: string;
features: array of string; params steps: array of IPipelineStep): DataPipeline;
class function DataPipeline.Build(
task: TaskKind;
target: string;
features: array of string;
params steps: array of IPipelineStep
): DataPipeline;
begin
if (target = nil) or (target = '') then
ArgumentError(ER_TARGET_EMPTY);
if (features = nil) or (features.Length = 0) then
ArgumentError(ER_FEATURES_EMPTY);
var seen := new HashSet<string>;
foreach var f in features do
@ -410,10 +441,12 @@ begin
var p := new DataPipeline;
p.fTarget := target;
p.fFeatures := features;
p.fFeatures := Copy(features);
p.fTask := task;
for var i := 0 to High(steps) do
p.Add(steps[i]);
if steps <> nil then
for var i := 0 to High(steps) do
p.Add(steps[i]);
Result := p;
end;
@ -465,7 +498,20 @@ begin
fFinalFeatures := feats.ToArray;
var X := current.ToMatrix(fFinalFeatures);
var y := current.ToVector(fTarget);
var classes: array of string;
var y: Vector;
case fTask of
tkRegression:
y := current.ToVector(fTarget);
tkClassification:
begin
var labels := current.EncodeLabels(fTarget, classes);
y := new Vector(labels);
end;
end;
if X.RowCount <> y.Length then
DimensionError(ER_XY_SIZE_MISMATCH, X.RowCount, y.Length);
@ -488,6 +534,10 @@ begin
if fModel is ISupervisedModel(var supModel) then
fModel := supModel.Fit(X, y)
else ArgumentError(ER_MODEL_NOT_SUPERVISED, fModel.GetType.Name);
if fTask = tkClassification then
if fModel is IClassifier(var cls) then
cls.SetClassLabels(classes);
fFitted := true;
Result := Self;
@ -568,6 +618,13 @@ 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]);
var X := current.ToMatrix(fFinalFeatures);
foreach var t in fMatrixSteps do
@ -576,14 +633,19 @@ begin
Result := (fModel as IProbabilisticClassifier).PredictProba(X);
end;
function DataPipeline.GetClasses: array of real;
function DataPipeline.GetClassLabels: array of string;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if not (fModel is IProbabilisticClassifier) then
ArgumentError(ER_PROBA_NOT_SUPPORTED);
Result := (fModel as IProbabilisticClassifier).GetClasses;
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;
procedure DataPipeline.ValidateSchema(df: DataFrame);
@ -605,7 +667,11 @@ begin
var cols := df.Schema.ColumnNames.JoinToString(', ');
ArgumentError(ER_DATAPIPE_TARGET_NOT_FOUND, fTarget, cols);
end;
if fTask = tkClassification then
if not df.IsCategorical(fTarget) then
ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, fTarget);
var seen := new HashSet<string>;
for var i := 0 to High(fFeatures) do

View file

@ -1057,59 +1057,6 @@ begin
Result := counts;
end;
{class procedure Plot.PairPlot(X: array[,] of real; labels: array of integer; names: array of string);
begin
var n := names.Length;
var fig := Plot.Grid(n,n);
var bins := Round(Sqrt(X.GetLength(0))); //20;
// Диапазоны признаков
var xmin := new real[n];
var xmax := new real[n];
// Верхние границы для гистограмм
var histYMax := new real[n];
for var j := 0 to n-1 do
begin
var col := X.Col(j);
xmin[j] := Floor(col.Min);
xmax[j] := Ceil(col.Max);
var counts := HistogramCounts(col, bins, xmin[j], xmax[j]);
histYMax[j] := counts.Max * 1.1;
end;
for var i := 0 to n-1 do
for var j := 0 to n-1 do
begin
var ax := fig[i,j];
if i = j then
begin
ax.Hist(X.Col(i), bins := bins);
ax.XLim(xmin[j], xmax[j]);
ax.YLim(0, histYMax[j]);
end
else
begin
ax.Points(X.Col(j), X.Col(i), labels, size := 3);
ax.XLim(xmin[j], xmax[j]);
ax.YLim(xmin[i], xmax[i]);
end;
if i = n-1 then
ax.XLabel(names[j]);
if j = 0 then
ax.YLabel(names[i]);
end;
end;}
class procedure Plot.PairPlot(X: array[,] of real; labels: array of integer; names: array of string);
begin
var n := names.Length;

View file

@ -37,9 +37,11 @@ type
function FitTransform(df: DataFrame): DataFrame;
end;
/// Кодирует строковый категориальный столбец в числовые значения
/// Категории фиксируются при Fit
/// Работает только со строковыми столбцами
/// Кодирует строковый категориальный столбец в целочисленные индексы (0,1,2,...).
/// Соответствие значений и индексов фиксируется при вызове Fit
/// в порядке первого появления категорий.
/// Работает только со строковыми столбцами и предназначен для признаков.
/// Не должен применяться к целевому столбцу (target).
LabelEncoder = class(IPreprocessor)
private
col: string;
@ -58,6 +60,8 @@ type
function FitTransform(df: DataFrame): DataFrame;
function ToString: string; override;
property ColumnName: string read col;
end;
/// Кодирует строковый категориальный столбец в набор бинарных (one-hot) столбцов
@ -199,6 +203,12 @@ end;
function LabelEncoder.Fit(df: DataFrame): IPreprocessor;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
if not df.HasColumn(col) then
ArgumentError(ER_COLUMN_NOT_FOUND, col);
var idx := df.Schema.IndexOf(col);
if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctStr then
@ -245,6 +255,8 @@ begin
Result := mapping[s];
end
);
Result := Result.SetCategorical([col]);
end;
function LabelEncoder.FitTransform(df: DataFrame): DataFrame;