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( 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( 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>(); 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; if classMap.TryGetValue(cls, lst) then lst.Add(i) else begin lst := new List; 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[k]; for var f := 0 to k - 1 do folds[f] := new List; // --- 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>(); for var i := 0 to n - 1 do begin var cls := y[i]; var lst: List; if classMap.TryGetValue(cls, lst) then lst.Add(i) else begin lst := new List; 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[k]; for var f := 0 to k - 1 do folds[f] := new List; 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( 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( 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.