From b9aa6ae65d6f85d6d54ab79cc714cd06aa86aab3 Mon Sep 17 00:00:00 2001 From: Mikhalkovich Stanislav Date: Sat, 11 Apr 2026 20:35:52 +0300 Subject: [PATCH] =?UTF-8?q?ML=20-=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3,=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BD=D0=BE=D1=81=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Configuration/GlobalAssemblyInfo.cs | 2 +- Configuration/Version.defs | 4 +- Release/pabcversion.txt | 2 +- ReleaseGenerators/PascalABCNET_version.nsh | 2 +- bin/Lib/DataAdapters.pas | 167 +-- bin/Lib/DataFrameABC.pas | 26 +- bin/Lib/DataFrameABCCore.pas | 27 +- bin/Lib/LinearAlgebraML.pas | 84 +- bin/Lib/MLABC.pas | 2 +- bin/Lib/MLModelsABC.pas | 1202 ++++++++++++++------ bin/Lib/MLPipelineABC.pas | 87 -- bin/Lib/MLUtilsABC.pas | 150 +++ bin/Lib/MetricsABC.pas | 924 ++++++++++++--- bin/Lib/PreprocessorABC.pas | 56 +- bin/Lib/ValidationML.pas | 13 + 15 files changed, 1964 insertions(+), 784 deletions(-) diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index f4c7e90f0..6534ba9a5 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 = "3801"; + public const string Revision = "3804"; 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 8bf88a7eb..a93f97fdf 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ -%MINOR%=11 -%REVISION%=3801 %COREVERSION%=1 +%REVISION%=3804 +%MINOR%=11 %MAJOR%=3 diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index 2699a476a..e49aea18b 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3801 +3.11.1.3804 diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index e81cbcd75..609925cd3 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3801' +!define VERSION '3.11.1.3804' diff --git a/bin/Lib/DataAdapters.pas b/bin/Lib/DataAdapters.pas index c1f9266c9..d64a6487e 100644 --- a/bin/Lib/DataAdapters.pas +++ b/bin/Lib/DataAdapters.pas @@ -2,42 +2,14 @@ interface -uses DataFrameABC; -uses LinearAlgebraML; - -/// Кодирует строковые метки классов в целочисленные индексы. -/// Каждому уникальному значению присваивается номер 0,1,2,... -/// Порядок кодирования соответствует порядку первого появления меток. -/// Используется при обучении моделей и визуализации. -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 (mapping). -/// classes должен быть получен из EncodeLabels. -/// Если встречается неизвестная метка — выбрасывается исключение. -/// Используется для применения кодирования к тестовым данным (Transform). -function TransformLabels(labels: array of string; classes: array of string): array of integer; - -/// Преобразует целочисленные индексы классов обратно в строковые метки. -/// Массив classes задаёт соответствие: classes[i] — имя класса с индексом i. -/// Используется для получения текстовых предсказаний моделей. -function DecodeLabels(y: array of integer; classes: array of string): array of string; - -/// Возвращает список уникальных меток классов. -/// Порядок соответствует первому появлению значений во входном массиве. -/// Используется для определения множества классов в задаче классификации. -function UniqueLabels(labels: array of string): array of string; +// Здесь пока только методы расширения поэтому секция interface - пуста implementation +uses MLUtilsABC; +uses DataFrameABC; uses MLExceptions; +uses LinearAlgebraML; const ER_TO_MATRIX_NO_COLUMNS = @@ -55,66 +27,7 @@ const 'Неподдерживаемый тип столбца "{0}" для EncodeLabels!!Unsupported column type "{0}" for EncodeLabels'; ER_UNKNOWN_CLASS_IN_TRANSFORM = 'Неизвестное значение класса "{0}" при преобразовании меток!!Unknown class value "{0}" in TransformLabels'; - ER_LABEL_INDEX_OUT_OF_RANGE = - 'Индекс метки {0} вне диапазона [0, {1})!!Label index {0} is out of range [0, {1})'; -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 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; - -function TransformLabels(labels: array of string; classes: array of string): array of integer; -begin - if labels = nil then - ArgumentNullError(ER_ARG_NULL, 'labels'); - - var map := new Dictionary; - for var i := 0 to classes.Length - 1 do - map[classes[i]] := i; - - var res := new integer[labels.Length]; - - for var i := 0 to labels.Length - 1 do - begin - var lbl := labels[i]; - - if not map.ContainsKey(lbl) then - Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, lbl); - - res[i] := map[lbl]; - end; - - 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 @@ -162,29 +75,6 @@ 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,... /// Порядок кодирования соответствует порядку первого появления меток. @@ -213,40 +103,15 @@ begin if Self.GetColumnType(target) = ColumnType.ctInt then begin var labels := Self.GetIntColumn(target).ToArray; - Result := EncodeLabelsIntHelper(labels); + var classes: array of integer; + Result := EncodeLabelsInt(labels,classes); 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,... @@ -436,26 +301,6 @@ begin Result := EncodeLabelsInt(labels, classes); end; -function DecodeLabels(y: array of integer; classes: array of string): array of string; -begin - var res := new string[y.Length]; - for var i := 0 to y.Length - 1 do - begin - var idx := y[i]; - - if (idx < 0) or (idx >= classes.Length) then - Error(ER_LABEL_INDEX_OUT_OF_RANGE, idx, classes.Length); - - res[i] := classes[idx]; - end; - - Result := res; -end; - -function UniqueLabels(labels: array of string): array of string; -begin - Result := labels.Distinct.ToArray; -end; end. \ No newline at end of file diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index b1216ccff..fd59ecfba 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -2073,7 +2073,7 @@ begin names[i] := info.Name; types[i] := info.ColType; - // 🔥 берем из старой schema + // берем из старой schema if (fSchema <> nil) and (i < fSchema.ColumnCount) then cats[i] := fSchema.CategoricalFlags[i] else @@ -2970,7 +2970,7 @@ begin Error(ER_UNKNOWN_COLUMN_TYPE); end; - // 🔥 КЛЮЧЕВОЕ: перенос schema + // КЛЮЧЕВОЕ: перенос schema var cats := new boolean[ColumnCount]; for var i := 0 to ColumnCount - 1 do cats[i] := fSchema.CategoricalFlags[i]; @@ -3070,7 +3070,7 @@ begin end; end; - // 🔥 КЛЮЧЕВОЕ: пересобираем schema + // КЛЮЧЕВОЕ: пересобираем schema var n := ColumnCount; var names := new string[n]; var types := new ColumnType[n]; @@ -3622,7 +3622,7 @@ begin end; end; - // 🔥 КЛЮЧЕВОЕ: schema НЕ меняется + // КЛЮЧЕВОЕ: schema НЕ меняется res.SetSchema(fSchema); Result := res; @@ -3677,7 +3677,7 @@ begin end; end; - // 🔥 schema просто копируется + // schema просто копируется res.SetSchema(fSchema); Result := res; @@ -3732,7 +3732,7 @@ begin end; end; - // 🔥 schema НЕ меняется + // schema НЕ меняется res.SetSchema(fSchema); Result := res; @@ -3787,7 +3787,7 @@ begin end; end; - // 🔥 schema просто копируется + // schema просто копируется res.SetSchema(fSchema); Result := res; @@ -4312,7 +4312,7 @@ begin res.AddColumnView(col); end; - // 🔥 пересобираем schema (меняются ТИПЫ) + // пересобираем schema (меняются ТИПЫ) var n := fSchema.ColumnCount; var namesArr := new string[n]; @@ -4743,7 +4743,7 @@ begin end; names.Add(colName); - cats.Add(true); // 🔥 ключи — categorical + cats.Add(true); // ключи — categorical end; res.AddIntColumn('count', counts, nil); @@ -4752,7 +4752,7 @@ begin cats.Add(false); end; - // 🔥 устанавливаем schema + // устанавливаем schema res.SetSchema(new DataFrameSchema( names.ToArray, types.ToArray, @@ -5322,7 +5322,7 @@ begin schemaNames.Add('Feature'); schemaTypes.Add(ctStr); - schemaCats.Add(true); // 🔥 categorical + schemaCats.Add(true); // categorical // 2️⃣ корреляции for var j := 0 to n - 1 do @@ -5342,7 +5342,7 @@ begin schemaCats.Add(false); end; - // 🔥 schema + // schema res.SetSchema(new DataFrameSchema( schemaNames.ToArray, schemaTypes.ToArray, @@ -6324,7 +6324,7 @@ begin else types[j] := ctStr; - // 🔥 вот сюда переносим логику categorical + // вот сюда переносим логику categorical cats[j] := ((catSet <> nil) and (headers[j] in catSet)) or autoCat[j]; end; diff --git a/bin/Lib/DataFrameABCCore.pas b/bin/Lib/DataFrameABCCore.pas index caac83eea..0e7e35aee 100644 --- a/bin/Lib/DataFrameABCCore.pas +++ b/bin/Lib/DataFrameABCCore.pas @@ -64,7 +64,7 @@ type ColumnInfo = auto class Name: string; ColType: ColumnType; - //IsCategorical: boolean; - мы убрали это отсюда - только Schema - источник истины! + //IsCategorical - только в Schema! end; DataFrameCursor = class; @@ -84,15 +84,11 @@ type function TryGetNumericValue(i: integer; var value: real): boolean; virtual; abstract; /// Возвращает количество строк в столбце function RowCount: integer; virtual; abstract; - /// Добавляет невалидное (NA) значение в конец столбца - //procedure AppendInvalid; virtual; abstract; - /// Добавляет значение из курсора в указанной позиции - //procedure AppendFromCursor(cur: DataFrameCursor; colIndex: integer); virtual; abstract; end; /// Столбец целых чисел IntColumn = class(Column) - // ⚠️ Data и IsValid считаются immutable после создания + // Data и IsValid считаются immutable после создания Data: array of integer; // Данные столбца IsValid: array of boolean; // Флаги валидности (может быть nil) public @@ -103,17 +99,6 @@ type function TryGetNumericValue(i: integer; var value: real): boolean; override; /// Возвращает количество строк в столбце function RowCount: integer; override := Data.Length; - /// Добавляет невалидное (NA) значение в конец столбца - /// ⚠ УСТАРЕВШИЙ МЕТОД. - /// Не должен использоваться в новом коде. - /// Сохраняется только для обратной совместимости. - //procedure AppendInvalid; override; - /// Добавляет значение из курсора в указанной позиции - /// ⚠ УСТАРЕВШИЙ МЕТОД. - /// Использует неэффективное поэлементное добавление (O(n²)). - /// Не должен использоваться в новом коде. - /// Сохраняется только для обратной совместимости. - //procedure AppendFromCursor(cur: DataFrameCursor; colIndex: integer); override; end; /// Столбец вещественных чисел @@ -128,10 +113,6 @@ type function TryGetNumericValue(i: integer; var value: real): boolean; override; /// Возвращает количество строк в столбце function RowCount: integer; override := Data.Length; - /// Добавляет невалидное (NA) значение в конец столбца - //procedure AppendInvalid; override; - /// Добавляет значение из курсора в указанной позиции - //procedure AppendFromCursor(cur: DataFrameCursor; colIndex: integer); override; end; /// Столбец строк @@ -164,10 +145,6 @@ type function TryGetNumericValue(i: integer; var value: real): boolean; override; /// Возвращает количество строк в столбце function RowCount: integer; override := Data.Length; - /// Добавляет невалидное (NA) значение в конец столбца - //procedure AppendInvalid; override; - /// Добавляет значение из курсора в указанной позиции - //procedure AppendFromCursor(cur: DataFrameCursor; colIndex: integer); override; end; // Accessor типы для курсора diff --git a/bin/Lib/LinearAlgebraML.pas b/bin/Lib/LinearAlgebraML.pas index 96772069b..1561f3f0a 100644 --- a/bin/Lib/LinearAlgebraML.pas +++ b/bin/Lib/LinearAlgebraML.pas @@ -195,6 +195,8 @@ type function GetRow(i: integer): Vector; function GetCol(j: integer): Vector; + + function TakeRows(indices: array of integer): Matrix; // ---------- Статические методы ---------- /// Возвращает единичную матрицу размера n @@ -931,7 +933,6 @@ begin end; - static function Matrix.operator *(A: Matrix; x: Vector): Vector; begin CheckVecSize(A, x); @@ -945,7 +946,7 @@ begin end; end; -static function Matrix.operator *(A, B: Matrix): Matrix; +{static function Matrix.operator *(A, B: Matrix): Matrix; begin CheckMulSize(A, B); Result := new Matrix(A.RowCount, B.ColCount); @@ -957,6 +958,30 @@ begin for var j := 0 to B.ColCount - 1 do Result.fdata[i, j] += aik * B.fdata[k, j]; end; +end;} + +static function Matrix.operator *(A, B: Matrix): Matrix; +begin + CheckMulSize(A, B); + + var m := A.RowCount; + var p := A.ColCount; + var n := B.ColCount; + + var BT := B.Transpose; + + Result := new Matrix(m, n); + + for var i := 0 to m - 1 do + for var j := 0 to n - 1 do + begin + var sum := 0.0; + + for var k := 0 to p - 1 do + sum += A.fdata[i, k] * BT.fdata[j, k]; + + Result.fdata[i, j] := sum; + end; end; static function Matrix.operator +=(A, B: Matrix): Matrix; @@ -1002,6 +1027,61 @@ begin Result[j] := fdata[i, j]; end; +function Matrix.TakeRows(indices: array of integer): Matrix; +begin + var n := indices.Length; + var p := ColCount; + + var res := new Matrix(n, p); + + var src := Data; + var dst := res.Data; + + // --- fast path: полный срез [0..n-1] + var isFull := n = RowCount; + + if isFull then + begin + for var i := 0 to n - 1 do + if indices[i] <> i then + begin + isFull := False; + break; + end; + end; + + if isFull then + begin + System.Array.Copy(src, 0, dst, 0, n * p); + Result := res; + exit; + end; + + // --- обычный block-copy + var i := 0; + var dstOffset := 0; + + while i < n do + begin + var start := indices[i]; + var len := 1; + + while (i + len < n) and (indices[i + len] = start + len) do + len += 1; + + System.Array.Copy( + src, start * p, + dst, dstOffset, + len * p + ); + + dstOffset += len * p; + i += len; + end; + + Result := res; +end; + function Matrix.GetCol(j: integer): Vector; begin if (j < 0) or (j >= ColCount) then diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index f83135189..bef02847e 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -125,6 +125,6 @@ begin Result := MLUtilsABC.LabelsToInts(y); end; -function EncodeLabels(labels: array of string): array of integer := DataAdapters.EncodeLabels(labels); +function EncodeLabels(labels: array of string): array of integer := MLUtilsABC.EncodeLabels(labels); end. \ No newline at end of file diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 1000b5f6e..23d204827 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -209,6 +209,44 @@ type function Name: string := Self.GetType.Name; end; + + /// Модель Lasso-регрессии (линейная регрессия с L1-регуляризацией). + /// Минимизирует квадратичную ошибку с L1-штрафом на веса, что приводит к разреженным решениям. + /// Внутренне реализована через ElasticNet с нулевой L2-регуляризацией + LassoRegression = class(IRegressor) + private + fModel: ElasticNet; + public + /// Создаёт модель Lasso-регрессии. + /// alpha — коэффициент L1-регуляризации. + /// maxIter — максимальное число итераций обучения. + /// tol — порог сходимости. + constructor Create( + alpha: real := 1.0; + maxIter: integer := 1000; + tol: real := 1e-6 + ); + + /// Обучает модель на данных. + /// X — матрица признаков размера [nSamples x nFeatures]. + /// y — вектор целевых значений длины nSamples. + /// Возвращает текущий экземпляр модели. + function Fit(X: Matrix; y: Vector): ISupervisedModel; + + /// Предсказывает значения целевой переменной для входных данных. + /// X — матрица признаков размера [nSamples x nFeatures]. + /// Возвращает вектор предсказаний длины nSamples. + function Predict(X: Matrix): Vector; + + /// Создаёт полную копию модели вместе с обученными параметрами. + function Clone: IModel; + + /// Показывает, была ли модель обучена. + /// После вызова Fit значение становится True. + property IsFitted: boolean read fModel.fFitted; + + function Name: string := Self.GetType.Name; + end; /// Логистическая регрессия. /// Поддерживает бинарную и многоклассовую классификацию. @@ -228,9 +266,9 @@ type fClassToIndex: Dictionary; fIndexToClass: array of integer; - fTol: real := 1e-6; - fCheckConvergence: boolean := true; - fMinImprovement: real := 1e-8; + fTol: real; + fCheckConvergence: boolean; + fMinImprovement: real; fClassLabels: array of string; // В каждой модели классификации @@ -240,7 +278,14 @@ type /// lambda — коэффициент L2-регуляризации. /// lr — шаг градиентного спуска. /// epochs — число итераций обучения - constructor Create(lambda: real := 0.0; lr: real := 0.1; epochs: integer := 1000); + constructor Create( + lambda: real := 0.0; + learningRate: real := 0.01; + epochs: integer := 1000; + tol: real := 1e-6; + checkConvergence: boolean := true; + minImprovement: real := 1e-8 + ); /// Обучает модель логистической регрессии. /// X — матрица признаков. @@ -372,7 +417,71 @@ type /// Вычисляет энтропию распределения классов в узле. /// Чем меньше значение — тем более однороден узел по классам. function Impurity(y: Vector; indices: array of integer): real; + end; + + /// Внутреннее ядро дерева решений (Decision Tree). + /// Реализует алгоритм построения и предсказания без учёта кодирования меток. + /// + /// Ожидает, что целевые значения y уже закодированы в диапазон 0..K-1. + /// Возвращает предсказания в том же закодированном виде. + /// + /// Не выполняет: + /// - кодирование/декодирование меток + /// - проверку корректности входных данных + /// + /// Используется как вычислительный модуль внутри моделей + /// (например, DecisionTreeClassifier, RandomForest и др.) + DecisionTreeCore = class + private + fRoot: DecisionTreeNode; + fMaxDepth: integer; + fMinSamplesSplit: integer; + fMinSamplesLeaf: integer; + fCriterion: ISplitCriterion; + fClassCount: integer; + fMaxFeatures: integer; + fUserProvidedSeed: boolean; + fRandomSeed: integer; + fRng: System.Random; + fFeatureImportances: Vector; + + public + constructor Create( + maxDepth: integer; + minSamplesSplit: integer; + minSamplesLeaf: integer; + criterion: ISplitCriterion; + maxFeatures: integer; + seed: integer := -1 + ); + + procedure Fit(X: Matrix; y: Vector); // y уже 0..K-1 + function Predict(X: Matrix): Vector; // возвращает 0..K-1 + function PredictRow(X: Matrix; row: integer): integer; + + private + function CreateLeaf(y: Vector; indices: array of integer): DecisionTreeNode; + + function BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; + + function FindBestSplit(X: Matrix; y: Vector; indices: array of integer; + var bestF: integer; var bestT: real): boolean; + + procedure Split( + X: Matrix; y: Vector; + f: integer; t: real; + var Xl: Matrix; var yl: Vector; + var Xr: Matrix; var yr: Vector + ); + + property FeatureImportances: Vector read fFeatureImportances; + + function MajorityClass(y: Vector; indices: array of integer): integer; + + function PredictOne(x: Vector; node: DecisionTreeNode): integer; + function Clone: DecisionTreeCore; end; + //============================ // DecisionTreeBase //============================ @@ -456,10 +565,8 @@ type /// В листьях хранится наиболее частый класс DecisionTreeClassifier = class(DecisionTreeBase, IClassifier) private - fClassToIndex: Dictionary; + fCore: DecisionTreeCore; fIndexToClass: array of integer; - fClassCount: integer; - fClassLabels: array of string; function PredictOne(X: Matrix; rowIndex: integer): integer; @@ -501,7 +608,7 @@ type /// Возвращает строковое представление модели. function ToString: string; override; - function ClassCount: integer := fClassCount; + function ClassCount: integer := fIndexToClass.Length; function IndexToClass: array of integer := Copy(fIndexToClass); @@ -723,7 +830,7 @@ type /// Итоговое предсказание формируется голосованием деревьев или агрегацией вероятностей классов RandomForestClassifier = class(RandomForestBase, IProbabilisticClassifier) private - fTrees: array of DecisionTreeClassifier; + fTrees: array of DecisionTreeCore; fIndexToClass: array of integer; fClassToIndex: Dictionary; fClassCount: integer; @@ -756,11 +863,17 @@ type function Predict(X: Matrix): Vector; override; /// Возвращает предсказанные метки классов для объектов из X. - /// Каждый элемент результата — индекс класса (целое число). + /// Каждый элемент результата — исходная метка класса (целое число). /// Порядок элементов соответствует строкам матрицы X. /// Требует предварительного вызова Fit. function PredictLabels(X: Matrix): array of integer; + /// Возвращает оценки вероятностей принадлежности объектов из X к каждому классу. + /// Результат — матрица размера [nSamples x nClasses]. + /// Элемент [i, k] содержит вероятность того, что объект i принадлежит классу k. + /// Порядок классов соответствует fIndexToClass. + /// Сумма вероятностей по строке равна 1. + /// Требует предварительного вызова Fit. function PredictProba(X: Matrix): Matrix; /// Создает глубокую копию случайного леса классификации. @@ -1019,8 +1132,8 @@ type function FitInternal(XTrain: Matrix; yTrain: Vector; XVal: Matrix; yVal: Vector; useValidation: boolean) : ISupervisedModel; - procedure BuildClassMapping(y: Vector); - function ApplyLabelEncoding(y: Vector): array of integer; + //procedure BuildClassMapping(y: Vector); + //function ApplyLabelEncoding(y: Vector): array of integer; procedure SoftmaxRow(var logits: array of real; var probs: array of real); procedure SoftmaxMatrix(logits: Matrix; probs: Matrix); @@ -1198,8 +1311,6 @@ type fTouched: array of integer; fEpoch: integer; - procedure EncodeClasses(y: Vector); - public /// Создаёт классификатор kNN. /// k — число ближайших соседей (k > 0). @@ -1815,14 +1926,17 @@ type {$endregion Utility functions} - -var - /// Проверять ли входные данные моделей на NaN, Inf - ValidateFiniteInputs := True; - +type + MLConfig = static class + public + /// Проверять ли входные данные моделей на NaN, Inf + static ValidateFiniteInputs: boolean := True; + end; + implementation uses MLExceptions; +uses MLUtilsABC; {$region ErrConstants} const @@ -2025,8 +2139,11 @@ const ER_PREDICT_NOT_SUPPORTED = 'Модель не поддерживает Predict для данного типа алгоритма!!' + 'Model does not support Predict for this type of algorithm'; + ER_NEED_AT_LEAST_TWO_CLASSES = + 'Необходимо как минимум два различных класса!!At least two distinct classes are required'; + ER_UNKNOWN_CLASS_LABEL = + 'Неизвестная метка класса: {0}!!Unknown class label: {0}'; - {$endregion ErrConstants} //----------------------------- @@ -2097,7 +2214,7 @@ end; function LinearRegression.Fit(X: Matrix; y: Vector): ISupervisedModel; begin - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -2143,7 +2260,7 @@ begin if not ffitted then NotFittedError(ER_FIT_NOT_CALLED); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fcoef.Length then @@ -2228,7 +2345,7 @@ end; function RidgeRegression.Fit(X: Matrix; y: Vector): ISupervisedModel; begin - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -2276,7 +2393,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fCoef.Length then @@ -2348,7 +2465,7 @@ begin if y = nil then ArgumentNullError(ER_Y_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -2474,7 +2591,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fCoef.Length then @@ -2509,16 +2626,48 @@ begin Result := m; end; +//----------------------------- +// LassoRegression +//----------------------------- + +constructor LassoRegression.Create(alpha: real; maxIter: integer; tol: real); +begin + // Lasso = ElasticNet с L2 = 0 + new ElasticNet(alpha, 0.0, maxIter, tol); +end; + +function LassoRegression.Fit(X: Matrix; y: Vector): ISupervisedModel; +begin + fModel.Fit(X, y); + Result := Self; +end; + +function LassoRegression.Predict(X: Matrix): Vector; +begin + Result := fModel.Predict(X); +end; + +function LassoRegression.Clone: IModel; +begin + var m := new LassoRegression; + m.fModel := ElasticNet(fModel.Clone); + Result := m; +end; + //----------------------------- // LogisticRegression //----------------------------- -constructor LogisticRegression.Create(lambda: real; lr: real; epochs: integer); +constructor LogisticRegression.Create(lambda: real; learningRate: real; epochs: integer; + tol: real; checkConvergence: boolean; minImprovement: real); begin fLambda := lambda; - fLearningRate := lr; + fLearningRate := learningRate; fEpochs := epochs; fFitted := false; + fTol := tol; + fCheckConvergence := checkConvergence; + fMinImprovement := minImprovement; end; function LogisticRegression.Fit(X: Matrix; y: Vector): ISupervisedModel; @@ -2529,7 +2678,7 @@ begin if y = nil then ArgumentNullError(ER_Y_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -2556,39 +2705,9 @@ begin var m := X.RowCount; var p := X.ColCount; - // --- internal encoding - var classes := new HashSet; + // --- convert to integer labels + var yInt := new integer[m]; - for var i := 0 to y.Length - 1 do - begin - var r := y[i]; - var ir := Round(r); - - if Abs(r - ir) > 1e-12 then - ArgumentError(ER_LABELS_NOT_INTEGER); - - classes.Add(ir); - end; - - var unique := classes.ToArray; - &Array.Sort(unique); - - fClassCount := unique.Length; - - if fClassCount < 2 then - ArgumentError(ER_LOGISTIC_NEED_AT_LEAST_TWO_CLASSES); - - fClassToIndex := new Dictionary; - SetLength(fIndexToClass, fClassCount); - - for var i := 0 to fClassCount - 1 do - begin - fClassToIndex[unique[i]] := i; - fIndexToClass[i] := unique[i]; - end; - - // --- encode labels - var yEncoded := new integer[m]; for var i := 0 to m - 1 do begin var r := y[i]; @@ -2597,7 +2716,26 @@ begin if Abs(r - ir) > 1e-12 then ArgumentError(ER_LABELS_NOT_INTEGER); - yEncoded[i] := fClassToIndex[ir]; + yInt[i] := ir; + end; + + // --- encode (порядок первого появления) + var unique: array of integer; + var yEncoded := EncodeLabelsInt(yInt, unique); + + fClassCount := unique.Length; + + if fClassCount < 2 then + ArgumentError(ER_LOGISTIC_NEED_AT_LEAST_TWO_CLASSES); + + // --- build mappings + fClassToIndex := new Dictionary; + SetLength(fIndexToClass, fClassCount); + + for var i := 0 to fClassCount - 1 do + begin + fClassToIndex[unique[i]] := i; + fIndexToClass[i] := unique[i]; end; // --- init @@ -2685,20 +2823,31 @@ begin for var i := 0 to m - 1 do begin var yi := yEncoded[i]; - + + // заранее считаем diff[k] + var diffArr := new real[fClassCount]; + for var k := 0 to fClassCount - 1 do begin var diff := Z[i,k]; if k = yi then diff -= 1.0; - + + diffArr[k] := diff; gradB[k] += diff; - - for var j := 0 to p - 1 do - gradW[j,k] += X[i,j] * diff; + end; + + // теперь идём по j (строка X читается линейно) + for var j := 0 to p - 1 do + begin + var xij := X[i,j]; + + for var k := 0 to fClassCount - 1 do + gradW[j,k] += xij * diffArr[k]; end; end; + gradW *= 1.0 / m; gradB *= 1.0 / m; @@ -2721,7 +2870,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fW.RowCount then @@ -2997,6 +3146,350 @@ end; const MAX_ALLOWED_TREE_DEPTH = 1000; +// DecisionTreeCore + +constructor DecisionTreeCore.Create( + maxDepth: integer; + minSamplesSplit: integer; + minSamplesLeaf: integer; + criterion: ISplitCriterion; + maxFeatures: integer; + seed: integer +); +begin + fMaxDepth := maxDepth; + fMinSamplesSplit := minSamplesSplit; + fMinSamplesLeaf := minSamplesLeaf; + fCriterion := criterion; + fMaxFeatures := maxFeatures; + + // --- seed (в твоём стиле) + if seed < 0 then + begin + fUserProvidedSeed := false; + fRandomSeed := System.Environment.TickCount and integer.MaxValue; + end + else + begin + fUserProvidedSeed := true; + fRandomSeed := seed; + end; + + fRng := new System.Random(fRandomSeed); +end; + +procedure DecisionTreeCore.Fit(X: Matrix; y: Vector); +begin + // --- init importance + fFeatureImportances := new Vector(X.ColCount); + + // --- определить число классов (0..K-1) + fClassCount := 0; + + for var i := 0 to y.Length - 1 do + begin + var v := Round(y[i]); + if v + 1 > fClassCount then + fClassCount := v + 1; + end; + + // --- build tree + var indices := new integer[X.RowCount]; + for var i := 0 to X.RowCount - 1 do + indices[i] := i; + + fRoot := BuildNode(X, y, indices, 0); + + // --- нормализация importance + var s := fFeatureImportances.Sum; + if s > 0 then + for var i := 0 to fFeatureImportances.Length - 1 do + fFeatureImportances[i] /= s; +end; + +function DecisionTreeCore.Predict(X: Matrix): Vector; +begin + Result := new Vector(X.RowCount); + + for var i := 0 to X.RowCount - 1 do + Result[i] := PredictOne(X.GetRow(i), fRoot); +end; + +function DecisionTreeCore.PredictRow(X: Matrix; row: integer): integer; +var node: DecisionTreeNode; +begin + node := fRoot; + + while not node.IsLeaf do + begin + if X[row, node.FeatureIndex] <= node.Threshold then + node := node.Left + else + node := node.Right; + end; + + Result := Round(node.LeafValue); +end; + +function DecisionTreeCore.PredictOne(x: Vector; node: DecisionTreeNode): integer; +begin + if node.IsLeaf then + exit(Round(node.LeafValue)); + + if x[node.FeatureIndex] <= node.Threshold then + Result := PredictOne(x, node.Left) + else + Result := PredictOne(x, node.Right); +end; + +function DecisionTreeCore.MajorityClass(y: Vector; indices: array of integer): integer; +begin + var counts := new integer[fClassCount]; + + for var i := 0 to indices.Length - 1 do + begin + var cls := Round(y[indices[i]]); + counts[cls] += 1; + end; + + var best := 0; + var bestCnt := counts[0]; + + for var k := 1 to fClassCount - 1 do + if counts[k] > bestCnt then + begin + bestCnt := counts[k]; + best := k; + end; + + Result := best; +end; + +function DecisionTreeCore.CreateLeaf(y: Vector; indices: array of integer): DecisionTreeNode; +begin + Result := new DecisionTreeNode; + Result.IsLeaf := true; + Result.LeafValue := MajorityClass(y, indices); +end; + +function DecisionTreeCore.BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode; +begin + // 1. stop: depth + if depth >= fMaxDepth then + exit(CreateLeaf(y, indices)); + + // 2. stop: min samples + if indices.Length < fMinSamplesSplit then + exit(CreateLeaf(y, indices)); + + // --- impurity родителя + var parentImp := fCriterion.Impurity(y, indices); + + // 3. лучший сплит (пока используем старый API) + var bestFeature: integer; + var bestThreshold: real; + + if not FindBestSplit(X, y, indices, bestFeature, bestThreshold) then + exit(CreateLeaf(y, indices)); + + // 4. split → только индексы + var leftList := new List; + var rightList := new List; + + for var ii := 0 to indices.Length - 1 do + begin + var i := indices[ii]; + + if X[i, bestFeature] <= bestThreshold then + leftList.Add(i) + else + rightList.Add(i); + end; + + if (leftList.Count < fMinSamplesLeaf) or (rightList.Count < fMinSamplesLeaf) then + exit(CreateLeaf(y, indices)); + + var leftIdx := leftList.ToArray; + var rightIdx := rightList.ToArray; + + // --- impurity детей + var leftImp := fCriterion.Impurity(y, leftIdx); + var rightImp := fCriterion.Impurity(y, rightIdx); + + var weighted := + (leftIdx.Length * leftImp + rightIdx.Length * rightImp) / indices.Length; + + var gain := parentImp - weighted; + + // --- FEATURE IMPORTANCE + if gain > 0 then + fFeatureImportances[bestFeature] += gain; + + // 5. recursion + var left := BuildNode(X, y, leftIdx, depth + 1); + var right := BuildNode(X, y, rightIdx, depth + 1); + + // 6. node + Result := new DecisionTreeNode; + Result.IsLeaf := false; + Result.FeatureIndex := bestFeature; + Result.Threshold := bestThreshold; + Result.Left := left; + Result.Right := right; +end; + +procedure DecisionTreeCore.Split(X: Matrix; y: Vector; f: integer; t: real; + var Xl: Matrix; var yl: Vector; + var Xr: Matrix; var yr: Vector +); +begin + var leftIdx := new List; + var rightIdx := new List; + + // 1. Разделяем индексы + for var i := 0 to X.RowCount - 1 do + if X[i, f] <= t then + leftIdx.Add(i) + else + rightIdx.Add(i); + + // 2. Формируем подматрицы и подвекторы + var leftArr := leftIdx.ToArray; + var rightArr := rightIdx.ToArray; + + Xl := X.TakeRows(leftArr); + yl := y.SubvectorBy(leftArr); + + Xr := X.TakeRows(rightArr); + yr := y.SubvectorBy(rightArr); +end; + +function DecisionTreeCore.FindBestSplit( + X: Matrix; + y: Vector; + indices: array of integer; + var bestF: integer; + var bestT: real +): boolean; +begin + Result := false; + + var n := indices.Length; + if n = 0 then exit; + + var parentImp := fCriterion.Impurity(y, indices); + + var bestScore := parentImp; + + // --- выбор признаков (с учётом maxFeatures) + var featureCount := X.ColCount; + var features: array of integer; + + if (fMaxFeatures > 0) and (fMaxFeatures < featureCount) then + begin + var perm := new integer[featureCount]; + for var i := 0 to featureCount - 1 do + perm[i] := i; + + for var i := 0 to fMaxFeatures - 1 do + begin + var j := i + fRng.Next(featureCount - i); + var tmp := perm[i]; + perm[i] := perm[j]; + perm[j] := tmp; + end; + + SetLength(features, fMaxFeatures); + for var i := 0 to fMaxFeatures - 1 do + features[i] := perm[i]; + end + else + begin + SetLength(features, featureCount); + for var i := 0 to featureCount - 1 do + features[i] := i; + end; + + // --- перебор + for var fi := 0 to features.Length - 1 do + begin + var f := features[fi]; + + var leftBuf := new integer[n]; + var rightBuf := new integer[n]; + + for var ii := 0 to n - 1 do + begin + var i := indices[ii]; + var t := X[i, f]; + + var lcnt := 0; + var rcnt := 0; + + for var jj := 0 to n - 1 do + begin + var j := indices[jj]; + + if X[j, f] <= t then + begin + leftBuf[lcnt] := j; + lcnt += 1; + end + else + begin + rightBuf[rcnt] := j; + rcnt += 1; + end; + end; + + if (lcnt < fMinSamplesLeaf) or (rcnt < fMinSamplesLeaf) then + continue; + + // создаём массивы нужного размера + var leftIdx := new integer[lcnt]; + var rightIdx := new integer[rcnt]; + + for var i1 := 0 to lcnt - 1 do + leftIdx[i1] := leftBuf[i1]; + + for var i1 := 0 to rcnt - 1 do + rightIdx[i1] := rightBuf[i1]; + + var leftImp := fCriterion.Impurity(y, leftIdx); + var rightImp := fCriterion.Impurity(y, rightIdx); + + var score := + (leftIdx.Length * leftImp + rightIdx.Length * rightImp) / n; + + if score < bestScore then + begin + bestScore := score; + bestF := f; + bestT := t; + Result := true; + end; + end; + end; +end; + +function DecisionTreeCore.Clone: DecisionTreeCore; +begin + Result := new DecisionTreeCore( + fMaxDepth, + fMinSamplesSplit, + fMinSamplesLeaf, + fCriterion, + fMaxFeatures, + fRandomSeed + ); + + // копируем дерево + if fRoot <> nil then + Result.fRoot := fRoot.Clone; +end; + +// DecisionTreeBase + constructor DecisionTreeBase.Create( maxDepth: integer; minSamplesSplit: integer; @@ -3373,7 +3866,7 @@ begin var cls := Round(y[row]); - if (cls < 0) or (cls >= fClassCount) then + if (cls < 0) or (cls >= ClassCount) then ArgumentError(ER_LABEL_INDEX_INVALID); labels[i] := cls; @@ -3381,11 +3874,11 @@ begin System.Array.Sort(values, labels); - var rightCounts := new integer[fClassCount]; + var rightCounts := new integer[ClassCount]; for var i := 0 to n - 1 do rightCounts[labels[i]] += 1; - var leftCounts := new integer[fClassCount]; + var leftCounts := new integer[ClassCount]; var leftSize := 0; var rightSize := n; @@ -3409,7 +3902,7 @@ begin // ----- GINI LEFT ----- var giniLeft := 1.0; - for var c := 0 to fClassCount - 1 do + for var c := 0 to ClassCount - 1 do begin if leftCounts[c] > 0 then begin @@ -3424,7 +3917,7 @@ begin // ----- GINI RIGHT ----- var giniRight := 1.0; - for var c := 0 to fClassCount - 1 do + for var c := 0 to ClassCount - 1 do begin if rightCounts[c] > 0 then begin @@ -3475,7 +3968,7 @@ end; function DecisionTreeClassifier.MajorityClass(y: Vector; indices: array of integer): integer; begin - var counts := new integer[fClassCount]; + var counts := new integer[ClassCount]; // Подсчёт частот foreach var i in indices do @@ -3488,7 +3981,7 @@ begin var bestClass := 0; var bestCount := -1; - for var c := 0 to fClassCount - 1 do + for var c := 0 to ClassCount - 1 do if counts[c] > bestCount then begin bestCount := counts[c]; @@ -3514,7 +4007,7 @@ begin if y = nil then ArgumentNullError(ER_Y_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -3526,69 +4019,53 @@ begin if X.RowCount <> y.Length then DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length); - fFeatureImportances := new Vector(X.ColCount); - // --- 1. Найти уникальные классы (с проверкой целочисленности) - var classes := new HashSet; - - for var i := 0 to y.Length - 1 do + // --- convert to integer labels + var m := y.Length; + var yInt := new integer[m]; + + for var i := 0 to m - 1 do begin var r := y[i]; var ir := Round(r); - + if Abs(r - ir) > 1e-12 then ArgumentError(ER_LABELS_NOT_INTEGER); - - classes.Add(ir); - end; - - fClassCount := classes.Count; - - // --- 2. Отсортировать классы для детерминизма - fIndexToClass := classes.ToArray; - &Array.Sort(fIndexToClass); - - fClassToIndex := new Dictionary; - - for var idx := 0 to fClassCount - 1 do - fClassToIndex[fIndexToClass[idx]] := idx; - - fCriterion := new GiniCriterion(fClassCount); - - // --- 3. Создать закодированный y - var yEncoded := new Vector(y.Length); - - for var i := 0 to y.Length - 1 do - begin - var ir := Round(y[i]); // безопасно после проверки - yEncoded[i] := fClassToIndex[ir]; + + yInt[i] := ir; end; - // --- 4. Создать массив индексов строк - var indices: array of integer; + // --- encode + var classes: array of integer; + var yEncArr := EncodeLabelsInt(yInt, classes); - if fRowIndices <> nil then - indices := fRowIndices - else - begin - indices := new integer[X.RowCount]; - for var i := 0 to X.RowCount - 1 do - indices[i] := i; - end; + if classes.Length < 2 then + ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES); - // --- 5. Построить дерево - fRoot := BuildTree(X, yEncoded, indices, 0); - // Сбросить подмножество строк (не должно протекать на следующий Fit) - fRowIndices := nil; - - var s := fFeatureImportances.Sum; - if s > 0 then - for var i := 0 to fFeatureImportances.Length-1 do - fFeatureImportances[i] /= s; + fIndexToClass := classes; + + // --- criterion + if fCriterion = nil then + fCriterion := new GiniCriterion(classes.Length); + + // --- encoded vector + var yEncoded := new Vector(m); + for var i := 0 to m - 1 do + yEncoded[i] := yEncArr[i]; + + // --- Core + fCore := new DecisionTreeCore( + fMaxDepth, + fMinSamplesSplit, + fMinSamplesLeaf, + fCriterion, + 0 // maxFeatures = 0 -> использовать все признаки + ); + + fCore.Fit(X, yEncoded); fFitted := true; - Result := Self; end; @@ -3600,19 +4077,21 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureImportances.Length then DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureImportances.Length); - var n := X.RowCount; + var predIdx := fCore.Predict(X); + + var n := predIdx.Length; Result := new Vector(n); for var i := 0 to n - 1 do begin - var internalClass := PredictOne(X, i); - Result[i] := fIndexToClass[internalClass]; + var k := Round(predIdx[i]); + Result[i] := fIndexToClass[k]; end; end; @@ -3623,7 +4102,7 @@ begin Result := new integer[v.Length]; for var i := 0 to v.Length - 1 do - Result[i] := integer(v[i]); + Result[i] := Round(v[i]); end; function DecisionTreeClassifier.Clone: IModel; @@ -3638,16 +4117,7 @@ begin CopyBaseState(m); - // --- классы - m.fClassCount := fClassCount; - - if fClassToIndex <> nil then - begin - m.fClassToIndex := new Dictionary; - foreach var kv in fClassToIndex do - m.fClassToIndex[kv.Key] := kv.Value; - end; - + // --- classes (единственный источник истины) if fIndexToClass <> nil then begin SetLength(m.fIndexToClass, Length(fIndexToClass)); @@ -3655,6 +4125,21 @@ begin m.fIndexToClass[i] := fIndexToClass[i]; end; + // --- core (глубокая копия дерева) + if fCore <> nil then + begin + // предполагаем, что Node.Clone уже есть + m.fCore := new DecisionTreeCore( + fMaxDepth, + fMinSamplesSplit, + fMinSamplesLeaf, + fCriterion, + fMaxFeatures + ); + + m.fCore.fRoot := fCore.fRoot.Clone; // ключевой момент + end; + Result := m; end; @@ -3762,7 +4247,7 @@ begin if y = nil then ArgumentNullError(ER_Y_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -3828,7 +4313,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureImportances.Length then @@ -3976,7 +4461,7 @@ begin if y = nil then ArgumentNullError(ER_Y_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -4093,7 +4578,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureCount then @@ -4211,7 +4696,7 @@ begin if y = nil then ArgumentNullError(ER_Y_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -4227,70 +4712,76 @@ begin var p := X.ColCount; fFeatureCount := p; - SetLength(fTrees, fNTrees); - // сброс + // --- reset fClassCount := 0; fIndexToClass := nil; - fClassToIndex := nil; - // --- OOB buffers (classification) --- - var yEnc: array of integer := nil; + // ========================================================= + // ЕДИНЫЙ ENCODING (forest-level) + // ========================================================= + + var yInt := new integer[n]; + + for var i := 0 to n - 1 do + begin + var r := y[i]; + var ir := Round(r); + + if Abs(r - ir) > 1e-12 then + ArgumentError(ER_LABELS_NOT_INTEGER); + + yInt[i] := ir; + end; + + var yEncArr := EncodeLabelsInt(yInt, fIndexToClass); + fClassCount := fIndexToClass.Length; + + if fClassCount < 2 then + ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES); + + var yEnc := new Vector(n); + for var i := 0 to n - 1 do + yEnc[i] := yEncArr[i]; + + // ========================================================= + + // --- OOB buffers var oobVotes: array[,] of integer := nil; var oobCnt: array of integer := nil; + if fUseOOB then + begin + oobVotes := new integer[n, fClassCount]; + oobCnt := new integer[n]; + end; + + // --- training for var t := 0 to fNTrees - 1 do begin var treeSeed := fRng.Next(integer.MaxValue); - var tree := new DecisionTreeClassifier( - fMaxDepth, - fMinSamplesSplit, - fMinSamplesLeaf, - nil, - treeSeed - ); - - var mfeat := ComputeMaxFeatures(p); - tree.fMaxFeatures := mfeat; - var rows: array of integer; BootstrapRowIndices(n, rows); - tree.SetRowIndices(rows); - tree.Fit(X, y); + var XBoot := X.TakeRows(rows); + var yBoot := yEnc.SubvectorBy(rows); - if t = 0 then - begin - // classes фиксируем по первому дереву - fClassCount := tree.ClassCount; - fIndexToClass := tree.IndexToClass; + var treeCriterion := new GiniCriterion(fClassCount); - fClassToIndex := new Dictionary; - for var k := 0 to fClassCount - 1 do - fClassToIndex[fIndexToClass[k]] := k; + var tree := new DecisionTreeCore( + fMaxDepth, + fMinSamplesSplit, + fMinSamplesLeaf, + treeCriterion, + ComputeMaxFeatures(p), + treeSeed + ); - // encode y в internal индексы (ОБОСНОВАННО: y должен быть целочисленным, как у дерева) - SetLength(yEnc, n); - for var i := 0 to n - 1 do - begin - var r := y[i]; - var ir := Round(r); - if Abs(r - ir) > 1e-12 then - ArgumentError(ER_LABELS_NOT_INTEGER); + tree.Fit(XBoot, yBoot); - yEnc[i] := fClassToIndex[ir]; - end; - - if fUseOOB then - begin - oobVotes := new integer[n, fClassCount]; - oobCnt := new integer[n]; - end; - end; - - // --- OOB accumulate --- + // --- OOB accumulate if fUseOOB then begin var inBag := new boolean[n]; @@ -4300,7 +4791,7 @@ begin for var i := 0 to n - 1 do if not inBag[i] then begin - var cls := tree.PredictOne(X, i); // internal index 0..K-1 + var cls := tree.PredictRow(X, i); // encoded class 0..K-1 oobVotes[i, cls] += 1; oobCnt[i] += 1; end; @@ -4309,7 +4800,7 @@ begin fTrees[t] := tree; end; - // --- finalize OOB score: accuracy --- + // --- finalize OOB score fHasOOBScore := false; fOOBScore := real.NaN; @@ -4321,9 +4812,9 @@ begin for var i := 0 to n - 1 do if oobCnt[i] > 0 then begin - // argmax по oobVotes[i,*] var bestK := 0; var bestV := oobVotes[i, 0]; + for var k := 1 to fClassCount - 1 do if oobVotes[i, k] > bestV then begin @@ -4331,7 +4822,7 @@ begin bestK := k; end; - if bestK = yEnc[i] then + if bestK = yEncArr[i] then correct += 1; cnt += 1; @@ -4356,7 +4847,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureCount then @@ -4370,19 +4861,20 @@ begin if fClassCount <= 0 then Error(ER_MODEL_NOT_INITIALIZED); - var resultVec := new Vector(n); var counts := new integer[fClassCount]; for var i := 0 to n - 1 do begin + // обнуление счётчиков for var c := 0 to fClassCount - 1 do counts[c] := 0; + // голосование деревьев for var t := 0 to treeCount - 1 do begin - var cls := fTrees[t].PredictOne(X, i); + var cls := fTrees[t].PredictRow(X, i); // <-- КЛЮЧЕВОЕ изменение if (cls < 0) or (cls >= fClassCount) then ArgumentError(ER_LABEL_INDEX_INVALID); @@ -4390,6 +4882,7 @@ begin counts[cls] += 1; end; + // выбор класса var bestClass := 0; var bestCount := counts[0]; @@ -4400,6 +4893,7 @@ begin bestClass := c; end; + // декодирование resultVec[i] := fIndexToClass[bestClass]; end; @@ -4424,7 +4918,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureCount then @@ -4439,17 +4933,19 @@ begin if fClassCount <= 0 then Error(ER_MODEL_NOT_INITIALIZED); - var resultMat := new Matrix(n, fClassCount); + Result := new Matrix(n, fClassCount); var counts := new integer[fClassCount]; for var i := 0 to n - 1 do begin + // reset for var c := 0 to fClassCount - 1 do counts[c] := 0; + // voting for var t := 0 to treeCount - 1 do begin - var cls := fTrees[t].PredictOne(X, i); + var cls := fTrees[t].PredictRow(X, i); // <-- главное изменение if (cls < 0) or (cls >= fClassCount) then ArgumentError(ER_LABEL_INDEX_INVALID); @@ -4457,11 +4953,10 @@ begin counts[cls] += 1; end; + // normalize for var c := 0 to fClassCount - 1 do - resultMat[i, c] := counts[c] / treeCount; + Result[i, c] := counts[c] / treeCount; end; - - Result := resultMat; end; function RandomForestClassifier.Clone: IModel; @@ -4510,7 +5005,7 @@ begin begin SetLength(rf.fTrees, fTrees.Length); for var i := 0 to fTrees.Length - 1 do - rf.fTrees[i] := DecisionTreeClassifier(fTrees[i].Clone); + rf.fTrees[i] := fTrees[i].Clone; end; Result := rf; @@ -4901,7 +5396,7 @@ begin ArgumentNullError(ER_Y_NULL); // --- finite checks --- - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(XTrain); CheckYForFit(yTrain); @@ -4922,7 +5417,7 @@ begin if yVal = nil then ArgumentNullError(ER_Y_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForPredict(XVal); CheckYForFit(yVal); @@ -5147,7 +5642,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureCount then @@ -5177,7 +5672,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureCount then @@ -5223,7 +5718,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureCount then @@ -5409,7 +5904,7 @@ begin ArgumentNullError(ER_Y_NULL); // --- finite checks --- - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(XTrain); CheckYForFit(yTrain); @@ -5430,7 +5925,7 @@ begin if yVal = nil then ArgumentNullError(ER_Y_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForPredict(XVal); CheckYForFit(yVal); @@ -5454,49 +5949,76 @@ begin fBestScoreLoss := real.PositiveInfinity; fFitted := false; - // --- mapping - BuildClassMapping(yTrain); - var yEncoded := ApplyLabelEncoding(yTrain); - var nTrain := XTrain.RowCount; + + // ========================================================= + // НОВЫЙ ЕДИНЫЙ ENCODING + // ========================================================= + + var yTrainInt := new integer[nTrain]; + + for var i := 0 to nTrain - 1 do + begin + var r := yTrain[i]; + var ir := Round(r); + + if Abs(r - ir) > 1e-12 then + ArgumentError(ER_LABELS_NOT_INTEGER); + + yTrainInt[i] := ir; + end; + + var yEncoded := EncodeLabelsInt(yTrainInt, fClasses); + + fClassCount := fClasses.Length; + + if fClassCount < 2 then + ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES); + + fClassIndex := new Dictionary; + for var cls := 0 to fClassCount - 1 do + fClassIndex[fClasses[cls]] := cls; + var classCount := fClassCount; - + + // ========================================================= + // --- compute class priors fInitLogits := new real[classCount]; - + var counts := new integer[classCount]; - + for var i := 0 to nTrain - 1 do counts[yEncoded[i]] += 1; - + for var cls := 0 to classCount - 1 do begin var pi := counts[cls] / nTrain; - + if pi <= 0 then - fInitLogits[cls] := -20.0 // защита от log(0) + fInitLogits[cls] := -20.0 else fInitLogits[cls] := Ln(pi); end; - + // --- init logits var logitsTrain := new Matrix(nTrain, classCount); - + for var i := 0 to nTrain - 1 do for var cls := 0 to classCount - 1 do logitsTrain[i, cls] := fInitLogits[cls]; - - // --- OOB init + + // --- OOB init var useOOB := (not useValidation) and (fSubsample < 1.0); - + var logitsOOB: Matrix := nil; var oobCount: array of integer; - + if useOOB then begin logitsOOB := new Matrix(nTrain, classCount); SetLength(oobCount, nTrain); - + for var i := 0 to nTrain - 1 do begin oobCount[i] := 0; @@ -5510,10 +6032,26 @@ begin if useValidation then begin + var nVal := yVal.Length; + + yValEncoded := new integer[nVal]; + + for var i := 0 to nVal - 1 do + begin + var r := yVal[i]; + var ir := Round(r); + + if Abs(r - ir) > 1e-12 then + ArgumentError(ER_LABELS_NOT_INTEGER); + + if not fClassIndex.ContainsKey(ir) then + ArgumentError(ER_UNKNOWN_CLASS_LABEL, ir); + + yValEncoded[i] := fClassIndex[ir]; + end; + logitsVal := new Matrix(XVal.RowCount, classCount); - yValEncoded := ApplyLabelEncoding(yVal); - - // --- инициализация log-prior + for var i := 0 to XVal.RowCount - 1 do for var cls := 0 to classCount - 1 do logitsVal[i, cls] := fInitLogits[cls]; @@ -5524,11 +6062,9 @@ begin // --- boosting loop for var iter := 0 to fNEstimators - 1 do begin - // --- compute probabilities (train) var probsTrain := new Matrix(nTrain, classCount); SoftmaxMatrix(logitsTrain, probsTrain); - // --- compute residuals var residuals := new Matrix(nTrain, classCount); for var i := 0 to nTrain - 1 do @@ -5540,37 +6076,26 @@ begin residuals[i, cls] := yik - probsTrain[i, cls]; end; - - // добавление - // --- subsample rows + var useSubsample := fSubsample < 1.0; var subIndices: array of integer := nil; - - // inBag var inBag: array of boolean := nil; - + if useSubsample then - begin subIndices := BuildSubsampleIndices(nTrain, fSubsample, fRng); - end; - + if useOOB then begin SetLength(inBag, nTrain); - + if useSubsample then - begin for var i := 0 to subIndices.Length - 1 do - inBag[subIndices[i]] := true; - end + inBag[subIndices[i]] := true else - begin for var i := 0 to nTrain - 1 do inBag[i] := true; - end; end; - // --- train trees var trees := new DecisionTreeRegressor[classCount]; for var cls := 0 to classCount - 1 do @@ -5578,7 +6103,7 @@ begin var rvec := new Vector(nTrain); for var i := 0 to nTrain - 1 do rvec[i] := residuals[i, cls]; - + var stageSeed := fRng.Next(integer.MaxValue); var tree := new DecisionTreeRegressor( @@ -5587,8 +6112,7 @@ begin fMinSamplesLeaf, seed := stageSeed ); - - // добавление. Тренировка по идее будет происходить по строкам в subIndices + if useSubsample then tree.SetRowIndices(subIndices); @@ -5600,7 +6124,6 @@ begin for var i := 0 to nTrain - 1 do logitsTrain[i, cls] += fLearningRate * deltaTrain[i]; - // --- OOB update --- if useOOB then begin for var i := 0 to nTrain - 1 do @@ -5610,8 +6133,7 @@ begin oobCount[i] += 1; end; end; - - // --- VALIDATION update --- + if useValidation then begin var deltaVal := tree.Predict(XVal); @@ -5622,7 +6144,6 @@ begin fEstimators.Add(trees); - // --- compute loss SoftmaxMatrix(logitsTrain, probsTrain); var trainLoss := ComputeLogLoss(yEncoded, probsTrain); fTrainLossHistory.Add(trainLoss); @@ -5633,24 +6154,24 @@ begin begin var probsVal := new Matrix(XVal.RowCount, classCount); SoftmaxMatrix(logitsVal, probsVal); - + var valLoss := ComputeLogLoss(yValEncoded, probsVal); fValLossHistory.Add(valLoss); - + scoreLoss := valLoss; end else if useOOB then begin var mask := new boolean[nTrain]; var cnt := 0; - + for var i := 0 to nTrain - 1 do begin mask[i] := oobCount[i] > 0; if mask[i] then cnt += 1; end; - + if cnt >= Max(1, nTrain div 10) then begin scoreLoss := ComputeLogLossMasked(yEncoded, logitsOOB, mask); @@ -5660,7 +6181,6 @@ begin scoreLoss := trainLoss; end; - // --- early stopping if fEarlyStoppingPatience > 0 then begin if fBestScoreLoss - scoreLoss > MinImprovement then @@ -5678,7 +6198,6 @@ begin end; end; - // --- cut trees after best iteration if (fEarlyStoppingPatience > 0) and (fBestIteration >= 0) then begin var keep := fBestIteration + 1; @@ -5692,43 +6211,6 @@ begin Result := Self; end; -procedure GradientBoostingClassifier.BuildClassMapping(y: Vector); -begin - var classes := new HashSet; - - for var i := 0 to y.Length - 1 do - begin - var r := y[i]; - var ir := Round(r); - - if Abs(r - ir) > 1e-12 then - ArgumentError(ER_LABELS_NOT_INTEGER); - - classes.Add(ir); - end; - - fClasses := classes.ToArray; - &Array.Sort(fClasses); - - fClassCount := fClasses.Length; - - fClassIndex := new Dictionary; - for var cls := 0 to fClassCount - 1 do - fClassIndex[fClasses[cls]] := cls; -end; - -function GradientBoostingClassifier.ApplyLabelEncoding(y: Vector): array of integer; -begin - var n := y.Length; - Result := new integer[n]; - - for var i := 0 to n - 1 do - begin - var ir := Round(y[i]); - Result[i] := fClassIndex[ir]; - end; -end; - procedure GradientBoostingClassifier.SoftmaxRow( var logits: array of real; var probs: array of real); @@ -6012,7 +6494,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureCount then @@ -6089,7 +6571,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fFeatureCount then @@ -6354,7 +6836,7 @@ begin if X = nil then ArgumentNullError(ER_X_NULL); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then CheckXForPredict(X); if X.ColCount <> fXTrain.ColCount then @@ -6428,36 +6910,6 @@ begin inherited Create(k, weighting); end; -procedure KNNClassifier.EncodeClasses(y: Vector); -begin - var n := y.Length; - - // собрать уникальные значения - var hs := new HashSet; - for var i := 0 to n - 1 do - begin - var v := y[i]; - if double.IsNaN(v) then - ArgumentError(ER_NAN_IN_Y); - hs.Add(v); - end; - - fClasses := hs.ToArray; - &Array.Sort(fClasses); - - fClassCount := fClasses.Length; - - // построить map label -> index - var dict := new Dictionary; - for var i := 0 to fClassCount - 1 do - dict[fClasses[i]] := i; - - SetLength(fYEnc, n); - - for var i := 0 to n - 1 do - fYEnc[i] := dict[y[i]]; -end; - function KNNClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel; begin if X = nil then @@ -6475,32 +6927,66 @@ begin if fK > X.RowCount then ArgumentOutOfRangeError(ER_K_EXCEEDS_SAMPLES); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); end; - // копия train data - fXTrain := X.Clone; // предполагаем, что Clone делает глубокую копию + var n := X.RowCount; - // кодирование классов - EncodeClasses(y); + // --- copy train data + fXTrain := X.Clone; - var n := fXTrain.RowCount; - var C := fClassCount; + // ========================================================= + // ЕДИНЫЙ ENCODING + // ========================================================= - // выделение буферов + var yInt := new integer[n]; + + for var i := 0 to n - 1 do + begin + var r := y[i]; + var ir := Round(r); + + if Abs(r - ir) > 1e-12 then + ArgumentError(ER_LABELS_NOT_INTEGER); + + yInt[i] := ir; + end; + + var classesInt: array of integer; + var yEncArr := EncodeLabelsInt(yInt, classesInt); + + fClassCount := classesInt.Length; + + if fClassCount < 2 then + ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES); + + // сохранить оригинальные метки (double API) + SetLength(fClasses, fClassCount); + for var i := 0 to fClassCount - 1 do + fClasses[i] := classesInt[i]; + + // сохранить encoded y + SetLength(fYEnc, n); + for var i := 0 to n - 1 do + fYEnc[i] := yEncArr[i]; + + // ========================================================= + + // --- buffers SetLength(fNeighbors, n); - SetLength(fVotes, C); - SetLength(fMark, C); - SetLength(fTouched, C); + SetLength(fVotes, fClassCount); + SetLength(fMark, fClassCount); + SetLength(fTouched, fClassCount); + fEpoch := 0; fFitted := true; - exit(self); + Result := Self; end; function KNNClassifier.GetClasses: array of real; @@ -6760,7 +7246,7 @@ begin sumW += w; end; - // 🔴 ОБЯЗАТЕЛЬНО проверить exact снова + // ОБЯЗАТЕЛЬНО проверить exact снова if exactCls <> -1 then begin @@ -6778,7 +7264,7 @@ begin else begin // fallback — равномерное распределение - var uniform := 1.0 / fK; + var uniform := 1.0 / touchCount; for var k2 := 0 to touchCount - 1 do begin var cls := fTouched[k2]; @@ -6829,7 +7315,7 @@ begin if fK > X.RowCount then ArgumentOutOfRangeError(ER_K_EXCEEDS_SAMPLES); - if ValidateFiniteInputs then + if MLConfig.ValidateFiniteInputs then begin CheckXForFit(X); CheckYForFit(y); @@ -7130,6 +7616,9 @@ begin if fNClusters > n then ArgumentOutOfRangeError(ER_K_INVALID, fNClusters); + + if MLConfig.ValidateFiniteInputs then + CheckXForFit(X); fFeatureCount := p; @@ -7175,6 +7664,9 @@ begin if X = nil then ArgumentNullError(ER_ARG_NULL, 'X'); + if MLConfig.ValidateFiniteInputs then + CheckXForPredict(X); + if X.ColCount <> fFeatureCount then DimensionError(ER_DIM_MISMATCH, X.ColCount, fFeatureCount); @@ -7220,6 +7712,9 @@ end; function KMeans.Predict(X: Matrix): Vector; begin + if MLConfig.ValidateFiniteInputs then + CheckXForPredict(X); + var labels := PredictLabels(X); var n := Length(labels); @@ -7327,6 +7822,12 @@ function DBSCAN.Fit(X: Matrix): IUnsupervisedModel; begin if X = nil then ArgumentNullError(ER_ARG_NULL, 'X'); + + if MLConfig.ValidateFiniteInputs then + CheckXForFit(X); + + if X.RowCount = 0 then + ArgumentError(ER_EMPTY_DATASET); var n := X.RowCount; var p := X.ColCount; @@ -7403,6 +7904,9 @@ begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); + + if MLConfig.ValidateFiniteInputs then + CheckXForPredict(X); if X.RowCount <> Length(fLabels) then ArgumentError(ER_DBSCAN_PREDICT_NEW_DATA); @@ -8493,7 +8997,7 @@ begin if fSelected <> nil then t.fSelected := Copy(fSelected); - t.fFeatureCount := fFeatureCount; // 🔥 ОБЯЗАТЕЛЬНО + t.fFeatureCount := fFeatureCount; t.fFitted := fFitted; Result := t; diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 3f9e5559f..f0a0e60b5 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -56,11 +56,6 @@ type fTask: TaskKind; fTarget: string; - // fDataSteps: List; - // fMatrixSteps: List; - // fFeatures: array of string; - // fFinalFeatures: array of string; - // fFitted: boolean; protected procedure ValidateSchema(df: DataFrame); override; public @@ -75,88 +70,6 @@ type /// Запрещено добавлять шаги после вызова Fit/FitTransform. function Add(step: IPipelineStep): DataPipeline; - { // Примеры использования препроцессоров и моделей в DataPipeline. - // Препроцессоры работают на уровне DataFrame. - // После них Pipeline автоматически преобразует данные в Matrix/Vector. - // Далее выполняются матричные трансформеры и модель. - - // ------------------------------------------------------------ - // Пример 1. Классификация с категориальным целевым признаком - var pipe := - DataPipeline.Build( - 'species', - ['length','width'], - new LabelEncoder('species'), // DataFrame-препроцессор (строки → числа) - new StandardScaler, // матричный трансформер - new LogisticRegression // модель - ); - - // ------------------------------------------------------------ - // Пример 2. Регрессия с пропущенными значениями и категориальным признаком - var pipe := - DataPipeline.Build( - 'price', - ['area','floor','district'], - new Imputer('area'), // DataFrame-препроцессор (заполнение NA) - new OneHotEncoder('district'), // DataFrame-препроцессор - new RandomForestRegressor // модель - ); - - // ------------------------------------------------------------ - // Пример 3. Сложный pipeline - var pipe := - DataPipeline.Build( - 'target', - ['f1','f2','f3','category'], - new Imputer('f2'), // DataFrame-препроцессор - new OneHotEncoder('category'), // DataFrame-препроцессор - new StandardScaler, // матричный трансформер - new PCATransformer(2), // матричный трансформер - new GradientBoostingRegressor // модель - ); - - // ------------------------------------------------------------ - // Пример 4. Классификация без Pipeline - var df := Datasets.Flowers; - - // --- Encode target (DataFrame уровень) - df := df.SetCategorical(['species']); - - var labels := df.EncodeLabels('species'); - - // --- X, y - var X := df.ToMatrix(['length','width']); - var y := new Vector(labels); - - // --- Matrix уровень - var scaler := new StandardScaler; - X := scaler.FitTransform(X); - - // --- Модель - var model := new LogisticRegression; - model.Fit(X, y); - - // Pipeline.Build используется, когда данные уже представлены - // в виде числовой матрицы признаков X и вектора целевой переменной y. - // В этом случае DataFrame и препроцессоры уровня таблицы не требуются. - // - // Типичные ситуации: - // • экспериментирование с ML-алгоритмами - // • сравнение моделей - // • кросс-валидация - // • подбор гиперпараметров - // • тестирование моделей - - // Пример 5. Pipeline на матричном уровне - var pipe := - Pipeline.Build( - new StandardScaler, - new PCATransformer(2), - new LogisticRegression - ); - - } - /// Строит конвейер из шагов обработки данных и модели. /// /// Используется в задачах с учителем (с target). diff --git a/bin/Lib/MLUtilsABC.pas b/bin/Lib/MLUtilsABC.pas index 1c241ce69..3c1b13987 100644 --- a/bin/Lib/MLUtilsABC.pas +++ b/bin/Lib/MLUtilsABC.pas @@ -18,6 +18,43 @@ function LabelsToInts(y: Vector): array of integer; /// Преобразует массив целых меток в Vector. function IntsToLabels(a: array of integer): Vector; +/// Кодирует строковые метки классов в целочисленные индексы. +/// Каждому уникальному значению присваивается номер 0,1,2,... +/// Порядок кодирования соответствует порядку первого появления меток. +/// Используется при обучении моделей и визуализации. +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; + +/// Кодирует целые метки классов в целочисленные индексы. +/// Каждому уникальному значению присваивается номер 0,1,2,... +/// Порядок кодирования соответствует порядку первого появления меток. +/// В параметр classes возвращается массив уникальных значений в порядке кодирования. +/// Используется при обучении моделей и визуализации +function EncodeLabelsInt(labels: array of integer; var classes: array of integer): array of integer; + +/// Преобразует строковые метки классов в целочисленные индексы +/// с использованием заранее заданного массива classes (mapping). +/// classes должен быть получен из EncodeLabels. +/// Если встречается неизвестная метка — выбрасывается исключение. +/// Используется для применения кодирования к тестовым данным (Transform). +function TransformLabels(labels: array of string; classes: array of string): array of integer; + +/// Преобразует целочисленные индексы классов обратно в строковые метки. +/// Массив classes задаёт соответствие: classes[i] — имя класса с индексом i. +/// Используется для получения текстовых предсказаний моделей. +function DecodeLabels(y: array of integer; classes: array of string): array of string; + +/// Возвращает список уникальных меток классов. +/// Порядок соответствует первому появлению значений во входном массиве. +/// Используется для определения множества классов в задаче классификации. +function UniqueLabels(labels: array of string): array of string; + implementation uses MLExceptions; @@ -27,6 +64,11 @@ const 'y не может быть nil!!y cannot be nil'; ER_LABELS_ARRAY_NULL = 'labels не может быть nil!!labels cannot be nil'; + ER_UNKNOWN_CLASS_IN_TRANSFORM = + 'Неизвестное значение класса "{0}" при преобразовании меток!!Unknown class value "{0}" in TransformLabels'; + ER_LABEL_INDEX_OUT_OF_RANGE = + 'Индекс метки {0} вне диапазона [0, {1})!!Label index {0} is out of range [0, {1})'; + function LabelsToInts(y: Vector): array of integer; begin @@ -47,4 +89,112 @@ begin Result := new Vector(a); end; +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 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; + +function TransformLabels(labels: array of string; classes: array of string): array of integer; +begin + if labels = nil then + ArgumentNullError(ER_ARG_NULL, 'labels'); + + var map := new Dictionary; + for var i := 0 to classes.Length - 1 do + map[classes[i]] := i; + + var res := new integer[labels.Length]; + + for var i := 0 to labels.Length - 1 do + begin + var lbl := labels[i]; + + if not map.ContainsKey(lbl) then + Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, lbl); + + res[i] := map[lbl]; + end; + + Result := res; +end; + +function EncodeLabels(labels: array of string): array of integer; +begin + var classes: array of string; + Result := EncodeLabels(labels, classes); +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; + +function DecodeLabels(y: array of integer; classes: array of string): array of string; +begin + var res := new string[y.Length]; + + for var i := 0 to y.Length - 1 do + begin + var idx := y[i]; + + if (idx < 0) or (idx >= classes.Length) then + Error(ER_LABEL_INDEX_OUT_OF_RANGE, idx, classes.Length); + + res[i] := classes[idx]; + end; + + Result := res; +end; + +function UniqueLabels(labels: array of string): array of string; +begin + Result := labels.Distinct.ToArray; +end; + end. \ No newline at end of file diff --git a/bin/Lib/MetricsABC.pas b/bin/Lib/MetricsABC.pas index 28df836d8..56714d42f 100644 --- a/bin/Lib/MetricsABC.pas +++ b/bin/Lib/MetricsABC.pas @@ -14,6 +14,8 @@ interface uses LinearAlgebraML; type + Averaging = (avMacro, avMicro, avWeighted); + /// Метрики оценки качества моделей машинного обучения /// для задач регрессии, классификации и кластеризации Metrics = static class @@ -95,35 +97,76 @@ type static function Accuracy(yTrue: Vector; yPred: array of integer): real; /// Точность (Precision) для положительного класса (1). - /// Среди всех объектов, которые модель предсказала как 1, + /// Precision = TP / (TP + FP) + /// Среди всех объектов, которые модель предсказала как положительные (1), /// показывает долю действительно принадлежащих этому классу. + /// Используется только для бинарной классификации. /// Важна, когда критичны ложные положительные результаты («ложные тревоги»). static function Precision(yTrue, yPred: Vector): real; - + + /// Точность (Precision) для многоклассовой классификации. + /// Вычисляется для каждого класса отдельно как: + /// Precision(c) = TP(c) / (TP(c) + FP(c)), + /// после чего агрегируется согласно параметру avg: + /// avMacro — среднее по всем классам (каждый класс равновесен); + /// avMicro — глобальная метрика по суммарным TP и FP; + /// avWeighted — среднее с весами, пропорциональными размеру классов. + /// Используется для задач с числом классов больше двух. + static function Precision(yTrue, yPred: Vector; avg: Averaging): real; + /// Полнота (Recall) для положительного класса (1). - /// Среди всех объектов, которые в действительности равны 1, + /// Recall = TP / (TP + FN) + /// Среди всех объектов, которые в действительности принадлежат положительному классу (1), /// показывает долю правильно найденных моделью. + /// Используется только для бинарной классификации. /// Важна, когда нежелательны пропуски положительных случаев (например, заболеваний). static function Recall(yTrue, yPred: Vector): real; - - /// F1-мера. - /// Объединённая метрика, учитывающая одновременно Precision и Recall. - /// Высока только тогда, когда и Precision, и Recall имеют высокие значения. + + /// Полнота (Recall) для многоклассовой классификации. + /// Вычисляется для каждого класса отдельно как: + /// Recall(c) = TP(c) / (TP(c) + FN(c)), + /// после чего агрегируется согласно параметру avg: + /// avMacro — среднее по всем классам (каждый класс равновесен); + /// avMicro — глобальная метрика по суммарным TP и FN; + /// avWeighted — среднее с весами, пропорциональными размеру классов. + /// Используется для задач с числом классов больше двух. + static function Recall(yTrue, yPred: Vector; avg: Averaging): real; + + /// F1-мера для бинарной классификации. + /// F1 = 2 * (Precision * Recall) / (Precision + Recall) + /// Гармоническое среднее точности (Precision) и полноты (Recall). + /// Принимает высокие значения только тогда, когда оба показателя высоки. + /// Используется только для бинарной классификации. /// Особенно полезна при несбалансированных классах. static function F1(yTrue, yPred: Vector): real; - /// Specificity (True Negative Rate). - /// Доля правильно предсказанных отрицательных объектов: - /// TN / (TN + FP). + /// F1-мера для многоклассовой классификации. + /// Вычисляется для каждого класса отдельно как: + /// F1(c) = 2 * Precision(c) * Recall(c) / (Precision(c) + Recall(c)), + /// после чего агрегируется согласно параметру avg: + /// avMacro — среднее по всем классам (каждый класс равновесен); + /// avMicro — вычисляется по суммарным TP, FP и FN; + /// avWeighted — среднее с весами, пропорциональными размеру классов. + /// Используется для задач с числом классов больше двух. + static function F1(yTrue, yPred: Vector; avg: Averaging): real; + + /// Специфичность (Specificity, True Negative Rate) для бинарной классификации. + /// Specificity = TN / (TN + FP) + /// Доля объектов отрицательного класса (0), правильно распознанных моделью. /// Показывает способность модели корректно распознавать отрицательный класс. + /// Используется только для бинарной классификации. static function Specificity(yTrue, yPred: Vector): real; - /// Balanced Accuracy. - /// Среднее арифметическое Recall (чувствительности) - /// и Specificity (доли истинно отрицательных). + /// Сбалансированная точность (Balanced Accuracy) для бинарной классификации. + /// Balanced Accuracy = (Recall + Specificity) / 2 + /// Среднее арифметическое полноты (Recall, чувствительности) + /// и специфичности (Specificity, доли истинно отрицательных). /// Устойчива к дисбалансу классов. + /// Используется только для бинарной классификации static function BalancedAccuracy(yTrue, yPred: Vector): real; + static function ClassificationReport(yTrue, yPred: Vector): string; + /// Вычисляет силуэт (Silhouette) для каждой точки. /// Для каждой строки матрицы X возвращает значение s(i) в диапазоне [-1, 1]. /// Требует как минимум 2 различных кластера. @@ -169,135 +212,271 @@ type end; type -/// Матрица ошибок (Confusion Matrix) для бинарной классификации. -/// Хранит количество истинно положительных (TP), -/// истинно отрицательных (TN), -/// ложно положительных (FP) -/// и ложно отрицательных (FN) предсказаний. -/// Используется для анализа качества классификатора -/// и вычисления метрик Accuracy, Precision, Recall и F1 + /// Матрица ошибок (Confusion Matrix). + /// Представляет собой квадратную матрицу размера K × K, + /// где K — число классов. + /// Элемент [i, j] содержит количество объектов, + /// истинный класс которых равен i, + /// а предсказанный — j. + /// Используется для анализа качества классификатора + /// и вычисления метрик (Accuracy, Precision, Recall, F1 и др.). + /// Поддерживает как бинарную, так и многоклассовую классификацию. + /// Для бинарной классификации дополнительно доступны + /// агрегированные показатели: TP, TN, FP, FN. ConfusionMatrix = class private - fTP, fTN, fFP, fFN: integer; - public -/// Создаёт матрицу ошибок по истинным меткам yTrue и предсказаниям модели yPred. -/// Ожидается, что векторы содержат значения 0 и 1 и имеют одинаковую длину. -/// Подсчитывает TP, TN, FP и FN - constructor Create(yTrue, yPred: Vector); + fMatrix: array[,] of integer; + fClassCount: integer; + + fLabels: array of integer; + fLabelToIndex: Dictionary; - /// Истинно положительные (True Positives). - /// Количество объектов класса 1, которые модель правильно предсказала как 1. - property TP: integer read fTP; + procedure EnsureBinary; + + function GetTPBinary: integer; + function GetFPBinary: integer; + function GetFNBinary: integer; + function GetTNBinary: integer; - /// Истинно отрицательные (True Negatives). - /// Количество объектов класса 0, которые модель правильно предсказала как 0. - property TN: integer read fTN; + procedure EnsureClassIndex(c: integer); + public + /// Создаёт матрицу ошибок по истинным меткам yTrue и предсказаниям модели yPred. + /// Векторы должны иметь одинаковую длину и содержать метки классов, + /// представленные целыми числами (0, 1, 2, ...). + /// Автоматически определяет множество классов и строит + /// матрицу размера K × K, где K — число различных классов. + /// Элемент [i, j] содержит количество объектов, + /// истинный класс которых равен i, + /// а предсказанный — j. + /// Для бинарной классификации дополнительно доступны агрегированные + /// значения TP, TN, FP и FN + constructor Create(yTrue, yPred: Vector); - /// Ложно положительные (False Positives). - /// Количество объектов класса 0, которые модель ошибочно предсказала как 1. - property FP: integer read fFP; + /// Возвращает исходную метку класса по внутреннему индексу c. + /// c — индекс класса в диапазоне [0, ClassCount). + /// Результат — метка класса (например, 2, 5, 8), соответствующая этому индексу. + /// Используется для интерпретации результатов, так как внутренние индексы + /// могут не совпадать с исходными значениями меток. + function GetLabel(c: integer): integer; - /// Ложно отрицательные (False Negatives). - /// Количество объектов класса 1, которые модель ошибочно предсказала как 0. - property FN: integer read fFN; + /// Возвращает внутренний индекс класса по его метке label. + /// label — исходная метка класса (например, 2, 5, 8). + /// Результат — индекс класса в диапазоне [0, ClassCount). + /// Используется для преобразования внешних меток в внутренние индексы, + /// применяемые внутри матрицы ошибок. + function GetIndex(labl: integer): integer; - /// Доля правильных предсказаний. + /// Массив меток классов в порядке внутренних индексов. + /// Labels[c] — исходная метка класса для индекса c. + property Labels: array of integer read fLabels; + + /// Число различных классов в данных. + /// Определяется автоматически по объединению меток из yTrue и yPred. + /// Используется для построения матрицы размера K × K. + property ClassCount: integer read fClassCount; + + /// Число истинно положительных предсказаний для класса c. + /// TP(c) — количество объектов, для которых истинный и предсказанный класс равны c. + /// Соответствует элементу матрицы [c, c]. + function TPForClass(c: integer): integer; + + /// Число ложно положительных предсказаний для класса c. + /// FP(c) — количество объектов, предсказанных как класс c, + /// но в действительности принадлежащих другим классам. + /// Вычисляется как сумма по столбцу c минус TP(c). + function FPForClass(c: integer): integer; + + /// Число ложно отрицательных предсказаний для класса c. + /// FN(c) — количество объектов, принадлежащих классу c, + /// но предсказанных как другие классы. + /// Вычисляется как сумма по строке c минус TP(c). + function FNForClass(c: integer): integer; + + /// Число истинно положительных предсказаний (TP) для бинарной классификации. + /// Количество объектов, для которых истинный и предсказанный класс равны 1. + /// Доступно только при числе классов, равном 2. + property TP: integer read GetTPBinary; + + /// Число ложно положительных предсказаний (FP) для бинарной классификации. + /// Количество объектов, предсказанных как класс 1, + /// но в действительности принадлежащих классу 0. + /// Доступно только при числе классов, равном 2. + property FP: integer read GetFPBinary; + + /// Число ложно отрицательных предсказаний (FN) для бинарной классификации. + /// Количество объектов, принадлежащих классу 1, + /// но предсказанных как класс 0. + /// Доступно только при числе классов, равном 2. + property FN: integer read GetFNBinary; + + /// Число истинно отрицательных предсказаний (TN) для бинарной классификации. + /// Количество объектов, для которых истинный и предсказанный класс равны 0. + /// Доступно только при числе классов, равном 2. + property TN: integer read GetTNBinary; + + /// Доля правильных предсказаний (Accuracy). + /// Accuracy = (число правильных предсказаний) / (общее число объектов). /// Показывает, какая часть объектов классифицирована верно. + /// Применима как для бинарной, так и для многоклассовой классификации. /// Удобна, когда классы примерно сбалансированы. - /// Пример: Accuracy = 0.9 означает 90% правильных ответов. function Accuracy: real; + /// Точность (Precision) для положительного класса (1). - /// Среди всех объектов, которые модель предсказала как 1, - /// показывает долю действительно принадлежащих этому классу. + /// Precision = TP / (TP + FP) + /// Среди всех объектов, которые модель предсказала как положительные (1), + /// показывает долю действительно принадлежащих этому классу. + /// Используется только для бинарной классификации. /// Важна, когда критичны ложные положительные результаты («ложные тревоги»). function Precision: real; + /// Полнота (Recall) для положительного класса (1). - /// Среди всех объектов, которые в действительности равны 1, - /// показывает долю правильно найденных моделью. + /// Recall = TP / (TP + FN) + /// Среди всех объектов, которые в действительности принадлежат классу 1, + /// показывает долю правильно найденных моделью. + /// Используется только для бинарной классификации. /// Важна, когда нежелательны пропуски положительных случаев (например, заболеваний). function Recall: real; - /// F1-мера. - /// Объединённая метрика, учитывающая и Precision, и Recall одновременно. - /// Высока только тогда, когда и Precision, и Recall высоки. - /// Используется, когда классы сильно различаются по количеству элементов + + /// F1-мера для бинарной классификации. + /// F1 = 2 * (Precision * Recall) / (Precision + Recall) + /// Гармоническое среднее точности и полноты. + /// Принимает высокие значения только тогда, когда оба показателя высоки. + /// Используется только для бинарной классификации. + /// Особенно полезна при несбалансированных классах. function F1: real; + /// Специфичность (Specificity, True Negative Rate) для бинарной классификации. + /// Specificity = TN / (TN + FP) + /// Доля объектов отрицательного класса (0), правильно распознанных моделью. + /// Показывает способность модели корректно распознавать отрицательный класс. + /// Используется только для бинарной классификации. function Specificity: real; + + /// Сбалансированная точность (Balanced Accuracy) для бинарной классификации. + /// Balanced Accuracy = (Recall + Specificity) / 2 + /// Среднее арифметическое полноты (Recall, чувствительности) + /// и специфичности (Specificity). + /// Устойчива к дисбалансу классов. + /// Используется только для бинарной классификации. function BalancedAccuracy: real; + + /// Точность (Precision) для класса c. + /// Precision(c) = TP(c) / (TP(c) + FP(c)) + /// Среди всех объектов, предсказанных как класс c, + /// показывает долю действительно принадлежащих этому классу. + /// Если модель ни разу не предсказала класс c (TP(c) + FP(c) = 0), + /// значение Precision(c) считается равным 0. + /// Применима для многоклассовой классификации. + function PrecisionForClass(c: integer): real; + + /// Полнота (Recall) для класса c. + /// Recall(c) = TP(c) / (TP(c) + FN(c)) + /// Среди всех объектов, принадлежащих классу c, + /// показывает долю правильно найденных моделью. + /// Применима для многоклассовой классификации. + function RecallForClass(c: integer): real; + + /// F1-мера для класса c. + /// F1(c) = 2 * Precision(c) * Recall(c) / (Precision(c) + Recall(c)) + /// Гармоническое среднее точности и полноты для класса c. + /// Принимает высокие значения только тогда, когда оба показателя высоки. + /// Применима для многоклассовой классификации. + function F1ForClass(c: integer): real; + end; - /// Метрики для оценки качества классификационных моделей - /// Используются когда целевая переменная является категорией (классом) - /// (например, определение вида цветка, спам/не спам, диагноз пациента) + /// Метрики для оценки качества классификационных моделей. + /// Используются, когда целевая переменная представляет собой категорию (класс) + /// (например, определение вида цветка, спам/не спам, диагноз пациента). ClassificationMetrics = static class public - /// Доля правильных предсказаний (Accuracy) - /// Accuracy = (TP + TN) / (TP + TN + FP + FN) - /// Показывает долю верно классифицированных объектов - /// Удобна когда классы примерно сбалансированы + /// Доля правильных предсказаний (Accuracy). + /// Accuracy = (число правильных предсказаний) / (общее число объектов). + /// Показывает долю верно классифицированных объектов. + /// Применима как для бинарной, так и для многоклассовой классификации. + /// Удобна, когда классы примерно сбалансированы. static function Accuracy(yTrue, yPred: Vector): real := Metrics.Accuracy(yTrue, yPred); - /// Доля правильных предсказаний (Accuracy) - /// Accuracy = (TP + TN) / (TP + TN + FP + FN) - /// Показывает долю верно классифицированных объектов - /// Удобна когда классы примерно сбалансированы + /// Доля правильных предсказаний (Accuracy). + /// Accuracy = (число правильных предсказаний) / (общее число объектов). + /// Показывает долю верно классифицированных объектов. + /// Применима как для бинарной, так и для многоклассовой классификации. + /// Удобна, когда классы примерно сбалансированы. static function Accuracy(yTrue, yPred: array of integer): real := Metrics.Accuracy(yTrue, yPred); - /// Доля правильных предсказаний (Accuracy) - /// Accuracy = (TP + TN) / (TP + TN + FP + FN) - /// Показывает долю верно классифицированных объектов - /// Удобна когда классы примерно сбалансированы + /// Доля правильных предсказаний (Accuracy). + /// Accuracy = (число правильных предсказаний) / (общее число объектов). + /// Показывает долю верно классифицированных объектов. + /// Применима как для бинарной, так и для многоклассовой классификации. + /// Удобна, когда классы примерно сбалансированы. static function Accuracy(yTrue: Vector; yPred: array of integer): real := Metrics.Accuracy(yTrue, yPred); - /// Точность (Precision) для положительного класса (1) + /// Точность (Precision) для положительного класса (1). /// Precision = TP / (TP + FP) - /// Среди всех предсказанных положительных объектов показывает долю истинно положительных - /// Важна когда критичны ложные положительные результаты ("ложные тревоги") + /// Среди всех объектов, предсказанных как положительные (1), + /// показывает долю действительно принадлежащих этому классу. + /// Используется только для бинарной классификации. + /// Важна, когда критичны ложные положительные результаты («ложные тревоги»). static function Precision(yTrue, yPred: Vector): real := Metrics.Precision(yTrue, yPred); - /// Полнота (Recall) для положительного класса (1) + /// Полнота (Recall) для положительного класса (1). /// Recall = TP / (TP + FN) - /// Среди всех истинно положительных объектов показывает долю правильно найденных - /// Важна когда нежелательны пропуски положительных случаев (например, заболеваний) + /// Среди всех объектов, принадлежащих положительному классу (1), + /// показывает долю правильно найденных моделью. + /// Используется только для бинарной классификации. + /// Важна, когда нежелательны пропуски положительных случаев (например, заболеваний). static function Recall(yTrue, yPred: Vector): real := Metrics.Recall(yTrue, yPred); - /// F1-мера + /// F1-мера для бинарной классификации. /// F1 = 2 * (Precision * Recall) / (Precision + Recall) - /// Гармоническое среднее точности и полноты - /// Высока только когда оба показателя высоки - /// Особенно полезна при несбалансированных классах + /// Гармоническое среднее точности и полноты. + /// Принимает высокие значения только тогда, когда оба показателя высоки. + /// Особенно полезна при несбалансированных классах. + /// Используется только для бинарной классификации. static function F1(yTrue, yPred: Vector): real := Metrics.F1(yTrue, yPred); - /// Специфичность (Specificity / True Negative Rate) + /// Специфичность (Specificity, True Negative Rate) для бинарной классификации. /// Specificity = TN / (TN + FP) - /// Доля правильно предсказанных отрицательных объектов - /// Показывает способность модели корректно распознавать отрицательный класс + /// Доля объектов отрицательного класса (0), правильно распознанных моделью. + /// Показывает способность модели корректно распознавать отрицательный класс. + /// Используется только для бинарной классификации. static function Specificity(yTrue, yPred: Vector): real := Metrics.Specificity(yTrue, yPred); - /// Сбалансированная точность (Balanced Accuracy) + /// Сбалансированная точность (Balanced Accuracy) для бинарной классификации. /// Balanced Accuracy = (Recall + Specificity) / 2 - /// Среднее арифметическое полноты и специфичности - /// Устойчива к дисбалансу классов + /// Среднее арифметическое полноты (Recall, чувствительности) + /// и специфичности (Specificity). + /// Устойчива к дисбалансу классов. + /// Используется только для бинарной классификации. static function BalancedAccuracy(yTrue, yPred: Vector): real := Metrics.BalancedAccuracy(yTrue, yPred); - /// Логарифмическая функция потерь (LogLoss) + /// Логарифмическая функция потерь (LogLoss) для бинарной классификации. /// LogLoss = - (1/n) * Σ [yTrue * log(yProb) + (1 - yTrue) * log(1 - yProb)] - /// Оценивает качество вероятностных предсказаний - /// Сильно штрафует уверенные, но ошибочные прогнозы - /// Чем меньше значение, тем лучше модель - static function LogLoss(yTrue, yPred: Vector): real := Metrics.LogLoss(yTrue, yPred); + /// Оценивает качество вероятностных предсказаний для класса 1. + /// Сильно штрафует уверенные, но ошибочные прогнозы. + /// Чем меньше значение, тем лучше модель. + /// Используется только для бинарной классификации. + static function LogLoss(yTrue, yPred: Vector): real := Metrics.LogLoss(yTrue, yPred); - /// Построение ROC-кривой - /// Возвращает кортеж из двух векторов: FPR (ложноположительные) и TPR (истинноположительные) - /// Используется для визуализации качества бинарной классификации + /// Построение ROC-кривой для бинарной классификации. + /// Возвращает кортеж из двух векторов: + /// FPR (False Positive Rate, доля ложноположительных), + /// TPR (True Positive Rate, полнота / Recall). + /// Вектор yPred должен содержать вероятности принадлежности к классу 1. + /// Используется для анализа качества вероятностных предсказаний. + /// Используется только для бинарной классификации. static function ROC(yTrue, yPred: Vector): (Vector, Vector) := Metrics.ROC(yTrue, yPred); - /// Площадь под ROC-кривой (AUC) + /// Площадь под ROC-кривой (AUC) для бинарной классификации. /// Значение от 0 до 1: - /// 1.0 — идеальная модель - /// 0.5 — случайная модель - /// <0.5 — модель хуже случайной - static function AUC(yTrue, yPred: Vector): real := Metrics.AUC(yTrue, yPred); + /// 1.0 — идеальная модель, + /// 0.5 — случайная модель, + /// <0.5 — модель хуже случайной. + /// Вектор yPred должен содержать вероятности принадлежности к классу 1. + /// Используется только для бинарной классификации. + static function AUC(yTrue, yPred: Vector): real := Metrics.AUC(yTrue, yPred); + + static function ClassificationReport(yTrue, yPred: Vector): string := Metrics.ClassificationReport(yTrue, yPred); end; /// Метрики для оценки качества регрессионных моделей @@ -422,8 +601,13 @@ const 'Метки кластеров должны быть целыми числами!!Cluster labels must be integers'; ER_MAPE_ALL_ZERO_TARGET = 'Все значения целевой переменной равны нулю — MAPE не определена!!All target values are zero — MAPE is undefined'; - - + ER_BINARY_ONLY = + 'Метрика доступна только для бинарной классификации!!This metric is available only for binary classification'; + ER_INVALID_PROBABILITY = + 'Вероятность вне диапазона [0,1]: {0}!!Probability out of range [0,1]: {0}'; + ER_ARG_OUT_OF_RANGE = + 'Аргумент {0} имеет недопустимое значение: {1}!!Argument {0} is out of range: {1}'; + //----------------------------- // Metrics //----------------------------- @@ -626,7 +810,7 @@ begin ArgumentNullError(ER_ARG_NULL, 'yTrue'); if yPred = nil then - ArgumentNullError(ER_ARG_NULL, 'yProb'); + ArgumentNullError(ER_ARG_NULL, 'yPred'); var n := yTrue.Length; @@ -652,6 +836,9 @@ begin if (yt <> 0.0) and (yt <> 1.0) then ArgumentError(ER_INVALID_CLASS_LABEL, yt); + + if (p < 0.0) or (p > 1.0) then + ArgumentError(ER_INVALID_PROBABILITY, p); if p < eps then p := eps; if p > 1.0 - eps then p := 1.0 - eps; @@ -680,7 +867,7 @@ begin var totalPos := 0; var totalNeg := 0; - + for var i := 0 to n - 1 do begin var yt := yTrue[i]; @@ -694,6 +881,9 @@ begin if (yt <> 0.0) and (yt <> 1.0) then ArgumentError(ER_INVALID_CLASS_LABEL, yt); + + if (p < 0.0) or (p > 1.0) then + ArgumentError(ER_INVALID_PROBABILITY, p); if yt = 1.0 then totalPos += 1 @@ -714,6 +904,9 @@ begin var fprList := new List; var tprList := new List; + + fprList.Add(0.0); + tprList.Add(0.0); for var k := 0 to n - 1 do begin @@ -730,6 +923,9 @@ begin tprList.Add(tpr); fprList.Add(fpr); end; + + fprList.Add(1.0); + tprList.Add(1.0); Result := (new Vector(fprList.ToArray), new Vector(tprList.ToArray)); @@ -835,7 +1031,6 @@ begin ArgumentNullError(ER_ARG_NULL, 'yTrue'); var data := yTrue.Data; - var n := data.Length; if n <> yPred.Length then @@ -847,8 +1042,16 @@ begin var correct := 0; for var i := 0 to n - 1 do - if integer(data[i]) = yPred[i] then + begin + var yt := data[i]; + + // проверка: метки должны быть целыми + if Abs(yt - integer(yt)) > 1e-12 then + ArgumentError(ER_INVALID_CLASS_LABEL, yt); + + if integer(yt) = yPred[i] then correct += 1; + end; Result := correct / n; end; @@ -868,6 +1071,191 @@ begin Result := ConfusionMatrix.Create(yTrue, yPred).F1; end; +static function Metrics.Precision(yTrue, yPred: Vector; avg: Averaging): real; +begin + var cm := new ConfusionMatrix(yTrue, yPred); + var k := cm.ClassCount; + + // --- Macro + if avg = avMacro then + begin + var sum := 0.0; + + for var c := 0 to k - 1 do + sum += cm.PrecisionForClass(c); + + Result := sum / k; + exit; + end; + + // --- Micro + if avg = avMicro then + begin + var tpSum := 0; + var fpSum := 0; + + for var c := 0 to k - 1 do + begin + tpSum += cm.TPForClass(c); + fpSum += cm.FPForClass(c); + end; + + if tpSum + fpSum = 0 then + exit(0.0); + + Result := tpSum / (tpSum + fpSum); + exit; + end; + + // --- Weighted + if avg = avWeighted then + begin + var sum := 0.0; + var total := 0; + + for var c := 0 to k - 1 do + begin + var support := cm.TPForClass(c) + cm.FNForClass(c); + if support = 0 then continue; + + sum += cm.PrecisionForClass(c) * support; + total += support; + end; + + if total = 0 then + exit(0.0); + + Result := sum / total; + exit; + end; + + Result := 0.0; +end; + +static function Metrics.Recall(yTrue, yPred: Vector; avg: Averaging): real; +begin + var cm := new ConfusionMatrix(yTrue, yPred); + var k := cm.ClassCount; + + // --- Macro + if avg = avMacro then + begin + var sum := 0.0; + + for var c := 0 to k - 1 do + sum += cm.RecallForClass(c); + + Result := sum / k; + exit; + end; + + // --- Micro + if avg = avMicro then + begin + var tpSum := 0; + var fnSum := 0; + + for var c := 0 to k - 1 do + begin + tpSum += cm.TPForClass(c); + fnSum += cm.FNForClass(c); + end; + + if tpSum + fnSum = 0 then + exit(0.0); + + Result := tpSum / (tpSum + fnSum); + exit; + end; + + // --- Weighted + if avg = avWeighted then + begin + var sum := 0.0; + var total := 0; + + for var c := 0 to k - 1 do + begin + var support := cm.TPForClass(c) + cm.FNForClass(c); + if support = 0 then continue; + + sum += cm.RecallForClass(c) * support; + total += support; + end; + + if total = 0 then + exit(0.0); + + Result := sum / total; + exit; + end; + + Result := 0.0; +end; + +static function Metrics.F1(yTrue, yPred: Vector; avg: Averaging): real; +begin + var cm := new ConfusionMatrix(yTrue, yPred); + var k := cm.ClassCount; + + // --- Macro + if avg = avMacro then + begin + var sum := 0.0; + + for var c := 0 to k - 1 do + sum += cm.F1ForClass(c); + + Result := sum / k; + exit; + end; + + // --- Micro + if avg = avMicro then + begin + var tpSum := 0; + var fpSum := 0; + var fnSum := 0; + + for var c := 0 to k - 1 do + begin + tpSum += cm.TPForClass(c); + fpSum += cm.FPForClass(c); + fnSum += cm.FNForClass(c); + end; + + if tpSum = 0 then + exit(0.0); + + Result := 2 * tpSum / (2 * tpSum + fpSum + fnSum); + exit; + end; + + // --- Weighted + if avg = avWeighted then + begin + var sum := 0.0; + var total := 0; + + for var c := 0 to k - 1 do + begin + var support := cm.TPForClass(c) + cm.FNForClass(c); + if support = 0 then continue; + + sum += cm.F1ForClass(c) * support; + total += support; + end; + + if total = 0 then + exit(0.0); + + Result := sum / total; + exit; + end; + + Result := 0.0; +end; + static function Metrics.Specificity(yTrue, yPred: Vector): real; begin Result := ConfusionMatrix.Create(yTrue, yPred).Specificity; @@ -1329,6 +1717,119 @@ begin end; end; +function R(s: string; w: integer): string; +begin + Result := s.PadLeft(w); +end; + +static function Metrics.ClassificationReport(yTrue, yPred: Vector): string; +begin + var cm := new ConfusionMatrix(yTrue, yPred); + + var rows := new List<(string, real, real, real, integer)>; + var total := yTrue.Length; + + // --- собираем строки классов + for var c := 0 to cm.ClassCount - 1 do + begin + var cls := cm.GetLabel(c).ToString; + + var p := cm.PrecisionForClass(c); + var r := cm.RecallForClass(c); + var f := cm.F1ForClass(c); + var support := cm.TPForClass(c) + cm.FNForClass(c); + + rows.Add((cls, p, r, f, support)); + end; + + // --- averages + var pMacro := Metrics.Precision(yTrue, yPred, avMacro); + var rMacro := Metrics.Recall(yTrue, yPred, avMacro); + var fMacro := Metrics.F1(yTrue, yPred, avMacro); + + var pW := Metrics.Precision(yTrue, yPred, avWeighted); + var rW := Metrics.Recall(yTrue, yPred, avWeighted); + var fW := Metrics.F1(yTrue, yPred, avWeighted); + + // --- helper форматирования + var F: real -> string := x -> x.ToString('0.00'); + + // --- считаем ширины колонок + var wClass := 'class'.Length; + var wP := 'precision'.Length; + var wR := 'recall'.Length; + var wF := 'f1-score'.Length; + var wS := 'support'.Length; + + foreach var row in rows do + begin + wClass := Max(wClass, row.Item1.Length); + wP := Max(wP, F(row.Item2).Length); + wR := Max(wR, F(row.Item3).Length); + wF := Max(wF, F(row.Item4).Length); + wS := Max(wS, row.Item5.ToString.Length); + end; + + wClass := Max(wClass, 'macro avg'.Length); + wClass := Max(wClass, 'weighted avg'.Length); + + wP := Max(wP, F(pMacro).Length); + wR := Max(wR, F(rMacro).Length); + wF := Max(wF, F(fMacro).Length); + + wP := Max(wP, F(pW).Length); + wR := Max(wR, F(rW).Length); + wF := Max(wF, F(fW).Length); + + wS := Max(wS, total.ToString.Length); + + // --- сборка строки + var sb := new System.Text.StringBuilder; + + // header + sb.AppendLine( + R('class', wClass) + ' ' + + R('precision', wP) + ' ' + + R('recall', wR) + ' ' + + R('f1-score', wF) + ' ' + + R('support', wS) + ); + + var totalWidth := wClass + 2 + wP + 2 + wR + 2 + wF + 2 + wS; + sb.AppendLine(new string('-', totalWidth)); + + // rows + foreach var row in rows do + sb.AppendLine( + R(row.Item1, wClass) + ' ' + + R(F(row.Item2), wP) + ' ' + + R(F(row.Item3), wR) + ' ' + + R(F(row.Item4), wF) + ' ' + + R(row.Item5.ToString, wS) + ); + + sb.AppendLine; + + // macro avg + sb.AppendLine( + R('macro avg', wClass) + ' ' + + R(F(pMacro), wP) + ' ' + + R(F(rMacro), wR) + ' ' + + R(F(fMacro), wF) + ' ' + + R(total.ToString, wS) + ); + + sb.AppendLine( + R('weighted avg', wClass) + ' ' + + R(F(pW), wP) + ' ' + + R(F(rW), wR) + ' ' + + R(F(fW), wF) + ' ' + + R(total.ToString, wS) + ); + + Result := sb.ToString; +end; + //----------------------------- // ConfusionMatrix //----------------------------- @@ -1349,15 +1850,16 @@ begin if n = 0 then ArgumentError(ER_EMPTY_DATA, 'ConfusionMatrix'); - fTP := 0; - fFP := 0; - fTN := 0; - fFN := 0; + var yArr := yTrue.Data; + var pArr := yPred.Data; + + // --- 1. собираем уникальные метки + var setLabels := new HashSet; for var i := 0 to n - 1 do begin - var yt := yTrue[i]; - var yp := yPred[i]; + var yt := yArr[i]; + var yp := pArr[i]; if double.IsNaN(yt) or double.IsInfinity(yt) then ArgumentError(ER_INVALID_VALUE, 'yTrue', i); @@ -1365,49 +1867,178 @@ begin if double.IsNaN(yp) or double.IsInfinity(yp) then ArgumentError(ER_INVALID_VALUE, 'yPred', i); - if (yt <> 0.0) and (yt <> 1.0) then + var ytInt := integer(yt); + var ypInt := integer(yp); + + if Abs(yt - ytInt) > 1e-12 then ArgumentError(ER_INVALID_CLASS_LABEL, yt); - if (yp <> 0.0) and (yp <> 1.0) then + if Abs(yp - ypInt) > 1e-12 then ArgumentError(ER_INVALID_CLASS_LABEL, yp); - if yt = 1.0 then - if yp = 1.0 then - fTP += 1 - else - fFN += 1 - else - if yp = 1.0 then - fFP += 1 - else - fTN += 1; + setLabels.Add(ytInt); + setLabels.Add(ypInt); end; + + // --- 2. сортируем метки + var labels := setLabels.ToArray; + &Array.Sort(labels); + + fLabels := labels; + + fClassCount := labels.Length; + + // --- 3. строим отображение label → index + fLabelToIndex := new Dictionary(fClassCount); + + for var i := 0 to fClassCount - 1 do + fLabelToIndex.Add(labels[i], i); + + // --- 4. создаём матрицу + fMatrix := new integer[fClassCount, fClassCount]; + + // --- 5. заполняем + for var i := 0 to n - 1 do + begin + var yt := integer(yArr[i]); + var yp := integer(pArr[i]); + + var row := fLabelToIndex[yt]; + var col := fLabelToIndex[yp]; + + fMatrix[row, col] += 1; + end; +end; + +function ConfusionMatrix.GetLabel(c: integer): integer; +begin + if (c < 0) or (c >= fClassCount) then + ArgumentOutOfRangeError(ER_ARG_OUT_OF_RANGE, 'c', c); + + Result := fLabels[c]; +end; + +function ConfusionMatrix.GetIndex(labl: integer): integer; +begin + if not fLabelToIndex.ContainsKey(labl) then + ArgumentError(ER_INVALID_CLASS_LABEL, labl); + + Result := fLabelToIndex[labl]; +end; + +function ConfusionMatrix.TPForClass(c: integer): integer; +begin + EnsureClassIndex(c); + Result := fMatrix[c,c]; +end; + +function ConfusionMatrix.FPForClass(c: integer): integer; +begin + EnsureClassIndex(c); + + var s := 0; + for var i := 0 to fClassCount - 1 do + s += fMatrix[i,c]; + + Result := s - fMatrix[c,c]; +end; + +function ConfusionMatrix.FNForClass(c: integer): integer; +begin + EnsureClassIndex(c); + + var s := 0; + for var j := 0 to fClassCount - 1 do + s += fMatrix[c,j]; + + Result := s - fMatrix[c,c]; +end; + +procedure ConfusionMatrix.EnsureBinary; +begin + if fClassCount <> 2 then + Error(ER_BINARY_ONLY); +end; + +function ConfusionMatrix.GetTPBinary: integer; +begin + EnsureBinary; + Result := fMatrix[1,1]; +end; + +function ConfusionMatrix.GetFPBinary: integer; +begin + EnsureBinary; + Result := fMatrix[0,1]; +end; + +function ConfusionMatrix.GetFNBinary: integer; +begin + EnsureBinary; + Result := fMatrix[1,0]; +end; + +function ConfusionMatrix.GetTNBinary: integer; +begin + EnsureBinary; + Result := fMatrix[0,0]; +end; + +procedure ConfusionMatrix.EnsureClassIndex(c: integer); +begin + if (c < 0) or (c >= fClassCount) then + ArgumentOutOfRangeError(ER_ARG_OUT_OF_RANGE, 'c', c); end; function ConfusionMatrix.Accuracy: real; begin - var total := fTP + fTN + fFP + fFN; + var total := 0; + var correct := 0; + + for var i := 0 to fClassCount - 1 do + for var j := 0 to fClassCount - 1 do + begin + total += fMatrix[i,j]; + if i = j then + correct += fMatrix[i,j]; + end; + if total = 0 then exit(0.0); - Result := (fTP + fTN) / total; + + Result := correct / total; end; function ConfusionMatrix.Precision: real; begin - if fTP + fFP = 0 then + EnsureBinary; + + var tp := TP; + var fp := FP; + + if tp + fp = 0 then exit(0.0); - Result := fTP / (fTP + fFP); + + Result := tp / (tp + fp); end; function ConfusionMatrix.Recall: real; begin - if fTP + fFN = 0 then + EnsureBinary; + + var tp := TP; + var fn := FN; + + if tp + fn = 0 then exit(0.0); - Result := fTP / (fTP + fFN); + + Result := tp / (tp + fn); end; function ConfusionMatrix.F1: real; begin + EnsureBinary; + var p := Precision; var r := Recall; @@ -1419,15 +2050,54 @@ end; function ConfusionMatrix.Specificity: real; begin - if fTN + fFP = 0 then + EnsureBinary; + + var tn := TN; + var fp := FP; + + if tn + fp = 0 then exit(0.0); - Result := fTN / (fTN + fFP); + Result := tn / (tn + fp); end; function ConfusionMatrix.BalancedAccuracy: real; begin + EnsureBinary; Result := (Recall + Specificity) / 2; -end; - -end. \ No newline at end of file +end; + +function ConfusionMatrix.PrecisionForClass(c: integer): real; +begin + var tp := TPForClass(c); + var fp := FPForClass(c); + + if tp + fp = 0 then + exit(0.0); + + Result := tp / (tp + fp); +end; + +function ConfusionMatrix.RecallForClass(c: integer): real; +begin + var tp := TPForClass(c); + var fn := FNForClass(c); + + if tp + fn = 0 then + exit(0.0); + + Result := tp / (tp + fn); +end; + +function ConfusionMatrix.F1ForClass(c: integer): real; +begin + var p := PrecisionForClass(c); + var r := RecallForClass(c); + + if p + r = 0 then + exit(0.0); + + Result := 2 * p * r / (p + r); +end; + +end. diff --git a/bin/Lib/PreprocessorABC.pas b/bin/Lib/PreprocessorABC.pas index 8bba06df3..8f646774a 100644 --- a/bin/Lib/PreprocessorABC.pas +++ b/bin/Lib/PreprocessorABC.pas @@ -248,22 +248,50 @@ begin NotFittedError(ER_FIT_NOT_CALLED); var idx := df.Schema.IndexOf(col); + var n := df.RowCount; - Result := df.ReplaceColumnInt( - col, - c -> - if not c.IsValid(idx) then - Error(ER_LABELENCODER_NA) // будет поймано как NA - else + var data := new integer[n]; + var valid: array of boolean := nil; + + var cur := df.GetCursor; + var row := 0; + while cur.MoveNext do + begin + if not cur.IsValid(idx) then + begin + if valid = nil then begin - var s := c.Str(idx); - if not mapping.ContainsKey(s) then - Error(ER_LABELENCODER_UNSEEN_CATEGORY, s); - Result := mapping[s]; - end - ); - - Result := Result.SetCategorical([col]); + valid := new boolean[n]; + for var j := 0 to row - 1 do + valid[j] := true; + end; + valid[row] := false; + data[row] := 0; + end + else + begin + var s := cur.Str(idx); + + if not mapping.ContainsKey(s) then + Error(ER_LABELENCODER_UNSEEN_CATEGORY, s); + + data[row] := mapping[s]; + if valid <> nil then + valid[row] := true; + end; + + row += 1; + end; + + var res := new DataFrame; + + foreach var src in df.GetColumns do + if src.Info.Name <> col then + res.AddColumnView(src) + else + res.AddIntColumn(col, data, valid); + + Result := res.SetCategorical([col]); end; function LabelEncoder.FitTransform(df: DataFrame): DataFrame; diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index 9ee913f85..592c93069 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -311,6 +311,19 @@ begin end; end; + // --- 1.1 ПРОВЕРКА НА МИНИМАЛЬНЫЙ РАЗМЕР КЛАССА + foreach var pair in classMap do + begin + var cls := pair.Key; + var cnt := pair.Value.Count; + + if cnt < k then + ArgumentError( + 'StratifiedKFold: класс %d содержит %d объектов, что меньше числа фолдов (%d)', + cls, cnt, k + ); + end; + // --- 2. Контейнеры фолдов var folds := new List[k]; for var f := 0 to k - 1 do