ML - Metrics, Validation - Production уровень

PABCSystem - добавлен huffle с параметром rnd

RandomForest - добавление OOB

MLModels - сообщения об ошибках и доведение до Production
This commit is contained in:
Mikhalkovich Stanislav 2026-03-03 14:14:59 +03:00
parent 03fa44dc8b
commit d2fd6c21d6
6 changed files with 1523 additions and 449 deletions

View file

@ -4,7 +4,7 @@ interface
const
ER_DIM_MISMATCH =
'Несоответствие размерностей: {0} и {1}!!Dimension mismatch: {0} and {1}';
'Несоответствие размерностей: {0} и {1}!!Dimension mismatch: {0} and {1}';
ER_PARAM_VALUES_EMPTY =
'Список paramValues пуст!!paramValues is empty';
ER_EMPTY_DATASET =
@ -17,7 +17,11 @@ const
'Модель не может быть nil!!Model cannot be nil';
ER_SELECTKBEST_K_INVALID =
'Параметр k в SelectKBest должен быть >= 1. Передано: {0}.!!SelectKBest parameter k must be >= 1. Given: {0}.';
ER_ARG_NULL =
'Аргумент {0} равен nil!!Argument {0} is null';
ER_EMPTY_DATA =
'Пустой набор данных для {0}!!Empty dataset for {0}';
type
/// Базовое исключение ML-библиотеки
MLException = class(Exception);

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,12 @@ type
/// Чувствительна к большим отклонениям.
static function MSE(yTrue, yPred: Vector): real;
/// Вычисляет среднеквадратичную ошибку (Root Mean Squared Error, RMSE)
/// между истинными и предсказанными значениями.
/// RMSE равна квадратному корню из MSE и измеряется
/// в тех же единицах, что и целевая переменная.
static function RMSE(yTrue, yPred: Vector): real;
/// Средняя абсолютная ошибка (MAE).
/// MAE = (1/n) * Σ |yTrue - yPred|.
/// Менее чувствительна к выбросам, чем MSE.
@ -69,10 +75,22 @@ type
static function Recall(yTrue, yPred: Vector): real;
/// F1-мера.
/// Объединённая метрика, учитывающая и Precision, и Recall одновременно.
/// Высока только тогда, когда и Precision, и Recall высоки.
/// Используется, когда классы сильно различаются по количеству элементов
/// Объединённая метрика, учитывающая одновременно Precision и Recall.
/// Высока только тогда, когда и Precision, и Recall имеют высокие значения.
/// Особенно полезна при несбалансированных классах.
static function F1(yTrue, yPred: Vector): real;
/// Specificity (True Negative Rate).
/// Доля правильно предсказанных отрицательных объектов:
/// TN / (TN + FP).
/// Показывает способность модели корректно распознавать отрицательный класс.
static function Specificity(yTrue, yPred: Vector): real;
/// Balanced Accuracy.
/// Среднее арифметическое Recall (чувствительности)
/// и Specificity (доли истинно отрицательных).
/// Устойчива к дисбалансу классов.
static function BalancedAccuracy(yTrue, yPred: Vector): real;
end;
type
@ -134,90 +152,196 @@ type
/// Высока только тогда, когда и Precision, и Recall высоки.
/// Используется, когда классы сильно различаются по количеству элементов
function F1: real;
function Specificity: real;
function BalancedAccuracy: real;
end;
implementation
uses MLExceptions;
const
ER_INVALID_VALUE =
'Некорректное значение {0} на позиции {1}!!Invalid value in {0} at index {1}';
ER_DIV_BY_ZERO =
'Деление на ноль в {0}!!Division by zero in {0}';
ER_INVALID_CLASS_LABEL =
'Некорректная метка класса: {0}!!Invalid class label: {0}';
//-----------------------------
// Metrics
//-----------------------------
static function Metrics.MSE(yTrue, yPred: Vector): real;
begin
if yTrue.Length <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
var n := yTrue.Length;
var s := 0.0;
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'MSE');
var sum := 0.0;
for var i := 0 to n - 1 do
begin
var d := yTrue[i] - yPred[i];
s += d * d;
var yt := yTrue[i];
var yp := yPred[i];
if double.IsNaN(yt) or double.IsInfinity(yt) then
ArgumentError(ER_INVALID_VALUE, 'yTrue', i);
if double.IsNaN(yp) or double.IsInfinity(yp) then
ArgumentError(ER_INVALID_VALUE, 'yPred', i);
var d := yt - yp;
sum += d * d;
end;
Result := s / n;
Result := sum / n;
end;
static function Metrics.RMSE(yTrue, yPred: Vector): real;
begin
Result := Sqrt(MSE(yTrue, yPred));
end;
static function Metrics.MAE(yTrue, yPred: Vector): real;
begin
if yTrue.Length <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
var n := yTrue.Length;
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'MAE');
var s := 0.0;
for var i := 0 to n - 1 do
s += Abs(yTrue[i] - yPred[i]);
begin
var yt := yTrue[i];
var yp := yPred[i];
if double.IsNaN(yt) or double.IsInfinity(yt) then
ArgumentError(ER_INVALID_VALUE, 'yTrue', i);
if double.IsNaN(yp) or double.IsInfinity(yp) then
ArgumentError(ER_INVALID_VALUE, 'yPred', i);
s += Abs(yt - yp);
end;
Result := s / n;
end;
static function Metrics.R2(yTrue, yPred: Vector): real;
begin
if yTrue.Length <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
var n := yTrue.Length;
var meanY := yTrue.Mean;
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'R2');
var sumY := 0.0;
for var i := 0 to n - 1 do
begin
var yt := yTrue[i];
if double.IsNaN(yt) or double.IsInfinity(yt) then
ArgumentError(ER_INVALID_VALUE, 'yTrue', i);
sumY += yt;
end;
var meanY := sumY / n;
var ssRes := 0.0;
var ssTot := 0.0;
for var i := 0 to n - 1 do
begin
var d1 := yTrue[i] - yPred[i];
var yt := yTrue[i];
var yp := yPred[i];
if double.IsNaN(yp) or double.IsInfinity(yp) then
ArgumentError(ER_INVALID_VALUE, 'yPred', i);
var d1 := yt - yp;
ssRes += d1 * d1;
var d2 := yTrue[i] - meanY;
var d2 := yt - meanY;
ssTot += d2 * d2;
end;
if ssTot = 0 then
exit(0.0);
if ssTot = 0.0 then
begin
Result := 0.0;
exit;
end;
Result := 1 - ssRes / ssTot;
end;
static function Metrics.LogLoss(yTrue, yProb: Vector): real;
begin
if yTrue.Length <> yProb.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yProb.Length);
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yProb = nil then
ArgumentNullError(ER_ARG_NULL, 'yProb');
var n := yTrue.Length;
if n <> yProb.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yProb.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'LogLoss');
var eps := 1e-15;
var s := 0.0;
for var i := 0 to n - 1 do
begin
var yt := yTrue[i];
var p := yProb[i];
if p < eps then p := eps;
if p > 1 - eps then p := 1 - eps;
if double.IsNaN(yt) or double.IsInfinity(yt) then
ArgumentError(ER_INVALID_VALUE, 'yTrue', i);
s += yTrue[i] * Ln(p) + (1 - yTrue[i]) * Ln(1 - p);
if double.IsNaN(p) or double.IsInfinity(p) then
ArgumentError(ER_INVALID_VALUE, 'yProb', i);
if (yt <> 0.0) and (yt <> 1.0) then
ArgumentError(ER_INVALID_CLASS_LABEL, yt);
if p < eps then p := eps;
if p > 1.0 - eps then p := 1.0 - eps;
s += yt * Ln(p) + (1.0 - yt) * Ln(1.0 - p);
end;
Result := -s / n;
@ -225,49 +349,68 @@ end;
static function Metrics.ROC(yTrue, yProb: Vector): (Vector, Vector);
begin
if yTrue.Length <> yProb.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yProb.Length);
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yProb = nil then
ArgumentNullError(ER_ARG_NULL, 'yProb');
var n := yTrue.Length;
// Индексы, отсортированные по убыванию вероятностей
var idx :=
(0..n-1)
.OrderByDescending(i -> yProb[i])
.ToArray;
if n <> yProb.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yProb.Length);
var tp := 0;
var fp := 0;
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'ROC');
var totalPos := 0;
var totalNeg := 0;
for var i := 0 to n-1 do
if yTrue[i] = 1 then
for var i := 0 to n - 1 do
begin
var yt := yTrue[i];
var p := yProb[i];
if double.IsNaN(yt) or double.IsInfinity(yt) then
ArgumentError(ER_INVALID_VALUE, 'yTrue', i);
if double.IsNaN(p) or double.IsInfinity(p) then
ArgumentError(ER_INVALID_VALUE, 'yProb', i);
if (yt <> 0.0) and (yt <> 1.0) then
ArgumentError(ER_INVALID_CLASS_LABEL, yt);
if yt = 1.0 then
totalPos += 1
else
totalNeg += 1;
end;
if (totalPos = 0) or (totalNeg = 0) then
ArgumentError(ER_DIV_BY_ZERO, 'ROC');
var idx :=
(0..n-1)
.OrderByDescending(i -> yProb[i])
.ToArray;
var tp := 0;
var fp := 0;
var fprList := new List<real>;
var tprList := new List<real>;
for var k := 0 to n-1 do
for var k := 0 to n - 1 do
begin
var i := idx[k];
if yTrue[i] = 1 then
if yTrue[i] = 1.0 then
tp += 1
else
fp += 1;
var tpr := 0.0;
var fpr := 0.0;
if totalPos > 0 then
tpr := tp / totalPos;
if totalNeg > 0 then
fpr := fp / totalNeg;
var tpr := tp / totalPos;
var fpr := fp / totalNeg;
tprList.Add(tpr);
fprList.Add(fpr);
@ -279,14 +422,42 @@ end;
static function Metrics.AUC(yTrue, yProb: Vector): real;
begin
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yProb = nil then
ArgumentNullError(ER_ARG_NULL, 'yProb');
var n := yTrue.Length;
if n <> yProb.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yProb.Length);
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'AUC');
var (fpr, tpr) := ROC(yTrue, yProb);
if fpr.Length <> tpr.Length then
DimensionError(ER_DIM_MISMATCH, fpr.Length, tpr.Length);
if fpr.Length < 2 then
ArgumentError(ER_EMPTY_DATA, 'AUC');
var area := 0.0;
for var i := 1 to fpr.Length - 1 do
begin
var dx := fpr[i] - fpr[i - 1];
if double.IsNaN(dx) or double.IsInfinity(dx) then
ArgumentError(ER_INVALID_VALUE, 'fpr', i);
var avgY := (tpr[i] + tpr[i - 1]) / 2;
if double.IsNaN(avgY) or double.IsInfinity(avgY) then
ArgumentError(ER_INVALID_VALUE, 'tpr', i);
area += dx * avgY;
end;
@ -313,6 +484,15 @@ begin
Result := ConfusionMatrix.Create(yTrue, yPred).F1;
end;
static function Metrics.Specificity(yTrue, yPred: Vector): real;
begin
Result := ConfusionMatrix.Create(yTrue, yPred).Specificity;
end;
static function Metrics.BalancedAccuracy(yTrue, yPred: Vector): real;
begin
Result := ConfusionMatrix.Create(yTrue, yPred).BalancedAccuracy;
end;
//-----------------------------
// ConfusionMatrix
@ -320,18 +500,49 @@ end;
constructor ConfusionMatrix.Create(yTrue, yPred: Vector);
begin
if yTrue.Length <> yPred.Length then
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
var n := yTrue.Length;
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
for var i := 0 to yTrue.Length - 1 do
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'ConfusionMatrix');
fTP := 0;
fFP := 0;
fTN := 0;
fFN := 0;
for var i := 0 to n - 1 do
begin
if yTrue[i] = 1 then
if yPred[i] = 1 then
var yt := yTrue[i];
var yp := yPred[i];
if double.IsNaN(yt) or double.IsInfinity(yt) then
ArgumentError(ER_INVALID_VALUE, 'yTrue', i);
if double.IsNaN(yp) or double.IsInfinity(yp) then
ArgumentError(ER_INVALID_VALUE, 'yPred', i);
if (yt <> 0.0) and (yt <> 1.0) then
ArgumentError(ER_INVALID_CLASS_LABEL, yt);
if (yp <> 0.0) and (yp <> 1.0) then
ArgumentError(ER_INVALID_CLASS_LABEL, yp);
if yt = 1.0 then
if yp = 1.0 then
fTP += 1
else
fFN += 1
else
if yPred[i] = 1 then
if yp = 1.0 then
fFP += 1
else
fTN += 1;
@ -371,6 +582,17 @@ begin
Result := 2 * p * r / (p + r);
end;
function ConfusionMatrix.Specificity: real;
begin
if fTN + fFP = 0 then
exit(0.0);
Result := fTN / (fTN + fFP);
end;
function ConfusionMatrix.BalancedAccuracy: real;
begin
Result := (Recall + Specificity) / 2;
end;
end.

View file

@ -2520,10 +2520,21 @@ procedure Reverse<T>(a: List<T>; index, count: integer);
procedure Reverse(var s: string);
/// Изменяет порядок символов в части строки длины count на противоположный, начиная с индекса index
procedure Reverse(var s: string; index, count: integer);
/// Случайно перемешивает элементы массива
/// Перемешивает элементы массива случайным образом.
/// Используется классический алгоритм перемешивания Фишера–Йетса
procedure Shuffle<T>(a: array of T);
/// Случайно перемешивает элементы списка
/// Перемешивает элементы массива случайным образом
/// с использованием заданного генератора случайных чисел.
/// Используется классический алгоритм перемешивания Фишера–Йетса
procedure Shuffle<T>(a: array of T; rnd: System.Random);
/// Перемешивает элементы списка случайным образом.
/// Используется классический алгоритм перемешивания Фишера–Йетса.
procedure Shuffle<T>(l: List<T>);
/// Перемешивает элементы списка случайным образом
/// с использованием заданного генератора случайных чисел.
/// Используется классический алгоритм перемешивания Фишера–Йетса.
procedure Shuffle<T>(l: List<T>; rnd: System.Random);
/// Проверяет равенство двух массивов
function ArrEqual<T>(a, b: array of T): boolean;
@ -3219,7 +3230,7 @@ implementation
function NewSet<T>.ToString: string := $'{ObjectToString(hs)}';
var
rnd: System.Random;
rnd: System.Random; // глобальный генератор случайных чисел
nfi: NumberFormatInfo;
StartTime: DateTime;// Для Milliseconds
@ -9997,21 +10008,46 @@ begin
s := new string(cc);
end;
procedure Shuffle<T>(a: array of T; rnd: System.Random);
begin
if a = nil then
raise new System.ArgumentNullException('a');
if rnd = nil then
raise new System.ArgumentNullException('rnd');
for var i := a.Length - 1 downto 1 do
begin
var j := rnd.Next(i + 1);
Swap(a[i], a[j]);
end;
end;
procedure Shuffle<T>(l: List<T>; rnd: System.Random);
begin
if l = nil then
raise new System.ArgumentNullException('l');
if rnd = nil then
raise new System.ArgumentNullException('rnd');
for var i := l.Count - 1 downto 1 do
begin
var j := rnd.Next(i + 1);
var tmp := l[i];
l[i] := l[j];
l[j] := tmp;
end;
end;
procedure Shuffle<T>(a: array of T);
begin
for var i := a.Length - 1 downto 1 do
Swap(a[i], a[Random(i + 1)]);
Shuffle(a, rnd);
end;
procedure Shuffle<T>(l: List<T>);
begin
for var i := l.Count - 1 downto 1 do
begin
var ind := Random(i + 1);
var v := l[i];
l[i] := l[ind];
l[ind] := v;
end;
Shuffle(l, rnd);
end;
// -----------------------------------------------------
@ -12249,14 +12285,16 @@ end;
/// Перемешивает элементы списка случайным образом
function Shuffle<T>(Self: List<T>): List<T>; extensionmethod;
begin
for var i := Self.Count - 1 downto 1 do
begin
var r := Random(i + 1);
var v := Self[i];
Self[i] := Self[r];
Self[r] := v;
end;
Result := Self;
Shuffle(Self, rnd);
Result := Self;
end;
/// Перемешивает элементы списка случайным образом
/// с использованием заданного генератора случайных чисел.
function Shuffle<T>(Self: List<T>; rnd: System.Random): List<T>; extensionmethod;
begin
Shuffle(Self, rnd);
Result := Self;
end;
/// Находит первую пару подряд идущих одинаковых элементов и возвращает индекс первого элемента пары. Если не найден, возвращается -1
@ -13421,13 +13459,23 @@ begin
end;
/// Перемешивает элементы массива случайным образом
/// с использованием глобального генератора случайных чисел.
/// Используется классический алгоритм перемешивания Фишера–Йетса
function Shuffle<T>(Self: array of T): array of T; extensionmethod;
begin
for var i := Self.Length - 1 downto 1 do
Swap(Self[i], Self[Random(i + 1)]);
Shuffle(Self, rnd);
Result := Self;
end;
/// Перемешивает элементы массива случайным образом
/// с использованием заданного генератора случайных чисел.
/// Используется классический алгоритм перемешивания Фишера–Йетса
function Shuffle<T>(Self: array of T; rnd: System.Random): array of T; extensionmethod;
begin
Shuffle(Self, rnd);
Result := Self;
end;
/// Меняет местами элементы массива с индексами i и j
function Swap<T>(Self: array of T; i, j: integer): array of T; extensionmethod;
begin

View file

@ -108,50 +108,6 @@ type
function FitTransform(df: DataFrame): DataFrame;
end;
{/// Приводит числовые столбцы к нулевому среднему и единичному стандартному отклонению.
/// При Fit вычисляет среднее значение и стандартное отклонение столбцов.
/// Применяет преобразование: x' = (x - mean) / std.
/// Пропущенные значения (NA) сохраняются.
DataStandardScaler = class(IPreprocessor)
private
cols: array of string;
means: array of real;
stds: array of real;
fitted: boolean;
public
/// Создаёт StandardScaler для указанных числовых столбцов.
constructor Create(params columns: array of string);
/// Вычисляет среднее значение и стандартное отклонение для каждого столбца.
function Fit(df: DataFrame): IPreprocessor;
/// Возвращает DataFrame со стандартизованными числовыми столбцами.
function Transform(df: DataFrame): DataFrame;
/// Последовательно выполняет Fit и Transform.
function FitTransform(df: DataFrame): DataFrame;
end;
/// Приводит числовые столбцы к заданному диапазону значений
/// При Fit вычисляет минимальное и максимальное значения столбцов
/// Применяет преобразование: x' = (x - min) / (max - min)
/// Пропущенные значения (NA) сохраняются
DataMinMaxScaler = class(IPreprocessor)
private
cols: array of string;
mins: array of real;
maxs: array of real;
fitted: boolean;
public
/// Создаёт MinMaxScaler для указанных столбцов
constructor Create(params columns: array of string);
/// Вычисляет минимальные и максимальные значения столбцов
function Fit(df: DataFrame): IPreprocessor;
/// Применяет масштабирование к DataFrame
/// Возвращает новый DataFrame
function Transform(df: DataFrame): DataFrame;
/// Последовательно выполняет Fit и Transform.
function FitTransform(df: DataFrame): DataFrame;
end;}
implementation

View file

@ -68,7 +68,8 @@ type
paramValues: array of real;
X: Matrix; y: Vector;
k: integer;
metric: (Vector, Vector) -> real
metric: (Vector, Vector) -> real;
maximize: boolean := True
): (real, real, T); where T: IModel;
end;
@ -91,7 +92,9 @@ const
'Invalid k in StratifiedKFold: k={0}, n={1}';
ER_STRATIFIED_LABELS_INVALID =
'StratifiedKFold поддерживает только метки 0/1!!' +
'StratifiedKFold supports only 0/1 labels';
'StratifiedKFold supports only 0/1 labels';
ER_INVALID_VALUE =
'Некорректное значение параметра {0}!!Invalid value for parameter {0}';
//-----------------------------
// Validation
@ -100,26 +103,34 @@ const
static function Validation.TrainTestSplit(X: Matrix; y: Vector;
testRatio: real; seed: integer): (Matrix, Matrix, Vector, Vector);
begin
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if y = nil then
ArgumentNullError(ER_ARG_NULL, 'y');
if X.RowCount <> y.Length then
DimensionError(ER_DIM_MISMATCH_TRAIN_TEST, X.RowCount, y.Length);
if (testRatio <= 0) or (testRatio >= 1) then
if (testRatio <= 0.0) or (testRatio >= 1.0) then
ArgumentError(ER_TEST_RATIO_INVALID, testRatio);
var n := X.RowCount;
var p := X.ColCount;
var rnd := if seed >= 0 then
new System.Random(seed)
else
new System.Random;
if n < 2 then
ArgumentError(ER_EMPTY_DATA, 'TrainTestSplit');
// Перемешанные индексы
var idx := (0..n-1).ToArray;
idx := idx.OrderBy(i -> rnd.Next).ToArray;
var actualSeed := if seed >= 0 then seed else System.Environment.TickCount and integer.MaxValue;
var rnd := new System.Random(actualSeed);
var idx := Arr(0..n-1);
// --- 2. Перемешивание через стандартный Shuffle
idx.Shuffle(rnd);
var rawSize := Round(n * testRatio);
var testSize := Max(1, Min(n - 1, rawSize));
var testSize := rawSize.Clamp(1, n - 1);
var trainSize := n - testSize;
var X_train := new Matrix(trainSize, p);
@ -128,25 +139,19 @@ begin
var y_train := new Vector(trainSize);
var y_test := new Vector(testSize);
// Заполняем train
for var i := 0 to trainSize - 1 do
begin
var row := idx[i];
for var j := 0 to p - 1 do
X_train[i,j] := X[row,j];
y_train[i] := y[row];
end;
// Заполняем test
for var i := 0 to testSize - 1 do
begin
var row := idx[trainSize + i];
for var j := 0 to p - 1 do
X_test[i,j] := X[row,j];
y_test[i] := y[row];
end;
@ -156,27 +161,22 @@ end;
static function Validation.KFold(n, k: integer; seed: integer):
sequence of (array of integer, array of integer);
begin
if n <= 0 then
ArgumentError(ER_EMPTY_DATA, 'KFold');
if (k < 2) or (k > n) then
ArgumentError(ER_K_INVALID, k, n);
var rnd := if seed >= 0 then
new System.Random(seed)
else
new System.Random;
var actualSeed := if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
// --- 1. Создаём массив индексов 0..n-1
var idx := new integer[n];
for var i := 0 to n - 1 do
idx[i] := i;
var rnd := new System.Random(actualSeed);
// --- 2. Fisher–Yates shuffle (in-place, O(n))
for var i := n - 1 downto 1 do
begin
var j := rnd.Next(i + 1);
var tmp := idx[i];
idx[i] := idx[j];
idx[j] := tmp;
end;
// --- 1. Индексы 0..n-1
var idx := Arr(0..n-1);
// --- 2. Перемешивание через стандартный Shuffle
idx.Shuffle(rnd);
var baseSize := n div k;
var extra := n mod k;
@ -187,19 +187,15 @@ begin
begin
var size := baseSize + Ord(fold < extra);
// --- test indices
var testIdx := new integer[size];
System.Array.Copy(idx, start, testIdx, 0, size);
// --- train indices
var trainSize := n - size;
var trainIdx := new integer[trainSize];
// левая часть
if start > 0 then
System.Array.Copy(idx, 0, trainIdx, 0, start);
// правая часть
var tailCount := n - (start + size);
if tailCount > 0 then
System.Array.Copy(idx, start + size, trainIdx, start, tailCount);
@ -213,22 +209,30 @@ end;
static function Validation.StratifiedKFold(y: Vector; k: integer; seed: integer):
sequence of (array of integer, array of integer);
begin
if y = nil then
ArgumentNullError(ER_ARG_NULL, 'y');
var n := y.Length;
if n <= 0 then
ArgumentError(ER_EMPTY_DATA, 'StratifiedKFold');
if (k < 2) or (k > n) then
ArgumentError(ER_K_INVALID_STRATIFIED, k, n);
var rnd :=
if seed >= 0 then new System.Random(seed)
else new System.Random;
var actualSeed := if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
// 1) Собираем индексы по классам за один проход
var rnd := new System.Random(actualSeed);
// --- 1. Индексы по классам
var classMap := new Dictionary<integer, List<integer>>();
for var i := 0 to n - 1 do
begin
var v := y[i];
var cls := integer(v);
// Валидация: метки должны быть целыми (хотя тип double)
if Abs(v - cls) > 1e-12 then
ArgumentError(ER_STRATIFIED_LABELS_INVALID);
@ -243,27 +247,18 @@ begin
end;
end;
// 2) Подготавливаем контейнеры фолдов
// --- 2. Контейнеры фолдов
var folds := new List<integer>[k];
for var f := 0 to k - 1 do
folds[f] := new List<integer>;
// 3) Для каждого класса: shuffle + равномерно раскладываем по k фолдам
// --- 3. Для каждого класса: shuffle + равномерное распределение
foreach var pair in classMap do
begin
var indices := pair.Value;
indices.Shuffle(rnd);
var m := indices.Count;
// Fisher–Yates shuffle in-place
for var i := m - 1 downto 1 do
begin
var j := rnd.Next(i + 1);
var tmp := indices[i];
indices[i] := indices[j];
indices[j] := tmp;
end;
// Равномерное распределение (разница размеров ≤ 1)
var baseSize := m div k;
var extra := m mod k;
var start := 0;
@ -277,18 +272,16 @@ begin
end;
end;
// 4) Возвращаем (trainIdx, testIdx) для каждого фолда
// --- 4. Формирование train/test
for var fold := 0 to k - 1 do
begin
var testIdx := folds[fold].ToArray();
var testIdx := folds[fold].ToArray;
// boolean-mask вместо Contains -> O(n)
var mask := new boolean[n];
foreach var id in testIdx do
mask[id] := true;
var trainSize := n - testIdx.Length;
var trainIdx := new integer[trainSize];
var trainIdx := new integer[n - testIdx.Length];
var p := 0;
for var i := 0 to n - 1 do
@ -303,12 +296,32 @@ begin
end;
static function Validation.CrossValidate(model: IModel; X: Matrix; y: Vector;
k: integer; metric: (Vector,Vector) -> real; seed: integer): real;
static function Validation.CrossValidate(
model: IModel;
X: Matrix;
y: Vector;
k: integer;
metric: (Vector,Vector) -> real;
seed: integer): real;
begin
if model = nil then
ArgumentNullError(ER_ARG_NULL, 'model');
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if y = nil then
ArgumentNullError(ER_ARG_NULL, 'y');
if metric = nil then
ArgumentNullError(ER_ARG_NULL, 'metric');
if X.RowCount <> y.Length then
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
if (k < 2) or (k > X.RowCount) then
ArgumentError(ER_K_INVALID, k, X.RowCount);
var total := 0.0;
var folds := 0;
var p := X.ColCount;
@ -339,23 +352,45 @@ begin
var m := model.Clone();
m.Fit(Xtr, ytr);
var pred := m.Predict(Xte);
total += metric(yte, pred);
folds += 1;
end;
if folds = 0 then
ArgumentError(ER_EMPTY_DATA, 'CrossValidate');
Result := total / folds;
end;
static function Validation.StratifiedCrossValidate(
model: IModel; X: Matrix; y: Vector;
k: integer; metric: (Vector,Vector) -> real; seed: integer
): real;
model: IModel;
X: Matrix;
y: Vector;
k: integer;
metric: (Vector,Vector) -> real;
seed: integer): real;
begin
if model = nil then
ArgumentNullError(ER_ARG_NULL, 'model');
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if y = nil then
ArgumentNullError(ER_ARG_NULL, 'y');
if metric = nil then
ArgumentNullError(ER_ARG_NULL, 'metric');
if X.RowCount <> y.Length then
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
if (k < 2) or (k > X.RowCount) then
ArgumentError(ER_K_INVALID_STRATIFIED, k, X.RowCount);
var total := 0.0;
var folds := 0;
var p := X.ColCount;
@ -386,12 +421,16 @@ begin
var m := model.Clone();
m.Fit(Xtr, ytr);
var pred := m.Predict(Xte);
total += metric(yte, pred);
folds += 1;
end;
if folds = 0 then
ArgumentError(ER_EMPTY_DATA, 'StratifiedCrossValidate');
Result := total / folds;
end;
@ -402,23 +441,51 @@ end;
class function GridSearch.Search<T>(
modelFactory: real -> T;
paramValues: array of real;
X: Matrix; y: Vector;
X: Matrix;
y: Vector;
k: integer;
metric: (Vector, Vector) -> real
metric: (Vector, Vector) -> real;
maximize: boolean
): (real, real, T); where T: IModel;
begin
if modelFactory = nil then
ArgumentNullError(ER_ARG_NULL, 'modelFactory');
if paramValues = nil then
ArgumentNullError(ER_ARG_NULL, 'paramValues');
if paramValues.Length = 0 then
ArgumentError(ER_PARAM_VALUES_EMPTY);
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
if y = nil then
ArgumentNullError(ER_ARG_NULL, 'y');
if metric = nil then
ArgumentNullError(ER_ARG_NULL, 'metric');
if X.RowCount <> y.Length then
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
var bestParam := paramValues[0];
var bestScore := -1e308;
var bestScore :=
if maximize then -1e308 else 1e308;
foreach var param in paramValues do
begin
var model := modelFactory(param);
var avgScore := Validation.CrossValidate(model, X, y, k, metric);
if avgScore > bestScore then
if double.IsNaN(avgScore) or double.IsInfinity(avgScore) then
ArgumentError(ER_INVALID_VALUE, 'avgScore');
var better :=
(maximize and (avgScore > bestScore)) or
(not maximize and (avgScore < bestScore));
if better then
begin
bestScore := avgScore;
bestParam := param;