LastMaxBy LastMinBy исправления

This commit is contained in:
Mikhalkovich Stanislav 2026-04-22 14:08:27 +03:00
parent dd7a305533
commit 2294ce5844
14 changed files with 593 additions and 448 deletions

View file

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

View file

@ -1,4 +1,4 @@
%COREVERSION%=1
%REVISION%=3808
%REVISION%=3810
%MINOR%=11
%MAJOR%=3

View file

@ -1 +1 @@
3.11.1.3808
3.11.1.3810

View file

@ -1 +1 @@
!define VERSION '3.11.1.3808'
!define VERSION '3.11.1.3810'

View file

@ -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);

View file

@ -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<real, List<integer>>;
var rcur := other.GetCursor;
while rcur.MoveNext do
if rcur.IsValid(rightKey) then
begin
var k := rcur.Float(rightKey);
var lst: List<integer>;
if not index.TryGetValue(k, lst) then
begin
lst := new List<integer>;
index[k] := lst;
end;
lst.Add(rcur.Position);
end;
var leftIdx := new List<integer>;
var rightIdx := new List<integer>;
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<string, List<integer>>;
@ -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<real, List<integer>>;
var rcur := other.GetCursor;
while rcur.MoveNext do
if rcur.IsValid(rightKey) then
begin
var k := rcur.Float(rightKey);
var lst: List<integer>;
if not index.TryGetValue(k, lst) then
begin
lst := new List<integer>;
index[k] := lst;
end;
lst.Add(rcur.Position);
end;
var leftIdx := new List<integer>;
var rightIdx := new List<integer>;
var lcur := GetCursor;
while lcur.MoveNext do
if lcur.IsValid(leftKey) then
begin
var k := lcur.Float(leftKey);
var rows: List<integer>;
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<string, List<integer>>;
@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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<ITransformer>;
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<ITransformer>;
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<integer, integer>;
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); // попробуй 20004000
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<ITransformer>;
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<ITransformer>;
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);

View file

@ -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;

View file

@ -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<string>;
var res := new List<string>;
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;

View file

@ -227,6 +227,9 @@ type
/// Ожидается, что yTrue и yPred содержат закодированные метки классов
/// (целые значения 0..K-1, допускается хранение в Vector типа real).
/// Для строковых меток необходимо предварительно применить EncodeLabels
///
/// Метки классов упорядочиваются по возрастанию для обеспечения
/// стабильного и предсказуемого представления результатов
ConfusionMatrix = class
private
fMatrix: array[,] of integer;

View file

@ -11641,103 +11641,128 @@ end;
// Partition, ZipTuple, UnZipTuple, Interleave, Numerate, Tabulate, Pairwise, Batch
// Дополнения 2024: Zip - синоним ZipTuple
/// Возвращает первый элемент последовательности с минимальным значением ключа
/// Возвращает первый элемент последовательности с минимальным значением ключа
function MinBy<T, TKey>(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&<TKey>.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<T, TKey>(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&<TKey>.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<T, TKey>(Self: sequence of T; keySelector: T -> TKey): T; extensionmethod;
function LastMinBy<T, TKey>(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&<TKey>.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<T, TKey>(Self: sequence of T; keySelector: T -> TKey): T; extensionmethod;
function LastMaxBy<T, TKey>(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&<TKey>.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;

View file

@ -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. Контейнеры фолдов