MLUtilsABC.pas

MAPE MedianAE
Правки в ML
This commit is contained in:
Mikhalkovich Stanislav 2026-04-06 23:04:46 +03:00
parent c69daac622
commit d41a519d53
14 changed files with 292 additions and 145 deletions

View file

@ -15,7 +15,7 @@ internal static class RevisionClass
public const string Major = "3";
public const string Minor = "11";
public const string Build = "1";
public const string Revision = "3793";
public const string Revision = "3799";
public const string MainVersion = Major + "." + Minor;
public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision;

View file

@ -1,4 +1,4 @@
%MINOR%=11
%REVISION%=3793
%REVISION%=3799
%COREVERSION%=1
%MAJOR%=3

View file

@ -1 +1 @@
3.11.1.3793
3.11.1.3799

View file

@ -1 +1 @@
!define VERSION '3.11.1.3793'
!define VERSION '3.11.1.3799'

View file

@ -22,7 +22,7 @@ uses
WPF,
DataFrameABC, DataFrameABCCore, LinearAlgebraML, PreprocessorABC,
MetricsABC, MLABC, MLCoreABC, MLModelsABC, ValidationML, MLExceptions, InspectionML, MLPipelineABC,
PlotML, MLDatasets, DataAdapters
PlotML, MLDatasets, DataAdapters, MLUtilsABC
;
begin

View file

@ -12,7 +12,7 @@
School, SF, TurtleABC,
DataFrameABC, DataFrameABCCore, LinearAlgebraML, PreprocessorABC,
MetricsABC, MLABC, MLCoreABC, MLModelsABC, ValidationML, MLExceptions, InspectionML, MLPipelineABC,
PlotML, MLDatasets, DataAdapters
MLDatasets, DataAdapters, MLUtilsABC
;
begin

View file

@ -194,6 +194,7 @@
File ..\bin\Lib\PlotML.pcu
File ..\bin\Lib\MLDatasets.pcu
File ..\bin\Lib\DataAdapters.pcu
File ..\bin\Lib\MLUtilsABC.pcu
File ..\bin\Lib\PABCRtl.dll
@ -303,6 +304,7 @@
${AddFile} "PlotML.pcu"
${AddFile} "MLDatasets.pcu"
${AddFile} "DataAdapters.pcu"
${AddFile} "MLUtilsABC.pcu"
${AddFile} "turtle.png"
@ -448,6 +450,7 @@
File ..\bin\Lib\PlotML.pas
File ..\bin\Lib\MLDatasets.pas
File ..\bin\Lib\DataAdapters.pas
File ..\bin\Lib\MLUtilsABC.pas
File ..\bin\Lib\__RedirectIOMode.vb
@ -544,6 +547,7 @@
${AddFile} "PlotML.pas"
${AddFile} "MLDatasets.pas"
${AddFile} "DataAdapters.pas"
${AddFile} "MLUtilsABC.pas"
${AddFile} "__RedirectIOMode.vb"

View file

@ -5,13 +5,6 @@ interface
uses DataFrameABC;
uses LinearAlgebraML;
/// Преобразует вектор меток классов в массив целых чисел.
/// Используется при визуализации и других задачах,
/// где метки должны быть представлены как 0,1,2,...
/// Значения округляются функцией Round, чтобы устранить
/// возможные небольшие численные ошибки
function LabelsToInts(y: Vector): array of integer;
/// Кодирует строковые метки классов в целочисленные индексы.
/// Каждому уникальному значению присваивается номер 0,1,2,...
/// Порядок кодирования соответствует порядку первого появления меток.
@ -65,14 +58,6 @@ const
ER_LABEL_INDEX_OUT_OF_RANGE =
'Индекс метки {0} вне диапазона [0, {1})!!Label index {0} is out of range [0, {1})';
function LabelsToInts(y: Vector): array of integer;
begin
if y = nil then
ArgumentNullError(ER_ARG_NULL, 'y');
Result := ArrGen(y.Length, i -> Round(y[i]));
end;
function EncodeLabels(labels: array of string; var classes: array of string): array of integer;
begin
if labels = nil then

View file

@ -45,7 +45,7 @@ type
/// Вектор важностей признаков длины nFeatures.
/// Чем больше значение, тем сильнее признак влияет на качество модели
static function PermutationImportance(model: IPredictiveModel; X: Matrix; y: Vector;
scoreFunc: (Vector, Vector) -> real; nRepeats: integer := 5; seed: integer := 0): Vector;
scoreFunc: (Vector, Vector) -> real; nRepeats: integer := 5; seed: integer := -1): Vector;
end;
implementation
@ -65,7 +65,7 @@ static function Inspection.PermutationImportance(
y: Vector;
scoreFunc: (Vector, Vector) -> real;
nRepeats: integer;
seed: integer
seed: integer
): Vector;
begin
if model = nil then
@ -88,6 +88,15 @@ begin
var resultVec := new Vector(p);
// Базовый seed для всех параметров.
// Все модели оцениваются на одинаковых фолдах,
// что обеспечивает корректное сравнение.
// При seed = -1 разбиение случайное, но фиксируется
// один раз для всего GridSearch.
var baseSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
for var j := 0 to p - 1 do
begin
var acc := 0.0;
@ -96,14 +105,12 @@ begin
begin
var Xperm := X.Clone;
// --- seed для каждого (признак, повтор)
var runSeed :=
if seed >= 0 then seed + j * 100000 + r
else System.Environment.TickCount and integer.MaxValue;
// --- детерминированный seed для (j, r)
var runSeed := baseSeed + j * 100000 + r;
var rnd := new System.Random(runSeed);
// --- shuffle столбца j
// --- shuffle столбца j (FisherYates)
for var i := n - 1 downto 1 do
begin
var k := rnd.Next(i + 1);

View file

@ -17,6 +17,7 @@ uses InspectionML;
uses MLPipelineABC;
uses MLDatasets;
uses DataAdapters;
uses MLUtilsABC;
type
Vector = LinearAlgebraML.Vector;
@ -121,7 +122,7 @@ implementation
function LabelsToInts(y: Vector): array of integer;
begin
Result := DataAdapters.LabelsToInts(y);
Result := MLUtilsABC.LabelsToInts(y);
end;
function EncodeLabels(labels: array of string): array of integer := DataAdapters.EncodeLabels(labels);

View file

@ -416,7 +416,8 @@ type
/// maxDepth максимальная глубина дерева.
/// minSamplesSplit минимальное число объектов для разбиения узла.
/// minSamplesLeaf минимальное число объектов в листе
constructor Create(maxDepth: integer := 10; minSamplesSplit: integer := 2; minSamplesLeaf: integer := 1; seed: integer := -1);
constructor Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer;
criterion: ISplitCriterion; seed: integer);
/// Возвращает вектор важности признаков.
/// Важность вычисляется как суммарное уменьшение
@ -474,7 +475,8 @@ type
/// maxDepth максимальная глубина дерева (-1 означает без ограничения).
/// minSamplesSplit минимальное число объектов для разбиения узла.
/// minSamplesLeaf минимальное число объектов в листе
constructor Create(maxDepth: integer := 10; minSamplesSplit: integer := 2; minSamplesLeaf: integer := 1; seed: integer := -1);
constructor Create(maxDepth: integer := 10; minSamplesSplit: integer := 2; minSamplesLeaf: integer := 1;
criterion: ISplitCriterion := nil; seed: integer := -1);
/// Обучает классификационное дерево.
/// X матрица признаков.
@ -903,8 +905,11 @@ type
seed: integer := -1
);
/// Обучает модель на всей обучающей выборке.
/// Если включен early stopping и subsample < 1, может использоваться OOB loss.
/// Обучает модель градиентного бустинга на обучающей выборке.
/// Обучение выполняется без early stopping.
/// Для предотвращения переобучения используйте FitWithValidation.
/// Early stopping работает только при наличии валидационной выборки
/// и параметре earlyStoppingPatience > 0.
/// Возвращает обученную модель.
function Fit(X: Matrix; y: Vector): ISupervisedModel;
/// Предсказывает значения целевой переменной.
@ -914,10 +919,12 @@ type
/// Копируются все деревья и внутреннее состояние.
function Clone: IModel;
/// Обучает модель с использованием отдельной validation-выборки.
/// Early stopping (если включен) происходит по validation loss.
function FitWithValidation(XTrain: Matrix; yTrain: Vector;
XVal: Matrix; yVal: Vector): IModel;
/// Обучает модель градиентного бустинга с использованием валидационной выборки.
/// Поддерживает early stopping:
/// обучение останавливается, если метрика не улучшается
/// в течение earlyStoppingPatience итераций.
/// Возвращает обученную модель.
function FitWithValidation(XTrain: Matrix; yTrain: Vector; XVal: Matrix; yVal: Vector): ISupervisedModel;
/// История значения функции потерь на обучающей выборке.
/// Один элемент на итерацию бустинга.
@ -1043,14 +1050,19 @@ type
earlyStoppingPatience: integer := 20;
seed: integer := -1);
/// Обучает классификатор на всей обучающей выборке.
/// Если включен early stopping и subsample < 1, то может использоваться OOB loss.
/// Обучает модель градиентного бустинга на обучающей выборке.
/// Обучение выполняется без early stopping.
/// Для предотвращения переобучения используйте FitWithValidation.
/// Early stopping работает только при наличии валидационной выборки
/// и параметре earlyStoppingPatience > 0.
/// Возвращает обученную модель.
function Fit(X: Matrix; y: Vector): ISupervisedModel;
/// Обучает классификатор с использованием validation-набора.
/// Если включен early stopping, он основан на validation loss.
function FitWithValidation(XTrain: Matrix; yTrain: Vector; XVal: Matrix; yVal: Vector): IModel;
/// Обучает модель градиентного бустинга с использованием валидационной выборки.
/// Поддерживает early stopping:
/// обучение останавливается, если метрика не улучшается в течение earlyStoppingPatience итераций.
/// Возвращает обученную модель.
function FitWithValidation(XTrain: Matrix; yTrain: Vector; XVal: Matrix; yVal: Vector): ISupervisedModel;
/// Предсказывает метки классов.
/// Возвращает исходные значения классов, а не внутренние индексы.
@ -1799,11 +1811,7 @@ type
{$region Utility functions}
/// Преобразует вектор меток классов в массив целых.
/// Предполагается, что значения y являются целыми
/// (0,1,2,...) и могут содержать небольшие
/// численные ошибки, поэтому используется Round.
function LabelsToInts(y: Vector): array of integer;
{$endregion Utility functions}
@ -2993,6 +3001,7 @@ constructor DecisionTreeBase.Create(
maxDepth: integer;
minSamplesSplit: integer;
minSamplesLeaf: integer;
criterion: ISplitCriterion;
seed: integer);
begin
if maxDepth < -1 then
@ -3018,6 +3027,7 @@ begin
fMaxDepth := maxDepth;
fMinSamplesSplit := minSamplesSplit;
fMinSamplesLeaf := minSamplesLeaf;
fCriterion := criterion;
if seed < 0 then
begin
@ -3488,9 +3498,12 @@ begin
Result := bestClass;
end;
constructor DecisionTreeClassifier.Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer; seed: integer);
constructor DecisionTreeClassifier.Create(maxDepth: integer; minSamplesSplit: integer; minSamplesLeaf: integer;
criterion: ISplitCriterion; seed: integer);
begin
inherited Create(maxDepth, minSamplesSplit, minSamplesLeaf, seed);
inherited Create(maxDepth, minSamplesSplit, minSamplesLeaf,
(if criterion = nil then new GiniCriterion else criterion),
seed);
end;
function DecisionTreeClassifier.Fit(X: Matrix; y: Vector): ISupervisedModel;
@ -3619,6 +3632,7 @@ begin
fMaxDepth,
fMinSamplesSplit,
fMinSamplesLeaf,
fCriterion,
fRandomSeed
);
@ -3674,15 +3688,25 @@ end;
// DecisionTreeRegressor
constructor DecisionTreeRegressor.Create(maxDepth: integer; minSamplesSplit: integer;
minSamplesLeaf: integer; leafL2: real; seed: integer);
constructor DecisionTreeRegressor.Create(
maxDepth: integer;
minSamplesSplit: integer;
minSamplesLeaf: integer;
leafL2: real;
seed: integer
);
begin
inherited Create(maxDepth, minSamplesSplit, minSamplesLeaf, seed);
inherited Create(
maxDepth,
minSamplesSplit,
minSamplesLeaf,
new VarianceCriterion,
seed
);
if leafL2 < 0 then
ArgumentOutOfRangeError(ER_LEAFL2_INVALID, leafL2);
fCriterion := new VarianceCriterion;
fLeafL2 := leafL2;
end;
@ -4224,6 +4248,7 @@ begin
fMaxDepth,
fMinSamplesSplit,
fMinSamplesLeaf,
nil,
treeSeed
);
@ -4858,7 +4883,7 @@ end;
function GradientBoostingRegressor.FitWithValidation(
XTrain: Matrix; yTrain: Vector;
XVal: Matrix; yVal: Vector): IModel;
XVal: Matrix; yVal: Vector): ISupervisedModel;
begin
Result := FitInternal(XTrain, yTrain, XVal, yVal, true);
end;
@ -6253,7 +6278,7 @@ end;
function GradientBoostingClassifier.FitWithValidation(
XTrain: Matrix; yTrain: Vector;
XVal: Matrix; yVal: Vector): IModel;
XVal: Matrix; yVal: Vector): ISupervisedModel;
begin
Result := FitInternal(XTrain, yTrain, XVal, yVal, true);
end;
@ -8558,10 +8583,5 @@ begin
Result := t;
end;
function LabelsToInts(y: Vector): array of integer;
begin
Result := ArrGen(y.Length, i -> Round(y[i]));
end;
end.

50
bin/Lib/MLUtilsABC.pas Normal file
View file

@ -0,0 +1,50 @@
/// Вспомогательные функции для ML.
///
/// Содержит утилиты, используемые в нескольких модулях,
/// не привязанные к DataFrame или конкретным моделям.
unit MLUtilsABC;
interface
uses LinearAlgebraML;
/// Преобразует вектор меток классов в массив целых чисел.
/// Используется при визуализации и других задачах,
/// где метки должны быть представлены как 0,1,2,...
/// Значения округляются функцией Round, чтобы устранить
/// возможные небольшие численные ошибки
function LabelsToInts(y: Vector): array of integer;
/// Преобразует массив целых меток в Vector.
function IntsToLabels(a: array of integer): Vector;
implementation
uses MLExceptions;
const
ER_LABELS_NULL =
'y не может быть nil!!y cannot be nil';
ER_LABELS_ARRAY_NULL =
'labels не может быть nil!!labels cannot be nil';
function LabelsToInts(y: Vector): array of integer;
begin
if y = nil then
ArgumentNullError(ER_LABELS_NULL, 'y');
Result := new integer[y.Length];
for var i := 0 to y.Length - 1 do
Result[i] := Round(y[i]);
end;
function IntsToLabels(a: array of integer): Vector;
begin
if a = nil then
ArgumentNullError(ER_LABELS_ARRAY_NULL, 'a');
Result := new Vector(a);
end;
end.

View file

@ -34,6 +34,17 @@ type
/// Менее чувствительна к выбросам, чем MSE.
static function MAE(yTrue, yPred: Vector): real;
/// Средняя абсолютная процентная ошибка (MAPE).
/// MAPE = (1/n) * Σ |(yTrue - yPred) / yTrue|.
/// Значения yTrue = 0 игнорируются.
/// Удобна для интерпретации в процентах.
static function MAPE(yTrue, yPred: Vector): real;
/// Медианная абсолютная ошибка (MedianAE).
/// MedianAE = median(|yTrue - yPred|).
/// Устойчива к выбросам.
static function MedianAE(yTrue, yPred: Vector): real;
/// Коэффициент детерминации ().
/// R2 = 1 - SS_res / SS_tot.
/// Показывает долю объяснённой дисперсии.
@ -312,6 +323,17 @@ type
/// Чем меньше значение, тем лучше модель
static function MAE(yTrue, yPred: Vector): real := Metrics.MAE(yTrue, yPred);
/// Средняя абсолютная процентная ошибка (MAPE).
/// MAPE = (1/n) * Σ |(yTrue - yPred) / yTrue|.
/// Значения yTrue = 0 игнорируются.
/// Удобна для интерпретации в процентах.
static function MAPE(yTrue, yPred: Vector): real := Metrics.MAPE(yTrue, yPred);
/// Медианная абсолютная ошибка (MedianAE).
/// MedianAE = median(|yTrue - yPred|).
/// Устойчива к выбросам.
static function MedianAE(yTrue, yPred: Vector): real := Metrics.MedianAE(yTrue, yPred);
/// Коэффициент детерминации ()
/// = 1 - SS_res / SS_tot
/// Показывает долю дисперсии, объяснённую моделью
@ -398,6 +420,10 @@ const
'Для CalinskiHarabasz требуется n > k!!CalinskiHarabasz requires n > k';
ER_LABELS_MUST_BE_INTEGERS =
'Метки кластеров должны быть целыми числами!!Cluster labels must be integers';
ER_MAPE_ALL_ZERO_TARGET =
'Все значения целевой переменной равны нулю — MAPE не определена!!All target values are zero — MAPE is undefined';
//-----------------------------
// Metrics
//-----------------------------
@ -478,6 +504,66 @@ begin
Result := s / n;
end;
static function Metrics.MAPE(yTrue, yPred: Vector): real;
begin
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
if yTrue.Length <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
var sum := 0.0;
var cnt := 0;
for var i := 0 to yTrue.Length - 1 do
begin
var y := yTrue[i];
var yp := yPred[i];
if y = 0.0 then continue;
sum += Abs((y - yp) / y);
cnt += 1;
end;
if cnt = 0 then
Error(ER_MAPE_ALL_ZERO_TARGET);
Result := sum / cnt;
end;
static function Metrics.MedianAE(yTrue, yPred: Vector): real;
begin
if yTrue = nil then
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yPred = nil then
ArgumentNullError(ER_ARG_NULL, 'yPred');
if yTrue.Length <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
var n := yTrue.Length;
if n = 0 then
ArgumentError(ER_EMPTY_DATA, 'MedianAbsoluteError');
var errors := new real[n];
for var i := 0 to n - 1 do
errors[i] := Abs(yTrue[i] - yPred[i]);
// сортировка
&Array.Sort(errors);
if n mod 2 = 1 then
Result := errors[n div 2]
else
Result := (errors[n div 2 - 1] + errors[n div 2]) / 2.0;
end;
static function Metrics.R2(yTrue, yPred: Vector): real;
begin
if yTrue = nil then

View file

@ -6,6 +6,11 @@ uses LinearAlgebraML, MLCoreABC;
type
Validation = static class
private
static function CrossValidateCore(model: ISupervisedModel;
X: Matrix; y: Vector;
folds: sequence of (array of integer, array of integer);
metric: (Vector, Vector) -> real): real;
public
/// Делит данные на обучающую и тестовую выборки.
/// testRatio доля объектов, попадающих в тестовую выборку (по умолчанию 0.2).
@ -74,6 +79,7 @@ type
k: integer;
metric: (Vector, Vector) -> real;
maximize: boolean := True;
stratified: boolean := False;
seed: integer := -1
): (P, real, T); where T: class, ISupervisedModel;
end;
@ -108,6 +114,57 @@ const
// Validation
//-----------------------------
static function Validation.CrossValidateCore(
model: ISupervisedModel;
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 ISupervisedModel;
m := m.Fit(Xtr, ytr);
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
@ -328,51 +385,17 @@ begin
if (k < 2) or (k > X.RowCount) then
ArgumentError(ER_K_INVALID, k, X.RowCount);
var total := 0.0;
var folds := 0;
var p := X.ColCount;
var baseSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
foreach var (trainIdx, testIdx) in KFold(X.RowCount, k, baseSeed) 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 ISupervisedModel;
m := m.Fit(Xtr, ytr);
var pred := m.Predict(Xte);
total += metric(yte, pred);
folds += 1;
end;
if folds = 0 then
ArgumentError(ER_EMPTY_DATA, 'CrossValidate');
Result := total / folds;
Result := CrossValidateCore(
model,
X,
y,
KFold(X.RowCount, k, baseSeed),
metric
);
end;
static function Validation.StratifiedCrossValidate(
@ -401,51 +424,17 @@ begin
if (k < 2) or (k > X.RowCount) then
ArgumentError(ER_K_INVALID_STRATIFIED, k, X.RowCount);
var total := 0.0;
var folds := 0;
var p := X.ColCount;
var baseSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
foreach var (trainIdx, testIdx) in StratifiedKFold(y, k, baseSeed) 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 ISupervisedModel;
m := m.Fit(Xtr, ytr);
var pred := m.Predict(Xte);
total += metric(yte, pred);
folds += 1;
end;
if folds = 0 then
ArgumentError(ER_EMPTY_DATA, 'StratifiedCrossValidate');
Result := total / folds;
Result := CrossValidateCore(
model,
X,
y,
StratifiedKFold(y, k, baseSeed),
metric
);
end;
//-----------------------------
@ -460,6 +449,7 @@ class function GridSearch.Search<T, P>(
k: integer;
metric: (Vector, Vector) -> real;
maximize: boolean;
stratified: boolean;
seed: integer
): (P, real, T); where T: class, ISupervisedModel;
begin
@ -498,7 +488,11 @@ begin
if model = nil then
ArgumentError(ER_MODEL_NULL);
var avgScore := Validation.CrossValidate(model, X, y, k, metric, baseSeed);
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');