ML - правки аудита 2

This commit is contained in:
Mikhalkovich Stanislav 2026-04-05 00:00:36 +03:00
parent 5c8f2b0caa
commit f61cd7770c
9 changed files with 366 additions and 214 deletions

View file

@ -62,6 +62,8 @@ const
'Неподдерживаемый тип столбца "{0}" для EncodeLabels!!Unsupported column type "{0}" for EncodeLabels';
ER_UNKNOWN_CLASS_IN_TRANSFORM =
'Неизвестное значение класса "{0}" при преобразовании меток!!Unknown class value "{0}" in TransformLabels';
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
@ -115,7 +117,7 @@ begin
var lbl := labels[i];
if not map.ContainsKey(lbl) then
Error('Unknown class in test');
Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, lbl);
res[i] := map[lbl];
end;
@ -363,7 +365,7 @@ begin
var lbl := data[i].ToString;
if not map.ContainsKey(lbl) then
Error('Unknown class in TransformLabels');
Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, lbl);
res[i] := map[lbl];
end;
@ -453,8 +455,15 @@ function DecodeLabels(y: array of integer; classes: array of string): array of s
begin
var res := new string[y.Length];
for var i := 0 to y.Length-1 do
res[i] := classes[y[i]];
for var i := 0 to y.Length - 1 do
begin
var idx := y[i];
if (idx < 0) or (idx >= classes.Length) then
Error(ER_LABEL_INDEX_OUT_OF_RANGE, idx, classes.Length);
res[i] := classes[idx];
end;
Result := res;
end;

View file

@ -504,8 +504,6 @@ const
'Тип соединения не реализован!!Join kind not implemented';
ER_JOIN_KEYS_LENGTH_MISMATCH =
'leftKeys и rightKeys должны иметь одинаковую длину!!leftKeys and rightKeys must have the same length';
ER_COLUMN_NOT_FOUND =
'Столбец "{0}" не найден!!Column not found: {0}';
ER_ADD_COLUMN_ROW_MISMATCH =
'Несоответствие числа строк при добавлении столбца!!Row count mismatch when adding column';
ER_COLUMN_VALID_LENGTH_MISMATCH =

View file

@ -45,7 +45,7 @@ type
/// Вектор важностей признаков длины nFeatures.
/// Чем больше значение, тем сильнее признак влияет на качество модели
static function PermutationImportance(model: IPredictiveModel; X: Matrix; y: Vector;
scoreFunc: (Vector, Vector) -> real; seed: integer := 0): Vector;
scoreFunc: (Vector, Vector) -> real; nRepeats: integer := 5; seed: integer := 0): Vector;
end;
implementation
@ -53,10 +53,20 @@ implementation
uses MLExceptions;
const
ER_SCORE_FUNC_NULL = 'scoreFunc не может быть nil!!scoreFunc cannot be nil';
ER_SCORE_FUNC_NULL =
'scoreFunc не может быть nil!!scoreFunc cannot be nil';
ER_ARG_OUT_OF_RANGE =
'Аргумент {0} имеет недопустимое значение {1}!!Argument {0} has invalid value {1}';
static function Inspection.PermutationImportance(model: IPredictiveModel; X: Matrix; y: Vector;
scoreFunc: (Vector, Vector) -> real; seed: integer): Vector;
static function Inspection.PermutationImportance(
model: IPredictiveModel;
X: Matrix;
y: Vector;
scoreFunc: (Vector, Vector) -> real;
nRepeats: integer;
seed: integer
): Vector;
begin
if model = nil then
ArgumentNullError(ER_MODEL_NULL);
@ -67,6 +77,9 @@ begin
if X.RowCount <> y.Length then
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
if nRepeats < 1 then
ArgumentOutOfRangeError(ER_ARG_OUT_OF_RANGE, 'nRepeats', nRepeats);
var baselinePred := model.Predict(X);
var baselineScore := scoreFunc(y, baselinePred);
@ -74,25 +87,38 @@ begin
var p := X.ColCount;
var resultVec := new Vector(p);
var rng := new System.Random(seed);
for var j := 0 to p-1 do
for var j := 0 to p - 1 do
begin
var Xperm := X.Clone;
var acc := 0.0;
// FisherYates shuffle столбца j (детерминированно через seed)
for var i := n-1 downto 1 do
for var r := 0 to nRepeats - 1 do
begin
var k := rng.Next(i+1);
var tmp := Xperm[i,j];
Xperm[i,j] := Xperm[k,j];
Xperm[k,j] := tmp;
var Xperm := X.Clone;
// --- seed для каждого (признак, повтор)
var runSeed :=
if seed >= 0 then seed + j * 100000 + r
else System.Environment.TickCount and integer.MaxValue;
var rnd := new System.Random(runSeed);
// --- shuffle столбца j
for var i := n - 1 downto 1 do
begin
var k := rnd.Next(i + 1);
var tmp := Xperm[i,j];
Xperm[i,j] := Xperm[k,j];
Xperm[k,j] := tmp;
end;
var permPred := model.Predict(Xperm);
var permScore := scoreFunc(y, permPred);
acc += (baselineScore - permScore);
end;
var permPred := model.Predict(Xperm);
var permScore := scoreFunc(y, permPred);
resultVec[j] := baselineScore - permScore;
resultVec[j] := acc / nRepeats;
end;
Result := resultVec;

View file

@ -152,9 +152,6 @@ type
static function operator *(A: Matrix; x: Vector): Vector;
static function operator *(A, B: Matrix): Matrix;
//static function operator *(alpha: real; A: Matrix): Matrix;
//static function operator *(A: Matrix; alpha: real): Matrix;
// ---------- in-place operators ----------
static function operator +=(A, B: Matrix): Matrix;
static function operator -=(A, B: Matrix): Matrix;

View file

@ -18,20 +18,20 @@ type
/// Предоставляет удобные методы для получения матриц признаков
/// и целевых значений для обучения моделей
Dataset = class
private
function ValueLabel(feature, value: string): string;
function CloneMeta(df: DataFrame): Dataset;
private
function ValueLabel(feature, value: string): string;
function CloneMeta(df: DataFrame): Dataset;
public
Name: string;
Data: DataFrame;
Features: array of string;
Target: string;
Task: TaskType;
FeatureLabels: Dictionary<string,string>;
ValueLabels: Dictionary<string,Dictionary<string,string>>;
Description: string;
auto property Name: string;
auto property Data: DataFrame;
auto property Features: array of string;
auto property Target: string;
auto property Task: TaskType;
/// Возвращает true, если датасет относится к задаче с учителем
/// (classification или regression).
function IsSupervised: boolean;

View file

@ -1271,6 +1271,10 @@ type
fIterations: integer;
fHasConverged: boolean;
fRandomSeed: integer;
fUserProvidedSeed: boolean;
fRng: System.Random;
function RunSingle(X: Matrix; rnd: System.Random): (Matrix, real, integer, boolean);
public
/// Создаёт модель KMeans.
@ -1361,9 +1365,6 @@ type
/// Обучает модель на матрице признаков.
function Fit(X: Matrix): IUnsupervisedModel;
/// Возвращает метки кластеров.
function Predict(X: Matrix): Vector;
/// Возвращает метки кластеров.
function PredictLabels(X: Matrix): array of integer;
@ -3042,16 +3043,24 @@ begin
dest.fMaxFeatures := fMaxFeatures;
dest.fUserProvidedSeed := fUserProvidedSeed;
dest.fRng := new System.Random(fRandomSeed);
if fUserProvidedSeed then
dest.fRng := new System.Random(fRandomSeed)
else
dest.fRng := new System.Random;
// MUST be stateless
if fCriterion <> nil then
dest.fCriterion := fCriterion; // можно так, если критерий stateless
dest.fCriterion := fCriterion;
if fFeatureImportances <> nil then
dest.fFeatureImportances := fFeatureImportances.Clone;
if fRoot <> nil then
dest.fRoot := fRoot.Clone;
if fRowIndices <> nil then
dest.fRowIndices := Copy(fRowIndices);
end;
function DecisionTreeBase.GetFeatureSubset(nFeatures: integer): array of integer;
@ -3613,11 +3622,8 @@ begin
fRandomSeed
);
// --- базовое состояние (глубокое копирование дерева!)
CopyBaseState(m);
m.fMaxFeatures := fMaxFeatures;
// --- классы
m.fClassCount := fClassCount;
@ -3825,8 +3831,6 @@ begin
CopyBaseState(m);
m.fLeafL2 := fLeafL2;
Result := m;
end;
@ -4099,7 +4103,15 @@ begin
fRandomSeed
);
// --- seed ---
rf.fRandomSeed := fRandomSeed;
rf.fUserProvidedSeed := fUserProvidedSeed;
if fUserProvidedSeed then
rf.fRng := new System.Random(fRandomSeed)
else
rf.fRng := new System.Random;
rf.fFitted := fFitted;
rf.fFeatureCount := fFeatureCount;
@ -4439,9 +4451,16 @@ begin
fRandomSeed
);
// --- seed ---
rf.fRandomSeed := fRandomSeed;
rf.fUserProvidedSeed := fUserProvidedSeed;
rf.fFitted := fFitted;
if fUserProvidedSeed then
rf.fRng := new System.Random(fRandomSeed)
else
rf.fRng := new System.Random;
rf.fFitted := fFitted;
rf.fFeatureCount := fFeatureCount;
// --- classes ---
@ -4467,7 +4486,7 @@ begin
SetLength(rf.fTrees, fTrees.Length);
for var i := 0 to fTrees.Length - 1 do
rf.fTrees[i] := DecisionTreeClassifier(fTrees[i].Clone);
end;
end;
Result := rf;
end;
@ -5301,31 +5320,35 @@ begin
fMinSamplesSplit,
fMinSamplesLeaf,
fSubsample,
seed := fRandomSeed
fLoss,
fHuberDelta,
fEarlyStoppingPatience,
fQuantileAlpha,
fLeafL2,
fUseOOBEarlyStopping,
fRandomSeed
);
// --- seed policy ---
// --- seed ---
copy.fRandomSeed := fRandomSeed;
copy.fUserProvidedSeed := fUserProvidedSeed;
if fUserProvidedSeed then
copy.fRng := new System.Random(fRandomSeed)
else
copy.fRng := new System.Random;
// --- basic state ---
copy.fInitValue := fInitValue;
copy.fFeatureCount := fFeatureCount;
copy.fFitted := fFitted;
// --- best iteration state ---
// --- best iteration ---
copy.fBestIteration := fBestIteration;
copy.fBestTrainLoss := fBestTrainLoss;
copy.fBestScoreLoss := fBestScoreLoss;
// --- loss configuration ---
copy.fLoss := fLoss;
copy.fHuberDelta := fHuberDelta;
copy.fQuantileAlpha := fQuantileAlpha;
copy.fLeafL2 := fLeafL2;
copy.fEarlyStoppingPatience := fEarlyStoppingPatience;
copy.fUseOOBEarlyStopping := fUseOOBEarlyStopping;
// --- deep copy histories ---
// --- histories ---
copy.fTrainLossHistory.Clear;
copy.fTrainLossHistory.AddRange(fTrainLossHistory);
@ -5335,7 +5358,7 @@ begin
copy.fOOBLossHistory.Clear;
copy.fOOBLossHistory.AddRange(fOOBLossHistory);
// --- deep copy trees ---
// --- estimators ---
copy.fEstimators.Clear;
foreach var tree in fEstimators do
copy.fEstimators.Add(tree.Clone as DecisionTreeRegressor);
@ -6174,9 +6197,15 @@ begin
fRandomSeed
);
// --- seed policy ---
// --- seed ---
model.fRandomSeed := fRandomSeed;
model.fUserProvidedSeed := fUserProvidedSeed;
if fUserProvidedSeed then
model.fRng := new System.Random(fRandomSeed)
else
model.fRng := new System.Random;
// --- fitted state ---
model.fFitted := fFitted;
model.fFeatureCount := fFeatureCount;
@ -6197,26 +6226,19 @@ begin
model.fClassIndex.Add(kv.Key, kv.Value);
end;
// --- estimators (deep copy) ---
// --- estimators ---
foreach var trees in fEstimators do
begin
var newTrees := new DecisionTreeRegressor[Length(trees)];
for var cls := 0 to Length(trees) - 1 do
newTrees[cls] := trees[cls].Clone as DecisionTreeRegressor;
model.fEstimators.Add(newTrees);
end;
// --- histories ---
foreach var v in fTrainLossHistory do
model.fTrainLossHistory.Add(v);
foreach var v in fValLossHistory do
model.fValLossHistory.Add(v);
foreach var v in fOOBLossHistory do
model.fOOBLossHistory.Add(v);
foreach var v in fTrainLossHistory do model.fTrainLossHistory.Add(v);
foreach var v in fValLossHistory do model.fValLossHistory.Add(v);
foreach var v in fOOBLossHistory do model.fOOBLossHistory.Add(v);
model.fBestIteration := fBestIteration;
model.fBestScoreLoss := fBestScoreLoss;
@ -6938,8 +6960,17 @@ begin
fMaxIter := maxIter;
fTol := tol;
fNInit := nInit;
fSeed := seed;
// --- seed (единый стиль)
fRandomSeed := seed;
fUserProvidedSeed := seed >= 0;
if fUserProvidedSeed then
fRng := new System.Random(seed)
else
fRng := new System.Random;
// --- state
fFitted := False;
fFeatureCount := 0;
@ -7077,11 +7108,8 @@ begin
fFeatureCount := p;
var actualSeed :=
if fSeed >= 0 then fSeed
else System.Environment.TickCount and integer.MaxValue;
var rndBase := new System.Random(actualSeed);
// --- базовый RNG (уже создан в конструкторе)
var rndBase := fRng;
var bestInertia := 1e308;
var bestCenters: Matrix := nil;
@ -7090,7 +7118,8 @@ begin
for var run := 1 to fNInit do
begin
var runSeed := rndBase.Next;
// --- независимый RNG для каждого запуска
var runSeed := rndBase.Next(integer.MaxValue);
var rnd := new System.Random(runSeed);
var (centers, inertia, iters, converged) := RunSingle(X, rnd);
@ -7109,7 +7138,7 @@ begin
fIterations := bestIterations;
fHasConverged := bestConverged;
fFitted := True;
Result := Self;
end;
@ -7188,25 +7217,28 @@ begin
fMaxIter,
fTol,
fNInit,
fSeed
fRandomSeed
);
// --- seed ---
km.fRandomSeed := fRandomSeed;
km.fUserProvidedSeed := fUserProvidedSeed;
if fUserProvidedSeed then
km.fRng := new System.Random(fRandomSeed)
else
km.fRng := new System.Random;
// --- state ---
km.fFitted := fFitted;
km.fFeatureCount := fFeatureCount;
km.fInertia := fInertia;
km.fIterations := fIterations;
km.fHasConverged := fHasConverged;
// --- centers ---
if fCenters <> nil then
begin
var k := fCenters.RowCount;
var p := fCenters.ColCount;
km.fCenters := new Matrix(k, p);
for var i := 0 to k - 1 do
for var j := 0 to p - 1 do
km.fCenters[i,j] := fCenters[i,j];
end;
km.fCenters := fCenters.Clone;
Result := km;
end;
@ -7339,25 +7371,6 @@ begin
Result := Self;
end;
function DBSCAN.Predict(X: Matrix): Vector;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if X = nil then
ArgumentNullError(ER_ARG_NULL, 'X');
// честная защита: Predict работает только для той же выборки
if X.RowCount <> Length(fLabels) then
ArgumentError(ER_DBSCAN_PREDICT_ONLY_TRAIN_DATA);
var n := Length(fLabels);
Result := new Vector(n);
for var i := 0 to n - 1 do
Result[i] := fLabels[i];
end;
function DBSCAN.PredictLabels(X: Matrix): array of integer;
begin
if X = nil then

View file

@ -1,4 +1,10 @@
unit PlotML;
/// Модуль визуализации на базе InteractiveDataDisplay (WPF).
/// ВАЖНО:
/// Требует Windows и WPF.
/// Использует GAC-сборку InteractiveDataDisplay.WPF.
/// Не поддерживается на Linux/macOS и в .NET Core без дополнительной настройки.
/// Рекомендуется использовать только в desktop-сценариях.
unit PlotML;
{$reference %GAC%\InteractiveDataDisplay.WPF.dll}
{$reference 'PresentationFramework.dll'}

View file

@ -91,7 +91,7 @@ type
property ColumnName: string read col;
end;
ImputeStrategy = (isMean, isConstant);
ImputeStrategy = (isMean, isConstant, isMedian);
/// Заполняет пропущенные значения (NA) в числовых столбцах
/// Поддерживает стратегии isMean и isConstant
@ -102,6 +102,7 @@ type
strategy: ImputeStrategy;
constants: array of object;
means: array of real;
medians: array of real;
fitted: boolean;
public
/// Создаёт Imputer с заполнением средним значением
@ -189,8 +190,10 @@ const
'Imputer(constant): value type mismatch for column "{0}"';
ER_ONEHOT_EMPTY_COLUMN =
'Столбец "{0}" не содержит категориальных значений!!Column "{0}" contains no categorical values';
ER_COLUMN_NOT_FOUND =
'Столбец "{0}" не найден!!Column "{0}" not found';
ER_IMPUTER_CONSTANTS_INVALID =
'Массив констант не задан или имеет неверный размер!!Constants array is null or has invalid length';
ER_IMPUTER_STRATEGY_NOT_SUPPORTED =
'Стратегия импутации {0} не поддерживается!!Imputation strategy {0} is not supported';
//-----------------------------
// LabelEncoder
@ -288,7 +291,13 @@ end;
function OneHotEncoder.Fit(df: DataFrame): IPreprocessor;
begin
if df = nil then
ArgumentNullError(ER_ARG_NULL, 'df');
var idx := df.Schema.IndexOf(col);
if idx < 0 then
Error(ER_COLUMN_NOT_FOUND, col);
if df.Schema.ColumnTypeAt(idx) <> ColumnType.ctStr then
Error(ER_ONEHOT_NOT_STRING, col);
@ -299,8 +308,11 @@ begin
while cur.MoveNext do
begin
if not cur.IsValid(idx) then continue;
var s := cur.Str(idx);
if not indexByValue.ContainsKey(s) then
var dummy: integer;
if not indexByValue.TryGetValue(s, dummy) then
begin
indexByValue[s] := values.Count;
values.Add(s);
@ -380,9 +392,6 @@ end;
constructor Imputer.Create(strategy: ImputeStrategy; params columns: array of string);
begin
if strategy <> isMean then
ArgumentError(ER_IMPUTER_INVALID_STRATEGY_MEAN);
if (columns = nil) or (columns.Length = 0) then
ArgumentError(ER_IMPUTER_NO_COLUMNS);
@ -415,35 +424,79 @@ end;
function Imputer.Fit(df: DataFrame): IPreprocessor;
begin
if strategy = isMean then
begin
SetLength(means, cols.Length);
for var i := 0 to cols.Length - 1 do
case strategy of
isMean:
begin
var name := cols[i];
var idx := df.Schema.IndexOf(name);
var ct := df.Schema.ColumnTypeAt(idx);
SetLength(means, cols.Length);
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
for var i := 0 to cols.Length - 1 do
begin
var name := cols[i];
var idx := df.Schema.IndexOf(name);
var ct := df.Schema.ColumnTypeAt(idx);
var sum := 0.0;
var cnt := 0;
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
var cur := df.GetCursor;
while cur.MoveNext do
if cur.IsValid(idx) then
begin
sum += cur.Float(idx);
cnt += 1;
end;
var sum := 0.0;
var cnt := 0;
if cnt = 0 then
Error(ER_IMPUTER_NO_VALID_VALUES, name);
var cur := df.GetCursor;
while cur.MoveNext do
if cur.IsValid(idx) then
begin
sum += cur.Float(idx);
cnt += 1;
end;
means[i] := sum / cnt;
if cnt = 0 then
Error(ER_IMPUTER_NO_VALID_VALUES, name);
means[i] := sum / cnt;
end;
end;
isConstant:
begin
// ничего делать не нужно
end;
isMedian:
begin
SetLength(medians, cols.Length);
for var i := 0 to cols.Length - 1 do
begin
var name := cols[i];
var idx := df.Schema.IndexOf(name);
var ct := df.Schema.ColumnTypeAt(idx);
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
// --- собираем значения
var values := new List<real>;
var cur := df.GetCursor;
while cur.MoveNext do
if cur.IsValid(idx) then
values.Add(cur.Float(idx));
if values.Count = 0 then
Error(ER_IMPUTER_NO_VALID_VALUES, name);
// --- сортируем
values.Sort;
// --- медиана
var n := values.Count;
if n mod 2 = 1 then
medians[i] := values[n div 2]
else
medians[i] := (values[n div 2 - 1] + values[n div 2]) / 2.0;
end;
end;
end;
fitted := true;
@ -455,6 +508,11 @@ begin
if not fitted then
NotFittedError(ER_FIT_NOT_CALLED);
// --- проверка constants
if (strategy = isConstant) and
((constants = nil) or (constants.Length <> cols.Length)) then
Error(ER_IMPUTER_CONSTANTS_INVALID);
var res := df;
for var i := 0 to cols.Length - 1 do
@ -466,50 +524,65 @@ begin
if not (ct in [ColumnType.ctInt, ColumnType.ctFloat]) then
Error(ER_IMPUTER_COLUMN_NOT_NUMERIC, name);
if strategy = isMean then
begin
var m := means[i];
res := res.ReplaceColumnFloat(
name,
c -> (if c.IsValid(idx) then c.Float(idx) else m)
);
end
else
begin
var v := constants[i];
if v = nil then
Error(ER_IMPUTER_CONSTANT_VALUE_NULL, name);
if ct = ColumnType.ctInt then
case strategy of
isMean:
begin
var k: integer;
try
k := integer(v);
except
on e: Exception do
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end;
res := res.ReplaceColumnInt(
name,
c -> (if c.IsValid(idx) then c.Int(idx) else k)
);
end
else
begin
var r: real;
try
r := real(v);
except
on e: Exception do
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end;
var m := means[i];
res := res.ReplaceColumnFloat(
name,
c -> (if c.IsValid(idx) then c.Float(idx) else r)
c -> (if c.IsValid(idx) then c.Float(idx) else m)
);
end;
isConstant:
begin
var v := constants[i];
if v = nil then
Error(ER_IMPUTER_CONSTANT_VALUE_NULL, name);
if ct = ColumnType.ctInt then
begin
var k: integer;
try
k := integer(v);
except
on e: Exception do
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end;
res := res.ReplaceColumnInt(
name,
c -> (if c.IsValid(idx) then c.Int(idx) else k)
);
end
else // ctFloat
begin
var r: real;
try
r := real(v);
except
on e: Exception do
Error(ER_IMPUTER_CONSTANT_TYPE_MISMATCH, name);
end;
res := res.ReplaceColumnFloat(
name,
c -> (if c.IsValid(idx) then c.Float(idx) else r)
);
end;
end;
isMedian:
begin
var m := medians[i];
res := res.ReplaceColumnFloat(
name,
c -> (if c.IsValid(idx) then c.Float(idx) else m)
);
end;
else
Error(ER_IMPUTER_STRATEGY_NOT_SUPPORTED, strategy);
end;
end;
@ -530,9 +603,23 @@ begin
isMean:
Result := 'Imputer(strategy=mean, columns=' + colsStr + ')';
isMedian:
Result := 'Imputer(strategy=median, columns=' + colsStr + ')';
isConstant:
begin
var valStr :=
if (constants <> nil) and (constants.Length > 0) and (constants[0] <> nil) then
constants[0].ToString
else
'null';
Result := 'Imputer(strategy=constant, value=' +
constants[0].ToString + ', columns=' + colsStr + ')';
valStr + ', columns=' + colsStr + ')';
end;
else
Result := 'Imputer(strategy=unknown, columns=' + colsStr + ')';
end;
end;

View file

@ -56,22 +56,26 @@ type
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;
maximize: boolean := True;
seed: integer := -1
): (real, real, T); where T: class,ISupervisedModel;
/// modelFactory функция создания модели по значению параметра (P -> T).
/// paramValues набор тестируемых значений гиперпараметра типа P.
/// X, y обучающие данные.
/// k число фолдов в кросс-валидации.
/// metric функция оценки качества (yTrue, yPred) real.
/// maximize если true, максимизируется метрика; иначе минимизируется.
/// 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;
seed: integer := -1
): (P, real, T); where T: class, ISupervisedModel;
end;
implementation
@ -171,15 +175,15 @@ begin
if (k < 2) or (k > n) then
ArgumentError(ER_K_INVALID, k, n);
var actualSeed := if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
var rnd := new System.Random(actualSeed);
// --- 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. Перемешивание через стандартный Shuffle
// --- 2. Перемешивание
idx.Shuffle(rnd);
var baseSize := n div k;
@ -224,10 +228,9 @@ begin
if (k < 2) or (k > n) then
ArgumentError(ER_K_INVALID_STRATIFIED, k, n);
var actualSeed := if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;
var rnd := new System.Random(actualSeed);
var rnd :=
if seed >= 0 then new System.Random(seed)
else new System.Random;
// --- 1. Индексы по классам
var classMap := new Dictionary<integer, List<integer>>();
@ -235,7 +238,7 @@ begin
for var i := 0 to n - 1 do
begin
var v := y[i];
var cls := integer(v);
var cls := Round(v);
if Abs(v - cls) > 1e-12 then
ArgumentError(ER_STRATIFIED_LABELS_INVALID);
@ -328,8 +331,12 @@ begin
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, seed) do
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);
@ -398,7 +405,11 @@ begin
var folds := 0;
var p := X.ColCount;
foreach var (trainIdx, testIdx) in StratifiedKFold(y, k, seed) do
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);
@ -441,16 +452,16 @@ end;
// GridSearch
//-----------------------------
class function GridSearch.Search<T>(
modelFactory: real -> T;
paramValues: array of real;
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;
seed: integer
): (real, real, T); where T: class,ISupervisedModel;
): (P, real, T); where T: class, ISupervisedModel;
begin
if modelFactory = nil then
ArgumentNullError(ER_ARG_NULL, 'modelFactory');
@ -474,15 +485,20 @@ begin
DimensionError(ER_DIM_MISMATCH, X.RowCount, y.Length);
var bestParam := paramValues[0];
var bestScore :=
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 := Validation.CrossValidate(model, X, y, k, metric, seed);
var avgScore := Validation.CrossValidate(model, X, y, k, metric, baseSeed);
if double.IsNaN(avgScore) or double.IsInfinity(avgScore) then
ArgumentError(ER_INVALID_VALUE, 'avgScore');
@ -501,7 +517,7 @@ begin
var bestModel := modelFactory(bestParam);
if bestModel = nil then
ArgumentError(ER_MODEL_NULL);
bestModel := bestModel.Fit(X, y) as T;
Result := (bestParam, bestScore, bestModel);