936 lines
29 KiB
ObjectPascal
936 lines
29 KiB
ObjectPascal
unit ValidationML;
|
||
|
||
interface
|
||
|
||
uses LinearAlgebraML, MLCoreABC;
|
||
|
||
type
|
||
/// Методы для разбиения данных и оценки моделей.
|
||
///
|
||
/// Содержит утилиты для:
|
||
/// • разделения выборки на обучающую и тестовую (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).
|
||
/// Перед разбиением объекты перемешиваются.
|
||
/// Возвращает кортеж (X_train, X_test, y_train, y_test).
|
||
static function TrainTestSplit(X: Matrix; y: Vector;
|
||
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).
|
||
/// На каждом шаге одна часть используется как тестовая, остальные — как обучающая выборка.
|
||
/// Используется для k-fold кросс-валидации.
|
||
/// Возвращает последовательность пар (trainIdx, testIdx).
|
||
static function KFold(n, k: integer; seed: integer := -1):
|
||
sequence of (array of integer, array of integer);
|
||
|
||
/// Разбивает данные на k частей (k-fold) с сохранением пропорций классов
|
||
/// (стратифицированная k-fold кросс-валидация).
|
||
/// В каждой части доля объектов каждого класса
|
||
/// максимально близка к их доле во всей выборке
|
||
/// (разница не превышает одного объекта на класс).
|
||
/// Рекомендуется для задач классификации,
|
||
/// особенно при несбалансированных классах.
|
||
/// Возвращает последовательность пар (trainIdx, testIdx)
|
||
static function StratifiedKFold(y: Vector; k: integer;
|
||
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 кросс-валидацию модели с учителем.
|
||
/// На каждом шаге модель обучается на обучающей части и оценивается на соответствующей тестовой части.
|
||
/// metric — функция качества, принимающая (y_true, y_pred)
|
||
/// и возвращающая значение метрики (например, Accuracy или MSE).
|
||
/// Возвращает среднее значение метрики по всем частям.
|
||
///
|
||
/// Перегрузка для регрессионных моделей.
|
||
/// DataPipeline сюда передавать нельзя, так как он работает с DataFrame.
|
||
static function CrossValidate(model: IRegressor; X: Matrix; y: Vector;
|
||
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 кросс-валидацию модели с учителем.
|
||
/// Разбиение данных выполняется методом StratifiedKFold
|
||
/// с сохранением пропорций классов в каждой части.
|
||
/// Рекомендуется для задач классификации, особенно при несбалансированных классах.
|
||
/// Возвращает среднее значение метрики по k разбиениям.
|
||
///
|
||
/// Перегрузка для регрессионных моделей.
|
||
/// DataPipeline сюда передавать нельзя, так как он работает с DataFrame.
|
||
static function StratifiedCrossValidate(model: IRegressor; X: Matrix; y: Vector;
|
||
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;
|
||
|
||
/// Класс для подбора гиперпараметров методом перебора по сетке (Grid Search).
|
||
/// Для каждого значения параметра выполняется k-кратная кросс-валидация.
|
||
/// Выбирается параметр, дающий наилучшее среднее значение метрики.
|
||
/// Используется для настройки регуляризации и других гиперпараметров моделей.
|
||
GridSearch = static class
|
||
public
|
||
/// Выполняет подбор гиперпараметра по заданной сетке значений.
|
||
/// • modelFactory — функция создания модели по значению параметра (P -> T).
|
||
/// • paramValues — набор тестируемых значений гиперпараметра типа P.
|
||
/// • X, y — обучающие данные.
|
||
/// • k — число фолдов в кросс-валидации.
|
||
/// • metric — функция оценки качества (yTrue, yPred) → real.
|
||
/// • maximize — если true, максимизируется метрика; иначе минимизируется.
|
||
/// • seed — seed для разбиения на фолды (для воспроизводимости).
|
||
/// Возвращает кортеж:
|
||
/// • лучший параметр,
|
||
/// • лучшее среднее значение метрики,
|
||
/// • модель, обученная на всём датасете с лучшим параметром
|
||
///
|
||
/// Все параметры оцениваются на одном и том же разбиении данных
|
||
/// (используется фиксированный seed), что обеспечивает корректное и сопоставимое сравнение моделей
|
||
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;
|
||
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;
|
||
end;
|
||
|
||
implementation
|
||
|
||
uses MLExceptions;
|
||
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.';
|
||
ER_STRATIFIED_K_TOO_LARGE =
|
||
'Stratified CV: число фолдов ({0}) превышает минимальный размер класса ({1})!!Stratified CV: number of folds ({0}) exceeds smallest class size ({1})';
|
||
|
||
//-----------------------------
|
||
// 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);
|
||
|
||
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);
|
||
|
||
// --- RNG (без дублирования логики seed)
|
||
var rnd :=
|
||
if seed >= 0 then new System.Random(seed)
|
||
else new System.Random;
|
||
|
||
// --- 1. Индексы 0..n-1
|
||
var idx := Arr(0..n-1);
|
||
|
||
// --- 2. Перемешивание
|
||
idx.Shuffle(rnd);
|
||
|
||
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 + Ord(fold < extra);
|
||
|
||
var testIdx := new integer[size];
|
||
System.Array.Copy(idx, start, testIdx, 0, size);
|
||
|
||
var trainSize := n - size;
|
||
var trainIdx := new integer[trainSize];
|
||
|
||
System.Array.Copy(idx, 0, trainIdx, 0, start);
|
||
System.Array.Copy(idx, start + size, trainIdx, start, n - (start + size));
|
||
|
||
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);
|
||
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;
|
||
|
||
// --- 1. Индексы по классам
|
||
var classMap := new Dictionary<integer, List<integer>>();
|
||
|
||
for var i := 0 to n - 1 do
|
||
begin
|
||
var v := y[i];
|
||
var cls := Round(v);
|
||
|
||
if Abs(v - cls) > 1e-12 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;
|
||
|
||
// --- 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. Контейнеры фолдов
|
||
var folds := new List<integer>[k];
|
||
for var f := 0 to k - 1 do
|
||
folds[f] := new List<integer>;
|
||
|
||
// --- 3. Для каждого класса: shuffle + равномерное распределение
|
||
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;
|
||
|
||
// --- 4. Формирование train/test
|
||
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.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);
|
||
|
||
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);
|
||
|
||
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;
|
||
|
||
//-----------------------------
|
||
// GridSearch
|
||
//-----------------------------
|
||
|
||
class function GridSearch.Search<T, P>(
|
||
modelFactory: P -> T;
|
||
paramValues: array of P;
|
||
X: Matrix;
|
||
y: Vector;
|
||
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;
|
||
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;
|
||
|
||
|
||
|
||
end.
|