ML - очередной аудит

This commit is contained in:
Mikhalkovich Stanislav 2026-04-23 16:30:27 +03:00
parent 2294ce5844
commit be48d378be
10 changed files with 204 additions and 150 deletions

View file

@ -54,6 +54,7 @@ type
implementation
uses MLExceptions;
uses MLUtilsABC;
const
ER_SCORE_FUNC_NULL =
@ -90,9 +91,8 @@ begin
var resultVec := new Vector(p);
var baseSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
var userProvidedSeed: boolean;
var baseSeed := ResolveRandomSeed(seed, userProvidedSeed);
for var j := 0 to p - 1 do
begin

View file

@ -4,6 +4,9 @@
// Используется дисперсия генеральной совокупности (деление на n),
// как принято в численных методах и алгоритмах машинного обучения.
//
// Исключение: Matrix.PCA использует (n-1) для ковариационной матрицы
// согласно статистической политике MLABC (пункт 3).
//
// См. статистическую политику в модуле MLABC.
// =============================================================
@ -339,7 +342,8 @@ const
'Матрица вырождена или плохо обусловлена!!Matrix is singular or ill-conditioned';
ER_EMPTY_MATRIX =
'Матрица пуста!!Matrix is empty';
ER_EIGEN_NOT_CONVERGED =
'EigenSymmetric: не сошлось за {0} итераций (off={1}, tol={2})!!EigenSymmetric did not converge in {0} iterations (off={1}, tol={2})';
type
MLNotSPDException = class(MLException);
@ -1276,10 +1280,11 @@ begin
var M := Clone;
var V := Matrix.Identity(n);
var off := 0.0;
for var iter := 0 to maxIter - 1 do
begin
// --- Норма вне-диагонали
var off := 0.0;
off := 0.0;
for var i := 0 to n - 1 do
for var j := i + 1 to n - 1 do
off += M[i, j] * M[i, j];
@ -1358,6 +1363,12 @@ begin
end;
end;
var offNorm := Sqrt(off);
if offNorm >= tol then
Error(ER_EIGEN_NOT_CONVERGED, maxIter, offNorm, tol);
// --- Eigenvalues
var values := new Vector(n);
for var i := 0 to n - 1 do
@ -1449,7 +1460,8 @@ begin
end;
// --- Eigen
var (values, V) := C.EigenSymmetric;
var adaptiveIter := Max(100, n * n * 10);
var (values, V) := C.EigenSymmetric(1e-12, adaptiveIter);
// --- Выбор k компонент
var components := new Matrix(n, k);

View file

@ -166,6 +166,10 @@ type
function Fit(X: Matrix): IUnsupervisedTransformer;
end;
IColumnExpander = interface
function GetExpandedColumns(sourceColumn: string): array of string;
end;
implementation
end.

View file

@ -88,7 +88,9 @@ type
/// clusterStd базовое стандартное отклонение
/// clusterStdVar разброс std между кластерами (0 одинаковые)
/// centerBox диапазон генерации центров [-centerBox, centerBox]
/// classBalance равномерность кластеров (0..1, 1 = равномерно)
/// classBalance равномерность распределения объектов по кластерам (0..1]
/// classBalance = 1.0 строго равномерное распределение (детерминированное, не зависит от seed)
/// classBalance < 1.0 случайное распределение; чем меньше значение, тем выше дисбаланс в среднем
/// noisePoints число шумовых точек (outliers)
/// shuffle перемешивание
/// seed генератор (seed < 0 случайный)
@ -748,7 +750,7 @@ begin
// --- вероятности кластеров
var probs := new real[centers];
if classBalance = 1 then
if Abs(classBalance - 1.0) < 1e-12 then
begin
for var c := 0 to centers - 1 do
probs[c] := 1.0 / centers;

View file

@ -32,7 +32,7 @@ const
ER_XY_SIZE_MISMATCH =
'Размеры X и y не совпадают: X={0}, y={1}!!X and y size mismatch: X={0}, y={1}';
ER_FEATURE_COUNT_MISMATCH =
'Число признаков не совпадает!!Feature count mismatch';
'Число признаков не совпадает: {0} и {1}!!Feature count mismatch: {0}, {1}';
ER_NAN_IN_X =
'X содержит NaN (пропуски)!!X contains NaN';
ER_NAN_IN_Y =

View file

@ -2475,7 +2475,15 @@ type
{$endregion Utility functions}
type
type
/// Проверять ли входные данные моделей на NaN и Infinity.
///
/// По умолчанию: True модели валидируют вход и выбрасывают исключение.
///
/// Установка в False отключает проверки ГЛОБАЛЬНО для всех моделей.
/// Использовать только если данные гарантированно очищены.
///
/// При наличии NaN/Inf поведение моделей не определено
MLConfig = static class
public
/// Проверять ли входные данные моделей на NaN, Inf
@ -2624,9 +2632,9 @@ const
ER_LEAFL2_INVALID =
'leafL2 должно быть >= 0 ({0}).!!' +
'leafL2 must be >= 0 ({0}).';
ER_MIN_LEAF_GE_SPLIT =
'minSamplesLeaf ({0}) должно быть меньше minSamplesSplit ({1}).!!' +
'minSamplesLeaf ({0}) must be less than minSamplesSplit ({1}).';
ER_MIN_LEAF_GT_SPLIT =
'minSamplesSplit ({1}) должен быть > 2 * minSamplesLeaf ({0}).!!' +
'minSamplesSplit ({1}) must be > 2 * minSamplesLeaf ({0}).';
ER_OOB_NOT_ENABLED =
'OOB score не включен для этой модели. Установите computeOOB = true в конструкторе.!!' +
'OOB score is not enabled for this model. Set computeOOB = true in the constructor.';
@ -3252,10 +3260,6 @@ begin
// --- init
fW := new Matrix(p, fClassCount);
var scale := 0.01;
for var j := 0 to p - 1 do
for var k := 0 to fClassCount - 1 do
fW.Data[j,k] := (Random - 0.5) * 2 * scale;
fIntercept := new Vector(fClassCount);
@ -3718,7 +3722,7 @@ begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
Result := Copy(fClassLabels);
end;
function GiniCriterion.Impurity(y: Vector; indices: array of integer): real;
@ -4375,11 +4379,9 @@ begin
if minSamplesLeaf < 1 then
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
if 2 * minSamplesLeaf >= minSamplesSplit then
ArgumentOutOfRangeError(
ER_MIN_LEAF_GE_SPLIT,
minSamplesLeaf, minSamplesSplit
);
if minSamplesSplit < 2 * minSamplesLeaf then
ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit);
// --- parameters valid assign
@ -4694,8 +4696,8 @@ begin
if minSamplesLeaf < 1 then
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
if minSamplesLeaf >= minSamplesSplit then
ArgumentOutOfRangeError(ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit);
if minSamplesSplit < 2 * minSamplesLeaf then
ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit);
if maxFeatures < 0 then
ArgumentOutOfRangeError(ER_MAX_FEATURES_INVALID);
@ -4753,22 +4755,17 @@ begin
var classes: array of integer;
var yEncArr := EncodeLabelsInt(yInt, classes);
if fCriterion = nil then
fCriterion := new GiniCriterion(classes.Length);
if classes.Length < 2 then
ArgumentError(ER_NEED_AT_LEAST_TWO_CLASSES);
fIndexToClass := classes;
// --- criterion
if fCriterion = nil then
fCriterion := new GiniCriterion(classes.Length);
fIndexToClass := classes;
// --- encoded vector
var yEncoded := new Vector(m);
for var i := 0 to m - 1 do
yEncoded[i] := yEncArr[i];
var yEncoded := new Vector(yEncArr);
// --- Core
fCore := new DecisionTreeCore(
@ -4854,7 +4851,7 @@ begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
Result := Copy(fClassLabels);
end;
// DecisionTreeRegressor
@ -5003,9 +5000,7 @@ begin
CheckXForPredict(X);
if X.ColCount <> fFeatureImportances.Length then
DimensionError(ER_FEATURE_COUNT_MISMATCH,
X.ColCount,
fFeatureImportances.Length);
DimensionError(ER_FEATURE_COUNT_MISMATCH, X.ColCount, fFeatureImportances.Length);
var n := X.RowCount;
Result := new Vector(n);
@ -5066,8 +5061,8 @@ begin
if minSamplesLeaf < 1 then
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
if minSamplesLeaf >= minSamplesSplit then
ArgumentOutOfRangeError(ER_MIN_LEAF_GE_SPLIT, minSamplesLeaf, minSamplesSplit);
if minSamplesSplit < 2 * minSamplesLeaf then
ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit);
fNTrees := nTrees;
fMaxDepth := maxDepth;
@ -5680,7 +5675,7 @@ begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
Result := Copy(fClassLabels);
end;
//-----------------------------
@ -5764,6 +5759,9 @@ begin
if minSamplesLeaf < 1 then
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
if minSamplesSplit < 2 * minSamplesLeaf then
ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit);
if maxDepth > MAX_ALLOWED_TREE_DEPTH then
ArgumentOutOfRangeError(ER_MAX_DEPTH_TOO_LARGE, maxDepth);
@ -6026,10 +6024,10 @@ begin
end;
if XVal.RowCount <> yVal.Length then
DimensionError(ER_XY_SIZE_MISMATCH);
DimensionError(ER_XY_SIZE_MISMATCH,XVal.RowCount,yVal.Length);
if XVal.ColCount <> XTrain.ColCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,XVal.ColCount,XTrain.ColCount);
end;
// --- OOB checks ---
@ -6241,7 +6239,7 @@ begin
CheckXForPredict(X);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var n := X.RowCount;
var yPred := new Vector(n);
@ -6473,7 +6471,7 @@ begin
ArgumentError(ER_EMPTY_DATASET);
if XTrain.RowCount <> yTrain.Length then
DimensionError(ER_XY_SIZE_MISMATCH);
DimensionError(ER_XY_SIZE_MISMATCH,XTrain.RowCount,yTrain.Length);
if useValidation then
begin
@ -6490,10 +6488,10 @@ begin
end;
if XVal.RowCount <> yVal.Length then
DimensionError(ER_XY_SIZE_MISMATCH);
DimensionError(ER_XY_SIZE_MISMATCH,XVal.RowCount,yVal.Length);
if XVal.ColCount <> XTrain.ColCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,XVal.ColCount,XTrain.ColCount);
end;
// --- reset state
@ -7007,6 +7005,9 @@ begin
if minSamplesLeaf < 1 then
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
if minSamplesSplit < 2 * minSamplesLeaf then
ArgumentOutOfRangeError(ER_MIN_LEAF_GT_SPLIT, minSamplesLeaf, minSamplesSplit);
if (subsample <= 0) or (subsample > 1) then
ArgumentOutOfRangeError(ER_SUBSAMPLE_OUT_OF_RANGE, subsample);
@ -7050,7 +7051,7 @@ begin
CheckXForPredict(X);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var nSamples := X.RowCount;
var classCount := fClassCount;
@ -7127,7 +7128,7 @@ begin
CheckXForPredict(X);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var total := fEstimators.Count;
@ -7318,7 +7319,7 @@ begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
Result := Copy(fClassLabels);
end;
//-----------------------------
@ -7344,7 +7345,7 @@ begin
CheckXForPredict(X);
if X.ColCount <> fXTrain.ColCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fXTrain.ColCount);
end;
function KNNBase.SquaredL2(trainRow: integer; XTest: Matrix; testRow: integer): double;
@ -7532,7 +7533,7 @@ begin
ArgumentNullError(ER_Y_NULL);
if X.RowCount <> y.Length then
DimensionError(ER_XY_SIZE_MISMATCH);
DimensionError(ER_XY_SIZE_MISMATCH,X.RowCount,y.Length);
if X.RowCount = 0 then
ArgumentError(ER_EMPTY_DATASET);
@ -7900,7 +7901,7 @@ begin
if fClassLabels = nil then
ArgumentError(ER_CLASSES_NOT_AVAILABLE);
Result := fClassLabels;
Result := Copy(fClassLabels);
end;
@ -7922,7 +7923,7 @@ begin
ArgumentNullError(ER_Y_NULL);
if X.RowCount <> y.Length then
DimensionError(ER_XY_SIZE_MISMATCH);
DimensionError(ER_XY_SIZE_MISMATCH,X.RowCount,y.Length);
if X.RowCount = 0 then
ArgumentError(ER_EMPTY_DATASET);
@ -8076,13 +8077,8 @@ begin
fNInit := nInit;
// --- seed (единый стиль)
fRandomSeed := seed;
fUserProvidedSeed := seed >= 0;
if fUserProvidedSeed then
fRng := new System.Random(seed)
else
fRng := new System.Random;
fRandomSeed := ResolveRandomSeed(seed, fUserProvidedSeed);
fRng := new System.Random(fRandomSeed);
// --- state
fFitted := False;
@ -8636,7 +8632,7 @@ begin
ArgumentNullError(ER_Y_NULL);
if X.RowCount <> y.Length then
DimensionError(ER_XY_SIZE_MISMATCH);
DimensionError(ER_XY_SIZE_MISMATCH,X.RowCount,y.Length);
if X.RowCount = 0 then
ArgumentError(ER_EMPTY_DATASET);
@ -8962,11 +8958,6 @@ begin
fMean := X.ColumnMeans;
fStd := X.ColumnStd;
// защита от нулевой дисперсии
for var j := 0 to fStd.Length - 1 do
if Abs(fStd[j]) < 1e-12 then
fStd[j] := 1.0;
fFitted := true;
Result := Self;
end;
@ -8980,7 +8971,7 @@ begin
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var n := X.RowCount;
var p := X.ColCount;
@ -8988,11 +8979,11 @@ begin
Result := new Matrix(n, p);
for var i := 0 to n - 1 do
for var j := 0 to p - 1 do
if fStd[j] <> 0 then
Result[i,j] := (X[i,j] - fMean[j]) / fStd[j]
else
Result[i,j] := 0.0;
for var j := 0 to p - 1 do
if Abs(fStd[j]) < 1e-12 then
Result[i,j] := 0.0
else
Result[i,j] := (X[i,j] - fMean[j]) / fStd[j];
end;
function StandardScaler.InverseTransform(X: Matrix): Matrix;
@ -9004,7 +8995,7 @@ begin
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var n := X.RowCount;
var p := X.ColCount;
@ -9074,7 +9065,7 @@ begin
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var n := X.RowCount;
var p := X.ColCount;
@ -9113,7 +9104,7 @@ begin
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var n := X.RowCount;
var p := X.ColCount;
@ -9198,7 +9189,7 @@ begin
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var n := X.RowCount;
var p := X.ColCount;
@ -9275,7 +9266,7 @@ begin
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
if fSelected = nil then
Error(ER_MODEL_NOT_INITIALIZED);
@ -9602,7 +9593,7 @@ begin
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
if fSelected = nil then
Error(ER_MODEL_NOT_INITIALIZED);
@ -9671,7 +9662,7 @@ begin
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
DimensionError(ER_FEATURE_COUNT_MISMATCH,X.ColCount,fFeatureCount);
var n := X.RowCount;
var p := X.ColCount;

View file

@ -334,16 +334,35 @@ begin
foreach var f in fFeatures do
begin
if current.HasColumn(f) then
var expanded := false;
// ищем expander-ы В ПРЯМОМ порядке pipeline
for var i := 0 to fDataSteps.Count - 1 do
begin
feats.Add(f);
continue;
var expander := fDataSteps[i] as IColumnExpander;
if expander = nil then
continue;
var cols := expander.GetExpandedColumns(f);
if (cols <> nil) and (cols.Length > 0) then
begin
foreach var c in cols do
if current.HasColumn(c) then
if not feats.Contains(c) then
feats.Add(c);
expanded := true;
break;
end;
end;
foreach var c in current.Schema.ColumnNames do
if c.StartsWith(f + '_') then
if not feats.Contains(c) then
feats.Add(c);
if expanded then
continue;
// fallback: обычный столбец
if current.HasColumn(f) then
if not feats.Contains(f) then
feats.Add(f);
end;
if feats.Count = 0 then

View file

@ -1681,10 +1681,10 @@ end;
static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real;
begin
if yTrue = nil then
ArgumentNullError(ER_Y_NULL);
ArgumentNullError(ER_ARG_NULL, 'yTrue');
if yPred = nil then
ArgumentNullError(ER_Y_NULL);
ArgumentNullError(ER_ARG_NULL, 'yPred');
var n := yTrue.Length;
if n <> yPred.Length then
@ -1774,56 +1774,6 @@ begin
Result := (sumNij - expected) / denom;
end;
{static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real;
begin
if yTrue.Length <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
var n := yTrue.Length;
if n < 2 then
Result := 1
else
begin
var tp := 0;
var tn := 0;
var fp := 0;
var fn := 0;
for var i := 0 to n-2 do
for var j := i+1 to n-1 do
begin
var sameTrue := yTrue[i] = yTrue[j];
var samePred := yPred[i] = yPred[j];
if sameTrue and samePred then
tp += 1
else if (not sameTrue) and (not samePred) then
tn += 1
else if (not sameTrue) and samePred then
fp += 1
else
fn += 1;
end;
var total := tp + tn + fp + fn;
var ri := (tp + tn) / total;
var a := tp + fp;
var b := tp + fn;
var c := fn + tn;
var d := fp + tn;
var expected := ((a*b) + (c*d)) / (total*total);
if 1 - expected = 0 then
Result := 0
else
Result := (ri - expected) / (1 - expected);
end;
end;}
function R(s: string; w: integer): string;
begin
Result := s.PadLeft(w);

View file

@ -88,7 +88,7 @@ type
/// Категории фиксируются при Fit
/// Неизвестные категории кодируются нулями
/// Пропущенные значения (NA) кодируются нулями
OneHotEncoder = class(IPreprocessor, IColumnBoundStep)
OneHotEncoder = class(IPreprocessor, IColumnBoundStep, IColumnExpander)
private
col: string;
categories: array of string;
@ -115,6 +115,8 @@ type
function ToString: string; override;
property ColumnName: string read col;
function GetExpandedColumns(sourceColumn: string): array of string;
/// Создаёт копию препроцессора с той же конфигурацией.
///
@ -251,7 +253,13 @@ const
ER_IMPUTER_STRATEGY_NOT_SUPPORTED =
'Стратегия импутации {0} не поддерживается!!Imputation strategy {0} is not supported';
ER_UNSUPPORTED_IMPUTE_STRATEGY =
'Неподдерживаемая стратегия заполнения: {0}!!Unsupported impute strategy: {0}';
'Неподдерживаемая стратегия заполнения: {0}!!Unsupported impute strategy: {0}';
ER_ONEHOT_NAME_EQUALS_SOURCE =
'Сгенерированная колонка совпадает с исходной: {0}!!Generated column equals source column: {0}';
ER_ONEHOT_COLUMN_COLLISION =
'Конфликт имён колонок: {0}!!Column name collision: {0}';
ER_ONEHOT_DUPLICATE_COLUMN =
'Дублирующаяся сгенерированная колонка: {0}!!Duplicate generated column: {0}';
//-----------------------------
@ -407,6 +415,29 @@ begin
Error(ER_ONEHOT_EMPTY_COLUMN, col);
categories := values.ToArray;
// --- проверка коллизий имён колонок
var used := new HashSet<string>;
for var i := 0 to categories.Length - 1 do
begin
var newName := col + '_' + categories[i];
// 1. совпадение с исходным именем
if newName = col then
Error(ER_ONEHOT_NAME_EQUALS_SOURCE, newName);
// 2. коллизия с существующими колонками DataFrame
if df.HasColumn(newName) then
Error(ER_ONEHOT_COLUMN_COLLISION, newName);
// 3. дубликаты среди сгенерированных колонок
if used.Contains(newName) then
Error(ER_ONEHOT_DUPLICATE_COLUMN, newName);
used.Add(newName);
end;
fitted := true;
Result := Self;
end;
@ -465,6 +496,21 @@ begin
Result := Transform(df);
end;
function OneHotEncoder.GetExpandedColumns(sourceColumn: string): array of string;
begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
if sourceColumn <> col then
exit(nil);
var res := new string[categories.Length];
for var i := 0 to categories.Length - 1 do
res[i] := col + '_' + categories[i];
Result := res;
end;
function OneHotEncoder.ToString: string;
begin
Result := 'OneHotEncoder(column=' + col + ')';
@ -615,13 +661,15 @@ begin
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
var capturedIdx := idx;
case strategy of
isMean:
begin
var m := means[i];
res := res.ReplaceColumnFloat(
name,
c -> (if c.IsValid(idx) then c.Float(idx) else m)
c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else m)
);
end;
@ -665,7 +713,7 @@ begin
res := res.ReplaceColumnFloat(
name,
c -> (if c.IsValid(idx) then c.Float(idx) else r)
c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else r)
);
end;
end;
@ -675,7 +723,7 @@ begin
var m := medians[i];
res := res.ReplaceColumnFloat(
name,
c -> (if c.IsValid(idx) then c.Float(idx) else m)
c -> (if c.IsValid(capturedIdx) then c.Float(capturedIdx) else m)
);
end;

View file

@ -5,6 +5,24 @@ interface
uses LinearAlgebraML, MLCoreABC;
type
/// Методы для разбиения данных и оценки моделей.
///
/// Содержит утилиты для:
/// разделения выборки на обучающую и тестовую (TrainTestSplit)
/// k-fold кросс-валидации (KFold)
/// стратифицированной кросс-валидации (StratifiedKFold)
/// оценки моделей через кросс-валидацию (CrossValidate, StratifiedCrossValidate)
///
/// Методы возвращают индексы или подвыборки без изменения исходных данных.
///
/// KFold простое разбиение без учёта распределения классов
/// StratifiedKFold сохраняет пропорции классов в каждом fold (для классификации)
///
/// Для стратифицированных методов требуется:
/// целочисленные метки классов
/// число объектов каждого класса числа фолдов
///
/// Все методы используют генератор случайных чисел (seed) для воспроизводимости
Validation = static class
private
static function CrossValidateCore(model: ISupervisedModel;
@ -93,6 +111,7 @@ type
implementation
uses MLExceptions;
uses MLUtilsABC;
const
ER_DIM_MISMATCH_TRAIN_TEST =
@ -114,7 +133,9 @@ const
'At least 2 samples are required for {0}';
ER_STRATIFIED_CLASS_TOO_SMALL =
'Класс {0} содержит {1} объектов, что меньше числа фолдов ({2})!!Class {0} has {1} samples, which is less than the number of folds ({2})';
ER_STRATIFIED_K_TOO_LARGE =
'Stratified CV: число фолдов ({0}) превышает минимальный размер класса ({1})!!Stratified CV: number of folds ({0}) exceeds smallest class size ({1})';
//-----------------------------
// Validation
//-----------------------------
@ -263,12 +284,8 @@ begin
var trainSize := n - size;
var trainIdx := new integer[trainSize];
if start > 0 then
System.Array.Copy(idx, 0, trainIdx, 0, start);
var tailCount := n - (start + size);
if tailCount > 0 then
System.Array.Copy(idx, start + size, trainIdx, start, tailCount);
System.Array.Copy(idx, 0, trainIdx, 0, start);
System.Array.Copy(idx, start + size, trainIdx, start, n - (start + size));
yield (trainIdx, testIdx);
@ -442,6 +459,17 @@ begin
if (k < 2) or (k > X.RowCount) then
ArgumentError(ER_K_INVALID_STRATIFIED, k, X.RowCount);
var labels := y.ToIntArray;
var classCounts := labels.EachCount;
var minCount := integer.MaxValue;
foreach var pair in classCounts do
if pair.Value < minCount then
minCount := pair.Value;
if k > minCount then
ArgumentError(ER_STRATIFIED_K_TOO_LARGE, k, minCount);
var baseSeed :=
if seed >= 0 then seed