pascalabcnet/bin/Lib/ValidationML.pas

936 lines
29 KiB
ObjectPascal
Raw Normal View History

unit ValidationML;
interface
2026-02-21 13:34:18 +03:00
uses LinearAlgebraML, MLCoreABC;
type
2026-04-23 16:30:27 +03:00
/// Методы для разбиения данных и оценки моделей.
///
/// Содержит утилиты для:
/// • разделения выборки на обучающую и тестовую (TrainTestSplit)
/// • k-fold кросс-валидации (KFold)
/// • стратифицированной кросс-валидации (StratifiedKFold)
/// • оценки моделей через кросс-валидацию (CrossValidate, StratifiedCrossValidate)
///
/// Методы возвращают индексы или подвыборки без изменения исходных данных.
///
/// • KFold — простое разбиение без учёта распределения классов
/// • StratifiedKFold — сохраняет пропорции классов в каждом fold (для классификации)
///
/// Для стратифицированных методов требуется:
/// • целочисленные метки классов
/// • число объектов каждого класса ≥ числа фолдов
///
/// Все методы используют генератор случайных чисел (seed) для воспроизводимости
Validation = static class
private
static function CrossValidateCore(model: IRegressor;
X: Matrix; y: Vector;
folds: sequence of (array of integer, array of integer);
metric: (Vector, Vector) -> real): real;
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;
public
/// Делит данные на обучающую и тестовую выборки.
/// testRatio — доля объектов, попадающих в тестовую выборку (по умолчанию 0.2).
2026-03-02 00:22:35 +03:00
/// Перед разбиением объекты перемешиваются.
/// Возвращает кортеж (X_train, X_test, y_train, y_test).
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);
static function TrainTestSplit(X: Matrix; y: array of integer;
testRatio: real := 0.2; seed: integer := -1): (Matrix, Matrix, array of integer, array of integer);
/// Разбивает индексы объектов на k непересекающихся частей (fold).
2026-03-02 00:22:35 +03:00
/// На каждом шаге одна часть используется как тестовая, остальные — как обучающая выборка.
/// Используется для k-fold кросс-валидации.
/// Возвращает последовательность пар (trainIdx, testIdx).
2026-03-02 00:22:35 +03:00
static function KFold(n, k: integer; seed: integer := -1):
sequence of (array of integer, array of integer);
2026-03-02 00:22:35 +03:00
/// Разбивает данные на k частей (k-fold) с сохранением пропорций классов
/// (стратифицированная k-fold кросс-валидация).
/// В каждой части доля объектов каждого класса
/// максимально близка к их доле во всей выборке
/// (разница не превышает одного объекта на класс).
/// Рекомендуется для задач классификации,
/// особенно при несбалансированных классах.
/// Возвращает последовательность пар (trainIdx, testIdx)
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);
static function StratifiedKFold(y: array of integer; k: integer;
seed: integer := -1): sequence of (array of integer, array of integer);
/// Выполняет k-fold кросс-валидацию модели с учителем.
2026-03-02 00:22:35 +03:00
/// На каждом шаге модель обучается на обучающей части и оценивается на соответствующей тестовой части.
/// metric — функция качества, принимающая (y_true, y_pred)
/// и возвращающая значение метрики (например, Accuracy или MSE).
/// Возвращает среднее значение метрики по всем частям.
2026-04-19 21:07:00 +03:00
///
/// Перегрузка для регрессионных моделей.
2026-04-19 21:07:00 +03:00
/// DataPipeline сюда передавать нельзя, так как он работает с DataFrame.
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;
/// Перегрузка для классификационных моделей.
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;
/// Выполняет стратифицированную k-fold кросс-валидацию модели с учителем.
2026-03-02 00:22:35 +03:00
/// Разбиение данных выполняется методом StratifiedKFold
/// с сохранением пропорций классов в каждой части.
/// Рекомендуется для задач классификации, особенно при несбалансированных классах.
/// Возвращает среднее значение метрики по k разбиениям.
2026-04-19 21:07:00 +03:00
///
/// Перегрузка для регрессионных моделей.
2026-04-19 21:07:00 +03:00
/// DataPipeline сюда передавать нельзя, так как он работает с DataFrame.
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;
/// Перегрузка для классификационных моделей.
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;
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;
stratified: boolean := False;
2026-04-05 00:00:36 +03:00
seed: integer := -1
): (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;
implementation
uses MLExceptions;
2026-04-23 16:30:27 +03:00
uses MLUtilsABC;
const
ER_DIM_MISMATCH_TRAIN_TEST =
'Несоответствие размерностей в TrainTestSplit: X.RowCount={0}, y.Length={1}!!' +
'Dimension mismatch in TrainTestSplit: X.RowCount={0}, y.Length={1}';
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 =
'StratifiedKFold поддерживает только целочисленные метки классов!!' +
'StratifiedKFold supports only integer class labels';
ER_INVALID_VALUE =
'Некорректное значение параметра {0}!!Invalid value for parameter {0}';
ER_DATASET_TOO_SMALL =
'Для {0} требуется как минимум 2 объекта!!' +
'At least 2 samples are required for {0}';
ER_STRATIFIED_CLASS_TOO_SMALL =
'Класс {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 =
'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
//-----------------------------
static function Validation.CrossValidateCore(
model: IRegressor;
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;
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;
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.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.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);
// --- 2. Перемешивание через стандартный Shuffle
idx.Shuffle(rnd);
2026-03-02 00:22:35 +03:00
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 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;
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;
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);
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
// --- 1. Индексы 0..n-1
var idx := Arr(0..n-1);
2026-04-05 00:00:36 +03:00
// --- 2. Перемешивание
idx.Shuffle(rnd);
var baseSize := n div k;
var extra := n mod k;
var start := 0;
2026-03-02 00:22:35 +03:00
// --- 3. Формируем фолды
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-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));
yield (trainIdx, testIdx);
2026-03-02 00:22:35 +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);
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);
2026-04-05 00:00:36 +03:00
var rnd :=
if seed >= 0 then new System.Random(seed)
else new System.Random;
// --- 1. Индексы по классам
2026-03-02 00:22:35 +03:00
var classMap := new Dictionary<integer, List<integer>>();
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-03-02 00:22:35 +03:00
if Abs(v - cls) > 1e-12 then
ArgumentError(ER_STRATIFIED_LABELS_INVALID);
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;
// --- 1.1 ПРОВЕРКА НА МИНИМАЛЬНЫЙ РАЗМЕР КЛАССА
foreach var pair in classMap do
begin
var cls := pair.Key;
var cnt := pair.Value.Count;
// Класс может иметь меньше объектов, чем число фолдов.
// В библиотеке принята строгая политика: такие случаи считаются ошибкой,
// так как не гарантируется присутствие класса во всех train-fold.
// Поэтому выполняется fail-fast проверка (см. ниже).
if cnt < k then
ArgumentError(ER_STRATIFIED_CLASS_TOO_SMALL, cls, cnt, k);
end;
// --- 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>;
// --- 3. Для каждого класса: shuffle + равномерное распределение
2026-03-02 00:22:35 +03:00
foreach var pair in classMap do
begin
2026-03-02 00:22:35 +03:00
var indices := pair.Value;
indices.Shuffle(rnd);
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-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;
// --- 4. Формирование train/test
2026-03-02 00:22:35 +03:00
for var fold := 0 to k - 1 do
begin
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;
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;
yield (trainIdx, testIdx);
end;
end;
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;
static function Validation.CrossValidate(
model: IRegressor;
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);
2026-04-05 00:00:36 +03:00
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;
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;
static function Validation.StratifiedCrossValidate(
model: IRegressor;
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);
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;
Result := CrossValidateCore(
model,
X,
y,
StratifiedKFold(y, k, baseSeed),
metric
);
end;
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;
X: Matrix;
y: Vector;
2026-02-15 08:43:44 +03:00
k: integer;
metric: (Vector, Vector) -> real;
maximize: boolean;
stratified: boolean;
seed: integer
): (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
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);
2026-02-15 08:43:44 +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);
var bestParam := paramValues[0];
2026-04-05 00:00:36 +03:00
var bestScore :=
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
var model := modelFactory(param);
if model = nil then
ArgumentError(ER_MODEL_NULL);
2026-04-05 00:00:36 +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
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;
var bestModel := modelFactory(bestParam);
if bestModel = nil then
ArgumentError(ER_MODEL_NULL);
2026-04-05 00:00:36 +03:00
bestModel := bestModel.Fit(X, y) as T;
Result := (bestParam, bestScore, bestModel);
2026-02-15 08:43:44 +03:00
end;
end.