diff --git a/Configuration/GlobalAssemblyInfo.cs b/Configuration/GlobalAssemblyInfo.cs index 297f063bf..de1b4c426 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 = "3808"; + public const string Revision = "3810"; 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 9ff4502d6..9bc6d5ec3 100644 --- a/Configuration/Version.defs +++ b/Configuration/Version.defs @@ -1,4 +1,4 @@ %COREVERSION%=1 -%REVISION%=3808 +%REVISION%=3810 %MINOR%=11 %MAJOR%=3 diff --git a/Release/pabcversion.txt b/Release/pabcversion.txt index 4bdc3c29c..812a9586e 100644 --- a/Release/pabcversion.txt +++ b/Release/pabcversion.txt @@ -1 +1 @@ -3.11.1.3808 +3.11.1.3810 diff --git a/ReleaseGenerators/PascalABCNET_version.nsh b/ReleaseGenerators/PascalABCNET_version.nsh index 58faf7f9e..7a41d17d7 100644 --- a/ReleaseGenerators/PascalABCNET_version.nsh +++ b/ReleaseGenerators/PascalABCNET_version.nsh @@ -1 +1 @@ -!define VERSION '3.11.1.3808' +!define VERSION '3.11.1.3810' diff --git a/bin/Lib/DataAdapters.pas b/bin/Lib/DataAdapters.pas index 48d4b4daa..89507073e 100644 --- a/bin/Lib/DataAdapters.pas +++ b/bin/Lib/DataAdapters.pas @@ -25,6 +25,8 @@ const 'Столбец "{0}" должен быть категориальным для EncodeLabels!!Column "{0}" must be categorical for EncodeLabels'; ER_ENCODELABELS_UNSUPPORTED_TYPE = 'Неподдерживаемый тип столбца "{0}" для EncodeLabels!!Unsupported column type "{0}" for EncodeLabels'; + ER_TARGET_HAS_NA = + 'Целевой столбец "{0}" содержит NA!!Target column "{0}" contains NA values'; function ToMatrix(Self: DataFrame; colNames: array of string): Matrix; extensionmethod; begin @@ -89,6 +91,11 @@ begin if not Self.IsCategorical(target) then ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, target); + + var col := Self[target]; + for var i := 0 to Self.RowCount - 1 do + if not col.IsValid[i] then + ArgumentError(ER_TARGET_HAS_NA, target); if Self.GetColumnType(target) = ColumnType.ctStr then begin @@ -130,15 +137,18 @@ begin if not Self.IsCategorical(target) then ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, target); + + var col := Self[target]; + for var i := 0 to Self.RowCount - 1 do + if not col.IsValid[i] then + ArgumentError(ER_TARGET_HAS_NA, target); case Self.GetColumnType(target) of - ColumnType.ctStr: begin var labels := Self.GetStrColumn(target).ToArray; Result := EncodeLabels(labels, classes); end; - ColumnType.ctInt: begin var labels := Self.GetIntColumn(target).ToArray; @@ -149,7 +159,6 @@ begin // если API требует string classes: classes := intClasses.Select(x -> x.ToString).ToArray; end; - else ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target); end; @@ -176,15 +185,18 @@ begin if not Self.IsCategorical(target) then ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, target); + + var col := Self[target]; + for var i := 0 to Self.RowCount - 1 do + if not col.IsValid[i] then + ArgumentError(ER_TARGET_HAS_NA, target); case Self.GetColumnType(target) of - ColumnType.ctStr: begin var data := Self.GetStrColumn(target).ToArray; Result := TransformLabels(data, classes); end; - ColumnType.ctInt: begin var data := Self.GetIntColumn(target).ToArray; @@ -195,7 +207,6 @@ begin Result := TransformLabels(strData, classes); end; - else ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target); end; @@ -224,6 +235,11 @@ begin if Self.GetColumnType(target) <> ColumnType.ctInt then ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target); + + var col := Self[target]; + for var i := 0 to Self.RowCount - 1 do + if not col.IsValid[i] then + ArgumentError(ER_TARGET_HAS_NA, target); var data := Self.GetIntColumn(target).ToArray; @@ -253,6 +269,11 @@ begin if Self.GetColumnType(target) <> ColumnType.ctInt then ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target); + var col := Self[target]; + for var i := 0 to Self.RowCount - 1 do + if not col.IsValid[i] then + ArgumentError(ER_TARGET_HAS_NA, target); + var labels := Self.GetIntColumn(target).ToArray; Result := EncodeLabelsInt(labels, classes); diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index e516c41b5..63e3fc97d 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -49,24 +49,14 @@ type procedure RebuildSchema; - // Join методы - - //procedure AppendJoinedRow(leftCur, rightCur: DataFrameCursor; leftKeyIdx, rightKeyIdx: array of integer); - //procedure AppendLeftOnlyRow(leftCur: DataFrameCursor; leftKeyIdx, rightKeyIdx: array of integer); - //procedure AppendRightOnlyRow(rightCur: DataFrameCursor; leftKeyIdx, rightKeyIdx: array of integer; leftColumnCount: integer); - // Single key методы - {function DataFrame.JoinInnerSingleKey(other: DataFrame; leftKey, rightKey: integer; - resultSchema: DataFrameSchema): DataFrame;} function JoinInnerSingleKey(other: DataFrame; key: string): DataFrame; function JoinInnerSingleKeyInt(other: DataFrame; leftKey, rightKey: integer): DataFrame; - function JoinInnerSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame; function JoinInnerSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame; function JoinInnerSingleKeyBool(other: DataFrame; leftKey, rightKey: integer): DataFrame; function LeftJoinSingleKey(other: DataFrame; key: string): DataFrame; function LeftJoinSingleKeyInt(other: DataFrame; leftKey, rightKey: integer): DataFrame; - function LeftJoinSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame; function LeftJoinSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame; function LeftJoinSingleKeyBool(other: DataFrame; leftKey, rightKey: integer): DataFrame; @@ -96,7 +86,6 @@ type function CreateEmptyBySchema(schema: DataFrameSchema): DataFrame; - function GetColumnIndex(name: string): integer; function GetColumn(name: string): Column; function CloneWithCopiedColumns: DataFrame; @@ -713,43 +702,6 @@ begin res.AddIntColumn(name, data, valid); end; -procedure BuildMergedFloatKeyColumnFromFullJoin( - res: DataFrame; - name: string; - leftCol: FloatColumn; - rightCol: FloatColumn; - leftIdx, rightIdx: array of integer -); -begin - var n := leftIdx.Length; - var data := new real[n]; - var valid := new boolean[n]; - - for var i := 0 to n - 1 do - begin - var li := leftIdx[i]; - var ri := rightIdx[i]; - - if li >= 0 then - begin - data[i] := leftCol.Data[li]; - valid[i] := leftCol.IsValid[li]; - end - else if ri >= 0 then - begin - data[i] := rightCol.Data[ri]; - valid[i] := rightCol.IsValid[ri]; - end - else - begin - data[i] := 0.0; - valid[i] := False; - end; - end; - - res.AddFloatColumn(name, data, valid); -end; - procedure BuildMergedStrKeyColumnFromFullJoin( res: DataFrame; name: string; @@ -1032,23 +984,21 @@ function DataFrame.BuildJoinKey(cur: DataFrameCursor; layout: JoinKeyLayout; var begin hasNA := false; - var ic := 0; var fc := 0; var sc := 0; var bc := 0; + var ic := 0; var sc := 0; var bc := 0; // считаем размеры for var i := 0 to layout.ColTypes.Length - 1 do case layout.ColTypes[i] of ctInt: inc(ic); - ctFloat: inc(fc); ctStr: inc(sc); ctBool: inc(bc); end; Result.Ints := new integer[ic]; - Result.Floats := new real[fc]; Result.Strs := new string[sc]; Result.Bools := new boolean[bc]; - ic := 0; fc := 0; sc := 0; bc := 0; + ic := 0; sc := 0; bc := 0; for var i := 0 to layout.ColIndices.Length - 1 do begin @@ -1066,10 +1016,7 @@ begin inc(ic); end; ctFloat: - begin - Result.Floats[fc] := cur.Float(col); - inc(fc); - end; + Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); ctStr: begin Result.Strs[sc] := cur.Str(col); @@ -1080,6 +1027,8 @@ begin Result.Bools[bc] := cur.Bool(col); inc(bc); end; + else + Error(ER_UNSUPPORTED_COLUMN_TYPE, layout.ColTypes[i]); end; end; end; @@ -1104,23 +1053,24 @@ end; function DataFrame.LeftJoinSingleKey(other: DataFrame; key: string): DataFrame; begin - // 1. индексы ключей — через Schema var leftKey := fSchema.IndexOf(key); var rightKey := other.fSchema.IndexOf(key); - // 2. проверка типов ключей — через Schema var lt := fSchema.ColumnTypeAt(leftKey); var rt := other.fSchema.ColumnTypeAt(rightKey); if lt <> rt then Error(ER_JOIN_KEY_TYPE_MISMATCH); - // 3. типоспецифичный алгоритм (КАК РАНЬШЕ) + if lt = ctFloat then + Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); + case lt of ctInt: Result := LeftJoinSingleKeyInt(other, leftKey, rightKey); - ctFloat: Result := LeftJoinSingleKeyFloat(other, leftKey, rightKey); ctStr: Result := LeftJoinSingleKeyStr(other, leftKey, rightKey); ctBool: Result := LeftJoinSingleKeyBool(other, leftKey, rightKey); + else + Error(ER_UNSUPPORTED_COLUMN_TYPE, lt); end; end; @@ -1177,62 +1127,6 @@ begin Result := BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey); end; -function DataFrame.LeftJoinSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame; -begin - Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); - Result := nil; - - {var index := new Dictionary>; - - var rcur := other.GetCursor; - while rcur.MoveNext do - if rcur.IsValid(rightKey) then - begin - var k := rcur.Float(rightKey); - - var lst: List; - if not index.TryGetValue(k, lst) then - begin - lst := new List; - index[k] := lst; - end; - - lst.Add(rcur.Position); - end; - - var leftIdx := new List; - var rightIdx := new List; - - var lcur := GetCursor; - while lcur.MoveNext do - begin - var lpos := lcur.Position; - - if not lcur.IsValid(leftKey) then - begin - leftIdx.Add(lpos); - rightIdx.Add(-1); - continue; - end; - - var k := lcur.Float(leftKey); - - if index.ContainsKey(k) then - foreach var r in index[k] do - begin - leftIdx.Add(lpos); - rightIdx.Add(r); - end - else - begin - leftIdx.Add(lpos); - rightIdx.Add(-1); - end; - end; - - Result := BuildLeftJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey);} -end; - function DataFrame.LeftJoinSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame; begin var index := new Dictionary>; @@ -1540,14 +1434,7 @@ begin ); ctFloat: - BuildMergedFloatKeyColumnFromFullJoin( - res, - name, - FloatColumn(col), - FloatColumn(other.columns[ri]), - leftArr, - rightArr - ); + Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); ctStr: BuildMergedStrKeyColumnFromFullJoin( @@ -1685,15 +1572,7 @@ begin IntColumn(other.columns[ri]), leftArr, rightArr ); - - ctFloat: - BuildMergedFloatKeyColumnFromFullJoin( - res, name, - FloatColumn(col), - FloatColumn(other.columns[ri]), - leftArr, rightArr - ); - + ctStr: BuildMergedStrKeyColumnFromFullJoin( res, name, @@ -1740,11 +1619,15 @@ begin if lt <> rt then Error(ER_JOIN_KEY_TYPE_MISMATCH); + if lt = ctFloat then + Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); + case lt of ctInt: Result := JoinInnerSingleKeyInt(other, leftKey, rightKey); - ctFloat: Result := JoinInnerSingleKeyFloat(other, leftKey, rightKey); ctStr: Result := JoinInnerSingleKeyStr(other, leftKey, rightKey); ctBool: Result := JoinInnerSingleKeyBool(other, leftKey, rightKey); + else + Error(ER_UNSUPPORTED_COLUMN_TYPE, lt); end; end; @@ -1823,49 +1706,6 @@ begin Result := BuildJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey); end; -function DataFrame.JoinInnerSingleKeyFloat(other: DataFrame; leftKey, rightKey: integer): DataFrame; -begin - Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); - Result := nil; - {var index := new Dictionary>; - - var rcur := other.GetCursor; - while rcur.MoveNext do - if rcur.IsValid(rightKey) then - begin - var k := rcur.Float(rightKey); - - var lst: List; - if not index.TryGetValue(k, lst) then - begin - lst := new List; - index[k] := lst; - end; - - lst.Add(rcur.Position); - end; - - var leftIdx := new List; - var rightIdx := new List; - - var lcur := GetCursor; - while lcur.MoveNext do - if lcur.IsValid(leftKey) then - begin - var k := lcur.Float(leftKey); - - var rows: List; - if index.TryGetValue(k, rows) then - foreach var r in rows do - begin - leftIdx.Add(lcur.Position); - rightIdx.Add(r); - end; - end; - - Result := BuildJoinResult(Self, other, leftIdx, rightIdx, leftKey, rightKey);} -end; - function DataFrame.JoinInnerSingleKeyStr(other: DataFrame; leftKey, rightKey: integer): DataFrame; begin var index := new Dictionary>; @@ -2028,9 +1868,17 @@ end; function DataFrame.BuildJoinKeyLayout(keyIndices: array of integer): JoinKeyLayout; begin Result.ColIndices := keyIndices; - Result.ColTypes := ArrGen(keyIndices.Length, - i -> columns[keyIndices[i]].Info.ColType - ); + Result.ColTypes := new ColumnType[keyIndices.Length]; + + for var i := 0 to keyIndices.Length - 1 do + begin + var t := columns[keyIndices[i]].Info.ColType; + + if t = ctFloat then + Error(ER_JOIN_FLOAT_KEY_NOT_SUPPORTED); + + Result.ColTypes[i] := t; + end; end; function DataFrame.Join(other: DataFrame; keys: array of string; kind: JoinKind): DataFrame; @@ -2131,10 +1979,7 @@ function DataFrame.ColumnCount: integer := columns.Count; function DataFrame.ColumnIndex(name: string): integer; begin - for var i := 0 to columns.Count - 1 do - if columns[i].Info.Name = name then - exit(i); - Error(ER_COLUMN_NOT_FOUND, name); + Result := fSchema.IndexOf(name); end; function DataFrame.HasColumn(name: string): boolean; @@ -3121,12 +2966,7 @@ begin rightIdx[i] := right.fSchema.IndexOf(rightKeys[i]); end; - Result := BuildJoinSchema( - right, - leftIdx, - rightIdx, - 'right_' - ); + Result := BuildJoinSchema(right, leftIdx, rightIdx, 'right_'); end; function DataFrame.CreateEmptyBySchema(schema: DataFrameSchema): DataFrame; @@ -3150,14 +2990,9 @@ begin Result := new DataFrame(cols, schema); end; -function DataFrame.GetColumnIndex(name: string): integer; -begin - Result := fschema.IndexOf(name); // если у вас есть такой метод -end; - function DataFrame.GetColumn(name: string): Column; begin - var idx := GetColumnIndex(name); + var idx := ColumnIndex(name); Result := columns[idx]; end; @@ -3212,20 +3047,8 @@ begin while cur.MoveNext do begin - try - data[i] := f(cur); - except - on e: Exception do - begin - data[i] := 0; - valid[i] := False; - i += 1; - continue; - end; - end; - + data[i] := f(cur); valid[i] := True; - i += 1; end; @@ -3251,20 +3074,8 @@ begin while cur.MoveNext do begin - try - data[i] := f(cur); - except - on e: Exception do - begin - data[i] := 0.0; - valid[i] := False; - i += 1; - continue; - end; - end; - + data[i] := f(cur); valid[i] := True; - i += 1; end; @@ -3290,20 +3101,8 @@ begin while cur.MoveNext do begin - try - data[i] := f(cur); - except - on e: Exception do - begin - data[i] := nil; - valid[i] := False; - i += 1; - continue; - end; - end; - + data[i] := f(cur); valid[i] := True; - i += 1; end; @@ -3329,20 +3128,8 @@ begin while cur.MoveNext do begin - try - data[i] := f(cur); - except - on e: Exception do - begin - data[i] := False; - valid[i] := False; - i += 1; - continue; - end; - end; - + data[i] := f(cur); valid[i] := True; - i += 1; end; @@ -3403,20 +3190,8 @@ begin end; // --- вычисление - try - data[row] := f(cur); - except - on e: Exception do - begin - data[row] := 0.0; - valid[row] := False; - row += 1; - continue; - end; - end; - + data[row] := f(cur); valid[row] := True; - row += 1; end; @@ -3455,18 +3230,22 @@ begin var cur := GetCursor; var row := 0; + while cur.MoveNext do begin - try - data[row] := f(cur); - valid[row] := True; - except - on e: Exception do - begin - data[row] := 0; - valid[row] := False; - end; + // NA из исходных данных + if not cur.IsValid(colIndex) then + begin + data[row] := 0; + valid[row] := false; + row += 1; + continue; end; + + // строгий расчёт + data[row] := f(cur); + valid[row] := True; + row += 1; end; @@ -4117,6 +3896,8 @@ end; procedure DataFrame.AssertSchemaConsistent; begin {$IFNDEF Test} + + if columns.Count = 0 then exit; // --- 1. одинаковая RowCount у всех столбцов --- var rc := columns[0].RowCount; @@ -4136,7 +3917,7 @@ begin if not fSchema.HasColumn(name) then Error(ER_SCHEMA_COLUMN_MISSING, name); - var idx := GetColumnIndex(name); + var idx := ColumnIndex(name); if idx <> i then Error(ER_SCHEMA_COLUMN_INDEX_INCONSISTENT, name, idx, i); end; diff --git a/bin/Lib/DataFrameABCCore.pas b/bin/Lib/DataFrameABCCore.pas index 6a18030d3..fd75b9899 100644 --- a/bin/Lib/DataFrameABCCore.pas +++ b/bin/Lib/DataFrameABCCore.pas @@ -98,6 +98,7 @@ type Column = abstract class Info: ColumnInfo; public + IsValid: array of boolean; // Флаги валидности (может быть nil) /// Пытается извлечь i-тое данное из столбца как числовое если это возможно function TryGetNumericValue(i: integer; var value: real): boolean; virtual; abstract; /// Возвращает количество строк в столбце @@ -108,7 +109,6 @@ type IntColumn = class(Column) // Data и IsValid считаются immutable после создания Data: array of integer; // Данные столбца - IsValid: array of boolean; // Флаги валидности (может быть nil) public constructor Create; begin end; constructor Create(name: string); @@ -121,7 +121,6 @@ type /// Столбец вещественных чисел FloatColumn = class(Column) Data: array of real; // Данные столбца - IsValid: array of boolean; // Флаги валидности public constructor Create; begin end; constructor Create(name: string); @@ -135,7 +134,6 @@ type /// Столбец строк StrColumn = class(Column) Data: array of string; // Данные столбца - IsValid: array of boolean; // Флаги валидности public constructor Create; begin end; constructor Create(name: string); @@ -153,7 +151,6 @@ type /// Столбец булевых значений BoolColumn = class(Column) Data: array of boolean; // Данные столбца - IsValid: array of boolean; // Флаги валидности public constructor Create; begin end; constructor Create(name: string); @@ -177,9 +174,9 @@ type ColTypes: array of ColumnType; end; + // Нет Floats - по ним нельзя Join!!! JoinKey = record Ints: array of integer; - Floats: array of real; Strs: array of string; Bools: array of boolean; function Equals(oth: object): boolean; override; @@ -731,16 +728,12 @@ begin var other := JoinKey(oth); if Ints.Length <> other.Ints.Length then exit(false); - if Floats.Length <> other.Floats.Length then exit(false); if Strs.Length <> other.Strs.Length then exit(false); if Bools.Length <> other.Bools.Length then exit(false); for var i := 0 to Ints.Length - 1 do if Ints[i] <> other.Ints[i] then exit(false); - for var i := 0 to Floats.Length - 1 do - if Floats[i] <> other.Floats[i] then exit(false); - for var i := 0 to Strs.Length - 1 do if Strs[i] <> other.Strs[i] then exit(false); @@ -757,9 +750,6 @@ begin foreach var v in Ints do h := h * 31 + v.GetHashCode; - foreach var v in Floats do - h := h * 31 + v.GetHashCode; - foreach var v in Strs do h := h * 31 + (if v = nil then 0 else v.GetHashCode); diff --git a/bin/Lib/MLABC.pas b/bin/Lib/MLABC.pas index cff013057..95b72a5c6 100644 --- a/bin/Lib/MLABC.pas +++ b/bin/Lib/MLABC.pas @@ -1,4 +1,8 @@ -// ============================================================= +/// Основной модуль библиотеки машинного обучения. +/// Объединяет модели, метрики, валидацию и вспомогательные компоненты. +unit MLABC; + +// ============================================================= // СТАТИСТИЧЕСКАЯ ПОЛИТИКА БИБЛИОТЕКИ ML PascalABC.NET // // В библиотеке используются следующие соглашения: @@ -17,9 +21,20 @@ // • алгоритмы ML — дисперсия генеральной совокупности // ============================================================= -/// Основной модуль библиотеки машинного обучения. -/// Объединяет модели, метрики, валидацию и вспомогательные компоненты. -unit MLABC; +// ============================================================= +// PIPELINES +// +// DataFrame-based: +// DataPipeline +// UDataPipeline +// +// Matrix/Vector-based: +// MatrixPipeline +// UMatrixPipeline +// +// Оба варианта являются равноправными и используются +// в зависимости от представления данных. +// ============================================================= interface @@ -74,7 +89,7 @@ type NormType = MLModelsABC.NormType; Activations = MLModelsABC.Activations; - Pipeline = MLModelsABC.Pipeline; + Pipeline = MLModelsABC.MatrixPipeline; LinearRegression = MLModelsABC.LinearRegression; LogisticRegression = MLModelsABC.LogisticRegression; @@ -113,7 +128,7 @@ type IModel = MLCoreABC.IModel; ISupervisedModel = MLCoreABC.ISupervisedModel; IUnsupervisedModel = MLCoreABC.IUnsupervisedModel; - UPipeline = MLModelsABC.UPipeline; + UPipeline = MLModelsABC.UMatrixPipeline; UDataPipeline = MLPipelineABC.UDataPipeline; TaskKind = MLPipelineABC.TaskKind; diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index a23e87d81..9de5e56f8 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -355,7 +355,9 @@ type fCheckConvergence: boolean; fMinImprovement: real; - fClassLabels: array of string; // В каждой модели классификации + fClassLabels: array of string; + + fUseFastExp: boolean; function GetWeights: Matrix; function GetIntercept: Vector; @@ -369,7 +371,8 @@ type epochs: integer := 1000; tol: real := 1e-6; checkConvergence: boolean := true; - minImprovement: real := 1e-8 + minImprovement: real := 1e-8; + useFastExp: boolean := True ); /// Обучает модель логистической регрессии. @@ -380,7 +383,7 @@ type /// Примечание: /// • обученное состояние модели НЕ копируется методом Clone function Fit(X: Matrix; y: Vector): ISupervisedModel; - + /// Возвращает матрицу вероятностей (m x k). function PredictProba(X: Matrix): Matrix; @@ -1571,9 +1574,6 @@ type fTouched: array of integer; fEpoch: integer; - fSampleIdx: array of integer; - fSampleSize: integer; - public /// Создаёт классификатор kNN. /// k — число ближайших соседей (k > 0). @@ -1589,14 +1589,22 @@ type /// • обученное состояние модели (обучающая выборка) НЕ копируется методом Clone function Fit(X: Matrix; y: Vector): ISupervisedModel; override; - /// Выполняет предсказание меток классов для объектов X. - /// Возвращает вектор предсказанных меток +/// Выполняет предсказание меток классов для объектов X. +/// Возвращает вектор предсказанных меток. +/// Не является потокобезопасным: не вызывать одновременно из нескольких потоков +/// для одного экземпляра модели. function Predict(X: Matrix): Vector; override; + /// Выполняет предсказание меток классов для объектов X. + /// Возвращает массив индексов классов. + /// Не является потокобезопасным: не вызывать одновременно из нескольких потоков + /// для одного экземпляра модели. function PredictLabels(X: Matrix): array of integer; /// Возвращает матрицу вероятностей размера (nSamples × nClasses). - /// Столбцы соответствуют классам в порядке, возвращаемом GetClasses() + /// Столбцы соответствуют классам в порядке, возвращаемом GetClasses(). + /// Не является потокобезопасным: не вызывать одновременно из нескольких потоков + /// для одного экземпляра модели. function PredictProba(X: Matrix): Matrix; /// Возвращает массив меток классов в порядке столбцов PredictProba @@ -1839,7 +1847,7 @@ type {$endregion Models} -{$region Pipeline} +{$region MatrixPipeline} /// Последовательный конвейер машинного обучения (supervised). /// Гарантирует строгий порядок выполнения шагов: /// [преобразователи] → [модель]. @@ -1854,7 +1862,10 @@ type /// /// Обеспечивает единый интерфейс Fit(X, y) / Predict(X) /// и воспроизводимость полного процесса обучения - Pipeline = class(ISupervisedModel) +/// +/// MatrixPipeline используется, когда данные уже представлены +/// в виде Matrix X и Vector y + MatrixPipeline = class(ISupervisedModel) private fTransformers: List; fModel: ISupervisedModel; @@ -1870,7 +1881,7 @@ type constructor Create; { - // Pipeline.Build используется, когда данные уже представлены + // MatrixPipeline.Build используется, когда данные уже представлены // в виде числовой матрицы признаков X и вектора целевой переменной y. // В этом случае DataFrame и препроцессоры уровня таблицы не требуются. // @@ -1881,7 +1892,7 @@ type // • подбор гиперпараметров // • тестирование моделей // - // Pipeline объединяет несколько матричных преобразований (ITransformer) + // MatrixPipeline объединяет несколько матричных преобразований (ITransformer) // и модель (IModel) в единый объект, который можно обучать и использовать // для предсказаний. // @@ -1892,13 +1903,13 @@ type // перед преобразованием данных в Matrix/Vector. var pipe1 := - Pipeline.Build( + MatrixPipeline.Build( new StandardScaler, new LogisticRegression ); var pipe2 := - Pipeline.Build( + MatrixPipeline.Build( new StandardScaler, new PCATransformer(2), new LogisticRegression @@ -1931,13 +1942,13 @@ type /// сначала преобразователи, затем модель. /// Последний шаг обязан быть моделью (IModel). /// Возвращает сконструированный конвейер. - static function Build(params steps: array of IPipelineStep): Pipeline; + static function Build(params steps: array of IPipelineStep): MatrixPipeline; /// Устанавливает или заменяет модель. - function SetModel(m: ISupervisedModel): Pipeline; + function SetModel(m: ISupervisedModel): MatrixPipeline; /// Добавляет преобразование в конец пайплайна - function Add(t: ITransformer): Pipeline; + function Add(t: ITransformer): MatrixPipeline; /// Обучает пайплайн на данных. /// X — матрица m × n (m объектов, n признаков). @@ -1962,6 +1973,7 @@ type /// Показывает, был ли пайплайн обучен (вызван метод Fit). property IsFitted: boolean read fFitted; +/// Возвращает строковое представление пайплайна. function ToString: string; override; /// Создаёт копию пайплайна с теми же шагами и моделью. @@ -1979,19 +1991,48 @@ type function Name: string := Self.GetType.Name; end; - UPipeline = class(IUnsupervisedModel) +/// Последовательный конвейер машинного обучения (unsupervised). +/// Гарантирует строгий порядок выполнения шагов: +/// [преобразователи]. +/// +/// Поддерживает: +/// • преобразователи без учёта целевой переменной (unsupervised). +/// +/// Все преобразователи применяются последовательно к признакам X. +/// +/// Обеспечивает единый интерфейс Fit(X) / Transform(X) +/// и воспроизводимость полного процесса преобразования данных. +/// +/// UMatrixPipeline используется, когда данные уже представлены +/// в виде Matrix X и отсутствует целевая переменная + UMatrixPipeline = class(IUnsupervisedModel) private fTransformers: List; fModel: IModel; fFitted: boolean; public + /// Создаёт конвейер машинного обучения для заданной модели: +/// • model — модель, которая будет применена +/// после последовательного применения всех преобразователей. +/// Модель должна реализовывать интерфейс IModel (без учёта целевой переменной) constructor Create(model: IModel); + +/// Создаёт пустой пайплайн (конвейер машинного обучения). +/// Модель должна быть установлена через SetModel constructor Create; - static function Build(params steps: array of IPipelineStep): UPipeline; +/// Строит конвейер машинного обучения из последовательности шагов. +/// Шаги указываются в порядке выполнения: +/// сначала преобразователи, затем модель. +/// Последний шаг обязан быть моделью (IModel). +/// Возвращает сконструированный конвейер. + static function Build(params steps: array of IPipelineStep): UMatrixPipeline; - function SetModel(m: IModel): UPipeline; - function Add(t: ITransformer): UPipeline; +/// Устанавливает или заменяет модель. + function SetModel(m: IModel): UMatrixPipeline; + +/// Добавляет преобразование в конец пайплайна + function Add(t: ITransformer): UMatrixPipeline; /// Обучает пайплайн на данных. /// X — матрица m × n (m объектов, n признаков). @@ -2001,11 +2042,30 @@ type /// Примечание: /// • обученное состояние пайплайна НЕ копируется методом Clone function Fit(X: Matrix): IUnsupervisedModel; + +/// Применяет последовательность преобразований к данным. +/// X — матрица m × n (m объектов, n признаков). +/// Последовательно применяет все трансформеры к данным, +/// после чего возвращает преобразованную матрицу. +/// +/// Примечание: +/// • обученное состояние пайплайна НЕ копируется методом Clone function Transform(X: Matrix): Matrix; + +/// Применяет пайплайн и возвращает результат модели. +/// X — матрица m × n (m объектов, n признаков). +/// После применения всех преобразований вызывает модель, +/// реализующую интерфейс IModel. +/// +/// Примечание: +/// • семантика результата зависит от конкретной модели +/// (например, кластерные метки, оценки и т.д.) function Predict(X: Matrix): Vector; +/// Показывает, был ли пайплайн обучен (вызван метод Fit у шагов пайплайна). property IsFitted: boolean read fFitted; +/// Возвращает строковое представление пайплайна. function ToString: string; override; /// Создаёт копию пайплайна с теми же шагами и моделью. @@ -2016,13 +2076,13 @@ type /// • Все шаги и модель клонируются через их Clone (config-only) /// /// Назначение: -/// • использование в CrossValidate и Pipeline +/// • использование в CrossValidate и MatrixPipeline /// • обеспечивает независимое переобучение пайплайна function Clone: IModel; function Name: string := Self.GetType.Name; end; -{$endregion Pipeline} +{$endregion MatrixPipeline} {$region Transformers} @@ -2078,7 +2138,7 @@ type /// • Возвращаемый объект эквивалентен новому экземпляру после Create(...) /// /// Назначение: -/// • использование в CrossValidate, GridSearch и Pipeline +/// • использование в CrossValidate, GridSearch и MatrixPipeline /// • гарантирует независимое переобучение трансформеров /// /// Примечание: @@ -2136,7 +2196,7 @@ type /// • Возвращаемый объект эквивалентен новому экземпляру после Create(...) /// /// Назначение: -/// • использование в CrossValidate, GridSearch и Pipeline +/// • использование в CrossValidate, GridSearch и MatrixPipeline /// • гарантирует независимое переобучение трансформеров /// /// Примечание: @@ -2190,7 +2250,7 @@ type /// • Возвращаемый объект эквивалентен новому экземпляру после Create(...) /// /// Назначение: -/// • использование в CrossValidate, GridSearch и Pipeline +/// • использование в CrossValidate, GridSearch и MatrixPipeline /// • гарантирует независимое переобучение трансформеров /// /// Примечание: @@ -2243,7 +2303,7 @@ type /// • Возвращаемый объект эквивалентен новому экземпляру после Create(...) /// /// Назначение: -/// • использование в CrossValidate, GridSearch и Pipeline +/// • использование в CrossValidate, GridSearch и MatrixPipeline /// • гарантирует независимое переобучение трансформеров /// /// Примечание: @@ -2340,7 +2400,7 @@ type /// • Возвращаемый объект эквивалентен новому экземпляру после Create(...) /// /// Назначение: -/// • использование в CrossValidate, GridSearch и Pipeline +/// • использование в CrossValidate, GridSearch и MatrixPipeline /// • гарантирует независимое переобучение трансформеров /// /// Примечание: @@ -2398,7 +2458,7 @@ type /// • Возвращаемый объект эквивалентен новому экземпляру после Create(...) /// /// Назначение: -/// • использование в CrossValidate, GridSearch и Pipeline +/// • использование в CrossValidate, GridSearch и MatrixPipeline /// • гарантирует независимое переобучение трансформеров /// /// Примечание: @@ -3100,7 +3160,7 @@ end; //----------------------------- constructor LogisticRegression.Create(lambda: real; learningRate: real; epochs: integer; - tol: real; checkConvergence: boolean; minImprovement: real); + tol: real; checkConvergence: boolean; minImprovement: real; useFastExp: boolean); begin fLambda := lambda; fLearningRate := learningRate; @@ -3109,8 +3169,19 @@ begin fTol := tol; fCheckConvergence := checkConvergence; fMinImprovement := minImprovement; + fUseFastExp := useFastExp end; +function FastExp(x: real): real; +begin + if x < -5 then exit(0.0); + if x > 5 then x := 5; + + Result := 1.0 + x + 0.5*x*x + (1.0/6.0)*x*x*x; +end; + +type RealArr = array of real; + function LogisticRegression.Fit(X: Matrix; y: Vector): ISupervisedModel; begin if X = nil then @@ -3179,6 +3250,228 @@ begin fIndexToClass[i] := unique[i]; end; + // --- init + fW := new Matrix(p, fClassCount); + var scale := 0.01; + for var j := 0 to p - 1 do + for var k := 0 to fClassCount - 1 do + fW.Data[j,k] := (Random - 0.5) * 2 * scale; + + fIntercept := new Vector(fClassCount); + + var prevLoss := real.PositiveInfinity; + + var xRows := X.Data.Rows; + var gradW := new RealArr[fClassCount]; + for var k := 0 to fClassCount - 1 do + gradW[k] := new real[p]; + + var gradB := new real[fClassCount]; + + var zi := new real[fClassCount]; + + for var epoch := 1 to fEpochs do + begin + var WCols := fW.Data.Cols; + //var WRows := fW.Data.Rows; + + var loss := 0.0; + + // --- zero gradients + &Array.Clear(gradB, 0, fClassCount); + for var k := 0 to fClassCount - 1 do + &Array.Clear(gradW[k], 0, p); + + // --- one pass: logits -> softmax -> loss -> gradient + for var i := 0 to m - 1 do + begin + var xi := XRows[i]; + var yi := yEncoded[i]; + + // --- logits + for var k := 0 to fClassCount - 1 do + begin + var wk := WCols[k]; + var s := fIntercept.Data[k]; + + for var j := 0 to p - 1 do + s += xi[j] * wk[j]; + + zi[k] := s; + end; + + // --- stable softmax + var maxVal := zi[0]; + for var k := 1 to fClassCount - 1 do + if zi[k] > maxVal then + maxVal := zi[k]; + + var sumExp := 0.0; + for var k := 0 to fClassCount - 1 do + begin + var v: real; + if fUseFastExp then + v := FastExp(zi[k] - maxVal) + else + v := Exp(zi[k] - maxVal); + zi[k] := v; + sumExp += v; + end; + + if sumExp <= 0 then + begin + var uniformProb := 1.0 / fClassCount; + + for var k := 0 to fClassCount - 1 do + zi[k] := uniformProb; + end + else + begin + var invSum := 1.0 / sumExp; + + for var k := 0 to fClassCount - 1 do + zi[k] *= invSum; + end; + + // --- loss + var prob := zi[yi]; + if prob < 1e-300 then + prob := 1e-300; + + loss -= Ln(prob); + + // --- gradient + for var k := 0 to fClassCount - 1 do + begin + var diff := zi[k] - Ord(k = yi); + + gradB[k] += diff; + + var gwk := gradW[k]; + for var j := 0 to p - 1 do + gwk[j] += xi[j] * diff; + end; + end; + + loss /= m; + + // --- L2 penalty + if fLambda <> 0 then + begin + var l2 := 0.0; + for var j := 0 to p - 1 do + for var k := 0 to fClassCount - 1 do + l2 += fW.Data[j,k] * fW.Data[j,k]; + + loss += 0.5 * fLambda * l2; + end; + + // --- divergence check + if double.IsNaN(loss) or double.IsInfinity(loss) then + ArgumentError(ER_LOGISTIC_INVALID_LOSS); + + // --- convergence check + if fCheckConvergence then + begin + if Abs(prevLoss - loss) < Max(fMinImprovement, fTol * Max(1.0, Abs(prevLoss))) then + break; + + prevLoss := loss; + end; + + // --- update + var invM := 1.0 / m; + + for var k := 0 to fClassCount - 1 do + begin + gradB[k] *= invM; + fIntercept.Data[k] -= fLearningRate * gradB[k]; + + for var j := 0 to p - 1 do + begin + var g := gradW[k][j] * invM; + + if fLambda <> 0 then + g += fLambda * fW.Data[j,k]; + + fW.Data[j,k] -= fLearningRate * g; + end; + end; + end; + + fFitted := true; + Result := Self; +end; + + +{function LogisticRegression.FitOld(X: Matrix; y: Vector): ISupervisedModel; +begin + if X = nil then + ArgumentNullError(ER_X_NULL); + + if y = nil then + ArgumentNullError(ER_Y_NULL); + + if MLConfig.ValidateFiniteInputs then + begin + CheckXForFit(X); + CheckYForFit(y); + end; + + if X.RowCount = 0 then + ArgumentError(ER_EMPTY_DATASET); + + if X.RowCount <> y.Length then + DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length); + + if fLearningRate <= 0 then + ArgumentOutOfRangeError(ER_LEARNING_RATE_INVALID, fLearningRate); + + if fEpochs <= 0 then + ArgumentOutOfRangeError(ER_EPOCHS_INVALID, fEpochs); + + if fLambda < 0 then + ArgumentOutOfRangeError(ER_LAMBDA_NEGATIVE, fLambda); + + if fCheckConvergence and (fTol <= 0) then + ArgumentOutOfRangeError(ER_TOL_INVALID, fTol); + + var m := X.RowCount; + var p := X.ColCount; + + // --- convert to integer labels + 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); + + 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 fW := new Matrix(p, fClassCount); fIntercept := new Vector(fClassCount); @@ -3301,7 +3594,7 @@ begin fFitted := true; Result := Self; -end; +end;} function LogisticRegression.PredictProba(X: Matrix): Matrix; begin @@ -3394,7 +3687,8 @@ begin fEpochs, fTol, fCheckConvergence, - fMinImprovement + fMinImprovement, + fUseFastExp ); end; @@ -4050,6 +4344,7 @@ begin fMinSamplesSplit, fMinSamplesLeaf, fCriterion, + fClassCount, fMaxFeatures, fRandomSeed ); @@ -7256,15 +7551,6 @@ begin // --- copy train data fXTrain := X.Clone; - fSampleSize := Min(3000, fXTrain.RowCount); // попробуй 2000–4000 - - SetLength(fSampleIdx, fSampleSize); - - var rnd := new System.Random(42); - - for var i := 0 to fSampleSize - 1 do - fSampleIdx[i] := rnd.Next(fXTrain.RowCount); - // ========================================================= // ЕДИНЫЙ ENCODING // ========================================================= @@ -8269,17 +8555,17 @@ end; function DBSCAN.ClustersCount: integer := fClusterCount; //----------------------------- -// Pipeline +// MatrixPipeline //----------------------------- -constructor Pipeline.Create; +constructor MatrixPipeline.Create; begin fTransformers := new List; fModel := nil; fFitted := false; end; -constructor Pipeline.Create(model: ISupervisedModel); +constructor MatrixPipeline.Create(model: ISupervisedModel); begin Create; if model = nil then @@ -8287,7 +8573,7 @@ begin fModel := model; end; -class function Pipeline.Build(params steps: array of IPipelineStep): Pipeline; +class function MatrixPipeline.Build(params steps: array of IPipelineStep): MatrixPipeline; begin if (steps = nil) or (Length(steps) = 0) then ArgumentError(ER_PIPELINE_NO_STEPS); @@ -8301,7 +8587,7 @@ begin if not (last is ISupervisedModel) then ArgumentError(ER_PIPELINE_LAST_NOT_SUPERVISED_MODEL); - var pipe := new Pipeline(last as ISupervisedModel); + var pipe := new MatrixPipeline(last as ISupervisedModel); // все шаги кроме последнего — трансформеры for var i := 0 to High(steps) - 1 do @@ -8320,7 +8606,7 @@ begin Result := pipe; end; -function Pipeline.Add(t: ITransformer): Pipeline; +function MatrixPipeline.Add(t: ITransformer): MatrixPipeline; begin if t = nil then ArgumentError(ER_TRANSFORMER_NULL); @@ -8329,7 +8615,7 @@ begin Result := Self; end; -function Pipeline.SetModel(m: ISupervisedModel): Pipeline; +function MatrixPipeline.SetModel(m: ISupervisedModel): MatrixPipeline; begin if m = nil then ArgumentError(ER_MODEL_NULL); @@ -8338,7 +8624,7 @@ begin Result := Self; end; -function Pipeline.Fit(X: Matrix; y: Vector): ISupervisedModel; +function MatrixPipeline.Fit(X: Matrix; y: Vector): ISupervisedModel; begin if fModel = nil then ArgumentError(ER_MODEL_NULL); @@ -8386,7 +8672,7 @@ begin Result := Self; end; -function Pipeline.Transform(X: Matrix): Matrix; +function MatrixPipeline.Transform(X: Matrix): Matrix; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -8410,7 +8696,7 @@ begin Result := Xt; end; -function Pipeline.Predict(X: Matrix): Vector; +function MatrixPipeline.Predict(X: Matrix): Vector; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -8422,7 +8708,7 @@ begin Result := fModel.Predict(Xt); end; -function Pipeline.PredictProba(X: Matrix): Matrix; +function MatrixPipeline.PredictProba(X: Matrix): Matrix; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -8439,9 +8725,9 @@ begin .PredictProba(Xt); end; -function Pipeline.ToString: string; +function MatrixPipeline.ToString: string; begin - var sb := 'Pipeline (' + + var sb := 'MatrixPipeline (' + (if fFitted then 'trained' else 'not trained') + '):' + NewLine; var idx := 1; @@ -8458,12 +8744,12 @@ begin Result := sb; end; -function Pipeline.Clone: IModel; +function MatrixPipeline.Clone: IModel; begin if fModel = nil then ArgumentError(ER_MODEL_NULL); - var p := new Pipeline; + var p := new MatrixPipeline; foreach var t in fTransformers do p.Add(t.Clone); @@ -8479,16 +8765,16 @@ begin end; //----------------------------- -// UPipeline +// UMatrixPipeline //----------------------------- -constructor UPipeline.Create; +constructor UMatrixPipeline.Create; begin fTransformers := new List; fModel := nil; fFitted := false; end; -constructor UPipeline.Create(model: IModel); +constructor UMatrixPipeline.Create(model: IModel); begin Create; if model = nil then @@ -8496,7 +8782,7 @@ begin fModel := model; end; -class function UPipeline.Build(params steps: array of IPipelineStep): UPipeline; +class function UMatrixPipeline.Build(params steps: array of IPipelineStep): UMatrixPipeline; begin if (steps = nil) or (Length(steps) = 0) then ArgumentError(ER_PIPELINE_NO_STEPS); @@ -8509,7 +8795,7 @@ begin if not (last is IModel) then ArgumentError(ER_PIPELINE_LAST_NOT_MODEL); - var pipe := new UPipeline(last as IModel); + var pipe := new UMatrixPipeline(last as IModel); for var i := 0 to High(steps) - 1 do begin @@ -8527,7 +8813,7 @@ begin Result := pipe; end; -function UPipeline.Add(t: ITransformer): UPipeline; +function UMatrixPipeline.Add(t: ITransformer): UMatrixPipeline; begin if t = nil then ArgumentError(ER_TRANSFORMER_NULL); @@ -8536,7 +8822,7 @@ begin Result := Self; end; -function UPipeline.SetModel(m: IModel): UPipeline; +function UMatrixPipeline.SetModel(m: IModel): UMatrixPipeline; begin if m = nil then ArgumentError(ER_MODEL_NULL); @@ -8545,7 +8831,7 @@ begin Result := Self; end; -function UPipeline.Fit(X: Matrix): IUnsupervisedModel; +function UMatrixPipeline.Fit(X: Matrix): IUnsupervisedModel; begin if fModel = nil then ArgumentError(ER_MODEL_NULL); @@ -8585,7 +8871,7 @@ begin Result := Self; end; -function UPipeline.Transform(X: Matrix): Matrix; +function UMatrixPipeline.Transform(X: Matrix): Matrix; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -8609,7 +8895,7 @@ begin Result := Xt; end; -function UPipeline.Predict(X: Matrix): Vector; +function UMatrixPipeline.Predict(X: Matrix): Vector; begin if not fFitted then NotFittedError(ER_FIT_NOT_CALLED); @@ -8625,9 +8911,9 @@ begin Result := (fModel as IPredictiveModel).Predict(Xt); end; -function UPipeline.ToString: string; +function UMatrixPipeline.ToString: string; begin - var sb := 'UPipeline (' + + var sb := 'UMatrixPipeline (' + (if fFitted then 'trained' else 'not trained') + '):' + NewLine; var idx := 1; @@ -8644,12 +8930,12 @@ begin Result := sb; end; -function UPipeline.Clone: IModel; +function UMatrixPipeline.Clone: IModel; begin if fModel = nil then ArgumentError(ER_MODEL_NULL); - var p := new UPipeline; + var p := new UMatrixPipeline; foreach var t in fTransformers do p.Add(t.Clone); diff --git a/bin/Lib/MLPipelineABC.pas b/bin/Lib/MLPipelineABC.pas index 363f0b6f3..9d132ca50 100644 --- a/bin/Lib/MLPipelineABC.pas +++ b/bin/Lib/MLPipelineABC.pas @@ -298,7 +298,8 @@ const 'Model is not a classifier'; ER_MODEL_CLONE_TYPE = 'Clone модели вернул неподдерживаемый тип!!Model Clone returned unsupported type'; - + ER_INVALID_MODEL_TYPE = + 'Clone модели вернул неподдерживаемый тип (ожидается {0})!!Model Clone returned unsupported type (expected {0})'; //----------------------------- // DataPipeline //----------------------------- @@ -1143,7 +1144,14 @@ begin // --- модель if fModel <> nil then - p.fModel := fModel.Clone as IUnsupervisedModel; + begin + var m := fModel.Clone; + + if not (m is IUnsupervisedModel) then + Error(ER_INVALID_MODEL_TYPE, 'IUnsupervisedModel'); + + p.fModel := m as IUnsupervisedModel; + end; // --- состояние НЕ копируем p.fFinalFeatures := nil; diff --git a/bin/Lib/MLUtilsABC.pas b/bin/Lib/MLUtilsABC.pas index 155c00e64..b5c6183d7 100644 --- a/bin/Lib/MLUtilsABC.pas +++ b/bin/Lib/MLUtilsABC.pas @@ -40,6 +40,7 @@ function IntsToLabels(a: array of integer): Vector; /// Каждому уникальному значению присваивается номер 0,1,2,... /// Порядок кодирования соответствует порядку первого появления меток. /// Используется при обучении моделей и визуализации. +/// Предполагается, что входные данные уже очищены от пропущенных значений. function EncodeLabels(labels: array of string): array of integer; /// Кодирует строковые метки классов в целочисленные индексы. @@ -47,6 +48,7 @@ function EncodeLabels(labels: array of string): array of integer; /// Порядок кодирования соответствует порядку первого появления меток. /// В параметр classes возвращается массив уникальных значений в порядке кодирования. /// Используется при обучении моделей и визуализации +/// Предполагается, что входные данные уже очищены от пропущенных значений. function EncodeLabels(labels: array of string; var classes: array of string): array of integer; /// Кодирует целые метки классов в целочисленные индексы. @@ -54,6 +56,7 @@ function EncodeLabels(labels: array of string; var classes: array of string): ar /// Порядок кодирования соответствует порядку первого появления меток. /// В параметр classes возвращается массив уникальных значений в порядке кодирования. /// Используется при обучении моделей и визуализации +/// Предполагается, что входные данные уже очищены от пропущенных значений. function EncodeLabelsInt(labels: array of integer; var classes: array of integer): array of integer; /// Преобразует строковые метки классов в целочисленные индексы @@ -61,6 +64,7 @@ function EncodeLabelsInt(labels: array of integer; var classes: array of integer /// classes должен быть получен из EncodeLabels. /// Если встречается неизвестная метка — выбрасывается исключение. /// Используется для применения кодирования к тестовым данным (Transform). +/// Предполагается, что входные данные уже очищены от пропущенных значений. function TransformLabels(labels: array of string; classes: array of string): array of integer; /// Преобразует целочисленные метки классов в индексы (0,1,2,...) @@ -68,11 +72,13 @@ function TransformLabels(labels: array of string; classes: array of string): arr /// classes должен быть получен из EncodeLabelsInt. /// Если встречается неизвестное значение — выбрасывается исключение. /// Используется для применения кодирования к тестовым данным (Transform). +/// Предполагается, что входные данные уже очищены от пропущенных значений. function TransformLabelsInt(labels: array of integer; classes: array of integer): array of integer; /// Преобразует целочисленные индексы классов обратно в строковые метки. /// Массив classes задаёт соответствие: classes[i] — имя класса с индексом i. /// Используется для получения текстовых предсказаний моделей. +/// Предполагается, что входные данные уже корректны. function DecodeLabels(y: array of integer; classes: array of string): array of string; /// Возвращает список уникальных меток классов. @@ -266,7 +272,17 @@ end; function UniqueLabels(labels: array of string): array of string; begin - Result := labels.Distinct.ToArray; + var seen := new HashSet; + var res := new List; + + for var i := 0 to labels.Length - 1 do + if not seen.Contains(labels[i]) then + begin + seen.Add(labels[i]); + res.Add(labels[i]); + end; + + Result := res.ToArray; end; function CloneOrNil(v: Vector): Vector; diff --git a/bin/Lib/MetricsABC.pas b/bin/Lib/MetricsABC.pas index 0f7bcfe96..a94176bde 100644 --- a/bin/Lib/MetricsABC.pas +++ b/bin/Lib/MetricsABC.pas @@ -227,6 +227,9 @@ type /// Ожидается, что yTrue и yPred содержат закодированные метки классов /// (целые значения 0..K-1, допускается хранение в Vector типа real). /// Для строковых меток необходимо предварительно применить EncodeLabels + /// + /// Метки классов упорядочиваются по возрастанию для обеспечения + /// стабильного и предсказуемого представления результатов ConfusionMatrix = class private fMatrix: array[,] of integer; diff --git a/bin/Lib/PABCSystem.pas b/bin/Lib/PABCSystem.pas index 4bd874cbf..f6da2be05 100644 --- a/bin/Lib/PABCSystem.pas +++ b/bin/Lib/PABCSystem.pas @@ -11641,103 +11641,128 @@ end; // Partition, ZipTuple, UnZipTuple, Interleave, Numerate, Tabulate, Pairwise, Batch // Дополнения 2024: Zip - синоним ZipTuple +/// Возвращает первый элемент последовательности с минимальным значением ключа /// Возвращает первый элемент последовательности с минимальным значением ключа function MinBy(Self: sequence of T; keySelector: T -> TKey): T; extensionmethod; begin - var enumerator := Self.GetEnumerator(); - if not enumerator.MoveNext() then - raise new System.ArgumentException(GetTranslation(SEQUENCE_CANNOT_BE_EMPTY)); - - var minElement := enumerator.Current; - var minKey := keySelector(minElement); - var comp := Comparer&.Default; - while enumerator.MoveNext() do - begin - var currentElement := enumerator.Current; - var currentKey := keySelector(currentElement); - if comp.Compare(currentKey,minKey) < 0 then - begin - minKey := currentKey; - minElement := currentElement; - end; - end; + var minElement: T; + var minKey: TKey; + var hasValue := False; + foreach var current in Self do + if not hasValue then + begin + minElement := current; + minKey := keySelector(current); + hasValue := True; + end + else + begin + var currentKey := keySelector(current); + if comp.Compare(currentKey, minKey) < 0 then // только строго меньше + begin + minKey := currentKey; + minElement := current; + end; + end; + + if not hasValue then + raise new ArgumentException(GetTranslation(SEQUENCE_CANNOT_BE_EMPTY)); + Result := minElement; end; /// Возвращает первый элемент последовательности с максимальным значением ключа function MaxBy(Self: sequence of T; keySelector: T -> TKey): T; extensionmethod; begin - var enumerator := Self.GetEnumerator(); - if not enumerator.MoveNext() then - raise new System.ArgumentException(GetTranslation(SEQUENCE_CANNOT_BE_EMPTY)); - - var maxElement := enumerator.Current; - var maxKey := keySelector(maxElement); - var comp := Comparer&.Default; - while enumerator.MoveNext() do - begin - var currentElement := enumerator.Current; - var currentKey := keySelector(currentElement); - if comp.Compare(currentKey,maxKey) > 0 then - begin - maxKey := currentKey; - maxElement := currentElement; - end; - end; + var maxElement: T; + var maxKey: TKey; + var hasValue := False; + foreach var current in Self do + if not hasValue then + begin + maxElement := current; + maxKey := keySelector(current); + hasValue := True; + end + else + begin + var currentKey := keySelector(current); + if comp.Compare(currentKey, maxKey) > 0 then + begin + maxKey := currentKey; + maxElement := current; + end; + end; + + if not hasValue then + raise new ArgumentException(GetTranslation(SEQUENCE_CANNOT_BE_EMPTY)); + Result := maxElement; end; /// Возвращает последний элемент последовательности с минимальным значением ключа -function LastMinBy(Self: sequence of T; keySelector: T -> TKey): T; extensionmethod; +function LastMinBy(Self: sequence of T; keySelector: T -> TKey): T; extensionmethod; begin - var enumerator := Self.GetEnumerator(); - if not enumerator.MoveNext() then - raise new System.ArgumentException(GetTranslation(SEQUENCE_CANNOT_BE_EMPTY)); - - var minElement := enumerator.Current; - var minKey := keySelector(minElement); - var comp := Comparer&.Default; - while enumerator.MoveNext() do - begin - var currentElement := enumerator.Current; - var currentKey := keySelector(currentElement); - if comp.Compare(currentKey,minKey) <= 0 then - begin - minKey := currentKey; - minElement := currentElement; - end; - end; + var minElement: T; + var minKey: TKey; + var hasValue := False; + foreach var current in Self do + if not hasValue then + begin + minElement := current; + minKey := keySelector(current); + hasValue := True; + end + else + begin + var currentKey := keySelector(current); + if comp.Compare(currentKey, minKey) <= 0 then // <= для последнего минимума + begin + minKey := currentKey; + minElement := current; + end; + end; + + if not hasValue then + raise new ArgumentException(GetTranslation(SEQUENCE_CANNOT_BE_EMPTY)); + Result := minElement; end; /// Возвращает последний элемент последовательности с максимальным значением ключа -function LastMaxBy(Self: sequence of T; keySelector: T -> TKey): T; extensionmethod; +function LastMaxBy(Self: sequence of T; keySelector: T -> TKey): T; extensionmethod; begin - var enumerator := Self.GetEnumerator(); - if not enumerator.MoveNext() then - raise new System.ArgumentException(GetTranslation(SEQUENCE_CANNOT_BE_EMPTY)); - - var maxElement := enumerator.Current; - var maxKey := keySelector(maxElement); - var comp := Comparer&.Default; - while enumerator.MoveNext() do - begin - var currentElement := enumerator.Current; - var currentKey := keySelector(currentElement); - if comp.Compare(currentKey,maxKey) > 0 then - begin - maxKey := currentKey; - maxElement := currentElement; - end; - end; + var maxElement: T; + var maxKey: TKey; + var hasValue := False; + foreach var current in Self do + if not hasValue then + begin + maxElement := current; + maxKey := keySelector(current); + hasValue := True; + end + else + begin + var currentKey := keySelector(current); + if comp.Compare(currentKey, maxKey) >= 0 then + begin + maxKey := currentKey; + maxElement := current; + end; + end; + + if not hasValue then + raise new ArgumentException(GetTranslation(SEQUENCE_CANNOT_BE_EMPTY)); + Result := maxElement; end; diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index a85a58a8a..e58ad6465 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -323,11 +323,11 @@ begin var cnt := pair.Value.Count; // Класс может иметь меньше объектов, чем число фолдов. - // В этом случае он не будет представлен во всех фолдах. - // Это допустимо, но может привести к ошибкам при обучении модели. - - //if cnt < k then - // ArgumentError(ER_STRATIFIED_CLASS_TOO_SMALL, cls, cnt, k); + // В библиотеке принята строгая политика: такие случаи считаются ошибкой, + // так как не гарантируется присутствие класса во всех train-fold. + // Поэтому выполняется fail-fast проверка (см. ниже). + if cnt < k then + ArgumentError(ER_STRATIFIED_CLASS_TOO_SMALL, cls, cnt, k); end; // --- 2. Контейнеры фолдов