diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index 4cf0591d7..c215bb72c 100644 --- a/Configuration/GlobalAssemblyInfo.cs +++ b/Configuration/GlobalAssemblyInfo.cs @@ -15,7 +15,7 @@ internal static class RevisionClass public const string Major = "3"; public const string Minor = "11"; public const string Build = "1"; - public const string Revision = "3784"; + public const string Revision = "3788"; public const string MainVersion = Major + "." + Minor; public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision; diff --git a/Configuration/Version.defs b/Configuration/Version.defs index 379cd499a..1f9ee1aba 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ %COREVERSION%=1 -%REVISION%=3784 +%REVISION%=3788 %MINOR%=11 %MAJOR%=3 diff --git a/InstallerSamples/MachineLearning/08_Datasets/Iris/05_CompareModels.pas b/InstallerSamples/MachineLearning/08_Datasets/Iris/05_CompareModels.pas index 6c6ae31da..a689797f3 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/Iris/05_CompareModels.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/Iris/05_CompareModels.pas @@ -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); diff --git a/InstallerSamples/MachineLearning/08_Datasets/Iris/06_CrossValidation.pas b/InstallerSamples/MachineLearning/08_Datasets/Iris/06_CrossValidation.pas index 3da1a3d5f..76cbf53ae 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/Iris/06_CrossValidation.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/Iris/06_CrossValidation.pas @@ -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), diff --git a/InstallerSamples/MachineLearning/08_Datasets/MoscowHousing/02_PriceHistogram.pas b/InstallerSamples/MachineLearning/08_Datasets/MoscowHousing/02_PriceHistogram.pas index f6679a19c..53ba5c3f3 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/MoscowHousing/02_PriceHistogram.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/MoscowHousing/02_PriceHistogram.pas @@ -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. \ No newline at end of file diff --git a/InstallerSamples/MachineLearning/08_Datasets/_Synthetic/01_MakeBlobs.pas b/InstallerSamples/MachineLearning/08_Datasets/_Synthetic/01_MakeBlobs.pas index e62b15fae..9246c3243 100644 --- a/InstallerSamples/MachineLearning/08_Datasets/_Synthetic/01_MakeBlobs.pas +++ b/InstallerSamples/MachineLearning/08_Datasets/_Synthetic/01_MakeBlobs.pas @@ -11,7 +11,7 @@ begin seed := 1 ); - Plot.Title('MakeBlobs синтетический датасет'); + Plot.Title := 'MakeBlobs синтетический датасет'; var xs := X.Col(0); var ys := X.Col(1); diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index 33bbf79c9..7c4c754a1 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3784 +3.11.1.3788 diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index f5f4b9366..6e04fcac2 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3784' +!define VERSION '3.11.1.3788' diff --git a/bin/Lib/DataAdapters.pas b/bin/Lib/DataAdapters.pas index 7828224d5..97be62488 100644 --- a/bin/Lib/DataAdapters.pas +++ b/bin/Lib/DataAdapters.pas @@ -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; + var map := new Dictionary; - var map := new Dictionary; - - 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; + var map := new Dictionary; + + 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; + var map := new Dictionary; + + // собираем уникальные значения в порядке первого появления + 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; diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index 0c25e9c38..06485efe5 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -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 diff --git a/bin/Lib/DataFrameABCCore.pas b/bin/Lib/DataFrameABCCore.pas index 1274b6646..85c64ad9a 100644 --- a/bin/Lib/DataFrameABCCore.pas +++ b/bin/Lib/DataFrameABCCore.pas @@ -16,7 +16,7 @@ type private fNames: array of string; fTypes: array of ColumnType; - fIsCategorical: array of boolean; + fCategoricalFlags: array of boolean; fIndexByName: Dictionary; class function BuildIndex(names: array of string): Dictionary; @@ -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; diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index cf0c2c218..cfd8e83a2 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -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; diff --git a/bin/Lib/MLCoreABC.pas b/bin/Lib/MLCoreABC.pas index 43cbc78c3..ed8a0186a 100644 --- a/bin/Lib/MLCoreABC.pas +++ b/bin/Lib/MLCoreABC.pas @@ -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; /// Интерфейс классификатора, возвращающего вероятности. diff --git a/bin/Lib/MLDatasets.pas b/bin/Lib/MLDatasets.pas index ff832e6eb..5883eee52 100644 --- a/bin/Lib/MLDatasets.pas +++ b/bin/Lib/MLDatasets.pas @@ -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; diff --git a/bin/Lib/MLExceptions.pas b/bin/Lib/MLExceptions.pas index e719b0548..db02c0e16 100644 --- a/bin/Lib/MLExceptions.pas +++ b/bin/Lib/MLExceptions.pas @@ -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); diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index f932df31b..158803d30 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -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; 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; 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; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 8b8c3b84c..bed5f7399 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -7,6 +7,9 @@ uses PreprocessorABC; uses DataFrameABC; uses LinearAlgebraML; +type + TaskKind = (tkRegression, tkClassification); + type /// DataPipeline — конвейер подготовки данных и обучения модели с учителем на DataFrame. /// @@ -30,6 +33,7 @@ type fDataSteps: List; fMatrixSteps: List; 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; 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; for var i := 0 to High(fFeatures) do diff --git a/bin/Lib/PlotML.pas b/bin/Lib/PlotML.pas index fa10ded78..a26e7b022 100644 --- a/bin/Lib/PlotML.pas +++ b/bin/Lib/PlotML.pas @@ -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; diff --git a/bin/Lib/PreprocessorABC.pas b/bin/Lib/PreprocessorABC.pas index 5f3136deb..2ae6ec475 100644 --- a/bin/Lib/PreprocessorABC.pas +++ b/bin/Lib/PreprocessorABC.pas @@ -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;