2026-02-14 11:55:00 +03:00
|
|
|
|
unit ValidationML;
|
|
|
|
|
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
2026-02-21 13:34:18 +03:00
|
|
|
|
uses LinearAlgebraML, MLCoreABC;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
|
|
|
|
|
type
|
2026-04-23 16:30:27 +03:00
|
|
|
|
/// Методы для разбиения данных и оценки моделей.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Содержит утилиты для:
|
|
|
|
|
|
/// • разделения выборки на обучающую и тестовую (TrainTestSplit)
|
|
|
|
|
|
/// • k-fold кросс-валидации (KFold)
|
|
|
|
|
|
/// • стратифицированной кросс-валидации (StratifiedKFold)
|
|
|
|
|
|
/// • оценки моделей через кросс-валидацию (CrossValidate, StratifiedCrossValidate)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Методы возвращают индексы или подвыборки без изменения исходных данных.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// • KFold — простое разбиение без учёта распределения классов
|
|
|
|
|
|
/// • StratifiedKFold — сохраняет пропорции классов в каждом fold (для классификации)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Для стратифицированных методов требуется:
|
|
|
|
|
|
/// • целочисленные метки классов
|
|
|
|
|
|
/// • число объектов каждого класса ≥ числа фолдов
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Все методы используют генератор случайных чисел (seed) для воспроизводимости
|
2026-02-14 11:55:00 +03:00
|
|
|
|
Validation = static class
|
2026-04-06 23:04:46 +03:00
|
|
|
|
private
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function CrossValidateCore(model: IRegressor;
|
2026-04-06 23:04:46 +03:00
|
|
|
|
X: Matrix; y: Vector;
|
|
|
|
|
|
folds: sequence of (array of integer, array of integer);
|
|
|
|
|
|
metric: (Vector, Vector) -> real): real;
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function CrossValidateCore(model: IClassifier;
|
|
|
|
|
|
X: Matrix; y: array of integer;
|
|
|
|
|
|
folds: sequence of (array of integer, array of integer);
|
|
|
|
|
|
metric: (array of integer, array of integer) -> real): real;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
public
|
|
|
|
|
|
/// Делит данные на обучающую и тестовую выборки.
|
|
|
|
|
|
/// testRatio — доля объектов, попадающих в тестовую выборку (по умолчанию 0.2).
|
2026-03-02 00:22:35 +03:00
|
|
|
|
/// Перед разбиением объекты перемешиваются.
|
|
|
|
|
|
/// Возвращает кортеж (X_train, X_test, y_train, y_test).
|
2026-02-14 11:55:00 +03:00
|
|
|
|
static function TrainTestSplit(X: Matrix; y: Vector;
|
2026-03-02 00:22:35 +03:00
|
|
|
|
testRatio: real := 0.2; seed: integer := -1): (Matrix, Matrix, Vector, Vector);
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function TrainTestSplit(X: Matrix; y: array of integer;
|
|
|
|
|
|
testRatio: real := 0.2; seed: integer := -1): (Matrix, Matrix, array of integer, array of integer);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
|
|
|
|
|
/// Разбивает индексы объектов на k непересекающихся частей (fold).
|
2026-03-02 00:22:35 +03:00
|
|
|
|
/// На каждом шаге одна часть используется как тестовая, остальные — как обучающая выборка.
|
|
|
|
|
|
/// Используется для k-fold кросс-валидации.
|
2026-02-14 11:55:00 +03:00
|
|
|
|
/// Возвращает последовательность пар (trainIdx, testIdx).
|
2026-03-02 00:22:35 +03:00
|
|
|
|
static function KFold(n, k: integer; seed: integer := -1):
|
2026-02-14 11:55:00 +03:00
|
|
|
|
sequence of (array of integer, array of integer);
|
|
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
/// Разбивает данные на k частей (k-fold) с сохранением пропорций классов
|
|
|
|
|
|
/// (стратифицированная k-fold кросс-валидация).
|
|
|
|
|
|
/// В каждой части доля объектов каждого класса
|
|
|
|
|
|
/// максимально близка к их доле во всей выборке
|
|
|
|
|
|
/// (разница не превышает одного объекта на класс).
|
|
|
|
|
|
/// Рекомендуется для задач классификации,
|
|
|
|
|
|
/// особенно при несбалансированных классах.
|
|
|
|
|
|
/// Возвращает последовательность пар (trainIdx, testIdx)
|
2026-02-14 11:55:00 +03:00
|
|
|
|
static function StratifiedKFold(y: Vector; k: integer;
|
2026-03-02 00:22:35 +03:00
|
|
|
|
seed: integer := -1): sequence of (array of integer, array of integer);
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function StratifiedKFold(y: array of integer; k: integer;
|
|
|
|
|
|
seed: integer := -1): sequence of (array of integer, array of integer);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-21 16:30:25 +03:00
|
|
|
|
/// Выполняет k-fold кросс-валидацию модели с учителем.
|
2026-03-02 00:22:35 +03:00
|
|
|
|
/// На каждом шаге модель обучается на обучающей части и оценивается на соответствующей тестовой части.
|
|
|
|
|
|
/// metric — функция качества, принимающая (y_true, y_pred)
|
|
|
|
|
|
/// и возвращающая значение метрики (например, Accuracy или MSE).
|
2026-02-14 11:55:00 +03:00
|
|
|
|
/// Возвращает среднее значение метрики по всем частям.
|
2026-04-19 21:07:00 +03:00
|
|
|
|
///
|
2026-05-28 10:23:22 +03:00
|
|
|
|
/// Перегрузка для регрессионных моделей.
|
2026-04-19 21:07:00 +03:00
|
|
|
|
/// DataPipeline сюда передавать нельзя, так как он работает с DataFrame.
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function CrossValidate(model: IRegressor; X: Matrix; y: Vector;
|
2026-03-02 00:22:35 +03:00
|
|
|
|
k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real;
|
2026-05-28 10:23:22 +03:00
|
|
|
|
/// Перегрузка для классификационных моделей.
|
|
|
|
|
|
static function CrossValidate(model: IClassifier; X: Matrix; y: array of integer;
|
|
|
|
|
|
k: integer; metric: (array of integer, array of integer) -> real; seed: integer := -1): real;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-21 16:30:25 +03:00
|
|
|
|
/// Выполняет стратифицированную k-fold кросс-валидацию модели с учителем.
|
2026-03-02 00:22:35 +03:00
|
|
|
|
/// Разбиение данных выполняется методом StratifiedKFold
|
|
|
|
|
|
/// с сохранением пропорций классов в каждой части.
|
|
|
|
|
|
/// Рекомендуется для задач классификации, особенно при несбалансированных классах.
|
|
|
|
|
|
/// Возвращает среднее значение метрики по k разбиениям.
|
2026-04-19 21:07:00 +03:00
|
|
|
|
///
|
2026-05-28 10:23:22 +03:00
|
|
|
|
/// Перегрузка для регрессионных моделей.
|
2026-04-19 21:07:00 +03:00
|
|
|
|
/// DataPipeline сюда передавать нельзя, так как он работает с DataFrame.
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function StratifiedCrossValidate(model: IRegressor; X: Matrix; y: Vector;
|
2026-03-02 00:22:35 +03:00
|
|
|
|
k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real;
|
2026-05-28 10:23:22 +03:00
|
|
|
|
/// Перегрузка для классификационных моделей.
|
|
|
|
|
|
static function StratifiedCrossValidate(model: IClassifier; X: Matrix; y: array of integer;
|
|
|
|
|
|
k: integer; metric: (array of integer, array of integer) -> real; seed: integer := -1): real;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
end;
|
2026-02-15 08:43:44 +03:00
|
|
|
|
|
|
|
|
|
|
/// Класс для подбора гиперпараметров методом перебора по сетке (Grid Search).
|
|
|
|
|
|
/// Для каждого значения параметра выполняется k-кратная кросс-валидация.
|
|
|
|
|
|
/// Выбирается параметр, дающий наилучшее среднее значение метрики.
|
|
|
|
|
|
/// Используется для настройки регуляризации и других гиперпараметров моделей.
|
|
|
|
|
|
GridSearch = static class
|
|
|
|
|
|
public
|
|
|
|
|
|
/// Выполняет подбор гиперпараметра по заданной сетке значений.
|
2026-04-05 00:00:36 +03:00
|
|
|
|
/// • modelFactory — функция создания модели по значению параметра (P -> T).
|
|
|
|
|
|
/// • paramValues — набор тестируемых значений гиперпараметра типа P.
|
|
|
|
|
|
/// • X, y — обучающие данные.
|
|
|
|
|
|
/// • k — число фолдов в кросс-валидации.
|
|
|
|
|
|
/// • metric — функция оценки качества (yTrue, yPred) → real.
|
|
|
|
|
|
/// • maximize — если true, максимизируется метрика; иначе минимизируется.
|
|
|
|
|
|
/// • seed — seed для разбиения на фолды (для воспроизводимости).
|
|
|
|
|
|
/// Возвращает кортеж:
|
|
|
|
|
|
/// • лучший параметр,
|
|
|
|
|
|
/// • лучшее среднее значение метрики,
|
|
|
|
|
|
/// • модель, обученная на всём датасете с лучшим параметром
|
2026-04-27 09:34:44 +03:00
|
|
|
|
///
|
|
|
|
|
|
/// Все параметры оцениваются на одном и том же разбиении данных
|
|
|
|
|
|
/// (используется фиксированный seed), что обеспечивает корректное и сопоставимое сравнение моделей
|
2026-04-05 00:00:36 +03:00
|
|
|
|
class function Search<T, P>(
|
|
|
|
|
|
modelFactory: P -> T;
|
|
|
|
|
|
paramValues: array of P;
|
|
|
|
|
|
X: Matrix; y: Vector;
|
|
|
|
|
|
k: integer;
|
|
|
|
|
|
metric: (Vector, Vector) -> real;
|
|
|
|
|
|
maximize: boolean := True;
|
2026-04-06 23:04:46 +03:00
|
|
|
|
stratified: boolean := False;
|
2026-04-05 00:00:36 +03:00
|
|
|
|
seed: integer := -1
|
2026-05-28 10:23:22 +03:00
|
|
|
|
): (P, real, T); where T: class, IRegressor;
|
|
|
|
|
|
class function Search<T, P>(
|
|
|
|
|
|
modelFactory: P -> T;
|
|
|
|
|
|
paramValues: array of P;
|
|
|
|
|
|
X: Matrix; y: array of integer;
|
|
|
|
|
|
k: integer;
|
|
|
|
|
|
metric: (array of integer, array of integer) -> real;
|
|
|
|
|
|
maximize: boolean := True;
|
|
|
|
|
|
stratified: boolean := False;
|
|
|
|
|
|
seed: integer := -1
|
|
|
|
|
|
): (P, real, T); where T: class, IClassifier;
|
2026-02-15 08:43:44 +03:00
|
|
|
|
end;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
|
|
|
|
|
implementation
|
|
|
|
|
|
|
2026-02-17 19:59:28 +03:00
|
|
|
|
uses MLExceptions;
|
2026-04-23 16:30:27 +03:00
|
|
|
|
uses MLUtilsABC;
|
2026-02-17 19:59:28 +03:00
|
|
|
|
|
|
|
|
|
|
const
|
|
|
|
|
|
ER_DIM_MISMATCH_TRAIN_TEST =
|
|
|
|
|
|
'Несоответствие размерностей в TrainTestSplit: X.RowCount={0}, y.Length={1}!!' +
|
|
|
|
|
|
'Dimension mismatch in TrainTestSplit: X.RowCount={0}, y.Length={1}';
|
2026-02-18 10:38:42 +03:00
|
|
|
|
ER_K_INVALID =
|
|
|
|
|
|
'Некорректное значение k в KFold: k={0}, n={1}!!' +
|
|
|
|
|
|
'Invalid k in KFold: k={0}, n={1}';
|
|
|
|
|
|
ER_K_INVALID_STRATIFIED =
|
|
|
|
|
|
'Некорректное значение k в StratifiedKFold: k={0}, n={1}!!' +
|
|
|
|
|
|
'Invalid k in StratifiedKFold: k={0}, n={1}';
|
|
|
|
|
|
ER_STRATIFIED_LABELS_INVALID =
|
2026-03-21 16:30:25 +03:00
|
|
|
|
'StratifiedKFold поддерживает только целочисленные метки классов!!' +
|
|
|
|
|
|
'StratifiedKFold supports only integer class labels';
|
2026-03-03 14:14:59 +03:00
|
|
|
|
ER_INVALID_VALUE =
|
2026-03-21 16:30:25 +03:00
|
|
|
|
'Некорректное значение параметра {0}!!Invalid value for parameter {0}';
|
|
|
|
|
|
ER_DATASET_TOO_SMALL =
|
|
|
|
|
|
'Для {0} требуется как минимум 2 объекта!!' +
|
2026-04-13 21:05:51 +03:00
|
|
|
|
'At least 2 samples are required for {0}';
|
|
|
|
|
|
ER_STRATIFIED_CLASS_TOO_SMALL =
|
2026-04-28 18:23:03 +03:00
|
|
|
|
'Класс {0} содержит {1} объектов, что меньше числа фолдов ({2}). Уменьшите k или объедините малочисленные классы.!!' +
|
|
|
|
|
|
'Class {0} has {1} samples, which is less than the number of folds ({2}). Reduce k or merge very small classes.';
|
2026-04-23 16:30:27 +03:00
|
|
|
|
ER_STRATIFIED_K_TOO_LARGE =
|
2026-05-28 10:23:22 +03:00
|
|
|
|
'Stratified CV: число фолдов ({0}) превышает минимальный размер класса ({1})!!Stratified CV: number of folds ({0}) exceeds smallest class size ({1})';
|
2026-04-23 16:30:27 +03:00
|
|
|
|
|
2026-02-15 08:43:44 +03:00
|
|
|
|
//-----------------------------
|
|
|
|
|
|
// Validation
|
|
|
|
|
|
//-----------------------------
|
|
|
|
|
|
|
2026-04-06 23:04:46 +03:00
|
|
|
|
static function Validation.CrossValidateCore(
|
2026-05-28 10:23:22 +03:00
|
|
|
|
model: IRegressor;
|
2026-04-06 23:04:46 +03:00
|
|
|
|
X: Matrix;
|
|
|
|
|
|
y: Vector;
|
|
|
|
|
|
folds: sequence of (array of integer, array of integer);
|
|
|
|
|
|
metric: (Vector, Vector) -> real
|
|
|
|
|
|
): real;
|
|
|
|
|
|
begin
|
|
|
|
|
|
var total := 0.0;
|
|
|
|
|
|
var foldsCount := 0;
|
|
|
|
|
|
var p := X.ColCount;
|
|
|
|
|
|
|
|
|
|
|
|
foreach var (trainIdx, testIdx) in folds do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var Xtr := new Matrix(trainIdx.Length, p);
|
|
|
|
|
|
var ytr := new Vector(trainIdx.Length);
|
|
|
|
|
|
|
|
|
|
|
|
for var i := 0 to trainIdx.Length - 1 do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var r := trainIdx[i];
|
|
|
|
|
|
for var j := 0 to p - 1 do
|
|
|
|
|
|
Xtr[i,j] := X[r,j];
|
|
|
|
|
|
ytr[i] := y[r];
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
var Xte := new Matrix(testIdx.Length, p);
|
|
|
|
|
|
var yte := new Vector(testIdx.Length);
|
|
|
|
|
|
|
|
|
|
|
|
for var i := 0 to testIdx.Length - 1 do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var r := testIdx[i];
|
|
|
|
|
|
for var j := 0 to p - 1 do
|
|
|
|
|
|
Xte[i,j] := X[r,j];
|
|
|
|
|
|
yte[i] := y[r];
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-05-28 10:23:22 +03:00
|
|
|
|
var m := model.Clone() as IRegressor;
|
|
|
|
|
|
m := m.Fit(Xtr, ytr) as IRegressor;
|
|
|
|
|
|
|
|
|
|
|
|
var pred := m.Predict(Xte);
|
|
|
|
|
|
|
|
|
|
|
|
total += metric(yte, pred);
|
|
|
|
|
|
foldsCount += 1;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
if foldsCount = 0 then
|
|
|
|
|
|
ArgumentError(ER_EMPTY_DATA, 'CrossValidate');
|
|
|
|
|
|
|
|
|
|
|
|
Result := total / foldsCount;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
static function Validation.CrossValidateCore(
|
|
|
|
|
|
model: IClassifier;
|
|
|
|
|
|
X: Matrix;
|
|
|
|
|
|
y: array of integer;
|
|
|
|
|
|
folds: sequence of (array of integer, array of integer);
|
|
|
|
|
|
metric: (array of integer, array of integer) -> real
|
|
|
|
|
|
): real;
|
|
|
|
|
|
begin
|
|
|
|
|
|
var total := 0.0;
|
|
|
|
|
|
var foldsCount := 0;
|
|
|
|
|
|
var p := X.ColCount;
|
|
|
|
|
|
|
|
|
|
|
|
foreach var (trainIdx, testIdx) in folds do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var Xtr := new Matrix(trainIdx.Length, p);
|
|
|
|
|
|
var ytr := new integer[trainIdx.Length];
|
|
|
|
|
|
|
|
|
|
|
|
for var i := 0 to trainIdx.Length - 1 do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var r := trainIdx[i];
|
|
|
|
|
|
for var j := 0 to p - 1 do
|
|
|
|
|
|
Xtr[i,j] := X[r,j];
|
|
|
|
|
|
ytr[i] := y[r];
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
var Xte := new Matrix(testIdx.Length, p);
|
|
|
|
|
|
var yte := new integer[testIdx.Length];
|
|
|
|
|
|
|
|
|
|
|
|
for var i := 0 to testIdx.Length - 1 do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var r := testIdx[i];
|
|
|
|
|
|
for var j := 0 to p - 1 do
|
|
|
|
|
|
Xte[i,j] := X[r,j];
|
|
|
|
|
|
yte[i] := y[r];
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
var m := model.Clone() as IClassifier;
|
|
|
|
|
|
m := m.Fit(Xtr, ytr) as IClassifier;
|
2026-04-06 23:04:46 +03:00
|
|
|
|
|
|
|
|
|
|
var pred := m.Predict(Xte);
|
|
|
|
|
|
|
|
|
|
|
|
total += metric(yte, pred);
|
|
|
|
|
|
foldsCount += 1;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
if foldsCount = 0 then
|
|
|
|
|
|
ArgumentError(ER_EMPTY_DATA, 'CrossValidate');
|
|
|
|
|
|
|
|
|
|
|
|
Result := total / foldsCount;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
static function Validation.TrainTestSplit(X: Matrix; y: Vector;
|
|
|
|
|
|
testRatio: real; seed: integer): (Matrix, Matrix, Vector, Vector);
|
|
|
|
|
|
begin
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if X = nil then
|
|
|
|
|
|
ArgumentNullError(ER_ARG_NULL, 'X');
|
|
|
|
|
|
|
|
|
|
|
|
if y = nil then
|
|
|
|
|
|
ArgumentNullError(ER_ARG_NULL, 'y');
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
if X.RowCount <> y.Length then
|
2026-02-17 19:59:28 +03:00
|
|
|
|
DimensionError(ER_DIM_MISMATCH_TRAIN_TEST, X.RowCount, y.Length);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if (testRatio <= 0.0) or (testRatio >= 1.0) then
|
2026-02-18 10:38:42 +03:00
|
|
|
|
ArgumentError(ER_TEST_RATIO_INVALID, testRatio);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
|
|
|
|
|
var n := X.RowCount;
|
|
|
|
|
|
var p := X.ColCount;
|
|
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if n < 2 then
|
2026-03-21 16:30:25 +03:00
|
|
|
|
ArgumentError(ER_DATASET_TOO_SMALL, 'TrainTestSplit');
|
2026-03-03 14:14:59 +03:00
|
|
|
|
|
|
|
|
|
|
var actualSeed := if seed >= 0 then seed else System.Environment.TickCount and integer.MaxValue;
|
|
|
|
|
|
var rnd := new System.Random(actualSeed);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
var idx := Arr(0..n-1);
|
|
|
|
|
|
|
|
|
|
|
|
// --- 2. Перемешивание через стандартный Shuffle
|
|
|
|
|
|
idx.Shuffle(rnd);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
var rawSize := Round(n * testRatio);
|
2026-03-03 14:14:59 +03:00
|
|
|
|
var testSize := rawSize.Clamp(1, n - 1);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
var trainSize := n - testSize;
|
|
|
|
|
|
|
|
|
|
|
|
var X_train := new Matrix(trainSize, p);
|
|
|
|
|
|
var X_test := new Matrix(testSize, p);
|
|
|
|
|
|
|
|
|
|
|
|
var y_train := new Vector(trainSize);
|
|
|
|
|
|
var y_test := new Vector(testSize);
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
Result := (X_train, X_test, y_train, y_test);
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function Validation.TrainTestSplit(X: Matrix; y: array of integer;
|
|
|
|
|
|
testRatio: real; seed: integer): (Matrix, Matrix, array of integer, array of integer);
|
|
|
|
|
|
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.0) or (testRatio >= 1.0) then
|
|
|
|
|
|
ArgumentError(ER_TEST_RATIO_INVALID, testRatio);
|
|
|
|
|
|
|
|
|
|
|
|
var n := X.RowCount;
|
|
|
|
|
|
var p := X.ColCount;
|
|
|
|
|
|
|
|
|
|
|
|
if n < 2 then
|
|
|
|
|
|
ArgumentError(ER_DATASET_TOO_SMALL, 'TrainTestSplit');
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
idx.Shuffle(rnd);
|
|
|
|
|
|
|
|
|
|
|
|
var rawSize := Round(n * testRatio);
|
|
|
|
|
|
var testSize := rawSize.Clamp(1, n - 1);
|
|
|
|
|
|
var trainSize := n - testSize;
|
|
|
|
|
|
|
|
|
|
|
|
var X_train := new Matrix(trainSize, p);
|
|
|
|
|
|
var X_test := new Matrix(testSize, p);
|
|
|
|
|
|
|
|
|
|
|
|
var y_train := new integer[trainSize];
|
|
|
|
|
|
var y_test := new integer[testSize];
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
Result := (X_train, X_test, y_train, y_test);
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
static function Validation.KFold(n, k: integer; seed: integer):
|
|
|
|
|
|
sequence of (array of integer, array of integer);
|
|
|
|
|
|
begin
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if n <= 0 then
|
|
|
|
|
|
ArgumentError(ER_EMPTY_DATA, 'KFold');
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
if (k < 2) or (k > n) then
|
2026-02-18 10:38:42 +03:00
|
|
|
|
ArgumentError(ER_K_INVALID, k, n);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-04-05 00:00:36 +03:00
|
|
|
|
// --- RNG (без дублирования логики seed)
|
|
|
|
|
|
var rnd :=
|
|
|
|
|
|
if seed >= 0 then new System.Random(seed)
|
|
|
|
|
|
else new System.Random;
|
2026-03-02 00:22:35 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
// --- 1. Индексы 0..n-1
|
|
|
|
|
|
var idx := Arr(0..n-1);
|
|
|
|
|
|
|
2026-04-05 00:00:36 +03:00
|
|
|
|
// --- 2. Перемешивание
|
2026-03-03 14:14:59 +03:00
|
|
|
|
idx.Shuffle(rnd);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
|
|
|
|
|
var baseSize := n div k;
|
|
|
|
|
|
var extra := n mod k;
|
|
|
|
|
|
var start := 0;
|
|
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
// --- 3. Формируем фолды
|
2026-02-14 11:55:00 +03:00
|
|
|
|
for var fold := 0 to k - 1 do
|
|
|
|
|
|
begin
|
2026-03-02 00:22:35 +03:00
|
|
|
|
var size := baseSize + Ord(fold < extra);
|
|
|
|
|
|
|
|
|
|
|
|
var testIdx := new integer[size];
|
|
|
|
|
|
System.Array.Copy(idx, start, testIdx, 0, size);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
var trainSize := n - size;
|
|
|
|
|
|
var trainIdx := new integer[trainSize];
|
|
|
|
|
|
|
2026-04-23 16:30:27 +03:00
|
|
|
|
System.Array.Copy(idx, 0, trainIdx, 0, start);
|
|
|
|
|
|
System.Array.Copy(idx, start + size, trainIdx, start, n - (start + size));
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
|
|
|
|
|
yield (trainIdx, testIdx);
|
2026-03-02 00:22:35 +03:00
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
start += size;
|
|
|
|
|
|
end;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
static function Validation.StratifiedKFold(y: Vector; k: integer; seed: integer):
|
|
|
|
|
|
sequence of (array of integer, array of integer);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
begin
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if y = nil then
|
|
|
|
|
|
ArgumentNullError(ER_ARG_NULL, 'y');
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
var n := y.Length;
|
2026-03-03 14:14:59 +03:00
|
|
|
|
|
|
|
|
|
|
if n <= 0 then
|
|
|
|
|
|
ArgumentError(ER_EMPTY_DATA, 'StratifiedKFold');
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
if (k < 2) or (k > n) then
|
2026-02-18 10:38:42 +03:00
|
|
|
|
ArgumentError(ER_K_INVALID_STRATIFIED, k, n);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-04-05 00:00:36 +03:00
|
|
|
|
var rnd :=
|
|
|
|
|
|
if seed >= 0 then new System.Random(seed)
|
|
|
|
|
|
else new System.Random;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
// --- 1. Индексы по классам
|
2026-03-02 00:22:35 +03:00
|
|
|
|
var classMap := new Dictionary<integer, List<integer>>();
|
2026-03-03 14:14:59 +03:00
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
for var i := 0 to n - 1 do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var v := y[i];
|
2026-04-05 00:00:36 +03:00
|
|
|
|
var cls := Round(v);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
if Abs(v - cls) > 1e-12 then
|
|
|
|
|
|
ArgumentError(ER_STRATIFIED_LABELS_INVALID);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
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;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-04-11 20:35:52 +03:00
|
|
|
|
// --- 1.1 ПРОВЕРКА НА МИНИМАЛЬНЫЙ РАЗМЕР КЛАССА
|
|
|
|
|
|
foreach var pair in classMap do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var cls := pair.Key;
|
|
|
|
|
|
var cnt := pair.Value.Count;
|
|
|
|
|
|
|
2026-04-15 20:11:49 +03:00
|
|
|
|
// Класс может иметь меньше объектов, чем число фолдов.
|
2026-04-22 14:08:27 +03:00
|
|
|
|
// В библиотеке принята строгая политика: такие случаи считаются ошибкой,
|
|
|
|
|
|
// так как не гарантируется присутствие класса во всех train-fold.
|
|
|
|
|
|
// Поэтому выполняется fail-fast проверка (см. ниже).
|
|
|
|
|
|
if cnt < k then
|
|
|
|
|
|
ArgumentError(ER_STRATIFIED_CLASS_TOO_SMALL, cls, cnt, k);
|
2026-04-11 20:35:52 +03:00
|
|
|
|
end;
|
|
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
// --- 2. Контейнеры фолдов
|
2026-03-02 00:22:35 +03:00
|
|
|
|
var folds := new List<integer>[k];
|
|
|
|
|
|
for var f := 0 to k - 1 do
|
|
|
|
|
|
folds[f] := new List<integer>;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
// --- 3. Для каждого класса: shuffle + равномерное распределение
|
2026-03-02 00:22:35 +03:00
|
|
|
|
foreach var pair in classMap do
|
2026-02-14 11:55:00 +03:00
|
|
|
|
begin
|
2026-03-02 00:22:35 +03:00
|
|
|
|
var indices := pair.Value;
|
2026-03-03 14:14:59 +03:00
|
|
|
|
indices.Shuffle(rnd);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
var m := indices.Count;
|
2026-03-02 00:22:35 +03:00
|
|
|
|
var baseSize := m div k;
|
|
|
|
|
|
var extra := m mod k;
|
|
|
|
|
|
var start := 0;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-02 00:22:35 +03:00
|
|
|
|
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;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
// --- 4. Формирование train/test
|
2026-03-02 00:22:35 +03:00
|
|
|
|
for var fold := 0 to k - 1 do
|
|
|
|
|
|
begin
|
2026-03-03 14:14:59 +03:00
|
|
|
|
var testIdx := folds[fold].ToArray;
|
2026-03-02 00:22:35 +03:00
|
|
|
|
|
|
|
|
|
|
var mask := new boolean[n];
|
|
|
|
|
|
foreach var id in testIdx do
|
|
|
|
|
|
mask[id] := true;
|
|
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
var trainIdx := new integer[n - testIdx.Length];
|
2026-03-02 00:22:35 +03:00
|
|
|
|
var p := 0;
|
|
|
|
|
|
|
|
|
|
|
|
for var i := 0 to n - 1 do
|
|
|
|
|
|
if not mask[i] then
|
|
|
|
|
|
begin
|
|
|
|
|
|
trainIdx[p] := i;
|
|
|
|
|
|
p += 1;
|
|
|
|
|
|
end;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
|
|
|
|
|
yield (trainIdx, testIdx);
|
|
|
|
|
|
end;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function Validation.StratifiedKFold(y: array of integer; 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 classMap := new Dictionary<integer, List<integer>>();
|
|
|
|
|
|
|
|
|
|
|
|
for var i := 0 to n - 1 do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var cls := y[i];
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
foreach var pair in classMap do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var cls := pair.Key;
|
|
|
|
|
|
var cnt := pair.Value.Count;
|
|
|
|
|
|
if cnt < k then
|
|
|
|
|
|
ArgumentError(ER_STRATIFIED_CLASS_TOO_SMALL, cls, cnt, k);
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
var folds := new List<integer>[k];
|
|
|
|
|
|
for var f := 0 to k - 1 do
|
|
|
|
|
|
folds[f] := new List<integer>;
|
|
|
|
|
|
|
|
|
|
|
|
foreach var pair in classMap do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var indices := pair.Value;
|
|
|
|
|
|
indices.Shuffle(rnd);
|
|
|
|
|
|
|
|
|
|
|
|
var m := indices.Count;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
for var fold := 0 to k - 1 do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var testIdx := folds[fold].ToArray;
|
|
|
|
|
|
|
|
|
|
|
|
var mask := new boolean[n];
|
|
|
|
|
|
foreach var id in testIdx do
|
|
|
|
|
|
mask[id] := true;
|
|
|
|
|
|
|
|
|
|
|
|
var trainIdx := new integer[n - testIdx.Length];
|
|
|
|
|
|
var p := 0;
|
|
|
|
|
|
|
|
|
|
|
|
for var i := 0 to n - 1 do
|
|
|
|
|
|
if not mask[i] then
|
|
|
|
|
|
begin
|
|
|
|
|
|
trainIdx[p] := i;
|
|
|
|
|
|
p += 1;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
yield (trainIdx, testIdx);
|
|
|
|
|
|
end;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
static function Validation.CrossValidate(
|
2026-05-28 10:23:22 +03:00
|
|
|
|
model: IRegressor;
|
2026-03-03 14:14:59 +03:00
|
|
|
|
X: Matrix;
|
|
|
|
|
|
y: Vector;
|
|
|
|
|
|
k: integer;
|
|
|
|
|
|
metric: (Vector,Vector) -> real;
|
|
|
|
|
|
seed: integer): real;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
begin
|
2026-03-03 14:14:59 +03:00
|
|
|
|
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');
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
if X.RowCount <> y.Length then
|
2026-02-18 10:38:42 +03:00
|
|
|
|
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if (k < 2) or (k > X.RowCount) then
|
|
|
|
|
|
ArgumentError(ER_K_INVALID, k, X.RowCount);
|
|
|
|
|
|
|
2026-04-05 00:00:36 +03:00
|
|
|
|
var baseSeed :=
|
|
|
|
|
|
if seed >= 0 then seed
|
|
|
|
|
|
else System.Environment.TickCount and integer.MaxValue;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-04-06 23:04:46 +03:00
|
|
|
|
Result := CrossValidateCore(
|
|
|
|
|
|
model,
|
|
|
|
|
|
X,
|
|
|
|
|
|
y,
|
|
|
|
|
|
KFold(X.RowCount, k, baseSeed),
|
|
|
|
|
|
metric
|
|
|
|
|
|
);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
end;
|
|
|
|
|
|
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function Validation.CrossValidate(
|
|
|
|
|
|
model: IClassifier;
|
|
|
|
|
|
X: Matrix;
|
|
|
|
|
|
y: array of integer;
|
|
|
|
|
|
k: integer;
|
|
|
|
|
|
metric: (array of integer, array of integer) -> 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 baseSeed :=
|
|
|
|
|
|
if seed >= 0 then seed
|
|
|
|
|
|
else System.Environment.TickCount and integer.MaxValue;
|
|
|
|
|
|
|
|
|
|
|
|
Result := CrossValidateCore(
|
|
|
|
|
|
model,
|
|
|
|
|
|
X,
|
|
|
|
|
|
y,
|
|
|
|
|
|
KFold(X.RowCount, k, baseSeed),
|
|
|
|
|
|
metric
|
|
|
|
|
|
);
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
static function Validation.StratifiedCrossValidate(
|
2026-05-28 10:23:22 +03:00
|
|
|
|
model: IRegressor;
|
2026-03-03 14:14:59 +03:00
|
|
|
|
X: Matrix;
|
|
|
|
|
|
y: Vector;
|
|
|
|
|
|
k: integer;
|
|
|
|
|
|
metric: (Vector,Vector) -> real;
|
|
|
|
|
|
seed: integer): real;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
begin
|
2026-03-03 14:14:59 +03:00
|
|
|
|
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');
|
|
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
if X.RowCount <> y.Length then
|
2026-02-18 10:38:42 +03:00
|
|
|
|
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if (k < 2) or (k > X.RowCount) then
|
|
|
|
|
|
ArgumentError(ER_K_INVALID_STRATIFIED, k, X.RowCount);
|
2026-04-23 16:30:27 +03:00
|
|
|
|
|
2026-04-05 00:00:36 +03:00
|
|
|
|
var baseSeed :=
|
|
|
|
|
|
if seed >= 0 then seed
|
|
|
|
|
|
else System.Environment.TickCount and integer.MaxValue;
|
|
|
|
|
|
|
2026-04-06 23:04:46 +03:00
|
|
|
|
Result := CrossValidateCore(
|
|
|
|
|
|
model,
|
|
|
|
|
|
X,
|
|
|
|
|
|
y,
|
|
|
|
|
|
StratifiedKFold(y, k, baseSeed),
|
|
|
|
|
|
metric
|
|
|
|
|
|
);
|
2026-02-14 11:55:00 +03:00
|
|
|
|
end;
|
|
|
|
|
|
|
2026-05-28 10:23:22 +03:00
|
|
|
|
static function Validation.StratifiedCrossValidate(
|
|
|
|
|
|
model: IClassifier;
|
|
|
|
|
|
X: Matrix;
|
|
|
|
|
|
y: array of integer;
|
|
|
|
|
|
k: integer;
|
|
|
|
|
|
metric: (array of integer, array of integer) -> 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 baseSeed :=
|
|
|
|
|
|
if seed >= 0 then seed
|
|
|
|
|
|
else System.Environment.TickCount and integer.MaxValue;
|
|
|
|
|
|
|
|
|
|
|
|
Result := CrossValidateCore(
|
|
|
|
|
|
model,
|
|
|
|
|
|
X,
|
|
|
|
|
|
y,
|
|
|
|
|
|
StratifiedKFold(y, k, baseSeed),
|
|
|
|
|
|
metric
|
|
|
|
|
|
);
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-02-15 08:43:44 +03:00
|
|
|
|
//-----------------------------
|
|
|
|
|
|
// GridSearch
|
|
|
|
|
|
//-----------------------------
|
|
|
|
|
|
|
2026-04-05 00:00:36 +03:00
|
|
|
|
class function GridSearch.Search<T, P>(
|
|
|
|
|
|
modelFactory: P -> T;
|
|
|
|
|
|
paramValues: array of P;
|
2026-03-03 14:14:59 +03:00
|
|
|
|
X: Matrix;
|
|
|
|
|
|
y: Vector;
|
2026-02-15 08:43:44 +03:00
|
|
|
|
k: integer;
|
2026-03-03 14:14:59 +03:00
|
|
|
|
metric: (Vector, Vector) -> real;
|
2026-04-04 21:48:15 +03:00
|
|
|
|
maximize: boolean;
|
2026-04-06 23:04:46 +03:00
|
|
|
|
stratified: boolean;
|
2026-04-04 21:48:15 +03:00
|
|
|
|
seed: integer
|
2026-05-28 10:23:22 +03:00
|
|
|
|
): (P, real, T); where T: class, IRegressor;
|
|
|
|
|
|
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 :=
|
|
|
|
|
|
if maximize then -1e308 else 1e308;
|
|
|
|
|
|
|
|
|
|
|
|
var baseSeed :=
|
|
|
|
|
|
if seed >= 0 then seed
|
|
|
|
|
|
else System.Environment.TickCount and integer.MaxValue;
|
|
|
|
|
|
|
|
|
|
|
|
foreach var param in paramValues do
|
|
|
|
|
|
begin
|
|
|
|
|
|
var model := modelFactory(param);
|
|
|
|
|
|
if model = nil then
|
|
|
|
|
|
ArgumentError(ER_MODEL_NULL);
|
|
|
|
|
|
|
|
|
|
|
|
var avgScore :=
|
|
|
|
|
|
if stratified then
|
|
|
|
|
|
Validation.StratifiedCrossValidate(model, X, y, k, metric, baseSeed)
|
|
|
|
|
|
else
|
|
|
|
|
|
Validation.CrossValidate(model, X, y, k, metric, baseSeed);
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
end;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
var bestModel := modelFactory(bestParam);
|
|
|
|
|
|
if bestModel = nil then
|
|
|
|
|
|
ArgumentError(ER_MODEL_NULL);
|
|
|
|
|
|
|
|
|
|
|
|
bestModel := bestModel.Fit(X, y) as T;
|
|
|
|
|
|
|
|
|
|
|
|
Result := (bestParam, bestScore, bestModel);
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
class function GridSearch.Search<T, P>(
|
|
|
|
|
|
modelFactory: P -> T;
|
|
|
|
|
|
paramValues: array of P;
|
|
|
|
|
|
X: Matrix;
|
|
|
|
|
|
y: array of integer;
|
|
|
|
|
|
k: integer;
|
|
|
|
|
|
metric: (array of integer, array of integer) -> real;
|
|
|
|
|
|
maximize: boolean;
|
|
|
|
|
|
stratified: boolean;
|
|
|
|
|
|
seed: integer
|
|
|
|
|
|
): (P, real, T); where T: class, IClassifier;
|
2026-02-15 08:43:44 +03:00
|
|
|
|
begin
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if modelFactory = nil then
|
|
|
|
|
|
ArgumentNullError(ER_ARG_NULL, 'modelFactory');
|
|
|
|
|
|
|
|
|
|
|
|
if paramValues = nil then
|
|
|
|
|
|
ArgumentNullError(ER_ARG_NULL, 'paramValues');
|
|
|
|
|
|
|
2026-02-17 16:18:17 +03:00
|
|
|
|
if paramValues.Length = 0 then
|
2026-02-18 10:38:42 +03:00
|
|
|
|
ArgumentError(ER_PARAM_VALUES_EMPTY);
|
2026-02-15 08:43:44 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-02-17 16:18:17 +03:00
|
|
|
|
var bestParam := paramValues[0];
|
2026-04-05 00:00:36 +03:00
|
|
|
|
var bestScore :=
|
2026-03-03 14:14:59 +03:00
|
|
|
|
if maximize then -1e308 else 1e308;
|
2026-02-15 08:43:44 +03:00
|
|
|
|
|
2026-04-05 00:00:36 +03:00
|
|
|
|
var baseSeed :=
|
|
|
|
|
|
if seed >= 0 then seed
|
|
|
|
|
|
else System.Environment.TickCount and integer.MaxValue;
|
|
|
|
|
|
|
2026-02-15 08:43:44 +03:00
|
|
|
|
foreach var param in paramValues do
|
|
|
|
|
|
begin
|
2026-02-17 16:18:17 +03:00
|
|
|
|
var model := modelFactory(param);
|
2026-03-21 16:30:25 +03:00
|
|
|
|
if model = nil then
|
|
|
|
|
|
ArgumentError(ER_MODEL_NULL);
|
2026-04-05 00:00:36 +03:00
|
|
|
|
|
2026-04-06 23:04:46 +03:00
|
|
|
|
var avgScore :=
|
|
|
|
|
|
if stratified then
|
|
|
|
|
|
Validation.StratifiedCrossValidate(model, X, y, k, metric, baseSeed)
|
|
|
|
|
|
else
|
|
|
|
|
|
Validation.CrossValidate(model, X, y, k, metric, baseSeed);
|
2026-02-15 08:43:44 +03:00
|
|
|
|
|
2026-03-03 14:14:59 +03:00
|
|
|
|
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
|
2026-02-15 08:43:44 +03:00
|
|
|
|
begin
|
|
|
|
|
|
bestScore := avgScore;
|
|
|
|
|
|
bestParam := param;
|
|
|
|
|
|
end;
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
2026-02-17 16:18:17 +03:00
|
|
|
|
var bestModel := modelFactory(bestParam);
|
2026-03-21 16:30:25 +03:00
|
|
|
|
if bestModel = nil then
|
|
|
|
|
|
ArgumentError(ER_MODEL_NULL);
|
2026-04-05 00:00:36 +03:00
|
|
|
|
|
2026-03-21 16:30:25 +03:00
|
|
|
|
bestModel := bestModel.Fit(X, y) as T;
|
2026-02-17 16:18:17 +03:00
|
|
|
|
|
|
|
|
|
|
Result := (bestParam, bestScore, bestModel);
|
2026-02-15 08:43:44 +03:00
|
|
|
|
end;
|
2026-02-14 11:55:00 +03:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-17 16:18:17 +03:00
|
|
|
|
|
2026-02-14 11:55:00 +03:00
|
|
|
|
end.
|