diff --git a/bin/Lib/DataAdapters.pas b/bin/Lib/DataAdapters.pas index 15ec19a1c..22c37a7e3 100644 --- a/bin/Lib/DataAdapters.pas +++ b/bin/Lib/DataAdapters.pas @@ -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; diff --git a/bin/Lib/DataFrameABC.pas b/bin/Lib/DataFrameABC.pas index 1ee86566f..b1216ccff 100644 --- a/bin/Lib/DataFrameABC.pas +++ b/bin/Lib/DataFrameABC.pas @@ -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 = diff --git a/bin/Lib/InspectionML.pas b/bin/Lib/InspectionML.pas index acdecf5c1..1af427028 100644 --- a/bin/Lib/InspectionML.pas +++ b/bin/Lib/InspectionML.pas @@ -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; - // Fisher–Yates 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; diff --git a/bin/Lib/LinearAlgebraML.pas b/bin/Lib/LinearAlgebraML.pas index 8801a17c4..96772069b 100644 --- a/bin/Lib/LinearAlgebraML.pas +++ b/bin/Lib/LinearAlgebraML.pas @@ -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; diff --git a/bin/Lib/MLDatasets.pas b/bin/Lib/MLDatasets.pas index fe1ad94f7..9a98e35ce 100644 --- a/bin/Lib/MLDatasets.pas +++ b/bin/Lib/MLDatasets.pas @@ -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; ValueLabels: Dictionary>; 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; diff --git a/bin/Lib/MLModelsABC.pas b/bin/Lib/MLModelsABC.pas index 181549a2d..5bb7cb199 100644 --- a/bin/Lib/MLModelsABC.pas +++ b/bin/Lib/MLModelsABC.pas @@ -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 diff --git a/bin/Lib/PlotML.pas b/bin/Lib/PlotML.pas index cfaaf6f0d..a545f3e26 100644 --- a/bin/Lib/PlotML.pas +++ b/bin/Lib/PlotML.pas @@ -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'} diff --git a/bin/Lib/PreprocessorABC.pas b/bin/Lib/PreprocessorABC.pas index b4e891031..8bba06df3 100644 --- a/bin/Lib/PreprocessorABC.pas +++ b/bin/Lib/PreprocessorABC.pas @@ -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; + + 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; diff --git a/bin/Lib/ValidationML.pas b/bin/Lib/ValidationML.pas index e9d675820..1d96ccbe7 100644 --- a/bin/Lib/ValidationML.pas +++ b/bin/Lib/ValidationML.pas @@ -56,22 +56,26 @@ type GridSearch = static class public /// Выполняет подбор гиперпараметра по заданной сетке значений. - /// modelFactory — функция создания модели по значению параметра. - /// paramValues — набор тестируемых значений гиперпараметра. - /// X, y — обучающие данные. - /// k — число фолдов в кросс-валидации. - /// metric — функция оценки качества (yTrue, yPred) → real. - /// Возвращает кортеж (лучший параметр, лучшее среднее значение метрики, - /// модель, обученная на всём датасете с лучшим параметром). - class function Search( - 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( + 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>(); @@ -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( - modelFactory: real -> T; - paramValues: array of real; +class function GridSearch.Search( + 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);