Правки ML

This commit is contained in:
Mikhalkovich Stanislav 2026-04-19 21:07:00 +03:00
parent 13a1804dda
commit 10c929fa1c
16 changed files with 662 additions and 237 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 = "3805";
public const string Revision = "3808";
public const string MainVersion = Major + "." + Minor;
public const string FullVersion = Major + "." + Minor + "." + Build + "." + Revision;

View file

@ -1,4 +1,4 @@
%MINOR%=11
%REVISION%=3805
%COREVERSION%=1
%REVISION%=3808
%MINOR%=11
%MAJOR%=3

View file

@ -1 +1 @@
3.11.1.3805
3.11.1.3808

View file

@ -1 +1 @@
!define VERSION '3.11.1.3805'
!define VERSION '3.11.1.3808'

View file

@ -25,9 +25,6 @@ const
'Столбец "{0}" должен быть категориальным для EncodeLabels!!Column "{0}" must be categorical for EncodeLabels';
ER_ENCODELABELS_UNSUPPORTED_TYPE =
'Неподдерживаемый тип столбца "{0}" для EncodeLabels!!Unsupported column type "{0}" for EncodeLabels';
ER_UNKNOWN_CLASS_IN_TRANSFORM =
'Неизвестное значение класса "{0}" при преобразовании меток!!Unknown class value "{0}" in TransformLabels';
function ToMatrix(Self: DataFrame; colNames: array of string): Matrix; extensionmethod;
begin
@ -180,49 +177,25 @@ begin
if not Self.IsCategorical(target) then
ArgumentError(ER_ENCODELABELS_NOT_CATEGORICAL, target);
// --- строим mapping
var map := new Dictionary<string, integer>;
for var i := 0 to classes.Length - 1 do
map[classes[i]] := i;
case Self.GetColumnType(target) of
ColumnType.ctStr:
begin
var data := Self.GetStrColumn(target).ToArray;
var res := new integer[data.Length];
for var i := 0 to data.Length - 1 do
begin
var lbl := data[i];
if not map.ContainsKey(lbl) then
Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, lbl);
res[i] := map[lbl];
end;
Result := res;
Result := TransformLabels(data, classes);
end;
ColumnType.ctInt:
begin
var data := Self.GetIntColumn(target).ToArray;
var res := new integer[data.Length];
var strData := new string[data.Length];
for var i := 0 to data.Length - 1 do
begin
var lbl := data[i].ToString;
if not map.ContainsKey(lbl) then
Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, lbl);
res[i] := map[lbl];
end;
Result := res;
strData[i] := data[i].ToString;
Result := TransformLabels(strData, classes);
end;
else
ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target);
end;
@ -252,25 +225,9 @@ begin
if Self.GetColumnType(target) <> ColumnType.ctInt then
ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, target);
// --- mapping: значение индекс
var map := new Dictionary<integer, integer>;
for var i := 0 to classes.Length - 1 do
map[classes[i]] := i;
var data := Self.GetIntColumn(target).ToArray;
var res := new integer[data.Length];
for var i := 0 to data.Length - 1 do
begin
var v := data[i];
if not map.ContainsKey(v) then
Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, v);
res[i] := map[v];
end;
Result := res;
Result := TransformLabelsInt(data, classes);
end;
/// Кодирует значения целочисленного категориального столбца DataFrame

View file

@ -6464,9 +6464,6 @@ static function CSVLoader.Load(filename: string;
begin
var enc := encoding;
//if enc = nil then
// enc := System.Text.Encoding.Utf8;
if enc = nil then
enc := DetectEncoding(filename);

View file

@ -72,7 +72,10 @@ type
rightPrefix: string
): DataFrameSchema;
{ --- debug --- }
{ --- DEBUG ONLY ---
Проверка внутренних инвариантов схемы.
Использует Assert и выполняется только в debug-сборке.
Не предназначена для обработки пользовательских ошибок. }
procedure AssertConsistent;
end;

View file

@ -90,11 +90,6 @@ begin
var resultVec := new Vector(p);
// Базовый seed для всех параметров.
// Все модели оцениваются на одинаковых фолдах,
// что обеспечивает корректное сравнение.
// При seed = -1 разбиение случайное, но фиксируется
// один раз для всего GridSearch.
var baseSeed :=
if seed >= 0 then seed
else System.Environment.TickCount and integer.MaxValue;

View file

@ -39,6 +39,9 @@ type
function Clone: Vector;
function Normalize: Vector;
function Normalized: Vector;
function ToArray: array of real;
function ToIntArray: array of integer;
@ -69,7 +72,9 @@ type
// ---------- Основные методы ----------
function Sum: real;
/// Среднее. Синоним Mean
function Average: real;
/// Среднее
function Mean: real;
function Norm2: real;
function Norm: real;
@ -295,8 +300,6 @@ const
'Вектор пуст!!Vector is empty';
ER_VECTOR_DIVIDE_BY_ZERO =
'Деление на ноль при делении вектора на скаляр!!Division by zero in Vector / scalar';
ER_DIM_MISMATCH =
'Несоответствие размерностей: {0} и {1}!!Dimension mismatch: {0} and {1}';
ER_MATRIX_SIZE_NEGATIVE =
'Размеры матрицы должны быть неотрицательными!!Matrix size must be non-negative';
ER_MATRIX_SIZE_MISMATCH =
@ -384,6 +387,25 @@ begin
Result := new Vector(fdata);
end;
function Vector.Normalize: Vector;
begin
var sum := 0.0;
for var i := 0 to Length - 1 do
sum += Self[i];
if sum > 0 then
for var i := 0 to Length - 1 do
Self[i] /= sum;
Result := Self;
end;
function Vector.Normalized: Vector;
begin
Result := Self.Clone;
Result.Normalize;
end;
static procedure Vector.CheckSameLength(a, b: Vector);
begin
if a.Length <> b.Length then
@ -428,6 +450,7 @@ static function Vector.operator /(v: Vector; alpha: real): Vector;
begin
if alpha = 0.0 then
raise new System.DivideByZeroException(GetTranslation(ER_VECTOR_DIVIDE_BY_ZERO));
Result := new Vector(v.Length);
var inv := 1.0 / alpha;
for var i := 0 to v.Length - 1 do
@ -479,12 +502,10 @@ begin
Result := s;
end;
// Алиас Mean
function Vector.Average: real;
begin
if Length = 0 then
ArgumentError(ER_VECTOR_EMPTY);
Result := Sum / Length;
Result := Mean;
end;
function Vector.Mean: real;
@ -541,6 +562,7 @@ constructor Matrix.Create(r, c: integer);
begin
if (r < 0) or (c < 0) then
ArgumentOutOfRangeError(ER_MATRIX_SIZE_NEGATIVE);
fdata := new real[r, c];
end;
@ -580,8 +602,10 @@ end;
function Matrix.ColumnMeans: Vector;
begin
var n := RowCount;
if n = 0 then
exit( new Vector(ColCount) );
if n = 0 then
Error(ER_EMPTY_MATRIX);
Result := ColumnSums / n;
end;
@ -598,20 +622,28 @@ end;
function Matrix.RowMeans: Vector;
begin
var n := RowCount;
var p := ColCount;
if n = 0 then
Error(ER_EMPTY_MATRIX);
if p = 0 then
exit (new Vector(RowCount));
Error(ER_EMPTY_MATRIX);
Result := RowSums / p;
end;
function Matrix.ColumnVariances: Vector;
begin
var n := RowCount;
if n = 0 then
exit (new Vector(ColCount));
Error(ER_EMPTY_MATRIX);
var means := ColumnMeans;
var p := ColCount;
Result := new Vector(p);
for var j := 0 to p - 1 do
@ -631,12 +663,17 @@ end;
function Matrix.RowVariances: Vector;
begin
var n := RowCount;
var p := ColCount;
if n = 0 then
Error(ER_EMPTY_MATRIX);
if p = 0 then
exit (new Vector(RowCount));
Error(ER_EMPTY_MATRIX);
var means := RowMeans;
var n := RowCount;
Result := new Vector(n);
for var i := 0 to n - 1 do
@ -658,10 +695,10 @@ function Matrix.ColumnMins: Vector;
begin
var n := RowCount;
var p := ColCount;
if n = 0 then
exit(new Vector(p));
if n = 0 then
Error(ER_EMPTY_MATRIX);
Result := new Vector(p);
for var j := 0 to p - 1 do
@ -678,10 +715,10 @@ function Matrix.ColumnMaxs: Vector;
begin
var n := RowCount;
var p := ColCount;
if n = 0 then
exit(new Vector(p));
if n = 0 then
Error(ER_EMPTY_MATRIX);
Result := new Vector(p);
for var j := 0 to p - 1 do
@ -700,8 +737,11 @@ begin
var p := ColCount;
if n = 0 then
exit(new Vector(p));
Error(ER_EMPTY_MATRIX);
if p = 0 then
Error(ER_EMPTY_MATRIX);
Result := new Vector(n);
for var i := 0 to n - 1 do
@ -720,8 +760,11 @@ begin
var p := ColCount;
if n = 0 then
exit(new Vector(p));
Error(ER_EMPTY_MATRIX);
if p = 0 then
Error(ER_EMPTY_MATRIX);
Result := new Vector(n);
for var i := 0 to n - 1 do
@ -736,6 +779,15 @@ end;
function Matrix.RowArgMin(i: integer): integer;
begin
if RowCount = 0 then
Error(ER_EMPTY_MATRIX);
if ColCount = 0 then
Error(ER_EMPTY_MATRIX);
if (i < 0) or (i >= RowCount) then
ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount);
var minVal := fdata[i,0];
var arg := 0;
@ -756,6 +808,15 @@ end;
function Matrix.RowArgMax(i: integer): integer;
begin
if RowCount = 0 then
Error(ER_EMPTY_MATRIX);
if ColCount = 0 then
Error(ER_EMPTY_MATRIX);
if (i < 0) or (i >= RowCount) then
ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount);
var maxVal := fdata[i,0];
var arg := 0;
@ -776,6 +837,12 @@ end;
function Matrix.RowSum(i: integer): real;
begin
if RowCount = 0 then
Error(ER_EMPTY_MATRIX);
if (i < 0) or (i >= RowCount) then
ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount);
var sum := 0.0;
for var j := 0 to ColCount - 1 do
@ -810,6 +877,9 @@ end;
function Matrix.ColumnArgMin(j: integer): integer;
begin
if (j < 0) or (j >= ColCount) then
ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, ColCount);
var n := RowCount;
if n = 0 then
@ -835,6 +905,9 @@ end;
function Matrix.ColumnArgMax(j: integer): integer;
begin
if (j < 0) or (j >= ColCount) then
ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, ColCount);
var n := RowCount;
if n = 0 then
@ -895,6 +968,9 @@ end;
function Matrix.FrobeniusNorm: real;
begin
if RowCount = 0 then
Error(ER_EMPTY_MATRIX);
var s := 0.0;
for var i := 0 to RowCount - 1 do
for var j := 0 to ColCount - 1 do
@ -905,6 +981,10 @@ end;
procedure Matrix.AddScaledIdentity(lambda: real);
begin
var n := RowCount;
if RowCount <> ColCount then
ArgumentError(ER_MATRIX_NOT_SQUARE);
for var i := 0 to n - 1 do
fdata[i, i] += lambda;
end;
@ -1060,6 +1140,7 @@ function Matrix.GetRow(i: integer): Vector;
begin
if (i < 0) or (i >= RowCount) then
ArgumentOutOfRangeError(ER_ROW_INDEX_OUT_OF_RANGE, i, RowCount);
Result := new Vector(ColCount);
for var j := 0 to ColCount - 1 do
Result[j] := fdata[i, j];
@ -1067,8 +1148,14 @@ end;
function Matrix.TakeRows(indices: array of integer): Matrix;
begin
if indices = nil then
ArgumentNullError(ER_ARG_NULL, 'indices');
var n := indices.Length;
var p := ColCount;
if RowCount = 0 then
Error(ER_EMPTY_MATRIX);
var res := new Matrix(n, p);
@ -1124,6 +1211,7 @@ function Matrix.GetCol(j: integer): Vector;
begin
if (j < 0) or (j >= ColCount) then
ArgumentOutOfRangeError(ER_COL_INDEX_OUT_OF_RANGE, j, ColCount);
Result := new Vector(RowCount);
for var i := 0 to RowCount - 1 do
Result[i] := fdata[i, j];
@ -1141,6 +1229,12 @@ end;
static function Matrix.OuterProduct(a, b: Vector): Matrix;
begin
if a = nil then
ArgumentNullError(ER_ARG_NULL, 'a');
if b = nil then
ArgumentNullError(ER_ARG_NULL, 'b');
var m := a.Length;
var n := b.Length;
Result := new Matrix(m, n);

View file

@ -295,10 +295,10 @@ const
ER_PARAM_BETWEEN_01 =
'Параметр {0} должен быть в диапазоне (0,1)!!Parameter {0} must be in range (0,1)';
ER_DATASET_NO_TARGET =
'У датасета нет целевой переменной (задача кластеризации).' +
'У датасета нет целевой переменной (задача кластеризации).!!' +
'Dataset has no target variable (clustering task).';
ER_DATASET_TARGET_NOT_FOUND =
'Целевая переменная "{0}" не найдена в таблице.' +
'Целевая переменная "{0}" не найдена в таблице.!!' +
'Target column "{0}" not found in DataFrame';
ER_DATASET_META_NOT_FOUND =
'Файл метаданных датасета "{0}" не найден!!Dataset meta file "{0}" not found';
@ -324,8 +324,6 @@ const
'Параметр {0} должен быть в диапазоне (0, 1)!!Parameter {0} must be in range (0, 1)';
ER_PARAM_LT =
'Параметр {0} должен быть меньше допустимого значения!!Parameter {0} must be less than allowed value';
ER_TEST_RATIO_INVALID =
'Некорректное значение testRatio (должно быть между 0 и 1)!!Invalid testRatio (must be between 0 and 1)';
ER_GROUPBY_UNSUPPORTED_KEY_TYPE =
'Неподдерживаемый тип ключа для группировки!!Unsupported key type for grouping';
ER_STRATIFIED_ONLY_FOR_CLASSIFICATION =
@ -334,7 +332,9 @@ const
'Слишком малое значение classBalance: {0}. Минимально допустимое значение — 1e-3!!classBalance is too small: {0}. Minimum allowed value is 1e-3';
ER_UNSUPPORTED_TARGET_TYPE =
'Неподдерживаемый тип целевого столбца: {0}!!Unsupported target column type: {0}';
ER_ENCODELABELS_UNSUPPORTED_TYPE =
'Неподдерживаемый тип столбца для кодирования меток: {0}!!' +
'Unsupported column type for label encoding: {0}';
C_DATASET = 'Датасет: {0}!!Dataset: {0}';
C_DESCRIPTION = 'Описание:!!Description:';
@ -613,15 +613,39 @@ begin
if Task <> TaskType.Classification then
ArgumentError(ER_VALUECOUNTS_ONLY_CLASSIFICATION);
var labels := Data.GetStrColumn(Target);
var dict := new Dictionary<string,integer>;
foreach var v in labels do
if dict.ContainsKey(v) then
dict[v] += 1
case Data.GetColumnType(Target) of
ColumnType.ctStr:
begin
var labels := Data.GetStrColumn(Target);
foreach var v in labels do
if dict.ContainsKey(v) then
dict[v] += 1
else
dict[v] := 1;
end;
ColumnType.ctInt:
begin
var labels := Data.GetIntColumn(Target);
foreach var v in labels do
begin
var s := v.ToString;
if dict.ContainsKey(s) then
dict[s] += 1
else
dict[s] := 1;
end;
end;
else
dict[v] := 1;
ArgumentError(ER_ENCODELABELS_UNSUPPORTED_TYPE, Target);
end;
Result := dict;
end;
@ -879,7 +903,7 @@ begin
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'nInformative');
if nInformative > nFeatures then
nInformative := nFeatures;
ArgumentOutOfRangeError(ER_PARAM_LE, 'nInformative');
if noise < 0 then
ArgumentOutOfRangeError(ER_PARAM_GE_ZERO, 'noise');
@ -1065,7 +1089,26 @@ begin
X[row,1] := r * Sin(t) + noise * Normal(rnd);
end;
end;
// --- обработка хвоста (если n не делится на classes)
for var i := classes * perClass to n - 1 do
begin
var row := idx[i];
var c := i mod classes; // равномернее, чем rnd
var u := rnd.NextDouble;
var r := radius * u;
var t := turns * 2 * Pi * u + c * 2 * Pi / classes + noise * 0.5 * Normal(rnd);
y[row] := c;
X[row,0] := r * Cos(t) + noise * Normal(rnd);
X[row,1] := r * Sin(t) + noise * Normal(rnd);
end;
Result := (X, y);
end;

View file

@ -30,7 +30,7 @@ const
ER_Y_NULL =
'y не может быть nil!!y cannot be nil';
ER_XY_SIZE_MISMATCH =
'Размерности X и y не согласованы!!X and y size mismatch';
'Размеры X и y не совпадают: X={0}, y={1}!!X and y size mismatch: X={0}, y={1}';
ER_FEATURE_COUNT_MISMATCH =
'Число признаков не совпадает!!Feature count mismatch';
ER_NAN_IN_X =
@ -50,8 +50,13 @@ const
'Данные для предсказания содержат Infinity. Проверьте данные на выбросы.!!' +
'Prediction data contains Infinity. Please check for extreme values.';
ER_MODEL_NOT_FITTED =
'Модель "{0}" не обучена. Сначала вызовите Fit()|Model "{0}" is not fitted. Call Fit() first';
'Модель "{0}" не обучена. Сначала вызовите Fit()!!Model "{0}" is not fitted. Call Fit() first';
ER_TEST_RATIO_INVALID =
'Параметр testRatio должен быть в интервале (0,1), получено {0}!!' +
'Parameter testRatio must be in (0,1), got {0}';
ER_PREDICT_NOT_SUPPORTED =
'Модель не поддерживает операцию Predict!!' +
'Model does not support Predict operation';
type
/// Базовое исключение ML-библиотеки

View file

@ -283,6 +283,8 @@ type
fAlpha: real;
fMaxIter: integer;
fTol: real;
function GetIsFitted: boolean;
public
/// Создаёт модель Lasso-регрессии.
/// alpha коэффициент L1-регуляризации.
@ -326,7 +328,7 @@ type
/// Показывает, была ли модель обучена.
/// После вызова Fit значение становится True.
property IsFitted: boolean read fModel.fFitted;
property IsFitted: boolean read GetIsFitted;
function Name: string := Self.GetType.Name;
end;
@ -557,8 +559,10 @@ type
procedure Fit(X: Matrix; y: Vector); // y уже 0..K-1
function Predict(X: Matrix): Vector; // возвращает 0..K-1
function PredictRow(X: Matrix; row: integer): integer;
private
function GetFeatureImportances: Vector;
function CreateLeaf(y: Vector; indices: array of integer): DecisionTreeNode;
function BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode;
@ -573,9 +577,9 @@ type
var Xr: Matrix; var yr: Vector
);
property FeatureImportances: Vector read fFeatureImportances;
function MajorityClass(y: Vector; indices: array of integer): integer;
public
property FeatureImportances: Vector read GetFeatureImportances;
function PredictOne(x: Vector; node: DecisionTreeNode): integer;
function Clone: DecisionTreeCore;
@ -1566,6 +1570,9 @@ type
fMark: array of integer;
fTouched: array of integer;
fEpoch: integer;
fSampleIdx: array of integer;
fSampleSize: integer;
public
/// Создаёт классификатор kNN.
@ -1675,7 +1682,6 @@ type
fMaxIter: integer;
fTol: real;
fNInit: integer;
fSeed: integer;
fFitted: boolean;
fFeatureCount: integer;
@ -1751,8 +1757,6 @@ type
property Tol: real read fTol;
/// Количество запусков алгоритма.
property NInit: integer read fNInit;
/// Используемый seed.
property Seed: integer read fSeed;
/// Матрица центроидов (k × p).
property ClusterCenters: Matrix read fCenters;
/// Значение инерции (сумма квадратов расстояний до центроидов).
@ -2051,6 +2055,9 @@ type
/// Применяет стандартизацию к данным.
function Transform(X: Matrix): Matrix;
/// Обратная операция к Transform.
function InverseTransform(X: Matrix): Matrix;
/// Средние значения признаков, вычисленные при обучении.
property Mean: Vector read fMean;
@ -2106,6 +2113,9 @@ type
/// Применяет линейное масштабирование признаков к диапазону [0, 1].
function Transform(X: Matrix): Matrix;
/// Преобразование, обратное Transform
function InverseTransform(X: Matrix): Matrix;
/// Минимальные значения признаков, вычисленные при обучении.
property Min: Vector read fMin;
@ -2587,9 +2597,6 @@ const
ER_INTERNAL_INVALID_MODEL_CLONE =
'Внутренняя ошибка: Clone модели вернул несовместимый тип!!' +
'Internal error: model Clone returned incompatible type';
ER_PREDICT_NOT_SUPPORTED =
'Модель не поддерживает Predict для данного типа алгоритма!!' +
'Model does not support Predict for this type of algorithm';
ER_NEED_AT_LEAST_TWO_CLASSES =
'Необходимо как минимум два различных класса!!At least two distinct classes are required';
ER_UNKNOWN_CLASS_LABEL =
@ -3084,6 +3091,10 @@ begin
Result := new LassoRegression(fAlpha, fMaxIter, fTol);
end;
function LassoRegression.GetIsFitted: boolean;
begin
Result := (fModel <> nil) and fModel.IsFitted;
end;
//-----------------------------
// LogisticRegression
//-----------------------------
@ -3576,6 +3587,14 @@ begin
fRng := new System.Random(fRandomSeed);
end;
function DecisionTreeCore.GetFeatureImportances: Vector;
begin
if fRoot = nil then
NotFittedError(ER_FIT_NOT_CALLED);
Result := fFeatureImportances.Normalized;
end;
procedure DecisionTreeCore.Fit(X: Matrix; y: Vector);
begin
// --- init importance
@ -3673,7 +3692,7 @@ end;
function DecisionTreeCore.BuildNode(X: Matrix; y: Vector; indices: array of integer; depth: integer): DecisionTreeNode;
begin
// 1. stop: depth
if depth >= fMaxDepth then
if (fMaxDepth > 0) and (depth >= fMaxDepth) then
exit(CreateLeaf(y, indices));
// 2. stop: min samples
@ -4061,7 +4080,7 @@ begin
if minSamplesLeaf < 1 then
ArgumentOutOfRangeError(ER_MIN_SAMPLES_LEAF_INVALID, minSamplesLeaf);
if minSamplesLeaf >= minSamplesSplit then
if 2 * minSamplesLeaf >= minSamplesSplit then
ArgumentOutOfRangeError(
ER_MIN_LEAF_GE_SPLIT,
minSamplesLeaf, minSamplesSplit
@ -5108,6 +5127,7 @@ begin
fMinSamplesSplit,
fMinSamplesLeaf,
treeCriterion,
fClassCount,
ComputeMaxFeatures(p),
treeSeed
);
@ -5309,6 +5329,9 @@ function RandomForestClassifier.FeatureImportances: Vector;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if fTrees.Length = 0 then
Error(ER_MODEL_NOT_INITIALIZED);
var p := fTrees[0].FeatureImportances.Length;
var resultVec := new Vector(p);
@ -5318,7 +5341,7 @@ begin
resultVec *= 1.0 / fTrees.Length;
Result := resultVec;
Result := resultVec.Normalized;
end;
function RandomForestClassifier.ToString: string;
@ -5691,7 +5714,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
@ -7034,12 +7057,15 @@ begin
var sum := 0.0;
var d := fXTrain.ColCount;
var train := fXTrain; // локальная ссылка
var test := XTest;
for var j := 0 to d - 1 do
begin
var diff := fXTrain[trainRow, j] - XTest[testRow, j];
var diff := train.Data[trainRow, j] - test.Data[testRow, j];
sum += diff * diff;
end;
exit(sum);
end;
@ -7085,6 +7111,112 @@ begin
end;
end;
//-----------------------------
// topK
//-----------------------------
type TopK = class
private
fDist: array of real;
fIdx: array of integer;
fCount: integer;
fK: integer;
fWorstIdx: integer;
procedure UpdateWorst;
public
constructor Create(k: integer);
procedure Clear;
function Count: integer;
function Worst: real;
procedure Add(d: real; id: integer);
function GetIndex(i: integer): integer;
function GetDist(i: integer): real;
end;
constructor TopK.Create(k: integer);
begin
fK := k;
SetLength(fDist, k);
SetLength(fIdx, k);
fCount := 0;
fWorstIdx := 0;
end;
procedure TopK.Clear;
begin
fCount := 0;
fWorstIdx := 0;
end;
function TopK.Count: integer;
begin
Result := fCount;
end;
function TopK.Worst: real;
begin
if fCount < fK then
Result := real.PositiveInfinity
else
Result := fDist[fWorstIdx];
end;
procedure TopK.UpdateWorst;
begin
var wi := 0;
var wd := fDist[0];
for var i := 1 to fCount - 1 do
if fDist[i] > wd then
begin
wd := fDist[i];
wi := i;
end;
fWorstIdx := wi;
end;
procedure TopK.Add(d: real; id: integer);
begin
// ещё не заполнено
if fCount < fK then
begin
fDist[fCount] := d;
fIdx[fCount] := id;
fCount += 1;
if fCount = fK then
UpdateWorst;
exit;
end;
// быстрый отсев
if d >= fDist[fWorstIdx] then
exit;
// замена худшего
fDist[fWorstIdx] := d;
fIdx[fWorstIdx] := id;
UpdateWorst;
end;
function TopK.GetIndex(i: integer): integer;
begin
Result := fIdx[i];
end;
function TopK.GetDist(i: integer): real;
begin
Result := fDist[i];
end;
//-----------------------------
// KNNClassifier
//-----------------------------
@ -7123,6 +7255,15 @@ begin
// --- copy train data
fXTrain := X.Clone;
fSampleSize := Min(3000, fXTrain.RowCount); // попробуй 20004000
SetLength(fSampleIdx, fSampleSize);
var rnd := new System.Random(42);
for var i := 0 to fSampleSize - 1 do
fSampleIdx[i] := rnd.Next(fXTrain.RowCount);
// =========================================================
// ЕДИНЫЙ ENCODING
@ -7197,15 +7338,36 @@ begin
var m := X.RowCount;
var n := fXTrain.RowCount;
var p := fXTrain.ColCount;
var trainRows := fXTrain.Data.Rows;
var testRows := X.Data.Rows;
Result := new Vector(m);
for var i := 0 to m - 1 do
begin
// заполнить расстояния
// for var t := 0 to n - 1 do
// begin
// fNeighbors[t].dist := SquaredL2(t, X, i);
// fNeighbors[t].idx := t;
// end;
var rowTest := testRows[i];
for var t := 0 to n - 1 do
begin
fNeighbors[t].dist := SquaredL2(t, X, i);
begin
var rowTrain := trainRows[t];
var sum := 0.0;
for var j := 0 to p - 1 do
begin
var diff := rowTrain[j] - rowTest[j];
sum += diff * diff;
end;
fNeighbors[t].dist := sum;
fNeighbors[t].idx := t;
end;
@ -7652,18 +7814,70 @@ begin
var p := X.ColCount;
var k := fNClusters;
// --- 1. Инициализация центроидов (случайные строки X)
var idx := Arr(0..n-1);
idx.Shuffle(rnd);
// --- 1. K-Means++ инициализация
var centers := new Matrix(k, p);
for var c := 0 to k - 1 do
// первый центр случайный
var first := rnd.Next(n);
for var j := 0 to p - 1 do
centers[0,j] := X[first,j];
// расстояния до ближайшего центра
var dist1 := new real[n];
for var i := 0 to n - 1 do
dist1[i] := double.MaxValue;
// выбираем остальные центры
for var c := 1 to k - 1 do
begin
var r := idx[c];
// обновляем dist (минимальное расстояние до уже выбранных центров)
for var i := 0 to n - 1 do
begin
var d := 0.0;
for var j := 0 to p - 1 do
begin
var diff := X[i,j] - centers[c-1,j];
d += diff * diff;
end;
if d < dist1[i] then
dist1[i] := d;
end;
// сумма расстояний
var sumDist := 0.0;
for var i := 0 to n - 1 do
sumDist += dist1[i];
// если всё совпало (редкий случай)
if sumDist = 0 then
begin
var r := rnd.Next(n);
for var j := 0 to p - 1 do
centers[c,j] := X[r,j];
continue;
end;
// выбор по вероятности
var target := rnd.NextDouble * sumDist;
var acc := 0.0;
var chosen := n - 1;
for var i := 0 to n - 1 do
begin
acc += dist1[i];
if acc >= target then
begin
chosen := i;
break;
end;
end;
for var j := 0 to p - 1 do
centers[c,j] := X[r,j];
centers[c,j] := X[chosen,j];
end;
var inertia := 0.0;
@ -8489,7 +8703,34 @@ begin
for var i := 0 to n - 1 do
for var j := 0 to p - 1 do
Result[i,j] := (X[i,j] - fMean[j]) / fStd[j];
if fStd[j] <> 0 then
Result[i,j] := (X[i,j] - fMean[j]) / fStd[j]
else
Result[i,j] := 0.0;
end;
function StandardScaler.InverseTransform(X: Matrix): Matrix;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if X = nil then
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
var n := X.RowCount;
var p := X.ColCount;
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] * fStd[j] + fMean[j]
else
Result[i,j] := fMean[j];
end;
function StandardScaler.ToString: string;
@ -8577,6 +8818,34 @@ begin
end;
end;
function MinMaxScaler.InverseTransform(X: Matrix): Matrix;
begin
if not fFitted then
NotFittedError(ER_FIT_NOT_CALLED);
if X = nil then
ArgumentNullError(ER_X_NULL);
if X.ColCount <> fFeatureCount then
DimensionError(ER_FEATURE_COUNT_MISMATCH);
var n := X.RowCount;
var p := X.ColCount;
Result := new Matrix(n, p);
for var i := 0 to n - 1 do
for var j := 0 to p - 1 do
begin
var range := fMax[j] - fMin[j];
if range <> 0 then
Result[i,j] := X[i,j] * range + fMin[j]
else
Result[i,j] := fMin[j];
end;
end;
function MinMaxScaler.ToString: string;
begin
Result := 'MinMaxScaler(min=' + fRangeMin + ', max=' + fRangeMax + ')';

View file

@ -137,15 +137,8 @@ type
/// (Fit/Transform/FitTransform)
UDataPipeline = class(PipelineBase, IModel)
private
//fDataSteps: List<IPreprocessor>;
//fMatrixSteps: List<ITransformer>;
fModel: IUnsupervisedModel;
//fFeatures: array of string;
//fFinalFeatures: array of string;
//fFitted: boolean;
procedure ValidateNumericFeatures(df: DataFrame);
function TransformToMatrix(df: DataFrame): Matrix;
protected
@ -226,9 +219,6 @@ const
'Матричный шаг не может идти после модели!!Matrix step cannot appear after the model';
ER_DATAPIPE_UNKNOWN_STEP_TYPE =
'Неизвестный тип шага конвейера: {0}!!Unknown pipeline step type: {0}';
ER_TO_MATRIX_NO_COLUMNS =
'ToMatrix: не указаны столбцы!!ToMatrix: no columns specified';
ER_TO_VECTOR_NON_NUMERIC =
'ToVector: столбец "{0}" содержит нечисловые или NA значения!!' +
'ToVector: column "{0}" contains non-numeric or NA values';
@ -267,9 +257,6 @@ const
ER_MODEL_NOT_SUPERVISED =
'Модель (тип: {0}) не поддерживает Fit(X, y)!!' +
'Model (type: {0}) does not support Fit(X, y)';
ER_XY_SIZE_MISMATCH =
'Несовпадение размеров X и y: X имеет {0} строк, y имеет {1} элементов!!' +
'X and y size mismatch: X has {0} rows, y has {1} elements';
ER_PIPELINE_FEATURE_NOT_FOUND =
'Признак "{0}" отсутствует во входных данных!!' +
'Feature "{0}" not found in input data';
@ -294,9 +281,6 @@ const
ER_DATAPIPE_INVALID_TASK =
'DataPipeline поддерживает только классификацию и регрессию!!' +
'DataPipeline supports classification and regression only';
ER_PREDICT_NOT_SUPPORTED =
'Модель не поддерживает операцию Predict!!' +
'Model does not support Predict operation';
ER_PIPELINE_NO_STEPS =
'Pipeline не содержит шагов!!' +
'Pipeline must contain at least one step';
@ -561,74 +545,31 @@ begin
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
var current := df;
var current: DataFrame;
var X := PrepareMatrix(df, current);
// --- 0) Проверка входной схемы
ValidateSchema(current);
// --- 1) DataFrame шаги
for var i := 0 to fDataSteps.Count - 1 do
begin
fDataSteps[i] := fDataSteps[i].Fit(current);
current := fDataSteps[i].Transform(current);
end;
// --- target должен остаться
if not current.HasColumn(fTarget) then
ArgumentError(ER_PIPELINE_TARGET_REMOVED, fTarget);
// --- 2) вычислить финальные признаки
var feats := new List<string>;
foreach var f in fFeatures do
begin
if current.HasColumn(f) then
begin
feats.Add(f);
continue;
end;
// искать производные признаки (OneHotEncoder)
foreach var c in current.Schema.ColumnNames do
if c.StartsWith(f + '_') then
if not feats.Contains(c) then
feats.Add(c);
end;
if feats.Count = 0 then
ArgumentError(ER_PIPELINE_NO_FEATURES);
// fFinalFeatures признаки DataFrame (ДО matrix-transform)
// После FitTransformMatrix пространство признаков меняется (PCA и т.п.),
// но fFinalFeatures используется только для ToMatrix в Predict.
fFinalFeatures := feats.ToArray;
var X := current.ToMatrix(fFinalFeatures);
var classes: array of string;
var y: Vector;
case fTask of
tkRegression:
y := current.ToVector(fTarget);
tkClassification:
begin
var labels := current.EncodeLabels(fTarget, classes);
y := new Vector(labels);
end;
end;
if X.RowCount <> y.Length then
DimensionError(ER_XY_SIZE_MISMATCH, X.RowCount, y.Length);
// --- 3) Matrix transformers
X := FitTransformMatrix(X, y);
// После этого X уже не соответствует fFinalFeatures по размерности,
// но это нормально: fFinalFeatures используется только до ToMatrix.
// --- 4) модель
fModel := fModel.Fit(X, y);
if fTask = tkClassification then
@ -949,47 +890,11 @@ begin
if fModel = nil then
ArgumentError(ER_MODEL_NULL);
var current := df;
var current: DataFrame;
var X := PrepareMatrix(df, current);
// --- 0) Проверка входной схемы
ValidateSchema(current);
// --- 1) DataFrame шаги
for var i := 0 to fDataSteps.Count - 1 do
begin
fDataSteps[i] := fDataSteps[i].Fit(current);
current := fDataSteps[i].Transform(current);
end;
// --- 2) вычислить финальные признаки
var feats := new List<string>;
foreach var f in fFeatures do
begin
if current.HasColumn(f) then
begin
feats.Add(f);
continue;
end;
// искать производные признаки (OneHotEncoder)
foreach var c in current.Schema.ColumnNames do
if c.StartsWith(f + '_') then
if not feats.Contains(c) then
feats.Add(c);
end;
if feats.Count = 0 then
ArgumentError(ER_PIPELINE_NO_FEATURES);
fFinalFeatures := feats.ToArray;
var X := current.ToMatrix(fFinalFeatures);
// --- 3) Matrix transformers
X := FitTransformMatrix(X);
// --- 4) модель
fModel := fModel.Fit(X);
fFitted := true;

View file

@ -63,6 +63,13 @@ function EncodeLabelsInt(labels: array of integer; var classes: array of integer
/// Используется для применения кодирования к тестовым данным (Transform).
function TransformLabels(labels: array of string; classes: array of string): array of integer;
/// Преобразует целочисленные метки классов в индексы (0,1,2,...)
/// с использованием заранее заданного массива classes (mapping: индекс значение).
/// classes должен быть получен из EncodeLabelsInt.
/// Если встречается неизвестное значение выбрасывается исключение.
/// Используется для применения кодирования к тестовым данным (Transform).
function TransformLabelsInt(labels: array of integer; classes: array of integer): array of integer;
/// Преобразует целочисленные индексы классов обратно в строковые метки.
/// Массив classes задаёт соответствие: classes[i] имя класса с индексом i.
/// Используется для получения текстовых предсказаний моделей.
@ -73,6 +80,11 @@ function DecodeLabels(y: array of integer; classes: array of string): array of s
/// Используется для определения множества классов в задаче классификации.
function UniqueLabels(labels: array of string): array of string;
function CloneOrNil(v: Vector): Vector;
procedure CheckSameLength(a, b: Vector);
implementation
uses MLExceptions;
@ -173,6 +185,34 @@ begin
Result := res;
end;
function TransformLabelsInt(labels: array of integer; classes: array of integer): array of integer;
begin
if labels = nil then
ArgumentNullError(ER_ARG_NULL, 'labels');
if classes = nil then
ArgumentNullError(ER_ARG_NULL, 'classes');
// mapping: значение индекс
var map := new Dictionary<integer, integer>;
for var i := 0 to classes.Length - 1 do
map[classes[i]] := i;
var res := new integer[labels.Length];
for var i := 0 to labels.Length - 1 do
begin
var v := labels[i];
if not map.ContainsKey(v) then
Error(ER_UNKNOWN_CLASS_IN_TRANSFORM, v);
res[i] := map[v];
end;
Result := res;
end;
function EncodeLabels(labels: array of string): array of integer;
begin
var classes: array of string;
@ -229,4 +269,17 @@ begin
Result := labels.Distinct.ToArray;
end;
function CloneOrNil(v: Vector): Vector;
begin
if v = nil then
exit(nil);
Result := v.Clone;
end;
procedure CheckSameLength(a, b: Vector);
begin
if a.Length <> b.Length then
DimensionError(ER_DIM_MISMATCH, a.Length, b.Length);
end;
end.

View file

@ -223,6 +223,10 @@ type
/// Поддерживает как бинарную, так и многоклассовую классификацию.
/// Для бинарной классификации дополнительно доступны
/// агрегированные показатели: TP, TN, FP, FN.
///
/// Ожидается, что yTrue и yPred содержат закодированные метки классов
/// (целые значения 0..K-1, допускается хранение в Vector типа real).
/// Для строковых меток необходимо предварительно применить EncodeLabels
ConfusionMatrix = class
private
fMatrix: array[,] of integer;
@ -579,6 +583,7 @@ type
implementation
uses MLExceptions;
uses MLUtilsABC;
const
ER_INVALID_VALUE =
@ -1671,6 +1676,102 @@ end;
/// Для больших выборок рекомендуется использовать альтернативную реализацию
/// через таблицу сопряжённости (contingency table) с асимптотикой O(n · k)
static function Metrics.AdjustedRandIndex(yTrue, yPred: Vector): real;
begin
if yTrue = nil then
ArgumentNullError(ER_Y_NULL);
if yPred = nil then
ArgumentNullError(ER_Y_NULL);
var n := yTrue.Length;
if n <> yPred.Length then
DimensionError(ER_DIM_MISMATCH, yTrue.Length, yPred.Length);
if n < 2 then
begin
Result := 1.0;
exit;
end;
// --- Vector -> int[]
var yTInt := new integer[n];
var yPInt := new integer[n];
for var i := 0 to n - 1 do
begin
yTInt[i] := Round(yTrue[i]);
yPInt[i] := Round(yPred[i]);
end;
// --- encoding
var classesTrue: array of integer;
var classesPred: array of integer;
var yT := EncodeLabelsInt(yTInt, classesTrue);
var yP := EncodeLabelsInt(yPInt, classesPred);
var kT := classesTrue.Length;
var kP := classesPred.Length;
// --- contingency
var table := new integer[kT, kP];
for var i := 0 to n - 1 do
table[yT[i], yP[i]] += 1;
// --- sums
var sumNij := 0.0;
var sumAi := 0.0;
var sumBj := 0.0;
// n_ij
for var i := 0 to kT - 1 do
for var j := 0 to kP - 1 do
begin
var x := table[i,j];
if x >= 2 then
sumNij += x * (x - 1) / 2.0;
end;
// a_i
for var i := 0 to kT - 1 do
begin
var s := 0;
for var j := 0 to kP - 1 do
s += table[i,j];
if s >= 2 then
sumAi += s * (s - 1) / 2.0;
end;
// b_j
for var j := 0 to kP - 1 do
begin
var s := 0;
for var i := 0 to kT - 1 do
s += table[i,j];
if s >= 2 then
sumBj += s * (s - 1) / 2.0;
end;
var total := n * (n - 1) / 2.0;
var expected := (sumAi * sumBj) / total;
var maxIndex := 0.5 * (sumAi + sumBj);
var denom := maxIndex - expected;
if denom = 0 then
begin
Result := 0.0;
exit;
end;
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);
@ -1718,7 +1819,7 @@ begin
else
Result := (ri - expected) / (1 - expected);
end;
end;
end;}
function R(s: string; w: integer): string;
begin

View file

@ -42,6 +42,9 @@ type
/// metric функция качества, принимающая (y_true, y_pred)
/// и возвращающая значение метрики (например, Accuracy или MSE).
/// Возвращает среднее значение метрики по всем частям.
///
/// Метод принимает только ISupervisedModel, работающие с матричными данными
/// DataPipeline сюда передавать нельзя, так как он работает с DataFrame.
static function CrossValidate(model: ISupervisedModel; X: Matrix; y: Vector;
k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real;
@ -50,6 +53,9 @@ type
/// с сохранением пропорций классов в каждой части.
/// Рекомендуется для задач классификации, особенно при несбалансированных классах.
/// Возвращает среднее значение метрики по k разбиениям.
///
/// Метод принимает только ISupervisedModel, работающие с матричными данными
/// DataPipeline сюда передавать нельзя, так как он работает с DataFrame.
static function StratifiedCrossValidate(model: ISupervisedModel; X: Matrix; y: Vector;
k: integer; metric: (Vector,Vector) -> real; seed: integer := -1): real;
end;
@ -92,9 +98,6 @@ const
ER_DIM_MISMATCH_TRAIN_TEST =
'Несоответствие размерностей в TrainTestSplit: X.RowCount={0}, y.Length={1}!!' +
'Dimension mismatch in TrainTestSplit: X.RowCount={0}, y.Length={1}';
ER_TEST_RATIO_INVALID =
'Параметр testRatio должен быть в интервале (0,1), получено {0}!!' +
'Parameter testRatio must be in (0,1), got {0}';
ER_K_INVALID =
'Некорректное значение k в KFold: k={0}, n={1}!!' +
'Invalid k in KFold: k={0}, n={1}';