355 lines
12 KiB
ObjectPascal
355 lines
12 KiB
ObjectPascal
unit ValidationML;
|
||
|
||
interface
|
||
|
||
uses LinearAlgebraML, MLCoreABC;
|
||
|
||
type
|
||
Validation = static class
|
||
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 := 0): (Matrix, Matrix, Vector, Vector);
|
||
|
||
/// Разбивает индексы объектов на k непересекающихся частей (fold).
|
||
/// На каждом шаге одна часть используется как тестовая, остальные — как обучающая.
|
||
/// Применяется для k-fold кросс-валидации.
|
||
/// Возвращает последовательность пар (trainIdx, testIdx).
|
||
static function KFold(n, k: integer; seed: integer := 0):
|
||
sequence of (array of integer, array of integer);
|
||
|
||
/// Разбивает данные на k частей с сохранением пропорций классов.
|
||
/// В каждой части сохраняется примерно то же соотношение
|
||
/// объектов разных классов, что и во всей выборке.
|
||
/// Рекомендуется для задач классификации,
|
||
/// особенно при неравномерном распределении классов.
|
||
/// Возвращает последовательность пар (trainIdx, testIdx).
|
||
static function StratifiedKFold(y: Vector; k: integer;
|
||
seed: integer := 0): sequence of (array of integer, array of integer);
|
||
|
||
/// Выполняет k-fold кросс-валидацию модели.
|
||
/// На каждом шаге модель обучается на обучающей части и оценивается на тестовой.
|
||
/// metric — функция качества (например, Accuracy или MSE).
|
||
/// Возвращает среднее значение метрики по всем частям.
|
||
static function CrossValidate(model: IModel; X: Matrix; y: Vector;
|
||
k: integer; metric: (Vector,Vector) -> real; seed: integer := 0): real;
|
||
|
||
/// Выполняет k-fold кросс-валидацию с сохранением пропорций классов.
|
||
/// Использует StratifiedKFold для разбиения данных.
|
||
/// Подходит для задач классификации.
|
||
/// Возвращает среднее значение метрики по всем частям.
|
||
static function StratifiedCrossValidate(model: IModel; X: Matrix; y: Vector;
|
||
k: integer; metric: (Vector,Vector) -> real; seed: integer := 0): real;
|
||
end;
|
||
|
||
/// Класс для подбора гиперпараметров методом перебора по сетке (Grid Search).
|
||
/// Для каждого значения параметра выполняется k-кратная кросс-валидация.
|
||
/// Выбирается параметр, дающий наилучшее среднее значение метрики.
|
||
/// Используется для настройки регуляризации и других гиперпараметров моделей.
|
||
GridSearch = static class
|
||
public
|
||
/// Выполняет подбор гиперпараметра по заданной сетке значений.
|
||
/// modelFactory — функция создания модели по значению параметра.
|
||
/// paramValues — набор тестируемых значений гиперпараметра.
|
||
/// X, y — обучающие данные.
|
||
/// k — число фолдов в кросс-валидации.
|
||
/// metric — функция оценки качества (yTrue, yPred) → real.
|
||
/// Возвращает кортеж (лучший параметр, лучшее среднее значение метрики,
|
||
/// модель, обученная на всём датасете с лучшим параметром).
|
||
class function Search<T>(
|
||
modelFactory: real -> T;
|
||
paramValues: array of real;
|
||
X: Matrix; y: Vector;
|
||
k: integer;
|
||
metric: (Vector, Vector) -> real
|
||
): (real, real, T); where T: IModel;
|
||
end;
|
||
|
||
implementation
|
||
|
||
uses MLExceptions;
|
||
|
||
const
|
||
ER_DIM_MISMATCH_TRAIN_TEST =
|
||
'Несоответствие размерностей в TrainTestSplit: X.RowCount={0}, y.Length={1}!!' +
|
||
'Dimension mismatch in TrainTestSplit: X.RowCount={0}, y.Length={1}';
|
||
ER_TEST_RATIO_INVALID =
|
||
'Параметр testRatio должен быть в интервале (0,1), получено {0}!!' +
|
||
'Parameter testRatio must be in (0,1), got {0}';
|
||
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 поддерживает только метки 0/1!!' +
|
||
'StratifiedKFold supports only 0/1 labels';
|
||
|
||
//-----------------------------
|
||
// Validation
|
||
//-----------------------------
|
||
|
||
static function Validation.TrainTestSplit(X: Matrix; y: Vector;
|
||
testRatio: real; seed: integer): (Matrix, Matrix, Vector, Vector);
|
||
begin
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH_TRAIN_TEST, X.RowCount, y.Length);
|
||
|
||
if (testRatio <= 0) or (testRatio >= 1) then
|
||
ArgumentError(ER_TEST_RATIO_INVALID, testRatio);
|
||
|
||
var n := X.RowCount;
|
||
var p := X.ColCount;
|
||
|
||
var rnd := new System.Random(seed);
|
||
|
||
// Перемешанные индексы
|
||
var idx := (0..n-1).ToArray;
|
||
idx := idx.OrderBy(i -> rnd.Next).ToArray;
|
||
|
||
var testSize := Round(n * testRatio);
|
||
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);
|
||
|
||
// Заполняем train
|
||
for var i := 0 to trainSize - 1 do
|
||
begin
|
||
var row := idx[i];
|
||
|
||
for var j := 0 to p - 1 do
|
||
X_train[i,j] := X[row,j];
|
||
|
||
y_train[i] := y[row];
|
||
end;
|
||
|
||
// Заполняем test
|
||
for var i := 0 to testSize - 1 do
|
||
begin
|
||
var row := idx[trainSize + i];
|
||
|
||
for var j := 0 to p - 1 do
|
||
X_test[i,j] := X[row,j];
|
||
|
||
y_test[i] := y[row];
|
||
end;
|
||
|
||
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 (k < 2) or (k > n) then
|
||
ArgumentError(ER_K_INVALID, k, n);
|
||
|
||
var rnd := new System.Random(seed);
|
||
var idx := (0..n-1).OrderBy(i -> rnd.Next).ToArray;
|
||
|
||
var baseSize := n div k;
|
||
var extra := n mod k;
|
||
var start := 0;
|
||
|
||
for var fold := 0 to k - 1 do
|
||
begin
|
||
var size := baseSize + (if fold < extra then 1 else 0);
|
||
|
||
var testIdx := idx.Skip(start).Take(size).ToArray;
|
||
var trainIdx := idx.Take(start).Concat(idx.Skip(start + size)).ToArray;
|
||
|
||
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
|
||
var n := y.Length;
|
||
if (k < 2) or (k > n) then
|
||
ArgumentError(ER_K_INVALID_STRATIFIED, k, n);
|
||
|
||
var rnd := new System.Random(seed);
|
||
|
||
var idx0 := (0..n-1)
|
||
.Where(i -> y[i] = 0)
|
||
.OrderBy(i -> rnd.Next)
|
||
.ToArray;
|
||
|
||
var idx1 := (0..n-1)
|
||
.Where(i -> y[i] = 1)
|
||
.OrderBy(i -> rnd.Next)
|
||
.ToArray;
|
||
|
||
if idx0.Length + idx1.Length <> n then
|
||
ArgumentError(ER_STRATIFIED_LABELS_INVALID);
|
||
|
||
var base0 := idx0.Length div k;
|
||
var extra0 := idx0.Length mod k;
|
||
|
||
var base1 := idx1.Length div k;
|
||
var extra1 := idx1.Length mod k;
|
||
|
||
for var fold := 0 to k - 1 do
|
||
begin
|
||
var start0 := fold * base0 + Min(fold, extra0);
|
||
var size0 := base0 + (if fold < extra0 then 1 else 0);
|
||
|
||
var start1 := fold * base1 + Min(fold, extra1);
|
||
var size1 := base1 + (if fold < extra1 then 1 else 0);
|
||
|
||
var testIdx :=
|
||
idx0.Skip(start0).Take(size0)
|
||
.Concat(idx1.Skip(start1).Take(size1))
|
||
.ToArray;
|
||
|
||
var trainIdx :=
|
||
(0..n-1)
|
||
.Where(i -> not testIdx.Contains(i))
|
||
.ToArray;
|
||
|
||
yield (trainIdx, testIdx);
|
||
end;
|
||
end;
|
||
|
||
|
||
static function Validation.CrossValidate(model: IModel; X: Matrix; y: Vector;
|
||
k: integer; metric: (Vector,Vector) -> real; seed: integer): real;
|
||
begin
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
var total := 0.0;
|
||
var folds := 0;
|
||
var p := X.ColCount;
|
||
|
||
foreach var (trainIdx, testIdx) in KFold(X.RowCount, k, seed) 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();
|
||
m.Fit(Xtr, ytr);
|
||
var pred := m.Predict(Xte);
|
||
|
||
total += metric(yte, pred);
|
||
folds += 1;
|
||
end;
|
||
|
||
Result := total / folds;
|
||
end;
|
||
|
||
static function Validation.StratifiedCrossValidate(
|
||
model: IModel; X: Matrix; y: Vector;
|
||
k: integer; metric: (Vector,Vector) -> real; seed: integer
|
||
): real;
|
||
begin
|
||
if X.RowCount <> y.Length then
|
||
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
|
||
|
||
var total := 0.0;
|
||
var folds := 0;
|
||
var p := X.ColCount;
|
||
|
||
foreach var (trainIdx, testIdx) in StratifiedKFold(y, k, seed) 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();
|
||
m.Fit(Xtr, ytr);
|
||
var pred := m.Predict(Xte);
|
||
|
||
total += metric(yte, pred);
|
||
folds += 1;
|
||
end;
|
||
|
||
Result := total / folds;
|
||
end;
|
||
|
||
//-----------------------------
|
||
// GridSearch
|
||
//-----------------------------
|
||
|
||
class function GridSearch.Search<T>(
|
||
modelFactory: real -> T;
|
||
paramValues: array of real;
|
||
X: Matrix; y: Vector;
|
||
k: integer;
|
||
metric: (Vector, Vector) -> real
|
||
): (real, real, T); where T: IModel;
|
||
begin
|
||
if paramValues.Length = 0 then
|
||
ArgumentError(ER_PARAM_VALUES_EMPTY);
|
||
|
||
var bestParam := paramValues[0];
|
||
var bestScore := -1e308;
|
||
|
||
foreach var param in paramValues do
|
||
begin
|
||
var model := modelFactory(param);
|
||
var avgScore := Validation.CrossValidate(model, X, y, k, metric);
|
||
|
||
if avgScore > bestScore then
|
||
begin
|
||
bestScore := avgScore;
|
||
bestParam := param;
|
||
end;
|
||
end;
|
||
|
||
var bestModel := modelFactory(bestParam);
|
||
bestModel.Fit(X, y);
|
||
|
||
Result := (bestParam, bestScore, bestModel);
|
||
end;
|
||
|
||
|
||
|
||
end.
|