Проверки на NaN Inf
This commit is contained in:
Mikhalkovich Stanislav 2026-03-02 00:22:35 +03:00
parent 398ea2440f
commit 03fa44dc8b
6 changed files with 1655 additions and 755 deletions

View file

@ -51,14 +51,14 @@ begin
if scoreFunc = nil then
ArgumentNullError(ER_SCORE_FUNC_NULL);
if X.Rows <> y.Length then
DimensionError(ER_DIM_MISMATCH, X.Rows, y.Length);
if X.RowCount <> y.Length then
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
var baselinePred := model.Predict(X);
var baselineScore := scoreFunc(y, baselinePred);
var n := X.Rows;
var p := X.Cols;
var n := X.RowCount;
var p := X.ColCount;
var resultVec := new Vector(p);
var rng := new System.Random(seed);

View file

@ -73,8 +73,8 @@ type
procedure SetData(i, j: integer; value: real) := data[i, j] := value;
public
data: array[,] of real;
property Rows: integer read data.GetLength(0);
property Cols: integer read data.GetLength(1);
property RowCount: integer read data.RowCount;
property ColCount: integer read data.ColCount;
property Item[i, j: integer]: real
read data[i, j] write SetData; default;
@ -140,9 +140,6 @@ type
// ---------- Основные методы ----------
function RowCount: integer := data.RowCount;
function ColCount: integer := data.ColCount;
/// Возвращает транспонированную матрицу Aᵀ
function Transpose: Matrix;
/// Проверяет, является ли матрица симметричной с заданным допуском.
@ -241,6 +238,8 @@ function SolveAuto(A: Matrix; b: Vector): Vector;
/// Входные A и b изменяются. Возвращаемый вектор x имеет длину n.
function SolveRidge(A: Matrix; b: Vector; lambda: real := 0): Vector;
function SolveLeastSquaresQR(A: Matrix; b: Vector): Vector;
implementation
uses MLExceptions;
@ -291,6 +290,10 @@ const
'Матрица вырождена!!Matrix is singular';
ER_VECTOR_SIZE_MISMATCH =
'Несоответствие длины вектора: {0} и {1}!!Vector size mismatch: {0} and {1}';
ER_QR_REQUIRES_M_GE_N =
'Для QR-разложения требуется m >= n!!QR decomposition requires m >= n';
ER_SINGULAR_MATRIX =
'Матрица вырождена или плохо обусловлена!!Matrix is singular or ill-conditioned';
//-----------------------------
// Vector
@ -813,20 +816,20 @@ end;
static procedure Matrix.CheckSameSize(A, B: Matrix);
begin
if (A.Rows <> B.Rows) or (A.Cols <> B.Cols) then
DimensionError(ER_MATRIX_SIZE_MISMATCH, A.Rows, A.Cols, B.Rows, B.Cols);
if (A.RowCount <> B.RowCount) or (A.ColCount <> B.ColCount) then
DimensionError(ER_MATRIX_SIZE_MISMATCH, A.RowCount, A.ColCount, B.RowCount, B.ColCount);
end;
static procedure Matrix.CheckMulSize(A, B: Matrix);
begin
if A.Cols <> B.Rows then
DimensionError(ER_MATRIX_MUL_SIZE_MISMATCH, A.Rows, A.Cols, B.Rows, B.Cols);
if A.ColCount <> B.RowCount then
DimensionError(ER_MATRIX_MUL_SIZE_MISMATCH, A.RowCount, A.ColCount, B.RowCount, B.ColCount);
end;
static procedure Matrix.CheckVecSize(A: Matrix; x: Vector);
begin
if A.Cols <> x.Length then
DimensionError(ER_MATRIX_VECTOR_SIZE_MISMATCH, A.Rows, A.Cols, x.Length);
if A.ColCount <> x.Length then
DimensionError(ER_MATRIX_VECTOR_SIZE_MISMATCH, A.RowCount, A.ColCount, x.Length);
end;
@ -834,26 +837,26 @@ end;
static function Matrix.operator +(A, B: Matrix): Matrix;
begin
CheckSameSize(A, B);
Result := new Matrix(A.Rows, A.Cols);
for var i := 0 to A.Rows - 1 do
for var j := 0 to A.Cols - 1 do
Result := new Matrix(A.RowCount, A.ColCount);
for var i := 0 to A.RowCount - 1 do
for var j := 0 to A.ColCount - 1 do
Result.data[i, j] := A.data[i, j] + B.data[i, j];
end;
static function Matrix.operator -(A, B: Matrix): Matrix;
begin
CheckSameSize(A, B);
Result := new Matrix(A.Rows, A.Cols);
for var i := 0 to A.Rows - 1 do
for var j := 0 to A.Cols - 1 do
Result := new Matrix(A.RowCount, A.ColCount);
for var i := 0 to A.RowCount - 1 do
for var j := 0 to A.ColCount - 1 do
Result.data[i, j] := A.data[i, j] - B.data[i, j];
end;
function operator *(A: Matrix; alpha: real): Matrix; extensionmethod;
begin
Result := new Matrix(A.Rows, A.Cols);
for var i := 0 to A.Rows - 1 do
for var j := 0 to A.Cols - 1 do
Result := new Matrix(A.RowCount, A.ColCount);
for var i := 0 to A.RowCount - 1 do
for var j := 0 to A.ColCount - 1 do
Result.data[i, j] := alpha * A.data[i, j];
end;
@ -877,11 +880,11 @@ end;
static function Matrix.operator *(A: Matrix; x: Vector): Vector;
begin
CheckVecSize(A, x);
Result := new Vector(A.Rows);
for var i := 0 to A.Rows - 1 do
Result := new Vector(A.RowCount);
for var i := 0 to A.RowCount - 1 do
begin
var s := 0.0;
for var j := 0 to A.Cols - 1 do
for var j := 0 to A.ColCount - 1 do
s += A.data[i, j] * x[j];
Result[i] := s;
end;
@ -890,13 +893,13 @@ end;
static function Matrix.operator *(A, B: Matrix): Matrix;
begin
CheckMulSize(A, B);
Result := new Matrix(A.Rows, B.Cols);
for var i := 0 to A.Rows - 1 do
for var k := 0 to A.Cols - 1 do
Result := new Matrix(A.RowCount, B.ColCount);
for var i := 0 to A.RowCount - 1 do
for var k := 0 to A.ColCount - 1 do
begin
var aik := A.data[i, k];
if aik <> 0.0 then
for var j := 0 to B.Cols - 1 do
for var j := 0 to B.ColCount - 1 do
Result.data[i, j] += aik * B.data[k, j];
end;
end;
@ -904,8 +907,8 @@ end;
static function Matrix.operator +=(A, B: Matrix): Matrix;
begin
CheckSameSize(A, B);
for var i := 0 to A.Rows - 1 do
for var j := 0 to A.Cols - 1 do
for var i := 0 to A.RowCount - 1 do
for var j := 0 to A.ColCount - 1 do
A.data[i, j] += B.data[i, j];
Result := A;
end;
@ -913,43 +916,43 @@ end;
static function Matrix.operator -=(A, B: Matrix): Matrix;
begin
CheckSameSize(A, B);
for var i := 0 to A.Rows - 1 do
for var j := 0 to A.Cols - 1 do
for var i := 0 to A.RowCount - 1 do
for var j := 0 to A.ColCount - 1 do
A.data[i, j] -= B.data[i, j];
Result := A;
end;
static function Matrix.operator *=(A: Matrix; alpha: real): Matrix;
begin
for var i := 0 to A.Rows - 1 do
for var j := 0 to A.Cols - 1 do
for var i := 0 to A.RowCount - 1 do
for var j := 0 to A.ColCount - 1 do
A.data[i, j] *= alpha;
Result := A;
end;
function Matrix.Transpose: Matrix;
begin
Result := new Matrix(Cols, Rows);
for var i := 0 to Rows - 1 do
for var j := 0 to Cols - 1 do
Result := new Matrix(ColCount, RowCount);
for var i := 0 to RowCount - 1 do
for var j := 0 to ColCount - 1 do
Result.data[j, i] := data[i, j];
end;
function Matrix.GetRow(i: integer): Vector;
begin
if (i < 0) or (i >= Rows) then
ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, Rows);
Result := new Vector(Cols);
for var j := 0 to Cols - 1 do
if (i < 0) or (i >= RowCount) then
ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount);
Result := new Vector(ColCount);
for var j := 0 to ColCount - 1 do
Result[j] := data[i, j];
end;
function Matrix.GetCol(j: integer): Vector;
begin
if (j < 0) or (j >= Cols) then
ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, Cols);
Result := new Vector(Rows);
for var i := 0 to Rows - 1 do
if (j < 0) or (j >= ColCount) then
ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, ColCount);
Result := new Vector(RowCount);
for var i := 0 to RowCount - 1 do
Result[i] := data[i, j];
end;
@ -976,14 +979,14 @@ end;
function Matrix.IsSymmetric(tol: real): boolean;
begin
if Rows <> Cols then
if RowCount <> ColCount then
begin
Result := false;
exit;
end;
for var i := 0 to Rows - 1 do
for var j := i + 1 to Cols - 1 do
for var i := 0 to RowCount - 1 do
for var j := i + 1 to ColCount - 1 do
if Abs(data[i, j] - data[j, i]) > tol then
begin
Result := false;
@ -995,13 +998,13 @@ end;
function Matrix.EigenSymmetric(tol: real; maxIter: integer): (Vector, Matrix);
begin
if Rows <> Cols then
if RowCount <> ColCount then
ArgumentError(ER_MATRIX_NOT_SQUARE);
if not IsSymmetric(tol) then
ArgumentError(ER_MATRIX_NOT_SYMMETRIC);
var n := Rows;
var n := RowCount;
var M := Clone;
var V := Matrix.Identity(n);
@ -1136,8 +1139,8 @@ end;
function Matrix.PCA(k: integer): (Matrix, Vector);
begin
var m := Rows;
var n := Cols;
var m := RowCount;
var n := ColCount;
if k < 1 then
ArgumentError(ER_PCA_K_INVALID);
@ -1200,10 +1203,10 @@ end;
// Helper
function Cholesky(A: Matrix): Matrix;
begin
if A.Rows <> A.Cols then
if A.RowCount <> A.ColCount then
ArgumentError(ER_CHOLESKY_NOT_SQUARE);
var n := A.Rows;
var n := A.RowCount;
var L := new Matrix(n, n);
for var i := 0 to n - 1 do
@ -1229,10 +1232,10 @@ end;
// Helper
function LUDecompose(A: Matrix): (Matrix, array of integer);
begin
if A.Rows <> A.Cols then
if A.RowCount <> A.ColCount then
ArgumentError(ER_MATRIX_NOT_SQUARE);
var n := A.Rows;
var n := A.RowCount;
var LU := A.Clone;
var p := new integer[n];
@ -1294,7 +1297,7 @@ end;
// Helper
function SolveLowerTriangularUnit(LU: Matrix; b: Vector): Vector;
begin
var n := LU.Rows;
var n := LU.RowCount;
var y := new Vector(n);
for var i := 0 to n - 1 do
@ -1310,7 +1313,7 @@ end;
function SolveLowerTriangular(L: Matrix; b: Vector): Vector;
begin
var n := L.Rows;
var n := L.RowCount;
var y := new Vector(n);
for var i := 0 to n - 1 do
@ -1328,7 +1331,7 @@ end;
// Helper
function SolveUpperTriangular(U: Matrix; y: Vector): Vector;
begin
var n := U.Rows;
var n := U.RowCount;
var x := new Vector(n);
for var i := n - 1 downto 0 do
@ -1344,11 +1347,11 @@ end;
function Solve(A: Matrix; b: Vector): Vector;
begin
if A.Rows <> A.Cols then
if A.RowCount <> A.ColCount then
ArgumentError(ER_MATRIX_NOT_SQUARE);
if b.Length <> A.Rows then
DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.Rows);
if b.Length <> A.RowCount then
DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.RowCount);
var (LU, p) := LUDecompose(A);
var pb := Permute(b, p);
@ -1358,11 +1361,11 @@ end;
function SolveSPD(A: Matrix; b: Vector): Vector;
begin
if A.Rows <> A.Cols then
if A.RowCount <> A.ColCount then
ArgumentError(ER_MATRIX_NOT_SQUARE);
if b.Length <> A.Rows then
DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.Rows);
if b.Length <> A.RowCount then
DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, A.RowCount);
var L := Cholesky(A);
var y := SolveLowerTriangular(L, b);
@ -1383,8 +1386,8 @@ begin
if lambda < 0.0 then
ArgumentError(ER_LAMBDA_NEGATIVE);
var m := A.Rows;
var n := A.Cols;
var m := A.RowCount;
var n := A.ColCount;
if b.Length <> m then
DimensionError(ER_VECTOR_SIZE_MISMATCH, b.Length, m);
@ -1430,6 +1433,89 @@ begin
end;
function SolveLeastSquaresQR(A: Matrix; b: Vector): Vector;
begin
if A.RowCount <> b.Length then
DimensionError(ER_DIM_MISMATCH, A.RowCount, b.Length);
var m := A.RowCount;
var n := A.ColCount;
if m < n then
ArgumentError(ER_QR_REQUIRES_M_GE_N);
var R := A.Clone;
var y := b.Clone;
for var k := 0 to n - 1 do
begin
// ---- вычислить норму столбца k начиная с строки k
var normx := 0.0;
for var i := k to m - 1 do
normx += R[i,k] * R[i,k];
normx := Sqrt(normx);
if normx = 0 then
continue;
// ---- знак для устойчивости
if R[k,k] >= 0 then
normx := -normx;
// ---- построить Householder-вектор v
var beta := 1.0 / (normx * R[k,k] - normx * normx);
R[k,k] -= normx;
// ---- применить отражение к R
for var j := k + 1 to n - 1 do
begin
var s := 0.0;
for var i := k to m - 1 do
s += R[i,k] * R[i,j];
s *= beta;
for var i := k to m - 1 do
R[i,j] -= s * R[i,k];
end;
// ---- применить отражение к y
var sy := 0.0;
for var i := k to m - 1 do
sy += R[i,k] * y[i];
sy *= beta;
for var i := k to m - 1 do
y[i] -= sy * R[i,k];
// ---- восстановить диагональный элемент
R[k,k] := normx;
for var i := k + 1 to m - 1 do
R[i,k] := 0.0;
end;
// ---- Back substitution (решаем R[0:n,0:n] * x = y[0:n])
var x := new Vector(n);
for var i := n - 1 downto 0 do
begin
var sum := y[i];
for var j := i + 1 to n - 1 do
sum -= R[i,j] * x[j];
if Abs(R[i,i]) < 1e-14 then
ArgumentError(ER_SINGULAR_MATRIX);
x[i] := sum / R[i,i];
end;
Result := x;
end;
end.

View file

@ -15,6 +15,8 @@ const
'Метод Fit() не вызван!!Fit() must be called first';
ER_MODEL_NULL =
'Модель не может быть nil!!Model cannot be nil';
ER_SELECTKBEST_K_INVALID =
'Параметр k в SelectKBest должен быть >= 1. Передано: {0}.!!SelectKBest parameter k must be >= 1. Given: {0}.';
type
/// Базовое исключение ML-библиотеки

File diff suppressed because it is too large Load diff

View file

@ -187,7 +187,7 @@ begin
if fFitted then
Error(ER_PIPELINE_MODIFY_AFTER_FIT);
if step is IDataStep then
if step is IPreprocessor then // Data Step
begin
if (fMatrixSteps.Count > 0) or (fModel <> nil) then
ArgumentError(ER_DATAPIPE_DF_AFTER_MATRIX);
@ -196,7 +196,7 @@ begin
exit(Self);
end;
if step is IMatrixStep then
if step is ITransformer then // Matrix Step
begin
if step is IModel then
begin
@ -270,7 +270,7 @@ begin
end;
// 5) Модель
fModel.Fit(X, y);
fModel := fModel.Fit(X, y);
fFitted := true;
Result := Self;

View file

@ -9,40 +9,44 @@ type
public
/// Делит данные на обучающую и тестовую выборки.
/// testRatio — доля объектов, попадающих в тестовую выборку (по умолчанию 0.2).
/// Перемешивает объекты перед разбиением.
/// Возвращает (X_train, X_test, y_train, y_test).
/// Перед разбиением объекты перемешиваются.
/// Возвращает кортеж (X_train, X_test, y_train, y_test).
static function TrainTestSplit(X: Matrix; y: Vector;
testRatio: real := 0.2; seed: integer := 0): (Matrix, Matrix, Vector, Vector);
testRatio: real := 0.2; seed: integer := -1): (Matrix, Matrix, Vector, Vector);
/// Разбивает индексы объектов на k непересекающихся частей (fold).
/// На каждом шаге одна часть используется как тестовая, остальные — как обучающая.
/// Применяется для k-fold кросс-валидации.
/// На каждом шаге одна часть используется как тестовая, остальные — как обучающая выборка.
/// Используется для k-fold кросс-валидации.
/// Возвращает последовательность пар (trainIdx, testIdx).
static function KFold(n, k: integer; seed: integer := 0):
static function KFold(n, k: integer; seed: integer := -1):
sequence of (array of integer, array of integer);
/// Разбивает данные на k частей с сохранением пропорций классов.
/// В каждой части сохраняется примерно то же соотношение
/// объектов разных классов, что и во всей выборке.
/// Рекомендуется для задач классификации,
/// особенно при неравномерном распределении классов.
/// Возвращает последовательность пар (trainIdx, testIdx).
/// Разбивает данные на k частей (k-fold) с сохранением пропорций классов
/// (стратифицированная k-fold кросс-валидация).
/// В каждой части доля объектов каждого класса
/// максимально близка к их доле во всей выборке
/// (разница не превышает одного объекта на класс).
/// Рекомендуется для задач классификации,
/// особенно при несбалансированных классах.
/// Возвращает последовательность пар (trainIdx, testIdx)
static function StratifiedKFold(y: Vector; k: integer;
seed: integer := 0): sequence of (array of integer, array of integer);
seed: integer := -1): sequence of (array of integer, array of integer);
/// Выполняет k-fold кросс-валидацию модели.
/// На каждом шаге модель обучается на обучающей части и оценивается на тестовой.
/// metric — функция качества (например, Accuracy или MSE).
/// На каждом шаге модель обучается на обучающей части и оценивается на соответствующей тестовой части.
/// metric — функция качества, принимающая (y_true, y_pred)
/// и возвращающая значение метрики (например, Accuracy или MSE).
/// Возвращает среднее значение метрики по всем частям.
static function CrossValidate(model: IModel; X: Matrix; y: Vector;
k: integer; metric: (Vector,Vector) -> real; seed: integer := 0): real;
k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real;
/// Выполняет k-fold кросс-валидацию с сохранением пропорций классов.
/// Использует StratifiedKFold для разбиения данных.
/// Подходит для задач классификации.
/// Возвращает среднее значение метрики по всем частям.
/// Выполняет стратифицированную k-fold кросс-валидацию модели.
/// Разбиение данных выполняется методом StratifiedKFold
/// с сохранением пропорций классов в каждой части.
/// Рекомендуется для задач классификации, особенно при несбалансированных классах.
/// Возвращает среднее значение метрики по k разбиениям.
static function StratifiedCrossValidate(model: IModel; X: Matrix; y: Vector;
k: integer; metric: (Vector,Vector) -> real; seed: integer := 0): real;
k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real;
end;
/// Класс для подбора гиперпараметров методом перебора по сетке (Grid Search).
@ -105,13 +109,17 @@ begin
var n := X.RowCount;
var p := X.ColCount;
var rnd := new System.Random(seed);
var rnd := if seed >= 0 then
new System.Random(seed)
else
new System.Random;
// Перемешанные индексы
var idx := (0..n-1).ToArray;
idx := idx.OrderBy(i -> rnd.Next).ToArray;
var testSize := Round(n * testRatio);
var rawSize := Round(n * testRatio);
var testSize := Max(1, Min(n - 1, rawSize));
var trainSize := n - testSize;
var X_train := new Matrix(trainSize, p);
@ -151,70 +159,144 @@ begin
if (k < 2) or (k > n) then
ArgumentError(ER_K_INVALID, k, n);
var rnd := new System.Random(seed);
var idx := (0..n-1).OrderBy(i -> rnd.Next).ToArray;
var rnd := if seed >= 0 then
new System.Random(seed)
else
new System.Random;
// --- 1. Создаём массив индексов 0..n-1
var idx := new integer[n];
for var i := 0 to n - 1 do
idx[i] := i;
// --- 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;
var baseSize := n div k;
var extra := n mod k;
var start := 0;
// --- 3. Формируем фолды
for var fold := 0 to k - 1 do
begin
var size := baseSize + (if fold < extra then 1 else 0);
var size := baseSize + Ord(fold < extra);
var testIdx := idx.Skip(start).Take(size).ToArray;
var trainIdx := idx.Take(start).Concat(idx.Skip(start + size)).ToArray;
// --- 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);
yield (trainIdx, testIdx);
start += size;
end;
end;
static function Validation.StratifiedKFold(y: Vector; k: integer;
seed: integer): sequence of (array of integer, array of integer);
static function Validation.StratifiedKFold(y: Vector; k: integer; seed: integer):
sequence of (array of integer, array of integer);
begin
var n := y.Length;
if (k < 2) or (k > n) then
ArgumentError(ER_K_INVALID_STRATIFIED, k, n);
var rnd := new System.Random(seed);
var rnd :=
if seed >= 0 then new System.Random(seed)
else new System.Random;
var idx0 := (0..n-1)
.Where(i -> y[i] = 0)
.OrderBy(i -> rnd.Next)
.ToArray;
// 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);
var idx1 := (0..n-1)
.Where(i -> y[i] = 1)
.OrderBy(i -> rnd.Next)
.ToArray;
// Валидация: метки должны быть целыми (хотя тип double)
if Abs(v - cls) > 1e-12 then
ArgumentError(ER_STRATIFIED_LABELS_INVALID);
if idx0.Length + idx1.Length <> n then
ArgumentError(ER_STRATIFIED_LABELS_INVALID);
var lst: List<integer>;
if classMap.TryGetValue(cls, lst) then
lst.Add(i)
else
begin
lst := new List<integer>;
lst.Add(i);
classMap.Add(cls, lst);
end;
end;
var base0 := idx0.Length div k;
var extra0 := idx0.Length mod k;
// 2) Подготавливаем контейнеры фолдов
var folds := new List<integer>[k];
for var f := 0 to k - 1 do
folds[f] := new List<integer>;
var base1 := idx1.Length div k;
var extra1 := idx1.Length mod k;
// 3) Для каждого класса: shuffle + равномерно раскладываем по k фолдам
foreach var pair in classMap do
begin
var indices := pair.Value;
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;
for var fold := 0 to k - 1 do
begin
var size := baseSize + Ord(fold < extra);
for var t := 0 to size - 1 do
folds[fold].Add(indices[start + t]);
start += size;
end;
end;
// 4) Возвращаем (trainIdx, testIdx) для каждого фолда
for var fold := 0 to k - 1 do
begin
var start0 := fold * base0 + Min(fold, extra0);
var size0 := base0 + (if fold < extra0 then 1 else 0);
var testIdx := folds[fold].ToArray();
var start1 := fold * base1 + Min(fold, extra1);
var size1 := base1 + (if fold < extra1 then 1 else 0);
// boolean-mask вместо Contains -> O(n)
var mask := new boolean[n];
foreach var id in testIdx do
mask[id] := true;
var testIdx :=
idx0.Skip(start0).Take(size0)
.Concat(idx1.Skip(start1).Take(size1))
.ToArray;
var trainSize := n - testIdx.Length;
var trainIdx := new integer[trainSize];
var p := 0;
var trainIdx :=
(0..n-1)
.Where(i -> not testIdx.Contains(i))
.ToArray;
for var i := 0 to n - 1 do
if not mask[i] then
begin
trainIdx[p] := i;
p += 1;
end;
yield (trainIdx, testIdx);
end;